Files
SillyTavern/public/scripts/extensions/regex/engine.js
kingbri 3c3eed0996 Regex: Change how MD formatting works
Previously, there was a checkbox to select placement as MD display.
This was counterintuitive since some users believed MD display
enclosed the other placement options.

Now, add a universal switch to finely tune what parts of a chat should
be excluded with MD formatting. For example a regex with the option
enabled with AI output selected will only format replies from the
character.

This is an improvement since users can now choose what parts of a
chat to format rather than sticking with a global display specifier.

Signed-off-by: kingbri <bdashore3@proton.me>
2023-07-08 14:10:15 -04:00

169 lines
5.4 KiB
JavaScript

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,
SYSTEM: 3,
SENDAS: 4
}
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) {
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;
if (replaceStrategy === regex_replace_strategy.OVERLAY) {
const splitReplace = finalString.split("{{match}}");
// There's a prefix
if (splitReplace[0]) {
const splicedPrefix = spliceSymbols(splitReplace[0], false);
overlaidMatch = overlaidMatch.replace(splicedPrefix, "").trim();
}
// There's a suffix
if (splitReplace[1]) {
const splicedSuffix = spliceSymbols(splitReplace[1], true);
overlaidMatch = overlaidMatch.replace(new RegExp(`${splicedSuffix}$`), "").trim();
}
}
// Only one match is replaced. This is by design
finalString = finalString.replace("{{match}}", overlaidMatch) || finalString.replace("{{match}}", regexMatch);
return finalString;
}
// Splices 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);;
}