Macros 2.0 [Fix] - Macro display override uses alias name if relevant + preserve whitespaces for autocomplete closing tag (#4953) * Refactor: Move MacroBrowser.js to macros/engine directory and update import paths * Replace macro name with alias in displayOverride for macro browser - Import escapeRegex utility for safe regex pattern creation - Check if macro has aliasOf property when displayOverride is present - Replace all occurrences of the original macro name with the alias name in displayOverride text - Use word boundary regex pattern to match macro names in {{macro}}, {{macro::}}, and {{macro }} contexts * Add whitespace padding preservation for macro closing tag autocomplete - Extract paddingBefore and paddingAfter from opening macro tags in MacroCstWalker - Pass padding info through unclosed scope detection in findUnclosedScopes() and findUnclosedScopesRegex() - Update MacroClosingTagAutoCompleteOption constructor to accept padding options - Apply same whitespace padding to closing tags as their corresponding opening tags - Update regex fallback to capture and preserve whitespace from opening tags * Add replacementStartOffset to AutoCompleteOption for whitespace normalization in closing tags - Add replacementStartOffset property to AutoCompleteOption class with JSDoc documentation - Update AutoComplete.select() to apply per-option replacement offset when calculating effectiveStart - Calculate replacementStartOffset in MacroClosingTagAutoCompleteOption based on currentPadding length - Pass currentPadding from context.paddingBefore to MacroClosingTagAutoCompleteOption in SlashCommandParser * Fix closing tag detection regex to correctly autocomplete // macro * Add flags and fullText to EnhancedMacroAutoCompleteOption context and fix {{//}} autocomplete offset - Add flags, currentFlag, and fullText properties to EnhancedMacroAutoCompleteOptions JSDoc - Store options object as #options field in EnhancedMacroAutoCompleteOption for later access - Pass flags, currentFlag, and fullText from context to macroContext in SlashCommandParser - Fix {{//}} macro autocomplete to adjust replacementStartOffset when typed after single slash to avoid treating it as flag * Add unnamedArgs definition to {{//}} comment macro for better documentation --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

b453fdc5d86898c9e64343c7541be49391b8d0fa

Wolfsblvt <wolfsblvt@gmail.com>

Signed
8 files changed, +141 -29Showing whitespace changes
public/scripts/autocomplete/AutoComplete.js+4 -2
@@ -692,8 +692,10 @@ export class AutoComplete {
692 */692 */
693 async select() {693 async select() {
694 if (this.isReplaceable && this.selectedItem.value !== null) {694 if (this.isReplaceable && this.selectedItem.value !== null) {
695 this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;695 // Apply per-option replacement offset (e.g., for closing tags that need to replace leading whitespace)
696 this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length;696 const effectiveStart = this.effectiveParserResult.start + (this.selectedItem.replacementStartOffset ?? 0);
697 this.textarea.value = `${this.text.slice(0, effectiveStart)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
698 this.textarea.selectionStart = effectiveStart + this.selectedItem.replacer.length;
697 this.textarea.selectionEnd = this.textarea.selectionStart;699 this.textarea.selectionEnd = this.textarea.selectionStart;
698 this.show(false, false, true);700 this.show(false, false, true);
699 } else {701 } else {
public/scripts/autocomplete/AutoCompleteOption.js+9 -0
@@ -13,6 +13,15 @@ export class AutoCompleteOption {
13 /** @type {(input:string)=>boolean} */ matchProvider;13 /** @type {(input:string)=>boolean} */ matchProvider;
14 /** @type {(input:string)=>string} */ valueProvider;14 /** @type {(input:string)=>string} */ valueProvider;
15 /** @type {boolean} */ makeSelectable = false;15 /** @type {boolean} */ makeSelectable = false;
16
17 /**
18 * Offset to adjust the replacement start position.
19 * Negative values start replacement earlier (e.g., -2 to include 2 chars before normal start).
20 * Used by closing tag autocomplete to replace leading whitespace.
21 * @type {number}
22 */
23 replacementStartOffset = 0;
24
16 /**25 /**
17 * Priority for sorting. Lower values = higher priority (sorted first).26 * Priority for sorting. Lower values = higher priority (sorted first).
18 * Default is 100 (normal priority). Use lower values for items that should appear at the top.27 * Default is 100 (normal priority). Use lower values for items that should appear at the top.
public/scripts/autocomplete/EnhancedMacroAutoCompleteOption.js+40 -10
@@ -9,7 +9,7 @@ import {
9 createSourceIndicator,9 createSourceIndicator,
10 createAliasIndicator,10 createAliasIndicator,
11 renderMacroDetails,11 renderMacroDetails,
12} from '../macros/MacroBrowser.js';12} from '../macros/engine/MacroBrowser.js';
13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';13import { enumIcons } from '../slash-commands/SlashCommandCommonEnumsProvider.js';
14import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js';14import { ValidFlagSymbols } from '../macros/engine/MacroFlags.js';
15import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js';15import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js';
@@ -54,6 +54,9 @@ import { MACRO_VARIABLE_SHORTHAND_PATTERN } from '../macros/engine/MacroLexer.js
54 * @property {boolean} [noBraces=false] - If true, display without {{ }} braces (for use as values, e.g., in {{if}} conditions).54 * @property {boolean} [noBraces=false] - If true, display without {{ }} braces (for use as values, e.g., in {{if}} conditions).
55 * @property {string} [paddingAfter=''] - Whitespace to add before closing }} (for matching opening whitespace style).55 * @property {string} [paddingAfter=''] - Whitespace to add before closing }} (for matching opening whitespace style).
56 * @property {boolean} [closeWithBraces=false] - If true, the completion will add }} to close the macro.56 * @property {boolean} [closeWithBraces=false] - If true, the completion will add }} to close the macro.
57 * @property {string[]} [flags=[]] - The currently already written flags for this autocomplete.
58 * @property {string} [currentFlag] - The current flag that is present, if any.
59 * @property {string} [fullText] - The currently written full text.
57 */60 */
5861
59export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {62export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
@@ -63,6 +66,9 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
63 /** @type {MacroAutoCompleteContext|null} */66 /** @type {MacroAutoCompleteContext|null} */
64 #context = null;67 #context = null;
6568
69 /** @type {EnhancedMacroAutoCompleteOptions|null} */
70 #options = null;
71
66 /** @type {boolean} */72 /** @type {boolean} */
67 #noBraces = false;73 #noBraces = false;
6874
@@ -83,12 +89,12 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
83 if (contextOrOptions && typeof contextOrOptions === 'object') {89 if (contextOrOptions && typeof contextOrOptions === 'object') {
84 if ('noBraces' in contextOrOptions || 'paddingAfter' in contextOrOptions || 'closeWithBraces' in contextOrOptions) {90 if ('noBraces' in contextOrOptions || 'paddingAfter' in contextOrOptions || 'closeWithBraces' in contextOrOptions) {
85 // It's an options object91 // It's an options object
86 const options = /** @type {EnhancedMacroAutoCompleteOptions} */ (contextOrOptions);92 this.#options = /** @type {EnhancedMacroAutoCompleteOptions} */ (contextOrOptions);
87 this.#noBraces = options.noBraces ?? false;93 this.#noBraces = this.#options.noBraces ?? false;
88 this.#paddingAfter = options.paddingAfter ?? '';94 this.#paddingAfter = this.#options.paddingAfter ?? '';
8995
90 // If noBraces mode with closeWithBraces, complete with name + padding + }}96 // If noBraces mode with closeWithBraces, complete with name + padding + }}
91 if (options.closeWithBraces) {97 if (this.#options.closeWithBraces) {
92 this.valueProvider = () => `${macro.name}${this.#paddingAfter}}}`;98 this.valueProvider = () => `${macro.name}${this.#paddingAfter}}}`;
93 this.makeSelectable = true;99 this.makeSelectable = true;
94 }100 }
@@ -110,6 +116,12 @@ export class EnhancedMacroAutoCompleteOption extends AutoCompleteOption {
110 this.makeSelectable = true; // Required when using valueProvider116 this.makeSelectable = true; // Required when using valueProvider
111 }117 }
112 }118 }
119
120 // {{//}} needs special handling. If we autocomplete right after **one** slash is already typed, we need to replace that, as it's treated as a flag otherwise.
121 const fullText = this.#options?.fullText ?? this.#context?.fullText ?? '';
122 if (macro.name === '//' && fullText.endsWith('/')) {
123 this.replacementStartOffset = (this.replacementStartOffset ?? 0) - 1; // Cut the leading slash
124 }
113 }125 }
114126
115 /** @returns {MacroDefinition} */127 /** @returns {MacroDefinition} */
@@ -907,20 +919,38 @@ export class MacroClosingTagAutoCompleteOption extends AutoCompleteOption {
907 /** @type {string} */919 /** @type {string} */
908 #macroName;920 #macroName;
909921
922 /** @type {string} */
923 #paddingBefore;
924
925 /** @type {string} */
926 #paddingAfter;
927
910 /**928 /**
911 * @param {string} macroName - The name of the macro to close.929 * @param {string} macroName - The name of the macro to close.
930 * @param {Object} [options] - Optional configuration.
931 * @param {string} [options.paddingBefore=''] - Whitespace after {{ in opening tag (target padding).
932 * @param {string} [options.paddingAfter=''] - Whitespace before }} in opening tag (target padding).
933 * @param {string} [options.currentPadding=''] - Whitespace the user has already typed after {{.
912 */934 */
913 constructor(macroName) {935 constructor(macroName, options = {}) {
914 // The closing tag is what we're suggesting - use /macroName as the name for matching936 // The closing tag is what we're suggesting - use /macroName as the name for matching
915 const closingTag = `/${macroName}`;937 const closingTag = `/${macroName}`;
916 super(closingTag, '{/');938 super(closingTag, '{/');
917 this.#macroName = macroName;939 this.#macroName = macroName;
940 this.#paddingBefore = options.paddingBefore ?? '';
941 this.#paddingAfter = options.paddingAfter ?? '';
942
943 // Calculate the replacement offset to replace any existing whitespace the user typed
944 // This allows us to normalize the whitespace to match the opening tag's style
945 const currentPadding = options.currentPadding ?? '';
946 // Negative offset to start replacement earlier (eating the user's whitespace)
947 this.replacementStartOffset = -currentPadding.length;
918948
919 // Custom valueProvider to return the correct replacement text949 // Custom valueProvider to return the correct replacement text
920 // Autocomplete REPLACES the typed identifier entirely, so return the full closing tag950 // Includes the target paddingBefore from the opening tag, replacing any user-typed whitespace
921 this.valueProvider = () => {951 this.valueProvider = () => {
922 // Return full closing tag content (without {{ since that's before the identifier)952 // Return: paddingBefore + /macroName + paddingAfter + }}
923 return `/${macroName}}}`;953 return `${this.#paddingBefore}/${macroName}${this.#paddingAfter}}}`;
924 };954 };
925955
926 // Make selectable so TAB completion works (valueProvider alone makes it non-selectable)956 // Make selectable so TAB completion works (valueProvider alone makes it non-selectable)
@@ -1033,7 +1063,7 @@ export function parseMacroContext(macroText, cursorOffset) {
1033 while (i < macroText.length) {1063 while (i < macroText.length) {
1034 const char = macroText[i];1064 const char = macroText[i];
1035 // Check if this looks like a closing tag: `/` followed by an identifier character1065 // Check if this looks like a closing tag: `/` followed by an identifier character
1036 if (char === '/' && i + 1 < macroText.length && /[a-zA-Z_]/.test(macroText[i + 1])) {1066 if (char === '/' && i + 1 < macroText.length && /[a-zA-Z/]/.test(macroText[i + 1])) {
1037 // This is a closing tag identifier, not a flag - stop parsing flags1067 // This is a closing tag identifier, not a flag - stop parsing flags
1038 break;1068 break;
1039 }1069 }
public/scripts/macros/definitions/core-macros.js+7 -0
@@ -261,6 +261,13 @@ export function registerCoreMacros() {
261 MacroRegistry.registerMacro('//', {261 MacroRegistry.registerMacro('//', {
262 aliases: [{ alias: 'comment', visible: false }],262 aliases: [{ alias: 'comment', visible: false }],
263 category: MacroCategory.UTILITY,263 category: MacroCategory.UTILITY,
264 unnamedArgs: [
265 {
266 name: 'comment',
267 type: MacroValueType.STRING,
268 description: 'Any kind of text as comment. If you want multiline comments, consider using a scoped macro like {{//}}First\nSecond{{///}}.',
269 },
270 ],
264 list: true, // We consume any arguments as if this is a list, but we'll ignore them in the handler anyway271 list: true, // We consume any arguments as if this is a list, but we'll ignore them in the handler anyway
265 strictArgs: false, // and we also always remove it, even if the parsing might say it's invalid272 strictArgs: false, // and we also always remove it, even if the parsing might say it's invalid
266 description: 'Comment macro that produces an empty string. Can be used for writing into prompt definitions, without being passed to the context.',273 description: 'Comment macro that produces an empty string. Can be used for writing into prompt definitions, without being passed to the context.',
public/scripts/macros/MacroBrowser.js → public/scripts/macros/engine/MacroBrowser.js+10 -4
@@ -3,11 +3,12 @@
3 * Similar to SlashCommandBrowser but for the macro system.3 * Similar to SlashCommandBrowser but for the macro system.
4 */4 */
55
6import { MacroRegistry, MacroCategory } from './engine/MacroRegistry.js';6import { MacroRegistry, MacroCategory } from './MacroRegistry.js';
7import { performFuzzySearch } from '../power-user.js';7import { performFuzzySearch } from '../../power-user.js';
8import { escapeRegex } from '/scripts/utils.js';
89
9/** @typedef {import('./engine/MacroRegistry.js').MacroDefinition} MacroDefinition */10/** @typedef {import('./MacroRegistry.js').MacroDefinition} MacroDefinition */
10/** @typedef {import('./engine/MacroRegistry.js').MacroValueType} MacroValueType */11/** @typedef {import('./MacroRegistry.js').MacroValueType} MacroValueType */
1112
12/**13/**
13 * Category display names and order for documentation.14 * Category display names and order for documentation.
@@ -328,6 +329,11 @@ function getCategoryConfig(category) {
328export function formatMacroSignature(macro) {329export function formatMacroSignature(macro) {
329 // Use displayOverride if provided330 // Use displayOverride if provided
330 if (macro.displayOverride) {331 if (macro.displayOverride) {
332 if (macro.aliasOf) {
333 // Replace all occurrences of the macro name with the alias for this list
334 const escapedMainName = escapeRegex(macro.aliasOf);
335 return macro.displayOverride.replace(new RegExp(`(?<=[\\b{\\s])${escapedMainName}(?=[\\b}:\\s])`, 'g'), `${macro.name}`);
336 }
331 return macro.displayOverride;337 return macro.displayOverride;
332 }338 }
333339
public/scripts/macros/engine/MacroCstWalker.js+40 -2
@@ -190,7 +190,7 @@ class MacroCstWalker {
190 * @param {Object} options190 * @param {Object} options
191 * @param {string} options.text - The document text.191 * @param {string} options.text - The document text.
192 * @param {CstNode} options.cst - The parsed CST.192 * @param {CstNode} options.cst - The parsed CST.
193 * @returns {Array<{ name: string, startOffset: number, endOffset: number }>} - Array of unclosed macro info, innermost last.193 * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>} - Array of unclosed macro info, innermost last.
194 */194 */
195 findUnclosedScopes(options) {195 findUnclosedScopes(options) {
196 const { text, cst } = options;196 const { text, cst } = options;
@@ -203,7 +203,7 @@ class MacroCstWalker {
203 // Don't process scoped macros - we want to find the raw opening/closing pairs203 // Don't process scoped macros - we want to find the raw opening/closing pairs
204 // Just extract macro info and find unmatched openers204 // Just extract macro info and find unmatched openers
205205
206 /** @type {Array<{ name: string, startOffset: number, endOffset: number }>} */206 /** @type {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>} */
207 const unclosedStack = [];207 const unclosedStack = [];
208208
209 // Extract macro names and closing status209 // Extract macro names and closing status
@@ -222,10 +222,15 @@ class MacroCstWalker {
222 } else {222 } else {
223 // Opening tag - check if this macro can accept scoped content223 // Opening tag - check if this macro can accept scoped content
224 if (this.#canAcceptScopedContent(item.node, info.name)) {224 if (this.#canAcceptScopedContent(item.node, info.name)) {
225 // Extract whitespace padding from the macro
226 const { paddingBefore, paddingAfter } = this.#extractMacroPadding(item.node, text);
227
225 unclosedStack.push({228 unclosedStack.push({
226 name: info.name,229 name: info.name,
227 startOffset: item.startOffset,230 startOffset: item.startOffset,
228 endOffset: item.endOffset,231 endOffset: item.endOffset,
232 paddingBefore,
233 paddingAfter,
229 });234 });
230 }235 }
231 }236 }
@@ -234,6 +239,39 @@ class MacroCstWalker {
234 return unclosedStack;239 return unclosedStack;
235 }240 }
236241
242 /**
243 * Extracts the whitespace padding from a macro node.
244 * Returns the whitespace after {{ and before }}.
245 *
246 * @param {CstNode} macroNode - The macro CST node.
247 * @param {string} text - The source text.
248 * @returns {{ paddingBefore: string, paddingAfter: string }}
249 */
250 #extractMacroPadding(macroNode, text) {
251 const children = macroNode.children || {};
252 const startToken = /** @type {IToken?} */ ((children['Macro.Start'] || [])[0]);
253 const endToken = /** @type {IToken?} */ ((children['Macro.End'] || [])[0]);
254
255 if (!startToken || !endToken) {
256 return { paddingBefore: '', paddingAfter: '' };
257 }
258
259 // Get the raw text inside the macro (between {{ and }})
260 const innerStart = startToken.endOffset + 1;
261 const innerEnd = endToken.startOffset;
262 const innerText = text.slice(innerStart, innerEnd);
263
264 // Extract leading whitespace (paddingBefore)
265 const leadingMatch = innerText.match(/^(\s*)/);
266 const paddingBefore = leadingMatch ? leadingMatch[1] : '';
267
268 // Extract trailing whitespace (paddingAfter)
269 const trailingMatch = innerText.match(/(\s*)$/);
270 const paddingAfter = trailingMatch ? trailingMatch[1] : '';
271
272 return { paddingBefore, paddingAfter };
273 }
274
237 /** @typedef {{ type: 'plaintext', startOffset: number, endOffset: number, token: IToken }} DocumentItemPlaintext */275 /** @typedef {{ type: 'plaintext', startOffset: number, endOffset: number, token: IToken }} DocumentItemPlaintext */
238 /** @typedef {{ type: 'macro', startOffset: number, endOffset: number, node: CstNode, scopedContent?: { startOffset: number, endOffset: number, closingEndOffset: number }, keepRaw?: boolean }} DocumentItemMacro */276 /** @typedef {{ type: 'macro', startOffset: number, endOffset: number, node: CstNode, scopedContent?: { startOffset: number, endOffset: number, closingEndOffset: number }, keepRaw?: boolean }} DocumentItemMacro */
239 /** @typedef {DocumentItemPlaintext | DocumentItemMacro} DocumentItem */277 /** @typedef {DocumentItemPlaintext | DocumentItemMacro} DocumentItem */
public/scripts/slash-commands/SlashCommandParser.js+30 -10
@@ -808,7 +808,13 @@ export class SlashCommandParser {
808 if (unclosedScopes.length > 0) {808 if (unclosedScopes.length > 0) {
809 // Suggest closing the innermost (last) unclosed scope first809 // Suggest closing the innermost (last) unclosed scope first
810 const innermostScope = unclosedScopes[unclosedScopes.length - 1];810 const innermostScope = unclosedScopes[unclosedScopes.length - 1];
811 const closingOption = new MacroClosingTagAutoCompleteOption(innermostScope.name);811 // Preserve whitespace padding from the opening tag
812 // Pass currentPadding so the closing tag can replace user-typed whitespace with the target padding
813 const closingOption = new MacroClosingTagAutoCompleteOption(innermostScope.name, {
814 paddingBefore: innermostScope.paddingBefore,
815 paddingAfter: innermostScope.paddingAfter,
816 currentPadding: context.paddingBefore,
817 });
812 options.push(closingOption);818 options.push(closingOption);
813819
814 // If inside a scoped {{if}}, also suggest {{else}}820 // If inside a scoped {{if}}, also suggest {{else}}
@@ -891,6 +897,9 @@ export class SlashCommandParser {
891 if (!macroContext) {897 if (!macroContext) {
892 macroContext = /** @type {EnhancedMacroAutoCompleteOptions} */ ({898 macroContext = /** @type {EnhancedMacroAutoCompleteOptions} */ ({
893 paddingAfter: context.paddingBefore, // Match whitespace before the macro - will only be used if the macro gets auto-closed899 paddingAfter: context.paddingBefore, // Match whitespace before the macro - will only be used if the macro gets auto-closed
900 flags: context.flags,
901 currentFlag: context.currentFlag,
902 fullText: context.fullText,
894 });903 });
895 }904 }
896905
@@ -1216,7 +1225,7 @@ export class SlashCommandParser {
1216 * Uses the MacroParser and MacroCstWalker for accurate analysis.1225 * Uses the MacroParser and MacroCstWalker for accurate analysis.
1217 *1226 *
1218 * @param {string} textUpToCursor - The document text up to the cursor position.1227 * @param {string} textUpToCursor - The document text up to the cursor position.
1219 * @returns {Array<{ name: string, startOffset: number, endOffset: number }>}1228 * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>}
1220 */1229 */
1221 #findUnclosedScopes(textUpToCursor) {1230 #findUnclosedScopes(textUpToCursor) {
1222 if (!textUpToCursor) return [];1231 if (!textUpToCursor) return [];
@@ -1239,32 +1248,43 @@ export class SlashCommandParser {
1239 * Used when the parser fails on incomplete input.1248 * Used when the parser fails on incomplete input.
1240 *1249 *
1241 * @param {string} text - The text to analyze.1250 * @param {string} text - The text to analyze.
1242 * @returns {Array<{ name: string, startOffset: number, endOffset: number }>}1251 * @returns {Array<{ name: string, startOffset: number, endOffset: number, paddingBefore: string, paddingAfter: string }>}
1243 */1252 */
1244 #findUnclosedScopesRegex(text) {1253 #findUnclosedScopesRegex(text) {
1245 // Simple regex to find macro openings and closings1254 // Regex to find macro openings and closings, capturing whitespace padding
1246 // This is a fallback - less accurate but works on partial input1255 // Group 1: padding after {{, Group 2: optional /, Group 3: macro name
1247 const macroPattern = /\{\{(\/?)([\w-]+)/g;1256 const macroPattern = /\{\{(\s*)(\/?)([\w-]+)/g;
1248 const stack = [];1257 const stack = [];
12491258
1250 let match;1259 let match;
1251 while ((match = macroPattern.exec(text)) !== null) {1260 while ((match = macroPattern.exec(text)) !== null) {
1252 const isClosing = match[1] === '/';1261 const paddingBefore = match[1];
1253 const name = match[2];1262 const isClosing = match[2] === '/';
1263 const name = match[3];
12541264
1255 if (isClosing) {1265 if (isClosing) {
1256 // Pop matching opener1266 // Pop matching opener (case-insensitive)
1257 if (stack.length > 0 && stack[stack.length - 1].name === name) {1267 if (stack.length > 0 && stack[stack.length - 1].name.toLowerCase() === name.toLowerCase()) {
1258 stack.pop();1268 stack.pop();
1259 }1269 }
1260 } else {1270 } else {
1261 // Check if macro can accept scoped content1271 // Check if macro can accept scoped content
1262 const macroDef = macroSystem.registry.getPrimaryMacro(name);1272 const macroDef = macroSystem.registry.getPrimaryMacro(name);
1263 if (macroDef && macroDef.maxArgs > 0) {1273 if (macroDef && macroDef.maxArgs > 0) {
1274 // Try to find closing }} to extract trailing whitespace
1275 let paddingAfter = '';
1276 const afterMatch = text.slice(match.index + match[0].length);
1277 const closingMatch = afterMatch.match(/^[^}]*?(\s*)\}\}/);
1278 if (closingMatch) {
1279 paddingAfter = closingMatch[1];
1280 }
1281
1264 stack.push({1282 stack.push({
1265 name,1283 name,
1266 startOffset: match.index,1284 startOffset: match.index,
1267 endOffset: match.index + match[0].length,1285 endOffset: match.index + match[0].length,
1286 paddingBefore,
1287 paddingAfter,
1268 });1288 });
1269 }1289 }
1270 }1290 }
public/scripts/system-messages.js+1 -1
@@ -4,7 +4,7 @@ import { t } from './i18n.js';
4import { getMessageTimeStamp } from './RossAscends-mods.js';4import { getMessageTimeStamp } from './RossAscends-mods.js';
5import { getSlashCommandsHelp } from './slash-commands.js';5import { getSlashCommandsHelp } from './slash-commands.js';
6import { SlashCommandBrowser } from './slash-commands/SlashCommandBrowser.js';6import { SlashCommandBrowser } from './slash-commands/SlashCommandBrowser.js';
7import { MacroBrowser, getMacrosHelp } from './macros/MacroBrowser.js';7import { MacroBrowser, getMacrosHelp } from './macros/engine/MacroBrowser.js';
8import { renderTemplateAsync } from './templates.js';8import { renderTemplateAsync } from './templates.js';
99
10/** @type {Record<string, ChatMessage>} */10/** @type {Record<string, ChatMessage>} */