SillyTavern/public/scripts/extensions/regex/engine.js

200 lines
6.8 KiB
JavaScript
Raw Normal View History

2023-12-02 19:04:51 +01:00
import { substituteParams } from '../../../script.js';
import { extension_settings } from '../../extensions.js';
2023-07-20 19:32:15 +02:00
export {
regex_placement,
getRegexedString,
2023-12-02 21:06:57 +01:00
runRegexScript,
2023-12-02 20:11:06 +01:00
};
2023-07-20 19:32:15 +02:00
const regex_placement = {
// MD Display is deprecated. Do not use.
MD_DISPLAY: 0,
USER_INPUT: 1,
AI_OUTPUT: 2,
2023-12-02 21:06:57 +01:00
SLASH_COMMAND: 3,
2023-12-02 20:11:06 +01:00
};
2023-07-20 19:32:15 +02:00
const regex_replace_strategy = {
REPLACE: 0,
2023-12-02 21:06:57 +01:00
OVERLAY: 1,
2023-12-02 20:11:06 +01:00
};
2023-07-20 19:32:15 +02:00
// 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);
2023-07-20 19:32:15 +02:00
// Invalid flags
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
return RegExp(input);
}
2023-07-20 19:32:15 +02:00
// 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, isPrompt } = {}) {
2023-07-20 19:32:15 +02:00
let finalString = rawString;
2023-12-02 19:04:51 +01:00
if (extension_settings.disabledExtensions.includes('regex') || !rawString || placement === undefined) {
2023-07-20 19:32:15 +02:00
return finalString;
}
extension_settings.regex.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)
) {
if (script.placement.includes(placement)) {
finalString = runRegexScript(script, finalString, { characterOverride });
}
2023-07-20 19:32:15 +02:00
}
});
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,
{
2023-07-20 19:32:15 +02:00
characterOverride,
2023-12-02 21:06:57 +01:00
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE,
},
2023-07-20 19:32:15 +02:00
);
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);
2023-12-02 19:04:51 +01:00
finalString = finalString.replaceAll(subTrimString, '');
2023-07-20 19:32:15 +02:00
});
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) {
2023-12-02 19:04:51 +01:00
const splitReplace = finalString.split('{{match}}');
2023-07-20 19:32:15 +02:00
// 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
2023-12-02 19:04:51 +01:00
finalString = finalString.replace('{{match}}', overlaidMatch) || finalString.replace('{{match}}', regexMatch);
2023-07-20 19:32:15 +02:00
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);
}