Preset Regex: refactor regex scope detection (#4618) * refactor: explicit type for regex scope + feat: bulk move regex * fix: eslint * i18n: polish * refactor: jsdoc + use object type for option * fix: default return type for `getScriptsByType` * refactor: simplify * refactor: share default options * refactor: remove unrequired css * fix: move regexes to global + refactor: make regex operation interfaces more consistant --------- Co-authored-by: ZoinkCN <zoinkcn@outlook.com>
Signed| @@ -1677,9 +1677,9 @@ | ||
| 1677 | 1677 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", |
| 1678 | 1678 | "No scripts found": "没有找到脚本", |
| 1679 | 1679 | "ext_regex_scoped_scripts": "局部正则脚本", |
| 1680 | + "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | |
| 1680 | 1681 | "ext_regex_disallow_scoped": "不允许使用局部正则", |
| 1681 | 1682 | "ext_regex_allow_scoped": "允许使用局部正则", |
| 1682 | - "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | |
| 1683 | 1683 | "Regex Editor": "正则表达式编辑器", |
| 1684 | 1684 | "Test Mode": "测试模式", |
| 1685 | 1685 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", |
| @@ -1728,8 +1728,8 @@ | ||
| 1728 | 1728 | "ext_regex_disable_script": "禁用脚本", |
| 1729 | 1729 | "ext_regex_enable_script": "启用脚本", |
| 1730 | 1730 | "ext_regex_edit_script": "编辑脚本", |
| 1731 | 1731 | "ext_regex_move_to_global": "移至全局脚本移至全局", |
| 1732 | 1732 | "ext_regex_move_to_scoped": "移至作用域脚本移至局部", |
| 1733 | 1733 | "ext_regex_export_script": "导出脚本", |
| 1734 | 1734 | "ext_regex_delete_script": "删除脚本", |
| 1735 | 1735 | "Trigger Stable Diffusion": "触发Stable Diffusion", |
| @@ -31,7 +31,8 @@ | ||
| 31 | 31 | <small data-i18n="ext_regex_debugger">Debugger</small> |
| 32 | 32 | </div> |
| 33 | 33 | </div> |
| 34 | 34 | <divhr class="regex_bulk_operations flex-container justifyCenterregex_bulk_operations_hr" /> |
| 35 | + <div class="regex_bulk_operations flex-container"> | |
| 35 | 36 | <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All"> |
| 36 | 37 | <i class="fa-solid fa-check-double"></i> |
| 37 | 38 | </div> |
| @@ -43,6 +44,14 @@ | ||
| 43 | 44 | <i class="fa-solid fa-toggle-off"></i> |
| 44 | 45 | <small data-i18n="Disable">Disable</small> |
| 45 | 46 | </div> |
| 47 | + <div id="bulk_regex_move_to_global" class="menu_button menu_button_icon" hidden> | |
| 48 | + <i class="fa-solid fa-globe"></i> | |
| 49 | + <small data-i18n="ext_regex_move_to_global">Move to global scripts</small> | |
| 50 | + </div> | |
| 51 | + <div id="bulk_regex_move_to_scoped" class="menu_button menu_button_icon" hidden> | |
| 52 | + <i class="fa-solid fa-address-card"></i> | |
| 53 | + <small data-i18n="ext_regex_move_to_scoped">Move to scoped scripts</small> | |
| 54 | + </div> | |
| 46 | 55 | <div id="bulk_export_regex" class="menu_button menu_button_icon"> |
| 47 | 56 | <i class="fa-solid fa-file-export"></i> |
| 48 | 57 | <small data-i18n="Export">Export</small> |
| @@ -8,6 +8,56 @@ export { | ||
| 8 | 8 | }; |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | + * @enum {number} Regex scripts types | |
| 12 | + */ | |
| 13 | +export const SCRIPT_TYPES = { | |
| 14 | + GLOBAL: 0, | |
| 15 | + SCOPED: 1, | |
| 16 | +}; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * @typedef {import('../../char-data.js').RegexScriptData} RegexScript | |
| 20 | + */ | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * @typedef {object} GetRegexScriptsOptions | |
| 24 | + * @property {boolean} allowedOnly only return allowed scripts | |
| 25 | + */ | |
| 26 | +const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = { allowedOnly: false }; | |
| 27 | + | |
| 28 | +/** | |
| 29 | + * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data | |
| 30 | + * | |
| 31 | + * @param {GetRegexScriptsOptions} option | |
| 32 | + * @returns {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. | |
| 33 | + */ | |
| 34 | +export function getRegexScripts(option = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { | |
| 35 | + return [...Object.values(SCRIPT_TYPES).flatMap(type => getScriptsByType(type, option))]; | |
| 36 | +} | |
| 37 | + | |
| 38 | +/** | |
| 39 | + * Retrieves the regex scripts for a specific type. | |
| 40 | + * @param {SCRIPT_TYPES} scriptType | |
| 41 | + * @param {GetRegexScriptsOptions} option | |
| 42 | + * @returns {RegexScript[]} An array of regex scripts for the specified type. | |
| 43 | + */ | |
| 44 | +export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { | |
| 45 | + switch (scriptType) { | |
| 46 | + case SCRIPT_TYPES.GLOBAL: | |
| 47 | + return extension_settings.regex ?? []; | |
| 48 | + case SCRIPT_TYPES.SCOPED: { | |
| 49 | + if (allowedOnly && !extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar)) { | |
| 50 | + return []; | |
| 51 | + } | |
| 52 | + const scopedScripts = characters[this_chid]?.data?.extensions?.regex_scripts; | |
| 53 | + return Array.isArray(scopedScripts) ? scopedScripts : []; | |
| 54 | + } | |
| 55 | + default: | |
| 56 | + return []; | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +/** | |
| 11 | 61 | * @enum {number} Where the regex script should be applied |
| 12 | 62 | */ |
| 13 | 63 | const regex_placement = { |
| @@ -51,22 +101,6 @@ function sanitizeRegexMacro(x) { | ||
| 51 | 101 | }) : x; |
| 52 | 102 | } |
| 53 | 103 | |
| 54 | -function getScopedRegex() { | |
| 55 | - const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); | |
| 56 | - | |
| 57 | - if (!isAllowed) { | |
| 58 | - return []; | |
| 59 | - } | |
| 60 | - | |
| 61 | - const scripts = characters[this_chid]?.data?.extensions?.regex_scripts; | |
| 62 | - | |
| 63 | - if (!Array.isArray(scripts)) { | |
| 64 | - return []; | |
| 65 | - } | |
| 66 | - | |
| 67 | - return scripts; | |
| 68 | -} | |
| 69 | - | |
| 70 | 104 | /** |
| 71 | 105 | * Parent function to fetch a regexed version of a raw string |
| 72 | 106 | * @param {string} rawString The raw string to be regexed |
| @@ -87,7 +121,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | ||
| 87 | 121 | return finalString; |
| 88 | 122 | } |
| 89 | 123 | |
| 90 | - const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])]; | |
| 124 | + const allRegex = getRegexScripts({ allowedOnly: true }); | |
| 91 | 125 | allRegex.forEach((script) => { |
| 92 | 126 | if ( |
| 93 | 127 | // Script applies to Markdown and input is Markdown |
| @@ -126,7 +160,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | ||
| 126 | 160 | |
| 127 | 161 | /** |
| 128 | 162 | * Runs the provided regex script on the given string |
| 129 | 163 | * @param {import('./index.js').RegexScript} regexScript The regex script to run |
| 130 | 164 | * @param {string} rawString The string to run the regex script on |
| 131 | 165 | * @param {RegexScriptParams} params The parameters to use for the regex script |
| 132 | 166 | * @returns {string} The new string |
| @@ -7,8 +7,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | ||
| 7 | 7 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 8 | 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 9 | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | 10 | import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4, escapeHtml } from '../../utils.js'; |
| 11 | 11 | import { getRegexScripts, getScriptsByType, regex_placement, runRegexScript, SCRIPT_TYPES, substitute_find_regex } from './engine.js'; |
| 12 | 12 | import { t } from '../../i18n.js'; |
| 13 | 13 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 14 | 14 | |
| @@ -65,8 +65,8 @@ class RegexPresetManager { | ||
| 65 | 65 | * @returns {RegexPresetState} The current state object |
| 66 | 66 | */ |
| 67 | 67 | captureCurrentState() { |
| 68 | 68 | const globalScripts = this.regexListToPresetItems(extension_settingsgetScriptsByType(SCRIPT_TYPES.regexGLOBAL) || []); |
| 69 | 69 | const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED) || []); |
| 70 | 70 | |
| 71 | 71 | return { |
| 72 | 72 | global: globalScripts.map(item => item.id).sort(), |
| @@ -418,8 +418,8 @@ class RegexPresetManager { | ||
| 418 | 418 | id: id, |
| 419 | 419 | name: name, |
| 420 | 420 | isSelected: false, |
| 421 | 421 | global: this.regexListToPresetItems(extension_settingsgetScriptsByType(SCRIPT_TYPES.regexGLOBAL)), |
| 422 | 422 | scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED)), |
| 423 | 423 | }; |
| 424 | 424 | |
| 425 | 425 | if (isUpdate) { |
| @@ -465,15 +465,6 @@ class RegexPresetManager { | ||
| 465 | 465 | const presetManager = new RegexPresetManager(); |
| 466 | 466 | |
| 467 | 467 | /** |
| 468 | - * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data | |
| 469 | - * | |
| 470 | - * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. | |
| 471 | - */ | |
| 472 | -export function getRegexScripts() { | |
| 473 | - return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])]; | |
| 474 | -} | |
| 475 | - | |
| 476 | -/** | |
| 477 | 468 | * Toggle the icon for the "select all" checkbox in the regex settings. |
| 478 | 469 | * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected). |
| 479 | 470 | * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected). |
| @@ -485,16 +476,25 @@ function setToggleAllIcon(allAreChecked) { | ||
| 485 | 476 | selectAllIcon.toggleClass('fa-minus', allAreChecked); |
| 486 | 477 | } |
| 487 | 478 | |
| 479 | +function setMoveButtonsVisibility() { | |
| 480 | + const hasGlobalScripts = $('#saved_regex_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | |
| 481 | + const hasScopedScripts = | |
| 482 | + $('#saved_scoped_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | |
| 483 | + $('#bulk_regex_move_to_global').toggle(hasScopedScripts); | |
| 484 | + $('#bulk_regex_move_to_scoped').toggle(hasGlobalScripts); | |
| 485 | +} | |
| 486 | + | |
| 488 | 487 | /** |
| 489 | 488 | * Saves a regex script to the extension settings or character data. |
| 490 | 489 | * @param {import('../../char-data.js').RegexScriptData} regexScript |
| 491 | 490 | * @param {number} existingScriptIndex Index of the existing script |
| 492 | 491 | * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character? |
| 492 | + * @param {boolean} [saveSettings=true] Whether to save the settings immediately | |
| 493 | 493 | * @returns {Promise<void>} |
| 494 | 494 | */ |
| 495 | 495 | async function saveRegexScript(regexScript, existingScriptIndex, isScopedscriptType, saveSettings = true) { |
| 496 | 496 | // If not editing |
| 497 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 497 | + const array = getScriptsByType(scriptType); | |
| 498 | 498 | |
| 499 | 499 | // Assign a UUID if it doesn't exist |
| 500 | 500 | if (!regexScript.id) { |
| @@ -523,7 +523,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | ||
| 523 | 523 | array.push(regexScript); |
| 524 | 524 | } |
| 525 | 525 | |
| 526 | 526 | if (isScopedscriptType === SCRIPT_TYPES.SCOPED) { |
| 527 | 527 | await writeExtensionField(this_chid, 'regex_scripts', array); |
| 528 | 528 | |
| 529 | 529 | // Add the character to the allowed list |
| @@ -532,6 +532,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | ||
| 532 | 532 | } |
| 533 | 533 | } |
| 534 | 534 | |
| 535 | + if (saveSettings) { | |
| 535 | 536 | saveSettingsDebounced(); |
| 536 | 537 | await loadRegexScripts(); |
| 537 | 538 | |
| @@ -540,6 +541,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | ||
| 540 | 541 | if (currentChatId !== undefined && currentChatId !== null) { |
| 541 | 542 | await reloadCurrentChat(); |
| 542 | 543 | } |
| 544 | + } | |
| 543 | 545 | |
| 544 | 546 | const debuggerPopup = $('#regex_debugger_popup'); |
| 545 | 547 | if (debuggerPopup.length) { |
| @@ -547,21 +549,48 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | ||
| 547 | 549 | } |
| 548 | 550 | } |
| 549 | 551 | |
| 550 | -async function deleteRegexScript({ id, isScoped }) { | |
| 552 | +/** | |
| 551 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 553 | + * Delete a regex script | |
| 554 | + * @param {string} id | |
| 555 | + * @param {SCRIPT_TYPES} scriptType | |
| 556 | + * @param {boolean} saveSettings | |
| 557 | + * @returns {Promise<void>} | |
| 558 | + */ | |
| 559 | +async function deleteRegexScript(id, scriptType, saveSettings = true) { | |
| 560 | + const array = getScriptsByType(scriptType); | |
| 552 | 561 | |
| 553 | 562 | const existingScriptIndex = array.findIndex((script) => script.id === id); |
| 554 | 563 | if (existingScriptIndex !== -1) { |
| 555 | 564 | array.splice(existingScriptIndex, 1); |
| 556 | 565 | |
| 557 | 566 | if (isScopedscriptType === SCRIPT_TYPES.SCOPED) { |
| 558 | 567 | await writeExtensionField(this_chid, 'regex_scripts', array); |
| 559 | 568 | } |
| 560 | - | |
| 569 | + if (saveSettings) { | |
| 561 | 570 | saveSettingsDebounced(); |
| 562 | 571 | await loadRegexScripts(); |
| 563 | 572 | } |
| 564 | 573 | } |
| 574 | +} | |
| 575 | + | |
| 576 | +/** | |
| 577 | + * Move a regex script from one type to another | |
| 578 | + * @param {import('../../char-data.js').RegexScriptData} script | |
| 579 | + * @param {SCRIPT_TYPES} toType | |
| 580 | + * @param {SCRIPT_TYPES|null} fromType | |
| 581 | + * @param {boolean} saveSettings | |
| 582 | + * @returns {Promise<void>} | |
| 583 | + */ | |
| 584 | +async function moveRegexScript(script, toType, fromType = null, saveSettings = true) { | |
| 585 | + if (!fromType) { | |
| 586 | + fromType = getScriptType(script); | |
| 587 | + } | |
| 588 | + if (fromType === toType || fromType === -1) { | |
| 589 | + return; | |
| 590 | + } | |
| 591 | + await deleteRegexScript(script.id, fromType, false); | |
| 592 | + await saveRegexScript(script, -1, toType, saveSettings); | |
| 593 | +} | |
| 565 | 594 | |
| 566 | 595 | async function loadRegexScripts() { |
| 567 | 596 | $('#saved_regex_scripts').empty(); |
| @@ -574,13 +603,13 @@ async function loadRegexScripts() { | ||
| 574 | 603 | * Renders a script to the UI. |
| 575 | 604 | * @param {string} container Container to render the script to |
| 576 | 605 | * @param {import('../../char-data.js').RegexScriptData} script Script data |
| 577 | 606 | * @param {booleanSCRIPT_TYPES} isScoped ScriptscriptType isglobal? scoped to a character? |
| 578 | 607 | * @param {number} index Index of the script in the array |
| 579 | 608 | */ |
| 580 | 609 | function renderScript(container, script, isScopedscriptType, index) { |
| 581 | 610 | // Have to clone here |
| 582 | 611 | const scriptHtml = scriptTemplate.clone(); |
| 583 | 612 | const save = () => saveRegexScript(script, index, isScopedscriptType); |
| 584 | 613 | |
| 585 | 614 | if (!script.id) { |
| 586 | 615 | script.id = uuidv4(); |
| @@ -600,7 +629,7 @@ async function loadRegexScripts() { | ||
| 600 | 629 | scriptHtml.find('.disable_regex').prop('checked', false).trigger('input'); |
| 601 | 630 | }); |
| 602 | 631 | scriptHtml.find('.edit_existing_regex').on('click', async function () { |
| 603 | 632 | await onRegexEditorOpenClick(scriptHtml.attr('id'), isScopedscriptType); |
| 604 | 633 | }); |
| 605 | 634 | scriptHtml.find('.move_to_global').on('click', async function () { |
| 606 | 635 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM); |
| @@ -608,29 +637,22 @@ async function loadRegexScripts() { | ||
| 608 | 637 | if (!confirm) { |
| 609 | 638 | return; |
| 610 | 639 | } |
| 611 | - | |
| 640 | + await moveRegexScript(script, SCRIPT_TYPES.GLOBAL, scriptType); | |
| 612 | - await deleteRegexScript({ id: script.id, isScoped: true }); | |
| 613 | - await saveRegexScript(script, -1, false); | |
| 614 | 641 | }); |
| 615 | 642 | scriptHtml.find('.move_to_scoped').on('click', async function () { |
| 616 | 643 | if (this_chid === undefined) { |
| 617 | 644 | toastr.error(t`No character selected.`); |
| 618 | 645 | return; |
| 619 | 646 | } |
| 620 | - | |
| 621 | 647 | if (selected_group) { |
| 622 | 648 | toastr.error(t`Cannot edit scoped scripts in group chats.`); |
| 623 | 649 | return; |
| 624 | 650 | } |
| 625 | - | |
| 626 | 651 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); |
| 627 | - | |
| 628 | 652 | if (!confirm) { |
| 629 | 653 | return; |
| 630 | 654 | } |
| 631 | - | |
| 655 | + await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType); | |
| 632 | - await deleteRegexScript({ id: script.id, isScoped: false }); | |
| 633 | - await saveRegexScript(script, -1, true); | |
| 634 | 656 | }); |
| 635 | 657 | scriptHtml.find('.export_regex').on('click', async function () { |
| 636 | 658 | const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; |
| @@ -639,15 +661,14 @@ async function loadRegexScripts() { | ||
| 639 | 661 | }); |
| 640 | 662 | scriptHtml.find('.delete_regex').on('click', async function () { |
| 641 | 663 | const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); |
| 642 | - | |
| 643 | 664 | if (!confirm) { |
| 644 | 665 | return; |
| 645 | 666 | } |
| 646 | - | |
| 667 | + await deleteRegexScript(script.id, scriptType); | |
| 647 | - await deleteRegexScript({ id: script.id, isScoped }); | |
| 648 | 668 | await reloadCurrentChat(); |
| 649 | 669 | }); |
| 650 | 670 | scriptHtml.find('.regex_bulk_checkbox').on('change', function () { |
| 671 | + setMoveButtonsVisibility(); | |
| 651 | 672 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 652 | 673 | const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; |
| 653 | 674 | setToggleAllIcon(allAreChecked); |
| @@ -656,22 +677,24 @@ async function loadRegexScripts() { | ||
| 656 | 677 | $(container).append(scriptHtml); |
| 657 | 678 | } |
| 658 | 679 | |
| 659 | 680 | extension_settings?getScriptsByType(SCRIPT_TYPES.regex?GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, falseSCRIPT_TYPES.GLOBAL, index)); |
| 660 | 681 | characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scripts?SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, trueSCRIPT_TYPES.SCOPED, index)); |
| 661 | 682 | |
| 662 | 683 | const isAllowedisScopedAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); |
| 663 | 684 | $('#regex_scoped_toggle').prop('checked', isAllowedisScopedAllowed); |
| 685 | + | |
| 686 | + setMoveButtonsVisibility(); | |
| 664 | 687 | } |
| 665 | 688 | |
| 666 | 689 | /** |
| 667 | 690 | * Opens the regex editor. |
| 668 | 691 | * @param {string|boolean} existingId Existing ID |
| 669 | 692 | * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character? |
| 670 | 693 | * @returns {Promise<void>} |
| 671 | 694 | */ |
| 672 | 695 | async function onRegexEditorOpenClick(existingId, isScopedscriptType) { |
| 673 | 696 | const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor')); |
| 674 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 697 | + const array = getScriptsByType(scriptType); | |
| 675 | 698 | |
| 676 | 699 | // If an ID exists, fill in all the values |
| 677 | 700 | let existingScriptIndex = -1; |
| @@ -776,7 +799,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) { | ||
| 776 | 799 | maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())), |
| 777 | 800 | }; |
| 778 | 801 | |
| 779 | 802 | saveRegexScript(newRegexScript, existingScriptIndex, isScopedscriptType); |
| 780 | 803 | } |
| 781 | 804 | } |
| 782 | 805 | |
| @@ -973,7 +996,7 @@ function populateDebuggerRuleList(container) { | ||
| 973 | 996 | return; |
| 974 | 997 | } |
| 975 | 998 | |
| 976 | 999 | const globalScriptIds = new Set(getScriptsByType(extension_settingsSCRIPT_TYPES.regex ?? []GLOBAL).map(s => s.id)); |
| 977 | 1000 | const globalScripts = []; |
| 978 | 1001 | const scopedScripts = []; |
| 979 | 1002 | |
| @@ -981,11 +1004,11 @@ function populateDebuggerRuleList(container) { | ||
| 981 | 1004 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy |
| 982 | 1005 | if (globalScriptIds.has(script.id)) { |
| 983 | 1006 | // @ts-ignore |
| 984 | 1007 | scriptCopy.isScopedtype = falseSCRIPT_TYPES.SCOPED; |
| 985 | 1008 | globalScripts.push(scriptCopy); |
| 986 | 1009 | } else { |
| 987 | 1010 | // @ts-ignore |
| 988 | 1011 | scriptCopy.isScopedtype = trueSCRIPT_TYPES.SCOPED; |
| 989 | 1012 | scopedScripts.push(scriptCopy); |
| 990 | 1013 | } |
| 991 | 1014 | }); |
| @@ -1002,10 +1025,17 @@ function populateDebuggerRuleList(container) { | ||
| 1002 | 1025 | ruleElement.find('.rule-name').text(script.scriptName); |
| 1003 | 1026 | ruleElement.find('.rule-regex').text(script.findRegex); |
| 1004 | 1027 | // @ts-ignore |
| 1005 | - ruleElement.find('.rule-scope').text(script.isScoped ? t`Scoped` : t`Global`); | |
| 1028 | + ruleElement | |
| 1029 | + .find('.rule-scope') | |
| 1030 | + .text( | |
| 1031 | + { | |
| 1032 | + [SCRIPT_TYPES.SCOPED]: t`Scoped`, | |
| 1033 | + [SCRIPT_TYPES.GLOBAL]: t`Global`, | |
| 1034 | + }[script.type], | |
| 1035 | + ); | |
| 1006 | 1036 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); |
| 1007 | 1037 | // @ts-ignore |
| 1008 | 1038 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScopedtype)); |
| 1009 | 1039 | |
| 1010 | 1040 | ruleElement.on('click', function (event) { |
| 1011 | 1041 | if ($(event.target).is('input, .menu_button, .menu_button i')) { |
| @@ -1364,10 +1394,10 @@ async function toggleRegexCallback(args, scriptName) { | ||
| 1364 | 1394 | break; |
| 1365 | 1395 | } |
| 1366 | 1396 | |
| 1367 | - const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | |
| 1397 | + const scriptType = getScriptType(script); | |
| 1368 | 1398 | const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOfgetScriptsByType(scriptscriptType) : scripts.indexOf(script); |
| 1369 | 1399 | |
| 1370 | 1400 | await saveRegexScript(script, index, isScopedscriptType); |
| 1371 | 1401 | if (script.disabled) { |
| 1372 | 1402 | !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); |
| 1373 | 1403 | } else { |
| @@ -1380,9 +1410,9 @@ async function toggleRegexCallback(args, scriptName) { | ||
| 1380 | 1410 | /** |
| 1381 | 1411 | * Performs the import of the regex object. |
| 1382 | 1412 | * @param {Object} regexScript Input object |
| 1383 | 1413 | * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character? |
| 1384 | 1414 | */ |
| 1385 | 1415 | async function onRegexImportObjectChange(regexScript, isScopedscriptType) { |
| 1386 | 1416 | try { |
| 1387 | 1417 | if (!regexScript.scriptName) { |
| 1388 | 1418 | throw new Error('No script name provided.'); |
| @@ -1391,10 +1421,10 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | ||
| 1391 | 1421 | // Assign a new UUID |
| 1392 | 1422 | regexScript.id = uuidv4(); |
| 1393 | 1423 | |
| 1394 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 1424 | + const array = getScriptsByType(scriptType); | |
| 1395 | 1425 | array.push(regexScript); |
| 1396 | 1426 | |
| 1397 | 1427 | if (isScopedscriptType === SCRIPT_TYPES.SCOPED) { |
| 1398 | 1428 | await writeExtensionField(this_chid, 'regex_scripts', array); |
| 1399 | 1429 | } |
| 1400 | 1430 | |
| @@ -1411,9 +1441,9 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | ||
| 1411 | 1441 | /** |
| 1412 | 1442 | * Performs the import of the regex file. |
| 1413 | 1443 | * @param {File} file Input file |
| 1414 | 1444 | * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character? |
| 1415 | 1445 | */ |
| 1416 | 1446 | async function onRegexImportFileChange(file, isScopedscriptType) { |
| 1417 | 1447 | if (!file) { |
| 1418 | 1448 | toastr.error('No file provided.'); |
| 1419 | 1449 | return; |
| @@ -1423,10 +1453,10 @@ async function onRegexImportFileChange(file, isScoped) { | ||
| 1423 | 1453 | const regexScripts = JSON.parse(await getFileText(file)); |
| 1424 | 1454 | if (Array.isArray(regexScripts)) { |
| 1425 | 1455 | for (const regexScript of regexScripts) { |
| 1426 | 1456 | await onRegexImportObjectChange(regexScript, isScopedscriptType); |
| 1427 | 1457 | } |
| 1428 | 1458 | } else { |
| 1429 | 1459 | await onRegexImportObjectChange(regexScripts, isScopedscriptType); |
| 1430 | 1460 | } |
| 1431 | 1461 | } catch (error) { |
| 1432 | 1462 | console.log(error); |
| @@ -1435,24 +1465,43 @@ async function onRegexImportFileChange(file, isScoped) { | ||
| 1435 | 1465 | } |
| 1436 | 1466 | } |
| 1437 | 1467 | |
| 1468 | +function getScriptType(script) { | |
| 1469 | + return getScriptsByType(SCRIPT_TYPES.SCOPED).some(s => s.id === script.id) | |
| 1470 | + ? SCRIPT_TYPES.SCOPED | |
| 1471 | + : SCRIPT_TYPES.GLOBAL; | |
| 1472 | +} | |
| 1473 | + | |
| 1474 | +function getSelectedScripts() { | |
| 1475 | + const scripts = getRegexScripts(); | |
| 1476 | + const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | |
| 1477 | + const selectedIds = Array.from(document.querySelectorAll(selector)) | |
| 1478 | + .map(e => e.getAttribute('id')) | |
| 1479 | + .filter(id => id); | |
| 1480 | + return scripts.filter(script => selectedIds.includes(script.id)); | |
| 1481 | +} | |
| 1482 | + | |
| 1438 | 1483 | function purgeEmbeddedRegexScripts({ character }) { |
| 1439 | 1484 | const avatar = character?.avatar; |
| 1440 | - | |
| 1485 | + if (!avatar) { | |
| 1441 | - if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) { | |
| 1486 | + return; | |
| 1487 | + } | |
| 1488 | + const checkKey = `AlertRegex_${characters[this_chid].avatar}`; | |
| 1489 | + if (accountStorage.getItem(checkKey)) { | |
| 1490 | + accountStorage.removeItem(checkKey); | |
| 1491 | + } | |
| 1442 | 1492 | const index = extension_settings.character_allowed_regex.indexOf(avatar); |
| 1443 | 1493 | if (index !== -1) { |
| 1444 | 1494 | extension_settings.character_allowed_regex.splice(index, 1); |
| 1445 | 1495 | saveSettingsDebounced(); |
| 1446 | 1496 | } |
| 1447 | 1497 | } |
| 1448 | -} | |
| 1449 | 1498 | |
| 1450 | 1499 | async function checkEmbeddedRegexScriptscheckCharEmbeddedRegexScripts() { |
| 1451 | 1500 | const chid = this_chid; |
| 1452 | 1501 | |
| 1453 | 1502 | if (chid !== undefined && !selected_group) { |
| 1454 | 1503 | const avatar = characters[chid]?.avatar; |
| 1455 | 1504 | const scripts = characters[chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED); |
| 1456 | 1505 | |
| 1457 | 1506 | if (Array.isArray(scripts) && scripts.length > 0) { |
| 1458 | 1507 | if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) { |
| @@ -1497,7 +1546,7 @@ jQuery(async () => { | ||
| 1497 | 1546 | const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); |
| 1498 | 1547 | $('#regex_container').append(settingsHtml); |
| 1499 | 1548 | $('#open_regex_editor').on('click', function () { |
| 1500 | 1549 | onRegexEditorOpenClick(false, falseSCRIPT_TYPES.GLOBAL); |
| 1501 | 1550 | }); |
| 1502 | 1551 | $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); |
| 1503 | 1552 | $('#open_scoped_editor').on('click', function () { |
| @@ -1511,19 +1560,19 @@ jQuery(async () => { | ||
| 1511 | 1560 | return; |
| 1512 | 1561 | } |
| 1513 | 1562 | |
| 1514 | 1563 | onRegexEditorOpenClick(false, trueSCRIPT_TYPES.SCOPED); |
| 1515 | 1564 | }); |
| 1516 | 1565 | $('#import_regex_file').on('change', async function () { |
| 1517 | 1566 | let target = 'global'SCRIPT_TYPES.GLOBAL; |
| 1518 | 1567 | const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); |
| 1519 | 1568 | template.find('#regex_import_target_global').on('input', () => (target = 'global'SCRIPT_TYPES.GLOBAL)); |
| 1520 | 1569 | template.find('#regex_import_target_scoped').on('input', () => (target = 'scoped'SCRIPT_TYPES.SCOPED)); |
| 1521 | 1570 | |
| 1522 | 1571 | await callGenericPopup(template, POPUP_TYPE.TEXT); |
| 1523 | 1572 | |
| 1524 | 1573 | const inputElement = this instanceof HTMLInputElement && this; |
| 1525 | 1574 | for (const file of inputElement.files) { |
| 1526 | 1575 | await onRegexImportFileChange(file, target === 'scoped'); |
| 1527 | 1576 | } |
| 1528 | 1577 | inputElement.value = ''; |
| 1529 | 1578 | }); |
| @@ -1531,13 +1580,6 @@ jQuery(async () => { | ||
| 1531 | 1580 | $('#import_regex_file').trigger('click'); |
| 1532 | 1581 | }); |
| 1533 | 1582 | |
| 1534 | - function getSelectedScripts() { | |
| 1535 | - const scripts = getRegexScripts(); | |
| 1536 | - const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | |
| 1537 | - const selectedIds = Array.from(document.querySelectorAll(selector)).map(e => e.getAttribute('id')).filter(id => id); | |
| 1538 | - return scripts.filter(script => selectedIds.includes(script.id)); | |
| 1539 | - } | |
| 1540 | - | |
| 1541 | 1583 | $('#bulk_select_all_toggle').on('click', async function () { |
| 1542 | 1584 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 1543 | 1585 | if (checkboxes.length === 0) { |
| @@ -1577,6 +1619,31 @@ jQuery(async () => { | ||
| 1577 | 1619 | await loadRegexScripts(); |
| 1578 | 1620 | }); |
| 1579 | 1621 | |
| 1622 | + /** | |
| 1623 | + * Bulk move regex scripts to the specified type | |
| 1624 | + * @param {SCRIPT_TYPES} toType destination type | |
| 1625 | + */ | |
| 1626 | + async function bulkMoveRegexScript(toType) { | |
| 1627 | + const scripts = getSelectedScripts(); | |
| 1628 | + if (scripts.length === 0) { | |
| 1629 | + toastr.warning(t`No regex scripts selected for moving.`); | |
| 1630 | + return; | |
| 1631 | + } | |
| 1632 | + for (const script of scripts) { | |
| 1633 | + await moveRegexScript(script, toType, getScriptType(script), false); | |
| 1634 | + } | |
| 1635 | + | |
| 1636 | + await loadRegexScripts(); | |
| 1637 | + | |
| 1638 | + // Reload the current chat to undo previous markdown | |
| 1639 | + const currentChatId = getCurrentChatId(); | |
| 1640 | + if (currentChatId !== undefined && currentChatId !== null) { | |
| 1641 | + await reloadCurrentChat(); | |
| 1642 | + } | |
| 1643 | + } | |
| 1644 | + $('#bulk_regex_move_to_global').on('click', () => bulkMoveRegexScript(SCRIPT_TYPES.GLOBAL)); | |
| 1645 | + $('#bulk_regex_move_to_scoped').on('click', () => bulkMoveRegexScript(SCRIPT_TYPES.SCOPED)); | |
| 1646 | + | |
| 1580 | 1647 | $('#bulk_delete_regex').on('click', async function () { |
| 1581 | 1648 | const scripts = getSelectedScripts(); |
| 1582 | 1649 | if (scripts.length === 0) { |
| @@ -1588,11 +1655,11 @@ jQuery(async () => { | ||
| 1588 | 1655 | return; |
| 1589 | 1656 | } |
| 1590 | 1657 | for (const script of scripts) { |
| 1591 | - const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | |
| 1658 | + await deleteRegexScript(script.id, getScriptType(script), false); | |
| 1592 | - await deleteRegexScript({ id: script.id, isScoped: isScoped }); | |
| 1593 | 1659 | } |
| 1594 | - await reloadCurrentChat(); | |
| 1595 | 1660 | saveSettingsDebounced(); |
| 1661 | + await loadRegexScripts(); | |
| 1662 | + await reloadCurrentChat(); | |
| 1596 | 1663 | }); |
| 1597 | 1664 | |
| 1598 | 1665 | $('#bulk_export_regex').on('click', async function () { |
| @@ -1611,12 +1678,12 @@ jQuery(async () => { | ||
| 1611 | 1678 | { |
| 1612 | 1679 | selector: '#saved_regex_scripts', |
| 1613 | 1680 | setter: x => extension_settings.regex = x, |
| 1614 | 1681 | getter: () => extension_settingsgetScriptsByType(SCRIPT_TYPES.regex ?? []GLOBAL), |
| 1615 | 1682 | }, |
| 1616 | 1683 | { |
| 1617 | 1684 | selector: '#saved_scoped_scripts', |
| 1618 | 1685 | setter: x => writeExtensionField(this_chid, 'regex_scripts', x), |
| 1619 | 1686 | getter: () => characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scripts ?? []SCOPED), |
| 1620 | 1687 | }, |
| 1621 | 1688 | ]; |
| 1622 | 1689 | for (const { selector, setter, getter } of sortableDatas) { |
| @@ -1638,6 +1705,7 @@ jQuery(async () => { | ||
| 1638 | 1705 | saveSettingsDebounced(); |
| 1639 | 1706 | |
| 1640 | 1707 | console.debug(`Regex scripts in ${selector} reordered`); |
| 1708 | + await reloadCurrentChat(); | |
| 1641 | 1709 | await loadRegexScripts(); |
| 1642 | 1710 | }, |
| 1643 | 1711 | }); |
| @@ -1676,11 +1744,48 @@ jQuery(async () => { | ||
| 1676 | 1744 | // @ts-ignore |
| 1677 | 1745 | $('#saved_regex_scripts').sortable('enable'); |
| 1678 | 1746 | |
| 1747 | + /** | |
| 1748 | + * @typedef {object} ScriptDecorators | |
| 1749 | + * @property {string} typename | |
| 1750 | + * @property {import('../../slash-commands/SlashCommandEnumValue.js').EnumType} color | |
| 1751 | + * @property {string} icon | |
| 1752 | + */ | |
| 1753 | + /** | |
| 1754 | + * @param {SCRIPT_TYPES} type | |
| 1755 | + * @returns {ScriptDecorators} | |
| 1756 | + */ | |
| 1757 | + function getScriptDecorators(type) { | |
| 1758 | + switch (type) { | |
| 1759 | + case SCRIPT_TYPES.GLOBAL: | |
| 1760 | + return { | |
| 1761 | + typename: 'global', | |
| 1762 | + color: enumTypes.enum, | |
| 1763 | + icon: 'G', | |
| 1764 | + }; | |
| 1765 | + case SCRIPT_TYPES.SCOPED: | |
| 1766 | + return { | |
| 1767 | + typename: 'scoped', | |
| 1768 | + color: enumTypes.name, | |
| 1769 | + icon: 'S', | |
| 1770 | + }; | |
| 1771 | + } | |
| 1772 | + return { | |
| 1773 | + typename: 'Unknown', | |
| 1774 | + color: enumTypes.variable, | |
| 1775 | + icon: 'Unknown', | |
| 1776 | + }; | |
| 1777 | + } | |
| 1679 | 1778 | const localEnumProviders = { |
| 1680 | 1779 | regexScripts: () => getRegexScripts().map(script => { |
| 1681 | - const isGlobal = extension_settings.regex?.some(x => x.scriptName === script.scriptName); | |
| 1780 | + getRegexScripts().map(script => { | |
| 1682 | - return new SlashCommandEnumValue(script.scriptName, `${enumIcons.getStateIcon(!script.disabled)} [${isGlobal ? 'global' : 'scoped'}] ${script.findRegex}`, | |
| 1781 | + const type = getScriptType(script); | |
| 1683 | - isGlobal ? enumTypes.enum : enumTypes.name, isGlobal ? 'G' : 'S'); | |
| 1782 | + const { typename, color, icon } = getScriptDecorators(type); | |
| 1783 | + return new SlashCommandEnumValue( | |
| 1784 | + script.scriptName, | |
| 1785 | + `${enumIcons.getStateIcon(!script.disabled)} [${typename}] ${script.findRegex}`, | |
| 1786 | + color, | |
| 1787 | + icon, | |
| 1788 | + ); | |
| 1684 | 1789 | }), |
| 1685 | 1790 | }; |
| 1686 | 1791 | |
| @@ -1750,7 +1855,7 @@ jQuery(async () => { | ||
| 1750 | 1855 | `, |
| 1751 | 1856 | })); |
| 1752 | 1857 | |
| 1753 | 1858 | eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScriptscheckCharEmbeddedRegexScripts); |
| 1754 | 1859 | eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts); |
| 1755 | 1860 | |
| 1756 | 1861 | presetManager.setupEventListeners(); |
| @@ -12,10 +12,10 @@ | ||
| 12 | 12 | <i class="fa-solid fa-pencil"></i> |
| 13 | 13 | </div> |
| 14 | 14 | <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts"> |
| 15 | 15 | <i class="fa-solid fa-arrow-upglobe"></i> |
| 16 | 16 | </div> |
| 17 | 17 | <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts"> |
| 18 | 18 | <i class="fa-solid fa-arrowaddress-downcard"></i> |
| 19 | 19 | </div> |
| 20 | 20 | <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script"> |
| 21 | 21 | <i class="fa-solid fa-file-export"></i> |
| @@ -139,7 +139,8 @@ input.enable_scoped { | ||
| 139 | 139 | } |
| 140 | 140 | |
| 141 | 141 | .regex_settings .regex_bulk_operations, |
| 142 | 142 | .regex_settings .regex_bulk_checkbox {, |
| 143 | +.regex_settings .regex_bulk_operations_hr { | |
| 143 | 144 | display: none; |
| 144 | 145 | } |
| 145 | 146 | |
| @@ -147,6 +148,10 @@ input.enable_scoped { | ||
| 147 | 148 | display: flex; |
| 148 | 149 | } |
| 149 | 150 | |
| 151 | +.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations_hr { | |
| 152 | + display: block; | |
| 153 | +} | |
| 154 | + | |
| 150 | 155 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { |
| 151 | 156 | display: inline-grid; |
| 152 | 157 | } |