|
|
/* 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;
|