Macros 2.0 [Fix] - Make macro name matching case-insensitive throughout the macro system (#4942)

* Make macro name matching case-insensitive throughout the macro system

- Normalize macro names and aliases to lowercase in MacroRegistry storage and lookup
- Update MacroCstWalker to use case-insensitive matching for block macro pairing
- Normalize dynamic macro keys to lowercase in MacroEnvBuilder
- Update MacroEngine to use lowercase keys when checking dynamic macros
- Preserve original casing in macro definitions for display purposes
- Add comments explaining case-insensitive matching behavior

* Make alias validation case-insensitive in MacroRegistry to prevent duplicate names

- Prevents registering aliases that differ only in casing from the macro name
This commit is contained in:
Wolfsblvt
2026-01-03 17:40:32 +01:00
committed by GitHub
parent b774a553f9
commit dbc4fe611c
4 changed files with 23 additions and 16 deletions
@@ -157,8 +157,8 @@ class MacroCstWalker {
if (!info) continue;
if (info.isClosing) {
// Closing tag - pop matching opener from stack
if (unclosedStack.length > 0 && unclosedStack[unclosedStack.length - 1].name === info.name) {
// Closing tag - pop matching opener from stack (case-insensitive match)
if (unclosedStack.length > 0 && unclosedStack[unclosedStack.length - 1].name.toLowerCase() === info.name.toLowerCase()) {
unclosedStack.pop();
}
// If no matching opener, ignore (orphan closing tag)
@@ -1015,8 +1015,8 @@ class MacroCstWalker {
for (let i = openingIdx + 1; i < macroInfos.length; i++) {
const info = macroInfos[i];
// Only consider macros with the same name
if (info.name !== targetName) continue;
// Only consider macros with the same name (case-insensitive)
if (info.name.toLowerCase() !== targetName.toLowerCase()) continue;
// Skip already matched macros
if (info.matched) continue;
+4 -2
View File
@@ -165,10 +165,12 @@ class MacroEngine {
if (!name) return raw;
// First check if this is a dynamic macro to use. If so, we will create a temporary macro definition for it and use that over any registered macro.
// Dynamic macro keys are normalized to lowercase for case-insensitive matching.
/** @type {MacroDefinition?} */
let defOverride = null;
if (Object.hasOwn(env.dynamicMacros, name)) {
const impl = env.dynamicMacros[name];
const nameLower = name.toLowerCase();
if (Object.hasOwn(env.dynamicMacros, nameLower)) {
const impl = env.dynamicMacros[nameLower];
defOverride = {
name,
aliases: [],
@@ -143,8 +143,11 @@ class MacroEnvBuilder {
env.functions.postProcess = typeof ctx.postProcessFn === 'function' ? ctx.postProcessFn : (x) => x;
// Dynamic, per-call macros that should be visible only for this evaluation run.
// Keys are normalized to lowercase for case-insensitive matching.
if (ctx.dynamicMacros && typeof ctx.dynamicMacros === 'object') {
env.dynamicMacros = { ...ctx.dynamicMacros };
for (const [key, value] of Object.entries(ctx.dynamicMacros)) {
env.dynamicMacros[key.toLowerCase()] = value;
}
}
// Let providers augment the env, if any are registered. Apply them in order,
+11 -9
View File
@@ -217,7 +217,7 @@ class MacroRegistry {
if (typeof aliasDef.alias !== 'string' || !aliasDef.alias.trim()) throw new Error(`Macro "${name}" options.aliases[${i}].alias must be a non-empty string.`);
const aliasName = aliasDef.alias.trim();
if (!isIdentifierValid(aliasName)) throw new Error(`Macro "${name}" options.aliases[${i}].alias "${aliasName}" is invalid. Must start with a letter, followed by word chars or hyphens.`);
if (aliasName === name) throw new Error(`Macro "${name}" options.aliases[${i}].alias cannot be the same as the macro name.`);
if (aliasName.toLowerCase() === name.toLowerCase()) throw new Error(`Macro "${name}" options.aliases[${i}].alias cannot be the same as the macro name (insensitive).`);
const visible = aliasDef.visible !== false; // Default to true
aliases.push({ alias: aliasName, visible });
}
@@ -358,7 +358,8 @@ class MacroRegistry {
}
}
if (this.#macros.has(name)) {
const nameKey = name.toLowerCase();
if (this.#macros.has(nameKey)) {
logMacroRegisterWarning({ macroName: name, message: `Macro "${name}" is already registered and will be overwritten.` });
}
@@ -390,21 +391,22 @@ class MacroRegistry {
aliasVisible: null,
};
this.#macros.set(name, definition);
this.#macros.set(nameKey, definition);
// Register alias entries pointing to the same definition
for (const { alias, visible } of aliases) {
if (this.#macros.has(alias)) {
const aliasKey = alias.toLowerCase();
if (this.#macros.has(aliasKey)) {
logMacroRegisterWarning({ macroName: name, message: `Alias "${alias}" for macro "${name}" overwrites an existing macro.` });
}
/** @type {MacroDefinition} */
const aliasEntry = {
...definition,
name: alias, // The lookup name is the alias
name: alias, // The lookup name is the alias (preserves original casing for display)
aliasOf: name,
aliasVisible: visible,
};
this.#macros.set(alias, aliasEntry);
this.#macros.set(aliasKey, aliasEntry);
}
return definition;
@@ -427,7 +429,7 @@ class MacroRegistry {
unregisterMacro(name) {
if (typeof name !== 'string' || !name.trim()) throw new Error('Macro name must be a non-empty string');
name = name.trim();
return this.#macros.delete(name);
return this.#macros.delete(name.toLowerCase());
}
/**
@@ -439,7 +441,7 @@ class MacroRegistry {
hasMacro(name) {
if (typeof name !== 'string' || !name.trim()) return false;
name = name.trim();
return this.#macros.has(name);
return this.#macros.has(name.toLowerCase());
}
/**
@@ -451,7 +453,7 @@ class MacroRegistry {
getMacro(name) {
if (typeof name !== 'string' || !name.trim()) return undefined;
name = name.trim();
return this.#macros.get(name);
return this.#macros.get(name.toLowerCase());
}
/**