Merge pull request #3967 from gakada/regex Regex: allow multiple definitions in a single file

1b5a11206fffe3c924a375089086cea654abf3ab

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
4 files changed, +143 -12Showing whitespace changes
public/scripts/extensions/regex/dropdown.html+23 -0
@@ -21,6 +21,29 @@
2121 <small data-i18n="ext_regex_import_script">Import</small>
2222 </div>
2323 <input type="file" id="import_regex_file" hidden accept="*.json" multiple />
24+ <label for="regex_bulk_edit" class="menu_button menu_button_icon">
25+ <input id="regex_bulk_edit" type="checkbox" class="displayNone" />
26+ <i class="fa-solid fa-edit"></i>
27+ <small data-i18n="ext_regex_bulk_edit">Bulk Edit</small>
28+ </label>
29+ </div>
30+ <div class="regex_bulk_operations flex-container justifyCenter">
31+ <div id="bulk_enable_regex" class="menu_button menu_button_icon">
32+ <i class="fa-solid fa-toggle-on"></i>
33+ <small data-i18n="Enable">Enable</small>
34+ </div>
35+ <div id="bulk_disable_regex" class="menu_button menu_button_icon">
36+ <i class="fa-solid fa-toggle-off"></i>
37+ <small data-i18n="Disable">Disable</small>
38+ </div>
39+ <div id="bulk_export_regex" class="menu_button menu_button_icon">
40+ <i class="fa-solid fa-file-export"></i>
41+ <small data-i18n="Export">Export</small>
42+ </div>
43+ <div id="bulk_delete_regex" class="menu_button menu_button_icon">
44+ <i class="fa-solid fa-trash"></i>
45+ <small data-i18n="Delete">Delete</small>
46+ </div>
2447 </div>
2548 <hr />
2649 <div id="global_scripts_block" class="padding5">
public/scripts/extensions/regex/index.js+96 -11
@@ -12,6 +12,8 @@ import { regex_placement, runRegexScript, substitute_find_regex } from './engine
1212import { t } from '../../i18n.js';
1313import { accountStorage } from '../../util/AccountStorage.js';
1414
15+const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase();
16+
1517/**
1618 * @typedef {import('../../char-data.js').RegexScriptData} RegexScript
1719 */
@@ -167,7 +169,7 @@ async function loadRegexScripts() {
167169 await saveRegexScript(script, -1, true);
168170 });
169171 scriptHtml.find('.export_regex').on('click', async function () {
170- const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
172+ const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`;
171173 const fileData = JSON.stringify(script, null, 4);
172174 download(fileData, fileName, 'application/json');
173175 });
@@ -459,19 +461,12 @@ async function toggleRegexCallback(args, scriptName) {
459461}
460462
461463/**
462464 * Performs the import of the regex fileobject.
463465 * @param {FileObject} fileregexScript Input fileobject
464466 * @param {boolean} isScoped Is the script scoped to a character?
465467 */
466468async function onRegexImportFileChangeonRegexImportObjectChange(fileregexScript, isScoped) {
467- if (!file) {
468- toastr.error('No file provided.');
469- return;
470- }
471-
472469 try {
473- const fileText = await getFileText(file);
474- const regexScript = JSON.parse(fileText);
475470 if (!regexScript.scriptName) {
476471 throw new Error('No script name provided.');
477472 }
@@ -491,6 +486,33 @@ async function onRegexImportFileChange(file, isScoped) {
491486 toastr.success(`Regex script "${regexScript.scriptName}" imported.`);
492487 } catch (error) {
493488 console.log(error);
489+ toastr.error('Invalid regex object.');
490+ return;
491+ }
492+}
493+
494+/**
495+ * Performs the import of the regex file.
496+ * @param {File} file Input file
497+ * @param {boolean} isScoped Is the script scoped to a character?
498+ */
499+async function onRegexImportFileChange(file, isScoped) {
500+ if (!file) {
501+ toastr.error('No file provided.');
502+ return;
503+ }
504+
505+ try {
506+ const regexScripts = JSON.parse(await getFileText(file));
507+ if (Array.isArray(regexScripts)) {
508+ for (const regexScript of regexScripts) {
509+ await onRegexImportObjectChange(regexScript, isScoped);
510+ }
511+ } else {
512+ await onRegexImportObjectChange(regexScripts, isScoped);
513+ }
514+ } catch (error) {
515+ console.log(error);
494516 toastr.error('Invalid JSON file.');
495517 return;
496518 }
@@ -585,6 +607,69 @@ jQuery(async () => {
585607 $('#import_regex_file').trigger('click');
586608 });
587609
610+ function getSelectedScripts() {
611+ const scripts = getRegexScripts();
612+ const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)';
613+ const selectedIds = Array.from(document.querySelectorAll(selector)).map(e => e.getAttribute('id')).filter(id => id);
614+ return scripts.filter(script => selectedIds.includes(script.id));
615+ }
616+
617+ $('#bulk_enable_regex').on('click', async function () {
618+ const scripts = getSelectedScripts().filter(script => script.disabled);
619+ if (scripts.length === 0) {
620+ toastr.warning(t`No regex scripts selected for enabling.`);
621+ return;
622+ }
623+ for (const script of scripts) {
624+ script.disabled = false;
625+ }
626+ saveSettingsDebounced();
627+ await loadRegexScripts();
628+ });
629+
630+ $('#bulk_disable_regex').on('click', async function () {
631+ const scripts = getSelectedScripts().filter(script => !script.disabled);
632+ if (scripts.length === 0) {
633+ toastr.warning(t`No regex scripts selected for disabling.`);
634+ return;
635+ }
636+ for (const script of scripts) {
637+ script.disabled = true;
638+ }
639+ saveSettingsDebounced();
640+ await loadRegexScripts();
641+ });
642+
643+ $('#bulk_delete_regex').on('click', async function () {
644+ const scripts = getSelectedScripts();
645+ if (scripts.length === 0) {
646+ toastr.warning(t`No regex scripts selected for deletion.`);
647+ return;
648+ }
649+ const confirm = await callGenericPopup('Are you sure you want to delete the selected regex scripts?', POPUP_TYPE.CONFIRM);
650+ if (!confirm) {
651+ return;
652+ }
653+ for (const script of scripts) {
654+ const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id);
655+ await deleteRegexScript({ id: script.id, isScoped: isScoped });
656+ }
657+ await reloadCurrentChat();
658+ saveSettingsDebounced();
659+ });
660+
661+ $('#bulk_export_regex').on('click', async function () {
662+ const scripts = getSelectedScripts();
663+ if (scripts.length === 0) {
664+ toastr.warning(t`No regex scripts selected for export.`);
665+ return;
666+ }
667+ const fileName = `regex-${new Date().toISOString()}.json`;
668+ const fileData = JSON.stringify(scripts, null, 4);
669+ download(fileData, fileName, 'application/json');
670+ await loadRegexScripts();
671+ });
672+
588673 let sortableDatas = [
589674 {
590675 selector: '#saved_regex_scripts',
public/scripts/extensions/regex/scriptTemplate.html+1 -0
@@ -1,4 +1,5 @@
11<div class="regex-script-label flex-container flexnowrap">
2+ <input type="checkbox" class="regex_bulk_checkbox" />
23 <span class="drag-handle menu-handle">&#9776;</span>
34 <div class="regex_script_name flexGrow overflow-hidden"></div>
45 <div class="flex-container flexnowrap">
public/scripts/extensions/regex/style.css+23 -1
@@ -56,7 +56,7 @@
5656}
5757
5858.regex-script-label {
5959 align-items: centerbaseline;
6060 border: 1px solid var(--SmartThemeBorderColor);
6161 border-radius: 10px;
6262 padding: 0 5px;
@@ -126,3 +126,25 @@ input.enable_scoped {
126126 right: 10px;
127127 transform: translateY(-50%);
128128}
129+
130+.regex_settings label[for="regex_bulk_edit"]:has(#regex_bulk_edit:checked) {
131+ color: var(--golden);
132+}
133+
134+.regex_settings .regex-script-container .regex-script-label .regex_bulk_checkbox {
135+ margin-left: 5px;
136+ margin-right: 5px;
137+}
138+
139+.regex_settings .regex_bulk_operations,
140+.regex_settings .regex_bulk_checkbox {
141+ display: none;
142+}
143+
144+.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations {
145+ display: flex;
146+}
147+
148+.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox {
149+ display: inline-grid;
150+}