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, +192 -195Showing whitespace changes
public/script.js+6 -4
@@ -2522,10 +2522,11 @@ export function scrollChatToBottom() {
2522 * Substitutes {{macro}} parameters in a string.2522 * Substitutes {{macro}} parameters in a string.
2523 * @param {string} content - The string to substitute parameters in.2523 * @param {string} content - The string to substitute parameters in.
2524 * @param {Record<string,any>} additionalMacro - Additional environment variables for substitution.2524 * @param {Record<string,any>} additionalMacro - Additional environment variables for substitution.
2525 * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
2525 * @returns {string} The string with substituted parameters.2526 * @returns {string} The string with substituted parameters.
2526 */2527 */
2527export function substituteParamsExtended(content, additionalMacro = {}) {2528export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) {
2528 return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro);2529 return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro, postProcessFn);
2529}2530}
25302531
2531/**2532/**
@@ -2537,9 +2538,10 @@ export function substituteParamsExtended(content, additionalMacro = {}) {
2537 * @param {string} [_group] - The group members list for {{group}} substitution.2538 * @param {string} [_group] - The group members list for {{group}} substitution.
2538 * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros.2539 * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros.
2539 * @param {Record<string,any>} [additionalMacro] - Additional environment variables for substitution.2540 * @param {Record<string,any>} [additionalMacro] - Additional environment variables for substitution.
2541 * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro.
2540 * @returns {string} The string with substituted parameters.2542 * @returns {string} The string with substituted parameters.
2541 */2543 */
2542export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}) {2544export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) {
2543 if (!content) {2545 if (!content) {
2544 return '';2546 return '';
2545 }2547 }
@@ -2597,7 +2599,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re
2597 Object.assign(environment, additionalMacro);2599 Object.assign(environment, additionalMacro);
2598 }2600 }
25992601
2600 return evaluateMacros(content, environment);2602 return evaluateMacros(content, environment, postProcessFn);
2601}2603}
26022604
26032605
public/scripts/extensions/regex/engine.js+27 -2
@@ -1,4 +1,4 @@
1import { characters, substituteParams, this_chid } from '../../../script.js';1import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js';
2import { extension_settings } from '../../extensions.js';2import { extension_settings } from '../../extensions.js';
3import { regexFromString } from '../../utils.js';3import { regexFromString } from '../../utils.js';
4export {4export {
@@ -22,6 +22,28 @@ const regex_placement = {
22 WORLD_INFO: 5,22 WORLD_INFO: 5,
23};23};
2424
25function 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
25function getScopedRegex() {47function getScopedRegex() {
26 const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);48 const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
2749
@@ -109,7 +131,10 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
109 return newString;131 return newString;
110 }132 }
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
114 // The user skill issued. Return with nothing.139 // The user skill issued. Return with nothing.
115 if (!findRegex) {140 if (!findRegex) {
public/scripts/instruct-mode.js+14 -22
@@ -565,22 +565,13 @@ 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 = {
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
583 const instructMacros = {573 const instructMacros = {
574 // Instruct template macros
584 'instructSystemPromptPrefix': power_user.instruct.system_sequence_prefix,575 'instructSystemPromptPrefix': power_user.instruct.system_sequence_prefix,
585 'instructSystemPromptSuffix': power_user.instruct.system_sequence_suffix,576 'instructSystemPromptSuffix': power_user.instruct.system_sequence_suffix,
586 'instructInput|instructUserPrefix': power_user.instruct.input_sequence,577 'instructInput|instructUserPrefix': power_user.instruct.input_sequence,
@@ -596,22 +587,23 @@ export function replaceInstructMacros(input, env) {
596 'instructSystemInstructionPrefix': power_user.instruct.last_system_sequence,587 'instructSystemInstructionPrefix': power_user.instruct.last_system_sequence,
597 'instructFirstInput|instructFirstUserPrefix': power_user.instruct.first_input_sequence || power_user.instruct.input_sequence,588 'instructFirstInput|instructFirstUserPrefix': power_user.instruct.first_input_sequence || power_user.instruct.input_sequence,
598 'instructLastInput|instructLastUserPrefix': power_user.instruct.last_input_sequence || power_user.instruct.input_sequence,589 '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,
599 };596 };
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
606 for (const [placeholder, value] of Object.entries(syspromptMacros)) {600 for (const [placeholder, value] of Object.entries(instructMacros)) {
607 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');601 const regex = new RegExp(`{{(${placeholder})}}`, 'gi');
608 input = input.replace(regex, power_user.sysprompt.enabled ? value : '');602 const replace = () => power_user.instruct.enabled ? value : '';
603 macros.push({ regex, replace });
609 }604 }
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;
615}607}
616608
617jQuery(() => {609jQuery(() => {
public/scripts/macros.js+124 -95
@@ -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/**
@@ -414,81 +424,100 @@ function timeDiffReplace(input) {
414 * @param {string} content - The string to substitute parameters in.424 * @param {string} content - The string to substitute parameters in.
415 * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param425 * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param
416 * values are functions, those functions will be called and their return values are used.426 * 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.
417 * @returns {string} The string with substituted parameters.428 * @returns {string} The string with substituted parameters.
418 */429 */
419export function evaluateMacros(content, env) {430export function evaluateMacros(content, env, postProcessFn) {
420 if (!content) {431 if (!content) {
421 return '';432 return '';
422 }433 }
423434
435 postProcessFn = typeof postProcessFn === 'function' ? postProcessFn : (x => x);
424 const rawContent = content;436 const rawContent = content;
425437
426 // Legacy non-macro substitutions438 /**
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
432444 { regex: /<USER>/gi, replace: () => typeof env.user === 'function' ? env.user() : env.user },
433 // Short circuit if there are no macros445 { 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()) },
444 content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val()));464 { regex: /{{lastMessage}}/gi, replace: () => getLastMessage() },
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
446 // Add all registered macros to the env object487 // Add all registered macros to the env object
447 const nonce = uuidv4();
448 MacrosParser.populateEnv(env);488 MacrosParser.populateEnv(env);
489 const nonce = uuidv4();
490 const envMacros = [];
449491
450 // Substitute passed-in variables492 // Substitute passed-in variables
451 for (const varName in env) {493 for (const varName in env) {
452 if (!Object.hasOwn(env, varName)) continue;494 if (!Object.hasOwn(env, varName)) continue;
453495
454 content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => {496 const envRegex = new RegExp(`{{${escapeRegex(varName)}}}`, 'gi');
497 const envReplace = () => {
455 const param = env[varName];498 const param = env[varName];
456 const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);499 const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param);
457 return value;500 return value;
458 });501 };
502
503 envMacros.push({ regex: envRegex, replace: envReplace });
459 }504 }
460505
461 content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize()));506 const macros = [...preEnvMacros, ...envMacros, ...postEnvMacros];
462 content = content.replace(/{{lastMessage}}/gi, () => getLastMessage());507
463 content = content.replace(/{{lastMessageId}}/gi, () => String(getLastMessageId() ?? ''));508 for (const macro of macros) {
464 content = content.replace(/{{lastUserMessage}}/gi, () => getLastUserMessage());509 // Stop if the content is empty
465 content = content.replace(/{{lastCharMessage}}/gi, () => getLastCharMessage());510 if (!content) {
466 content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? ''));511 break;
467 content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? ''));512 }
468 content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? ''));
469 content = content.replace(/{{reverse:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join(''));
470513
471 content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, '');514 // Short-circuit if no curly braces are found
515 if (!macro.regex.source.startsWith('<') && !content.includes('{{')) {
516 break;
517 }
472518
473 content = content.replace(/{{time}}/gi, () => moment().format('LT'));519 content = content.replace(macro.regex, (...args) => postProcessFn(macro.replace(...args)));
474 content = content.replace(/{{date}}/gi, () => moment().format('LL'));520 }
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'));
478521
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;522 return content;
494}523}
public/scripts/variables.js+21 -72
@@ -223,85 +223,33 @@ 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 built-in variable macros.
228228 * @returns {import('./macros.js').Macro[]}
229 for (let i = 0; i < lines.length; i++) {229 */
230 let line = lines[i];230export function getVariableMacros() {
231231 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
243 // Replace {{setvar::name::value}} with empty string and set the variable name to value232 // Replace {{setvar::name::value}} with empty string and set the variable name to value
244 line = line.replace(/{{setvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {233 { 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 value234 // Replace {{addvar::name::value}} with empty string and add value to the variable value
251 line = line.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {235 { 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 1236 // Replace {{incvar::name}} with empty string and increment the variable name by 1
258 line = line.replace(/{{incvar::([^}]+)}}/gi, (_, name) => {237 { 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 1238 // Replace {{decvar::name}} with empty string and decrement the variable name by 1
264 line = line.replace(/{{decvar::([^}]+)}}/gi, (_, name) => {239 { regex: /{{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
275 // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value242 // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value
276 line = line.replace(/{{setglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {243 { 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 value244 // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value
283 line = line.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => {245 { 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 1246 // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1
290 line = line.replace(/{{incglobalvar::([^}]+)}}/gi, (_, name) => {247 { 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 1248 // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1
296 line = line.replace(/{{decglobalvar::([^}]+)}}/gi, (_, name) => {249 { regex: /{{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');
305}253}
306254
307async function listVariablesCallback(args) {255async function listVariablesCallback(args) {
@@ -2148,7 +2096,8 @@ export function registerVariableCommands() {
2148 callback: sortArrayObjectCallback,2096 callback: sortArrayObjectCallback,
2149 returns: 'the sorted list or dictionary keys',2097 returns: 'the sorted list or dictionary keys',
2150 namedArgumentList: [2098 namedArgumentList: [
2151 SlashCommandNamedArgument.fromProps({ name: 'keysort',2099 SlashCommandNamedArgument.fromProps({
2100 name: 'keysort',
2152 description: 'whether to sort by key or value; ignored for lists',2101 description: 'whether to sort by key or value; ignored for lists',
2153 typeList: [ARGUMENT_TYPE.BOOLEAN],2102 typeList: [ARGUMENT_TYPE.BOOLEAN],
2154 enumList: ['true', 'false'],2103 enumList: ['true', 'false'],