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