diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 6ef10b59b..ad064e4e1 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -36,6 +36,8 @@ export const enumIcons = { true: '✔️', false: '❌', + null: '🚫', + undefined: '❓', // Value types boolean: '🔲', @@ -230,4 +232,19 @@ export const commonEnumProviders = { enumTypes.enum, '💉'); }); }, + + /** + * Gets somewhat recognizable STscript types. + * + * @returns {SlashCommandEnumValue[]} + */ + types: () => [ + new SlashCommandEnumValue('string', null, enumTypes.type, enumIcons.string), + new SlashCommandEnumValue('number', null, enumTypes.type, enumIcons.number), + new SlashCommandEnumValue('boolean', null, enumTypes.type, enumIcons.boolean), + new SlashCommandEnumValue('array', null, enumTypes.type, enumIcons.array), + new SlashCommandEnumValue('object', null, enumTypes.type, enumIcons.dictionary), + new SlashCommandEnumValue('null', null, enumTypes.type, enumIcons.null), + new SlashCommandEnumValue('undefined', null, enumTypes.type, enumIcons.undefined), + ], }; diff --git a/public/scripts/slash-commands/SlashCommandScope.js b/public/scripts/slash-commands/SlashCommandScope.js index 78edb78ae..e7ec2a58b 100644 --- a/public/scripts/slash-commands/SlashCommandScope.js +++ b/public/scripts/slash-commands/SlashCommandScope.js @@ -1,4 +1,5 @@ import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { convertValueType } from '../utils.js'; export class SlashCommandScope { /**@type {string[]}*/ variableNames = []; @@ -55,7 +56,7 @@ export class SlashCommandScope { if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`); this.variables[key] = value; } - setVariable(key, value, index = null) { + setVariable(key, value, index = null, type = null) { if (this.existsVariableInScope(key)) { if (index !== null && index !== undefined) { let v = this.variables[key]; @@ -63,13 +64,13 @@ export class SlashCommandScope { v = JSON.parse(v); const numIndex = Number(index); if (Number.isNaN(numIndex)) { - v[index] = value; + v[index] = convertValueType(value, type); } else { - v[numIndex] = value; + v[numIndex] = convertValueType(value, type); } v = JSON.stringify(v); } catch { - v[index] = value; + v[index] = convertValueType(value, type); } this.variables[key] = v; } else { @@ -78,7 +79,7 @@ export class SlashCommandScope { return value; } if (this.parent) { - return this.parent.setVariable(key, value, index); + return this.parent.setVariable(key, value, index, type); } throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`); } diff --git a/public/scripts/utils.js b/public/scripts/utils.js index dd3ff5622..8f0ca59a7 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -4,6 +4,7 @@ import { isMobile } from './RossAscends-mods.js'; import { collapseNewlines } from './power-user.js'; import { debounce_timeout } from './constants.js'; import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; +import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; /** * Pagination status string template. @@ -33,6 +34,74 @@ export function isValidUrl(value) { } } +/** + * Converts string to a value of a given type. Includes pythonista-friendly aliases. + * @param {string|SlashCommandClosure} value String value + * @param {string} type Type to convert to + * @returns {any} Converted value + */ +export function convertValueType(value, type) { + if (value instanceof SlashCommandClosure || typeof type !== 'string') { + return value; + } + + switch (type.trim().toLowerCase()) { + case 'string': + case 'str': + return String(value); + + case 'null': + return null; + + case 'undefined': + case 'none': + return undefined; + + case 'number': + return Number(value); + + case 'int': + return parseInt(value, 10); + + case 'float': + return parseFloat(value); + + case 'boolean': + case 'bool': + return isTrueBoolean(value); + + case 'list': + case 'array': + try { + const parsedArray = JSON.parse(value); + if (Array.isArray(parsedArray)) { + return parsedArray; + } + // The value is not an array + return []; + } catch { + return []; + } + + case 'object': + case 'dict': + case 'dictionary': + try { + const parsedObject = JSON.parse(value); + if (typeof parsedObject === 'object') { + return parsedObject; + } + // The value is not an object + return {}; + } catch { + return {}; + } + + default: + return value; + } +} + /** * Parses ranges like 10-20 or 10. * Range is inclusive. Start must be less than end. diff --git a/public/scripts/variables.js b/public/scripts/variables.js index fa56dca5e..3e9028591 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -11,7 +11,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; -import { isFalseBoolean } from './utils.js'; +import { isFalseBoolean, convertValueType } from './utils.js'; /** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */ /** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */ @@ -57,12 +57,12 @@ function setLocalVariable(name, value, args = {}) { if (localVariable === null) { localVariable = {}; } - localVariable[args.index] = value; + localVariable[args.index] = convertValueType(value, args.as); } else { if (localVariable === null) { localVariable = []; } - localVariable[numIndex] = value; + localVariable[numIndex] = convertValueType(value, args.as); } chat_metadata.variables[name] = JSON.stringify(localVariable); } catch { @@ -106,12 +106,12 @@ function setGlobalVariable(name, value, args = {}) { if (globalVariable === null) { globalVariable = {}; } - globalVariable[args.index] = value; + globalVariable[args.index] = convertValueType(value, args.as); } else { if (globalVariable === null) { globalVariable = []; } - globalVariable[numIndex] = value; + globalVariable[numIndex] = convertValueType(value, args.as); } extension_settings.variables.global[name] = JSON.stringify(globalVariable); } catch { @@ -667,23 +667,28 @@ function parseNumericSeries(value, scope = null) { } function performOperation(value, operation, singleOperand = false, scope = null) { - if (!value) { - return 0; + function getResult() { + if (!value) { + return 0; + } + + const array = parseNumericSeries(value, scope); + + if (array.length === 0) { + return 0; + } + + const result = singleOperand ? operation(array[0]) : operation(array); + + if (isNaN(result) || !isFinite(result)) { + return 0; + } + + return result; } - const array = parseNumericSeries(value, scope); - - if (array.length === 0) { - return 0; - } - - const result = singleOperand ? operation(array[0]) : operation(array); - - if (isNaN(result) || !isFinite(result)) { - return 0; - } - - return result; + const result = getResult(); + return String(result); } function addValuesCallback(args, value) { @@ -836,7 +841,7 @@ function varCallback(args, value) { if (typeof key != 'string') throw new Error('Key must be a string'); if (args._hasUnnamedArgument) { const val = typeof value[0] == 'string' ? value.join(' ') : value[0]; - args._scope.setVariable(key, val, args.index); + args._scope.setVariable(key, val, args.index, args.as); return val; } else { return args._scope.getVariable(key, args.index); @@ -846,7 +851,7 @@ function varCallback(args, value) { if (typeof key != 'string') throw new Error('Key must be a string'); if (value.length > 0) { const val = typeof value[0] == 'string' ? value.join(' ') : value[0]; - args._scope.setVariable(key, val, args.index); + args._scope.setVariable(key, val, args.index, args.as); return val; } else { return args._scope.getVariable(key, args.index); @@ -901,6 +906,14 @@ export function registerVariableCommands() { new SlashCommandNamedArgument( 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false, ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'change the type of the value when used with index', + forceEnum: true, + enumProvider: commonEnumProviders.types, + isRequired: false, + defaultValue: 'string', + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -910,6 +923,7 @@ export function registerVariableCommands() { helpString: `
Set a local variable value and pass it down the pipe. The index argument is optional. + To convert the value to a specific JSON type when using index, use the as argument.
Example: @@ -917,6 +931,9 @@ export function registerVariableCommands() {
  • /setvar key=color green
  • +
  • +
    /setvar key=ages index=John as=number 21
    +
  • `, @@ -1015,6 +1032,14 @@ export function registerVariableCommands() { new SlashCommandNamedArgument( 'index', 'list index', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], false, ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'change the type of the value when used with index', + forceEnum: true, + enumProvider: commonEnumProviders.types, + isRequired: false, + defaultValue: 'string', + }), ], unnamedArgumentList: [ new SlashCommandArgument( @@ -1024,6 +1049,7 @@ export function registerVariableCommands() { helpString: `
    Set a global variable value and pass it down the pipe. The index argument is optional. + To convert the value to a specific JSON type when using index, use the as argument.
    Example: @@ -1031,6 +1057,9 @@ export function registerVariableCommands() {
  • /setglobalvar key=color green
  • +
  • +
    /setglobalvar key=ages index=John as=number 21
    +
  • `, @@ -2030,6 +2059,14 @@ export function registerVariableCommands() { false, // isRequired false, // acceptsMultiple ), + SlashCommandNamedArgument.fromProps({ + name: 'as', + description: 'change the type of the value when used with index', + forceEnum: true, + enumProvider: commonEnumProviders.types, + isRequired: false, + defaultValue: 'string', + }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -2049,7 +2086,8 @@ export function registerVariableCommands() { splitUnnamedArgumentCount: 1, helpString: `
    - Get or set a variable. + Get or set a variable. Use index to access elements of a JSON-serialized list or dictionary. + To convert the value to a specific JSON type when using with index, use the as argument.
    Examples: @@ -2060,6 +2098,9 @@ export function registerVariableCommands() {
  • /let x foo | /var key=x foo bar | /var x | /echo
  • +
  • +
    /let x {} | /var index=cool as=number x 1337 | /echo {{var::x}}
    +
  • `,