546 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			546 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/*
 | 
						|
	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
						|
	Author Tobias Koppers @sokra
 | 
						|
*/
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
const Source = require("./Source");
 | 
						|
const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
 | 
						|
const splitIntoLines = require("./helpers/splitIntoLines");
 | 
						|
const streamChunks = require("./helpers/streamChunks");
 | 
						|
 | 
						|
/** @typedef {import("./Source").HashLike} HashLike */
 | 
						|
/** @typedef {import("./Source").MapOptions} MapOptions */
 | 
						|
/** @typedef {import("./Source").RawSourceMap} RawSourceMap */
 | 
						|
/** @typedef {import("./Source").SourceAndMap} SourceAndMap */
 | 
						|
/** @typedef {import("./Source").SourceValue} SourceValue */
 | 
						|
/** @typedef {import("./helpers/getGeneratedSourceInfo").GeneratedSourceInfo} GeneratedSourceInfo */
 | 
						|
/** @typedef {import("./helpers/streamChunks").OnChunk} OnChunk */
 | 
						|
/** @typedef {import("./helpers/streamChunks").OnName} OnName */
 | 
						|
/** @typedef {import("./helpers/streamChunks").OnSource} OnSource */
 | 
						|
/** @typedef {import("./helpers/streamChunks").Options} Options */
 | 
						|
 | 
						|
