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

dbc4fe611c69244b83258880ac58bd0084ded45d

Wolfsblvt <wolfsblvt@gmail.com>

Signed
4 files changed, +23 -16Ignore whitespace
public/scripts/macros/engine/MacroCstWalker.js+4 -4
@@ -157,8 +157,8 @@ class MacroCstWalker {
157157 if (!info) continue;
158158
159159 if (info.isClosing) {
160160 // Closing tag - pop matching opener from stack (case-insensitive match)
161161 if (unclosedStack.length > 0 && unclosedStack[unclosedStack.length - 1].name.toLowerCase() === info.name.toLowerCase()) {
162162 unclosedStack.pop();
163163 }
164164 // If no matching opener, ignore (orphan closing tag)
@@ -1015,8 +1015,8 @@ class MacroCstWalker {
10151015 for (let i = openingIdx + 1; i < macroInfos.length; i++) {
10161016 const info = macroInfos[i];
10171017
10181018 // Only consider macros with the same name (case-insensitive)
10191019 if (info.name.toLowerCase() !== targetName.toLowerCase()) continue;
10201020
10211021 // Skip already matched macros
10221022 if (info.matched) continue;
public/scripts/macros/engine/MacroEngine.js+4 -2
@@ -165,10 +165,12 @@ class MacroEngine {
165165 if (!name) return raw;
166166
167167 // 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.
168+ // Dynamic macro keys are normalized to lowercase for case-insensitive matching.
168169 /** @type {MacroDefinition?} */
169170 let defOverride = null;
170- if (Object.hasOwn(env.dynamicMacros, name)) {
171+ const nameLower = name.toLowerCase();
171- const impl = env.dynamicMacros[name];
172+ if (Object.hasOwn(env.dynamicMacros, nameLower)) {
173+ const impl = env.dynamicMacros[nameLower];
172174 defOverride = {
173175 name,
174176 aliases: [],
public/scripts/macros/engine/MacroEnvBuilder.js+4 -1
@@ -143,8 +143,11 @@ class MacroEnvBuilder {
143143 env.functions.postProcess = typeof ctx.postProcessFn === 'function' ? ctx.postProcessFn : (x) => x;
144144
145145 // Dynamic, per-call macros that should be visible only for this evaluation run.
146+ // Keys are normalized to lowercase for case-insensitive matching.
146147 if (ctx.dynamicMacros && typeof ctx.dynamicMacros === 'object') {
147- env.dynamicMacros = { ...ctx.dynamicMacros };
148+ for (const [key, value] of Object.entries(ctx.dynamicMacros)) {
149+ env.dynamicMacros[key.toLowerCase()] = value;
150+ }
148151 }
149152
150153 // Let providers augment the env, if any are registered. Apply them in order,
public/scripts/macros/engine/MacroRegistry.js+11 -9
@@ -217,7 +217,7 @@ class MacroRegistry {
217217 if (typeof aliasDef.alias !== 'string' || !aliasDef.alias.trim()) throw new Error(`Macro "${name}" options.aliases[${i}].alias must be a non-empty string.`);
218218 const aliasName = aliasDef.alias.trim();
219219 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.`);
220220 if (aliasName.toLowerCase() === name.toLowerCase()) throw new Error(`Macro "${name}" options.aliases[${i}].alias cannot be the same as the macro name (insensitive).`);
221221 const visible = aliasDef.visible !== false; // Default to true
222222 aliases.push({ alias: aliasName, visible });
223223 }
@@ -358,7 +358,8 @@ class MacroRegistry {
358358 }
359359 }
360360
361- if (this.#macros.has(name)) {
361+ const nameKey = name.toLowerCase();
362+ if (this.#macros.has(nameKey)) {
362363 logMacroRegisterWarning({ macroName: name, message: `Macro "${name}" is already registered and will be overwritten.` });
363364 }
364365
@@ -390,21 +391,22 @@ class MacroRegistry {
390391 aliasVisible: null,
391392 };
392393
393394 this.#macros.set(namenameKey, definition);
394395
395396 // Register alias entries pointing to the same definition
396397 for (const { alias, visible } of aliases) {
397- if (this.#macros.has(alias)) {
398+ const aliasKey = alias.toLowerCase();
399+ if (this.#macros.has(aliasKey)) {
398400 logMacroRegisterWarning({ macroName: name, message: `Alias "${alias}" for macro "${name}" overwrites an existing macro.` });
399401 }
400402 /** @type {MacroDefinition} */
401403 const aliasEntry = {
402404 ...definition,
403405 name: alias, // The lookup name is the alias (preserves original casing for display)
404406 aliasOf: name,
405407 aliasVisible: visible,
406408 };
407409 this.#macros.set(aliasaliasKey, aliasEntry);
408410 }
409411
410412 return definition;
@@ -427,7 +429,7 @@ class MacroRegistry {
427429 unregisterMacro(name) {
428430 if (typeof name !== 'string' || !name.trim()) throw new Error('Macro name must be a non-empty string');
429431 name = name.trim();
430432 return this.#macros.delete(name.toLowerCase());
431433 }
432434
433435 /**
@@ -439,7 +441,7 @@ class MacroRegistry {
439441 hasMacro(name) {
440442 if (typeof name !== 'string' || !name.trim()) return false;
441443 name = name.trim();
442444 return this.#macros.has(name.toLowerCase());
443445 }
444446
445447 /**
@@ -451,7 +453,7 @@ class MacroRegistry {
451453 getMacro(name) {
452454 if (typeof name !== 'string' || !name.trim()) return undefined;
453455 name = name.trim();
454456 return this.#macros.get(name.toLowerCase());
455457 }
456458
457459 /**