SillyTavern/public/scripts/slash-commands/SlashCommand.js
Len 5cb319771d
Parser followup (#2377)
* set pipe to empty string on empty closure

* fix missing parser flags and scope

* add closure serializing

* add enum provider function to slash command arguments

* add enum providers for /bg, /ask, and /go

* fix index out of bounds returning undefined

* keep whitespace as is in mixed unnamed args (string+closure)

* add _hasUnnamedArgument to named arguments dictionary

* allow /var key=x retrieval

* add enum provider to /tag-add

* fix typo (case)

* add option to make enum matching optional

* add executor to enum provider

* change /tag-add enum provider to only show tags not already assigned

* add enum provider to /tag-remove

* fix name enum provider excluding groups

* remove void from slash command callback return types

* Lint undefined and null pipes

* enable pointer events in chat autocomplete

* fix type hint

---------

Co-authored-by: LenAnderson <Anderson.Len@outlook.com>
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
2024-06-15 00:48:41 +03:00

385 lines
21 KiB
JavaScript

import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { PARSER_FLAG } from './SlashCommandParser.js';
import { SlashCommandScope } from './SlashCommandScope.js';
/**
* @typedef {{
* _scope:SlashCommandScope,
* _parserFlags:{[id:PARSER_FLAG]:boolean},
* _abortController:SlashCommandAbortController,
* _hasUnnamedArgument:boolean,
* [id:string]:string|SlashCommandClosure,
* }} NamedArguments
*/
/**
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
* @typedef {{[id:string]:string|SlashCommandClosure}} NamedArgumentsCapture
*/
/**
* @typedef {string|SlashCommandClosure|(string|SlashCommandClosure)[]} UnnamedArguments
*/
export class SlashCommand {
/**
* Creates a SlashCommand from a properties object.
* @param {Object} props
* @param {string} [props.name]
* @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback]
* @param {string} [props.helpString]
* @param {boolean} [props.splitUnnamedArgument]
* @param {string[]} [props.aliases]
* @param {string} [props.returns]
* @param {SlashCommandNamedArgument[]} [props.namedArgumentList]
* @param {SlashCommandArgument[]} [props.unnamedArgumentList]
*/
static fromProps(props) {
const instance = Object.assign(new this(), props);
return instance;
}
/**@type {string}*/ name;
/**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
/**@type {string}*/ helpString;
/**@type {boolean}*/ splitUnnamedArgument = false;
/**@type {string[]}*/ aliases = [];
/**@type {string}*/ returns;
/**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];
/**@type {SlashCommandArgument[]}*/ unnamedArgumentList = [];
/**@type {Object.<string, HTMLElement>}*/ helpCache = {};
/**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};
renderHelpItem(key = null) {
key = key ?? this.name;
if (!this.helpCache[key]) {
const typeIcon = '[/]';
const li = document.createElement('li'); {
li.classList.add('item');
const type = document.createElement('span'); {
type.classList.add('type');
type.classList.add('monospace');
type.textContent = typeIcon;
li.append(type);
}
const specs = document.createElement('span'); {
specs.classList.add('specs');
const name = document.createElement('span'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = '/';
key.split('').forEach(char=>{
const span = document.createElement('span'); {
span.textContent = char;
name.append(span);
}
});
specs.append(name);
}
const body = document.createElement('span'); {
body.classList.add('body');
const args = document.createElement('span'); {
args.classList.add('arguments');
for (const arg of this.namedArgumentList) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('namedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
const name = document.createElement('span'); {
name.classList.add('argument-name');
name.textContent = arg.name;
argItem.append(name);
}
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e.value;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
for (const arg of this.unnamedArgumentList) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e.value;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
body.append(args);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.textContent = this.returns ?? 'void';
body.append(returns);
}
specs.append(body);
}
li.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
const content = document.createElement('span'); {
content.classList.add('helpContent');
content.innerHTML = this.helpString;
const text = content.textContent;
content.innerHTML = '';
content.textContent = text;
help.append(content);
}
li.append(help);
}
if (this.aliases.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
aliases.append(' (alias: ');
for (const aliasName of this.aliases) {
const alias = document.createElement('span'); {
alias.classList.add('monospace');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
aliases.append(')');
// li.append(aliases);
}
}
}
this.helpCache[key] = li;
}
return /**@type {HTMLElement}*/(this.helpCache[key].cloneNode(true));
}
renderHelpDetails(key = null) {
key = key ?? this.name;
if (!this.helpDetailsCache[key]) {
const frag = document.createDocumentFragment();
const cmd = this;
const namedArguments = cmd.namedArgumentList ?? [];
const unnamedArguments = cmd.unnamedArgumentList ?? [];
const returnType = cmd.returns ?? 'void';
const helpString = cmd.helpString ?? 'NO DETAILS';
const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.title = 'command name';
name.textContent = `/${key}`;
specs.append(name);
}
const body = document.createElement('div'); {
body.classList.add('body');
const args = document.createElement('ul'); {
args.classList.add('arguments');
for (const arg of namedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argSpec = document.createElement('div'); {
argSpec.classList.add('argumentSpec');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('namedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}named argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
const name = document.createElement('span'); {
name.classList.add('argument-name');
name.title = `${argItem.title} - name`;
name.textContent = arg.name;
argItem.append(name);
}
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
enums.title = `${argItem.title} - accepted values`;
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e.value;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
types.title = `${argItem.title} - accepted types`;
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
argSpec.append(argItem);
}
if (arg.defaultValue !== null) {
const argDefault = document.createElement('div'); {
argDefault.classList.add('argument-default');
argDefault.title = 'default value';
argDefault.textContent = arg.defaultValue.toString();
argSpec.append(argDefault);
}
}
listItem.append(argSpec);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');
desc.innerHTML = arg.description;
listItem.append(desc);
}
args.append(listItem);
}
}
for (const arg of unnamedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
enums.title = `${argItem.title} - accepted values`;
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e.value;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
types.title = `${argItem.title} - accepted types`;
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
listItem.append(argItem);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');
desc.innerHTML = arg.description;
listItem.append(desc);
}
args.append(listItem);
}
}
body.append(args);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.title = [null, undefined, 'void'].includes(returnType) ? 'command does not return anything' : 'return value';
returns.textContent = returnType ?? 'void';
body.append(returns);
}
specs.append(body);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.innerHTML = helpString;
for (const code of help.querySelectorAll('pre > code')) {
code.classList.add('language-stscript');
hljs.highlightElement(code);
}
frag.append(help);
}
if (aliasList.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
for (const aliasName of aliasList) {
const alias = document.createElement('span'); {
alias.classList.add('alias');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
frag.append(aliases);
}
}
this.helpDetailsCache[key] = frag;
}
const frag = document.createDocumentFragment();
frag.append(this.helpDetailsCache[key].cloneNode(true));
return frag;
}
}