mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			533 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { substituteParams } from '../../script.js';
 | |
| import { delay, escapeRegex, uuidv4 } from '../utils.js';
 | |
| import { SlashCommand } from './SlashCommand.js';
 | |
| import { SlashCommandAbortController } from './SlashCommandAbortController.js';
 | |
| import { SlashCommandBreak } from './SlashCommandBreak.js';
 | |
| import { SlashCommandBreakController } from './SlashCommandBreakController.js';
 | |
| import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
 | |
| import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
 | |
| import { SlashCommandDebugController } from './SlashCommandDebugController.js';
 | |
| import { SlashCommandExecutionError } from './SlashCommandExecutionError.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;
 | |
|     /** @type {SlashCommandNamedArgumentAssignment[]} */ argumentList = [];
 | |
|     /** @type {SlashCommandNamedArgumentAssignment[]} */ providedArgumentList = [];
 | |
|     /** @type {SlashCommandExecutor[]} */ executorList = [];
 | |
|     /** @type {SlashCommandAbortController} */ abortController;
 | |
|     /** @type {SlashCommandBreakController} */ breakController;
 | |
|     /** @type {SlashCommandDebugController} */ debugController;
 | |
|     /** @type {(done:number, total:number)=>void} */ onProgress;
 | |
|     /** @type {string} */ rawText;
 | |
|     /** @type {string} */ fullText;
 | |
|     /** @type {string} */ parserContext;
 | |
|     /** @type {string} */ #source = uuidv4();
 | |
