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