import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js'; import { extension_settings } from '../../extensions.js'; import { regexFromString } from '../../utils.js'; export { regex_placement, getRegexedString, runRegexScript, }; /** * @enum {number} Where the regex script should be applied */ 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, }; 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; } function getScopedRegex() { const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); if (!isAllowed) { return []; } const scripts = characters[this_chid]?.data?.extensions?.regex_scripts; if (!Array.isArray(scripts)) { return []; } return scripts; } /** * 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 */ 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 = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])]; 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) ) { 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' && depth >= 0) { if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && 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 {import('./index.js').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 */ function runRegexScript(regexScript, rawString, { characterOverride } = {}) { let newString = rawString; if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) { return newString; } const regexString = regexScript.substituteRegex ? substituteParamsExtended(regexScript.findRegex, {}, sanitizeRegexMacro) : regexScript.findRegex; const findRegex = regexFromString(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) => { // Get a full match or a capture group const match = args[Number(num)]; // No match found - return the empty string if (!match) { return ''; } // Remove trim strings from the match const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride }); // TODO: Handle overlay here 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, undefined, characterOverride); finalString = finalString.replaceAll(subTrimString, ''); }); return finalString; }