tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

source-node.js (13793B)


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