Move source maps package to the dart repo, so it can be used by other internal
tools.
Review URL: https://codereview.chromium.org//12207008
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/source_maps@20300 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..df182d9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+Source Maps
+===========
+
+This project implements a Dart pub package to work with source maps. The
+implementation is based on the [source map version 3 spec][spec] which was
+originated from the [Closure Compiler][closure] and has been implemented in
+Chrome and Firefox.
+
+In this package we provide:
+ * Data types defining file locations and spans: these are not part of the
+ original source map specification. These data types are great for tracking
+ source locations on source maps, but they can also be used by tools to
+ reporting useful error messages that include on source locations.
+ * A builder that creates a source map programatically and produces the encoded
+ source map format.
+ * A parser that reads the source map format and provides APIs to read the
+ mapping information.
+
+Some upcoming features we are planning to add to this package are:
+ * A printer that lets you generate code, but record source map information in
+ the process.
+ * A tool that can compose source maps together. This would be useful for
+ instance, if you have 2 tools that produce source maps and you call one with
+ the result of the other.
+
+[closure]: http://code.google.com/p/closure-compiler/wiki/SourceMaps
+[spec]: https://docs.google.com/a/google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
diff --git a/lib/builder.dart b/lib/builder.dart
new file mode 100644
index 0000000..ad80fa0
--- /dev/null
+++ b/lib/builder.dart
@@ -0,0 +1,146 @@
+// Copyright (c) 2013, 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.
+
+/// Contains a builder object useful for creating source maps programatically.
+library source_maps.builder;
+
+// TODO(sigmund): add a builder for multi-section mappings.
+
+import 'dart:json' as json;
+import 'dart:collection';
+
+import 'span.dart';
+import 'src/vlq.dart';
+
+/// Builds a source map given a set of mappings.
+class SourceMapBuilder {
+
+ final List<Entry> _entries = <Entry>[];
+
+ /// Indices associated with file urls that will be part of the source map. We
+ /// use a linked hash-map so that `_urls.keys[_urls[u]] == u`
+ final Map<String, int> _urls = new LinkedHashMap<String, int>();
+
+ /// Indices associated with identifiers that will be part of the source map.
+ /// We use a linked hash-map so that `_names.keys[_names[n]] == n`
+ final Map<String, int> _names = new LinkedHashMap<String, int>();
+
+ /// Adds an entry mapping the [targetOffset] to [source].
+ void addFromOffset(Location source,
+ SourceFile targetFile, int targetOffset, String identifier) {
+ if (targetFile == null) {
+ throw new ArgumentError('targetFile cannot be null');
+ }
+ _entries.add(new Entry(source,
+ new FileLocation(targetFile, targetOffset), identifier));
+ }
+
+ /// Adds an entry mapping [target] to [source].
+ void addSpan(Span source, Span target) {
+ var name = source.isIdentifier ? source.text : null;
+ _entries.add(new Entry(source.start, target.start, name));
+ }
+
+ void addLocation(Location source, Location target, String identifier) {
+ _entries.add(new Entry(source, target, identifier));
+ }
+
+ /// Encodes all mappings added to this builder as a json map.
+ Map build(String fileUrl) {
+ var buff = new StringBuffer();
+ var line = 0;
+ var column = 0;
+ var srcLine = 0;
+ var srcColumn = 0;
+ var srcUrlId = 0;
+ var srcNameId = 0;
+ var first = true;
+
+ // The encoding needs to be sorted by the target offsets.
+ _entries.sort();
+ for (var entry in _entries) {
+ int nextLine = entry.target.line;
+ if (nextLine > line) {
+ for (int i = line; i < nextLine; ++i) {
+ buff.write(';');
+ }
+ line = nextLine;
+ column = 0;
+ first = true;
+ }
+
+ if (!first) buff.write(',');
+ first = false;
+ column = _append(buff, column, entry.target.column);
+
+ if (entry.source == null) continue;
+
+ srcUrlId = _append(buff, srcUrlId,
+ _indexOf(_urls, entry.source.sourceUrl));
+ srcLine = _append(buff, srcLine, entry.source.line);
+ srcColumn = _append(buff, srcColumn, entry.source.column);
+
+ if (entry.identifierName == null) continue;
+ srcNameId = _append(buff, srcNameId,
+ _indexOf(_names, entry.identifierName));
+ }
+
+ var result = {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': _urls.keys.toList(),
+ 'names' : _names.keys.toList(),
+ 'mappings' : buff.toString()
+ };
+ if (fileUrl != null) {
+ result['file'] = fileUrl;
+ }
+ return result;
+ }
+
+ /// Encodes all mappings added to this builder as a json string.
+ String toJson(String fileUrl) => json.stringify(build(fileUrl));
+
+ /// Get the index of [value] in [map], or create one if it doesn't exist.
+ int _indexOf(Map<String, int> map, String value) {
+ return map.putIfAbsent(value, () {
+ int index = map.length;
+ map[value] = index;
+ return index;
+ });
+ }
+
+ /// Appends to [buff] a VLQ encoding of [newValue] using the difference
+ /// between [oldValue] and [newValue]
+ static int _append(StringBuffer buff, int oldValue, int newValue) {
+ buff.writeAll(encodeVlq(newValue - oldValue));
+ return newValue;
+ }
+}
+
+/// An entry in the source map builder.
+class Entry implements Comparable {
+ /// Span denoting the original location in the input source file
+ final Location source;
+
+ /// Span indicating the corresponding location in the target file.
+ final Location target;
+
+ /// An identifier name, when this location is the start of an identifier.
+ final String identifierName;
+
+ Entry(this.source, this.target, this.identifierName);
+
+ /// Implements [Comparable] to ensure that entries are ordered by their
+ /// location in the target file. We sort primarily by the target offset
+ /// because source map files are encoded by printing each mapping in order as
+ /// they appear in the target file.
+ int compareTo(Entry other) {
+ int res = target.compareTo(other.target);
+ if (res != 0) return res;
+ res = source.sourceUrl.compareTo(other.source.sourceUrl);
+ if (res != 0) return res;
+ return source.compareTo(other.source);
+ }
+}
diff --git a/lib/parser.dart b/lib/parser.dart
new file mode 100644
index 0000000..3849913
--- /dev/null
+++ b/lib/parser.dart
@@ -0,0 +1,391 @@
+// Copyright (c) 2013, 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.
+
+/// Contains the top-level function to parse source maps version 3.
+library source_maps.parser;
+
+import 'dart:json' as json;
+
+import 'span.dart';
+import 'src/utils.dart';
+import 'src/vlq.dart';
+
+/// Parses a source map directly from a json string.
+// TODO(sigmund): evaluate whether other maps should have the json parsed, or
+// the string represenation.
+Mapping parse(String jsonMap, {Map<String, Map> otherMaps}) =>
+ parseJson(json.parse(jsonMap), otherMaps: otherMaps);
+
+/// Parses a source map directly from a json map object.
+Mapping parseJson(Map map, {Map<String, Map> otherMaps}) {
+ if (map['version'] != 3) {
+ throw new ArgumentError(
+ 'unexpected source map version: ${map["version"]}. '
+ 'Only version 3 is supported.');
+ }
+
+ // TODO(sigmund): relax this? dart2js doesn't generate the file entry.
+ if (!map.containsKey('file')) {
+ throw new ArgumentError('missing "file" in source map');
+ }
+
+ if (map.containsKey('sections')) {
+ if (map.containsKey('mappings') || map.containsKey('sources') ||
+ map.containsKey('names')) {
+ throw new FormatException('map containing "sections" '
+ 'cannot contain "mappings", "sources", or "names".');
+ }
+ return new MultiSectionMapping.fromJson(map['sections'], otherMaps);
+ }
+ return new SingleMapping.fromJson(map);
+}
+
+
+/// A mapping parsed our of a source map.
+abstract class Mapping {
+ Span spanFor(int line, int column, {Map<String, SourceFile> files});
+
+ Span spanForLocation(Location loc, {Map<String, SourceFile> files}) {
+ return spanFor(loc.line, loc.column, files: files);
+ }
+}
+
+/// A meta-level map containing sections.
+class MultiSectionMapping extends Mapping {
+ /// For each section, the start line offset.
+ final List<int> _lineStart = <int>[];
+
+ /// For each section, the start column offset.
+ final List<int> _columnStart = <int>[];
+
+ /// For each section, the actual source map information, which is not adjusted
+ /// for offsets.
+ final List<Mapping> _maps = <Mapping>[];
+
+ /// Creates a section mapping from json.
+ MultiSectionMapping.fromJson(List sections, Map<String, Map> otherMaps) {
+ for (var section in sections) {
+ var offset = section['offset'];
+ if (offset == null) throw new FormatException('section missing offset');
+
+ var line = section['offset']['line'];
+ if (line == null) throw new FormatException('offset missing line');
+
+ var column = section['offset']['column'];
+ if (column == null) throw new FormatException('offset missing column');
+
+ _lineStart.add(line);
+ _columnStart.add(column);
+
+ var url = section['url'];
+ var map = section['map'];
+
+ if (url != null && map != null) {
+ throw new FormatException("section can't use both url and map entries");
+ } else if (url != null) {
+ if (otherMaps == null || otherMaps[url] == null) {
+ throw new FormatException(
+ 'section contains refers to $url, but no map was '
+ 'given for it. Make sure a map is passed in "otherMaps"');
+ }
+ _maps.add(parseJson(otherMaps[url], otherMaps: otherMaps));
+ } else if (map != null) {
+ _maps.add(parseJson(map, otherMaps: otherMaps));
+ } else {
+ throw new FormatException('section missing url or map');
+ }
+ }
+ if (_lineStart.length == 0) {
+ throw new FormatException('expected at least one section');
+ }
+ }
+
+ int _indexFor(line, column) {
+ for(int i = 0; i < _lineStart.length; i++) {
+ if (line < _lineStart[i]) return i - 1;
+ if (line == _lineStart[i] && column < _columnStart[i]) return i - 1;
+ }
+ return _lineStart.length - 1;
+ }
+
+ Span spanFor(int line, int column, {Map<String, SourceFile> files}) {
+ int index = _indexFor(line, column);
+ return _maps[index].spanFor(
+ line - _lineStart[index], column - _columnStart[index], files: files);
+ }
+
+ String toString() {
+ var buff = new StringBuffer("$runtimeType : [");
+ for (int i = 0; i < _lineStart.length; i++) {
+ buff..write('(')
+ ..write(_lineStart[i])
+ ..write(',')
+ ..write(_columnStart[i])
+ ..write(':')
+ ..write(_maps[i])
+ ..write(')');
+ }
+ buff.write(']');
+ return buff.toString();
+ }
+}
+
+/// A map containing direct source mappings.
+// TODO(sigmund): integrate mapping and sourcemap builder?
+class SingleMapping extends Mapping {
+ /// Url of the target file.
+ final String targetUrl;
+
+ /// Source urls used in the mapping, indexed by id.
+ final List<String> urls;
+
+ /// Source names used in the mapping, indexed by id.
+ final List<String> names;
+
+ /// Entries indicating the beginning of each span.
+ final List<TargetLineEntry> lines = <TargetLineEntry>[];
+
+ SingleMapping.fromJson(Map map)
+ : targetUrl = map['file'],
+ // TODO(sigmund): add support for 'sourceRoot'
+ urls = map['sources'],
+ names = map['names'] {
+ int line = 0;
+ int column = 0;
+ int srcUrlId = 0;
+ int srcLine = 0;
+ int srcColumn = 0;
+ int srcNameId = 0;
+ var tokenizer = new _MappingTokenizer(map['mappings']);
+ var entries = <TargetEntry>[];
+
+ while (tokenizer.hasTokens) {
+ if (tokenizer.nextKind.isNewLine) {
+ if (!entries.isEmpty) {
+ lines.add(new TargetLineEntry(line, entries));
+ entries = <TargetEntry>[];
+ }
+ line++;
+ column = 0;
+ tokenizer._consumeNewLine();
+ continue;
+ }
+
+ // Decode the next entry, using the previous encountered values to
+ // decode the relative values.
+ //
+ // We expect 1, 4, or 5 values. If present, values are expected in the
+ // following order:
+ // 0: the starting column in the current line of the generated file
+ // 1: the id of the original source file
+ // 2: the starting line in the original source
+ // 3: the starting column in the original source
+ // 4: the id of the original symbol name
+ // The values are relative to the previous encountered values.
+ if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line);
+ column += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) {
+ entries.add(new TargetEntry(column));
+ } else {
+ srcUrlId += tokenizer._consumeValue();
+ if (srcUrlId >= urls.length) {
+ throw new StateError(
+ 'Invalid source url id. $targetUrl, $line, $srcUrlId');
+ }
+ if (!tokenizer.nextKind.isValue) throw _segmentError(2, line);
+ srcLine += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) throw _segmentError(3, line);
+ srcColumn += tokenizer._consumeValue();
+ if (!tokenizer.nextKind.isValue) {
+ entries.add(new TargetEntry(column, srcUrlId, srcLine, srcColumn));
+ } else {
+ srcNameId += tokenizer._consumeValue();
+ if (srcNameId >= names.length) {
+ throw new StateError(
+ 'Invalid name id: $targetUrl, $line, $srcNameId');
+ }
+ entries.add(
+ new TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId));
+ }
+ }
+ if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment();
+ }
+ if (!entries.isEmpty) {
+ lines.add(new TargetLineEntry(line, entries));
+ }
+ }
+
+ _segmentError(int seen, int line) => new StateError(
+ 'Invalid entry in sourcemap, expected 1, 4, or 5'
+ ' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
+
+ /// Returns [TargetLineEntry] which includes the location in the target [line]
+ /// number. In particular, the resulting entry is the last entry whose line
+ /// number is lower or equal to [line].
+ TargetLineEntry _findLine(int line) {
+ int index = binarySearch(lines, (e) => e.line > line);
+ return (index <= 0) ? null : lines[index - 1];
+ }
+
+ /// Returns [TargetEntry] which includes the location denoted by
+ /// [line], [column]. If [lineEntry] corresponds to [line], then this will be
+ /// the last entry whose column is lower or equal than [column]. If
+ /// [lineEntry] corresponds to a line prior to [line], then the result will be
+ /// the very last entry on that line.
+ TargetEntry _findColumn(int line, int column, TargetLineEntry lineEntry) {
+ if (lineEntry == null || lineEntry.entries.length == 0) return null;
+ if (lineEntry.line != line) return lineEntry.entries.last;
+ var entries = lineEntry.entries;
+ int index = binarySearch(entries, (e) => e.column > column);
+ return (index <= 0) ? null : entries[index - 1];
+ }
+
+ Span spanFor(int line, int column, {Map<String, SourceFile> files}) {
+ var lineEntry = _findLine(line);
+ var entry = _findColumn(line, column, _findLine(line));
+ if (entry == null) return null;
+ var url = urls[entry.sourceUrlId];
+ if (files != null && files[url] != null) {
+ var file = files[url];
+ var start = file.getOffset(entry.sourceLine, entry.sourceColumn);
+ if (entry.sourceNameId != null) {
+ var text = names[entry.sourceNameId];
+ return new FileSpan(files[url], start, start + text.length, true);
+ } else {
+ return new FileSpan(files[url], start);
+ }
+ } else {
+ // Offset and other context is not available.
+ if (entry.sourceNameId != null) {
+ return new FixedSpan(url, 0, entry.sourceLine, entry.sourceColumn,
+ text: names[entry.sourceNameId], isIdentifier: true);
+ } else {
+ return new FixedSpan(url, 0, entry.sourceLine, entry.sourceColumn);
+ }
+ }
+ }
+
+ String toString() {
+ return (new StringBuffer("$runtimeType : [")
+ ..write('targetUrl: ')
+ ..write(targetUrl)
+ ..write(', urls: ')
+ ..write(urls)
+ ..write(', names: ')
+ ..write(names)
+ ..write(', lines: ')
+ ..write(lines)
+ ..write(']')).toString();
+ }
+
+ String get debugString {
+ var buff = new StringBuffer();
+ for (var lineEntry in lines) {
+ var line = lineEntry.line;
+ for (var entry in lineEntry.entries) {
+ buff..write(targetUrl)
+ ..write(': ')
+ ..write(line)
+ ..write(':')
+ ..write(entry.column)
+ ..write(' --> ')
+ ..write(urls[entry.sourceUrlId])
+ ..write(': ')
+ ..write(entry.sourceLine)
+ ..write(':')
+ ..write(entry.sourceColumn);
+ if (entry.sourceNameId != null) {
+ buff..write(' (')
+ ..write(names[entry.sourceNameId])
+ ..write(')');
+ }
+ buff.write('\n');
+ }
+ }
+ return buff.toString();
+ }
+}
+
+/// A line entry read from a source map.
+class TargetLineEntry {
+ final int line;
+ List<TargetEntry> entries = <TargetEntry>[];
+ TargetLineEntry(this.line, this.entries);
+
+ String toString() => '$runtimeType: $line $entries';
+}
+
+/// A target segment entry read from a source map
+class TargetEntry {
+ final int column;
+ final int sourceUrlId;
+ final int sourceLine;
+ final int sourceColumn;
+ final int sourceNameId;
+ TargetEntry(this.column, [this.sourceUrlId, this.sourceLine,
+ this.sourceColumn, this.sourceNameId]);
+
+ String toString() => '$runtimeType: '
+ '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)';
+}
+
+/** A character iterator over a string that can peek one character ahead. */
+class _MappingTokenizer implements Iterator<String> {
+ final String _internal;
+ final int _length;
+ int index = -1;
+ _MappingTokenizer(String internal)
+ : _internal = internal,
+ _length = internal.length;
+
+ // Iterator API is used by decodeVlq to consume VLQ entries.
+ bool moveNext() => ++index < _length;
+ String get current =>
+ (index >= 0 && index < _length) ? _internal[index] : null;
+
+ bool get hasTokens => index < _length - 1 && _length > 0;
+
+ _TokenKind get nextKind {
+ if (!hasTokens) return _TokenKind.EOF;
+ var next = _internal[index + 1];
+ if (next == ';') return _TokenKind.LINE;
+ if (next == ',') return _TokenKind.SEGMENT;
+ return _TokenKind.VALUE;
+ }
+
+ int _consumeValue() => decodeVlq(this);
+ void _consumeNewLine() { ++index; }
+ void _consumeNewSegment() { ++index; }
+
+ // Print the state of the iterator, with colors indicating the current
+ // position.
+ String toString() {
+ var buff = new StringBuffer();
+ for (int i = 0; i < index; i++) {
+ buff.write(_internal[i]);
+ }
+ buff.write('[31m');
+ buff.write(current == null ? '' : current);
+ buff.write('[0m');
+ for (int i = index + 1; i < _internal.length; i++) {
+ buff.write(_internal[i]);
+ }
+ buff.write(' ($index)');
+ return buff.toString();
+ }
+}
+
+class _TokenKind {
+ static const _TokenKind LINE = const _TokenKind(isNewLine: true);
+ static const _TokenKind SEGMENT = const _TokenKind(isNewSegment: true);
+ static const _TokenKind EOF = const _TokenKind(isEof: true);
+ static const _TokenKind VALUE = const _TokenKind();
+ final bool isNewLine;
+ final bool isNewSegment;
+ final bool isEof;
+ bool get isValue => !isNewLine && !isNewSegment && !isEof;
+
+ const _TokenKind(
+ {this.isNewLine: false, this.isNewSegment: false, this.isEof: false});
+}
diff --git a/lib/printer.dart b/lib/printer.dart
new file mode 100644
index 0000000..333aadc
--- /dev/null
+++ b/lib/printer.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2013, 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.
+
+/// Contains a code printer that generates code by recording the source maps.
+library source_maps.printer;
+
+import 'dart:utf' show stringToCodepoints;
+import 'builder.dart';
+import 'span.dart';
+
+const int _LF = 10;
+const int _CR = 13;
+
+/// A printer that keeps track of offset locations and records source maps
+/// locations.
+class Printer {
+ final String filename;
+ final StringBuffer _buff = new StringBuffer();
+ final SourceMapBuilder _maps = new SourceMapBuilder();
+ String get text => _buff.toString();
+ String get map => _maps.toJson(filename);
+
+ /// Current source location mapping.
+ Location _loc;
+
+ /// Current line in the buffer;
+ int _line = 0;
+
+ /// Current column in the buffer.
+ int _column = 0;
+
+ Printer(this.filename);
+
+ /// Add [str] contents to the output, tracking new lines to track correct
+ /// positions for span locations. When [projectMarks] is true, this method
+ /// adds a source map location on each new line, projecting that every new
+ /// line in the target file (printed here) corresponds to a new line in the
+ /// source file.
+ void add(String str, {projectMarks: false}) {
+ var chars = stringToCodepoints(str);
+ var length = chars.length;
+ for (int i = 0; i < length; i++) {
+ var c = chars[i];
+ if (c == _LF || (c == _CR && (i + 1 == length || chars[i + 1] != _LF))) {
+ // Return not followed by line-feed is treated as a new line.
+ _line++;
+ _column = 0;
+ if (projectMarks && _loc != null) {
+ if (_loc is FixedLocation) {
+ mark(new FixedLocation(0, _loc.sourceUrl, _loc.line + 1, 0));
+ } else if (_loc is FileLocation) {
+ var file = (_loc as FileLocation).file;
+ mark(new FileLocation(file, file.getOffset(_loc.line + 1, 0)));
+ }
+ }
+ } else {
+ _column++;
+ }
+ }
+ _buff.write(str);
+ }
+
+
+ /// Append a [total] number of spaces in the target file. Typically used for
+ /// formatting indentation.
+ void addSpaces(int total) {
+ for (int i = 0; i < total; i++) _buff.write(' ');
+ _column += total;
+ }
+
+ /// Marks that the current point in the target file corresponds to the [mark]
+ /// in the source file, which can be either a [Location] or a [Span]. When the
+ /// mark is an identifier's Span, this also records the name of the identifier
+ /// in the source map information.
+ void mark(mark) {
+ var loc;
+ var identifier = null;
+ if (mark is Location) {
+ loc = mark;
+ } else if (mark is Span) {
+ loc = mark.start;
+ if (mark.isIdentifier) identifier = mark.text;
+ }
+ _maps.addLocation(loc,
+ new FixedLocation(_buff.length, null, _line, _column), identifier);
+ _loc = loc;
+ }
+}
diff --git a/lib/source_maps.dart b/lib/source_maps.dart
new file mode 100644
index 0000000..4e24dea
--- /dev/null
+++ b/lib/source_maps.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2013, 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.
+
+/// Source maps library.
+///
+/// Create a source map using [SourceMapBuilder]. For example:
+/// var json = (new SourceMapBuilder()
+/// ..add(inputSpan1, outputSpan1)
+/// ..add(inputSpan2, outputSpan2)
+/// ..add(inputSpan3, outputSpan3)
+/// .toJson(outputFile);
+///
+/// Use the [Span] and [SourceFile] classes to specify span locations.
+///
+/// Parse a source map using [parse], and call `spanFor` on the returned mapping
+/// object. For example:
+/// var mapping = parse(json);
+/// mapping.spanFor(outputSpan1.line, outputSpan1.column)
+library source_maps;
+
+export "builder.dart";
+export "parser.dart";
+export "printer.dart";
+export "span.dart";
diff --git a/lib/span.dart b/lib/span.dart
new file mode 100644
index 0000000..1982994
--- /dev/null
+++ b/lib/span.dart
@@ -0,0 +1,330 @@
+// Copyright (c) 2013, 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.
+
+/// Dart classes representing the souce spans and source files.
+library source_maps.span;
+
+import 'dart:utf' show stringToCodepoints, codepointsToString;
+import 'dart:math' show min;
+
+import 'src/utils.dart';
+
+/// A simple class that describe a segment of source text.
+abstract class Span implements Comparable {
+ /// The start location of this span.
+ final Location start;
+
+ /// The end location of this span, exclusive.
+ final Location end;
+
+ /// Url of the source (typically a file) containing this span.
+ String get sourceUrl => start.sourceUrl;
+
+ /// The length of this span, in characters.
+ int get length => end.offset - start.offset;
+
+ /// The source text for this span, if available.
+ String get text;
+
+ /// Whether [text] corresponds to an identifier symbol.
+ final bool isIdentifier;
+
+ Span(this.start, this.end, bool isIdentifier)
+ : isIdentifier = isIdentifier != null ? isIdentifier : false {
+ _checkRange();
+ }
+
+ /// Creates a new span that is the union of two existing spans [start] and
+ /// [end]. Note that the resulting span might contain some positions that were
+ /// not in either of the original spans if [start] and [end] are disjoint.
+ Span.union(Span start, Span end)
+ : start = start.start, end = end.end, isIdentifier = false {
+ _checkRange();
+ }
+
+ void _checkRange() {
+ if (start.offset < 0) throw new ArgumentError('start $start must be >= 0');
+ if (end.offset < start.offset) {
+ throw new ArgumentError('end $end must be >= start $start');
+ }
+ }
+
+ /// Compares two spans. If the spans are not in the same source, this method
+ /// generates an error.
+ int compareTo(Span other) {
+ int d = start.compareTo(other.start);
+ return d == 0 ? end.compareTo(other.end) : d;
+ }
+
+ /// Gets the location in standard printed form `filename:line:column`, where
+ /// line and column are adjusted by 1 to match the convention in editors.
+ String get formatLocation => start.formatString;
+
+ String getLocationMessage(String message,
+ {bool useColors: false, String color}) {
+ return '$formatLocation: $message';
+ }
+
+ bool operator ==(Span other) =>
+ sourceUrl == other.sourceUrl && start == other.start && end == other.end;
+
+ String toString() => '<$runtimeType: $start $end $formatLocation $text>';
+}
+
+/// A location in the source text
+abstract class Location implements Comparable {
+ /// Url of the source containing this span.
+ String get sourceUrl;
+
+ /// The offset of this location, 0-based.
+ final int offset;
+
+ /// The 0-based line in the source of this location.
+ int get line;
+
+ /// The 0-based column in the source of this location.
+ int get column;
+
+ Location(this.offset);
+
+ /// Compares two locations. If the locations are not in the same source, this
+ /// method generates an error.
+ int compareTo(Location other) {
+ if (sourceUrl != other.sourceUrl) {
+ throw new ArgumentError('can only compare locations of the same source');
+ }
+ return offset - other.offset;
+ }
+
+ String toString() => '(Location $offset)';
+ String get formatString => '$sourceUrl:${line + 1}:${column + 1}';
+}
+
+/// Implementation of [Location] with fixed values given at allocation time.
+class FixedLocation extends Location {
+ final String sourceUrl;
+ final int line;
+ final int column;
+
+ FixedLocation(int offset, this.sourceUrl, this.line, this.column)
+ : super(offset);
+}
+
+/// Implementation of [Span] where all the values are given at allocation time.
+class FixedSpan extends Span {
+ /// The source text for this span, if available.
+ final String text;
+
+ /// Creates a span which starts and end in the same line.
+ FixedSpan(String sourceUrl, int start, int line, int column,
+ {String text: '', bool isIdentifier: false})
+ : text = text, super(new FixedLocation(start, sourceUrl, line, column),
+ new FixedLocation(start + text.length, sourceUrl, line,
+ column + text.length),
+ isIdentifier);
+}
+
+/// [Location] with values computed from an underling [SourceFile].
+class FileLocation extends Location {
+ /// The source file containing this location.
+ final SourceFile file;
+
+ String get sourceUrl => file.url;
+ int get line => file.getLine(offset);
+ int get column => file.getColumn(line, offset);
+
+ FileLocation(this.file, int offset): super(offset);
+}
+
+/// [Span] where values are computed from an underling [SourceFile].
+class FileSpan extends Span {
+ /// The source file containing this span.
+ final SourceFile file;
+
+ /// The source text for this span, if available.
+ String get text => file.getText(start.offset, end.offset);
+
+ factory FileSpan(SourceFile file, int start,
+ [int end, bool isIdentifier = false]) {
+ var startLoc = new FileLocation(file, start);
+ var endLoc = end == null ? startLoc : new FileLocation(file, end);
+ return new FileSpan.locations(startLoc, endLoc, isIdentifier);
+ }
+
+ FileSpan.locations(FileLocation start, FileLocation end,
+ bool isIdentifier)
+ : file = start.file, super(start, end, isIdentifier);
+
+ /// Creates a new span that is the union of two existing spans [start] and
+ /// [end]. Note that the resulting span might contain some positions that were
+ /// not in either of the original spans if [start] and [end] are disjoint.
+ FileSpan.union(FileSpan start, FileSpan end)
+ : file = start.file, super.union(start, end) {
+ if (start.file != end.file) {
+ throw new ArgumentError('start and end must be from the same file');
+ }
+ }
+
+ String getLocationMessage(String message,
+ {bool useColors: false, String color}) {
+ return file.getLocationMessage(message, start.offset, end.offset,
+ useColors: useColors, color: color);
+ }
+}
+
+// Constants to determine end-of-lines.
+const int _LF = 10;
+const int _CR = 13;
+
+// Color constants used for generating messages.
+const String _RED_COLOR = '\u001b[31m';
+const String _NO_COLOR = '\u001b[0m';
+
+/// Stores information about a source file, to permit computation of the line
+/// and column. Also contains a nice default error message highlighting the code
+/// location.
+class SourceFile {
+ /// Url where the source file is located.
+ final String url;
+ final List<int> _lineStarts;
+ final List<int> _decodedChars;
+
+ SourceFile(this.url, this._lineStarts, this._decodedChars);
+
+ SourceFile.text(this.url, String text)
+ : _lineStarts = <int>[0],
+ _decodedChars = stringToCodepoints(text) {
+ for (int i = 0; i < _decodedChars.length; i++) {
+ var c = _decodedChars[i];
+ if (c == _CR) {
+ // Return not followed by newline is treated as a newline
+ int j = i + 1;
+ if (j >= _decodedChars.length || _decodedChars[j] != _LF) {
+ c = _LF;
+ }
+ }
+ if (c == _LF) _lineStarts.add(i + 1);
+ }
+ }
+
+ /// Returns a span in this [SourceFile] with the given offsets.
+ Span span(int start, [int end, bool isIdentifier = false]) =>
+ new FileSpan(this, start, end, isIdentifier);
+
+ /// Returns a location in this [SourceFile] with the given offset.
+ Location location(int offset) => new FileLocation(this, offset);
+
+ /// Gets the 0-based line corresponding to an offset.
+ int getLine(int offset) {
+ return binarySearch(_lineStarts, (o) => o > offset) - 1;
+ }
+
+ /// Gets the 0-based column corresponding to an offset.
+ int getColumn(int line, int offset) {
+ return offset - _lineStarts[line];
+ }
+
+ /// Get the offset for a given line and column
+ int getOffset(int line, int column) {
+ return _lineStarts[min(line, _lineStarts.length - 1)] + column;
+ }
+
+ /// Gets the text at the given offsets.
+ String getText(int start, [int end]) {
+ return codepointsToString(_decodedChars.sublist(start, end));
+ }
+
+ /// Create a pretty string representation from a span.
+ String getLocationMessage(String message, int start, int end,
+ {bool useColors: false, String color}) {
+ // TODO(jmesserly): it would be more useful to pass in an object that
+ // controls how the errors are printed. This method is a bit too smart.
+ var line = getLine(start);
+ var column = getColumn(line, start);
+
+ var src = url == null ? '' : url;
+ var msg = '$src:${line + 1}:${column + 1}: $message';
+
+ if (_decodedChars == null) {
+ // We don't have any text to include, so exit.
+ return msg;
+ }
+
+ var buf = new StringBuffer(msg);
+ buf.write('\n');
+ var textLine;
+
+ // +1 for 0-indexing, +1 again to avoid the last line
+ if ((line + 2) < _lineStarts.length) {
+ textLine = getText(_lineStarts[line], _lineStarts[line + 1]);
+ } else {
+ textLine = getText(_lineStarts[line]);
+ textLine = '$textLine\n';
+ }
+
+ int toColumn = min(column + end - start, textLine.length);
+ if (useColors) {
+ if (color == null) {
+ color = _RED_COLOR;
+ }
+ buf.write(textLine.substring(0, column));
+ buf.write(color);
+ buf.write(textLine.substring(column, toColumn));
+ buf.write(_NO_COLOR);
+ buf.write(textLine.substring(toColumn));
+ } else {
+ buf.write(textLine);
+ }
+
+ int i = 0;
+ for (; i < column; i++) {
+ buf.write(' ');
+ }
+
+ if (useColors) buf.write(color);
+ for (; i < toColumn; i++) {
+ buf.write('^');
+ }
+ if (useColors) buf.write(_NO_COLOR);
+ return buf.toString();
+ }
+}
+
+/// A convenience type to treat a code segment as if it were a separate
+/// [SourceFile]. A [SourceFileSegment] shifts all locations by an offset, which
+/// allows you to set source-map locations based on the locations relative to
+/// the start of the segment, but that get translated to absolute locations in
+/// the original source file.
+class SourceFileSegment extends SourceFile {
+ final int _baseOffset;
+ final int _baseLine;
+ final int _baseColumn;
+
+ SourceFileSegment(String url, String textSegment, Location startOffset)
+ : _baseOffset = startOffset.offset,
+ _baseLine = startOffset.line,
+ _baseColumn = startOffset.column,
+ super.text(url, textSegment);
+
+ Span span(int start, [int end, bool isIdentifier = false]) =>
+ super.span(start + _baseOffset,
+ end == null ? null : end + _baseOffset, isIdentifier);
+
+ Location location(int offset) => super.location(offset + _baseOffset);
+
+ int getLine(int offset) =>
+ super.getLine(offset - _baseOffset) + _baseLine;
+
+ int getColumn(int line, int offset) {
+ var col = super.getColumn(line - _baseLine, offset - _baseOffset);
+ return line == _baseLine ? col + _baseColumn : col;
+ }
+
+ int getOffset(int line, int column) =>
+ super.getOffset(line - _baseLine,
+ line == _baseLine ? column - _baseColumn : column) + _baseOffset;
+
+ String getText(int start, [int end]) =>
+ super.getText(start - _baseOffset, end - _baseOffset);
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..78f098e
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,29 @@
+// Copyright (c) 2013, 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.
+
+/// Utilities that shouldn't be in this package.
+library source_maps.utils;
+
+/// Find the first entry in a sorted [list] that matches a monotonic predicate.
+/// Given a result `n`, that all items before `n` will not match, `n` matches,
+/// and all items after `n` match too. The result is -1 when there are no
+/// items, 0 when all items match, and list.length when none does.
+// TODO(sigmund): remove this function after dartbug.com/5624 is fixed.
+int binarySearch(List list, bool matches(item)) {
+ if (list.length == 0) return -1;
+ if (matches(list.first)) return 0;
+ if (!matches(list.last)) return list.length;
+
+ int min = 0;
+ int max = list.length - 1;
+ while (min < max) {
+ var half = min + ((max - min) ~/ 2);
+ if (matches(list[half])) {
+ max = half;
+ } else {
+ min = half + 1;
+ }
+ }
+ return max;
+}
diff --git a/lib/src/vlq.dart b/lib/src/vlq.dart
new file mode 100644
index 0000000..e4ab4eb
--- /dev/null
+++ b/lib/src/vlq.dart
@@ -0,0 +1,103 @@
+// Copyright (c) 2013, 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.
+
+
+/// Utilities to encode and decode VLQ values used in source maps.
+///
+/// Sourcemaps are encoded with variable length numbers as base64 encoded
+/// strings with the least significant digit coming first. Each base64 digit
+/// encodes a 5-bit value (0-31) and a continuation bit. Signed values can be
+/// represented by using the least significant bit of the value as the sign bit.
+///
+/// For more details see the source map [version 3 documentation][spec].
+/// [spec]: https://docs.google.com/a/google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
+library source_maps.src.vlq;
+
+import 'dart:math';
+
+const int VLQ_BASE_SHIFT = 5;
+
+const int VLQ_BASE_MASK = (1 << 5) - 1;
+
+const int VLQ_CONTINUATION_BIT = 1 << 5;
+
+const int VLQ_CONTINUATION_MASK = 1 << 5;
+
+const String BASE64_DIGITS =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+
+final Map<String, int> _digits = () {
+ var map = <String, int>{};
+ for (int i = 0; i < 64; i++) {
+ map[BASE64_DIGITS[i]] = i;
+ }
+ return map;
+}();
+
+final int MAX_INT32 = pow(2, 31) - 1;
+final int MIN_INT32 = -pow(2, 31);
+
+/// Creates the VLQ encoding of [value] as a sequence of characters
+Iterable<String> encodeVlq(int value) {
+ if (value < MIN_INT32 || value > MAX_INT32) {
+ throw new ArgumentError('expected 32 bit int, got: $value');
+ }
+ var res = <String>[];
+ int signBit = 0;
+ if (value < 0) {
+ signBit = 1;
+ value = -value;
+ }
+ value = (value << 1) | signBit;
+ do {
+ int digit = value & VLQ_BASE_MASK;
+ value >>= VLQ_BASE_SHIFT;
+ if (value > 0) {
+ digit |= VLQ_CONTINUATION_BIT;
+ }
+ res.add(BASE64_DIGITS[digit]);
+ } while (value > 0);
+ return res;
+}
+
+/// Decodes a value written as a sequence of VLQ characters. The first input
+/// character will be `chars.current` after calling `chars.moveNext` once. The
+/// iterator is advanced until a stop character is found (a character without
+/// the [VLQ_CONTINUATION_BIT]).
+int decodeVlq(Iterator<String> chars) {
+ int result = 0;
+ bool stop = false;
+ int shift = 0;
+ while (!stop) {
+ if (!chars.moveNext()) throw new StateError('incomplete VLQ value');
+ var char = chars.current;
+ if (!_digits.containsKey(char)) {
+ throw new FormatException('invalid character in VLQ encoding: $char');
+ }
+ var digit = _digits[char];
+ stop = (digit & VLQ_CONTINUATION_BIT) == 0;
+ digit &= VLQ_BASE_MASK;
+ result += (digit << shift);
+ shift += VLQ_BASE_SHIFT;
+ }
+
+ // Result uses the least significant bit as a sign bit. We convert it into a
+ // two-complement value. For example,
+ // 2 (10 binary) becomes 1
+ // 3 (11 binary) becomes -1
+ // 4 (100 binary) becomes 2
+ // 5 (101 binary) becomes -2
+ // 6 (110 binary) becomes 3
+ // 7 (111 binary) becomes -3
+ bool negate = (result & 1) == 1;
+ result = result >> 1;
+ result = negate ? -result : result;
+
+ // TODO(sigmund): can we detect this earlier?
+ if (result < MIN_INT32 || result > MAX_INT32) {
+ throw new FormatException(
+ 'expected an encoded 32 bit int, but we got: $result');
+ }
+ return result;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..a559b58
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,6 @@
+name: source_maps
+author: "Dart Team <misc@dartlang.org>"
+homepage: https://github.com/dart-lang/source-maps
+description: Library to programmatically manipulate source map files.
+dev_dependencies:
+ unittest: any
diff --git a/test/builder_test.dart b/test/builder_test.dart
new file mode 100644
index 0000000..7bf2ee4
--- /dev/null
+++ b/test/builder_test.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2013, 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 test.source_maps_test;
+
+import 'dart:json' as json;
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/source_maps.dart';
+import 'common.dart';
+
+main() {
+ test('builder - with span', () {
+ var map = (new SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url);
+ expect(map, equals(EXPECTED_MAP));
+ });
+
+ test('builder - with location', () {
+ var str = (new SourceMapBuilder()
+ ..addLocation(inputVar1.start, outputVar1.start, 'longVar1')
+ ..addLocation(inputFunction.start, outputFunction.start, 'longName')
+ ..addLocation(inputVar2.start, outputVar2.start, 'longVar2')
+ ..addLocation(inputExpr.start, outputExpr.start, null))
+ .toJson(output.url);
+ expect(str, json.stringify(EXPECTED_MAP));
+ });
+}
diff --git a/test/common.dart b/test/common.dart
new file mode 100644
index 0000000..661979b
--- /dev/null
+++ b/test/common.dart
@@ -0,0 +1,97 @@
+// Copyright (c) 2013, 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.
+
+/// Common input/output used by builder, parser and end2end tests
+library test.common;
+
+import 'package:source_maps/source_maps.dart';
+import 'package:unittest/unittest.dart';
+
+/// Content of the source file
+const String INPUT = '''
+/** this is a comment. */
+int longVar1 = 3;
+
+// this is a comment too
+int longName(int longVar2) {
+ return longVar1 + longVar2;
+}
+''';
+var input = new SourceFile.text('input.dart', INPUT);
+
+/// A span in the input file
+Span ispan(int start, int end, [bool isIdentifier = false]) =>
+ new FileSpan(input, start, end, isIdentifier);
+
+Span inputVar1 = ispan(30, 38, true);
+Span inputFunction = ispan(74, 82, true);
+Span inputVar2 = ispan(87, 95, true);
+Span inputExpr = ispan(108, 127);
+
+/// Content of the target file
+const String OUTPUT = '''
+var x = 3;
+f(y) => x + y;
+''';
+var output = new SourceFile.text('output.dart', OUTPUT);
+
+/// A span in the output file
+Span ospan(int start, int end, [bool isIdentifier = false]) =>
+ new FileSpan(output, start, end, isIdentifier);
+
+Span outputVar1 = ospan(4, 5, true);
+Span outputFunction = ospan(11, 12, true);
+Span outputVar2 = ospan(13, 14, true);
+Span outputExpr = ospan(19, 24);
+
+/// Expected output mapping when recording the following four mappings:
+/// inputVar1 <= outputVar1
+/// inputFunction <= outputFunction
+/// inputVar2 <= outputVar2
+/// inputExpr <= outputExpr
+///
+/// This mapping is stored in the tests so we can independently test the builder
+/// and parser algorithms without relying entirely on end2end tests.
+const Map<String, dynamic> EXPECTED_MAP = const {
+ 'version': 3,
+ 'sourceRoot': '',
+ 'sources': const ['input.dart'],
+ 'names': const ['longVar1','longName','longVar2'],
+ 'mappings': 'IACIA;AAGAC,EAAaC,MACR',
+ 'file': 'output.dart'
+};
+
+check(Span outputSpan, Mapping mapping, Span inputSpan, bool realOffsets) {
+ var line = outputSpan.start.line;
+ var column = outputSpan.start.column;
+ var files = realOffsets ? {'input.dart': input} : null;
+ var span = mapping.spanFor(line, column, files: files);
+ var span2 = mapping.spanForLocation(outputSpan.start, files: files);
+
+ // Both mapping APIs are equivalent.
+ expect(span.start.offset, span2.start.offset);
+ expect(span.start.line, span2.start.line);
+ expect(span.start.column, span2.start.column);
+ expect(span.end.offset, span2.end.offset);
+ expect(span.end.line, span2.end.line);
+ expect(span.end.column, span2.end.column);
+
+ // Mapping matches our input location (modulo using real offsets)
+ expect(span.start.line, inputSpan.start.line);
+ expect(span.start.column, inputSpan.start.column);
+ expect(span.sourceUrl, inputSpan.sourceUrl);
+ expect(span.start.offset, realOffsets ? inputSpan.start.offset : 0);
+
+ // Mapping includes the identifier, if any
+ if (inputSpan.isIdentifier) {
+ expect(span.end.line, inputSpan.end.line);
+ expect(span.end.column, inputSpan.end.column);
+ expect(span.end.offset, span.start.offset + inputSpan.text.length);
+ if (realOffsets) expect(span.end.offset, inputSpan.end.offset);
+ } else {
+ expect(span.end.offset, span.start.offset);
+ expect(span.end.line, span.start.line);
+ expect(span.end.column, span.start.column);
+ }
+}
diff --git a/test/end2end_test.dart b/test/end2end_test.dart
new file mode 100644
index 0000000..5ea958a
--- /dev/null
+++ b/test/end2end_test.dart
@@ -0,0 +1,106 @@
+// Copyright (c) 2013, 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 test.end2end_test;
+
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/source_maps.dart';
+import 'common.dart';
+
+main() {
+ test('end-to-end setup', () {
+ expect(inputVar1.text, 'longVar1');
+ expect(inputFunction.text, 'longName');
+ expect(inputVar2.text, 'longVar2');
+ expect(inputExpr.text, 'longVar1 + longVar2');
+
+ expect(outputVar1.text, 'x');
+ expect(outputFunction.text, 'f');
+ expect(outputVar2.text, 'y');
+ expect(outputExpr.text, 'x + y');
+ });
+
+ test('build + parse', () {
+ var map = (new SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .build(output.url);
+ var mapping = parseJson(map);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('build + parse with file', () {
+ var json = (new SourceMapBuilder()
+ ..addSpan(inputVar1, outputVar1)
+ ..addSpan(inputFunction, outputFunction)
+ ..addSpan(inputVar2, outputVar2)
+ ..addSpan(inputExpr, outputExpr))
+ .toJson(output.url);
+ var mapping = parse(json);
+ check(outputVar1, mapping, inputVar1, true);
+ check(outputVar2, mapping, inputVar2, true);
+ check(outputFunction, mapping, inputFunction, true);
+ check(outputExpr, mapping, inputExpr, true);
+ });
+
+ test('printer projecting marks + parse', () {
+ var out = INPUT.replaceAll('long', '_s');
+ var file = new SourceFile.text('output2.dart', out);
+ var printer = new Printer('output2.dart');
+ printer.mark(ispan(0, 0));
+
+ bool first = true;
+ var segments = INPUT.split('long');
+ expect(segments.length, 6);
+ printer.add(segments[0], projectMarks: true);
+ printer.mark(inputVar1);
+ printer.add('_s');
+ printer.add(segments[1], projectMarks: true);
+ printer.mark(inputFunction);
+ printer.add('_s');
+ printer.add(segments[2], projectMarks: true);
+ printer.mark(inputVar2);
+ printer.add('_s');
+ printer.add(segments[3], projectMarks: true);
+ printer.mark(inputExpr);
+ printer.add('_s');
+ printer.add(segments[4], projectMarks: true);
+ printer.add('_s');
+ printer.add(segments[5], projectMarks: true);
+
+ expect(printer.text, out);
+
+ var mapping = parse(printer.map);
+ checkHelper(Span inputSpan, int adjustment) {
+ var start = inputSpan.start.offset - adjustment;
+ var end = (inputSpan.end.offset - adjustment) - 2;
+ var span = new FileSpan(file, start, end, inputSpan.isIdentifier);
+ check(span, mapping, inputSpan, true);
+ }
+
+ checkHelper(inputVar1, 0);
+ checkHelper(inputFunction, 2);
+ checkHelper(inputVar2, 4);
+ checkHelper(inputExpr, 6);
+
+ // We projected correctly lines that have no mappings
+ check(new FileSpan(file, 66, 66, false), mapping, ispan(45, 45), true);
+ check(new FileSpan(file, 63, 64, false), mapping, ispan(45, 45), true);
+ check(new FileSpan(file, 68, 68, false), mapping, ispan(70, 70), true);
+ check(new FileSpan(file, 71, 71, false), mapping, ispan(70, 70), true);
+
+ // Start of the last line
+ var oOffset = out.length - 2;
+ var iOffset = INPUT.length - 2;
+ check(new FileSpan(file, oOffset, oOffset, false), mapping,
+ ispan(iOffset, iOffset), true);
+ check(new FileSpan(file, oOffset + 1, oOffset + 1, false), mapping,
+ ispan(iOffset, iOffset), true);
+ });
+}
diff --git a/test/parser_test.dart b/test/parser_test.dart
new file mode 100644
index 0000000..1c32cbd
--- /dev/null
+++ b/test/parser_test.dart
@@ -0,0 +1,36 @@
+// Copyright (c) 2013, 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 test.parser_test;
+
+import 'dart:json' as json;
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/source_maps.dart';
+import 'common.dart';
+
+main() {
+ test('parse', () {
+ var mapping = parseJson(EXPECTED_MAP);
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('parse + json', () {
+ var mapping = parse(json.stringify(EXPECTED_MAP));
+ check(outputVar1, mapping, inputVar1, false);
+ check(outputVar2, mapping, inputVar2, false);
+ check(outputFunction, mapping, inputFunction, false);
+ check(outputExpr, mapping, inputExpr, false);
+ });
+
+ test('parse with file', () {
+ var mapping = parseJson(EXPECTED_MAP);
+ check(outputVar1, mapping, inputVar1, true);
+ check(outputVar2, mapping, inputVar2, true);
+ check(outputFunction, mapping, inputFunction, true);
+ check(outputExpr, mapping, inputExpr, true);
+ });
+}
diff --git a/test/printer_test.dart b/test/printer_test.dart
new file mode 100644
index 0000000..d038ad5
--- /dev/null
+++ b/test/printer_test.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2013, 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 test.printer_test;
+
+import 'dart:json' as json;
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/printer.dart';
+import 'package:source_maps/span.dart';
+import 'common.dart';
+
+main() {
+ test('printer', () {
+ var printer = new Printer('output.dart');
+ printer..add('var ')
+ ..mark(inputVar1)
+ ..add('x = 3;\n')
+ ..mark(inputFunction)
+ ..add('f(')
+ ..mark(inputVar2)
+ ..add('y) => ')
+ ..mark(inputExpr)
+ ..add('x + y;\n');
+ expect(printer.text, OUTPUT);
+ expect(printer.map, json.stringify(EXPECTED_MAP));
+ });
+
+ test('printer projecting marks', () {
+ var out = INPUT.replaceAll('long', '_s');
+ var printer = new Printer('output2.dart');
+
+ var segments = INPUT.split('long');
+ expect(segments.length, 6);
+ printer..mark(ispan(0, 0))
+ ..add(segments[0], projectMarks: true)
+ ..mark(inputVar1)
+ ..add('_s')
+ ..add(segments[1], projectMarks: true)
+ ..mark(inputFunction)
+ ..add('_s')
+ ..add(segments[2], projectMarks: true)
+ ..mark(inputVar2)
+ ..add('_s')
+ ..add(segments[3], projectMarks: true)
+ ..mark(inputExpr)
+ ..add('_s')
+ ..add(segments[4], projectMarks: true)
+ ..add('_s')
+ ..add(segments[5], projectMarks: true);
+
+ expect(printer.text, out);
+ // 8 new lines in the source map:
+ expect(printer.map.split(';').length, 8);
+
+ asFixed(Span s) => new FixedSpan(s.sourceUrl,
+ s.start.offset, s.start.line, s.start.column,
+ text: s.text, isIdentifier: s.isIdentifier);
+
+ // The result is the same if we use fixed positions
+ var printer2 = new Printer('output2.dart');
+ printer2..mark(new FixedSpan('input.dart', 0, 0, 0))
+ ..add(segments[0], projectMarks: true)
+ ..mark(asFixed(inputVar1))
+ ..add('_s')
+ ..add(segments[1], projectMarks: true)
+ ..mark(asFixed(inputFunction))
+ ..add('_s')
+ ..add(segments[2], projectMarks: true)
+ ..mark(asFixed(inputVar2))
+ ..add('_s')
+ ..add(segments[3], projectMarks: true)
+ ..mark(asFixed(inputExpr))
+ ..add('_s')
+ ..add(segments[4], projectMarks: true)
+ ..add('_s')
+ ..add(segments[5], projectMarks: true);
+
+ expect(printer2.text, out);
+ expect(printer2.map, printer.map);
+ });
+}
diff --git a/test/run.dart b/test/run.dart
new file mode 100755
index 0000000..9a19785
--- /dev/null
+++ b/test/run.dart
@@ -0,0 +1,38 @@
+#!/usr/bin/env dart
+// Copyright (c) 2013, 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 test.run;
+
+import 'package:unittest/compact_vm_config.dart';
+import 'package:unittest/unittest.dart';
+import 'dart:io' show Options;
+
+import 'builder_test.dart' as builder_test;
+import 'end2end_test.dart' as end2end_test;
+import 'parser_test.dart' as parser_test;
+import 'printer_test.dart' as printer_test;
+import 'span_test.dart' as span_test;
+import 'utils_test.dart' as utils_test;
+import 'vlq_test.dart' as vlq_test;
+
+main() {
+ var args = new Options().arguments;
+ var pattern = new RegExp(args.length > 0 ? args[0] : '.');
+ useCompactVMConfiguration();
+
+ void addGroup(testFile, testMain) {
+ if (pattern.hasMatch(testFile)) {
+ group(testFile.replaceAll('_test.dart', ':'), testMain);
+ }
+ }
+
+ addGroup('builder_test.dart', builder_test.main);
+ addGroup('end2end_test.dart', end2end_test.main);
+ addGroup('parser_test.dart', parser_test.main);
+ addGroup('printer_test.dart', printer_test.main);
+ addGroup('span_test.dart', span_test.main);
+ addGroup('utils_test.dart', utils_test.main);
+ addGroup('vlq_test.dart', vlq_test.main);
+}
diff --git a/test/span_test.dart b/test/span_test.dart
new file mode 100644
index 0000000..8f61b58
--- /dev/null
+++ b/test/span_test.dart
@@ -0,0 +1,215 @@
+// Copyright (c) 2013, 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 test.span_test;
+
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/span.dart';
+
+const String TEST_FILE = '''
++23456789_
+ + _123456789_123456789_123456789_123456789_123456789_123456789_123456789_
+ + _123456789_1
+123+56789_123456789_1234567
+1234+6789_1234
+12345+789_123456789_12345
+123456+89_123456789_123456789_123456789_123456789_123456789_123456789_123456789
+1234567+9_123456789_123456789_123456789_123456789_123456789_123456789_123
+12345678+_123456789_123456789_123456789_123456789_1
+123456789+123456789_123456789_12345678
+123456789_+23456789_123456789_123456789_123
+123456789_1+3456789_123456789
+''';
+
+List<int> newLines = TEST_FILE.split('\n').map((s) => s.length).toList();
+
+main() {
+ var file = new SourceFile.text('file', TEST_FILE);
+ span(int start, int end) => file.span(start, end);
+ loc(int offset) => file.location(offset);
+
+ test('validate test input', () {
+ expect(newLines,
+ const [10, 80, 31, 27, 14, 25, 79, 73, 51, 38, 43, 29, 0]);
+ });
+
+ test('get line and column', () {
+ line(int n) => file.getLine(n);
+ col(int n) => file.getColumn(file.getLine(n), n);
+
+ expect(line(8), 0);
+ expect(line(10), 0);
+ expect(line(11), 1);
+ expect(line(12), 1);
+ expect(line(91), 1);
+ expect(line(92), 2);
+ expect(line(93), 2);
+ expect(col(11), 0);
+ expect(col(12), 1);
+ expect(col(91), 80);
+ expect(col(92), 0);
+ expect(col(93), 1);
+
+ int j = 0;
+ int lineOffset = 0;
+ for (int i = 0; i < TEST_FILE.length; i++) {
+ if (i > lineOffset + newLines[j]) {
+ lineOffset += newLines[j] + 1;
+ j++;
+ }
+ expect(line(i), j, reason: 'position: $i');
+ expect(col(i), i - lineOffset, reason: 'position: $i');
+ }
+ });
+
+ test('get text', () {
+ // fifth line (including 4 new lines), columns 2 .. 11
+ var line = 10 + 80 + 31 + 27 + 4;
+ expect(file.getText(line + 2, line + 11), '34+6789_1');
+ });
+
+ test('get location message', () {
+ // fifth line (including 4 new lines), columns 2 .. 11
+ var line = 10 + 80 + 31 + 27 + 4;
+ expect(file.getLocationMessage('the message', line + 2, line + 11),
+ 'file:5:3: the message\n'
+ '1234+6789_1234\n'
+ ' ^^^^^^^^^');
+ });
+
+ test('get location message - no file url', () {
+ var line = 10 + 80 + 31 + 27 + 4;
+ expect(new SourceFile.text(null, TEST_FILE).getLocationMessage(
+ 'the message', line + 2, line + 11),
+ ':5:3: the message\n'
+ '1234+6789_1234\n'
+ ' ^^^^^^^^^');
+ });
+
+ test('location getters', () {
+ expect(loc(8).line, 0);
+ expect(loc(8).column, 8);
+ expect(loc(9).line, 0);
+ expect(loc(9).column, 9);
+ expect(loc(8).formatString, 'file:1:9');
+ expect(loc(12).line, 1);
+ expect(loc(12).column, 1);
+ expect(loc(95).line, 2);
+ expect(loc(95).column, 3);
+ });
+
+ test('location compare', () {
+ var list = [9, 8, 11, 14, 6, 6, 1, 1].map((n) => loc(n)).toList();
+ list.sort();
+ var lastOffset = 0;
+ for (var location in list) {
+ expect(location.offset, greaterThanOrEqualTo(lastOffset));
+ lastOffset = location.offset;
+ }
+ });
+
+ test('span getters', () {
+ expect(span(8, 9).start.line, 0);
+ expect(span(8, 9).start.column, 8);
+ expect(span(8, 9).end.line, 0);
+ expect(span(8, 9).end.column, 9);
+ expect(span(8, 9).text, '9');
+ expect(span(8, 9).isIdentifier, false);
+ expect(span(8, 9).formatLocation, 'file:1:9');
+
+ var line = 10 + 80 + 31 + 27 + 4;
+ expect(span(line + 2, line + 11).getLocationMessage('the message'),
+ 'file:5:3: the message\n'
+ '1234+6789_1234\n'
+ ' ^^^^^^^^^');
+
+ expect(span(12, 95).start.line, 1);
+ expect(span(12, 95).start.column, 1);
+ expect(span(12, 95).end.line, 2);
+ expect(span(12, 95).end.column, 3);
+ expect(span(12, 95).text,
+ '+ _123456789_123456789_123456789_123456789_123456789_1234567'
+ '89_123456789_\n +');
+ expect(span(12, 95).formatLocation, 'file:2:2');
+ });
+
+ test('span union', () {
+ var union = new FileSpan.union(span(8, 9), span(12, 95));
+ expect(union.start.offset, 8);
+ expect(union.start.line, 0);
+ expect(union.start.column, 8);
+ expect(union.end.offset, 95);
+ expect(union.end.line, 2);
+ expect(union.end.column, 3);
+ expect(union.text,
+ '9_\n'
+ ' + _123456789_123456789_123456789_123456789_123456789_'
+ '123456789_123456789_\n +');
+ expect(union.formatLocation, 'file:1:9');
+ });
+
+ test('span compare', () {
+ var list = [span(9, 10), span(8, 9), span(11, 12), span(14, 19),
+ span(6, 12), span(6, 8), span(1, 9), span(1, 2)];
+ list.sort();
+ var lastStart = 0;
+ var lastEnd = 0;
+ for (var span in list) {
+ expect(span.start.offset, greaterThanOrEqualTo(lastStart));
+ if (span.start.offset == lastStart) {
+ expect(span.end.offset, greaterThanOrEqualTo(lastEnd));
+ }
+ lastStart = span.start.offset;
+ lastEnd = span.end.offset;
+ }
+ });
+
+ test('file segment', () {
+ var segment = new SourceFileSegment('file',
+ TEST_FILE.substring(12), loc(12));
+ sline(int n) => segment.getLine(n);
+ scol(int n) => segment.getColumn(segment.getLine(n), n);
+
+ line(int n) => file.getLine(n);
+ col(int n) => file.getColumn(file.getLine(n), n);
+
+ int j = 0;
+ int lineOffset = 0;
+ for (int i = 12; i < TEST_FILE.length; i++) {
+ if (i > lineOffset + newLines[j]) {
+ lineOffset += newLines[j] + 1;
+ j++;
+ }
+ expect(segment.location(i - 12).offset, i);
+ expect(segment.location(i - 12).line, line(i));
+ expect(segment.location(i - 12).column, col(i));
+ expect(segment.span(i - 12).start.offset, i);
+ expect(segment.span(i - 12).start.line, line(i));
+ expect(segment.span(i - 12).start.column, col(i));
+
+ expect(sline(i), line(i));
+ expect(scol(i), col(i));
+ }
+ });
+
+ test('span isIdentifier defaults to false', () {
+ var start = new TestLocation(0);
+ var end = new TestLocation(1);
+ expect(new TestSpan(start, end).isIdentifier, false);
+ expect(file.span(8, 9, null).isIdentifier, false);
+ expect(new FixedSpan('', 8, 1, 8, isIdentifier: null).isIdentifier, false);
+ });
+}
+
+class TestSpan extends Span {
+ TestSpan(Location start, Location end) : super(start, end, null);
+ get text => null;
+}
+
+class TestLocation extends Location {
+ String get sourceUrl => '';
+ TestLocation(int offset) : super(offset);
+ get line => 0;
+ get column => 0;
+}
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..79a7de7
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,54 @@
+// Copyright (c) 2013, 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.
+
+/// Tests for the binary search utility algorithm.
+library test.utils_test;
+
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/src/utils.dart';
+
+main() {
+ group('binary search', () {
+ test('empty', () {
+ expect(binarySearch([], (x) => true), -1);
+ });
+
+ test('single element', () {
+ expect(binarySearch([1], (x) => true), 0);
+ expect(binarySearch([1], (x) => false), 1);
+ });
+
+ test('no matches', () {
+ var list = [1, 2, 3, 4, 5, 6, 7];
+ expect(binarySearch(list, (x) => false), list.length);
+ });
+
+ test('all match', () {
+ var list = [1, 2, 3, 4, 5, 6, 7];
+ expect(binarySearch(list, (x) => true), 0);
+ });
+
+ test('compare with linear search', () {
+ for (int size = 0; size < 100; size++) {
+ var list = [];
+ for (int i = 0; i < size; i++) {
+ list.add(i);
+ }
+ for (int pos = 0; pos <= size; pos++) {
+ expect(binarySearch(list, (x) => x >= pos),
+ _linearSearch(list, (x) => x >= pos));
+ }
+ }
+ });
+ });
+}
+
+_linearSearch(list, predicate) {
+ if (list.length == 0) return -1;
+ for (int i = 0; i < list.length; i++) {
+ if (predicate(list[i])) return i;
+ }
+ return list.length;
+}
+
diff --git a/test/vlq_test.dart b/test/vlq_test.dart
new file mode 100644
index 0000000..0abdc47
--- /dev/null
+++ b/test/vlq_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2013, 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 test.vlq_test;
+
+import 'dart:math';
+import 'package:unittest/unittest.dart';
+import 'package:source_maps/src/vlq.dart';
+
+main() {
+ test('encode and decode - simple values', () {
+ expect(encodeVlq(1).join(''), 'C');
+ expect(encodeVlq(2).join(''), 'E');
+ expect(encodeVlq(3).join(''), 'G');
+ expect(encodeVlq(100).join(''), 'oG');
+ expect(decodeVlq('C'.split('').iterator), 1);
+ expect(decodeVlq('E'.split('').iterator), 2);
+ expect(decodeVlq('G'.split('').iterator), 3);
+ expect(decodeVlq('oG'.split('').iterator), 100);
+ });
+
+ test('encode and decode', () {
+ for (int i = -10000; i < 10000; i++) {
+ _checkEncodeDecode(i);
+ }
+ });
+
+ test('only 32-bit ints allowed', () {
+ var max_int = pow(2, 31) - 1;
+ var min_int = -pow(2, 31);
+ _checkEncodeDecode(max_int - 1);
+ _checkEncodeDecode(min_int + 1);
+ _checkEncodeDecode(max_int);
+ _checkEncodeDecode(min_int);
+
+ expect(encodeVlq(min_int).join(''), 'hgggggE');
+ expect(decodeVlq('hgggggE'.split('').iterator), min_int);
+
+ expect(() => encodeVlq(max_int + 1), throws);
+ expect(() => encodeVlq(max_int + 2), throws);
+ expect(() => encodeVlq(min_int - 1), throws);
+ expect(() => encodeVlq(min_int - 2), throws);
+
+
+ // if we allowed more than 32 bits, these would be the expected encodings
+ // for the large numbers above.
+ expect(() => decodeVlq('ggggggE'.split('').iterator), throws);
+ expect(() => decodeVlq('igggggE'.split('').iterator), throws);
+ expect(() => decodeVlq('jgggggE'.split('').iterator), throws);
+ expect(() => decodeVlq('lgggggE'.split('').iterator), throws);
+ });
+}
+
+_checkEncodeDecode(int value) {
+ var encoded = encodeVlq(value);
+ expect(decodeVlq(encoded.iterator), value);
+ expect(decodeVlq(encoded.join('').split('').iterator), value);
+}