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>

f15c91fa8ed34b30fdf981757d7def2e9c4e28bb

StageDog <aksanajw845@gmail.com>

Signed
6 files changed, +282 -129Ignore whitespace
public/locales/zh-cn.json+3 -3
@@ -1677,9 +1677,9 @@
1677 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",1677 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",
1678 "No scripts found": "没有找到脚本",1678 "No scripts found": "没有找到脚本",
1679 "ext_regex_scoped_scripts": "局部正则脚本",1679 "ext_regex_scoped_scripts": "局部正则脚本",
1680 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
1680 "ext_regex_disallow_scoped": "不允许使用局部正则",1681 "ext_regex_disallow_scoped": "不允许使用局部正则",
1681 "ext_regex_allow_scoped": "允许使用局部正则",1682 "ext_regex_allow_scoped": "允许使用局部正则",
1682 "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
1683 "Regex Editor": "正则表达式编辑器",1683 "Regex Editor": "正则表达式编辑器",
1684 "Test Mode": "测试模式",1684 "Test Mode": "测试模式",
1685 "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。",1685 "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。",
@@ -1728,8 +1728,8 @@
1728 "ext_regex_disable_script": "禁用脚本",1728 "ext_regex_disable_script": "禁用脚本",
1729 "ext_regex_enable_script": "启用脚本",1729 "ext_regex_enable_script": "启用脚本",
1730 "ext_regex_edit_script": "编辑脚本",1730 "ext_regex_edit_script": "编辑脚本",
1731 "ext_regex_move_to_global": "移至全局脚本",1731 "ext_regex_move_to_global": "移至全局",
1732 "ext_regex_move_to_scoped": "移至作用域脚本",1732 "ext_regex_move_to_scoped": "移至局部",
1733 "ext_regex_export_script": "导出脚本",1733 "ext_regex_export_script": "导出脚本",
1734 "ext_regex_delete_script": "删除脚本",1734 "ext_regex_delete_script": "删除脚本",
1735 "Trigger Stable Diffusion": "触发Stable Diffusion",1735 "Trigger Stable Diffusion": "触发Stable Diffusion",
public/scripts/extensions/regex/dropdown.html+10 -1
@@ -31,7 +31,8 @@
31 <small data-i18n="ext_regex_debugger">Debugger</small>31 <small data-i18n="ext_regex_debugger">Debugger</small>
32 </div>32 </div>
33 </div>33 </div>
34 <div class="regex_bulk_operations flex-container justifyCenter">34 <hr class="regex_bulk_operations_hr" />
35 <div class="regex_bulk_operations flex-container">
35 <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All">36 <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All">
36 <i class="fa-solid fa-check-double"></i>37 <i class="fa-solid fa-check-double"></i>
37 </div>38 </div>
@@ -43,6 +44,14 @@
43 <i class="fa-solid fa-toggle-off"></i>44 <i class="fa-solid fa-toggle-off"></i>
44 <small data-i18n="Disable">Disable</small>45 <small data-i18n="Disable">Disable</small>
45 </div>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 <div id="bulk_export_regex" class="menu_button menu_button_icon">55 <div id="bulk_export_regex" class="menu_button menu_button_icon">
47 <i class="fa-solid fa-file-export"></i>56 <i class="fa-solid fa-file-export"></i>
48 <small data-i18n="Export">Export</small>57 <small data-i18n="Export">Export</small>
public/scripts/extensions/regex/engine.js+52 -18
@@ -8,6 +8,56 @@ export {
8};8};
99
10/**10/**
11 * @enum {number} Regex scripts types
12 */
13export 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 */
26const 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 */
34export 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 */
44export 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 * @enum {number} Where the regex script should be applied61 * @enum {number} Where the regex script should be applied
12 */62 */
13const regex_placement = {63const regex_placement = {
@@ -51,22 +101,6 @@ function sanitizeRegexMacro(x) {
51 }) : x;101 }) : x;
52}102}
53103
54function 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 * Parent function to fetch a regexed version of a raw string105 * Parent function to fetch a regexed version of a raw string
72 * @param {string} rawString The raw string to be regexed106 * @param {string} rawString The raw string to be regexed
@@ -87,7 +121,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
87 return finalString;121 return finalString;
88 }122 }
89123
90 const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];124 const allRegex = getRegexScripts({ allowedOnly: true });
91 allRegex.forEach((script) => {125 allRegex.forEach((script) => {
92 if (126 if (
93 // Script applies to Markdown and input is Markdown127 // Script applies to Markdown and input is Markdown
@@ -126,7 +160,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
126160
127/**161/**
128 * Runs the provided regex script on the given string162 * Runs the provided regex script on the given string
129 * @param {import('./index.js').RegexScript} regexScript The regex script to run163 * @param {RegexScript} regexScript The regex script to run
130 * @param {string} rawString The string to run the regex script on164 * @param {string} rawString The string to run the regex script on
131 * @param {RegexScriptParams} params The parameters to use for the regex script165 * @param {RegexScriptParams} params The parameters to use for the regex script
132 * @returns {string} The new string166 * @returns {string} The new string
public/scripts/extensions/regex/index.js+209 -104
@@ -7,8 +7,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
7import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';7import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
8import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';8import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
10import { download, equalsIgnoreCaseAndAccents, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4, escapeHtml } from '../../utils.js';10import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';
11import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';11import { getRegexScripts, getScriptsByType, regex_placement, runRegexScript, SCRIPT_TYPES, substitute_find_regex } from './engine.js';
12import { t } from '../../i18n.js';12import { t } from '../../i18n.js';
13import { accountStorage } from '../../util/AccountStorage.js';13import { accountStorage } from '../../util/AccountStorage.js';
1414
@@ -65,8 +65,8 @@ class RegexPresetManager {
65 * @returns {RegexPresetState} The current state object65 * @returns {RegexPresetState} The current state object
66 */66 */
67 captureCurrentState() {67 captureCurrentState() {
68 const globalScripts = this.regexListToPresetItems(extension_settings.regex) || [];68 const globalScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL));
69 const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts) || [];69 const scopedScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED));
7070
71 return {71 return {
72 global: globalScripts.map(item => item.id).sort(),72 global: globalScripts.map(item => item.id).sort(),
@@ -418,8 +418,8 @@ class RegexPresetManager {
418 id: id,418 id: id,
419 name: name,419 name: name,
420 isSelected: false,420 isSelected: false,
421 global: this.regexListToPresetItems(extension_settings.regex),421 global: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)),
422 scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts),422 scoped: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)),
423 };423 };
424424
425 if (isUpdate) {425 if (isUpdate) {
@@ -465,15 +465,6 @@ class RegexPresetManager {
465const presetManager = new RegexPresetManager();465const presetManager = new RegexPresetManager();
466466
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 */
472export function getRegexScripts() {
473 return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
474}
475
476/**
477 * Toggle the icon for the "select all" checkbox in the regex settings.468 * Toggle the icon for the "select all" checkbox in the regex settings.
478 * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected).469 * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected).
479 * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected).470 * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected).
@@ -485,16 +476,25 @@ function setToggleAllIcon(allAreChecked) {
485 selectAllIcon.toggleClass('fa-minus', allAreChecked);476 selectAllIcon.toggleClass('fa-minus', allAreChecked);
486}477}
487478
479function 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 * Saves a regex script to the extension settings or character data.488 * Saves a regex script to the extension settings or character data.
490 * @param {import('../../char-data.js').RegexScriptData} regexScript489 * @param {import('../../char-data.js').RegexScriptData} regexScript
491 * @param {number} existingScriptIndex Index of the existing script490 * @param {number} existingScriptIndex Index of the existing script
492 * @param {boolean} isScoped Is the script scoped to a character?491 * @param {SCRIPT_TYPES} scriptType global? scoped?
492 * @param {boolean} [saveSettings=true] Whether to save the settings immediately
493 * @returns {Promise<void>}493 * @returns {Promise<void>}
494 */494 */
495async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {495async function saveRegexScript(regexScript, existingScriptIndex, scriptType, saveSettings = true) {
496 // If not editing496 // If not editing
497 const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];497 const array = getScriptsByType(scriptType);
498498
499 // Assign a UUID if it doesn't exist499 // Assign a UUID if it doesn't exist
500 if (!regexScript.id) {500 if (!regexScript.id) {
@@ -523,7 +523,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
523 array.push(regexScript);523 array.push(regexScript);
524 }524 }
525525
526 if (isScoped) {526 if (scriptType === SCRIPT_TYPES.SCOPED) {
527 await writeExtensionField(this_chid, 'regex_scripts', array);527 await writeExtensionField(this_chid, 'regex_scripts', array);
528528
529 // Add the character to the allowed list529 // Add the character to the allowed list
@@ -532,13 +532,15 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
532 }532 }
533 }533 }
534534
535 saveSettingsDebounced();535 if (saveSettings) {
536 await loadRegexScripts();536 saveSettingsDebounced();
537 await loadRegexScripts();
537538
538 // Reload the current chat to undo previous markdown539 // Reload the current chat to undo previous markdown
539 const currentChatId = getCurrentChatId();540 const currentChatId = getCurrentChatId();
540 if (currentChatId !== undefined && currentChatId !== null) {541 if (currentChatId !== undefined && currentChatId !== null) {
541 await reloadCurrentChat();542 await reloadCurrentChat();
543 }
542 }544 }
543545
544 const debuggerPopup = $('#regex_debugger_popup');546 const debuggerPopup = $('#regex_debugger_popup');
@@ -547,20 +549,47 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
547 }549 }
548}550}
549551
550async 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 */
559async function deleteRegexScript(id, scriptType, saveSettings = true) {
560 const array = getScriptsByType(scriptType);
552561
553 const existingScriptIndex = array.findIndex((script) => script.id === id);562 const existingScriptIndex = array.findIndex(script => script.id === id);
554 if (existingScriptIndex !== -1) {563 if (existingScriptIndex !== -1) {
555 array.splice(existingScriptIndex, 1);564 array.splice(existingScriptIndex, 1);
556565
557 if (isScoped) {566 if (scriptType === SCRIPT_TYPES.SCOPED) {
558 await writeExtensionField(this_chid, 'regex_scripts', array);567 await writeExtensionField(this_chid, 'regex_scripts', array);
559 }568 }
569 if (saveSettings) {
570 saveSettingsDebounced();
571 await loadRegexScripts();
572 }
573 }
574}
560575
561 saveSettingsDebounced();576/**
562 await loadRegexScripts();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 */
584async function moveRegexScript(script, toType, fromType = null, saveSettings = true) {
585 if (!fromType) {
586 fromType = getScriptType(script);
563 }587 }
588 if (fromType === toType || fromType === -1) {
589 return;
590 }
591 await deleteRegexScript(script.id, fromType, false);
592 await saveRegexScript(script, -1, toType, saveSettings);
564}593}
565594
566async function loadRegexScripts() {595async function loadRegexScripts() {
@@ -574,13 +603,13 @@ async function loadRegexScripts() {
574 * Renders a script to the UI.603 * Renders a script to the UI.
575 * @param {string} container Container to render the script to604 * @param {string} container Container to render the script to
576 * @param {import('../../char-data.js').RegexScriptData} script Script data605 * @param {import('../../char-data.js').RegexScriptData} script Script data
577 * @param {boolean} isScoped Script is scoped to a character606 * @param {SCRIPT_TYPES} scriptType global? scoped?
578 * @param {number} index Index of the script in the array607 * @param {number} index Index of the script in the array
579 */608 */
580 function renderScript(container, script, isScoped, index) {609 function renderScript(container, script, scriptType, index) {
581 // Have to clone here610 // Have to clone here
582 const scriptHtml = scriptTemplate.clone();611 const scriptHtml = scriptTemplate.clone();
583 const save = () => saveRegexScript(script, index, isScoped);612 const save = () => saveRegexScript(script, index, scriptType);
584613
585 if (!script.id) {614 if (!script.id) {
586 script.id = uuidv4();615 script.id = uuidv4();
@@ -600,7 +629,7 @@ async function loadRegexScripts() {
600 scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');629 scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
601 });630 });
602 scriptHtml.find('.edit_existing_regex').on('click', async function () {631 scriptHtml.find('.edit_existing_regex').on('click', async function () {
603 await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped);632 await onRegexEditorOpenClick(scriptHtml.attr('id'), scriptType);
604 });633 });
605 scriptHtml.find('.move_to_global').on('click', async function () {634 scriptHtml.find('.move_to_global').on('click', async function () {
606 const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM);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 if (!confirm) {637 if (!confirm) {
609 return;638 return;
610 }639 }
611640 await moveRegexScript(script, SCRIPT_TYPES.GLOBAL, scriptType);
612 await deleteRegexScript({ id: script.id, isScoped: true });
613 await saveRegexScript(script, -1, false);
614 });641 });
615 scriptHtml.find('.move_to_scoped').on('click', async function () {642 scriptHtml.find('.move_to_scoped').on('click', async function () {
616 if (this_chid === undefined) {643 if (this_chid === undefined) {
617 toastr.error(t`No character selected.`);644 toastr.error(t`No character selected.`);
618 return;645 return;
619 }646 }
620
621 if (selected_group) {647 if (selected_group) {
622 toastr.error(t`Cannot edit scoped scripts in group chats.`);648 toastr.error(t`Cannot edit scoped scripts in group chats.`);
623 return;649 return;
624 }650 }
625
626 const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM);651 const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM);
627
628 if (!confirm) {652 if (!confirm) {
629 return;653 return;
630 }654 }
631655 await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType);
632 await deleteRegexScript({ id: script.id, isScoped: false });
633 await saveRegexScript(script, -1, true);
634 });656 });
635 scriptHtml.find('.export_regex').on('click', async function () {657 scriptHtml.find('.export_regex').on('click', async function () {
636 const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`;658 const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`;
@@ -639,15 +661,14 @@ async function loadRegexScripts() {
639 });661 });
640 scriptHtml.find('.delete_regex').on('click', async function () {662 scriptHtml.find('.delete_regex').on('click', async function () {
641 const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM);663 const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM);
642
643 if (!confirm) {664 if (!confirm) {
644 return;665 return;
645 }666 }
646667 await deleteRegexScript(script.id, scriptType);
647 await deleteRegexScript({ id: script.id, isScoped });
648 await reloadCurrentChat();668 await reloadCurrentChat();
649 });669 });
650 scriptHtml.find('.regex_bulk_checkbox').on('change', function () {670 scriptHtml.find('.regex_bulk_checkbox').on('change', function () {
671 setMoveButtonsVisibility();
651 const checkboxes = $('#regex_container .regex_bulk_checkbox');672 const checkboxes = $('#regex_container .regex_bulk_checkbox');
652 const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length;673 const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length;
653 setToggleAllIcon(allAreChecked);674 setToggleAllIcon(allAreChecked);
@@ -656,22 +677,24 @@ async function loadRegexScripts() {
656 $(container).append(scriptHtml);677 $(container).append(scriptHtml);
657 }678 }
658679
659 extension_settings?.regex?.forEach((script, index) => renderScript('#saved_regex_scripts', script, false, index));680 getScriptsByType(SCRIPT_TYPES.GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, SCRIPT_TYPES.GLOBAL, index));
660 characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index) => renderScript('#saved_scoped_scripts', script, true, index));681 getScriptsByType(SCRIPT_TYPES.SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, SCRIPT_TYPES.SCOPED, index));
682
683 const isScopedAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
684 $('#regex_scoped_toggle').prop('checked', isScopedAllowed);
661685
662 const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);686 setMoveButtonsVisibility();
663 $('#regex_scoped_toggle').prop('checked', isAllowed);
664}687}
665688
666/**689/**
667 * Opens the regex editor.690 * Opens the regex editor.
668 * @param {string|boolean} existingId Existing ID691 * @param {string|boolean} existingId Existing ID
669 * @param {boolean} isScoped Is the script scoped to a character?692 * @param {SCRIPT_TYPES} scriptType global? scoped?
670 * @returns {Promise<void>}693 * @returns {Promise<void>}
671 */694 */
672async function onRegexEditorOpenClick(existingId, isScoped) {695async function onRegexEditorOpenClick(existingId, scriptType) {
673 const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));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);
675698
676 // If an ID exists, fill in all the values699 // If an ID exists, fill in all the values
677 let existingScriptIndex = -1;700 let existingScriptIndex = -1;
@@ -776,7 +799,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) {
776 maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),799 maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
777 };800 };
778801
779 saveRegexScript(newRegexScript, existingScriptIndex, isScoped);802 saveRegexScript(newRegexScript, existingScriptIndex, scriptType);
780 }803 }
781}804}
782805
@@ -973,7 +996,7 @@ function populateDebuggerRuleList(container) {
973 return;996 return;
974 }997 }
975998
976 const globalScriptIds = new Set((extension_settings.regex ?? []).map(s => s.id));999 const globalScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.GLOBAL).map(s => s.id));
977 const globalScripts = [];1000 const globalScripts = [];
978 const scopedScripts = [];1001 const scopedScripts = [];
9791002
@@ -981,11 +1004,11 @@ function populateDebuggerRuleList(container) {
981 const scriptCopy = structuredClone(script); // Use structuredClone for deep copy1004 const scriptCopy = structuredClone(script); // Use structuredClone for deep copy
982 if (globalScriptIds.has(script.id)) {1005 if (globalScriptIds.has(script.id)) {
983 // @ts-ignore1006 // @ts-ignore
984 scriptCopy.isScoped = false;1007 scriptCopy.type = SCRIPT_TYPES.SCOPED;
985 globalScripts.push(scriptCopy);1008 globalScripts.push(scriptCopy);
986 } else {1009 } else {
987 // @ts-ignore1010 // @ts-ignore
988 scriptCopy.isScoped = true;1011 scriptCopy.type = SCRIPT_TYPES.SCOPED;
989 scopedScripts.push(scriptCopy);1012 scopedScripts.push(scriptCopy);
990 }1013 }
991 });1014 });
@@ -1002,10 +1025,17 @@ function populateDebuggerRuleList(container) {
1002 ruleElement.find('.rule-name').text(script.scriptName);1025 ruleElement.find('.rule-name').text(script.scriptName);
1003 ruleElement.find('.rule-regex').text(script.findRegex);1026 ruleElement.find('.rule-regex').text(script.findRegex);
1004 // @ts-ignore1027 // @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 ruleElement.find('.rule-enabled').prop('checked', !script.disabled);1036 ruleElement.find('.rule-enabled').prop('checked', !script.disabled);
1007 // @ts-ignore1037 // @ts-ignore
1008 ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScoped));1038 ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.type));
10091039
1010 ruleElement.on('click', function (event) {1040 ruleElement.on('click', function (event) {
1011 if ($(event.target).is('input, .menu_button, .menu_button i')) {1041 if ($(event.target).is('input, .menu_button, .menu_button i')) {
@@ -1364,10 +1394,10 @@ async function toggleRegexCallback(args, scriptName) {
1364 break;1394 break;
1365 }1395 }
13661396
1367 const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id);1397 const scriptType = getScriptType(script);
1368 const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOf(script) : scripts.indexOf(script);1398 const index = getScriptsByType(scriptType).indexOf(script);
13691399
1370 await saveRegexScript(script, index, isScoped);1400 await saveRegexScript(script, index, scriptType);
1371 if (script.disabled) {1401 if (script.disabled) {
1372 !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`);1402 !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`);
1373 } else {1403 } else {
@@ -1380,9 +1410,9 @@ async function toggleRegexCallback(args, scriptName) {
1380/**1410/**
1381 * Performs the import of the regex object.1411 * Performs the import of the regex object.
1382 * @param {Object} regexScript Input object1412 * @param {Object} regexScript Input object
1383 * @param {boolean} isScoped Is the script scoped to a character?1413 * @param {SCRIPT_TYPES} scriptType global? scoped?
1384 */1414 */
1385async function onRegexImportObjectChange(regexScript, isScoped) {1415async function onRegexImportObjectChange(regexScript, scriptType) {
1386 try {1416 try {
1387 if (!regexScript.scriptName) {1417 if (!regexScript.scriptName) {
1388 throw new Error('No script name provided.');1418 throw new Error('No script name provided.');
@@ -1391,10 +1421,10 @@ async function onRegexImportObjectChange(regexScript, isScoped) {
1391 // Assign a new UUID1421 // Assign a new UUID
1392 regexScript.id = uuidv4();1422 regexScript.id = uuidv4();
13931423
1394 const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];1424 const array = getScriptsByType(scriptType);
1395 array.push(regexScript);1425 array.push(regexScript);
13961426
1397 if (isScoped) {1427 if (scriptType === SCRIPT_TYPES.SCOPED) {
1398 await writeExtensionField(this_chid, 'regex_scripts', array);1428 await writeExtensionField(this_chid, 'regex_scripts', array);
1399 }1429 }
14001430
@@ -1411,9 +1441,9 @@ async function onRegexImportObjectChange(regexScript, isScoped) {
1411/**1441/**
1412 * Performs the import of the regex file.1442 * Performs the import of the regex file.
1413 * @param {File} file Input file1443 * @param {File} file Input file
1414 * @param {boolean} isScoped Is the script scoped to a character?1444 * @param {SCRIPT_TYPES} scriptType global? scoped?
1415 */1445 */
1416async function onRegexImportFileChange(file, isScoped) {1446async function onRegexImportFileChange(file, scriptType) {
1417 if (!file) {1447 if (!file) {
1418 toastr.error('No file provided.');1448 toastr.error('No file provided.');
1419 return;1449 return;
@@ -1423,10 +1453,10 @@ async function onRegexImportFileChange(file, isScoped) {
1423 const regexScripts = JSON.parse(await getFileText(file));1453 const regexScripts = JSON.parse(await getFileText(file));
1424 if (Array.isArray(regexScripts)) {1454 if (Array.isArray(regexScripts)) {
1425 for (const regexScript of regexScripts) {1455 for (const regexScript of regexScripts) {
1426 await onRegexImportObjectChange(regexScript, isScoped);1456 await onRegexImportObjectChange(regexScript, scriptType);
1427 }1457 }
1428 } else {1458 } else {
1429 await onRegexImportObjectChange(regexScripts, isScoped);1459 await onRegexImportObjectChange(regexScripts, scriptType);
1430 }1460 }
1431 } catch (error) {1461 } catch (error) {
1432 console.log(error);1462 console.log(error);
@@ -1435,24 +1465,43 @@ async function onRegexImportFileChange(file, isScoped) {
1435 }1465 }
1436}1466}
14371467
1468function getScriptType(script) {
1469 return getScriptsByType(SCRIPT_TYPES.SCOPED).some(s => s.id === script.id)
1470 ? SCRIPT_TYPES.SCOPED
1471 : SCRIPT_TYPES.GLOBAL;
1472}
1473
1474function 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
1438function purgeEmbeddedRegexScripts({ character }) {1483function purgeEmbeddedRegexScripts({ character }) {
1439 const avatar = character?.avatar;1484 const avatar = character?.avatar;
14401485 if (!avatar) {
1441 if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) {1486 return;
1442 const index = extension_settings.character_allowed_regex.indexOf(avatar);1487 }
1443 if (index !== -1) {1488 const checkKey = `AlertRegex_${characters[this_chid].avatar}`;
1444 extension_settings.character_allowed_regex.splice(index, 1);1489 if (accountStorage.getItem(checkKey)) {
1445 saveSettingsDebounced();1490 accountStorage.removeItem(checkKey);
1446 }1491 }
1492 const index = extension_settings.character_allowed_regex.indexOf(avatar);
1493 if (index !== -1) {
1494 extension_settings.character_allowed_regex.splice(index, 1);
1495 saveSettingsDebounced();
1447 }1496 }
1448}1497}
14491498
1450async function checkEmbeddedRegexScripts() {1499async function checkCharEmbeddedRegexScripts() {
1451 const chid = this_chid;1500 const chid = this_chid;
14521501
1453 if (chid !== undefined && !selected_group) {1502 if (chid !== undefined && !selected_group) {
1454 const avatar = characters[chid]?.avatar;1503 const avatar = characters[chid]?.avatar;
1455 const scripts = characters[chid]?.data?.extensions?.regex_scripts;1504 const scripts = getScriptsByType(SCRIPT_TYPES.SCOPED);
14561505
1457 if (Array.isArray(scripts) && scripts.length > 0) {1506 if (Array.isArray(scripts) && scripts.length > 0) {
1458 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {1507 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
@@ -1497,7 +1546,7 @@ jQuery(async () => {
1497 const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));1546 const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
1498 $('#regex_container').append(settingsHtml);1547 $('#regex_container').append(settingsHtml);
1499 $('#open_regex_editor').on('click', function () {1548 $('#open_regex_editor').on('click', function () {
1500 onRegexEditorOpenClick(false, false);1549 onRegexEditorOpenClick(false, SCRIPT_TYPES.GLOBAL);
1501 });1550 });
1502 $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick);1551 $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick);
1503 $('#open_scoped_editor').on('click', function () {1552 $('#open_scoped_editor').on('click', function () {
@@ -1511,19 +1560,19 @@ jQuery(async () => {
1511 return;1560 return;
1512 }1561 }
15131562
1514 onRegexEditorOpenClick(false, true);1563 onRegexEditorOpenClick(false, SCRIPT_TYPES.SCOPED);
1515 });1564 });
1516 $('#import_regex_file').on('change', async function () {1565 $('#import_regex_file').on('change', async function () {
1517 let target = 'global';1566 let target = SCRIPT_TYPES.GLOBAL;
1518 const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));1567 const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
1519 template.find('#regex_import_target_global').on('input', () => target = 'global');1568 template.find('#regex_import_target_global').on('input', () => (target = SCRIPT_TYPES.GLOBAL));
1520 template.find('#regex_import_target_scoped').on('input', () => target = 'scoped');1569 template.find('#regex_import_target_scoped').on('input', () => (target = SCRIPT_TYPES.SCOPED));
15211570
1522 await callGenericPopup(template, POPUP_TYPE.TEXT);1571 await callGenericPopup(template, POPUP_TYPE.TEXT);
15231572
1524 const inputElement = this instanceof HTMLInputElement && this;1573 const inputElement = this instanceof HTMLInputElement && this;
1525 for (const file of inputElement.files) {1574 for (const file of inputElement.files) {
1526 await onRegexImportFileChange(file, target === 'scoped');1575 await onRegexImportFileChange(file, target);
1527 }1576 }
1528 inputElement.value = '';1577 inputElement.value = '';
1529 });1578 });
@@ -1531,13 +1580,6 @@ jQuery(async () => {
1531 $('#import_regex_file').trigger('click');1580 $('#import_regex_file').trigger('click');
1532 });1581 });
15331582
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 $('#bulk_select_all_toggle').on('click', async function () {1583 $('#bulk_select_all_toggle').on('click', async function () {
1542 const checkboxes = $('#regex_container .regex_bulk_checkbox');1584 const checkboxes = $('#regex_container .regex_bulk_checkbox');
1543 if (checkboxes.length === 0) {1585 if (checkboxes.length === 0) {
@@ -1577,6 +1619,31 @@ jQuery(async () => {
1577 await loadRegexScripts();1619 await loadRegexScripts();
1578 });1620 });
15791621
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 $('#bulk_delete_regex').on('click', async function () {1647 $('#bulk_delete_regex').on('click', async function () {
1581 const scripts = getSelectedScripts();1648 const scripts = getSelectedScripts();
1582 if (scripts.length === 0) {1649 if (scripts.length === 0) {
@@ -1588,11 +1655,11 @@ jQuery(async () => {
1588 return;1655 return;
1589 }1656 }
1590 for (const script of scripts) {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 saveSettingsDebounced();1660 saveSettingsDebounced();
1661 await loadRegexScripts();
1662 await reloadCurrentChat();
1596 });1663 });
15971664
1598 $('#bulk_export_regex').on('click', async function () {1665 $('#bulk_export_regex').on('click', async function () {
@@ -1611,12 +1678,12 @@ jQuery(async () => {
1611 {1678 {
1612 selector: '#saved_regex_scripts',1679 selector: '#saved_regex_scripts',
1613 setter: x => extension_settings.regex = x,1680 setter: x => extension_settings.regex = x,
1614 getter: () => extension_settings.regex ?? [],1681 getter: () => getScriptsByType(SCRIPT_TYPES.GLOBAL),
1615 },1682 },
1616 {1683 {
1617 selector: '#saved_scoped_scripts',1684 selector: '#saved_scoped_scripts',
1618 setter: x => writeExtensionField(this_chid, 'regex_scripts', x),1685 setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
1619 getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [],1686 getter: () => getScriptsByType(SCRIPT_TYPES.SCOPED),
1620 },1687 },
1621 ];1688 ];
1622 for (const { selector, setter, getter } of sortableDatas) {1689 for (const { selector, setter, getter } of sortableDatas) {
@@ -1638,6 +1705,7 @@ jQuery(async () => {
1638 saveSettingsDebounced();1705 saveSettingsDebounced();
16391706
1640 console.debug(`Regex scripts in ${selector} reordered`);1707 console.debug(`Regex scripts in ${selector} reordered`);
1708 await reloadCurrentChat();
1641 await loadRegexScripts();1709 await loadRegexScripts();
1642 },1710 },
1643 });1711 });
@@ -1676,12 +1744,49 @@ jQuery(async () => {
1676 // @ts-ignore1744 // @ts-ignore
1677 $('#saved_regex_scripts').sortable('enable');1745 $('#saved_regex_scripts').sortable('enable');
16781746
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 const localEnumProviders = {1778 const localEnumProviders = {
1680 regexScripts: () => getRegexScripts().map(script => {1779 regexScripts: () =>
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);
1684 }),1783 return new SlashCommandEnumValue(
1784 script.scriptName,
1785 `${enumIcons.getStateIcon(!script.disabled)} [${typename}] ${script.findRegex}`,
1786 color,
1787 icon,
1788 );
1789 }),
1685 };1790 };
16861791
1687 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1792 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
@@ -1750,7 +1855,7 @@ jQuery(async () => {
1750 `,1855 `,
1751 }));1856 }));
17521857
1753 eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);1858 eventSource.on(event_types.CHAT_CHANGED, checkCharEmbeddedRegexScripts);
1754 eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);1859 eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
17551860
1756 presetManager.setupEventListeners();1861 presetManager.setupEventListeners();
public/scripts/extensions/regex/scriptTemplate.html+2 -2
@@ -12,10 +12,10 @@
12 <i class="fa-solid fa-pencil"></i>12 <i class="fa-solid fa-pencil"></i>
13 </div>13 </div>
14 <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">14 <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
15 <i class="fa-solid fa-arrow-up"></i>15 <i class="fa-solid fa-globe"></i>
16 </div>16 </div>
17 <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">17 <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
18 <i class="fa-solid fa-arrow-down"></i>18 <i class="fa-solid fa-address-card"></i>
19 </div>19 </div>
20 <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">20 <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
21 <i class="fa-solid fa-file-export"></i>21 <i class="fa-solid fa-file-export"></i>
public/scripts/extensions/regex/style.css+6 -1
@@ -139,7 +139,8 @@ input.enable_scoped {
139}139}
140140
141.regex_settings .regex_bulk_operations,141.regex_settings .regex_bulk_operations,
142.regex_settings .regex_bulk_checkbox {142.regex_settings .regex_bulk_checkbox,
143.regex_settings .regex_bulk_operations_hr {
143 display: none;144 display: none;
144}145}
145146
@@ -147,6 +148,10 @@ input.enable_scoped {
147 display: flex;148 display: flex;
148}149}
149150
151.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations_hr {
152 display: block;
153}
154
150.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox {155.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox {
151 display: inline-grid;156 display: inline-grid;
152}157}