Add LineScanner and SpanScanner classes to string_scanner.
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//299973002
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/string_scanner@36815 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2c41c9d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,19 @@
+## 0.0.2
+
+* `new StringScanner()` now takes an optional `sourceUrl` argument that provides
+ the URL of the source file. This is used for error reporting.
+
+* Add `StringScanner.readChar()` and `StringScanner.peekChar()` methods for
+ doing character-by-character scanning.
+
+* Scanners now throw `StringScannerException`s which provide more detailed
+ access to information about the errors that were thrown and can provide
+ terminal-colored messages.
+
+* Add a `LineScanner` subclass of `StringScanner` that automatically tracks line
+ and column information of the text being scanned.
+
+* Add a `SpanScanner` subclass of `LineScanner` that exposes matched ranges as
+ [source map][] `Span` objects.
+
+[source_map]: http://pub.dartlang.org/packages/source_maps
diff --git a/lib/src/exception.dart b/lib/src/exception.dart
new file mode 100644
index 0000000..c398687
--- /dev/null
+++ b/lib/src/exception.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.exception;
+
+import 'package:source_maps/source_maps.dart';
+
+/// An exception thrown by a [StringScanner] that failed to parse a string.
+class StringScannerException implements FormatException {
+ /// The error message.
+ final String message;
+
+ /// The source string being parsed.
+ final String string;
+
+ /// The URL of the source file being parsed.
+ ///
+ /// This may be `null`, indicating that the source URL is unknown.
+ final Uri sourceUrl;
+
+ /// The span within [string] that caused the exception.
+ final Span span;
+
+ StringScannerException(this.message, this.string, this.sourceUrl, this.span);
+
+ /// Returns a detailed description of this exception.
+ ///
+ /// If [useColors] is true, the section of the source that caused the
+ /// exception will be colored using ANSI color codes. By default, it's colored
+ /// red, but a different ANSI code may passed via [color].
+ String toString({bool useColors: false, String color}) {
+ return "Error on " + span.getLocationMessage(
+ message, useColors: useColors, color: color);
+ }
+}
+
diff --git a/lib/src/line_scanner.dart b/lib/src/line_scanner.dart
new file mode 100644
index 0000000..4e12173
--- /dev/null
+++ b/lib/src/line_scanner.dart
@@ -0,0 +1,108 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.line_scanner;
+
+import 'string_scanner.dart';
+
+/// A subclass of [StringScanner] that tracks line and column information.
+class LineScanner extends StringScanner {
+ /// The scanner's current (zero-based) line number.
+ int get line => _line;
+ int _line = 0;
+
+ /// The scanner's current (zero-based) column number.
+ int get column => _column;
+ int _column = 0;
+
+ /// The scanner's state, including line and column information.
+ ///
+ /// This can be used to efficiently save and restore the state of the scanner
+ /// when backtracking. A given [LineScannerState] is only valid for the
+ /// [LineScanner] that created it.
+ LineScannerState get state =>
+ new LineScannerState._(this, position, line, column);
+
+ set state(LineScannerState state) {
+ if (!identical(state._scanner, this)) {
+ throw new ArgumentError("The given LineScannerState was not returned by "
+ "this LineScanner.");
+ }
+
+ super.position = state.position;
+ _line = state.line;
+ _column = state.column;
+ }
+
+ set position(int newPosition) {
+ var oldPosition = position;
+ super.position = newPosition;
+
+ if (newPosition > oldPosition) {
+ var newlines = "\n".allMatches(string.substring(oldPosition, newPosition))
+ .toList();
+ _line += newlines.length;
+ if (newlines.isEmpty) {
+ _column += newPosition - oldPosition;
+ } else {
+ _column = newPosition - newlines.last.end;
+ }
+ } else {
+ var newlines = "\n".allMatches(string.substring(newPosition, oldPosition))
+ .toList();
+ _line -= newlines.length;
+ if (newlines.isEmpty) {
+ _column -= oldPosition - newPosition;
+ } else {
+ _column = newPosition - string.lastIndexOf("\n", newPosition) - 1;
+ }
+ }
+ }
+
+ LineScanner(String string, {sourceUrl, int position})
+ : super(string, sourceUrl: sourceUrl, position: position);
+
+ int readChar() {
+ var char = super.readChar();
+ if (char == 0xA) {
+ _line += 1;
+ _column = 0;
+ } else {
+ _column += 1;
+ }
+ return char;
+ }
+
+ bool scan(Pattern pattern) {
+ var oldPosition = position;
+ if (!super.scan(pattern)) return false;
+
+ var newlines = "\n".allMatches(lastMatch[0]).toList();
+ _line += newlines.length;
+ if (newlines.isEmpty) {
+ _column += lastMatch[0].length;
+ } else {
+ _column = lastMatch[0].length - newlines.last.end;
+ }
+
+ return true;
+ }
+}
+
+/// A class representing the state of a [LineScanner].
+class LineScannerState {
+ /// The [LineScanner] that created this.
+ final LineScanner _scanner;
+
+ /// The position of the scanner in this state.
+ final int position;
+
+ /// The zero-based line number of the scanner in this state.
+ final int line;
+
+ /// The zero-based column number of the scanner in this state.
+ final int column;
+
+ LineScannerState._(this._scanner, this.position, this.line, this.column);
+}
diff --git a/lib/src/span_scanner.dart b/lib/src/span_scanner.dart
new file mode 100644
index 0000000..9060e62
--- /dev/null
+++ b/lib/src/span_scanner.dart
@@ -0,0 +1,95 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.span_scanner;
+
+import 'package:source_maps/source_maps.dart';
+
+import 'exception.dart';
+import 'line_scanner.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+/// A subclass of [LineScanner] that exposes matched ranges as source map
+/// [Span]s.
+class SpanScanner extends StringScanner implements LineScanner {
+ /// The source of the scanner.
+ ///
+ /// This caches line break information and is used to generate [Span]s.
+ final SourceFile _sourceFile;
+
+ int get line => _sourceFile.getLine(position);
+ int get column => _sourceFile.getColumn(line, position);
+
+ 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;
+ }
+
+ /// The [Span] for [lastMatch].
+ ///
+ /// This is the span for the entire match. There's no way to get spans for
+ /// subgroups since [Match] exposes no information about their positions.
+ Span get lastSpan => _lastSpan;
+ Span _lastSpan;
+
+ /// Returns an empty span at the current location.
+ Span get emptySpan => _sourceFile.span(position);
+
+ /// Creates a new [SpanScanner] that starts scanning from [position].
+ ///
+ /// [sourceUrl] is used as [Location.sourceUrl] for the returned [Span]s as
+ /// well as for error reporting.
+ SpanScanner(String string, sourceUrl, {int position})
+ : _sourceFile = new SourceFile.text(
+ sourceUrl is Uri ? sourceUrl.toString() : sourceUrl, string),
+ super(string, sourceUrl: sourceUrl, position: position);
+
+ /// Creates a [Span] representing the source range between [startState] and
+ /// the current position.
+ Span spanFrom(LineScannerState startState) =>
+ _sourceFile.span(startState.position, position);
+
+ bool matches(Pattern pattern) {
+ if (!super.matches(pattern)) {
+ _lastSpan = null;
+ return false;
+ }
+
+ _lastSpan = _sourceFile.span(position, 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(position, position + length);
+ throw new StringScannerException(message, string, sourceUrl, span);
+ }
+}
+
+/// A class representing the state of a [SpanScanner].
+class _SpanScannerState implements LineScannerState {
+ /// The [SpanScanner] that created this.
+ final SpanScanner _scanner;
+
+ final int position;
+ int get line => _scanner._sourceFile.getLine(position);
+ int get column => _scanner._sourceFile.getColumn(line, position);
+
+ _SpanScannerState(this._scanner, this.position);
+}
diff --git a/lib/src/string_scanner.dart b/lib/src/string_scanner.dart
new file mode 100644
index 0000000..c9e7459
--- /dev/null
+++ b/lib/src/string_scanner.dart
@@ -0,0 +1,168 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.string_scanner;
+
+import 'package:source_maps/source_maps.dart';
+
+import 'exception.dart';
+import 'utils.dart';
+
+/// When compiled to JS, forward slashes are always escaped in [RegExp.pattern].
+///
+/// See issue 17998.
+final _slashAutoEscape = new RegExp("/").pattern == "\\/";
+
+/// A class that scans through a string using [Pattern]s.
+class StringScanner {
+ /// The URL of the source of the string being scanned.
+ ///
+ /// This is used for error reporting. It may be `null`, indicating that the
+ /// source URL is unknown or unavailable.
+ final Uri sourceUrl;
+
+ /// The string being scanned through.
+ final String string;
+
+ /// The current position of the scanner in the string, in characters.
+ int get position => _position;
+ set position(int position) {
+ if (position < 0 || position > string.length) {
+ throw new ArgumentError("Invalid position $position");
+ }
+
+ _position = position;
+ }
+ int _position = 0;
+
+ /// The data about the previous match made by the scanner.
+ ///
+ /// If the last match failed, this will be `null`.
+ Match get lastMatch => _lastMatch;
+ Match _lastMatch;
+
+ /// The portion of the string that hasn't yet been scanned.
+ String get rest => string.substring(position);
+
+ /// Whether the scanner has completely consumed [string].
+ bool get isDone => position == string.length;
+
+ /// Creates a new [StringScanner] that starts scanning from [position].
+ ///
+ /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the
+ /// URL of the source of the string being scanned, if available. It can be
+ /// either a [String] or a [Uri].
+ StringScanner(this.string, {sourceUrl, int position})
+ : sourceUrl = sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl {
+ if (position != null) this.position = position;
+ }
+
+ /// Consumes a single character and returns its character code.
+ ///
+ /// This throws a [FormatException] if the string has been fully consumed. It
+ /// doesn't affect [lastMatch].
+ int readChar() {
+ if (isDone) _fail("more input");
+ return string.codeUnitAt(_position++);
+ }
+
+ /// Returns the character code of the character [offset] away from [position].
+ ///
+ /// [offset] defaults to zero, and may be negative to inspect already-consumed
+ /// characters.
+ ///
+ /// This returns `null` if [offset] points outside the string. It doesn't
+ /// affect [lastMatch].
+ int peekChar([int offset]) {
+ if (offset == null) offset = 0;
+ var index = position + offset;
+ if (index < 0 || index >= string.length) return null;
+ return string.codeUnitAt(index);
+ }
+
+ /// If [pattern] matches at the current position of the string, scans forward
+ /// until the end of the match.
+ ///
+ /// Returns whether or not [pattern] matched.
+ bool scan(Pattern pattern) {
+ var success = matches(pattern);
+ if (success) _position = _lastMatch.end;
+ return success;
+ }
+
+ /// If [pattern] matches at the current position of the string, scans forward
+ /// until the end of the match.
+ ///
+ /// If [pattern] did not match, throws a [FormatException] describing the
+ /// position of the failure. [name] is used in this error as the expected name
+ /// of the pattern being matched; if it's `null`, the pattern itself is used
+ /// instead.
+ void expect(Pattern pattern, {String name}) {
+ if (scan(pattern)) return;
+
+ if (name == null) {
+ if (pattern is RegExp) {
+ var source = pattern.pattern;
+ if (!_slashAutoEscape) source = source.replaceAll("/", "\\/");
+ name = "/$source/";
+ } else {
+ name = pattern.toString()
+ .replaceAll("\\", "\\\\").replaceAll('"', '\\"');
+ name = '"$name"';
+ }
+ }
+ _fail(name);
+ }
+
+ /// If the string has not been fully consumed, this throws a
+ /// [FormatException].
+ void expectDone() {
+ if (isDone) return;
+ _fail("no more input");
+ }
+
+ /// Returns whether or not [pattern] matches at the current position of the
+ /// string.
+ ///
+ /// This doesn't move the scan pointer forward.
+ bool matches(Pattern pattern) {
+ _lastMatch = pattern.matchAsPrefix(string, position);
+ return _lastMatch != null;
+ }
+
+ /// Throws a [FormatException] with [message] as well as a detailed
+ /// description of the location of the error in the string.
+ ///
+ /// [match] is the match information for the span of the string with which the
+ /// error is associated. This should be a match returned by this scanner's
+ /// [lastMatch] property. By default, the error is associated with the last
+ /// match.
+ ///
+ /// If [position] and/or [length] are passed, they are used as the error span
+ /// instead. If only [length] is passed, [position] defaults to the current
+ /// position; if only [position] is passed, [length] defaults to 1.
+ ///
+ /// It's an error to pass [match] at the same time as [position] or [length].
+ 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 url = sourceUrl == null ? null : sourceUrl.toString();
+ var sourceFile = new SourceFile.text(url, string);
+ var span = sourceFile.span(position, position + length);
+ throw new StringScannerException(message, string, sourceUrl, span);
+ }
+
+ // TODO(nweiz): Make this handle long lines more gracefully.
+ /// Throws a [FormatException] describing that [name] is expected at the
+ /// current position in the string.
+ void _fail(String name) {
+ error("expected $name.", position: this.position, length: 0);
+ }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..e556236
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.utils;
+
+/// Validates the arguments passed to [StringScanner.error].
+void validateErrorArgs(String string, Match match, int position, int length) {
+ if (match != null && (position != null || length != null)) {
+ throw new ArgumentError("Can't pass both match and position/length.");
+ }
+
+ if (position != null) {
+ if (position < 0) {
+ throw new RangeError("position must be greater than or equal to 0.");
+ } else if (position > string.length) {
+ throw new RangeError("position must be less than or equal to the "
+ "string length.");
+ }
+ }
+
+ if (length != null && length < 0) {
+ throw new RangeError("length must be greater than or equal to 0.");
+ }
+
+ if (position != null && length != null && position + length > string.length) {
+ throw new RangeError("position plus length must not go beyond the end of "
+ "the string.");
+ }
+}
\ No newline at end of file
diff --git a/lib/string_scanner.dart b/lib/string_scanner.dart
index 3e3913b..be294f4 100644
--- a/lib/string_scanner.dart
+++ b/lib/string_scanner.dart
@@ -5,165 +5,7 @@
/// A library for parsing strings using a sequence of patterns.
library string_scanner;
-import 'dart:math' as math;
-
-/// When compiled to JS, forward slashes are always escaped in [RegExp.pattern].
-///
-/// See issue 17998.
-final _slashAutoEscape = new RegExp("/").pattern == "\\/";
-
-// TODO(nweiz): Add some integration between this and source maps.
-/// A class that scans through a string using [Pattern]s.
-class StringScanner {
- /// The string being scanned through.
- final String string;
-
- /// The current position of the scanner in the string, in characters.
- int get position => _position;
- set position(int position) {
- if (position < 0 || position > string.length) {
- throw new ArgumentError("Invalid position $position");
- }
-
- _position = position;
- }
- int _position = 0;
-
- /// The data about the previous match made by the scanner.
- ///
- /// If the last match failed, this will be `null`.
- Match get lastMatch => _lastMatch;
- Match _lastMatch;
-
- /// The portion of the string that hasn't yet been scanned.
- String get rest => string.substring(position);
-
- /// Whether the scanner has completely consumed [string].
- bool get isDone => position == string.length;
-
- /// Creates a new [StringScanner] that starts scanning from [position].
- ///
- /// [position] defaults to 0, the beginning of the string.
- StringScanner(this.string, {int position}) {
- if (position != null) this.position = position;
- }
-
- /// If [pattern] matches at the current position of the string, scans forward
- /// until the end of the match.
- ///
- /// Returns whether or not [pattern] matched.
- bool scan(Pattern pattern) {
- var success = matches(pattern);
- if (success) _position = _lastMatch.end;
- return success;
- }
-
- /// If [pattern] matches at the current position of the string, scans forward
- /// until the end of the match.
- ///
- /// If [pattern] did not match, throws a [FormatException] describing the
- /// position of the failure. [name] is used in this error as the expected name
- /// of the pattern being matched; if it's `null`, the pattern itself is used
- /// instead.
- void expect(Pattern pattern, {String name}) {
- if (scan(pattern)) return;
-
- if (name == null) {
- if (pattern is RegExp) {
- var source = pattern.pattern;
- if (!_slashAutoEscape) source = source.replaceAll("/", "\\/");
- name = "/$source/";
- } else {
- name = pattern.toString()
- .replaceAll("\\", "\\\\").replaceAll('"', '\\"');
- name = '"$name"';
- }
- }
- _fail(name);
- }
-
- /// If the string has not been fully consumed, this throws a
- /// [FormatException].
- void expectDone() {
- if (isDone) return;
- _fail("no more input");
- }
-
- /// Returns whether or not [pattern] matches at the current position of the
- /// string.
- ///
- /// This doesn't move the scan pointer forward.
- bool matches(Pattern pattern) {
- _lastMatch = pattern.matchAsPrefix(string, position);
- return _lastMatch != null;
- }
-
- /// Throws a [FormatException] with [message] as well as a detailed
- /// description of the location of the error in the string.
- ///
- /// [match] is the match information for the span of the string with which the
- /// error is associated. This should be a match returned by this scanner's
- /// [lastMatch] property. By default, the error is associated with the last
- /// match.
- ///
- /// If [position] and/or [length] are passed, they are used as the error span
- /// instead. If only [length] is passed, [position] defaults to the current
- /// position; if only [position] is passed, [length] defaults to 1.
- ///
- /// It's an error to pass [match] at the same time as [position] or [length].
- void error(String message, {Match match, int position, int length}) {
- if (match != null && (position != null || length != null)) {
- throw new ArgumentError("Can't pass both match and position/length.");
- }
-
- if (position != null && position < 0) {
- throw new RangeError("position must be greater than or equal to 0.");
- }
-
- if (length != null && length < 1) {
- throw new RangeError("length must be greater than or equal to 0.");
- }
-
- 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 newlines = "\n".allMatches(string.substring(0, position)).toList();
- var line = newlines.length + 1;
- var column;
- var lastLine;
- if (newlines.isEmpty) {
- column = position + 1;
- lastLine = string.substring(0, position);
- } else {
- column = position - newlines.last.end + 1;
- lastLine = string.substring(newlines.last.end, position);
- }
-
- var remaining = string.substring(position);
- var nextNewline = remaining.indexOf("\n");
- if (nextNewline == -1) {
- lastLine += remaining;
- } else {
- length = math.min(length, nextNewline);
- lastLine += remaining.substring(0, nextNewline);
- }
-
- var spaces = new List.filled(column - 1, ' ').join();
- var underline = new List.filled(length, '^').join();
-
- throw new FormatException(
- "Error on line $line, column $column: $message\n"
- "$lastLine\n"
- "$spaces$underline");
- }
-
- // TODO(nweiz): Make this handle long lines more gracefully.
- /// Throws a [FormatException] describing that [name] is expected at the
- /// current position in the string.
- void _fail(String name) {
- error("expected $name.", position: this.position, length: 1);
- }
-}
+export 'src/exception.dart';
+export 'src/line_scanner.dart';
+export 'src/span_scanner.dart';
+export 'src/string_scanner.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index 3035901..86ae238 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,9 +1,12 @@
name: string_scanner
-version: 0.0.1
+version: 0.0.2
author: "Dart Team <misc@dartlang.org>"
homepage: http://www.dartlang.org
description: >
A class for parsing strings using a sequence of patterns.
+dependencies:
+ path: ">=1.2.0 <2.0.0"
+ source_maps: ">=0.9.0 <0.10.0"
dev_dependencies:
unittest: ">=0.10.0 <0.11.0"
environment:
diff --git a/test/error_test.dart b/test/error_test.dart
index 6432197..4fe9083 100644
--- a/test/error_test.dart
+++ b/test/error_test.dart
@@ -14,10 +14,7 @@
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo ');
scanner.expect('bar');
- expect(() => scanner.error('oh no!'), throwsFormattedError('''
-Error on line 1, column 5: oh no!
-foo bar baz
- ^^^'''));
+ expect(() => scanner.error('oh no!'), throwsStringScannerException('bar'));
});
group("with match", () {
@@ -27,10 +24,7 @@
var match = scanner.lastMatch;
scanner.expect('bar');
expect(() => scanner.error('oh no!', match: match),
- throwsFormattedError('''
-Error on line 1, column 1: oh no!
-foo bar baz
-^^^^'''));
+ throwsStringScannerException('foo '));
});
test('supports a match on a previous line', () {
@@ -40,10 +34,7 @@
var match = scanner.lastMatch;
scanner.expect(' mi\nearth ');
expect(() => scanner.error('oh no!', match: match),
- throwsFormattedError('''
-Error on line 2, column 4: oh no!
-do re mi
- ^^'''));
+ throwsStringScannerException('re'));
});
test('supports a multiline match', () {
@@ -53,10 +44,7 @@
var match = scanner.lastMatch;
scanner.expect(' re mi');
expect(() => scanner.error('oh no!', match: match),
- throwsFormattedError('''
-Error on line 1, column 9: oh no!
-foo bar baz
- ^^^'''));
+ throwsStringScannerException('baz\ndo'));
});
test('supports a match after position', () {
@@ -66,10 +54,7 @@
var match = scanner.lastMatch;
scanner.position = 0;
expect(() => scanner.error('oh no!', match: match),
- throwsFormattedError('''
-Error on line 1, column 5: oh no!
-foo bar baz
- ^^^'''));
+ throwsStringScannerException('bar'));
});
});
@@ -78,59 +63,47 @@
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo ');
expect(() => scanner.error('oh no!', position: 1),
- throwsFormattedError('''
-Error on line 1, column 2: oh no!
-foo bar baz
- ^'''));
+ throwsStringScannerException('o'));
});
test('defaults to the current position', () {
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo ');
expect(() => scanner.error('oh no!', length: 3),
- throwsFormattedError('''
-Error on line 1, column 5: oh no!
-foo bar baz
- ^^^'''));
+ throwsStringScannerException('bar'));
});
test('supports an earlier position', () {
var scanner = new StringScanner('foo bar baz');
scanner.expect('foo ');
expect(() => scanner.error('oh no!', position: 1, length: 2),
- throwsFormattedError('''
-Error on line 1, column 2: oh no!
-foo bar baz
- ^^'''));
+ throwsStringScannerException('oo'));
});
test('supports a position on a previous line', () {
var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
scanner.expect('foo bar baz\ndo re mi\nearth');
expect(() => scanner.error('oh no!', position: 15, length: 2),
- throwsFormattedError('''
-Error on line 2, column 4: oh no!
-do re mi
- ^^'''));
+ throwsStringScannerException('re'));
});
test('supports a multiline length', () {
var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
scanner.expect('foo bar baz\ndo re mi\nearth');
expect(() => scanner.error('oh no!', position: 8, length: 8),
- throwsFormattedError('''
-Error on line 1, column 9: oh no!
-foo bar baz
- ^^^'''));
+ throwsStringScannerException('baz\ndo r'));
});
test('supports a position after the current one', () {
var scanner = new StringScanner('foo bar baz');
expect(() => scanner.error('oh no!', position: 4, length: 3),
- throwsFormattedError('''
-Error on line 1, column 5: oh no!
-foo bar baz
- ^^^'''));
+ throwsStringScannerException('bar'));
+ });
+
+ test('supports a length of zero', () {
+ var scanner = new StringScanner('foo bar baz');
+ expect(() => scanner.error('oh no!', position: 4, length: 0),
+ throwsStringScannerException(''));
});
});
@@ -161,8 +134,13 @@
expect(() => scanner.error("oh no!", position: 100), throwsArgumentError);
});
- test("if length is zero", () {
- expect(() => scanner.error("oh no!", length: 0), throwsArgumentError);
+ test("if position + length is outside the string", () {
+ expect(() => scanner.error("oh no!", position: 7, length: 7),
+ throwsArgumentError);
+ });
+
+ test("if length is negative", () {
+ expect(() => scanner.error("oh no!", length: -1), throwsArgumentError);
});
});
}
diff --git a/test/expect_error_test.dart b/test/expect_error_test.dart
deleted file mode 100644
index 3596e15..0000000
--- a/test/expect_error_test.dart
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright (c) 2014, 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.
-
-library string_scanner.expect_error_test;
-
-import 'package:string_scanner/string_scanner.dart';
-import 'package:unittest/unittest.dart';
-
-import 'utils.dart';
-
-void main() {
- test('points to the first unconsumed character', () {
- var scanner = new StringScanner('foo bar baz');
- scanner.expect('foo ');
- expect(() => scanner.expect('foo'), throwsFormattedError('''
-Error on line 1, column 5: expected "foo".
-foo bar baz
- ^'''));
- });
-
- test('prints the correct line', () {
- var scanner = new StringScanner('foo bar baz\ndo re mi\nearth fire water');
- scanner.expect('foo bar baz\ndo ');
- expect(() => scanner.expect('foo'), throwsFormattedError('''
-Error on line 2, column 4: expected "foo".
-do re mi
- ^'''));
- });
-
- test('handles the beginning of the string correctly', () {
- var scanner = new StringScanner('foo bar baz');
- expect(() => scanner.expect('zap'), throwsFormattedError('''
-Error on line 1, column 1: expected "zap".
-foo bar baz
-^'''));
- });
-
- test('handles the end of the string correctly', () {
- var scanner = new StringScanner('foo bar baz');
- scanner.expect('foo bar baz');
- expect(() => scanner.expect('bang'), throwsFormattedError('''
-Error on line 1, column 12: expected "bang".
-foo bar baz
- ^'''));
- });
-
- test('handles an empty string correctly', () {
- expect(() => new StringScanner('').expect('foo'), throwsFormattedError('''
-Error on line 1, column 1: expected "foo".
-
-^'''));
- });
-
- group("expected name", () {
- test("uses the provided name", () {
- expect(() => new StringScanner('').expect('foo bar', name: 'zap'),
- throwsFormattedError('''
-Error on line 1, column 1: expected zap.
-
-^'''));
- });
-
- test("escapes string quotes", () {
- expect(() => new StringScanner('').expect('foo"bar'),
- throwsFormattedError('''
-Error on line 1, column 1: expected "foo\\"bar".
-
-^'''));
- });
-
- test("escapes string backslashes", () {
- expect(() => new StringScanner('').expect('foo\\bar'),
- throwsFormattedError('''
-Error on line 1, column 1: expected "foo\\\\bar".
-
-^'''));
- });
-
- test("prints PERL-style regexps", () {
- expect(() => new StringScanner('').expect(new RegExp(r'foo')),
- throwsFormattedError('''
-Error on line 1, column 1: expected /foo/.
-
-^'''));
- });
-
- test("escape regexp forward slashes", () {
- expect(() => new StringScanner('').expect(new RegExp(r'foo/bar')),
- throwsFormattedError('''
-Error on line 1, column 1: expected /foo\\/bar/.
-
-^'''));
- });
-
- test("does not escape regexp backslashes", () {
- expect(() => new StringScanner('').expect(new RegExp(r'foo\bar')),
- throwsFormattedError('''
-Error on line 1, column 1: expected /foo\\bar/.
-
-^'''));
- });
- });
-}
diff --git a/test/line_scanner_test.dart b/test/line_scanner_test.dart
new file mode 100644
index 0000000..8cc79a6
--- /dev/null
+++ b/test/line_scanner_test.dart
@@ -0,0 +1,106 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.line_scanner_test;
+
+import 'package:string_scanner/string_scanner.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+ var scanner;
+ setUp(() {
+ scanner = new LineScanner('foo\nbar\nbaz');
+ });
+
+ test('begins with line and column 0', () {
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(0));
+ });
+
+ group("scan()", () {
+ test("consuming no newlines increases the column but not the line", () {
+ scanner.scan('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+ });
+
+ test("consuming a newline resets the column and increases the line", () {
+ scanner.expect('foo\nba');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(2));
+ });
+
+ test("consuming multiple newlines resets the column and increases the line",
+ () {
+ scanner.expect('foo\nbar\nb');
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+ });
+
+ group("readChar()", () {
+ test("on a non-newline character increases the column but not the line",
+ () {
+ scanner.readChar();
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(1));
+ });
+
+ test("consuming a newline resets the column and increases the line", () {
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.readChar();
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+ });
+
+ group("position=", () {
+ test("forward through newlines sets the line and column", () {
+ scanner.position = 9; // "foo\nbar\nb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test("forward through no newlines sets the column", () {
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test("backward through newlines sets the line and column", () {
+ scanner.scan("foo\nbar\nbaz");
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test("backward through no newlines sets the column", () {
+ scanner.scan("foo\nbar\nbaz");
+ scanner.position = 9; // "foo\nbar\nb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+ });
+
+ test("state= restores the line, column, and position", () {
+ scanner.scan('foo\nb');
+ var state = scanner.state;
+
+ scanner.scan('ar\nba');
+ scanner.state = state;
+ expect(scanner.rest, equals('ar\nbaz'));
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(1));
+ });
+
+ test("state= rejects a foreign state", () {
+ scanner.scan('foo\nb');
+
+ expect(() => new LineScanner(scanner.string).state = scanner.state,
+ throwsArgumentError);
+ });
+}
diff --git a/test/span_scanner_test.dart b/test/span_scanner_test.dart
new file mode 100644
index 0000000..93ba0b6
--- /dev/null
+++ b/test/span_scanner_test.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2014, 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.
+
+library string_scanner.span_scanner_test;
+
+import 'package:string_scanner/string_scanner.dart';
+import 'package:unittest/unittest.dart';
+
+void main() {
+ var scanner;
+ setUp(() {
+ scanner = new SpanScanner('foo\nbar\nbaz', 'source');
+ });
+
+ test("tracks the span for the last match", () {
+ scanner.scan('fo');
+ scanner.scan('o\nba');
+
+ var span = scanner.lastSpan;
+ expect(span.start.offset, equals(2));
+ expect(span.start.line, equals(0));
+ expect(span.start.column, equals(2));
+ expect(span.start.sourceUrl, equals('source'));
+
+ expect(span.end.offset, equals(6));
+ expect(span.end.line, equals(1));
+ expect(span.end.column, equals(2));
+ expect(span.start.sourceUrl, equals('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(6));
+ expect(span.start.line, equals(1));
+ expect(span.start.column, equals(2));
+ expect(span.start.sourceUrl, equals('source'));
+
+ expect(span.end.offset, equals(6));
+ expect(span.end.line, equals(1));
+ expect(span.end.column, equals(2));
+ expect(span.start.sourceUrl, equals('source'));
+
+ expect(span.text, equals(''));
+ });
+}
diff --git a/test/string_scanner_test.dart b/test/string_scanner_test.dart
index 0cab627..6144bf9 100644
--- a/test/string_scanner_test.dart
+++ b/test/string_scanner_test.dart
@@ -31,6 +31,18 @@
expect(scanner.position, equals(0));
});
+ test("readChar fails and doesn't change the state", () {
+ expect(scanner.readChar, throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("peekChar returns null and doesn't change the state", () {
+ expect(scanner.peekChar(), isNull);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
test("scan returns false and doesn't change the state", () {
expect(scanner.scan(new RegExp('.')), isFalse);
expect(scanner.lastMatch, isNull);
@@ -85,6 +97,24 @@
expect(scanner.position, equals(0));
});
+ test('readChar returns the first character and moves forward', () {
+ expect(scanner.readChar(), equals(0x66));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(1));
+ });
+
+ test('peekChar returns the first character', () {
+ expect(scanner.peekChar(), equals(0x66));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('peekChar with an argument returns the nth character', () {
+ expect(scanner.peekChar(4), equals(0x62));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
test("a matching scan returns true and changes the state", () {
expect(scanner.scan(new RegExp('f(..)')), isTrue);
expect(scanner.lastMatch[1], equals('oo'));
@@ -200,6 +230,18 @@
expect(scanner.position, equals(7));
});
+ test("readChar fails and doesn't change the state", () {
+ expect(scanner.readChar, throwsFormatException);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test("peekChar returns null and doesn't change the state", () {
+ expect(scanner.peekChar(), isNull);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
test("scan returns false and sets lastMatch to null", () {
expect(scanner.scan(new RegExp('.')), isFalse);
expect(scanner.lastMatch, isNull);
diff --git a/test/utils.dart b/test/utils.dart
index eee93d8..3de601c 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -4,14 +4,15 @@
library string_scanner.test.utils;
+import 'package:string_scanner/string_scanner.dart';
import 'package:unittest/unittest.dart';
/// Returns a matcher that asserts that a closure throws a [FormatException]
/// with the given [message].
-Matcher throwsFormattedError(String message) {
+Matcher throwsStringScannerException(String text) {
return throwsA(predicate((error) {
- expect(error, isFormatException);
- expect(error.message, equals(message));
+ expect(error, new isInstanceOf<StringScannerException>());
+ expect(error.span.text, equals(text));
return true;
}));
}