🎉 初始化项目

This commit is contained in:
2026-02-10 17:48:27 +08:00
parent f3da9c506a
commit db934ebed7
1575 changed files with 348967 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
/* Styles for the debugger UI */
#regex_debugger_rules {
margin: 10px 0;
}
#regex_debugger_rules,
#regex_debugger_rules .sortable-list {
padding-left: 0;
}
.regex-debugger-rules-list {
position: relative;
}
.regex-debugger-rule {
display: flex;
align-items: center;
padding: 8px 10px;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
cursor: pointer;
background-color: var(--black30a);
gap: 5px;
margin-bottom: 5px;
}
#regex_debugger_run_test_header {
justify-content: space-between;
align-items: center;
}
#regex_debugger_expand_steps,
#regex_debugger_expand_final,
#regex_debugger_save_order {
position: absolute;
top: 0;
right: 0;
margin: 0;
}
.regex-debugger-rule:hover {
filter: brightness(1.1);
}
.regex-debugger-rule .handle {
cursor: grab;
margin-right: 5px;
}
.regex-debugger-rule .rule-details {
flex-grow: 1;
text-align: left;
display: flex;
align-items: baseline;
gap: 5px;
}
.regex-debugger-rule .rule-name {
font-weight: bold;
}
.regex-debugger-rule .rule-regex {
font-size: 0.8em;
opacity: 0.8;
font-family: var(--monoFontFamily);
}
.regex-debugger-rule .rule-scope {
font-size: 0.8em;
padding: 2px 6px;
border-radius: 5px;
background-color: var(--black30a);
margin-left: auto;
margin-right: 10px;
}
.regex-debugger-rule .menu_button {
margin: 0;
}
#regex_debugger_raw_input {
min-height: 1.8em;
}
#regex_debugger_steps_output {
min-height: 2em;
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
padding: 5px;
background-color: var(--black30a);
font-family: var(--monoFontFamily);
font-size: 0.9em;
}
#regex_debugger_final_output {
min-height: 2em;
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
padding: 5px;
background-color: var(--black30a);
white-space: pre-wrap;
word-break: break-word;
text-align: left;
}
.step-header {
margin-top: 10px;
margin-bottom: 5px;
}
.step-output {
white-space: pre-wrap;
word-break: break-all;
padding: 5px;
background-color: var(--black30a);
border-radius: 5px;
text-align: left;
}
/* Classes to replace inline styles */
.regex-debugger-no-rules {
padding: 10px;
text-align: center;
opacity: 0.8;
}
.regex-debugger-list-header {
font-weight: bold;
padding: 10px;
}
/* Styles for statistics */
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.step-metrics {
font-size: 0.8em;
opacity: 0.8;
font-weight: normal;
}
.regex-debugger-summary {
padding: 8px;
margin-bottom: 10px;
border: 1px solid var(--SmartThemeBorderColor);
background-color: var(--black30a);
border-radius: 5px;
text-align: center;
font-size: 0.9em;
}
.regex-debugger-tester .results-header {
position: relative;
margin: 10px 0;
}
.regex-debugger-tester .radio_group {
text-align: left;
}
/* Styles for statistics and highlighting additions */
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
/* Allow wrapping on small screens */
}
.step-metrics {
font-size: 0.8em;
opacity: 0.8;
font-weight: normal;
white-space: nowrap;
/* Prevent metrics from breaking line */
margin-left: 10px;
}
.regex-debugger-summary {
padding: 8px;
margin-bottom: 10px;
border: 1px solid var(--SmartThemeBorderColor);
background-color: var(--black30a);
border-radius: 5px;
text-align: center;
font-size: 0.9em;
}
/* New highlight color for added text */
mark.green_hl {
background-color: #28a745;
/* A standard green color */
color: white;
}
/* New highlight color for deleted text */
mark.red_hl {
background-color: #dc3545;
/* A standard red color */
color: white;
text-decoration: line-through;
}
/* Styles for the expanded view with navigation */
.expanded-regex-container {
display: flex;
height: 75vh;
/* Give the container a good height */
overflow: hidden;
}
.expanded-regex-nav {
flex: 0 0 200px;
/* Fixed width for the nav bar */
border-right: 1px solid var(--SmartThemeBorderColor);
padding: 5px;
overflow-y: auto;
background-color: var(--black30a);
}
.expanded-regex-nav a {
display: block;
padding: 6px 8px;
text-decoration: none;
color: var(--SmartThemeMainColor);
border-radius: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.expanded-regex-nav a:hover {
background-color: var(--background_hover_color);
}
.expanded-regex-nav a.active {
background-color: var(--highlight_color);
color: var(--text_color_black);
}
.expanded-regex-content {
flex-grow: 1;
overflow-y: auto;
padding-left: 10px;
}
#regex_debugger_render_mode {
padding-right: 20px;
margin-top: 5px;
}
.regex-popup-content {
white-space: pre-wrap;
word-break: break-all;
text-align: left;
}

