|
|
- /* -*- Mode: js; js-indent-level: 2; -*- */
- /*
- * Copyright 2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE or:
- * http://opensource.org/licenses/BSD-3-Clause
- */
-
- const SourceMapGenerator = require("./source-map-generator").SourceMapGenerator;
- const util = require("./util");
-
- // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
- // operating systems these days (capturing the result).
- const REGEX_NEWLINE = /(\r?\n)/;
-
- // Newline character code for charCodeAt() comparisons
- const NEWLINE_CODE = 10;
-
- // Private symbol for identifying `SourceNode`s when multiple versions of
- // the source-map library are loaded. This MUST NOT CHANGE across
- // versions!
- const isSourceNode = "$$$isSourceNode$$$";
-
- /**
- * SourceNodes provide a way to abstract over interpolating/concatenating
- * snippets of generated JavaScript source code while maintaining the line and
- * column information associated with the original source code.
- *
- * @param aLine The original line number.
- * @param aColumn The original column number.
- * @param aSource The original source's filename.
- * @param aChunks Optional. An array of strings which are snippets of
- * generated JS, or other SourceNodes.
- * @param aName The original identifier.
- */
- class SourceNode {
- constructor(aLine, aColumn, aSource, aChunks, aName) {
- this.children = [];
- this.sourceContents = {};
- this.line = aLine == null ? null : aLine;
- this.column = aColumn == null ? null : aColumn;
- this.source = aSource == null ? null : aSource;
- this.name = aName == null ? null : aName;
- this[isSourceNode] = true;
- if (aChunks != null) this.add(aChunks);
- }
-
- /**
- * Creates a SourceNode from generated code and a SourceMapConsumer.
- *
- * @param aGeneratedCode The generated code
- * @param aSourceMapConsumer The SourceMap for the generated code
- * @param aRelativePath Optional. The path that relative sources in the
- * SourceMapConsumer should be relative to.
- */
- static fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
- // The SourceNode we want to fill with the generated code
- // and the SourceMap
- const node = new SourceNode();
-
- // All even indices of this array are one line of the generated code,
- // while all odd indices are the newlines between two adjacent lines
- // (since `REGEX_NEWLINE` captures its match).
- // Processed fragments are accessed by calling `shiftNextLine`.
- const remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
- let remainingLinesIndex = 0;
- const shiftNextLine = function() {
- const lineContents = getNextLine();
- // The last line of a file might not have a newline.
- const newLine = getNextLine() || "";
- return lineContents + newLine;
-
- function getNextLine() {
- return remainingLinesIndex < remainingLines.length ?
- remainingLines[remainingLinesIndex++] : undefined;
- }
- };
-
- // We need to remember the position of "remainingLines"
- let lastGeneratedLine = 1, lastGeneratedColumn = 0;
-
- // The generate SourceNodes we need a code range.
- // To extract it current and last mapping is used.
- // Here we store the last mapping.
- let lastMapping = null;
- let nextLine;
-
- aSourceMapConsumer.eachMapping(function(mapping) {
- if (lastMapping !== null) {
- // We add the code from "lastMapping" to "mapping":
- // First check if there is a new line in between.
- if (lastGeneratedLine < mapping.generatedLine) {
- // Associate first line with "lastMapping"
- addMappingWithCode(lastMapping, shiftNextLine());
- lastGeneratedLine++;
- lastGeneratedColumn = 0;
- // The remaining code is added without mapping
- } else {
- // There is no new line in between.
- // Associate the code between "lastGeneratedColumn" and
- // "mapping.generatedColumn" with "lastMapping"
- nextLine = remainingLines[remainingLinesIndex] || "";
- const code = nextLine.substr(0, mapping.generatedColumn -
- lastGeneratedColumn);
- remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn -
- lastGeneratedColumn);
- lastGeneratedColumn = mapping.generatedColumn;
- addMappingWithCode(lastMapping, code);
- // No more remaining code, continue
- lastMapping = mapping;
- return;
- }
- }
- // We add the generated code until the first mapping
- // to the SourceNode without any mapping.
- // Each line is added as separate string.
- while (lastGeneratedLine < mapping.generatedLine) {
- node.add(shiftNextLine());
- lastGeneratedLine++;
- }
- if (lastGeneratedColumn < mapping.generatedColumn) {
- nextLine = remainingLines[remainingLinesIndex] || "";
- node.add(nextLine.substr(0, mapping.generatedColumn));
- remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
- lastGeneratedColumn = mapping.generatedColumn;
- }
- lastMapping = mapping;
- }, this);
- // We have processed all mappings.
- if (remainingLinesIndex < remainingLines.length) {
- if (lastMapping) {
- // Associate the remaining code in the current line with "lastMapping"
- addMappingWithCode(lastMapping, shiftNextLine());
- }
- // and add the remaining lines without any mapping
- node.add(remainingLines.splice(remainingLinesIndex).join(""));
- }
-
- // Copy sourcesContent into SourceNode
- aSourceMapConsumer.sources.forEach(function(sourceFile) {
- const content = aSourceMapConsumer.sourceContentFor(sourceFile);
- if (content != null) {
- if (aRelativePath != null) {
- sourceFile = util.join(aRelativePath, sourceFile);
- }
- node.setSourceContent(sourceFile, content);
- }
- });
-
- return node;
-
- function addMappingWithCode(mapping, code) {
- if (mapping === null || mapping.source === undefined) {
- node.add(code);
- } else {
- const source = aRelativePath
- ? util.join(aRelativePath, mapping.source)
- : mapping.source;
- node.add(new SourceNode(mapping.originalLine,
- mapping.originalColumn,
- source,
- code,
- mapping.name));
- }
- }
- }
-
- /**
- * Add a chunk of generated JS to this source node.
- *
- * @param aChunk A string snippet of generated JS code, another instance of
- * SourceNode, or an array where each member is one of those things.
- */
- add(aChunk) {
- if (Array.isArray(aChunk)) {
- aChunk.forEach(function(chunk) {
- this.add(chunk);
- }, this);
- } else if (aChunk[isSourceNode] || typeof aChunk === "string") {
- if (aChunk) {
- this.children.push(aChunk);
- }
- } else {
- throw new TypeError(
- "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
- );
- }
- return this;
- }
-
- /**
- * Add a chunk of generated JS to the beginning of this source node.
- *
- * @param aChunk A string snippet of generated JS code, another instance of
- * SourceNode, or an array where each member is one of those things.
- */
- prepend(aChunk) {
- if (Array.isArray(aChunk)) {
- for (let i = aChunk.length - 1; i >= 0; i--) {
- this.prepend(aChunk[i]);
- }
- } else if (aChunk[isSourceNode] || typeof aChunk === "string") {
- this.children.unshift(aChunk);
- } else {
- throw new TypeError(
- "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
- );
- }
- return this;
- }
-
- /**
- * Walk over the tree of JS snippets in this node and its children. The
- * walking function is called once for each snippet of JS and is passed that
- * snippet and the its original associated source's line/column location.
- *
- * @param aFn The traversal function.
- */
- walk(aFn) {
- let chunk;
- for (let i = 0, len = this.children.length; i < len; i++) {
- chunk = this.children[i];
- if (chunk[isSourceNode]) {
- chunk.walk(aFn);
- } else if (chunk !== "") {
- aFn(chunk, { source: this.source,
- line: this.line,
- column: this.column,
- name: this.name });
- }
- }
- }
-
- /**
- * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
- * each of `this.children`.
- *
- * @param aSep The separator.
- */
- join(aSep) {
- let newChildren;
- let i;
- const len = this.children.length;
- if (len > 0) {
- newChildren = [];
- for (i = 0; i < len - 1; i++) {
- newChildren.push(this.children[i]);
- newChildren.push(aSep);
- }
- newChildren.push(this.children[i]);
- this.children = newChildren;
- }
- return this;
- }
-
- /**
- * Call String.prototype.replace on the very right-most source snippet. Useful
- * for trimming whitespace from the end of a source node, etc.
- *
- * @param aPattern The pattern to replace.
- * @param aReplacement The thing to replace the pattern with.
- */
- replaceRight(aPattern, aReplacement) {
- const lastChild = this.children[this.children.length - 1];
- if (lastChild[isSourceNode]) {
- lastChild.replaceRight(aPattern, aReplacement);
- } else if (typeof lastChild === "string") {
- this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
- } else {
- this.children.push("".replace(aPattern, aReplacement));
- }
- return this;
- }
-
- /**
- * Set the source content for a source file. This will be added to the SourceMapGenerator
- * in the sourcesContent field.
- *
- * @param aSourceFile The filename of the source file
- * @param aSourceContent The content of the source file
- */
- setSourceContent(aSourceFile, aSourceContent) {
- this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
- }
-
- /**
- * Walk over the tree of SourceNodes. The walking function is called for each
- * source file content and is passed the filename and source content.
- *
- * @param aFn The traversal function.
- */
- walkSourceContents(aFn) {
- for (let i = 0, len = this.children.length; i < len; i++) {
- if (this.children[i][isSourceNode]) {
- this.children[i].walkSourceContents(aFn);
- }
- }
-
- const sources = Object.keys(this.sourceContents);
- for (let i = 0, len = sources.length; i < len; i++) {
- aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
- }
- }
-
- /**
- * Return the string representation of this source node. Walks over the tree
- * and concatenates all the various snippets together to one string.
- */
- toString() {
- let str = "";
- this.walk(function(chunk) {
- str += chunk;
- });
- return str;
- }
-
- /**
- * Returns the string representation of this source node along with a source
- * map.
- */
- toStringWithSourceMap(aArgs) {
- const generated = {
- code: "",
- line: 1,
- column: 0
- };
- const map = new SourceMapGenerator(aArgs);
- let sourceMappingActive = false;
- let lastOriginalSource = null;
- let lastOriginalLine = null;
- let lastOriginalColumn = null;
- let lastOriginalName = null;
- this.walk(function(chunk, original) {
- generated.code += chunk;
- if (original.source !== null
- && original.line !== null
- && original.column !== null) {
- if (lastOriginalSource !== original.source
- || lastOriginalLine !== original.line
- || lastOriginalColumn !== original.column
- || lastOriginalName !== original.name) {
- map.addMapping({
- source: original.source,
- original: {
- line: original.line,
- column: original.column
- },
- generated: {
- line: generated.line,
- column: generated.column
- },
- name: original.name
- });
- }
- lastOriginalSource = original.source;
- lastOriginalLine = original.line;
- lastOriginalColumn = original.column;
- lastOriginalName = original.name;
- sourceMappingActive = true;
- } else if (sourceMappingActive) {
- map.addMapping({
- generated: {
- line: generated.line,
- column: generated.column
- }
- });
- lastOriginalSource = null;
- sourceMappingActive = false;
- }
- for (let idx = 0, length = chunk.length; idx < length; idx++) {
- if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
- generated.line++;
- generated.column = 0;
- // Mappings end at eol
- if (idx + 1 === length) {
- lastOriginalSource = null;
- sourceMappingActive = false;
- } else if (sourceMappingActive) {
- map.addMapping({
- source: original.source,
- original: {
- line: original.line,
- column: original.column
- },
- generated: {
- line: generated.line,
- column: generated.column
- },
- name: original.name
- });
- }
- } else {
- generated.column++;
- }
- }
- });
- this.walkSourceContents(function(sourceFile, sourceContent) {
- map.setSourceContent(sourceFile, sourceContent);
- });
-
- return { code: generated.code, map };
- }
- }
-
- exports.SourceNode = SourceNode;
|