update-file-content
Version:
A simple utility for executing RegEx replacement on files, powered by stream.
281 lines (241 loc) • 8.56 kB
JavaScript
import { process_stream, rw_stream } from "./process.mjs";
import { Readable, Writable } from "stream";
import { WriteStream } from "fs";
function _getReplaceFunc ( options ) {
let replace = [];
let global_limit = 0;
let global_counter = 0;
if(options.search && "replacement" in options) { // will be validated in replace.map
replace.push({
search: options.search,
replacement: options.replacement,
limit: options.limit
});
} else if(validate(options.limit, 1)) {
global_limit = options.limit;
}
if(validate(options.replace, Array)) // will be validated in replace.map
replace = replace.concat(options.replace);
let join;
if("join" in options) {
const join_option = options.join; // for garbage collection
switch(typeof join_option) {
case "function":
join = options.join;
break;
case "string":
join = part => part.concat(join_option);
break;
case "undefined":
join = part => part;
break;
default: throw new TypeError(
"update-file-content: options.join "
+ String(options.join)
+ " is invalid."
)
}
} else {
join = part => part;
}
const callback = (part, EOF) => {
if(typeof part !== "string") return ""; // "Adbfdbdafb".split(/(?=([^,\n]+(,\n)?|(,\n)))/)
replace.forEach(rule => {
part = part.replace(
rule.pattern,
rule.replacement
);
});
return EOF ? part : join(part);
};
/**/ const _nuke_ = () => callback._nuke_(); /**/
replace = replace.map(({search, replacement, full_replacement, limit}) => {
if(!is(search, RegExp, "") || !is(replacement, Function, ""))
throw new TypeError("update-file-content: !is(search, RegExp, \"\") || !is(replacement, Function, \"\")");
let rule;
if(typeof search === "string") {
full_replacement = true; // must be
const escapeRegEx = new RegExp(
"(" + "[]\^$.|?*+(){}".split("").map(c => "\\".concat(c)).join("|") + ")",
"g"
);
search = {
source: search.replace(escapeRegEx, "\\$1"),
flags: "g"
};
if(typeof replacement === "string") {
const temp_str = replacement;
replacement = () => temp_str;
} // make sure replacement is a funciton so that limitation can be applied
}
/**
* Set the global flag to ensure the search pattern is "stateful",
* while preserving flags the original search pattern.
*/
let flags = search.flags;
if (!flags.includes("g"))
flags = "g".concat(flags);
if(full_replacement || typeof replacement === "function" || /(?<!\\)\$.+/.test(replacement)) {
rule = {
pattern: new RegExp (search.source, flags),
replacement: replacement
}
} else { // Replace the 1st parenthesized substring match with replacement.
rule = {
pattern:
new RegExp (
search.source // add parentheses for matching substrings exactly,
.replace(/(.*?)\((.*)\)(.*)/, "($1)($2)$3"),
flags
),
replacement:
(match_whole, prefix, match_substr) =>
match_whole.replace(
prefix.concat(match_substr),
prefix.concat(replacement)
) // using prefix as a hook
}
}
// limit
if(validate(limit, 1) || global_limit) {
if(typeof rule.replacement === "function") {
let counter = 0;
const func_ptr = rule.replacement;
rule.replacement = function (_nuke_, ...args) {
if(
( global_limit && ++global_counter >= global_limit )
|| ++counter >= limit
)
if(_nuke_() === Symbol.for("nuked"))
return args[0]; // return the whole unmodified match string
return func_ptr.apply(this, args);
}.bind(rule, _nuke_);
callback.with_limit = true;
callback.truncate = options.truncate;
} else {
throw new TypeError("update-file-content: received non-function full replacement "
+ rule.replacement
+ " while limit being specified");
}
}
return rule;
});
return callback;
}
async function updateFileContent( options ) {
const callback = _getReplaceFunc(options);
const separator = "separator" in options ? options.separator : /(?=\r?\n)/; // NOTE
const encoding = options.encoding || "utf8";
const truncate = "truncate" in options ? options.truncate : false;
if("file" in options) {
if(validate(options.file, "."))
return rw_stream (
options.file,
{
separator,
callback,
encoding,
truncate
}
);
else throw new TypeError("updateFileContent: options.file is invalid.")
} else {
const readStream = options.readStream || options.from;
const writeStream = options.writeStream || options.to;
if(validate(readStream, Readable) && validate(writeStream, WriteStream))
return process_stream (
readStream,
writeStream,
{
separator,
callback,
encoding,
truncate
}
);
else throw new TypeError("updateFileContent: options.(readStream|writeStream|from|to) is invalid.")
}
}
async function updateFiles ( options ) {
const callback = _getReplaceFunc(options);
const separator = "separator" in options ? options.separator : /(?=\r?\n)/;
const encoding = options.encoding || "utf8";
const truncate = "truncate" in options ? options.truncate : false;
if(validate(options.files, Array) && validate(...options.files, ".")) {
return Promise.all(
options.file.map(file =>
rw_stream (
file,
{
separator,
callback,
encoding,
truncate
}
)
)
);
} else {
const readStream = options.readStream || options.from;
const dests = options.writeStream || options.to;
// superset Readable instead of ReadStream
if(validate(readStream, Readable) && validate(dests, Array)) {
if(validate(...dests, WriteStream)) {
return process_stream (
readStream,
new Writable({
async write (chunk, encoding, cb) {
await Promise.all(
dests.map(writeStream =>
new Promise((resolve, reject) => {
writeStream.write(chunk, encoding, resolve)
}) // .write should never return false
)
);
return cb();
},
destroy (err, cb) {
dests.forEach(
writeStream => writeStream.destroy(err)
);
return cb();
},
autoDestroy: true, // Default: true.
final (cb) {
dests.forEach(
writeStream => writeStream.end()
);
return cb();
}
}),
{
separator,
callback,
encoding,
truncate
}
)
} else {
throw new TypeError("updateFiles: options.(writeStream|to) is not an instance of Array<WriteStream>");
}
}
throw new Error("updateFiles: incorrect options.");
}
}
function is (toValidate, ...types) {
return types.some(type => validate(toValidate, type));
}
function validate (...args) {
const should_be = args.splice(args.length - 1, 1)[0];
if(should_be === Array)
return args.every(arg => Array.isArray(arg) && arg.length);
const type = typeof should_be;
switch (type) {
case "function": return args.every(arg => arg instanceof should_be);
case "object": return args.every(arg => typeof arg === "object" && arg.constructor === should_be.constructor);
case "string": return args.every(arg => typeof arg === "string" && arg.length >= should_be.length);
case "number": return args.every(arg => typeof arg === "number" && arg >= should_be);
default: return args.every(arg => typeof arg === type);
}
}
export { updateFileContent, updateFiles };