View File

@@ -0,0 +1,179 @@
<div class="regex-debugger-container">
<!-- Rules List Column -->
<div class="regex-debugger-rules-list">
<h3>
<i class="fa-solid fa-list-ol"></i>
<span data-i18n="ext_regex_debugger_active_rules"
>Active Rules</span
>
</h3>
<div class="flex-container">
<button
id="regex_debugger_save_order"
class="menu_button menu_button_icon interactable"
data-i18n="[title]ext_regex_debugger_save_order_help"
title="Save current rule order"
tabindex="0"
>
<i class="fa-solid fa-floppy-disk"></i>
<span data-i18n="ext_regex_debugger_save_order"
>Save Order</span
>
</button>
</div>
<ul id="regex_debugger_rules" class="sortable-list">
<!-- Rules will be populated here by JavaScript -->
</ul>
</div>
<!-- Testing Area Column -->
<div class="regex-debugger-tester">
<h3>
<i class="fa-solid fa-vial"></i>
<span data-i18n="ext_regex_debugger_testing_area"
>Testing Area</span
>
</h3>
<div class="regex-debugger-io">
<div class="regex-debugger-input">
<label
for="regex_debugger_raw_input"
data-i18n="ext_regex_debugger_raw_input"
>Raw Input</label
>
<textarea
id="regex_debugger_raw_input"
class="text_pole autoSetHeight"
rows="4"
></textarea>
</div>
<div
id="regex_debugger_run_test_header"
class="flex-container"
>
<button
id="regex_debugger_run_test"
class="menu_button menu_button_icon interactable"
data-i18n="[title]ext_regex_debugger_run_test_help"
title="Run the test pipeline"
tabindex="0"
>
<i class="fa-solid fa-play"></i>
<span data-i18n="ext_regex_debugger_run_test"
>Run Test</span
>
</button>
<div class="flex-container gap10px">
<div class="radio_group">
<label
><input
type="radio"
name="display_mode"
value="replace"
checked
/>
<span data-i18n="ext_regex_debugger_display_replace"
>Replace</span
></label
>
<label
><input
type="radio"
name="display_mode"
value="highlight"
/>
<span
data-i18n="ext_regex_debugger_display_highlight"
>Highlight</span
></label
>
</div>
<select
id="regex_debugger_render_mode"
>
<option
value="text"
data-i18n="ext_regex_debugger_render_text"
>
Render as Text
</option>
<option
value="message"
data-i18n="ext_regex_debugger_render_message"
>
Render as Message
</option>
</select>
</div>
</div>
<div class="regex-debugger-results">
<div class="results-header">
<h4>
<i class="fa-solid fa-shoe-prints"></i>
<span data-i18n="ext_regex_debugger_step_by_step"
>Step-by-step Transformation</span
>
</h4>
<div
id="regex_debugger_expand_steps"
class="menu_button menu_button_icon"
data-i18n="[title]Expand view"
title="Expand view"
>
<i class="fa-solid fa-expand"></i>
</div>
</div>
<div id="regex_debugger_steps_output" class="results-box"></div>
<div class="results-header">
<h4>
<i class="fa-solid fa-flag-checkered"></i>
<span data-i18n="ext_regex_debugger_final_output"
>Final Output</span
>
</h4>
<div
id="regex_debugger_expand_final"
class="menu_button menu_button_icon"
data-i18n="[title]Expand view"
title="Expand view"
>
<i class="fa-solid fa-expand"></i>
</div>
</div>
<div
id="regex_debugger_final_output"
class="results-box final-output"
></div>
</div>
</div>
</div>
</div>
<!-- Template for a single rule item -->
<template id="regex_debugger_rule_template">
<li class="regex-debugger-rule" draggable="true">
<i class="fa-solid fa-grip-vertical handle"></i>
<label class="checkbox">
<input type="checkbox" class="rule-enabled" checked />
</label>
<div class="rule-details">
<span class="rule-name"></span>
<code class="rule-regex"></code>
<small class="rule-scope"></small>
</div>
<div class="menu_button menu_button_icon edit_rule" data-i18n="[title]Edit Rule" title="Edit Rule">
<i class="fa-solid fa-pencil"></i>
</div>
</li>
</template>
<!-- Template for a single transformation step -->
<template id="regex_debugger_step_template">
<div class="step-result">
<div class="step-header">
<strong></strong>
</div>
<pre class="step-output"></pre>
</div>
</template>

