migrate
This commit is contained in:
52
public/scripts/extensions/regex/dropdown.html
Normal file
52
public/scripts/extensions/regex/dropdown.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="regex_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="ext_regex_title">
|
||||
Regex
|
||||
</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div class="flex-container">
|
||||
<div id="open_regex_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_global_script_desc" title="New global regex script">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
<small data-i18n="ext_regex_new_global_script">+ Global</small>
|
||||
</div>
|
||||
<div id="open_scoped_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_scoped_script_desc" title="New scoped regex script">
|
||||
<i class="fa-solid fa-address-card"></i>
|
||||
<small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
|
||||
</div>
|
||||
<div id="import_regex" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<small data-i18n="ext_regex_import_script">Import</small>
|
||||
</div>
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
|
||||
</div>
|
||||
<hr />
|
||||
<div id="global_scripts_block" class="padding5">
|
||||
<div>
|
||||
<strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_global_scripts_desc">
|
||||
Available for all characters. Saved to local settings.
|
||||
</small>
|
||||
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="scoped_scripts_block" class="padding5">
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
<strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong>
|
||||
<label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
|
||||
<input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" />
|
||||
<span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" data-i18n="[title]ext_regex_disallow_scoped" title="Disallow using scoped regex"></span>
|
||||
<span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" data-i18n="[title]ext_regex_allow_scoped" title="Allow using scoped regex"></span>
|
||||
</label>
|
||||
</div>
|
||||
<small data-i18n="ext_regex_scoped_scripts_desc">
|
||||
Only available for this character. Saved to the card data.
|
||||
</small>
|
||||
<div id="saved_scoped_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
164
public/scripts/extensions/regex/editor.html
Normal file
164
public/scripts/extensions/regex/editor.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<div id="regex_editor_template">
|
||||
<div class="regex_editor">
|
||||
<h3 class="flex-container justifyCenter alignItemsBaseline">
|
||||
<strong data-i18n="Regex Editor">Regex Editor</strong>
|
||||
<a href="https://docs.ChuQuadrant.app/extensions/regex/" class="notes-link" target="_blank" rel="noopener noreferrer">
|
||||
<span class="note-link-span">?</span>
|
||||
</a>
|
||||
<div id="regex_test_mode_toggle" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-bug fa-sm"></i>
|
||||
<span class="menu_button_text" data-i18n="Test Mode">Test Mode</span>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<small class="flex-container extensions_info" data-i18n="ext_regex_desc">
|
||||
Regex is a tool to find/replace strings using regular expressions. If you want to learn more, click on the ? next to the title.
|
||||
</small>
|
||||
<hr />
|
||||
|
||||
<div id="regex_info_block_wrapper">
|
||||
<div id="regex_info_block" class="info-block"></div>
|
||||
<a id="regex_info_block_flags_hint" href="https://docs.ChuQuadrant.app/extensions/regex/#flags" target="_blank" rel="noopener noreferrer">
|
||||
<i class="fa-solid fa-circle-info" data-i18n="[title]ext_regex_flags_help" title="Click here to learn more about regex flags."></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="regex_test_mode" class="flex1 flex-container displayNone">
|
||||
<div class="flex1">
|
||||
<label class="title_restorable" for="regex_test_input">
|
||||
<small data-i18n="Input">Input</small>
|
||||
</label>
|
||||
<textarea id="regex_test_input" class="text_pole textarea_compact" rows="4" data-i18n="[placeholder]ext_regex_test_input_placeholder" placeholder="Type here..."></textarea>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<label class="title_restorable" for="regex_test_output">
|
||||
<small data-i18n="Output">Output</small>
|
||||
</label>
|
||||
<textarea id="regex_test_output" class="text_pole textarea_compact" rows="4" data-i18n="[placeholder]ext_regex_output_placeholder" placeholder="Empty" readonly></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div class="flex1">
|
||||
<label for="regex_script_name" class="title_restorable">
|
||||
<small data-i18n="Script Name">Script Name</small>
|
||||
</label>
|
||||
<div>
|
||||
<input class="regex_script_name text_pole textarea_compact" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<label for="find_regex" class="title_restorable">
|
||||
<small data-i18n="Find Regex">Find Regex</small>
|
||||
</label>
|
||||
<div>
|
||||
<input class="find_regex text_pole textarea_compact" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<label for="regex_replace_string" class="title_restorable">
|
||||
<small data-i18n="Replace With">Replace With</small>
|
||||
</label>
|
||||
<div>
|
||||
<textarea class="regex_replace_string text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_replace_string_placeholder" placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups." rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1">
|
||||
<label for="regex_trim_strings" class="title_restorable">
|
||||
<small data-i18n="Trim Out">Trim Out</small>
|
||||
</label>
|
||||
<div>
|
||||
<textarea class="regex_trim_strings text_pole wide100p textarea_compact" data-i18n="[placeholder]ext_regex_trim_placeholder" placeholder="Globally trims any unwanted parts from a regex match before replacement. Separate each element by an enter." rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
||||
<small data-i18n="ext_regex_affects">Affects</small>
|
||||
<div data-i18n="[title]ext_regex_user_input_desc" title="Messages sent by the user.">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="1">
|
||||
<span data-i18n="ext_regex_user_input">User Input</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-i18n="[title]ext_regex_ai_input_desc" title="Messages received from the Generation API.">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="2">
|
||||
<span data-i18n="ext_regex_ai_output">AI Output</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-i18n="[title]ext_regex_slash_desc" title="Messages sent using STscript commands.">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="3">
|
||||
<span data-i18n="Slash Commands">Slash Commands</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-i18n="[title]ext_regex_wi_desc" title="Lorebook/World Info entry contents. Requires 'Only Format Prompt' to be checked!">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="5">
|
||||
<span data-i18n="World Info">World Info</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="6">
|
||||
<span data-i18n="Reasoning">Reasoning</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-container wide100p marginTop5">
|
||||
<div class="flex1 flex-container flexNoGap">
|
||||
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. When blank / 'Unlimited' or -1, also affect message to continue on Continue.">
|
||||
<span data-i18n="Min Depth">Min Depth</span>
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</small>
|
||||
<input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
|
||||
</div>
|
||||
<div class="flex1 flex-container flexNoGap">
|
||||
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. Max must be greater than Min for regex to apply.">
|
||||
<span data-i18n="Max Depth">Max Depth</span>
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</small>
|
||||
<input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex1 wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
|
||||
<small data-i18n="ext_regex_other_options">Other Options</small>
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="disabled" />
|
||||
<span data-i18n="Disabled">Disabled</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container" data-i18n="[title]ext_regex_run_on_edit_desc" title="Run the regex script when the message belonging a to specified role(s) is edited.">
|
||||
<input type="checkbox" name="run_on_edit" />
|
||||
<span data-i18n="Run On Edit">Run On Edit</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container flexNoGap marginBot5" data-i18n="[title]ext_regex_substitute_regex_desc" title="Substitute {{macros}} in Find Regex before running it">
|
||||
<span>
|
||||
<small data-i18n="Macro in Find Regex">Macros in Find Regex</small>
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</span>
|
||||
<select name="substitute_regex" class="text_pole textarea_compact margin0">
|
||||
<option value="0" data-i18n="Don't substitute">Don't substitute</option>
|
||||
<option value="1" data-i18n="Substitute (raw)">Substitute (raw)</option>
|
||||
<option value="2" data-i18n="Substitute (escaped)">Substitute (escaped)</option>
|
||||
</select>
|
||||
</label>
|
||||
<span>
|
||||
<small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
|
||||
<span class="fa-solid fa-circle-question note-link-span" data-i18n="[title]ext_regex_other_options_desc" title="By default, regex scripts alter the chat file directly and irreversibly. Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
|
||||
</span>
|
||||
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_visual_desc" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
|
||||
<input type="checkbox" name="only_format_display" />
|
||||
<span data-i18n="Only Format Display">Alter Chat Display</span>
|
||||
</label>
|
||||
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_prompt_desc" title="Chat history file contents won't change, but regex will be applied to the outgoing prompt before it is sent to the LLM.">
|
||||
<input type="checkbox" name="only_format_prompt" />
|
||||
<span data-i18n="Only Format Prompt (?)">Alter Outgoing Prompt</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
public/scripts/extensions/regex/embeddedScripts.html
Normal file
5
public/scripts/extensions/regex/embeddedScripts.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<h3>This character has embedded regex script(s).</h3>
|
||||
<h3>Would you like to allow using them?</h3>
|
||||
<div class="m-b-1">If you want to do it later, select "Regex" from the extensions menu.</div>
|
||||
</div>
|
205
public/scripts/extensions/regex/engine.js
Normal file
205
public/scripts/extensions/regex/engine.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { regexFromString } from '../../utils.js';
|
||||
export {
|
||||
regex_placement,
|
||||
getRegexedString,
|
||||
runRegexScript,
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {number} Where the regex script should be applied
|
||||
*/
|
||||
const regex_placement = {
|
||||
/**
|
||||
* @deprecated MD Display is deprecated. Do not use.
|
||||
*/
|
||||
MD_DISPLAY: 0,
|
||||
USER_INPUT: 1,
|
||||
AI_OUTPUT: 2,
|
||||
SLASH_COMMAND: 3,
|
||||
// 4 - sendAs (legacy)
|
||||
WORLD_INFO: 5,
|
||||
REASONING: 6,
|
||||
};
|
||||
|
||||
export const substitute_find_regex = {
|
||||
NONE: 0,
|
||||
RAW: 1,
|
||||
ESCAPED: 2,
|
||||
};
|
||||
|
||||
function sanitizeRegexMacro(x) {
|
||||
return (x && typeof x === 'string') ?
|
||||
x.replaceAll(/[\n\r\t\v\f\0.^$*+?{}[\]\\/|()]/gs, function (s) {
|
||||
switch (s) {
|
||||
case '\n':
|
||||
return '\\n';
|
||||
case '\r':
|
||||
return '\\r';
|
||||
case '\t':
|
||||
return '\\t';
|
||||
case '\v':
|
||||
return '\\v';
|
||||
case '\f':
|
||||
return '\\f';
|
||||
case '\0':
|
||||
return '\\0';
|
||||
default:
|
||||
return '\\' + s;
|
||||
}
|
||||
}) : x;
|
||||
}
|
||||
|
||||
function getScopedRegex() {
|
||||
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
|
||||
|
||||
if (!isAllowed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scripts = characters[this_chid]?.data?.extensions?.regex_scripts;
|
||||
|
||||
if (!Array.isArray(scripts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return scripts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent function to fetch a regexed version of a raw string
|
||||
* @param {string} rawString The raw string to be regexed
|
||||
* @param {regex_placement} placement The placement of the string
|
||||
* @param {RegexParams} params The parameters to use for the regex script
|
||||
* @returns {string} The regexed string
|
||||
* @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script
|
||||
*/
|
||||
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) {
|
||||
// WTF have you passed me?
|
||||
if (typeof rawString !== 'string') {
|
||||
console.warn('getRegexedString: rawString is not a string. Returning empty string.');
|
||||
return '';
|
||||
}
|
||||
|
||||
let finalString = rawString;
|
||||
if (extension_settings.disabledExtensions.includes('regex') || !rawString || placement === undefined) {
|
||||
return finalString;
|
||||
}
|
||||
|
||||
const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];
|
||||
allRegex.forEach((script) => {
|
||||
if (
|
||||
// Script applies to Markdown and input is Markdown
|
||||
(script.markdownOnly && isMarkdown) ||
|
||||
// Script applies to Generate and input is Generate
|
||||
(script.promptOnly && isPrompt) ||
|
||||
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
|
||||
(!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
|
||||
) {
|
||||
if (isEdit && !script.runOnEdit) {
|
||||
console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the depth is within the min/max depth
|
||||
if (typeof depth === 'number') {
|
||||
if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= -1 && depth < script.minDepth) {
|
||||
console.debug(`getRegexedString: Skipping script ${script.scriptName} because depth ${depth} is less than minDepth ${script.minDepth}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNaN(script.maxDepth) && script.maxDepth !== null && script.maxDepth >= 0 && depth > script.maxDepth) {
|
||||
console.debug(`getRegexedString: Skipping script ${script.scriptName} because depth ${depth} is greater than maxDepth ${script.maxDepth}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (script.placement.includes(placement)) {
|
||||
finalString = runRegexScript(script, finalString, { characterOverride });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return finalString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the provided regex script on the given string
|
||||
* @param {import('./index.js').RegexScript} regexScript The regex script to run
|
||||
* @param {string} rawString The string to run the regex script on
|
||||
* @param {RegexScriptParams} params The parameters to use for the regex script
|
||||
* @returns {string} The new string
|
||||
* @typedef {{characterOverride?: string}} RegexScriptParams The parameters to use for the regex script
|
||||
*/
|
||||
function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
let newString = rawString;
|
||||
if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) {
|
||||
return newString;
|
||||
}
|
||||
|
||||
const getRegexString = () => {
|
||||
switch (Number(regexScript.substituteRegex)) {
|
||||
case substitute_find_regex.NONE:
|
||||
return regexScript.findRegex;
|
||||
case substitute_find_regex.RAW:
|
||||
return substituteParamsExtended(regexScript.findRegex);
|
||||
case substitute_find_regex.ESCAPED:
|
||||
return substituteParamsExtended(regexScript.findRegex, {}, sanitizeRegexMacro);
|
||||
default:
|
||||
console.warn(`runRegexScript: Unknown substituteRegex value ${regexScript.substituteRegex}. Using raw regex.`);
|
||||
return regexScript.findRegex;
|
||||
}
|
||||
};
|
||||
const regexString = getRegexString();
|
||||
const findRegex = regexFromString(regexString);
|
||||
|
||||
// The user skill issued. Return with nothing.
|
||||
if (!findRegex) {
|
||||
return newString;
|
||||
}
|
||||
|
||||
// Run replacement. Currently does not support the Overlay strategy
|
||||
newString = rawString.replace(findRegex, function (match) {
|
||||
const args = [...arguments];
|
||||
const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
|
||||
const replaceWithGroups = replaceString.replaceAll(/\$(\d+)/g, (_, num) => {
|
||||
// Get a full match or a capture group
|
||||
const match = args[Number(num)];
|
||||
|
||||
// No match found - return the empty string
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove trim strings from the match
|
||||
const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride });
|
||||
|
||||
// TODO: Handle overlay here
|
||||
|
||||
return filteredMatch;
|
||||
});
|
||||
|
||||
// Substitute at the end
|
||||
return substituteParams(replaceWithGroups);
|
||||
});
|
||||
|
||||
return newString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters anything to trim from the regex match
|
||||
* @param {string} rawString The raw string to filter
|
||||
* @param {string[]} trimStrings The strings to trim
|
||||
* @param {RegexScriptParams} params The parameters to use for the regex filter
|
||||
* @returns {string} The filtered string
|
||||
*/
|
||||
function filterString(rawString, trimStrings, { characterOverride } = {}) {
|
||||
let finalString = rawString;
|
||||
trimStrings.forEach((trimString) => {
|
||||
const subTrimString = substituteParams(trimString, undefined, characterOverride);
|
||||
finalString = finalString.replaceAll(subTrimString, '');
|
||||
});
|
||||
|
||||
return finalString;
|
||||
}
|
19
public/scripts/extensions/regex/importTarget.html
Normal file
19
public/scripts/extensions/regex/importTarget.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div>
|
||||
<h3 data-i18n="ext_regex_import_target">
|
||||
Import To:
|
||||
</h3>
|
||||
<div class="flex-container flexFlowColumn wide100p padding10 justifyLeft">
|
||||
<label for="regex_import_target_global">
|
||||
<input type="radio" name="regex_import_target" id="regex_import_target_global" value="global" checked />
|
||||
<span data-i18n="ext_regex_global_scripts">
|
||||
Global Scripts
|
||||
</span>
|
||||
</label>
|
||||
<label for="regex_import_target_scoped">
|
||||
<input type="radio" name="regex_import_target" id="regex_import_target_scoped" value="scoped" />
|
||||
<span data-i18n="ext_regex_scoped_scripts">
|
||||
Scoped Scripts
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
731
public/scripts/extensions/regex/index.js
Normal file
731
public/scripts/extensions/regex/index.js
Normal file
@@ -0,0 +1,731 @@
|
||||
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 { 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, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js';
|
||||
import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
|
||||
import { t } from '../../i18n.js';
|
||||
import { accountStorage } from '../../util/AccountStorage.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../char-data.js').RegexScriptData} RegexScript
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
updateInfoBlock(editorHtml);
|
||||
|
||||
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);
|
||||
updateInfoBlock(editorHtml);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the info block in the regex editor with hints regarding the find regex.
|
||||
* @param {JQuery<HTMLElement>} editorHtml The editor HTML
|
||||
*/
|
||||
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();
|
||||
|
||||
// Clear the info block if the find regex is empty
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// 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(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* /regex-toggle slash command callback
|
||||
* @param {{state: string, quiet: string}} args Named arguments
|
||||
* @param {string} scriptName The name of the script to toggle
|
||||
* @returns {Promise<string>} The name of the script
|
||||
*/
|
||||
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 isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id);
|
||||
const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOf(script) : scripts.indexOf(script);
|
||||
|
||||
await saveRegexScript(script, index, isScoped);
|
||||
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 || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!accountStorage.getItem(checkKey)) {
|
||||
accountStorage.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.',
|
||||
}));
|
||||
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.CHAT_CHANGED, checkEmbeddedRegexScripts);
|
||||
eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
|
||||
});
|
11
public/scripts/extensions/regex/manifest.json
Normal file
11
public/scripts/extensions/regex/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Regex",
|
||||
"loading_order": 1,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "kingbri",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/ChuQuadrant/ChuQuadrant"
|
||||
}
|
26
public/scripts/extensions/regex/scriptTemplate.html
Normal file
26
public/scripts/extensions/regex/scriptTemplate.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="regex-script-label flex-container flexnowrap">
|
||||
<span class="drag-handle menu-handle">☰</span>
|
||||
<div class="regex_script_name flexGrow overflow-hidden"></div>
|
||||
<div class="flex-container flexnowrap">
|
||||
<label class="checkbox flex-container" for="regex_disable">
|
||||
<input type="checkbox" name="regex_disable" class="disable_regex" />
|
||||
<span class="regex-toggle-on fa-solid fa-toggle-on" data-i18n="[title]ext_regex_disable_script" title="Disable script"></span>
|
||||
<span class="regex-toggle-off fa-solid fa-toggle-off" data-i18n="[title]ext_regex_enable_script" title="Enable script"></span>
|
||||
</label>
|
||||
<div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</div>
|
||||
<div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
</div>
|
||||
<div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
</div>
|
||||
<div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
|
||||
<i class="fa-solid fa-file-export"></i>
|
||||
</div>
|
||||
<div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
128
public/scripts/extensions/regex/style.css
Normal file
128
public/scripts/extensions/regex/style.css
Normal file
@@ -0,0 +1,128 @@
|
||||
.regex_settings .menu_button {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.regex_settings .checkbox {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.regex-script-container {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.regex-script-container:empty::after {
|
||||
content: "No scripts found";
|
||||
font-size: 0.95em;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#scoped_scripts_block {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#scoped_scripts_block .move_to_scoped {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#global_scripts_block .move_to_global {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.enable_scoped:checked ~ .regex-toggle-on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.enable_scoped:checked ~ .regex-toggle-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enable_scoped:not(:checked) ~ .regex-toggle-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enable_scoped:not(:checked) ~ .regex-toggle-off {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.regex-script-label {
|
||||
align-items: center;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
padding: 0 5px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.regex-script-label:has(.disable_regex:checked) .regex_script_name {
|
||||
text-decoration: line-through;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
input.disable_regex,
|
||||
input.enable_scoped {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.regex-toggle-off {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.5);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.regex-toggle-off:hover {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.regex-toggle-on {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disable_regex:checked ~ .regex-toggle-off {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.disable_regex:checked ~ .regex-toggle-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disable_regex:not(:checked) ~ .regex-toggle-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disable_regex:not(:checked) ~ .regex-toggle-on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#regex_info_block_wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#regex_info_block {
|
||||
margin: 10px 0;
|
||||
padding: 5px 20px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#regex_info_block_wrapper:has(#regex_info_block:empty) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#regex_info_block_flags_hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
}
|
Reference in New Issue
Block a user