468 lines
12 KiB
JavaScript
468 lines
12 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
"use strict";
|
|
|
|
const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
|
|
const streamChunks = require("./helpers/streamChunks");
|
|
const Source = require("./Source");
|
|
const splitIntoLines = require("./helpers/splitIntoLines");
|
|
|
|
// since v8 7.0, Array.prototype.sort is stable
|
|
const hasStableSort =
|
|
typeof process === "object" &&
|
|
process.versions &&
|
|
typeof process.versions.v8 === "string" &&
|
|
!/^[0-6]\./.test(process.versions.v8);
|
|
|
|
// This is larger than max string length
|
|
const MAX_SOURCE_POSITION = 0x20000000;
|
|
|
|
class Replacement {
|
|
constructor(start, end, content, name) {
|
|
this.start = start;
|
|
this.end = end;
|
|
this.content = content;
|
|
this.name = name;
|
|
if (!hasStableSort) {
|
|
this.index = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
this._sortReplacements();
|
|
return 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, 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, name));
|
|
this._isSorted = false;
|
|
}
|
|
|
|
source() {
|
|
if (this._replacements.length === 0) {
|
|
return this._source.source();
|
|
}
|
|
let current = this._source.source();
|
|
let pos = 0;
|
|
const result = [];
|
|
|
|
this._sortReplacements();
|
|
for (const replacement of this._replacements) {
|
|
const start = Math.floor(replacement.start);
|
|
const end = Math.floor(replacement.end + 1);
|
|
if (pos < start) {
|
|
const offset = start - pos;
|
|
result.push(current.slice(0, offset));
|
|
current = current.slice(offset);
|
|
pos = start;
|
|
}
|
|
result.push(replacement.content);
|
|
if (pos < end) {
|
|
const offset = end - pos;
|
|
current = current.slice(offset);
|
|
pos = end;
|
|
}
|
|
}
|
|
result.push(current);
|
|
return result.join("");
|
|
}
|
|
|
|
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;
|
|
if (hasStableSort) {
|
|
this._replacements.sort(function (a, b) {
|
|
const diff1 = a.start - b.start;
|
|
if (diff1 !== 0) return diff1;
|
|
const diff2 = a.end - b.end;
|
|
if (diff2 !== 0) return diff2;
|
|
return 0;
|
|
});
|
|
} else {
|
|
this._replacements.forEach((repl, i) => (repl.index = i));
|
|
this._replacements.sort(function (a, b) {
|
|
const diff1 = a.start - b.start;
|
|
if (diff1 !== 0) return diff1;
|
|
const diff2 = a.end - b.end;
|
|
if (diff2 !== 0) return diff2;
|
|
return a.index - b.index;
|
|
});
|
|
}
|
|
this._isSorted = true;
|
|
}
|
|
|
|
streamChunks(options, onChunk, onSource, onName) {
|
|
this._sortReplacements();
|
|
const repls = this._replacements;
|
|
let pos = 0;
|
|
let i = 0;
|
|
let replacmentEnd = -1;
|
|
let nextReplacement =
|
|
i < repls.length ? Math.floor(repls[i].start) : MAX_SOURCE_POSITION;
|
|
let generatedLineOffset = 0;
|
|
let generatedColumnOffset = 0;
|
|
let generatedColumnOffsetLine = 0;
|
|
const sourceContents = [];
|
|
const nameMapping = new Map();
|
|
const nameIndexMapping = [];
|
|
const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
|
|
let content =
|
|
sourceIndex < sourceContents.length
|
|
? sourceContents[sourceIndex]
|
|
: undefined;
|
|
if (content === undefined) return false;
|
|
if (typeof content === "string") {
|
|
content = splitIntoLines(content);
|
|
sourceContents[sourceIndex] = content;
|
|
}
|
|
const contentLine = line <= content.length ? content[line - 1] : null;
|
|
if (contentLine === null) return false;
|
|
return (
|
|
contentLine.slice(column, column + expectedChunk.length) ===
|
|
expectedChunk
|
|
);
|
|
};
|
|
let { generatedLine, generatedColumn } = streamChunks(
|
|
this._source,
|
|
Object.assign({}, options, { finalSource: false }),
|
|
(
|
|
chunk,
|
|
generatedLine,
|
|
generatedColumn,
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
nameIndex
|
|
) => {
|
|
let chunkPos = 0;
|
|
let endPos = pos + chunk.length;
|
|
|
|
// Skip over when it has been replaced
|
|
if (replacmentEnd > pos) {
|
|
// Skip over the whole chunk
|
|
if (replacmentEnd >= endPos) {
|
|
const line = generatedLine + generatedLineOffset;
|
|
if (chunk.endsWith("\n")) {
|
|
generatedLineOffset--;
|
|
if (generatedColumnOffsetLine === line) {
|
|
// undo exiting corrections form the current line
|
|
generatedColumnOffset += generatedColumn;
|
|
}
|
|
} else if (generatedColumnOffsetLine === line) {
|
|
generatedColumnOffset -= chunk.length;
|
|
} else {
|
|
generatedColumnOffset = -chunk.length;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
pos = endPos;
|
|
return;
|
|
}
|
|
|
|
// Partially skip over chunk
|
|
chunkPos = replacmentEnd - pos;
|
|
if (
|
|
checkOriginalContent(
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
chunk.slice(0, chunkPos)
|
|
)
|
|
) {
|
|
originalColumn += chunkPos;
|
|
}
|
|
pos += chunkPos;
|
|
const line = generatedLine + generatedLineOffset;
|
|
if (generatedColumnOffsetLine === line) {
|
|
generatedColumnOffset -= chunkPos;
|
|
} else {
|
|
generatedColumnOffset = -chunkPos;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
generatedColumn += chunkPos;
|
|
}
|
|
|
|
// Is a replacement in the chunk?
|
|
if (nextReplacement < endPos) {
|
|
do {
|
|
let line = generatedLine + generatedLineOffset;
|
|
if (nextReplacement > pos) {
|
|
// Emit chunk until replacement
|
|
const offset = nextReplacement - pos;
|
|
const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
|
|
onChunk(
|
|
chunkSlice,
|
|
line,
|
|
generatedColumn +
|
|
(line === generatedColumnOffsetLine
|
|
? generatedColumnOffset
|
|
: 0),
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
nameIndex < 0 || nameIndex >= nameIndexMapping.length
|
|
? -1
|
|
: nameIndexMapping[nameIndex]
|
|
);
|
|
generatedColumn += offset;
|
|
chunkPos += offset;
|
|
pos = nextReplacement;
|
|
if (
|
|
checkOriginalContent(
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
chunkSlice
|
|
)
|
|
) {
|
|
originalColumn += chunkSlice.length;
|
|
}
|
|
}
|
|
|
|
// Insert replacement content splitted into chunks by lines
|
|
const { content, name } = repls[i];
|
|
let matches = splitIntoLines(content);
|
|
let replacementNameIndex = nameIndex;
|
|
if (sourceIndex >= 0 && name) {
|
|
let globalIndex = nameMapping.get(name);
|
|
if (globalIndex === undefined) {
|
|
globalIndex = nameMapping.size;
|
|
nameMapping.set(name, globalIndex);
|
|
onName(globalIndex, name);
|
|
}
|
|
replacementNameIndex = globalIndex;
|
|
}
|
|
for (let m = 0; m < matches.length; m++) {
|
|
const contentLine = matches[m];
|
|
onChunk(
|
|
contentLine,
|
|
line,
|
|
generatedColumn +
|
|
(line === generatedColumnOffsetLine
|
|
? generatedColumnOffset
|
|
: 0),
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
replacementNameIndex
|
|
);
|
|
|
|
// Only the first chunk has name assigned
|
|
replacementNameIndex = -1;
|
|
|
|
if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
|
|
if (generatedColumnOffsetLine === line) {
|
|
generatedColumnOffset += contentLine.length;
|
|
} else {
|
|
generatedColumnOffset = contentLine.length;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
} else {
|
|
generatedLineOffset++;
|
|
line++;
|
|
generatedColumnOffset = -generatedColumn;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
}
|
|
|
|
// Remove replaced content by settings this variable
|
|
replacmentEnd = Math.max(
|
|
replacmentEnd,
|
|
Math.floor(repls[i].end + 1)
|
|
);
|
|
|
|
// Move to next replacment
|
|
i++;
|
|
nextReplacement =
|
|
i < repls.length
|
|
? Math.floor(repls[i].start)
|
|
: MAX_SOURCE_POSITION;
|
|
|
|
// Skip over when it has been replaced
|
|
const offset = chunk.length - endPos + replacmentEnd - chunkPos;
|
|
if (offset > 0) {
|
|
// Skip over whole chunk
|
|
if (replacmentEnd >= endPos) {
|
|
let line = generatedLine + generatedLineOffset;
|
|
if (chunk.endsWith("\n")) {
|
|
generatedLineOffset--;
|
|
if (generatedColumnOffsetLine === line) {
|
|
// undo exiting corrections form the current line
|
|
generatedColumnOffset += generatedColumn;
|
|
}
|
|
} else if (generatedColumnOffsetLine === line) {
|
|
generatedColumnOffset -= chunk.length - chunkPos;
|
|
} else {
|
|
generatedColumnOffset = chunkPos - chunk.length;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
pos = endPos;
|
|
return;
|
|
}
|
|
|
|
// Partially skip over chunk
|
|
const line = generatedLine + generatedLineOffset;
|
|
if (
|
|
checkOriginalContent(
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
chunk.slice(chunkPos, chunkPos + offset)
|
|
)
|
|
) {
|
|
originalColumn += offset;
|
|
}
|
|
chunkPos += offset;
|
|
pos += offset;
|
|
if (generatedColumnOffsetLine === line) {
|
|
generatedColumnOffset -= offset;
|
|
} else {
|
|
generatedColumnOffset = -offset;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
generatedColumn += offset;
|
|
}
|
|
} while (nextReplacement < endPos);
|
|
}
|
|
|
|
// Emit remaining chunk
|
|
if (chunkPos < chunk.length) {
|
|
const chunkSlice = chunkPos === 0 ? chunk : chunk.slice(chunkPos);
|
|
const line = generatedLine + generatedLineOffset;
|
|
onChunk(
|
|
chunkSlice,
|
|
line,
|
|
generatedColumn +
|
|
(line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
|
|
sourceIndex,
|
|
originalLine,
|
|
originalColumn,
|
|
nameIndex < 0 ? -1 : nameIndexMapping[nameIndex]
|
|
);
|
|
}
|
|
pos = endPos;
|
|
},
|
|
(sourceIndex, source, sourceContent) => {
|
|
while (sourceContents.length < sourceIndex)
|
|
sourceContents.push(undefined);
|
|
sourceContents[sourceIndex] = sourceContent;
|
|
onSource(sourceIndex, source, sourceContent);
|
|
},
|
|
(nameIndex, name) => {
|
|
let globalIndex = nameMapping.get(name);
|
|
if (globalIndex === undefined) {
|
|
globalIndex = nameMapping.size;
|
|
nameMapping.set(name, globalIndex);
|
|
onName(globalIndex, name);
|
|
}
|
|
nameIndexMapping[nameIndex] = globalIndex;
|
|
}
|
|
);
|
|
|
|
// Handle remaining replacements
|
|
let remainer = "";
|
|
for (; i < repls.length; i++) {
|
|
remainer += repls[i].content;
|
|
}
|
|
|
|
// Insert remaining replacements content splitted into chunks by lines
|
|
let line = generatedLine + generatedLineOffset;
|
|
let matches = splitIntoLines(remainer);
|
|
for (let m = 0; m < matches.length; m++) {
|
|
const contentLine = matches[m];
|
|
onChunk(
|
|
contentLine,
|
|
line,
|
|
generatedColumn +
|
|
(line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
|
|
-1,
|
|
-1,
|
|
-1,
|
|
-1
|
|
);
|
|
|
|
if (m === matches.length - 1 && !contentLine.endsWith("\n")) {
|
|
if (generatedColumnOffsetLine === line) {
|
|
generatedColumnOffset += contentLine.length;
|
|
} else {
|
|
generatedColumnOffset = contentLine.length;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
} else {
|
|
generatedLineOffset++;
|
|
line++;
|
|
generatedColumnOffset = -generatedColumn;
|
|
generatedColumnOffsetLine = line;
|
|
}
|
|
}
|
|
|
|
return {
|
|
generatedLine: line,
|
|
generatedColumn:
|
|
generatedColumn +
|
|
(line === generatedColumnOffsetLine ? generatedColumnOffset : 0)
|
|
};
|
|
}
|
|
|
|
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}${repl.end}${repl.content}${repl.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = ReplaceSource;
|