|     get source() { return this.#source; }
 | |
|     set source(value) {
 | |
|         this.#source = value;
 | |
|         for (const executor of this.executorList) {
 | |
|             executor.source = value;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**@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]${this.executeNow ? '()' : ''}`;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      *
 | |
|      * @param {string} text
 | |
|      * @param {SlashCommandScope} scope
 | |
|      * @returns {string|SlashCommandClosure|(string|SlashCommandClosure)[]}
 | |
|      */
 | |
|     substituteParams(text, scope = null) {
 | |
|         let isList = false;
 | |
|         let listValues = [];
 | |
|         scope = scope ?? this.scope;
 | |
|         const escapeMacro = (it, isAnchored = false)=>{
 | |
|             const regexText = escapeRegex(it.key.replace(/\*/g, '~~~WILDCARD~~~'))
 | |
|                 .replaceAll('~~~WILDCARD~~~', '(?:(?:(?!(?:::|}})).)*)')
 | |
|             ;
 | |
|             if (isAnchored) {
 | |
|                 return `^${regexText}$`;
 | |
|             }
 | |
|             return regexText;
 | |
|         };
 | |
|         const macroList = scope.macroList.toSorted((a,b)=>{
 | |
|             if (a.key.includes('*') && !b.key.includes('*')) return 1;
 | |
|             if (!a.key.includes('*') && b.key.includes('*')) return -1;
 | |
|             if (a.key.includes('*') && b.key.includes('*')) return b.key.indexOf('*') - a.key.indexOf('*');
 | |
|             return 0;
 | |
|         });
 | |
|         const macros = macroList.map(it=>escapeMacro(it)).join('|');
 | |
|         const re = new RegExp(`(?<pipe>{{pipe}})|(?:{{var::(?<var>[^\\s]+?)(?:::(?<varIndex>(?!}}).+))?}})|(?:{{(?<macro>${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.groups.pipe ? scope.pipe : match.groups.var ? scope.getVariable(match.groups.var, match.groups.index) : macroList.find(it=>it.key == match.groups.macro || new RegExp(escapeMacro(it, true)).test(match.groups.macro))?.value;
 | |
|             if (replacer instanceof SlashCommandClosure) {
 | |
|                 replacer.abortController = this.abortController;
 | |
|                 replacer.breakController = this.breakController;
 | |
|                 replacer.scope.parent = this.scope;
 | |
|                 if (this.debugController && !replacer.debugController) {
 | |
|                     replacer.debugController = this.debugController;
 | |
|                 }
 | |
|                 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.breakController = this.breakController;
 | |
|         closure.debugController = this.debugController;
 | |
|         closure.rawText = this.rawText;
 | |
|         closure.fullText = this.fullText;
 | |
|         closure.parserContext = this.parserContext;
 | |
|         closure.source = this.source;
 | |
|         closure.onProgress = this.onProgress;
 | |
|         return closure;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      *
 | |
|      * @returns {Promise<SlashCommandClosureResult>}
 | |
|      */
 | |
|     async execute() {
 | |
|         // execute a copy of the closure to no taint it and its scope with the effects of its execution
 | |
|         // as this would affect the closure being called a second time (e.g., loop, multiple /run calls)
 | |
|         const closure = this.getCopy();
 | |
|         const gen = closure.executeDirect();
 | |
|         let step;
 | |
|         while (!step?.done) {
 | |
|             step = await gen.next(this.debugController?.testStepping(this) ?? false);
 | |
|             if (!(step.value instanceof SlashCommandClosureResult) && this.debugController) {
 | |
|                 this.debugController.isStepping = await this.debugController.awaitBreakPoint(step.value.closure, step.value.executor);
 | |
|             }
 | |
|         }
 | |
|         return step.value;
 | |
|     }
 | |
| 
 | |
|     async * executeDirect() {
 | |
|         this.debugController?.down(this);
 | |
|         // 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;
 | |
|                 closure.breakController = this.breakController;
 | |
|                 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;
 | |
|                 closure.breakController = this.breakController;
 | |
|                 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);
 | |
|         }
 | |
| 
 | |
|         if (this.executorList.length == 0) {
 | |
|             this.scope.pipe = '';
 | |
|         }
 | |
|         const stepper = this.executeStep();
 | |
|         let step;
 | |
|         while (!step?.done && !this.breakController?.isBreak) {
 | |
|             // get executor before execution
 | |
|             step = await stepper.next();
 | |
|             if (step.value instanceof SlashCommandBreakPoint) {
 | |
|                 console.log('encountered SlashCommandBreakPoint');
 | |
|                 if (this.debugController) {
 | |
|                     // resolve args
 | |
|                     step = await stepper.next();
 | |
|                     // "execute" breakpoint
 | |
|                     step = await stepper.next();
 | |
|                     // get next executor
 | |
|                     step = await stepper.next();
 | |
|                     // breakpoint has to yield before arguments are resolved if one of the
 | |
|                     // arguments is an immediate closure, otherwise you cannot step into the
 | |
|                     // immediate closure
 | |
|                     const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
 | |
|                     const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
 | |
|                     if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) {
 | |
|                         this.debugController.isStepping = yield { closure:this, executor:step.value };
 | |
|                     } else {
 | |
|                         this.debugController.isStepping = true;
 | |
|                         this.debugController.stepStack[this.debugController.stepStack.length - 1] = true;
 | |
|                     }
 | |
|                 }
 | |
|             } else if (!step.done && this.debugController?.testStepping(this)) {
 | |
|                 this.debugController.isSteppingInto = false;
 | |
|                 // if stepping, have to yield before arguments are resolved if one of the arguments
 | |
|                 // is an immediate closure, otherwise you cannot step into the immediate closure
 | |
|                 const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
 | |
|                 const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
 | |
|                 if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) {
 | |
|                     this.debugController.isStepping = yield { closure:this, executor:step.value };
 | |
|                 }
 | |
|             }
 | |
|             // resolve args
 | |
|             step = await stepper.next();
 | |
|             if (step.value instanceof SlashCommandBreak) {
 | |
|                 console.log('encountered SlashCommandBreak');
 | |
|                 if (this.breakController) {
 | |
|                     this.breakController?.break();
 | |
|                     break;
 | |
|                 }
 | |
|             } else if (!step.done && this.debugController?.testStepping(this)) {
 | |
|                 this.debugController.isSteppingInto = false;
 | |
|                 this.debugController.isStepping = yield { closure:this, executor:step.value };
 | |
|             }
 | |
|             // execute executor
 | |
|             step = await stepper.next();
 | |
|         }
 | |
| 
 | |
|         // if execution has returned a closure result, return that (should only happen on abort)
 | |
|         if (step.value instanceof SlashCommandClosureResult) {
 | |
|             this.debugController?.up();
 | |
|             return step.value;
 | |
|         }
 | |
|         /**@type {SlashCommandClosureResult} */
 | |
|         const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe, isBreak: this.breakController?.isBreak ?? false });
 | |
|         this.debugController?.up();
 | |
|         return result;
 | |
|     }
 | |
|     /**
 | |
|      * Generator that steps through the executor list.
 | |
|      * Every executor is split into three steps:
 | |
|      *  - before arguments are resolved
 | |
|      *  - after arguments are resolved
 | |
|      *  - after execution
 | |
|      */
 | |
