407 lines
10 KiB
JavaScript
407 lines
10 KiB
JavaScript
|
/*
|
||
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
||
|
Author Tobias Koppers @sokra
|
||
|
*/
|
||
|
"use strict";
|
||
|
|
||
|
const Source = require("./Source");
|
||
|
const { SourceNode } = require("source-map");
|
||
|
const { getSourceAndMap, getMap, getNode, getListMap } = require("./helpers");
|
||
|
|
||
|
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 = [];
|
||
|
this._isSorted = true;
|
||
|
}
|
||
|
|
||
|
getName() {
|
||
|
return this._name;
|
||
|
}
|
||
|
|
||
|
getReplacements() {
|
||
|
const replacements = Array.from(this._replacements);
|
||
|
replacements.sort((a, b) => {
|
||
|
return a.insertIndex - b.insertIndex;
|
||
|
});
|
||
|
return 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)
|
||
|
);
|
||
|
this._isSorted = false;
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
);
|
||
|
this._isSorted = false;
|
||
|
}
|
||
|
|
||
|
source() {
|
||
|
return this._replaceString(this._source.source());
|
||
|
}
|
||
|
|
||
|
map(options) {
|
||
|
if (this._replacements.length === 0) {
|
||
|
return this._source.map(options);
|
||
|
}
|
||
|
return getMap(this, options);
|
||
|
}
|
||
|
|
||
|
sourceAndMap(options) {
|
||
|
if (this._replacements.length === 0) {
|
||
|
return this._source.sourceAndMap(options);
|
||
|
}
|
||
|
return getSourceAndMap(this, options);
|
||
|
}
|
||
|
|
||
|
original() {
|
||
|
return this._source;
|
||
|
}
|
||
|
|
||
|
_sortReplacements() {
|
||
|
if (this._isSorted) return;
|
||
|
this._replacements.sort(function (a, b) {
|
||
|
const diff1 = b.end - a.end;
|
||
|
if (diff1 !== 0) return diff1;
|
||
|
const diff2 = b.start - a.start;
|
||
|
if (diff2 !== 0) return diff2;
|
||
|
return b.insertIndex - a.insertIndex;
|
||
|
});
|
||
|
this._isSorted = true;
|
||
|
}
|
||
|
|
||
|
_replaceString(str) {
|
||
|
if (typeof str !== "string")
|
||
|
throw new Error(
|
||
|
"str must be a string, but is a " + typeof str + ": " + str
|
||
|
);
|
||
|
this._sortReplacements();
|
||
|
const result = [str];
|
||
|
this._replacements.forEach(function (repl) {
|
||
|
const remSource = result.pop();
|
||
|
const splitted1 = this._splitString(remSource, Math.floor(repl.end + 1));
|
||
|
const 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) {
|
||
|
const node = getNode(this._source, options);
|
||
|
if (this._replacements.length === 0) {
|
||
|
return node;
|
||
|
}
|
||
|
this._sortReplacements();
|
||
|
const replace = new ReplacementEnumerator(this._replacements);
|
||
|
const output = [];
|
||
|
let position = 0;
|
||
|
const sources = Object.create(null);
|
||
|
const sourcesInLines = Object.create(null);
|
||
|
|
||
|
// We build a new list of SourceNodes in "output"
|
||
|
// from the original mapping data
|
||
|
|
||
|
const 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;
|
||
|
});
|
||
|
|
||
|
const replaceInStringNode = this._replaceInStringNode.bind(
|
||
|
this,
|
||
|
output,
|
||
|
replace,
|
||
|
function getOriginalSource(mapping) {
|
||
|
const key = "$" + mapping.source;
|
||
|
let lines = sourcesInLines[key];
|
||
|
if (!lines) {
|
||
|
const 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;
|
||
|
const 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
|
||
|
const remaining = replace.footer();
|
||
|
if (remaining) {
|
||
|
output.push(remaining);
|
||
|
}
|
||
|
|
||
|
result.add(output);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
listMap(options) {
|
||
|
let map = getListMap(this._source, options);
|
||
|
this._sortReplacements();
|
||
|
let currentIndex = 0;
|
||
|
const replacements = this._replacements;
|
||
|
let idxReplacement = replacements.length - 1;
|
||
|
let removeChars = 0;
|
||
|
map = map.mapGeneratedCode(function (str) {
|
||
|
const newCurrentIndex = currentIndex + str.length;
|
||
|
if (removeChars > str.length) {
|
||
|
removeChars -= str.length;
|
||
|
str = "";
|
||
|
} else {
|
||
|
if (removeChars > 0) {
|
||
|
str = str.substr(removeChars);
|
||
|
currentIndex += removeChars;
|
||
|
removeChars = 0;
|
||
|
}
|
||
|
let finalStr = "";
|
||
|
while (
|
||
|
idxReplacement >= 0 &&
|
||
|
replacements[idxReplacement].start < newCurrentIndex
|
||
|
) {
|
||
|
const repl = replacements[idxReplacement];
|
||
|
const start = Math.floor(repl.start);
|
||
|
const end = Math.floor(repl.end + 1);
|
||
|
const before = str.substr(0, Math.max(0, start - currentIndex));
|
||
|
if (end <= newCurrentIndex) {
|
||
|
const 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;
|
||
|
});
|
||
|
let 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
|
||
|
) {
|
||
|
let original = undefined;
|
||
|
|
||
|
do {
|
||
|
let 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) {
|
||
|
const nodeEnd = new SourceNode(
|
||
|
mapping.line,
|
||
|
mapping.column,
|
||
|
mapping.source,
|
||
|
node,
|
||
|
mapping.name
|
||
|
);
|
||
|
output.push(nodeEnd);
|
||
|
}
|
||
|
return position + node.length;
|
||
|
}
|
||
|
|
||
|
const 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
|
||
|
|
||
|
let 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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const 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) {
|
||
|
const 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;
|
||
|
// eslint-disable-next-line no-constant-condition
|
||
|
} while (true);
|
||
|
}
|
||
|
|
||
|
updateHash(hash) {
|
||
|
this._sortReplacements();
|
||
|
hash.update("ReplaceSource");
|
||
|
this._source.updateHash(hash);
|
||
|
hash.update(this._name || "");
|
||
|
for (const repl of this._replacements) {
|
||
|
hash.update(`${repl.start}`);
|
||
|
hash.update(`${repl.end}`);
|
||
|
hash.update(`${repl.content}`);
|
||
|
hash.update(`${repl.insertIndex}`);
|
||
|
hash.update(`${repl.name}`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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
|
||
|
const repl = this.replacements[this.index];
|
||
|
const 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 {
|
||
|
const nextRepl = this.replacements[this.index];
|
||
|
const 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 {
|
||
|
let resultStr = "";
|
||
|
for (let i = this.index; i >= 0; i--) {
|
||
|
const 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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = ReplaceSource;
|