View File

@@ -0,0 +1,130 @@
<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_preset_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_preset_script_desc" title="New preset regex script">
<i class="fa-solid fa-sliders"></i>
<small data-i18n="ext_regex_new_preset_script">+ Preset</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 />
<label for="regex_bulk_edit" class="menu_button menu_button_icon">
<input id="regex_bulk_edit" type="checkbox" class="displayNone" />
<i class="fa-solid fa-edit"></i>
<small data-i18n="ext_regex_bulk_edit">Bulk Edit</small>
</label>
<div id="open_regex_debugger" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_debugger_desc" title="Advanced Regex Debugger">
<i class="fa-solid fa-bug-slash"></i>
<small data-i18n="ext_regex_debugger">Debugger</small>
</div>
</div>
<hr class="regex_bulk_operations_hr" />
<div class="regex_bulk_operations flex-container">
<div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All">
<i class="fa-solid fa-check-double"></i>
</div>
<div id="bulk_enable_regex" class="menu_button menu_button_icon">
<i class="fa-solid fa-toggle-on"></i>
<small data-i18n="Enable">Enable</small>
</div>
<div id="bulk_disable_regex" class="menu_button menu_button_icon">
<i class="fa-solid fa-toggle-off"></i>
<small data-i18n="Disable">Disable</small>
</div>
<div id="bulk_regex_move_to_global" class="menu_button menu_button_icon" hidden>
<i class="fa-solid fa-globe"></i>
<small data-i18n="ext_regex_move_to_global">Move to global scripts</small>
</div>
<div id="bulk_regex_move_to_preset" class="menu_button menu_button_icon" hidden>
<i class="fa-solid fa-sliders"></i>
<small data-i18n="ext_regex_move_to_preset">Move to preset scripts</small>
</div>
<div id="bulk_regex_move_to_scoped" class="menu_button menu_button_icon" hidden>
<i class="fa-solid fa-address-card"></i>
<small data-i18n="ext_regex_move_to_scoped">Move to scoped scripts</small>
</div>
<div id="bulk_export_regex" class="menu_button menu_button_icon">
<i class="fa-solid fa-file-export"></i>
<small data-i18n="Export">Export</small>
</div>
<div id="bulk_delete_regex" class="menu_button menu_button_icon">
<i class="fa-solid fa-trash"></i>
<small data-i18n="Delete">Delete</small>
</div>
</div>
<hr />
<div id="regex_presets_block">
<div class="flex-container alignItemsBaseline">
<strong class="flex1" data-i18n="ext_regex_presets">Regex Presets</strong>
</div>
<small data-i18n="ext_regex_presets_desc">
Save and switch between groups of enabled regex scripts.
</small>
<div class="flex-container marginTop5">
<select id="regex_presets" class="text_pole flex1"></select>
<div id="regex_preset_create" class="menu_button fa-solid fa-file-circle-plus" data-i18n="[title]ext_regex_preset_create" title="Create a new regex preset"></div>
<div id="regex_preset_update" class="menu_button fa-solid fa-save" data-i18n="[title]ext_regex_preset_update" title="Update existing regex preset"></div>
<div id="regex_preset_apply" class="menu_button fa-solid fa-recycle" data-i18n="[title]ext_regex_preset_apply" title="Re-apply current preset"></div>
<div id="regex_preset_delete" class="menu_button fa-solid fa-trash" data-i18n="[title]ext_regex_preset_delete" title="Delete current preset"></div>
</div>
</div>
<hr />
<div id="global_scripts_block">
<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" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
<hr />
<div id="preset_scripts_block">
<div class="flex-container alignItemsBaseline">
<strong class="flex1" data-i18n="ext_regex_preset_scripts">Preset Scripts</strong>
<label id="toggle_preset_regex" class="checkbox flex-container" for="regex_preset_toggle">
<input type="checkbox" id="regex_preset_toggle" class="enable_scoped" />
<span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" data-i18n="[title]ext_regex_disallow_preset" title="Disallow using preset regex"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" data-i18n="[title]ext_regex_allow_preset" title="Allow using preset regex"></span>
</label>
</div>
<small data-i18n="ext_regex_preset_scripts_desc">
Only available for this preset. Saved to the preset data.
</small>
<div id="saved_preset_scripts" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
<hr />
<div id="scoped_scripts_block">
<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" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
</div>
</div>
</div>

