Macros: refactor with a single replace point

8f373cf1dc2179d3c088fd8f80d3cfd9bdb575c7

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

3 files changed, +151 -175Showing whitespace changes
public/scripts/instruct-mode.js+11 -12
@@ -565,16 +565,11 @@ function selectMatchingContextTemplate(name) {
565565
566/**566/**
567 * Replaces instruct mode macros in the given input string.567 * Replaces instruct mode macros in the given input string.
568 * @param {string} input Input string.
569 * @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param568 * @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param
570 * values are functions, those functions will be called and their return values are used.569 * 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.
572 */571 */
573export function replaceInstructMacros(input, env) {572export function getInstructMacros(env) {
574 if (!input) {
575 return '';
576 }
577
578 const syspromptMacros = {573 const syspromptMacros = {
579 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content),574 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content),
580 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content,575 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content,
@@ -598,20 +593,24 @@ export function replaceInstructMacros(input, env) {
598 'instructLastInput|instructLastUserPrefix': power_user.instruct.last_input_sequence || power_user.instruct.input_sequence,593 'instructLastInput|instructLastUserPrefix': power_user.instruct.last_input_sequence || power_user.instruct.input_sequence,
599 };594 };
600595
596 const macros = [];
597
601 for (const [placeholder, value] of Object.entries(instructMacros)) {598 for (const [placeholder, value] of Object.entries(instructMacros)) {
602 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');599 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');
603 input = input.replace(regex, power_user.instruct.enabled ? value : '');600 const replace = () => power_user.instruct.enabled ? value : '';
601 macros.push({ regex, replace });
604 }602 }
605603
606 for (const [placeholder, value] of Object.entries(syspromptMacros)) {604 for (const [placeholder, value] of Object.entries(syspromptMacros)) {
607 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');605 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');
608 input = input.replace(regex, power_user.sysprompt.enabled ? value : '');606 const replace = () => power_user.sysprompt.enabled ? value : '';
607 macros.push({ regex, replace });
609 }608 }
610609
611 input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator);610 macros.push({ regex: /{{exampleSeparator}}/gi, replace: () => power_user.context.example_separator });
612 input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start);611 macros.push({ regex: /{{chatStart}}/gi, replace: () => power_user.context.chat_start });
613612
614 return input;613 return macros;
615}614}
616615
617jQuery(() => {616jQuery(() => {
public/scripts/macros.js+121 -94
@@ -2,8 +2,14 @@ import { Handlebars, moment, seedrandom, droll } from '../lib.js';
2import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';2import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';
3import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js';3import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js';
4import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';4import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
5import { replaceInstructMacros } from './instruct-mode.js';5import { getInstructMacros } from './instruct-mode.js';
6import { replaceVariableMacros } from './variables.js';6import { getVariableMacros } 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
8// Register any macro that you want to leave in the compiled story string14// Register any macro that you want to leave in the compiled story string
9Handlebars.registerHelper('trim', () => '{{trim}}');15Handlebars.registerHelper('trim', () => '{{trim}}');
@@ -261,28 +267,19 @@ function getCurrentSwipeId() {
261/**267/**
262 * Replaces banned words in macros with an empty string.268 * Replaces banned words in macros with an empty string.
263 * Adds them to textgenerationwebui ban list.269 * Adds them to textgenerationwebui ban list.
264 * @param {string} inText Text to replace banned words in270 * @returns {Macro}
265 * @returns {string} Text without the "banned" macro
266 */271 */
267function bannedWordsReplace(inText) {272function getBannedWordsMacro() {
268 if (!inText) {
269 return '';
270 }
271
272 const banPattern = /{{banned "(.*)"}}/gi;273 const banPattern = /{{banned "(.*)"}}/gi;
273274 const banReplace = (match, bannedWord) => {
274 if (main_api == 'textgenerationwebui') {275 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 }
281 }
282 }278 }
279 return '';
280 };
283281
284 inText = inText.replaceAll(banPattern, '');282 return { regex: banPattern, replace: banReplace };
285 return inText;
286}283}
287284
288function getTimeSinceLastMessage() {285function getTimeSinceLastMessage() {
@@ -317,10 +314,13 @@ function getTimeSinceLastMessage() {
317 return 'just now';314 return 'just now';
318}315}
319316
320function randomReplace(input, emptyListPlaceholder = '') {317/**
318 * Returns a macro that picks a random item from a list.
319 * @returns {Macro} The random replace macro
320 */
321function getRandomReplaceMacro() {
321 const randomPattern = /{{random\s?::?([^}]+)}}/gi;322 const randomPattern = /{{random\s?::?([^}]+)}}/gi;
322323 const randomReplace = (match, listString) => {
323 input = input.replace(randomPattern, (match, listString) => {
324 // Split on either double colons or comma. If comma is the separator, we are also trimming all items.324 // Split on either double colons or comma. If comma is the separator, we are also trimming all items.
325 const list = listString.includes('::')325 const list = listString.includes('::')
326 ? listString.split('::')326 ? listString.split('::')
@@ -328,24 +328,29 @@ function randomReplace(input, emptyListPlaceholder = '') {
328 : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ','));328 : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ','));
329329
330 if (list.length === 0) {330 if (list.length === 0) {
331 return emptyListPlaceholder;331 return '';
332 }332 }
333 const rng = seedrandom('added entropy.', { entropy: true });333 const rng = seedrandom('added entropy.', { entropy: true });
334 const randomIndex = Math.floor(rng() * list.length);334 const randomIndex = Math.floor(rng() * list.length);
335 return list[randomIndex];335 return list[randomIndex];
336 });336 };
337 return input;
338}
339337
340function 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 */
346function getPickReplaceMacro(rawContent) {
343 // We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches347 // We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches
344 // No need to save metadata here - branching and renaming will implicitly do the save for us, and until then loading it like this is consistent348 // 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
345 const chatIdHash = getChatIdHash();349 const chatIdHash = getChatIdHash();
346 const rawContentHash = getStringHash(rawContent);350 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) => {
349 // Split on either double colons or comma. If comma is the separator, we are also trimming all items.354 // Split on either double colons or comma. If comma is the separator, we are also trimming all items.
350 const list = listString.includes('::')355 const list = listString.includes('::')
351 ? listString.split('::')356 ? listString.split('::')
@@ -353,7 +358,7 @@ function pickReplace(input, rawContent, emptyListPlaceholder = '') {
353 : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ','));358 : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ','));
354359
355 if (list.length === 0) {360 if (list.length === 0) {
356 return emptyListPlaceholder;361 return '';
357 }362 }
358363
359 // We build a hash seed based on: unique chat file, raw content, and the placement inside this content364 // 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 = '') {
364 const rng = seedrandom(finalSeed);369 const rng = seedrandom(finalSeed);
365 const randomIndex = Math.floor(rng() * list.length);370 const randomIndex = Math.floor(rng() * list.length);
366 return list[randomIndex];371 return list[randomIndex];
367 });372 };
373
374 return { regex: pickPattern, replace: pickReplace };
368}375}
369376
370function diceRollReplace(input, invalidRollPlaceholder = '') {377/**
378 * @returns {Macro} The dire roll macro
379 */
380function getDiceRollMacro() {
371 const rollPattern = /{{roll[ : ]([^}]+)}}/gi;381 const rollPattern = /{{roll[ : ]([^}]+)}}/gi;
372382 const rollReplace = (match, matchValue) => {
373 return input.replace(rollPattern, (match, matchValue) => {
374 let formula = matchValue.trim();383 let formula = matchValue.trim();
375384
376 if (isDigitsOnly(formula)) {385 if (isDigitsOnly(formula)) {
@@ -381,32 +390,33 @@ function diceRollReplace(input, invalidRollPlaceholder = '') {
381390
382 if (!isValid) {391 if (!isValid) {
383 console.debug(`Invalid roll formula: ${formula}`);392 console.debug(`Invalid roll formula: ${formula}`);
384 return invalidRollPlaceholder;393 return '';
385 }394 }
386395
387 const result = droll.roll(formula);396 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 };
390}402}
391403
392/**404/**
393 * Returns the difference between two times. Works with any time format acceptable by moment().405 * Returns the difference between two times. Works with any time format acceptable by moment().
394 * Can work with {{date}} {{time}} macros406 * Can work with {{date}} {{time}} macros
395 * @param {string} input - The string to replace time difference macros in.407 * @returns {Macro} The time difference macro
396 * @returns {string} The string with replaced time difference macros.
397 */408 */
398function timeDiffReplace(input) {409function getTimeDiffMacro() {
399 const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi;410 const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi;
400411 const timeDiffReplace = (_match, matchPart1, matchPart2) => {
401 const output = input.replace(timeDiffPattern, (_match, matchPart1, matchPart2) => {
402 const time1 = moment(matchPart1);412 const time1 = moment(matchPart1);
403 const time2 = moment(matchPart2);413 const time2 = moment(matchPart2);
404414
405 const timeDifference = moment.duration(time1.diff(time2));415 const timeDifference = moment.duration(time1.diff(time2));
406 return timeDifference.humanize(true);416 return timeDifference.humanize(true);
407 });417 };
408418
409 return output;419 return { regex: timeDiffPattern, replace: timeDiffReplace };
410}420}
411421
412/**422/**
@@ -423,72 +433,89 @@ export function evaluateMacros(content, env) {
423433
424 const rawContent = content;434 const rawContent = content;
425435
426 // Legacy non-macro substitutions436 /**
427 content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user);437 * Built-ins running before the env variables
428 content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char);438 * @type {Macro[]}
429 content = content.replace(/<CHAR>/gi, typeof env.char === 'function' ? env.char() : env.char);439 * */
430 content = content.replace(/<CHARIFNOTGROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);440 const preEnvMacros = [
431 content = content.replace(/<GROUP>/gi, typeof env.group === 'function' ? env.group() : env.group);441 // Legacy non-curly macros
432442 { regex: /<USER>/gi, replace: () => typeof env.user === 'function' ? env.user() : env.user },
433 // Short circuit if there are no macros443 { regex: /<BOT>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char },
434 if (!content.includes('{{')) {444 { regex: /<CHAR>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char },
435 return content;445 { regex: /<CHARIFNOTGROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group },
436 }446 { regex: /<GROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group },
447 getDiceRollMacro(),
448 ...getInstructMacros(env),
449 ...getVariableMacros(),
450 { regex: /{{newline}}/gi, replace: () => '\n' },
451 { regex: /(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, replace: () => '' },
452 { regex: /{{noop}}/gi, replace: () => '' },
453 { regex: /{{input}}/gi, replace: () => String($('#send_textarea').val()) },
454 ];
437455
438 content = diceRollReplace(content);456 /**
439 content = replaceInstructMacros(content, env);457 * Built-ins running after the env variables
440 content = replaceVariableMacros(content);458 * @type {Macro[]}
441 content = content.replace(/{{newline}}/gi, '\n');459 */
442 content = content.replace(/(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, '');460 const postEnvMacros = [
443 content = content.replace(/{{noop}}/gi, '');461 { regex: /{{maxPrompt}}/gi, replace: () => String(getMaxContextSize()) },
444 content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val()));462 { regex: /{{lastMessage}}/gi, replace: () => getLastMessage() },
463 { regex: /{{lastMessageId}}/gi, replace: () => String(getLastMessageId() ?? '') },
464 { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() },
465 { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() },
466 { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') },
467 { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') },
468 { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') },
469 { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') },
470 { regex: /\{\{\/\/([\s\S]*?)\}\}/gm, replace: () => '' },
471 { regex: /{{time}}/gi, replace: () => moment().format('LT') },
472 { regex: /{{date}}/gi, replace: () => moment().format('LL') },
473 { regex: /{{weekday}}/gi, replace: () => moment().format('dddd') },
474 { regex: /{{isotime}}/gi, replace: () => moment().format('HH:mm') },
475 { regex: /{{isodate}}/gi, replace: () => moment().format('YYYY-MM-DD') },
476 { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) },
477 { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() },
478 { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') },
479 getTimeDiffMacro(),
480 getBannedWordsMacro(),
481 getRandomReplaceMacro(),
482 getPickReplaceMacro(rawContent),
483 ];
445484
446 // Add all registered macros to the env object485 // Add all registered macros to the env object
447 const nonce = uuidv4();
448 MacrosParser.populateEnv(env);486 MacrosParser.populateEnv(env);
487 const nonce = uuidv4();
488 const envMacros = [];
449489
450 // Substitute passed-in variables490 // Substitute passed-in variables
451 for (const varName in env) {491 for (const varName in env) {
452 if (!Object.hasOwn(env, varName)) continue;492 if (!Object.hasOwn(env, varName)) continue;
453493
454 content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => {494 const envRegex = new RegExp(`{{${escapeRegex(varName)}}}`, 'gi');
495 const envReplace = () => {
455 const param = env[varName];496 const param = env[varName];
456 const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);497 const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);
457 return value;498 return value;
458 });499 };
500
501 envMacros.push({ regex: envRegex, replace: envReplace });
459 }502 }
460503
461 content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));504 const macros = [...preEnvMacros, ...envMacros, ...postEnvMacros];
462 content = content.replace(/{{lastMessage}}/gi, () => getLastMessage());505
463 content = content.replace(/{{lastMessageId}}/gi, () => String(getLastMessageId() ?? ''));506 for (const macro of macros) {
464 content = content.replace(/{{lastUserMessage}}/gi, () => getLastUserMessage());507 // Stop if the content is empty
465 content = content.replace(/{{lastCharMessage}}/gi, () => getLastCharMessage());508 if (!content) {
466 content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? ''));509 break;
467 content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? ''));510 }
468 content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? ''));
469 content = content.replace(/{{reverse:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join(''));
470511
471 content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');512 // Short-circuit if no curly braces are found
513 if (!macro.regex.source.startsWith('<') && !content.includes('{{')) {
514 break;
515 }
472516
473 content = content.replace(/{{time}}/gi, () => moment().format('LT'));517 content = content.replace(macro.regex, macro.replace);
474 content = content.replace(/{{date}}/gi, () => moment().format('LL'));518 }
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'));
478519
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);
493 return content;520 return content;
494}521}
public/scripts/variables.js+19 -69
@@ -223,85 +223,34 @@ export function resolveVariable(name, scope = null) {
223 return name;223 return name;
224}224}
225225
226export function replaceVariableMacros(input) {226/**
227 const lines = input.split('\n');227 * @returns {import('./macros.js').Macro[]}
228228 */
229 for (let i = 0; i < lines.length; i++) {229export function getVariableMacros() {
230 let line = lines[i];230 const macros = [
231
232 // Skip lines without macros
233 if (!line || !line.includes('{{')) {
234 continue;
235 }
236
237 // Replace {{getvar::name}} with the value of the variable name231 // Replace {{getvar::name}} with the value of the variable name
238 line = line.replace(/{{getvar::([^}]+)}}/gi, (_, name) => {232 { regex: /{{getvar::([^}]+)}}/gi, replace: (_, name) => getLocalVariable(name.trim()) },
239 name = name.trim();
240 return getLocalVariable(name);
241 });
242
243 // Replace {{setvar::name::value}} with empty string and set the variable name to value233 // Replace {{setvar::name::value}} with empty string and set the variable name to value
244 line = line.replace(/{{setvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {234 { regex: /{{setvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setLocalVariable(name.trim(), value); return ''; } },
245 name = name.trim();
246 setLocalVariable(name, value);
247 return '';
248 });
249
250 // Replace {{addvar::name::value}} with empty string and add value to the variable value235 // Replace {{addvar::name::value}} with empty string and add value to the variable value
251 line = line.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {236 { regex: /{{addvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addLocalVariable(name.trim(), value); return ''; } },
252 name = name.trim();
253 addLocalVariable(name, value);
254 return '';
255 });
256
257 // Replace {{incvar::name}} with empty string and increment the variable name by 1237 // Replace {{incvar::name}} with empty string and increment the variable name by 1
258 line = line.replace(/{{incvar::([^}]+)}}/gi, (_, name) => {238 { regex: /{{incvar::([^}]+)}}/gi, replace: (_, name) => incrementLocalVariable(name.trim()) },
259 name = name.trim();
260 return incrementLocalVariable(name);
261 });
262
263 // Replace {{decvar::name}} with empty string and decrement the variable name by 1239 // Replace {{decvar::name}} with empty string and decrement the variable name by 1
264 line = line.replace(/{{decvar::([^}]+)}}/gi, (_, name) => {240 { regex: /{{decvar::([^}]+)}}/gi, replace: (_, name) => decrementLocalVariable(name.trim()) },
265 name = name.trim();
266 return decrementLocalVariable(name);
267 });
268
269 // Replace {{getglobalvar::name}} with the value of the global variable name241 // Replace {{getglobalvar::name}} with the value of the global variable name
270 line = line.replace(/{{getglobalvar::([^}]+)}}/gi, (_, name) => {242 { regex: /{{getglobalvar::([^}]+)}}/gi, replace: (_, name) => getGlobalVariable(name.trim()) },
271 name = name.trim();
272 return getGlobalVariable(name);
273 });
274
275 // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value243 // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value
276 line = line.replace(/{{setglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {244 { regex: /{{setglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setGlobalVariable(name.trim(), value); return ''; } },
277 name = name.trim();
278 setGlobalVariable(name, value);
279 return '';
280 });
281
282 // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value245 // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value
283 line = line.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {246 { regex: /{{addglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addGlobalVariable(name.trim(), value); return ''; } },
284 name = name.trim();
285 addGlobalVariable(name, value);
286 return '';
287 });
288
289 // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1247 // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1
290 line = line.replace(/{{incglobalvar::([^}]+)}}/gi, (_, name) => {248 { regex: /{{incglobalvar::([^}]+)}}/gi, replace: (_, name) => incrementGlobalVariable(name.trim()) },
291 name = name.trim();
292 return incrementGlobalVariable(name);
293 });
294
295 // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1249 // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1
296 line = line.replace(/{{decglobalvar::([^}]+)}}/gi, (_, name) => {250 { regex: /{{decglobalvar::([^}]+)}}/gi, replace: (_, name) => decrementGlobalVariable(name.trim()) },
297 name = name.trim();251 ];
298 return decrementGlobalVariable(name);
299 });
300
301 lines[i] = line;
302 }
303252
304 return lines.join('\n');253 return macros;
305}254}
306255
307async function listVariablesCallback(args) {256async function listVariablesCallback(args) {
@@ -2148,7 +2097,8 @@ export function registerVariableCommands() {
2148 callback: sortArrayObjectCallback,2097 callback: sortArrayObjectCallback,
2149 returns: 'the sorted list or dictionary keys',2098 returns: 'the sorted list or dictionary keys',
2150 namedArgumentList: [2099 namedArgumentList: [
2151 SlashCommandNamedArgument.fromProps({ name: 'keysort',2100 SlashCommandNamedArgument.fromProps({
2101 name: 'keysort',
2152 description: 'whether to sort by key or value; ignored for lists',2102 description: 'whether to sort by key or value; ignored for lists',
2153 typeList: [ARGUMENT_TYPE.BOOLEAN],2103 typeList: [ARGUMENT_TYPE.BOOLEAN],
2154 enumList: ['true', 'false'],2104 enumList: ['true', 'false'],