Merge pull request #2917 from SillyTavern/st-parser-accept-multiple-named STscript allow named arguments to be an array

53424d4c8e5a8ee9b8d53723de27a2500aec6186

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +60 -13Ignore whitespace
public/scripts/slash-commands.js+6 -0
@@ -3561,11 +3561,17 @@ function setPromptEntryCallback(args, targetState) {
35613561 const prompts = promptManager.serviceSettings.prompts;
35623562
35633563 function parseArgs(arg) {
3564+ // Arg is already an array
3565+ if (Array.isArray(arg)) {
3566+ return arg;
3567+ }
35643568 const list = [];
35653569 try {
3570+ // Arg is a JSON-stringified array
35663571 const parsedArg = JSON.parse(arg);
35673572 list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
35683573 } catch {
3574+ // Arg is a string
35693575 list.push(arg);
35703576 }
35713577 return list;
public/scripts/slash-commands/SlashCommand.js+2 -2
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
1515 * _abortController:SlashCommandAbortController,
1616 * _debugController:SlashCommandDebugController,
1717 * _hasUnnamedArgument:boolean,
1818 * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[],
1919 * }} NamedArguments
2020 */
2121
2222/**
2323 * Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
2424 * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]}} NamedArgumentsCapture
2525 */
2626
2727/**
public/scripts/slash-commands/SlashCommandClosure.js+52 -11
@@ -2,6 +2,7 @@ import { substituteParams } from '../../script.js';
22import { delay, escapeRegex, uuidv4 } from '../utils.js';
33import { SlashCommand } from './SlashCommand.js';
44import { SlashCommandAbortController } from './SlashCommandAbortController.js';
5+import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
56import { SlashCommandBreak } from './SlashCommandBreak.js';
67import { SlashCommandBreakController } from './SlashCommandBreakController.js';
78import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
@@ -53,7 +54,7 @@ export class SlashCommandClosure {
5354 *
5455 * @param {string} text
5556 * @param {SlashCommandScope} scope
56- * @returns
57+ * @returns {string|SlashCommandClosure|(string|SlashCommandClosure)[]}
5758 */
5859 substituteParams(text, scope = null) {
5960 let isList = false;
@@ -379,6 +380,52 @@ export class SlashCommandClosure {
379380 * @param {import('./SlashCommand.js').NamedArguments} args
380381 */
381382 async substituteNamedArguments(executor, args) {
383+ /**
384+ * Handles the assignment of named arguments, considering if they accept multiple values
385+ * @param {string} name The name of the argument, as defined for the command execution
386+ * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value The value to be assigned
387+ */
388+ const assign = (name, value) => {
389+ // If an array is supposed to be assigned, assign it one by one
390+ if (Array.isArray(value)) {
391+ for (const val of value) {
392+ assign(name, val);
393+ }
394+ return;
395+ }
396+
397+ const definition = executor.command.namedArgumentList.find(x => x.name == name);
398+
399+ // Prefer definition name if a valid named args defintion is found
400+ name = definition?.name ?? name;
401+
402+ // Unescape named argument
403+ if (value && typeof value == 'string') {
404+ value = value
405+ .replace(/\\\{/g, '{')
406+ .replace(/\\\}/g, '}');
407+ }
408+
409+ // If the named argument accepts multiple values, we have to make sure to build an array correctly
410+ if (definition?.acceptsMultiple) {
411+ if (args[name] !== undefined) {
412+ // If there already is something for that named arg, make the value is an array and add to it
413+ let currentValue = args[name];
414+ if (!Array.isArray(currentValue)) {
415+ currentValue = [currentValue];
416+ }
417+ currentValue.push(value);
418+ args[name] = currentValue;
419+ } else {
420+ // If there is nothing in there, we create an array with that singular value
421+ args[name] = [value];
422+ }
423+ } else {
424+ args[name] !== undefined && console.debug(`Named argument assigned multiple times: ${name}`);
425+ args[name] = value;
426+ }
427+ };
428+
382429 // substitute named arguments
383430 for (const arg of executor.namedArgumentList) {
384431 if (arg.value instanceof SlashCommandClosure) {
@@ -390,19 +437,12 @@ export class SlashCommandClosure {
390437 closure.debugController = this.debugController;
391438 }
392439 if (closure.executeNow) {
393440 args[assign(arg.name] =, (await closure.execute())?.pipe);
394441 } else {
395442 args[assign(arg.name] =, closure);
396443 }
397444 } else {
398445 args[assign(arg.name] =, this.substituteParams(arg.value));
399- }
400- // unescape named argument
401- if (typeof args[arg.name] == 'string') {
402- args[arg.name] = args[arg.name]
403- ?.replace(/\\\{/g, '{')
404- ?.replace(/\\\}/g, '}')
405- ;
406446 }
407447 }
408448 }
@@ -424,6 +464,7 @@ export class SlashCommandClosure {
424464 } else {
425465 value = [];
426466 for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
467+ /** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */
427468 let v = executor.unnamedArgumentList[i].value;
428469 if (v instanceof SlashCommandClosure) {
429470 /**@type {SlashCommandClosure}*/