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('');
+    buff.write(current == null ? '' : current);
+    buff.write('');
+    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);
+}