1 /* -*- Mode: js; js-indent-level: 2; -*- */
3 * Copyright 2011 Mozilla Foundation and contributors
4 * Licensed under the New BSD license. See LICENSE or:
5 * http://opensource.org/licenses/BSD-3-Clause
8 var base64VLQ = require('./base64-vlq');
9 var util = require('./util');
10 var ArraySet = require('./array-set').ArraySet;
11 var MappingList = require('./mapping-list').MappingList;
14 * An instance of the SourceMapGenerator represents a source map which is
15 * being built incrementally. You may pass an object with the following
18 * - file: The filename of the generated source.
19 * - sourceRoot: A root for all relative URLs in this source map.
21 function SourceMapGenerator(aArgs) {
25 this._file = util.getArg(aArgs, 'file', null);
26 this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
27 this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
28 this._sources = new ArraySet();
29 this._names = new ArraySet();
30 this._mappings = new MappingList();
31 this._sourcesContents = null;
34 SourceMapGenerator.prototype._version = 3;
37 * Creates a new SourceMapGenerator based on a SourceMapConsumer
39 * @param aSourceMapConsumer The SourceMap.
41 SourceMapGenerator.fromSourceMap =
42 function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
43 var sourceRoot = aSourceMapConsumer.sourceRoot;
44 var generator = new SourceMapGenerator({
45 file: aSourceMapConsumer.file,
46 sourceRoot: sourceRoot
48 aSourceMapConsumer.eachMapping(function (mapping) {
51 line: mapping.generatedLine,
52 column: mapping.generatedColumn
56 if (mapping.source != null) {
57 newMapping.source = mapping.source;
58 if (sourceRoot != null) {
59 newMapping.source = util.relative(sourceRoot, newMapping.source);
62 newMapping.original = {
63 line: mapping.originalLine,
64 column: mapping.originalColumn
67 if (mapping.name != null) {
68 newMapping.name = mapping.name;
72 generator.addMapping(newMapping);
74 aSourceMapConsumer.sources.forEach(function (sourceFile) {
75 var content = aSourceMapConsumer.sourceContentFor(sourceFile);
76 if (content != null) {
77 generator.setSourceContent(sourceFile, content);
84 * Add a single mapping from original source line and column to the generated
85 * source's line and column for this source map being created. The mapping
86 * object should have the following properties:
88 * - generated: An object with the generated line and column positions.
89 * - original: An object with the original line and column positions.
90 * - source: The original source file (relative to the sourceRoot).
91 * - name: An optional original token name for this mapping.
93 SourceMapGenerator.prototype.addMapping =
94 function SourceMapGenerator_addMapping(aArgs) {
95 var generated = util.getArg(aArgs, 'generated');
96 var original = util.getArg(aArgs, 'original', null);
97 var source = util.getArg(aArgs, 'source', null);
98 var name = util.getArg(aArgs, 'name', null);
100 if (!this._skipValidation) {
101 this._validateMapping(generated, original, source, name);
104 if (source != null) {
105 source = String(source);
106 if (!this._sources.has(source)) {
107 this._sources.add(source);
113 if (!this._names.has(name)) {
114 this._names.add(name);
119 generatedLine: generated.line,
120 generatedColumn: generated.column,
121 originalLine: original != null && original.line,
122 originalColumn: original != null && original.column,
129 * Set the source content for a source file.
131 SourceMapGenerator.prototype.setSourceContent =
132 function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
133 var source = aSourceFile;
134 if (this._sourceRoot != null) {
135 source = util.relative(this._sourceRoot, source);
138 if (aSourceContent != null) {
139 // Add the source content to the _sourcesContents map.
140 // Create a new _sourcesContents map if the property is null.
141 if (!this._sourcesContents) {
142 this._sourcesContents = Object.create(null);
144 this._sourcesContents[util.toSetString(source)] = aSourceContent;
145 } else if (this._sourcesContents) {
146 // Remove the source file from the _sourcesContents map.
147 // If the _sourcesContents map is empty, set the property to null.
148 delete this._sourcesContents[util.toSetString(source)];
149 if (Object.keys(this._sourcesContents).length === 0) {
150 this._sourcesContents = null;
156 * Applies the mappings of a sub-source-map for a specific source file to the
157 * source map being generated. Each mapping to the supplied source file is
158 * rewritten using the supplied source map. Note: The resolution for the
159 * resulting mappings is the minimium of this map and the supplied map.
161 * @param aSourceMapConsumer The source map to be applied.
162 * @param aSourceFile Optional. The filename of the source file.
163 * If omitted, SourceMapConsumer's file property will be used.
164 * @param aSourceMapPath Optional. The dirname of the path to the source map
165 * to be applied. If relative, it is relative to the SourceMapConsumer.
166 * This parameter is needed when the two source maps aren't in the same
167 * directory, and the source map to be applied contains relative source
168 * paths. If so, those relative source paths need to be rewritten
169 * relative to the SourceMapGenerator.
171 SourceMapGenerator.prototype.applySourceMap =
172 function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
173 var sourceFile = aSourceFile;
174 // If aSourceFile is omitted, we will use the file property of the SourceMap
175 if (aSourceFile == null) {
176 if (aSourceMapConsumer.file == null) {
178 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
179 'or the source map\'s "file" property. Both were omitted.'
182 sourceFile = aSourceMapConsumer.file;
184 var sourceRoot = this._sourceRoot;
185 // Make "sourceFile" relative if an absolute Url is passed.
186 if (sourceRoot != null) {
187 sourceFile = util.relative(sourceRoot, sourceFile);
189 // Applying the SourceMap can add and remove items from the sources and
191 var newSources = new ArraySet();
192 var newNames = new ArraySet();
194 // Find mappings for the "sourceFile"
195 this._mappings.unsortedForEach(function (mapping) {
196 if (mapping.source === sourceFile && mapping.originalLine != null) {
197 // Check if it can be mapped by the source map, then update the mapping.
198 var original = aSourceMapConsumer.originalPositionFor({
199 line: mapping.originalLine,
200 column: mapping.originalColumn
202 if (original.source != null) {
204 mapping.source = original.source;
205 if (aSourceMapPath != null) {
206 mapping.source = util.join(aSourceMapPath, mapping.source)
208 if (sourceRoot != null) {
209 mapping.source = util.relative(sourceRoot, mapping.source);
211 mapping.originalLine = original.line;
212 mapping.originalColumn = original.column;
213 if (original.name != null) {
214 mapping.name = original.name;
219 var source = mapping.source;
220 if (source != null && !newSources.has(source)) {
221 newSources.add(source);
224 var name = mapping.name;
225 if (name != null && !newNames.has(name)) {
230 this._sources = newSources;
231 this._names = newNames;
233 // Copy sourcesContents of applied map.
234 aSourceMapConsumer.sources.forEach(function (sourceFile) {
235 var content = aSourceMapConsumer.sourceContentFor(sourceFile);
236 if (content != null) {
237 if (aSourceMapPath != null) {
238 sourceFile = util.join(aSourceMapPath, sourceFile);
240 if (sourceRoot != null) {
241 sourceFile = util.relative(sourceRoot, sourceFile);
243 this.setSourceContent(sourceFile, content);
249 * A mapping can have one of the three levels of data:
251 * 1. Just the generated position.
252 * 2. The Generated position, original position, and original source.
253 * 3. Generated and original position, original source, as well as a name
256 * To maintain consistency, we validate that any new mapping being added falls
257 * in to one of these categories.
259 SourceMapGenerator.prototype._validateMapping =
260 function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,
262 if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
263 && aGenerated.line > 0 && aGenerated.column >= 0
264 && !aOriginal && !aSource && !aName) {
268 else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
269 && aOriginal && 'line' in aOriginal && 'column' in aOriginal
270 && aGenerated.line > 0 && aGenerated.column >= 0
271 && aOriginal.line > 0 && aOriginal.column >= 0
277 throw new Error('Invalid mapping: ' + JSON.stringify({
278 generated: aGenerated,
287 * Serialize the accumulated mappings in to the stream of base 64 VLQs
288 * specified by the source map format.
290 SourceMapGenerator.prototype._serializeMappings =
291 function SourceMapGenerator_serializeMappings() {
292 var previousGeneratedColumn = 0;
293 var previousGeneratedLine = 1;
294 var previousOriginalColumn = 0;
295 var previousOriginalLine = 0;
296 var previousName = 0;
297 var previousSource = 0;
304 var mappings = this._mappings.toArray();
305 for (var i = 0, len = mappings.length; i < len; i++) {
306 mapping = mappings[i];
309 if (mapping.generatedLine !== previousGeneratedLine) {
310 previousGeneratedColumn = 0;
311 while (mapping.generatedLine !== previousGeneratedLine) {
313 previousGeneratedLine++;
318 if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {
325 next += base64VLQ.encode(mapping.generatedColumn
326 - previousGeneratedColumn);
327 previousGeneratedColumn = mapping.generatedColumn;
329 if (mapping.source != null) {
330 sourceIdx = this._sources.indexOf(mapping.source);
331 next += base64VLQ.encode(sourceIdx - previousSource);
332 previousSource = sourceIdx;
334 // lines are stored 0-based in SourceMap spec version 3
335 next += base64VLQ.encode(mapping.originalLine - 1
336 - previousOriginalLine);
337 previousOriginalLine = mapping.originalLine - 1;
339 next += base64VLQ.encode(mapping.originalColumn
340 - previousOriginalColumn);
341 previousOriginalColumn = mapping.originalColumn;
343 if (mapping.name != null) {
344 nameIdx = this._names.indexOf(mapping.name);
345 next += base64VLQ.encode(nameIdx - previousName);
346 previousName = nameIdx;
356 SourceMapGenerator.prototype._generateSourcesContent =
357 function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
358 return aSources.map(function (source) {
359 if (!this._sourcesContents) {
362 if (aSourceRoot != null) {
363 source = util.relative(aSourceRoot, source);
365 var key = util.toSetString(source);
366 return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
367 ? this._sourcesContents[key]
373 * Externalize the source map.
375 SourceMapGenerator.prototype.toJSON =
376 function SourceMapGenerator_toJSON() {
378 version: this._version,
379 sources: this._sources.toArray(),
380 names: this._names.toArray(),
381 mappings: this._serializeMappings()
383 if (this._file != null) {
384 map.file = this._file;
386 if (this._sourceRoot != null) {
387 map.sourceRoot = this._sourceRoot;
389 if (this._sourcesContents) {
390 map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
397 * Render the source map being generated to a string.
399 SourceMapGenerator.prototype.toString =
400 function SourceMapGenerator_toString() {
401 return JSON.stringify(this.toJSON());
404 exports.SourceMapGenerator = SourceMapGenerator;