Add type conversion for /setvar commands with index

3746f08590ea623656fb4eecbdf802be7c59c10a

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

4 files changed, +162 -35Ignore whitespace
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+17 -0
@@ -36,6 +36,8 @@ export const enumIcons = {
3636
3737 true: '✔️',
3838 false: '❌',
39+ null: '🚫',
40+ undefined: '❓',
3941
4042 // Value types
4143 boolean: '🔲',
@@ -230,4 +232,19 @@ export const commonEnumProviders = {
230232 enumTypes.enum, '💉');
231233 });
232234 },
235+
236+ /**
237+ * Gets somewhat recognizable STscript types.
238+ *
239+ * @returns {SlashCommandEnumValue[]}
240+ */
241+ types: () => [
242+ new SlashCommandEnumValue('string', null, enumTypes.type, enumIcons.string),
243+ new SlashCommandEnumValue('number', null, enumTypes.type, enumIcons.number),
244+ new SlashCommandEnumValue('boolean', null, enumTypes.type, enumIcons.boolean),
245+ new SlashCommandEnumValue('array', null, enumTypes.type, enumIcons.array),
246+ new SlashCommandEnumValue('object', null, enumTypes.type, enumIcons.dictionary),
247+ new SlashCommandEnumValue('null', null, enumTypes.type, enumIcons.null),
248+ new SlashCommandEnumValue('undefined', null, enumTypes.type, enumIcons.undefined),
249+ ],
233250};
public/scripts/slash-commands/SlashCommandScope.js+3 -1
@@ -1,4 +1,5 @@
11import { SlashCommandClosure } from './SlashCommandClosure.js';
2+import { convertValueType } from '../utils.js';
23
34export class SlashCommandScope {
45 /**@type {string[]}*/ variableNames = [];
@@ -55,7 +56,8 @@ export class SlashCommandScope {
5556 if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`);
5657 this.variables[key] = value;
5758 }
5859 setVariable(key, value, index = null, type = null) {
60+ value = convertValueType(value, type);
5961 if (this.existsVariableInScope(key)) {
6062 if (index !== null && index !== undefined) {
6163 let v = this.variables[key];
public/scripts/utils.js+69 -0
@@ -4,6 +4,7 @@ import { isMobile } from './RossAscends-mods.js';
44import { collapseNewlines } from './power-user.js';
55import { debounce_timeout } from './constants.js';
66import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
7+import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
78
89/**
910 * Pagination status string template.
@@ -34,6 +35,74 @@ export function isValidUrl(value) {
3435}
3536
3637/**
38+ * Converts string to a value of a given type. Includes pythonista-friendly aliases.
39+ * @param {string|SlashCommandClosure} value String value
40+ * @param {string} type Type to convert to
41+ * @returns {any} Converted value
42+ */
43+export function convertValueType(value, type) {
44+ if (value instanceof SlashCommandClosure || typeof type !== 'string') {
45+ return value;
46+ }
47+
48+ type = type.trim().toLowerCase();
49+
50+ switch (type) {
51+ case 'string':
52+ case 'str':
53+ return String(value);
54+
55+ case 'null':
56+ return null;
57+
58+ case 'undefined':
59+ case 'none':
60+ return undefined;
61+
62+ case 'number':
63+ return Number(value);
64+
65+ case 'int':
66+ return parseInt(value, 10);
67+
68+ case 'float':
69+ return parseFloat(value);
70+
71+ case 'boolean':
72+ case 'bool':
73+ return isTrueBoolean(value);
74+
75+ case 'list':
76+ case 'array':
77+ try {
78+ const parsedArray = JSON.parse(value);
79+ if (Array.isArray(parsedArray)) {
80+ return parsedArray;
81+ }
82+ throw new Error('Value is not an array.');
83+ } catch {
84+ return [];
85+ }
86+
87+ case 'object':
88+ case 'dict':
89+ case 'dictionary':
90+ try {
91+ const parsedObject = JSON.parse(value);
92+ if (typeof parsedObject === 'object') {
93+ return parsedObject;
94+ }
95+ throw new Error('Value is not an object.');
96+ } catch {
97+ return {};
98+ }
99+
100+ default:
101+ return value;
102+ }
103+}
104+
105+/**
37106 * Parses ranges like 10-20 or 10.
38107 * Range is inclusive. Start must be less than end.
39108 * Returns null if invalid.
public/scripts/variables.js+73 -34
@@ -11,7 +11,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
1111import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
1212import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1313import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
1414import { isFalseBoolean, convertValueType } from './utils.js';
1515
1616/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
1717/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@@ -51,6 +51,7 @@ function setLocalVariable(name, value, args = {}) {
5151
5252 if (args.index !== undefined) {
5353 try {
54+ value = convertValueType(value, args.type);
5455 let localVariable = JSON.parse(chat_metadata.variables[name] ?? 'null');
5556 const numIndex = Number(args.index);
5657 if (Number.isNaN(numIndex)) {
@@ -100,6 +101,7 @@ function getGlobalVariable(name, args = {}) {
100101function setGlobalVariable(name, value, args = {}) {
101102 if (args.index !== undefined) {
102103 try {
104+ value = convertValueType(value, args.type);
103105 let globalVariable = JSON.parse(extension_settings.variables.global[name] ?? 'null');
104106 const numIndex = Number(args.index);
105107 if (Number.isNaN(numIndex)) {
@@ -667,23 +669,28 @@ function parseNumericSeries(value, scope = null) {
667669}
668670
669671function performOperation(value, operation, singleOperand = false, scope = null) {
670672 iffunction getResult(!value) {
671- return 0;
673+ if (!value) {
672- }
674+ return 0;
675+ }
673676
674677 const array = parseNumericSeries(value, scope);
675678
676679 if (array.length === 0) {
677680 return 0;
678681 }
679682
680683 const result = singleOperand ? operation(array[0]) : operation(array);
681684
682685 if (isNaN(result) || !isFinite(result)) {
683686 return 0;
687+ }
688+
689+ return result;
684690 }
685691
686692 returnconst result = getResult();
693+ return String(result);
687694}
688695
689696function addValuesCallback(args, value) {
@@ -836,7 +843,7 @@ function varCallback(args, value) {
836843 if (typeof key != 'string') throw new Error('Key must be a string');
837844 if (args._hasUnnamedArgument) {
838845 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
839846 args._scope.setVariable(key, val, args.index, args.type);
840847 return val;
841848 } else {
842849 return args._scope.getVariable(key, args.index);
@@ -846,7 +853,7 @@ function varCallback(args, value) {
846853 if (typeof key != 'string') throw new Error('Key must be a string');
847854 if (value.length > 0) {
848855 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
849856 args._scope.setVariable(key, val, args.index, args.type);
850857 return val;
851858 } else {
852859 return args._scope.getVariable(key, args.index);
@@ -901,6 +908,14 @@ export function registerVariableCommands() {
901908 new SlashCommandNamedArgument(
902909 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
903910 ),
911+ SlashCommandNamedArgument.fromProps({
912+ name: 'type',
913+ description: 'type of the value when used with index',
914+ forceEnum: true,
915+ enumProvider: commonEnumProviders.types,
916+ isRequired: false,
917+ defaultValue: 'string',
918+ }),
904919 ],
905920 unnamedArgumentList: [
906921 new SlashCommandArgument(
@@ -910,6 +925,7 @@ export function registerVariableCommands() {
910925 helpString: `
911926 <div>
912927 Set a local variable value and pass it down the pipe. The <code>index</code> argument is optional.
928+ To perform a type conversion when using <code>index</code>, use the <code>type</code> argument.
913929 </div>
914930 <div>
915931 <strong>Example:</strong>
@@ -917,6 +933,9 @@ export function registerVariableCommands() {
917933 <li>
918934 <pre><code class="language-stscript">/setvar key=color green</code></pre>
919935 </li>
936+ <li>
937+ <pre><code class="language-stscript">/setvar key=colors index=3 type=string blue</code></pre>
938+ </li>
920939 </ul>
921940 </div>
922941 `,
@@ -1015,6 +1034,14 @@ export function registerVariableCommands() {
10151034 new SlashCommandNamedArgument(
10161035 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
10171036 ),
1037+ SlashCommandNamedArgument.fromProps({
1038+ name: 'type',
1039+ description: 'type of the value when used with index',
1040+ forceEnum: true,
1041+ enumProvider: commonEnumProviders.types,
1042+ isRequired: false,
1043+ defaultValue: 'string',
1044+ }),
10181045 ],
10191046 unnamedArgumentList: [
10201047 new SlashCommandArgument(
@@ -1024,6 +1051,7 @@ export function registerVariableCommands() {
10241051 helpString: `
10251052 <div>
10261053 Set a global variable value and pass it down the pipe. The <code>index</code> argument is optional.
1054+ To perform a type conversion when using <code>index</code>, use the <code>type</code> argument.
10271055 </div>
10281056 <div>
10291057 <strong>Example:</strong>
@@ -1031,6 +1059,9 @@ export function registerVariableCommands() {
10311059 <li>
10321060 <pre><code class="language-stscript">/setglobalvar key=color green</code></pre>
10331061 </li>
1062+ <li>
1063+ <pre><code class="language-stscript">/setglobalvar key=colors index=3 type=string blue</code></pre>
1064+ </li>
10341065 </ul>
10351066 </div>
10361067 `,
@@ -1247,16 +1278,16 @@ export function registerVariableCommands() {
12471278 }),
12481279 new SlashCommandNamedArgument(
12491280 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
12501281 new SlashCommandEnumValue('gt', 'a > b'),
12511282 new SlashCommandEnumValue('gte', 'a >= b'),
12521283 new SlashCommandEnumValue('lt', 'a < b'),
12531284 new SlashCommandEnumValue('lte', 'a <= b'),
12541285 new SlashCommandEnumValue('eq', 'a == b'),
12551286 new SlashCommandEnumValue('neq', 'a !== b'),
12561287 new SlashCommandEnumValue('not', '!a'),
12571288 new SlashCommandEnumValue('in', 'a includes b'),
12581289 new SlashCommandEnumValue('nin', 'a not includes b'),
12591290 ],
12601291 ),
12611292 new SlashCommandNamedArgument(
12621293 'else', 'command to execute if not true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], false,
@@ -1325,16 +1356,16 @@ export function registerVariableCommands() {
13251356 }),
13261357 new SlashCommandNamedArgument(
13271358 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
13281359 new SlashCommandEnumValue('gt', 'a > b'),
13291360 new SlashCommandEnumValue('gte', 'a >= b'),
13301361 new SlashCommandEnumValue('lt', 'a < b'),
13311362 new SlashCommandEnumValue('lte', 'a <= b'),
13321363 new SlashCommandEnumValue('eq', 'a == b'),
13331364 new SlashCommandEnumValue('neq', 'a !== b'),
13341365 new SlashCommandEnumValue('not', '!a'),
13351366 new SlashCommandEnumValue('in', 'a includes b'),
13361367 new SlashCommandEnumValue('nin', 'a not includes b'),
13371368 ],
13381369 ),
13391370 new SlashCommandNamedArgument(
13401371 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
@@ -2030,6 +2061,14 @@ export function registerVariableCommands() {
20302061 false, // isRequired
20312062 false, // acceptsMultiple
20322063 ),
2064+ SlashCommandNamedArgument.fromProps({
2065+ name: 'type',
2066+ description: 'type of the value when used with index',
2067+ forceEnum: true,
2068+ enumProvider: commonEnumProviders.types,
2069+ isRequired: false,
2070+ defaultValue: 'string',
2071+ }),
20332072 ],
20342073 unnamedArgumentList: [
20352074 SlashCommandArgument.fromProps({