mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
* 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;
|
|
}
|
|
}
|
|
}
|