mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
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>
This commit is contained in:
@ -9,10 +9,10 @@ import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* _pipe:string|SlashCommandClosure,
|
||||
* _scope:SlashCommandScope,
|
||||
* _parserFlags:{[id:PARSER_FLAG]:boolean},
|
||||
* _abortController:SlashCommandAbortController,
|
||||
* _hasUnnamedArgument:boolean,
|
||||
* [id:string]:string|SlashCommandClosure,
|
||||
* }} NamedArguments
|
||||
*/
|
||||
@ -33,7 +33,7 @@ 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|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @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]
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
|
||||
|
||||
|
||||
@ -29,6 +30,8 @@ export class SlashCommandArgument {
|
||||
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
|
||||
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
|
||||
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options
|
||||
* @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values
|
||||
*/
|
||||
static fromProps(props) {
|
||||
return new SlashCommandArgument(
|
||||
@ -38,6 +41,8 @@ export class SlashCommandArgument {
|
||||
props.acceptsMultiple ?? false,
|
||||
props.defaultValue ?? null,
|
||||
props.enumList ?? [],
|
||||
props.enumProvider ?? null,
|
||||
props.forceEnum ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
@ -50,6 +55,8 @@ export class SlashCommandArgument {
|
||||
/**@type {boolean}*/ acceptsMultiple = false;
|
||||
/**@type {string|SlashCommandClosure}*/ defaultValue;
|
||||
/**@type {SlashCommandEnumValue[]}*/ enumList = [];
|
||||
/**@type {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]}*/ enumProvider = null;
|
||||
/**@type {boolean}*/ forceEnum = true;
|
||||
|
||||
|
||||
/**
|
||||
@ -57,8 +64,9 @@ export class SlashCommandArgument {
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
|
||||
* @param {string|SlashCommandClosure} defaultValue
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
|
||||
*/
|
||||
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = []) {
|
||||
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = true) {
|
||||
this.description = description;
|
||||
this.typeList = types ? Array.isArray(types) ? types : [types] : [];
|
||||
this.isRequired = isRequired ?? false;
|
||||
@ -68,6 +76,8 @@ export class SlashCommandArgument {
|
||||
if (it instanceof SlashCommandEnumValue) return it;
|
||||
return new SlashCommandEnumValue(it);
|
||||
});
|
||||
this.enumProvider = enumProvider;
|
||||
this.forceEnum = forceEnum;
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +95,8 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
|
||||
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
|
||||
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options
|
||||
* @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values
|
||||
*/
|
||||
static fromProps(props) {
|
||||
return new SlashCommandNamedArgument(
|
||||
@ -96,6 +108,8 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
props.defaultValue ?? null,
|
||||
props.enumList ?? [],
|
||||
props.aliasList ?? [],
|
||||
props.enumProvider ?? null,
|
||||
props.forceEnum ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,9 +126,11 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
|
||||
* @param {string|SlashCommandClosure} defaultValue
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
|
||||
* @param {boolean} forceEnum
|
||||
*/
|
||||
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = []) {
|
||||
super(description, types, isRequired, acceptsMultiple, defaultValue, enums);
|
||||
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) {
|
||||
super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);
|
||||
this.name = name;
|
||||
this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
[...namedResult.optionList, ...unnamedResult.optionList],
|
||||
);
|
||||
combinedResult.isRequired = namedResult.isRequired || unnamedResult.isRequired;
|
||||
combinedResult.forceMatch = namedResult.forceMatch && unnamedResult.forceMatch;
|
||||
return combinedResult;
|
||||
}
|
||||
}
|
||||
@ -102,18 +103,19 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
|
||||
if (name.includes('=') && cmdArg) {
|
||||
// if cursor is already behind "=" check for enums
|
||||
/**@type {SlashCommandNamedArgument} */
|
||||
if (cmdArg && cmdArg.enumList?.length) {
|
||||
if (isSelect && cmdArg.enumList.includes(value) && argAssign && argAssign.end == index) {
|
||||
const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
|
||||
if (cmdArg && enumList?.length) {
|
||||
if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) {
|
||||
return null;
|
||||
}
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start + name.length,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
true,
|
||||
);
|
||||
result.isRequired = true;
|
||||
result.forceMatch = cmdArg.forceEnum;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -148,7 +150,8 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
if (idx > -1) {
|
||||
argAssign = this.executor.unnamedArgumentList[idx];
|
||||
cmdArg = this.executor.command.unnamedArgumentList[idx];
|
||||
if (cmdArg && cmdArg.enumList.length > 0) {
|
||||
const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
|
||||
if (cmdArg && enumList.length > 0) {
|
||||
value = argAssign.value.toString().slice(0, index - argAssign.start);
|
||||
start = argAssign.start;
|
||||
} else {
|
||||
@ -163,17 +166,19 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cmdArg == null || cmdArg.enumList.length == 0) return null;
|
||||
const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
|
||||
if (cmdArg == null || enumList.length == 0) return null;
|
||||
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
false,
|
||||
);
|
||||
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value);
|
||||
const isCompleteValue = enumList.find(it=>it.value == value);
|
||||
const isSelectedValue = isSelect && isCompleteValue;
|
||||
result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue;
|
||||
result.forceMatch = cmdArg.forceEnum;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { substituteParams } from '../../script.js';
|
||||
import { delay, escapeRegex } from '../utils.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
||||
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
||||
@ -17,6 +18,7 @@ export class SlashCommandClosure {
|
||||
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
||||
/**@type {SlashCommandAbortController}*/ abortController;
|
||||
/**@type {(done:number, total:number)=>void}*/ onProgress;
|
||||
/**@type {string}*/ rawText;
|
||||
|
||||
/**@type {number}*/
|
||||
get commandCount() {
|
||||
@ -148,6 +150,9 @@ export class SlashCommandClosure {
|
||||
}
|
||||
|
||||
let done = 0;
|
||||
if (this.executorList.length == 0) {
|
||||
this.scope.pipe = '';
|
||||
}
|
||||
for (const executor of this.executorList) {
|
||||
this.onProgress?.(done, this.commandCount);
|
||||
if (executor instanceof SlashCommandClosureExecutor) {
|
||||
@ -158,10 +163,12 @@ export class SlashCommandClosure {
|
||||
const result = await closure.execute();
|
||||
this.scope.pipe = result.pipe;
|
||||
} else {
|
||||
/**@type {import('./SlashCommand.js').NamedArguments} */
|
||||
let args = {
|
||||
_scope: this.scope,
|
||||
_parserFlags: executor.parserFlags,
|
||||
_abortController: this.abortController,
|
||||
_hasUnnamedArgument: executor.unnamedArgumentList.length > 0,
|
||||
};
|
||||
let value;
|
||||
// substitute named arguments
|
||||
@ -191,6 +198,7 @@ export class SlashCommandClosure {
|
||||
if (executor.unnamedArgumentList.length == 0) {
|
||||
if (executor.injectPipe) {
|
||||
value = this.scope.pipe;
|
||||
args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;
|
||||
}
|
||||
} else {
|
||||
value = [];
|
||||
@ -214,7 +222,7 @@ export class SlashCommandClosure {
|
||||
if (value.length == 1) {
|
||||
value = value[0];
|
||||
} else if (!value.find(it=>it instanceof SlashCommandClosure)) {
|
||||
value = value.join(' ');
|
||||
value = value.join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,6 +249,7 @@ export class SlashCommandClosure {
|
||||
}
|
||||
executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
|
||||
this.scope.pipe = await executor.command.callback(args, value ?? '');
|
||||
this.#lintPipe(executor.command);
|
||||
done += executor.commandCount;
|
||||
this.onProgress?.(done, this.commandCount);
|
||||
abortResult = await this.testAbortController();
|
||||
@ -269,4 +278,15 @@ export class SlashCommandClosure {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-fixes the pipe if it is not a valid result for STscript.
|
||||
* @param {SlashCommand} command Command being executed
|
||||
*/
|
||||
#lintPipe(command) {
|
||||
if (this.scope.pipe === undefined || this.scope.pipe === null) {
|
||||
console.warn(`${command.name} returned undefined or null. Auto-fixing to empty string.`);
|
||||
this.scope.pipe = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||
* @param {SlashCommandEnumValue} enumValue
|
||||
*/
|
||||
constructor(cmd, enumValue) {
|
||||
super(enumValue.value, '◊');
|
||||
super(enumValue.value, enumValue.typeIcon, enumValue.type);
|
||||
this.cmd = cmd;
|
||||
this.enumValue = enumValue;
|
||||
}
|
||||
@ -21,9 +21,9 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||
|
||||
renderItem() {
|
||||
let li;
|
||||
li = this.makeItem(this.name, '◊', true, [], [], null, this.enumValue.description);
|
||||
li = this.makeItem(this.name, this.typeIcon, true, [], [], null, this.enumValue.description);
|
||||
li.setAttribute('data-name', this.name);
|
||||
li.setAttribute('data-option-type', 'enum');
|
||||
li.setAttribute('data-option-type', this.type);
|
||||
return li;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
export class SlashCommandEnumValue {
|
||||
/**@type {string}*/ value;
|
||||
/**@type {string}*/ description;
|
||||
/**@type {string}*/ type = 'enum';
|
||||
/**@type {string}*/ typeIcon = '◊';
|
||||
|
||||
constructor(value, description = null) {
|
||||
constructor(value, description = null, type = 'enum', typeIcon = '◊') {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.typeIcon = typeIcon;
|
||||
}
|
||||
|
||||
toString() {
|
||||
|
@ -598,6 +598,7 @@ export class SlashCommandParser {
|
||||
this.closureIndex.push(closureIndexEntry);
|
||||
let injectPipe = true;
|
||||
if (!isRoot) this.take(2); // discard opening {:
|
||||
const textStart = this.index;
|
||||
let closure = new SlashCommandClosure(this.scope);
|
||||
closure.abortController = this.abortController;
|
||||
this.scope = closure.scope;
|
||||
@ -638,13 +639,13 @@ export class SlashCommandParser {
|
||||
}
|
||||
this.discardWhitespace(); // discard further whitespace
|
||||
}
|
||||
closure.rawText = this.text.slice(textStart, this.index);
|
||||
if (!isRoot) this.take(2); // discard closing :}
|
||||
if (this.testSymbol('()')) {
|
||||
this.take(2); // discard ()
|
||||
closure.executeNow = true;
|
||||
}
|
||||
closureIndexEntry.end = this.index - 1;
|
||||
this.discardWhitespace(); // discard trailing whitespace
|
||||
this.scope = closure.scope.parent;
|
||||
return closure;
|
||||
}
|
||||
@ -820,9 +821,8 @@ export class SlashCommandParser {
|
||||
if (this.testClosure()) {
|
||||
isList = true;
|
||||
if (value.length > 0) {
|
||||
assignment.end = assignment.end - (value.length - value.trim().length);
|
||||
this.indexMacros(this.index - value.length, value);
|
||||
assignment.value = value.trim();
|
||||
assignment.value = value;
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
@ -834,6 +834,7 @@ export class SlashCommandParser {
|
||||
listValues.push(assignment);
|
||||
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||
assignment.start = this.index;
|
||||
if (split) this.discardWhitespace();
|
||||
} else if (split) {
|
||||
if (this.testQuotedValue()) {
|
||||
assignment.start = this.index;
|
||||
@ -862,8 +863,8 @@ export class SlashCommandParser {
|
||||
assignment.end = this.index;
|
||||
}
|
||||
}
|
||||
if (isList && value.trim().length > 0) {
|
||||
assignment.value = value.trim();
|
||||
if (isList && value.length > 0) {
|
||||
assignment.value = value;
|
||||
listValues.push(assignment);
|
||||
}
|
||||
if (isList) {
|
||||
|
@ -92,7 +92,7 @@ export class SlashCommandScope {
|
||||
v = v[numIndex];
|
||||
}
|
||||
if (typeof v == 'object') return JSON.stringify(v);
|
||||
return v;
|
||||
return v ?? '';
|
||||
} else {
|
||||
const value = this.variables[key];
|
||||
return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);
|
||||
|
Reference in New Issue
Block a user