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 SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
9 var util = require('./util');
11 // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
12 // operating systems these days (capturing the result).
13 var REGEX_NEWLINE = /(\r?\n)/;
15 // Newline character code for charCodeAt() comparisons
16 var NEWLINE_CODE = 10;
18 // Private symbol for identifying `SourceNode`s when multiple versions of
19 // the source-map library are loaded. This MUST NOT CHANGE across
21 var isSourceNode = "$$$isSourceNode$$$";
24 * SourceNodes provide a way to abstract over interpolating/concatenating
25 * snippets of generated JavaScript source code while maintaining the line and
26 * column information associated with the original source code.
28 * @param aLine The original line number.
29 * @param aColumn The original column number.
30 * @param aSource The original source's filename.
31 * @param aChunks Optional. An array of strings which are snippets of
32 * generated JS, or other SourceNodes.
33 * @param aName The original identifier.
35 function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
37 this.sourceContents = {};
38 this.line = aLine == null ? null : aLine;
39 this.column = aColumn == null ? null : aColumn;
40 this.source = aSource == null ? null : aSource;
41 this.name = aName == null ? null : aName;
42 this[isSourceNode] = true;
43 if (aChunks != null) this.add(aChunks);
47 * Creates a SourceNode from generated code and a SourceMapConsumer.
49 * @param aGeneratedCode The generated code
50 * @param aSourceMapConsumer The SourceMap for the generated code
51 * @param aRelativePath Optional. The path that relative sources in the
52 * SourceMapConsumer should be relative to.
54 SourceNode.fromStringWithSourceMap =
55 function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
56 // The SourceNode we want to fill with the generated code
58 var node = new SourceNode();
60 // All even indices of this array are one line of the generated code,
61 // while all odd indices are the newlines between two adjacent lines
62 // (since `REGEX_NEWLINE` captures its match).
63 // Processed fragments are removed from this array, by calling `shiftNextLine`.
64 var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
65 var shiftNextLine = function() {
66 var lineContents = remainingLines.shift();
67 // The last line of a file might not have a newline.
68 var newLine = remainingLines.shift() || "";
69 return lineContents + newLine;
72 // We need to remember the position of "remainingLines"
73 var lastGeneratedLine = 1, lastGeneratedColumn = 0;
75 // The generate SourceNodes we need a code range.
76 // To extract it current and last mapping is used.
77 // Here we store the last mapping.
78 var lastMapping = null;
80 aSourceMapConsumer.eachMapping(function (mapping) {
81 if (lastMapping !== null) {
82 // We add the code from "lastMapping" to "mapping":
83 // First check if there is a new line in between.
84 if (lastGeneratedLine < mapping.generatedLine) {
85 // Associate first line with "lastMapping"
86 addMappingWithCode(lastMapping, shiftNextLine());
88 lastGeneratedColumn = 0;
89 // The remaining code is added without mapping
91 // There is no new line in between.
92 // Associate the code between "lastGeneratedColumn" and
93 // "mapping.generatedColumn" with "lastMapping"
94 var nextLine = remainingLines[0];
95 var code = nextLine.substr(0, mapping.generatedColumn -
97 remainingLines[0] = nextLine.substr(mapping.generatedColumn -
99 lastGeneratedColumn = mapping.generatedColumn;
100 addMappingWithCode(lastMapping, code);
101 // No more remaining code, continue
102 lastMapping = mapping;
106 // We add the generated code until the first mapping
107 // to the SourceNode without any mapping.
108 // Each line is added as separate string.
109 while (lastGeneratedLine < mapping.generatedLine) {
110 node.add(shiftNextLine());
113 if (lastGeneratedColumn < mapping.generatedColumn) {
114 var nextLine = remainingLines[0];
115 node.add(nextLine.substr(0, mapping.generatedColumn));
116 remainingLines[0] = nextLine.substr(mapping.generatedColumn);
117 lastGeneratedColumn = mapping.generatedColumn;
119 lastMapping = mapping;
121 // We have processed all mappings.
122 if (remainingLines.length > 0) {
124 // Associate the remaining code in the current line with "lastMapping"
125 addMappingWithCode(lastMapping, shiftNextLine());
127 // and add the remaining lines without any mapping
128 node.add(remainingLines.join(""));
131 // Copy sourcesContent into SourceNode
132 aSourceMapConsumer.sources.forEach(function (sourceFile) {
133 var content = aSourceMapConsumer.sourceContentFor(sourceFile);
134 if (content != null) {
135 if (aRelativePath != null) {
136 sourceFile = util.join(aRelativePath, sourceFile);
138 node.setSourceContent(sourceFile, content);
144 function addMappingWithCode(mapping, code) {
145 if (mapping === null || mapping.source === undefined) {
148 var source = aRelativePath
149 ? util.join(aRelativePath, mapping.source)
151 node.add(new SourceNode(mapping.originalLine,
152 mapping.originalColumn,
161 * Add a chunk of generated JS to this source node.
163 * @param aChunk A string snippet of generated JS code, another instance of
164 * SourceNode, or an array where each member is one of those things.
166 SourceNode.prototype.add = function SourceNode_add(aChunk) {
167 if (Array.isArray(aChunk)) {
168 aChunk.forEach(function (chunk) {
172 else if (aChunk[isSourceNode] || typeof aChunk === "string") {
174 this.children.push(aChunk);
179 "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
186 * Add a chunk of generated JS to the beginning of this source node.
188 * @param aChunk A string snippet of generated JS code, another instance of
189 * SourceNode, or an array where each member is one of those things.
191 SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
192 if (Array.isArray(aChunk)) {
193 for (var i = aChunk.length-1; i >= 0; i--) {
194 this.prepend(aChunk[i]);
197 else if (aChunk[isSourceNode] || typeof aChunk === "string") {
198 this.children.unshift(aChunk);
202 "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
209 * Walk over the tree of JS snippets in this node and its children. The
210 * walking function is called once for each snippet of JS and is passed that
211 * snippet and the its original associated source's line/column location.
213 * @param aFn The traversal function.
215 SourceNode.prototype.walk = function SourceNode_walk(aFn) {
217 for (var i = 0, len = this.children.length; i < len; i++) {
218 chunk = this.children[i];
219 if (chunk[isSourceNode]) {
224 aFn(chunk, { source: this.source,
234 * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
235 * each of `this.children`.
237 * @param aSep The separator.
239 SourceNode.prototype.join = function SourceNode_join(aSep) {
242 var len = this.children.length;
245 for (i = 0; i < len-1; i++) {
246 newChildren.push(this.children[i]);
247 newChildren.push(aSep);
249 newChildren.push(this.children[i]);
250 this.children = newChildren;
256 * Call String.prototype.replace on the very right-most source snippet. Useful
257 * for trimming whitespace from the end of a source node, etc.
259 * @param aPattern The pattern to replace.
260 * @param aReplacement The thing to replace the pattern with.
262 SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
263 var lastChild = this.children[this.children.length - 1];
264 if (lastChild[isSourceNode]) {
265 lastChild.replaceRight(aPattern, aReplacement);
267 else if (typeof lastChild === 'string') {
268 this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
271 this.children.push(''.replace(aPattern, aReplacement));
277 * Set the source content for a source file. This will be added to the SourceMapGenerator
278 * in the sourcesContent field.
280 * @param aSourceFile The filename of the source file
281 * @param aSourceContent The content of the source file
283 SourceNode.prototype.setSourceContent =
284 function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
285 this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
289 * Walk over the tree of SourceNodes. The walking function is called for each
290 * source file content and is passed the filename and source content.
292 * @param aFn The traversal function.
294 SourceNode.prototype.walkSourceContents =
295 function SourceNode_walkSourceContents(aFn) {
296 for (var i = 0, len = this.children.length; i < len; i++) {
297 if (this.children[i][isSourceNode]) {
298 this.children[i].walkSourceContents(aFn);
302 var sources = Object.keys(this.sourceContents);
303 for (var i = 0, len = sources.length; i < len; i++) {
304 aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
309 * Return the string representation of this source node. Walks over the tree
310 * and concatenates all the various snippets together to one string.
312 SourceNode.prototype.toString = function SourceNode_toString() {
314 this.walk(function (chunk) {
321 * Returns the string representation of this source node along with a source
324 SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
330 var map = new SourceMapGenerator(aArgs);
331 var sourceMappingActive = false;
332 var lastOriginalSource = null;
333 var lastOriginalLine = null;
334 var lastOriginalColumn = null;
335 var lastOriginalName = null;
336 this.walk(function (chunk, original) {
337 generated.code += chunk;
338 if (original.source !== null
339 && original.line !== null
340 && original.column !== null) {
341 if(lastOriginalSource !== original.source
342 || lastOriginalLine !== original.line
343 || lastOriginalColumn !== original.column
344 || lastOriginalName !== original.name) {
346 source: original.source,
349 column: original.column
352 line: generated.line,
353 column: generated.column
358 lastOriginalSource = original.source;
359 lastOriginalLine = original.line;
360 lastOriginalColumn = original.column;
361 lastOriginalName = original.name;
362 sourceMappingActive = true;
363 } else if (sourceMappingActive) {
366 line: generated.line,
367 column: generated.column
370 lastOriginalSource = null;
371 sourceMappingActive = false;
373 for (var idx = 0, length = chunk.length; idx < length; idx++) {
374 if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
376 generated.column = 0;
377 // Mappings end at eol
378 if (idx + 1 === length) {
379 lastOriginalSource = null;
380 sourceMappingActive = false;
381 } else if (sourceMappingActive) {
383 source: original.source,
386 column: original.column
389 line: generated.line,
390 column: generated.column
400 this.walkSourceContents(function (sourceFile, sourceContent) {
401 map.setSourceContent(sourceFile, sourceContent);
404 return { code: generated.code, map: map };
407 exports.SourceNode = SourceNode;