|     async * executeStep() {
 | |
|         let done = 0;
 | |
|         let isFirst = true;
 | |
|         for (const executor of this.executorList) {
 | |
|             this.onProgress?.(done, this.commandCount);
 | |
|             if (this.debugController) {
 | |
|                 this.debugController.setExecutor(executor);
 | |
|                 this.debugController.namedArguments = undefined;
 | |
|                 this.debugController.unnamedArguments = undefined;
 | |
|             }
 | |
|             // yield before doing anything with this executor, the debugger might want to do
 | |
|             // something with it (e.g., breakpoint, immediate closures that need resolving
 | |
|             // or stepping into)
 | |
|             yield executor;
 | |
|             /**@type {import('./SlashCommand.js').NamedArguments} */
 | |
|             // @ts-ignore
 | |
|             let args = {
 | |
|                 _scope: this.scope,
 | |
|                 _parserFlags: executor.parserFlags,
 | |
|                 _abortController: this.abortController,
 | |
|                 _debugController: this.debugController,
 | |
|                 _hasUnnamedArgument: executor.unnamedArgumentList.length > 0,
 | |
|             };
 | |
|             if (executor instanceof SlashCommandBreakPoint) {
 | |
|                 // nothing to do for breakpoints, just raise counter and yield for "before exec"
 | |
|                 done++;
 | |
|                 yield executor;
 | |
|                 isFirst = false;
 | |
|             } else if (executor instanceof SlashCommandBreak) {
 | |
|                 // /break need to resolve the unnamed arg and put it into pipe, then yield
 | |
|                 // for "before exec"
 | |
|                 const value = await this.substituteUnnamedArgument(executor, isFirst, args);
 | |
|                 done += this.executorList.length - this.executorList.indexOf(executor);
 | |
|                 this.scope.pipe = value ?? this.scope.pipe;
 | |
|                 yield executor;
 | |
|                 isFirst = false;
 | |
|             } else {
 | |
|                 // regular commands do all the argument resolving logic...
 | |
|                 await this.substituteNamedArguments(executor, args);
 | |
|                 let value = await this.substituteUnnamedArgument(executor, isFirst, args);
 | |
| 
 | |
|                 let abortResult = await this.testAbortController();
 | |
|                 if (abortResult) {
 | |
|                     return abortResult;
 | |
|                 }
 | |
|                 if (this.debugController) {
 | |
|                     this.debugController.namedArguments = args;
 | |
|                     this.debugController.unnamedArguments = value ?? '';
 | |
|                 }
 | |
|                 // then yield for "before exec"
 | |
|                 yield executor;
 | |
|                 // followed by command execution
 | |
|                 executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
 | |
|                 const isStepping = this.debugController?.testStepping(this);
 | |
|                 if (this.debugController) {
 | |
|                     this.debugController.isStepping = false || this.debugController.isSteppingInto;
 | |
|                 }
 | |
|                 try {
 | |
|                     this.scope.pipe = await executor.command.callback(args, value ?? '');
 | |
|                 } catch (ex) {
 | |
|                     throw new SlashCommandExecutionError(ex, ex.message, executor.name, executor.start, executor.end, this.fullText.slice(executor.start, executor.end), this.fullText);
 | |
|                 }
 | |
|                 if (this.debugController) {
 | |
|                     this.debugController.namedArguments = undefined;
 | |
|                     this.debugController.unnamedArguments = undefined;
 | |
|                     this.debugController.isStepping = isStepping;
 | |
|                 }
 | |
|                 this.#lintPipe(executor.command);
 | |
|                 done += executor.commandCount;
 | |
|                 this.onProgress?.(done, this.commandCount);
 | |
|                 abortResult = await this.testAbortController();
 | |
|                 if (abortResult) {
 | |
|                     return abortResult;
 | |
|                 }
 | |
|             }
 | |
|             // finally, yield for "after exec"
 | |
|             yield executor;
 | |
|             isFirst = false;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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.isQuietlyAborted = this.abortController.signal.isQuiet;
 | |
|             result.abortReason = this.abortController.signal.reason.toString();
 | |
|             return result;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param {SlashCommandExecutor} executor
 | |
|      * @param {import('./SlashCommand.js').NamedArguments} args
 | |
|      */
 | |
|     async substituteNamedArguments(executor, args) {
 | |
|         /**
 | |
|          * Handles the assignment of named arguments, considering if they accept multiple values
 | |
|          * @param {string} name The name of the argument, as defined for the command execution
 | |
|          * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value The value to be assigned
 | |
|          */
 | |
|         const assign = (name, value) => {
 | |
|             // If an array is supposed to be assigned, assign it one by one
 | |
|             if (Array.isArray(value)) {
 | |
|                 for (const val of value) {
 | |
|                     assign(name, val);
 | |
|                 }
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             const definition = executor.command.namedArgumentList.find(x => x.name == name);
 | |
| 
 | |
|             // Prefer definition name if a valid named args defintion is found
 | |
|             name = definition?.name ?? name;
 | |
| 
 | |
|             // Unescape named argument
 | |
|             if (value && typeof value == 'string') {
 | |
|                 value = value
 | |
|                     .replace(/\\\{/g, '{')
 | |
|                     .replace(/\\\}/g, '}');
 | |
|             }
 | |
| 
 | |
|             // If the named argument accepts multiple values, we have to make sure to build an array correctly
 | |
|             if (definition?.acceptsMultiple) {
 | |
|                 if (args[name] !== undefined) {
 | |
|                     // If there already is something for that named arg, make the value is an array and add to it
 | |
|                     let currentValue = args[name];
 | |
|                     if (!Array.isArray(currentValue)) {
 | |
|                         currentValue = [currentValue];
 | |
|                     }
 | |
|                     currentValue.push(value);
 | |
|                     args[name] = currentValue;
 | |
|                 } else {
 | |
|                     // If there is nothing in there, we create an array with that singular value
 | |
|                     args[name] = [value];
 | |
|                 }
 | |
|             } else {
 | |
|                 args[name] !== undefined && console.debug(`Named argument assigned multiple times: ${name}`);
 | |
|                 args[name] = 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;
 | |
|                 closure.breakController = this.breakController;
 | |
|                 if (this.debugController && !closure.debugController) {
 | |
|                     closure.debugController = this.debugController;
 | |
|                 }
 | |
|                 if (closure.executeNow) {
 | |
|                     assign(arg.name, (await closure.execute())?.pipe);
 | |
|                 } else {
 | |
|                     assign(arg.name, closure);
 | |
|                 }
 | |
|             } else {
 | |
|                 assign(arg.name, this.substituteParams(arg.value));
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param {SlashCommandExecutor} executor
 | |
|      * @param {boolean} isFirst
 | |
|      * @param {import('./SlashCommand.js').NamedArguments} args
 | |
|      * @returns {Promise<string|SlashCommandClosure|(string|SlashCommandClosure)[]>}
 | |
|      */
 | |
|     async substituteUnnamedArgument(executor, isFirst, args) {
 | |
|         let value;
 | |
|         // substitute unnamed argument
 | |
|         if (executor.unnamedArgumentList.length == 0) {
 | |
|             if (!isFirst && executor.injectPipe) {
 | |
|                 value = this.scope.pipe;
 | |
|                 args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;
 | |
|             }
 | |
|         } else {
 | |
|             value = [];
 | |
|             for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
 | |
|                 /** @type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */
 | |
|                 let v = executor.unnamedArgumentList[i].value;
 | |
|                 if (v instanceof SlashCommandClosure) {
 | |
|                     /**@type {SlashCommandClosure}*/
 | |
|                     const closure = v;
 | |
|                     closure.scope.parent = this.scope;
 | |
|                     closure.breakController = this.breakController;
 | |
|                     if (this.debugController && !closure.debugController) {
 | |
|                         closure.debugController = this.debugController;
 | |
|                     }
 | |
|                     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, '}')
 | |
|             ;
 | |
|         } else if (Array.isArray(value)) {
 | |
|             value = value.map(v=>{
 | |
|                 if (typeof v == 'string') {
 | |
|                     return v
 | |
|                         ?.replace(/\\\{/g, '{')
 | |
|                         ?.replace(/\\\}/g, '}');
 | |
|                 }
 | |
|                 return v;
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         value ??= '';
 | |
| 
 | |
|         // Make sure that if unnamed args are split, it should always return an array
 | |
|         if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
 | |
|             value = [value];
 | |
|         }
 | |
| 
 | |
|         return value;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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 = '';
 | |
|         } else if (!(typeof this.scope.pipe == 'string' || this.scope.pipe instanceof SlashCommandClosure)) {
 | |
|             console.warn(`/${command.name} returned illegal type (${typeof this.scope.pipe} - ${this.scope.pipe.constructor?.name ?? ''}). Auto-fixing to stringified JSON.`);
 | |
|             this.scope.pipe = JSON.stringify(this.scope.pipe) ?? '';
 | |
|         }
 | |
|     }
 | |
| }
 |