Merge pull request #2891 from SillyTavern/if-command-default-arguments `/if` and `/while` now allow "right" and "rule" to be optional

b025765a1eafb8b80a33f5a33b4b099f4568908f

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

Signed
1 files changed, +150 -119Ignore whitespace
public/scripts/variables.js+150 -119
@@ -12,7 +12,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
1212import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
1313import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
1414import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
1515import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1616
1717/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
1818/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@@ -488,7 +488,7 @@ function existsGlobalVariable(name) {
488488/**
489489 * Parses boolean operands from command arguments.
490490 * @param {object} args Command arguments
491491 * @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands
492492 */
493493export function parseBooleanOperands(args) {
494494 // Resolution order: numeric literal, local variable, global variable, string literal
@@ -497,6 +497,9 @@ export function parseBooleanOperands(args) {
497497 */
498498 function getOperand(operand) {
499499 if (operand === undefined) {
500+ return undefined;
501+ }
502+ if (operand === '') {
500503 return '';
501504 }
502505
@@ -525,8 +528,8 @@ export function parseBooleanOperands(args) {
525528 return stringLiteral || '';
526529 }
527530
528531 const left = getOperand(args.a ||?? args.left ||?? args.first ||?? args.x);
529532 const right = getOperand(args.b ||?? args.right ||?? args.second ||?? args.y);
530533 const rule = args.rule;
531534
532535 return { a: left, b: right, rule };
@@ -534,84 +537,79 @@ export function parseBooleanOperands(args) {
534537
535538/**
536539 * Evaluates a boolean comparison rule.
537- * @param {string} rule Boolean comparison rule
540+ *
541+ * @param {string?} rule Boolean comparison rule
538542 * @param {string|number} a The left operand
539543 * @param {string|number?} b The right operand
540544 * @returns {boolean} True if the rule yields true, false otherwise
541545 */
542546export function evalBoolean(rule, a, b) {
543547 if (!rulea === undefined) {
544- toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
548+ throw new Error('Left operand is not provided');
545- throw new Error('Invalid command.');
549+ }
550+
551+ // If right-hand side was not provided, whe just check if the left side is truthy
552+ if (b === undefined) {
553+ switch (rule) {
554+ case undefined:
555+ case 'not': {
556+ const resultOnTruthy = rule !== 'not';
557+ if (isTrueBoolean(String(a))) return resultOnTruthy;
558+ if (isFalseBoolean(String(a))) return !resultOnTruthy;
559+ return a ? resultOnTruthy : !resultOnTruthy;
560+ }
561+ default:
562+ 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}`);
563+ }
546564 }
547565
548- let result = false;
566+ // If no rule was provided, we are implicitly using 'eq', as defined for the slash commands
567+ rule ??= 'eq';
568+
549569 if (typeof a === 'number' && typeof b === 'number') {
550570 // only do numeric comparison if both operands are numbers
551571 const aNumber = Number(a);
552572 const bNumber = Number(b);
553573
554574 switch (rule) {
555- case 'not':
556- result = !aNumber;
557- break;
558575 case 'gt':
559576 result =return aNumber > bNumber;
560- break;
561577 case 'gte':
562578 result =return aNumber >= bNumber;
563- break;
564579 case 'lt':
565580 result =return aNumber < bNumber;
566- break;
567581 case 'lte':
568582 result =return aNumber <= bNumber;
569- break;
570583 case 'eq':
571584 result =return aNumber === bNumber;
572- break;
573585 case 'neq':
574586 result =return aNumber !== bNumber;
575- break;
576- default:
577- toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
578- throw new Error('Invalid command.');
579- }
580- } else {
581- // otherwise do case-insensitive string comparsion, stringify non-strings
582- let aString;
583- let bString;
584- if (typeof a == 'string') {
585- aString = a.toLowerCase();
586- } else {
587- aString = JSON.stringify(a).toLowerCase();
588- }
589- if (typeof b == 'string') {
590- bString = b.toLowerCase();
591- } else {
592- bString = JSON.stringify(b).toLowerCase();
593- }
594-
595- switch (rule) {
596587 case 'in':
597- result = aString.includes(bString);
598- break;
599588 case 'nin':
600- result = !aString.includes(bString);
589+ // Fall through to string comparison. Otherwise you could not check if 12345 contains 45 for example.
601- break;
590+ console.debug(`Boolean comparison rule '${rule}' is not supported for type number. Falling back to string comparison.`);
602- case 'eq':
603- result = aString === bString;
604- break;
605- case 'neq':
606- result = aString !== bString;
607591 break;
608592 default:
609- toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
593+ throw new Error(`Unknown boolean comparison rule for type number. Accepted: gt, gte, lt, lte, eq, neq. Provided: ${rule}`);
610- throw new Error('Invalid command.');
611594 }
612595 }
613596
614- return result;
597+ // otherwise do case-insensitive string comparsion, stringify non-strings
598+ let aString = (typeof a === 'string') ? a.toLowerCase() : JSON.stringify(a).toLowerCase();
599+ let bString = (typeof b === 'string') ? b.toLowerCase() : JSON.stringify(b).toLowerCase();
600+
601+ switch (rule) {
602+ case 'in':
603+ return aString.includes(bString);
604+ case 'nin':
605+ return !aString.includes(bString);
606+ case 'eq':
607+ return aString === bString;
608+ case 'neq':
609+ return aString !== bString;
610+ default:
611+ throw new Error(`Unknown boolean comparison rule for type number. Accepted: in, nin, eq, neq. Provided: ${rule}`);
612+ }
615613}
616614
617615/**
@@ -1317,32 +1315,36 @@ export function registerVariableCommands() {
13171315 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
13181316 isRequired: true,
13191317 enumProvider: commonEnumProviders.variables('all'),
1320- forceEnum: false,
13211318 }),
13221319 SlashCommandNamedArgument.fromProps({
13231320 name: 'right',
13241321 description: 'right operand',
13251322 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1326- isRequired: true,
13271323 enumProvider: commonEnumProviders.variables('all'),
1328- forceEnum: false,
13291324 }),
13301325 new SlashCommandNamedArgument.fromProps({
1331- 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
1326+ name: 'rule',
1332- new SlashCommandEnumValue('gt', 'a > b'),
1327+ description: 'comparison rule',
1333- new SlashCommandEnumValue('gte', 'a >= b'),
1328+ typeList: [ARGUMENT_TYPE.STRING],
1334- new SlashCommandEnumValue('lt', 'a < b'),
1329+ defaultValue: 'eq',
1335- new SlashCommandEnumValue('lte', 'a <= b'),
1330+ enumList: [
13361331 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
13371332 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
13381333 new SlashCommandEnumValue('notin', '!a includes b (strings & numbers as strings)'),
13391334 new SlashCommandEnumValue('innin', 'a not includes b (strings & numbers as strings)'),
13401335 new SlashCommandEnumValue('ningt', 'a not includes> b (numbers)'),
1341- ],
1336+ new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1342- ),
1337+ new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1343- new SlashCommandNamedArgument(
1338+ new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1344- 'else', 'command to execute if not true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], false,
1339+ new SlashCommandEnumValue('not', '!a (truthy)'),
1345- ),
1340+ ],
1341+ forceEnum: true,
1342+ }),
1343+ SlashCommandNamedArgument.fromProps({
1344+ name: 'else',
1345+ description: 'command to execute if not true',
1346+ typeList: [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
1347+ }),
13461348 ],
13471349 unnamedArgumentList: [
13481350 new SlashCommandArgument(
@@ -1360,17 +1362,25 @@ export function registerVariableCommands() {
13601362 Numeric values and string literals for left and right operands supported.
13611363 </div>
13621364 <div>
1365+ If the rule is not provided, it defaults to <code>eq</code>.
1366+ </div>
1367+ <div>
1368+ If no right operand is provided, it defaults to checking the <code>left</code> value to be truthy.
1369+ A non-empty string or non-zero number is considered truthy, as is the value <code>true</code> or <code>on</code>.<br />
1370+ 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.
1371+ </div>
1372+ <div>
13631373 <strong>Available rules:</strong>
13641374 <ul>
13651375 <li>gt<code>eq</code> => a >== b <small>(strings & numbers)</small></li>
13661376 <li>gte<code>neq</code> => a >!== b <small>(strings & numbers)</small></li>
13671377 <li>lt<code>in</code> => a <includes b <small>(strings & numbers as strings)</small></li>
1368- <li>lte => a <= b</li>
1378+ <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
13691379 <li>eq<code>gt</code> => a ==> b <small>(numbers)</small></li>
13701380 <li>neq<code>gte</code> => a !>= b <small>(numbers)</small></li>
13711381 <li>not<code>lt</code> => !a < b <small>(numbers)</small></li>
13721382 <li>in (strings)<code>lte</code> => a includes<= b <small>(numbers)</small></li>
1373- <li>nin (strings) => a not includes b</li>
1383+ <li><code>not</code> => !a <small>(truthy)</small></li>
13741384 </ul>
13751385 </div>
13761386 <div>
@@ -1380,6 +1390,17 @@ export function registerVariableCommands() {
13801390 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>
13811391 triggers a /speak command if the value of "score" is greater or equals 10.
13821392 </li>
1393+ <li>
1394+ <pre><code class="language-stscript">/if left={{lastMessage}} rule=in right=surprise {: /echo SURPISE! :}</code></pre>
1395+ executes a subcommand defined as a closure if the given value contains a specified word.
1396+ <li>
1397+ <pre><code class="language-stscript">/if left=myContent {: /echo My content had some content. :}</code></pre>
1398+ executes the defined subcommand, if the provided value of left is truthy (contains some kind of contant that is not empty or false)
1399+ </li>
1400+ <li>
1401+ <pre><code class="language-stscript">/if left=tree right={{getvar::object}} {: /echo The object is a tree! :}</code></pre>
1402+ executes the defined subcommand, if the left and right values are equals.
1403+ </li>
13831404 </ul>
13841405 </div>
13851406 `,
@@ -1395,32 +1416,38 @@ export function registerVariableCommands() {
13951416 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
13961417 isRequired: true,
13971418 enumProvider: commonEnumProviders.variables('all'),
1398- forceEnum: false,
13991419 }),
14001420 SlashCommandNamedArgument.fromProps({
14011421 name: 'right',
14021422 description: 'right operand',
14031423 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1404- isRequired: true,
14051424 enumProvider: commonEnumProviders.variables('all'),
1406- forceEnum: false,
14071425 }),
14081426 new SlashCommandNamedArgument.fromProps({
1409- 'rule', 'comparison rule', [ARGUMENT_TYPE.STRING], true, false, null, [
1427+ name: 'rule',
1410- new SlashCommandEnumValue('gt', 'a > b'),
1428+ description: 'comparison rule',
1411- new SlashCommandEnumValue('gte', 'a >= b'),
1429+ typeList: [ARGUMENT_TYPE.STRING],
1412- new SlashCommandEnumValue('lt', 'a < b'),
1430+ defaultValue: 'eq',
1413- new SlashCommandEnumValue('lte', 'a <= b'),
1431+ enumList: [
14141432 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
14151433 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
14161434 new SlashCommandEnumValue('notin', '!a includes b (strings & numbers as strings)'),
14171435 new SlashCommandEnumValue('innin', 'a not includes b (strings & numbers as strings)'),
14181436 new SlashCommandEnumValue('ningt', 'a not includes> b (numbers)'),
1419- ],
1437+ new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1420- ),
1438+ new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1421- new SlashCommandNamedArgument(
1439+ new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1422- 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
1440+ new SlashCommandEnumValue('not', '!a (truthy)'),
1423- ),
1441+ ],
1442+ forceEnum: true,
1443+ }),
1444+ SlashCommandNamedArgument.fromProps({
1445+ name: 'guard',
1446+ description: 'disable loop iteration limit',
1447+ typeList: [ARGUMENT_TYPE.STRING],
1448+ defaultValue: 'off',
1449+ enumList: commonEnumProviders.boolean('onOff')(),
1450+ }),
14241451 ],
14251452 unnamedArgumentList: [
14261453 new SlashCommandArgument(
@@ -1439,15 +1466,15 @@ export function registerVariableCommands() {
14391466 <div>
14401467 <strong>Available rules:</strong>
14411468 <ul>
14421469 <li>gt<code>eq</code> => a >== b <small>(strings & numbers)</small></li>
14431470 <li>gte<code>neq</code> => a >!== b <small>(strings & numbers)</small></li>
14441471 <li>lt<code>in</code> => a <includes b <small>(strings & numbers as strings)</small></li>
1445- <li>lte => a <= b</li>
1472+ <li><code>nin</code> => a not includes b <small>(strings & numbers as strings)</small></li>
14461473 <li>eq<code>gt</code> => a ==> b <small>(numbers)</small></li>
14471474 <li>neq<code>gte</code> => a !>= b <small>(numbers)</small></li>
14481475 <li>not<code>lt</code> => !a < b <small>(numbers)</small></li>
14491476 <li>in (strings)<code>lte</code> => a includes<= b <small>(numbers)</small></li>
1450- <li>nin (strings) => a not includes b</li>
1477+ <li><code>not</code> => !a <small>(truthy)</small></li>
14511478 </ul>
14521479 </div>
14531480 <div>
@@ -1457,7 +1484,11 @@ export function registerVariableCommands() {
14571484 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
14581485 adds 1 to the value of "i" until it reaches 10.
14591486 </li>
1460- </ul>
1487+ <li>
1488+ <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>
1489+ executes the defined subcommand as long as the "currentword" variable is truthy (has any content that is not false/empty)
1490+ </ul>
1491+ </li>
14611492 </div>
14621493 <div>
14631494 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
@@ -1572,7 +1603,7 @@ export function registerVariableCommands() {
15721603 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
15731604 isRequired: true,
15741605 acceptsMultiple: true,
15751606 enumProvider: (executor, scope) => {
15761607 const vars = commonEnumProviders.variables('all')(executor, scope);
15771608 vars.push(
15781609 new SlashCommandEnumValue(
@@ -1580,16 +1611,16 @@ export function registerVariableCommands() {
15801611 null,
15811612 enumTypes.variable,
15821613 enumIcons.variable,
15831614 (input) => /^\w*$/.test(input),
15841615 (input) => input,
15851616 ),
15861617 new SlashCommandEnumValue(
15871618 'any number',
15881619 null,
15891620 enumTypes.number,
15901621 enumIcons.number,
15911622 (input) => input == '' || !Number.isNaN(Number(input)),
15921623 (input) => input,
15931624 ),
15941625 );
15951626 return vars;