Moving logic for writing source maps from SourceMapBuilder into the Mapping class.

R=sigmund@google.com

Review URL: https://codereview.chromium.org//372153002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/source_maps@38073 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/builder.dart b/lib/builder.dart
index ef22e31..cd30ccb 100644
--- a/lib/builder.dart
+++ b/lib/builder.dart
@@ -7,25 +7,16 @@
 
 // TODO(sigmund): add a builder for multi-section mappings.
 
-import 'dart:collection';
 import 'dart:convert';
 
+import 'parser.dart';
 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) {
@@ -48,78 +39,11 @@
 
   /// 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);
-
-      // Encoding can be just the column offset if there is no source
-      // information.
-      var source = entry.source;
-      if (source == null) continue;
-      var newUrlId = _indexOf(_urls, source.sourceUrl);
-
-      srcUrlId = _append(buff, srcUrlId, newUrlId);
-      srcLine = _append(buff, srcLine, source.line);
-      srcColumn = _append(buff, srcColumn, 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;
+    return new SingleMapping.fromEntries(this._entries, fileUrl).toJson();
   }
 
   /// Encodes all mappings added to this builder as a json string.
   String toJson(String fileUrl) => JSON.encode(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.
diff --git a/lib/parser.dart b/lib/parser.dart
index c92a8bb..23835a6 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -5,8 +5,10 @@
 /// Contains the top-level function to parse source maps version 3.
 library source_maps.parser;
 
+import 'dart:collection';
 import 'dart:convert';
 
+import 'builder.dart' as builder;
 import 'span.dart';
 import 'src/utils.dart';
 import 'src/vlq.dart';
@@ -41,7 +43,7 @@
 }
 
 
-/// A mapping parsed our of a source map.
+/// A mapping parsed out of a source map.
 abstract class Mapping {
   Span spanFor(int line, int column, {Map<String, SourceFile> files});
 
@@ -131,7 +133,6 @@
 }
 
 /// 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;
@@ -143,13 +144,58 @@
   final List<String> names;
 
   /// Entries indicating the beginning of each span.
-  final List<TargetLineEntry> lines = <TargetLineEntry>[];
+  final List<TargetLineEntry> lines;
+
+  SingleMapping._internal(this.targetUrl, this.urls, this.names, this.lines);
+
+  factory SingleMapping.fromEntries(
+      Iterable<builder.Entry> entries, [String fileUrl]) {
+    // The entries needs to be sorted by the target offsets.
+    var sourceEntries = new List.from(entries)..sort();
+    var lines = <TargetLineEntry>[];
+
+    // 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`
+    var 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`
+    var names = new LinkedHashMap<String, int>();
+
+    var lineNum;
+    var targetEntries;
+    for (var sourceEntry in sourceEntries) {
+      if (lineNum == null || sourceEntry.target.line > lineNum) {
+        lineNum = sourceEntry.target.line;
+        targetEntries = <TargetEntry>[];
+        lines.add(new TargetLineEntry(lineNum, targetEntries));
+      }
+
+      if (sourceEntry.source == null) {
+        targetEntries.add(new TargetEntry(sourceEntry.target.column));
+      } else {
+        var urlId = urls.putIfAbsent(
+            sourceEntry.source.sourceUrl, () => urls.length);
+        var srcNameId = sourceEntry.identifierName == null ? null :
+            names.putIfAbsent(sourceEntry.identifierName, () => names.length);
+        targetEntries.add(new TargetEntry(
+            sourceEntry.target.column,
+            urlId,
+            sourceEntry.source.line,
+            sourceEntry.source.column,
+            srcNameId));
+      }
+    }
+    return new SingleMapping._internal(
+        fileUrl, urls.keys.toList(), names.keys.toList(), lines);
+  }
 
   SingleMapping.fromJson(Map map)
       : targetUrl = map['file'],
         // TODO(sigmund): add support for 'sourceRoot'
         urls = map['sources'],
-        names = map['names'] {
+        names = map['names'],
+        lines = <TargetLineEntry>[] {
     int line = 0;
     int column = 0;
     int srcUrlId = 0;
@@ -215,6 +261,66 @@
     }
   }
 
+  /// Encodes the Mapping mappings as a json map.
+  Map toJson() {
+    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;
+
+    for (var entry in lines) {
+      int nextLine = entry.line;
+      if (nextLine > line) {
+        for (int i = line; i < nextLine; ++i) {
+          buff.write(';');
+        }
+        line = nextLine;
+        column = 0;
+        first = true;
+      }
+
+      for (var segment in entry.entries) {
+        if (!first) buff.write(',');
+        first = false;
+        column = _append(buff, column, segment.column);
+
+        // Encoding can be just the column offset if there is no source
+        // information.
+        var newUrlId = segment.sourceUrlId;
+        if (newUrlId == null) continue;
+        srcUrlId = _append(buff, srcUrlId, newUrlId);
+        srcLine = _append(buff, srcLine, segment.sourceLine);
+        srcColumn = _append(buff, srcColumn, segment.sourceColumn);
+
+        if (segment.sourceNameId == null) continue;
+        srcNameId = _append(buff, srcNameId, segment.sourceNameId);
+      }
+    }
+
+    var result = {
+      'version': 3,
+      'sourceRoot': '',
+      'sources': urls,
+      'names' : names,
+      'mappings' : buff.toString()
+    };
+    if (targetUrl != null) {
+      result['file'] = targetUrl;
+    }
+    return result;
+  }
+
+  /// 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;
+  }
+
   _segmentError(int seen, int line) => new StateError(
       'Invalid entry in sourcemap, expected 1, 4, or 5'
       ' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
@@ -308,7 +414,7 @@
 /// A line entry read from a source map.
 class TargetLineEntry {
   final int line;
-  List<TargetEntry> entries = <TargetEntry>[];
+  List<TargetEntry> entries;
   TargetLineEntry(this.line, this.entries);
 
   String toString() => '$runtimeType: $line $entries';
diff --git a/test/parser_test.dart b/test/parser_test.dart
index afdd7fb..8f1be3d 100644
--- a/test/parser_test.dart
+++ b/test/parser_test.dart
@@ -99,4 +99,15 @@
     expect(entry.sourceLine, 0);
     expect(entry.sourceNameId, 0);
   });
+
+  test('parse and re-emit', () {
+    for (var expected in [
+        EXPECTED_MAP,
+        MAP_WITH_NO_SOURCE_LOCATION,
+        MAP_WITH_SOURCE_LOCATION,
+        MAP_WITH_SOURCE_LOCATION_AND_NAME]) {
+      var mapping = parseJson(expected);
+      expect(mapping.toJson(), equals(expected));
+    }
+  });
 }