diff --git a/package.json b/package.json index a2b9d62e..47790918 100644 --- a/package.json +++ b/package.json @@ -33,4 +33,4 @@ "engines": { "node": ">=20.0.0" } -} +} \ No newline at end of file diff --git a/packages/core/src/formatters/base.ts b/packages/core/src/formatters/base.ts index d8434002..37edff35 100644 --- a/packages/core/src/formatters/base.ts +++ b/packages/core/src/formatters/base.ts @@ -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; - + 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 { 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 { 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(::)* (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 {>::::>...} 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. `.(::)*` * @param value - ParseValue object * @param fullStringModifiers - modifiers that are applied to the entire string (e.g. `::`) - * + * * @returns (parseValue) => `{ result: }` or `{ error: "" }` */ 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(['"]..., ['"]...) + const content = _mod.substring(9, _mod.length - 2); + + // split on findStartChar,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: `.` - * + * * (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: `::` - * + * * (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: `::::` - * + * * (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: `::` - * + * * (with named capture group `tzLocale`) */ public buildTZLocaleRegexPattern(): string { @@ -605,7 +736,7 @@ class BaseFormatterRegexBuilder { } /** * RegEx Capture Pattern: `["||"]` - * + * * (with named capture group `` and `` and `mod_check`=`"||"`) */ 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})*(?(${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: ::string::reverse {stream.languages} -> {stream.languages::join("::")::reverse} ::length::>=2 {stream.languages} -> {stream.languages::length::>=2["true"||"false"]} `, - - comparator : ` + + comparator: ` Comparators: ::comparator:: ::and:: {stream.library::and::stream.proxied["true"||"false"]} ::or:: {stream.library::or::stream.proxied["true"||"false"]} @@ -800,4 +942,4 @@ Comparators: ::comparator::10["true"||"false"] -> {service.cached::istrue::or::stream.library::or::stream.seeders::>10["true"||"false"]} `, -} \ No newline at end of file +};