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:
DavidGracias
2025-09-18 18:13:35 -04:00
committed by GitHub
parent 3d706ea484
commit cc26bf9295
2 changed files with 318 additions and 176 deletions
+1 -1
View File
@@ -33,4 +33,4 @@
"engines": {
"node": ">=20.0.0"
}
}
}
+317 -175
View File
@@ -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"]}
`,
}
};