Blame Raw
Cohee · e3f41666 · · 2348 lines (89.4 KB)
2 contributors
1import { chat_metadata, getCurrentChatId, saveSettingsDebounced } from '../script.js';
2import { extension_settings, saveMetadataDebounced } from './extensions.js';
3import { executeSlashCommandsWithOptions } from './slash-commands.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
7import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
8import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
9import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
10import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
11import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
12import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
13import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
15import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
16
17/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
18/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
19
20const MAX_LOOPS = 100;
21
22export function getLocalVariable(name, args = {}) {
23 if (!chat_metadata.variables) {
24 chat_metadata.variables = {};
25 }
26
27 let localVariable = chat_metadata?.variables[args.key ?? name];
28 if (args.index !== undefined) {
29 try {
30 localVariable = JSON.parse(localVariable);
31 const numIndex = Number(args.index);
32 if (Number.isNaN(numIndex)) {
33 localVariable = localVariable[args.index];
34 } else {
35 localVariable = localVariable[Number(args.index)];
36 }
37 if (typeof localVariable == 'object') {
38 localVariable = JSON.stringify(localVariable);
39 }
40 } catch {
41 // that didn't work
42 }
43 }
44
45 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
46}
47
48export function setLocalVariable(name, value, args = {}) {
49 if (!name) {
50 throw new Error('Variable name cannot be empty or undefined.');
51 }
52
53 if (!chat_metadata.variables) {
54 chat_metadata.variables = {};
55 }
56
57 if (args.index !== undefined) {
58 try {
59 let localVariable = JSON.parse(chat_metadata.variables[name] ?? 'null');
60 const numIndex = Number(args.index);
61 if (Number.isNaN(numIndex)) {
62 if (localVariable === null) {
63 localVariable = {};
64 }
65 localVariable[args.index] = convertValueType(value, args.as);
66 } else {
67 if (localVariable === null) {
68 localVariable = [];
69 }
70 localVariable[numIndex] = convertValueType(value, args.as);
71 }
72 chat_metadata.variables[name] = JSON.stringify(localVariable);
73 } catch {
74 // that didn't work
75 }
76 } else {
77 chat_metadata.variables[name] = value;
78 }
79 saveMetadataDebounced();
80 return value;
81}
82
83export function getGlobalVariable(name, args = {}) {
84 let globalVariable = extension_settings.variables.global[args.key ?? name];
85 if (args.index !== undefined) {
86 try {
87 globalVariable = JSON.parse(globalVariable);
88 const numIndex = Number(args.index);
89 if (Number.isNaN(numIndex)) {
90 globalVariable = globalVariable[args.index];
91 } else {
92 globalVariable = globalVariable[Number(args.index)];
93 }
94 if (typeof globalVariable == 'object') {
95 globalVariable = JSON.stringify(globalVariable);
96 }
97 } catch {
98 // that didn't work
99 }
100 }
101
102 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
103}
104
105export function setGlobalVariable(name, value, args = {}) {
106 if (!name) {
107 throw new Error('Variable name cannot be empty or undefined.');
108 }
109
110 if (args.index !== undefined) {
111 try {
112 let globalVariable = JSON.parse(extension_settings.variables.global[name] ?? 'null');
113 const numIndex = Number(args.index);
114 if (Number.isNaN(numIndex)) {
115 if (globalVariable === null) {
116 globalVariable = {};
117 }
118 globalVariable[args.index] = convertValueType(value, args.as);
119 } else {
120 if (globalVariable === null) {
121 globalVariable = [];
122 }
123 globalVariable[numIndex] = convertValueType(value, args.as);
124 }
125 extension_settings.variables.global[name] = JSON.stringify(globalVariable);
126 } catch {
127 // that didn't work
128 }
129 } else {
130 extension_settings.variables.global[name] = value;
131 }
132 saveSettingsDebounced();
133 return value;
134}
135
136export function addLocalVariable(name, value) {
137 const currentValue = getLocalVariable(name) || 0;
138 try {
139 const parsedValue = JSON.parse(currentValue);
140 if (Array.isArray(parsedValue)) {
141 parsedValue.push(value);
142 setLocalVariable(name, JSON.stringify(parsedValue));
143 return parsedValue;
144 }
145 } catch {
146 // ignore non-array values
147 }
148 const increment = Number(value);
149
150 if (isNaN(increment) || isNaN(Number(currentValue))) {
151 const stringValue = String(currentValue || '') + value;
152 setLocalVariable(name, stringValue);
153 return stringValue;
154 }
155
156 const newValue = Number(currentValue) + increment;
157
158 if (isNaN(newValue)) {
159 return '';
160 }
161
162 setLocalVariable(name, newValue);
163 return newValue;
164}
165
166export function addGlobalVariable(name, value) {
167 const currentValue = getGlobalVariable(name) || 0;
168 try {
169 const parsedValue = JSON.parse(currentValue);
170 if (Array.isArray(parsedValue)) {
171 parsedValue.push(value);
172 setGlobalVariable(name, JSON.stringify(parsedValue));
173 return parsedValue;
174 }
175 } catch {
176 // ignore non-array values
177 }
178 const increment = Number(value);
179
180 if (isNaN(increment) || isNaN(Number(currentValue))) {
181 const stringValue = String(currentValue || '') + value;
182 setGlobalVariable(name, stringValue);
183 return stringValue;
184 }
185
186 const newValue = Number(currentValue) + increment;
187
188 if (isNaN(newValue)) {
189 return '';
190 }
191
192 setGlobalVariable(name, newValue);
193 return newValue;
194}
195
196export function incrementLocalVariable(name) {
197 return addLocalVariable(name, 1);
198}
199
200export function incrementGlobalVariable(name) {
201 return addGlobalVariable(name, 1);
202}
203
204export function decrementLocalVariable(name) {
205 return addLocalVariable(name, -1);
206}
207
208export function decrementGlobalVariable(name) {
209 return addGlobalVariable(name, -1);
210}
211
212/**
213 * Resolves a variable name to its value or returns the string as is if the variable does not exist.
214 * @param {string} name Variable name
215 * @param {SlashCommandScope} scope Scope
216 * @returns {string} Variable value or the string literal
217 */
218export function resolveVariable(name, scope = null) {
219 if (scope?.existsVariable(name)) {
220 return scope.getVariable(name);
221 }
222
223 if (existsLocalVariable(name)) {
224 return getLocalVariable(name);
225 }
226
227 if (existsGlobalVariable(name)) {
228 return getGlobalVariable(name);
229 }
230
231 return name;
232}
233
234/**
235 * Returns built-in variable macros.
236 * @returns {import('./macros.js').Macro[]}
237 */
238export function getVariableMacros() {
239 return [
240 // Replace {{setvar::name::value}} with empty string and set the variable name to value
241 { regex: /{{setvar::([^:]+)::([^}]*)}}/gi, replace: (_, name, value) => { setLocalVariable(name.trim(), value); return ''; } },
242 // Replace {{addvar::name::value}} with empty string and add value to the variable value
243 { regex: /{{addvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addLocalVariable(name.trim(), value); return ''; } },
244 // Replace {{incvar::name}} with empty string and increment the variable name by 1
245 { regex: /{{incvar::([^}]+)}}/gi, replace: (_, name) => incrementLocalVariable(name.trim()) },
246 // Replace {{decvar::name}} with empty string and decrement the variable name by 1
247 { regex: /{{decvar::([^}]+)}}/gi, replace: (_, name) => decrementLocalVariable(name.trim()) },
248 // Replace {{getvar::name}} with the value of the variable name
249 { regex: /{{getvar::([^}]+)}}/gi, replace: (_, name) => getLocalVariable(name.trim()) },
250 // Replace {{setglobalvar::name::value}} with empty string and set the global variable name to value
251 { regex: /{{setglobalvar::([^:]+)::([^}]*)}}/gi, replace: (_, name, value) => { setGlobalVariable(name.trim(), value); return ''; } },
252 // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value
253 { regex: /{{addglobalvar::([^:]+)::([^}]+)}}/gi, replace: (_, name, value) => { addGlobalVariable(name.trim(), value); return ''; } },
254 // Replace {{incglobalvar::name}} with empty string and increment the global variable name by 1
255 { regex: /{{incglobalvar::([^}]+)}}/gi, replace: (_, name) => incrementGlobalVariable(name.trim()) },
256 // Replace {{decglobalvar::name}} with empty string and decrement the global variable name by 1
257 { regex: /{{decglobalvar::([^}]+)}}/gi, replace: (_, name) => decrementGlobalVariable(name.trim()) },
258 // Replace {{getglobalvar::name}} with the value of the global variable name
259 { regex: /{{getglobalvar::([^}]+)}}/gi, replace: (_, name) => getGlobalVariable(name.trim()) },
260 ];
261}
262
263async function listVariablesCallback(args) {
264 /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
265 let returnType = args.return;
266
267 // Now the actual new return type handling
268 const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
269 if (!chat_metadata.variables) {
270 chat_metadata.variables = {};
271 }
272
273 const includeLocalVariables = scope === 'all' || scope === 'local';
274 const includeGlobalVariables = scope === 'all' || scope === 'global';
275
276 const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
277 const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
278
279 const buildTextValue = (_) => {
280 const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
281 const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
282 const chatName = getCurrentChatId();
283
284 const message = [
285 includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
286 includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
287 ].filter(x => x).join('\n\n');
288 return message;
289 };
290
291 const jsonVariables = [
292 ...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
293 ...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
294 ];
295
296 return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue });
297}
298
299/**
300 *
301 * @param {NamedArguments} args
302 * @param {(string|SlashCommandClosure)[]} value
303 */
304async function whileCallback(args, value) {
305 if (args.guard instanceof SlashCommandClosure) throw new Error('argument \'guard\' cannot be a closure for command /while');
306 const isGuardOff = isFalseBoolean(args.guard?.toString());
307 const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
308 /**@type {string|SlashCommandClosure} */
309 let command;
310 if (value) {
311 if (value[0] instanceof SlashCommandClosure) {
312 command = value[0];
313 } else {
314 command = value.join(' ');
315 }
316 }
317
318 let commandResult;
319 for (let i = 0; i < iterations; i++) {
320 const { a, b, rule } = parseBooleanOperands(args);
321 const result = evalBoolean(rule, a, b);
322
323 if (result && command) {
324 if (command instanceof SlashCommandClosure) {
325 command.breakController = new SlashCommandBreakController();
326 commandResult = await command.execute();
327 } else {
328 commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
329 }
330 if (commandResult.isAborted) break;
331 if (commandResult.isBreak) break;
332 } else {
333 break;
334 }
335 }
336
337 if (commandResult) {
338 return commandResult.pipe;
339 }
340
341 return '';
342}
343
344/**
345 *
346 * @param {NamedArguments} args
347 * @param {UnnamedArguments} value
348 * @returns
349 */
350async function timesCallback(args, value) {
351 if (args.guard instanceof SlashCommandClosure) throw new Error('argument \'guard\' cannot be a closure for command /while');
352 let repeats;
353 let command;
354 if (Array.isArray(value)) {
355 [repeats, ...command] = value;
356 if (command[0] instanceof SlashCommandClosure) {
357 command = command[0];
358 } else {
359 command = command.join(' ');
360 }
361 } else {
362 [repeats, ...command] = /**@type {string}*/(value).split(' ');
363 command = command.join(' ');
364 }
365 const isGuardOff = isFalseBoolean(args.guard?.toString());
366 const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
367 let result;
368 for (let i = 0; i < iterations; i++) {
369 if (command instanceof SlashCommandClosure) {
370 command.breakController = new SlashCommandBreakController();
371 command.scope.setMacro('timesIndex', i);
372 result = await command.execute();
373 } else {
374 result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController);
375 }
376 if (result.isAborted) break;
377 if (result.isBreak) break;
378 }
379
380 return result?.pipe ?? '';
381}
382
383/**
384 *
385 * @param {NamedArguments} args
386 * @param {(string|SlashCommandClosure)[]} value
387 */
388async function ifCallback(args, value) {
389 const { a, b, rule } = parseBooleanOperands(args);
390 const result = evalBoolean(rule, a, b);
391
392 /** @type {string|SlashCommandClosure} */
393 let command;
394 if (value) {
395 if (value[0] instanceof SlashCommandClosure) {
396 command = value[0];
397 } else {
398 command = value.join(' ');
399 }
400 }
401
402 let commandResult;
403 if (result && command) {
404 if (command instanceof SlashCommandClosure) return (await command.execute()).pipe;
405 commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
406 } else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
407 if (args.else instanceof SlashCommandClosure) return (await args.else.execute()).pipe;
408 commandResult = await executeSubCommands(args.else, args._scope, args._parserFlags, args._abortController);
409 }
410
411 if (commandResult) {
412 return commandResult.pipe;
413 }
414 return '';
415}
416
417/**
418 * Checks if a local variable exists.
419 * @param {string} name Local variable name
420 * @returns {boolean} True if the local variable exists, false otherwise
421 */
422export function existsLocalVariable(name) {
423 return chat_metadata.variables && chat_metadata.variables[name] !== undefined;
424}
425
426/**
427 * Checks if a global variable exists.
428 * @param {string} name Global variable name
429 * @returns {boolean} True if the global variable exists, false otherwise
430 */
431export function existsGlobalVariable(name) {
432 return extension_settings.variables.global && extension_settings.variables.global[name] !== undefined;
433}
434
435/**
436 * Parses boolean operands from command arguments.
437 * @param {object} args Command arguments
438 * @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands
439 */
440export function parseBooleanOperands(args) {
441 // Resolution order: numeric literal, local variable, global variable, string literal
442 /**
443 * @param {string} operand Boolean operand candidate
444 */
445 function getOperand(operand) {
446 if (operand === undefined) {
447 return undefined;
448 }
449 if (operand === '') {
450 return '';
451 }
452
453 // Number parses spaces as 0, and parseFloat is weird
454 const operandNumber = typeof operand === 'string' && operand.trim().length ? Number(operand) : NaN;
455
456 if (!isNaN(operandNumber)) {
457 return operandNumber;
458 }
459
460 if (args._scope.existsVariable(operand)) {
461 const operandVariable = args._scope.getVariable(operand);
462 return operandVariable ?? '';
463 }
464
465 if (existsLocalVariable(operand)) {
466 const operandLocalVariable = getLocalVariable(operand);
467 return operandLocalVariable ?? '';
468 }
469
470 if (existsGlobalVariable(operand)) {
471 const operandGlobalVariable = getGlobalVariable(operand);
472 return operandGlobalVariable ?? '';
473 }
474
475 const stringLiteral = String(operand);
476 return stringLiteral || '';
477 }
478
479 const left = getOperand(args.a ?? args.left ?? args.first ?? args.x);
480 const right = getOperand(args.b ?? args.right ?? args.second ?? args.y);
481 const rule = args.rule;
482
483 return { a: left, b: right, rule };
484}
485
486/**
487 * Evaluates a boolean comparison rule.
488 *
489 * @param {string?} rule Boolean comparison rule
490 * @param {string|number} a The left operand
491 * @param {string|number?} b The right operand
492 * @returns {boolean} True if the rule yields true, false otherwise
493 */
494export function evalBoolean(rule, a, b) {
495 if (a === undefined) {
496 throw new Error('Left operand is not provided');
497 }
498
499 // If right-hand side was not provided, whe just check if the left side is truthy
500 if (b === undefined) {
501 switch (rule) {
502 case undefined:
503 case 'not': {
504 const resultOnTruthy = rule !== 'not';
505 if (isTrueBoolean(String(a))) return resultOnTruthy;
506 if (isFalseBoolean(String(a))) return !resultOnTruthy;
507 return a ? resultOnTruthy : !resultOnTruthy;
508 }
509 default:
510 throw new Error(`Unknown boolean comparison rule for truthy check. If right operand is not provided, the rule must not provided or be 'not'. Provided: ${rule}`);
511 }
512 }
513
514 // If no rule was provided, we are implicitly using 'eq', as defined for the slash commands
515 rule ??= 'eq';
516
517 if (typeof a === 'number' && typeof b === 'number') {
518 // only do numeric comparison if both operands are numbers
519 const aNumber = Number(a);
520 const bNumber = Number(b);
521
522 switch (rule) {
523 case 'gt':
524 return aNumber > bNumber;
525 case 'gte':
526 return aNumber >= bNumber;
527 case 'lt':
528 return aNumber < bNumber;
529 case 'lte':
530 return aNumber <= bNumber;
531 case 'eq':
532 return aNumber === bNumber;
533 case 'neq':
534 return aNumber !== bNumber;
535 case 'in':
536 case 'nin':
537 // Fall through to string comparison. Otherwise you could not check if 12345 contains 45 for example.
538 console.debug(`Boolean comparison rule '${rule}' is not supported for type number. Falling back to string comparison.`);
539 break;
540 default:
541 throw new Error(`Unknown boolean comparison rule for type number. Accepted: gt, gte, lt, lte, eq, neq. Provided: ${rule}`);
542 }
543 }
544
545 // otherwise do case-insensitive string comparsion, stringify non-strings
546 let aString = (typeof a === 'string') ? a.toLowerCase() : JSON.stringify(a).toLowerCase();
547 let bString = (typeof b === 'string') ? b.toLowerCase() : JSON.stringify(b).toLowerCase();
548
549 switch (rule) {
550 case 'in':
551 return aString.includes(bString);
552 case 'nin':
553 return !aString.includes(bString);
554 case 'eq':
555 return aString === bString;
556 case 'neq':
557 return aString !== bString;
558 default:
559 throw new Error(`Unknown boolean comparison rule for type string. Accepted: in, nin, eq, neq. Provided: ${rule}`);
560 }
561}
562
563/**
564 * Executes a slash command from a string (may be enclosed in quotes) and returns the result.
565 * @param {string} command Command to execute. May contain escaped macro and batch separators.
566 * @param {SlashCommandScope} [scope] The scope to use.
567 * @param {import('./slash-commands/SlashCommandParser.js').ParserFlags} [parserFlags] The parser flags to use.
568 * @param {SlashCommandAbortController} [abortController] The abort controller to use.
569 * @returns {Promise<SlashCommandClosureResult>} Closure execution result
570 */
571async function executeSubCommands(command, scope = null, parserFlags = null, abortController = null) {
572 if (command.startsWith('"') && command.endsWith('"')) {
573 command = command.slice(1, -1);
574 }
575
576 const result = await executeSlashCommandsWithOptions(command, {
577 handleExecutionErrors: false,
578 handleParserErrors: false,
579 parserFlags,
580 scope,
581 abortController: abortController ?? new SlashCommandAbortController(),
582 });
583
584 return result;
585}
586
587/**
588 * Deletes a local variable.
589 * @param {string} name Variable name to delete
590 * @returns {string} Empty string
591 */
592export function deleteLocalVariable(name) {
593 if (!existsLocalVariable(name)) {
594 console.warn(`The local variable "${name}" does not exist.`);
595 return '';
596 }
597
598 delete chat_metadata.variables[name];
599 saveMetadataDebounced();
600 return '';
601}
602
603/**
604 * Deletes a global variable.
605 * @param {string} name Variable name to delete
606 * @returns {string} Empty string
607 */
608export function deleteGlobalVariable(name) {
609 if (!existsGlobalVariable(name)) {
610 console.warn(`The global variable "${name}" does not exist.`);
611 return '';
612 }
613
614 delete extension_settings.variables.global[name];
615 saveSettingsDebounced();
616 return '';
617}
618
619/**
620 * Parses a series of numeric values from a string or a string array.
621 * @param {string|string[]} value A space-separated list of numeric values or variable names
622 * @param {SlashCommandScope} scope Scope
623 * @returns {number[]} An array of numeric values
624 */
625function parseNumericSeries(value, scope = null) {
626 if (typeof value === 'number') {
627 return [value];
628 }
629
630 /** @type {(string|number)[]} */
631 let values = Array.isArray(value) ? value : value.split(' ');
632
633 // If an array of strings was provided as the only value, convert it to an array
634 if (values.length === 1 && typeof values[0] === 'string') {
635 if (values[0].startsWith('[')) {
636 // JSON-style array
637 values = convertValueType(values[0], 'array');
638 } else {
639 // Space-separated string
640 values = values[0].split(' ');
641 }
642 }
643
644 const array = values.map(i => typeof i === 'string' ? i.trim() : i)
645 .filter(i => i !== '')
646 .map(i => isNaN(Number(i)) ? Number(resolveVariable(String(i), scope)) : Number(i))
647 .filter(i => !isNaN(i));
648
649 return array;
650}
651
652function performOperation(value, operation, singleOperand = false, scope = null) {
653 function getResult() {
654 if (!value) {
655 return 0;
656 }
657
658 const array = parseNumericSeries(value, scope);
659
660 if (array.length === 0) {
661 return 0;
662 }
663
664 const result = singleOperand ? operation(array[0]) : operation(array);
665
666 if (isNaN(result)) {
667 return 0;
668 }
669
670 return result;
671 }
672
673 const result = getResult();
674 return String(result);
675}
676
677function addValuesCallback(args, value) {
678 return performOperation(value, (array) => array.reduce((a, b) => a + b, 0), false, args._scope);
679}
680
681function mulValuesCallback(args, value) {
682 return performOperation(value, (array) => array.reduce((a, b) => a * b, 1), false, args._scope);
683}
684
685function minValuesCallback(args, value) {
686 return performOperation(value, (array) => Math.min(...array), false, args._scope);
687}
688
689function maxValuesCallback(args, value) {
690 return performOperation(value, (array) => Math.max(...array), false, args._scope);
691}
692
693function subValuesCallback(args, value) {
694 return performOperation(value, (array) => array.reduce((a, b) => a - b, array.shift() ?? 0), false, args._scope);
695}
696
697function divValuesCallback(args, value) {
698 return performOperation(value, (array) => {
699 if (array[1] === 0) {
700 console.warn('Division by zero.');
701 return 0;
702 }
703 return array[0] / array[1];
704 }, false, args._scope);
705}
706
707function modValuesCallback(args, value) {
708 return performOperation(value, (array) => {
709 if (array[1] === 0) {
710 console.warn('Division by zero.');
711 return 0;
712 }
713 return array[0] % array[1];
714 }, false, args._scope);
715}
716
717function powValuesCallback(args, value) {
718 return performOperation(value, (array) => Math.pow(array[0], array[1]), false, args._scope);
719}
720
721function sinValuesCallback(args, value) {
722 return performOperation(value, Math.sin, true, args._scope);
723}
724
725function cosValuesCallback(args, value) {
726 return performOperation(value, Math.cos, true, args._scope);
727}
728
729function logValuesCallback(args, value) {
730 return performOperation(value, Math.log, true, args._scope);
731}
732
733function roundValuesCallback(args, value) {
734 return performOperation(value, Math.round, true, args._scope);
735}
736
737function absValuesCallback(args, value) {
738 return performOperation(value, Math.abs, true, args._scope);
739}
740
741function sqrtValuesCallback(args, value) {
742 return performOperation(value, Math.sqrt, true, args._scope);
743}
744
745function lenValuesCallback(value) {
746 let parsedValue = value;
747 try {
748 parsedValue = JSON.parse(value);
749 } catch {
750 // could not parse
751 }
752 if (Array.isArray(parsedValue)) {
753 return parsedValue.length;
754 }
755 switch (typeof parsedValue) {
756 case 'string':
757 return parsedValue.length;
758 case 'object':
759 return Object.keys(parsedValue).length;
760 case 'number':
761 return String(parsedValue).length;
762 default:
763 return 0;
764 }
765}
766
767function randValuesCallback(from, to, args) {
768 const range = to - from;
769 const value = from + Math.random() * range;
770 if (args.round == 'round') {
771 return Math.round(value);
772 }
773 if (args.round == 'ceil') {
774 return Math.ceil(value);
775 }
776 if (args.round == 'floor') {
777 return Math.floor(value);
778 }
779 return value;
780}
781
782function customSortComparitor(a, b) {
783 if (typeof a != typeof b) {
784 a = typeof a;
785 b = typeof b;
786 }
787 return a > b ? 1 : a < b ? -1 : 0;
788}
789
790function sortArrayObjectCallback(args, value) {
791 let parsedValue;
792 if (typeof value == 'string') {
793 try {
794 parsedValue = JSON.parse(value);
795 } catch {
796 // return the original input if it was invalid
797 return value;
798 }
799 } else {
800 parsedValue = value;
801 }
802 if (Array.isArray(parsedValue)) {
803 // always sort lists by value
804 parsedValue.sort(customSortComparitor);
805 } else if (typeof parsedValue == 'object') {
806 let keysort = args.keysort;
807 if (isFalseBoolean(keysort)) {
808 parsedValue = Object.keys(parsedValue).sort(function (a, b) { return customSortComparitor(parsedValue[a], parsedValue[b]); });
809 } else {
810 parsedValue = Object.keys(parsedValue).sort(customSortComparitor);
811 }
812 }
813 return JSON.stringify(parsedValue);
814}
815
816/**
817 * Declare a new variable in the current scope.
818 * @param {NamedArguments} args Named arguments.
819 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value Name and optional value for the variable.
820 * @returns The variable's value
821 */
822function letCallback(args, value) {
823 if (!Array.isArray(value)) value = [value];
824 if (args.key !== undefined) {
825 const key = args.key;
826 if (typeof key != 'string') throw new Error('Key must be a string');
827 if (args._hasUnnamedArgument) {
828 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
829 args._scope.letVariable(key, val);
830 return val;
831 } else {
832 args._scope.letVariable(key);
833 return '';
834 }
835 }
836 const key = value.shift();
837 if (typeof key != 'string') throw new Error('Key must be a string');
838 if (value.length > 0) {
839 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
840 args._scope.letVariable(key, val);
841 return val;
842 } else {
843 args._scope.letVariable(key);
844 return '';
845 }
846}
847
848/**
849 * Set or retrieve a variable in the current scope or nearest ancestor scope.
850 * @param {NamedArguments} args Named arguments.
851 * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value Name and optional value for the variable.
852 * @returns The variable's value
853 */
854function varCallback(args, value) {
855 if (!Array.isArray(value)) value = [value];
856 if (args.key !== undefined) {
857 const key = args.key;
858 if (typeof key != 'string') throw new Error('Key must be a string');
859 if (args._hasUnnamedArgument) {
860 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
861 args._scope.setVariable(key, val, args.index, args.as);
862 return val;
863 } else {
864 return args._scope.getVariable(key, args.index);
865 }
866 }
867 const key = value.shift();
868 if (typeof key != 'string') throw new Error('Key must be a string');
869 if (value.length > 0) {
870 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
871 args._scope.setVariable(key, val, args.index, args.as);
872 return val;
873 } else {
874 return args._scope.getVariable(key, args.index);
875 }
876}
877
878/**
879 * @param {NamedArguments} args
880 * @param {SlashCommandClosure} value
881 * @returns {string}
882 */
883function closureSerializeCallback(args, value) {
884 if (!(value instanceof SlashCommandClosure)) {
885 throw new Error('unnamed argument must be a closure');
886 }
887 return value.rawText;
888}
889
890/**
891 * @param {NamedArguments} args
892 * @param {UnnamedArguments} value
893 * @returns {SlashCommandClosure}
894 */
895function closureDeserializeCallback(args, value) {
896 const parser = new SlashCommandParser();
897 const closure = parser.parse(value, true, args._parserFlags, args._abortController);
898 closure.scope.parent = args._scope;
899 return closure;
900}
901
902export function registerVariableCommands() {
903 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
904 name: 'listvar',
905 callback: listVariablesCallback,
906 aliases: ['listchatvar'],
907 helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>return</code> argument to change the return type.',
908 returns: 'JSON list of local variables',
909 namedArgumentList: [
910 SlashCommandNamedArgument.fromProps({
911 name: 'scope',
912 description: 'filter variables by scope',
913 typeList: [ARGUMENT_TYPE.STRING],
914 defaultValue: 'all',
915 isRequired: false,
916 forceEnum: true,
917 enumList: [
918 new SlashCommandEnumValue('all', 'All variables', enumTypes.enum, enumIcons.variable),
919 new SlashCommandEnumValue('local', 'Local variables', enumTypes.enum, enumIcons.localVariable),
920 new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable),
921 ],
922 }),
923 SlashCommandNamedArgument.fromProps({
924 name: 'return',
925 description: 'The way how you want the return value to be provided',
926 typeList: [ARGUMENT_TYPE.STRING],
927 defaultValue: 'popup-html',
928 enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
929 forceEnum: true,
930 }),
931 ],
932 }));
933 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
934 name: 'setvar',
935 callback: (args, value) => String(setLocalVariable(args.key || args.name, value, args)),
936 aliases: ['setchatvar'],
937 returns: 'the set variable value',
938 namedArgumentList: [
939 SlashCommandNamedArgument.fromProps({
940 name: 'key',
941 description: 'variable name',
942 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
943 isRequired: true,
944 enumProvider: commonEnumProviders.variables('local'),
945 forceEnum: false,
946 }),
947 new SlashCommandNamedArgument(
948 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
949 ),
950 SlashCommandNamedArgument.fromProps({
951 name: 'as',
952 description: 'change the type of the value when used with index',
953 forceEnum: true,
954 enumProvider: commonEnumProviders.types,
955 isRequired: false,
956 defaultValue: 'string',
957 }),
958 ],
959 unnamedArgumentList: [
960 new SlashCommandArgument(
961 'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], true,
962 ),
963 ],
964 helpString: `
965 <div>
966 Set a local variable value and pass it down the pipe. The <code>index</code> argument is optional.
967 To convert the value to a specific JSON type when using <code>index</code>, use the <code>as</code> argument.
968 </div>
969 <div>
970 <strong>Example:</strong>
971 <ul>
972 <li>
973 <pre><code class="language-stscript">/setvar key=color green</code></pre>
974 </li>
975 <li>
976 <pre><code class="language-stscript">/setvar key=ages index=John as=number 21</code></pre>
977 </li>
978 </ul>
979 </div>
980 `,
981 }));
982 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
983 name: 'getvar',
984 callback: (args, value) => String(getLocalVariable(value, args)),
985 aliases: ['getchatvar'],
986 returns: 'the variable value',
987 namedArgumentList: [
988 SlashCommandNamedArgument.fromProps({
989 name: 'key',
990 description: 'variable name',
991 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
992 enumProvider: commonEnumProviders.variables('local'),
993 }),
994 new SlashCommandNamedArgument(
995 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
996 ),
997 ],
998 unnamedArgumentList: [
999 SlashCommandArgument.fromProps({
1000 description: 'key',
1001 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1002 isRequired: false,
1003 enumProvider: commonEnumProviders.variables('local'),
1004 }),
1005 ],
1006 helpString: `
1007 <div>
1008 Get a local variable value and pass it down the pipe. The <code>index</code> argument is optional.
1009 </div>
1010 <div>
1011 <strong>Examples:</strong>
1012 <ul>
1013 <li>
1014 <pre><code class="language-stscript">/getvar height</code></pre>
1015 </li>
1016 <li>
1017 <pre><code class="language-stscript">/getvar key=height</code></pre>
1018 </li>
1019 <li>
1020 <pre><code class="language-stscript">/getvar index=3 costumes</code></pre>
1021 </li>
1022 </ul>
1023 </div>
1024 `,
1025 }));
1026 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1027 name: 'addvar',
1028 callback: (args, value) => String(addLocalVariable(args.key || args.name, value)),
1029 aliases: ['addchatvar'],
1030 returns: 'the new variable value',
1031 namedArgumentList: [
1032 SlashCommandNamedArgument.fromProps({
1033 name: 'key',
1034 description: 'variable name',
1035 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1036 isRequired: true,
1037 enumProvider: commonEnumProviders.variables('local'),
1038 forceEnum: false,
1039 }),
1040 ],
1041 unnamedArgumentList: [
1042 new SlashCommandArgument(
1043 'value to add to the variable', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
1044 ),
1045 ],
1046 helpString: `
1047 <div>
1048 Add a value to a local variable and pass the result down the pipe.
1049 </div>
1050 <div>
1051 <strong>Example:</strong>
1052 <ul>
1053 <li>
1054 <pre><code class="language-stscript">/addvar key=score 10</code></pre>
1055 </li>
1056 </ul>
1057 </div>
1058 `,
1059 }));
1060 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1061 name: 'setglobalvar',
1062 callback: (args, value) => String(setGlobalVariable(args.key || args.name, value, args)),
1063 returns: 'the set global variable value',
1064 namedArgumentList: [
1065 SlashCommandNamedArgument.fromProps({
1066 name: 'key',
1067 description: 'variable name',
1068 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1069 isRequired: true,
1070 enumProvider: commonEnumProviders.variables('global'),
1071 forceEnum: false,
1072 }),
1073 new SlashCommandNamedArgument(
1074 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
1075 ),
1076 SlashCommandNamedArgument.fromProps({
1077 name: 'as',
1078 description: 'change the type of the value when used with index',
1079 forceEnum: true,
1080 enumProvider: commonEnumProviders.types,
1081 isRequired: false,
1082 defaultValue: 'string',
1083 }),
1084 ],
1085 unnamedArgumentList: [
1086 new SlashCommandArgument(
1087 'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], true,
1088 ),
1089 ],
1090 helpString: `
1091 <div>
1092 Set a global variable value and pass it down the pipe. The <code>index</code> argument is optional.
1093 To convert the value to a specific JSON type when using <code>index</code>, use the <code>as</code> argument.
1094 </div>
1095 <div>
1096 <strong>Example:</strong>
1097 <ul>
1098 <li>
1099 <pre><code class="language-stscript">/setglobalvar key=color green</code></pre>
1100 </li>
1101 <li>
1102 <pre><code class="language-stscript">/setglobalvar key=ages index=John as=number 21</code></pre>
1103 </li>
1104 </ul>
1105 </div>
1106 `,
1107 }));
1108 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1109 name: 'getglobalvar',
1110 callback: (args, value) => String(getGlobalVariable(value, args)),
1111 returns: 'global variable value',
1112 namedArgumentList: [
1113 SlashCommandNamedArgument.fromProps({
1114 name: 'key',
1115 description: 'variable name',
1116 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1117 enumProvider: commonEnumProviders.variables('global'),
1118 }),
1119 new SlashCommandNamedArgument(
1120 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
1121 ),
1122 ],
1123 unnamedArgumentList: [
1124 SlashCommandArgument.fromProps({
1125 description: 'key',
1126 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1127 enumProvider: commonEnumProviders.variables('global'),
1128 }),
1129 ],
1130 helpString: `
1131 <div>
1132 Get a global variable value and pass it down the pipe. The <code>index</code> argument is optional.
1133 </div>
1134 <div>
1135 <strong>Examples:</strong>
1136 <ul>
1137 <li>
1138 <pre><code class="language-stscript">/getglobalvar height</code></pre>
1139 </li>
1140 <li>
1141 <pre><code class="language-stscript">/getglobalvar key=height</code></pre>
1142 </li>
1143 <li>
1144 <pre><code class="language-stscript">/getglobalvar index=3 costumes</code></pre>
1145 </li>
1146 </ul>
1147 </div>
1148 `,
1149 }));
1150 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1151 name: 'addglobalvar',
1152 callback: (args, value) => String(addGlobalVariable(args.key || args.name, value)),
1153 returns: 'the new variable value',
1154 namedArgumentList: [
1155 SlashCommandNamedArgument.fromProps({
1156 name: 'key',
1157 description: 'variable name',
1158 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1159 isRequired: true,
1160 enumProvider: commonEnumProviders.variables('global'),
1161 forceEnum: false,
1162 }),
1163 ],
1164 unnamedArgumentList: [
1165 new SlashCommandArgument(
1166 'value to add to the variable', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
1167 ),
1168 ],
1169 helpString: `
1170 <div>
1171 Add a value to a global variable and pass the result down the pipe.
1172 </div>
1173 <div>
1174 <strong>Example:</strong>
1175 <ul>
1176 <li>
1177 <pre><code class="language-stscript">/addglobalvar key=score 10</code></pre>
1178 </li>
1179 </ul>
1180 </div>
1181 `,
1182 }));
1183 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1184 name: 'incvar',
1185 callback: (_, value) => String(incrementLocalVariable(value)),
1186 aliases: ['incchatvar'],
1187 returns: 'the new variable value',
1188 unnamedArgumentList: [
1189 SlashCommandNamedArgument.fromProps({
1190 name: 'key',
1191 description: 'variable name',
1192 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1193 isRequired: true,
1194 enumProvider: commonEnumProviders.variables('local'),
1195 forceEnum: false,
1196 }),
1197 ],
1198 helpString: `
1199 <div>
1200 Increment a local variable by 1 and pass the result down the pipe.
1201 </div>
1202 <div>
1203 <strong>Example:</strong>
1204 <ul>
1205 <li>
1206 <pre><code class="language-stscript">/incvar score</code></pre>
1207 </li>
1208 </ul>
1209 </div>
1210 `,
1211 }));
1212 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1213 name: 'decvar',
1214 callback: (_, value) => String(decrementLocalVariable(value)),
1215 aliases: ['decchatvar'],
1216 returns: 'the new variable value',
1217 unnamedArgumentList: [
1218 SlashCommandNamedArgument.fromProps({
1219 name: 'key',
1220 description: 'variable name',
1221 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1222 isRequired: true,
1223 enumProvider: commonEnumProviders.variables('local'),
1224 forceEnum: false,
1225 }),
1226 ],
1227 helpString: `
1228 <div>
1229 Decrement a local variable by 1 and pass the result down the pipe.
1230 </div>
1231 <div>
1232 <strong>Example:</strong>
1233 <ul>
1234 <li>
1235 <pre><code class="language-stscript">/decvar score</code></pre>
1236 </li>
1237 </ul>
1238 </div>
1239 `,
1240 }));
1241 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1242 name: 'incglobalvar',
1243 callback: (_, value) => String(incrementGlobalVariable(value)),
1244 returns: 'the new variable value',
1245 unnamedArgumentList: [
1246 SlashCommandNamedArgument.fromProps({
1247 name: 'key',
1248 description: 'variable name',
1249 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1250 isRequired: true,
1251 enumProvider: commonEnumProviders.variables('global'),
1252 forceEnum: false,
1253 }),
1254 ],
1255 helpString: `
1256 <div>
1257 Increment a global variable by 1 and pass the result down the pipe.
1258 </div>
1259 <div>
1260 <strong>Example:</strong>
1261 <ul>
1262 <li>
1263 <pre><code class="language-stscript">/incglobalvar score</code></pre>
1264 </li>
1265 </ul>
1266 </div>
1267 `,
1268 }));
1269 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1270 name: 'decglobalvar',
1271 callback: (_, value) => String(decrementGlobalVariable(value)),
1272 returns: 'the new variable value',
1273 unnamedArgumentList: [
1274 SlashCommandNamedArgument.fromProps({
1275 name: 'key',
1276 description: 'variable name',
1277 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
1278 isRequired: true,
1279 enumProvider: commonEnumProviders.variables('global'),
1280 forceEnum: false,
1281 }),
1282 ],
1283 helpString: `
1284 <div>
1285 Decrement a global variable by 1 and pass the result down the pipe.
1286 </div>
1287 <div>
1288 <strong>Example:</strong>
1289 <ul>
1290 <li>
1291 <pre><code class="language-stscript">/decglobalvar score</code></pre>
1292 </li>
1293 </ul>
1294 </div>
1295 `,
1296 }));
1297 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1298 name: 'if',
1299 callback: ifCallback,
1300 returns: 'result of the executed command ("then" or "else")',
1301 namedArgumentList: [
1302 SlashCommandNamedArgument.fromProps({
1303 name: 'left',
1304 description: 'left operand',
1305 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1306 isRequired: true,
1307 enumProvider: commonEnumProviders.variables('all'),
1308 }),
1309 SlashCommandNamedArgument.fromProps({
1310 name: 'right',
1311 description: 'right operand',
1312 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1313 enumProvider: commonEnumProviders.variables('all'),
1314 }),
1315 SlashCommandNamedArgument.fromProps({
1316 name: 'rule',
1317 description: 'comparison rule',
1318 typeList: [ARGUMENT_TYPE.STRING],
1319 defaultValue: 'eq',
1320 enumList: [
1321 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
1322 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
1323 new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
1324 new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
1325 new SlashCommandEnumValue('gt', 'a > b (numbers)'),
1326 new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1327 new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1328 new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1329 new SlashCommandEnumValue('not', '!a (truthy)'),
1330 ],
1331 forceEnum: true,
1332 }),
1333 SlashCommandNamedArgument.fromProps({
1334 name: 'else',
1335 description: 'command to execute if not true',
1336 typeList: [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
1337 }),
1338 ],
1339 unnamedArgumentList: [
1340 new SlashCommandArgument(
1341 'command to execute if true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
1342 ),
1343 ],
1344 splitUnnamedArgument: true,
1345 helpString: `
1346 <div>
1347 Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
1348 and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the
1349 result of the command execution down the pipe.
1350 </div>
1351 <div>
1352 Numeric values and string literals for left and right operands supported.
1353 </div>
1354 <div>
1355 If the rule is not provided, it defaults to <code>eq</code>.
1356 </div>
1357 <div>
1358 If no right operand is provided, it defaults to checking the <code>left</code> value to be truthy.
1359 A non-empty string or non-zero number is considered truthy, as is the value <code>true</code> or <code>on</code>.<br />
1360 Only acceptable rules for no provided right operand are <code>not</code>, and no provided rule - which default to returning whether it is not or is truthy.
1361 </div>
1362 <div>
1363 <strong>Available rules:</strong>
1364 <ul>
1365 <li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
1366 <li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
1367 <li><code>in</code> => a includes b <small>(strings & numbers as strings)</small></li>
1368 <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
1369 <li><code>gt</code> => a > b <small>(numbers)</small></li>
1370 <li><code>gte</code> => a >= b <small>(numbers)</small></li>
1371 <li><code>lt</code> => a < b <small>(numbers)</small></li>
1372 <li><code>lte</code> => a <= b <small>(numbers)</small></li>
1373 <li><code>not</code> => !a <small>(truthy)</small></li>
1374 </ul>
1375 </div>
1376 <div>
1377 <strong>Examples:</strong>
1378 <ul>
1379 <li>
1380 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>
1381 triggers a /speak command if the value of "score" is greater or equals 10.
1382 </li>
1383 <li>
1384 <pre><code class="language-stscript">/if left={{lastMessage}} rule=in right=surprise {: /echo SURPISE! :}</code></pre>
1385 executes a subcommand defined as a closure if the given value contains a specified word.
1386 <li>
1387 <pre><code class="language-stscript">/if left=myContent {: /echo My content had some content. :}</code></pre>
1388 executes the defined subcommand, if the provided value of left is truthy (contains some kind of contant that is not empty or false)
1389 </li>
1390 <li>
1391 <pre><code class="language-stscript">/if left=tree right={{getvar::object}} {: /echo The object is a tree! :}</code></pre>
1392 executes the defined subcommand, if the left and right values are equals.
1393 </li>
1394 </ul>
1395 </div>
1396 `,
1397 }));
1398 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1399 name: 'while',
1400 callback: whileCallback,
1401 returns: 'result of the last executed command',
1402 namedArgumentList: [
1403 SlashCommandNamedArgument.fromProps({
1404 name: 'left',
1405 description: 'left operand',
1406 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1407 isRequired: true,
1408 enumProvider: commonEnumProviders.variables('all'),
1409 }),
1410 SlashCommandNamedArgument.fromProps({
1411 name: 'right',
1412 description: 'right operand',
1413 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1414 enumProvider: commonEnumProviders.variables('all'),
1415 }),
1416 SlashCommandNamedArgument.fromProps({
1417 name: 'rule',
1418 description: 'comparison rule',
1419 typeList: [ARGUMENT_TYPE.STRING],
1420 defaultValue: 'eq',
1421 enumList: [
1422 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
1423 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
1424 new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
1425 new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
1426 new SlashCommandEnumValue('gt', 'a > b (numbers)'),
1427 new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1428 new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1429 new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1430 new SlashCommandEnumValue('not', '!a (truthy)'),
1431 ],
1432 forceEnum: true,
1433 }),
1434 SlashCommandNamedArgument.fromProps({
1435 name: 'guard',
1436 description: 'disable loop iteration limit',
1437 typeList: [ARGUMENT_TYPE.STRING],
1438 defaultValue: 'off',
1439 enumList: commonEnumProviders.boolean('onOff')(),
1440 }),
1441 ],
1442 unnamedArgumentList: [
1443 new SlashCommandArgument(
1444 'command to execute while true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
1445 ),
1446 ],
1447 splitUnnamedArgument: true,
1448 helpString: `
1449 <div>
1450 Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
1451 and if the condition yields true, then execute any valid slash command enclosed in quotes.
1452 </div>
1453 <div>
1454 Numeric values and string literals for left and right operands supported.
1455 </div>
1456 <div>
1457 <strong>Available rules:</strong>
1458 <ul>
1459 <li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
1460 <li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
1461 <li><code>in</code> => a includes b <small>(strings & numbers as strings)</small></li>
1462 <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
1463 <li><code>gt</code> => a > b <small>(numbers)</small></li>
1464 <li><code>gte</code> => a >= b <small>(numbers)</small></li>
1465 <li><code>lt</code> => a < b <small>(numbers)</small></li>
1466 <li><code>lte</code> => a <= b <small>(numbers)</small></li>
1467 <li><code>not</code> => !a <small>(truthy)</small></li>
1468 </ul>
1469 </div>
1470 <div>
1471 <strong>Examples:</strong>
1472 <ul>
1473 <li>
1474 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
1475 adds 1 to the value of "i" until it reaches 10.
1476 </li>
1477 <li>
1478 <pre><code class="language-stscript">/while left={{getvar::currentword}} {: /setvar key=currentword {: /do-something-and-return :}() | /echo The current work is "{{getvar::currentword}}" :}</code></pre>
1479 executes the defined subcommand as long as the "currentword" variable is truthy (has any content that is not false/empty)
1480 </ul>
1481 </li>
1482 </div>
1483 <div>
1484 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
1485 </div>
1486 `,
1487 }));
1488 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1489 name: 'times',
1490 callback: timesCallback,
1491 returns: 'result of the last executed command',
1492 namedArgumentList: [
1493 new SlashCommandNamedArgument(
1494 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
1495 ),
1496 ],
1497 unnamedArgumentList: [
1498 new SlashCommandArgument(
1499 'repeats',
1500 [ARGUMENT_TYPE.NUMBER],
1501 true,
1502 ),
1503 new SlashCommandArgument(
1504 'command',
1505 [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
1506 true,
1507 ),
1508 ],
1509 splitUnnamedArgument: true,
1510 splitUnnamedArgumentCount: 1,
1511 helpString: `
1512 <div>
1513 Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.
1514 </div>
1515 <div>
1516 <strong>Examples:</strong>
1517 <ul>
1518 <li>
1519 <pre><code class="language-stscript">/setvar key=i 1 | /times 5 "/addvar key=i 1"</code></pre>
1520 adds 1 to the value of "i" 5 times.
1521 </li>
1522 <li>
1523 <pre><code class="language-stscript">/times 4 "/echo {{timesIndex}}"</code></pre>
1524 echos the numbers 0 through 4. <code>{{timesIndex}}</code> is replaced with the iteration number (zero-based).
1525 </li>
1526 </ul>
1527 </div>
1528 <div>
1529 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
1530 </div>
1531 `,
1532 }));
1533 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1534 name: 'flushvar',
1535 callback: async (_, value) => deleteLocalVariable(value instanceof SlashCommandClosure ? (await value.execute())?.pipe : String(value)),
1536 aliases: ['flushchatvar'],
1537 unnamedArgumentList: [
1538 SlashCommandNamedArgument.fromProps({
1539 name: 'key',
1540 description: 'variable name or closure that returns a variable name',
1541 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.CLOSURE],
1542 enumProvider: commonEnumProviders.variables('local'),
1543 }),
1544 ],
1545 helpString: `
1546 <div>
1547 Delete a local variable.
1548 </div>
1549 <div>
1550 <strong>Example:</strong>
1551 <ul>
1552 <li>
1553 <pre><code class="language-stscript">/flushvar score</code></pre>
1554 </li>
1555 </ul>
1556 </div>
1557 `,
1558 }));
1559 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1560 name: 'flushglobalvar',
1561 callback: async (_, value) => deleteGlobalVariable(value instanceof SlashCommandClosure ? (await value.execute())?.pipe : String(value)),
1562 namedArgumentList: [],
1563 unnamedArgumentList: [
1564 SlashCommandNamedArgument.fromProps({
1565 name: 'key',
1566 description: 'variable name or closure that returns a variable name',
1567 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.CLOSURE],
1568 enumProvider: commonEnumProviders.variables('global'),
1569 }),
1570 ],
1571 helpString: `
1572 <div>
1573 Deletes the specified global variable.
1574 </div>
1575 <div>
1576 <strong>Example:</strong>
1577 <ul>
1578 <li>
1579 <pre><code class="language-stscript">/flushglobalvar score</code></pre>
1580 Deletes the global variable <code>score</code>.
1581 </li>
1582 </ul>
1583 </div>
1584 `,
1585 }));
1586 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1587 name: 'add',
1588 callback: (args, value) => addValuesCallback(args, value),
1589 returns: 'sum of the provided values',
1590 unnamedArgumentList: [
1591 SlashCommandArgument.fromProps({
1592 description: 'values to sum',
1593 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1594 isRequired: true,
1595 acceptsMultiple: true,
1596 enumProvider: commonEnumProviders.numbersAndVariables,
1597 forceEnum: false,
1598 }),
1599 ],
1600 splitUnnamedArgument: true,
1601 helpString: `
1602 <div>
1603 Performs an addition of the set of values and passes the result down the pipe.
1604 </div>
1605 <div>
1606 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1607 </div>
1608 <div>
1609 <strong>Example:</strong>
1610 <ul>
1611 <li>
1612 <pre><code class="language-stscript">/add 10 i 30 j</code></pre>
1613 </li>
1614 <li>
1615 <pre><code class="language-stscript">/add ["count", 15, 2, "i"]</code></pre>
1616 </li>
1617 </ul>
1618 </div>
1619 `,
1620 }));
1621 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1622 name: 'mul',
1623 callback: (args, value) => mulValuesCallback(args, value),
1624 returns: 'product of the provided values',
1625 unnamedArgumentList: [
1626 SlashCommandArgument.fromProps({
1627 description: 'values to multiply',
1628 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1629 isRequired: true,
1630 acceptsMultiple: true,
1631 enumProvider: commonEnumProviders.numbersAndVariables,
1632 forceEnum: false,
1633 }),
1634 ],
1635 splitUnnamedArgument: true,
1636 helpString: `
1637 <div>
1638 Performs a multiplication of the set of values and passes the result down the pipe.
1639 </div>
1640 <div>
1641 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1642 </div>
1643 <div>
1644 <strong>Examples:</strong>
1645 <ul>
1646 <li>
1647 <pre><code class="language-stscript">/mul 10 i 30 j</code></pre>
1648 </li>
1649 <li>
1650 <pre><code class="language-stscript">/mul ["count", 15, 2, "i"]</code></pre>
1651 </li>
1652 </ul>
1653 </div>
1654 `,
1655 }));
1656 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1657 name: 'max',
1658 callback: maxValuesCallback,
1659 returns: 'maximum value of the set of values',
1660 unnamedArgumentList: [
1661 SlashCommandArgument.fromProps({
1662 description: 'values to find the max',
1663 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1664 isRequired: true,
1665 acceptsMultiple: true,
1666 enumProvider: commonEnumProviders.numbersAndVariables,
1667 forceEnum: false,
1668 }),
1669 ],
1670 splitUnnamedArgument: true,
1671 helpString: `
1672 <div>
1673 Returns the maximum value of the set of values and passes the result down the pipe.
1674 </div>
1675 <div>
1676 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1677 </div>
1678 <div>
1679 <strong>Examples:</strong>
1680 <ul>
1681 <li>
1682 <pre><code class="language-stscript">/max 10 i 30 j</code></pre>
1683 </li>
1684 <li>
1685 <pre><code class="language-stscript">/max ["count", 15, 2, "i"]</code></pre>
1686 </li>
1687 </ul>
1688 </div>
1689 `,
1690 }));
1691 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1692 name: 'min',
1693 callback: minValuesCallback,
1694 returns: 'minimum value of the set of values',
1695 unnamedArgumentList: [
1696 SlashCommandArgument.fromProps({
1697 description: 'values to find the min',
1698 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1699 isRequired: true,
1700 acceptsMultiple: true,
1701 enumProvider: commonEnumProviders.numbersAndVariables,
1702 forceEnum: false,
1703 }),
1704 ],
1705 splitUnnamedArgument: true,
1706 helpString: `
1707 <div>
1708 Returns the minimum value of the set of values and passes the result down the pipe.
1709 </div>
1710 <div>
1711 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1712 </div>
1713 <div>
1714 <strong>Example:</strong>
1715 <ul>
1716 <li>
1717 <pre><code class="language-stscript">/min 10 i 30 j</code></pre>
1718 </li>
1719 <li>
1720 <pre><code class="language-stscript">/min ["count", 15, 2, "i"]</code></pre>
1721 </li>
1722 </ul>
1723 </div>
1724 `,
1725 }));
1726 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1727 name: 'sub',
1728 callback: subValuesCallback,
1729 returns: 'difference of the provided values',
1730 unnamedArgumentList: [
1731 SlashCommandArgument.fromProps({
1732 description: 'values to subtract, starting form the first provided value',
1733 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
1734 isRequired: true,
1735 acceptsMultiple: true,
1736 enumProvider: commonEnumProviders.numbersAndVariables,
1737 forceEnum: false,
1738 }),
1739 ],
1740 splitUnnamedArgument: true,
1741 helpString: `
1742 <div>
1743 Performs a subtraction of the set of values and passes the result down the pipe.
1744 </div>
1745 <div>
1746 Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
1747 </div>
1748 <div>
1749 <strong>Example:</strong>
1750 <ul>
1751 <li>
1752 <pre><code class="language-stscript">/sub i 5</code></pre>
1753 </li>
1754 <li>
1755 <pre><code class="language-stscript">/sub ["count", 4, "i"]</code></pre>
1756 </li>
1757 </ul>
1758 </div>
1759 `,
1760 }));
1761 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1762 name: 'div',
1763 callback: divValuesCallback,
1764 returns: 'result of division',
1765 unnamedArgumentList: [
1766 SlashCommandArgument.fromProps({
1767 description: 'dividend',
1768 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1769 isRequired: true,
1770 enumProvider: commonEnumProviders.numbersAndVariables,
1771 forceEnum: false,
1772 }),
1773 SlashCommandArgument.fromProps({
1774 description: 'divisor',
1775 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1776 isRequired: true,
1777 enumProvider: commonEnumProviders.numbersAndVariables,
1778 forceEnum: false,
1779 }),
1780 ],
1781 splitUnnamedArgument: true,
1782 helpString: `
1783 <div>
1784 Performs a division of two values and passes the result down the pipe.
1785 Can use variable names.
1786 </div>
1787 <div>
1788 <strong>Example:</strong>
1789 <ul>
1790 <li>
1791 <pre><code class="language-stscript">/div 10 i</code></pre>
1792 </li>
1793 </ul>
1794 </div>
1795 `,
1796 }));
1797 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1798 name: 'mod',
1799 callback: modValuesCallback,
1800 returns: 'result of modulo operation',
1801 unnamedArgumentList: [
1802 SlashCommandArgument.fromProps({
1803 description: 'dividend',
1804 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1805 isRequired: true,
1806 enumProvider: commonEnumProviders.numbersAndVariables,
1807 forceEnum: false,
1808 }),
1809 SlashCommandArgument.fromProps({
1810 description: 'divisor',
1811 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1812 isRequired: true,
1813 enumProvider: commonEnumProviders.numbersAndVariables,
1814 forceEnum: false,
1815 }),
1816 ],
1817 splitUnnamedArgument: true,
1818 helpString: `
1819 <div>
1820 Performs a modulo operation of two values and passes the result down the pipe.
1821 Can use variable names.
1822 </div>
1823 <div>
1824 <strong>Example:</strong>
1825 <ul>
1826 <li>
1827 <pre><code class="language-stscript">/mod i 2</code></pre>
1828 </li>
1829 </ul>
1830 </div>
1831 `,
1832 }));
1833 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1834 name: 'pow',
1835 callback: powValuesCallback,
1836 returns: 'result of power operation',
1837 unnamedArgumentList: [
1838 SlashCommandArgument.fromProps({
1839 description: 'base',
1840 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1841 isRequired: true,
1842 enumProvider: commonEnumProviders.numbersAndVariables,
1843 forceEnum: false,
1844 }),
1845 SlashCommandArgument.fromProps({
1846 description: 'exponent',
1847 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1848 isRequired: true,
1849 enumProvider: commonEnumProviders.numbersAndVariables,
1850 forceEnum: false,
1851 }),
1852 ],
1853 splitUnnamedArgument: true,
1854 helpString: `
1855 <div>
1856 Performs a power operation of two values and passes the result down the pipe.
1857 Can use variable names.
1858 </div>
1859 <div>
1860 <strong>Example:</strong>
1861 <ul>
1862 <li>
1863 <pre><code class="language-stscript">/pow i 2</code></pre>
1864 </li>
1865 </ul>
1866 </div>
1867 `,
1868 }));
1869 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1870 name: 'sin',
1871 callback: sinValuesCallback,
1872 returns: 'sine of the provided value',
1873 unnamedArgumentList: [
1874 SlashCommandArgument.fromProps({
1875 description: 'value',
1876 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1877 isRequired: true,
1878 enumProvider: commonEnumProviders.numbersAndVariables,
1879 forceEnum: false,
1880 }),
1881 ],
1882 helpString: `
1883 <div>
1884 Performs a sine operation of a value and passes the result down the pipe.
1885 Can use variable names.
1886 </div>
1887 <div>
1888 <strong>Example:</strong>
1889 <ul>
1890 <li>
1891 <pre><code class="language-stscript">/sin i</code></pre>
1892 </li>
1893 </ul>
1894 </div>
1895 `,
1896 }));
1897 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1898 name: 'cos',
1899 callback: cosValuesCallback,
1900 returns: 'cosine of the provided value',
1901 unnamedArgumentList: [
1902 SlashCommandArgument.fromProps({
1903 description: 'value',
1904 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1905 isRequired: true,
1906 enumProvider: commonEnumProviders.numbersAndVariables,
1907 forceEnum: false,
1908 }),
1909 ],
1910 helpString: `
1911 <div>
1912 Performs a cosine operation of a value and passes the result down the pipe.
1913 Can use variable names.
1914 </div>
1915 <div>
1916 <strong>Example:</strong>
1917 <ul>
1918 <li>
1919 <pre><code class="language-stscript">/cos i</code></pre>
1920 </li>
1921 </ul>
1922 </div>
1923 `,
1924 }));
1925 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1926 name: 'log',
1927 callback: logValuesCallback,
1928 returns: 'log of the provided value',
1929 namedArgumentList: [],
1930 unnamedArgumentList: [
1931 SlashCommandArgument.fromProps({
1932 description: 'value',
1933 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1934 isRequired: true,
1935 enumProvider: commonEnumProviders.numbersAndVariables,
1936 forceEnum: false,
1937 }),
1938 ],
1939 helpString: `
1940 <div>
1941 Performs a logarithm operation of a value and passes the result down the pipe.
1942 Can use variable names.
1943 </div>
1944 <div>
1945 <strong>Example:</strong>
1946 <ul>
1947 <li>
1948 <pre><code class="language-stscript">/log i</code></pre>
1949 </li>
1950 </ul>
1951 </div>
1952 `,
1953 }));
1954 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1955 name: 'abs',
1956 callback: absValuesCallback,
1957 returns: 'absolute value of the provided value',
1958 unnamedArgumentList: [
1959 SlashCommandArgument.fromProps({
1960 description: 'value',
1961 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1962 isRequired: true,
1963 enumProvider: commonEnumProviders.numbersAndVariables,
1964 forceEnum: false,
1965 }),
1966 ],
1967 helpString: `
1968 <div>
1969 Performs an absolute value operation of a value and passes the result down the pipe.
1970 Can use variable names.
1971 </div>
1972 <div>
1973 <strong>Example:</strong>
1974 <ul>
1975 <li>
1976 <pre><code class="language-stscript">/abs i</code></pre>
1977 </li>
1978 </ul>
1979 </div>
1980 `,
1981 }));
1982 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1983 name: 'sqrt',
1984 callback: sqrtValuesCallback,
1985 returns: 'square root of the provided value',
1986 unnamedArgumentList: [
1987 SlashCommandArgument.fromProps({
1988 description: 'value',
1989 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1990 isRequired: true,
1991 enumProvider: commonEnumProviders.numbersAndVariables,
1992 forceEnum: false,
1993 }),
1994 ],
1995 helpString: `
1996 <div>
1997 Performs a square root operation of a value and passes the result down the pipe.
1998 Can use variable names.
1999 </div>
2000 <div>
2001 <strong>Example:</strong>
2002 <ul>
2003 <li>
2004 <pre><code class="language-stscript">/sqrt i</code></pre>
2005 </li>
2006 </ul>
2007 </div>
2008 `,
2009 }));
2010 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2011 name: 'round',
2012 callback: roundValuesCallback,
2013 returns: 'rounded value',
2014 unnamedArgumentList: [
2015 SlashCommandArgument.fromProps({
2016 description: 'value',
2017 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
2018 isRequired: true,
2019 enumProvider: commonEnumProviders.numbersAndVariables,
2020 forceEnum: false,
2021 }),
2022 ],
2023 helpString: `
2024 <div>
2025 Rounds a value and passes the result down the pipe.
2026 Can use variable names.
2027 </div>
2028 <div>
2029 <strong>Example:</strong>
2030 <ul>
2031 <li>
2032 <pre><code class="language-stscript">/round i</code></pre>
2033 </li>
2034 </ul>
2035 </div>
2036 `,
2037 }));
2038 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2039 name: 'len',
2040 callback: (_, value) => String(lenValuesCallback(value)),
2041 aliases: ['length'],
2042 returns: 'length of the provided value',
2043 unnamedArgumentList: [
2044 SlashCommandArgument.fromProps({
2045 description: 'value',
2046 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
2047 isRequired: true,
2048 forceEnum: false,
2049 }),
2050 ],
2051 helpString: `
2052 <div>
2053 Gets the length of a value and passes the result down the pipe.
2054 <ul>
2055 <li>
2056 For strings, returns the number of characters.
2057 </li>
2058 <li>
2059 For lists and dictionaries, returns the number of elements.
2060 </li>
2061 <li>
2062 For numbers, returns the number of digits (including the sign and decimal point).
2063 </li>
2064 </ul>
2065 </div>
2066 <div>
2067 <strong>Example:</strong>
2068 <ul>
2069 <li>
2070 <pre><code class="language-stscript">/len Lorem ipsum | /echo</code></pre>
2071 </li>
2072 </ul>
2073 </div>
2074 `,
2075 }));
2076 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2077 name: 'sort',
2078 callback: sortArrayObjectCallback,
2079 returns: 'the sorted list or dictionary keys',
2080 namedArgumentList: [
2081 SlashCommandNamedArgument.fromProps({
2082 name: 'keysort',
2083 description: 'whether to sort by key or value; ignored for lists',
2084 typeList: [ARGUMENT_TYPE.BOOLEAN],
2085 enumList: ['true', 'false'],
2086 defaultValue: 'true',
2087 }),
2088 ],
2089 unnamedArgumentList: [
2090 SlashCommandArgument.fromProps({
2091 description: 'value',
2092 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
2093 isRequired: true,
2094 forceEnum: false,
2095 }),
2096 ],
2097 helpString: `
2098 <div>
2099 Sorts a list or dictionary in ascending order and passes the result down the pipe.
2100 <ul>
2101 <li>
2102 For lists, returns the list sorted by value.
2103 </li>
2104 <li>
2105 For dictionaries, returns the ordered list of keys after sorting. Setting keysort=false means keys are sorted by associated value.
2106 </li>
2107 </ul>
2108 </div>
2109 <div>
2110 <strong>Examples:</strong>
2111 <ul>
2112 <li>
2113 <pre><code class="language-stscript">/sort [5,3,4,1,2] | /echo</code></pre>
2114 </li>
2115 <li>
2116 <pre><code class="language-stscript">/sort keysort=false {"a": 1, "d": 3, "c": 2, "b": 5} | /echo</code></pre>
2117 </li>
2118 </ul>
2119 </div>
2120 `,
2121 }));
2122 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2123 name: 'rand',
2124 callback: (args, value) => String(randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value ? value : 1)), args)),
2125 returns: 'random number',
2126 namedArgumentList: [
2127 new SlashCommandNamedArgument(
2128 'from',
2129 'starting value for the range (inclusive)',
2130 [ARGUMENT_TYPE.NUMBER],
2131 false,
2132 false,
2133 '0',
2134 ),
2135 new SlashCommandNamedArgument(
2136 'to',
2137 'ending value for the range (inclusive)',
2138 [ARGUMENT_TYPE.NUMBER],
2139 false,
2140 false,
2141 '1',
2142 ),
2143 new SlashCommandNamedArgument(
2144 'round',
2145 'rounding method for the result',
2146 [ARGUMENT_TYPE.STRING],
2147 false,
2148 false,
2149 null,
2150 ['round', 'ceil', 'floor'],
2151 ),
2152 ],
2153 helpString: `
2154 <div>
2155 Returns a random number between <code>from</code> and <code>to</code> (inclusive).
2156 </div>
2157 <div>
2158 <strong>Examples:</strong>
2159 <ul>
2160 <li>
2161 <pre><code class="language-stscript">/rand</code></pre>
2162 Returns a random number between 0 and 1.
2163 </li>
2164 <li>
2165 <pre><code class="language-stscript">/rand 10</code></pre>
2166 Returns a random number between 0 and 10.
2167 </li>
2168 <li>
2169 <pre><code class="language-stscript">/rand from=5 to=10</code></pre>
2170 Returns a random number between 5 and 10.
2171 </li>
2172 </ul>
2173 </div>
2174 `,
2175 }));
2176 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2177 name: 'var',
2178 callback: (/** @type {NamedArguments} */ args, value) => varCallback(args, value),
2179 returns: 'the variable value',
2180 namedArgumentList: [
2181 SlashCommandNamedArgument.fromProps({
2182 name: 'key',
2183 description: 'variable name; forces setting the variable, even if no value is provided',
2184 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
2185 enumProvider: commonEnumProviders.variables('scope'),
2186 forceEnum: false,
2187 }),
2188 new SlashCommandNamedArgument(
2189 'index',
2190 'optional index for list or dictionary',
2191 [ARGUMENT_TYPE.NUMBER],
2192 false, // isRequired
2193 false, // acceptsMultiple
2194 ),
2195 SlashCommandNamedArgument.fromProps({
2196 name: 'as',
2197 description: 'change the type of the value when used with index',
2198 forceEnum: true,
2199 enumProvider: commonEnumProviders.types,
2200 isRequired: false,
2201 defaultValue: 'string',
2202 }),
2203 ],
2204 unnamedArgumentList: [
2205 SlashCommandArgument.fromProps({
2206 description: 'variable name',
2207 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
2208 enumProvider: commonEnumProviders.variables('scope'),
2209 forceEnum: false,
2210 }),
2211 new SlashCommandArgument(
2212 'variable value',
2213 [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE],
2214 false, // isRequired
2215 false, // acceptsMultiple
2216 ),
2217 ],
2218 splitUnnamedArgument: true,
2219 splitUnnamedArgumentCount: 1,
2220 helpString: `
2221 <div>
2222 Get or set a variable. Use <code>index</code> to access elements of a JSON-serialized list or dictionary.
2223 To convert the value to a specific JSON type when using with <code>index</code>, use the <code>as</code> argument.
2224 </div>
2225 <div>
2226 <strong>Examples:</strong>
2227 <ul>
2228 <li>
2229 <pre><code class="language-stscript">/let x foo | /var x foo bar | /var x | /echo</code></pre>
2230 </li>
2231 <li>
2232 <pre><code class="language-stscript">/let x foo | /var key=x foo bar | /var x | /echo</code></pre>
2233 </li>
2234 <li>
2235 <pre><code class="language-stscript">/let x {} | /var index=cool as=number x 1337 | /echo {{var::x}}</code></pre>
2236 </li>
2237 </ul>
2238 </div>
2239 `,
2240 }));
2241 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2242 name: 'let',
2243 callback: (/** @type {NamedArguments} */ args, value) => letCallback(args, value),
2244 returns: 'the variable value',
2245 namedArgumentList: [
2246 SlashCommandNamedArgument.fromProps({
2247 name: 'key',
2248 description: 'variable name',
2249 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
2250 enumProvider: commonEnumProviders.variables('scope'),
2251 forceEnum: false,
2252 }),
2253 ],
2254 unnamedArgumentList: [
2255 SlashCommandArgument.fromProps({
2256 description: 'variable name',
2257 typeList: [ARGUMENT_TYPE.VARIABLE_NAME],
2258 enumProvider: commonEnumProviders.variables('scope'),
2259 forceEnum: false,
2260 }),
2261 new SlashCommandArgument(
2262 'variable value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY, ARGUMENT_TYPE.CLOSURE],
2263 ),
2264 ],
2265 splitUnnamedArgument: true,
2266 splitUnnamedArgumentCount: 1,
2267 helpString: `
2268 <div>
2269 Declares a new variable in the current scope.
2270 </div>
2271 <div>
2272 <strong>Examples:</strong>
2273 <ul>
2274 <li>
2275 <pre><code class="language-stscript">/let x foo bar | /echo {{var::x}}</code></pre>
2276 </li>
2277 <li>
2278 <pre><code class="language-stscript">/let key=x foo bar | /echo {{var::x}}</code></pre>
2279 </li>
2280 <li>
2281 <pre><code class="language-stscript">/let y</code></pre>
2282 </li>
2283 </ul>
2284 </div>
2285 `,
2286 }));
2287 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2288 name: 'closure-serialize',
2289 /**
2290 *
2291 * @param {NamedArguments} args
2292 * @param {SlashCommandClosure} value
2293 * @returns {string}
2294 */
2295 callback: (args, value) => closureSerializeCallback(args, value),
2296 unnamedArgumentList: [
2297 SlashCommandArgument.fromProps({
2298 description: 'the closure to serialize',
2299 typeList: [ARGUMENT_TYPE.CLOSURE],
2300 isRequired: true,
2301 }),
2302 ],
2303 returns: 'serialized closure as string',
2304 helpString: `
2305 <div>
2306 Serialize a closure as text that can be stored in global and chat variables.
2307 </div>
2308 <div>
2309 <strong>Examples:</strong>
2310 <ul>
2311 <li>
2312 <pre><code class="language-stscript">/closure-serialize {: x=1 /echo x is {{var::x}} and y is {{var::y}} :} |\n/setvar key=myClosure</code></pre>
2313 </li>
2314 </ul>
2315 </div>
2316 `,
2317 }));
2318 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
2319 name: 'closure-deserialize',
2320 /**
2321 * @param {NamedArguments} args
2322 * @param {UnnamedArguments} value
2323 * @returns {SlashCommandClosure}
2324 */
2325 callback: (args, value) => closureDeserializeCallback(args, value),
2326 unnamedArgumentList: [
2327 SlashCommandArgument.fromProps({
2328 description: 'serialized closure',
2329 typeList: [ARGUMENT_TYPE.STRING],
2330 isRequired: true,
2331 }),
2332 ],
2333 returns: 'deserialized closure',
2334 helpString: `
2335 <div>
2336 Deserialize a closure from text.
2337 </div>
2338 <div>
2339 <strong>Examples:</strong>
2340 <ul>
2341 <li>
2342 <pre><code class="language-stscript">/closure-deserialize {{getvar::myClosure}} |\n/let myClosure {{pipe}} |\n/let y bar |\n/:myClosure x=foo</code></pre>
2343 </li>
2344 </ul>
2345 </div>
2346 `,
2347 }));
2348}