Merge pull request #3035 from SillyTavern/macro-1.5 Totally not a new macro engine
Signed| @@ -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 | */ |
| 2527 | export function substituteParamsExtended(content, additionalMacro = {}) { | 2528 | export 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 | } |
| 2530 | 2531 | ||
| 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 | */ |
| 2542 | export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}) { | 2544 | export 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 | } |
| 2599 | 2601 | ||
| 2600 | return evaluateMacros(content, environment); | 2602 | return evaluateMacros(content, environment, postProcessFn); |
| 2601 | } | 2603 | } |
| 2602 | 2604 | ||
| 2603 | 2605 | ||
| @@ -1,4 +1,4 @@ | |||
| 1 | import { characters, substituteParams, this_chid } from '../../../script.js'; | 1 | import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js'; |
| 2 | import { extension_settings } from '../../extensions.js'; | 2 | import { extension_settings } from '../../extensions.js'; |
| 3 | import { regexFromString } from '../../utils.js'; | 3 | import { regexFromString } from '../../utils.js'; |
| 4 | export { | 4 | export { |
| @@ -22,6 +22,28 @@ const regex_placement = { | |||
| 22 | WORLD_INFO: 5, | 22 | WORLD_INFO: 5, |
| 23 | }; | 23 | }; |
| 24 | 24 | ||
| 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 | |||
| 25 | function getScopedRegex() { | 47 | function 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); |
| 27 | 49 | ||
| @@ -109,7 +131,10 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) { | |||
| 109 | return newString; | 131 | return newString; |
| 110 | } | 132 | } |
| 111 | 133 | ||
| 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); | ||
| 113 | 138 | ||
| 114 | // The user skill issued. Return with nothing. | 139 | // The user skill issued. Return with nothing. |
| 115 | if (!findRegex) { | 140 | if (!findRegex) { |
| @@ -565,22 +565,13 @@ function selectMatchingContextTemplate(name) { | |||
| 565 | 565 | ||
| 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 param | 568 | * @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 | */ |
| 573 | export function replaceInstructMacros(input, env) { | 572 | export 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 | }; |
| 600 | 597 | ||
| 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 | } | ||
| 605 | 599 | ||
| 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 | } |
| 610 | 605 | ||
| 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 | } |
| 616 | 608 | ||
| 617 | jQuery(() => { | 609 | jQuery(() => { |
| @@ -2,8 +2,14 @@ import { Handlebars, moment, seedrandom, droll } from '../lib.js'; | |||
| 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js'; | 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js'; |
| 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; | 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; |
| 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; | 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; |
| 5 | import { replaceInstructMacros } from './instruct-mode.js'; | 5 | import { getInstructMacros } from './instruct-mode.js'; |
| 6 | import { replaceVariableMacros } from './variables.js'; | 6 | import { 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 | */ | ||
| 7 | 13 | ||
| 8 | // Register any macro that you want to leave in the compiled story string | 14 | // Register any macro that you want to leave in the compiled story string |
| 9 | Handlebars.registerHelper('trim', () => '{{trim}}'); | 15 | Handlebars.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 in | 270 | * @returns {Macro} |
| 265 | * @returns {string} Text without the "banned" macro | ||
| 266 | */ | 271 | */ |
| 267 | function bannedWordsReplace(inText) { | 272 | function getBannedWordsMacro() { |
| 268 | if (!inText) { | ||
| 269 | return ''; | ||
| 270 | } | ||
| 271 | |||
| 272 | const banPattern = /{{banned "(.*)"}}/gi; | 273 | const banPattern = /{{banned "(.*)"}}/gi; |
| 273 | 274 | 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 | }; | ||
| 283 | 281 | ||
| 284 | inText = inText.replaceAll(banPattern, ''); | 282 | return { regex: banPattern, replace: banReplace }; |
| 285 | return inText; | ||
| 286 | } | 283 | } |
| 287 | 284 | ||
| 288 | function getTimeSinceLastMessage() { | 285 | function getTimeSinceLastMessage() { |
| @@ -317,10 +314,13 @@ function getTimeSinceLastMessage() { | |||
| 317 | return 'just now'; | 314 | return 'just now'; |
| 318 | } | 315 | } |
| 319 | 316 | ||
| 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() { | ||
| 321 | const randomPattern = /{{random\s?::?([^}]+)}}/gi; | 322 | const randomPattern = /{{random\s?::?([^}]+)}}/gi; |
| 322 | 323 | 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, ',')); |
| 329 | 329 | ||
| 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 | } | ||
| 339 | 337 | ||
| 340 | function pickReplace(input, rawContent, emptyListPlaceholder = '') { | 338 | return { regex: randomPattern, replace: randomReplace }; |
| 341 | const pickPattern = /{{pick\s?::?([^}]+)}}/gi; | 339 | } |
| 342 | 340 | ||
| 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) { | ||
| 343 | // We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches | 347 | // 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 consistent | 348 | // 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); |
| 347 | 351 | ||
| 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, ',')); |
| 354 | 359 | ||
| 355 | if (list.length === 0) { | 360 | if (list.length === 0) { |
| 356 | return emptyListPlaceholder; | 361 | return ''; |
| 357 | } | 362 | } |
| 358 | 363 | ||
| 359 | // We build a hash seed based on: unique chat file, raw content, and the placement inside this content | 364 | // 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 | } |
| 369 | 376 | ||
| 370 | function diceRollReplace(input, invalidRollPlaceholder = '') { | 377 | /** |
| 378 | * @returns {Macro} The dire roll macro | ||
| 379 | */ | ||
| 380 | function getDiceRollMacro() { | ||
| 371 | const rollPattern = /{{roll[ : ]([^}]+)}}/gi; | 381 | const rollPattern = /{{roll[ : ]([^}]+)}}/gi; |
| 372 | 382 | const rollReplace = (match, matchValue) => { | |
| 373 | return input.replace(rollPattern, (match, matchValue) => { | ||
| 374 | let formula = matchValue.trim(); | 383 | let formula = matchValue.trim(); |
| 375 | 384 | ||
| 376 | if (isDigitsOnly(formula)) { | 385 | if (isDigitsOnly(formula)) { |
| @@ -381,32 +390,33 @@ function diceRollReplace(input, invalidRollPlaceholder = '') { | |||
| 381 | 390 | ||
| 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 | } |
| 386 | 395 | ||
| 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 | } |
| 391 | 403 | ||
| 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}} macros | 406 | * 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 | */ |
| 398 | function timeDiffReplace(input) { | 409 | function getTimeDiffMacro() { |
| 399 | const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi; | 410 | const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi; |
| 400 | 411 | 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); |
| 404 | 414 | ||
| 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 | }; |
| 408 | 418 | ||
| 409 | return output; | 419 | return { regex: timeDiffPattern, replace: timeDiffReplace }; |
| 410 | } | 420 | } |
| 411 | 421 | ||
| 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 param | 425 | * @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 | */ |
| 419 | export function evaluateMacros(content, env) { | 430 | export function evaluateMacros(content, env, postProcessFn) { |
| 420 | if (!content) { | 431 | if (!content) { |
| 421 | return ''; | 432 | return ''; |
| 422 | } | 433 | } |
| 423 | 434 | ||
| 435 | postProcessFn = typeof postProcessFn === 'function' ? postProcessFn : (x => x); | ||
| 424 | const rawContent = content; | 436 | const rawContent = content; |
| 425 | 437 | ||
| 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 | ]; | ||
| 437 | 457 | ||
| 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 | ]; | ||
| 445 | 486 | ||
| 446 | // Add all registered macros to the env object | 487 | // 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 = []; | ||
| 449 | 491 | ||
| 450 | // Substitute passed-in variables | 492 | // 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; |
| 453 | 495 | ||
| 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 | } |
| 460 | 505 | ||
| 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('')); | ||
| 470 | 513 | ||
| 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 | } | ||
| 472 | 518 | ||
| 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')); | ||
| 478 | 521 | ||
| 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 | } |
| @@ -223,85 +223,33 @@ export function resolveVariable(name, scope = null) { | |||
| 223 | return name; | 223 | return name; |
| 224 | } | 224 | } |
| 225 | 225 | ||
| 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 | |||
| 243 | // Replace {{setvar::name::value}} with empty string and set the variable name to value | 232 | // 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 value | 234 | // 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 1 | 236 | // 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 1 | 238 | // 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 value | 242 | // 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 value | 244 | // 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 1 | 246 | // 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 1 | 248 | // 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 | } |
| 306 | 254 | ||
| 307 | async function listVariablesCallback(args) { | 255 | async 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'], |