|
|
import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js'; |
|
|
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; |
|
|
import { selected_group } from '../../group-chats.js'; |
|
|
import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
|
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
|
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
|
|
import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
|
|
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
|
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
|
|
import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js'; |
|
|
import { allowPresetScripts, allowScopedScripts, disallowPresetScripts, disallowScopedScripts, getCurrentPresetAPI, getCurrentPresetName, getRegexScripts, getScriptsByType, isPresetScriptsAllowed, isScopedScriptsAllowed, regex_placement, runRegexScript, saveScriptsByType, SCRIPT_TYPE_UNKNOWN, SCRIPT_TYPES, substitute_find_regex } from './engine.js'; |
|
|
import { t } from '../../i18n.js'; |
|
|
import { accountStorage } from '../../util/AccountStorage.js'; |
|
|
import { getPresetManager } from '../../preset-manager.js'; |
|
|
|
|
|
|
|
|
export { getRegexScripts }; |
|
|
|
|
|
const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RegexPresetManager { |
|
|
|
|
|
presetSelect = null; |
|
|
|
|
|
|
|
|
presetCreateButton = null; |
|
|
|
|
|
|
|
|
presetUpdateButton = null; |
|
|
|
|
|
|
|
|
presetApplyButton = null; |
|
|
|
|
|
|
|
|
presetDeleteButton = null; |
|
|
|
|
|
|
|
|
currentPresetId = null; |
|
|
|
|
|
|
|
|
lastKnownState = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
captureCurrentState() { |
|
|
const globalScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)); |
|
|
const scopedScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)); |
|
|
const presetScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)); |
|
|
|
|
|
return { |
|
|
global: globalScripts.map(item => item.id).sort(), |
|
|
scoped: scopedScripts.map(item => item.id).sort(), |
|
|
preset: presetScripts.map(item => item.id).sort(), |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hasStateChanged(state1, state2) { |
|
|
if (!state1 || !state2) return false; |
|
|
|
|
|
const global1 = state1.global || []; |
|
|
const global2 = state2.global || []; |
|
|
const scoped1 = state1.scoped || []; |
|
|
const scoped2 = state2.scoped || []; |
|
|
const preset1 = state1.preset || []; |
|
|
const preset2 = state2.preset || []; |
|
|
|
|
|
if (global1.length !== global2.length || scoped1.length !== scoped2.length) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
return !global1.every(id => global2.includes(id)) || |
|
|
!scoped1.every(id => scoped2.includes(id)) || |
|
|
!preset1.every(id => preset2.includes(id)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateStoredState(presetId) { |
|
|
this.currentPresetId = presetId; |
|
|
this.lastKnownState = this.captureCurrentState(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async checkUnsavedChanges() { |
|
|
if (!this.currentPresetId || !this.lastKnownState) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
const currentState = this.captureCurrentState(); |
|
|
if (!this.hasStateChanged(this.lastKnownState, currentState)) { |
|
|
return true; |
|
|
} |
|
|
|
|
|
const currentPreset = extension_settings.regex_presets.find(p => p.id === this.currentPresetId); |
|
|
const presetName = currentPreset ? currentPreset.name : t`Unknown Preset`; |
|
|
|
|
|
const choice = await Popup.show.confirm( |
|
|
t`You have unsaved changes to the "${presetName}" preset.`, |
|
|
t`Do you want to save them before switching?`, |
|
|
{ |
|
|
okButton: t`Save Changes`, |
|
|
cancelButton: t`Discard Changes`, |
|
|
}, |
|
|
); |
|
|
|
|
|
if (choice) { |
|
|
|
|
|
await this.savePreset(this.currentPresetId, true); |
|
|
this.renderPresetList(); |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setupEventListeners() { |
|
|
this.presetSelect = (document.getElementById('regex_presets')); |
|
|
if (!this.presetSelect) { |
|
|
console.error('RegexPresetManager: Could not find preset select element in the DOM.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.presetSelect.addEventListener('change', async (event) => { |
|
|
const selectedPresetId = this.presetSelect.value; |
|
|
const fromSlashCommand = event instanceof CustomEvent && event?.detail?.fromSlashCommand === true; |
|
|
|
|
|
|
|
|
if (!fromSlashCommand) { |
|
|
const canProceed = await this.checkUnsavedChanges(); |
|
|
if (!canProceed) { |
|
|
|
|
|
event.preventDefault(); |
|
|
const currentPreset = extension_settings.regex_presets.find(p => p.id === this.currentPresetId); |
|
|
if (currentPreset) { |
|
|
this.presetSelect.value = currentPreset.id; |
|
|
} |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
await this.applyPreset(selectedPresetId); |
|
|
extension_settings.regex_presets.forEach(p => { p.isSelected = p.id === selectedPresetId; }); |
|
|
saveSettingsDebounced(); |
|
|
this.updateStoredState(selectedPresetId); |
|
|
}); |
|
|
|
|
|
this.presetCreateButton = document.getElementById('regex_preset_create'); |
|
|
if (!this.presetCreateButton) { |
|
|
console.error('RegexPresetManager: Could not find preset create button in the DOM.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.presetCreateButton.addEventListener('click', async () => { |
|
|
const newId = uuidv4(); |
|
|
await this.savePreset(newId, false); |
|
|
this.renderPresetList(); |
|
|
this.updateStoredState(newId); |
|
|
}); |
|
|
|
|
|
this.presetUpdateButton = document.getElementById('regex_preset_update'); |
|
|
if (!this.presetUpdateButton) { |
|
|
console.error('RegexPresetManager: Could not find preset update button in the DOM.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.presetUpdateButton.addEventListener('click', async () => { |
|
|
const selectedPresetId = this.presetSelect.value; |
|
|
await this.savePreset(selectedPresetId, true); |
|
|
this.renderPresetList(); |
|
|
this.updateStoredState(selectedPresetId); |
|
|
}); |
|
|
|
|
|
this.presetApplyButton = document.getElementById('regex_preset_apply'); |
|
|
if (!this.presetApplyButton) { |
|
|
console.error('RegexPresetManager: Could not find preset apply button in the DOM.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.presetApplyButton.addEventListener('click', async () => { |
|
|
const selectedPresetId = this.presetSelect.value; |
|
|
await this.applyPreset(selectedPresetId); |
|
|
this.updateStoredState(selectedPresetId); |
|
|
}); |
|
|
|
|
|
this.presetDeleteButton = document.getElementById('regex_preset_delete'); |
|
|
if (!this.presetDeleteButton) { |
|
|
console.error('RegexPresetManager: Could not find preset delete button in the DOM.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.presetDeleteButton.addEventListener('click', async () => { |
|
|
const selectedPresetId = this.presetSelect.value; |
|
|
await this.deletePreset(selectedPresetId); |
|
|
this.renderPresetList(); |
|
|
|
|
|
const newSelectedPresetId = extension_settings.regex_presets.find(p => p.isSelected)?.id; |
|
|
if (newSelectedPresetId) { |
|
|
await this.applyPreset(newSelectedPresetId); |
|
|
this.presetSelect.value = newSelectedPresetId; |
|
|
this.updateStoredState(newSelectedPresetId); |
|
|
} else { |
|
|
this.currentPresetId = null; |
|
|
this.lastKnownState = null; |
|
|
} |
|
|
}); |
|
|
|
|
|
this.renderPresetList(); |
|
|
|
|
|
|
|
|
const selectedPreset = extension_settings.regex_presets?.find(p => p.isSelected); |
|
|
if (selectedPreset) { |
|
|
this.updateStoredState(selectedPreset.id); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
registerSlashCommands() { |
|
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
|
|
name: 'regex-preset', |
|
|
helpString: t`Selects a regex preset by name or ID. Gets the current regex preset ID if no argument is provided.`, |
|
|
callback: (args, name) => { |
|
|
if (!this.presetSelect) { |
|
|
return ''; |
|
|
} |
|
|
|
|
|
name = String(name ?? '').trim(); |
|
|
|
|
|
if (name) { |
|
|
const quiet = isTrueBoolean(args?.quiet?.toString()); |
|
|
const foundId = extension_settings.regex_presets.find(p => equalsIgnoreCaseAndAccents(p.id, name) || equalsIgnoreCaseAndAccents(p.name, name))?.id; |
|
|
|
|
|
if (foundId) { |
|
|
this.presetSelect.value = foundId; |
|
|
this.presetSelect.dispatchEvent(new CustomEvent('change', { detail: { fromSlashCommand: true } })); |
|
|
return foundId; |
|
|
} |
|
|
|
|
|
!quiet && toastr.warning(`Regex preset "${name}" not found`); |
|
|
return ''; |
|
|
} |
|
|
|
|
|
return this.presetSelect.value; |
|
|
}, |
|
|
returns: 'current preset ID', |
|
|
namedArgumentList: [ |
|
|
SlashCommandNamedArgument.fromProps({ |
|
|
name: 'quiet', |
|
|
description: 'Suppress the toast message on preset change', |
|
|
typeList: [ARGUMENT_TYPE.BOOLEAN], |
|
|
defaultValue: 'false', |
|
|
enumList: commonEnumProviders.boolean('trueFalse')(), |
|
|
}), |
|
|
], |
|
|
unnamedArgumentList: [ |
|
|
SlashCommandArgument.fromProps({ |
|
|
description: 'regex preset name or ID', |
|
|
typeList: [ARGUMENT_TYPE.STRING], |
|
|
enumProvider: () => extension_settings.regex_presets.map(x => new SlashCommandEnumValue(x.id, x.name, enumTypes.enum, enumIcons.preset)), |
|
|
}), |
|
|
], |
|
|
})); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderPresetList() { |
|
|
if (!this.presetSelect) { |
|
|
return; |
|
|
} |
|
|
|
|
|
this.presetSelect.innerHTML = ''; |
|
|
|
|
|
if (!Array.isArray(extension_settings.regex_presets) || extension_settings.regex_presets.length === 0) { |
|
|
const fallbackOption = new Option(t`[No presets saved]`, '', true, true); |
|
|
this.presetSelect.appendChild(fallbackOption); |
|
|
this.presetSelect.disabled = true; |
|
|
return; |
|
|
} |
|
|
|
|
|
extension_settings.regex_presets.forEach(preset => { |
|
|
const option = new Option(preset.name, preset.id, preset.isSelected, preset.isSelected); |
|
|
this.presetSelect.appendChild(option); |
|
|
}); |
|
|
|
|
|
this.presetSelect.disabled = false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async applyPresetList({ presetList, targetList, saveFunction }) { |
|
|
if (!Array.isArray(targetList) || !Array.isArray(presetList)) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
targetList.forEach((script => { |
|
|
script.disabled = !presetList.some(p => p.id === script.id); |
|
|
})); |
|
|
|
|
|
|
|
|
targetList.sort((a, b) => { |
|
|
const aIndex = presetList.findIndex(p => p.id === a.id); |
|
|
const bIndex = presetList.findIndex(p => p.id === b.id); |
|
|
return aIndex - bIndex || targetList.indexOf(a) - targetList.indexOf(b); |
|
|
}); |
|
|
|
|
|
await saveFunction(targetList); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async applyPreset(presetId) { |
|
|
const preset = extension_settings.regex_presets.find(p => p.id === presetId); |
|
|
if (!preset) { |
|
|
toastr.error(t`Could not find the selected preset.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
for (const scriptType of Object.values(SCRIPT_TYPES)) { |
|
|
await this.applyPresetList({ |
|
|
presetList: { |
|
|
[SCRIPT_TYPES.GLOBAL]: preset.global, |
|
|
[SCRIPT_TYPES.SCOPED]: preset.scoped, |
|
|
[SCRIPT_TYPES.PRESET]: preset.preset, |
|
|
}[scriptType], |
|
|
targetList: getScriptsByType(scriptType), |
|
|
saveFunction: scripts => saveScriptsByType(scripts, scriptType), |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
await loadRegexScripts(); |
|
|
|
|
|
await reloadCurrentChat(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
regexListToPresetItems(list) { |
|
|
if (!Array.isArray(list)) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
return list.filter(x => !x.disabled).map(s => ({ id: s.id })); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async savePreset(presetId, isUpdate) { |
|
|
const existingPreset = isUpdate ? extension_settings.regex_presets.find(p => p.id === presetId) : null; |
|
|
|
|
|
if (isUpdate && !existingPreset) { |
|
|
toastr.error(t`Could not find the preset to update.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
const name = isUpdate ? existingPreset.name : await Popup.show.input(t`Enter a name for the new regex preset:`, ''); |
|
|
const id = isUpdate ? existingPreset.id : presetId; |
|
|
|
|
|
if (!name || !name.trim().length) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const preset = { |
|
|
id: id, |
|
|
name: name, |
|
|
isSelected: false, |
|
|
global: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)), |
|
|
scoped: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)), |
|
|
preset: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)), |
|
|
}; |
|
|
|
|
|
if (isUpdate) { |
|
|
Object.assign(existingPreset, preset); |
|
|
} else { |
|
|
extension_settings.regex_presets.push(preset); |
|
|
} |
|
|
|
|
|
extension_settings.regex_presets.forEach(p => { p.isSelected = p.id === id; }); |
|
|
saveSettingsDebounced(); |
|
|
|
|
|
toastr.success(isUpdate ? t`Regex preset updated` : t`Regex preset saved`); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async deletePreset(presetId) { |
|
|
const presetIndex = extension_settings.regex_presets.findIndex(p => p.id === presetId); |
|
|
if (presetIndex === -1) { |
|
|
toastr.error(t`Could not find the preset to delete.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
const presetName = extension_settings.regex_presets[presetIndex].name; |
|
|
const confirm = await Popup.show.confirm(t`Are you sure you want to delete this regex preset?`, presetName); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
|
|
|
extension_settings.regex_presets.splice(presetIndex, 1); |
|
|
|
|
|
|
|
|
extension_settings.regex_presets.forEach((p, i) => { p.isSelected = i === 0; }); |
|
|
saveSettingsDebounced(); |
|
|
|
|
|
toastr.success(t`Regex preset deleted`); |
|
|
} |
|
|
} |
|
|
|
|
|
const presetManager = new RegexPresetManager(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setToggleAllIcon(allAreChecked) { |
|
|
const selectAllIcon = $('#bulk_select_all_toggle').find('i'); |
|
|
selectAllIcon.toggleClass('fa-check-double', !allAreChecked); |
|
|
selectAllIcon.toggleClass('fa-minus', allAreChecked); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setMoveButtonsVisibility() { |
|
|
const hasGlobalScripts = $('#saved_regex_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; |
|
|
const hasScopedScripts = $('#saved_scoped_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; |
|
|
const hasPresetScripts = $('#saved_preset_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; |
|
|
$('#bulk_regex_move_to_global').toggle(hasScopedScripts || hasPresetScripts); |
|
|
$('#bulk_regex_move_to_scoped').toggle(hasGlobalScripts || hasPresetScripts); |
|
|
$('#bulk_regex_move_to_preset').toggle(hasGlobalScripts || hasScopedScripts); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function saveRegexScript(regexScript, existingScriptIndex, scriptType, saveSettings = true) { |
|
|
|
|
|
const array = getScriptsByType(scriptType); |
|
|
|
|
|
|
|
|
if (!regexScript.id) { |
|
|
regexScript.id = uuidv4(); |
|
|
} |
|
|
|
|
|
|
|
|
if (!regexScript.scriptName) { |
|
|
toastr.error(t`Could not save regex script: The script name was undefined or empty!`); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (regexScript.findRegex.length === 0) { |
|
|
toastr.warning(t`This regex script will not work, but was saved anyway: A find regex isn't present.`); |
|
|
} |
|
|
|
|
|
|
|
|
if (regexScript.placement.length === 0) { |
|
|
toastr.warning(t`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 (scriptType === SCRIPT_TYPES.SCOPED) { |
|
|
await saveScriptsByType(array, SCRIPT_TYPES.SCOPED); |
|
|
allowScopedScripts(characters?.[this_chid]); |
|
|
} |
|
|
|
|
|
if (scriptType === SCRIPT_TYPES.PRESET) { |
|
|
await saveScriptsByType(array, SCRIPT_TYPES.PRESET); |
|
|
allowPresetScripts(getCurrentPresetAPI(), getCurrentPresetName()); |
|
|
} |
|
|
|
|
|
if (saveSettings) { |
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
|
|
|
|
|
|
const currentChatId = getCurrentChatId(); |
|
|
if (currentChatId) { |
|
|
await reloadCurrentChat(); |
|
|
} |
|
|
} |
|
|
|
|
|
const debuggerPopup = $('#regex_debugger_popup'); |
|
|
if (debuggerPopup.length) { |
|
|
populateDebuggerRuleList(debuggerPopup.parent()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function deleteRegexScript(id, scriptType, saveSettings = true) { |
|
|
const array = getScriptsByType(scriptType); |
|
|
|
|
|
const existingScriptIndex = array.findIndex(script => script.id === id); |
|
|
if (existingScriptIndex !== -1) { |
|
|
array.splice(existingScriptIndex, 1); |
|
|
|
|
|
switch (scriptType) { |
|
|
case SCRIPT_TYPES.GLOBAL: |
|
|
|
|
|
break; |
|
|
case SCRIPT_TYPES.SCOPED: |
|
|
await saveScriptsByType(array, SCRIPT_TYPES.SCOPED); |
|
|
break; |
|
|
case SCRIPT_TYPES.PRESET: |
|
|
await saveScriptsByType(array, SCRIPT_TYPES.PRESET); |
|
|
break; |
|
|
default: |
|
|
break; |
|
|
} |
|
|
if (saveSettings) { |
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function moveRegexScript(script, toType, fromType = null, saveSettings = true) { |
|
|
if (!Object.values(SCRIPT_TYPES).includes(toType)) { |
|
|
console.warn(`moveRegexScript: Invalid target script type ${toType}`); |
|
|
return; |
|
|
} |
|
|
if (!Object.values(SCRIPT_TYPES).includes(fromType)) { |
|
|
fromType = getScriptType(script); |
|
|
} |
|
|
if (fromType === toType || fromType === SCRIPT_TYPE_UNKNOWN || toType === SCRIPT_TYPE_UNKNOWN) { |
|
|
return; |
|
|
} |
|
|
await deleteRegexScript(script.id, fromType, false); |
|
|
await saveRegexScript(script, -1, toType, saveSettings); |
|
|
} |
|
|
|
|
|
async function loadRegexScripts() { |
|
|
$('#saved_regex_scripts').empty(); |
|
|
$('#saved_scoped_scripts').empty(); |
|
|
$('#saved_preset_scripts').empty(); |
|
|
setToggleAllIcon(false); |
|
|
|
|
|
const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate')); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderScript(container, script, scriptType, index) { |
|
|
|
|
|
const scriptHtml = scriptTemplate.clone(); |
|
|
const save = () => saveRegexScript(script, index, scriptType); |
|
|
|
|
|
if (!script.id) { |
|
|
script.id = uuidv4(); |
|
|
} |
|
|
|
|
|
scriptHtml.attr('id', script.id); |
|
|
scriptHtml.find('.regex_script_name').text(script.scriptName).attr('title', 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'), scriptType); |
|
|
}); |
|
|
scriptHtml.find('.move_to_global').on('click', async function () { |
|
|
const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM); |
|
|
|
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await moveRegexScript(script, SCRIPT_TYPES.GLOBAL, scriptType); |
|
|
}); |
|
|
scriptHtml.find('.move_to_scoped').on('click', async function () { |
|
|
if (this_chid === undefined) { |
|
|
toastr.error(t`No character selected.`); |
|
|
return; |
|
|
} |
|
|
if (selected_group) { |
|
|
toastr.error(t`Cannot edit scoped scripts in group chats.`); |
|
|
return; |
|
|
} |
|
|
const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType); |
|
|
}); |
|
|
scriptHtml.find('.move_to_preset').on('click', async function () { |
|
|
const confirm = await callGenericPopup( |
|
|
t`Are you sure you want to move this regex script to preset?`, |
|
|
POPUP_TYPE.CONFIRM, |
|
|
); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await moveRegexScript(script, SCRIPT_TYPES.PRESET, scriptType); |
|
|
}); |
|
|
scriptHtml.find('.export_regex').on('click', async function () { |
|
|
const fileName = `regex-${sanitizeFileName(script.scriptName)}.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(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await deleteRegexScript(script.id, scriptType); |
|
|
await reloadCurrentChat(); |
|
|
}); |
|
|
scriptHtml.find('.regex_bulk_checkbox').on('change', function () { |
|
|
setMoveButtonsVisibility(); |
|
|
const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
|
|
const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; |
|
|
setToggleAllIcon(allAreChecked); |
|
|
}); |
|
|
scriptHtml.find('input[name="regex_expand"]').on('change', function () { |
|
|
if (!(this instanceof HTMLInputElement)) { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!this.checked) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const closeMenuHandler = (e) => { |
|
|
if (e.target instanceof HTMLElement) { |
|
|
if (e.target.closest('.regex-script-label')) { |
|
|
return; |
|
|
} |
|
|
this.checked = false; |
|
|
document.removeEventListener('click', closeMenuHandler); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
document.addEventListener('click', closeMenuHandler, { passive: true, once: false }); |
|
|
}, 0); |
|
|
}); |
|
|
|
|
|
$(container).append(scriptHtml); |
|
|
} |
|
|
|
|
|
getScriptsByType(SCRIPT_TYPES.GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, SCRIPT_TYPES.GLOBAL, index)); |
|
|
getScriptsByType(SCRIPT_TYPES.SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, SCRIPT_TYPES.SCOPED, index)); |
|
|
getScriptsByType(SCRIPT_TYPES.PRESET).forEach((script, index) => renderScript('#saved_preset_scripts', script, SCRIPT_TYPES.PRESET, index)); |
|
|
|
|
|
$('#regex_scoped_toggle').prop('checked', isScopedScriptsAllowed(characters?.[this_chid])); |
|
|
$('#regex_preset_toggle').prop('checked', isPresetScriptsAllowed(getCurrentPresetAPI(), getCurrentPresetName())); |
|
|
|
|
|
setMoveButtonsVisibility(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function onRegexEditorOpenClick(existingId, scriptType) { |
|
|
const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor')); |
|
|
const array = getScriptsByType(scriptType); |
|
|
|
|
|
|
|
|
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() { |
|
|
updateInfoBlock(editorHtml); |
|
|
|
|
|
if (!editorHtml.find('#regex_test_mode').is(':visible')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const testScript = { |
|
|
id: uuidv4(), |
|
|
scriptName: editorHtml.find('.regex_script_name').val().toString(), |
|
|
findRegex: editorHtml.find('.find_regex').val().toString(), |
|
|
replaceString: editorHtml.find('.regex_replace_string').val().toString(), |
|
|
trimStrings: String(editorHtml.find('.regex_trim_strings').val()).split('\n').filter((e) => e.length !== 0) || [], |
|
|
substituteRegex: Number(editorHtml.find('select[name="substitute_regex"]').val()), |
|
|
disabled: false, |
|
|
promptOnly: false, |
|
|
markdownOnly: false, |
|
|
runOnEdit: false, |
|
|
minDepth: null, |
|
|
maxDepth: null, |
|
|
placement: null, |
|
|
}; |
|
|
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); |
|
|
updateInfoBlock(editorHtml); |
|
|
|
|
|
const popupResult = await callGenericPopup(editorHtml, POPUP_TYPE.CONFIRM, '', { okButton: t`Save`, cancelButton: t`Cancel`, allowVerticalScrolling: true }); |
|
|
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: String(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().toString()); }) |
|
|
.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, scriptType); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildReplacementHtml(match, pattern) { |
|
|
const container = document.createDocumentFragment(); |
|
|
let lastIndex = 0; |
|
|
const backrefRegex = /\$\$|\$&|\$`|\$'|\$(\d{1,2})/g; |
|
|
|
|
|
let reMatch; |
|
|
while ((reMatch = backrefRegex.exec(pattern)) !== null) { |
|
|
|
|
|
const literalPart = pattern.substring(lastIndex, reMatch.index); |
|
|
if (literalPart) { |
|
|
const mark = document.createElement('mark'); |
|
|
mark.className = 'green_hl'; |
|
|
mark.innerText = literalPart; |
|
|
container.appendChild(mark); |
|
|
} |
|
|
|
|
|
const backref = reMatch[0]; |
|
|
if (backref === '$$') { |
|
|
container.appendChild(document.createTextNode('$')); |
|
|
} else if (backref === '$&') { |
|
|
const mark = document.createElement('mark'); |
|
|
mark.className = 'yellow_hl'; |
|
|
mark.innerText = match[0]; |
|
|
container.appendChild(mark); |
|
|
} else if (backref === '$`') { |
|
|
container.appendChild(document.createTextNode(match.input.substring(0, match.index))); |
|
|
} else if (backref === '$\'') { |
|
|
container.appendChild(document.createTextNode(match.input.substring(match.index + match[0].length))); |
|
|
} else { |
|
|
const groupIndex = parseInt(reMatch[1], 10); |
|
|
if (groupIndex > 0 && groupIndex < match.length && match[groupIndex] !== undefined) { |
|
|
const mark = document.createElement('mark'); |
|
|
mark.className = 'yellow_hl'; |
|
|
mark.innerText = match[groupIndex]; |
|
|
container.appendChild(mark); |
|
|
} else { |
|
|
|
|
|
const mark = document.createElement('mark'); |
|
|
mark.className = 'green_hl'; |
|
|
mark.innerText = backref; |
|
|
container.appendChild(mark); |
|
|
} |
|
|
} |
|
|
lastIndex = backrefRegex.lastIndex; |
|
|
} |
|
|
|
|
|
|
|
|
const finalLiteralPart = pattern.substring(lastIndex); |
|
|
if (finalLiteralPart) { |
|
|
const mark = document.createElement('mark'); |
|
|
mark.className = 'green_hl'; |
|
|
mark.innerText = finalLiteralPart; |
|
|
container.appendChild(mark); |
|
|
} |
|
|
|
|
|
|
|
|
const tempDiv = document.createElement('div'); |
|
|
tempDiv.appendChild(container); |
|
|
return tempDiv.innerHTML; |
|
|
} |
|
|
|
|
|
function executeRegexScriptForDebugging(script, text) { |
|
|
let err; |
|
|
let originalRegex; |
|
|
|
|
|
try { |
|
|
originalRegex = regexFromString(script.findRegex); |
|
|
if (!originalRegex) throw new Error('Invalid regex string'); |
|
|
} catch (e) { |
|
|
err = `Compile error: ${e.message}`; |
|
|
return { output: text, highlightedOutput: text, error: err, charsCaptured: 0, charsAdded: 0, charsRemoved: 0 }; |
|
|
} |
|
|
|
|
|
const globalRegex = new RegExp(originalRegex.source, originalRegex.flags.includes('g') ? originalRegex.flags : originalRegex.flags + 'g'); |
|
|
const matches = [...text.matchAll(globalRegex)]; |
|
|
|
|
|
if (matches.length === 0) { |
|
|
return { output: text, highlightedOutput: escapeHtml(text), error: null, charsCaptured: 0, charsAdded: 0, charsRemoved: 0 }; |
|
|
} |
|
|
|
|
|
let outputText = ''; |
|
|
let highlightedOutput = ''; |
|
|
let lastIndex = 0; |
|
|
let totalCharsCaptured = 0; |
|
|
let totalCharsAdded = 0; |
|
|
let totalCharsRemoved = 0; |
|
|
|
|
|
try { |
|
|
for (const match of matches) { |
|
|
const originalMatchText = match[0]; |
|
|
totalCharsCaptured += originalMatchText.length; |
|
|
|
|
|
|
|
|
const precedingText = text.substring(lastIndex, match.index); |
|
|
outputText += precedingText; |
|
|
highlightedOutput += escapeHtml(precedingText); |
|
|
|
|
|
|
|
|
let charsAddedInMatch = 0; |
|
|
let charsKeptFromMatch = 0; |
|
|
const backrefRegex = /\$\$|\$&|\$`|\$'|\$(\d{1,2})/g; |
|
|
let lastPatternIndex = 0; |
|
|
let reMatch; |
|
|
let replacementForPlainText = ''; |
|
|
|
|
|
|
|
|
while ((reMatch = backrefRegex.exec(script.replaceString)) !== null) { |
|
|
const literalPart = script.replaceString.substring(lastPatternIndex, reMatch.index); |
|
|
charsAddedInMatch += literalPart.length; |
|
|
replacementForPlainText += literalPart; |
|
|
const backref = reMatch[0]; |
|
|
if (backref === '$$') { |
|
|
replacementForPlainText += '$'; |
|
|
} else if (backref === '$&') { |
|
|
charsKeptFromMatch += (match[0] || '').length; replacementForPlainText += (match[0] || ''); |
|
|
} else if (backref === '$`') { |
|
|
const part = match.input.substring(0, match.index); charsKeptFromMatch += part.length; replacementForPlainText += part; |
|
|
} else if (backref === '$\'') { |
|
|
const part = match.input.substring(match.index + match[0].length); charsKeptFromMatch += part.length; replacementForPlainText += part; |
|
|
} else { |
|
|
const groupIndex = parseInt(reMatch[1], 10); |
|
|
if (groupIndex > 0 && groupIndex < match.length && match[groupIndex] !== undefined) { |
|
|
charsKeptFromMatch += match[groupIndex].length; |
|
|
replacementForPlainText += match[groupIndex]; |
|
|
} |
|
|
} |
|
|
lastPatternIndex = backrefRegex.lastIndex; |
|
|
} |
|
|
const finalLiteralPart = script.replaceString.substring(lastPatternIndex); |
|
|
charsAddedInMatch += finalLiteralPart.length; |
|
|
replacementForPlainText += finalLiteralPart; |
|
|
|
|
|
totalCharsAdded += charsAddedInMatch; |
|
|
totalCharsRemoved += (originalMatchText.length - charsKeptFromMatch); |
|
|
|
|
|
outputText += replacementForPlainText; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
highlightedOutput += `<mark class='red_hl'>${escapeHtml(originalMatchText)}</mark>`; |
|
|
|
|
|
highlightedOutput += ' → '; |
|
|
|
|
|
highlightedOutput += buildReplacementHtml(match, script.replaceString); |
|
|
|
|
|
lastIndex = match.index + originalMatchText.length; |
|
|
} |
|
|
|
|
|
|
|
|
const trailingText = text.substring(lastIndex); |
|
|
outputText += trailingText; |
|
|
highlightedOutput += escapeHtml(trailingText); |
|
|
|
|
|
} catch (e) { |
|
|
err = (err ? err + '; ' : '') + `Replace error: ${e.message}`; |
|
|
outputText = text; |
|
|
highlightedOutput = escapeHtml(text); |
|
|
} |
|
|
|
|
|
return { |
|
|
output: outputText, |
|
|
highlightedOutput: highlightedOutput, |
|
|
error: err, |
|
|
charsCaptured: totalCharsCaptured, |
|
|
charsAdded: totalCharsAdded, |
|
|
charsRemoved: totalCharsRemoved, |
|
|
}; |
|
|
} |
|
|
|
|
|
function populateDebuggerRuleList(container) { |
|
|
const rulesContainer = container.find('#regex_debugger_rules'); |
|
|
const ruleTemplate = container.find('#regex_debugger_rule_template'); |
|
|
if (!rulesContainer.length || !ruleTemplate.length) { |
|
|
console.error('Regex Debugger: Could not find rule list or template in the DOM.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
rulesContainer.empty(); |
|
|
|
|
|
const allScripts = getRegexScripts(); |
|
|
if (!allScripts || allScripts.length === 0) { |
|
|
rulesContainer.append('<div class="regex-debugger-no-rules">' + t`No regex rules found.` + '</div>'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const globalScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.GLOBAL).map(s => s.id)); |
|
|
const scopedScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.SCOPED).map(s => s.id)); |
|
|
const presetScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.PRESET).map(s => s.id)); |
|
|
const globalScripts = []; |
|
|
const scopedScripts = []; |
|
|
const presetScripts = []; |
|
|
|
|
|
allScripts.forEach(script => { |
|
|
const scriptCopy = structuredClone(script); |
|
|
if (globalScriptIds.has(script.id)) { |
|
|
|
|
|
scriptCopy.type = SCRIPT_TYPES.GLOBAL; |
|
|
globalScripts.push(scriptCopy); |
|
|
} else if (scopedScriptIds.has(script.id)) { |
|
|
|
|
|
scriptCopy.type = SCRIPT_TYPES.SCOPED; |
|
|
scopedScripts.push(scriptCopy); |
|
|
} else if (presetScriptIds.has(script.id)) { |
|
|
|
|
|
scriptCopy.type = SCRIPT_TYPES.PRESET; |
|
|
presetScripts.push(scriptCopy); |
|
|
} |
|
|
}); |
|
|
|
|
|
container.data('allScripts', [...globalScripts, ...presetScripts, ...scopedScripts]); |
|
|
|
|
|
const renderRule = (script) => { |
|
|
if (!script.id) script.id = uuidv4(); |
|
|
const ruleElementContent = $(ruleTemplate.prop('content')).clone(); |
|
|
const ruleElement = ruleElementContent.find('.regex-debugger-rule'); |
|
|
|
|
|
ruleElement.attr('data-id', script.id); |
|
|
|
|
|
ruleElement.find('.rule-name').text(script.scriptName); |
|
|
ruleElement.find('.rule-regex').text(script.findRegex); |
|
|
|
|
|
ruleElement |
|
|
.find('.rule-scope') |
|
|
.text( |
|
|
{ |
|
|
[SCRIPT_TYPES.SCOPED]: t`Scoped`, |
|
|
[SCRIPT_TYPES.GLOBAL]: t`Global`, |
|
|
[SCRIPT_TYPES.PRESET]: t`Preset`, |
|
|
}[script.type], |
|
|
); |
|
|
ruleElement.find('.rule-enabled').prop('checked', !script.disabled); |
|
|
|
|
|
ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.type)); |
|
|
|
|
|
ruleElement.on('click', function (event) { |
|
|
if ($(event.target).is('input, .menu_button, .menu_button i')) { |
|
|
return; |
|
|
} |
|
|
const scriptId = $(this).data('id'); |
|
|
const stepElement = $(`#step-result-${scriptId}`); |
|
|
const container = $('#regex_debugger_steps_output'); |
|
|
|
|
|
if (stepElement.length && container.length) { |
|
|
|
|
|
const targetTop = stepElement.position().top; |
|
|
const containerScrollTop = container.scrollTop(); |
|
|
const containerHeight = container.height(); |
|
|
|
|
|
|
|
|
let scrollTo = containerScrollTop + targetTop - (containerHeight / 2) + (stepElement.height() / 2); |
|
|
|
|
|
container.animate({ scrollTop: scrollTo }, 300); |
|
|
|
|
|
stepElement.css('transition', 'background-color 0.5s').css('background-color', 'var(--highlight_color)'); |
|
|
setTimeout(() => stepElement.css('background-color', ''), 1000); |
|
|
} |
|
|
}); |
|
|
|
|
|
return ruleElementContent; |
|
|
}; |
|
|
|
|
|
if (globalScripts.length > 0) { |
|
|
rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Global Rules` + '</div>'); |
|
|
const globalList = $('<ul id="regex_debugger_rules_global" class="sortable-list"></ul>'); |
|
|
globalScripts.forEach(script => globalList.append(renderRule(script))); |
|
|
rulesContainer.append(globalList); |
|
|
} |
|
|
|
|
|
if (presetScripts.length > 0) { |
|
|
rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Preset Rules` + '</div>'); |
|
|
const presetList = $('<ul id="regex_debugger_rules_preset" class="sortable-list"></ul>'); |
|
|
presetScripts.forEach(script => presetList.append(renderRule(script))); |
|
|
rulesContainer.append(presetList); |
|
|
} |
|
|
|
|
|
if (scopedScripts.length > 0) { |
|
|
rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Scoped Rules` + '</div>'); |
|
|
const scopedList = $('<ul id="regex_debugger_rules_scoped" class="sortable-list"></ul>'); |
|
|
scopedScripts.forEach(script => scopedList.append(renderRule(script))); |
|
|
rulesContainer.append(scopedList); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function onRegexDebuggerOpenClick() { |
|
|
const templateContent = await renderExtensionTemplateAsync('regex', 'debugger'); |
|
|
const debuggerHtml = $('<div>').html(templateContent); |
|
|
|
|
|
const stepTemplate = debuggerHtml.find('#regex_debugger_step_template'); |
|
|
|
|
|
populateDebuggerRuleList(debuggerHtml); |
|
|
|
|
|
|
|
|
debuggerHtml.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
|
|
|
|
|
debuggerHtml.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); |
|
|
|
|
|
debuggerHtml.find('#regex_debugger_rules_preset').sortable({ delay: getSortableDelay() }).disableSelection(); |
|
|
|
|
|
debuggerHtml.find('#regex_debugger_run_test').on('click', function () { |
|
|
const allScripts = debuggerHtml.data('allScripts'); |
|
|
const orderedRuleIds = [ |
|
|
...$('#regex_debugger_rules_global').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
|
|
...$('#regex_debugger_rules_scoped').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
|
|
...$('#regex_debugger_rules_preset').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
|
|
]; |
|
|
|
|
|
const rawInput = String($('#regex_debugger_raw_input').val()); |
|
|
const stepsOutput = $('#regex_debugger_steps_output'); |
|
|
const finalOutput = $('#regex_debugger_final_output'); |
|
|
|
|
|
if (!stepsOutput.length || !finalOutput.length) return; |
|
|
|
|
|
const displayMode = $('input[name="display_mode"]:checked').val(); |
|
|
stepsOutput.empty(); |
|
|
finalOutput.empty(); |
|
|
$('#regex_debugger_final_summary').remove(); |
|
|
|
|
|
if (!allScripts) return; |
|
|
let textForNextStep = rawInput; |
|
|
let totalCharsCaptured = 0; |
|
|
let totalCharsAdded = 0; |
|
|
let totalCharsRemoved = 0; |
|
|
|
|
|
orderedRuleIds.forEach(scriptId => { |
|
|
const ruleElement = $(`#regex_debugger_rules [data-id="${scriptId}"]`); |
|
|
if (!ruleElement.find('.rule-enabled').is(':checked')) return; |
|
|
|
|
|
const script = allScripts.find(s => s.id === scriptId); |
|
|
|
|
|
if (script) { |
|
|
const result = executeRegexScriptForDebugging(script, textForNextStep); |
|
|
totalCharsCaptured += result.charsCaptured; |
|
|
totalCharsAdded += result.charsAdded; |
|
|
totalCharsRemoved += result.charsRemoved; |
|
|
|
|
|
const stepElement = $(stepTemplate.prop('content')).clone(); |
|
|
|
|
|
stepElement.find('>:first-child').attr('id', `step-result-${script.id}`); |
|
|
const stepHeader = stepElement.find('.step-header'); |
|
|
stepHeader.find('strong').text(t`After:` + ` ${script.scriptName}`); |
|
|
|
|
|
const metricsHtml = '<span class="step-metrics">' + t`Captured:` + ` ${result.charsCaptured}, ` + t`Added:` + ` +${result.charsAdded}, ` + t`Removed:` + ` -${result.charsRemoved}</span>`; |
|
|
stepHeader.append(metricsHtml); |
|
|
|
|
|
if (displayMode === 'highlight') { |
|
|
stepElement.find('.step-output').html(result.highlightedOutput); |
|
|
} else { |
|
|
stepElement.find('.step-output').text(result.output); |
|
|
} |
|
|
|
|
|
if (result.error) { |
|
|
stepHeader.append($(`<div class='warning_text text_rose-500'>${result.error}</div>`)); |
|
|
} |
|
|
|
|
|
stepsOutput.append(stepElement); |
|
|
textForNextStep = result.output; |
|
|
} |
|
|
}); |
|
|
|
|
|
const summaryHtml = ` |
|
|
<div id="regex_debugger_final_summary" class="regex-debugger-summary"> |
|
|
<strong>` + t`Total Captured:` + `</strong> ${totalCharsCaptured} | <strong>` + t`Total Added:` + `</strong> +${totalCharsAdded} | <strong>` + t`Total Removed:` + `</strong> -${totalCharsRemoved} |
|
|
</div> |
|
|
`; |
|
|
finalOutput.before(summaryHtml); |
|
|
|
|
|
const renderMode = $('#regex_debugger_render_mode').val(); |
|
|
if (renderMode === 'message') { |
|
|
const formattedHtml = messageFormatting(textForNextStep, 'Debugger', true, false, null); |
|
|
const messageBlock = $('<div class="mes"><div class="mes_text"></div></div>'); |
|
|
messageBlock.find('.mes_text').html(formattedHtml); |
|
|
finalOutput.append(messageBlock); |
|
|
} else { |
|
|
finalOutput.text(textForNextStep); |
|
|
} |
|
|
}); |
|
|
|
|
|
debuggerHtml.find('#regex_debugger_save_order').on('click', async function () { |
|
|
const allKnownScripts = getRegexScripts(); |
|
|
const newGlobalScripts = $('#regex_debugger_rules_global').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
|
|
const newScopedScripts = $('#regex_debugger_rules_scoped').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
|
|
const newPresetScripts = $('#regex_debugger_rules_preset').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
|
|
|
|
|
extension_settings.regex = newGlobalScripts; |
|
|
if (this_chid !== undefined) { |
|
|
await saveScriptsByType(newScopedScripts, SCRIPT_TYPES.SCOPED); |
|
|
} |
|
|
await saveScriptsByType(newPresetScripts, SCRIPT_TYPES.PRESET); |
|
|
|
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
toastr.success(t`Regex script order saved!`); |
|
|
|
|
|
const currentPopupContent = $('div:has(> #regex_debugger_rules)'); |
|
|
populateDebuggerRuleList(currentPopupContent); |
|
|
|
|
|
currentPopupContent.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
|
|
|
|
|
currentPopupContent.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); |
|
|
|
|
|
currentPopupContent.find('#regex_debugger_rules_preset').sortable({ delay: getSortableDelay() }).disableSelection(); |
|
|
}); |
|
|
|
|
|
debuggerHtml.find('#regex_debugger_expand_steps').on('click', function () { |
|
|
const popupContainer = $('<div class="expanded-regex-container"></div>'); |
|
|
const navPanel = $('<div class="expanded-regex-nav"><h4>Steps</h4></div>'); |
|
|
const contentPanel = $('<div class="expanded-regex-content"></div>'); |
|
|
|
|
|
const content = $('#regex_debugger_steps_output').clone().html(); |
|
|
contentPanel.html(content); |
|
|
|
|
|
$('#regex_debugger_rules .regex-debugger-rule').each(function () { |
|
|
const ruleElement = $(this); |
|
|
const scriptId = ruleElement.data('id'); |
|
|
const scriptName = ruleElement.find('.rule-name').text(); |
|
|
|
|
|
const link = $(`<a href="#">${escapeHtml(scriptName)}</a>`); |
|
|
link.data('target-id', `step-result-${scriptId}`); |
|
|
|
|
|
link.on('click', function (e) { |
|
|
e.preventDefault(); |
|
|
navPanel.find('a').removeClass('active'); |
|
|
$(this).addClass('active'); |
|
|
|
|
|
const targetId = $(this).data('target-id'); |
|
|
|
|
|
const targetElement = contentPanel.find(`#${targetId}`); |
|
|
|
|
|
if (targetElement.length) { |
|
|
const scrollTo = contentPanel.scrollTop() + targetElement.position().top; |
|
|
contentPanel.animate({ scrollTop: scrollTo }, 300); |
|
|
|
|
|
targetElement.css('transition', 'background-color 0.5s').css('background-color', 'var(--highlight_color)'); |
|
|
setTimeout(() => targetElement.css('background-color', ''), 1000); |
|
|
} |
|
|
}); |
|
|
|
|
|
navPanel.append(link); |
|
|
}); |
|
|
|
|
|
popupContainer.append(navPanel).append(contentPanel); |
|
|
callGenericPopup(popupContainer, POPUP_TYPE.TEXT, t`Step-by-step Transformation`, { wide: true, allowVerticalScrolling: false }); |
|
|
}); |
|
|
|
|
|
debuggerHtml.find('#regex_debugger_expand_final').on('click', function () { |
|
|
const content = $('#regex_debugger_final_output').html(); |
|
|
const popupContent = $('<div class="regex-popup-content"></div>').html(content); |
|
|
callGenericPopup(popupContent, POPUP_TYPE.TEXT, t`Final Output`, { wide: true, large: true, allowVerticalScrolling: true }); |
|
|
}); |
|
|
|
|
|
await callGenericPopup(debuggerHtml.children(), POPUP_TYPE.TEXT, '', { wide: true, allowVerticalScrolling: true }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateInfoBlock(editorHtml) { |
|
|
const infoBlock = editorHtml.find('.info-block').get(0); |
|
|
const infoBlockFlagsHint = editorHtml.find('#regex_info_block_flags_hint'); |
|
|
const findRegex = String(editorHtml.find('.find_regex').val()); |
|
|
|
|
|
infoBlockFlagsHint.hide(); |
|
|
|
|
|
|
|
|
if (!findRegex) { |
|
|
setInfoBlock(infoBlock, t`Find Regex is empty`, 'info'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const regex = regexFromString(findRegex); |
|
|
if (!regex) { |
|
|
throw new Error(t`Invalid Find Regex`); |
|
|
} |
|
|
|
|
|
const flagInfo = []; |
|
|
flagInfo.push(regex.flags.includes('g') ? t`Applies to all matches` : t`Applies to the first match`); |
|
|
flagInfo.push(regex.flags.includes('i') ? t`Case insensitive` : t`Case sensitive`); |
|
|
|
|
|
setInfoBlock(infoBlock, flagInfo.join('. '), 'hint'); |
|
|
infoBlockFlagsHint.show(); |
|
|
} catch (error) { |
|
|
setInfoBlock(infoBlock, error.message, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function migrateSettings() { |
|
|
let performSave = false; |
|
|
|
|
|
|
|
|
extension_settings.regex.forEach((script) => { |
|
|
if (!script.id) { |
|
|
script.id = uuidv4(); |
|
|
performSave = true; |
|
|
} |
|
|
|
|
|
if (!Array.isArray(script.placement)) { |
|
|
script.placement = []; |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(t`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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleRegexCallback(args, scriptName) { |
|
|
if (typeof scriptName !== 'string') throw new Error('Script name must be a string.'); |
|
|
|
|
|
const quiet = isTrueBoolean(args?.quiet); |
|
|
const action = isTrueBoolean(args?.state) ? 'enable' : |
|
|
isFalseBoolean(args?.state) ? 'disable' : |
|
|
'toggle'; |
|
|
|
|
|
const scripts = getRegexScripts(); |
|
|
const script = scripts.find(s => equalsIgnoreCaseAndAccents(s.scriptName, scriptName)); |
|
|
|
|
|
if (!script) { |
|
|
toastr.warning(t`Regex script '${scriptName}' not found.`); |
|
|
return ''; |
|
|
} |
|
|
|
|
|
switch (action) { |
|
|
case 'enable': |
|
|
script.disabled = false; |
|
|
break; |
|
|
case 'disable': |
|
|
script.disabled = true; |
|
|
break; |
|
|
default: |
|
|
script.disabled = !script.disabled; |
|
|
break; |
|
|
} |
|
|
|
|
|
const scriptType = getScriptType(script); |
|
|
const index = getScriptsByType(scriptType).indexOf(script); |
|
|
|
|
|
await saveRegexScript(script, index, scriptType); |
|
|
if (script.disabled) { |
|
|
!quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); |
|
|
} else { |
|
|
!quiet && toastr.success(t`Regex script '${scriptName}' has been enabled.`); |
|
|
} |
|
|
|
|
|
return script.scriptName || ''; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function onRegexImportObjectChange(regexScript, scriptType) { |
|
|
try { |
|
|
if (!regexScript.scriptName) { |
|
|
throw new Error('No script name provided.'); |
|
|
} |
|
|
|
|
|
|
|
|
regexScript.id = uuidv4(); |
|
|
|
|
|
const array = getScriptsByType(scriptType); |
|
|
array.push(regexScript); |
|
|
|
|
|
switch (scriptType) { |
|
|
case SCRIPT_TYPES.GLOBAL: |
|
|
|
|
|
break; |
|
|
case SCRIPT_TYPES.SCOPED: |
|
|
await saveScriptsByType(array, SCRIPT_TYPES.SCOPED); |
|
|
break; |
|
|
case SCRIPT_TYPES.PRESET: |
|
|
await saveScriptsByType(array, SCRIPT_TYPES.PRESET); |
|
|
break; |
|
|
default: |
|
|
break; |
|
|
} |
|
|
|
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
toastr.success(t`Regex script "${regexScript.scriptName}" imported.`); |
|
|
} catch (error) { |
|
|
console.log(error); |
|
|
toastr.error(t`Invalid regex object.`); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function onRegexImportFileChange(file, scriptType) { |
|
|
if (!file) { |
|
|
toastr.error('No file provided.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const regexScripts = JSON.parse(await getFileText(file)); |
|
|
if (Array.isArray(regexScripts)) { |
|
|
for (const regexScript of regexScripts) { |
|
|
await onRegexImportObjectChange(regexScript, scriptType); |
|
|
} |
|
|
} else { |
|
|
await onRegexImportObjectChange(regexScripts, scriptType); |
|
|
} |
|
|
} catch (error) { |
|
|
console.log(error); |
|
|
toastr.error('Invalid JSON file.'); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getScriptType(script) { |
|
|
for (const scriptType of Object.values(SCRIPT_TYPES)) { |
|
|
const scripts = getScriptsByType(scriptType); |
|
|
if (scripts.some(s => s.id === script.id)) { |
|
|
return scriptType; |
|
|
} |
|
|
} |
|
|
return SCRIPT_TYPE_UNKNOWN; |
|
|
} |
|
|
|
|
|
function getSelectedScripts() { |
|
|
const scripts = getRegexScripts(); |
|
|
const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; |
|
|
const selectedIds = Array.from(document.querySelectorAll(selector)) |
|
|
.map(e => e.getAttribute('id')) |
|
|
.filter(id => id); |
|
|
return scripts.filter(script => selectedIds.includes(script.id)); |
|
|
} |
|
|
|
|
|
function purgeEmbeddedRegexScripts({ character }) { |
|
|
const avatar = character?.avatar; |
|
|
if (!avatar) { |
|
|
return; |
|
|
} |
|
|
const checkKey = `AlertRegex_${avatar}`; |
|
|
if (accountStorage.getItem(checkKey)) { |
|
|
accountStorage.removeItem(checkKey); |
|
|
} |
|
|
disallowScopedScripts(characters?.[this_chid]); |
|
|
} |
|
|
|
|
|
function purgePresetEmbeddedRegexScripts({ apiId, name }) { |
|
|
const checkKey = `AlertRegex_${apiId}_${name}`; |
|
|
if (accountStorage.getItem(checkKey)) { |
|
|
accountStorage.removeItem(checkKey); |
|
|
} |
|
|
disallowPresetScripts(apiId, name); |
|
|
} |
|
|
|
|
|
async function checkCharEmbeddedRegexScripts() { |
|
|
const chid = this_chid; |
|
|
|
|
|
if (chid !== undefined && !selected_group) { |
|
|
const character = characters[chid]; |
|
|
const scripts = getScriptsByType(SCRIPT_TYPES.SCOPED); |
|
|
|
|
|
if (Array.isArray(scripts) && scripts.length > 0) { |
|
|
if (!isScopedScriptsAllowed(character)) { |
|
|
const checkKey = `AlertRegex_${character.avatar}`; |
|
|
if (!accountStorage.getItem(checkKey)) { |
|
|
accountStorage.setItem(checkKey, 'true'); |
|
|
const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {}); |
|
|
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, ''); |
|
|
|
|
|
if (result) { |
|
|
allowScopedScripts(character); |
|
|
await reloadCurrentChat(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
await loadRegexScripts(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function notifyReloadCurrentChat(presetName) { |
|
|
toastr.info( |
|
|
t`Reload the chat for regex to take effect` + '<br><u>' + t`Click here to reload immediately` + '</u>', |
|
|
t`Preset '${presetName}' contains enabled regex scripts`, |
|
|
{ |
|
|
timeOut: 5000, |
|
|
escapeHtml: false, |
|
|
onclick: reloadCurrentChat, |
|
|
}); |
|
|
} |
|
|
|
|
|
async function checkPresetEmbeddedRegexScripts() { |
|
|
const apiId = getCurrentPresetAPI(); |
|
|
const name = getCurrentPresetName(); |
|
|
const scripts = getScriptsByType(SCRIPT_TYPES.PRESET); |
|
|
|
|
|
if (Array.isArray(scripts) && scripts.length > 0) { |
|
|
if (!isPresetScriptsAllowed(apiId, name)) { |
|
|
const checkKey = `AlertRegex_${apiId}_${name}`; |
|
|
|
|
|
if (!accountStorage.getItem(checkKey)) { |
|
|
accountStorage.setItem(checkKey, 'true'); |
|
|
const template = await renderExtensionTemplateAsync('regex', 'presetEmbeddedScripts', {}); |
|
|
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, ''); |
|
|
|
|
|
if (result) { |
|
|
allowPresetScripts(apiId, name); |
|
|
if (getCurrentChatId()) { |
|
|
await reloadCurrentChat(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} else if (getCurrentChatId() && scripts.filter(script => !script.disabled).length > 0) { |
|
|
notifyReloadCurrentChat(name); |
|
|
} |
|
|
} |
|
|
|
|
|
await loadRegexScripts(); |
|
|
} |
|
|
|
|
|
async function onMainApiChanged({ apiId }) { |
|
|
const presetManager = getPresetManager(apiId); |
|
|
if (!presetManager) { |
|
|
return; |
|
|
} |
|
|
const presetName = presetManager.getSelectedPresetName(); |
|
|
const presetScripts = presetManager.readPresetExtensionField({ path: 'regex_scripts' }) ?? []; |
|
|
if (getCurrentChatId() && |
|
|
isPresetScriptsAllowed(apiId, presetName) && |
|
|
Array.isArray(presetScripts) && |
|
|
presetScripts.filter(script => !script.disabled).length > 0) { |
|
|
notifyReloadCurrentChat(presetName); |
|
|
} |
|
|
|
|
|
await loadRegexScripts(); |
|
|
} |
|
|
|
|
|
function onPresetRenamed({ apiId, oldName, newName }) { |
|
|
const oldCheckKey = `AlertRegex_${apiId}_${oldName}`; |
|
|
const checkKey = `AlertRegex_${apiId}_${newName}`; |
|
|
const value = accountStorage.getItem(oldCheckKey); |
|
|
if (value) { |
|
|
accountStorage.setItem(checkKey, value); |
|
|
accountStorage.removeItem(oldCheckKey); |
|
|
} |
|
|
if (isPresetScriptsAllowed(apiId, oldName)) { |
|
|
disallowPresetScripts(apiId, oldName); |
|
|
allowPresetScripts(apiId, newName); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
jQuery(async () => { |
|
|
if (!Array.isArray(extension_settings.regex)) { |
|
|
extension_settings.regex = []; |
|
|
} |
|
|
|
|
|
if (!Array.isArray(extension_settings.regex_presets)) { |
|
|
extension_settings.regex_presets = []; |
|
|
} |
|
|
|
|
|
|
|
|
if (extension_settings.disabledExtensions.includes('regex')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
migrateSettings(); |
|
|
|
|
|
const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); |
|
|
$('#regex_container').append(settingsHtml); |
|
|
$('#open_regex_editor').on('click', function () { |
|
|
onRegexEditorOpenClick(false, SCRIPT_TYPES.GLOBAL); |
|
|
}); |
|
|
$('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); |
|
|
$('#open_scoped_editor').on('click', function () { |
|
|
if (this_chid === undefined) { |
|
|
toastr.error(t`No character selected.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (selected_group) { |
|
|
toastr.error(t`Cannot edit scoped scripts in group chats.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
onRegexEditorOpenClick(false, SCRIPT_TYPES.SCOPED); |
|
|
}); |
|
|
$('#open_preset_editor').on('click', function () { |
|
|
onRegexEditorOpenClick(false, SCRIPT_TYPES.PRESET); |
|
|
}); |
|
|
$('#import_regex_file').on('change', async function () { |
|
|
let target = SCRIPT_TYPES.GLOBAL; |
|
|
const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); |
|
|
template.find('#regex_import_target_global').on('input', () => (target = SCRIPT_TYPES.GLOBAL)); |
|
|
template.find('#regex_import_target_scoped').on('input', () => (target = SCRIPT_TYPES.SCOPED)); |
|
|
template.find('#regex_import_target_preset').on('input', () => (target = SCRIPT_TYPES.PRESET)); |
|
|
|
|
|
await callGenericPopup(template, POPUP_TYPE.TEXT); |
|
|
|
|
|
const inputElement = this instanceof HTMLInputElement && this; |
|
|
for (const file of inputElement.files) { |
|
|
await onRegexImportFileChange(file, target); |
|
|
} |
|
|
inputElement.value = ''; |
|
|
}); |
|
|
$('#import_regex').on('click', function () { |
|
|
$('#import_regex_file').trigger('click'); |
|
|
}); |
|
|
|
|
|
$('#bulk_select_all_toggle').on('click', async function () { |
|
|
const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
|
|
if (checkboxes.length === 0) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; |
|
|
const newState = !allAreChecked; |
|
|
|
|
|
checkboxes.prop('checked', newState); |
|
|
setToggleAllIcon(newState); |
|
|
setMoveButtonsVisibility(); |
|
|
}); |
|
|
|
|
|
$('#bulk_enable_regex').on('click', async function () { |
|
|
await bulkToggleRegexScripts(true); |
|
|
}); |
|
|
|
|
|
$('#bulk_disable_regex').on('click', async function () { |
|
|
await bulkToggleRegexScripts(false); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function bulkToggleRegexScripts(newState) { |
|
|
const scripts = getSelectedScripts().filter(script => script.disabled === newState); |
|
|
if (scripts.length === 0) { |
|
|
toastr.warning(newState |
|
|
? t`No regex scripts selected for enabling.` |
|
|
: t`No regex scripts selected for disabling.`, |
|
|
); |
|
|
return; |
|
|
} |
|
|
const scriptTypesToSave = new Set(); |
|
|
for (const script of scripts) { |
|
|
const scriptType = getScriptType(script); |
|
|
scriptTypesToSave.add(scriptType); |
|
|
script.disabled = !newState; |
|
|
} |
|
|
for (const scriptType of scriptTypesToSave) { |
|
|
const scriptsOfType = getScriptsByType(scriptType); |
|
|
await saveScriptsByType(scriptsOfType, scriptType); |
|
|
} |
|
|
|
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
|
|
|
|
|
|
const currentChatId = getCurrentChatId(); |
|
|
if (currentChatId) { |
|
|
await reloadCurrentChat(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function bulkMoveRegexScript(toType) { |
|
|
const scripts = getSelectedScripts(); |
|
|
if (scripts.length === 0) { |
|
|
toastr.warning(t`No regex scripts selected for moving.`); |
|
|
return; |
|
|
} |
|
|
for (const script of scripts) { |
|
|
await moveRegexScript(script, toType, getScriptType(script), false); |
|
|
} |
|
|
|
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
|
|
|
|
|
|
const currentChatId = getCurrentChatId(); |
|
|
if (currentChatId) { |
|
|
await reloadCurrentChat(); |
|
|
} |
|
|
} |
|
|
|
|
|
$('#bulk_regex_move_to_global').on('click', async () => { |
|
|
const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to global?`, POPUP_TYPE.CONFIRM); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await bulkMoveRegexScript(SCRIPT_TYPES.GLOBAL); |
|
|
}); |
|
|
|
|
|
$('#bulk_regex_move_to_scoped').on('click', async () => { |
|
|
if (this_chid === undefined) { |
|
|
toastr.error(t`No character selected.`); |
|
|
return; |
|
|
} |
|
|
if (selected_group) { |
|
|
toastr.error(t`Cannot edit scoped scripts in group chats.`); |
|
|
return; |
|
|
} |
|
|
const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to scoped?`, POPUP_TYPE.CONFIRM); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await bulkMoveRegexScript(SCRIPT_TYPES.SCOPED); |
|
|
}); |
|
|
|
|
|
$('#bulk_regex_move_to_preset').on('click', async function () { |
|
|
const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to preset?`, POPUP_TYPE.CONFIRM); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
await bulkMoveRegexScript(SCRIPT_TYPES.PRESET); |
|
|
}); |
|
|
|
|
|
$('#bulk_delete_regex').on('click', async function () { |
|
|
const scripts = getSelectedScripts(); |
|
|
if (scripts.length === 0) { |
|
|
toastr.warning(t`No regex scripts selected for deletion.`); |
|
|
return; |
|
|
} |
|
|
const confirm = await callGenericPopup(t`Are you sure you want to delete the selected regex scripts?`, POPUP_TYPE.CONFIRM); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
for (const script of scripts) { |
|
|
await deleteRegexScript(script.id, getScriptType(script), false); |
|
|
} |
|
|
saveSettingsDebounced(); |
|
|
await loadRegexScripts(); |
|
|
await reloadCurrentChat(); |
|
|
}); |
|
|
|
|
|
$('#bulk_export_regex').on('click', async function () { |
|
|
const scripts = getSelectedScripts(); |
|
|
if (scripts.length === 0) { |
|
|
toastr.warning(t`No regex scripts selected for export.`); |
|
|
return; |
|
|
} |
|
|
const fileName = `regex-${new Date().toISOString()}.json`; |
|
|
const fileData = JSON.stringify(scripts, null, 4); |
|
|
download(fileData, fileName, 'application/json'); |
|
|
await loadRegexScripts(); |
|
|
}); |
|
|
|
|
|
let sortableDatas = [ |
|
|
{ |
|
|
selector: '#saved_regex_scripts', |
|
|
setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.GLOBAL), |
|
|
getter: () => getScriptsByType(SCRIPT_TYPES.GLOBAL), |
|
|
}, |
|
|
{ |
|
|
selector: '#saved_scoped_scripts', |
|
|
setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.SCOPED), |
|
|
getter: () => getScriptsByType(SCRIPT_TYPES.SCOPED), |
|
|
}, |
|
|
{ |
|
|
selector: '#saved_preset_scripts', |
|
|
setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.PRESET), |
|
|
getter: () => getScriptsByType(SCRIPT_TYPES.PRESET), |
|
|
}, |
|
|
]; |
|
|
for (const { selector, setter, getter } of sortableDatas) { |
|
|
|
|
|
$(selector).sortable({ |
|
|
delay: getSortableDelay(), |
|
|
handle: '.drag-handle', |
|
|
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 reloadCurrentChat(); |
|
|
await loadRegexScripts(); |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
$('#regex_scoped_toggle').on('input', function () { |
|
|
if (this_chid === undefined) { |
|
|
toastr.error(t`No character selected.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (selected_group) { |
|
|
toastr.error(t`Cannot edit scoped scripts in group chats.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
const isEnable = !!$(this).prop('checked'); |
|
|
const character = characters[this_chid]; |
|
|
|
|
|
if (isEnable) { |
|
|
allowScopedScripts(character); |
|
|
} else { |
|
|
disallowScopedScripts(character); |
|
|
} |
|
|
|
|
|
saveSettingsDebounced(); |
|
|
reloadCurrentChat(); |
|
|
}); |
|
|
|
|
|
$('#regex_preset_toggle').on('input', function () { |
|
|
const isEnable = !!$(this).prop('checked'); |
|
|
const name = getCurrentPresetName(); |
|
|
|
|
|
if (isEnable) { |
|
|
allowPresetScripts(getCurrentPresetAPI(), name); |
|
|
} else { |
|
|
disallowPresetScripts(getCurrentPresetAPI(), name); |
|
|
} |
|
|
|
|
|
saveSettingsDebounced(); |
|
|
reloadCurrentChat(); |
|
|
}); |
|
|
|
|
|
await loadRegexScripts(); |
|
|
|
|
|
$('#saved_regex_scripts').sortable('enable'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getScriptDecorators(type) { |
|
|
switch (type) { |
|
|
case SCRIPT_TYPES.GLOBAL: |
|
|
return { |
|
|
typename: 'global', |
|
|
color: enumTypes.enum, |
|
|
icon: 'G', |
|
|
}; |
|
|
case SCRIPT_TYPES.SCOPED: |
|
|
return { |
|
|
typename: 'scoped', |
|
|
color: enumTypes.name, |
|
|
icon: 'S', |
|
|
}; |
|
|
case SCRIPT_TYPES.PRESET: |
|
|
return { |
|
|
typename: 'preset', |
|
|
color: enumTypes.name, |
|
|
icon: 'P', |
|
|
}; |
|
|
default: |
|
|
return { |
|
|
typename: 'Unknown', |
|
|
color: enumTypes.variable, |
|
|
icon: 'Unknown', |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
const localEnumProviders = { |
|
|
regexScripts: () => |
|
|
getRegexScripts().map(script => { |
|
|
const type = getScriptType(script); |
|
|
const { typename, color, icon } = getScriptDecorators(type); |
|
|
return new SlashCommandEnumValue( |
|
|
script.scriptName, |
|
|
`${enumIcons.getStateIcon(!script.disabled)} [${typename}] ${script.findRegex}`, |
|
|
color, |
|
|
icon, |
|
|
); |
|
|
}), |
|
|
}; |
|
|
|
|
|
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.', |
|
|
})); |
|
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
|
|
name: 'regex-toggle', |
|
|
callback: toggleRegexCallback, |
|
|
returns: 'The name of the script that was toggled', |
|
|
namedArgumentList: [ |
|
|
SlashCommandNamedArgument.fromProps({ |
|
|
name: 'state', |
|
|
description: 'Explicitly set the state of the script (\'on\' to enable, \'off\' to disable). If not provided, the state will be toggled to the opposite of the current state.', |
|
|
typeList: [ARGUMENT_TYPE.BOOLEAN], |
|
|
defaultValue: 'toggle', |
|
|
enumList: commonEnumProviders.boolean('onOffToggle')(), |
|
|
}), |
|
|
SlashCommandNamedArgument.fromProps({ |
|
|
name: 'quiet', |
|
|
description: 'Suppress the toast message script toggled', |
|
|
typeList: [ARGUMENT_TYPE.BOOLEAN], |
|
|
defaultValue: 'false', |
|
|
enumList: commonEnumProviders.boolean('trueFalse')(), |
|
|
}), |
|
|
], |
|
|
unnamedArgumentList: [ |
|
|
SlashCommandArgument.fromProps({ |
|
|
description: 'script name', |
|
|
typeList: [ARGUMENT_TYPE.STRING], |
|
|
isRequired: true, |
|
|
enumProvider: localEnumProviders.regexScripts, |
|
|
}), |
|
|
], |
|
|
helpString: ` |
|
|
<div> |
|
|
Toggles the state of a specified regex script. |
|
|
</div> |
|
|
<div> |
|
|
<strong>Example:</strong> |
|
|
<ul> |
|
|
<li> |
|
|
<pre><code class="language-stscript">/regex-toggle MyScript</code></pre> |
|
|
</li> |
|
|
<li> |
|
|
<pre><code class="language-stscript">/regex-toggle state=off Character-specific Script</code></pre> |
|
|
</li> |
|
|
</ul> |
|
|
</div> |
|
|
`, |
|
|
})); |
|
|
|
|
|
eventSource.on(event_types.MAIN_API_CHANGED, onMainApiChanged); |
|
|
eventSource.on(event_types.CHAT_CHANGED, checkCharEmbeddedRegexScripts); |
|
|
eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts); |
|
|
eventSource.on(event_types.PRESET_RENAMED_BEFORE, onPresetRenamed); |
|
|
eventSource.on(event_types.PRESET_CHANGED, checkPresetEmbeddedRegexScripts); |
|
|
eventSource.on(event_types.PRESET_DELETED, purgePresetEmbeddedRegexScripts); |
|
|
|
|
|
presetManager.setupEventListeners(); |
|
|
presetManager.registerSlashCommands(); |
|
|
}); |
|
|
|