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