🎉 初始化项目
This commit is contained in:
263
web-app/public/scripts/extensions/regex/debugger.css
Normal file
263
web-app/public/scripts/extensions/regex/debugger.css
Normal 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;
|
||||
}
|
||||
179
web-app/public/scripts/extensions/regex/debugger.html
Normal file
179
web-app/public/scripts/extensions/regex/debugger.html
Normal 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>
|
||||
130
web-app/public/scripts/extensions/regex/dropdown.html
Normal file
130
web-app/public/scripts/extensions/regex/dropdown.html
Normal 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>
|
||||
164
web-app/public/scripts/extensions/regex/editor.html
Normal file
164
web-app/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.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 $<name> 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 {{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="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>
|
||||
@@ -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>
|
||||
465
web-app/public/scripts/extensions/regex/engine.js
Normal file
465
web-app/public/scripts/extensions/regex/engine.js
Normal 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;
|
||||
}
|
||||
25
web-app/public/scripts/extensions/regex/importTarget.html
Normal file
25
web-app/public/scripts/extensions/regex/importTarget.html
Normal 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>
|
||||
2127
web-app/public/scripts/extensions/regex/index.js
Normal file
2127
web-app/public/scripts/extensions/regex/index.js
Normal file
File diff suppressed because it is too large
Load Diff
11
web-app/public/scripts/extensions/regex/manifest.json
Normal file
11
web-app/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/SillyTavern/SillyTavern"
|
||||
}
|
||||
@@ -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>
|
||||
36
web-app/public/scripts/extensions/regex/scriptTemplate.html
Normal file
36
web-app/public/scripts/extensions/regex/scriptTemplate.html
Normal 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">☰</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>
|
||||
161
web-app/public/scripts/extensions/regex/style.css
Normal file
161
web-app/public/scripts/extensions/regex/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user