mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	* Regex: rework ephemerality options. * Fix ephemeral regex being executed when unwanted * Revert to old code style
		
			
				
	
	
		
			608 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			608 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { callPopup, characters, eventSource, event_types, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
 | |
| import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
 | |
| import { selected_group } from '../../group-chats.js';
 | |
| import { callGenericPopup, POPUP_TYPE } from '../../popup.js';
 | |
| import { SlashCommand } from '../../slash-commands/SlashCommand.js';
 | |
| import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
 | |
| import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
 | |
| import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
 | |
| import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
 | |
| import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
 | |
| import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
 | |
| import { t } from '../../i18n.js';
 | |
| 
 | |
| /**
 | |
|  * @typedef {object} RegexScript
 | |
|  * @property {string} scriptName - The name of the script
 | |
|  * @property {boolean} disabled - Whether the script is disabled
 | |
|  * @property {string} replaceString - The replace string
 | |
|  * @property {string[]} trimStrings - The trim strings
 | |
|  * @property {string?} findRegex - The find regex
 | |
|  * @property {number?} substituteRegex - The substitute regex
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
 | |
|  *
 | |
|  * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information.
 | |
|  */
 | |
| export function getRegexScripts() {
 | |
|     return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Saves a regex script to the extension settings or character data.
 | |
|  * @param {import('../../char-data.js').RegexScriptData} regexScript
 | |
|  * @param {number} existingScriptIndex Index of the existing script
 | |
|  * @param {boolean} isScoped Is the script scoped to a character?
 | |
|  * @returns {Promise<void>}
 | |
|  */
 | |
| async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
 | |
|     // If not editing
 | |
|     const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | |
| 
 | |
|     // Assign a UUID if it doesn't exist
 | |
|     if (!regexScript.id) {
 | |
|         regexScript.id = uuidv4();
 | |
|     }
 | |
| 
 | |
|     // Is the script name undefined or empty?
 | |
|     if (!regexScript.scriptName) {
 | |
|         toastr.error('Could not save regex script: The script name was undefined or empty!');
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     // Is a find regex present?
 | |
|     if (regexScript.findRegex.length === 0) {
 | |
|         toastr.warning('This regex script will not work, but was saved anyway: A find regex isn\'t present.');
 | |
|     }
 | |
| 
 | |
|     // Is there someplace to place results?
 | |
|     if (regexScript.placement.length === 0) {
 | |
|         toastr.warning('This regex script will not work, but was saved anyway: One "Affects" checkbox must be selected!');
 | |
|     }
 | |
| 
 | |
|     if (existingScriptIndex !== -1) {
 | |
|         array[existingScriptIndex] = regexScript;
 | |
|     } else {
 | |
|         array.push(regexScript);
 | |
|     }
 | |
| 
 | |
|     if (isScoped) {
 | |
|         await writeExtensionField(this_chid, 'regex_scripts', array);
 | |
| 
 | |
|         // Add the character to the allowed list
 | |
|         if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) {
 | |
|             extension_settings.character_allowed_regex.push(characters[this_chid].avatar);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     saveSettingsDebounced();
 | |
|     await loadRegexScripts();
 | |
| 
 | |
|     // Reload the current chat to undo previous markdown
 | |
|     const currentChatId = getCurrentChatId();
 | |
|     if (currentChatId !== undefined && currentChatId !== null) {
 | |
|         await reloadCurrentChat();
 | |
|     }
 | |
| }
 | |
| 
 | |
| async function deleteRegexScript({ id, isScoped }) {
 | |
|     const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | |
| 
 | |
|     const existingScriptIndex = array.findIndex((script) => script.id === id);
 | |
|     if (!existingScriptIndex || existingScriptIndex !== -1) {
 | |
|         array.splice(existingScriptIndex, 1);
 | |
| 
 | |
|         if (isScoped) {
 | |
|             await writeExtensionField(this_chid, 'regex_scripts', array);
 | |
|         }
 | |
| 
 | |
|         saveSettingsDebounced();
 | |
|         await loadRegexScripts();
 | |
|     }
 | |
| }
 | |
| 
 | |
| async function loadRegexScripts() {
 | |
|     $('#saved_regex_scripts').empty();
 | |
|     $('#saved_scoped_scripts').empty();
 | |
| 
 | |
|     const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
 | |
| 
 | |
|     /**
 | |
|      * Renders a script to the UI.
 | |
|      * @param {string} container Container to render the script to
 | |
|      * @param {import('../../char-data.js').RegexScriptData} script Script data
 | |
|      * @param {boolean} isScoped Script is scoped to a character
 | |
|      * @param {number} index Index of the script in the array
 | |
|      */
 | |
|     function renderScript(container, script, isScoped, index) {
 | |
|         // Have to clone here
 | |
|         const scriptHtml = scriptTemplate.clone();
 | |
|         const save = () => saveRegexScript(script, index, isScoped);
 | |
| 
 | |
|         if (!script.id) {
 | |
|             script.id = uuidv4();
 | |
|         }
 | |
| 
 | |
|         scriptHtml.attr('id', script.id);
 | |
|         scriptHtml.find('.regex_script_name').text(script.scriptName);
 | |
|         scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
 | |
|             .on('input', async function () {
 | |
|                 script.disabled = !!$(this).prop('checked');
 | |
|                 await save();
 | |
|             });
 | |
|         scriptHtml.find('.regex-toggle-on').on('click', function () {
 | |
|             scriptHtml.find('.disable_regex').prop('checked', true).trigger('input');
 | |
|         });
 | |
|         scriptHtml.find('.regex-toggle-off').on('click', function () {
 | |
|             scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
 | |
|         });
 | |
|         scriptHtml.find('.edit_existing_regex').on('click', async function () {
 | |
|             await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped);
 | |
|         });
 | |
|         scriptHtml.find('.move_to_global').on('click', async function () {
 | |
|             const confirm = await callGenericPopup('Are you sure you want to move this regex script to global?', POPUP_TYPE.CONFIRM);
 | |
| 
 | |
|             if (!confirm) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             await deleteRegexScript({ id: script.id, isScoped: true });
 | |
|             await saveRegexScript(script, -1, false);
 | |
|         });
 | |
|         scriptHtml.find('.move_to_scoped').on('click', async function () {
 | |
|             if (this_chid === undefined) {
 | |
|                 toastr.error('No character selected.');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if (selected_group) {
 | |
|                 toastr.error('Cannot edit scoped scripts in group chats.');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             const confirm = await callGenericPopup('Are you sure you want to move this regex script to scoped?', POPUP_TYPE.CONFIRM);
 | |
| 
 | |
|             if (!confirm) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             await deleteRegexScript({ id: script.id, isScoped: false });
 | |
|             await saveRegexScript(script, -1, true);
 | |
|         });
 | |
|         scriptHtml.find('.export_regex').on('click', async function () {
 | |
|             const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
 | |
|             const fileData = JSON.stringify(script, null, 4);
 | |
|             download(fileData, fileName, 'application/json');
 | |
|         });
 | |
|         scriptHtml.find('.delete_regex').on('click', async function () {
 | |
|             const confirm = await callGenericPopup('Are you sure you want to delete this regex script?', POPUP_TYPE.CONFIRM);
 | |
| 
 | |
|             if (!confirm) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             await deleteRegexScript({ id: script.id, isScoped });
 | |
|             await reloadCurrentChat();
 | |
|         });
 | |
| 
 | |
|         $(container).append(scriptHtml);
 | |
|     }
 | |
| 
 | |
|     extension_settings?.regex?.forEach((script, index, array) => renderScript('#saved_regex_scripts', script, false, index, array));
 | |
|     characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index, array) => renderScript('#saved_scoped_scripts', script, true, index, array));
 | |
| 
 | |
|     const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
 | |
|     $('#regex_scoped_toggle').prop('checked', isAllowed);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Opens the regex editor.
 | |
|  * @param {string|boolean} existingId Existing ID
 | |
|  * @param {boolean} isScoped Is the script scoped to a character?
 | |
|  * @returns {Promise<void>}
 | |
|  */
 | |
| async function onRegexEditorOpenClick(existingId, isScoped) {
 | |
|     const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));
 | |
|     const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | |
| 
 | |
|     // If an ID exists, fill in all the values
 | |
|     let existingScriptIndex = -1;
 | |
|     if (existingId) {
 | |
|         existingScriptIndex = array.findIndex((script) => script.id === existingId);
 | |
|         if (existingScriptIndex !== -1) {
 | |
|             const existingScript = array[existingScriptIndex];
 | |
|             if (existingScript.scriptName) {
 | |
|                 editorHtml.find('.regex_script_name').val(existingScript.scriptName);
 | |
|             } else {
 | |
|                 toastr.error('This script doesn\'t have a name! Please delete it.');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             editorHtml.find('.find_regex').val(existingScript.findRegex || '');
 | |
|             editorHtml.find('.regex_replace_string').val(existingScript.replaceString || '');
 | |
|             editorHtml.find('.regex_trim_strings').val(existingScript.trimStrings?.join('\n') || []);
 | |
|             editorHtml.find('input[name="disabled"]').prop('checked', existingScript.disabled ?? false);
 | |
|             editorHtml.find('input[name="only_format_display"]').prop('checked', existingScript.markdownOnly ?? false);
 | |
|             editorHtml.find('input[name="only_format_prompt"]').prop('checked', existingScript.promptOnly ?? false);
 | |
|             editorHtml.find('input[name="run_on_edit"]').prop('checked', existingScript.runOnEdit ?? false);
 | |
|             editorHtml.find('select[name="substitute_regex"]').val(existingScript.substituteRegex ?? substitute_find_regex.NONE);
 | |
|             editorHtml.find('input[name="min_depth"]').val(existingScript.minDepth ?? '');
 | |
|             editorHtml.find('input[name="max_depth"]').val(existingScript.maxDepth ?? '');
 | |
| 
 | |
|             existingScript.placement.forEach((element) => {
 | |
|                 editorHtml
 | |
|                     .find(`input[name="replace_position"][value="${element}"]`)
 | |
|                     .prop('checked', true);
 | |
|             });
 | |
|         }
 | |
|     } else {
 | |
|         editorHtml
 | |
|             .find('input[name="only_format_display"]')
 | |
|             .prop('checked', true);
 | |
| 
 | |
|         editorHtml
 | |
|             .find('input[name="run_on_edit"]')
 | |
|             .prop('checked', true);
 | |
| 
 | |
|         editorHtml
 | |
|             .find('input[name="replace_position"][value="1"]')
 | |
|             .prop('checked', true);
 | |
|     }
 | |
| 
 | |
|     editorHtml.find('#regex_test_mode_toggle').on('click', function () {
 | |
|         editorHtml.find('#regex_test_mode').toggleClass('displayNone');
 | |
|         updateTestResult();
 | |
|     });
 | |
| 
 | |
|     function updateTestResult() {
 | |
|         if (!editorHtml.find('#regex_test_mode').is(':visible')) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const testScript = {
 | |
|             id: uuidv4(),
 | |
|             scriptName: editorHtml.find('.regex_script_name').val(),
 | |
|             findRegex: editorHtml.find('.find_regex').val(),
 | |
|             replaceString: editorHtml.find('.regex_replace_string').val(),
 | |
|             trimStrings: String(editorHtml.find('.regex_trim_strings').val()).split('\n').filter((e) => e.length !== 0) || [],
 | |
|             substituteRegex: Number(editorHtml.find('select[name="substitute_regex"]').val()),
 | |
|         };
 | |
|         const rawTestString = String(editorHtml.find('#regex_test_input').val());
 | |
|         const result = runRegexScript(testScript, rawTestString);
 | |
|         editorHtml.find('#regex_test_output').text(result);
 | |
|     }
 | |
| 
 | |
|     editorHtml.find('input, textarea, select').on('input', updateTestResult);
 | |
| 
 | |
|     const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: t`Save` });
 | |
|     if (popupResult) {
 | |
|         const newRegexScript = {
 | |
|             id: existingId ? String(existingId) : uuidv4(),
 | |
|             scriptName: String(editorHtml.find('.regex_script_name').val()),
 | |
|             findRegex: String(editorHtml.find('.find_regex').val()),
 | |
|             replaceString: String(editorHtml.find('.regex_replace_string').val()),
 | |
|             trimStrings: editorHtml.find('.regex_trim_strings').val().split('\n').filter((e) => e.length !== 0) || [],
 | |
|             placement:
 | |
|                 editorHtml
 | |
|                     .find('input[name="replace_position"]')
 | |
|                     .filter(':checked')
 | |
|                     .map(function () { return parseInt($(this).val()); })
 | |
|                     .get()
 | |
|                     .filter((e) => !isNaN(e)) || [],
 | |
|             disabled: editorHtml.find('input[name="disabled"]').prop('checked'),
 | |
|             markdownOnly: editorHtml.find('input[name="only_format_display"]').prop('checked'),
 | |
|             promptOnly: editorHtml.find('input[name="only_format_prompt"]').prop('checked'),
 | |
|             runOnEdit: editorHtml.find('input[name="run_on_edit"]').prop('checked'),
 | |
|             substituteRegex: Number(editorHtml.find('select[name="substitute_regex"]').val()),
 | |
|             minDepth: parseInt(String(editorHtml.find('input[name="min_depth"]').val())),
 | |
|             maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
 | |
|         };
 | |
| 
 | |
|         saveRegexScript(newRegexScript, existingScriptIndex, isScoped);
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Common settings migration function. Some parts will eventually be removed
 | |
| // TODO: Maybe migrate placement to strings?
 | |
| function migrateSettings() {
 | |
|     let performSave = false;
 | |
| 
 | |
|     // Current: If MD Display is present in placement, remove it and add new placements/MD option
 | |
|     extension_settings.regex.forEach((script) => {
 | |
|         if (!script.id) {
 | |
|             script.id = uuidv4();
 | |
|             performSave = true;
 | |
|         }
 | |
| 
 | |
|         if (script.placement.includes(regex_placement.MD_DISPLAY)) {
 | |
|             script.placement = script.placement.length === 1 ?
 | |
|                 Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
 | |
|                 script.placement = script.placement.filter((e) => e !== regex_placement.MD_DISPLAY);
 | |
| 
 | |
|             script.markdownOnly = true;
 | |
|             script.promptOnly = true;
 | |
| 
 | |
|             performSave = true;
 | |
|         }
 | |
| 
 | |
|         // Old system and sendas placement migration
 | |
|         // 4 - sendAs
 | |
|         if (script.placement.includes(4)) {
 | |
|             script.placement = script.placement.length === 1 ?
 | |
|                 [regex_placement.SLASH_COMMAND] :
 | |
|                 script.placement = script.placement.filter((e) => e !== 4);
 | |
| 
 | |
|             performSave = true;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     if (!extension_settings.character_allowed_regex) {
 | |
|         extension_settings.character_allowed_regex = [];
 | |
|         performSave = true;
 | |
|     }
 | |
| 
 | |
|     if (performSave) {
 | |
|         saveSettingsDebounced();
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * /regex slash command callback
 | |
|  * @param {{name: string}} args Named arguments
 | |
|  * @param {string} value Unnamed argument
 | |
|  * @returns {string} The regexed string
 | |
|  */
 | |
| function runRegexCallback(args, value) {
 | |
|     if (!args.name) {
 | |
|         toastr.warning('No regex script name provided.');
 | |
|         return value;
 | |
|     }
 | |
| 
 | |
|     const scriptName = args.name;
 | |
|     const scripts = getRegexScripts();
 | |
| 
 | |
|     for (const script of scripts) {
 | |
|         if (script.scriptName.toLowerCase() === scriptName.toLowerCase()) {
 | |
|             if (script.disabled) {
 | |
|                 toastr.warning(`Regex script "${scriptName}" is disabled.`);
 | |
|                 return value;
 | |
|             }
 | |
| 
 | |
|             console.debug(`Running regex callback for ${scriptName}`);
 | |
|             return runRegexScript(script, value);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     toastr.warning(`Regex script "${scriptName}" not found.`);
 | |
|     return value;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Performs the import of the regex file.
 | |
|  * @param {File} file Input file
 | |
|  * @param {boolean} isScoped Is the script scoped to a character?
 | |
|  */
 | |
| async function onRegexImportFileChange(file, isScoped) {
 | |
|     if (!file) {
 | |
|         toastr.error('No file provided.');
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         const fileText = await getFileText(file);
 | |
|         const regexScript = JSON.parse(fileText);
 | |
|         if (!regexScript.scriptName) {
 | |
|             throw new Error('No script name provided.');
 | |
|         }
 | |
| 
 | |
|         // Assign a new UUID
 | |
|         regexScript.id = uuidv4();
 | |
| 
 | |
|         const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
 | |
|         array.push(regexScript);
 | |
| 
 | |
|         if (isScoped) {
 | |
|             await writeExtensionField(this_chid, 'regex_scripts', array);
 | |
|         }
 | |
| 
 | |
|         saveSettingsDebounced();
 | |
|         await loadRegexScripts();
 | |
|         toastr.success(`Regex script "${regexScript.scriptName}" imported.`);
 | |
|     } catch (error) {
 | |
|         console.log(error);
 | |
|         toastr.error('Invalid JSON file.');
 | |
|         return;
 | |
|     }
 | |
| }
 | |
| 
 | |
| function purgeEmbeddedRegexScripts( { character }){
 | |
|     const avatar = character?.avatar;
 | |
| 
 | |
|     if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) {
 | |
|         const index = extension_settings.character_allowed_regex.indexOf(avatar);
 | |
|         if (index !== -1) {
 | |
|             extension_settings.character_allowed_regex.splice(index, 1);
 | |
|             saveSettingsDebounced();
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| async function checkEmbeddedRegexScripts() {
 | |
|     const chid = this_chid;
 | |
| 
 | |
|     if (chid !== undefined && !selected_group) {
 | |
|         const avatar = characters[chid]?.avatar;
 | |
|         const scripts = characters[chid]?.data?.extensions?.regex_scripts;
 | |
| 
 | |
|         if (Array.isArray(scripts) && scripts.length > 0) {
 | |
|             if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
 | |
|                 const checkKey = `AlertRegex_${characters[chid].avatar}`;
 | |
| 
 | |
|                 if (!localStorage.getItem(checkKey)) {
 | |
|                     localStorage.setItem(checkKey, 'true');
 | |
|                     const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
 | |
|                     const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
 | |
| 
 | |
|                     if (result) {
 | |
|                         extension_settings.character_allowed_regex.push(avatar);
 | |
|                         await reloadCurrentChat();
 | |
|                         saveSettingsDebounced();
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     loadRegexScripts();
 | |
| }
 | |
| 
 | |
| // Workaround for loading in sequence with other extensions
 | |
| // NOTE: Always puts extension at the top of the list, but this is fine since it's static
 | |
| jQuery(async () => {
 | |
|     if (extension_settings.regex) {
 | |
|         migrateSettings();
 | |
|     }
 | |
| 
 | |
|     // Manually disable the extension since static imports auto-import the JS file
 | |
|     if (extension_settings.disabledExtensions.includes('regex')) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
 | |
|     $('#regex_container').append(settingsHtml);
 | |
|     $('#open_regex_editor').on('click', function () {
 | |
|         onRegexEditorOpenClick(false, false);
 | |
|     });
 | |
|     $('#open_scoped_editor').on('click', function () {
 | |
|         if (this_chid === undefined) {
 | |
|             toastr.error('No character selected.');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (selected_group) {
 | |
|             toastr.error('Cannot edit scoped scripts in group chats.');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         onRegexEditorOpenClick(false, true);
 | |
|     });
 | |
|     $('#import_regex_file').on('change', async function () {
 | |
|         let target = 'global';
 | |
|         const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
 | |
|         template.find('#regex_import_target_global').on('input', () => target = 'global');
 | |
|         template.find('#regex_import_target_scoped').on('input', () => target = 'scoped');
 | |
| 
 | |
|         await callGenericPopup(template, POPUP_TYPE.TEXT);
 | |
| 
 | |
|         const inputElement = this instanceof HTMLInputElement && this;
 | |
|         for (const file of inputElement.files) {
 | |
|             await onRegexImportFileChange(file, target === 'scoped');
 | |
|         }
 | |
|         inputElement.value = '';
 | |
|     });
 | |
|     $('#import_regex').on('click', function () {
 | |
|         $('#import_regex_file').trigger('click');
 | |
|     });
 | |
| 
 | |
|     let sortableDatas = [
 | |
|         {
 | |
|             selector: '#saved_regex_scripts',
 | |
|             setter: x => extension_settings.regex = x,
 | |
|             getter: () => extension_settings.regex ?? [],
 | |
|         },
 | |
|         {
 | |
|             selector: '#saved_scoped_scripts',
 | |
|             setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
 | |
|             getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [],
 | |
|         },
 | |
|     ];
 | |
|     for (const { selector, setter, getter } of sortableDatas) {
 | |
|         $(selector).sortable({
 | |
|             delay: getSortableDelay(),
 | |
|             stop: async function () {
 | |
|                 const oldScripts = getter();
 | |
|                 const newScripts = [];
 | |
|                 $(selector).children().each(function () {
 | |
|                     const id = $(this).attr('id');
 | |
|                     const existingScript = oldScripts.find((e) => e.id === id);
 | |
|                     if (existingScript) {
 | |
|                         newScripts.push(existingScript);
 | |
|                     }
 | |
|                 });
 | |
| 
 | |
|                 await setter(newScripts);
 | |
|                 saveSettingsDebounced();
 | |
| 
 | |
|                 console.debug(`Regex scripts in ${selector} reordered`);
 | |
|                 await loadRegexScripts();
 | |
|             },
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     $('#regex_scoped_toggle').on('input', function () {
 | |
|         if (this_chid === undefined) {
 | |
|             toastr.error('No character selected.');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if (selected_group) {
 | |
|             toastr.error('Cannot edit scoped scripts in group chats.');
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const isEnable = !!$(this).prop('checked');
 | |
|         const avatar = characters[this_chid].avatar;
 | |
| 
 | |
|         if (isEnable) {
 | |
|             if (!extension_settings.character_allowed_regex.includes(avatar)) {
 | |
|                 extension_settings.character_allowed_regex.push(avatar);
 | |
|             }
 | |
|         } else {
 | |
|             const index = extension_settings.character_allowed_regex.indexOf(avatar);
 | |
|             if (index !== -1) {
 | |
|                 extension_settings.character_allowed_regex.splice(index, 1);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         saveSettingsDebounced();
 | |
|         reloadCurrentChat();
 | |
|     });
 | |
| 
 | |
|     await loadRegexScripts();
 | |
|     $('#saved_regex_scripts').sortable('enable');
 | |
| 
 | |
|     const localEnumProviders = {
 | |
|         regexScripts: () => getRegexScripts().map(script => {
 | |
|             const isGlobal = extension_settings.regex?.some(x => x.scriptName === script.scriptName);
 | |
|             return new SlashCommandEnumValue(script.scriptName, `${enumIcons.getStateIcon(!script.disabled)} [${isGlobal ? 'global' : 'scoped'}] ${script.findRegex}`,
 | |
|                 isGlobal ? enumTypes.enum : enumTypes.name, isGlobal ? 'G' : 'S');
 | |
|         }),
 | |
|     };
 | |
| 
 | |
|     SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | |
|         name: 'regex',
 | |
|         callback: runRegexCallback,
 | |
|         returns: 'replaced text',
 | |
|         namedArgumentList: [
 | |
|             SlashCommandNamedArgument.fromProps({
 | |
|                 name: 'name',
 | |
|                 description: 'script name',
 | |
|                 typeList: [ARGUMENT_TYPE.STRING],
 | |
|                 isRequired: true,
 | |
|                 enumProvider: localEnumProviders.regexScripts,
 | |
|             }),
 | |
|         ],
 | |
|         unnamedArgumentList: [
 | |
|             new SlashCommandArgument(
 | |
|                 'input', [ARGUMENT_TYPE.STRING], false,
 | |
|             ),
 | |
|         ],
 | |
|         helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
 | |
|     }));
 | |
| 
 | |
|     eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);
 | |
|     eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
 | |
| });
 |