OLD | NEW |
---|---|
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /// A library for parsing strings using a sequence of patterns. | 5 library string_scanner.string_scanner; |
6 library string_scanner; | |
7 | 6 |
8 import 'dart:math' as math; | 7 import 'package:source_maps/source_maps.dart'; |
8 | |
9 import 'exception.dart'; | |
10 import 'utils.dart'; | |
9 | 11 |
10 /// When compiled to JS, forward slashes are always escaped in [RegExp.pattern]. | 12 /// When compiled to JS, forward slashes are always escaped in [RegExp.pattern]. |
11 /// | 13 /// |
12 /// See issue 17998. | 14 /// See issue 17998. |
13 final _slashAutoEscape = new RegExp("/").pattern == "\\/"; | 15 final _slashAutoEscape = new RegExp("/").pattern == "\\/"; |
14 | 16 |
15 // TODO(nweiz): Add some integration between this and source maps. | |
16 /// A class that scans through a string using [Pattern]s. | 17 /// A class that scans through a string using [Pattern]s. |
17 class StringScanner { | 18 class StringScanner { |
19 /// The URL of the source of the string being scanned. | |
20 /// | |
21 /// This is used for error reporting. It may be `null`, indicating that the | |
22 /// source URL is unknown or unavailable. | |
23 final Uri sourceUrl; | |
24 | |
18 /// The string being scanned through. | 25 /// The string being scanned through. |
19 final String string; | 26 final String string; |
20 | 27 |
21 /// The current position of the scanner in the string, in characters. | 28 /// The current position of the scanner in the string, in characters. |
22 int get position => _position; | 29 int get position => _position; |
23 set position(int position) { | 30 set position(int position) { |
24 if (position < 0 || position > string.length) { | 31 if (position < 0 || position > string.length) { |
25 throw new ArgumentError("Invalid position $position"); | 32 throw new ArgumentError("Invalid position $position"); |
26 } | 33 } |
27 | 34 |
28 _position = position; | 35 _position = position; |
29 } | 36 } |
30 int _position = 0; | 37 int _position = 0; |
31 | 38 |
32 /// The data about the previous match made by the scanner. | 39 /// The data about the previous match made by the scanner. |
33 /// | 40 /// |
34 /// If the last match failed, this will be `null`. | 41 /// If the last match failed, this will be `null`. |
35 Match get lastMatch => _lastMatch; | 42 Match get lastMatch => _lastMatch; |
36 Match _lastMatch; | 43 Match _lastMatch; |
37 | 44 |
38 /// The portion of the string that hasn't yet been scanned. | 45 /// The portion of the string that hasn't yet been scanned. |
39 String get rest => string.substring(position); | 46 String get rest => string.substring(position); |
40 | 47 |
41 /// Whether the scanner has completely consumed [string]. | 48 /// Whether the scanner has completely consumed [string]. |
42 bool get isDone => position == string.length; | 49 bool get isDone => position == string.length; |
43 | 50 |
44 /// Creates a new [StringScanner] that starts scanning from [position]. | 51 /// Creates a new [StringScanner] that starts scanning from [position]. |
45 /// | 52 /// |
46 /// [position] defaults to 0, the beginning of the string. | 53 /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the |
47 StringScanner(this.string, {int position}) { | 54 /// URL of the source of the string being scanned, if available. It can be |
55 /// either a [String] or a [Uri]. | |
56 StringScanner(this.string, {sourceUrl, int position}) | |
57 : sourceUrl = sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl { | |
48 if (position != null) this.position = position; | 58 if (position != null) this.position = position; |
49 } | 59 } |
50 | 60 |
61 /// Consumes a single character and returns its character code. | |
62 /// | |
63 /// This throws a [FormatException] if the string has been fully consumed. It | |
64 /// doesn't affect [lastMatch]. | |
65 int readChar() { | |
66 if (isDone) _fail("more input"); | |
67 return string.codeUnitAt(_position++); | |
68 } | |
69 | |
70 /// Returns the character code of the character [offset] away from [position]. | |
71 /// | |
72 /// [offset] defaults to zero, and may be negative to inspect already-consumed | |
73 /// characters. | |
74 /// | |
75 /// This returns `null` if [offset] points outside the string. It doesn't | |
76 /// affect [lastMatch]. | |
77 int peekChar([int offset]) { | |
78 if (offset == null) offset = 0; | |
79 var index = position + offset; | |
80 if (index >= string.length || index < 0) return null; | |
Bob Nystrom
2014/05/28 21:28:57
Tiny nit, but I think it reads a bit better if the
nweiz
2014/05/28 23:56:34
Done.
| |
81 return string.codeUnitAt(index); | |
82 } | |
83 | |
51 /// If [pattern] matches at the current position of the string, scans forward | 84 /// If [pattern] matches at the current position of the string, scans forward |
52 /// until the end of the match. | 85 /// until the end of the match. |
53 /// | 86 /// |
54 /// Returns whether or not [pattern] matched. | 87 /// Returns whether or not [pattern] matched. |
55 bool scan(Pattern pattern) { | 88 bool scan(Pattern pattern) { |
56 var success = matches(pattern); | 89 var success = matches(pattern); |
57 if (success) _position = _lastMatch.end; | 90 if (success) _position = _lastMatch.end; |
58 return success; | 91 return success; |
59 } | 92 } |
60 | 93 |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
105 /// error is associated. This should be a match returned by this scanner's | 138 /// error is associated. This should be a match returned by this scanner's |
106 /// [lastMatch] property. By default, the error is associated with the last | 139 /// [lastMatch] property. By default, the error is associated with the last |
107 /// match. | 140 /// match. |
108 /// | 141 /// |
109 /// If [position] and/or [length] are passed, they are used as the error span | 142 /// If [position] and/or [length] are passed, they are used as the error span |
110 /// instead. If only [length] is passed, [position] defaults to the current | 143 /// instead. If only [length] is passed, [position] defaults to the current |
111 /// position; if only [position] is passed, [length] defaults to 1. | 144 /// position; if only [position] is passed, [length] defaults to 1. |
112 /// | 145 /// |
113 /// It's an error to pass [match] at the same time as [position] or [length]. | 146 /// It's an error to pass [match] at the same time as [position] or [length]. |
114 void error(String message, {Match match, int position, int length}) { | 147 void error(String message, {Match match, int position, int length}) { |
115 if (match != null && (position != null || length != null)) { | 148 validateErrorArgs(string, match, position, length); |
116 throw new ArgumentError("Can't pass both match and position/length."); | |
117 } | |
118 | |
119 if (position != null && position < 0) { | |
120 throw new RangeError("position must be greater than or equal to 0."); | |
121 } | |
122 | |
123 if (length != null && length < 1) { | |
124 throw new RangeError("length must be greater than or equal to 0."); | |
125 } | |
126 | 149 |
127 if (match == null && position == null && length == null) match = lastMatch; | 150 if (match == null && position == null && length == null) match = lastMatch; |
128 if (position == null) { | 151 if (position == null) { |
129 position = match == null ? this.position : match.start; | 152 position = match == null ? this.position : match.start; |
130 } | 153 } |
131 if (length == null) length = match == null ? 1 : match.end - match.start; | 154 if (length == null) length = match == null ? 1 : match.end - match.start; |
132 | 155 |
133 var newlines = "\n".allMatches(string.substring(0, position)).toList(); | 156 var url = sourceUrl == null ? null : sourceUrl.toString(); |
134 var line = newlines.length + 1; | 157 var sourceFile = new SourceFile.text(url, string); |
135 var column; | 158 var span = sourceFile.span(position, position + length); |
136 var lastLine; | 159 throw new StringScannerException(message, string, sourceUrl, span); |
137 if (newlines.isEmpty) { | |
138 column = position + 1; | |
139 lastLine = string.substring(0, position); | |
140 } else { | |
141 column = position - newlines.last.end + 1; | |
142 lastLine = string.substring(newlines.last.end, position); | |
143 } | |
144 | |
145 var remaining = string.substring(position); | |
146 var nextNewline = remaining.indexOf("\n"); | |
147 if (nextNewline == -1) { | |
148 lastLine += remaining; | |
149 } else { | |
150 length = math.min(length, nextNewline); | |
151 lastLine += remaining.substring(0, nextNewline); | |
152 } | |
153 | |
154 var spaces = new List.filled(column - 1, ' ').join(); | |
155 var underline = new List.filled(length, '^').join(); | |
156 | |
157 throw new FormatException( | |
158 "Error on line $line, column $column: $message\n" | |
159 "$lastLine\n" | |
160 "$spaces$underline"); | |
161 } | 160 } |
162 | 161 |
163 // TODO(nweiz): Make this handle long lines more gracefully. | 162 // TODO(nweiz): Make this handle long lines more gracefully. |
164 /// Throws a [FormatException] describing that [name] is expected at the | 163 /// Throws a [FormatException] describing that [name] is expected at the |
165 /// current position in the string. | 164 /// current position in the string. |
166 void _fail(String name) { | 165 void _fail(String name) { |
167 error("expected $name.", position: this.position, length: 1); | 166 error("expected $name.", position: this.position, length: 1); |
168 } | 167 } |
169 } | 168 } |
OLD | NEW |