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/CHANGELOG.md b/CHANGELOG.md
index a5650cc..ea1268c 100644
--- a/CHANGELOG.md
+++ b/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/lib/src/relative_span_scanner.dart b/lib/src/relative_span_scanner.dart
new file mode 100644
index 0000000..fdcd03f
--- /dev/null
+++ b/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/lib/src/span_scanner.dart b/lib/src/span_scanner.dart
index dd16e47..dd7f0e4 100644
--- a/lib/src/span_scanner.dart
+++ b/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/pubspec.yaml b/pubspec.yaml
index b249240..24bd6bb 100644
--- a/pubspec.yaml
+++ b/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/test/span_scanner_test.dart b/test/span_scanner_test.dart
index b078f6e..84d7b94 100644
--- a/test/span_scanner_test.dart
+++ b/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()) {