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 /// A library for parsing strings using a sequence of patterns. |
6 library string_scanner; | 6 library string_scanner; |
7 | 7 |
8 import 'dart:math' as math; | 8 export 'src/exception.dart'; |
9 | 9 export 'src/line_scanner.dart'; |
10 /// When compiled to JS, forward slashes are always escaped in [RegExp.pattern]. | 10 export 'src/span_scanner.dart'; |
11 /// | 11 export 'src/string_scanner.dart'; |
12 /// See issue 17998. | |
13 final _slashAutoEscape = new RegExp("/").pattern == "\\/"; | |
14 | |
15 // TODO(nweiz): Add some integration between this and source maps. | |
16 /// A class that scans through a string using [Pattern]s. | |
17 class StringScanner { | |
18 /// The string being scanned through. | |
19 final String string; | |
20 | |
21 /// The current position of the scanner in the string, in characters. | |
22 int get position => _position; | |
23 set position(int position) { | |
24 if (position < 0 || position > string.length) { | |
25 throw new ArgumentError("Invalid position $position"); | |
26 } | |
27 | |
28 _position = position; | |
29 } | |
30 int _position = 0; | |
31 | |
32 /// The data about the previous match made by the scanner. | |
33 /// | |
34 /// If the last match failed, this will be `null`. | |
35 Match get lastMatch => _lastMatch; | |
36 Match _lastMatch; | |
37 | |
38 /// The portion of the string that hasn't yet been scanned. | |
39 String get rest => string.substring(position); | |
40 | |
41 /// Whether the scanner has completely consumed [string]. | |
42 bool get isDone => position == string.length; | |
43 | |
44 /// Creates a new [StringScanner] that starts scanning from [position]. | |
45 /// | |
46 /// [position] defaults to 0, the beginning of the string. | |
47 StringScanner(this.string, {int position}) { | |
48 if (position != null) this.position = position; | |
49 } | |
50 | |
51 /// If [pattern] matches at the current position of the string, scans forward | |
52 /// until the end of the match. | |
53 /// | |
54 /// Returns whether or not [pattern] matched. | |
55 bool scan(Pattern pattern) { | |
56 var success = matches(pattern); | |
57 if (success) _position = _lastMatch.end; | |
58 return success; | |
59 } | |
60 | |
61 /// If [pattern] matches at the current position of the string, scans forward | |
62 /// until the end of the match. | |
63 /// | |
64 /// If [pattern] did not match, throws a [FormatException] describing the | |
65 /// position of the failure. [name] is used in this error as the expected name | |
66 /// of the pattern being matched; if it's `null`, the pattern itself is used | |
67 /// instead. | |
68 void expect(Pattern pattern, {String name}) { | |
69 if (scan(pattern)) return; | |
70 | |
71 if (name == null) { | |
72 if (pattern is RegExp) { | |
73 var source = pattern.pattern; | |
74 if (!_slashAutoEscape) source = source.replaceAll("/", "\\/"); | |
75 name = "/$source/"; | |
76 } else { | |
77 name = pattern.toString() | |
78 .replaceAll("\\", "\\\\").replaceAll('"', '\\"'); | |
79 name = '"$name"'; | |
80 } | |
81 } | |
82 _fail(name); | |
83 } | |
84 | |
85 /// If the string has not been fully consumed, this throws a | |
86 /// [FormatException]. | |
87 void expectDone() { | |
88 if (isDone) return; | |
89 _fail("no more input"); | |
90 } | |
91 | |
92 /// Returns whether or not [pattern] matches at the current position of the | |
93 /// string. | |
94 /// | |
95 /// This doesn't move the scan pointer forward. | |
96 bool matches(Pattern pattern) { | |
97 _lastMatch = pattern.matchAsPrefix(string, position); | |
98 return _lastMatch != null; | |
99 } | |
100 | |
101 /// Throws a [FormatException] with [message] as well as a detailed | |
102 /// description of the location of the error in the string. | |
103 /// | |
104 /// [match] is the match information for the span of the string with which the | |
105 /// 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 | |
107 /// match. | |
108 /// | |
109 /// 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 | |
111 /// position; if only [position] is passed, [length] defaults to 1. | |
112 /// | |
113 /// 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}) { | |
115 if (match != null && (position != null || length != null)) { | |
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 | |
127 if (match == null && position == null && length == null) match = lastMatch; | |
128 if (position == null) { | |
129 position = match == null ? this.position : match.start; | |
130 } | |
131 if (length == null) length = match == null ? 1 : match.end - match.start; | |
132 | |
133 var newlines = "\n".allMatches(string.substring(0, position)).toList(); | |
134 var line = newlines.length + 1; | |
135 var column; | |
136 var lastLine; | |
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 } | |
162 | |
163 // TODO(nweiz): Make this handle long lines more gracefully. | |
164 /// Throws a [FormatException] describing that [name] is expected at the | |
165 /// current position in the string. | |
166 void _fail(String name) { | |
167 error("expected $name.", position: this.position, length: 1); | |
168 } | |
169 } | |
OLD | NEW |