/* -*- 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 */ if (typeof define !== 'function') { var define = require('amdefine')(module); } define(function (require, exports, module) { var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator; var util = require('./util'); /** * 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 === undefined ? null : aLine; this.column = aColumn === undefined ? null : aColumn; this.source = aSource === undefined ? null : aSource; this.name = aName === undefined ? null : aName; 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 */ SourceNode.fromStringWithSourceMap = function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer) { // The SourceNode we want to fill with the generated code // and the SourceMap var node = new SourceNode(); // The generated code // Processed fragments are removed from this array. var remainingLines = aGeneratedCode.split('\n'); // 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 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(remainingLines.shift() + "\n"); lastGeneratedLine++; } if (lastGeneratedColumn < mapping.generatedColumn) { var nextLine = remainingLines[0]; node.add(nextLine.substr(0, mapping.generatedColumn)); remainingLines[0] = nextLine.substr(mapping.generatedColumn); lastGeneratedColumn = mapping.generatedColumn; } } else { // We add the code from "lastMapping" to "mapping": // First check if there is a new line in between. if (lastGeneratedLine < mapping.generatedLine) { var code = ""; // Associate full lines with "lastMapping" do { code += remainingLines.shift() + "\n"; lastGeneratedLine++; lastGeneratedColumn = 0; } while (lastGeneratedLine < mapping.generatedLine); // When we reached the correct line, we add code until we // reach the correct column too. if (lastGeneratedColumn < mapping.generatedColumn) { var nextLine = remainingLines[0]; code += nextLine.substr(0, mapping.generatedColumn); remainingLines[0] = nextLine.substr(mapping.generatedColumn); lastGeneratedColumn = mapping.generatedColumn; } // Create the SourceNode. addMappingWithCode(lastMapping, code); } else { // There is no new line in between. // Associate the code between "lastGeneratedColumn" and // "mapping.generatedColumn" with "lastMapping" var nextLine = remainingLines[0]; var code = nextLine.substr(0, mapping.generatedColumn - lastGeneratedColumn); remainingLines[0] = nextLine.substr(mapping.generatedColumn - lastGeneratedColumn); lastGeneratedColumn = mapping.generatedColumn; addMappingWithCode(lastMapping, code); } } lastMapping = mapping; }, this); // We have processed all mappings. // Associate the remaining code in the current line with "lastMapping" // and add the remaining lines without any mapping addMappingWithCode(lastMapping, remainingLines.join("\n")); // Copy sourcesContent into SourceNode aSourceMapConsumer.sources.forEach(function (sourceFile) { var content = aSourceMapConsumer.sourceContentFor(sourceFile); if (content) { node.setSourceContent(sourceFile, content); } }); return node; function addMappingWithCode(mapping, code) { if (mapping.source === undefined) { node.add(code); } else { node.add(new SourceNode(mapping.originalLine, mapping.originalColumn, mapping.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 instanceof SourceNode || 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 instanceof SourceNode || 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) { this.children.forEach(function (chunk) { if (chunk instanceof SourceNode) { chunk.walk(aFn); } else { if (chunk !== '') { aFn(chunk, { source: this.source, line: this.line, column: this.column, name: this.name }); } } }, this); }; /** * 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 instanceof SourceNode) { 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) { this.children.forEach(function (chunk) { if (chunk instanceof SourceNode) { chunk.walkSourceContents(aFn); } }, this); Object.keys(this.sourceContents).forEach(function (sourceFileKey) { aFn(util.fromSetString(sourceFileKey), this.sourceContents[sourceFileKey]); }, this); }; /** * 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; this.walk(function (chunk, original) { generated.code += chunk; if (original.source !== null && original.line !== null && original.column !== null) { map.addMapping({ source: original.source, original: { line: original.line, column: original.column }, generated: { line: generated.line, column: generated.column }, name: original.name }); sourceMappingActive = true; } else if (sourceMappingActive) { map.addMapping({ generated: { line: generated.line, column: generated.column } }); sourceMappingActive = false; } chunk.split('').forEach(function (ch) { if (ch === '\n') { generated.line++; generated.column = 0; } else { generated.column++; } }); }); this.walkSourceContents(function (sourceFile, sourceContent) { map.setSourceContent(sourceFile, sourceContent); }); return { code: generated.code, map: map }; }; exports.SourceNode = SourceNode; });