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, +141 -13Showing whitespace changes
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+17 -0
@@ -36,6 +36,8 @@ export const enumIcons = {
3636
37 true: '✔️',37 true: '✔️',
38 false: '❌',38 false: '❌',
39 null: '🚫',
40 undefined: '❓',
3941
40 // Value types42 // Value types
41 boolean: '🔲',43 boolean: '🔲',
@@ -230,4 +232,19 @@ export const commonEnumProviders = {
230 enumTypes.enum, '💉');232 enumTypes.enum, '💉');
231 });233 });
232 },234 },
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 ],
233};250};
public/scripts/slash-commands/SlashCommandScope.js+6 -5
@@ -1,4 +1,5 @@
1import { SlashCommandClosure } from './SlashCommandClosure.js';1import { SlashCommandClosure } from './SlashCommandClosure.js';
2import { convertValueType } from '../utils.js';
23
3export class SlashCommandScope {4export class SlashCommandScope {
4 /**@type {string[]}*/ variableNames = [];5 /**@type {string[]}*/ variableNames = [];
@@ -55,7 +56,7 @@ export class SlashCommandScope {
55 if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`);56 if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`);
56 this.variables[key] = value;57 this.variables[key] = value;
57 }58 }
58 setVariable(key, value, index = null) {59 setVariable(key, value, index = null, type = null) {
59 if (this.existsVariableInScope(key)) {60 if (this.existsVariableInScope(key)) {
60 if (index !== null && index !== undefined) {61 if (index !== null && index !== undefined) {
61 let v = this.variables[key];62 let v = this.variables[key];
@@ -63,13 +64,13 @@ export class SlashCommandScope {
63 v = JSON.parse(v);64 v = JSON.parse(v);
64 const numIndex = Number(index);65 const numIndex = Number(index);
65 if (Number.isNaN(numIndex)) {66 if (Number.isNaN(numIndex)) {
66 v[index] = value;67 v[index] = convertValueType(value, type);
67 } else {68 } else {
68 v[numIndex] = value;69 v[numIndex] = convertValueType(value, type);
69 }70 }
70 v = JSON.stringify(v);71 v = JSON.stringify(v);
71 } catch {72 } catch {
72 v[index] = value;73 v[index] = convertValueType(value, type);
73 }74 }
74 this.variables[key] = v;75 this.variables[key] = v;
75 } else {76 } else {
@@ -78,7 +79,7 @@ export class SlashCommandScope {
78 return value;79 return value;
79 }80 }
80 if (this.parent) {81 if (this.parent) {
81 return this.parent.setVariable(key, value, index);82 return this.parent.setVariable(key, value, index, type);
82 }83 }
83 throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);84 throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
84 }85 }
public/scripts/utils.js+69 -0
@@ -4,6 +4,7 @@ import { isMobile } from './RossAscends-mods.js';
4import { collapseNewlines } from './power-user.js';4import { collapseNewlines } from './power-user.js';
5import { debounce_timeout } from './constants.js';5import { debounce_timeout } from './constants.js';
6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
7import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
78
8/**9/**
9 * Pagination status string template.10 * Pagination status string template.
@@ -34,6 +35,74 @@ export function isValidUrl(value) {
34}35}
3536
36/**37/**
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 */
43export 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/**
37 * Parses ranges like 10-20 or 10.106 * Parses ranges like 10-20 or 10.
38 * Range is inclusive. Start must be less than end.107 * Range is inclusive. Start must be less than end.
39 * Returns null if invalid.108 * Returns null if invalid.
public/scripts/variables.js+49 -8
@@ -11,7 +11,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
11import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';11import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
12import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';12import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
13import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';13import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
14import { isFalseBoolean } from './utils.js';14import { isFalseBoolean, convertValueType } from './utils.js';
1515
16/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */16/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
17/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */17/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@@ -57,12 +57,12 @@ function setLocalVariable(name, value, args = {}) {
57 if (localVariable === null) {57 if (localVariable === null) {
58 localVariable = {};58 localVariable = {};
59 }59 }
60 localVariable[args.index] = value;60 localVariable[args.index] = convertValueType(value, args.as);
61 } else {61 } else {
62 if (localVariable === null) {62 if (localVariable === null) {
63 localVariable = [];63 localVariable = [];
64 }64 }
65 localVariable[numIndex] = value;65 localVariable[numIndex] = convertValueType(value, args.as);
66 }66 }
67 chat_metadata.variables[name] = JSON.stringify(localVariable);67 chat_metadata.variables[name] = JSON.stringify(localVariable);
68 } catch {68 } catch {
@@ -106,12 +106,12 @@ function setGlobalVariable(name, value, args = {}) {
106 if (globalVariable === null) {106 if (globalVariable === null) {
107 globalVariable = {};107 globalVariable = {};
108 }108 }
109 globalVariable[args.index] = value;109 globalVariable[args.index] = convertValueType(value, args.as);
110 } else {110 } else {
111 if (globalVariable === null) {111 if (globalVariable === null) {
112 globalVariable = [];112 globalVariable = [];
113 }113 }
114 globalVariable[numIndex] = value;114 globalVariable[numIndex] = convertValueType(value, args.as);
115 }115 }
116 extension_settings.variables.global[name] = JSON.stringify(globalVariable);116 extension_settings.variables.global[name] = JSON.stringify(globalVariable);
117 } catch {117 } catch {
@@ -667,6 +667,7 @@ function parseNumericSeries(value, scope = null) {
667}667}
668668
669function performOperation(value, operation, singleOperand = false, scope = null) {669function performOperation(value, operation, singleOperand = false, scope = null) {
670 function getResult() {
670 if (!value) {671 if (!value) {
671 return 0;672 return 0;
672 }673 }
@@ -686,6 +687,10 @@ function performOperation(value, operation, singleOperand = false, scope = null)
686 return result;687 return result;
687 }688 }
688689
690 const result = getResult();
691 return String(result);
692}
693
689function addValuesCallback(args, value) {694function addValuesCallback(args, value) {
690 return performOperation(value, (array) => array.reduce((a, b) => a + b, 0), false, args._scope);695 return performOperation(value, (array) => array.reduce((a, b) => a + b, 0), false, args._scope);
691}696}
@@ -836,7 +841,7 @@ function varCallback(args, value) {
836 if (typeof key != 'string') throw new Error('Key must be a string');841 if (typeof key != 'string') throw new Error('Key must be a string');
837 if (args._hasUnnamedArgument) {842 if (args._hasUnnamedArgument) {
838 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];843 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
839 args._scope.setVariable(key, val, args.index);844 args._scope.setVariable(key, val, args.index, args.as);
840 return val;845 return val;
841 } else {846 } else {
842 return args._scope.getVariable(key, args.index);847 return args._scope.getVariable(key, args.index);
@@ -846,7 +851,7 @@ function varCallback(args, value) {
846 if (typeof key != 'string') throw new Error('Key must be a string');851 if (typeof key != 'string') throw new Error('Key must be a string');
847 if (value.length > 0) {852 if (value.length > 0) {
848 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];853 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
849 args._scope.setVariable(key, val, args.index);854 args._scope.setVariable(key, val, args.index, args.as);
850 return val;855 return val;
851 } else {856 } else {
852 return args._scope.getVariable(key, args.index);857 return args._scope.getVariable(key, args.index);
@@ -901,6 +906,14 @@ export function registerVariableCommands() {
901 new SlashCommandNamedArgument(906 new SlashCommandNamedArgument(
902 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,907 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
903 ),908 ),
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 }),
904 ],917 ],
905 unnamedArgumentList: [918 unnamedArgumentList: [
906 new SlashCommandArgument(919 new SlashCommandArgument(
@@ -910,6 +923,7 @@ export function registerVariableCommands() {
910 helpString: `923 helpString: `
911 <div>924 <div>
912 Set a local variable value and pass it down the pipe. The <code>index</code> argument is optional.925 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.
913 </div>927 </div>
914 <div>928 <div>
915 <strong>Example:</strong>929 <strong>Example:</strong>
@@ -917,6 +931,9 @@ export function registerVariableCommands() {
917 <li>931 <li>
918 <pre><code class="language-stscript">/setvar key=color green</code></pre>932 <pre><code class="language-stscript">/setvar key=color green</code></pre>
919 </li>933 </li>
934 <li>
935 <pre><code class="language-stscript">/setvar key=ages index=John as=number 21</code></pre>
936 </li>
920 </ul>937 </ul>
921 </div>938 </div>
922 `,939 `,
@@ -1015,6 +1032,14 @@ export function registerVariableCommands() {
1015 new SlashCommandNamedArgument(1032 new SlashCommandNamedArgument(
1016 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,1033 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false,
1017 ),1034 ),
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 }),
1018 ],1043 ],
1019 unnamedArgumentList: [1044 unnamedArgumentList: [
1020 new SlashCommandArgument(1045 new SlashCommandArgument(
@@ -1024,6 +1049,7 @@ export function registerVariableCommands() {
1024 helpString: `1049 helpString: `
1025 <div>1050 <div>
1026 Set a global variable value and pass it down the pipe. The <code>index</code> argument is optional.1051 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.
1027 </div>1053 </div>
1028 <div>1054 <div>
1029 <strong>Example:</strong>1055 <strong>Example:</strong>
@@ -1031,6 +1057,9 @@ export function registerVariableCommands() {
1031 <li>1057 <li>
1032 <pre><code class="language-stscript">/setglobalvar key=color green</code></pre>1058 <pre><code class="language-stscript">/setglobalvar key=color green</code></pre>
1033 </li>1059 </li>
1060 <li>
1061 <pre><code class="language-stscript">/setglobalvar key=ages index=John as=number 21</code></pre>
1062 </li>
1034 </ul>1063 </ul>
1035 </div>1064 </div>
1036 `,1065 `,
@@ -2030,6 +2059,14 @@ export function registerVariableCommands() {
2030 false, // isRequired2059 false, // isRequired
2031 false, // acceptsMultiple2060 false, // acceptsMultiple
2032 ),2061 ),
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 }),
2033 ],2070 ],
2034 unnamedArgumentList: [2071 unnamedArgumentList: [
2035 SlashCommandArgument.fromProps({2072 SlashCommandArgument.fromProps({
@@ -2049,7 +2086,8 @@ export function registerVariableCommands() {
2049 splitUnnamedArgumentCount: 1,2086 splitUnnamedArgumentCount: 1,
2050 helpString: `2087 helpString: `
2051 <div>2088 <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.
2053 </div>2091 </div>
2054 <div>2092 <div>
2055 <strong>Examples:</strong>2093 <strong>Examples:</strong>
@@ -2060,6 +2098,9 @@ export function registerVariableCommands() {
2060 <li>2098 <li>
2061 <pre><code class="language-stscript">/let x foo | /var key=x foo bar | /var x | /echo</code></pre>2099 <pre><code class="language-stscript">/let x foo | /var key=x foo bar | /var x | /echo</code></pre>
2062 </li>2100 </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>
2063 </ul>2104 </ul>
2064 </div>2105 </div>
2065 `,2106 `,