source-node.js
  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  
  8  var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
  9  var 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).
 13  var REGEX_NEWLINE = /(\r?\n)/;
 14  
 15  // Newline character code for charCodeAt() comparisons
 16  var 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!
 21  var 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   */
 35  function 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   */
 54  SourceNode.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   */
172  SourceNode.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   */
197  SourceNode.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   */
221  SourceNode.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   */
245  SourceNode.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   */
268  SourceNode.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   */
289  SourceNode.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   */
300  SourceNode.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   */
318  SourceNode.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   */
330  SourceNode.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  
413  exports.SourceNode = SourceNode;