// 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:collection'; import 'dart:convert'; import 'span.dart'; import 'src/vlq.dart'; /// Builds a source map given a set of mappings. class SourceMapBuilder { final List _entries = []; /// 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 _urls = new LinkedHashMap(); /// 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 _names = new LinkedHashMap(); /// 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); // Encoding can be just the column offset if there is no source // information, or if two consecutive mappings share exactly the same // source information. var source = entry.source; if (source == null) continue; var newUrlId = _indexOf(_urls, source.sourceUrl); if (newUrlId == srcUrlId && source.line == srcLine && source.column == srcColumn && entry.identifierName == null) { continue; } 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; } /// 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 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); } }