You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

467 lines
12 KiB

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