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 -12Ignore whitespace
public/scripts/extensions/regex/dropdown.html+23 -0
@@ -21,6 +21,29 @@
21 <small data-i18n="ext_regex_import_script">Import</small>21 <small data-i18n="ext_regex_import_script">Import</small>
22 </div>22 </div>
23 <input type="file" id="import_regex_file" hidden accept="*.json" multiple />23 <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>
24 </div>47 </div>
25 <hr />48 <hr />
26 <div id="global_scripts_block" class="padding5">49 <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
12import { t } from '../../i18n.js';12import { t } from '../../i18n.js';
13import { accountStorage } from '../../util/AccountStorage.js';13import { accountStorage } from '../../util/AccountStorage.js';
1414
15const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase();
16
15/**17/**
16 * @typedef {import('../../char-data.js').RegexScriptData} RegexScript18 * @typedef {import('../../char-data.js').RegexScriptData} RegexScript
17 */19 */
@@ -167,7 +169,7 @@ async function loadRegexScripts() {
167 await saveRegexScript(script, -1, true);169 await saveRegexScript(script, -1, true);
168 });170 });
169 scriptHtml.find('.export_regex').on('click', async function () {171 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`;
171 const fileData = JSON.stringify(script, null, 4);173 const fileData = JSON.stringify(script, null, 4);
172 download(fileData, fileName, 'application/json');174 download(fileData, fileName, 'application/json');
173 });175 });
@@ -459,19 +461,12 @@ async function toggleRegexCallback(args, scriptName) {
459}461}
460462
461/**463/**
462 * Performs the import of the regex file.464 * Performs the import of the regex object.
463 * @param {File} file Input file465 * @param {Object} regexScript Input object
464 * @param {boolean} isScoped Is the script scoped to a character?466 * @param {boolean} isScoped Is the script scoped to a character?
465 */467 */
466async function onRegexImportFileChange(file, isScoped) {468async function onRegexImportObjectChange(regexScript, isScoped) {
467 if (!file) {
468 toastr.error('No file provided.');
469 return;
470 }
471
472 try {469 try {
473 const fileText = await getFileText(file);
474 const regexScript = JSON.parse(fileText);
475 if (!regexScript.scriptName) {470 if (!regexScript.scriptName) {
476 throw new Error('No script name provided.');471 throw new Error('No script name provided.');
477 }472 }
@@ -491,6 +486,33 @@ async function onRegexImportFileChange(file, isScoped) {
491 toastr.success(`Regex script "${regexScript.scriptName}" imported.`);486 toastr.success(`Regex script "${regexScript.scriptName}" imported.`);
492 } catch (error) {487 } catch (error) {
493 console.log(error);488 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 */
499async 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);
494 toastr.error('Invalid JSON file.');516 toastr.error('Invalid JSON file.');
495 return;517 return;
496 }518 }
@@ -585,6 +607,69 @@ jQuery(async () => {
585 $('#import_regex_file').trigger('click');607 $('#import_regex_file').trigger('click');
586 });608 });
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
588 let sortableDatas = [673 let sortableDatas = [
589 {674 {
590 selector: '#saved_regex_scripts',675 selector: '#saved_regex_scripts',
public/scripts/extensions/regex/scriptTemplate.html+1 -0
@@ -1,4 +1,5 @@
1<div class="regex-script-label flex-container flexnowrap">1<div class="regex-script-label flex-container flexnowrap">
2 <input type="checkbox" class="regex_bulk_checkbox" />
2 <span class="drag-handle menu-handle">&#9776;</span>3 <span class="drag-handle menu-handle">&#9776;</span>
3 <div class="regex_script_name flexGrow overflow-hidden"></div>4 <div class="regex_script_name flexGrow overflow-hidden"></div>
4 <div class="flex-container flexnowrap">5 <div class="flex-container flexnowrap">
public/scripts/extensions/regex/style.css+23 -1
@@ -56,7 +56,7 @@
56}56}
5757
58.regex-script-label {58.regex-script-label {
59 align-items: center;59 align-items: baseline;
60 border: 1px solid var(--SmartThemeBorderColor);60 border: 1px solid var(--SmartThemeBorderColor);
61 border-radius: 10px;61 border-radius: 10px;
62 padding: 0 5px;62 padding: 0 5px;
@@ -126,3 +126,25 @@ input.enable_scoped {
126 right: 10px;126 right: 10px;
127 transform: translateY(-50%);127 transform: translateY(-50%);
128}128}
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}