Add support for the sourcesContent field (#28)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7c1b06..8ef25be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.10.5
+
+* Add a `SingleMapping.files` field which provides access to `SourceFile`s
+  representing the `"sourcesContent"` fields in the source map.
+
+* Add an `includeSourceContents` flag to `SingleMapping.toJson()` which
+  indicates whether to include source file contents in the source map.
+
 ## 0.10.4
 * Implement `highlight` in `SourceMapFileSpan`.
 * Require version `^1.3.0` of `source_span`.
diff --git a/lib/parser.dart b/lib/parser.dart
index 3b65e89..e46b783 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -5,7 +5,6 @@
 /// Contains the top-level function to parse source maps version 3.
 library source_maps.parser;
 
-import 'dart:collection';
 import 'dart:convert';
 
 import 'package:source_span/source_span.dart';
@@ -258,6 +257,16 @@
   /// Source names used in the mapping, indexed by id.
   final List<String> names;
 
+  /// The [SourceFile]s to which the entries in [lines] refer.
+  ///
+  /// This is in the same order as [urls]. If this was constructed using
+  /// [fromEntries], this contains files from any [FileLocation]s used to build
+  /// the mapping. If it was parsed from JSON, it contains files for any sources
+  /// whose contents were provided via the `"sourcesContent"` field.
+  ///
+  /// Files whose contents aren't available are `null`.
+  final List<SourceFile> files;
+
   /// Entries indicating the beginning of each span.
   final List<TargetLineEntry> lines;
 
@@ -269,7 +278,7 @@
 
   final Uri _mapUrl;
 
-  SingleMapping._(this.targetUrl, this.urls, this.names, this.lines)
+  SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines)
       : _mapUrl = null;
 
   factory SingleMapping.fromEntries(Iterable<builder.Entry> entries,
@@ -279,12 +288,15 @@
     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>();
+    // rely on map order so that `urls.keys[urls[u]] == u`
+    var urls = <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>();
+    // We rely on map order so that `names.keys[names[n]] == n`
+    var names = <String, int>{};
+
+    /// The file for each URL, indexed by [urls]' values.
+    var files = <int, SourceFile>{};
 
     var lineNum;
     List<TargetEntry> targetEntries;
@@ -301,6 +313,12 @@
         var sourceUrl = sourceEntry.source.sourceUrl;
         var urlId = urls.putIfAbsent(
             sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length);
+
+        if (sourceEntry.source is FileLocation) {
+          files.putIfAbsent(
+              urlId, () => (sourceEntry.source as FileLocation).file);
+        }
+
         var srcNameId = sourceEntry.identifierName == null
             ? null
             : names.putIfAbsent(sourceEntry.identifierName, () => names.length);
@@ -309,16 +327,30 @@
       }
     }
     return new SingleMapping._(
-        fileUrl, urls.keys.toList(), names.keys.toList(), lines);
+        fileUrl,
+        urls.values.map((i) => files[i]).toList(),
+        urls.keys.toList(),
+        names.keys.toList(),
+        lines);
   }
 
   SingleMapping.fromJson(Map map, {mapUrl})
       : targetUrl = map['file'],
         urls = new List<String>.from(map['sources']),
         names = new List<String>.from(map['names']),
+        files = new List(map['sources'].length),
         sourceRoot = map['sourceRoot'],
         lines = <TargetLineEntry>[],
         _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : mapUrl {
+    var sourcesContent = map['sourcesContent'] == null
+        ? const []
+        : new List<String>.from(map['sourcesContent']);
+    for (var i = 0; i < urls.length && i < sourcesContent.length; i++) {
+      var source = sourcesContent[i];
+      if (source == null) continue;
+      files[i] = new SourceFile.fromString(source, url: urls[i]);
+    }
+
     int line = 0;
     int column = 0;
     int srcUrlId = 0;
@@ -385,7 +417,10 @@
   }
 
   /// Encodes the Mapping mappings as a json map.
-  Map toJson() {
+  ///
+  /// If [sourcesContent] is `true`, this includes the source file contents from
+  /// [files] in the map if possible.
+  Map toJson({bool includeSourceContents: false}) {
     var buff = new StringBuffer();
     var line = 0;
     var column = 0;
@@ -431,9 +466,12 @@
       'names': names,
       'mappings': buff.toString()
     };
-    if (targetUrl != null) {
-      result['file'] = targetUrl;
+    if (targetUrl != null) result['file'] = targetUrl;
+
+    if (includeSourceContents) {
+      result['sourcesContent'] = files.map((file) => file?.getText(0)).toList();
     }
+
     return result;
   }
 
diff --git a/pubspec.yaml b/pubspec.yaml
index ecf277b..7bf7bcd 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_maps
-version: 0.10.5-dev
+version: 0.10.5
 author: Dart Team <misc@dartlang.org>
 description: Library to programmatically manipulate source map files.
 homepage: http://github.com/dart-lang/source_maps
diff --git a/test/parser_test.dart b/test/parser_test.dart
index 8c98c81..7896341 100644
--- a/test/parser_test.dart
+++ b/test/parser_test.dart
@@ -341,4 +341,55 @@
     var mapping = parseJsonExtended(SOURCE_MAP_BUNDLE) as MappingBundle;
     expect(mapping.toJson(), equals(SOURCE_MAP_BUNDLE));
   });
+
+  group("source files", () {
+    group("from fromEntries()", () {
+      test("are null for non-FileLocations", () {
+        var mapping = new SingleMapping.fromEntries([
+          new Entry(new SourceLocation(10, line: 1, column: 8),
+              outputVar1.start, null)
+        ]);
+        expect(mapping.files, equals([null]));
+      });
+
+      test("use a file location's file", () {
+        var mapping = new SingleMapping.fromEntries(
+            [new Entry(inputVar1.start, outputVar1.start, null)]);
+        expect(mapping.files, equals([input]));
+      });
+    });
+
+    group("from parse()", () {
+      group("are null", () {
+        test("with no sourcesContent field", () {
+          var mapping = parseJson(EXPECTED_MAP) as SingleMapping;
+          expect(mapping.files, equals([null]));
+        });
+
+        test("with null sourcesContent values", () {
+          var map = new Map.from(EXPECTED_MAP);
+          map["sourcesContent"] = [null];
+          var mapping = parseJson(map) as SingleMapping;
+          expect(mapping.files, equals([null]));
+        });
+
+        test("with a too-short sourcesContent", () {
+          var map = new Map.from(EXPECTED_MAP);
+          map["sourcesContent"] = [];
+          var mapping = parseJson(map) as SingleMapping;
+          expect(mapping.files, equals([null]));
+        });
+      });
+
+      test("are parsed from sourcesContent", () {
+        var map = new Map.from(EXPECTED_MAP);
+        map["sourcesContent"] = ["hello, world!"];
+        var mapping = parseJson(map) as SingleMapping;
+
+        var file = mapping.files[0];
+        expect(file.url, equals(Uri.parse("input.dart")));
+        expect(file.getText(0), equals("hello, world!"));
+      });
+    });
+  });
 }