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, +270 -117Showing whitespace changes
public/locales/zh-cn.json+3 -3
@@ -1677,9 +1677,9 @@
16771677 "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中",
16781678 "No scripts found": "没有找到脚本",
16791679 "ext_regex_scoped_scripts": "局部正则脚本",
1680+ "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
16801681 "ext_regex_disallow_scoped": "不允许使用局部正则",
16811682 "ext_regex_allow_scoped": "允许使用局部正则",
1682- "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中",
16831683 "Regex Editor": "正则表达式编辑器",
16841684 "Test Mode": "测试模式",
16851685 "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。",
@@ -1728,8 +1728,8 @@
17281728 "ext_regex_disable_script": "禁用脚本",
17291729 "ext_regex_enable_script": "启用脚本",
17301730 "ext_regex_edit_script": "编辑脚本",
17311731 "ext_regex_move_to_global": "移至全局脚本移至全局",
17321732 "ext_regex_move_to_scoped": "移至作用域脚本移至局部",
17331733 "ext_regex_export_script": "导出脚本",
17341734 "ext_regex_delete_script": "删除脚本",
17351735 "Trigger Stable Diffusion": "触发Stable Diffusion",
public/scripts/extensions/regex/dropdown.html+10 -1
@@ -31,7 +31,8 @@
3131 <small data-i18n="ext_regex_debugger">Debugger</small>
3232 </div>
3333 </div>
3434 <divhr class="regex_bulk_operations flex-container justifyCenterregex_bulk_operations_hr" />
35+ <div class="regex_bulk_operations flex-container">
3536 <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All">
3637 <i class="fa-solid fa-check-double"></i>
3738 </div>
@@ -43,6 +44,14 @@
4344 <i class="fa-solid fa-toggle-off"></i>
4445 <small data-i18n="Disable">Disable</small>
4546 </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>
4655 <div id="bulk_export_regex" class="menu_button menu_button_icon">
4756 <i class="fa-solid fa-file-export"></i>
4857 <small data-i18n="Export">Export</small>
public/scripts/extensions/regex/engine.js+52 -18
@@ -8,6 +8,56 @@ export {
88};
99
1010/**
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+/**
1161 * @enum {number} Where the regex script should be applied
1262 */
1363const regex_placement = {
@@ -51,22 +101,6 @@ function sanitizeRegexMacro(x) {
51101 }) : x;
52102}
53103
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-
70104/**
71105 * Parent function to fetch a regexed version of a raw string
72106 * @param {string} rawString The raw string to be regexed
@@ -87,7 +121,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
87121 return finalString;
88122 }
89123
90- const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];
124+ const allRegex = getRegexScripts({ allowedOnly: true });
91125 allRegex.forEach((script) => {
92126 if (
93127 // Script applies to Markdown and input is Markdown
@@ -126,7 +160,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
126160
127161/**
128162 * Runs the provided regex script on the given string
129163 * @param {import('./index.js').RegexScript} regexScript The regex script to run
130164 * @param {string} rawString The string to run the regex script on
131165 * @param {RegexScriptParams} params The parameters to use for the regex script
132166 * @returns {string} The new string
public/scripts/extensions/regex/index.js+197 -92
@@ -7,8 +7,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
77import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
88import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
99import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4, escapeHtml } from '../../utils.js';
1111import { getRegexScripts, getScriptsByType, regex_placement, runRegexScript, SCRIPT_TYPES, substitute_find_regex } from './engine.js';
1212import { t } from '../../i18n.js';
1313import { accountStorage } from '../../util/AccountStorage.js';
1414
@@ -65,8 +65,8 @@ class RegexPresetManager {
6565 * @returns {RegexPresetState} The current state object
6666 */
6767 captureCurrentState() {
6868 const globalScripts = this.regexListToPresetItems(extension_settingsgetScriptsByType(SCRIPT_TYPES.regexGLOBAL) || []);
6969 const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED) || []);
7070
7171 return {
7272 global: globalScripts.map(item => item.id).sort(),
@@ -418,8 +418,8 @@ class RegexPresetManager {
418418 id: id,
419419 name: name,
420420 isSelected: false,
421421 global: this.regexListToPresetItems(extension_settingsgetScriptsByType(SCRIPT_TYPES.regexGLOBAL)),
422422 scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED)),
423423 };
424424
425425 if (isUpdate) {
@@ -465,15 +465,6 @@ class RegexPresetManager {
465465const presetManager = new RegexPresetManager();
466466
467467/**
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-/**
477468 * Toggle the icon for the "select all" checkbox in the regex settings.
478469 * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected).
479470 * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected).
@@ -485,16 +476,25 @@ function setToggleAllIcon(allAreChecked) {
485476 selectAllIcon.toggleClass('fa-minus', allAreChecked);
486477}
487478
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+
488487/**
489488 * Saves a regex script to the extension settings or character data.
490489 * @param {import('../../char-data.js').RegexScriptData} regexScript
491490 * @param {number} existingScriptIndex Index of the existing script
492491 * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character?
492+ * @param {boolean} [saveSettings=true] Whether to save the settings immediately
493493 * @returns {Promise<void>}
494494 */
495495async function saveRegexScript(regexScript, existingScriptIndex, isScopedscriptType, saveSettings = true) {
496496 // If not editing
497- const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
497+ const array = getScriptsByType(scriptType);
498498
499499 // Assign a UUID if it doesn't exist
500500 if (!regexScript.id) {
@@ -523,7 +523,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
523523 array.push(regexScript);
524524 }
525525
526526 if (isScopedscriptType === SCRIPT_TYPES.SCOPED) {
527527 await writeExtensionField(this_chid, 'regex_scripts', array);
528528
529529 // Add the character to the allowed list
@@ -532,6 +532,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
532532 }
533533 }
534534
535+ if (saveSettings) {
535536 saveSettingsDebounced();
536537 await loadRegexScripts();
537538
@@ -540,6 +541,7 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
540541 if (currentChatId !== undefined && currentChatId !== null) {
541542 await reloadCurrentChat();
542543 }
544+ }
543545
544546 const debuggerPopup = $('#regex_debugger_popup');
545547 if (debuggerPopup.length) {
@@ -547,21 +549,48 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
547549 }
548550}
549551
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);
552561
553562 const existingScriptIndex = array.findIndex((script) => script.id === id);
554563 if (existingScriptIndex !== -1) {
555564 array.splice(existingScriptIndex, 1);
556565
557566 if (isScopedscriptType === SCRIPT_TYPES.SCOPED) {
558567 await writeExtensionField(this_chid, 'regex_scripts', array);
559568 }
560-
569+ if (saveSettings) {
561570 saveSettingsDebounced();
562571 await loadRegexScripts();
563572 }
564573 }
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+}
565594
566595async function loadRegexScripts() {
567596 $('#saved_regex_scripts').empty();
@@ -574,13 +603,13 @@ async function loadRegexScripts() {
574603 * Renders a script to the UI.
575604 * @param {string} container Container to render the script to
576605 * @param {import('../../char-data.js').RegexScriptData} script Script data
577606 * @param {booleanSCRIPT_TYPES} isScoped ScriptscriptType isglobal? scoped to a character?
578607 * @param {number} index Index of the script in the array
579608 */
580609 function renderScript(container, script, isScopedscriptType, index) {
581610 // Have to clone here
582611 const scriptHtml = scriptTemplate.clone();
583612 const save = () => saveRegexScript(script, index, isScopedscriptType);
584613
585614 if (!script.id) {
586615 script.id = uuidv4();
@@ -600,7 +629,7 @@ async function loadRegexScripts() {
600629 scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
601630 });
602631 scriptHtml.find('.edit_existing_regex').on('click', async function () {
603632 await onRegexEditorOpenClick(scriptHtml.attr('id'), isScopedscriptType);
604633 });
605634 scriptHtml.find('.move_to_global').on('click', async function () {
606635 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() {
608637 if (!confirm) {
609638 return;
610639 }
611-
640+ await moveRegexScript(script, SCRIPT_TYPES.GLOBAL, scriptType);
612- await deleteRegexScript({ id: script.id, isScoped: true });
613- await saveRegexScript(script, -1, false);
614641 });
615642 scriptHtml.find('.move_to_scoped').on('click', async function () {
616643 if (this_chid === undefined) {
617644 toastr.error(t`No character selected.`);
618645 return;
619646 }
620-
621647 if (selected_group) {
622648 toastr.error(t`Cannot edit scoped scripts in group chats.`);
623649 return;
624650 }
625-
626651 const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM);
627-
628652 if (!confirm) {
629653 return;
630654 }
631-
655+ await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType);
632- await deleteRegexScript({ id: script.id, isScoped: false });
633- await saveRegexScript(script, -1, true);
634656 });
635657 scriptHtml.find('.export_regex').on('click', async function () {
636658 const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`;
@@ -639,15 +661,14 @@ async function loadRegexScripts() {
639661 });
640662 scriptHtml.find('.delete_regex').on('click', async function () {
641663 const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM);
642-
643664 if (!confirm) {
644665 return;
645666 }
646-
667+ await deleteRegexScript(script.id, scriptType);
647- await deleteRegexScript({ id: script.id, isScoped });
648668 await reloadCurrentChat();
649669 });
650670 scriptHtml.find('.regex_bulk_checkbox').on('change', function () {
671+ setMoveButtonsVisibility();
651672 const checkboxes = $('#regex_container .regex_bulk_checkbox');
652673 const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length;
653674 setToggleAllIcon(allAreChecked);
@@ -656,22 +677,24 @@ async function loadRegexScripts() {
656677 $(container).append(scriptHtml);
657678 }
658679
659680 extension_settings?getScriptsByType(SCRIPT_TYPES.regex?GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, falseSCRIPT_TYPES.GLOBAL, index));
660681 characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scripts?SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, trueSCRIPT_TYPES.SCOPED, index));
661682
662683 const isAllowedisScopedAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
663684 $('#regex_scoped_toggle').prop('checked', isAllowedisScopedAllowed);
685+
686+ setMoveButtonsVisibility();
664687}
665688
666689/**
667690 * Opens the regex editor.
668691 * @param {string|boolean} existingId Existing ID
669692 * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character?
670693 * @returns {Promise<void>}
671694 */
672695async function onRegexEditorOpenClick(existingId, isScopedscriptType) {
673696 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
676699 // If an ID exists, fill in all the values
677700 let existingScriptIndex = -1;
@@ -776,7 +799,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) {
776799 maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
777800 };
778801
779802 saveRegexScript(newRegexScript, existingScriptIndex, isScopedscriptType);
780803 }
781804}
782805
@@ -973,7 +996,7 @@ function populateDebuggerRuleList(container) {
973996 return;
974997 }
975998
976999 const globalScriptIds = new Set(getScriptsByType(extension_settingsSCRIPT_TYPES.regex ?? []GLOBAL).map(s => s.id));
9771000 const globalScripts = [];
9781001 const scopedScripts = [];
9791002
@@ -981,11 +1004,11 @@ function populateDebuggerRuleList(container) {
9811004 const scriptCopy = structuredClone(script); // Use structuredClone for deep copy
9821005 if (globalScriptIds.has(script.id)) {
9831006 // @ts-ignore
9841007 scriptCopy.isScopedtype = falseSCRIPT_TYPES.SCOPED;
9851008 globalScripts.push(scriptCopy);
9861009 } else {
9871010 // @ts-ignore
9881011 scriptCopy.isScopedtype = trueSCRIPT_TYPES.SCOPED;
9891012 scopedScripts.push(scriptCopy);
9901013 }
9911014 });
@@ -1002,10 +1025,17 @@ function populateDebuggerRuleList(container) {
10021025 ruleElement.find('.rule-name').text(script.scriptName);
10031026 ruleElement.find('.rule-regex').text(script.findRegex);
10041027 // @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+ );
10061036 ruleElement.find('.rule-enabled').prop('checked', !script.disabled);
10071037 // @ts-ignore
10081038 ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScopedtype));
10091039
10101040 ruleElement.on('click', function (event) {
10111041 if ($(event.target).is('input, .menu_button, .menu_button i')) {
@@ -1364,10 +1394,10 @@ async function toggleRegexCallback(args, scriptName) {
13641394 break;
13651395 }
13661396
1367- const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id);
1397+ const scriptType = getScriptType(script);
13681398 const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOfgetScriptsByType(scriptscriptType) : scripts.indexOf(script);
13691399
13701400 await saveRegexScript(script, index, isScopedscriptType);
13711401 if (script.disabled) {
13721402 !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`);
13731403 } else {
@@ -1380,9 +1410,9 @@ async function toggleRegexCallback(args, scriptName) {
13801410/**
13811411 * Performs the import of the regex object.
13821412 * @param {Object} regexScript Input object
13831413 * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character?
13841414 */
13851415async function onRegexImportObjectChange(regexScript, isScopedscriptType) {
13861416 try {
13871417 if (!regexScript.scriptName) {
13881418 throw new Error('No script name provided.');
@@ -1391,10 +1421,10 @@ async function onRegexImportObjectChange(regexScript, isScoped) {
13911421 // Assign a new UUID
13921422 regexScript.id = uuidv4();
13931423
1394- const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
1424+ const array = getScriptsByType(scriptType);
13951425 array.push(regexScript);
13961426
13971427 if (isScopedscriptType === SCRIPT_TYPES.SCOPED) {
13981428 await writeExtensionField(this_chid, 'regex_scripts', array);
13991429 }
14001430
@@ -1411,9 +1441,9 @@ async function onRegexImportObjectChange(regexScript, isScoped) {
14111441/**
14121442 * Performs the import of the regex file.
14131443 * @param {File} file Input file
14141444 * @param {booleanSCRIPT_TYPES} isScoped Is thescriptType scriptglobal? scoped to a character?
14151445 */
14161446async function onRegexImportFileChange(file, isScopedscriptType) {
14171447 if (!file) {
14181448 toastr.error('No file provided.');
14191449 return;
@@ -1423,10 +1453,10 @@ async function onRegexImportFileChange(file, isScoped) {
14231453 const regexScripts = JSON.parse(await getFileText(file));
14241454 if (Array.isArray(regexScripts)) {
14251455 for (const regexScript of regexScripts) {
14261456 await onRegexImportObjectChange(regexScript, isScopedscriptType);
14271457 }
14281458 } else {
14291459 await onRegexImportObjectChange(regexScripts, isScopedscriptType);
14301460 }
14311461 } catch (error) {
14321462 console.log(error);
@@ -1435,24 +1465,43 @@ async function onRegexImportFileChange(file, isScoped) {
14351465 }
14361466}
14371467
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+
14381483function purgeEmbeddedRegexScripts({ character }) {
14391484 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+ }
14421492 const index = extension_settings.character_allowed_regex.indexOf(avatar);
14431493 if (index !== -1) {
14441494 extension_settings.character_allowed_regex.splice(index, 1);
14451495 saveSettingsDebounced();
14461496 }
14471497}
1448-}
14491498
14501499async function checkEmbeddedRegexScriptscheckCharEmbeddedRegexScripts() {
14511500 const chid = this_chid;
14521501
14531502 if (chid !== undefined && !selected_group) {
14541503 const avatar = characters[chid]?.avatar;
14551504 const scripts = characters[chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED);
14561505
14571506 if (Array.isArray(scripts) && scripts.length > 0) {
14581507 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
@@ -1497,7 +1546,7 @@ jQuery(async () => {
14971546 const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
14981547 $('#regex_container').append(settingsHtml);
14991548 $('#open_regex_editor').on('click', function () {
15001549 onRegexEditorOpenClick(false, falseSCRIPT_TYPES.GLOBAL);
15011550 });
15021551 $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick);
15031552 $('#open_scoped_editor').on('click', function () {
@@ -1511,19 +1560,19 @@ jQuery(async () => {
15111560 return;
15121561 }
15131562
15141563 onRegexEditorOpenClick(false, trueSCRIPT_TYPES.SCOPED);
15151564 });
15161565 $('#import_regex_file').on('change', async function () {
15171566 let target = 'global'SCRIPT_TYPES.GLOBAL;
15181567 const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
15191568 template.find('#regex_import_target_global').on('input', () => (target = 'global'SCRIPT_TYPES.GLOBAL));
15201569 template.find('#regex_import_target_scoped').on('input', () => (target = 'scoped'SCRIPT_TYPES.SCOPED));
15211570
15221571 await callGenericPopup(template, POPUP_TYPE.TEXT);
15231572
15241573 const inputElement = this instanceof HTMLInputElement && this;
15251574 for (const file of inputElement.files) {
15261575 await onRegexImportFileChange(file, target === 'scoped');
15271576 }
15281577 inputElement.value = '';
15291578 });
@@ -1531,13 +1580,6 @@ jQuery(async () => {
15311580 $('#import_regex_file').trigger('click');
15321581 });
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-
15411583 $('#bulk_select_all_toggle').on('click', async function () {
15421584 const checkboxes = $('#regex_container .regex_bulk_checkbox');
15431585 if (checkboxes.length === 0) {
@@ -1577,6 +1619,31 @@ jQuery(async () => {
15771619 await loadRegexScripts();
15781620 });
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+
15801647 $('#bulk_delete_regex').on('click', async function () {
15811648 const scripts = getSelectedScripts();
15821649 if (scripts.length === 0) {
@@ -1588,11 +1655,11 @@ jQuery(async () => {
15881655 return;
15891656 }
15901657 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 });
15931659 }
1594- await reloadCurrentChat();
15951660 saveSettingsDebounced();
1661+ await loadRegexScripts();
1662+ await reloadCurrentChat();
15961663 });
15971664
15981665 $('#bulk_export_regex').on('click', async function () {
@@ -1611,12 +1678,12 @@ jQuery(async () => {
16111678 {
16121679 selector: '#saved_regex_scripts',
16131680 setter: x => extension_settings.regex = x,
16141681 getter: () => extension_settingsgetScriptsByType(SCRIPT_TYPES.regex ?? []GLOBAL),
16151682 },
16161683 {
16171684 selector: '#saved_scoped_scripts',
16181685 setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
16191686 getter: () => characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scripts ?? []SCOPED),
16201687 },
16211688 ];
16221689 for (const { selector, setter, getter } of sortableDatas) {
@@ -1638,6 +1705,7 @@ jQuery(async () => {
16381705 saveSettingsDebounced();
16391706
16401707 console.debug(`Regex scripts in ${selector} reordered`);
1708+ await reloadCurrentChat();
16411709 await loadRegexScripts();
16421710 },
16431711 });
@@ -1676,11 +1744,48 @@ jQuery(async () => {
16761744 // @ts-ignore
16771745 $('#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+ }
16791778 const localEnumProviders = {
16801779 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+ );
16841789 }),
16851790 };
16861791
@@ -1750,7 +1855,7 @@ jQuery(async () => {
17501855 `,
17511856 }));
17521857
17531858 eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScriptscheckCharEmbeddedRegexScripts);
17541859 eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
17551860
17561861 presetManager.setupEventListeners();
public/scripts/extensions/regex/scriptTemplate.html+2 -2
@@ -12,10 +12,10 @@
1212 <i class="fa-solid fa-pencil"></i>
1313 </div>
1414 <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
1515 <i class="fa-solid fa-arrow-upglobe"></i>
1616 </div>
1717 <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
1818 <i class="fa-solid fa-arrowaddress-downcard"></i>
1919 </div>
2020 <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
2121 <i class="fa-solid fa-file-export"></i>
public/scripts/extensions/regex/style.css+6 -1
@@ -139,7 +139,8 @@ input.enable_scoped {
139139}
140140
141141.regex_settings .regex_bulk_operations,
142142.regex_settings .regex_bulk_checkbox {,
143+.regex_settings .regex_bulk_operations_hr {
143144 display: none;
144145}
145146
@@ -147,6 +148,10 @@ input.enable_scoped {
147148 display: flex;
148149}
149150
151+.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations_hr {
152+ display: block;
153+}
154+
150155.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox {
151156 display: inline-grid;
152157}