Macros 2.0 (v0.7.3) - Variable Shorthand: Comparison Operators & Autocomplete Improvements (#5050) * feat: Add numeric comparison operators (>, >=, <, <=) to variable shorthand syntax - Added four new comparison operators for local and global variables - Implemented greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual operations in MacroCstWalker - Updated lexer to recognize >=, >, <=, < operators (longer patterns before shorter to avoid conflicts) - Simplified parser by consolidating operator alternatives into single OR block - Enhanced autocomplete with operator definitions, examples, and usage * Improve autocomplete for variable shorthand operators with cursor-aware filtering - Track `variableNameEnd` and `variableOperatorEnd` positions in parsed macro context for accurate cursor position checks - Add `isShortOperatorPrefix()` helper to detect operators that could be prefixes of longer ones (e.g., `>` → `>=`) - Fix operator autocomplete to show longer variants when typing short operators (typing `>` now shows both `>` and `>=`) - Show operator suggestions immediately when variable
Signed| @@ -39,7 +39,9 @@ import { onboardingExperimentalMacroEngine } from '../macros/engine/MacroDiagnos | |||
| 39 | * @property {boolean} isVariableShorthand - Whether this is a variable shorthand (starts with . or $). | 39 | * @property {boolean} isVariableShorthand - Whether this is a variable shorthand (starts with . or $). |
| 40 | * @property {'.'|'$'|null} variablePrefix - The variable prefix (. for local, $ for global), or null. | 40 | * @property {'.'|'$'|null} variablePrefix - The variable prefix (. for local, $ for global), or null. |
| 41 | * @property {string} variableName - The variable name being typed (after the prefix). | 41 | * @property {string} variableName - The variable name being typed (after the prefix). |
| 42 | * @property {number} variableNameEnd - The end of the variable name (for partial matches). | ||
| 42 | * @property {string|null} variableOperator - The operator typed (=, ++, --, +=), or null. | 43 | * @property {string|null} variableOperator - The operator typed (=, ++, --, +=), or null. |
| 44 | * @property {number} variableOperatorEnd - The end of the variable operator (for partial matches). | ||
| 43 | * @property {string} variableValue - The value after the operator (for = and +=). | 45 | * @property {string} variableValue - The value after the operator (for = and +=). |
| 44 | * @property {boolean} isTypingVariableName - Whether cursor is in the variable name area. | 46 | * @property {boolean} isTypingVariableName - Whether cursor is in the variable name area. |
| 45 | * @property {boolean} isTypingOperator - Whether cursor is at/after variable name, ready for operator. | 47 | * @property {boolean} isTypingOperator - Whether cursor is at/after variable name, ready for operator. |
| @@ -499,13 +501,13 @@ export const VariableShorthandDefinitions = new Map([ | |||
| 499 | type: VariableShorthandType.LOCAL, | 501 | type: VariableShorthandType.LOCAL, |
| 500 | name: 'Local Variable', | 502 | name: 'Local Variable', |
| 501 | description: 'Access or modify a local variable (scoped to current chat).', | 503 | description: 'Access or modify a local variable (scoped to current chat).', |
| 502 | operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)'], | 504 | operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)', 'greater than (>)', 'greater than or equal (>=)', 'less than (<)', 'less than or equal (<=)'], |
| 503 | }], | 505 | }], |
| 504 | [VariableShorthandType.GLOBAL, { | 506 | [VariableShorthandType.GLOBAL, { |
| 505 | type: VariableShorthandType.GLOBAL, | 507 | type: VariableShorthandType.GLOBAL, |
| 506 | name: 'Global Variable', | 508 | name: 'Global Variable', |
| 507 | description: 'Access or modify a global variable (shared across all chats).', | 509 | description: 'Access or modify a global variable (shared across all chats).', |
| 508 | operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)'], | 510 | operations: ['get', 'set (=)', 'increment (++)', 'decrement (--)', 'add (+=)', 'subtract (-=)', 'logical or (||)', 'nullish coalescing (??)', 'logical or assign (||=)', 'nullish coalescing assign (??=)', 'equals (==)', 'not equals (!=)', 'greater than (>)', 'greater than or equal (>=)', 'less than (<)', 'less than or equal (<=)'], |
| 509 | }], | 511 | }], |
| 510 | ]); | 512 | ]); |
| 511 | 513 | ||
| @@ -633,6 +635,10 @@ export class VariableShorthandAutoCompleteOption extends AutoCompleteOption { | |||
| 633 | `{{${prefix}myvar ??= value}} - Set if undefined, get value`, | 635 | `{{${prefix}myvar ??= value}} - Set if undefined, get value`, |
| 634 | `{{${prefix}myvar == test}} - Compare (returns true/false)`, | 636 | `{{${prefix}myvar == test}} - Compare (returns true/false)`, |
| 635 | `{{${prefix}myvar != test}} - Compare not equal (returns true/false)`, | 637 | `{{${prefix}myvar != test}} - Compare not equal (returns true/false)`, |
| 638 | `{{${prefix}score > 10}} - Greater than (numeric, returns true/false)`, | ||
| 639 | `{{${prefix}score >= 10}} - Greater than or equal (numeric)`, | ||
| 640 | `{{${prefix}score < 10}} - Less than (numeric, returns true/false)`, | ||
| 641 | `{{${prefix}score <= 10}} - Less than or equal (numeric)`, | ||
| 636 | ]; | 642 | ]; |
| 637 | for (const ex of examples) { | 643 | for (const ex of examples) { |
| 638 | const li = document.createElement('li'); | 644 | const li = document.createElement('li'); |
| @@ -810,6 +816,10 @@ export class VariableNameAutoCompleteOption extends AutoCompleteOption { | |||
| 810 | `{{${prefix}${this.#varName} ??= value}} - Set if undefined, get value`, | 816 | `{{${prefix}${this.#varName} ??= value}} - Set if undefined, get value`, |
| 811 | `{{${prefix}${this.#varName} == test}} - Compare (returns true/false)`, | 817 | `{{${prefix}${this.#varName} == test}} - Compare (returns true/false)`, |
| 812 | `{{${prefix}${this.#varName} != test}} - Compare not equal (returns true/false)`, | 818 | `{{${prefix}${this.#varName} != test}} - Compare not equal (returns true/false)`, |
| 819 | `{{${prefix}${this.#varName} > 10}} - Greater than (numeric)`, | ||
| 820 | `{{${prefix}${this.#varName} >= 10}} - Greater than or equal (numeric)`, | ||
| 821 | `{{${prefix}${this.#varName} < 10}} - Less than (numeric)`, | ||
| 822 | `{{${prefix}${this.#varName} <= 10}} - Less than or equal (numeric)`, | ||
| 813 | ]; | 823 | ]; |
| 814 | for (const ex of examples) { | 824 | for (const ex of examples) { |
| 815 | const li = document.createElement('li'); | 825 | const li = document.createElement('li'); |
| @@ -824,6 +834,18 @@ export class VariableNameAutoCompleteOption extends AutoCompleteOption { | |||
| 824 | } | 834 | } |
| 825 | 835 | ||
| 826 | /** | 836 | /** |
| 837 | * Checks if an operator is a short one that could be a prefix of a longer operator. | ||
| 838 | * For example, '>' is a prefix of '>=', '<' is a prefix of '<='. | ||
| 839 | * @param {string} op - The operator to check. | ||
| 840 | * @returns {boolean} True if the operator could be a prefix of a longer operator. | ||
| 841 | */ | ||
| 842 | function isShortOperatorPrefix(op) { | ||
| 843 | // These operators could have longer variants typed after them | ||
| 844 | const shortPrefixes = ['>', '<', '=', '|', '?', '+', '-', '!']; | ||
| 845 | return shortPrefixes.includes(op); | ||
| 846 | } | ||
| 847 | |||
| 848 | /** | ||
| 827 | * Variable shorthand operators with metadata. | 849 | * Variable shorthand operators with metadata. |
| 828 | * @type {Map<string, { symbol: string, name: string, description: string, needsValue: boolean }>} | 850 | * @type {Map<string, { symbol: string, name: string, description: string, needsValue: boolean }>} |
| 829 | */ | 851 | */ |
| @@ -894,6 +916,30 @@ export const VariableOperatorDefinitions = new Map([ | |||
| 894 | description: 'Compare the variable value to another value. Returns "true" if not equal, "false" if equal.', | 916 | description: 'Compare the variable value to another value. Returns "true" if not equal, "false" if equal.', |
| 895 | needsValue: true, | 917 | needsValue: true, |
| 896 | }], | 918 | }], |
| 919 | ['>', { | ||
| 920 | symbol: '>', | ||
| 921 | name: 'Greater Than', | ||
| 922 | description: 'Numeric comparison. Returns "true" if variable is greater than value, "false" otherwise.', | ||
| 923 | needsValue: true, | ||
| 924 | }], | ||
| 925 | ['>=', { | ||
| 926 | symbol: '>=', | ||
| 927 | name: 'Greater Than or Equal', | ||
| 928 | description: 'Numeric comparison. Returns "true" if variable is greater than or equal to value, "false" otherwise.', | ||
| 929 | needsValue: true, | ||
| 930 | }], | ||
| 931 | ['<', { | ||
| 932 | symbol: '<', | ||
| 933 | name: 'Less Than', | ||
| 934 | description: 'Numeric comparison. Returns "true" if variable is less than value, "false" otherwise.', | ||
| 935 | needsValue: true, | ||
| 936 | }], | ||
| 937 | ['<=', { | ||
| 938 | symbol: '<=', | ||
| 939 | name: 'Less Than or Equal', | ||
| 940 | description: 'Numeric comparison. Returns "true" if variable is less than or equal to value, "false" otherwise.', | ||
| 941 | needsValue: true, | ||
| 942 | }], | ||
| 897 | ]); | 943 | ]); |
| 898 | 944 | ||
| 899 | /** | 945 | /** |
| @@ -1224,19 +1270,35 @@ export function parseMacroContext(macroText, cursorOffset) { | |||
| 1224 | } else if (operatorText.startsWith('!=')) { | 1270 | } else if (operatorText.startsWith('!=')) { |
| 1225 | variableOperator = '!='; | 1271 | variableOperator = '!='; |
| 1226 | i += 2; | 1272 | i += 2; |
| 1273 | } else if (operatorText.startsWith('>=')) { | ||
| 1274 | variableOperator = '>='; | ||
| 1275 | i += 2; | ||
| 1276 | } else if (operatorText.startsWith('>')) { | ||
| 1277 | variableOperator = '>'; | ||
| 1278 | i += 1; | ||
| 1279 | } else if (operatorText.startsWith('<=')) { | ||
| 1280 | variableOperator = '<='; | ||
| 1281 | i += 2; | ||
| 1282 | } else if (operatorText.startsWith('<')) { | ||
| 1283 | variableOperator = '<'; | ||
| 1284 | i += 1; | ||
| 1227 | } else if (operatorText.startsWith('=')) { | 1285 | } else if (operatorText.startsWith('=')) { |
| 1228 | variableOperator = '='; | 1286 | variableOperator = '='; |
| 1229 | i += 1; | 1287 | i += 1; |
| 1230 | } else if (operatorText.startsWith('+') || operatorText.startsWith('-') || operatorText.startsWith('|') || operatorText.startsWith('?') || operatorText.startsWith('!')) { | 1288 | } else if (operatorText.startsWith('+') || operatorText.startsWith('-') || operatorText.startsWith('|') || operatorText.startsWith('?') || operatorText.startsWith('!') || operatorText.startsWith('>') || operatorText.startsWith('<')) { |
| 1231 | // Partial operator prefix - user is typing an operator | 1289 | // Partial operator prefix - user is typing an operator |
| 1232 | partialOperator = operatorText[0]; | 1290 | partialOperator = operatorText[0]; |
| 1233 | } else if (operatorText.length > 0 && !/^\s/.test(operatorText)) { | 1291 | } else if (operatorText.length > 0 && !/^\s/.test(operatorText) && !operatorText.startsWith('}')) { |
| 1234 | // There's non-whitespace after the variable name that isn't a valid operator | 1292 | // There's non-whitespace after the variable name that isn't a valid operator |
| 1235 | // This is an invalid trailing character (e.g., $my$ or .var@test) | 1293 | // This is an invalid trailing character (e.g., $my$ or .var@test) |
| 1294 | // Exception: } is the closing brace, not an invalid char | ||
| 1236 | hasInvalidTrailingChars = true; | 1295 | hasInvalidTrailingChars = true; |
| 1237 | invalidTrailingChars = operatorText.trim(); | 1296 | invalidTrailingChars = operatorText.trim(); |
| 1238 | } | 1297 | } |
| 1239 | 1298 | ||
| 1299 | // Track where the operator ends (for cursor position checks) | ||
| 1300 | const variableOperatorEnd = i; | ||
| 1301 | |||
| 1240 | // Check if operator requires a value | 1302 | // Check if operator requires a value |
| 1241 | const operatorDef = variableOperator ? VariableOperatorDefinitions.get(variableOperator) : null; | 1303 | const operatorDef = variableOperator ? VariableOperatorDefinitions.get(variableOperator) : null; |
| 1242 | const operatorNeedsValue = operatorDef?.needsValue ?? false; | 1304 | const operatorNeedsValue = operatorDef?.needsValue ?? false; |
| @@ -1256,11 +1318,15 @@ export function parseMacroContext(macroText, cursorOffset) { | |||
| 1256 | // Cursor is before the prefix - still in flags area conceptually | 1318 | // Cursor is before the prefix - still in flags area conceptually |
| 1257 | isTypingVariableName = false; | 1319 | isTypingVariableName = false; |
| 1258 | } else if (cursorOffset <= variableNameEnd) { | 1320 | } else if (cursorOffset <= variableNameEnd) { |
| 1259 | // Cursor is in the variable name | 1321 | // Cursor is in the variable name area (including at the end) |
| 1260 | isTypingVariableName = true; | 1322 | isTypingVariableName = true; |
| 1261 | } else if (!variableOperator && !hasInvalidTrailingChars) { | 1323 | } else if (variableName.length > 0 && !variableOperator && !hasInvalidTrailingChars) { |
| 1262 | // Cursor is after variable name but no operator yet (and no invalid chars) | 1324 | // Cursor is after variable name but no operator yet (and no invalid chars) |
| 1263 | // This includes partial operator prefixes like '+', '-', '|', '?' | 1325 | // This includes partial operator prefixes like '+', '-', '|', '?', '>', '<' |
| 1326 | isTypingOperator = true; | ||
| 1327 | } else if (variableName.length > 0 && variableOperator && isShortOperatorPrefix(variableOperator) && cursorOffset <= variableOperatorEnd) { | ||
| 1328 | // Short operator that could be prefix of longer one (e.g., > could become >=) | ||
| 1329 | // But ONLY if cursor is still in the operator area, not past it into value | ||
| 1264 | isTypingOperator = true; | 1330 | isTypingOperator = true; |
| 1265 | } else if (operatorNeedsValue) { | 1331 | } else if (operatorNeedsValue) { |
| 1266 | // Operator that requires value - cursor is in value area | 1332 | // Operator that requires value - cursor is in value area |
| @@ -1292,7 +1358,9 @@ export function parseMacroContext(macroText, cursorOffset) { | |||
| 1292 | isVariableShorthand, | 1358 | isVariableShorthand, |
| 1293 | variablePrefix, | 1359 | variablePrefix, |
| 1294 | variableName, | 1360 | variableName, |
| 1361 | variableNameEnd, | ||
| 1295 | variableOperator, | 1362 | variableOperator, |
| 1363 | variableOperatorEnd, | ||
| 1296 | variableValue, | 1364 | variableValue, |
| 1297 | isTypingVariableName, | 1365 | isTypingVariableName, |
| 1298 | isTypingOperator, | 1366 | isTypingOperator, |
| @@ -574,6 +574,22 @@ class MacroCstWalker { | |||
| 574 | operation = 'notEquals'; | 574 | operation = 'notEquals'; |
| 575 | hasValueExpr = true; | 575 | hasValueExpr = true; |
| 576 | break; | 576 | break; |
| 577 | case '>': | ||
| 578 | operation = 'greaterThan'; | ||
| 579 | hasValueExpr = true; | ||
| 580 | break; | ||
| 581 | case '>=': | ||
| 582 | operation = 'greaterThanOrEqual'; | ||
| 583 | hasValueExpr = true; | ||
| 584 | break; | ||
| 585 | case '<': | ||
| 586 | operation = 'lessThan'; | ||
| 587 | hasValueExpr = true; | ||
| 588 | break; | ||
| 589 | case '<=': | ||
| 590 | operation = 'lessThanOrEqual'; | ||
| 591 | hasValueExpr = true; | ||
| 592 | break; | ||
| 577 | default: | 593 | default: |
| 578 | logMacroInternalError({ message: `Lexer found macro operator that is not implemented for variable shorthand expressions in macro node '${macroNode.name}'.` }); | 594 | logMacroInternalError({ message: `Lexer found macro operator that is not implemented for variable shorthand expressions in macro node '${macroNode.name}'.` }); |
| 579 | break; | 595 | break; |
| @@ -714,6 +730,50 @@ class MacroCstWalker { | |||
| 714 | return currentValue !== compareValue ? 'true' : 'false'; | 730 | return currentValue !== compareValue ? 'true' : 'false'; |
| 715 | } | 731 | } |
| 716 | 732 | ||
| 733 | case 'greaterThan': { | ||
| 734 | // Numeric greater than comparison | ||
| 735 | const currentNum = Number(vars.get(varName)); | ||
| 736 | const compareNum = Number(lazyValue()); | ||
| 737 | if (isNaN(currentNum) || isNaN(compareNum)) { | ||
| 738 | logMacroRuntimeWarning({ message: `Variable shorthand ">" operator requires numeric values. Got: "${vars.get(varName)}" > "${lazyValue()}"` }); | ||
| 739 | return 'false'; | ||
| 740 | } | ||
| 741 | return currentNum > compareNum ? 'true' : 'false'; | ||
| 742 | } | ||
| 743 | |||
| 744 | case 'greaterThanOrEqual': { | ||
| 745 | // Numeric greater than or equal comparison | ||
| 746 | const currentNum = Number(vars.get(varName)); | ||
| 747 | const compareNum = Number(lazyValue()); | ||
| 748 | if (isNaN(currentNum) || isNaN(compareNum)) { | ||
| 749 | logMacroRuntimeWarning({ message: `Variable shorthand ">=" operator requires numeric values. Got: "${vars.get(varName)}" >= "${lazyValue()}"` }); | ||
| 750 | return 'false'; | ||
| 751 | } | ||
| 752 | return currentNum >= compareNum ? 'true' : 'false'; | ||
| 753 | } | ||
| 754 | |||
| 755 | case 'lessThan': { | ||
| 756 | // Numeric less than comparison | ||
| 757 | const currentNum = Number(vars.get(varName)); | ||
| 758 | const compareNum = Number(lazyValue()); | ||
| 759 | if (isNaN(currentNum) || isNaN(compareNum)) { | ||
| 760 | logMacroRuntimeWarning({ message: `Variable shorthand "<" operator requires numeric values. Got: "${vars.get(varName)}" < "${lazyValue()}"` }); | ||
| 761 | return 'false'; | ||
| 762 | } | ||
| 763 | return currentNum < compareNum ? 'true' : 'false'; | ||
| 764 | } | ||
| 765 | |||
| 766 | case 'lessThanOrEqual': { | ||
| 767 | // Numeric less than or equal comparison | ||
| 768 | const currentNum = Number(vars.get(varName)); | ||
| 769 | const compareNum = Number(lazyValue()); | ||
| 770 | if (isNaN(currentNum) || isNaN(compareNum)) { | ||
| 771 | logMacroRuntimeWarning({ message: `Variable shorthand "<=" operator requires numeric values. Got: "${vars.get(varName)}" <= "${lazyValue()}"` }); | ||
| 772 | return 'false'; | ||
| 773 | } | ||
| 774 | return currentNum <= compareNum ? 'true' : 'false'; | ||
| 775 | } | ||
| 776 | |||
| 717 | default: | 777 | default: |
| 718 | logMacroRuntimeWarning({ message: `Unknown variable shorthand operation: "${operation}"` }); | 778 | logMacroRuntimeWarning({ message: `Unknown variable shorthand operation: "${operation}"` }); |
| 719 | return ''; | 779 | return ''; |
| @@ -132,6 +132,14 @@ const Tokens = Object.freeze({ | |||
| 132 | DoubleEquals: createToken({ name: 'Var.DoubleEquals', pattern: /==/ }), | 132 | DoubleEquals: createToken({ name: 'Var.DoubleEquals', pattern: /==/ }), |
| 133 | /** Not equals comparison operator (`!=`) - compares variable to value, returns inverted result */ | 133 | /** Not equals comparison operator (`!=`) - compares variable to value, returns inverted result */ |
| 134 | NotEquals: createToken({ name: 'Var.NotEquals', pattern: /!=/ }), | 134 | NotEquals: createToken({ name: 'Var.NotEquals', pattern: /!=/ }), |
| 135 | /** Greater than or equal comparison operator (`>=`) - must come before GreaterThan */ | ||
| 136 | GreaterThanOrEqual: createToken({ name: 'Var.GreaterThanOrEqual', pattern: />=/ }), | ||
| 137 | /** Greater than comparison operator (`>`) */ | ||
| 138 | GreaterThan: createToken({ name: 'Var.GreaterThan', pattern: />/ }), | ||
| 139 | /** Less than or equal comparison operator (`<=`) - must come before LessThan */ | ||
| 140 | LessThanOrEqual: createToken({ name: 'Var.LessThanOrEqual', pattern: /<=/ }), | ||
| 141 | /** Less than comparison operator (`<`) */ | ||
| 142 | LessThan: createToken({ name: 'Var.LessThan', pattern: /</ }), | ||
| 135 | /** Add/append operator (`+=`) - must come before Equals to avoid conflict */ | 143 | /** Add/append operator (`+=`) - must come before Equals to avoid conflict */ |
| 136 | PlusEquals: createToken({ name: 'Var.PlusEquals', pattern: /\+=/ }), | 144 | PlusEquals: createToken({ name: 'Var.PlusEquals', pattern: /\+=/ }), |
| 137 | /** Set operator (`=`) */ | 145 | /** Set operator (`=`) */ |
| @@ -258,6 +266,10 @@ const Def = { | |||
| 258 | enter(Tokens.Var.Operators.MinusEquals, modes.var_value, { andExits: modes.var_after_identifier }), | 266 | enter(Tokens.Var.Operators.MinusEquals, modes.var_value, { andExits: modes.var_after_identifier }), |
| 259 | enter(Tokens.Var.Operators.DoubleEquals, modes.var_value, { andExits: modes.var_after_identifier }), | 267 | enter(Tokens.Var.Operators.DoubleEquals, modes.var_value, { andExits: modes.var_after_identifier }), |
| 260 | enter(Tokens.Var.Operators.NotEquals, modes.var_value, { andExits: modes.var_after_identifier }), | 268 | enter(Tokens.Var.Operators.NotEquals, modes.var_value, { andExits: modes.var_after_identifier }), |
| 269 | enter(Tokens.Var.Operators.GreaterThanOrEqual, modes.var_value, { andExits: modes.var_after_identifier }), | ||
| 270 | enter(Tokens.Var.Operators.GreaterThan, modes.var_value, { andExits: modes.var_after_identifier }), | ||
| 271 | enter(Tokens.Var.Operators.LessThanOrEqual, modes.var_value, { andExits: modes.var_after_identifier }), | ||
| 272 | enter(Tokens.Var.Operators.LessThan, modes.var_value, { andExits: modes.var_after_identifier }), | ||
| 261 | enter(Tokens.Var.Operators.PlusEquals, modes.var_value, { andExits: modes.var_after_identifier }), | 273 | enter(Tokens.Var.Operators.PlusEquals, modes.var_value, { andExits: modes.var_after_identifier }), |
| 262 | enter(Tokens.Var.Operators.Equals, modes.var_value, { andExits: modes.var_after_identifier }), | 274 | enter(Tokens.Var.Operators.Equals, modes.var_value, { andExits: modes.var_after_identifier }), |
| 263 | // If we see the end, exit | 275 | // If we see the end, exit |
| @@ -92,65 +92,31 @@ class MacroParser extends CstParser { | |||
| 92 | $.OPTION2(() => $.SUBRULE($.variableOperator)); | 92 | $.OPTION2(() => $.SUBRULE($.variableOperator)); |
| 93 | }); | 93 | }); |
| 94 | 94 | ||
| 95 | // Variable operator: ++, --, = value, += value, -= value, ||, ??, ||=, ??=, ==, != | 95 | // Variable operator: ++, --, = value, += value, -= value, ||, ??, ||=, ??=, ==, !=, >, >=, <, <= |
| 96 | $.variableOperator = $.RULE('variableOperator', () => { | 96 | $.variableOperator = $.RULE('variableOperator', () => { |
| 97 | $.OR4([ | 97 | $.OR4([ |
| 98 | { ALT: () => $.CONSUME(Tokens.Var.Operators.Increment, { LABEL: 'Var.operator' }) }, | 98 | { ALT: () => $.CONSUME(Tokens.Var.Operators.Increment, { LABEL: 'Var.operator' }) }, |
| 99 | { ALT: () => $.CONSUME(Tokens.Var.Operators.Decrement, { LABEL: 'Var.operator' }) }, | 99 | { ALT: () => $.CONSUME(Tokens.Var.Operators.Decrement, { LABEL: 'Var.operator' }) }, |
| 100 | { | 100 | { |
| 101 | ALT: () => { | 101 | ALT: () => { |
| 102 | $.CONSUME(Tokens.Var.Operators.NullishCoalescingEquals, { LABEL: 'Var.operator' }); | 102 | $.OR5([ |
| 103 | { ALT: () => $.CONSUME(Tokens.Var.Operators.NullishCoalescingEquals, { LABEL: 'Var.operator' }) }, | ||
| 104 | { ALT: () => $.CONSUME(Tokens.Var.Operators.NullishCoalescing, { LABEL: 'Var.operator' }) }, | ||
| 105 | { ALT: () => $.CONSUME(Tokens.Var.Operators.LogicalOrEquals, { LABEL: 'Var.operator' }) }, | ||
| 106 | { ALT: () => $.CONSUME(Tokens.Var.Operators.LogicalOr, { LABEL: 'Var.operator' }) }, | ||
| 107 | { ALT: () => $.CONSUME(Tokens.Var.Operators.MinusEquals, { LABEL: 'Var.operator' }) }, | ||
| 108 | { ALT: () => $.CONSUME(Tokens.Var.Operators.DoubleEquals, { LABEL: 'Var.operator' }) }, | ||
| 109 | { ALT: () => $.CONSUME(Tokens.Var.Operators.NotEquals, { LABEL: 'Var.operator' }) }, | ||
| 110 | { ALT: () => $.CONSUME(Tokens.Var.Operators.GreaterThanOrEqual, { LABEL: 'Var.operator' }) }, | ||
| 111 | { ALT: () => $.CONSUME(Tokens.Var.Operators.GreaterThan, { LABEL: 'Var.operator' }) }, | ||
| 112 | { ALT: () => $.CONSUME(Tokens.Var.Operators.LessThanOrEqual, { LABEL: 'Var.operator' }) }, | ||
| 113 | { ALT: () => $.CONSUME(Tokens.Var.Operators.LessThan, { LABEL: 'Var.operator' }) }, | ||
| 114 | { ALT: () => $.CONSUME(Tokens.Var.Operators.PlusEquals, { LABEL: 'Var.operator' }) }, | ||
| 115 | { ALT: () => $.CONSUME(Tokens.Var.Operators.Equals, { LABEL: 'Var.operator' }) }, | ||
| 116 | ]); | ||
| 103 | $.SUBRULE($.variableValue, { LABEL: 'Var.value' }); | 117 | $.SUBRULE($.variableValue, { LABEL: 'Var.value' }); |
| 104 | }, | 118 | }, |
| 105 | }, | 119 | }, |
| 106 | { | ||
| 107 | ALT: () => { | ||
| 108 | $.CONSUME(Tokens.Var.Operators.NullishCoalescing, { LABEL: 'Var.operator' }); | ||
| 109 | $.SUBRULE2($.variableValue, { LABEL: 'Var.value' }); | ||
| 110 | }, | ||
| 111 | }, | ||
| 112 | { | ||
| 113 | ALT: () => { | ||
| 114 | $.CONSUME(Tokens.Var.Operators.LogicalOrEquals, { LABEL: 'Var.operator' }); | ||
| 115 | $.SUBRULE3($.variableValue, { LABEL: 'Var.value' }); | ||
| 116 | }, | ||
| 117 | }, | ||
| 118 | { | ||
| 119 | ALT: () => { | ||
| 120 | $.CONSUME(Tokens.Var.Operators.LogicalOr, { LABEL: 'Var.operator' }); | ||
| 121 | $.SUBRULE4($.variableValue, { LABEL: 'Var.value' }); | ||
| 122 | }, | ||
| 123 | }, | ||
| 124 | { | ||
| 125 | ALT: () => { | ||
| 126 | $.CONSUME(Tokens.Var.Operators.MinusEquals, { LABEL: 'Var.operator' }); | ||
| 127 | $.SUBRULE5($.variableValue, { LABEL: 'Var.value' }); | ||
| 128 | }, | ||
| 129 | }, | ||
| 130 | { | ||
| 131 | ALT: () => { | ||
| 132 | $.CONSUME(Tokens.Var.Operators.DoubleEquals, { LABEL: 'Var.operator' }); | ||
| 133 | $.SUBRULE6($.variableValue, { LABEL: 'Var.value' }); | ||
| 134 | }, | ||
| 135 | }, | ||
| 136 | { | ||
| 137 | ALT: () => { | ||
| 138 | $.CONSUME(Tokens.Var.Operators.NotEquals, { LABEL: 'Var.operator' }); | ||
| 139 | $.SUBRULE7($.variableValue, { LABEL: 'Var.value' }); | ||
| 140 | }, | ||
| 141 | }, | ||
| 142 | { | ||
| 143 | ALT: () => { | ||
| 144 | $.CONSUME(Tokens.Var.Operators.PlusEquals, { LABEL: 'Var.operator' }); | ||
| 145 | $.SUBRULE8($.variableValue, { LABEL: 'Var.value' }); | ||
| 146 | }, | ||
| 147 | }, | ||
| 148 | { | ||
| 149 | ALT: () => { | ||
| 150 | $.CONSUME(Tokens.Var.Operators.Equals, { LABEL: 'Var.operator' }); | ||
| 151 | $.SUBRULE9($.variableValue, { LABEL: 'Var.value' }); | ||
| 152 | }, | ||
| 153 | }, | ||
| 154 | ]); | 120 | ]); |
| 155 | }); | 121 | }); |
| 156 | 122 | ||
| @@ -658,17 +658,13 @@ export class SlashCommandParser { | |||
| 658 | resultStart = macro.start + 2 + prefixIndex + 1; // +1 to skip the prefix | 658 | resultStart = macro.start + 2 + prefixIndex + 1; // +1 to skip the prefix |
| 659 | } | 659 | } |
| 660 | } else if (context.isTypingOperator) { | 660 | } else if (context.isTypingOperator) { |
| 661 | // Typing operator: identifier = partial operator text (if any), start = after variable name | 661 | // Typing operator: identifier = partial operator or current operator, start = after variable name |
| 662 | // Using partial operator as identifier ensures cursor is within name range for filtering | 662 | resultIdentifier = context.partialOperator || context.variableOperator || ''; |
| 663 | resultIdentifier = context.partialOperator || ''; | 663 | // Use actual variableNameEnd position from parsing (accounts for whitespace) |
| 664 | if (prefixIndex >= 0) { | 664 | resultStart = macro.start + 2 + context.variableNameEnd; |
| 665 | // Start after prefix + variable name length | 665 | // Skip whitespace between variable name and operator |
| 666 | resultStart = macro.start + 2 + prefixIndex + 1 + context.variableName.length; | 666 | while (resultStart < index && /\s/.test(text[resultStart])) { |
| 667 | // If no partial operator (just whitespace after var name), set start to cursor | 667 | resultStart++; |
| 668 | // This ensures cursor is in the name range for filtering | ||
| 669 | if (!context.partialOperator) { | ||
| 670 | resultStart = index; | ||
| 671 | } | ||
| 672 | } | 668 | } |
| 673 | } else if (context.isOperatorComplete) { | 669 | } else if (context.isOperatorComplete) { |
| 674 | // Operator complete (++ or --) - show context but no value input needed | 670 | // Operator complete (++ or --) - show context but no value input needed |
| @@ -677,23 +673,20 @@ export class SlashCommandParser { | |||
| 677 | } else if (context.hasInvalidTrailingChars) { | 673 | } else if (context.hasInvalidTrailingChars) { |
| 678 | // Invalid chars after variable name: show the invalid chars for warning | 674 | // Invalid chars after variable name: show the invalid chars for warning |
| 679 | resultIdentifier = context.invalidTrailingChars || ''; | 675 | resultIdentifier = context.invalidTrailingChars || ''; |
| 680 | if (prefixIndex >= 0) { | 676 | // Use actual variableNameEnd position from parsing |
| 681 | resultStart = macro.start + 2 + prefixIndex + 1 + context.variableName.length; | 677 | resultStart = macro.start + 2 + context.variableNameEnd; |
| 682 | } | ||
| 683 | } else if (context.isTypingValue) { | 678 | } else if (context.isTypingValue) { |
| 684 | // Typing value: identifier = value being typed, start = after operator | 679 | // Typing value: identifier = value being typed, start = after operator |
| 685 | resultIdentifier = context.variableValue; | 680 | resultIdentifier = context.variableValue; |
| 686 | if (prefixIndex >= 0) { | 681 | // Use actual operatorEnd position from parsing (accounts for whitespace) |
| 687 | const operatorLen = context.variableOperator?.length ?? 0; | 682 | resultStart = macro.start + 2 + context.variableOperatorEnd; |
| 688 | resultStart = macro.start + 2 + prefixIndex + 1 + context.variableName.length + operatorLen; | 683 | // Skip any whitespace between operator and value |
| 689 | // Skip any whitespace between operator and value | 684 | while (resultStart < index && /\s/.test(text[resultStart])) { |
| 690 | while (resultStart < index && /\s/.test(text[resultStart])) { | 685 | resultStart++; |
| 691 | resultStart++; | ||
| 692 | } | ||
| 693 | |||
| 694 | makeNoMatchText = () => `Type any value you want to ${context.variableOperator == '+=' ? `add to the variable '${context.variableName}'` : `set the variable '${context.variableName}' to`}.`; | ||
| 695 | makeNoOptionsText = () => 'Enter a variable value'; | ||
| 696 | } | 686 | } |
| 687 | |||
| 688 | makeNoMatchText = () => `Type any value you want to ${context.variableOperator == '+=' ? `add to the variable '${context.variableName}'` : `set the variable '${context.variableName}' to`}.`; | ||
| 689 | makeNoOptionsText = () => 'Enter a variable value'; | ||
| 697 | } else { | 690 | } else { |
| 698 | // Fallback: use variable name | 691 | // Fallback: use variable name |
| 699 | resultIdentifier = context.variableName; | 692 | resultIdentifier = context.variableName; |
| @@ -952,16 +945,20 @@ export class SlashCommandParser { | |||
| 952 | prefixOption.valueProvider = () => ''; // Already typed, don't re-insert | 945 | prefixOption.valueProvider = () => ''; // Already typed, don't re-insert |
| 953 | prefixOption.makeSelectable = false; | 946 | prefixOption.makeSelectable = false; |
| 954 | prefixOption.sortPriority = 1; // Show at top | 947 | prefixOption.sortPriority = 1; // Show at top |
| 948 | prefixOption.matchProvider = () => true; // Always show regardless of filtering | ||
| 955 | options.push(prefixOption); | 949 | options.push(prefixOption); |
| 956 | } | 950 | } |
| 957 | 951 | ||
| 958 | // If typing the variable name, suggest existing variables | 952 | // If typing the variable name, suggest existing variables |
| 959 | if (context.isTypingVariableName) { | 953 | // Get existing variable names from the appropriate scope |
| 960 | // Get existing variable names from the appropriate scope | 954 | // Filter to only include names that are valid for shorthand syntax |
| 961 | // Filter to only include names that are valid for shorthand syntax | 955 | const existingVariables = this.#getVariableNames(scope) |
| 962 | const existingVariables = this.#getVariableNames(scope) | 956 | .filter(name => isValidVariableShorthandName(name)); |
| 963 | .filter(name => isValidVariableShorthandName(name)); | ||
| 964 | 957 | ||
| 958 | // Check if the typed variable name exactly matches an existing variable | ||
| 959 | const variableNameMatchesExisting = context.variableName.length > 0 && existingVariables.includes(context.variableName); | ||
| 960 | |||
| 961 | if (context.isTypingVariableName) { | ||
| 965 | // Add existing variables that match the typed name | 962 | // Add existing variables that match the typed name |
| 966 | for (const varName of existingVariables) { | 963 | for (const varName of existingVariables) { |
| 967 | const option = new VariableNameAutoCompleteOption(varName, scope, false); | 964 | const option = new VariableNameAutoCompleteOption(varName, scope, false); |
| @@ -995,6 +992,20 @@ export class SlashCommandParser { | |||
| 995 | } | 992 | } |
| 996 | options.push(newVarOption); | 993 | options.push(newVarOption); |
| 997 | } | 994 | } |
| 995 | |||
| 996 | // If the typed variable name exactly matches an existing variable, also show operators | ||
| 997 | // This allows users to see available operators without having to type a space first | ||
| 998 | if (variableNameMatchesExisting) { | ||
| 999 | for (const [, operatorDef] of VariableOperatorDefinitions) { | ||
| 1000 | const opOption = new VariableOperatorAutoCompleteOption(operatorDef); | ||
| 1001 | opOption.sortPriority = 6; // Lower priority than variable suggestions | ||
| 1002 | opOption.matchProvider = () => true; // Always show | ||
| 1003 | // IMPORTANT: Operators should INSERT after variable name, not replace it | ||
| 1004 | // Use replacementStartOffset to shift insertion point past the variable name | ||
| 1005 | opOption.replacementStartOffset = context.variableName.length; | ||
| 1006 | options.push(opOption); | ||
| 1007 | } | ||
| 1008 | } | ||
| 998 | } | 1009 | } |
| 999 | 1010 | ||
| 1000 | // If there are invalid trailing characters after the variable name, show a warning | 1011 | // If there are invalid trailing characters after the variable name, show a warning |
| @@ -1026,14 +1037,23 @@ export class SlashCommandParser { | |||
| 1026 | options.push(varNameOption); | 1037 | options.push(varNameOption); |
| 1027 | 1038 | ||
| 1028 | // Then show available operators, filtered by partial prefix if any | 1039 | // Then show available operators, filtered by partial prefix if any |
| 1040 | // Also filter by current complete operator to show longer variants (e.g., > shows >=) | ||
| 1029 | const partialOp = context.partialOperator || ''; | 1041 | const partialOp = context.partialOperator || ''; |
| 1042 | const currentOp = context.variableOperator || ''; | ||
| 1043 | const filterPrefix = partialOp || currentOp; | ||
| 1030 | for (const [, operatorDef] of VariableOperatorDefinitions) { | 1044 | for (const [, operatorDef] of VariableOperatorDefinitions) { |
| 1031 | // Filter by partial operator prefix if user is typing one | 1045 | // Filter by operator prefix if user is typing one |
| 1032 | if (partialOp && !operatorDef.symbol.startsWith(partialOp)) { | 1046 | // This allows typing ">" to show both ">" and ">=" |
| 1047 | if (filterPrefix && !operatorDef.symbol.startsWith(filterPrefix)) { | ||
| 1033 | continue; | 1048 | continue; |
| 1034 | } | 1049 | } |
| 1035 | const opOption = new VariableOperatorAutoCompleteOption(operatorDef); | 1050 | const opOption = new VariableOperatorAutoCompleteOption(operatorDef); |
| 1036 | opOption.sortPriority = 5; | 1051 | // Exact match gets higher priority |
| 1052 | opOption.sortPriority = operatorDef.symbol === currentOp ? 4 : 5; | ||
| 1053 | // Already-typed operator is non-selectable | ||
| 1054 | if (operatorDef.symbol === currentOp) { | ||
| 1055 | opOption.valueProvider = () => ''; | ||
| 1056 | } | ||
| 1037 | // Always match operators when showing operator suggestions | 1057 | // Always match operators when showing operator suggestions |
| 1038 | opOption.matchProvider = () => true; | 1058 | opOption.matchProvider = () => true; |
| 1039 | options.push(opOption); | 1059 | options.push(opOption); |
| @@ -1041,21 +1061,23 @@ export class SlashCommandParser { | |||
| 1041 | } | 1061 | } |
| 1042 | 1062 | ||
| 1043 | // If typing value (after = or +=), no autocomplete needed - freeform text | 1063 | // If typing value (after = or +=), no autocomplete needed - freeform text |
| 1044 | // But we can show the current context for reference | 1064 | // But we show the current context for reference (greyed out, non-selectable) |
| 1045 | if (context.isTypingValue) { | 1065 | if (context.isTypingValue && !context.isTypingOperator) { |
| 1046 | // Show the current variable name as context | 1066 | // Show the current variable name as context (non-selectable) |
| 1047 | const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false); | 1067 | const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false); |
| 1048 | varNameOption.valueProvider = () => ''; // Context only | 1068 | varNameOption.valueProvider = () => ''; // Context only |
| 1069 | varNameOption.makeSelectable = false; | ||
| 1049 | varNameOption.sortPriority = 2; | 1070 | varNameOption.sortPriority = 2; |
| 1050 | varNameOption.matchProvider = () => true; // Always show | 1071 | varNameOption.matchProvider = () => true; // Always show |
| 1051 | options.push(varNameOption); | 1072 | options.push(varNameOption); |
| 1052 | 1073 | ||
| 1053 | // Show the operator that was used | 1074 | // Show the operator that was used (non-selectable) |
| 1054 | if (context.variableOperator) { | 1075 | if (context.variableOperator) { |
| 1055 | const opDef = VariableOperatorDefinitions.get(context.variableOperator); | 1076 | const opDef = VariableOperatorDefinitions.get(context.variableOperator); |
| 1056 | if (opDef) { | 1077 | if (opDef) { |
| 1057 | const opOption = new VariableOperatorAutoCompleteOption(opDef); | 1078 | const opOption = new VariableOperatorAutoCompleteOption(opDef); |
| 1058 | opOption.valueProvider = () => ''; // Already typed | 1079 | opOption.valueProvider = () => ''; // Already typed |
| 1080 | opOption.makeSelectable = false; | ||
| 1059 | opOption.sortPriority = 3; | 1081 | opOption.sortPriority = 3; |
| 1060 | opOption.matchProvider = () => true; // Always show | 1082 | opOption.matchProvider = () => true; // Always show |
| 1061 | options.push(opOption); | 1083 | options.push(opOption); |
| @@ -1063,21 +1085,23 @@ export class SlashCommandParser { | |||
| 1063 | } | 1085 | } |
| 1064 | } | 1086 | } |
| 1065 | 1087 | ||
| 1066 | // If operator is complete (++ or --), show context without value input | 1088 | // If operator is complete (++ or --), show context without value input (non-selectable) |
| 1067 | if (context.isOperatorComplete) { | 1089 | if (context.isOperatorComplete && !context.isTypingOperator) { |
| 1068 | // Show the current variable name as context | 1090 | // Show the current variable name as context (non-selectable) |
| 1069 | const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false); | 1091 | const varNameOption = new VariableNameAutoCompleteOption(context.variableName, scope, false); |
| 1070 | varNameOption.valueProvider = () => ''; // Context only | 1092 | varNameOption.valueProvider = () => ''; // Context only |
| 1093 | varNameOption.makeSelectable = false; | ||
| 1071 | varNameOption.sortPriority = 2; | 1094 | varNameOption.sortPriority = 2; |
| 1072 | varNameOption.matchProvider = () => true; // Always show | 1095 | varNameOption.matchProvider = () => true; // Always show |
| 1073 | options.push(varNameOption); | 1096 | options.push(varNameOption); |
| 1074 | 1097 | ||
| 1075 | // Show the operator that was used | 1098 | // Show the operator that was used (non-selectable) |
| 1076 | if (context.variableOperator) { | 1099 | if (context.variableOperator) { |
| 1077 | const opDef = VariableOperatorDefinitions.get(context.variableOperator); | 1100 | const opDef = VariableOperatorDefinitions.get(context.variableOperator); |
| 1078 | if (opDef) { | 1101 | if (opDef) { |
| 1079 | const opOption = new VariableOperatorAutoCompleteOption(opDef); | 1102 | const opOption = new VariableOperatorAutoCompleteOption(opDef); |
| 1080 | opOption.valueProvider = () => ''; // Already typed | 1103 | opOption.valueProvider = () => ''; // Already typed |
| 1104 | opOption.makeSelectable = false; | ||
| 1081 | opOption.sortPriority = 3; | 1105 | opOption.sortPriority = 3; |
| 1082 | opOption.matchProvider = () => true; // Always show | 1106 | opOption.matchProvider = () => true; // Always show |
| 1083 | options.push(opOption); | 1107 | options.push(opOption); |
| @@ -2699,6 +2699,102 @@ test.describe('MacroEngine', () => { | |||
| 2699 | expect(output).toBe('true'); | 2699 | expect(output).toBe('true'); |
| 2700 | }); | 2700 | }); |
| 2701 | 2701 | ||
| 2702 | // {{.myvar > value}} - greater than comparison (numeric) | ||
| 2703 | test('should return true when variable is greater than value with >', async ({ page }) => { | ||
| 2704 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 5}}', { local: { myvar: '10' } }); | ||
| 2705 | expect(output).toBe('true'); | ||
| 2706 | }); | ||
| 2707 | |||
| 2708 | test('should return false when variable is not greater than value with >', async ({ page }) => { | ||
| 2709 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 10}}', { local: { myvar: '5' } }); | ||
| 2710 | expect(output).toBe('false'); | ||
| 2711 | }); | ||
| 2712 | |||
| 2713 | test('should return false when variable equals value with >', async ({ page }) => { | ||
| 2714 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 10}}', { local: { myvar: '10' } }); | ||
| 2715 | expect(output).toBe('false'); | ||
| 2716 | }); | ||
| 2717 | |||
| 2718 | test('should return false for non-numeric values with >', async ({ page }) => { | ||
| 2719 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar > 5}}', { local: { myvar: 'abc' } }); | ||
| 2720 | expect(output).toBe('false'); | ||
| 2721 | }); | ||
| 2722 | |||
| 2723 | // {{.myvar >= value}} - greater than or equal comparison (numeric) | ||
| 2724 | test('should return true when variable is greater than value with >=', async ({ page }) => { | ||
| 2725 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 5}}', { local: { myvar: '10' } }); | ||
| 2726 | expect(output).toBe('true'); | ||
| 2727 | }); | ||
| 2728 | |||
| 2729 | test('should return true when variable equals value with >=', async ({ page }) => { | ||
| 2730 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 10}}', { local: { myvar: '10' } }); | ||
| 2731 | expect(output).toBe('true'); | ||
| 2732 | }); | ||
| 2733 | |||
| 2734 | test('should return false when variable is less than value with >=', async ({ page }) => { | ||
| 2735 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 10}}', { local: { myvar: '5' } }); | ||
| 2736 | expect(output).toBe('false'); | ||
| 2737 | }); | ||
| 2738 | |||
| 2739 | // {{.myvar < value}} - less than comparison (numeric) | ||
| 2740 | test('should return true when variable is less than value with <', async ({ page }) => { | ||
| 2741 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 10}}', { local: { myvar: '5' } }); | ||
| 2742 | expect(output).toBe('true'); | ||
| 2743 | }); | ||
| 2744 | |||
| 2745 | test('should return false when variable is not less than value with <', async ({ page }) => { | ||
| 2746 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 5}}', { local: { myvar: '10' } }); | ||
| 2747 | expect(output).toBe('false'); | ||
| 2748 | }); | ||
| 2749 | |||
| 2750 | test('should return false when variable equals value with <', async ({ page }) => { | ||
| 2751 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 10}}', { local: { myvar: '10' } }); | ||
| 2752 | expect(output).toBe('false'); | ||
| 2753 | }); | ||
| 2754 | |||
| 2755 | test('should return false for non-numeric values with <', async ({ page }) => { | ||
| 2756 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 5}}', { local: { myvar: 'abc' } }); | ||
| 2757 | expect(output).toBe('false'); | ||
| 2758 | }); | ||
| 2759 | |||
| 2760 | // {{.myvar <= value}} - less than or equal comparison (numeric) | ||
| 2761 | test('should return true when variable is less than value with <=', async ({ page }) => { | ||
| 2762 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 10}}', { local: { myvar: '5' } }); | ||
| 2763 | expect(output).toBe('true'); | ||
| 2764 | }); | ||
| 2765 | |||
| 2766 | test('should return true when variable equals value with <=', async ({ page }) => { | ||
| 2767 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 10}}', { local: { myvar: '10' } }); | ||
| 2768 | expect(output).toBe('true'); | ||
| 2769 | }); | ||
| 2770 | |||
| 2771 | test('should return false when variable is greater than value with <=', async ({ page }) => { | ||
| 2772 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 5}}', { local: { myvar: '10' } }); | ||
| 2773 | expect(output).toBe('false'); | ||
| 2774 | }); | ||
| 2775 | |||
| 2776 | // Negative numbers with comparison operators | ||
| 2777 | test('should handle negative numbers with > operator', async ({ page }) => { | ||
| 2778 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar > -5}}', { local: { myvar: '0' } }); | ||
| 2779 | expect(output).toBe('true'); | ||
| 2780 | }); | ||
| 2781 | |||
| 2782 | test('should handle negative numbers with < operator', async ({ page }) => { | ||
| 2783 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar < 0}}', { local: { myvar: '-5' } }); | ||
| 2784 | expect(output).toBe('true'); | ||
| 2785 | }); | ||
| 2786 | |||
| 2787 | // Decimal numbers with comparison operators | ||
| 2788 | test('should handle decimal numbers with >= operator', async ({ page }) => { | ||
| 2789 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar >= 3.14}}', { local: { myvar: '3.14' } }); | ||
| 2790 | expect(output).toBe('true'); | ||
| 2791 | }); | ||
| 2792 | |||
| 2793 | test('should handle decimal numbers with <= operator', async ({ page }) => { | ||
| 2794 | const output = await evaluateWithEngineAndVariables(page, '{{.myvar <= 2.5}}', { local: { myvar: '2.49' } }); | ||
| 2795 | expect(output).toBe('true'); | ||
| 2796 | }); | ||
| 2797 | |||
| 2702 | // Global variable versions of new operators | 2798 | // Global variable versions of new operators |
| 2703 | test('should use || with global variable', async ({ page }) => { | 2799 | test('should use || with global variable', async ({ page }) => { |
| 2704 | const output = await evaluateWithEngineAndVariables(page, '{{$myvar || globaldefault}}', { global: { myvar: '' } }); | 2800 | const output = await evaluateWithEngineAndVariables(page, '{{$myvar || globaldefault}}', { global: { myvar: '' } }); |