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 -16Showing whitespace changes
public/scripts/macros/engine/MacroCstWalker.js+4 -4
@@ -157,8 +157,8 @@ class MacroCstWalker {
157 if (!info) continue;157 if (!info) continue;
158158
159 if (info.isClosing) {159 if (info.isClosing) {
160 // Closing tag - pop matching opener from stack160 // Closing tag - pop matching opener from stack (case-insensitive match)
161 if (unclosedStack.length > 0 && unclosedStack[unclosedStack.length - 1].name === info.name) {161 if (unclosedStack.length > 0 && unclosedStack[unclosedStack.length - 1].name.toLowerCase() === info.name.toLowerCase()) {
162 unclosedStack.pop();162 unclosedStack.pop();
163 }163 }
164 // If no matching opener, ignore (orphan closing tag)164 // If no matching opener, ignore (orphan closing tag)
@@ -1015,8 +1015,8 @@ class MacroCstWalker {
1015 for (let i = openingIdx + 1; i < macroInfos.length; i++) {1015 for (let i = openingIdx + 1; i < macroInfos.length; i++) {
1016 const info = macroInfos[i];1016 const info = macroInfos[i];
10171017
1018 // Only consider macros with the same name1018 // Only consider macros with the same name (case-insensitive)
1019 if (info.name !== targetName) continue;1019 if (info.name.toLowerCase() !== targetName.toLowerCase()) continue;
10201020
1021 // Skip already matched macros1021 // Skip already matched macros
1022 if (info.matched) continue;1022 if (info.matched) continue;
public/scripts/macros/engine/MacroEngine.js+4 -2
@@ -165,10 +165,12 @@ class MacroEngine {
165 if (!name) return raw;165 if (!name) return raw;
166166
167 // 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.167 // 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.
168 /** @type {MacroDefinition?} */169 /** @type {MacroDefinition?} */
169 let defOverride = null;170 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];
172 defOverride = {174 defOverride = {
173 name,175 name,
174 aliases: [],176 aliases: [],
public/scripts/macros/engine/MacroEnvBuilder.js+4 -1
@@ -143,8 +143,11 @@ class MacroEnvBuilder {
143 env.functions.postProcess = typeof ctx.postProcessFn === 'function' ? ctx.postProcessFn : (x) => x;143 env.functions.postProcess = typeof ctx.postProcessFn === 'function' ? ctx.postProcessFn : (x) => x;
144144
145 // Dynamic, per-call macros that should be visible only for this evaluation run.145 // Dynamic, per-call macros that should be visible only for this evaluation run.
146 // Keys are normalized to lowercase for case-insensitive matching.
146 if (ctx.dynamicMacros && typeof ctx.dynamicMacros === 'object') {147 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 }
148 }151 }
149152
150 // Let providers augment the env, if any are registered. Apply them in order,153 // 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 {
217 if (typeof aliasDef.alias !== 'string' || !aliasDef.alias.trim()) throw new Error(`Macro "${name}" options.aliases[${i}].alias must be a non-empty string.`);217 if (typeof aliasDef.alias !== 'string' || !aliasDef.alias.trim()) throw new Error(`Macro "${name}" options.aliases[${i}].alias must be a non-empty string.`);
218 const aliasName = aliasDef.alias.trim();218 const aliasName = aliasDef.alias.trim();
219 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.`);219 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.`);
220 if (aliasName === name) throw new Error(`Macro "${name}" options.aliases[${i}].alias cannot be the same as the macro name.`);220 if (aliasName.toLowerCase() === name.toLowerCase()) throw new Error(`Macro "${name}" options.aliases[${i}].alias cannot be the same as the macro name (insensitive).`);
221 const visible = aliasDef.visible !== false; // Default to true221 const visible = aliasDef.visible !== false; // Default to true
222 aliases.push({ alias: aliasName, visible });222 aliases.push({ alias: aliasName, visible });
223 }223 }
@@ -358,7 +358,8 @@ class MacroRegistry {
358 }358 }
359 }359 }
360360
361 if (this.#macros.has(name)) {361 const nameKey = name.toLowerCase();
362 if (this.#macros.has(nameKey)) {
362 logMacroRegisterWarning({ macroName: name, message: `Macro "${name}" is already registered and will be overwritten.` });363 logMacroRegisterWarning({ macroName: name, message: `Macro "${name}" is already registered and will be overwritten.` });
363 }364 }
364365
@@ -390,21 +391,22 @@ class MacroRegistry {
390 aliasVisible: null,391 aliasVisible: null,
391 };392 };
392393
393 this.#macros.set(name, definition);394 this.#macros.set(nameKey, definition);
394395
395 // Register alias entries pointing to the same definition396 // Register alias entries pointing to the same definition
396 for (const { alias, visible } of aliases) {397 for (const { alias, visible } of aliases) {
397 if (this.#macros.has(alias)) {398 const aliasKey = alias.toLowerCase();
399 if (this.#macros.has(aliasKey)) {
398 logMacroRegisterWarning({ macroName: name, message: `Alias "${alias}" for macro "${name}" overwrites an existing macro.` });400 logMacroRegisterWarning({ macroName: name, message: `Alias "${alias}" for macro "${name}" overwrites an existing macro.` });
399 }401 }
400 /** @type {MacroDefinition} */402 /** @type {MacroDefinition} */
401 const aliasEntry = {403 const aliasEntry = {
402 ...definition,404 ...definition,
403 name: alias, // The lookup name is the alias405 name: alias, // The lookup name is the alias (preserves original casing for display)
404 aliasOf: name,406 aliasOf: name,
405 aliasVisible: visible,407 aliasVisible: visible,
406 };408 };
407 this.#macros.set(alias, aliasEntry);409 this.#macros.set(aliasKey, aliasEntry);
408 }410 }
409411
410 return definition;412 return definition;
@@ -427,7 +429,7 @@ class MacroRegistry {
427 unregisterMacro(name) {429 unregisterMacro(name) {
428 if (typeof name !== 'string' || !name.trim()) throw new Error('Macro name must be a non-empty string');430 if (typeof name !== 'string' || !name.trim()) throw new Error('Macro name must be a non-empty string');
429 name = name.trim();431 name = name.trim();
430 return this.#macros.delete(name);432 return this.#macros.delete(name.toLowerCase());
431 }433 }
432434
433 /**435 /**
@@ -439,7 +441,7 @@ class MacroRegistry {
439 hasMacro(name) {441 hasMacro(name) {
440 if (typeof name !== 'string' || !name.trim()) return false;442 if (typeof name !== 'string' || !name.trim()) return false;
441 name = name.trim();443 name = name.trim();
442 return this.#macros.has(name);444 return this.#macros.has(name.toLowerCase());
443 }445 }
444446
445 /**447 /**
@@ -451,7 +453,7 @@ class MacroRegistry {
451 getMacro(name) {453 getMacro(name) {
452 if (typeof name !== 'string' || !name.trim()) return undefined;454 if (typeof name !== 'string' || !name.trim()) return undefined;
453 name = name.trim();455 name = name.trim();
454 return this.#macros.get(name);456 return this.#macros.get(name.toLowerCase());
455 }457 }
456458
457 /**459 /**