This commit is contained in:
Kevin McIntyre
2025-06-18 01:00:00 -04:00
commit f84b511895
228 changed files with 42509 additions and 0 deletions

View File

@@ -0,0 +1,672 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
const { Transform } = require("stream");
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
/**
* Finds substrings and replaces them with other strings, keeping any input source map up-to-date.
*
* @example
* const replacement = new Replacement([
* ["find this", "replace with this"],
* [/find this/gi, "replace with this"]
* ]);
* replacement.replace(input, inputSourceMap);
*
* @example
* const replacement = new Replacement("find this", "replace with this");
* replacement.replace(input, inputSourceMap);
*/
class Replacement {
constructor(...definition) {
/**
* @type {function(Array<OverwriteRange>, string): void}
*/
this._matchers = [];
/**
* @type {Array<OverwriteRange>}
*/
this._ranges = [];
this.add(definition);
}
/**
* @param {string} input
* @returns {Array<OverwriteRange>}
*/
matchAll(input) {
const ranges = [...this._ranges];
this._matchers.forEach(matcher => matcher(ranges, input));
let lastReplacement;
return ranges
.sort((a, b) => a.start - b.start)
.filter(replacement => {
if (!lastReplacement || lastReplacement.end <= replacement.start) {
lastReplacement = replacement;
return true;
}
});
}
/**
* @param {string} input
* @param {SourceMap=} inputSourceMap
* @returns {{ output: string, sourceMap: SourceMap }}
*/
replace(input, inputSourceMap) {
const ranges = this.matchAll(input);
const offset = new Offset();
const reader = new InputReader(input);
const sourceMap = new SourceMapSpooler(inputSourceMap);
const output = [];
if (sourceMap.isEmpty()) {
sourceMap.initEmpty(reader.lines);
}
ranges.forEach((range, rangeIndex) => {
output.push(reader.readTo(range.start));
output.push(range.replacement);
const inputStart = reader.pos;
const replacedText = reader.readTo(range.end);
if (replacedText === range.replacement) {
return; // Nothing to do
}
const inputEnd = reader.pos;
const replacementLines = range.replacement.split(/\n/g);
const lineDifference = replacementLines.length + inputStart.line - inputEnd.line - 1;
const outputStart = {
line: inputStart.line + offset.lineOffset,
column: inputStart.column + offset.getColumnOffset(inputStart.line)
};
const outputEnd = {
line: inputEnd.line + offset.lineOffset + lineDifference,
column: replacementLines.length > 1 ?
replacementLines[replacementLines.length - 1].length :
inputStart.column + offset.getColumnOffset(inputStart.line) +
range.replacement.length
}
sourceMap.spoolTo(inputStart.line, inputStart.column, offset);
offset.lineOffset += lineDifference;
offset.setColumnOffset(inputEnd.line, outputEnd.column - inputEnd.column);
if (range.name || replacementLines.length === 1 && range.replacement) {
const mappingBeforeStart = sourceMap.lastMapping();
const mappingAfterStart = sourceMap.nextMapping();
if (mappingAfterStart &&
mappingAfterStart.generatedLine === inputStart.line &&
mappingAfterStart.generatedColumn === inputStart.column
) {
sourceMap.addMapping({
original: {
line: mappingAfterStart.originalLine,
column: mappingAfterStart.originalColumn,
},
generated: {
line: outputStart.line,
column: outputStart.column
},
source: mappingAfterStart.source,
name: range.name,
});
} else if (mappingBeforeStart && mappingBeforeStart.generatedLine === inputStart.line) {
sourceMap.addMapping({
original: {
line: mappingBeforeStart.originalLine + inputStart.line - mappingBeforeStart.generatedLine,
column: mappingBeforeStart.originalColumn + inputStart.column - mappingBeforeStart.generatedColumn,
},
generated: {
line: outputStart.line,
column: outputStart.column
},
source: mappingBeforeStart.source,
name: range.name,
});
}
} else if (range.replacement) {
// Map longer replacements to a virtual file defined in the source map
const generatedSourceName = sourceMap.addSourceContent(replacedText, range.replacement);
for (var i = 0; i < replacementLines.length; i++) {
// Don't map empty lines
if (replacementLines[i]) {
sourceMap.addMapping({
original: {
line: i + 1,
column: 0,
},
generated: {
line: outputStart.line + i,
column: i ? 0 : outputStart.column,
},
source: generatedSourceName,
});
}
}
}
sourceMap.skipTo(inputEnd.line, inputEnd.column, offset);
// Add a source map node directly after the replacement to terminate the replacement
const mappingAfterEnd = sourceMap.nextMapping();
const mappingBeforeEnd = sourceMap.lastMapping();
if (mappingAfterEnd &&
mappingAfterEnd.generatedLine === inputEnd.line &&
mappingAfterEnd.generatedColumn === inputEnd.column
) {
// No extra source map node needed when the replacement is directly followed by another node
} else if (rangeIndex + 1 < ranges.length && range.end === ranges[rangeIndex + 1].start) {
// The next replacement range is adjacent to this one
} else if (reader.endOfLine()) {
// End of line, no point in adding a following node
} else if (!mappingBeforeEnd || mappingBeforeEnd.generatedLine !== inputEnd.line) {
// No applicable preceding node found
} else {
sourceMap.addMapping({
original: {
line: mappingBeforeEnd.originalLine + inputEnd.line - mappingBeforeEnd.generatedLine,
column: mappingBeforeEnd.originalColumn + inputEnd.column - mappingBeforeEnd.generatedColumn,
},
generated: {
line: outputEnd.line,
column: outputEnd.column
},
source: mappingBeforeEnd.source,
});
}
});
// Flush remaining input to output and source map
output.push(reader.readToEnd());
sourceMap.spoolToEnd(offset);
return {
output: output.join(""),
sourceMap: sourceMap.toJSON(),
};
}
add(value) {
const target = this;
function addRecursive(innerValue) {
if (innerValue != null) {
if (Array.isArray(innerValue)) {
const needle = innerValue[0];
if (typeof needle === "string") {
target.addText(...innerValue);
} else if (needle instanceof RegExp) {
target.addRegExp(...innerValue);
} else {
innerValue.forEach(addRecursive);
}
} else if (innerValue instanceof Replacement) {
target._matchers.push(innerValue._matchers);
target._ranges.push(innerValue._ranges);
} else if (typeof innerValue === "object") {
target.addRange(innerValue);
} else {
throw new Error("Unknown replacement argument specified.");
}
}
}
addRecursive(value);
}
/**
* @param {RegExp} re
* @param {string|function(string, ...):string} replacement
* @param {{ name: string }=} rangeOpts
*/
addRegExp(re, replacement, rangeOpts) {
const replacementFactory = this._createReplacementFactory(replacement);
this._matchers.push((ranges, input) => {
const isGlobalRegExp = /g/.test(re.flags);
let match;
let isFirstIteration = true;
while ((isFirstIteration || isGlobalRegExp) && (match = re.exec(input))) {
ranges.push(new OverwriteRange({
...rangeOpts,
start: match.index,
end: match.index + match[0].length,
replacement: replacementFactory(match, match.index, input),
}));
isFirstIteration = false;
}
});
}
/**
* @param {string} needle
* @param {string|function(string, ...):string} replacement
* @param {{ name: string }=} rangeOpts
*/
addText(needle, replacement, rangeOpts) {
const replacementFactory = this._createReplacementFactory(replacement);
this._matchers.push((ranges, input) => {
let index = -needle.length;
while ((index = input.indexOf(needle, index + needle.length)) >= 0) {
ranges.push(new OverwriteRange({
...rangeOpts,
start: index,
end: index + needle.length,
replacement: replacementFactory([needle], index, input),
}));
}
});
}
/**
* @param {OverwriteRange} range
*/
addRange(range) {
this._ranges.push(new OverwriteRange(range));
}
/**
* @param {string|function(string, ...):string} replacement
* @returns {function(Array<string>, number, string):string}
*/
_createReplacementFactory(replacement) {
if (typeof replacement === "function") {
return (match, index, input) => replacement(...match, index, input);
}
if (replacement == null) {
return () => "";
}
replacement = replacement.toString();
if (replacement.indexOf("$") < 0) {
return () => replacement;
}
return (match, index, input) =>
replacement.replace(/\$(\d+|[$&`'])/g, matchedPattern => {
if (matchedPattern === "$$") {
return "$";
}
if (matchedPattern === "$&") {
return match[0];
}
if (matchedPattern === "$`") {
return input.substring(0, index);
}
if (matchedPattern === "$'") {
return input.substring(index + match[0].length);
}
const matchArrayIndex = Number(matchedPattern.substring(1));
return match[matchArrayIndex];
});
}
}
class InputReader {
/**
* @param {string} input
*/
constructor (input) {
// Find index of all line breaks
const lineBreakIndexes = [];
let index = -1;
while ((index = input.indexOf("\n", index + 1)) >= 0) {
lineBreakIndexes.push(index);
}
this._input = input;
this._inputCursorExclusive = 0;
this._output = [];
this._lineBreakIndexes = lineBreakIndexes;
/**
* Number of lines in the input file.
* @type {number}
*/
this.lines = this._lineBreakIndexes.length + 1;
/**
* Position of the input cursor. Line number is one-based and column number is zero-based.
* @type {{ line: number, column: number }}
*/
this.pos = { line: 1, column: 0 };
}
readTo(exclusiveIndex) {
let result = "";
if (this._inputCursorExclusive < exclusiveIndex) {
result = this._input.substring(this._inputCursorExclusive, exclusiveIndex);
this._inputCursorExclusive = exclusiveIndex;
this._updatePos();
}
return result;
}
readToEnd() {
return this.readTo(this._input.length);
}
endOfLine() {
const nextChar = this._input[this._inputCursorExclusive];
return !nextChar || nextChar === "\r" || nextChar === "\n";
}
_updatePos() {
let line = this.pos.line;
while (
line - 1 < this._lineBreakIndexes.length &&
this._lineBreakIndexes[line - 1] < this._inputCursorExclusive
) {
line++;
}
const lineStartIndex = this._lineBreakIndexes[line - 2];
const column = this._inputCursorExclusive - (lineStartIndex || -1) - 1;
this.pos = { line, column };
}
}
class SourceMapSpooler {
/**
* @param {SourceMap=} inputSourceMap
*/
constructor(inputSourceMap) {
let generator;
let file;
let sources;
let mappings = [];
if (inputSourceMap) {
if (!(inputSourceMap instanceof SourceMapConsumer)) {
inputSourceMap = new SourceMapConsumer(inputSourceMap);
}
generator = new SourceMapGenerator({
file: inputSourceMap.file,
sourceRoot: inputSourceMap.sourceRoot,
});
inputSourceMap.sources.forEach(function(sourceFile) {
const content = inputSourceMap.sourceContentFor(sourceFile);
if (content != null) {
generator.setSourceContent(sourceFile, content);
}
});
inputSourceMap.eachMapping(mapping => {
mappings.push(mapping);
});
mappings.sort((a, b) => a.generatedLine == b.generatedLine
? a.generatedColumn - b.generatedColumn : a.generatedLine - b.generatedLine);
file = inputSourceMap.file;
sources = inputSourceMap.sources;
} else {
generator = new SourceMapGenerator();
file = "input";
sources = [];
}
this._generator = generator;
this._sources = new Set(sources);
this._file = file;
this._mappingsCursor = 0;
this._mappings = mappings;
this._contents = new Map();
}
lastMapping() {
return this._mappings[this._mappingsCursor - 1];
}
nextMapping() {
return this._mappings[this._mappingsCursor];
}
addMapping(mapping) {
this._generator.addMapping(mapping);
}
isEmpty() {
return this._mappings.length === 0;
}
initEmpty(lines) {
this._mappings = [];
for (var i = 0; i < lines; i++) {
this._mappings.push({
originalLine: i + 1,
originalColumn: 0,
generatedLine: i + 1,
generatedColumn: 0,
source: this._file
});
}
}
addSourceContent(replacedText, content) {
let sourceName = this._contents.get(content);
if (!sourceName) {
const PREFIX = "replacement/";
let sourceNameWithoutNumber = PREFIX;
sourceName = sourceNameWithoutNumber + "1";
if (replacedText.length > 0 && replacedText.length < 25) {
replacedText = replacedText
.replace(/^[^0-9a-z-_]+|[^0-9a-z-_]+$/ig, "")
.replace(/\s+/g, "-")
.replace(/[^0-9a-z-_]/ig, "");
if (replacedText) {
sourceNameWithoutNumber = PREFIX + replacedText + "-";
sourceName = PREFIX + replacedText;
}
}
let counter = 2;
while (this._sources.has(sourceName)) {
sourceName = sourceNameWithoutNumber + counter++;
}
this._sources.add(sourceName);
this._contents.set(content, sourceName);
this._generator.setSourceContent(sourceName, content);
}
return sourceName;
}
/**
* Copies source map info from input to output up to but not including the specified position.
* @param {number} line
* @param {number} column
* @param {Offset} offset
*/
spoolTo(line, column, offset) {
this._consume(line, column, offset, true);
}
/**
* Copies remaining source map info from input to output.
* @param {number} line
* @param {number} column
* @param {Offset} offset
*/
spoolToEnd(offset) {
this._consume(null, null, offset, true);
}
/**
* Discards source map info from input up to but not including the specified position.
* @param {number} line
* @param {number} column
* @param {Offset} offset
*/
skipTo(line, column, offset) {
this._consume(line, column, offset, false);
}
toJSON() {
return this._generator.toJSON();
}
_consume(line, column, offset, keep) {
let mapping;
while (
(mapping = this._mappings[this._mappingsCursor]) &&
(
line == null ||
mapping.generatedLine < line ||
mapping.generatedLine == line && mapping.generatedColumn < column
)
) {
if (keep) {
this._generator.addMapping({
original: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
generated: {
line: mapping.generatedLine + offset.lineOffset,
column: mapping.generatedColumn + offset.getColumnOffset(mapping.generatedLine),
},
source: mapping.source,
name: mapping.name,
});
}
this._mappingsCursor++;
}
}
}
class Offset {
constructor() {
this.lineOffset = 0;
this._columnOffset = 0;
this._columnOffsetForLine = 0;
}
setColumnOffset(lineNumber, columnOffset) {
this._columnOffsetForLine = lineNumber;
this._columnOffset = columnOffset;
}
getColumnOffset(lineNumber) {
return this._columnOffsetForLine === lineNumber ?
this._columnOffset : 0;
}
}
class OverwriteRange {
constructor(options) {
if (!isFinite(options.start)) {
throw new Error("A replacement start index is required.");
}
if (!isFinite(options.end)) {
throw new Error("A replacement end index is required.");
}
if (options.end < options.start) {
throw new Error("Replacement end index cannot precede its start index.");
}
/**
* Inclusive start index.
* @type {number}
*/
this.start = options.start;
/**
* Exclusive start index.
* @type {number}
*/
this.end = options.end;
/**
* The replacement interval will be replaced with this string.
* @type string
*/
this.replacement = "" + options.replacement;
/**
* Optional name that will be mapped in the source map.
* @type string
*/
this.name = options.name;
}
}
function gulp(replacements) {
if (typeof replacements === "string" || replacements instanceof RegExp) {
replacements = Array.from(arguments);
}
const replacer = new Replacement(replacements);
return new Transform({
objectMode: true,
transform(inputFile, _, fileDone) {
const input = inputFile.contents.toString();
const output = replacer.replace(input, inputFile.sourceMap);
inputFile.contents = Buffer.from(output.output);
if (inputFile.sourceMap) {
inputFile.sourceMap = output.sourceMap;
}
fileDone(null, inputFile);
}
});
}
module.exports = { Replacement, gulp };