|
|
import { MacroParser } from './MacroParser.js'; |
|
|
import { MacroCstWalker } from './MacroCstWalker.js'; |
|
|
import { MacroRegistry } from './MacroRegistry.js'; |
|
|
import { logMacroGeneralError, logMacroInternalError, logMacroRuntimeWarning, logMacroSyntaxWarning } from './MacroDiagnostics.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let instance; |
|
|
export { instance as MacroEngine }; |
|
|
|
|
|
class MacroEngine { |
|
|
static #instance; |
|
|
static get instance() { return MacroEngine.#instance ?? (MacroEngine.#instance = new MacroEngine()); } |
|
|
|
|
|
constructor() { } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
evaluate(input, env) { |
|
|
if (!input) { |
|
|
return ''; |
|
|
} |
|
|
const safeEnv = Object.freeze({ ...env }); |
|
|
|
|
|
const preProcessed = this.#runPreProcessors(input, safeEnv); |
|
|
|
|
|
const { cst, lexingErrors, parserErrors } = MacroParser.parseDocument(preProcessed); |
|
|
|
|
|
|
|
|
if (lexingErrors && lexingErrors.length > 0) { |
|
|
logMacroSyntaxWarning({ phase: 'lexing', input, errors: lexingErrors }); |
|
|
} |
|
|
if (parserErrors && parserErrors.length > 0) { |
|
|
logMacroSyntaxWarning({ phase: 'parsing', input, errors: parserErrors }); |
|
|
} |
|
|
|
|
|
|
|
|
if (!cst || typeof cst !== 'object' || !cst.children) { |
|
|
logMacroGeneralError({ message: 'Macro parser produced an invalid CST. Returning original input.', error: { input, lexingErrors, parserErrors } }); |
|
|
return input; |
|
|
} |
|
|
|
|
|
let evaluated; |
|
|
try { |
|
|
evaluated = MacroCstWalker.evaluateDocument({ |
|
|
text: preProcessed, |
|
|
cst, |
|
|
env: safeEnv, |
|
|
resolveMacro: this.#resolveMacro.bind(this), |
|
|
}); |
|
|
} catch (error) { |
|
|
logMacroGeneralError({ message: 'Macro evaluation failed. Returning original input.', error: { input, error } }); |
|
|
return input; |
|
|
} |
|
|
|
|
|
const result = this.#runPostProcessors(evaluated, safeEnv); |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#resolveMacro(call) { |
|
|
const { name, env } = call; |
|
|
|
|
|
const raw = `{{${call.rawInner}}}`; |
|
|
if (!name) return raw; |
|
|
|
|
|
|
|
|
|
|
|
let defOverride = null; |
|
|
if (Object.hasOwn(env.dynamicMacros, name)) { |
|
|
const impl = env.dynamicMacros[name]; |
|
|
defOverride = { |
|
|
name, |
|
|
aliases: [], |
|
|
category: 'dynamic', |
|
|
description: 'Dynamic macro', |
|
|
minArgs: 0, |
|
|
maxArgs: 0, |
|
|
unnamedArgDefs: [], |
|
|
list: null, |
|
|
strictArgs: true, |
|
|
returns: null, |
|
|
returnType: 'string', |
|
|
displayOverride: null, |
|
|
exampleUsage: [], |
|
|
source: { name: 'dynamic', isExtension: false, isThirdParty: false }, |
|
|
aliasOf: null, |
|
|
aliasVisible: null, |
|
|
handler: typeof impl === 'function' ? impl : () => impl, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if (!defOverride && !MacroRegistry.hasMacro(name)) { |
|
|
return raw; |
|
|
} |
|
|
|
|
|
try { |
|
|
const result = MacroRegistry.executeMacro(call, { defOverride }); |
|
|
|
|
|
try { |
|
|
return call.env.functions.postProcess(result); |
|
|
} catch (error) { |
|
|
logMacroInternalError({ message: `Macro "${name}" postProcess function failed.`, call, error }); |
|
|
return result; |
|
|
} |
|
|
} catch (error) { |
|
|
const isRuntimeError = !!(error && (error.name === 'MacroRuntimeError' || error.isMacroRuntimeError)); |
|
|
if (isRuntimeError) { |
|
|
logMacroRuntimeWarning({ message: (error.message || `Macro "${name}" execution failed.`), call, error }); |
|
|
} else { |
|
|
logMacroInternalError({ message: `Macro "${name}" internal execution error.`, call, error }); |
|
|
} |
|
|
return raw; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#runPreProcessors(text, env) { |
|
|
let result = text; |
|
|
|
|
|
|
|
|
|
|
|
result = result.replace(/{{time_(UTC[+-]\d+)}}/gi, (_match, utcOffset) => { |
|
|
return `{{time::${utcOffset}}}`; |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
result = result.replace(/<USER>/gi, '{{user}}'); |
|
|
result = result.replace(/<BOT>/gi, '{{char}}'); |
|
|
result = result.replace(/<CHAR>/gi, '{{char}}'); |
|
|
result = result.replace(/<GROUP>/gi, '{{group}}'); |
|
|
result = result.replace(/<CHARIFNOTGROUP>/gi, '{{charIfNotGroup}}'); |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#runPostProcessors(text, env) { |
|
|
let result = text; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = result.replace(/\\([{}])/g, '$1'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = result.replace(/(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, ''); |
|
|
|
|
|
return result; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
normalizeMacroResult(value) { |
|
|
if (value === null || value === undefined) { |
|
|
return ''; |
|
|
} |
|
|
if (value instanceof Date) { |
|
|
return value.toISOString(); |
|
|
} |
|
|
if (typeof value === 'object' || Array.isArray(value)) { |
|
|
try { |
|
|
return JSON.stringify(value); |
|
|
} catch (_error) { |
|
|
return String(value); |
|
|
} |
|
|
} |
|
|
|
|
|
return String(value); |
|
|
} |
|
|
} |
|
|
|
|
|
instance = MacroEngine.instance; |
|
|
|