Merge pull request #3035 from SillyTavern/macro-1.5 Totally not a new macro engine
Signed| @@ -2522,10 +2522,11 @@ export function scrollChatToBottom() { | ||
| 2522 | 2522 | * Substitutes {{macro}} parameters in a string. |
| 2523 | 2523 | * @param {string} content - The string to substitute parameters in. |
| 2524 | 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 | 2526 | * @returns {string} The string with substituted parameters. |
| 2526 | 2527 | */ |
| 2527 | 2528 | export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) { |
| 2528 | 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 | 2538 | * @param {string} [_group] - The group members list for {{group}} substitution. |
| 2538 | 2539 | * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros. |
| 2539 | 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 | 2542 | * @returns {string} The string with substituted parameters. |
| 2541 | 2543 | */ |
| 2542 | 2544 | export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) { |
| 2543 | 2545 | if (!content) { |
| 2544 | 2546 | return ''; |
| 2545 | 2547 | } |
| @@ -2597,7 +2599,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re | ||
| 2597 | 2599 | Object.assign(environment, additionalMacro); |
| 2598 | 2600 | } |
| 2599 | 2601 | |
| 2600 | 2602 | return evaluateMacros(content, environment, postProcessFn); |
| 2601 | 2603 | } |
| 2602 | 2604 | |
| 2603 | 2605 | |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js'; |
| 2 | 2 | import { extension_settings } from '../../extensions.js'; |
| 3 | 3 | import { regexFromString } from '../../utils.js'; |
| 4 | 4 | export { |
| @@ -22,6 +22,28 @@ const regex_placement = { | ||
| 22 | 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 | 47 | function getScopedRegex() { |
| 26 | 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 | 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 | 139 | // The user skill issued. Return with nothing. |
| 115 | 140 | if (!findRegex) { |
| @@ -565,22 +565,13 @@ function selectMatchingContextTemplate(name) { | ||
| 565 | 565 | |
| 566 | 566 | /** |
| 567 | 567 | * Replaces instruct mode macros in the given input string. |
| 568 | - * @param {string} input Input string. | |
| 569 | 568 | * @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param |
| 570 | 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 | 572 | export function replaceInstructMacrosgetInstructMacros(input, env) { |
| 574 | - if (!input) { | |
| 575 | - return ''; | |
| 576 | - } | |
| 577 | - | |
| 578 | - const syspromptMacros = { | |
| 579 | - 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content), | |
| 580 | - 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content, | |
| 581 | - }; | |
| 582 | - | |
| 583 | 573 | const instructMacros = { |
| 574 | + // Instruct template macros | |
| 584 | 575 | 'instructSystemPromptPrefix': power_user.instruct.system_sequence_prefix, |
| 585 | 576 | 'instructSystemPromptSuffix': power_user.instruct.system_sequence_suffix, |
| 586 | 577 | 'instructInput|instructUserPrefix': power_user.instruct.input_sequence, |
| @@ -596,22 +587,23 @@ export function replaceInstructMacros(input, env) { | ||
| 596 | 587 | 'instructSystemInstructionPrefix': power_user.instruct.last_system_sequence, |
| 597 | 588 | 'instructFirstInput|instructFirstUserPrefix': power_user.instruct.first_input_sequence || power_user.instruct.input_sequence, |
| 598 | 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 | 600 | for (const [placeholder, value] of Object.entries(syspromptMacrosinstructMacros)) { |
| 607 | 601 | const regex = new RegExp(`{{(${placeholder})}}`, 'gi'); |
| 608 | 602 | inputconst replace = input.replace(regex,) => power_user.syspromptinstruct.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 | 609 | jQuery(() => { |
| @@ -2,8 +2,14 @@ import { Handlebars, moment, seedrandom, droll } from '../lib.js'; | ||
| 2 | 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js'; |
| 3 | 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; |
| 4 | 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; |
| 5 | 5 | import { replaceInstructMacrosgetInstructMacros } from './instruct-mode.js'; |
| 6 | 6 | import { replaceVariableMacrosgetVariableMacros } from './variables.js'; |
| 7 | + | |
| 8 | +/** | |
| 9 | + * @typedef Macro | |
| 10 | + * @property {RegExp} regex - Regular expression to match the macro | |
| 11 | + * @property {(substring: string, ...args: any[]) => string} replace - Function to replace the macro | |
| 12 | + */ | |
| 7 | 13 | |
| 8 | 14 | // Register any macro that you want to leave in the compiled story string |
| 9 | 15 | Handlebars.registerHelper('trim', () => '{{trim}}'); |
| @@ -261,28 +267,19 @@ function getCurrentSwipeId() { | ||
| 261 | 267 | /** |
| 262 | 268 | * Replaces banned words in macros with an empty string. |
| 263 | 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 | 272 | function bannedWordsReplacegetBannedWordsMacro(inText) { |
| 268 | - if (!inText) { | |
| 269 | - return ''; | |
| 270 | - } | |
| 271 | - | |
| 272 | 273 | const banPattern = /{{banned "(.*)"}}/gi; |
| 273 | - | |
| 274 | + const banReplace = (match, bannedWord) => { | |
| 274 | 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 | 285 | function getTimeSinceLastMessage() { |
| @@ -317,10 +314,13 @@ function getTimeSinceLastMessage() { | ||
| 317 | 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 | 322 | const randomPattern = /{{random\s?::?([^}]+)}}/gi; |
| 322 | - | |
| 323 | + const randomReplace = (match, listString) => { | |
| 323 | - input = input.replace(randomPattern, (match, listString) => { | |
| 324 | 324 | // Split on either double colons or comma. If comma is the separator, we are also trimming all items. |
| 325 | 325 | const list = listString.includes('::') |
| 326 | 326 | ? listString.split('::') |
| @@ -328,24 +328,29 @@ function randomReplace(input, emptyListPlaceholder = '') { | ||
| 328 | 328 | : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ',')); |
| 329 | 329 | |
| 330 | 330 | if (list.length === 0) { |
| 331 | 331 | return emptyListPlaceholder''; |
| 332 | 332 | } |
| 333 | 333 | const rng = seedrandom('added entropy.', { entropy: true }); |
| 334 | 334 | const randomIndex = Math.floor(rng() * list.length); |
| 335 | 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 | 347 | // We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches |
| 344 | 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 | 349 | const chatIdHash = getChatIdHash(); |
| 346 | 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 | 354 | // Split on either double colons or comma. If comma is the separator, we are also trimming all items. |
| 350 | 355 | const list = listString.includes('::') |
| 351 | 356 | ? listString.split('::') |
| @@ -353,7 +358,7 @@ function pickReplace(input, rawContent, emptyListPlaceholder = '') { | ||
| 353 | 358 | : listString.replace(/\\,/g, '##�COMMA�##').split(',').map(item => item.trim().replace(/##�COMMA�##/g, ',')); |
| 354 | 359 | |
| 355 | 360 | if (list.length === 0) { |
| 356 | 361 | return emptyListPlaceholder''; |
| 357 | 362 | } |
| 358 | 363 | |
| 359 | 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 | 369 | const rng = seedrandom(finalSeed); |
| 365 | 370 | const randomIndex = Math.floor(rng() * list.length); |
| 366 | 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 | 381 | const rollPattern = /{{roll[ : ]([^}]+)}}/gi; |
| 372 | - | |
| 382 | + const rollReplace = (match, matchValue) => { | |
| 373 | - return input.replace(rollPattern, (match, matchValue) => { | |
| 374 | 383 | let formula = matchValue.trim(); |
| 375 | 384 | |
| 376 | 385 | if (isDigitsOnly(formula)) { |
| @@ -381,32 +390,33 @@ function diceRollReplace(input, invalidRollPlaceholder = '') { | ||
| 381 | 390 | |
| 382 | 391 | if (!isValid) { |
| 383 | 392 | console.debug(`Invalid roll formula: ${formula}`); |
| 384 | 393 | return invalidRollPlaceholder''; |
| 385 | 394 | } |
| 386 | 395 | |
| 387 | 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 | 405 | * Returns the difference between two times. Works with any time format acceptable by moment(). |
| 394 | 406 | * Can work with {{date}} {{time}} macros |
| 395 | 407 | * @paramreturns {stringMacro} input - The string to replace time difference macros in.macro |
| 396 | - * @returns {string} The string with replaced time difference macros. | |
| 397 | 408 | */ |
| 398 | 409 | function timeDiffReplacegetTimeDiffMacro(input) { |
| 399 | 410 | const timeDiffPattern = /{{timeDiff::(.*?)::(.*?)}}/gi; |
| 400 | - | |
| 411 | + const timeDiffReplace = (_match, matchPart1, matchPart2) => { | |
| 401 | - const output = input.replace(timeDiffPattern, (_match, matchPart1, matchPart2) => { | |
| 402 | 412 | const time1 = moment(matchPart1); |
| 403 | 413 | const time2 = moment(matchPart2); |
| 404 | 414 | |
| 405 | 415 | const timeDifference = moment.duration(time1.diff(time2)); |
| 406 | 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 | 424 | * @param {string} content - The string to substitute parameters in. |
| 415 | 425 | * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param |
| 416 | 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 | 428 | * @returns {string} The string with substituted parameters. |
| 418 | 429 | */ |
| 419 | 430 | export function evaluateMacros(content, env, postProcessFn) { |
| 420 | 431 | if (!content) { |
| 421 | 432 | return ''; |
| 422 | 433 | } |
| 423 | 434 | |
| 435 | + postProcessFn = typeof postProcessFn === 'function' ? postProcessFn : (x => x); | |
| 424 | 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 | 464 | content { =regex: content.replace(/{{inputlastMessage}}/gi, replace: () => String($('#send_textarea').valgetLastMessage())); }, |
| 465 | + { regex: /{{lastMessageId}}/gi, replace: () => String(getLastMessageId() ?? '') }, | |
| 466 | + { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, | |
| 467 | + { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, | |
| 468 | + { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, | |
| 469 | + { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, | |
| 470 | + { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, | |
| 471 | + { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, | |
| 472 | + { regex: /\{\{\/\/([\s\S]*?)\}\}/gm, replace: () => '' }, | |
| 473 | + { regex: /{{time}}/gi, replace: () => moment().format('LT') }, | |
| 474 | + { regex: /{{date}}/gi, replace: () => moment().format('LL') }, | |
| 475 | + { regex: /{{weekday}}/gi, replace: () => moment().format('dddd') }, | |
| 476 | + { regex: /{{isotime}}/gi, replace: () => moment().format('HH:mm') }, | |
| 477 | + { regex: /{{isodate}}/gi, replace: () => moment().format('YYYY-MM-DD') }, | |
| 478 | + { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, | |
| 479 | + { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, | |
| 480 | + { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, | |
| 481 | + getTimeDiffMacro(), | |
| 482 | + getBannedWordsMacro(), | |
| 483 | + getRandomReplaceMacro(), | |
| 484 | + getPickReplaceMacro(rawContent), | |
| 485 | + ]; | |
| 445 | 486 | |
| 446 | 487 | // Add all registered macros to the env object |
| 447 | - const nonce = uuidv4(); | |
| 448 | 488 | MacrosParser.populateEnv(env); |
| 489 | + const nonce = uuidv4(); | |
| 490 | + const envMacros = []; | |
| 449 | 491 | |
| 450 | 492 | // Substitute passed-in variables |
| 451 | 493 | for (const varName in env) { |
| 452 | 494 | if (!Object.hasOwn(env, varName)) continue; |
| 453 | 495 | |
| 454 | 496 | contentconst envRegex = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => {; |
| 497 | + const envReplace = () => { | |
| 455 | 498 | const param = env[varName]; |
| 456 | 499 | const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param); |
| 457 | 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 | 519 | content = content.replace(/{{time}}/gimacro.regex, (...args) => momentpostProcessFn()macro.formatreplace('LT'...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 | 522 | return content; |
| 494 | 523 | } |
| @@ -223,85 +223,33 @@ export function resolveVariable(name, scope = null) { | ||
| 223 | 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 | 232 | // Replace {{setvar::name::value}} with empty string and set the variable name to value |
| 244 | 233 | line{ =regex: line.replace(/{{setvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setLocalVariable(name.trim(), value); return ''; } }, |
| 245 | - name = name.trim(); | |
| 246 | - setLocalVariable(name, value); | |
| 247 | - return ''; | |
| 248 | - }); | |
| 249 | - | |
| 250 | 234 | // Replace {{addvar::name::value}} with empty string and add value to the variable value |
| 251 | 235 | line{ =regex: line.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addLocalVariable(name.trim(), value); return ''; } }, |
| 252 | - name = name.trim(); | |
| 253 | - addLocalVariable(name, value); | |
| 254 | - return ''; | |
| 255 | - }); | |
| 256 | - | |
| 257 | 236 | // Replace {{incvar::name}} with empty string and increment the variable name by 1 |
| 258 | 237 | line{ =regex: line.replace(/{{incvar::([^}]+)}}/gi, replace: (_, name) => {incrementLocalVariable(name.trim()) }, |
| 259 | - name = name.trim(); | |
| 260 | - return incrementLocalVariable(name); | |
| 261 | - }); | |
| 262 | - | |
| 263 | 238 | // Replace {{decvar::name}} with empty string and decrement the variable name by 1 |
| 264 | 239 | line{ =regex: line.replace(/{{decvar::([^}]+)}}/gi, replace: (_, name) => {decrementLocalVariable(name.trim()) }, |
| 265 | - name = name.trim(); | |
| 240 | + // Replace {{getvar::name}} with the value of the variable name | |
| 266 | - return decrementLocalVariable(name); | |
| 241 | + { regex: /{{getvar::([^}]+)}}/gi, replace: (_, name) => getLocalVariable(name.trim()) }, | |
| 267 | - }); | |
| 268 | - | |
| 269 | - // Replace {{getglobalvar::name}} with the value of the global variable name | |
| 270 | - line = line.replace(/{{getglobalvar::([^}]+)}}/gi, (_, name) => { | |
| 271 | - name = name.trim(); | |
| 272 | - return getGlobalVariable(name); | |
| 273 | - }); | |
| 274 | - | |
| 275 | 242 | // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value |
| 276 | 243 | line{ =regex: line.replace(/{{setglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setGlobalVariable(name.trim(), value); return ''; } }, |
| 277 | - name = name.trim(); | |
| 278 | - setGlobalVariable(name, value); | |
| 279 | - return ''; | |
| 280 | - }); | |
| 281 | - | |
| 282 | 244 | // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value |
| 283 | 245 | line{ =regex: line.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addGlobalVariable(name.trim(), value); return ''; } }, |
| 284 | - name = name.trim(); | |
| 285 | - addGlobalVariable(name, value); | |
| 286 | - return ''; | |
| 287 | - }); | |
| 288 | - | |
| 289 | 246 | // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1 |
| 290 | 247 | line{ =regex: line.replace(/{{incglobalvar::([^}]+)}}/gi, replace: (_, name) => {incrementGlobalVariable(name.trim()) }, |
| 291 | - name = name.trim(); | |
| 292 | - return incrementGlobalVariable(name); | |
| 293 | - }); | |
| 294 | - | |
| 295 | 248 | // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1 |
| 296 | 249 | line{ =regex: line.replace(/{{decglobalvar::([^}]+)}}/gi, replace: (_, name) => {decrementGlobalVariable(name.trim()) }, |
| 297 | - name = name.trim(); | |
| 250 | + // Replace {{getglobalvar::name}} with the value of the global variable name | |
| 298 | - return decrementGlobalVariable(name); | |
| 251 | + { regex: /{{getglobalvar::([^}]+)}}/gi, replace: (_, name) => getGlobalVariable(name.trim()) }, | |
| 299 | - }); | |
| 252 | + ]; | |
| 300 | - | |
| 301 | - lines[i] = line; | |
| 302 | - } | |
| 303 | - | |
| 304 | - return lines.join('\n'); | |
| 305 | 253 | } |
| 306 | 254 | |
| 307 | 255 | async function listVariablesCallback(args) { |
| @@ -2148,7 +2096,8 @@ export function registerVariableCommands() { | ||
| 2148 | 2096 | callback: sortArrayObjectCallback, |
| 2149 | 2097 | returns: 'the sorted list or dictionary keys', |
| 2150 | 2098 | namedArgumentList: [ |
| 2151 | 2099 | SlashCommandNamedArgument.fromProps({ name: 'keysort', |
| 2100 | + name: 'keysort', | |
| 2152 | 2101 | description: 'whether to sort by key or value; ignored for lists', |
| 2153 | 2102 | typeList: [ARGUMENT_TYPE.BOOLEAN], |
| 2154 | 2103 | enumList: ['true', 'false'], |