STscript Parser Rewrite (#1965)

* 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>
This commit is contained in:
Len
2024-05-12 15:15:05 -04:00
committed by GitHub
parent c7d75b7789
commit 1d75b98393
57 changed files with 8777 additions and 736 deletions

View File

@ -1,5 +1,9 @@
import { POPUP_TYPE, Popup } from '../../../popup.js';
import { getSortableDelay } from '../../../utils.js';
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounce, getSortableDelay } from '../../../utils.js';
import { log, warn } from '../index.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
import { QuickReplySet } from './QuickReplySet.js';
@ -47,9 +51,14 @@ export class QuickReply {
/**@type {Popup}*/ editorPopup;
/**@type {HTMLElement}*/ editorExecuteBtn;
/**@type {HTMLElement}*/ editorExecuteBtnPause;
/**@type {HTMLElement}*/ editorExecuteBtnStop;
/**@type {HTMLElement}*/ editorExecuteProgress;
/**@type {HTMLElement}*/ editorExecuteErrors;
/**@type {HTMLElement}*/ editorExecuteResult;
/**@type {HTMLInputElement}*/ editorExecuteHide;
/**@type {Promise}*/ editorExecutePromise;
/**@type {SlashCommandAbortController}*/ abortController;
get hasContext() {
@ -225,15 +234,43 @@ export class QuickReply {
const updateWrap = () => {
if (wrap.checked) {
message.style.whiteSpace = 'pre-wrap';
messageSyntaxInner.style.whiteSpace = 'pre-wrap';
} else {
message.style.whiteSpace = 'pre';
messageSyntaxInner.style.whiteSpace = 'pre';
}
updateScrollDebounced();
};
const updateScroll = (evt) => {
let left = message.scrollLeft;
let top = message.scrollTop;
if (evt) {
evt.preventDefault();
left = message.scrollLeft + evt.deltaX;
top = message.scrollTop + evt.deltaY;
message.scrollTo({
behavior: 'instant',
left,
top,
});
}
messageSyntaxInner.scrollTo({
behavior: 'instant',
left,
top,
});
};
const updateScrollDebounced = updateScroll;
const updateSyntax = ()=>{
messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
};
/**@type {HTMLInputElement}*/
const tabSize = dom.querySelector('#qr--modal-tabSize');
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
const updateTabSize = () => {
message.style.tabSize = tabSize.value;
messageSyntaxInner.style.tabSize = tabSize.value;
updateScrollDebounced();
};
tabSize.addEventListener('change', () => {
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
@ -247,14 +284,15 @@ export class QuickReply {
});
/**@type {HTMLTextAreaElement}*/
const message = dom.querySelector('#qr--modal-message');
updateWrap();
updateTabSize();
message.value = this.message;
message.addEventListener('input', () => {
updateSyntax();
this.updateMessage(message.value);
updateScrollDebounced();
});
setSlashCommandAutoComplete(message, true);
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
message.addEventListener('keydown', (evt) => {
message.addEventListener('keydown', async(evt) => {
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
const start = message.selectionStart;
@ -265,12 +303,12 @@ export class QuickReply {
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
message.selectionStart = start + 1;
message.selectionEnd = end + count;
this.updateMessage(message.value);
updateSyntax();
} else {
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
message.selectionStart = start + 1;
message.selectionEnd = end + 1;
this.updateMessage(message.value);
updateSyntax();
}
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
@ -281,15 +319,37 @@ export class QuickReply {
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
message.selectionStart = start - 1;
message.selectionEnd = end - count;
this.updateMessage(message.value);
updateSyntax();
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
evt.stopPropagation();
evt.preventDefault();
if (executeShortcut.checked) {
this.executeFromEditor();
const selectionStart = message.selectionStart;
const selectionEnd = message.selectionEnd;
message.blur();
await this.executeFromEditor();
if (document.activeElement != message) {
message.focus();
message.selectionStart = selectionStart;
message.selectionEnd = selectionEnd;
}
}
}
});
message.addEventListener('wheel', (evt)=>{
updateScrollDebounced(evt);
});
message.addEventListener('scroll', (evt)=>{
updateScrollDebounced();
});
message.style.color = 'transparent';
message.style.background = 'transparent';
message.style.setProperty('text-shadow', 'none', 'important');
/**@type {HTMLElement}*/
const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
updateSyntax();
updateWrap();
updateTabSize();
// context menu
/**@type {HTMLTemplateElement}*/
@ -414,9 +474,15 @@ export class QuickReply {
this.updateContext();
});
/**@type {HTMLElement}*/
const executeProgress = dom.querySelector('#qr--modal-executeProgress');
this.editorExecuteProgress = executeProgress;
/**@type {HTMLElement}*/
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
this.editorExecuteErrors = executeErrors;
/**@type {HTMLElement}*/
const executeResult = dom.querySelector('#qr--modal-executeResult');
this.editorExecuteResult = executeResult;
/**@type {HTMLInputElement}*/
const executeHide = dom.querySelector('#qr--modal-executeHide');
this.editorExecuteHide = executeHide;
@ -426,6 +492,26 @@ export class QuickReply {
executeBtn.addEventListener('click', async()=>{
await this.executeFromEditor();
});
/**@type {HTMLElement}*/
const executeBtnPause = dom.querySelector('#qr--modal-pause');
this.editorExecuteBtnPause = executeBtnPause;
executeBtnPause.addEventListener('click', async()=>{
if (this.abortController) {
if (this.abortController.signal.paused) {
this.abortController.continue('Continue button clicked');
this.editorExecuteProgress.classList.remove('qr--paused');
} else {
this.abortController.pause('Pause button clicked');
this.editorExecuteProgress.classList.add('qr--paused');
}
}
});
/**@type {HTMLElement}*/
const executeBtnStop = dom.querySelector('#qr--modal-stop');
this.editorExecuteBtnStop = executeBtnStop;
executeBtnStop.addEventListener('click', async()=>{
this.abortController?.abort('Stop button clicked');
});
await popupResult;
} else {
@ -436,21 +522,54 @@ export class QuickReply {
async executeFromEditor() {
if (this.editorExecutePromise) return;
this.editorExecuteBtn.classList.add('qr--busy');
this.editorExecuteProgress.style.setProperty('--prog', '0');
this.editorExecuteErrors.classList.remove('qr--hasErrors');
this.editorExecuteResult.classList.remove('qr--hasResult');
this.editorExecuteProgress.classList.remove('qr--error');
this.editorExecuteProgress.classList.remove('qr--success');
this.editorExecuteProgress.classList.remove('qr--paused');
this.editorExecuteProgress.classList.remove('qr--aborted');
this.editorExecuteErrors.innerHTML = '';
this.editorExecuteResult.innerHTML = '';
if (this.editorExecuteHide.checked) {
this.editorPopup.dom.classList.add('qr--hide');
}
try {
this.editorExecutePromise = this.execute();
await this.editorExecutePromise;
this.editorExecutePromise = this.execute({}, true);
const result = await this.editorExecutePromise;
if (this.abortController?.signal?.aborted) {
this.editorExecuteProgress.classList.add('qr--aborted');
} else {
this.editorExecuteResult.textContent = result?.toString();
this.editorExecuteResult.classList.add('qr--hasResult');
this.editorExecuteProgress.classList.add('qr--success');
}
this.editorExecuteProgress.classList.remove('qr--paused');
} catch (ex) {
this.editorExecuteErrors.textContent = ex.message;
this.editorExecuteErrors.classList.add('qr--hasErrors');
this.editorExecuteProgress.classList.add('qr--error');
this.editorExecuteProgress.classList.remove('qr--paused');
if (ex instanceof SlashCommandParserError) {
this.editorExecuteErrors.innerHTML = `
<div>${ex.message}</div>
<div>Line: ${ex.line} Column: ${ex.column}</div>
<pre style="text-align:left;">${ex.hint}</pre>
`;
} else {
this.editorExecuteErrors.innerHTML = `
<div>${ex.message}</div>
`;
}
}
this.editorExecutePromise = null;
this.editorExecuteBtn.classList.remove('qr--busy');
this.editorPopup.dom.classList.remove('qr--hide');
}
updateEditorProgress(done, total) {
this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`);
}
@ -526,12 +645,22 @@ export class QuickReply {
}
async execute(args = {}) {
async execute(args = {}, isEditor = false, isRun = false) {
if (this.message?.length > 0 && this.onExecute) {
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
return args[key] ?? '';
const scope = new SlashCommandScope();
for (const key of Object.keys(args)) {
scope.setMacro(`arg::${key}`, args[key]);
}
if (isEditor) {
this.abortController = new SlashCommandAbortController();
}
return await this.onExecute(this, {
message:this.message,
isAutoExecute: args.isAutoExecute ?? false,
isEditor,
isRun,
scope,
});
return await this.onExecute(this, message, args.isAutoExecute ?? false);
}
}

View File

@ -1,5 +1,6 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { executeSlashCommands } from '../../../slash-commands.js';
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounceAsync, warn } from '../index.js';
import { QuickReply } from './QuickReply.js';
@ -100,15 +101,29 @@ export class QuickReplySet {
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
*
* @param {QuickReply} qr The QR to execute.
* @param {object} options
* @param {string} [options.message] (null) altered message to be used
* @param {boolean} [options.isAutoExecute] (false) whether the execution is triggered by auto execute
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
* @returns
*/
async execute(qr, message = null, isAutoExecute = false) {
async executeWithOptions(qr, options = {}) {
options = Object.assign({
message:null,
isAutoExecute:false,
isEditor:false,
isRun:false,
scope:null,
}, options);
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
const finalMessage = message ?? qr.message;
const finalMessage = options.message ?? qr.message;
let input = ta.value;
if (!isAutoExecute && this.injectInput && input.length > 0) {
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
if (this.placeBeforeInput) {
input = `${finalMessage} ${input}`;
} else {
@ -119,7 +134,24 @@ export class QuickReplySet {
}
if (input[0] == '/' && !this.disableSend) {
const result = await executeSlashCommands(input);
let result;
if (options.isAutoExecute || options.isRun) {
result = await executeSlashCommandsWithOptions(input, {
handleParserErrors: true,
scope: options.scope,
});
} else if (options.isEditor) {
result = await executeSlashCommandsWithOptions(input, {
handleParserErrors: false,
scope: options.scope,
abortController: qr.abortController,
onProgress: (done, total) => qr.updateEditorProgress(done, total),
});
} else {
result = await executeSlashCommandsOnChatInput(input, {
scope: options.scope,
});
}
return typeof result === 'object' ? result?.pipe : '';
}
@ -131,6 +163,18 @@ export class QuickReplySet {
document.querySelector('#send_but').click();
}
}
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command
*/
async execute(qr, message = null, isAutoExecute = false, scope = null) {
return this.executeWithOptions(qr, {
message,
isAutoExecute,
scope,
});
}
@ -152,7 +196,7 @@ export class QuickReplySet {
}
hookQuickReply(qr) {
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
qr.onDelete = ()=>this.removeQuickReply(qr);
qr.onUpdate = ()=>this.save();
}

View File

@ -1,4 +1,6 @@
import { registerSlashCommand } from '../../../slash-commands.js';
import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { isTrueBoolean } from '../../../utils.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplyApi } from '../api/QuickReplyApi.js';
@ -17,46 +19,331 @@ export class SlashCommandHandler {
init() {
registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', ()=>toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), [], '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', true, true);
registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle global QR set', true, true);
registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate global QR set', true, true);
registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '<span class="monospace">(number)</span> deactivate global QR set', true, true);
registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle chat QR set', true, true);
registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate chat QR set', true, true);
registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '<span class="monospace">(number)</span> deactivate chat QR set', true, true);
registerSlashCommand('qr-set-list', (_, value)=>this.listSets(value ?? 'all'), [], '(all|global|chat) gets a list of the names of all quick reply sets', true, true);
registerSlashCommand('qr-list', (_, value)=>this.listQuickReplies(value), [], '(set name) gets a list of the names of all quick replies in this quick reply set', true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
unnamedArgumentList: [
new SlashCommandArgument(
'number', [ARGUMENT_TYPE.NUMBER], true,
),
],
helpString: 'Activates the specified Quick Reply',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset',
callback: () => toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'),
helpString: '<strong>DEPRECATED</strong> The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set',
callback: (args, value) => this.toggleGlobalSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Toggle global QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on',
callback: (args, value) => this.addGlobalSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Activate global QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off',
callback: (_, value) => this.removeGlobalSet(value),
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Deactivate global QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set',
callback: (args, value) => this.toggleChatSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Toggle chat QR set',
}));
const qrArgs = `
label - string - text on the button, e.g., label=MyButton
set - string - name of the QR set, e.g., set=PresetName1
hidden - bool - whether the button should be hidden, e.g., hidden=true
startup - bool - auto execute on app startup, e.g., startup=true
user - bool - auto execute on user message, e.g., user=true
bot - bool - auto execute on AI message, e.g., bot=true
load - bool - auto execute on chat load, e.g., load=true
group - bool - auto execute on group member selection, e.g., group=true
title - string - title / tooltip to be shown on button, e.g., title="My Fancy Button"
`.trim();
const qrUpdateArgs = `
newlabel - string - new text for the button, e.g. newlabel=MyRenamedButton
${qrArgs}
`.trim();
registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrArgs}</span> creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrUpdateArgs}</span> updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '<span class="monospace">set=string [label]</span> deletes Quick Reply', true, true);
registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '<span class="monospace">set=string label=string [chain=false] (preset name)</span> add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '<span class="monospace">set=string label=string (preset name)</span> remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '<span class="monospace">set=string (label)</span> remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
const presetArgs = `
nosend - bool - disable send / insert in user input (invalid for slash commands)
before - bool - place QR before user input
inject - bool - inject user input automatically (if disabled use {{input}})
`.trim();
registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> create a new preset (overrides existing ones), example: <tt>/qr-set-add MyNewPreset</tt>`, true, true);
registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> update an existing preset, example: <tt>/qr-set-update enabled=false MyPreset</tt>`, true, true);
registerSlashCommand('qr-set-delete', (args, name)=>this.deleteSet(name), ['qr-presetdelete'], `<span class="monospace" style="white-space:pre-line;">(name)\n arguments:\n ${presetArgs}</span> delete an existing preset, example: <tt>/qr-set-delete MyPreset</tt>`, true, true);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on',
callback: (args, value) => this.addChatSet(value, args),
namedArgumentList: [
new SlashCommandNamedArgument(
'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ['true', 'false'],
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Activate chat QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off',
callback: (_, value) => this.removeChatSet(value),
unnamedArgumentList: [
new SlashCommandArgument(
'QR set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Deactivate chat QR set',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list',
callback: (_, value) => this.listSets(value ?? 'all'),
returns: 'list of QR sets',
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'set type', [ARGUMENT_TYPE.STRING], false, false, null, ['all', 'global', 'chat'],
),
],
helpString: 'Gets a list of the names of all quick reply sets.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list',
callback: (_, value) => this.listQuickReplies(value),
returns: 'list of QRs',
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'set name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Gets a list of the names of all quick replies in this quick reply set.',
}));
const qrArgs = [
new SlashCommandNamedArgument('label', 'text on the button, e.g., label=MyButton', [ARGUMENT_TYPE.STRING]),
new SlashCommandNamedArgument('set', 'name of the QR set, e.g., set=PresetName1', [ARGUMENT_TYPE.STRING]),
new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('bot', 'auto execute on AI message, e.g., bot=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
];
const qrUpdateArgs = [
new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
callback: (args, message) => this.createQuickReply(args, message),
namedArgumentList: qrArgs,
unnamedArgumentList: [
new SlashCommandArgument(
'command', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>Creates a new Quick Reply.</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-create set=MyPreset label=MyButton /echo 123</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
callback: (args, message) => this.updateQuickReply(args, message),
returns: 'updated quick reply',
namedArgumentList: [...qrUpdateArgs, ...qrArgs],
helpString: `
<div>
Updates Quick Reply.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete',
callback: (args, name) => this.deleteQuickReply(args, name),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'Quick Reply set', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'label', 'Quick Reply label', [ARGUMENT_TYPE.STRING], false,
),
],
helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd',
callback: (args, name) => this.createContextItem(args, name),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'string', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'label', 'string', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'preset name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Add context menu preset to a QR.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel',
callback: (args, name) => this.deleteContextItem(args, name),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'string', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandNamedArgument(
'label', 'string', [ARGUMENT_TYPE.STRING], true,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'preset name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Remove context menu preset from a QR.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear',
callback: (args, label) => this.clearContextMenu(args, label),
namedArgumentList: [
new SlashCommandNamedArgument(
'set', 'context menu preset name', [ARGUMENT_TYPE.STRING], true,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'label', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Remove all context menu presets from a QR.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-contextclear set=MyPreset MyButton</code></pre>
</li>
</ul>
</div>
`,
}));
const presetArgs = [
new SlashCommandNamedArgument('nosend', 'disable send / insert in user input (invalid for slash commands)', [ARGUMENT_TYPE.BOOLEAN], false),
new SlashCommandNamedArgument('before', 'place QR before user input', [ARGUMENT_TYPE.BOOLEAN], false),
new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
callback: (args, name) => this.createSet(name, args),
aliases: ['qr-presetadd'],
namedArgumentList: presetArgs,
unnamedArgumentList: [
new SlashCommandArgument(
'name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: `
<div>
Create a new preset (overrides existing ones).
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/qr-set-add MyNewPreset</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
callback: (args, name) => this.updateSet(name, args),
aliases: ['qr-presetupdate'],
namedArgumentList: presetArgs,
unnamedArgumentList: [
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Update an existing preset.
</div>
<div>
<strong>Example:</strong>
<pre><code>/qr-set-update enabled=false MyPreset</code></pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
callback: (args, name) => this.deleteSet(name),
aliases: ['qr-presetdelete'],
unnamedArgumentList: [
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Delete an existing preset.
</div>
<div>
<strong>Example:</strong>
<pre><code>/qr-set-delete MyPreset</code></pre>
</div>
`,
}));
}