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
Signed| @@ -157,8 +157,8 @@ class MacroCstWalker { | ||
| 157 | 157 | if (!info) continue; |
| 158 | 158 | |
| 159 | 159 | if (info.isClosing) { |
| 160 | 160 | // Closing tag - pop matching opener from stack (case-insensitive match) |
| 161 | 161 | if (unclosedStack.length > 0 && unclosedStack[unclosedStack.length - 1].name.toLowerCase() === info.name.toLowerCase()) { |
| 162 | 162 | unclosedStack.pop(); |
| 163 | 163 | } |
| 164 | 164 | // If no matching opener, ignore (orphan closing tag) |
| @@ -1015,8 +1015,8 @@ class MacroCstWalker { | ||
| 1015 | 1015 | for (let i = openingIdx + 1; i < macroInfos.length; i++) { |
| 1016 | 1016 | const info = macroInfos[i]; |
| 1017 | 1017 | |
| 1018 | 1018 | // Only consider macros with the same name (case-insensitive) |
| 1019 | 1019 | if (info.name.toLowerCase() !== targetName.toLowerCase()) continue; |
| 1020 | 1020 | |
| 1021 | 1021 | // Skip already matched macros |
| 1022 | 1022 | if (info.matched) continue; |
| @@ -165,10 +165,12 @@ class MacroEngine { | ||
| 165 | 165 | if (!name) return raw; |
| 166 | 166 | |
| 167 | 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 | 169 | /** @type {MacroDefinition?} */ |
| 169 | 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 | 174 | defOverride = { |
| 173 | 175 | name, |
| 174 | 176 | aliases: [], |
| @@ -143,8 +143,11 @@ class MacroEnvBuilder { | ||
| 143 | 143 | env.functions.postProcess = typeof ctx.postProcessFn === 'function' ? ctx.postProcessFn : (x) => x; |
| 144 | 144 | |
| 145 | 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 | 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 | } |
| 149 | 152 | |
| 150 | 153 | // Let providers augment the env, if any are registered. Apply them in order, |
| @@ -217,7 +217,7 @@ class MacroRegistry { | ||
| 217 | 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 | 218 | const aliasName = aliasDef.alias.trim(); |
| 219 | 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 | 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 | 221 | const visible = aliasDef.visible !== false; // Default to true |
| 222 | 222 | aliases.push({ alias: aliasName, visible }); |
| 223 | 223 | } |
| @@ -358,7 +358,8 @@ class MacroRegistry { | ||
| 358 | 358 | } |
| 359 | 359 | } |
| 360 | 360 | |
| 361 | - if (this.#macros.has(name)) { | |
| 361 | + const nameKey = name.toLowerCase(); | |
| 362 | + if (this.#macros.has(nameKey)) { | |
| 362 | 363 | logMacroRegisterWarning({ macroName: name, message: `Macro "${name}" is already registered and will be overwritten.` }); |
| 363 | 364 | } |
| 364 | 365 | |
| @@ -390,21 +391,22 @@ class MacroRegistry { | ||
| 390 | 391 | aliasVisible: null, |
| 391 | 392 | }; |
| 392 | 393 | |
| 393 | 394 | this.#macros.set(namenameKey, definition); |
| 394 | 395 | |
| 395 | 396 | // Register alias entries pointing to the same definition |
| 396 | 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 | 400 | logMacroRegisterWarning({ macroName: name, message: `Alias "${alias}" for macro "${name}" overwrites an existing macro.` }); |
| 399 | 401 | } |
| 400 | 402 | /** @type {MacroDefinition} */ |
| 401 | 403 | const aliasEntry = { |
| 402 | 404 | ...definition, |
| 403 | 405 | name: alias, // The lookup name is the alias (preserves original casing for display) |
| 404 | 406 | aliasOf: name, |
| 405 | 407 | aliasVisible: visible, |
| 406 | 408 | }; |
| 407 | 409 | this.#macros.set(aliasaliasKey, aliasEntry); |
| 408 | 410 | } |
| 409 | 411 | |
| 410 | 412 | return definition; |
| @@ -427,7 +429,7 @@ class MacroRegistry { | ||
| 427 | 429 | unregisterMacro(name) { |
| 428 | 430 | if (typeof name !== 'string' || !name.trim()) throw new Error('Macro name must be a non-empty string'); |
| 429 | 431 | name = name.trim(); |
| 430 | 432 | return this.#macros.delete(name.toLowerCase()); |
| 431 | 433 | } |
| 432 | 434 | |
| 433 | 435 | /** |
| @@ -439,7 +441,7 @@ class MacroRegistry { | ||
| 439 | 441 | hasMacro(name) { |
| 440 | 442 | if (typeof name !== 'string' || !name.trim()) return false; |
| 441 | 443 | name = name.trim(); |
| 442 | 444 | return this.#macros.has(name.toLowerCase()); |
| 443 | 445 | } |
| 444 | 446 | |
| 445 | 447 | /** |
| @@ -451,7 +453,7 @@ class MacroRegistry { | ||
| 451 | 453 | getMacro(name) { |
| 452 | 454 | if (typeof name !== 'string' || !name.trim()) return undefined; |
| 453 | 455 | name = name.trim(); |
| 454 | 456 | return this.#macros.get(name.toLowerCase()); |
| 455 | 457 | } |
| 456 | 458 | |
| 457 | 459 | /** |