mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat(core/formatters): add ::<comparator>:: to formatter for Advanced Custom Formatting Logic! (#381)
Co-authored-by: David Garcia <dgarcia3@atlassian.com>
This commit is contained in:
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
}
|
||||||
@@ -106,24 +106,55 @@ export interface ParseValue {
|
|||||||
} & typeof DebugToolReplacementConstants;
|
} & typeof DebugToolReplacementConstants;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-compiled function that takes ParseValue and returns formatted string
|
||||||
|
*/
|
||||||
|
type CompiledParseFunction = (parseValue: ParseValue) => string;
|
||||||
|
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;
|
||||||
|
|
||||||
export abstract class BaseFormatter {
|
export abstract class BaseFormatter {
|
||||||
protected config: FormatterConfig;
|
protected config: FormatterConfig;
|
||||||
protected userData: UserData;
|
protected userData: UserData;
|
||||||
|
|
||||||
private regexBuilder: BaseFormatterRegexBuilder;
|
private regexBuilder: BaseFormatterRegexBuilder;
|
||||||
|
private precompiledNameFunction: CompiledParseFunction | null = null;
|
||||||
|
private precompiledDescriptionFunction: CompiledParseFunction | null = null;
|
||||||
|
|
||||||
|
private _compilationPromise: Promise<void>;
|
||||||
|
|
||||||
constructor(config: FormatterConfig, userData: UserData) {
|
constructor(config: FormatterConfig, userData: UserData) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.userData = userData;
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public format(stream: ParsedStream): { name: string; description: string } {
|
private async compileTemplatesAsync(): Promise<void> {
|
||||||
|
this.precompiledNameFunction = await this.compileTemplate(this.config.name);
|
||||||
|
this.precompiledDescriptionFunction = await this.compileTemplate(this.config.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
const parseValue = this.convertStreamToParseValue(stream);
|
||||||
return {
|
return {
|
||||||
name: this.parseString(this.config.name, parseValue) || '',
|
name: this.precompiledNameFunction(parseValue),
|
||||||
description: this.parseString(this.config.description, parseValue) || '',
|
description: this.precompiledDescriptionFunction(parseValue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +189,7 @@ export abstract class BaseFormatter {
|
|||||||
const onlyUserSpecifiedLanguages = sortedLanguages?.filter((lang) =>
|
const onlyUserSpecifiedLanguages = sortedLanguages?.filter((lang) =>
|
||||||
userSpecifiedLanguages.includes(lang as any)
|
userSpecifiedLanguages.includes(lang as any)
|
||||||
);
|
);
|
||||||
let parseValue: ParseValue = {
|
const parseValue: ParseValue = {
|
||||||
config: {
|
config: {
|
||||||
addonName: this.userData.addonName || Env.ADDON_NAME,
|
addonName: this.userData.addonName || Env.ADDON_NAME,
|
||||||
},
|
},
|
||||||
@@ -266,124 +297,171 @@ export abstract class BaseFormatter {
|
|||||||
return parseValue;
|
return parseValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseString(str: string, value: ParseValue): string | null {
|
|
||||||
if (!str) return null;
|
|
||||||
|
|
||||||
|
protected async compileTemplate(str: string): Promise<CompiledParseFunction> {
|
||||||
|
if (!str) return () => '';
|
||||||
const re = this.regexBuilder.buildRegexExpression();
|
const re = this.regexBuilder.buildRegexExpression();
|
||||||
let matches: RegExpExecArray | null;
|
let matches: RegExpExecArray | null;
|
||||||
|
|
||||||
|
let compiledMatchTemplateFns: CompiledVariableWInsertFn[] = [];
|
||||||
|
|
||||||
|
for (const key in DebugToolReplacementConstants) {
|
||||||
|
str = str.replace(`{debug.${key}}`, DebugToolReplacementConstants[key as keyof typeof DebugToolReplacementConstants]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all {...} matches
|
||||||
while (matches = re.exec(str)) {
|
while (matches = re.exec(str)) {
|
||||||
if (!matches.groups) continue;
|
if (!matches.groups) continue;
|
||||||
|
|
||||||
const index = matches.index as number;
|
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);
|
||||||
|
|
||||||
// Validate - variableType (exists in value)
|
// Split {<var1_with_modifiers>>::<comparator1>::<var2_with_modifiers>>...} into variableWithModifiers array and comparators array
|
||||||
const variableDict = value[matches.groups.variableType as keyof ParseValue];
|
const splitOnComparators = matchWithoutSuffix.split(RegExp(this.regexBuilder.buildComparatorRegexPattern(), 'gi'));
|
||||||
if (!variableDict) {
|
const variableWithModifiers = splitOnComparators.filter((_, i) => i % 2 == 0);
|
||||||
str = this.replaceCharsFromString(
|
const comparators = splitOnComparators.filter((_, i) => i % 2 != 0);
|
||||||
str,
|
const foundComparators = comparators.map(c => c as keyof typeof ComparatorConstants.comparatorKeyToFuncs);
|
||||||
'{unknown_variableName}',
|
let precompiledResolvedVariableFns: CompiledModifiedVariableFn[] = variableWithModifiers
|
||||||
index,
|
.map(baseString => this.parseModifiedVariable(baseString, {
|
||||||
re.lastIndex
|
mod_tzlocale: matches?.groups?.mod_tzlocale ?? undefined
|
||||||
);
|
})
|
||||||
re.lastIndex = index;
|
);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
// Validate - property: variableDict[propertyName]
|
const resolvedVariablesWithContext = precompiledResolvedVariableFns.map(fn => fn(parseValue));
|
||||||
const property = variableDict[matches.groups.propertyName as keyof typeof variableDict];
|
const reducedResolvedVarWContext = resolvedVariablesWithContext.reduce((prev, cur, i) => {
|
||||||
if (property === undefined) {
|
if (prev.error !== undefined) return prev;
|
||||||
str = this.replaceCharsFromString(
|
if (cur.error !== undefined) return cur;
|
||||||
str,
|
// the comparator key between prev and cur (from splitOnComparators)
|
||||||
'{unknown_propertyName}',
|
const compareKey = foundComparators[i - 1] as keyof typeof ComparatorConstants.comparatorKeyToFuncs;
|
||||||
index,
|
const comparatorFn = ComparatorConstants.comparatorKeyToFuncs[compareKey];
|
||||||
re.lastIndex
|
|
||||||
);
|
try {
|
||||||
re.lastIndex = index;
|
const result = comparatorFn(prev.result, cur.result);
|
||||||
continue;
|
const finalResult = { result: result };
|
||||||
}
|
return finalResult;
|
||||||
|
} catch (e) {
|
||||||
// Validate and Process - Modifier(s)
|
const errorResult = { error: `{unable_to_compare(<${prev.result}>::${compareKey}::<${cur.result}>, ${e})}` };
|
||||||
if (matches.groups.modifiers) {
|
return errorResult;
|
||||||
let result = this.applyModifiers(matches.groups, property, value);
|
|
||||||
// handle unknown modifier result
|
|
||||||
if (result === undefined) {
|
|
||||||
result = `{unknown_modifier(${matches.groups.modifiers})}`;
|
|
||||||
if (['string', 'number', 'boolean', 'object', 'array'].includes(typeof property)) {
|
|
||||||
result = `{unknown_${typeof property}_modifier(${matches.groups.modifiers})}`;
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
str = this.replaceCharsFromString(
|
return reducedResolvedVarWContext;
|
||||||
str,
|
}; // end of COMPARATOR logic
|
||||||
result,
|
|
||||||
index,
|
|
||||||
re.lastIndex
|
|
||||||
);
|
|
||||||
re.lastIndex = index;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
str = this.replaceCharsFromString(str, property, index, re.lastIndex);
|
|
||||||
|
// 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 _compiledResolvedVariableFn = precompiledResolvedVariableFn;
|
||||||
|
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 { 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;
|
re.lastIndex = index;
|
||||||
}
|
compiledMatchTemplateFns.push({ resultFn: precompiledResolvedVariableFn, insertIndex: index });
|
||||||
|
} // end of while loop
|
||||||
|
|
||||||
return str
|
return (parseValue: ParseValue) => {
|
||||||
.replace(/\\n/g, '\n')
|
let resultStr = str;
|
||||||
.split('\n')
|
|
||||||
.filter(
|
// Sort by startIndex to process in reverse order
|
||||||
(line) => line.trim() !== '' && !line.includes('{tools.removeLine}')
|
for (const { resultFn, insertIndex } of compiledMatchTemplateFns.sort((a, b) => b.insertIndex - a.insertIndex)) {
|
||||||
)
|
const resolvedResult = resultFn(parseValue);
|
||||||
.join('\n')
|
const replacement = resolvedResult.error ?? resolvedResult.result?.toString() ?? '';
|
||||||
.replace(/\{tools.newLine\}/g, '\n');
|
resultStr = resultStr.slice(0, insertIndex) + replacement + resultStr.slice(insertIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultStr
|
||||||
|
.replace(/\\n/g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.filter(
|
||||||
|
(line) => line.trim() !== '' && !line.includes('{tools.removeLine}')
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
|
.replace(/\{tools.newLine\}/g, '\n');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected applyModifiers(
|
/**
|
||||||
groups: {[key: string]: string},
|
* @param baseString - string to parse, e.g. `<variableType>.<propertyName>(::<modifier>)*`
|
||||||
input: any,
|
* @param value - ParseValue object
|
||||||
parseValue: ParseValue,
|
* @param fullStringModifiers - modifiers that are applied to the entire string (e.g. `::<tzLocale>`)
|
||||||
): string | undefined {
|
*
|
||||||
const singleModTerminator = '((::)|($))'; // :: if there's multiple modifiers or $ for the end of the string
|
* @returns (parseValue) => `{ result: <resolved modified variable> }` or `{ error: "<errorMessage>" }`
|
||||||
const singleValidModRe = new RegExp(this.regexBuilder.buildModifierRegexPattern() + singleModTerminator, 'gi');
|
*/
|
||||||
|
protected parseModifiedVariable(
|
||||||
let result = input as any;
|
baseString: string,
|
||||||
// iterate over modifiers in order of appearance
|
fullStringModifiers: {
|
||||||
for (const modMatch of [...groups.modifiers.matchAll(singleValidModRe)].sort((a, b) => (a.index ?? 0) - (b.index ?? 0))) {
|
mod_tzlocale: string | undefined,
|
||||||
if (result === undefined) break;
|
},
|
||||||
result = this.applySingleModifier(
|
): CompiledModifiedVariableFn {
|
||||||
result,
|
// get variableType and propertyName from baseString without regex
|
||||||
modMatch[1], // First capture group (the modifier name)
|
const variableType = baseString.split('.')[0];
|
||||||
groups.mod_tzlocale ?? "",
|
baseString = baseString.substring(variableType.length + 1);
|
||||||
);
|
const propertyName = baseString.split('::')[0];
|
||||||
|
const allModifiers = baseString.substring(propertyName.length);
|
||||||
|
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 */);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle unknown modifier result
|
return (parseValue: ParseValue) => {
|
||||||
switch (typeof result) {
|
// PARSE VARIABLE logic
|
||||||
case 'undefined': return undefined;
|
const variableDict = parseValue[variableType as keyof ParseValue];
|
||||||
case 'boolean':
|
if (!variableDict) return { error: `{unknown_variableType(${variableType})}` }; // should never happen
|
||||||
let check_true = groups.mod_check_true ?? "";
|
const property = variableDict![propertyName as keyof typeof variableDict] as any;
|
||||||
let check_false = groups.mod_check_false ?? "";
|
if (property === undefined) return { error: `{unknown_propertyName(${variableType}.${propertyName})}` }; // should never happen
|
||||||
if (typeof check_true !== 'string' || typeof check_false !== 'string')
|
// end of PARSE VARIABLE logic
|
||||||
return `{unknown_conditional_modifier_check_true_or_false}`;
|
|
||||||
|
|
||||||
if (parseValue) {
|
// APPLY MULTIPLE MODIFIERS logic
|
||||||
check_true = this.parseString(check_true, parseValue) || check_true;
|
let result = property;
|
||||||
check_false = this.parseString(check_false, parseValue) || check_false;
|
for (const lastModMatched of sortedModMatches) {
|
||||||
|
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})}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getErrorResult();
|
||||||
}
|
}
|
||||||
return result ? check_true : check_false;
|
}
|
||||||
default:
|
// end of APPLY MULTIPLE MODIFIERS logic
|
||||||
return result;
|
|
||||||
|
return { result: result } as ResolvedVariable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param variable - the variable to apply the modifier to (e.g. `123`, `"TorBox"`, `["English", "Italian"]`, etc.)
|
* @param variable - the variable to apply the modifier to (e.g. `123`, `"TorBox"`, `["English", "Italian"]`, etc.)
|
||||||
* @param mod - the modifier to apply
|
* @param mod - the modifier to apply
|
||||||
|
* @param fullStringModifiers - modifiers that are applied to the entire string (e.g. `::<tzLocale>`)
|
||||||
|
* @returns `{ result: <resolved modified variable> }` or `{ error: "<errorMessage>" }`
|
||||||
*/
|
*/
|
||||||
protected applySingleModifier(
|
protected applySingleModifier(
|
||||||
variable: any,
|
variable: any,
|
||||||
mod: string,
|
mod: string,
|
||||||
tzlocale?: string,
|
fullStringModifiers: {
|
||||||
|
mod_tzlocale: string | undefined,
|
||||||
|
},
|
||||||
): string | boolean | undefined {
|
): string | boolean | undefined {
|
||||||
const _mod = mod;
|
const _mod = mod;
|
||||||
mod = mod.toLowerCase();
|
mod = mod.toLowerCase();
|
||||||
@@ -400,13 +478,13 @@ export abstract class BaseFormatter {
|
|||||||
if (!ModifierConstants.conditionalModifiers.exact.exists(variable)) {
|
if (!ModifierConstants.conditionalModifiers.exact.exists(variable)) {
|
||||||
conditional = false;
|
conditional = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXACT
|
// EXACT
|
||||||
else if (isExact) {
|
else if (isExact) {
|
||||||
const modAsKey = mod as keyof typeof ModifierConstants.conditionalModifiers.exact;
|
const modAsKey = mod as keyof typeof ModifierConstants.conditionalModifiers.exact;
|
||||||
conditional = ModifierConstants.conditionalModifiers.exact[modAsKey](variable);
|
conditional = ModifierConstants.conditionalModifiers.exact[modAsKey](variable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PREFIX
|
// PREFIX
|
||||||
else if (isPrefix) {
|
else if (isPrefix) {
|
||||||
// get the longest prefix match
|
// get the longest prefix match
|
||||||
@@ -417,7 +495,7 @@ export abstract class BaseFormatter {
|
|||||||
let stringCheck = mod.substring(modPrefix.length).toLowerCase();
|
let stringCheck = mod.substring(modPrefix.length).toLowerCase();
|
||||||
// remove whitespace from stringCheck if it isn't in stringValue
|
// 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)
|
// 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 [parsedNumericValue, parsedNumericCheck] = [Number(stringValue.replace(/,\s/g, '')), Number(stringCheck.replace(/,\s/g, ''))];
|
||||||
const isNumericComparison = ["<", "<=", ">", ">=", "="].includes(modPrefix) &&
|
const isNumericComparison = ["<", "<=", ">", ">=", "="].includes(modPrefix) &&
|
||||||
@@ -463,17 +541,9 @@ export abstract class BaseFormatter {
|
|||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected replaceCharsFromString(
|
|
||||||
str: string,
|
|
||||||
replace: string,
|
|
||||||
start: number,
|
|
||||||
end: number
|
|
||||||
): string {
|
|
||||||
return str.slice(0, start) + replace + str.slice(end);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to store the actual value of a parsed, and potentially modified, variable
|
* 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
|
* or an error message if the parsed/modified result becomes invalid for any reason
|
||||||
@@ -491,27 +561,39 @@ class BaseFormatterRegexBuilder {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* RegEx Capture Pattern: `<variableType>.<propertyName>`
|
* RegEx Capture Pattern: `<variableType>.<propertyName>`
|
||||||
|
*
|
||||||
|
* (no named capture group)
|
||||||
*/
|
*/
|
||||||
public buildVariableRegexPattern(): string {
|
public buildVariableRegexPattern(): string {
|
||||||
const validVariables: (keyof ParseValue)[] = Object.keys(this.hardcodedParseValueKeysForRegexMatching) as (keyof ParseValue)[];
|
// Get all valid variable names (keys as well as subkeys) from ParseValue structure
|
||||||
// Get all valid properties (subkeys) from ParseValue structure
|
const validVariableNames = Object.keys(this.hardcodedParseValueKeysForRegexMatching).flatMap(sectionKey => {
|
||||||
const validProperties = validVariables.flatMap(sectionKey => {
|
|
||||||
const section = this.hardcodedParseValueKeysForRegexMatching[sectionKey as keyof ParseValue];
|
const section = this.hardcodedParseValueKeysForRegexMatching[sectionKey as keyof ParseValue];
|
||||||
if (section && typeof section === 'object' && section !== null) {
|
if (section && typeof section === 'object' && section !== null) {
|
||||||
return Object.keys(section);
|
return Object.keys(section).map((key) => `${sectionKey}\\.${key}`);
|
||||||
}
|
}
|
||||||
return [];
|
return []; // @flatMap
|
||||||
});
|
});
|
||||||
return `(?<variableType>${validVariables.join('|')})\\.(?<propertyName>${validProperties.join('|')})`;
|
return `(${validVariableNames.join('|')})`;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* RegEx Capture Pattern: `::<modifier>`
|
* RegEx Capture Pattern: `::<modifier>`
|
||||||
|
*
|
||||||
|
* (no named capture group)
|
||||||
*/
|
*/
|
||||||
public buildModifierRegexPattern(): string {
|
public buildModifierRegexPattern(): string {
|
||||||
const validModifiers = Object.keys(ModifierConstants.modifiers)
|
const validModifiers = Object.keys(ModifierConstants.modifiers)
|
||||||
.map(key => key.replace(/[\(\)\'\"\$\^\~\=\>\<]/g, '\\$&'));
|
.map(key => key.replace(/[\(\)\'\"\$\^\~\=\>\<]/g, '\\$&'));
|
||||||
return `::(${validModifiers.join('|')})`;
|
return `::(${validModifiers.join('|')})`;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* RegEx Capture Pattern: `::<comparator>::`
|
||||||
|
*
|
||||||
|
* (no named capture group)
|
||||||
|
*/
|
||||||
|
public buildComparatorRegexPattern(): string {
|
||||||
|
const comparatorKeys = Object.keys(ComparatorConstants.comparatorKeyToFuncs)
|
||||||
|
return `::(${comparatorKeys.join("|")})::`
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* RegEx Capture Pattern: `::<tzLocale>`
|
* RegEx Capture Pattern: `::<tzLocale>`
|
||||||
*
|
*
|
||||||
@@ -539,11 +621,13 @@ class BaseFormatterRegexBuilder {
|
|||||||
public buildRegexExpression(): RegExp {
|
public buildRegexExpression(): RegExp {
|
||||||
const variable = this.buildVariableRegexPattern();
|
const variable = this.buildVariableRegexPattern();
|
||||||
const modifier = this.buildModifierRegexPattern();
|
const modifier = this.buildModifierRegexPattern();
|
||||||
|
const comparator = this.buildComparatorRegexPattern();
|
||||||
const modTZLocale = this.buildTZLocaleRegexPattern();
|
const modTZLocale = this.buildTZLocaleRegexPattern();
|
||||||
const checkTF = this.buildCheckRegexPattern();
|
const checkTF = this.buildCheckRegexPattern();
|
||||||
|
|
||||||
const regexPattern = `\\{${variable}(?<modifiers>(${modifier})+)?(${modTZLocale})?(${checkTF})?\\}`;
|
const variableAndModifiers = `${variable}(${modifier})*`;
|
||||||
|
const regexPattern = `\\{${variableAndModifiers}(${comparator}${variableAndModifiers})*(?<suffix>(${modTZLocale})?(${checkTF})?)\\}`;
|
||||||
|
|
||||||
return new RegExp(regexPattern, 'gi');
|
return new RegExp(regexPattern, 'gi');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +648,7 @@ class ModifierConstants {
|
|||||||
'reverse': (value: string) => value.split('').reverse().join(''),
|
'reverse': (value: string) => value.split('').reverse().join(''),
|
||||||
'base64': (value: string) => btoa(value),
|
'base64': (value: string) => btoa(value),
|
||||||
'string': (value: string) => 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 = {
|
static arrayModifiers = {
|
||||||
@@ -638,6 +722,18 @@ class ModifierConstants {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DebugToolReplacementConstants = {
|
const DebugToolReplacementConstants = {
|
||||||
modifier: `
|
modifier: `
|
||||||
String: {config.addonName}
|
String: {config.addonName}
|
||||||
@@ -647,6 +743,7 @@ String: {config.addonName}
|
|||||||
::length {config.addonName::length}
|
::length {config.addonName::length}
|
||||||
::reverse {config.addonName::reverse}
|
::reverse {config.addonName::reverse}
|
||||||
{tools.newLine}
|
{tools.newLine}
|
||||||
|
|
||||||
Number: {stream.size}
|
Number: {stream.size}
|
||||||
::bytes {stream.size::bytes}
|
::bytes {stream.size::bytes}
|
||||||
::time {stream.size::time}
|
::time {stream.size::time}
|
||||||
@@ -654,12 +751,14 @@ Number: {stream.size}
|
|||||||
::octal {stream.size::octal}
|
::octal {stream.size::octal}
|
||||||
::binary {stream.size::binary}
|
::binary {stream.size::binary}
|
||||||
{tools.newLine}
|
{tools.newLine}
|
||||||
|
|
||||||
Array: {stream.languages}
|
Array: {stream.languages}
|
||||||
::join('-separator-') {stream.languages::join("-separator-")}
|
::join('-separator-') {stream.languages::join("-separator-")}
|
||||||
::length {stream.languages::length}
|
::length {stream.languages::length}
|
||||||
::first {stream.languages::first}
|
::first {stream.languages::first}
|
||||||
::last {stream.languages::last}
|
::last {stream.languages::last}
|
||||||
{tools.newLine}
|
{tools.newLine}
|
||||||
|
|
||||||
Conditional:
|
Conditional:
|
||||||
String: {stream.filename}
|
String: {stream.filename}
|
||||||
filename::exists {stream.filename::exists["true"||"false"]}
|
filename::exists {stream.filename::exists["true"||"false"]}
|
||||||
@@ -676,10 +775,29 @@ Conditional:
|
|||||||
::istrue {stream.proxied::istrue["true"||"false"]}
|
::istrue {stream.proxied::istrue["true"||"false"]}
|
||||||
::isfalse {stream.proxied::isfalse["true"||"false"]}
|
::isfalse {stream.proxied::isfalse["true"||"false"]}
|
||||||
{tools.newLine}
|
{tools.newLine}
|
||||||
|
|
||||||
[Advanced] Multiple modifiers
|
[Advanced] Multiple modifiers
|
||||||
<string>::reverse::title::reverse {config.addonName} -> {config.addonName::reverse::title::reverse}
|
<string>::reverse::title::reverse {config.addonName} -> {config.addonName::reverse::title::reverse}
|
||||||
<number>::string::reverse {stream.size} -> {stream.size::string::reverse}
|
<number>::string::reverse {stream.size} -> {stream.size::string::reverse}
|
||||||
<array>::string::reverse {stream.languages} -> {stream.languages::join("::")::reverse}
|
<array>::string::reverse {stream.languages} -> {stream.languages::join("::")::reverse}
|
||||||
<boolean>::length::>=2 {stream.languages} -> {stream.languages::length::>=2["true"||"false"]}
|
<boolean>::length::>=2 {stream.languages} -> {stream.languages::length::>=2["true"||"false"]}
|
||||||
`,
|
`,
|
||||||
}
|
|
||||||
|
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"]}
|
||||||
|
::xor:: {stream.library::xor::stream.proxied["true"||"false"]}
|
||||||
|
::neq:: {stream.library::neq::stream.proxied["true"||"false"]}
|
||||||
|
::equal:: {stream.library::equal::stream.proxied["true"||"false"]}
|
||||||
|
::left:: {stream.library::left::stream.proxied["true"||"false"]}
|
||||||
|
::right:: {stream.library::right::stream.proxied["true"||"false"]}
|
||||||
|
{tools.newLine}
|
||||||
|
|
||||||
|
[Advanced] Multiple Comparators
|
||||||
|
Is English
|
||||||
|
stream.languages::~English::or::stream.languages::~dub::and::stream.languages::length::>0["Yes"||"Unknown"] -> {stream.languages::~English::or::stream.languages::~dub::and::stream.languages::length::>0["Yes"||"Unknown"]}
|
||||||
|
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"]}
|
||||||
|
`,
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ export class StremioTransformer {
|
|||||||
private async convertParsedStreamToStream(
|
private async convertParsedStreamToStream(
|
||||||
stream: ParsedStream,
|
stream: ParsedStream,
|
||||||
formatter: {
|
formatter: {
|
||||||
format: (stream: ParsedStream) => { name: string; description: string };
|
format: (stream: ParsedStream) => Promise<{ name: string; description: string }>;
|
||||||
},
|
},
|
||||||
index: number,
|
index: number,
|
||||||
provideStreamData: boolean
|
provideStreamData: boolean
|
||||||
@@ -56,7 +56,7 @@ export class StremioTransformer {
|
|||||||
name: stream.originalName || stream.addon.name,
|
name: stream.originalName || stream.addon.name,
|
||||||
description: stream.originalDescription,
|
description: stream.originalDescription,
|
||||||
}
|
}
|
||||||
: formatter.format(stream);
|
: await formatter.format(stream);
|
||||||
|
|
||||||
const autoPlaySettings = {
|
const autoPlaySettings = {
|
||||||
enabled: this.userData.autoPlay?.enabled ?? true,
|
enabled: this.userData.autoPlay?.enabled ?? true,
|
||||||
@@ -203,6 +203,7 @@ export class StremioTransformer {
|
|||||||
}>,
|
}>,
|
||||||
options?: { provideStreamData: boolean }
|
options?: { provideStreamData: boolean }
|
||||||
): Promise<AIOStreamResponse> {
|
): Promise<AIOStreamResponse> {
|
||||||
|
const formatter = createFormatter(this.userData);
|
||||||
const {
|
const {
|
||||||
data: { streams, statistics },
|
data: { streams, statistics },
|
||||||
errors,
|
errors,
|
||||||
@@ -211,8 +212,6 @@ export class StremioTransformer {
|
|||||||
|
|
||||||
let transformedStreams: AIOStream[] = [];
|
let transformedStreams: AIOStream[] = [];
|
||||||
|
|
||||||
const formatter = createFormatter(this.userData);
|
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
transformedStreams = await Promise.all(
|
transformedStreams = await Promise.all(
|
||||||
@@ -328,7 +327,7 @@ export class StremioTransformer {
|
|||||||
|
|
||||||
// Create formatter for stream conversion if needed
|
// Create formatter for stream conversion if needed
|
||||||
let formatter: {
|
let formatter: {
|
||||||
format: (stream: ParsedStream) => { name: string; description: string };
|
format: (stream: ParsedStream) => Promise<{ name: string; description: string }>;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
if (
|
if (
|
||||||
meta.videos?.some((video) => video.streams && video.streams.length > 0)
|
meta.videos?.some((video) => video.streams && video.streams.length > 0)
|
||||||
|
|||||||
@@ -17,21 +17,9 @@ router.use(formatApiRateLimiter);
|
|||||||
|
|
||||||
const logger = createLogger('server');
|
const logger = createLogger('server');
|
||||||
|
|
||||||
router.post('/', (req: Request, res: Response) => {
|
router.post('/', async (req: Request, res: Response) => {
|
||||||
const { userData, stream } = req.body;
|
const { userData, stream } = req.body;
|
||||||
const {
|
|
||||||
success: streamSuccess,
|
|
||||||
error: streamError,
|
|
||||||
data: streamData,
|
|
||||||
} = ParsedStreamSchema.safeParse(stream);
|
|
||||||
if (!streamSuccess) {
|
|
||||||
logger.error('Invalid stream', { error: streamError });
|
|
||||||
throw new APIError(
|
|
||||||
constants.ErrorCode.FORMAT_INVALID_STREAM,
|
|
||||||
400,
|
|
||||||
formatZodError(streamError)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const {
|
const {
|
||||||
success: userDataSuccess,
|
success: userDataSuccess,
|
||||||
error: userDataError,
|
error: userDataError,
|
||||||
@@ -45,7 +33,21 @@ router.post('/', (req: Request, res: Response) => {
|
|||||||
formatZodError(userDataError)
|
formatZodError(userDataError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const formattedStream = createFormatter(userDataData).format(streamData);
|
const formatter = createFormatter(userDataData);
|
||||||
|
const {
|
||||||
|
success: streamSuccess,
|
||||||
|
error: streamError,
|
||||||
|
data: streamData,
|
||||||
|
} = ParsedStreamSchema.safeParse(stream);
|
||||||
|
if (!streamSuccess) {
|
||||||
|
logger.error('Invalid stream', { error: streamError });
|
||||||
|
throw new APIError(
|
||||||
|
constants.ErrorCode.FORMAT_INVALID_STREAM,
|
||||||
|
400,
|
||||||
|
formatZodError(streamError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const formattedStream = await formatter.format(streamData);
|
||||||
res
|
res
|
||||||
.status(200)
|
.status(200)
|
||||||
.json(createResponse({ success: true, data: formattedStream }));
|
.json(createResponse({ success: true, data: formattedStream }));
|
||||||
|
|||||||
Reference in New Issue
Block a user