Merge pull request #2731 from SillyTavern/fix-pipe-types Add type conversion for /setvar commands with index

ddd5dc1207da2e6fb741291f9fdf5deaedd1a83c

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

Signed
4 files changed, +152 -24Ignore 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+6 -5
@@ -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,7 @@ 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) {
5960 if (this.existsVariableInScope(key)) {
6061 if (index !== null && index !== undefined) {
6162 let v = this.variables[key];
@@ -63,13 +64,13 @@ export class SlashCommandScope {
6364 v = JSON.parse(v);
6465 const numIndex = Number(index);
6566 if (Number.isNaN(numIndex)) {
6667 v[index] = convertValueType(value, type);
6768 } else {
6869 v[numIndex] = convertValueType(value, type);
6970 }
7071 v = JSON.stringify(v);
7172 } catch {
7273 v[index] = convertValueType(value, type);
7374 }
7475 this.variables[key] = v;
7576 } else {
@@ -78,7 +79,7 @@ export class SlashCommandScope {
7879 return value;
7980 }
8081 if (this.parent) {
8182 return this.parent.setVariable(key, value, index, type);
8283 }
8384 throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
8485 }
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+ switch (type.trim().toLowerCase()) {
49+ case 'string':
50+ case 'str':
51+ return String(value);
52+
53+ case 'null':
54+ return null;
55+
56+ case 'undefined':
57+ case 'none':
58+ return undefined;
59+
60+ case 'number':
61+ return Number(value);
62+
63+ case 'int':
64+ return parseInt(value, 10);
65+
66+ case 'float':
67+ return parseFloat(value);
68+
69+ case 'boolean':
70+ case 'bool':
71+ return isTrueBoolean(value);
72+
73+ case 'list':
74+ case 'array':
75+ try {
76+ const parsedArray = JSON.parse(value);
77+ if (Array.isArray(parsedArray)) {
78+ return parsedArray;
79+ }
80+ // The value is not an array
81+ return [];
82+ } catch {
83+ return [];
84+ }
85+
86+ case 'object':
87+ case 'dict':
88+ case 'dictionary':
89+ try {
90+ const parsedObject = JSON.parse(value);
91+ if (typeof parsedObject === 'object') {
92+ return parsedObject;
93+ }
94+ // The value is not an object
95+ return {};
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+60 -19
@@ -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 */
@@ -57,12 +57,12 @@ function setLocalVariable(name, value, args = {}) {
5757 if (localVariable === null) {
5858 localVariable = {};
5959 }
6060 localVariable[args.index] = convertValueType(value, args.as);
6161 } else {
6262 if (localVariable === null) {
6363 localVariable = [];
6464 }
6565 localVariable[numIndex] = convertValueType(value, args.as);
6666 }
6767 chat_metadata.variables[name] = JSON.stringify(localVariable);
6868 } catch {
@@ -106,12 +106,12 @@ function setGlobalVariable(name, value, args = {}) {
106106 if (globalVariable === null) {
107107 globalVariable = {};
108108 }
109109 globalVariable[args.index] = convertValueType(value, args.as);
110110 } else {
111111 if (globalVariable === null) {
112112 globalVariable = [];
113113 }
114114 globalVariable[numIndex] = convertValueType(value, args.as);
115115 }
116116 extension_settings.variables.global[name] = JSON.stringify(globalVariable);
117117 } catch {
@@ -667,23 +667,28 @@ function parseNumericSeries(value, scope = null) {
667667}
668668
669669function performOperation(value, operation, singleOperand = false, scope = null) {
670670 iffunction getResult(!value) {
671- return 0;
671+ if (!value) {
672- }
672+ return 0;
673+ }
673674
674675 const array = parseNumericSeries(value, scope);
675676
676677 if (array.length === 0) {
677678 return 0;
678679 }
679680
680681 const result = singleOperand ? operation(array[0]) : operation(array);
681682
682683 if (isNaN(result) || !isFinite(result)) {
683684 return 0;
685+ }
686+
687+ return result;
684688 }
685689
686690 returnconst result = getResult();
691+ return String(result);
687692}
688693
689694function addValuesCallback(args, value) {
@@ -836,7 +841,7 @@ function varCallback(args, value) {
836841 if (typeof key != 'string') throw new Error('Key must be a string');
837842 if (args._hasUnnamedArgument) {
838843 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
839844 args._scope.setVariable(key, val, args.index, args.as);
840845 return val;
841846 } else {
842847 return args._scope.getVariable(key, args.index);
@@ -846,7 +851,7 @@ function varCallback(args, value) {
846851 if (typeof key != 'string') throw new Error('Key must be a string');
847852 if (value.length > 0) {
848853 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
849854 args._scope.setVariable(key, val, args.index, args.as);
850855 return val;
851856 } else {
852857 return args._scope.getVariable(key, args.index);
@@ -901,6 +906,14 @@ export function registerVariableCommands() {
901906 new SlashCommandNamedArgument(
902907 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
903908 ),
909+ SlashCommandNamedArgument.fromProps({
910+ name: 'as',
911+ description: 'change the type of the value when used with index',
912+ forceEnum: true,
913+ enumProvider: commonEnumProviders.types,
914+ isRequired: false,
915+ defaultValue: 'string',
916+ }),
904917 ],
905918 unnamedArgumentList: [
906919 new SlashCommandArgument(
@@ -910,6 +923,7 @@ export function registerVariableCommands() {
910923 helpString: `
911924 <div>
912925 Set a local variable value and pass it down the pipe. The <code>index</code> argument is optional.
926+ To convert the value to a specific JSON type when using <code>index</code>, use the <code>as</code> argument.
913927 </div>
914928 <div>
915929 <strong>Example:</strong>
@@ -917,6 +931,9 @@ export function registerVariableCommands() {
917931 <li>
918932 <pre><code class="language-stscript">/setvar key=color green</code></pre>
919933 </li>
934+ <li>
935+ <pre><code class="language-stscript">/setvar key=ages index=John as=number 21</code></pre>
936+ </li>
920937 </ul>
921938 </div>
922939 `,
@@ -1015,6 +1032,14 @@ export function registerVariableCommands() {
10151032 new SlashCommandNamedArgument(
10161033 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
10171034 ),
1035+ SlashCommandNamedArgument.fromProps({
1036+ name: 'as',
1037+ description: 'change the type of the value when used with index',
1038+ forceEnum: true,
1039+ enumProvider: commonEnumProviders.types,
1040+ isRequired: false,
1041+ defaultValue: 'string',
1042+ }),
10181043 ],
10191044 unnamedArgumentList: [
10201045 new SlashCommandArgument(
@@ -1024,6 +1049,7 @@ export function registerVariableCommands() {
10241049 helpString: `
10251050 <div>
10261051 Set a global variable value and pass it down the pipe. The <code>index</code> argument is optional.
1052+ To convert the value to a specific JSON type when using <code>index</code>, use the <code>as</code> argument.
10271053 </div>
10281054 <div>
10291055 <strong>Example:</strong>
@@ -1031,6 +1057,9 @@ export function registerVariableCommands() {
10311057 <li>
10321058 <pre><code class="language-stscript">/setglobalvar key=color green</code></pre>
10331059 </li>
1060+ <li>
1061+ <pre><code class="language-stscript">/setglobalvar key=ages index=John as=number 21</code></pre>
1062+ </li>
10341063 </ul>
10351064 </div>
10361065 `,
@@ -2030,6 +2059,14 @@ export function registerVariableCommands() {
20302059 false, // isRequired
20312060 false, // acceptsMultiple
20322061 ),
2062+ SlashCommandNamedArgument.fromProps({
2063+ name: 'as',
2064+ description: 'change the type of the value when used with index',
2065+ forceEnum: true,
2066+ enumProvider: commonEnumProviders.types,
2067+ isRequired: false,
2068+ defaultValue: 'string',
2069+ }),
20332070 ],
20342071 unnamedArgumentList: [
20352072 SlashCommandArgument.fromProps({
@@ -2049,7 +2086,8 @@ export function registerVariableCommands() {
20492086 splitUnnamedArgumentCount: 1,
20502087 helpString: `
20512088 <div>
2052- Get or set a variable.
2089+ Get or set a variable. Use <code>index</code> to access elements of a JSON-serialized list or dictionary.
2090+ To convert the value to a specific JSON type when using with <code>index</code>, use the <code>as</code> argument.
20532091 </div>
20542092 <div>
20552093 <strong>Examples:</strong>
@@ -2060,6 +2098,9 @@ export function registerVariableCommands() {
20602098 <li>
20612099 <pre><code class="language-stscript">/let x foo | /var key=x foo bar | /var x | /echo</code></pre>
20622100 </li>
2101+ <li>
2102+ <pre><code class="language-stscript">/let x {} | /var index=cool as=number x 1337 | /echo {{var::x}}</code></pre>
2103+ </li>
20632104 </ul>
20642105 </div>
20652106 `,