View 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.sillytavern.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.sillytavern.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, $1, $2, etc. for numbered capture groups, or $&lt;name&gt; for named capture groups." rows="3"></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="9999" 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="9999" 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 &lcub;&lcub;macros&rcub;&rcub; 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="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.&#13;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>

View File

@@ -0,0 +1,5 @@
<div>
<h3 data-i18n="This character has embedded regex script(s).">This character has embedded regex script(s).</h3>
<h3 data-i18n="Would you like to allow using them?">Would you like to allow using them?</h3>
<div class="m-b-1" data-i18n="If you want to do it later, select 'Regex' from the extensions menu.">If you want to do it later, select "Regex" from the extensions menu.</div>
</div>

View File

@@ -0,0 +1,465 @@
import { characters, saveSettingsDebounced, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js';
import { extension_settings, writeExtensionField } from '../../extensions.js';
import { getPresetManager } from '../../preset-manager.js';
import { regexFromString } from '../../utils.js';
import { lodash } from '../../../lib.js';
/**
* @readonly
* @enum {number} Regex scripts types
*/
export const SCRIPT_TYPES = {
// ORDER MATTERS: defines the regex script priority
GLOBAL: 0,
PRESET: 2,
SCOPED: 1,
};
/**
* Special type for unknown/invalid script types.
*/
export const SCRIPT_TYPE_UNKNOWN = -1;
/**
* @typedef {import('../../char-data.js').RegexScriptData} RegexScript
*/
/**
* @typedef {object} GetRegexScriptsOptions
* @property {boolean} allowedOnly Only return allowed scripts
*/
/**
* @type {Readonly<GetRegexScriptsOptions>}
*/
const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false });
/**
* Manages the compiled regex cache with LRU eviction.
*/
export class RegexProvider {
/** @type {Map<string, RegExp>} */
#cache = new Map();
/** @type {number} */
#maxSize = 1000;
static instance = new RegexProvider();
/**
* Gets a regex instance by its string representation.
* @param {string} regexString The regex string to retrieve
* @returns {RegExp?} Compiled regex or null if invalid
*/
get(regexString) {
const isCached = this.#cache.has(regexString);
const regex = isCached
? this.#cache.get(regexString)
: regexFromString(regexString);
if (!regex) {
return null;
}
if (isCached) {
// LRU: Move to end by re-inserting
this.#cache.delete(regexString);
this.#cache.set(regexString, regex);
} else {
// Evict oldest if at capacity
if (this.#cache.size >= this.#maxSize) {
const firstKey = this.#cache.keys().next().value;
this.#cache.delete(firstKey);
}
this.#cache.set(regexString, regex);
}
// Reset lastIndex for global/sticky regexes
if (regex.global || regex.sticky) {
regex.lastIndex = 0;
}
return regex;
}
/**
* Clears the entire cache.
*/
clear() {
this.#cache.clear();
}
}
/**
* Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data
*
* @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts
* @returns {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information.
*/
export function getRegexScripts(options = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) {
return [...Object.values(SCRIPT_TYPES).flatMap(type => getScriptsByType(type, options))];
}
/**
* Retrieves the regex scripts for a specific type.
* @param {SCRIPT_TYPES} scriptType The type of regex scripts to retrieve.
* @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts
* @returns {RegexScript[]} An array of regex scripts for the specified type.
*/
export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) {
switch (scriptType) {
case SCRIPT_TYPE_UNKNOWN:
return [];
case SCRIPT_TYPES.GLOBAL:
return extension_settings.regex ?? [];
case SCRIPT_TYPES.SCOPED: {
if (allowedOnly && !extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar)) {
return [];
}
const scopedScripts = characters[this_chid]?.data?.extensions?.regex_scripts;
return Array.isArray(scopedScripts) ? scopedScripts : [];
}
case SCRIPT_TYPES.PRESET: {
if (allowedOnly && !extension_settings?.preset_allowed_regex?.[getCurrentPresetAPI()]?.includes(getCurrentPresetName())) {
return [];
}
const presetManager = getPresetManager();
const presetScripts = presetManager?.readPresetExtensionField({ path: 'regex_scripts' });
return Array.isArray(presetScripts) ? presetScripts : [];
}
default:
console.warn(`getScriptsByType: Invalid script type ${scriptType}`);
return [];
}
}
/**
* Saves an array of regex scripts for a specific type.
* @param {RegexScript[]} scripts An array of regex scripts to save.
* @param {SCRIPT_TYPES} scriptType The type of regex scripts to save.
* @returns {Promise<void>}
*/
export async function saveScriptsByType(scripts, scriptType) {
switch (scriptType) {
case SCRIPT_TYPES.GLOBAL:
extension_settings.regex = scripts;
saveSettingsDebounced();
break;
case SCRIPT_TYPES.SCOPED:
await writeExtensionField(this_chid, 'regex_scripts', scripts);
break;
case SCRIPT_TYPES.PRESET: {
const presetManager = getPresetManager();
await presetManager.writePresetExtensionField({ path: 'regex_scripts', value: scripts });
break;
}
default:
console.warn(`saveScriptsByType: Invalid script type ${scriptType}`);
break;
}
}
/**
* Check if character's regexes are allowed to be used; if character is undefined, returns false
* @param {Character|undefined} character
* @returns {boolean}
*/
export function isScopedScriptsAllowed(character) {
return !!extension_settings?.character_allowed_regex?.includes(character?.avatar);
}
/**
* Allow character's regexes to be used; if character is undefined, do nothing
* @param {Character|undefined} character
* @returns {void}
*/
export function allowScopedScripts(character) {
const avatar = character?.avatar;
if (!avatar) {
return;
}
if (!Array.isArray(extension_settings?.character_allowed_regex)) {
extension_settings.character_allowed_regex = [];
}
if (!extension_settings.character_allowed_regex.includes(avatar)) {
extension_settings.character_allowed_regex.push(avatar);
saveSettingsDebounced();
}
}
/**
* Disallow character's regexes to be used; if character is undefined, do nothing
* @param {Character|undefined} character
* @returns {void}
*/
export function disallowScopedScripts(character) {
const avatar = character?.avatar;
if (!avatar) {
return;
}
if (!Array.isArray(extension_settings?.character_allowed_regex)) {
return;
}
const index = extension_settings.character_allowed_regex.indexOf(avatar);
if (index !== -1) {
extension_settings.character_allowed_regex.splice(index, 1);
saveSettingsDebounced();
}
}
/**
* Check if preset's regexes are allowed to be used
* @param {string} apiId API ID
* @param {string} presetName Preset name
* @returns {boolean} True if allowed, false if not
*/
export function isPresetScriptsAllowed(apiId, presetName) {
if (!apiId || !presetName) {
return false;
}
return !!extension_settings?.preset_allowed_regex?.[apiId]?.includes(presetName);
}
/**
* Allow preset's regexes to be used
* @param {string} apiId API ID
* @param {string} presetName Preset name
* @returns {void}
*/
export function allowPresetScripts(apiId, presetName) {
if (!apiId || !presetName) {
return;
}
if (!Array.isArray(extension_settings?.preset_allowed_regex?.[apiId])) {
lodash.set(extension_settings, ['preset_allowed_regex', apiId], []);
}
if (!extension_settings.preset_allowed_regex[apiId].includes(presetName)) {
extension_settings.preset_allowed_regex[apiId].push(presetName);
saveSettingsDebounced();
}
}
/**
* Disallow preset's regexes to be used
* @param {string} apiId API ID
* @param {string} presetName Preset name
* @returns {void}
*/
export function disallowPresetScripts(apiId, presetName) {
if (!apiId || !presetName) {
return;
}
if (!Array.isArray(extension_settings?.preset_allowed_regex?.[apiId])) {
return;
}
const index = extension_settings.preset_allowed_regex[apiId].indexOf(presetName);
if (index !== -1) {
extension_settings.preset_allowed_regex[apiId].splice(index, 1);
saveSettingsDebounced();
}
}
/**
* Gets the current API ID from the preset manager.
* @returns {string|null} Current API ID, or null if no preset manager
*/
export function getCurrentPresetAPI() {
return getPresetManager()?.apiId ?? null;
}
/**
* Gets the name of the currently selected preset.
* @returns {string|null} The name of the currently selected preset, or null if no preset manager
*/
export function getCurrentPresetName() {
return getPresetManager()?.getSelectedPresetName() ?? null;
}
/**
* @readonly
* @enum {number} Where the regex script should be applied
*/
export 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,
};
/**
* @readonly
* @enum {number} How to substitute parameters in the find regex
*/
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;
}
/**
* 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
*/
export 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 = getRegexScripts({ allowedOnly: true });
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 {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
*/
export 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 = RegexProvider.instance.get(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, groupName) => {
if (num) {
// Handle numbered capture groups ($1, $2, etc.)
match = args[Number(num)];
} else if (groupName) {
// Handle named capture groups ($<name>)
const groups = args[args.length - 1];
match = groups && typeof groups === 'object' && groups[groupName];
}
// No match found - return the empty string
if (!match) {
return '';
}
// Remove trim strings from the match
const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride });
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, { name2Override: characterOverride });
finalString = finalString.replaceAll(subTrimString, '');
});
return finalString;
}

