You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			414 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			414 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											9 months ago
										 | /* -*- 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; |