Line data Source code
1 : // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
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.
4 :
5 : /// Contains the top-level function to parse source maps version 3.
6 : library source_maps.parser;
7 :
8 : import 'dart:collection';
9 : import 'dart:convert';
10 :
11 : import 'package:source_span/source_span.dart';
12 :
13 : import 'builder.dart' as builder;
14 : import 'src/source_map_span.dart';
15 : import 'src/utils.dart';
16 : import 'src/vlq.dart';
17 :
18 : /// Parses a source map directly from a json string.
19 : ///
20 : /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
21 : /// the source map file itself. If it's passed, any URLs in the source
22 : /// map will be interpreted as relative to this URL when generating spans.
23 : // TODO(sigmund): evaluate whether other maps should have the json parsed, or
24 : // the string represenation.
25 : // TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string
26 : // `)]}'` begins the string representation of the map.
27 : Mapping parse(String jsonMap, {Map<String, Map> otherMaps, mapUrl}) =>
28 0 : parseJson(JSON.decode(jsonMap), otherMaps: otherMaps, mapUrl: mapUrl);
29 :
30 : /// Parses a source map or source map bundle directly from a json string.
31 : ///
32 : /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
33 : /// the source map file itself. If it's passed, any URLs in the source
34 : /// map will be interpreted as relative to this URL when generating spans.
35 : Mapping parseExtended(String jsonMap, {Map<String, Map> otherMaps, mapUrl}) =>
36 0 : parseJsonExtended(JSON.decode(jsonMap),
37 : otherMaps: otherMaps, mapUrl: mapUrl);
38 :
39 : /// Parses a source map or source map bundle.
40 : ///
41 : /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
42 : /// the source map file itself. If it's passed, any URLs in the source
43 : /// map will be interpreted as relative to this URL when generating spans.
44 : Mapping parseJsonExtended(/*List|Map*/ json,
45 : {Map<String, Map> otherMaps, mapUrl}) {
46 0 : if (json is List) {
47 0 : return new MappingBundle.fromJson(json, mapUrl: mapUrl);
48 : }
49 0 : return parseJson(json as Map);
50 : }
51 :
52 : /// Parses a source map
53 : ///
54 : /// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
55 : /// the source map file itself. If it's passed, any URLs in the source
56 : /// map will be interpreted as relative to this URL when generating spans.
57 : Mapping parseJson(Map map, {Map<String, Map> otherMaps, mapUrl}) {
58 0 : if (map['version'] != 3) {
59 0 : throw new ArgumentError('unexpected source map version: ${map["version"]}. '
60 0 : 'Only version 3 is supported.');
61 : }
62 :
63 0 : if (map.containsKey('sections')) {
64 0 : if (map.containsKey('mappings') ||
65 0 : map.containsKey('sources') ||
66 0 : map.containsKey('names')) {
67 0 : throw new FormatException('map containing "sections" '
68 : 'cannot contain "mappings", "sources", or "names".');
69 : }
70 0 : return new MultiSectionMapping.fromJson(map['sections'], otherMaps,
71 : mapUrl: mapUrl);
72 : }
73 0 : return new SingleMapping.fromJson(map, mapUrl: mapUrl);
74 : }
75 :
76 : /// A mapping parsed out of a source map.
77 : abstract class Mapping {
78 : /// Returns the span associated with [line] and [column].
79 : ///
80 : /// [uri] is the optional location of the output file to find the span for
81 : /// to disambiguate cases where a mapping may have different mappings for
82 : /// different output files.
83 : SourceMapSpan spanFor(int line, int column,
84 : {Map<String, SourceFile> files, String uri});
85 :
86 : /// Returns the span associated with [location].
87 : SourceMapSpan spanForLocation(SourceLocation location,
88 : {Map<String, SourceFile> files}) {
89 0 : return spanFor(location.line, location.column,
90 0 : uri: location.sourceUrl?.toString(), files: files);
91 : }
92 : }
93 :
94 : /// A meta-level map containing sections.
95 : class MultiSectionMapping extends Mapping {
96 : /// For each section, the start line offset.
97 : final List<int> _lineStart = <int>[];
98 :
99 : /// For each section, the start column offset.
100 : final List<int> _columnStart = <int>[];
101 :
102 : /// For each section, the actual source map information, which is not adjusted
103 : /// for offsets.
104 : final List<Mapping> _maps = <Mapping>[];
105 :
106 : /// Creates a section mapping from json.
107 : MultiSectionMapping.fromJson(List sections, Map<String, Map> otherMaps,
108 0 : {mapUrl}) {
109 0 : for (var section in sections) {
110 0 : var offset = section['offset'];
111 0 : if (offset == null) throw new FormatException('section missing offset');
112 :
113 0 : var line = section['offset']['line'];
114 0 : if (line == null) throw new FormatException('offset missing line');
115 :
116 0 : var column = section['offset']['column'];
117 0 : if (column == null) throw new FormatException('offset missing column');
118 :
119 0 : _lineStart.add(line);
120 0 : _columnStart.add(column);
121 :
122 0 : var url = section['url'];
123 0 : var map = section['map'];
124 :
125 : if (url != null && map != null) {
126 0 : throw new FormatException("section can't use both url and map entries");
127 : } else if (url != null) {
128 0 : if (otherMaps == null || otherMaps[url] == null) {
129 0 : throw new FormatException(
130 : 'section contains refers to $url, but no map was '
131 0 : 'given for it. Make sure a map is passed in "otherMaps"');
132 : }
133 0 : _maps.add(parseJson(otherMaps[url], otherMaps: otherMaps, mapUrl: url));
134 : } else if (map != null) {
135 0 : _maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl));
136 : } else {
137 0 : throw new FormatException('section missing url or map');
138 : }
139 : }
140 0 : if (_lineStart.length == 0) {
141 0 : throw new FormatException('expected at least one section');
142 : }
143 : }
144 :
145 : int _indexFor(line, column) {
146 0 : for (int i = 0; i < _lineStart.length; i++) {
147 0 : if (line < _lineStart[i]) return i - 1;
148 0 : if (line == _lineStart[i] && column < _columnStart[i]) return i - 1;
149 : }
150 0 : return _lineStart.length - 1;
151 : }
152 :
153 : SourceMapSpan spanFor(int line, int column,
154 : {Map<String, SourceFile> files, String uri}) {
155 : // TODO(jacobr): perhaps verify that targetUrl matches the actual uri
156 : // or at least ends in the same file name.
157 0 : int index = _indexFor(line, column);
158 0 : return _maps[index].spanFor(
159 0 : line - _lineStart[index], column - _columnStart[index],
160 : files: files);
161 : }
162 :
163 : String toString() {
164 0 : var buff = new StringBuffer("$runtimeType : [");
165 0 : for (int i = 0; i < _lineStart.length; i++) {
166 : buff
167 0 : ..write('(')
168 0 : ..write(_lineStart[i])
169 0 : ..write(',')
170 0 : ..write(_columnStart[i])
171 0 : ..write(':')
172 0 : ..write(_maps[i])
173 0 : ..write(')');
174 : }
175 0 : buff.write(']');
176 0 : return buff.toString();
177 : }
178 : }
179 :
180 : class MappingBundle extends Mapping {
181 : Map<String, SingleMapping> _mappings = {};
182 :
183 0 : MappingBundle() {}
184 :
185 0 : MappingBundle.fromJson(List json, {String mapUrl}) {
186 0 : for (var map in json) {
187 0 : addMapping(parseJson(map, mapUrl: mapUrl) as SingleMapping);
188 : }
189 : }
190 :
191 : addMapping(SingleMapping mapping) {
192 : // TODO(jacobr): verify that targetUrl is valid uri instead of a windows
193 : // path.
194 0 : _mappings[mapping.targetUrl] = mapping;
195 : }
196 :
197 : /// Encodes the Mapping mappings as a json map.
198 0 : List toJson() => _mappings.values.map((v) => v.toJson()).toList();
199 :
200 : String toString() {
201 0 : var buff = new StringBuffer();
202 0 : for (var map in _mappings.values) {
203 0 : buff.write(map.toString());
204 : }
205 0 : return buff.toString();
206 : }
207 :
208 0 : bool containsMapping(String url) => _mappings.containsKey(url);
209 :
210 : SourceMapSpan spanFor(int line, int column,
211 : {Map<String, SourceFile> files, String uri}) {
212 : if (uri == null) {
213 0 : throw new ArgumentError.notNull('uri');
214 : }
215 :
216 : // Find the longest suffix of the uri that matches the sourcemap
217 : // where the suffix starts after a path segment boundary.
218 : // We consider ":" and "/" as path segment boundaries so that
219 : // "package:" uris can be handled with minimal special casing. Having a
220 : // few false positive path segment boundaries is not a significant issue
221 : // as we prefer the longest matching prefix.
222 : // Using package:path `path.split` to find path segment boundaries would
223 : // not generate all of the path segment boundaries we want for "package:"
224 : // urls as "package:package_name" would be one path segment when we want
225 : // "package" and "package_name" to be sepearate path segments.
226 :
227 : bool onBoundary = true;
228 0 : var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)];
229 0 : for (var i = 0; i < uri.length; ++i) {
230 : if (onBoundary) {
231 0 : var candidate = uri.substring(i);
232 0 : if (_mappings.containsKey(candidate)) {
233 0 : return _mappings[candidate]
234 0 : .spanFor(line, column, files: files, uri: candidate);
235 : }
236 : }
237 0 : onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i));
238 : }
239 :
240 : // Note: when there is no source map for an uri, this behaves like an
241 : // identity function, returning the requested location as the result.
242 :
243 : // Create a mock offset for the output location. We compute it in terms
244 : // of the input line and column to minimize the chances that two different
245 : // line and column locations are mapped to the same offset.
246 0 : var offset = line * 1000000 + column;
247 0 : var location = new SourceLocation(offset,
248 0 : line: line, column: column, sourceUrl: Uri.parse(uri));
249 0 : return new SourceMapSpan(location, location, "");
250 : }
251 : }
252 :
253 : /// A map containing direct source mappings.
254 : class SingleMapping extends Mapping {
255 : /// Source urls used in the mapping, indexed by id.
256 : final List<String> urls;
257 :
258 : /// Source names used in the mapping, indexed by id.
259 : final List<String> names;
260 :
261 : /// Entries indicating the beginning of each span.
262 : final List<TargetLineEntry> lines;
263 :
264 : /// Url of the target file.
265 : String targetUrl;
266 :
267 : /// Source root prepended to all entries in [urls].
268 : String sourceRoot;
269 :
270 : final Uri _mapUrl;
271 :
272 : SingleMapping._(this.targetUrl, this.urls, this.names, this.lines)
273 0 : : _mapUrl = null;
274 :
275 : factory SingleMapping.fromEntries(Iterable<builder.Entry> entries,
276 : [String fileUrl]) {
277 : // The entries needs to be sorted by the target offsets.
278 0 : var sourceEntries = new List.from(entries)..sort();
279 0 : var lines = <TargetLineEntry>[];
280 :
281 : // Indices associated with file urls that will be part of the source map. We
282 : // use a linked hash-map so that `_urls.keys[_urls[u]] == u`
283 0 : var urls = new LinkedHashMap<String, int>();
284 :
285 : // Indices associated with identifiers that will be part of the source map.
286 : // We use a linked hash-map so that `_names.keys[_names[n]] == n`
287 0 : var names = new LinkedHashMap<String, int>();
288 :
289 : var lineNum;
290 : List<TargetEntry> targetEntries;
291 0 : for (var sourceEntry in sourceEntries) {
292 0 : if (lineNum == null || sourceEntry.target.line > lineNum) {
293 0 : lineNum = sourceEntry.target.line;
294 0 : targetEntries = <TargetEntry>[];
295 0 : lines.add(new TargetLineEntry(lineNum, targetEntries));
296 : }
297 :
298 0 : if (sourceEntry.source == null) {
299 0 : targetEntries.add(new TargetEntry(sourceEntry.target.column));
300 : } else {
301 0 : var sourceUrl = sourceEntry.source.sourceUrl;
302 0 : var urlId = urls.putIfAbsent(
303 0 : sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length);
304 0 : var srcNameId = sourceEntry.identifierName == null
305 : ? null
306 0 : : names.putIfAbsent(sourceEntry.identifierName, () => names.length);
307 0 : targetEntries.add(new TargetEntry(sourceEntry.target.column, urlId,
308 0 : sourceEntry.source.line, sourceEntry.source.column, srcNameId));
309 : }
310 : }
311 0 : return new SingleMapping._(
312 0 : fileUrl, urls.keys.toList(), names.keys.toList(), lines);
313 : }
314 :
315 : SingleMapping.fromJson(Map map, {mapUrl})
316 0 : : targetUrl = map['file'],
317 0 : urls = new List<String>.from(map['sources']),
318 0 : names = new List<String>.from(map['names']),
319 0 : sourceRoot = map['sourceRoot'],
320 0 : lines = <TargetLineEntry>[],
321 0 : _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : mapUrl {
322 : int line = 0;
323 : int column = 0;
324 : int srcUrlId = 0;
325 : int srcLine = 0;
326 : int srcColumn = 0;
327 : int srcNameId = 0;
328 0 : var tokenizer = new _MappingTokenizer(map['mappings']);
329 0 : var entries = <TargetEntry>[];
330 :
331 0 : while (tokenizer.hasTokens) {
332 0 : if (tokenizer.nextKind.isNewLine) {
333 0 : if (!entries.isEmpty) {
334 0 : lines.add(new TargetLineEntry(line, entries));
335 0 : entries = <TargetEntry>[];
336 : }
337 0 : line++;
338 : column = 0;
339 0 : tokenizer._consumeNewLine();
340 : continue;
341 : }
342 :
343 : // Decode the next entry, using the previous encountered values to
344 : // decode the relative values.
345 : //
346 : // We expect 1, 4, or 5 values. If present, values are expected in the
347 : // following order:
348 : // 0: the starting column in the current line of the generated file
349 : // 1: the id of the original source file
350 : // 2: the starting line in the original source
351 : // 3: the starting column in the original source
352 : // 4: the id of the original symbol name
353 : // The values are relative to the previous encountered values.
354 0 : if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line);
355 0 : column += tokenizer._consumeValue();
356 0 : if (!tokenizer.nextKind.isValue) {
357 0 : entries.add(new TargetEntry(column));
358 : } else {
359 0 : srcUrlId += tokenizer._consumeValue();
360 0 : if (srcUrlId >= urls.length) {
361 0 : throw new StateError(
362 0 : 'Invalid source url id. $targetUrl, $line, $srcUrlId');
363 : }
364 0 : if (!tokenizer.nextKind.isValue) throw _segmentError(2, line);
365 0 : srcLine += tokenizer._consumeValue();
366 0 : if (!tokenizer.nextKind.isValue) throw _segmentError(3, line);
367 0 : srcColumn += tokenizer._consumeValue();
368 0 : if (!tokenizer.nextKind.isValue) {
369 0 : entries.add(new TargetEntry(column, srcUrlId, srcLine, srcColumn));
370 : } else {
371 0 : srcNameId += tokenizer._consumeValue();
372 0 : if (srcNameId >= names.length) {
373 0 : throw new StateError(
374 0 : 'Invalid name id: $targetUrl, $line, $srcNameId');
375 : }
376 0 : entries.add(
377 0 : new TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId));
378 : }
379 : }
380 0 : if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment();
381 : }
382 0 : if (!entries.isEmpty) {
383 0 : lines.add(new TargetLineEntry(line, entries));
384 : }
385 : }
386 :
387 : /// Encodes the Mapping mappings as a json map.
388 : Map toJson() {
389 0 : var buff = new StringBuffer();
390 : var line = 0;
391 : var column = 0;
392 : var srcLine = 0;
393 : var srcColumn = 0;
394 : var srcUrlId = 0;
395 : var srcNameId = 0;
396 : var first = true;
397 :
398 0 : for (var entry in lines) {
399 0 : int nextLine = entry.line;
400 0 : if (nextLine > line) {
401 0 : for (int i = line; i < nextLine; ++i) {
402 0 : buff.write(';');
403 : }
404 : line = nextLine;
405 : column = 0;
406 : first = true;
407 : }
408 :
409 0 : for (var segment in entry.entries) {
410 0 : if (!first) buff.write(',');
411 : first = false;
412 0 : column = _append(buff, column, segment.column);
413 :
414 : // Encoding can be just the column offset if there is no source
415 : // information.
416 0 : var newUrlId = segment.sourceUrlId;
417 : if (newUrlId == null) continue;
418 0 : srcUrlId = _append(buff, srcUrlId, newUrlId);
419 0 : srcLine = _append(buff, srcLine, segment.sourceLine);
420 0 : srcColumn = _append(buff, srcColumn, segment.sourceColumn);
421 :
422 0 : if (segment.sourceNameId == null) continue;
423 0 : srcNameId = _append(buff, srcNameId, segment.sourceNameId);
424 : }
425 : }
426 :
427 0 : var result = {
428 : 'version': 3,
429 0 : 'sourceRoot': sourceRoot == null ? '' : sourceRoot,
430 0 : 'sources': urls,
431 0 : 'names': names,
432 0 : 'mappings': buff.toString()
433 : };
434 0 : if (targetUrl != null) {
435 0 : result['file'] = targetUrl;
436 : }
437 : return result;
438 : }
439 :
440 : /// Appends to [buff] a VLQ encoding of [newValue] using the difference
441 : /// between [oldValue] and [newValue]
442 : static int _append(StringBuffer buff, int oldValue, int newValue) {
443 0 : buff.writeAll(encodeVlq(newValue - oldValue));
444 : return newValue;
445 : }
446 :
447 : _segmentError(int seen, int line) =>
448 0 : new StateError('Invalid entry in sourcemap, expected 1, 4, or 5'
449 0 : ' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
450 :
451 : /// Returns [TargetLineEntry] which includes the location in the target [line]
452 : /// number. In particular, the resulting entry is the last entry whose line
453 : /// number is lower or equal to [line].
454 : TargetLineEntry _findLine(int line) {
455 0 : int index = binarySearch(lines, (e) => e.line > line);
456 0 : return (index <= 0) ? null : lines[index - 1];
457 : }
458 :
459 : /// Returns [TargetEntry] which includes the location denoted by
460 : /// [line], [column]. If [lineEntry] corresponds to [line], then this will be
461 : /// the last entry whose column is lower or equal than [column]. If
462 : /// [lineEntry] corresponds to a line prior to [line], then the result will be
463 : /// the very last entry on that line.
464 : TargetEntry _findColumn(int line, int column, TargetLineEntry lineEntry) {
465 0 : if (lineEntry == null || lineEntry.entries.length == 0) return null;
466 0 : if (lineEntry.line != line) return lineEntry.entries.last;
467 0 : var entries = lineEntry.entries;
468 0 : int index = binarySearch(entries, (e) => e.column > column);
469 0 : return (index <= 0) ? null : entries[index - 1];
470 : }
471 :
472 : SourceMapSpan spanFor(int line, int column,
473 : {Map<String, SourceFile> files, String uri}) {
474 0 : var entry = _findColumn(line, column, _findLine(line));
475 0 : if (entry == null || entry.sourceUrlId == null) return null;
476 0 : var url = urls[entry.sourceUrlId];
477 0 : if (sourceRoot != null) {
478 0 : url = '${sourceRoot}${url}';
479 : }
480 0 : if (files != null && files[url] != null) {
481 0 : var file = files[url];
482 0 : var start = file.getOffset(entry.sourceLine, entry.sourceColumn);
483 0 : if (entry.sourceNameId != null) {
484 0 : var text = names[entry.sourceNameId];
485 0 : return new SourceMapFileSpan(
486 0 : files[url].span(start, start + text.length),
487 : isIdentifier: true);
488 : } else {
489 0 : return new SourceMapFileSpan(files[url].location(start).pointSpan());
490 : }
491 : } else {
492 0 : var start = new SourceLocation(0,
493 0 : sourceUrl: _mapUrl == null ? url : _mapUrl.resolve(url),
494 0 : line: entry.sourceLine,
495 0 : column: entry.sourceColumn);
496 :
497 : // Offset and other context is not available.
498 0 : if (entry.sourceNameId != null) {
499 0 : return new SourceMapSpan.identifier(start, names[entry.sourceNameId]);
500 : } else {
501 0 : return new SourceMapSpan(start, start, '');
502 : }
503 : }
504 : }
505 :
506 : String toString() {
507 0 : return (new StringBuffer("$runtimeType : [")
508 0 : ..write('targetUrl: ')
509 0 : ..write(targetUrl)
510 0 : ..write(', sourceRoot: ')
511 0 : ..write(sourceRoot)
512 0 : ..write(', urls: ')
513 0 : ..write(urls)
514 0 : ..write(', names: ')
515 0 : ..write(names)
516 0 : ..write(', lines: ')
517 0 : ..write(lines)
518 0 : ..write(']'))
519 0 : .toString();
520 : }
521 :
522 : String get debugString {
523 0 : var buff = new StringBuffer();
524 0 : for (var lineEntry in lines) {
525 0 : var line = lineEntry.line;
526 0 : for (var entry in lineEntry.entries) {
527 : buff
528 0 : ..write(targetUrl)
529 0 : ..write(': ')
530 0 : ..write(line)
531 0 : ..write(':')
532 0 : ..write(entry.column);
533 0 : if (entry.sourceUrlId != null) {
534 : buff
535 0 : ..write(' --> ')
536 0 : ..write(sourceRoot)
537 0 : ..write(urls[entry.sourceUrlId])
538 0 : ..write(': ')
539 0 : ..write(entry.sourceLine)
540 0 : ..write(':')
541 0 : ..write(entry.sourceColumn);
542 : }
543 0 : if (entry.sourceNameId != null) {
544 0 : buff..write(' (')..write(names[entry.sourceNameId])..write(')');
545 : }
546 0 : buff.write('\n');
547 : }
548 : }
549 0 : return buff.toString();
550 : }
551 : }
552 :
553 : /// A line entry read from a source map.
554 : class TargetLineEntry {
555 : final int line;
556 : List<TargetEntry> entries;
557 0 : TargetLineEntry(this.line, this.entries);
558 :
559 0 : String toString() => '$runtimeType: $line $entries';
560 : }
561 :
562 : /// A target segment entry read from a source map
563 : class TargetEntry {
564 : final int column;
565 : final int sourceUrlId;
566 : final int sourceLine;
567 : final int sourceColumn;
568 : final int sourceNameId;
569 :
570 : TargetEntry(this.column,
571 : [this.sourceUrlId,
572 : this.sourceLine,
573 : this.sourceColumn,
574 0 : this.sourceNameId]);
575 :
576 0 : String toString() => '$runtimeType: '
577 0 : '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)';
578 : }
579 :
580 : /** A character iterator over a string that can peek one character ahead. */
581 : class _MappingTokenizer implements Iterator<String> {
582 : final String _internal;
583 : final int _length;
584 : int index = -1;
585 : _MappingTokenizer(String internal)
586 : : _internal = internal,
587 0 : _length = internal.length;
588 :
589 : // Iterator API is used by decodeVlq to consume VLQ entries.
590 0 : bool moveNext() => ++index < _length;
591 : String get current =>
592 0 : (index >= 0 && index < _length) ? _internal[index] : null;
593 :
594 0 : bool get hasTokens => index < _length - 1 && _length > 0;
595 :
596 : _TokenKind get nextKind {
597 0 : if (!hasTokens) return _TokenKind.EOF;
598 0 : var next = _internal[index + 1];
599 0 : if (next == ';') return _TokenKind.LINE;
600 0 : if (next == ',') return _TokenKind.SEGMENT;
601 : return _TokenKind.VALUE;
602 : }
603 :
604 0 : int _consumeValue() => decodeVlq(this);
605 : void _consumeNewLine() {
606 0 : ++index;
607 : }
608 :
609 : void _consumeNewSegment() {
610 0 : ++index;
611 : }
612 :
613 : // Print the state of the iterator, with colors indicating the current
614 : // position.
615 : String toString() {
616 0 : var buff = new StringBuffer();
617 0 : for (int i = 0; i < index; i++) {
618 0 : buff.write(_internal[i]);
619 : }
620 0 : buff.write('[31m');
621 0 : buff.write(current == null ? '' : current);
622 0 : buff.write('[0m');
623 0 : for (int i = index + 1; i < _internal.length; i++) {
624 0 : buff.write(_internal[i]);
625 : }
626 0 : buff.write(' ($index)');
627 0 : return buff.toString();
628 : }
629 : }
630 :
631 : class _TokenKind {
632 : static const _TokenKind LINE = const _TokenKind(isNewLine: true);
633 : static const _TokenKind SEGMENT = const _TokenKind(isNewSegment: true);
634 : static const _TokenKind EOF = const _TokenKind(isEof: true);
635 : static const _TokenKind VALUE = const _TokenKind();
636 : final bool isNewLine;
637 : final bool isNewSegment;
638 : final bool isEof;
639 0 : bool get isValue => !isNewLine && !isNewSegment && !isEof;
640 :
641 : const _TokenKind(
642 5 : {this.isNewLine: false, this.isNewSegment: false, this.isEof: false});
643 : }
|