View File

@@ -0,0 +1,25 @@
<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_preset">
<input type="radio" name="regex_import_target" id="regex_import_target_preset" value="preset" />
<span data-i18n="ext_regex_preset_scripts">
Preset 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>

File diff suppressed because it is too large Load Diff

View 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/SillyTavern/SillyTavern"
}

View File

@@ -0,0 +1,5 @@
<div>
<h3 data-i18n="This preset has embedded regex script(s).">This preset has embedded regex script(s).</h3>
<h3 data-i18n="Would you like to allow using them?">Would you like to allow using them?</h3>
<div class="m-b-1" data-i18n="If you want to do it later, select 'Regex' from the extensions menu.">If you want to do it later, select "Regex" from the extensions menu.</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="regex-script-label flex-container flexnowrap">
<input type="checkbox" class="regex_bulk_checkbox" />
<span class="drag-handle menu-handle">&#9776;</span>
<div class="regex_script_name flex1 overflow-hidden"></div>
<div class="flex-container flexnowrap">
<label class="checkbox flex-container margin-r5" 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>
<label class="menu_button regex_script_expand" title="Show more options" data-i18n="[title]Show more options">
<input type="checkbox" name="regex_expand" />
<span class="fa-solid fa-ellipsis"></span>
</label>
<div class="flex-container regex_script_buttons">
<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-globe"></i>
</div>
<div class="move_to_preset menu_button" data-i18n="[title]ext_regex_move_to_preset" title="Move to preset scripts">
<i class="fa-solid fa-sliders"></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-address-card"></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>
<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="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script">
<i class="fa-solid fa-trash"></i>
</div>
</div>
</div>

