Macros: refactor with a single replace point
| @@ -565,16 +565,11 @@ 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 = { | 573 | const syspromptMacros = { |
| 579 | 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content), | 574 | 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content), |
| 580 | 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content, | 575 | 'defaultSystemPrompt|instructSystem|instructSystemPrompt': power_user.sysprompt.content, |
| @@ -598,20 +593,24 @@ export function replaceInstructMacros(input, env) { | |||
| 598 | 'instructLastInput|instructLastUserPrefix': power_user.instruct.last_input_sequence || power_user.instruct.input_sequence, | 593 | 'instructLastInput|instructLastUserPrefix': power_user.instruct.last_input_sequence || power_user.instruct.input_sequence, |
| 599 | }; | 594 | }; |
| 600 | 595 | ||
| 596 | const macros = []; | ||
| 597 | |||
| 601 | for (const [placeholder, value] of Object.entries(instructMacros)) { | 598 | for (const [placeholder, value] of Object.entries(instructMacros)) { |
| 602 | const regex = new RegExp(`{{(${placeholder})}}`, 'gi'); | 599 | const regex = new RegExp(`{{(${placeholder})}}`, 'gi'); |
| 603 | input = input.replace(regex, power_user.instruct.enabled ? value : ''); | 600 | const replace = () => power_user.instruct.enabled ? value : ''; |
| 601 | macros.push({ regex, replace }); | ||
| 604 | } | 602 | } |
| 605 | 603 | ||
| 606 | for (const [placeholder, value] of Object.entries(syspromptMacros)) { | 604 | for (const [placeholder, value] of Object.entries(syspromptMacros)) { |
| 607 | const regex = new RegExp(`{{(${placeholder})}}`, 'gi'); | 605 | const regex = new RegExp(`{{(${placeholder})}}`, 'gi'); |
| 608 | input = input.replace(regex, power_user.sysprompt.enabled ? value : ''); | 606 | const replace = () => power_user.sysprompt.enabled ? value : ''; |
| 607 | macros.push({ regex, replace }); | ||
| 609 | } | 608 | } |
| 610 | 609 | ||
| 611 | input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator); | 610 | macros.push({ regex: /{{exampleSeparator}}/gi, replace: () => power_user.context.example_separator }); |
| 612 | input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start); | 611 | macros.push({ regex: /{{chatStart}}/gi, replace: () => power_user.context.chat_start }); |
| 613 | 612 | ||
| 614 | return input; | 613 | return macros; |
| 615 | } | 614 | } |
| 616 | 615 | ||
| 617 | jQuery(() => { | 616 | 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 | } | 278 | } |
| 282 | } | 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 | /** |
| @@ -423,72 +433,89 @@ export function evaluateMacros(content, env) { | |||
| 423 | 433 | ||
| 424 | const rawContent = content; | 434 | const rawContent = content; |
| 425 | 435 | ||
| 426 | // Legacy non-macro substitutions | 436 | /** |
| 427 | content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user); | 437 | * Built-ins running before the env variables |
| 428 | content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char); | 438 | * @type {Macro[]} |
| 429 | content = content.replace(/<CHAR>/gi, typeof env.char === 'function' ? env.char() : env.char); | 439 | * */ |
| 430 | content = content.replace(/<CHARIFNOTGROUP>/gi, typeof env.group === 'function' ? env.group() : env.group); | 440 | const preEnvMacros = [ |
| 431 | content = content.replace(/<GROUP>/gi, typeof env.group === 'function' ? env.group() : env.group); | 441 | // Legacy non-curly macros |
| 432 | 442 | { regex: /<USER>/gi, replace: () => typeof env.user === 'function' ? env.user() : env.user }, | |
| 433 | // Short circuit if there are no macros | 443 | { regex: /<BOT>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char }, |
| 434 | if (!content.includes('{{')) { | 444 | { regex: /<CHAR>/gi, replace: () => typeof env.char === 'function' ? env.char() : env.char }, |
| 435 | return content; | 445 | { regex: /<CHARIFNOTGROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group }, |
| 436 | } | 446 | { regex: /<GROUP>/gi, replace: () => typeof env.group === 'function' ? env.group() : env.group }, |
| 447 | getDiceRollMacro(), | ||
| 448 | ...getInstructMacros(env), | ||
| 449 | ...getVariableMacros(), | ||
| 450 | { regex: /{{newline}}/gi, replace: () => '\n' }, | ||
| 451 | { regex: /(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, replace: () => '' }, | ||
| 452 | { regex: /{{noop}}/gi, replace: () => '' }, | ||
| 453 | { regex: /{{input}}/gi, replace: () => String($('#send_textarea').val()) }, | ||
| 454 | ]; | ||
| 437 | 455 | ||
| 438 | content = diceRollReplace(content); | 456 | /** |
| 439 | content = replaceInstructMacros(content, env); | 457 | * Built-ins running after the env variables |
| 440 | content = replaceVariableMacros(content); | 458 | * @type {Macro[]} |
| 441 | content = content.replace(/{{newline}}/gi, '\n'); | 459 | */ |
| 442 | content = content.replace(/(?:\r?\n)*{{trim}}(?:\r?\n)*/gi, ''); | 460 | const postEnvMacros = [ |
| 443 | content = content.replace(/{{noop}}/gi, ''); | 461 | { regex: /{{maxPrompt}}/gi, replace: () => String(getMaxContextSize()) }, |
| 444 | content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val())); | 462 | { regex: /{{lastMessage}}/gi, replace: () => getLastMessage() }, |
| 463 | { regex: /{{lastMessageId}}/gi, replace: () => String(getLastMessageId() ?? '') }, | ||
| 464 | { regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() }, | ||
| 465 | { regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() }, | ||
| 466 | { regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') }, | ||
| 467 | { regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') }, | ||
| 468 | { regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') }, | ||
| 469 | { regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') }, | ||
| 470 | { regex: /\{\{\/\/([\s\S]*?)\}\}/gm, replace: () => '' }, | ||
| 471 | { regex: /{{time}}/gi, replace: () => moment().format('LT') }, | ||
| 472 | { regex: /{{date}}/gi, replace: () => moment().format('LL') }, | ||
| 473 | { regex: /{{weekday}}/gi, replace: () => moment().format('dddd') }, | ||
| 474 | { regex: /{{isotime}}/gi, replace: () => moment().format('HH:mm') }, | ||
| 475 | { regex: /{{isodate}}/gi, replace: () => moment().format('YYYY-MM-DD') }, | ||
| 476 | { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, | ||
| 477 | { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, | ||
| 478 | { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, | ||
| 479 | getTimeDiffMacro(), | ||
| 480 | getBannedWordsMacro(), | ||
| 481 | getRandomReplaceMacro(), | ||
| 482 | getPickReplaceMacro(rawContent), | ||
| 483 | ]; | ||
| 445 | 484 | ||
| 446 | // Add all registered macros to the env object | 485 | // Add all registered macros to the env object |
| 447 | const nonce = uuidv4(); | ||
| 448 | MacrosParser.populateEnv(env); | 486 | MacrosParser.populateEnv(env); |
| 487 | const nonce = uuidv4(); | ||
| 488 | const envMacros = []; | ||
| 449 | 489 | ||
| 450 | // Substitute passed-in variables | 490 | // Substitute passed-in variables |
| 451 | for (const varName in env) { | 491 | for (const varName in env) { |
| 452 | if (!Object.hasOwn(env, varName)) continue; | 492 | if (!Object.hasOwn(env, varName)) continue; |
| 453 | 493 | ||
| 454 | content = content.replace(new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'), () => { | 494 | const envRegex = new RegExp(`{{${escapeRegex(varName)}}}`, 'gi'); |
| 495 | const envReplace = () => { | ||
| 455 | const param = env[varName]; | 496 | const param = env[varName]; |
| 456 | const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param); | 497 | const value = MacrosParser.sanitizeMacroValue(typeof param === 'function' ? param(nonce) : param); |
| 457 | return value; | 498 | return value; |
| 458 | }); | 499 | }; |
| 500 | |||
| 501 | envMacros.push({ regex: envRegex, replace: envReplace }); | ||
| 502 | } | ||
| 503 | |||
| 504 | const macros = [...preEnvMacros, ...envMacros, ...postEnvMacros]; | ||
| 505 | |||
| 506 | for (const macro of macros) { | ||
| 507 | // Stop if the content is empty | ||
| 508 | if (!content) { | ||
| 509 | break; | ||
| 510 | } | ||
| 511 | |||
| 512 | // Short-circuit if no curly braces are found | ||
| 513 | if (!macro.regex.source.startsWith('<') && !content.includes('{{')) { | ||
| 514 | break; | ||
| 515 | } | ||
| 516 | |||
| 517 | content = content.replace(macro.regex, macro.replace); | ||
| 459 | } | 518 | } |
| 460 | 519 | ||
| 461 | content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize())); | ||
| 462 | content = content.replace(/{{lastMessage}}/gi, () => getLastMessage()); | ||
| 463 | content = content.replace(/{{lastMessageId}}/gi, () => String(getLastMessageId() ?? '')); | ||
| 464 | content = content.replace(/{{lastUserMessage}}/gi, () => getLastUserMessage()); | ||
| 465 | content = content.replace(/{{lastCharMessage}}/gi, () => getLastCharMessage()); | ||
| 466 | content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? '')); | ||
| 467 | content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? '')); | ||
| 468 | content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? '')); | ||
| 469 | content = content.replace(/{{reverse:(.+?)}}/gi, (_, str) => Array.from(str).reverse().join('')); | ||
| 470 | |||
| 471 | content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, ''); | ||
| 472 | |||
| 473 | content = content.replace(/{{time}}/gi, () => moment().format('LT')); | ||
| 474 | content = content.replace(/{{date}}/gi, () => moment().format('LL')); | ||
| 475 | content = content.replace(/{{weekday}}/gi, () => moment().format('dddd')); | ||
| 476 | content = content.replace(/{{isotime}}/gi, () => moment().format('HH:mm')); | ||
| 477 | content = content.replace(/{{isodate}}/gi, () => moment().format('YYYY-MM-DD')); | ||
| 478 | |||
| 479 | content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => { | ||
| 480 | const formattedTime = moment().format(format); | ||
| 481 | return formattedTime; | ||
| 482 | }); | ||
| 483 | content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage()); | ||
| 484 | content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => { | ||
| 485 | const utcOffset = parseInt(offset, 10); | ||
| 486 | const utcTime = moment().utc().utcOffset(utcOffset).format('LT'); | ||
| 487 | return utcTime; | ||
| 488 | }); | ||
| 489 | content = timeDiffReplace(content); | ||
| 490 | content = bannedWordsReplace(content); | ||
| 491 | content = randomReplace(content); | ||
| 492 | content = pickReplace(content, rawContent); | ||
| 493 | return content; | 520 | return content; |
| 494 | } | 521 | } |
| @@ -223,85 +223,34 @@ 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 {import('./macros.js').Macro[]} |
| 228 | 228 | */ | |
| 229 | for (let i = 0; i < lines.length; i++) { | 229 | export function getVariableMacros() { |
| 230 | let line = lines[i]; | 230 | const macros = [ |
| 231 | |||
| 232 | // Skip lines without macros | ||
| 233 | if (!line || !line.includes('{{')) { | ||
| 234 | continue; | ||
| 235 | } | ||
| 236 | |||
| 237 | // Replace {{getvar::name}} with the value of the variable name | 231 | // Replace {{getvar::name}} with the value of the variable name |
| 238 | line = line.replace(/{{getvar::([^}]+)}}/gi, (_, name) => { | 232 | { regex: /{{getvar::([^}]+)}}/gi, replace: (_, name) => getLocalVariable(name.trim()) }, |
| 239 | name = name.trim(); | ||
| 240 | return getLocalVariable(name); | ||
| 241 | }); | ||
| 242 | |||
| 243 | // Replace {{setvar::name::value}} with empty string and set the variable name to value | 233 | // Replace {{setvar::name::value}} with empty string and set the variable name to value |
| 244 | line = line.replace(/{{setvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => { | 234 | { regex: /{{setvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setLocalVariable(name.trim(), value); return ''; } }, |
| 245 | name = name.trim(); | ||
| 246 | setLocalVariable(name, value); | ||
| 247 | return ''; | ||
| 248 | }); | ||
| 249 | |||
| 250 | // Replace {{addvar::name::value}} with empty string and add value to the variable value | 235 | // Replace {{addvar::name::value}} with empty string and add value to the variable value |
| 251 | line = line.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => { | 236 | { regex: /{{addvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addLocalVariable(name.trim(), value); return ''; } }, |
| 252 | name = name.trim(); | ||
| 253 | addLocalVariable(name, value); | ||
| 254 | return ''; | ||
| 255 | }); | ||
| 256 | |||
| 257 | // Replace {{incvar::name}} with empty string and increment the variable name by 1 | 237 | // Replace {{incvar::name}} with empty string and increment the variable name by 1 |
| 258 | line = line.replace(/{{incvar::([^}]+)}}/gi, (_, name) => { | 238 | { regex: /{{incvar::([^}]+)}}/gi, replace: (_, name) => incrementLocalVariable(name.trim()) }, |
| 259 | name = name.trim(); | ||
| 260 | return incrementLocalVariable(name); | ||
| 261 | }); | ||
| 262 | |||
| 263 | // Replace {{decvar::name}} with empty string and decrement the variable name by 1 | 239 | // Replace {{decvar::name}} with empty string and decrement the variable name by 1 |
| 264 | line = line.replace(/{{decvar::([^}]+)}}/gi, (_, name) => { | 240 | { regex: /{{decvar::([^}]+)}}/gi, replace: (_, name) => decrementLocalVariable(name.trim()) }, |
| 265 | name = name.trim(); | ||
| 266 | return decrementLocalVariable(name); | ||
| 267 | }); | ||
| 268 | |||
| 269 | // Replace {{getglobalvar::name}} with the value of the global variable name | 241 | // Replace {{getglobalvar::name}} with the value of the global variable name |
| 270 | line = line.replace(/{{getglobalvar::([^}]+)}}/gi, (_, name) => { | 242 | { regex: /{{getglobalvar::([^}]+)}}/gi, replace: (_, name) => getGlobalVariable(name.trim()) }, |
| 271 | name = name.trim(); | ||
| 272 | return getGlobalVariable(name); | ||
| 273 | }); | ||
| 274 | |||
| 275 | // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value | 243 | // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value |
| 276 | line = line.replace(/{{setglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => { | 244 | { regex: /{{setglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { setGlobalVariable(name.trim(), value); return ''; } }, |
| 277 | name = name.trim(); | ||
| 278 | setGlobalVariable(name, value); | ||
| 279 | return ''; | ||
| 280 | }); | ||
| 281 | |||
| 282 | // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value | 245 | // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value |
| 283 | line = line.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => { | 246 | { regex: /{{addglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addGlobalVariable(name.trim(), value); return ''; } }, |
| 284 | name = name.trim(); | ||
| 285 | addGlobalVariable(name, value); | ||
| 286 | return ''; | ||
| 287 | }); | ||
| 288 | |||
| 289 | // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1 | 247 | // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1 |
| 290 | line = line.replace(/{{incglobalvar::([^}]+)}}/gi, (_, name) => { | 248 | { regex: /{{incglobalvar::([^}]+)}}/gi, replace: (_, name) => incrementGlobalVariable(name.trim()) }, |
| 291 | name = name.trim(); | ||
| 292 | return incrementGlobalVariable(name); | ||
| 293 | }); | ||
| 294 | |||
| 295 | // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1 | 249 | // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1 |
| 296 | line = line.replace(/{{decglobalvar::([^}]+)}}/gi, (_, name) => { | 250 | { regex: /{{decglobalvar::([^}]+)}}/gi, replace: (_, name) => decrementGlobalVariable(name.trim()) }, |
| 297 | name = name.trim(); | 251 | ]; |
| 298 | return decrementGlobalVariable(name); | ||
| 299 | }); | ||
| 300 | |||
| 301 | lines[i] = line; | ||
| 302 | } | ||
| 303 | 252 | ||
| 304 | return lines.join('\n'); | 253 | return macros; |
| 305 | } | 254 | } |
| 306 | 255 | ||
| 307 | async function listVariablesCallback(args) { | 256 | async function listVariablesCallback(args) { |
| @@ -2148,7 +2097,8 @@ export function registerVariableCommands() { | |||
| 2148 | callback: sortArrayObjectCallback, | 2097 | callback: sortArrayObjectCallback, |
| 2149 | returns: 'the sorted list or dictionary keys', | 2098 | returns: 'the sorted list or dictionary keys', |
| 2150 | namedArgumentList: [ | 2099 | namedArgumentList: [ |
| 2151 | SlashCommandNamedArgument.fromProps({ name: 'keysort', | 2100 | SlashCommandNamedArgument.fromProps({ |
| 2101 | name: 'keysort', | ||
| 2152 | description: 'whether to sort by key or value; ignored for lists', | 2102 | description: 'whether to sort by key or value; ignored for lists', |
| 2153 | typeList: [ARGUMENT_TYPE.BOOLEAN], | 2103 | typeList: [ARGUMENT_TYPE.BOOLEAN], |
| 2154 | enumList: ['true', 'false'], | 2104 | enumList: ['true', 'false'], |