mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat(core/formatters): add replace("needle", "replaceValue") modifier for strings (#386)
fix(core/formatters): fix ordering of parsed variables (#386) Co-authored-by: David Garcia <dgarcia3@atlassian.com>
This commit is contained in:
+1
-1
@@ -33,4 +33,4 @@
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,10 +110,13 @@ export interface ParseValue {
|
||||
* Pre-compiled function that takes ParseValue and returns formatted string
|
||||
*/
|
||||
type CompiledParseFunction = (parseValue: ParseValue) => string;
|
||||
type CompiledVariableWInsertFn = { resultFn: (parseValue: ParseValue) => ResolvedVariable, insertIndex: number };
|
||||
type CompiledVariableWInsertFn = {
|
||||
resultFn: (parseValue: ParseValue) => ResolvedVariable;
|
||||
insertIndex: number;
|
||||
};
|
||||
/**
|
||||
* Pre-compiled function that takes ParseValue and returns `ResolvedVariable` (future: and the variable's context for caching purposes)
|
||||
*
|
||||
*
|
||||
* Retrieves the resolved variable (including modifiers) given a ParseValue (e.g. `stream.cached:istrue` -> `{result: true}` or `stream.languages::istrue` -> `{error: "unknown_array_modifier(istrue)"}`)
|
||||
*/
|
||||
type CompiledModifiedVariableFn = (parseValue: ParseValue) => ResolvedVariable;
|
||||
@@ -125,14 +128,16 @@ export abstract class BaseFormatter {
|
||||
private regexBuilder: BaseFormatterRegexBuilder;
|
||||
private precompiledNameFunction: CompiledParseFunction | null = null;
|
||||
private precompiledDescriptionFunction: CompiledParseFunction | null = null;
|
||||
|
||||
|
||||
private _compilationPromise: Promise<void>;
|
||||
|
||||
|
||||
constructor(config: FormatterConfig, userData: UserData) {
|
||||
this.config = config;
|
||||
this.userData = userData;
|
||||
|
||||
this.regexBuilder = new BaseFormatterRegexBuilder(this.convertStreamToParseValue({} as ParsedStream));
|
||||
this.regexBuilder = new BaseFormatterRegexBuilder(
|
||||
this.convertStreamToParseValue({} as ParsedStream)
|
||||
);
|
||||
|
||||
// Start template compilation asynchronously in the background
|
||||
this._compilationPromise = this.compileTemplatesAsync();
|
||||
@@ -140,17 +145,21 @@ export abstract class BaseFormatter {
|
||||
|
||||
private async compileTemplatesAsync(): Promise<void> {
|
||||
this.precompiledNameFunction = await this.compileTemplate(this.config.name);
|
||||
this.precompiledDescriptionFunction = await this.compileTemplate(this.config.description);
|
||||
this.precompiledDescriptionFunction = await this.compileTemplate(
|
||||
this.config.description
|
||||
);
|
||||
}
|
||||
|
||||
public async format(stream: ParsedStream): Promise<{ name: string; description: string }> {
|
||||
public async format(
|
||||
stream: ParsedStream
|
||||
): Promise<{ name: string; description: string }> {
|
||||
// Wait for template compilation to complete if it hasn't already
|
||||
await this._compilationPromise;
|
||||
|
||||
|
||||
if (!this.precompiledNameFunction || !this.precompiledDescriptionFunction) {
|
||||
throw new Error('Template compilation failed - formatter not ready');
|
||||
}
|
||||
|
||||
|
||||
const parseValue = this.convertStreamToParseValue(stream);
|
||||
return {
|
||||
name: this.precompiledNameFunction(parseValue),
|
||||
@@ -292,12 +301,15 @@ export abstract class BaseFormatter {
|
||||
parseValue.debug = {
|
||||
...DebugToolReplacementConstants,
|
||||
json: JSON.stringify({ ...parseValue, debug: undefined }),
|
||||
jsonf: JSON.stringify({ ...parseValue, debug: undefined }, (_, value) => value, 2),
|
||||
jsonf: JSON.stringify(
|
||||
{ ...parseValue, debug: undefined },
|
||||
(_, value) => value,
|
||||
2
|
||||
),
|
||||
};
|
||||
return parseValue;
|
||||
}
|
||||
|
||||
|
||||
protected async compileTemplate(str: string): Promise<CompiledParseFunction> {
|
||||
if (!str) return () => '';
|
||||
const re = this.regexBuilder.buildRegexExpression();
|
||||
@@ -306,84 +318,131 @@ export abstract class BaseFormatter {
|
||||
let compiledMatchTemplateFns: CompiledVariableWInsertFn[] = [];
|
||||
|
||||
for (const key in DebugToolReplacementConstants) {
|
||||
str = str.replace(`{debug.${key}}`, DebugToolReplacementConstants[key as keyof typeof DebugToolReplacementConstants]);
|
||||
str = str.replace(
|
||||
`{debug.${key}}`,
|
||||
DebugToolReplacementConstants[
|
||||
key as keyof typeof DebugToolReplacementConstants
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const placeHolder = " ";
|
||||
|
||||
// Iterate through all {...} matches
|
||||
while (matches = re.exec(str)) {
|
||||
while ((matches = re.exec(str))) {
|
||||
if (!matches.groups) continue;
|
||||
const index = matches.index as number;
|
||||
|
||||
// looks like variableType.propertyName(::<modifier|comparator>)* (no timezone or check)
|
||||
let matchWithoutSuffix = matches[0].substring(1, (matches[0].length-1) - (matches.groups.suffix ?? "").length);
|
||||
let matchWithoutSuffix = matches[0].substring(
|
||||
1,
|
||||
matches[0].length - 1 - (matches.groups.suffix ?? '').length
|
||||
);
|
||||
|
||||
// Split {<var1_with_modifiers>>::<comparator1>::<var2_with_modifiers>>...} into variableWithModifiers array and comparators array
|
||||
const splitOnComparators = matchWithoutSuffix.split(RegExp(this.regexBuilder.buildComparatorRegexPattern(), 'gi'));
|
||||
const variableWithModifiers = splitOnComparators.filter((_, i) => i % 2 == 0);
|
||||
const comparators = splitOnComparators.filter((_, i) => i % 2 != 0);
|
||||
const foundComparators = comparators.map(c => c as keyof typeof ComparatorConstants.comparatorKeyToFuncs);
|
||||
let precompiledResolvedVariableFns: CompiledModifiedVariableFn[] = variableWithModifiers
|
||||
.map(baseString => this.parseModifiedVariable(baseString, {
|
||||
mod_tzlocale: matches?.groups?.mod_tzlocale ?? undefined
|
||||
})
|
||||
const splitOnComparators = matchWithoutSuffix.split(
|
||||
RegExp(this.regexBuilder.buildComparatorRegexPattern(), 'gi')
|
||||
);
|
||||
|
||||
|
||||
// COMPARATOR logic: compare all ResolvedVariables against each other to make one ResolvedVariable (as precompiled wrapper function (parseValue) => ResolvedVariable)
|
||||
let precompiledResolvedVariableFn = (parseValue: ParseValue): ResolvedVariable => {
|
||||
if (precompiledResolvedVariableFns.length == 1) return precompiledResolvedVariableFns[0](parseValue);
|
||||
const variableWithModifiers = splitOnComparators.filter(
|
||||
(_, i) => i % 2 == 0
|
||||
);
|
||||
const comparators = splitOnComparators.filter((_, i) => i % 2 != 0);
|
||||
const foundComparators = comparators.map(
|
||||
(c) => c as keyof typeof ComparatorConstants.comparatorKeyToFuncs
|
||||
);
|
||||
let precompiledResolvedVariableFns: CompiledModifiedVariableFn[] =
|
||||
variableWithModifiers.map((baseString) =>
|
||||
this.parseModifiedVariable(baseString, {
|
||||
mod_tzlocale: matches?.groups?.mod_tzlocale ?? undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const resolvedVariablesWithContext = precompiledResolvedVariableFns.map(fn => fn(parseValue));
|
||||
const reducedResolvedVarWContext = resolvedVariablesWithContext.reduce((prev, cur, i) => {
|
||||
if (prev.error !== undefined) return prev;
|
||||
if (cur.error !== undefined) return cur;
|
||||
// the comparator key between prev and cur (from splitOnComparators)
|
||||
const compareKey = foundComparators[i - 1] as keyof typeof ComparatorConstants.comparatorKeyToFuncs;
|
||||
const comparatorFn = ComparatorConstants.comparatorKeyToFuncs[compareKey];
|
||||
|
||||
try {
|
||||
const result = comparatorFn(prev.result, cur.result);
|
||||
const finalResult = { result: result };
|
||||
return finalResult;
|
||||
} catch (e) {
|
||||
const errorResult = { error: `{unable_to_compare(<${prev.result}>::${compareKey}::<${cur.result}>, ${e})}` };
|
||||
return errorResult;
|
||||
// COMPARATOR logic: compare all ResolvedVariables against each other to make one ResolvedVariable (as precompiled wrapper function (parseValue) => ResolvedVariable)
|
||||
let precompiledResolvedVariableFn = (
|
||||
parseValue: ParseValue
|
||||
): ResolvedVariable => {
|
||||
if (precompiledResolvedVariableFns.length == 1)
|
||||
return precompiledResolvedVariableFns[0](parseValue);
|
||||
|
||||
const resolvedVariablesWithContext = precompiledResolvedVariableFns.map(
|
||||
(fn) => fn(parseValue)
|
||||
);
|
||||
const reducedResolvedVarWContext = resolvedVariablesWithContext.reduce(
|
||||
(prev, cur, i) => {
|
||||
if (prev.error !== undefined) return prev;
|
||||
if (cur.error !== undefined) return cur;
|
||||
// the comparator key between prev and cur (from splitOnComparators)
|
||||
const compareKey = foundComparators[
|
||||
i - 1
|
||||
] as keyof typeof ComparatorConstants.comparatorKeyToFuncs;
|
||||
const comparatorFn =
|
||||
ComparatorConstants.comparatorKeyToFuncs[compareKey];
|
||||
|
||||
try {
|
||||
const result = comparatorFn(prev.result, cur.result);
|
||||
const finalResult = { result: result };
|
||||
return finalResult;
|
||||
} catch (e) {
|
||||
const errorResult = {
|
||||
error: `{unable_to_compare(<${prev.result}>::${compareKey}::<${cur.result}>, ${e})}`,
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
return reducedResolvedVarWContext;
|
||||
}; // end of COMPARATOR logic
|
||||
|
||||
|
||||
// CHECK TRUE/FALSE logic: compile the true/false templates and apply them to the resolved variable
|
||||
if (matches.groups.mod_check !== undefined) {
|
||||
const check_trueFn = await this.compileTemplate(matches?.groups?.mod_check_true ?? "");
|
||||
const check_falseFn = await this.compileTemplate(matches?.groups?.mod_check_false ?? "");
|
||||
const check_trueFn = await this.compileTemplate(
|
||||
matches?.groups?.mod_check_true ?? ''
|
||||
);
|
||||
const check_falseFn = await this.compileTemplate(
|
||||
matches?.groups?.mod_check_false ?? ''
|
||||
);
|
||||
|
||||
const _compiledResolvedVariableFn = precompiledResolvedVariableFn;
|
||||
precompiledResolvedVariableFn = (parseValue: ParseValue): ResolvedVariable => {
|
||||
precompiledResolvedVariableFn = (
|
||||
parseValue: ParseValue
|
||||
): ResolvedVariable => {
|
||||
const resolved = _compiledResolvedVariableFn(parseValue);
|
||||
if (![true, false].includes(resolved.result)) {
|
||||
return { error: `{cannot_coerce_boolean_for_check_from(${resolved.result})}` };
|
||||
return {
|
||||
error: `{cannot_coerce_boolean_for_check_from(${resolved.result})}`,
|
||||
};
|
||||
}
|
||||
return { result: (resolved.result ? check_trueFn(parseValue) : check_falseFn(parseValue)) };
|
||||
return {
|
||||
result: resolved.result
|
||||
? check_trueFn(parseValue)
|
||||
: check_falseFn(parseValue),
|
||||
};
|
||||
};
|
||||
} // end of CHECK TRUE/FALSE logic
|
||||
|
||||
str = str.slice(0, index) + str.slice(re.lastIndex);
|
||||
re.lastIndex = index;
|
||||
compiledMatchTemplateFns.push({ resultFn: precompiledResolvedVariableFn, insertIndex: index });
|
||||
} // end of while loop
|
||||
|
||||
str = str.slice(0, index) +placeHolder+ str.slice(re.lastIndex);
|
||||
re.lastIndex = index+placeHolder.length;
|
||||
compiledMatchTemplateFns.push({
|
||||
resultFn: precompiledResolvedVariableFn,
|
||||
insertIndex: index,
|
||||
});
|
||||
} // end of while loop
|
||||
|
||||
compiledMatchTemplateFns = compiledMatchTemplateFns.sort((a, b) => (b.insertIndex - a.insertIndex ));
|
||||
return (parseValue: ParseValue) => {
|
||||
let resultStr = str;
|
||||
|
||||
|
||||
// Sort by startIndex to process in reverse order
|
||||
for (const { resultFn, insertIndex } of compiledMatchTemplateFns.sort((a, b) => b.insertIndex - a.insertIndex)) {
|
||||
const resolvedResult = resultFn(parseValue);
|
||||
const replacement = resolvedResult.error ?? resolvedResult.result?.toString() ?? '';
|
||||
resultStr = resultStr.slice(0, insertIndex) + replacement + resultStr.slice(insertIndex);
|
||||
for (const { resultFn, insertIndex } of compiledMatchTemplateFns) {
|
||||
const resolvedResult = resultFn(parseValue);
|
||||
const replacement =
|
||||
resolvedResult.error ?? resolvedResult.result?.toString() ?? '';
|
||||
resultStr =
|
||||
resultStr.slice(0, insertIndex) +
|
||||
replacement +
|
||||
resultStr.slice(insertIndex+placeHolder.length);
|
||||
}
|
||||
|
||||
|
||||
return resultStr
|
||||
.replace(/\\n/g, '\n')
|
||||
.split('\n')
|
||||
@@ -392,21 +451,21 @@ export abstract class BaseFormatter {
|
||||
)
|
||||
.join('\n')
|
||||
.replace(/\{tools.newLine\}/g, '\n');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param baseString - string to parse, e.g. `<variableType>.<propertyName>(::<modifier>)*`
|
||||
* @param value - ParseValue object
|
||||
* @param fullStringModifiers - modifiers that are applied to the entire string (e.g. `::<tzLocale>`)
|
||||
*
|
||||
*
|
||||
* @returns (parseValue) => `{ result: <resolved modified variable> }` or `{ error: "<errorMessage>" }`
|
||||
*/
|
||||
protected parseModifiedVariable(
|
||||
baseString: string,
|
||||
fullStringModifiers: {
|
||||
mod_tzlocale: string | undefined,
|
||||
},
|
||||
mod_tzlocale: string | undefined;
|
||||
}
|
||||
): CompiledModifiedVariableFn {
|
||||
// get variableType and propertyName from baseString without regex
|
||||
const variableType = baseString.split('.')[0];
|
||||
@@ -416,38 +475,63 @@ export abstract class BaseFormatter {
|
||||
let sortedModMatches: string[] = [];
|
||||
if (allModifiers.length) {
|
||||
const singleModTerminator = '(?=::|$)'; // :: if there's multiple modifiers, or $ for the end of the string
|
||||
const singleValidModRe = new RegExp(`${this.regexBuilder.buildModifierRegexPattern()}${singleModTerminator}`, 'g');
|
||||
|
||||
sortedModMatches = [...allModifiers.matchAll(singleValidModRe)].sort((a, b) => (a.index ?? 0) - (b.index ?? 0)).map(regExpExecArray => regExpExecArray[1] /* First capture group, aka the modifier name */);
|
||||
const singleValidModRe = new RegExp(
|
||||
`${this.regexBuilder.buildModifierRegexPattern()}${singleModTerminator}`,
|
||||
'g'
|
||||
);
|
||||
|
||||
sortedModMatches = [...allModifiers.matchAll(singleValidModRe)]
|
||||
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
||||
.map(
|
||||
(regExpExecArray) =>
|
||||
regExpExecArray[1] /* First capture group, aka the modifier name */
|
||||
);
|
||||
}
|
||||
|
||||
return (parseValue: ParseValue) => {
|
||||
// PARSE VARIABLE logic
|
||||
const variableDict = parseValue[variableType as keyof ParseValue];
|
||||
if (!variableDict) return { error: `{unknown_variableType(${variableType})}` }; // should never happen
|
||||
const property = variableDict![propertyName as keyof typeof variableDict] as any;
|
||||
if (property === undefined) return { error: `{unknown_propertyName(${variableType}.${propertyName})}` }; // should never happen
|
||||
if (!variableDict)
|
||||
return { error: `{unknown_variableType(${variableType})}` }; // should never happen
|
||||
const property = variableDict![
|
||||
propertyName as keyof typeof variableDict
|
||||
] as any;
|
||||
if (property === undefined)
|
||||
return {
|
||||
error: `{unknown_propertyName(${variableType}.${propertyName})}`,
|
||||
}; // should never happen
|
||||
// end of PARSE VARIABLE logic
|
||||
|
||||
// APPLY MULTIPLE MODIFIERS logic
|
||||
let result = property;
|
||||
for (const lastModMatched of sortedModMatches) {
|
||||
result = this.applySingleModifier(result, lastModMatched, fullStringModifiers);
|
||||
result = this.applySingleModifier(
|
||||
result,
|
||||
lastModMatched,
|
||||
fullStringModifiers
|
||||
);
|
||||
if (result === undefined) {
|
||||
let getErrorResult = () => {
|
||||
switch (typeof property) {
|
||||
case "string": case "number": case "boolean": return { error: `{unknown_${typeof property}_modifier(${lastModMatched})}` };
|
||||
case "object": return { error: `{unknown_array_modifier(${lastModMatched})}` };
|
||||
default: return { error: `{unknown_modifier(${lastModMatched})}` };
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'boolean':
|
||||
return {
|
||||
error: `{unknown_${typeof property}_modifier(${lastModMatched})}`,
|
||||
};
|
||||
case 'object':
|
||||
return { error: `{unknown_array_modifier(${lastModMatched})}` };
|
||||
default:
|
||||
return { error: `{unknown_modifier(${lastModMatched})}` };
|
||||
}
|
||||
}
|
||||
};
|
||||
return getErrorResult();
|
||||
}
|
||||
}
|
||||
// end of APPLY MULTIPLE MODIFIERS logic
|
||||
|
||||
return { result: result } as ResolvedVariable;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,50 +544,68 @@ export abstract class BaseFormatter {
|
||||
variable: any,
|
||||
mod: string,
|
||||
fullStringModifiers: {
|
||||
mod_tzlocale: string | undefined,
|
||||
},
|
||||
mod_tzlocale: string | undefined;
|
||||
}
|
||||
): string | boolean | undefined {
|
||||
const _mod = mod;
|
||||
mod = mod.toLowerCase();
|
||||
|
||||
// CONDITIONAL MODIFIERS
|
||||
const isExact = Object.keys(ModifierConstants.conditionalModifiers.exact).includes(mod);
|
||||
const isPrefix = Object.keys(ModifierConstants.conditionalModifiers.prefix).some(key => mod.startsWith(key));
|
||||
const isExact = Object.keys(
|
||||
ModifierConstants.conditionalModifiers.exact
|
||||
).includes(mod);
|
||||
const isPrefix = Object.keys(
|
||||
ModifierConstants.conditionalModifiers.prefix
|
||||
).some((key) => mod.startsWith(key));
|
||||
if (isExact || isPrefix) {
|
||||
// try to coerce true/false value from modifier
|
||||
let conditional: boolean | undefined;
|
||||
try {
|
||||
|
||||
// PRE-CHECK(s) -- skip resolving conditional modifier if value DNE, defaulting to false conditional
|
||||
if (!ModifierConstants.conditionalModifiers.exact.exists(variable)) {
|
||||
conditional = false;
|
||||
}
|
||||
|
||||
|
||||
// EXACT
|
||||
else if (isExact) {
|
||||
const modAsKey = mod as keyof typeof ModifierConstants.conditionalModifiers.exact;
|
||||
conditional = ModifierConstants.conditionalModifiers.exact[modAsKey](variable);
|
||||
const modAsKey =
|
||||
mod as keyof typeof ModifierConstants.conditionalModifiers.exact;
|
||||
conditional =
|
||||
ModifierConstants.conditionalModifiers.exact[modAsKey](variable);
|
||||
}
|
||||
|
||||
|
||||
// PREFIX
|
||||
else if (isPrefix) {
|
||||
// get the longest prefix match
|
||||
const modPrefix = Object.keys(ModifierConstants.conditionalModifiers.prefix).sort((a, b) => b.length - a.length).find(key => mod.startsWith(key))!!;
|
||||
|
||||
const modPrefix = Object.keys(
|
||||
ModifierConstants.conditionalModifiers.prefix
|
||||
)
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.find((key) => mod.startsWith(key))!!;
|
||||
|
||||
// Pre-process string value and check to allow for intuitive comparisons
|
||||
const stringValue = variable.toString().toLowerCase();
|
||||
let stringCheck = mod.substring(modPrefix.length).toLowerCase();
|
||||
// remove whitespace from stringCheck if it isn't in stringValue
|
||||
stringCheck = !/\s/.test(stringValue) ? stringCheck.replace(/\s/g, '') : stringCheck;
|
||||
|
||||
stringCheck = !/\s/.test(stringValue)
|
||||
? stringCheck.replace(/\s/g, '')
|
||||
: stringCheck;
|
||||
|
||||
// parse value/check as if they're numbers (123,456 -> 123456)
|
||||
const [parsedNumericValue, parsedNumericCheck] = [Number(stringValue.replace(/,\s/g, '')), Number(stringCheck.replace(/,\s/g, ''))];
|
||||
const isNumericComparison = ["<", "<=", ">", ">=", "="].includes(modPrefix) &&
|
||||
!isNaN(parsedNumericValue) && !isNaN(parsedNumericCheck);
|
||||
|
||||
conditional = ModifierConstants.conditionalModifiers.prefix[modPrefix as keyof typeof ModifierConstants.conditionalModifiers.prefix](
|
||||
isNumericComparison ? parsedNumericValue as any : stringValue,
|
||||
isNumericComparison ? parsedNumericCheck as any : stringCheck,
|
||||
const [parsedNumericValue, parsedNumericCheck] = [
|
||||
Number(stringValue.replace(/,\s/g, '')),
|
||||
Number(stringCheck.replace(/,\s/g, '')),
|
||||
];
|
||||
const isNumericComparison =
|
||||
['<', '<=', '>', '>=', '='].includes(modPrefix) &&
|
||||
!isNaN(parsedNumericValue) &&
|
||||
!isNaN(parsedNumericCheck);
|
||||
|
||||
conditional = ModifierConstants.conditionalModifiers.prefix[
|
||||
modPrefix as keyof typeof ModifierConstants.conditionalModifiers.prefix
|
||||
](
|
||||
isNumericComparison ? (parsedNumericValue as any) : stringValue,
|
||||
isNumericComparison ? (parsedNumericCheck as any) : stringCheck
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -515,19 +617,39 @@ export abstract class BaseFormatter {
|
||||
// --- STRING MODIFIERS ---
|
||||
else if (typeof variable === 'string') {
|
||||
if (mod in ModifierConstants.stringModifiers)
|
||||
return ModifierConstants.stringModifiers[mod as keyof typeof ModifierConstants.stringModifiers](variable);
|
||||
return ModifierConstants.stringModifiers[
|
||||
mod as keyof typeof ModifierConstants.stringModifiers
|
||||
](variable);
|
||||
|
||||
// handle hardcoded modifiers here
|
||||
switch (true) {
|
||||
case mod.startsWith('replace(') && mod.endsWith(')'): {
|
||||
const findStartChar = mod.charAt(8); // either " or '
|
||||
const findEndChar = mod.charAt(mod.length - 2); // either " or '
|
||||
|
||||
// Extract the separator from replace(['"]...<matching'">, ['"]...<matching'">)
|
||||
const content = _mod.substring(9, _mod.length - 2);
|
||||
|
||||
// split on findStartChar<whitespace?>,<whitespace?>findEndChar
|
||||
const [key, replaceKey, shouldBeUndefined] = content.split(new RegExp(`${findStartChar}\\s*,\\s*${findEndChar}`))
|
||||
|
||||
if (!shouldBeUndefined && key && replaceKey) return variable.replaceAll(key, replaceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ARRAY MODIFIERS ---
|
||||
else if (Array.isArray(variable)) {
|
||||
if (mod in ModifierConstants.arrayModifiers)
|
||||
return ModifierConstants.arrayModifiers[mod as keyof typeof ModifierConstants.arrayModifiers](variable)?.toString();
|
||||
return ModifierConstants.arrayModifiers[
|
||||
mod as keyof typeof ModifierConstants.arrayModifiers
|
||||
](variable)?.toString();
|
||||
|
||||
// handle hardcoded modifiers here
|
||||
switch (true) {
|
||||
case mod.startsWith('join(') && mod.endsWith(')'): {
|
||||
// Extract the separator from join('separator') or join("separator")
|
||||
const separator = _mod.substring(6, _mod.length - 2)
|
||||
const separator = _mod.substring(6, _mod.length - 2);
|
||||
return variable.join(separator);
|
||||
}
|
||||
}
|
||||
@@ -536,38 +658,44 @@ export abstract class BaseFormatter {
|
||||
// --- NUMBER MODIFIERS ---
|
||||
else if (typeof variable === 'number') {
|
||||
if (mod in ModifierConstants.numberModifiers)
|
||||
return ModifierConstants.numberModifiers[mod as keyof typeof ModifierConstants.numberModifiers](variable);
|
||||
return ModifierConstants.numberModifiers[
|
||||
mod as keyof typeof ModifierConstants.numberModifiers
|
||||
](variable);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to store the actual value of a parsed, and potentially modified, variable
|
||||
* or an error message if the parsed/modified result becomes invalid for any reason
|
||||
*/
|
||||
type ResolvedVariable = {
|
||||
result?: any,
|
||||
result?: any;
|
||||
error?: string | undefined;
|
||||
};
|
||||
|
||||
|
||||
class BaseFormatterRegexBuilder {
|
||||
private hardcodedParseValueKeysForRegexMatching: ParseValue;
|
||||
constructor(hardcodedParseValueKeysForRegexMatching: ParseValue) {
|
||||
this.hardcodedParseValueKeysForRegexMatching = hardcodedParseValueKeysForRegexMatching;
|
||||
this.hardcodedParseValueKeysForRegexMatching =
|
||||
hardcodedParseValueKeysForRegexMatching;
|
||||
}
|
||||
/**
|
||||
* RegEx Capture Pattern: `<variableType>.<propertyName>`
|
||||
*
|
||||
*
|
||||
* (no named capture group)
|
||||
*/
|
||||
public buildVariableRegexPattern(): string {
|
||||
// Get all valid variable names (keys as well as subkeys) from ParseValue structure
|
||||
const validVariableNames = Object.keys(this.hardcodedParseValueKeysForRegexMatching).flatMap(sectionKey => {
|
||||
const section = this.hardcodedParseValueKeysForRegexMatching[sectionKey as keyof ParseValue];
|
||||
const validVariableNames = Object.keys(
|
||||
this.hardcodedParseValueKeysForRegexMatching
|
||||
).flatMap((sectionKey) => {
|
||||
const section =
|
||||
this.hardcodedParseValueKeysForRegexMatching[
|
||||
sectionKey as keyof ParseValue
|
||||
];
|
||||
if (section && typeof section === 'object' && section !== null) {
|
||||
return Object.keys(section).map((key) => `${sectionKey}\\.${key}`);
|
||||
}
|
||||
@@ -577,26 +705,29 @@ class BaseFormatterRegexBuilder {
|
||||
}
|
||||
/**
|
||||
* RegEx Capture Pattern: `::<modifier>`
|
||||
*
|
||||
*
|
||||
* (no named capture group)
|
||||
*/
|
||||
public buildModifierRegexPattern(): string {
|
||||
const validModifiers = Object.keys(ModifierConstants.modifiers)
|
||||
.map(key => key.replace(/[\(\)\'\"\$\^\~\=\>\<]/g, '\\$&'));
|
||||
const validModifiers = Object.keys(ModifierConstants.modifiers).map((key) =>
|
||||
key.replace(/[\(\)\'\"\$\^\~\=\>\<]/g, '\\$&')
|
||||
);
|
||||
return `::(${validModifiers.join('|')})`;
|
||||
}
|
||||
/**
|
||||
* RegEx Capture Pattern: `::<comparator>::`
|
||||
*
|
||||
*
|
||||
* (no named capture group)
|
||||
*/
|
||||
public buildComparatorRegexPattern(): string {
|
||||
const comparatorKeys = Object.keys(ComparatorConstants.comparatorKeyToFuncs)
|
||||
return `::(${comparatorKeys.join("|")})::`
|
||||
const comparatorKeys = Object.keys(
|
||||
ComparatorConstants.comparatorKeyToFuncs
|
||||
);
|
||||
return `::(${comparatorKeys.join('|')})::`;
|
||||
}
|
||||
/**
|
||||
* RegEx Capture Pattern: `::<tzLocale>`
|
||||
*
|
||||
*
|
||||
* (with named capture group `tzLocale`)
|
||||
*/
|
||||
public buildTZLocaleRegexPattern(): string {
|
||||
@@ -605,7 +736,7 @@ class BaseFormatterRegexBuilder {
|
||||
}
|
||||
/**
|
||||
* RegEx Capture Pattern: `["<check_true>||<check_false>"]`
|
||||
*
|
||||
*
|
||||
* (with named capture group `<mod_check_true>` and `<mod_check_false>` and `mod_check`=`"<check_true>||<check_false>"`)
|
||||
*/
|
||||
public buildCheckRegexPattern(): string {
|
||||
@@ -624,10 +755,10 @@ class BaseFormatterRegexBuilder {
|
||||
const comparator = this.buildComparatorRegexPattern();
|
||||
const modTZLocale = this.buildTZLocaleRegexPattern();
|
||||
const checkTF = this.buildCheckRegexPattern();
|
||||
|
||||
|
||||
const variableAndModifiers = `${variable}(${modifier})*`;
|
||||
const regexPattern = `\\{${variableAndModifiers}(${comparator}${variableAndModifiers})*(?<suffix>(${modTZLocale})?(${checkTF})?)\\}`;
|
||||
|
||||
|
||||
return new RegExp(regexPattern, 'gi');
|
||||
}
|
||||
}
|
||||
@@ -637,47 +768,54 @@ class BaseFormatterRegexBuilder {
|
||||
*/
|
||||
class ModifierConstants {
|
||||
static stringModifiers = {
|
||||
'upper': (value: string) => value.toUpperCase(),
|
||||
'lower': (value: string) => value.toLowerCase(),
|
||||
'title': (value: string) => value
|
||||
.split(' ')
|
||||
.map((word) => word.toLowerCase())
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' '),
|
||||
'length': (value: string) => value.length.toString(),
|
||||
'reverse': (value: string) => value.split('').reverse().join(''),
|
||||
'base64': (value: string) => btoa(value),
|
||||
'string': (value: string) => value,
|
||||
}
|
||||
upper: (value: string) => value.toUpperCase(),
|
||||
lower: (value: string) => value.toLowerCase(),
|
||||
title: (value: string) =>
|
||||
value
|
||||
.split(' ')
|
||||
.map((word) => word.toLowerCase())
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' '),
|
||||
length: (value: string) => value.length.toString(),
|
||||
reverse: (value: string) => value.split('').reverse().join(''),
|
||||
base64: (value: string) => btoa(value),
|
||||
string: (value: string) => value,
|
||||
};
|
||||
|
||||
static arrayModifierGetOrDefault = (value: string[], i: number) => value.length > 0 ? String(value[i]) : '';
|
||||
static arrayModifierGetOrDefault = (value: string[], i: number) =>
|
||||
value.length > 0 ? String(value[i]) : '';
|
||||
static arrayModifiers = {
|
||||
'join': (value: string[]) => value.join(", "),
|
||||
'length': (value: string[]) => value.length.toString(),
|
||||
'first': (value: string[]) => this.arrayModifierGetOrDefault(value, 0),
|
||||
'last': (value: string[]) => this.arrayModifierGetOrDefault(value, value.length - 1),
|
||||
'random': (value: string[]) => this.arrayModifierGetOrDefault(value, Math.floor(Math.random() * value.length)),
|
||||
'sort': (value: string[]) => [...value].sort(),
|
||||
'reverse': (value: string[]) => [...value].reverse(),
|
||||
}
|
||||
join: (value: string[]) => value.join(', '),
|
||||
length: (value: string[]) => value.length.toString(),
|
||||
first: (value: string[]) => this.arrayModifierGetOrDefault(value, 0),
|
||||
last: (value: string[]) =>
|
||||
this.arrayModifierGetOrDefault(value, value.length - 1),
|
||||
random: (value: string[]) =>
|
||||
this.arrayModifierGetOrDefault(
|
||||
value,
|
||||
Math.floor(Math.random() * value.length)
|
||||
),
|
||||
sort: (value: string[]) => [...value].sort(),
|
||||
reverse: (value: string[]) => [...value].reverse(),
|
||||
};
|
||||
|
||||
static numberModifiers = {
|
||||
'comma': (value: number) => value.toLocaleString(),
|
||||
'hex': (value: number) => value.toString(16),
|
||||
'octal': (value: number) => value.toString(8),
|
||||
'binary': (value: number) => value.toString(2),
|
||||
'bytes': (value: number) => formatBytes(value, 1000),
|
||||
'bytes10': (value: number) => formatBytes(value, 1000),
|
||||
'bytes2': (value: number) => formatBytes(value, 1024),
|
||||
'string': (value: number) => value.toString(),
|
||||
'time': (value: number) => formatDuration(value),
|
||||
}
|
||||
comma: (value: number) => value.toLocaleString(),
|
||||
hex: (value: number) => value.toString(16),
|
||||
octal: (value: number) => value.toString(8),
|
||||
binary: (value: number) => value.toString(2),
|
||||
bytes: (value: number) => formatBytes(value, 1000),
|
||||
bytes10: (value: number) => formatBytes(value, 1000),
|
||||
bytes2: (value: number) => formatBytes(value, 1024),
|
||||
string: (value: number) => value.toString(),
|
||||
time: (value: number) => formatDuration(value),
|
||||
};
|
||||
|
||||
static conditionalModifiers = {
|
||||
exact: {
|
||||
'istrue': (value: any) => value === true,
|
||||
'isfalse': (value: any) => value === false,
|
||||
'exists': (value: any) => {
|
||||
istrue: (value: any) => value === true,
|
||||
isfalse: (value: any) => value === false,
|
||||
exists: (value: any) => {
|
||||
// Handle null, undefined, empty strings, empty arrays
|
||||
if (value === undefined || value === null) return false;
|
||||
if (typeof value === 'string') return /\S/.test(value); // has at least one non-whitespace character
|
||||
@@ -688,7 +826,7 @@ class ModifierConstants {
|
||||
},
|
||||
|
||||
prefix: {
|
||||
'$': (value: string, check: string) => value.startsWith(check),
|
||||
$: (value: string, check: string) => value.startsWith(check),
|
||||
'^': (value: string, check: string) => value.endsWith(check),
|
||||
'~': (value: string, check: string) => value.includes(check),
|
||||
'=': (value: string, check: string) => value == check,
|
||||
@@ -697,20 +835,24 @@ class ModifierConstants {
|
||||
'<=': (value: string | number, check: string | number) => value <= check,
|
||||
'<': (value: string | number, check: string | number) => value < check,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
static hardcodedModifiersForRegexMatching = {
|
||||
static hardcodedModifiersForRegexMatching = {
|
||||
"replace('.*?'\\s*?,\\s*?'.*?')": null,
|
||||
"replace(\".*?\"\\s*?,\\s*?'.*?')": null,
|
||||
"replace('.*?'\\s*?,\\s*?\".*?\")": null,
|
||||
'replace(".*?"\\s*?,\\s*?\".*?\")': null,
|
||||
"join('.*?')": null,
|
||||
'join(".*?")': null,
|
||||
"$.*?": null,
|
||||
"^.*?": null,
|
||||
"~.*?": null,
|
||||
"=.*?": null,
|
||||
">=.*?": null,
|
||||
">.*?": null,
|
||||
"<=.*?": null,
|
||||
"<.*?": null,
|
||||
}
|
||||
'$.*?': null,
|
||||
'^.*?': null,
|
||||
'~.*?': null,
|
||||
'=.*?': null,
|
||||
'>=.*?': null,
|
||||
'>.*?': null,
|
||||
'<=.*?': null,
|
||||
'<.*?': null,
|
||||
};
|
||||
|
||||
static modifiers = {
|
||||
...this.hardcodedModifiersForRegexMatching,
|
||||
@@ -719,19 +861,19 @@ class ModifierConstants {
|
||||
...this.arrayModifiers,
|
||||
...this.conditionalModifiers.exact,
|
||||
...this.conditionalModifiers.prefix,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ComparatorConstants {
|
||||
static comparatorKeyToFuncs = {
|
||||
"and": (v1: any, v2: any) => v1 && v2,
|
||||
"or": (v1: any, v2: any) => v1 || v2,
|
||||
"xor": (v1: any, v2: any) => (v1 || v2) && !(v1 && v2),
|
||||
"neq": (v1: any, v2: any) => v1 !== v2,
|
||||
"equal": (v1: any, v2: any) => v1 === v2,
|
||||
"left": (v1: any, _: any) => v1,
|
||||
"right": (_: any, v2: any) => v2,
|
||||
}
|
||||
and: (v1: any, v2: any) => v1 && v2,
|
||||
or: (v1: any, v2: any) => v1 || v2,
|
||||
xor: (v1: any, v2: any) => (v1 || v2) && !(v1 && v2),
|
||||
neq: (v1: any, v2: any) => v1 !== v2,
|
||||
equal: (v1: any, v2: any) => v1 === v2,
|
||||
left: (v1: any, _: any) => v1,
|
||||
right: (_: any, v2: any) => v2,
|
||||
};
|
||||
}
|
||||
|
||||
const DebugToolReplacementConstants = {
|
||||
@@ -782,8 +924,8 @@ Conditional:
|
||||
<array>::string::reverse {stream.languages} -> {stream.languages::join("::")::reverse}
|
||||
<boolean>::length::>=2 {stream.languages} -> {stream.languages::length::>=2["true"||"false"]}
|
||||
`,
|
||||
|
||||
comparator : `
|
||||
|
||||
comparator: `
|
||||
Comparators: <stream.library({stream.library})>::comparator::<stream.proxied({stream.proxied})>
|
||||
::and:: {stream.library::and::stream.proxied["true"||"false"]}
|
||||
::or:: {stream.library::or::stream.proxied["true"||"false"]}
|
||||
@@ -800,4 +942,4 @@ Comparators: <stream.library({stream.library})>::comparator::<stream.proxied({st
|
||||
Is Fast Enough Link
|
||||
service.cached::or::stream.library::or::stream.seeders::>10["true"||"false"] -> {service.cached::istrue::or::stream.library::or::stream.seeders::>10["true"||"false"]}
|
||||
`,
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user