View File

@@ -0,0 +1,161 @@
@import "debugger.css";
.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: attr(no-scripts-text);
font-size: 0.95em;
opacity: 0.7;
display: block;
text-align: center;
}
#scoped_scripts_block,
#preset_scripts_block {
opacity: 1;
transition: opacity var(--animation-duration-2x) ease-in-out;
}
#scoped_scripts_block .move_to_scoped,
#global_scripts_block .move_to_global,
#preset_scripts_block .move_to_preset {
display: none;
}
#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)),
#preset_scripts_block:not(:has(#regex_preset_toggle:checked)) {
opacity: 0.5;
}
.enable_scoped:checked~.regex-toggle-on,
.enable_scoped:not(:checked)~.regex-toggle-off {
display: block;
}
.enable_scoped:checked~.regex-toggle-off,
.enable_scoped:not(:checked)~.regex-toggle-on {
display: none;
}
.regex-script-label {
align-items: baseline;
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 var(--animation-duration-2x) ease-in-out;
}
.regex-toggle-off:hover {
opacity: 1;
filter: none;
}
.regex-toggle-on {
cursor: pointer;
}
.disable_regex:checked~.regex-toggle-on,
.disable_regex:not(:checked)~.regex-toggle-off {
display: none;
}
.disable_regex:not(:checked)~.regex-toggle-on,
.disable_regex:checked~.regex-toggle-off {
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%);
}
.regex_settings label[for="regex_bulk_edit"]:has(#regex_bulk_edit:checked) {
color: var(--golden);
}
.regex_settings .regex-script-container .regex-script-label .regex_bulk_checkbox {
margin-left: 5px;
margin-right: 5px;
}
.regex_settings .regex_bulk_operations,
.regex_settings .regex_bulk_checkbox,
.regex_settings .regex_bulk_operations_hr {
display: none;
}
.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations {
display: flex;
}
.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations_hr {
display: block;
}
.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox {
display: inline-grid;
}
@supports not selector(:has(*)) {
.regex-script-label label.regex_script_expand {
display: none;
}
.regex-script-label .regex_script_buttons {
display: flex;
}
}
.regex-script-label label.regex_script_expand input[name="regex_expand"],
.regex-script-label:has(input[name="regex_expand"]:checked) label.regex_script_expand,
.regex-script-label:not(:has(input[name="regex_expand"]:checked)) .regex_script_buttons {
display: none;
}