mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-14 10:24:47 +01:00
358 lines
14 KiB
JavaScript
358 lines
14 KiB
JavaScript
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
||
import { extension_settings } from '../../extensions.js';
|
||
import { registerSlashCommand } from '../../slash-commands.js';
|
||
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
|
||
import { resolveVariable } from '../../variables.js';
|
||
import { regex_placement, runRegexScript } from './engine.js';
|
||
|
||
async function saveRegexScript(regexScript, existingScriptIndex) {
|
||
// If not editing
|
||
|
||
// 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;
|
||
}
|
||
|
||
if (existingScriptIndex === -1) {
|
||
// Does the script name already exist?
|
||
if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
|
||
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
|
||
return;
|
||
}
|
||
} else {
|
||
// Does the script name already exist somewhere else?
|
||
// (If this fails, make it a .filter().map() to index array)
|
||
const foundIndex = extension_settings.regex.findIndex((e) => e.scriptName === regexScript.scriptName);
|
||
if (foundIndex !== existingScriptIndex && foundIndex !== -1) {
|
||
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
|
||
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) {
|
||
extension_settings.regex[existingScriptIndex] = regexScript;
|
||
} else {
|
||
extension_settings.regex.push(regexScript);
|
||
}
|
||
|
||
saveSettingsDebounced();
|
||
await loadRegexScripts();
|
||
|
||
// Reload the current chat to undo previous markdown
|
||
const currentChatId = getCurrentChatId();
|
||
if (currentChatId !== undefined && currentChatId !== null) {
|
||
await reloadCurrentChat();
|
||
}
|
||
}
|
||
|
||
async function deleteRegexScript({ existingId }) {
|
||
let scriptName = $(`#${existingId}`).find('.regex_script_name').text();
|
||
|
||
const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName);
|
||
if (!existingScriptIndex || existingScriptIndex !== -1) {
|
||
extension_settings.regex.splice(existingScriptIndex, 1);
|
||
|
||
saveSettingsDebounced();
|
||
await loadRegexScripts();
|
||
}
|
||
}
|
||
|
||
async function loadRegexScripts() {
|
||
$('#saved_regex_scripts').empty();
|
||
|
||
const scriptTemplate = $(await $.get('scripts/extensions/regex/scriptTemplate.html'));
|
||
|
||
extension_settings.regex.forEach((script) => {
|
||
// Have to clone here
|
||
const scriptHtml = scriptTemplate.clone();
|
||
scriptHtml.attr('id', uuidv4());
|
||
scriptHtml.find('.regex_script_name').text(script.scriptName);
|
||
scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
|
||
.on('input', function () {
|
||
script.disabled = !!$(this).prop('checked');
|
||
reloadCurrentChat();
|
||
saveSettingsDebounced();
|
||
});
|
||
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'));
|
||
});
|
||
scriptHtml.find('.export_regex').on('click', async function () {
|
||
const fileName = `${script.scriptName.replace(/[^a-z0-9]/gi, '_').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 callPopup('Are you sure you want to delete this regex script?', 'confirm');
|
||
|
||
if (!confirm) {
|
||
return;
|
||
}
|
||
|
||
await deleteRegexScript({ existingId: scriptHtml.attr('id') });
|
||
});
|
||
|
||
$('#saved_regex_scripts').append(scriptHtml);
|
||
});
|
||
}
|
||
|
||
async function onRegexEditorOpenClick(existingId) {
|
||
const editorHtml = $(await $.get('scripts/extensions/regex/editor.html'));
|
||
|
||
// If an ID exists, fill in all the values
|
||
let existingScriptIndex = -1;
|
||
if (existingId) {
|
||
const existingScriptName = $(`#${existingId}`).find('.regex_script_name').text();
|
||
existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === existingScriptName);
|
||
if (existingScriptIndex !== -1) {
|
||
const existingScript = extension_settings.regex[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('input[name="substitute_regex"]').prop('checked', existingScript.substituteRegex ?? false);
|
||
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 = {
|
||
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: editorHtml.find('input[name="substitute_regex"]').prop('checked'),
|
||
};
|
||
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: 'Save' });
|
||
if (popupResult) {
|
||
const newRegexScript = {
|
||
scriptName: editorHtml.find('.regex_script_name').val(),
|
||
findRegex: editorHtml.find('.find_regex').val(),
|
||
replaceString: 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: editorHtml.find('input[name="substitute_regex"]').prop('checked'),
|
||
minDepth: parseInt(String(editorHtml.find('input[name="min_depth"]').val())),
|
||
maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
|
||
};
|
||
|
||
saveRegexScript(newRegexScript, existingScriptIndex);
|
||
}
|
||
}
|
||
|
||
// 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.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 (performSave) {
|
||
saveSettingsDebounced();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* /regex slash command callback
|
||
* @param {object} 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 = String(resolveVariable(args.name));
|
||
|
||
for (const script of extension_settings.regex) {
|
||
if (String(script.scriptName).toLowerCase() === String(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
|
||
*/
|
||
async function onRegexImportFileChange(file) {
|
||
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.');
|
||
}
|
||
|
||
extension_settings.regex.push(regexScript);
|
||
|
||
saveSettingsDebounced();
|
||
await loadRegexScripts();
|
||
toastr.success(`Regex script "${regexScript.scriptName}" imported.`);
|
||
} catch (error) {
|
||
console.log(error);
|
||
toastr.error('Invalid JSON file.');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 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 $.get('scripts/extensions/regex/dropdown.html');
|
||
$('#extensions_settings2').append(settingsHtml);
|
||
$('#open_regex_editor').on('click', function () {
|
||
onRegexEditorOpenClick(false);
|
||
});
|
||
$('#import_regex_file').on('change', async function () {
|
||
const inputElement = this instanceof HTMLInputElement && this;
|
||
await onRegexImportFileChange(inputElement.files[0]);
|
||
inputElement.value = '';
|
||
});
|
||
$('#import_regex').on('click', function () {
|
||
$('#import_regex_file').trigger('click');
|
||
});
|
||
|
||
$('#saved_regex_scripts').sortable({
|
||
delay: getSortableDelay(),
|
||
stop: function () {
|
||
let newScripts = [];
|
||
$('#saved_regex_scripts').children().each(function () {
|
||
const scriptName = $(this).find('.regex_script_name').text();
|
||
const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
|
||
if (existingScript) {
|
||
newScripts.push(existingScript);
|
||
}
|
||
});
|
||
|
||
extension_settings.regex = newScripts;
|
||
saveSettingsDebounced();
|
||
|
||
console.debug('Regex scripts reordered');
|
||
// TODO: Maybe reload regex scripts after move
|
||
},
|
||
});
|
||
|
||
await loadRegexScripts();
|
||
$('#saved_regex_scripts').sortable('enable');
|
||
|
||
registerSlashCommand('regex', runRegexCallback, [], '(name=scriptName [input]) – runs a Regex extension script by name on the provided string. The script must be enabled.', true, true);
|
||
});
|