| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "use strict"; |
| |
| var Source = require("./Source"); |
| var SourceNode = require("source-map").SourceNode; |
| |
| class Replacement { |
| constructor(start, end, content, insertIndex, name) { |
| this.start = start; |
| this.end = end; |
| this.content = content; |
| this.insertIndex = insertIndex; |
| this.name = name; |
| } |
| } |
| |
| class ReplaceSource extends Source { |
| constructor(source, name) { |
| super(); |
| this._source = source; |
| this._name = name; |
| /** @type {Replacement[]} */ |
| this.replacements = []; |
| } |
| |
| replace(start, end, newValue, name) { |
| if(typeof newValue !== "string") |
| throw new Error("insertion must be a string, but is a " + typeof newValue); |
| this.replacements.push(new Replacement(start, end, newValue, this.replacements.length, name)); |
| } |
| |
| insert(pos, newValue, name) { |
| if(typeof newValue !== "string") |
| throw new Error("insertion must be a string, but is a " + typeof newValue + ": " + newValue); |
| this.replacements.push(new Replacement(pos, pos - 1, newValue, this.replacements.length, name)); |
| } |
| |
| source(options) { |
| return this._replaceString(this._source.source()); |
| } |
| |
| original() { |
| return this._source; |
| } |
| |
| _sortReplacements() { |
| this.replacements.sort(function(a, b) { |
| var diff = b.end - a.end; |
| if(diff !== 0) |
| return diff; |
| diff = b.start - a.start; |
| if(diff !== 0) |
| return diff; |
| return b.insertIndex - a.insertIndex; |
| }); |
| } |
| |
| _replaceString(str) { |
| if(typeof str !== "string") |
| throw new Error("str must be a string, but is a " + typeof str + ": " + str); |
| this._sortReplacements(); |
| var result = [str]; |
| this.replacements.forEach(function(repl) { |
| var remSource = result.pop(); |
| var splitted1 = this._splitString(remSource, Math.floor(repl.end + 1)); |
| var splitted2 = this._splitString(splitted1[0], Math.floor(repl.start)); |
| result.push(splitted1[1], repl.content, splitted2[0]); |
| }, this); |
| |
| // write out result array in reverse order |
| let resultStr = ""; |
| for(let i = result.length - 1; i >= 0; --i) { |
| resultStr += result[i]; |
| } |
| return resultStr; |
| } |
| |
| node(options) { |
| var node = this._source.node(options); |
| if(this.replacements.length === 0) { |
| return node; |
| } |
| this._sortReplacements(); |
| var replace = new ReplacementEnumerator(this.replacements); |
| var output = []; |
| var position = 0; |
| var sources = Object.create(null); |
| var sourcesInLines = Object.create(null); |
| |
| // We build a new list of SourceNodes in "output" |
| // from the original mapping data |
| |
| var result = new SourceNode(); |
| |
| // We need to add source contents manually |
| // because "walk" will not handle it |
| node.walkSourceContents(function(sourceFile, sourceContent) { |
| result.setSourceContent(sourceFile, sourceContent); |
| sources["$" + sourceFile] = sourceContent; |
| }); |
| |
| var replaceInStringNode = this._replaceInStringNode.bind(this, output, replace, function getOriginalSource(mapping) { |
| var key = "$" + mapping.source; |
| var lines = sourcesInLines[key]; |
| if(!lines) { |
| var source = sources[key]; |
| if(!source) return null; |
| lines = source.split("\n").map(function(line) { |
| return line + "\n"; |
| }); |
| sourcesInLines[key] = lines; |
| } |
| // line is 1-based |
| if(mapping.line > lines.length) return null; |
| var line = lines[mapping.line - 1]; |
| return line.substr(mapping.column); |
| }); |
| |
| node.walk(function(chunk, mapping) { |
| position = replaceInStringNode(chunk, position, mapping); |
| }); |
| |
| // If any replacements occur after the end of the original file, then we append them |
| // directly to the end of the output |
| var remaining = replace.footer(); |
| if(remaining) { |
| output.push(remaining); |
| } |
| |
| result.add(output); |
| |
| return result; |
| } |
| |
| listMap(options) { |
| this._sortReplacements(); |
| var map = this._source.listMap(options); |
| var currentIndex = 0; |
| var replacements = this.replacements; |
| var idxReplacement = replacements.length - 1; |
| var removeChars = 0; |
| map = map.mapGeneratedCode(function(str) { |
| var newCurrentIndex = currentIndex + str.length; |
| if(removeChars > str.length) { |
| removeChars -= str.length; |
| str = ""; |
| } else { |
| if(removeChars > 0) { |
| str = str.substr(removeChars); |
| currentIndex += removeChars; |
| removeChars = 0; |
| } |
| var finalStr = ""; |
| while(idxReplacement >= 0 && replacements[idxReplacement].start < newCurrentIndex) { |
| var repl = replacements[idxReplacement]; |
| var start = Math.floor(repl.start); |
| var end = Math.floor(repl.end + 1); |
| var before = str.substr(0, Math.max(0, start - currentIndex)); |
| if(end <= newCurrentIndex) { |
| var after = str.substr(Math.max(0, end - currentIndex)); |
| finalStr += before + repl.content; |
| str = after; |
| currentIndex = Math.max(currentIndex, end); |
| } else { |
| finalStr += before + repl.content; |
| str = ""; |
| removeChars = end - newCurrentIndex; |
| } |
| idxReplacement--; |
| } |
| str = finalStr + str; |
| } |
| currentIndex = newCurrentIndex; |
| return str; |
| }); |
| var extraCode = ""; |
| while(idxReplacement >= 0) { |
| extraCode += replacements[idxReplacement].content; |
| idxReplacement--; |
| } |
| if(extraCode) { |
| map.add(extraCode); |
| } |
| return map; |
| } |
| |
| _splitString(str, position) { |
| return position <= 0 ? ["", str] : [str.substr(0, position), str.substr(position)]; |
| } |
| |
| _replaceInStringNode(output, replace, getOriginalSource, node, position, mapping) { |
| var original = undefined; |
| |
| do { |
| var splitPosition = replace.position - position; |
| // If multiple replaces occur in the same location then the splitPosition may be |
| // before the current position for the subsequent splits. Ensure it is >= 0 |
| if(splitPosition < 0) { |
| splitPosition = 0; |
| } |
| if(splitPosition >= node.length || replace.done) { |
| if(replace.emit) { |
| var nodeEnd = new SourceNode( |
| mapping.line, |
| mapping.column, |
| mapping.source, |
| node, |
| mapping.name |
| ); |
| output.push(nodeEnd); |
| } |
| return position + node.length; |
| } |
| |
| var originalColumn = mapping.column; |
| |
| // Try to figure out if generated code matches original code of this segement |
| // If this is the case we assume that it's allowed to move mapping.column |
| // Because getOriginalSource can be expensive we only do it when neccessary |
| |
| var nodePart; |
| if(splitPosition > 0) { |
| nodePart = node.slice(0, splitPosition); |
| if(original === undefined) { |
| original = getOriginalSource(mapping); |
| } |
| if(original && original.length >= splitPosition && original.startsWith(nodePart)) { |
| mapping.column += splitPosition; |
| original = original.substr(splitPosition); |
| } |
| } |
| |
| var emit = replace.next(); |
| if(!emit) { |
| // Stop emitting when we have found the beginning of the string to replace. |
| // Emit the part of the string before splitPosition |
| if(splitPosition > 0) { |
| var nodeStart = new SourceNode( |
| mapping.line, |
| originalColumn, |
| mapping.source, |
| nodePart, |
| mapping.name |
| ); |
| output.push(nodeStart); |
| } |
| |
| // Emit the replacement value |
| if(replace.value) { |
| output.push(new SourceNode( |
| mapping.line, |
| mapping.column, |
| mapping.source, |
| replace.value, |
| mapping.name || replace.name |
| )); |
| } |
| } |
| |
| // Recurse with remainder of the string as there may be multiple replaces within a single node |
| node = node.substr(splitPosition); |
| position += splitPosition; |
| } while (true); |
| } |
| } |
| |
| class ReplacementEnumerator { |
| /** |
| * @param {Replacement[]} replacements list of replacements |
| */ |
| constructor(replacements) { |
| this.replacements = replacements || []; |
| this.index = this.replacements.length; |
| this.done = false; |
| this.emit = false; |
| // Set initial start position |
| this.next(); |
| } |
| |
| next() { |
| if(this.done) |
| return true; |
| if(this.emit) { |
| // Start point found. stop emitting. set position to find end |
| var repl = this.replacements[this.index]; |
| var end = Math.floor(repl.end + 1); |
| this.position = end; |
| this.value = repl.content; |
| this.name = repl.name; |
| } else { |
| // End point found. start emitting. set position to find next start |
| this.index--; |
| if(this.index < 0) { |
| this.done = true; |
| } else { |
| var nextRepl = this.replacements[this.index]; |
| var start = Math.floor(nextRepl.start); |
| this.position = start; |
| } |
| } |
| if(this.position < 0) |
| this.position = 0; |
| this.emit = !this.emit; |
| return this.emit; |
| } |
| |
| footer() { |
| if(!this.done && !this.emit) |
| this.next(); // If we finished _replaceInNode mid emit we advance to next entry |
| if(this.done) { |
| return []; |
| } else { |
| var resultStr = ""; |
| for(var i = this.index; i >= 0; i--) { |
| var repl = this.replacements[i]; |
| // this doesn't need to handle repl.name, because in SourceMaps generated code |
| // without pointer to original source can't have a name |
| resultStr += repl.content; |
| } |
| return resultStr; |
| } |
| } |
| } |
| |
| require("./SourceAndMapMixin")(ReplaceSource.prototype); |
| |
| module.exports = ReplaceSource; |