Add SpanScanner.within(). This is useful for doing more detailed parses of sub-sections of larger text. R=rnystrom@google.com Review URL: https://codereview.chromium.org//2039163002 .
diff --git a/pkgs/string_scanner/CHANGELOG.md b/pkgs/string_scanner/CHANGELOG.md index a5650cc..ea1268c 100644 --- a/pkgs/string_scanner/CHANGELOG.md +++ b/pkgs/string_scanner/CHANGELOG.md
@@ -1,3 +1,7 @@ +## 0.1.5 + +* Add `new SpanScanner.within()`, which scans within a existing `FileSpan`. + ## 0.1.4+1 * Remove the dependency on `path`, since we don't actually import it.
diff --git a/pkgs/string_scanner/lib/src/relative_span_scanner.dart b/pkgs/string_scanner/lib/src/relative_span_scanner.dart new file mode 100644 index 0000000..fdcd03f --- /dev/null +++ b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
@@ -0,0 +1,112 @@ +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:source_span/source_span.dart'; + +import 'exception.dart'; +import 'line_scanner.dart'; +import 'span_scanner.dart'; +import 'string_scanner.dart'; +import 'utils.dart'; + +/// A [SpanScanner] that scans within an existing [FileSpan]. +/// +/// This re-implements chunks of [SpanScanner] rather than using a dummy span or +/// inheritance because scanning is often a performance-critical operation, so +/// it's important to avoid adding extra overhead when relative scanning isn't +/// needed. +class RelativeSpanScanner extends StringScanner implements SpanScanner { + /// The source of the scanner. + /// + /// This caches line break information and is used to generate [Span]s. + final SourceFile _sourceFile; + + /// The start location of the span within which this scanner is scanning. + /// + /// This is used to convert between span-relative and file-relative fields. + final FileLocation _startLocation; + + int get line => _sourceFile.getLine(_startLocation.offset + position) - + _startLocation.line; + + int get column { + var line = _sourceFile.getLine(_startLocation.offset + position); + var column = _sourceFile.getColumn(_startLocation.offset + position, + line: line); + return line == _startLocation.line + ? column - _startLocation.column + : column; + } + + LineScannerState get state => new _SpanScannerState(this, position); + + set state(LineScannerState state) { + if (state is! _SpanScannerState || + !identical((state as _SpanScannerState)._scanner, this)) { + throw new ArgumentError("The given LineScannerState was not returned by " + "this LineScanner."); + } + + this.position = state.position; + } + + FileSpan get lastSpan => _lastSpan; + FileSpan _lastSpan; + + FileLocation get location => + _sourceFile.location(_startLocation.offset + position); + + FileSpan get emptySpan => location.pointSpan(); + + RelativeSpanScanner(FileSpan span) + : _sourceFile = span.file, + _startLocation = span.start, + super(span.text, sourceUrl: span.sourceUrl); + + FileSpan spanFrom(LineScannerState startState, [LineScannerState endState]) { + var endPosition = endState == null ? position : endState.position; + return _sourceFile.span( + _startLocation.offset + startState.position, + _startLocation.offset + endPosition); + } + + bool matches(Pattern pattern) { + if (!super.matches(pattern)) { + _lastSpan = null; + return false; + } + + _lastSpan = _sourceFile.span( + _startLocation.offset + position, + _startLocation.offset + lastMatch.end); + return true; + } + + void error(String message, {Match match, int position, int length}) { + validateErrorArgs(string, match, position, length); + + if (match == null && position == null && length == null) match = lastMatch; + if (position == null) { + position = match == null ? this.position : match.start; + } + if (length == null) length = match == null ? 1 : match.end - match.start; + + var span = _sourceFile.span( + _startLocation.offset + position, + _startLocation.offset + position + length); + throw new StringScannerException(message, span, string); + } +} + +/// A class representing the state of a [SpanScanner]. +class _SpanScannerState implements LineScannerState { + /// The [SpanScanner] that created this. + final RelativeSpanScanner _scanner; + + final int position; + int get line => _scanner._sourceFile.getLine(position); + int get column => _scanner._sourceFile.getColumn(position); + + _SpanScannerState(this._scanner, this.position); +}
diff --git a/pkgs/string_scanner/lib/src/span_scanner.dart b/pkgs/string_scanner/lib/src/span_scanner.dart index dd16e47..dd7f0e4 100644 --- a/pkgs/string_scanner/lib/src/span_scanner.dart +++ b/pkgs/string_scanner/lib/src/span_scanner.dart
@@ -7,6 +7,7 @@ import 'eager_span_scanner.dart'; import 'exception.dart'; import 'line_scanner.dart'; +import 'relative_span_scanner.dart'; import 'string_scanner.dart'; import 'utils.dart'; @@ -69,6 +70,14 @@ factory SpanScanner.eager(String string, {sourceUrl, int position}) = EagerSpanScanner; + /// Creates a new [SpanScanner] that scans within [span]. + /// + /// This scans through [span.text], but emits new spans from [span.file] in + /// their appropriate relative positions. The [string] field contains only + /// [span.text], and [position], [line], and [column] are all relative to the + /// span. + factory SpanScanner.within(FileSpan span) = RelativeSpanScanner; + /// Creates a [FileSpan] representing the source range between [startState] /// and the current position. FileSpan spanFrom(LineScannerState startState, [LineScannerState endState]) {
diff --git a/pkgs/string_scanner/pubspec.yaml b/pkgs/string_scanner/pubspec.yaml index b249240..24bd6bb 100644 --- a/pkgs/string_scanner/pubspec.yaml +++ b/pkgs/string_scanner/pubspec.yaml
@@ -1,5 +1,5 @@ name: string_scanner -version: 0.1.4+1 +version: 0.1.5-dev author: "Dart Team <misc@dartlang.org>" homepage: https://github.com/dart-lang/string_scanner description: >
diff --git a/pkgs/string_scanner/test/span_scanner_test.dart b/pkgs/string_scanner/test/span_scanner_test.dart index b078f6e..84d7b94 100644 --- a/pkgs/string_scanner/test/span_scanner_test.dart +++ b/pkgs/string_scanner/test/span_scanner_test.dart
@@ -2,9 +2,12 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:source_span/source_span.dart'; import 'package:string_scanner/string_scanner.dart'; import 'package:test/test.dart'; +import 'utils.dart'; + void main() { testForImplementation("lazy", () { return new SpanScanner('foo\nbar\nbaz', sourceUrl: 'source'); @@ -13,6 +16,91 @@ testForImplementation("eager", () { return new SpanScanner.eager('foo\nbar\nbaz', sourceUrl: 'source'); }); + + group("within", () { + var text = 'first\nbefore: foo\nbar\nbaz :after\nlast'; + var startOffset = text.indexOf('foo'); + + var scanner; + setUp(() { + var file = new SourceFile(text, url: 'source'); + scanner = new SpanScanner.within( + file.span(startOffset, text.indexOf(' :after'))); + }); + + test("string only includes the span text", () { + expect(scanner.string, equals("foo\nbar\nbaz")); + }); + + test("line and column are span-relative", () { + expect(scanner.line, equals(0)); + expect(scanner.column, equals(0)); + + scanner.scan("foo"); + expect(scanner.line, equals(0)); + expect(scanner.column, equals(3)); + + scanner.scan("\n"); + expect(scanner.line, equals(1)); + expect(scanner.column, equals(0)); + }); + + test("tracks the span for the last match", () { + scanner.scan('fo'); + scanner.scan('o\nba'); + + var span = scanner.lastSpan; + expect(span.start.offset, equals(startOffset + 2)); + expect(span.start.line, equals(1)); + expect(span.start.column, equals(10)); + expect(span.start.sourceUrl, equals(Uri.parse('source'))); + + expect(span.end.offset, equals(startOffset + 6)); + expect(span.end.line, equals(2)); + expect(span.end.column, equals(2)); + expect(span.start.sourceUrl, equals(Uri.parse('source'))); + + expect(span.text, equals('o\nba')); + }); + + test(".spanFrom() returns a span from a previous state", () { + scanner.scan('fo'); + var state = scanner.state; + scanner.scan('o\nba'); + scanner.scan('r\nba'); + + var span = scanner.spanFrom(state); + expect(span.text, equals('o\nbar\nba')); + }); + + test(".emptySpan returns an empty span at the current location", () { + scanner.scan('foo\nba'); + + var span = scanner.emptySpan; + expect(span.start.offset, equals(startOffset + 6)); + expect(span.start.line, equals(2)); + expect(span.start.column, equals(2)); + expect(span.start.sourceUrl, equals(Uri.parse('source'))); + + expect(span.end.offset, equals(startOffset + 6)); + expect(span.end.line, equals(2)); + expect(span.end.column, equals(2)); + expect(span.start.sourceUrl, equals(Uri.parse('source'))); + + expect(span.text, equals('')); + }); + + test(".error() uses an absolute span", () { + scanner.expect("foo"); + expect(() => scanner.error('oh no!'), + throwsStringScannerException("foo")); + }); + + test(".isDone returns true at the end of the span", () { + scanner.expect("foo\nbar\nbaz"); + expect(scanner.isDone, isTrue); + }); + }); } void testForImplementation(String name, SpanScanner create()) {