Add a mapUrl parameter to parse() and parseJson().

Closes #2

R=sigmund@google.com

Review URL: https://codereview.chromium.org//1112743002
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7477655..3066293 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.10.1
+
+* Add a `mapUrl` named argument to `parse` and `parseJson`. This argument is
+  used to resolve source URLs for source spans.
+
 ## 0.10.0+2
 
 * Fix analyzer error (FileSpan has a new field since `source_span` 1.1.1)
diff --git a/lib/parser.dart b/lib/parser.dart
index 369c27d..a9fcff5 100644
--- a/lib/parser.dart
+++ b/lib/parser.dart
@@ -16,15 +16,23 @@
 import 'src/vlq.dart';
 
 /// Parses a source map directly from a json string.
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
 // TODO(sigmund): evaluate whether other maps should have the json parsed, or
 // the string represenation.
 // TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string
 // `)]}'` begins the string representation of the map.
-Mapping parse(String jsonMap, {Map<String, Map> otherMaps}) =>
-  parseJson(JSON.decode(jsonMap), otherMaps: otherMaps);
+Mapping parse(String jsonMap, {Map<String, Map> otherMaps, mapUrl}) =>
+  parseJson(JSON.decode(jsonMap), otherMaps: otherMaps, mapUrl: mapUrl);
 
 /// Parses a source map directly from a json map object.
-Mapping parseJson(Map map, {Map<String, Map> otherMaps}) {
+///
+/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
+/// the source map file itself. If it's passed, any URLs in the source
+/// map will be interpreted as relative to this URL when generating spans.
+Mapping parseJson(Map map, {Map<String, Map> otherMaps, mapUrl}) {
   if (map['version'] != 3) {
     throw new ArgumentError(
         'unexpected source map version: ${map["version"]}. '
@@ -37,9 +45,10 @@
       throw new FormatException('map containing "sections" '
           'cannot contain "mappings", "sources", or "names".');
     }
-    return new MultiSectionMapping.fromJson(map['sections'], otherMaps);
+    return new MultiSectionMapping.fromJson(map['sections'], otherMaps,
+        mapUrl: mapUrl);
   }
-  return new SingleMapping.fromJson(map);
+  return new SingleMapping.fromJson(map, mapUrl: mapUrl);
 }
 
 
@@ -68,7 +77,8 @@
   final List<Mapping> _maps = <Mapping>[];
 
   /// Creates a section mapping from json.
-  MultiSectionMapping.fromJson(List sections, Map<String, Map> otherMaps) {
+  MultiSectionMapping.fromJson(List sections, Map<String, Map> otherMaps,
+      {mapUrl}) {
     for (var section in sections) {
       var offset = section['offset'];
       if (offset == null) throw new FormatException('section missing offset');
@@ -93,9 +103,9 @@
               '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));
+        _maps.add(parseJson(otherMaps[url], otherMaps: otherMaps, mapUrl: url));
       } else if (map != null) {
-        _maps.add(parseJson(map, otherMaps: otherMaps));
+        _maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl));
       } else {
         throw new FormatException('section missing url or map');
       }
@@ -149,10 +159,13 @@
   /// Url of the target file.
   String targetUrl;
 
-  /// Source root appended to the start of all entries in [urls].
+  /// Source root prepended to all entries in [urls].
   String sourceRoot;
 
-  SingleMapping._(this.targetUrl, this.urls, this.names, this.lines);
+  final Uri _mapUrl;
+
+  SingleMapping._(this.targetUrl, this.urls, this.names, this.lines)
+      : _mapUrl = null;
 
   factory SingleMapping.fromEntries(
       Iterable<builder.Entry> entries, [String fileUrl]) {
@@ -197,12 +210,13 @@
         fileUrl, urls.keys.toList(), names.keys.toList(), lines);
   }
 
-  SingleMapping.fromJson(Map map)
+  SingleMapping.fromJson(Map map, {mapUrl})
       : targetUrl = map['file'],
         urls = map['sources'],
         names = map['names'],
         sourceRoot = map['sourceRoot'],
-        lines = <TargetLineEntry>[] {
+        lines = <TargetLineEntry>[],
+        _mapUrl = mapUrl is String ? Uri.parse(mapUrl) : mapUrl {
     int line = 0;
     int column = 0;
     int srcUrlId = 0;
@@ -373,7 +387,10 @@
       }
     } else {
       var start = new SourceLocation(0,
-          sourceUrl: url, line: entry.sourceLine, column: entry.sourceColumn);
+          sourceUrl: _mapUrl == null ? url : _mapUrl.resolve(url),
+          line: entry.sourceLine,
+          column: entry.sourceColumn);
+
       // Offset and other context is not available.
       if (entry.sourceNameId != null) {
         return new SourceMapSpan.identifier(start, names[entry.sourceNameId]);
diff --git a/pubspec.yaml b/pubspec.yaml
index 200f8f8..fc0fe9b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: source_maps
-version: 0.10.0+2
+version: 0.10.1
 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 8528683..b14fdf4 100644
--- a/test/parser_test.dart
+++ b/test/parser_test.dart
@@ -114,6 +114,14 @@
     expect(mapping.toJson(), equals(inputMap));
   });
 
+  test('parse with map URL', () {
+    var inputMap = new Map.from(MAP_WITH_SOURCE_LOCATION);
+    inputMap['sourceRoot'] = 'pkg/';
+    var mapping = parseJson(inputMap, mapUrl: "file:///path/to/map");
+    expect(mapping.spanFor(0, 0).sourceUrl,
+        Uri.parse("file:///path/to/pkg/input.dart"));
+  });
+
   test('parse and re-emit', () {
     for (var expected in [
         EXPECTED_MAP,