Merge branch 'staging' into connection-profile-omit

3b1435ba77fef03f5788b4b666276254156f61cd

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

5 files changed, +200 -119Showing whitespace changes
public/index.html+13 -3
@@ -6090,7 +6090,10 @@
6090 <div class="alternate_grettings flexFlowColumn flex-container">6090 <div class="alternate_grettings flexFlowColumn flex-container">
6091 <div class="title_restorable">6091 <div class="title_restorable">
6092 <h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>6092 <h3><span data-i18n="Alternate Greetings" class="mdhotkey_location">Alternate Greetings</span></h3>
6093 <div title="Add" class="menu_button fa-solid fa-plus add_alternate_greeting" data-i18n="[title]Add"></div>6093 <div class="menu_button menu_button_icon add_alternate_greeting">
6094 <i class="fa-solid fa-plus"></i>
6095 <span data-i18n="Add">Add</span>
6096 </div>
6094 </div>6097 </div>
6095 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">6098 <small class="justifyLeft" data-i18n="Alternate_Greetings_desc">
6096 These will be displayed as swipes on the first message when starting a new chat.6099 These will be displayed as swipes on the first message when starting a new chat.
@@ -6106,11 +6109,18 @@
6106 </div>6109 </div>
6107 <div id="alternate_greeting_form_template" class="template_element">6110 <div id="alternate_greeting_form_template" class="template_element">
6108 <div class="alternate_greeting">6111 <div class="alternate_greeting">
6112 <details open>
6113 <summary>
6109 <div class="title_restorable">6114 <div class="title_restorable">
6110 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>6115 <strong><span data-i18n="Alternate Greeting #">Alternate Greeting #</span><span class="greeting_index"></span></strong>
6111 <div class="menu_button fa-solid fa-trash-alt delete_alternate_greeting"></div>6116 <div class="menu_button menu_button_icon delete_alternate_greeting">
6117 <i class="fa-solid fa-trash-alt"></i>
6118 <span data-i18n="Delete">Delete</span>
6112 </div>6119 </div>
6113 <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="16"></textarea>6120 </div>
6121 </summary>
6122 <textarea name="alternate_greetings" data-i18n="[placeholder](This will be the first message from the character that starts every chat)" placeholder="(This will be the first message from the character that starts every chat)" class="text_pole textarea_compact alternate_greeting_text mdHotkeys" value="" autocomplete="off" rows="12"></textarea>
6123 </details>
6114 </div>6124 </div>
6115 </div>6125 </div>
61166126
public/script.js+29 -9
@@ -7390,7 +7390,7 @@ function onScenarioOverrideRemoveClick() {
7390 */7390 */
7391export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {7391export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
7392 function getOkButtonText() {7392 function getOkButtonText() {
7393 if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {7393 if (['text', 'char_not_selected'].includes(popup_type)) {
7394 $dialoguePopupCancel.css('display', 'none');7394 $dialoguePopupCancel.css('display', 'none');
7395 return okButton ?? 'Ok';7395 return okButton ?? 'Ok';
7396 } else if (['delete_extension'].includes(popup_type)) {7396 } else if (['delete_extension'].includes(popup_type)) {
@@ -7827,24 +7827,42 @@ function openAlternateGreetings() {
78277827
7828 const template = $('#alternate_greetings_template .alternate_grettings').clone();7828 const template = $('#alternate_greetings_template .alternate_grettings').clone();
7829 const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;7829 const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings;
7830 const popup = new Popup(template, POPUP_TYPE.TEXT, '', {
7831 wide: true,
7832 large: true,
7833 allowVerticalScrolling: true,
7834 onClose: async () => {
7835 if (menu_type !== 'create') {
7836 await createOrEditCharacter();
7837 }
7838 }
7839 });
78307840
7831 for (let index = 0; index < getArray().length; index++) {7841 for (let index = 0; index < getArray().length; index++) {
7832 addAlternateGreeting(template, getArray()[index], index, getArray);7842 addAlternateGreeting(template, getArray()[index], index, getArray, popup);
7833 }7843 }
78347844
7835 template.find('.add_alternate_greeting').on('click', function () {7845 template.find('.add_alternate_greeting').on('click', function () {
7836 const array = getArray();7846 const array = getArray();
7837 const index = array.length;7847 const index = array.length;
7838 array.push('');7848 array.push('');
7839 addAlternateGreeting(template, '', index, getArray);7849 addAlternateGreeting(template, '', index, getArray, popup);
7840 updateAlternateGreetingsHintVisibility(template);7850 updateAlternateGreetingsHintVisibility(template);
7841 });7851 });
78427852
7843 updateAlternateGreetingsHintVisibility(template);7853 updateAlternateGreetingsHintVisibility(template);
7844 callPopup(template, 'alternate_greeting', '', { wide: true, large: true });7854 popup.show();
7845}7855}
78467856
7847function addAlternateGreeting(template, greeting, index, getArray) {7857/**
7858 * Adds an alternate greeting to the template.
7859 * @param {JQuery<HTMLElement>} template
7860 * @param {string} greeting
7861 * @param {number} index
7862 * @param {() => any[]} getArray
7863 * @param {Popup} popup
7864 */
7865function addAlternateGreeting(template, greeting, index, getArray, popup) {
7848 const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();7866 const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone();
7849 greetingBlock.find('.alternate_greeting_text').on('input', async function () {7867 greetingBlock.find('.alternate_greeting_text').on('input', async function () {
7850 const value = $(this).val();7868 const value = $(this).val();
@@ -7852,11 +7870,16 @@ function addAlternateGreeting(template, greeting, index, getArray) {
7852 array[index] = value;7870 array[index] = value;
7853 }).val(greeting);7871 }).val(greeting);
7854 greetingBlock.find('.greeting_index').text(index + 1);7872 greetingBlock.find('.greeting_index').text(index + 1);
7855 greetingBlock.find('.delete_alternate_greeting').on('click', async function () {7873 greetingBlock.find('.delete_alternate_greeting').on('click', async function (event) {
7874 event.preventDefault();
7875 event.stopPropagation();
7876
7856 if (confirm('Are you sure you want to delete this alternate greeting?')) {7877 if (confirm('Are you sure you want to delete this alternate greeting?')) {
7857 const array = getArray();7878 const array = getArray();
7858 array.splice(index, 1);7879 array.splice(index, 1);
7880
7859 // We need to reopen the popup to update the index numbers7881 // We need to reopen the popup to update the index numbers
7882 await popup.complete(POPUP_RESULT.AFFIRMATIVE);
7860 openAlternateGreetings();7883 openAlternateGreetings();
7861 }7884 }
7862 });7885 });
@@ -9574,9 +9597,6 @@ jQuery(async function () {
9574 }, 2000);9597 }, 2000);
9575 }9598 }
9576 }9599 }
9577 if (popup_type == 'alternate_greeting' && menu_type !== 'create') {
9578 createOrEditCharacter();
9579 }
95809600
9581 if (dialogueResolve) {9601 if (dialogueResolve) {
9582 if (popup_type == 'input') {9602 if (popup_type == 'input') {
public/scripts/slash-commands.js+1 -1
@@ -3469,7 +3469,7 @@ function getModelOptions(quiet) {
3469 case 'openai':3469 case 'openai':
3470 return oai_settings.chat_completion_source;3470 return oai_settings.chat_completion_source;
3471 default:3471 default:
3472 return nullResult;3472 return null;
3473 }3473 }
3474 }3474 }
34753475
public/scripts/variables.js+137 -106
@@ -12,7 +12,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';12import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';13import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';14import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
15import { isFalseBoolean, convertValueType } from './utils.js';15import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
1616
17/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */17/** @typedef {import('./slash-commands/SlashCommandParser.js').NamedArguments} NamedArguments */
18/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */18/** @typedef {import('./slash-commands/SlashCommand.js').UnnamedArguments} UnnamedArguments */
@@ -488,7 +488,7 @@ function existsGlobalVariable(name) {
488/**488/**
489 * Parses boolean operands from command arguments.489 * Parses boolean operands from command arguments.
490 * @param {object} args Command arguments490 * @param {object} args Command arguments
491 * @returns {{a: string | number, b: string | number, rule: string}} Boolean operands491 * @returns {{a: string | number, b: string | number?, rule: string}} Boolean operands
492 */492 */
493export function parseBooleanOperands(args) {493export function parseBooleanOperands(args) {
494 // Resolution order: numeric literal, local variable, global variable, string literal494 // Resolution order: numeric literal, local variable, global variable, string literal
@@ -497,6 +497,9 @@ export function parseBooleanOperands(args) {
497 */497 */
498 function getOperand(operand) {498 function getOperand(operand) {
499 if (operand === undefined) {499 if (operand === undefined) {
500 return undefined;
501 }
502 if (operand === '') {
500 return '';503 return '';
501 }504 }
502505
@@ -525,8 +528,8 @@ export function parseBooleanOperands(args) {
525 return stringLiteral || '';528 return stringLiteral || '';
526 }529 }
527530
528 const left = getOperand(args.a || args.left || args.first || args.x);531 const left = getOperand(args.a ?? args.left ?? args.first ?? args.x);
529 const right = getOperand(args.b || args.right || args.second || args.y);532 const right = getOperand(args.b ?? args.right ?? args.second ?? args.y);
530 const rule = args.rule;533 const rule = args.rule;
531534
532 return { a: left, b: right, rule };535 return { a: left, b: right, rule };
@@ -534,86 +537,81 @@ export function parseBooleanOperands(args) {
534537
535/**538/**
536 * Evaluates a boolean comparison rule.539 * Evaluates a boolean comparison rule.
537 * @param {string} rule Boolean comparison rule540 *
541 * @param {string?} rule Boolean comparison rule
538 * @param {string|number} a The left operand542 * @param {string|number} a The left operand
539 * @param {string|number} b The right operand543 * @param {string|number?} b The right operand
540 * @returns {boolean} True if the rule yields true, false otherwise544 * @returns {boolean} True if the rule yields true, false otherwise
541 */545 */
542export function evalBoolean(rule, a, b) {546export function evalBoolean(rule, a, b) {
543 if (!rule) {547 if (a === 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;
546 }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 }
564 }
565
566 // If no rule was provided, we are implicitly using 'eq', as defined for the slash commands
567 rule ??= 'eq';
547568
548 let result = false;
549 if (typeof a === 'number' && typeof b === 'number') {569 if (typeof a === 'number' && typeof b === 'number') {
550 // only do numeric comparison if both operands are numbers570 // only do numeric comparison if both operands are numbers
551 const aNumber = Number(a);571 const aNumber = Number(a);
552 const bNumber = Number(b);572 const bNumber = Number(b);
553573
554 switch (rule) {574 switch (rule) {
555 case 'not':
556 result = !aNumber;
557 break;
558 case 'gt':575 case 'gt':
559 result = aNumber > bNumber;576 return aNumber > bNumber;
560 break;
561 case 'gte':577 case 'gte':
562 result = aNumber >= bNumber;578 return aNumber >= bNumber;
563 break;
564 case 'lt':579 case 'lt':
565 result = aNumber < bNumber;580 return aNumber < bNumber;
566 break;
567 case 'lte':581 case 'lte':
568 result = aNumber <= bNumber;582 return aNumber <= bNumber;
569 break;
570 case 'eq':583 case 'eq':
571 result = aNumber === bNumber;584 return aNumber === bNumber;
572 break;
573 case 'neq':585 case 'neq':
574 result = aNumber !== bNumber;586 return aNumber !== bNumber;
587 case 'in':
588 case 'nin':
589 // Fall through to string comparison. Otherwise you could not check if 12345 contains 45 for example.
590 console.debug(`Boolean comparison rule '${rule}' is not supported for type number. Falling back to string comparison.`);
575 break;591 break;
576 default:592 default:
577 toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');593 throw new Error(`Unknown boolean comparison rule for type number. Accepted: gt, gte, lt, lte, eq, neq. Provided: ${rule}`);
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 }594 }
589 if (typeof b == 'string') {
590 bString = b.toLowerCase();
591 } else {
592 bString = JSON.stringify(b).toLowerCase();
593 }595 }
594596
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
595 switch (rule) {601 switch (rule) {
596 case 'in':602 case 'in':
597 result = aString.includes(bString);603 return aString.includes(bString);
598 break;
599 case 'nin':604 case 'nin':
600 result = !aString.includes(bString);605 return !aString.includes(bString);
601 break;
602 case 'eq':606 case 'eq':
603 result = aString === bString;607 return aString === bString;
604 break;
605 case 'neq':608 case 'neq':
606 result = aString !== bString;609 return aString !== bString;
607 break;
608 default:610 default:
609 toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');611 throw new Error(`Unknown boolean comparison rule for type number. Accepted: in, nin, eq, neq. Provided: ${rule}`);
610 throw new Error('Invalid command.');
611 }612 }
612}613}
613614
614 return result;
615}
616
617/**615/**
618 * Executes a slash command from a string (may be enclosed in quotes) and returns the result.616 * Executes a slash command from a string (may be enclosed in quotes) and returns the result.
619 * @param {string} command Command to execute. May contain escaped macro and batch separators.617 * @param {string} command Command to execute. May contain escaped macro and batch separators.
@@ -1317,32 +1315,36 @@ export function registerVariableCommands() {
1317 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1315 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1318 isRequired: true,1316 isRequired: true,
1319 enumProvider: commonEnumProviders.variables('all'),1317 enumProvider: commonEnumProviders.variables('all'),
1320 forceEnum: false,
1321 }),1318 }),
1322 SlashCommandNamedArgument.fromProps({1319 SlashCommandNamedArgument.fromProps({
1323 name: 'right',1320 name: 'right',
1324 description: 'right operand',1321 description: 'right operand',
1325 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1322 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1326 isRequired: true,
1327 enumProvider: commonEnumProviders.variables('all'),1323 enumProvider: commonEnumProviders.variables('all'),
1328 forceEnum: false,
1329 }),1324 }),
1330 new SlashCommandNamedArgument(1325 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: [
1336 new SlashCommandEnumValue('eq', 'a == b'),1331 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
1337 new SlashCommandEnumValue('neq', 'a !== b'),1332 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
1338 new SlashCommandEnumValue('not', '!a'),1333 new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
1339 new SlashCommandEnumValue('in', 'a includes b'),1334 new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
1340 new SlashCommandEnumValue('nin', 'a not includes b'),1335 new SlashCommandEnumValue('gt', 'a > b (numbers)'),
1336 new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1337 new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1338 new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1339 new SlashCommandEnumValue('not', '!a (truthy)'),
1341 ],1340 ],
1342 ),1341 forceEnum: true,
1343 new SlashCommandNamedArgument(1342 }),
1344 'else', 'command to execute if not true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], false,1343 SlashCommandNamedArgument.fromProps({
1345 ),1344 name: 'else',
1345 description: 'command to execute if not true',
1346 typeList: [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND],
1347 }),
1346 ],1348 ],
1347 unnamedArgumentList: [1349 unnamedArgumentList: [
1348 new SlashCommandArgument(1350 new SlashCommandArgument(
@@ -1360,17 +1362,25 @@ export function registerVariableCommands() {
1360 Numeric values and string literals for left and right operands supported.1362 Numeric values and string literals for left and right operands supported.
1361 </div>1363 </div>
1362 <div>1364 <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>
1363 <strong>Available rules:</strong>1373 <strong>Available rules:</strong>
1364 <ul>1374 <ul>
1365 <li>gt => a > b</li>1375 <li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
1366 <li>gte => a >= b</li>1376 <li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
1367 <li>lt => a < b</li>1377 <li><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>
1369 <li>eq => a == b</li>1379 <li><code>gt</code> => a > b <small>(numbers)</small></li>
1370 <li>neq => a != b</li>1380 <li><code>gte</code> => a >= b <small>(numbers)</small></li>
1371 <li>not => !a</li>1381 <li><code>lt</code> => a < b <small>(numbers)</small></li>
1372 <li>in (strings) => a includes b</li>1382 <li><code>lte</code> => a <= b <small>(numbers)</small></li>
1373 <li>nin (strings) => a not includes b</li>1383 <li><code>not</code> => !a <small>(truthy)</small></li>
1374 </ul>1384 </ul>
1375 </div>1385 </div>
1376 <div>1386 <div>
@@ -1380,6 +1390,17 @@ export function registerVariableCommands() {
1380 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>1390 <pre><code class="language-stscript">/if left=score right=10 rule=gte "/speak You win"</code></pre>
1381 triggers a /speak command if the value of "score" is greater or equals 10.1391 triggers a /speak command if the value of "score" is greater or equals 10.
1382 </li>1392 </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>
1383 </ul>1404 </ul>
1384 </div>1405 </div>
1385 `,1406 `,
@@ -1395,32 +1416,38 @@ export function registerVariableCommands() {
1395 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1416 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1396 isRequired: true,1417 isRequired: true,
1397 enumProvider: commonEnumProviders.variables('all'),1418 enumProvider: commonEnumProviders.variables('all'),
1398 forceEnum: false,
1399 }),1419 }),
1400 SlashCommandNamedArgument.fromProps({1420 SlashCommandNamedArgument.fromProps({
1401 name: 'right',1421 name: 'right',
1402 description: 'right operand',1422 description: 'right operand',
1403 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],1423 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER],
1404 isRequired: true,
1405 enumProvider: commonEnumProviders.variables('all'),1424 enumProvider: commonEnumProviders.variables('all'),
1406 forceEnum: false,
1407 }),1425 }),
1408 new SlashCommandNamedArgument(1426 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: [
1414 new SlashCommandEnumValue('eq', 'a == b'),1432 new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
1415 new SlashCommandEnumValue('neq', 'a !== b'),1433 new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
1416 new SlashCommandEnumValue('not', '!a'),1434 new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
1417 new SlashCommandEnumValue('in', 'a includes b'),1435 new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
1418 new SlashCommandEnumValue('nin', 'a not includes b'),1436 new SlashCommandEnumValue('gt', 'a > b (numbers)'),
1437 new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
1438 new SlashCommandEnumValue('lt', 'a < b (numbers)'),
1439 new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
1440 new SlashCommandEnumValue('not', '!a (truthy)'),
1419 ],1441 ],
1420 ),1442 forceEnum: true,
1421 new SlashCommandNamedArgument(1443 }),
1422 'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),1444 SlashCommandNamedArgument.fromProps({
1423 ),1445 name: 'guard',
1446 description: 'disable loop iteration limit',
1447 typeList: [ARGUMENT_TYPE.STRING],
1448 defaultValue: 'off',
1449 enumList: commonEnumProviders.boolean('onOff')(),
1450 }),
1424 ],1451 ],
1425 unnamedArgumentList: [1452 unnamedArgumentList: [
1426 new SlashCommandArgument(1453 new SlashCommandArgument(
@@ -1439,15 +1466,15 @@ export function registerVariableCommands() {
1439 <div>1466 <div>
1440 <strong>Available rules:</strong>1467 <strong>Available rules:</strong>
1441 <ul>1468 <ul>
1442 <li>gt => a > b</li>1469 <li><code>eq</code> => a == b <small>(strings & numbers)</small></li>
1443 <li>gte => a >= b</li>1470 <li><code>neq</code> => a !== b <small>(strings & numbers)</small></li>
1444 <li>lt => a < b</li>1471 <li><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>
1446 <li>eq => a == b</li>1473 <li><code>gt</code> => a > b <small>(numbers)</small></li>
1447 <li>neq => a != b</li>1474 <li><code>gte</code> => a >= b <small>(numbers)</small></li>
1448 <li>not => !a</li>1475 <li><code>lt</code> => a < b <small>(numbers)</small></li>
1449 <li>in (strings) => a includes b</li>1476 <li><code>lte</code> => a <= b <small>(numbers)</small></li>
1450 <li>nin (strings) => a not includes b</li>1477 <li><code>not</code> => !a <small>(truthy)</small></li>
1451 </ul>1478 </ul>
1452 </div>1479 </div>
1453 <div>1480 <div>
@@ -1457,7 +1484,11 @@ export function registerVariableCommands() {
1457 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>1484 <pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
1458 adds 1 to the value of "i" until it reaches 10.1485 adds 1 to the value of "i" until it reaches 10.
1459 </li>1486 </li>
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)
1460 </ul>1490 </ul>
1491 </li>
1461 </div>1492 </div>
1462 <div>1493 <div>
1463 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.1494 Loops are limited to 100 iterations by default, pass <code>guard=off</code> to disable.
public/style.css+20 -0
@@ -3154,6 +3154,26 @@ grammarly-extension {
3154 align-items: center;3154 align-items: center;
3155}3155}
31563156
3157.alternate_greeting details {
3158 padding: 2px;
3159}
3160
3161.alternate_greeting summary {
3162 list-style-position: outside;
3163 margin-left: 1em;
3164 padding-left: 1em;
3165}
3166
3167.alternate_greeting textarea {
3168 field-sizing: content;
3169 max-height: 50dvh;
3170}
3171
3172.alternate_greeting summary::marker,
3173.alternate_greeting summary strong {
3174 cursor: pointer;
3175}
3176
3157#rm_characters_block .form_create_bottom_buttons_block {3177#rm_characters_block .form_create_bottom_buttons_block {
3158 justify-content: space-evenly !important;3178 justify-content: space-evenly !important;
3159 flex-grow: 0;3179 flex-grow: 0;