Merge pull request #3035 from SillyTavern/macro-1.5 Totally not a new macro engine

237381ac3d0db93bbc6d0c8a963813546afa2f73

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
5 files changed, +196 -199Ignore whitespace
public/script.js+6 -4
@@ -2522,10 +2522,11 @@ export function scrollChatToBottom() {
25222522 * Substitutes {{macro}} parameters in a string.
25232523 * @param {string} content - The string to substitute parameters in.
25242524 * @param {Record<string,any>} additionalMacro - Additional environment variables for substitution.
2525+ * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
25252526 * @returns {string} The string with substituted parameters.
25262527 */
25272528export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) {
25282529 return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro, postProcessFn);
25292530}
25302531
25312532/**
@@ -2537,9 +2538,10 @@ export function substituteParamsExtended(content, additionalMacro = {}) {
25372538 * @param {string} [_group] - The group members list for {{group}} substitution.
25382539 * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros.
25392540 * @param {Record<string,any>} [additionalMacro] - Additional environment variables for substitution.
2541+ * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
25402542 * @returns {string} The string with substituted parameters.
25412543 */
25422544export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) {
25432545 if (!content) {
25442546 return '';
25452547 }
@@ -2597,7 +2599,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
25972599 Object.assign(environment, additionalMacro);
25982600 }
25992601
26002602 return evaluateMacros(content, environment, postProcessFn);
26012603}
26022604
26032605
public/scripts/extensions/regex/engine.js+27 -2
@@ -1,4 +1,4 @@
11import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js';
22import { extension_settings } from '../../extensions.js';
33import { regexFromString } from '../../utils.js';
44export {
@@ -22,6 +22,28 @@ const regex_placement = {
2222 WORLD_INFO: 5,
2323};
2424
25+function sanitizeRegexMacro(x) {
26+ return (x && typeof x === 'string') ?
27+ x.replaceAll(/[\n\r\t\v\f\0.^$*+?{}[\]\\/|()]/gs, function (s) {
28+ switch (s) {
29+ case '\n':
30+ return '\\n';
31+ case '\r':
32+ return '\\r';
33+ case '\t':
34+ return '\\t';
35+ case '\v':
36+ return '\\v';
37+ case '\f':
38+ return '\\f';
39+ case '\0':
40+ return '\\0';
41+ default:
42+ return '\\' + s;
43+ }
44+ }) : x;
45+}
46+
2547function getScopedRegex() {
2648 const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
2749
@@ -109,7 +131,10 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
109131 return newString;
110132 }
111133
112- const findRegex = regexFromString(regexScript.substituteRegex ? substituteParams(regexScript.findRegex) : regexScript.findRegex);
134+ const regexString = regexScript.substituteRegex
135+ ? substituteParamsExtended(regexScript.findRegex, {}, sanitizeRegexMacro)
136+ : regexScript.findRegex;
137+ const findRegex = regexFromString(regexString);
113138
114139 // The user skill issued. Return with nothing.
115140 if (!findRegex) {
public/scripts/instruct-mode.js+14 -22
@@ -565,22 +565,13 @@ function selectMatchingContextTemplate(name) {
565565
566566/**
567567 * Replaces instruct mode macros in the given input string.
568- * @param {string} input Input string.
569568 * @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param
570569 * values are functions, those functions will be called and their return values are used.
571- * @returns {string} String with macros replaced.
570+ * @returns {import('./macros.js').Macro[]} Macro objects.
572571 */
573572export function replaceInstructMacrosgetInstructMacros(input, env) {
574- if (!input) {
575- return '';
576- }
577-
578- const syspromptMacros = {
579- 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content),
580- 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content,
581- };
582-
583573 const instructMacros = {
574+ // Instruct template macros
584575 'instructSystemPromptPrefix': power_user.instruct.system_sequence_prefix,
585576 'instructSystemPromptSuffix': power_user.instruct.system_sequence_suffix,
586577 'instructInput|instructUserPrefix': power_user.instruct.input_sequence,
@@ -596,22 +587,23 @@ export function replaceInstructMacros(input, env) {
596587 'instructSystemInstructionPrefix': power_user.instruct.last_system_sequence,
597588 'instructFirstInput|instructFirstUserPrefix': power_user.instruct.first_input_sequence || power_user.instruct.input_sequence,
598589 'instructLastInput|instructLastUserPrefix': power_user.instruct.last_input_sequence || power_user.instruct.input_sequence,
590+ // System prompt macros
591+ 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content),
592+ 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content,
593+ // Context template macros
594+ 'chatSeparator': power_user.context.example_separator,
595+ 'chatStart': power_user.context.chat_start,
599596 };
600597
601- for (const [placeholder, value] of Object.entries(instructMacros)) {
598+ const macros = [];
602- const regex = new RegExp(`{{(${placeholder})}}`, 'gi');
603- input = input.replace(regex, power_user.instruct.enabled ? value : '');
604- }
605599
606600 for (const [placeholder, value] of Object.entries(syspromptMacrosinstructMacros)) {
607601 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');
608602 inputconst replace = input.replace(regex,) => power_user.syspromptinstruct.enabled ? value : '');
603+ macros.push({ regex, replace });
609604 }
610605
611- input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator);
606+ return macros;
612- input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start);
613-
614- return input;
615607}
616608
617609jQuery(() => {
public/scripts/macros.js+128 -99
@@ -2,8 +2,14 @@ import { Handlebars, moment, seedrandom, droll } from '../lib.js';
22import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';
33import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js';
44import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
55import { replaceInstructMacrosgetInstructMacros } from './instruct-mode.js';
66import { replaceVariableMacrosgetVariableMacros } from './variables.js';
7+
8+/**
9+ * @typedef Macro
10+ * @property {RegExp} regex - Regular expression to match the macro
11+ * @property {(substring: string, ...args: any[]) => string} replace - Function to replace the macro
12+ */
713
814// Register any macro that you want to leave in the compiled story string
915Handlebars.registerHelper('trim', () => '{{trim}}');
@@ -261,28 +267,19 @@ function getCurrentSwipeId() {
261267/**
262268 * Replaces banned words in macros with an empty string.
263269 * Adds them to textgenerationwebui ban list.
264- * @param {string} inText Text to replace banned words in
270+ * @returns {Macro}
265- * @returns {string} Text without the "banned" macro
266271 */
267272function bannedWordsReplacegetBannedWordsMacro(inText) {
268- if (!inText) {
269- return '';
270- }
271-
272273 const banPattern = /{{banned "(.*)"}}/gi;
273-
274+ const banReplace = (match, bannedWord) => {
274275 if (main_api == 'textgenerationwebui') {
275- const bans = inText.matchAll(banPattern);
276+ console.log('Found banned word in macros: ' + bannedWord);
276- if (bans) {
277+ textgenerationwebui_banned_in_macros.push(bannedWord);
277- for (const banCase of bans) {
278- console.log('Found banned words in macros: ' + banCase[1]);
279- textgenerationwebui_banned_in_macros.push(banCase[1]);
280- }
281278 }
282- }
279+ return '';
280+ };
283281
284- inText = inText.replaceAll(banPattern, '');
282+ return { regex: banPattern, replace: banReplace };
285- return inText;
286283}
287284
288285function getTimeSinceLastMessage() {
@@ -317,10 +314,13 @@ function getTimeSinceLastMessage() {
317314 return 'just now';
318315}
319316
320-function randomReplace(input, emptyListPlaceholder = '') {
317+/**
318+ * Returns a macro that picks a random item from a list.
319+ * @returns {Macro} The random replace macro
320+ */
321+function getRandomReplaceMacro() {
321322 const randomPattern = /{{random\s?::?([^}]+)}}/gi;
322-
323+ const randomReplace = (match, listString) => {
323- input = input.replace(randomPattern, (match, listString) => {
324324 // Split on either double colons or comma. If comma is the separator, we are also trimming all items.
325325 const list = listString.includes('::')
326326 ? listString.split('::')
@@ -328,24 +328,29 @@ function randomReplace(input, emptyListPlaceholder = '') {
328328 : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ','));
329329
330330 if (list.length === 0) {
331331 return emptyListPlaceholder'';
332332 }
333333 const rng = seedrandom('added entropy.', { entropy: true });
334334 const randomIndex = Math.floor(rng() * list.length);
335335 return list[randomIndex];
336336 });
337- return input;
338-}
339337
340-function pickReplace(input, rawContent, emptyListPlaceholder = '') {
338+ return { regex: randomPattern, replace: randomReplace };
341- const pickPattern = /{{pick\s?::?([^}]+)}}/gi;
339+}
342340
341+/**
342+ * Returns a macro that picks a random item from a list with a consistent seed.
343+ * @param {string} rawContent The raw content of the string
344+ * @returns {Macro} The pick replace macro
345+ */
346+function getPickReplaceMacro(rawContent) {
343347 // We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches
344348 // No need to save metadata here - branching and renaming will implicitly do the save for us, and until then loading it like this is consistent
345349 const chatIdHash = getChatIdHash();
346350 const rawContentHash = getStringHash(rawContent);
347351
348- return input.replace(pickPattern, (match, listString, offset) => {
352+ const pickPattern = /{{pick\s?::?([^}]+)}}/gi;
353+ const pickReplace = (match, listString, offset) => {
349354 // Split on either double colons or comma. If comma is the separator, we are also trimming all items.
350355 const list = listString.includes('::')
351356 ? listString.split('::')
@@ -353,7 +358,7 @@ function pickReplace(input, rawContent, emptyListPlaceholder = '') {
353358 : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ','));
354359
355360 if (list.length === 0) {
356361 return emptyListPlaceholder'';
357362 }
358363
359364 // We build a hash seed based on: unique chat file, raw content, and the placement inside this content
@@ -364,13 +369,17 @@ function pickReplace(input, rawContent, emptyListPlaceholder = '') {
364369 const rng = seedrandom(finalSeed);
365370 const randomIndex = Math.floor(rng() * list.length);
366371 return list[randomIndex];
367372 });
373+
374+ return { regex: pickPattern, replace: pickReplace };
368375}
369376
370-function diceRollReplace(input, invalidRollPlaceholder = '') {
377+/**
378+ * @returns {Macro} The dire roll macro
379+ */
380+function getDiceRollMacro() {
371381 const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
372-
382+ const rollReplace = (match, matchValue) => {
373- return input.replace(rollPattern, (match, matchValue) => {
374383 let formula = matchValue.trim();
375384
376385 if (isDigitsOnly(formula)) {
@@ -381,32 +390,33 @@ function diceRollReplace(input, invalidRollPlaceholder = '') {
381390
382391 if (!isValid) {
383392 console.debug(`Invalid roll formula: ${formula}`);
384393 return invalidRollPlaceholder'';
385394 }
386395
387396 const result = droll.roll(formula);
388- return new String(result.total);
397+ if (result === false) return '';
389- });
398+ return String(result.total);
399+ };
400+
401+ return { regex: rollPattern, replace: rollReplace };
390402}
391403
392404/**
393405 * Returns the difference between two times. Works with any time format acceptable by moment().
394406 * Can work with {{date}} {{time}} macros
395407 * @paramreturns {stringMacro} input - The string to replace time difference macros in.macro
396- * @returns {string} The string with replaced time difference macros.
397408 */
398409function timeDiffReplacegetTimeDiffMacro(input) {
399410 const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi;
400-
411+ const timeDiffReplace = (_match, matchPart1, matchPart2) => {
401- const output = input.replace(timeDiffPattern, (_match, matchPart1, matchPart2) => {
402412 const time1 = moment(matchPart1);
403413 const time2 = moment(matchPart2);
404414
405415 const timeDifference = moment.duration(time1.diff(time2));
406416 return timeDifference.humanize(true);
407417 });
408418
409- return output;
419+ return { regex: timeDiffPattern, replace: timeDiffReplace };
410420}
411421
412422/**
@@ -414,81 +424,100 @@ function timeDiffReplace(input) {
414424 * @param {string} content - The string to substitute parameters in.
415425 * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param
416426 * values are functions, those functions will be called and their return values are used.
427+ * @param {function(string): string} postProcessFn - Function to run on the macro value before replacing it.
417428 * @returns {string} The string with substituted parameters.
418429 */
419430export function evaluateMacros(content, env, postProcessFn) {
420431 if (!content) {
421432 return '';
422433 }
423434
435+ postProcessFn = typeof postProcessFn === 'function' ? postProcessFn : (x => x);
424436 const rawContent = content;
425437
426- // Legacy non-macro substitutions
438+ /**
427- content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user);
439+ * Built-ins running before the env variables
428- content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char);
440+ * @type {Macro[]}
429- content = content.replace(/<CHAR>/gi, typeof env.char === 'function' ? env.char() : env.char);
441+ * */
430- content = content.replace(/<CHARIFNOTGROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);
442+ const preEnvMacros = [
431- content = content.replace(/<GROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);
443+ // Legacy non-curly macros
432-
444+ { regex: /<USER>/gi, replace: () => typeof env.user === 'function' ? env.user() : env.user },
433- // Short circuit if there are no macros
445+ { regex: /<BOT>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char },
434- if (!content.includes('{{')) {
446+ { regex: /<CHAR>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char },
435- return content;
447+ { regex: /<CHARIFNOTGROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group },
436- }
448+ { regex: /<GROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group },
449+ getDiceRollMacro(),
450+ ...getInstructMacros(env),
451+ ...getVariableMacros(),
452+ { regex: /{{newline}}/gi, replace: () => '\n' },
453+ { regex: /(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, replace: () => '' },
454+ { regex: /{{noop}}/gi, replace: () => '' },
455+ { regex: /{{input}}/gi, replace: () => String($('#send_textarea').val()) },
456+ ];
437457
438- content = diceRollReplace(content);
458+ /**
439- content = replaceInstructMacros(content, env);
459+ * Built-ins running after the env variables
440- content = replaceVariableMacros(content);
460+ * @type {Macro[]}
441- content = content.replace(/{{newline}}/gi, '\n');
461+ */
442- content = content.replace(/(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, '');
462+ const postEnvMacros = [
443- content = content.replace(/{{noop}}/gi, '');
463+ { regex: /{{maxPrompt}}/gi, replace: () => String(getMaxContextSize()) },
444464 content { =regex: content.replace(/{{inputlastMessage}}/gi, replace: () => String($('#send_textarea').valgetLastMessage())); },
465+ { regex: /{{lastMessageId}}/gi, replace: () => String(getLastMessageId() ?? '') },
466+ { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() },
467+ { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() },
468+ { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') },
469+ { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') },
470+ { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') },
471+ { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') },
472+ { regex: /\{\{\/\/([\s\S]*?)\}\}/gm, replace: () => '' },
473+ { regex: /{{time}}/gi, replace: () => moment().format('LT') },
474+ { regex: /{{date}}/gi, replace: () => moment().format('LL') },
475+ { regex: /{{weekday}}/gi, replace: () => moment().format('dddd') },
476+ { regex: /{{isotime}}/gi, replace: () => moment().format('HH:mm') },
477+ { regex: /{{isodate}}/gi, replace: () => moment().format('YYYY-MM-DD') },
478+ { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) },
479+ { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() },
480+ { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') },
481+ getTimeDiffMacro(),
482+ getBannedWordsMacro(),
483+ getRandomReplaceMacro(),
484+ getPickReplaceMacro(rawContent),
485+ ];
445486
446487 // Add all registered macros to the env object
447- const nonce = uuidv4();
448488 MacrosParser.populateEnv(env);
489+ const nonce = uuidv4();
490+ const envMacros = [];
449491
450492 // Substitute passed-in variables
451493 for (const varName in env) {
452494 if (!Object.hasOwn(env, varName)) continue;
453495
454496 contentconst envRegex = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => {;
497+ const envReplace = () => {
455498 const param = env[varName];
456499 const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);
457500 return value;
458501 });
502+
503+ envMacros.push({ regex: envRegex, replace: envReplace });
504+ }
505+
506+ const macros = [...preEnvMacros, ...envMacros, ...postEnvMacros];
507+
508+ for (const macro of macros) {
509+ // Stop if the content is empty
510+ if (!content) {
511+ break;
512+ }
513+
514+ // Short-circuit if no curly braces are found
515+ if (!macro.regex.source.startsWith('<') && !content.includes('{{')) {
516+ break;
517+ }
518+
519+ content = content.replace(macro.regex, (...args) => postProcessFn(macro.replace(...args)));
459520 }
460521
461- content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));
462- content = content.replace(/{{lastMessage}}/gi, () => getLastMessage());
463- content = content.replace(/{{lastMessageId}}/gi, () => String(getLastMessageId() ?? ''));
464- content = content.replace(/{{lastUserMessage}}/gi, () => getLastUserMessage());
465- content = content.replace(/{{lastCharMessage}}/gi, () => getLastCharMessage());
466- content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? ''));
467- content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? ''));
468- content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? ''));
469- content = content.replace(/{{reverse:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join(''));
470-
471- content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');
472-
473- content = content.replace(/{{time}}/gi, () => moment().format('LT'));
474- content = content.replace(/{{date}}/gi, () => moment().format('LL'));
475- content = content.replace(/{{weekday}}/gi, () => moment().format('dddd'));
476- content = content.replace(/{{isotime}}/gi, () => moment().format('HH:mm'));
477- content = content.replace(/{{isodate}}/gi, () => moment().format('YYYY-MM-DD'));
478-
479- content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => {
480- const formattedTime = moment().format(format);
481- return formattedTime;
482- });
483- content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage());
484- content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => {
485- const utcOffset = parseInt(offset, 10);
486- const utcTime = moment().utc().utcOffset(utcOffset).format('LT');
487- return utcTime;
488- });
489- content = timeDiffReplace(content);
490- content = bannedWordsReplace(content);
491- content = randomReplace(content);
492- content = pickReplace(content, rawContent);
493522 return content;
494523}
public/scripts/variables.js+21 -72
@@ -223,85 +223,33 @@ export function resolveVariable(name, scope = null) {
223223 return name;
224224}
225225
226-export function replaceVariableMacros(input) {
226+/**
227- const lines = input.split('\n');
227+ * Returns built-in variable macros.
228-
228+ * @returns {import('./macros.js').Macro[]}
229- for (let i = 0; i < lines.length; i++) {
229+ */
230- let line = lines[i];
230+export function getVariableMacros() {
231-
231+ return [
232- // Skip lines without macros
233- if (!line || !line.includes('{{')) {
234- continue;
235- }
236-
237- // Replace {{getvar::name}} with the value of the variable name
238- line = line.replace(/{{getvar::([^}]+)}}/gi, (_, name) => {
239- name = name.trim();
240- return getLocalVariable(name);
241- });
242-
243232 // Replace {{setvar::name::value}} with empty string and set the variable name to value
244233 line{ =regex: line.replace(/{{setvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setLocalVariable(name.trim(), value); return ''; } },
245- name = name.trim();
246- setLocalVariable(name, value);
247- return '';
248- });
249-
250234 // Replace {{addvar::name::value}} with empty string and add value to the variable value
251235 line{ =regex: line.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addLocalVariable(name.trim(), value); return ''; } },
252- name = name.trim();
253- addLocalVariable(name, value);
254- return '';
255- });
256-
257236 // Replace {{incvar::name}} with empty string and increment the variable name by 1
258237 line{ =regex: line.replace(/{{incvar::([^}]+)}}/gi, replace: (_, name) => {incrementLocalVariable(name.trim()) },
259- name = name.trim();
260- return incrementLocalVariable(name);
261- });
262-
263238 // Replace {{decvar::name}} with empty string and decrement the variable name by 1
264239 line{ =regex: line.replace(/{{decvar::([^}]+)}}/gi, replace: (_, name) => {decrementLocalVariable(name.trim()) },
265- name = name.trim();
240+ // Replace {{getvar::name}} with the value of the variable name
266- return decrementLocalVariable(name);
241+ { regex: /{{getvar::([^}]+)}}/gi, replace: (_, name) => getLocalVariable(name.trim()) },
267- });
268-
269- // Replace {{getglobalvar::name}} with the value of the global variable name
270- line = line.replace(/{{getglobalvar::([^}]+)}}/gi, (_, name) => {
271- name = name.trim();
272- return getGlobalVariable(name);
273- });
274-
275242 // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value
276243 line{ =regex: line.replace(/{{setglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setGlobalVariable(name.trim(), value); return ''; } },
277- name = name.trim();
278- setGlobalVariable(name, value);
279- return '';
280- });
281-
282244 // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value
283245 line{ =regex: line.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addGlobalVariable(name.trim(), value); return ''; } },
284- name = name.trim();
285- addGlobalVariable(name, value);
286- return '';
287- });
288-
289246 // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1
290247 line{ =regex: line.replace(/{{incglobalvar::([^}]+)}}/gi, replace: (_, name) => {incrementGlobalVariable(name.trim()) },
291- name = name.trim();
292- return incrementGlobalVariable(name);
293- });
294-
295248 // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1
296249 line{ =regex: line.replace(/{{decglobalvar::([^}]+)}}/gi, replace: (_, name) => {decrementGlobalVariable(name.trim()) },
297- name = name.trim();
250+ // Replace {{getglobalvar::name}} with the value of the global variable name
298- return decrementGlobalVariable(name);
251+ { regex: /{{getglobalvar::([^}]+)}}/gi, replace: (_, name) => getGlobalVariable(name.trim()) },
299- });
252+ ];
300-
301- lines[i] = line;
302- }
303-
304- return lines.join('\n');
305253}
306254
307255async function listVariablesCallback(args) {
@@ -2148,7 +2096,8 @@ export function registerVariableCommands() {
21482096 callback: sortArrayObjectCallback,
21492097 returns: 'the sorted list or dictionary keys',
21502098 namedArgumentList: [
21512099 SlashCommandNamedArgument.fromProps({ name: 'keysort',
2100+ name: 'keysort',
21522101 description: 'whether to sort by key or value; ignored for lists',
21532102 typeList: [ARGUMENT_TYPE.BOOLEAN],
21542103 enumList: ['true', 'false'],