Initial commit

This commit is contained in:
Cohee
2023-07-20 20:32:15 +03:00
commit edd41989fd
362 changed files with 76295 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
<div class="regex_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Regex</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div id="open_regex_editor" class="menu_button">
<i class="fa-solid fa-pen-to-square"></i>
<span>Open Editor</span>
</div>
<hr />
<label>Saved Scripts</label>
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,107 @@
<div id="regex_editor_template">
<div class="regex_editor">
<h3><strong data-i18n="Regex Editor">Regex Editor</strong>
<a href="https://regexr.com/" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
</h3>
<small class="flex-container extensions_info">
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 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"
placeholder="Use {{match}} to include the matched text from the Find Regex"
rows="2"
></textarea>
</div>
</div>
<div class="flex1">
<label for="regex_trim_strings" class="title_restorable">
<small data-i18n="Trim Out">Trim Out</small>
</label>
<div>
<textarea
class="regex_trim_strings text_pole wide100p textarea_compact"
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="wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
<small>Affects</small>
<div>
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="1">
<span data-i18n="Before Char">User Input</span>
</label>
</div>
<div>
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="2">
<span data-i18n="After Char">AI Output</span>
</label>
</div>
<div>
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="3">
<span data-i18n="Slash Commands">Slash Commands</span>
</label>
</div>
</div>
<div class="wi-enter-footer-text flex-container flexFlowColumn flexNoGap alignitemsstart">
<small>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">
<input type="checkbox" name="only_format_display" />
<span data-i18n="Only Format Display">Only Format Display</span>
</label>
<label class="checkbox flex-container">
<input type="checkbox" name="run_on_edit" />
<span data-i18n="Run On Edit">Run On Edit</span>
</label>
<label class="checkbox flex-container">
<input type="checkbox" name="substitute_regex" />
<span data-i18n="Substitute Regex">Substitute Regex</span>
</label>
</div>
<div class="flex-container flexFlowColumn alignitemsstart">
<small>Replacement Strategy</small>
<select name="replace_strategy_select" class="margin0">
<option value="0">Replace</option>
<option value="1">Overlay</option>
</select>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,194 @@
import { substituteParams } from "../../../script.js";
import { extension_settings } from "../../extensions.js";
export {
regex_placement,
getRegexedString,
runRegexScript
}
const regex_placement = {
// MD Display is deprecated. Do not use.
MD_DISPLAY: 0,
USER_INPUT: 1,
AI_OUTPUT: 2,
SLASH_COMMAND: 3
}
const regex_replace_strategy = {
REPLACE: 0,
OVERLAY: 1
}
// Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
function regexFromString(input) {
try {
// Parse input
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
// Invalid flags
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
return RegExp(input);
}
// Create the regular expression
return new RegExp(m[2], m[3]);
} catch {
return;
}
}
// Parent function to fetch a regexed version of a raw string
function getRegexedString(rawString, placement, { characterOverride, isMarkdown } = {}) {
let finalString = rawString;
if (extension_settings.disabledExtensions.includes("regex") || !rawString || placement === undefined) {
return finalString;
}
extension_settings.regex.forEach((script) => {
if ((script.markdownOnly && !isMarkdown) || (!script.markdownOnly && isMarkdown)) {
return;
}
if (script.placement.includes(placement)) {
finalString = runRegexScript(script, finalString, { characterOverride });
}
});
return finalString;
}
// Runs the provided regex script on the given string
function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
let newString = rawString;
if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) {
return newString;
}
let match;
const findRegex = regexFromString(regexScript.substituteRegex ? substituteParams(regexScript.findRegex) : regexScript.findRegex);
// The user skill issued. Return with nothing.
if (!findRegex) {
return newString;
}
while ((match = findRegex.exec(rawString)) !== null) {
const fencedMatch = match[0];
const capturedMatch = match[1];
let trimCapturedMatch;
let trimFencedMatch;
if (capturedMatch) {
const tempTrimCapture = filterString(capturedMatch, regexScript.trimStrings, { characterOverride });
trimFencedMatch = fencedMatch.replaceAll(capturedMatch, tempTrimCapture);
trimCapturedMatch = tempTrimCapture;
} else {
trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings, { characterOverride });
}
// TODO: Use substrings for replacement. But not necessary at this time.
// A substring is from match.index to match.index + match[0].length or fencedMatch.length
const subReplaceString = substituteRegexParams(
regexScript.replaceString,
trimCapturedMatch ?? trimFencedMatch,
{
characterOverride,
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE
}
);
if (!newString) {
newString = rawString.replace(fencedMatch, subReplaceString);
} else {
newString = newString.replace(fencedMatch, subReplaceString);
}
// If the regex isn't global, break out of the loop
if (!findRegex.flags.includes('g')) {
break;
}
}
return newString;
}
// Filters anything to trim from the regex match
function filterString(rawString, trimStrings, { characterOverride } = {}) {
let finalString = rawString;
trimStrings.forEach((trimString) => {
const subTrimString = substituteParams(trimString, undefined, characterOverride);
finalString = finalString.replaceAll(subTrimString, "");
});
return finalString;
}
// Substitutes regex-specific and normal parameters
function substituteRegexParams(rawString, regexMatch, { characterOverride, replaceStrategy } = {}) {
let finalString = rawString;
finalString = substituteParams(finalString, undefined, characterOverride);
let overlaidMatch = regexMatch;
// TODO: Maybe move the for loops into a separate function?
if (replaceStrategy === regex_replace_strategy.OVERLAY) {
const splitReplace = finalString.split("{{match}}");
// There's a prefix
if (splitReplace[0]) {
// Fetch the prefix
const splicedPrefix = spliceSymbols(splitReplace[0], false);
// Sequentially remove all occurrences of prefix from start of split
const splitMatch = overlaidMatch.split(splicedPrefix);
let sliceNum = 0;
for (let index = 0; index < splitMatch.length; index++) {
if (splitMatch[index].length === 0) {
sliceNum++;
} else {
break;
}
}
overlaidMatch = splitMatch.slice(sliceNum, splitMatch.length).join(splicedPrefix);
}
// There's a suffix
if (splitReplace[1]) {
// Fetch the suffix
const splicedSuffix = spliceSymbols(splitReplace[1], true);
// Sequential removal of all suffix occurrences from end of split
const splitMatch = overlaidMatch.split(splicedSuffix);
let sliceNum = 0;
for (let index = splitMatch.length - 1; index >= 0; index--) {
if (splitMatch[index].length === 0) {
sliceNum++;
} else {
break;
}
}
overlaidMatch = splitMatch.slice(0, splitMatch.length - sliceNum).join(splicedSuffix);
}
}
// Only one match is replaced. This is by design
finalString = finalString.replace("{{match}}", overlaidMatch) || finalString.replace("{{match}}", regexMatch);
return finalString;
}
// Splices common sentence symbols and whitespace from the beginning and end of a string
// Using a for loop due to sequential ordering
function spliceSymbols(rawString, isSuffix) {
let offset = 0;
for (const ch of isSuffix ? rawString.split('').reverse() : rawString) {
if (ch.match(/[^\w.,?'!]/)) {
offset++;
} else {
break;
}
}
return isSuffix ? rawString.substring(0, rawString.length - offset) : rawString.substring(offset);
}

View File

@@ -0,0 +1,260 @@
import { callPopup, eventSource, event_types, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from "../../../script.js";
import { extension_settings } from "../../extensions.js";
import { uuidv4, waitUntilCondition } from "../../utils.js";
import { regex_placement } from "./engine.js";
async function saveRegexScript(regexScript, existingScriptIndex) {
// If not editing
if (existingScriptIndex === -1) {
// Is the script name undefined?
if (!regexScript.scriptName) {
toastr.error(`Could not save regex script: The script name was undefined or empty!`);
return;
}
// Does the script name already exist?
if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
return;
}
} else {
// Does the script name already exist somewhere else?
// (If this fails, make it a .filter().map() to index array)
const foundIndex = extension_settings.regex.findIndex((e) => e.scriptName === regexScript.scriptName);
if (foundIndex !== existingScriptIndex && foundIndex !== -1) {
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
return;
}
}
// Is a find regex present?
if (regexScript.findRegex.length === 0) {
toastr.error(`Could not save regex script: A find regex is required!`);
return;
}
// Is there someplace to place results?
if (regexScript.placement.length === 0) {
toastr.error(`Could not save regex script: One placement checkbox must be selected!`);
return;
}
if (existingScriptIndex !== -1) {
extension_settings.regex[existingScriptIndex] = regexScript;
} else {
extension_settings.regex.push(regexScript);
}
saveSettingsDebounced();
await loadRegexScripts();
// Reload the current chat to undo previous markdown
const currentChatId = getCurrentChatId();
if (currentChatId !== undefined && currentChatId !== null) {
await reloadCurrentChat();
}
}
async function deleteRegexScript({ existingId }) {
let scriptName = $(`#${existingId}`).find('.regex_script_name').text();
const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName);
if (!existingScriptIndex || existingScriptIndex !== -1) {
extension_settings.regex.splice(existingScriptIndex, 1);
saveSettingsDebounced();
await loadRegexScripts();
}
}
async function loadRegexScripts() {
$("#saved_regex_scripts").empty();
const scriptTemplate = $(await $.get("scripts/extensions/regex/scriptTemplate.html"));
extension_settings.regex.forEach((script) => {
// Have to clone here
const scriptHtml = scriptTemplate.clone();
scriptHtml.attr('id', uuidv4());
scriptHtml.find('.regex_script_name').text(script.scriptName);
scriptHtml.find('.edit_existing_regex').on('click', async function() {
await onRegexEditorOpenClick(scriptHtml.attr("id"));
});
scriptHtml.find('.delete_regex').on('click', async function() {
await deleteRegexScript({ existingId: scriptHtml.attr("id") });
});
$("#saved_regex_scripts").append(scriptHtml);
});
}
async function onRegexEditorOpenClick(existingId) {
const editorHtml = $(await $.get("scripts/extensions/regex/editor.html"));
// If an ID exists, fill in all the values
let existingScriptIndex = -1;
if (existingId) {
const existingScriptName = $(`#${existingId}`).find('.regex_script_name').text();
existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === existingScriptName);
if (existingScriptIndex !== -1) {
const existingScript = extension_settings.regex[existingScriptIndex];
if (existingScript.scriptName) {
editorHtml.find(`.regex_script_name`).val(existingScript.scriptName);
} else {
toastr.error("This script doesn't have a name! Please delete it.")
return;
}
editorHtml.find(`.find_regex`).val(existingScript.findRegex || "");
editorHtml.find(`.regex_replace_string`).val(existingScript.replaceString || "");
editorHtml.find(`.regex_trim_strings`).val(existingScript.trimStrings?.join("\n") || []);
editorHtml
.find(`input[name="disabled"]`)
.prop("checked", existingScript.disabled ?? false);
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked", existingScript.markdownOnly ?? false);
editorHtml
.find(`input[name="run_on_edit"]`)
.prop("checked", existingScript.runOnEdit ?? false);
editorHtml
.find(`input[name="substitute_regex"]`)
.prop("checked", existingScript.substituteRegex ?? false);
editorHtml
.find(`select[name="replace_strategy_select"]`)
.val(existingScript.replaceStrategy ?? 0);
existingScript.placement.forEach((element) => {
editorHtml
.find(`input[name="replace_position"][value="${element}"]`)
.prop("checked", true);
});
}
} else {
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked", true);
editorHtml
.find(`input[name="run_on_edit"]`)
.prop("checked", true);
editorHtml
.find(`input[name="replace_position"][value="0"]`)
.prop("checked", true);
}
const popupResult = await callPopup(editorHtml, "confirm", undefined, { okButton: "Save" });
if (popupResult) {
const newRegexScript = {
scriptName: editorHtml.find(".regex_script_name").val(),
findRegex: editorHtml.find(".find_regex").val(),
replaceString: editorHtml.find(".regex_replace_string").val(),
trimStrings: editorHtml.find(".regex_trim_strings").val().split("\n").filter((e) => e.length !== 0) || [],
placement:
editorHtml
.find(`input[name="replace_position"]`)
.filter(":checked")
.map(function() { return parseInt($(this).val()) })
.get()
.filter((e) => e !== NaN) || [],
disabled:
editorHtml
.find(`input[name="disabled"]`)
.prop("checked"),
markdownOnly:
editorHtml
.find(`input[name="only_format_display"]`)
.prop("checked"),
runOnEdit:
editorHtml
.find(`input[name="run_on_edit"]`)
.prop("checked"),
substituteRegex:
editorHtml
.find(`input[name="substitute_regex"]`)
.prop("checked"),
replaceStrategy:
parseInt(editorHtml
.find(`select[name="replace_strategy_select"]`)
.find(`:selected`)
.val()) ?? 0
};
saveRegexScript(newRegexScript, existingScriptIndex);
}
}
// Common settings migration function. Some parts will eventually be removed
// TODO: Maybe migrate placement to strings?
function migrateSettings() {
let performSave = false;
// Current: If MD Display is present in placement, remove it and add new placements/MD option
extension_settings.regex.forEach((script) => {
if (script.placement.includes(regex_placement.MD_DISPLAY)) {
script.placement = script.placement.length === 1 ?
Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
script.placement = script.placement.filter((e) => e !== regex_placement.MD_DISPLAY);
script.markdownOnly = true
performSave = true;
}
// Old system and sendas placement migration
// 4 - sendAs
if (script.placement.includes(4)) {
script.placement = script.placement.length === 1 ?
[regex_placement.SLASH_COMMAND] :
script.placement = script.placement.filter((e) => e !== 4);
performSave = true;
}
});
if (performSave) {
saveSettingsDebounced();
}
}
// Workaround for loading in sequence with other extensions
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
jQuery(async () => {
if (extension_settings.regex) {
migrateSettings();
}
// Manually disable the extension since static imports auto-import the JS file
if (extension_settings.disabledExtensions.includes("regex")) {
return;
}
const settingsHtml = await $.get("scripts/extensions/regex/dropdown.html");
$("#extensions_settings2").append(settingsHtml);
$("#open_regex_editor").on("click", function() {
onRegexEditorOpenClick(false);
});
$('#saved_regex_scripts').sortable({
stop: function () {
let newScripts = [];
$('#saved_regex_scripts').children().each(function () {
const scriptName = $(this).find(".regex_script_name").text();
const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
if (existingScript) {
newScripts.push(existingScript);
}
});
extension_settings.regex = newScripts;
saveSettingsDebounced();
console.debug("Regex scripts reordered");
// TODO: Maybe reload regex scripts after move
},
});
await loadRegexScripts();
$("#saved_regex_scripts").sortable("enable");
});

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,12 @@
<div class="regex-script-label flex-container flexnowrap">
<span class="drag-handle menu-handle">&#9776;</span>
<div class="regex_script_name flexGrow overflow-hidden"></div>
<div class="flex-container flexnowrap">
<div class="edit_existing_regex menu_button">
<i class="fa-solid fa-pencil"></i>
</div>
<div class="delete_regex menu_button">
<i class="fa-solid fa-trash"></i>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
.regex_settings .menu_button {
width: fit-content;
display: flex;
gap: 10px;
flex-direction: row;
}
.regex-script-container {
margin-top: 10px;
margin-bottom: 10px;
}
.regex-script-label {
align-items: center;
border: 1px solid rgba(128, 128, 128, 0.5);
border-radius: 10px;
padding: 0 5px;
margin-top: 1px;
margin-bottom: 1px;
}