You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
413 lines
14 KiB
413 lines
14 KiB
/* -*- 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 |
|
*/ |
|
|
|
var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator; |
|
var 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). |
|
var REGEX_NEWLINE = /(\r?\n)/; |
|
|
|
// Newline character code for charCodeAt() comparisons |
|
var 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! |
|
var 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. |
|
*/ |
|
function SourceNode(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. |
|
*/ |
|
SourceNode.fromStringWithSourceMap = |
|
function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) { |
|
// The SourceNode we want to fill with the generated code |
|
// and the SourceMap |
|
var 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`. |
|
var remainingLines = aGeneratedCode.split(REGEX_NEWLINE); |
|
var remainingLinesIndex = 0; |
|
var shiftNextLine = function() { |
|
var lineContents = getNextLine(); |
|
// The last line of a file might not have a newline. |
|
var newLine = getNextLine() || ""; |
|
return lineContents + newLine; |
|
|
|
function getNextLine() { |
|
return remainingLinesIndex < remainingLines.length ? |
|
remainingLines[remainingLinesIndex++] : undefined; |
|
} |
|
}; |
|
|
|
// We need to remember the position of "remainingLines" |
|
var 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. |
|
var lastMapping = null; |
|
|
|
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" |
|
var nextLine = remainingLines[remainingLinesIndex]; |
|
var 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) { |
|
var 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) { |
|
var 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 { |
|
var 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. |
|
*/ |
|
SourceNode.prototype.add = function SourceNode_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. |
|
*/ |
|
SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) { |
|
if (Array.isArray(aChunk)) { |
|
for (var 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. |
|
*/ |
|
SourceNode.prototype.walk = function SourceNode_walk(aFn) { |
|
var chunk; |
|
for (var 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. |
|
*/ |
|
SourceNode.prototype.join = function SourceNode_join(aSep) { |
|
var newChildren; |
|
var i; |
|
var 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. |
|
*/ |
|
SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) { |
|
var 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 |
|
*/ |
|
SourceNode.prototype.setSourceContent = |
|
function SourceNode_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. |
|
*/ |
|
SourceNode.prototype.walkSourceContents = |
|
function SourceNode_walkSourceContents(aFn) { |
|
for (var i = 0, len = this.children.length; i < len; i++) { |
|
if (this.children[i][isSourceNode]) { |
|
this.children[i].walkSourceContents(aFn); |
|
} |
|
} |
|
|
|
var sources = Object.keys(this.sourceContents); |
|
for (var 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. |
|
*/ |
|
SourceNode.prototype.toString = function SourceNode_toString() { |
|
var str = ""; |
|
this.walk(function (chunk) { |
|
str += chunk; |
|
}); |
|
return str; |
|
}; |
|
|
|
/** |
|
* Returns the string representation of this source node along with a source |
|
* map. |
|
*/ |
|
SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) { |
|
var generated = { |
|
code: "", |
|
line: 1, |
|
column: 0 |
|
}; |
|
var map = new SourceMapGenerator(aArgs); |
|
var sourceMappingActive = false; |
|
var lastOriginalSource = null; |
|
var lastOriginalLine = null; |
|
var lastOriginalColumn = null; |
|
var 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 (var 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: map }; |
|
}; |
|
|
|
exports.SourceNode = SourceNode;
|
|
|