mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-01-31 11:35:37 +01:00
1d75b98393
* set isForced to true on input * make floating auto-complete follow horizontal scrolling * add callable closure vars * changes to /let and /var for callable closures * fix error message * fix scope for closure arguments * if should return the pipe result from closures * use /run to call closures and no arguments on immediate closures * throw exception from QRs window-function if no match * when to show autocomplete vs info only * autocomplete positioning * autocomplete styling * add theming to autocomplete (theme, dark, light) * improve autocomplete show/hide logic and editor selection * use blur tint color instead of chat tint color and use blur setting * cleanup and docs * use scope macros for QR args * add enter to select autocomplete * fix no executor found * cleanup and comment * fix alias list in help string * fallback to empty string piped value if null or undefined * fix typo * blur textarea on ctrl+enter execute (and refocus after) * stop executeSlashCommand if parser throws * move /let and /var callbacks into functions * switch textarea to monospace when value starts with slash * add double pipe a pipe breaker * fix /? slash * remove some logging * add "/:name" as shorthand for "/run name" after all * move shit around * fix error message * use testRunShorthandEnd * use parseQuotedValue and parseValue to determine name for "/:" QR labels and set names can include spaces * add some adjustments to make autocomplete work properly some hint in there about "/:" would still be nice * add autocomplete style selector * only strip quotes from subcommand if they are at both ends * fix JSDoc * escaping * allow open quotes on dry run * throwing shit at the wall for /: autocomplete * escapes only for symbols * clean up autocomplete * improve performance * fix scope macros * remove unescaping of pipes * fix macros in scope copy * fix "/? slash" * don't run parser for getNameAt if text has not changed * fix options filter * re-enable blur listener * restore selection on non-replace select * fix for escaping first character of value * add support for {{pipe}} and {{var::}} closures * add index support to var macro * add scoped var macro to macro help * more escape fixes * reduce autocomplete render debounce * cleanup * restore old escape handling and parser flag for strict escaping * fix "no match" autocomplete message * add dummy commands for comments and parser flag * fix type annotations * somewhat safer macro replacements * fix autocomplete select on blank / "no match" * fix cutting off handled part in substitution * add parser flag REPLACE_GETVAR Replaces all {{getvar::}} and {{getglobalvar::}} macros with {{var::}}. Inserts a series of command executors before the command with the macros that: - save {{pipe}} to a var - call /getvar or /getglobalvar to get the variable used in the macro - call /let to save the retrieved variable - return the saved {{pipe}} value This helps to avoid double-substitutions when the var values contain text that could be interpreted as macros. * remove old parser * fix send on enter when no match * deal with pipes in quoted values (loose escaping) * add default parser flags to user settings * allow quoted values in unnamed argument * set parser flag without explicit state to "on" * add click hint on parser error toast * dirty more detailed cmd defs * remove name from unnamed arg * move autocomplete into class and floating with details * replace jQuery's trigger('input') on #send_textarea with native events because jQuery does not dispatch the native event * fix ctrl+space * fix arrow navigation * add comments * fix pointer block * add static fromProps * fix up dummy commands * migrate all commands to addCommandObject * remove commented comment command * fix alias in details * add range as argument type * switch to addCommandObject * switch to addCommandObject * fix height * fix floating details position on left * re-enable blur event * use auto width for full details on floating autocomplete * auto-size floating full details * fix typo * re-enable blur listener * don't prevent enter when selected item is fully typed out * add autocomplete details tooltips * add language to slash command examples * move makeItem into option and command and fix click select * use autocomplete parts in /? slash * fix alias formatting * add language to slash command examples * fix details position on initial input history * small screen styles * replace registerSlashCommand with detailed declarations * put name on first line * add missing returns * fix missing comma * fix alias display in autocomplete list * remove args from help string * move parser settings to its own section * jsdoc * hljs stscript lang * add hljs to autocomplete help examples * add missing import * apply autocomplete colors to stscript codeblocks (hljs) * add fromProps * cache autocomplete elements * towards generic autocomplete * remove unused imports * fix blanks * add return types * re-enable blur * fix blank check * Caption messages by id * add aborting command execution * fix return type * fix chat input font reset * add slash command progress indicator * add missing return * mark registerSlashCommand deprecated * why?? * separate abort logic for commands * remove parsing of quoted values from unnamed arg * add adjustable autocomplete width * revert stop button pulse * add progress and pause/abort to QR editor * add resize event on autocomplete width change * add key= argument to all get vars * refactoring * introduce NamedArgumentAsignment * add TODOs * refactoring * record start and end of named arg assignment * refactoring * prevent duplicate calls to show * refactoring * remove macro ac * add secondary autocomplete and enum descriptions * add syntax highlighting to QR editor * add enum descriptions to /while * add /let key=... to scope variable names * add unnamed argument assignment class and unnamed argument splitting * fix QR editor style * remove dash before autocomplete help text * add autocomplete for unnamed enums * fix remaining dom after holding backslash * fix for unnamed enums * fix autocomplete for /parser-flag * add parser-flag enum help * fix type annotations * fix autocomplete result for /: * add colored autocomplete type icons * collapse second line autocomplete help if empty * mark optional named args in autocomplete * fix when what * remove duplicate debug buttons * dispatch input on autocomplete select * prevent grow from editor syntax layer * add auto-adjust qr editor caret color * remove text-shadow from autocomplete * join value strings in /let and /var * add /abort syntax highlight * fix attempting secondary result when there is none * rename settings headers and split autocomplete / stscript * add parser flag tooltips * add tooltips to chat width stops * fix typo * return clone of help item * fix enum string * don't make optional notice for autocomplete arguments smaller * avoid scrollbar in chat input * add rudimentary macro autocomplete * strip macro from helptext * finally remove closure delimiters around root * cleanup * fix index stuff for removed closure delimiters * fix type hint * add child commands to progress indicator * include sub-separator in macro autocomplete * remove all mentions of interruptsGeneration and purge * remove unused imports * fix syntax highlight with newline at end of input * cleanup select pointer events * coalesce onProgress call * add regex to STscript syntax highlighting * fix closure end * fix autocomplete type icon alignment * adjustments for small screens * fix removing wrong element * add missing "at=" arg to /sys, /comment, /sendas * add font scale setting for autocomplete * add target=_blank for parser flag links * fix for searching enums * remove REGEXP_MODE from hljs just causes trouble * fix autocomplete in closures * fix typo * fix type hint * Get rid of scroll bar on load * Add type hint for /send name argument. Fix 'at' types * Add 'negative' arg hint to /sd command * reenable blur event * Allow /summarize to process any text * Compact layout of script toggles * Expand CSS by default * fix double ranger indicator and adjust to narrow container * make custom css input fill available vertical space * reduce scroll lag * use default cursor on scrollbar * Clean-up module loading in index.html * fix tab indent with hljs --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
262 lines
10 KiB
JavaScript
262 lines
10 KiB
JavaScript
import { substituteParams } from '../../script.js';
|
|
import { delay, escapeRegex } from '../utils.js';
|
|
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
|
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
|
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
|
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
|
import { SlashCommandScope } from './SlashCommandScope.js';
|
|
|
|
export class SlashCommandClosure {
|
|
/**@type {SlashCommandScope}*/ scope;
|
|
/**@type {boolean}*/ executeNow = false;
|
|
// @ts-ignore
|
|
/**@type {SlashCommandNamedArgumentAssignment[]}*/ argumentList = [];
|
|
// @ts-ignore
|
|
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
|
|
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
|
/**@type {SlashCommandAbortController}*/ abortController;
|
|
/**@type {(done:number, total:number)=>void}*/ onProgress;
|
|
|
|
/**@type {number}*/
|
|
get commandCount() {
|
|
return this.executorList.map(executor=>executor.commandCount).reduce((sum,cur)=>sum + cur, 0);
|
|
}
|
|
|
|
constructor(parent) {
|
|
this.scope = new SlashCommandScope(parent);
|
|
}
|
|
|
|
toString() {
|
|
return '[Closure]';
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} text
|
|
* @param {SlashCommandScope} scope
|
|
* @returns
|
|
*/
|
|
substituteParams(text, scope = null) {
|
|
let isList = false;
|
|
let listValues = [];
|
|
scope = scope ?? this.scope;
|
|
const macros = scope.macroList.map(it=>escapeRegex(it.key)).join('|');
|
|
const re = new RegExp(`({{pipe}})|(?:{{var::([^\\s]+?)(?:::((?!}}).+))?}})|(?:{{(${macros})}})`);
|
|
let done = '';
|
|
let remaining = text;
|
|
while (re.test(remaining)) {
|
|
const match = re.exec(remaining);
|
|
const before = substituteParams(remaining.slice(0, match.index));
|
|
const after = remaining.slice(match.index + match[0].length);
|
|
const replacer = match[1] ? scope.pipe : match[2] ? scope.getVariable(match[2], match[3]) : scope.macroList.find(it=>it.key == match[4])?.value;
|
|
if (replacer instanceof SlashCommandClosure) {
|
|
isList = true;
|
|
if (match.index > 0) {
|
|
listValues.push(before);
|
|
}
|
|
listValues.push(replacer);
|
|
if (match.index + match[0].length + 1 < remaining.length) {
|
|
const rest = this.substituteParams(after, scope);
|
|
listValues.push(...(Array.isArray(rest) ? rest : [rest]));
|
|
}
|
|
break;
|
|
} else {
|
|
done = `${done}${before}${replacer}`;
|
|
remaining = after;
|
|
}
|
|
}
|
|
if (!isList) {
|
|
text = `${done}${substituteParams(remaining)}`;
|
|
}
|
|
|
|
if (isList) {
|
|
if (listValues.length > 1) return listValues;
|
|
return listValues[0];
|
|
}
|
|
return text;
|
|
}
|
|
|
|
getCopy() {
|
|
const closure = new SlashCommandClosure();
|
|
closure.scope = this.scope.getCopy();
|
|
closure.executeNow = this.executeNow;
|
|
closure.argumentList = this.argumentList;
|
|
closure.providedArgumentList = this.providedArgumentList;
|
|
closure.executorList = this.executorList;
|
|
closure.abortController = this.abortController;
|
|
closure.onProgress = this.onProgress;
|
|
return closure;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns Promise<SlashCommandClosureResult>
|
|
*/
|
|
async execute() {
|
|
const closure = this.getCopy();
|
|
return await closure.executeDirect();
|
|
}
|
|
|
|
async executeDirect() {
|
|
// closure arguments
|
|
for (const arg of this.argumentList) {
|
|
let v = arg.value;
|
|
if (v instanceof SlashCommandClosure) {
|
|
/**@type {SlashCommandClosure}*/
|
|
const closure = v;
|
|
closure.scope.parent = this.scope;
|
|
if (closure.executeNow) {
|
|
v = (await closure.execute())?.pipe;
|
|
} else {
|
|
v = closure;
|
|
}
|
|
} else {
|
|
v = this.substituteParams(v);
|
|
}
|
|
// unescape value
|
|
if (typeof v == 'string') {
|
|
v = v
|
|
?.replace(/\\\{/g, '{')
|
|
?.replace(/\\\}/g, '}')
|
|
;
|
|
}
|
|
this.scope.letVariable(arg.name, v);
|
|
}
|
|
for (const arg of this.providedArgumentList) {
|
|
let v = arg.value;
|
|
if (v instanceof SlashCommandClosure) {
|
|
/**@type {SlashCommandClosure}*/
|
|
const closure = v;
|
|
closure.scope.parent = this.scope;
|
|
if (closure.executeNow) {
|
|
v = (await closure.execute())?.pipe;
|
|
} else {
|
|
v = closure;
|
|
}
|
|
} else {
|
|
v = this.substituteParams(v, this.scope.parent);
|
|
}
|
|
// unescape value
|
|
if (typeof v == 'string') {
|
|
v = v
|
|
?.replace(/\\\{/g, '{')
|
|
?.replace(/\\\}/g, '}')
|
|
;
|
|
}
|
|
this.scope.setVariable(arg.name, v);
|
|
}
|
|
|
|
let done = 0;
|
|
for (const executor of this.executorList) {
|
|
this.onProgress?.(done, this.commandCount);
|
|
if (executor instanceof SlashCommandClosureExecutor) {
|
|
const closure = this.scope.getVariable(executor.name);
|
|
if (!closure || !(closure instanceof SlashCommandClosure)) throw new Error(`${executor.name} is not a closure.`);
|
|
closure.scope.parent = this.scope;
|
|
closure.providedArgumentList = executor.providedArgumentList;
|
|
const result = await closure.execute();
|
|
this.scope.pipe = result.pipe;
|
|
} else {
|
|
let args = {
|
|
_scope: this.scope,
|
|
_parserFlags: executor.parserFlags,
|
|
};
|
|
let value;
|
|
// substitute named arguments
|
|
for (const arg of executor.namedArgumentList) {
|
|
if (arg.value instanceof SlashCommandClosure) {
|
|
/**@type {SlashCommandClosure}*/
|
|
const closure = arg.value;
|
|
closure.scope.parent = this.scope;
|
|
if (closure.executeNow) {
|
|
args[arg.name] = (await closure.execute())?.pipe;
|
|
} else {
|
|
args[arg.name] = closure;
|
|
}
|
|
} else {
|
|
args[arg.name] = this.substituteParams(arg.value);
|
|
}
|
|
// unescape named argument
|
|
if (typeof args[arg.name] == 'string') {
|
|
args[arg.name] = args[arg.name]
|
|
?.replace(/\\\{/g, '{')
|
|
?.replace(/\\\}/g, '}')
|
|
;
|
|
}
|
|
}
|
|
|
|
// substitute unnamed argument
|
|
if (executor.unnamedArgumentList.length == 0) {
|
|
if (executor.injectPipe) {
|
|
value = this.scope.pipe;
|
|
}
|
|
} else {
|
|
value = [];
|
|
for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
|
|
let v = executor.unnamedArgumentList[i].value;
|
|
if (v instanceof SlashCommandClosure) {
|
|
/**@type {SlashCommandClosure}*/
|
|
const closure = v;
|
|
closure.scope.parent = this.scope;
|
|
if (closure.executeNow) {
|
|
v = (await closure.execute())?.pipe;
|
|
} else {
|
|
v = closure;
|
|
}
|
|
} else {
|
|
v = this.substituteParams(v);
|
|
}
|
|
value[i] = v;
|
|
}
|
|
if (!executor.command.splitUnnamedArgument) {
|
|
if (value.length == 1) {
|
|
value = value[0];
|
|
} else if (!value.find(it=>it instanceof SlashCommandClosure)) {
|
|
value = value.join(' ');
|
|
}
|
|
}
|
|
}
|
|
// unescape unnamed argument
|
|
if (typeof value == 'string') {
|
|
value = value
|
|
?.replace(/\\\{/g, '{')
|
|
?.replace(/\\\}/g, '}')
|
|
;
|
|
}
|
|
|
|
let abortResult = await this.testAbortController();
|
|
if (abortResult) {
|
|
return abortResult;
|
|
}
|
|
executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
|
|
this.scope.pipe = await executor.command.callback(args, value ?? '');
|
|
done += executor.commandCount;
|
|
this.onProgress?.(done, this.commandCount);
|
|
abortResult = await this.testAbortController();
|
|
if (abortResult) {
|
|
return abortResult;
|
|
}
|
|
}
|
|
}
|
|
/**@type {SlashCommandClosureResult} */
|
|
const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe });
|
|
return result;
|
|
}
|
|
|
|
async testPaused() {
|
|
while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) {
|
|
await delay(200);
|
|
}
|
|
}
|
|
async testAbortController() {
|
|
await this.testPaused();
|
|
if (this.abortController?.signal?.aborted) {
|
|
const result = new SlashCommandClosureResult();
|
|
result.isAborted = true;
|
|
result.abortReason = this.abortController.signal.reason.toString();
|
|
return result;
|
|
}
|
|
}
|
|
}
|