// 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 {
 | 
						|
	/**
 | 
						|
	 * @param {number} start start
 | 
						|
	 * @param {number} end end
 | 
						|
	 * @param {string} content content
 | 
						|
	 * @param {string=} name name
 | 
						|
	 */
 | 
						|
	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 {
 | 
						|
	/**
 | 
						|
	 * @param {Source} source source
 | 
						|
	 * @param {string=} name name
 | 
						|
	 */
 | 
						|
	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;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {number} start start
 | 
						|
	 * @param {number} end end
 | 
						|
	 * @param {string} newValue new value
 | 
						|
	 * @param {string=} name name
 | 
						|
	 * @returns {void}
 | 
						|
	 */
 | 
						|
	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;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {number} pos pos
 | 
						|
	 * @param {string} newValue new value
 | 
						|
	 * @param {string=} name name
 | 
						|
	 * @returns {void}
 | 
						|
	 */
 | 
						|
	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;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @returns {SourceValue} source
 | 
						|
	 */
 | 
						|
	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("");
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {MapOptions=} options map options
 | 
						|
	 * @returns {RawSourceMap | null} map
 | 
						|
	 */
 | 
						|
	map(options) {
 | 
						|
		if (this._replacements.length === 0) {
 | 
						|
			return this._source.map(options);
 | 
						|
		}
 | 
						|
		return getMap(this, options);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {MapOptions=} options map options
 | 
						|
	 * @returns {SourceAndMap} source and map
 | 
						|
	 */
 | 
						|
	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((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 {
 | 
						|
			for (const [i, repl] of this._replacements.entries()) repl.index = i;
 | 
						|
			this._replacements.sort((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 (
 | 
						|
					/** @type {number} */ (a.index) - /** @type {number} */ (b.index)
 | 
						|
				);
 | 
						|
			});
 | 
						|
		}
 | 
						|
		this._isSorted = true;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {Options} options options
 | 
						|
	 * @param {OnChunk} onChunk called for each chunk of code
 | 
						|
	 * @param {OnSource} onSource called for each source
 | 
						|
	 * @param {OnName} onName called for each name
 | 
						|
	 * @returns {GeneratedSourceInfo} generated source info
 | 
						|
	 */
 | 
						|
	streamChunks(options, onChunk, onSource, onName) {
 | 
						|
		this._sortReplacements();
 | 
						|
		const replacements = this._replacements;
 | 
						|
		let pos = 0;
 | 
						|
		let i = 0;
 | 
						|
		let replacementEnd = -1;
 | 
						|
		let nextReplacement =
 | 
						|
			i < replacements.length
 | 
						|
				? Math.floor(replacements[i].start)
 | 
						|
				: MAX_SOURCE_POSITION;
 | 
						|
		let generatedLineOffset = 0;
 | 
						|
		let generatedColumnOffset = 0;
 | 
						|
		let generatedColumnOffsetLine = 0;
 | 
						|
		/** @type {(string | string[] | undefined)[]} */
 | 
						|
		const sourceContents = [];
 | 
						|
		/** @type {Map<string, number>} */
 | 
						|
		const nameMapping = new Map();
 | 
						|
		/** @type {number[]} */
 | 
						|
		const nameIndexMapping = [];
 | 
						|
		/**
 | 
						|
		 * @param {number} sourceIndex source index
 | 
						|
		 * @param {number} line line
 | 
						|
		 * @param {number} column column
 | 
						|
		 * @param {string} expectedChunk expected chunk
 | 
						|
		 * @returns {boolean} result
 | 
						|
		 */
 | 
						|
		const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
 | 
						|
			/** @type {undefined | string | string[]} */
 | 
						|
			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
 | 
						|
			);
 | 
						|
		};
 | 
						|
		const { generatedLine, generatedColumn } = streamChunks(
 | 
						|
			this._source,
 | 
						|
			{ ...options, finalSource: false },
 | 
						|
			(
 | 
						|
				_chunk,
 | 
						|
				generatedLine,
 | 
						|
				generatedColumn,
 | 
						|
				sourceIndex,
 | 
						|
				originalLine,
 | 
						|
				originalColumn,
 | 
						|
				nameIndex,
 | 
						|
			) => {
 | 
						|
				let chunkPos = 0;
 | 
						|
				const chunk = /** @type {string} */ (_chunk);
 | 
						|
				const endPos = pos + chunk.length;
 | 
						|
 | 
						|
				// Skip over when it has been replaced
 | 
						|
				if (replacementEnd > pos) {
 | 
						|
					// Skip over the whole chunk
 | 
						|
					if (replacementEnd >= 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 = replacementEnd - 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 } = replacements[i];
 | 
						|
						const 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
 | 
						|
						replacementEnd = Math.max(
 | 
						|
							replacementEnd,
 | 
						|
							Math.floor(replacements[i].end + 1),
 | 
						|
						);
 | 
						|
 | 
						|
						// Move to next replacement
 | 
						|
						i++;
 | 
						|
						nextReplacement =
 | 
						|
							i < replacements.length
 | 
						|
								? Math.floor(replacements[i].start)
 | 
						|
								: MAX_SOURCE_POSITION;
 | 
						|
 | 
						|
						// Skip over when it has been replaced
 | 
						|
						const offset = chunk.length - endPos + replacementEnd - chunkPos;
 | 
						|
						if (offset > 0) {
 | 
						|
							// Skip over whole chunk
 | 
						|
							if (replacementEnd >= 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 - 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 < replacements.length; i++) {
 | 
						|
			remainer += replacements[i].content;
 | 
						|
		}
 | 
						|
 | 
						|
		// Insert remaining replacements content splitted into chunks by lines
 | 
						|
		let line = /** @type {number} */ (generatedLine) + generatedLineOffset;
 | 
						|
		const matches = splitIntoLines(remainer);
 | 
						|
		for (let m = 0; m < matches.length; m++) {
 | 
						|
			const contentLine = matches[m];
 | 
						|
			onChunk(
 | 
						|
				contentLine,
 | 
						|
				line,
 | 
						|
				/** @type {number} */
 | 
						|
				(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 = -(/** @type {number} */ (generatedColumn));
 | 
						|
				generatedColumnOffsetLine = line;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return {
 | 
						|
			generatedLine: line,
 | 
						|
			generatedColumn:
 | 
						|
				/** @type {number} */
 | 
						|
				(generatedColumn) +
 | 
						|
				(line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
 | 
						|
		};
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @param {HashLike} hash hash
 | 
						|
	 * @returns {void}
 | 
						|
	 */
 | 
						|
	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 ? repl.name : ""}`,
 | 
						|
			);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
module.exports = ReplaceSource;
 | 
						|
module.exports.Replacement = Replacement;
 |