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