1/* -*- Mode: js; js-indent-level: 2; -*- */
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
6 */
7
8var base64VLQ = require('./base64-vlq');
9var util = require('./util');
10var ArraySet = require('./array-set').ArraySet;
11var MappingList = require('./mapping-list').MappingList;
12
13/**
14 * An instance of the SourceMapGenerator represents a source map which is
15 * being built incrementally. You may pass an object with the following
16 * properties:
17 *
18 *   - file: The filename of the generated source.
19 *   - sourceRoot: A root for all relative URLs in this source map.
20 */
21function SourceMapGenerator(aArgs) {
22  if (!aArgs) {
23    aArgs = {};
24  }
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;
32}
33
34SourceMapGenerator.prototype._version = 3;
35
36/**
37 * Creates a new SourceMapGenerator based on a SourceMapConsumer
38 *
39 * @param aSourceMapConsumer The SourceMap.
40 */
41SourceMapGenerator.fromSourceMap =
42  function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
43    var sourceRoot = aSourceMapConsumer.sourceRoot;
44    var generator = new SourceMapGenerator({
45      file: aSourceMapConsumer.file,
46      sourceRoot: sourceRoot
47    });
48    aSourceMapConsumer.eachMapping(function (mapping) {
49      var newMapping = {
50        generated: {
51          line: mapping.generatedLine,
52          column: mapping.generatedColumn
53        }
54      };
55
56      if (mapping.source != null) {
57        newMapping.source = mapping.source;
58        if (sourceRoot != null) {
59          newMapping.source = util.relative(sourceRoot, newMapping.source);
60        }
61
62        newMapping.original = {
63          line: mapping.originalLine,
64          column: mapping.originalColumn
65        };
66
67        if (mapping.name != null) {
68          newMapping.name = mapping.name;
69        }
70      }
71
72      generator.addMapping(newMapping);
73    });
74    aSourceMapConsumer.sources.forEach(function (sourceFile) {
75      var sourceRelative = sourceFile;
76      if (sourceRoot !== null) {
77        sourceRelative = util.relative(sourceRoot, sourceFile);
78      }
79
80      if (!generator._sources.has(sourceRelative)) {
81        generator._sources.add(sourceRelative);
82      }
83
84      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
85      if (content != null) {
86        generator.setSourceContent(sourceFile, content);
87      }
88    });
89    return generator;
90  };
91
92/**
93 * Add a single mapping from original source line and column to the generated
94 * source's line and column for this source map being created. The mapping
95 * object should have the following properties:
96 *
97 *   - generated: An object with the generated line and column positions.
98 *   - original: An object with the original line and column positions.
99 *   - source: The original source file (relative to the sourceRoot).
100 *   - name: An optional original token name for this mapping.
101 */
102SourceMapGenerator.prototype.addMapping =
103  function SourceMapGenerator_addMapping(aArgs) {
104    var generated = util.getArg(aArgs, 'generated');
105    var original = util.getArg(aArgs, 'original', null);
106    var source = util.getArg(aArgs, 'source', null);
107    var name = util.getArg(aArgs, 'name', null);
108
109    if (!this._skipValidation) {
110      this._validateMapping(generated, original, source, name);
111    }
112
113    if (source != null) {
114      source = String(source);
115      if (!this._sources.has(source)) {
116        this._sources.add(source);
117      }
118    }
119
120    if (name != null) {
121      name = String(name);
122      if (!this._names.has(name)) {
123        this._names.add(name);
124      }
125    }
126
127    this._mappings.add({
128      generatedLine: generated.line,
129      generatedColumn: generated.column,
130      originalLine: original != null && original.line,
131      originalColumn: original != null && original.column,
132      source: source,
133      name: name
134    });
135  };
136
137/**
138 * Set the source content for a source file.
139 */
140SourceMapGenerator.prototype.setSourceContent =
141  function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
142    var source = aSourceFile;
143    if (this._sourceRoot != null) {
144      source = util.relative(this._sourceRoot, source);
145    }
146
147    if (aSourceContent != null) {
148      // Add the source content to the _sourcesContents map.
149      // Create a new _sourcesContents map if the property is null.
150      if (!this._sourcesContents) {
151        this._sourcesContents = Object.create(null);
152      }
153      this._sourcesContents[util.toSetString(source)] = aSourceContent;
154    } else if (this._sourcesContents) {
155      // Remove the source file from the _sourcesContents map.
156      // If the _sourcesContents map is empty, set the property to null.
157      delete this._sourcesContents[util.toSetString(source)];
158      if (Object.keys(this._sourcesContents).length === 0) {
159        this._sourcesContents = null;
160      }
161    }
162  };
163
164/**
165 * Applies the mappings of a sub-source-map for a specific source file to the
166 * source map being generated. Each mapping to the supplied source file is
167 * rewritten using the supplied source map. Note: The resolution for the
168 * resulting mappings is the minimium of this map and the supplied map.
169 *
170 * @param aSourceMapConsumer The source map to be applied.
171 * @param aSourceFile Optional. The filename of the source file.
172 *        If omitted, SourceMapConsumer's file property will be used.
173 * @param aSourceMapPath Optional. The dirname of the path to the source map
174 *        to be applied. If relative, it is relative to the SourceMapConsumer.
175 *        This parameter is needed when the two source maps aren't in the same
176 *        directory, and the source map to be applied contains relative source
177 *        paths. If so, those relative source paths need to be rewritten
178 *        relative to the SourceMapGenerator.
179 */
180SourceMapGenerator.prototype.applySourceMap =
181  function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
182    var sourceFile = aSourceFile;
183    // If aSourceFile is omitted, we will use the file property of the SourceMap
184    if (aSourceFile == null) {
185      if (aSourceMapConsumer.file == null) {
186        throw new Error(
187          'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
188          'or the source map\'s "file" property. Both were omitted.'
189        );
190      }
191      sourceFile = aSourceMapConsumer.file;
192    }
193    var sourceRoot = this._sourceRoot;
194    // Make "sourceFile" relative if an absolute Url is passed.
195    if (sourceRoot != null) {
196      sourceFile = util.relative(sourceRoot, sourceFile);
197    }
198    // Applying the SourceMap can add and remove items from the sources and
199    // the names array.
200    var newSources = new ArraySet();
201    var newNames = new ArraySet();
202
203    // Find mappings for the "sourceFile"
204    this._mappings.unsortedForEach(function (mapping) {
205      if (mapping.source === sourceFile && mapping.originalLine != null) {
206        // Check if it can be mapped by the source map, then update the mapping.
207        var original = aSourceMapConsumer.originalPositionFor({
208          line: mapping.originalLine,
209          column: mapping.originalColumn
210        });
211        if (original.source != null) {
212          // Copy mapping
213          mapping.source = original.source;
214          if (aSourceMapPath != null) {
215            mapping.source = util.join(aSourceMapPath, mapping.source)
216          }
217          if (sourceRoot != null) {
218            mapping.source = util.relative(sourceRoot, mapping.source);
219          }
220          mapping.originalLine = original.line;
221          mapping.originalColumn = original.column;
222          if (original.name != null) {
223            mapping.name = original.name;
224          }
225        }
226      }
227
228      var source = mapping.source;
229      if (source != null && !newSources.has(source)) {
230        newSources.add(source);
231      }
232
233      var name = mapping.name;
234      if (name != null && !newNames.has(name)) {
235        newNames.add(name);
236      }
237
238    }, this);
239    this._sources = newSources;
240    this._names = newNames;
241
242    // Copy sourcesContents of applied map.
243    aSourceMapConsumer.sources.forEach(function (sourceFile) {
244      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
245      if (content != null) {
246        if (aSourceMapPath != null) {
247          sourceFile = util.join(aSourceMapPath, sourceFile);
248        }
249        if (sourceRoot != null) {
250          sourceFile = util.relative(sourceRoot, sourceFile);
251        }
252        this.setSourceContent(sourceFile, content);
253      }
254    }, this);
255  };
256
257/**
258 * A mapping can have one of the three levels of data:
259 *
260 *   1. Just the generated position.
261 *   2. The Generated position, original position, and original source.
262 *   3. Generated and original position, original source, as well as a name
263 *      token.
264 *
265 * To maintain consistency, we validate that any new mapping being added falls
266 * in to one of these categories.
267 */
268SourceMapGenerator.prototype._validateMapping =
269  function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,
270                                              aName) {
271    // When aOriginal is truthy but has empty values for .line and .column,
272    // it is most likely a programmer error. In this case we throw a very
273    // specific error message to try to guide them the right way.
274    // For example: https://github.com/Polymer/polymer-bundler/pull/519
275    if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') {
276        throw new Error(
277            'original.line and original.column are not numbers -- you probably meant to omit ' +
278            'the original mapping entirely and only map the generated position. If so, pass ' +
279            'null for the original mapping instead of an object with empty or null values.'
280        );
281    }
282
283    if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
284        && aGenerated.line > 0 && aGenerated.column >= 0
285        && !aOriginal && !aSource && !aName) {
286      // Case 1.
287      return;
288    }
289    else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
290             && aOriginal && 'line' in aOriginal && 'column' in aOriginal
291             && aGenerated.line > 0 && aGenerated.column >= 0
292             && aOriginal.line > 0 && aOriginal.column >= 0
293             && aSource) {
294      // Cases 2 and 3.
295      return;
296    }
297    else {
298      throw new Error('Invalid mapping: ' + JSON.stringify({
299        generated: aGenerated,
300        source: aSource,
301        original: aOriginal,
302        name: aName
303      }));
304    }
305  };
306
307/**
308 * Serialize the accumulated mappings in to the stream of base 64 VLQs
309 * specified by the source map format.
310 */
311SourceMapGenerator.prototype._serializeMappings =
312  function SourceMapGenerator_serializeMappings() {
313    var previousGeneratedColumn = 0;
314    var previousGeneratedLine = 1;
315    var previousOriginalColumn = 0;
316    var previousOriginalLine = 0;
317    var previousName = 0;
318    var previousSource = 0;
319    var result = '';
320    var next;
321    var mapping;
322    var nameIdx;
323    var sourceIdx;
324
325    var mappings = this._mappings.toArray();
326    for (var i = 0, len = mappings.length; i < len; i++) {
327      mapping = mappings[i];
328      next = ''
329
330      if (mapping.generatedLine !== previousGeneratedLine) {
331        previousGeneratedColumn = 0;
332        while (mapping.generatedLine !== previousGeneratedLine) {
333          next += ';';
334          previousGeneratedLine++;
335        }
336      }
337      else {
338        if (i > 0) {
339          if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {
340            continue;
341          }
342          next += ',';
343        }
344      }
345
346      next += base64VLQ.encode(mapping.generatedColumn
347                                 - previousGeneratedColumn);
348      previousGeneratedColumn = mapping.generatedColumn;
349
350      if (mapping.source != null) {
351        sourceIdx = this._sources.indexOf(mapping.source);
352        next += base64VLQ.encode(sourceIdx - previousSource);
353        previousSource = sourceIdx;
354
355        // lines are stored 0-based in SourceMap spec version 3
356        next += base64VLQ.encode(mapping.originalLine - 1
357                                   - previousOriginalLine);
358        previousOriginalLine = mapping.originalLine - 1;
359
360        next += base64VLQ.encode(mapping.originalColumn
361                                   - previousOriginalColumn);
362        previousOriginalColumn = mapping.originalColumn;
363
364        if (mapping.name != null) {
365          nameIdx = this._names.indexOf(mapping.name);
366          next += base64VLQ.encode(nameIdx - previousName);
367          previousName = nameIdx;
368        }
369      }
370
371      result += next;
372    }
373
374    return result;
375  };
376
377SourceMapGenerator.prototype._generateSourcesContent =
378  function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
379    return aSources.map(function (source) {
380      if (!this._sourcesContents) {
381        return null;
382      }
383      if (aSourceRoot != null) {
384        source = util.relative(aSourceRoot, source);
385      }
386      var key = util.toSetString(source);
387      return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)
388        ? this._sourcesContents[key]
389        : null;
390    }, this);
391  };
392
393/**
394 * Externalize the source map.
395 */
396SourceMapGenerator.prototype.toJSON =
397  function SourceMapGenerator_toJSON() {
398    var map = {
399      version: this._version,
400      sources: this._sources.toArray(),
401      names: this._names.toArray(),
402      mappings: this._serializeMappings()
403    };
404    if (this._file != null) {
405      map.file = this._file;
406    }
407    if (this._sourceRoot != null) {
408      map.sourceRoot = this._sourceRoot;
409    }
410    if (this._sourcesContents) {
411      map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
412    }
413
414    return map;
415  };
416
417/**
418 * Render the source map being generated to a string.
419 */
420SourceMapGenerator.prototype.toString =
421  function SourceMapGenerator_toString() {
422    return JSON.stringify(this.toJSON());
423  };
424
425exports.SourceMapGenerator = SourceMapGenerator;
426