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
7 if (typeof define !== 'function') {
8 var define = require('amdefine')(module, require);
10 define(function (require, exports, module) {
12 var base64VLQ = require('./base64-vlq');
13 var util = require('./util');
14 var ArraySet = require('./array-set').ArraySet;
15 var MappingList = require('./mapping-list').MappingList;
18 * An instance of the SourceMapGenerator represents a source map which is
19 * being built incrementally. You may pass an object with the following
22 * - file: The filename of the generated source.
23 * - sourceRoot: A root for all relative URLs in this source map.
25 function SourceMapGenerator(aArgs) {
29 this._file = util.getArg(aArgs, 'file', null);
30 this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
31 this._skipValidation = util.getArg(aArgs, 'skipValidation', false);
32 this._sources = new ArraySet();
33 this._names = new ArraySet();
34 this._mappings = new MappingList();
35 this._sourcesContents = null;
38 SourceMapGenerator.prototype._version = 3;
41 * Creates a new SourceMapGenerator based on a SourceMapConsumer
43 * @param aSourceMapConsumer The SourceMap.
45 SourceMapGenerator.fromSourceMap =
46 function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
47 var sourceRoot = aSourceMapConsumer.sourceRoot;
48 var generator = new SourceMapGenerator({
49 file: aSourceMapConsumer.file,
50 sourceRoot: sourceRoot
52 aSourceMapConsumer.eachMapping(function (mapping) {
55 line: mapping.generatedLine,
56 column: mapping.generatedColumn
60 if (mapping.source != null) {
61 newMapping.source = mapping.source;
62 if (sourceRoot != null) {
63 newMapping.source = util.relative(sourceRoot, newMapping.source);
66 newMapping.original = {
67 line: mapping.originalLine,
68 column: mapping.originalColumn
71 if (mapping.name != null) {
72 newMapping.name = mapping.name;
76 generator.addMapping(newMapping);
78 aSourceMapConsumer.sources.forEach(function (sourceFile) {
79 var content = aSourceMapConsumer.sourceContentFor(sourceFile);
80 if (content != null) {
81 generator.setSourceContent(sourceFile, content);
88 * Add a single mapping from original source line and column to the generated
89 * source's line and column for this source map being created. The mapping
90 * object should have the following properties:
92 * - generated: An object with the generated line and column positions.
93 * - original: An object with the original line and column positions.
94 * - source: The original source file (relative to the sourceRoot).
95 * - name: An optional original token name for this mapping.
97 SourceMapGenerator.prototype.addMapping =
98 function SourceMapGenerator_addMapping(aArgs) {
99 var generated = util.getArg(aArgs, 'generated');
100 var original = util.getArg(aArgs, 'original', null);
101 var source = util.getArg(aArgs, 'source', null);
102 var name = util.getArg(aArgs, 'name', null);
104 if (!this._skipValidation) {
105 this._validateMapping(generated, original, source, name);
108 if (source != null && !this._sources.has(source)) {
109 this._sources.add(source);
112 if (name != null && !this._names.has(name)) {
113 this._names.add(name);
117 generatedLine: generated.line,
118 generatedColumn: generated.column,
119 originalLine: original != null && original.line,
120 originalColumn: original != null && original.column,
127 * Set the source content for a source file.
129 SourceMapGenerator.prototype.setSourceContent =
130 function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
131 var source = aSourceFile;
132 if (this._sourceRoot != null) {
133 source = util.relative(this._sourceRoot, source);
136 if (aSourceContent != null) {
137 // Add the source content to the _sourcesContents map.
138 // Create a new _sourcesContents map if the property is null.
139 if (!this._sourcesContents) {
140 this._sourcesContents = {};
142 this._sourcesContents[util.toSetString(source)] = aSourceContent;
143 } else if (this._sourcesContents) {
144 // Remove the source file from the _sourcesContents map.
145 // If the _sourcesContents map is empty, set the property to null.
146 delete this._sourcesContents[util.toSetString(source)];
147 if (Object.keys(this._sourcesContents).length === 0) {
148 this._sourcesContents = null;
154 * Applies the mappings of a sub-source-map for a specific source file to the
155 * source map being generated. Each mapping to the supplied source file is
156 * rewritten using the supplied source map. Note: The resolution for the
157 * resulting mappings is the minimium of this map and the supplied map.
159 * @param aSourceMapConsumer The source map to be applied.
160 * @param aSourceFile Optional. The filename of the source file.
161 * If omitted, SourceMapConsumer's file property will be used.
162 * @param aSourceMapPath Optional. The dirname of the path to the source map
163 * to be applied. If relative, it is relative to the SourceMapConsumer.
164 * This parameter is needed when the two source maps aren't in the same
165 * directory, and the source map to be applied contains relative source
166 * paths. If so, those relative source paths need to be rewritten
167 * relative to the SourceMapGenerator.
169 SourceMapGenerator.prototype.applySourceMap =
170 function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
171 var sourceFile = aSourceFile;
172 // If aSourceFile is omitted, we will use the file property of the SourceMap
173 if (aSourceFile == null) {
174 if (aSourceMapConsumer.file == null) {
176 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
177 'or the source map\'s "file" property. Both were omitted.'
180 sourceFile = aSourceMapConsumer.file;
182 var sourceRoot = this._sourceRoot;
183 // Make "sourceFile" relative if an absolute Url is passed.
184 if (sourceRoot != null) {
185 sourceFile = util.relative(sourceRoot, sourceFile);
187 // Applying the SourceMap can add and remove items from the sources and
189 var newSources = new ArraySet();
190 var newNames = new ArraySet();
192 // Find mappings for the "sourceFile"
193 this._mappings.unsortedForEach(function (mapping) {
194 if (mapping.source === sourceFile && mapping.originalLine != null) {
195 // Check if it can be mapped by the source map, then update the mapping.
196 var original = aSourceMapConsumer.originalPositionFor({
197 line: mapping.originalLine,
198 column: mapping.originalColumn
200 if (original.source != null) {
202 mapping.source = original.source;
203 if (aSourceMapPath != null) {
204 mapping.source = util.join(aSourceMapPath, mapping.source)
206 if (sourceRoot != null) {
207 mapping.source = util.relative(sourceRoot, mapping.source);
209 mapping.originalLine = original.line;
210 mapping.originalColumn = original.column;
211 if (original.name != null) {
212 mapping.name = original.name;
217 var source = mapping.source;
218 if (source != null && !newSources.has(source)) {
219 newSources.add(source);
222 var name = mapping.name;
223 if (name != null && !newNames.has(name)) {
228 this._sources = newSources;
229 this._names = newNames;
231 // Copy sourcesContents of applied map.
232 aSourceMapConsumer.sources.forEach(function (sourceFile) {
233 var content = aSourceMapConsumer.sourceContentFor(sourceFile);
234 if (content != null) {
235 if (aSourceMapPath != null) {
236 sourceFile = util.join(aSourceMapPath, sourceFile);
238 if (sourceRoot != null) {
239 sourceFile = util.relative(sourceRoot, sourceFile);
241 this.setSourceContent(sourceFile, content);
247 * A mapping can have one of the three levels of data:
249 * 1. Just the generated position.
250 * 2. The Generated position, original position, and original source.
251 * 3. Generated and original position, original source, as well as a name
254 * To maintain consistency, we validate that any new mapping being added falls
255 * in to one of these categories.
257 SourceMapGenerator.prototype._validateMapping =
258 function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,
260 if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
261 && aGenerated.line > 0 && aGenerated.column >= 0
262 && !aOriginal && !aSource && !aName) {
266 else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated
267 && aOriginal && 'line' in aOriginal && 'column' in aOriginal
268 && aGenerated.line > 0 && aGenerated.column >= 0
269 && aOriginal.line > 0 && aOriginal.column >= 0
275 throw new Error('Invalid mapping: ' + JSON.stringify({
276 generated: aGenerated,
285 * Serialize the accumulated mappings in to the stream of base 64 VLQs
286 * specified by the source map format.
288 SourceMapGenerator.prototype._serializeMappings =
289 function SourceMapGenerator_serializeMappings() {
290 var previousGeneratedColumn = 0;
291 var previousGeneratedLine = 1;
292 var previousOriginalColumn = 0;
293 var previousOriginalLine = 0;
294 var previousName = 0;
295 var previousSource = 0;
299 var mappings = this._mappings.toArray();
301 for (var i = 0, len = mappings.length; i < len; i++) {
302 mapping = mappings[i];
304 if (mapping.generatedLine !== previousGeneratedLine) {
305 previousGeneratedColumn = 0;
306 while (mapping.generatedLine !== previousGeneratedLine) {
308 previousGeneratedLine++;
313 if (!util.compareByGeneratedPositions(mapping, mappings[i - 1])) {
320 result += base64VLQ.encode(mapping.generatedColumn
321 - previousGeneratedColumn);
322 previousGeneratedColumn = mapping.generatedColumn;
324 if (mapping.source != null) {
325 result += base64VLQ.encode(this._sources.indexOf(mapping.source)
327 previousSource = this._sources.indexOf(mapping.source);
329 // lines are stored 0-based in SourceMap spec version 3
330 result += base64VLQ.encode(mapping.originalLine - 1
331 - previousOriginalLine);
332 previousOriginalLine = mapping.originalLine - 1;
334 result += base64VLQ.encode(mapping.originalColumn
335 - previousOriginalColumn);
336 previousOriginalColumn = mapping.originalColumn;
338 if (mapping.name != null) {
339 result += base64VLQ.encode(this._names.indexOf(mapping.name)
341 previousName = this._names.indexOf(mapping.name);
349 SourceMapGenerator.prototype._generateSourcesContent =
350 function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
351 return aSources.map(function (source) {
352 if (!this._sourcesContents) {
355 if (aSourceRoot != null) {
356 source = util.relative(aSourceRoot, source);
358 var key = util.toSetString(source);
359 return Object.prototype.hasOwnProperty.call(this._sourcesContents,
361 ? this._sourcesContents[key]
367 * Externalize the source map.
369 SourceMapGenerator.prototype.toJSON =
370 function SourceMapGenerator_toJSON() {
372 version: this._version,
373 sources: this._sources.toArray(),
374 names: this._names.toArray(),
375 mappings: this._serializeMappings()
377 if (this._file != null) {
378 map.file = this._file;
380 if (this._sourceRoot != null) {
381 map.sourceRoot = this._sourceRoot;
383 if (this._sourcesContents) {
384 map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
391 * Render the source map being generated to a string.
393 SourceMapGenerator.prototype.toString =
394 function SourceMapGenerator_toString() {
395 return JSON.stringify(this);
398 exports.SourceMapGenerator = SourceMapGenerator;