| 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'; |
|
|
| |
| |
| |
| |
| export const SCRIPT_TYPES = { |
| |
| GLOBAL: 0, |
| PRESET: 2, |
| SCOPED: 1, |
| }; |
|
|
| |
| |
| |
| export const SCRIPT_TYPE_UNKNOWN = -1; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false }); |
|
|
| |
| |
| |
| export class RegexProvider { |
| |
| #cache = new Map(); |
| |
| #maxSize = 1000; |
|
|
| static instance = new RegexProvider(); |
|
|
| |
| |
| |
| |
| |
| get(regexString) { |
| const isCached = this.#cache.has(regexString); |
| const regex = isCached |
| ? this.#cache.get(regexString) |
| : regexFromString(regexString); |
|
|
| if (!regex) { |
| return null; |
| } |
|
|
| if (isCached) { |
| |
| this.#cache.delete(regexString); |
| this.#cache.set(regexString, regex); |
| } else { |
| |
| if (this.#cache.size >= this.#maxSize) { |
| const firstKey = this.#cache.keys().next().value; |
| this.#cache.delete(firstKey); |
| } |
| this.#cache.set(regexString, regex); |
| } |
|
|
| |
| if (regex.global || regex.sticky) { |
| regex.lastIndex = 0; |
| } |
|
|
| return regex; |
| } |
|
|
| |
| |
| |
| clear() { |
| this.#cache.clear(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function getRegexScripts(options = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { |
| return [...Object.values(SCRIPT_TYPES).flatMap(type => getScriptsByType(type, options))]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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 []; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function isScopedScriptsAllowed(character) { |
| return !!extension_settings?.character_allowed_regex?.includes(character?.avatar); |
| } |
|
|
| |
| |
| |
| |
| |
| 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(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| 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(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function isPresetScriptsAllowed(apiId, presetName) { |
| if (!apiId || !presetName) { |
| return false; |
| } |
| return !!extension_settings?.preset_allowed_regex?.[apiId]?.includes(presetName); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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(); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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(); |
| } |
| } |
|
|
| |
| |
| |
| |
| export function getCurrentPresetAPI() { |
| return getPresetManager()?.apiId ?? null; |
| } |
|
|
| |
| |
| |
| |
| export function getCurrentPresetName() { |
| return getPresetManager()?.getSelectedPresetName() ?? null; |
| } |
|
|
| |
| |
| |
| |
| export const regex_placement = { |
| |
| |
| |
| MD_DISPLAY: 0, |
| USER_INPUT: 1, |
| AI_OUTPUT: 2, |
| SLASH_COMMAND: 3, |
| |
| WORLD_INFO: 5, |
| REASONING: 6, |
| }; |
|
|
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) { |
| |
| 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.markdownOnly && isMarkdown) || |
| |
| (script.promptOnly && isPrompt) || |
| |
| (!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; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
|
|
| |
| if (!findRegex) { |
| return newString; |
| } |
|
|
| |
| 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) { |
| |
| match = args[Number(num)]; |
| } else if (groupName) { |
| |
| const groups = args[args.length - 1]; |
| match = groups && typeof groups === 'object' && groups[groupName]; |
| } |
|
|
| |
| if (!match) { |
| return ''; |
| } |
|
|
| |
| const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride }); |
|
|
| return filteredMatch; |
| }); |
|
|
| |
| return substituteParams(replaceWithGroups); |
| }); |
|
|
| return newString; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function filterString(rawString, trimStrings, { characterOverride } = {}) { |
| let finalString = rawString; |
| trimStrings.forEach((trimString) => { |
| const subTrimString = substituteParams(trimString, { name2Override: characterOverride }); |
| finalString = finalString.replaceAll(subTrimString, ''); |
| }); |
|
|
| return finalString; |
| } |
|
|