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 SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
9var util = require('./util');
10
11// Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
12// operating systems these days (capturing the result).
13var REGEX_NEWLINE = /(\r?\n)/;
14
15// Newline character code for charCodeAt() comparisons
16var NEWLINE_CODE = 10;
17
18// Private symbol for identifying `SourceNode`s when multiple versions of
19// the source-map library are loaded. This MUST NOT CHANGE across
20// versions!
21var isSourceNode = "$$$isSourceNode$$$";
22
23/**
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.
27 *
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.
34 */
35function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
36  this.children = [];
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);
44}
45
46/**
47 * Creates a SourceNode from generated code and a SourceMapConsumer.
48 *
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.
53 */
54SourceNode.fromStringWithSourceMap =
55  function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
56    // The SourceNode we want to fill with the generated code
57    // and the SourceMap
58    var node = new SourceNode();
59
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 accessed by calling `shiftNextLine`.
64    var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
65    var remainingLinesIndex = 0;
66    var shiftNextLine = function() {
67      var lineContents = getNextLine();
68      // The last line of a file might not have a newline.
69      var newLine = getNextLine() || "";
70      return lineContents + newLine;
71
72      function getNextLine() {
73        return remainingLinesIndex < remainingLines.length ?
74            remainingLines[remainingLinesIndex++] : undefined;
75      }
76    };
77
78    // We need to remember the position of "remainingLines"
79    var lastGeneratedLine = 1, lastGeneratedColumn = 0;
80
81    // The generate SourceNodes we need a code range.
82    // To extract it current and last mapping is used.
83    // Here we store the last mapping.
84    var lastMapping = null;
85
86    aSourceMapConsumer.eachMapping(function (mapping) {
87      if (lastMapping !== null) {
88        // We add the code from "lastMapping" to "mapping":
89        // First check if there is a new line in between.
90        if (lastGeneratedLine < mapping.generatedLine) {
91          // Associate first line with "lastMapping"
92          addMappingWithCode(lastMapping, shiftNextLine());
93          lastGeneratedLine++;
94          lastGeneratedColumn = 0;
95          // The remaining code is added without mapping
96        } else {
97          // There is no new line in between.
98          // Associate the code between "lastGeneratedColumn" and
99          // "mapping.generatedColumn" with "lastMapping"
100          var nextLine = remainingLines[remainingLinesIndex] || '';
101          var code = nextLine.substr(0, mapping.generatedColumn -
102                                        lastGeneratedColumn);
103          remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn -
104                                              lastGeneratedColumn);
105          lastGeneratedColumn = mapping.generatedColumn;
106          addMappingWithCode(lastMapping, code);
107          // No more remaining code, continue
108          lastMapping = mapping;
109          return;
110        }
111      }
112      // We add the generated code until the first mapping
113      // to the SourceNode without any mapping.
114      // Each line is added as separate string.
115      while (lastGeneratedLine < mapping.generatedLine) {
116        node.add(shiftNextLine());
117        lastGeneratedLine++;
118      }
119      if (lastGeneratedColumn < mapping.generatedColumn) {
120        var nextLine = remainingLines[remainingLinesIndex] || '';
121        node.add(nextLine.substr(0, mapping.generatedColumn));
122        remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
123        lastGeneratedColumn = mapping.generatedColumn;
124      }
125      lastMapping = mapping;
126    }, this);
127    // We have processed all mappings.
128    if (remainingLinesIndex < remainingLines.length) {
129      if (lastMapping) {
130        // Associate the remaining code in the current line with "lastMapping"
131        addMappingWithCode(lastMapping, shiftNextLine());
132      }
133      // and add the remaining lines without any mapping
134      node.add(remainingLines.splice(remainingLinesIndex).join(""));
135    }
136
137    // Copy sourcesContent into SourceNode
138    aSourceMapConsumer.sources.forEach(function (sourceFile) {
139      var content = aSourceMapConsumer.sourceContentFor(sourceFile);
140      if (content != null) {
141        if (aRelativePath != null) {
142          sourceFile = util.join(aRelativePath, sourceFile);
143        }
144        node.setSourceContent(sourceFile, content);
145      }
146    });
147
148    return node;
149
150    function addMappingWithCode(mapping, code) {
151      if (mapping === null || mapping.source === undefined) {
152        node.add(code);
153      } else {
154        var source = aRelativePath
155          ? util.join(aRelativePath, mapping.source)
156          : mapping.source;
157        node.add(new SourceNode(mapping.originalLine,
158                                mapping.originalColumn,
159                                source,
160                                code,
161                                mapping.name));
162      }
163    }
164  };
165
166/**
167 * Add a chunk of generated JS to this source node.
168 *
169 * @param aChunk A string snippet of generated JS code, another instance of
170 *        SourceNode, or an array where each member is one of those things.
171 */
172SourceNode.prototype.add = function SourceNode_add(aChunk) {
173  if (Array.isArray(aChunk)) {
174    aChunk.forEach(function (chunk) {
175      this.add(chunk);
176    }, this);
177  }
178  else if (aChunk[isSourceNode] || typeof aChunk === "string") {
179    if (aChunk) {
180      this.children.push(aChunk);
181    }
182  }
183  else {
184    throw new TypeError(
185      "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
186    );
187  }
188  return this;
189};
190
191/**
192 * Add a chunk of generated JS to the beginning of this source node.
193 *
194 * @param aChunk A string snippet of generated JS code, another instance of
195 *        SourceNode, or an array where each member is one of those things.
196 */
197SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
198  if (Array.isArray(aChunk)) {
199    for (var i = aChunk.length-1; i >= 0; i--) {
200      this.prepend(aChunk[i]);
201    }
202  }
203  else if (aChunk[isSourceNode] || typeof aChunk === "string") {
204    this.children.unshift(aChunk);
205  }
206  else {
207    throw new TypeError(
208      "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
209    );
210  }
211  return this;
212};
213
214/**
215 * Walk over the tree of JS snippets in this node and its children. The
216 * walking function is called once for each snippet of JS and is passed that
217 * snippet and the its original associated source's line/column location.
218 *
219 * @param aFn The traversal function.
220 */
221SourceNode.prototype.walk = function SourceNode_walk(aFn) {
222  var chunk;
223  for (var i = 0, len = this.children.length; i < len; i++) {
224    chunk = this.children[i];
225    if (chunk[isSourceNode]) {
226      chunk.walk(aFn);
227    }
228    else {
229      if (chunk !== '') {
230        aFn(chunk, { source: this.source,
231                     line: this.line,
232                     column: this.column,
233                     name: this.name });
234      }
235    }
236  }
237};
238
239/**
240 * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
241 * each of `this.children`.
242 *
243 * @param aSep The separator.
244 */
245SourceNode.prototype.join = function SourceNode_join(aSep) {
246  var newChildren;
247  var i;
248  var len = this.children.length;
249  if (len > 0) {
250    newChildren = [];
251    for (i = 0; i < len-1; i++) {
252      newChildren.push(this.children[i]);
253      newChildren.push(aSep);
254    }
255    newChildren.push(this.children[i]);
256    this.children = newChildren;
257  }
258  return this;
259};
260
261/**
262 * Call String.prototype.replace on the very right-most source snippet. Useful
263 * for trimming whitespace from the end of a source node, etc.
264 *
265 * @param aPattern The pattern to replace.
266 * @param aReplacement The thing to replace the pattern with.
267 */
268SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
269  var lastChild = this.children[this.children.length - 1];
270  if (lastChild[isSourceNode]) {
271    lastChild.replaceRight(aPattern, aReplacement);
272  }
273  else if (typeof lastChild === 'string') {
274    this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
275  }
276  else {
277    this.children.push(''.replace(aPattern, aReplacement));
278  }
279  return this;
280};
281
282/**
283 * Set the source content for a source file. This will be added to the SourceMapGenerator
284 * in the sourcesContent field.
285 *
286 * @param aSourceFile The filename of the source file
287 * @param aSourceContent The content of the source file
288 */
289SourceNode.prototype.setSourceContent =
290  function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
291    this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
292  };
293
294/**
295 * Walk over the tree of SourceNodes. The walking function is called for each
296 * source file content and is passed the filename and source content.
297 *
298 * @param aFn The traversal function.
299 */
300SourceNode.prototype.walkSourceContents =
301  function SourceNode_walkSourceContents(aFn) {
302    for (var i = 0, len = this.children.length; i < len; i++) {
303      if (this.children[i][isSourceNode]) {
304        this.children[i].walkSourceContents(aFn);
305      }
306    }
307
308    var sources = Object.keys(this.sourceContents);
309    for (var i = 0, len = sources.length; i < len; i++) {
310      aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
311    }
312  };
313
314/**
315 * Return the string representation of this source node. Walks over the tree
316 * and concatenates all the various snippets together to one string.
317 */
318SourceNode.prototype.toString = function SourceNode_toString() {
319  var str = "";
320  this.walk(function (chunk) {
321    str += chunk;
322  });
323  return str;
324};
325
326/**
327 * Returns the string representation of this source node along with a source
328 * map.
329 */
330SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
331  var generated = {
332    code: "",
333    line: 1,
334    column: 0
335  };
336  var map = new SourceMapGenerator(aArgs);
337  var sourceMappingActive = false;
338  var lastOriginalSource = null;
339  var lastOriginalLine = null;
340  var lastOriginalColumn = null;
341  var lastOriginalName = null;
342  this.walk(function (chunk, original) {
343    generated.code += chunk;
344    if (original.source !== null
345        && original.line !== null
346        && original.column !== null) {
347      if(lastOriginalSource !== original.source
348         || lastOriginalLine !== original.line
349         || lastOriginalColumn !== original.column
350         || lastOriginalName !== original.name) {
351        map.addMapping({
352          source: original.source,
353          original: {
354            line: original.line,
355            column: original.column
356          },
357          generated: {
358            line: generated.line,
359            column: generated.column
360          },
361          name: original.name
362        });
363      }
364      lastOriginalSource = original.source;
365      lastOriginalLine = original.line;
366      lastOriginalColumn = original.column;
367      lastOriginalName = original.name;
368      sourceMappingActive = true;
369    } else if (sourceMappingActive) {
370      map.addMapping({
371        generated: {
372          line: generated.line,
373          column: generated.column
374        }
375      });
376      lastOriginalSource = null;
377      sourceMappingActive = false;
378    }
379    for (var idx = 0, length = chunk.length; idx < length; idx++) {
380      if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
381        generated.line++;
382        generated.column = 0;
383        // Mappings end at eol
384        if (idx + 1 === length) {
385          lastOriginalSource = null;
386          sourceMappingActive = false;
387        } else if (sourceMappingActive) {
388          map.addMapping({
389            source: original.source,
390            original: {
391              line: original.line,
392              column: original.column
393            },
394            generated: {
395              line: generated.line,
396              column: generated.column
397            },
398            name: original.name
399          });
400        }
401      } else {
402        generated.column++;
403      }
404    }
405  });
406  this.walkSourceContents(function (sourceFile, sourceContent) {
407    map.setSourceContent(sourceFile, sourceContent);
408  });
409
410  return { code: generated.code, map: map };
411};
412
413exports.SourceNode = SourceNode;
414