init
This commit is contained in:
672
jdenticon-js/build/gulp/replacement.js
Normal file
672
jdenticon-js/build/gulp/replacement.js
Normal 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 };
|
||||
Reference in New Issue
Block a user