@hyperfixi/core 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +8 -11
  2. package/dist/api/dom-processor.d.ts +8 -4
  3. package/dist/api/hyperscript-api.d.ts +5 -1
  4. package/dist/ast-utils/index.js +25320 -94
  5. package/dist/ast-utils/index.mjs +25320 -94
  6. package/dist/ast-utils/interchange/types.d.ts +7 -1
  7. package/dist/behaviors/index.js +54 -100
  8. package/dist/behaviors/index.mjs +54 -100
  9. package/dist/bundle-generator/index.js +44 -6
  10. package/dist/bundle-generator/index.mjs +44 -6
  11. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  12. package/dist/bundle-generator/template-capabilities.d.ts +1 -1
  13. package/dist/chunks/bridge-C4d3blZX.js +2 -0
  14. package/dist/chunks/browser-modular-BwIRlrTM.js +2 -0
  15. package/dist/chunks/feature-eventsource-BpZvPy_K.js +2 -0
  16. package/dist/chunks/{feature-sockets-ClOH7vk7.js → feature-sockets-CrYvjZ4j.js} +2 -2
  17. package/dist/chunks/feature-webworker-BSYguEIW.js +2 -0
  18. package/dist/chunks/index-Beno_SBy.js +2 -0
  19. package/dist/commands/advanced/async.d.ts +6 -2
  20. package/dist/commands/advanced/js.d.ts +1 -1
  21. package/dist/commands/animation/start-view-transition.d.ts +24 -0
  22. package/dist/commands/async/fetch.d.ts +6 -1
  23. package/dist/commands/control-flow/repeat.d.ts +2 -0
  24. package/dist/commands/data/clear.d.ts +23 -0
  25. package/dist/commands/data/set.d.ts +6 -0
  26. package/dist/commands/dom/close.d.ts +19 -0
  27. package/dist/commands/dom/empty.d.ts +19 -0
  28. package/dist/commands/dom/open.d.ts +21 -0
  29. package/dist/commands/dom/reset.d.ts +19 -0
  30. package/dist/commands/dom/select.d.ts +19 -0
  31. package/dist/commands/dom/swap.d.ts +7 -4
  32. package/dist/commands/events/trigger.d.ts +1 -1
  33. package/dist/commands/execution/blur.d.ts +19 -0
  34. package/dist/commands/execution/call.d.ts +1 -2
  35. package/dist/commands/execution/focus.d.ts +19 -0
  36. package/dist/commands/helpers/element-resolution.d.ts +2 -2
  37. package/dist/commands/helpers/event-waiting.d.ts +1 -1
  38. package/dist/commands/helpers/numeric-target-parser.d.ts +7 -0
  39. package/dist/commands/index.d.ts +34 -2
  40. package/dist/commands/index.js +19374 -4848
  41. package/dist/commands/index.mjs +19342 -4849
  42. package/dist/commands/navigation/go.d.ts +3 -0
  43. package/dist/commands/navigation/scroll-to.d.ts +26 -0
  44. package/dist/commands/utility/beep.d.ts +2 -2
  45. package/dist/commands/utility/breakpoint.d.ts +19 -0
  46. package/dist/commands/utility/pick.d.ts +11 -2
  47. package/dist/compatibility/browser-bundle-modular.d.ts +2 -2
  48. package/dist/compatibility/browser-bundle-multilingual.d.ts +1 -1
  49. package/dist/compatibility/browser-bundle-semantic-complete.d.ts +3 -3
  50. package/dist/compatibility/browser-bundle.d.ts +13 -6
  51. package/dist/compatibility/browser-modular.d.ts +1 -3
  52. package/dist/core/expression-evaluator.d.ts +4 -4
  53. package/dist/core/expression-registry.d.ts +8 -0
  54. package/dist/expressions/bundles/common-expressions.d.ts +2 -2
  55. package/dist/expressions/bundles/core-expressions.d.ts +2 -2
  56. package/dist/expressions/bundles/full-expressions.d.ts +2 -2
  57. package/dist/expressions/bundles/index.d.ts +3 -3
  58. package/dist/expressions/collection/index.d.ts +35 -0
  59. package/dist/expressions/conversion/impl/index.d.ts +1 -1
  60. package/dist/expressions/index.d.ts +4 -3
  61. package/dist/expressions/index.js +1117 -1590
  62. package/dist/expressions/index.mjs +1113 -1586
  63. package/dist/expressions/logical/index.d.ts +2 -0
  64. package/dist/expressions/mathematical/index.d.ts +11 -0
  65. package/dist/expressions/shared/index.d.ts +1 -1
  66. package/dist/expressions/shared/number-utils.d.ts +1 -0
  67. package/dist/htmx/htmx-attribute-processor.d.ts +37 -1
  68. package/dist/htmx/htmx-translator.d.ts +2 -0
  69. package/dist/htmx/i18n-hooks.d.ts +15 -0
  70. package/dist/htmx/i18n-orchestrator.d.ts +15 -0
  71. package/dist/htmx/lang-resolver.d.ts +3 -0
  72. package/dist/htmx/sse.d.ts +60 -0
  73. package/dist/htmx/ws.d.ts +59 -0
  74. package/dist/hyperfixi-browser-classic-i18n.js +2 -0
  75. package/dist/hyperfixi-browser-minimal.js +1 -0
  76. package/dist/hyperfixi-browser-standard.js +2 -0
  77. package/dist/hyperfixi-browser.js +2 -0
  78. package/dist/hyperfixi-classic-i18n.js +1 -1
  79. package/dist/hyperfixi-hx-v4.js +1 -0
  80. package/dist/hyperfixi-hx.js +1 -1
  81. package/dist/hyperfixi-hybrid-complete.js +1 -1
  82. package/dist/hyperfixi-hybrid-hx.js +1 -0
  83. package/dist/hyperfixi-minimal.js +1 -1
  84. package/dist/hyperfixi-multilingual.js +1 -1
  85. package/dist/hyperfixi-standard.js +1 -1
  86. package/dist/hyperfixi.js +1 -1
  87. package/dist/hyperfixi.mjs +1 -1
  88. package/dist/index.d.ts +2 -0
  89. package/dist/index.js +43613 -45063
  90. package/dist/index.min.js +1 -1
  91. package/dist/index.mjs +43610 -45064
  92. package/dist/lib/index.d.ts +2 -2
  93. package/dist/lib/morph-adapter.d.ts +0 -13
  94. package/dist/lib/swap-executor.d.ts +0 -10
  95. package/dist/lib/view-transitions.d.ts +1 -30
  96. package/dist/lokascript-browser-classic-i18n.js +1 -1
  97. package/dist/lokascript-browser-minimal.js +1 -1
  98. package/dist/lokascript-browser-standard.js +1 -1
  99. package/dist/lokascript-browser.js +1 -1
  100. package/dist/lokascript-hybrid-complete.js +1 -1
  101. package/dist/lokascript-hybrid-hx.js +1 -1
  102. package/dist/lokascript-multilingual.js +1 -1
  103. package/dist/lsp-metadata.d.ts +9 -4
  104. package/dist/lsp-metadata.js +187 -3
  105. package/dist/lsp-metadata.mjs +185 -4
  106. package/dist/metadata.d.ts +1 -1
  107. package/dist/metadata.js +3 -3
  108. package/dist/metadata.mjs +3 -3
  109. package/dist/multilingual/bridge.d.ts +1 -1
  110. package/dist/multilingual/index.js +79 -22
  111. package/dist/multilingual/index.mjs +79 -22
  112. package/dist/parser/command-parsers/animation-commands.d.ts +1 -0
  113. package/dist/parser/command-parsers/utility-commands.d.ts +1 -0
  114. package/dist/parser/extensions.d.ts +51 -0
  115. package/dist/parser/full-parser.js +1224 -899
  116. package/dist/parser/full-parser.mjs +1224 -899
  117. package/dist/parser/helpers/ast-helpers.d.ts +1 -0
  118. package/dist/parser/helpers/parsing-helpers.d.ts +4 -0
  119. package/dist/parser/hybrid/index.js +7 -0
  120. package/dist/parser/hybrid/index.mjs +7 -0
  121. package/dist/parser/hybrid/parser-core.js +7 -0
  122. package/dist/parser/hybrid/parser-core.mjs +7 -0
  123. package/dist/parser/hybrid/tokenizer.js +7 -0
  124. package/dist/parser/hybrid/tokenizer.mjs +7 -0
  125. package/dist/parser/hybrid-parser.js +7 -0
  126. package/dist/parser/hybrid-parser.mjs +7 -0
  127. package/dist/parser/parser-types.d.ts +8 -28
  128. package/dist/parser/parser.d.ts +3 -7
  129. package/dist/parser/pratt-parser.d.ts +0 -3
  130. package/dist/parser/runtime.d.ts +4 -0
  131. package/dist/parser/semantic-integration.d.ts +17 -0
  132. package/dist/parser/types.d.ts +7 -1
  133. package/dist/reference/index.js +91 -0
  134. package/dist/reference/index.mjs +91 -0
  135. package/dist/registry/index.js +12866 -5876
  136. package/dist/registry/index.mjs +12866 -5876
  137. package/dist/registry/universal-types.d.ts +2 -1
  138. package/dist/runtime/command-adapter.d.ts +23 -16
  139. package/dist/runtime/plugin.d.ts +14 -0
  140. package/dist/runtime/runtime-base.d.ts +32 -7
  141. package/dist/runtime/runtime-factory.d.ts +3 -3
  142. package/dist/runtime/runtime.d.ts +2 -2
  143. package/dist/test-setup.d.ts +1 -0
  144. package/dist/types/base-types.d.ts +3 -0
  145. package/dist/types/feature-types.d.ts +1 -1
  146. package/dist/types/index.d.ts +2 -2
  147. package/package.json +26 -20
  148. package/vocab/htmx/ar.js +60 -0
  149. package/vocab/htmx/bn.js +49 -0
  150. package/vocab/htmx/de.js +60 -0
  151. package/vocab/htmx/en.js +21 -0
  152. package/vocab/htmx/es.js +60 -0
  153. package/vocab/htmx/fr.js +59 -0
  154. package/vocab/htmx/he.js +40 -0
  155. package/vocab/htmx/hi.js +60 -0
  156. package/vocab/htmx/id.js +57 -0
  157. package/vocab/htmx/it.js +58 -0
  158. package/vocab/htmx/ja.js +60 -0
  159. package/vocab/htmx/ko.js +60 -0
  160. package/vocab/htmx/ms.js +35 -0
  161. package/vocab/htmx/pl.js +60 -0
  162. package/vocab/htmx/pt.js +60 -0
  163. package/vocab/htmx/qu.js +60 -0
  164. package/vocab/htmx/ru.js +60 -0
  165. package/vocab/htmx/sw.js +59 -0
  166. package/vocab/htmx/th.js +49 -0
  167. package/vocab/htmx/tl.js +33 -0
  168. package/vocab/htmx/tr.js +60 -0
  169. package/vocab/htmx/uk.js +60 -0
  170. package/vocab/htmx/vi.js +51 -0
  171. package/vocab/htmx/zh.js +60 -0
  172. package/dist/bundles/test-minimal.d.ts +0 -3
  173. package/dist/bundles/test-standard.d.ts +0 -3
  174. package/dist/chunks/bridge-BlRqsZT4.js +0 -2
  175. package/dist/chunks/browser-modular-AbV0Ql4i.js +0 -2
  176. package/dist/chunks/feature-eventsource-B5F2-H1r.js +0 -2
  177. package/dist/chunks/feature-webworker-3bAp0ac9.js +0 -2
  178. package/dist/chunks/index-BDYQHwCF.js +0 -2
  179. package/dist/compatibility/browser-bundle-minimal.d.ts +0 -8
  180. package/dist/compatibility/browser-bundle-standard.d.ts +0 -8
  181. package/dist/compatibility/hyperscript-tests/test-adapter.d.ts +0 -13
  182. package/dist/core/base-expression-evaluator.d.ts +0 -74
  183. package/dist/core/binary-expression-evaluator.d.ts +0 -7
  184. package/dist/core/call-expression-evaluator.d.ts +0 -7
  185. package/dist/core/configurable-expression-evaluator.d.ts +0 -5
  186. package/dist/core/lazy-expression-evaluator.d.ts +0 -22
  187. package/dist/core/parser.d.ts +0 -21
  188. package/dist/core/selector-evaluator.d.ts +0 -15
  189. package/dist/core/template-literal-evaluator.d.ts +0 -5
  190. package/dist/expressions/comparison/index.d.ts +0 -80
  191. package/dist/expressions/comparison/utils.d.ts +0 -2
  192. package/dist/expressions/conversion/impl/bridge.d.ts +0 -117
  193. package/dist/expressions/logical/impl/pattern-matching.d.ts +0 -58
  194. package/dist/expressions/positional/impl/bridge.d.ts +0 -95
  195. package/dist/expressions/property/index.d.ts +0 -55
  196. package/dist/expressions/references/impl/bridge.d.ts +0 -54
  197. package/dist/extensions/index.d.ts +0 -3
  198. package/dist/extensions/tailwind.d.ts +0 -22
  199. package/dist/mod.d.ts +0 -63
  200. package/dist/parser/expression-parser.d.ts +0 -6
  201. package/dist/runtime/runtime-experimental.d.ts +0 -18
  202. package/dist/scripts/code-generator.d.ts +0 -64
  203. package/dist/scripts/generate-missing-commands.d.ts +0 -4
@@ -40,19 +40,26 @@ const COMMANDS = new Set([
40
40
  'append',
41
41
  'async',
42
42
  'beep',
43
+ 'blur',
43
44
  'break',
45
+ 'breakpoint',
44
46
  'call',
47
+ 'clear',
48
+ 'close',
45
49
  'continue',
46
50
  'copy',
47
51
  'decrement',
48
52
  'default',
53
+ 'empty',
49
54
  'exit',
50
55
  'fetch',
56
+ 'focus',
51
57
  'for',
52
58
  'get',
53
59
  'go',
54
60
  'halt',
55
61
  'hide',
62
+ 'open',
56
63
  'if',
57
64
  'increment',
58
65
  'install',
@@ -69,11 +76,15 @@ const COMMANDS = new Set([
69
76
  'render',
70
77
  'repeat',
71
78
  'replace',
79
+ 'reset',
72
80
  'return',
81
+ 'scroll',
82
+ 'select',
73
83
  'send',
74
84
  'set',
75
85
  'settle',
76
86
  'show',
87
+ 'start',
77
88
  'swap',
78
89
  'take',
79
90
  'tell',
@@ -99,6 +110,8 @@ const COMPOUND_COMMANDS = new Set([
99
110
  'measure',
100
111
  'js',
101
112
  'tell',
113
+ 'pick',
114
+ 'start',
102
115
  'swap',
103
116
  'morph',
104
117
  'push',
@@ -217,11 +230,20 @@ const COMPARISON_OPERATORS = new Set([
217
230
  '>=',
218
231
  'is',
219
232
  'is not',
233
+ 'am',
220
234
  'is a',
221
235
  'is an',
222
236
  'is not a',
223
237
  'is not an',
238
+ 'precedes',
239
+ 'does not precede',
240
+ 'follows',
241
+ 'does not follow',
224
242
  'contains',
243
+ 'starts with',
244
+ 'ends with',
245
+ 'does not start with',
246
+ 'does not end with',
225
247
  'has',
226
248
  'have',
227
249
  'does not contain',
@@ -236,6 +258,8 @@ const COMPARISON_OPERATORS = new Set([
236
258
  'is not empty',
237
259
  'is in',
238
260
  'is not in',
261
+ 'is between',
262
+ 'is not between',
239
263
  'equals',
240
264
  'in',
241
265
  'is equal to',
@@ -247,6 +271,11 @@ const COMPARISON_OPERATORS = new Set([
247
271
  'is greater than or equal to',
248
272
  'is less than or equal to',
249
273
  'really equals',
274
+ 'ignoring case',
275
+ 'sorted by',
276
+ 'mapped to',
277
+ 'split by',
278
+ 'joined by',
250
279
  ]);
251
280
  const DOM_EVENTS = new Set([
252
281
  'click',
@@ -1786,205 +1815,639 @@ SemanticIntegrationAdapter.SKIP_SEMANTIC_COMMANDS = new Set([
1786
1815
  'tell',
1787
1816
  ]);
1788
1817
 
1789
- function isCommand(token) {
1790
- if (token.kind !== TokenKind.IDENTIFIER)
1791
- return false;
1792
- return COMMANDS.has(token.value.toLowerCase());
1793
- }
1794
- function isKeyword$1(token) {
1795
- if (token.kind !== TokenKind.IDENTIFIER)
1796
- return false;
1797
- return TOKENIZER_KEYWORDS.has(token.value.toLowerCase());
1798
- }
1799
- function isEvent(token) {
1800
- if (token.kind !== TokenKind.IDENTIFIER)
1801
- return false;
1802
- return DOM_EVENTS.has(token.value.toLowerCase());
1803
- }
1804
- function isContextVar(token) {
1805
- if (token.kind !== TokenKind.IDENTIFIER)
1806
- return false;
1807
- return CONTEXT_VARS.has(token.value.toLowerCase());
1808
- }
1809
- function isComparisonOperator(token) {
1810
- if (token.kind === TokenKind.OPERATOR) {
1811
- return COMPARISON_OPERATORS.has(token.value.toLowerCase());
1812
- }
1813
- return COMPARISON_OPERATORS.has(token.value.toLowerCase());
1814
- }
1815
- function isIdentifierLike(token) {
1816
- return token.kind === TokenKind.IDENTIFIER;
1817
- }
1818
- function isSelector(token) {
1819
- return token.kind === TokenKind.SELECTOR;
1820
- }
1821
- function isBasicSelector(token) {
1822
- if (token.kind !== TokenKind.SELECTOR)
1823
- return false;
1824
- return !token.value.startsWith('<');
1825
- }
1826
- function isLiteral(token) {
1827
- return (token.kind === TokenKind.STRING ||
1828
- token.kind === TokenKind.NUMBER ||
1829
- token.kind === TokenKind.TEMPLATE);
1830
- }
1831
- function isReference(token) {
1832
- return token.kind === TokenKind.IDENTIFIER;
1833
- }
1834
- function isTimeExpression(token) {
1835
- return token.kind === TokenKind.TIME;
1836
- }
1837
- function isSymbol(token) {
1838
- return token.kind === TokenKind.SYMBOL;
1839
- }
1840
- function isComment(token) {
1841
- return token.kind === TokenKind.COMMENT;
1842
- }
1843
- function isIdentifier(token) {
1844
- if (token.kind !== TokenKind.IDENTIFIER)
1845
- return false;
1846
- const lowerValue = token.value.toLowerCase();
1847
- return (!COMMANDS.has(lowerValue) &&
1848
- !TOKENIZER_KEYWORDS.has(lowerValue) &&
1849
- !DOM_EVENTS.has(lowerValue) &&
1850
- !CONTEXT_VARS.has(lowerValue));
1851
- }
1852
- function isString(token) {
1853
- return token.kind === TokenKind.STRING;
1854
- }
1855
- function isNumber(token) {
1856
- return token.kind === TokenKind.NUMBER;
1857
- }
1858
- function isBoolean(token) {
1859
- if (token.kind === TokenKind.IDENTIFIER) {
1860
- const v = token.value;
1861
- return v === 'true' || v === 'false' || v === 'null' || v === 'undefined';
1818
+ const STOP_TOKENS = new Set([
1819
+ 'then',
1820
+ 'end',
1821
+ 'to',
1822
+ 'into',
1823
+ 'on',
1824
+ 'with',
1825
+ 'from',
1826
+ 'in',
1827
+ 'by',
1828
+ 'for',
1829
+ 'while',
1830
+ 'until',
1831
+ 'unless',
1832
+ 'else',
1833
+ 'catch',
1834
+ 'finally',
1835
+ ]);
1836
+ const STOP_DELIMITERS = new Set([')', ']', '}', ',']);
1837
+ function mergeFragments(...fragments) {
1838
+ const merged = new Map();
1839
+ for (const fragment of fragments) {
1840
+ for (const [key, entry] of fragment) {
1841
+ const existing = merged.get(key);
1842
+ if (existing) {
1843
+ merged.set(key, {
1844
+ prefix: entry.prefix ?? existing.prefix,
1845
+ infix: entry.infix ?? existing.infix,
1846
+ });
1847
+ }
1848
+ else {
1849
+ merged.set(key, { ...entry });
1850
+ }
1851
+ }
1862
1852
  }
1863
- return false;
1864
- }
1865
- function isTemplateLiteral(token) {
1866
- return token.kind === TokenKind.TEMPLATE;
1867
- }
1868
- function isQueryReference(token) {
1869
- return token.kind === TokenKind.SELECTOR && token.value.startsWith('<');
1870
- }
1871
- function isIdSelector(token) {
1872
- return token.kind === TokenKind.SELECTOR && token.value.startsWith('#');
1873
- }
1874
- function isClassSelector(token) {
1875
- return token.kind === TokenKind.SELECTOR && token.value.startsWith('.');
1876
- }
1877
- function isCssSelector(token) {
1878
- if (token.kind !== TokenKind.SELECTOR)
1879
- return false;
1880
- return !token.value.startsWith('#') && !token.value.startsWith('.');
1881
- }
1882
- function isBasicOperator(token) {
1883
- if (token.kind !== TokenKind.OPERATOR)
1884
- return false;
1885
- const lowerValue = token.value.toLowerCase();
1886
- return !LOGICAL_OPERATORS.has(lowerValue) && !COMPARISON_OPERATORS.has(lowerValue);
1887
- }
1888
- function isCommandTerminator(token) {
1889
- const value = token.value.toLowerCase();
1890
- return (value === 'then' || value === 'and' || value === 'else' || value === 'end' || value === 'on');
1891
- }
1892
-
1893
- function createLiteral(value, raw, pos) {
1894
- return {
1895
- type: 'literal',
1896
- value,
1897
- raw,
1898
- start: pos.start,
1899
- end: pos.end,
1900
- line: pos.line,
1901
- column: pos.column,
1902
- };
1903
- }
1904
- function createIdentifier(name, pos) {
1905
- return {
1906
- type: 'identifier',
1907
- name,
1908
- start: pos.start,
1909
- end: pos.end,
1910
- line: pos.line,
1911
- column: pos.column,
1912
- };
1913
- }
1914
- function createBinaryExpression(operator, left, right, pos) {
1915
- return {
1916
- type: 'binaryExpression',
1917
- operator,
1918
- left,
1919
- right,
1920
- start: pos.start,
1921
- end: pos.end,
1922
- line: pos.line,
1923
- column: pos.column,
1924
- };
1925
- }
1926
- function createUnaryExpression(operator, argument, prefix, pos) {
1927
- return {
1928
- type: 'unaryExpression',
1929
- operator,
1930
- argument,
1931
- prefix,
1932
- start: pos.start,
1933
- end: pos.end,
1934
- line: pos.line,
1935
- column: pos.column,
1936
- };
1937
- }
1938
- function createCallExpression(callee, args, pos) {
1939
- return {
1940
- type: 'callExpression',
1941
- callee,
1942
- arguments: args,
1943
- start: pos.start,
1944
- end: pos.end,
1945
- line: pos.line,
1946
- column: pos.column,
1947
- };
1853
+ return merged;
1948
1854
  }
1949
- function createMemberExpression(object, property, computed, pos) {
1855
+ function leftAssoc(bp, handler) {
1950
1856
  return {
1951
- type: 'memberExpression',
1952
- object,
1953
- property,
1954
- computed,
1955
- start: pos.start,
1956
- end: pos.end,
1957
- line: pos.line,
1958
- column: pos.column,
1857
+ infix: {
1858
+ bp: [bp, bp + 1],
1859
+ handler: handler ??
1860
+ ((left, token, ctx) => {
1861
+ const right = ctx.parseExpr(bp + 1);
1862
+ return {
1863
+ type: 'binaryExpression',
1864
+ operator: token.value,
1865
+ left,
1866
+ right,
1867
+ start: left.start,
1868
+ end: right.end ?? token.end,
1869
+ line: left.line ?? token.line,
1870
+ column: left.column ?? token.column,
1871
+ };
1872
+ }),
1873
+ },
1959
1874
  };
1960
1875
  }
1961
- function createSelector(value, pos) {
1876
+ function rightAssoc(bp, handler) {
1962
1877
  return {
1963
- type: 'selector',
1964
- value,
1965
- start: pos.start,
1966
- end: pos.end,
1967
- line: pos.line,
1968
- column: pos.column,
1878
+ infix: {
1879
+ bp: [bp + 1, bp],
1880
+ handler: ((left, token, ctx) => {
1881
+ const right = ctx.parseExpr(bp);
1882
+ return {
1883
+ type: 'binaryExpression',
1884
+ operator: token.value,
1885
+ left,
1886
+ right,
1887
+ start: left.start,
1888
+ end: right.end ?? token.end,
1889
+ line: left.line ?? token.line,
1890
+ column: left.column ?? token.column,
1891
+ };
1892
+ }),
1893
+ },
1969
1894
  };
1970
1895
  }
1971
- function createPossessiveExpression(object, property, pos) {
1896
+ function prefix(bp, handler) {
1972
1897
  return {
1973
- type: 'possessiveExpression',
1974
- object,
1975
- property,
1976
- start: pos.start,
1977
- end: pos.end,
1978
- line: pos.line,
1979
- column: pos.column,
1898
+ prefix: {
1899
+ bp,
1900
+ handler: handler ??
1901
+ ((token, ctx) => {
1902
+ const operand = ctx.parseExpr(bp);
1903
+ return {
1904
+ type: 'unaryExpression',
1905
+ operator: token.value,
1906
+ operand,
1907
+ argument: operand,
1908
+ prefix: true,
1909
+ start: token.start,
1910
+ end: operand.end ?? token.end,
1911
+ line: token.line,
1912
+ column: token.column,
1913
+ };
1914
+ }),
1915
+ },
1980
1916
  };
1981
1917
  }
1982
- function createBlock(commands, pos) {
1983
- return {
1984
- type: 'block',
1985
- commands,
1986
- start: pos.start,
1987
- end: pos.end,
1918
+ const CORE_FRAGMENT = new Map([
1919
+ ['or', leftAssoc(10)],
1920
+ ['||', leftAssoc(10)],
1921
+ ['and', leftAssoc(20)],
1922
+ ['&&', leftAssoc(20)],
1923
+ ['==', leftAssoc(30)],
1924
+ ['!=', leftAssoc(30)],
1925
+ ['<', leftAssoc(30)],
1926
+ ['>', leftAssoc(30)],
1927
+ ['<=', leftAssoc(30)],
1928
+ ['>=', leftAssoc(30)],
1929
+ ['is', leftAssoc(30)],
1930
+ ['matches', leftAssoc(30)],
1931
+ ['contains', leftAssoc(30)],
1932
+ ['starts with', leftAssoc(30)],
1933
+ ['ends with', leftAssoc(30)],
1934
+ ['does not start with', leftAssoc(30)],
1935
+ ['does not end with', leftAssoc(30)],
1936
+ [
1937
+ 'is between',
1938
+ leftAssoc(30, (left, _token, ctx) => {
1939
+ const min = ctx.parseExpr(31);
1940
+ const next = ctx.peek();
1941
+ if (!next || next.value.toLowerCase() !== 'and') {
1942
+ throw new Error(`between requires 'and' between min and max operands, got: ${next?.value ?? '<end>'}`);
1943
+ }
1944
+ ctx.advance();
1945
+ const max = ctx.parseExpr(31);
1946
+ return {
1947
+ type: 'betweenExpression',
1948
+ value: left,
1949
+ min,
1950
+ max,
1951
+ negated: false,
1952
+ start: left.start,
1953
+ end: max.end,
1954
+ };
1955
+ }),
1956
+ ],
1957
+ [
1958
+ 'is not between',
1959
+ leftAssoc(30, (left, _token, ctx) => {
1960
+ const min = ctx.parseExpr(31);
1961
+ const next = ctx.peek();
1962
+ if (!next || next.value.toLowerCase() !== 'and') {
1963
+ throw new Error(`between requires 'and' between min and max operands, got: ${next?.value ?? '<end>'}`);
1964
+ }
1965
+ ctx.advance();
1966
+ const max = ctx.parseExpr(31);
1967
+ return {
1968
+ type: 'betweenExpression',
1969
+ value: left,
1970
+ min,
1971
+ max,
1972
+ negated: true,
1973
+ start: left.start,
1974
+ end: max.end,
1975
+ };
1976
+ }),
1977
+ ],
1978
+ [
1979
+ 'ignoring case',
1980
+ {
1981
+ infix: {
1982
+ bp: [25, 26],
1983
+ handler: (left, _token, _ctx) => {
1984
+ left.ignoringCase = true;
1985
+ return left;
1986
+ },
1987
+ },
1988
+ },
1989
+ ],
1990
+ [
1991
+ 'where',
1992
+ leftAssoc(28, (left, _token, ctx) => {
1993
+ const predicate = ctx.parseExpr(29);
1994
+ return {
1995
+ type: 'collectionExpression',
1996
+ operator: 'where',
1997
+ collection: left,
1998
+ right: predicate,
1999
+ start: left.start,
2000
+ end: predicate.end,
2001
+ };
2002
+ }),
2003
+ ],
2004
+ [
2005
+ 'sorted by',
2006
+ leftAssoc(28, (left, _token, ctx) => {
2007
+ const keyExpr = ctx.parseExpr(29);
2008
+ let order = 'asc';
2009
+ const next = ctx.peek();
2010
+ if (next && typeof next.value === 'string') {
2011
+ const v = next.value.toLowerCase();
2012
+ if (v === 'asc' || v === 'ascending') {
2013
+ ctx.advance();
2014
+ order = 'asc';
2015
+ }
2016
+ else if (v === 'desc' || v === 'descending') {
2017
+ ctx.advance();
2018
+ order = 'desc';
2019
+ }
2020
+ }
2021
+ return {
2022
+ type: 'collectionExpression',
2023
+ operator: 'sorted by',
2024
+ collection: left,
2025
+ right: keyExpr,
2026
+ order,
2027
+ start: left.start,
2028
+ end: keyExpr.end,
2029
+ };
2030
+ }),
2031
+ ],
2032
+ [
2033
+ 'mapped to',
2034
+ leftAssoc(28, (left, _token, ctx) => {
2035
+ const expr = ctx.parseExpr(29);
2036
+ return {
2037
+ type: 'collectionExpression',
2038
+ operator: 'mapped to',
2039
+ collection: left,
2040
+ right: expr,
2041
+ start: left.start,
2042
+ end: expr.end,
2043
+ };
2044
+ }),
2045
+ ],
2046
+ [
2047
+ 'split by',
2048
+ leftAssoc(28, (left, _token, ctx) => {
2049
+ const sep = ctx.parseExpr(29);
2050
+ return {
2051
+ type: 'collectionExpression',
2052
+ operator: 'split by',
2053
+ collection: left,
2054
+ right: sep,
2055
+ start: left.start,
2056
+ end: sep.end,
2057
+ };
2058
+ }),
2059
+ ],
2060
+ [
2061
+ 'joined by',
2062
+ leftAssoc(28, (left, _token, ctx) => {
2063
+ const sep = ctx.parseExpr(29);
2064
+ return {
2065
+ type: 'collectionExpression',
2066
+ operator: 'joined by',
2067
+ collection: left,
2068
+ right: sep,
2069
+ start: left.start,
2070
+ end: sep.end,
2071
+ };
2072
+ }),
2073
+ ],
2074
+ ['+', { ...leftAssoc(40), ...prefix(80) }],
2075
+ ['-', { ...leftAssoc(40), ...prefix(80) }],
2076
+ ['*', leftAssoc(50)],
2077
+ ['/', leftAssoc(50)],
2078
+ ['%', leftAssoc(50)],
2079
+ ['mod', leftAssoc(50)],
2080
+ ['^', rightAssoc(60)],
2081
+ ['**', rightAssoc(60)],
2082
+ [
2083
+ 'as',
2084
+ leftAssoc(70, (left, token, ctx) => {
2085
+ const targetType = ctx.parseExpr(71);
2086
+ const peeked = ctx.peek();
2087
+ if (peeked &&
2088
+ peeked.value === ':' &&
2089
+ targetType &&
2090
+ targetType.type === 'identifier') {
2091
+ ctx.advance();
2092
+ const suffix = ctx.advance();
2093
+ if (suffix && suffix.value !== undefined) {
2094
+ targetType.name = `${targetType.name}:${suffix.value}`;
2095
+ targetType.end = suffix.end;
2096
+ }
2097
+ }
2098
+ return {
2099
+ type: 'asExpression',
2100
+ expression: left,
2101
+ targetType,
2102
+ start: left.start,
2103
+ end: targetType.end ?? token.end,
2104
+ line: left.line ?? token.line,
2105
+ column: left.column ?? token.column,
2106
+ };
2107
+ }),
2108
+ ],
2109
+ ['not', prefix(80)],
2110
+ ['!', prefix(80)],
2111
+ ['no', prefix(80)],
2112
+ ]);
2113
+ function makeTypeCheckHandler(negated) {
2114
+ return (left, _token, ctx) => {
2115
+ const typeToken = ctx.advance();
2116
+ if (!typeToken || !typeToken.value) {
2117
+ throw new Error(`Type check requires a type name after 'is ${negated ? 'not ' : ''}a/an', got: <end>`);
2118
+ }
2119
+ let nullOk = true;
2120
+ const peeked = ctx.peek();
2121
+ if (peeked && peeked.value === '!') {
2122
+ ctx.advance();
2123
+ nullOk = false;
2124
+ }
2125
+ return {
2126
+ type: 'typeCheckExpression',
2127
+ value: left,
2128
+ typeName: typeToken.value,
2129
+ nullOk,
2130
+ negated,
2131
+ start: left.start,
2132
+ end: typeToken.end,
2133
+ };
2134
+ };
2135
+ }
2136
+ const PARSER_COMPARISON_FRAGMENT = new Map([
2137
+ ['===', leftAssoc(30)],
2138
+ ['!==', leftAssoc(30)],
2139
+ ['is not', leftAssoc(30)],
2140
+ ['am', leftAssoc(30)],
2141
+ ['is in', leftAssoc(30)],
2142
+ ['is not in', leftAssoc(30)],
2143
+ ['precedes', leftAssoc(30)],
2144
+ ['does not precede', leftAssoc(30)],
2145
+ ['follows', leftAssoc(30)],
2146
+ ['does not follow', leftAssoc(30)],
2147
+ ['is a', leftAssoc(30, makeTypeCheckHandler(false))],
2148
+ ['is an', leftAssoc(30, makeTypeCheckHandler(false))],
2149
+ ['is not a', leftAssoc(30, makeTypeCheckHandler(true))],
2150
+ ['is not an', leftAssoc(30, makeTypeCheckHandler(true))],
2151
+ ['has', leftAssoc(30)],
2152
+ ['have', leftAssoc(30)],
2153
+ ['match', leftAssoc(30)],
2154
+ ['include', leftAssoc(30)],
2155
+ ['includes', leftAssoc(30)],
2156
+ ['equals', leftAssoc(30)],
2157
+ ['does not contain', leftAssoc(30)],
2158
+ ['does not include', leftAssoc(30)],
2159
+ ['is equal to', leftAssoc(30)],
2160
+ ['is not equal to', leftAssoc(30)],
2161
+ ['is really equal to', leftAssoc(30)],
2162
+ ['is not really equal to', leftAssoc(30)],
2163
+ ['really equals', leftAssoc(30)],
2164
+ ['is greater than', leftAssoc(30)],
2165
+ ['is less than', leftAssoc(30)],
2166
+ ['is greater than or equal to', leftAssoc(30)],
2167
+ ['is less than or equal to', leftAssoc(30)],
2168
+ ['in', leftAssoc(30)],
2169
+ ['of', leftAssoc(30)],
2170
+ ['really', leftAssoc(30)],
2171
+ [
2172
+ 'exists',
2173
+ {
2174
+ prefix: {
2175
+ bp: 80,
2176
+ handler: (token, ctx) => ({
2177
+ type: 'unaryExpression',
2178
+ operator: token.value,
2179
+ operand: ctx.parseExpr(80),
2180
+ start: token.start,
2181
+ }),
2182
+ },
2183
+ infix: {
2184
+ bp: [30, 31],
2185
+ handler: (left, token) => ({
2186
+ type: 'unaryExpression',
2187
+ operator: token.value,
2188
+ operand: left,
2189
+ prefix: false,
2190
+ start: left.start,
2191
+ }),
2192
+ },
2193
+ },
2194
+ ],
2195
+ [
2196
+ 'does not exist',
2197
+ {
2198
+ infix: {
2199
+ bp: [30, 31],
2200
+ handler: (left, token) => ({
2201
+ type: 'unaryExpression',
2202
+ operator: token.value,
2203
+ operand: left,
2204
+ prefix: false,
2205
+ start: left.start,
2206
+ }),
2207
+ },
2208
+ },
2209
+ ],
2210
+ [
2211
+ 'is empty',
2212
+ {
2213
+ infix: {
2214
+ bp: [30, 31],
2215
+ handler: (left, token) => ({
2216
+ type: 'unaryExpression',
2217
+ operator: token.value,
2218
+ operand: left,
2219
+ prefix: false,
2220
+ start: left.start,
2221
+ }),
2222
+ },
2223
+ },
2224
+ ],
2225
+ [
2226
+ 'is not empty',
2227
+ {
2228
+ infix: {
2229
+ bp: [30, 31],
2230
+ handler: (left, token) => ({
2231
+ type: 'unaryExpression',
2232
+ operator: token.value,
2233
+ operand: left,
2234
+ prefix: false,
2235
+ start: left.start,
2236
+ }),
2237
+ },
2238
+ },
2239
+ ],
2240
+ ['some', prefix(80)],
2241
+ ]);
2242
+ const ASSIGNMENT_FRAGMENT = new Map([
2243
+ ['=', rightAssoc(5)],
2244
+ ]);
2245
+ const PARSER_TABLE = mergeFragments(CORE_FRAGMENT, PARSER_COMPARISON_FRAGMENT, ASSIGNMENT_FRAGMENT);
2246
+
2247
+ const FEATURE_REGISTRY = new Map();
2248
+ function getRegisteredFeature(name) {
2249
+ return FEATURE_REGISTRY.get(name.toLowerCase());
2250
+ }
2251
+
2252
+ function isCommand(token) {
2253
+ if (token.kind !== TokenKind.IDENTIFIER)
2254
+ return false;
2255
+ return COMMANDS.has(token.value.toLowerCase());
2256
+ }
2257
+ function isKeyword$1(token) {
2258
+ if (token.kind !== TokenKind.IDENTIFIER)
2259
+ return false;
2260
+ return TOKENIZER_KEYWORDS.has(token.value.toLowerCase());
2261
+ }
2262
+ function isEvent(token) {
2263
+ if (token.kind !== TokenKind.IDENTIFIER)
2264
+ return false;
2265
+ return DOM_EVENTS.has(token.value.toLowerCase());
2266
+ }
2267
+ function isContextVar(token) {
2268
+ if (token.kind !== TokenKind.IDENTIFIER)
2269
+ return false;
2270
+ return CONTEXT_VARS.has(token.value.toLowerCase());
2271
+ }
2272
+ function isComparisonOperator(token) {
2273
+ if (token.kind === TokenKind.OPERATOR) {
2274
+ return COMPARISON_OPERATORS.has(token.value.toLowerCase());
2275
+ }
2276
+ return COMPARISON_OPERATORS.has(token.value.toLowerCase());
2277
+ }
2278
+ function isIdentifierLike(token) {
2279
+ return token.kind === TokenKind.IDENTIFIER;
2280
+ }
2281
+ function isSelector(token) {
2282
+ return token.kind === TokenKind.SELECTOR;
2283
+ }
2284
+ function isBasicSelector(token) {
2285
+ if (token.kind !== TokenKind.SELECTOR)
2286
+ return false;
2287
+ return !token.value.startsWith('<');
2288
+ }
2289
+ function isLiteral(token) {
2290
+ return (token.kind === TokenKind.STRING ||
2291
+ token.kind === TokenKind.NUMBER ||
2292
+ token.kind === TokenKind.TEMPLATE);
2293
+ }
2294
+ function isReference(token) {
2295
+ return token.kind === TokenKind.IDENTIFIER;
2296
+ }
2297
+ function isTimeExpression(token) {
2298
+ return token.kind === TokenKind.TIME;
2299
+ }
2300
+ function isSymbol(token) {
2301
+ return token.kind === TokenKind.SYMBOL;
2302
+ }
2303
+ function isComment(token) {
2304
+ return token.kind === TokenKind.COMMENT;
2305
+ }
2306
+ function isIdentifier(token) {
2307
+ if (token.kind !== TokenKind.IDENTIFIER)
2308
+ return false;
2309
+ const lowerValue = token.value.toLowerCase();
2310
+ return (!COMMANDS.has(lowerValue) &&
2311
+ !TOKENIZER_KEYWORDS.has(lowerValue) &&
2312
+ !DOM_EVENTS.has(lowerValue) &&
2313
+ !CONTEXT_VARS.has(lowerValue));
2314
+ }
2315
+ function isString(token) {
2316
+ return token.kind === TokenKind.STRING;
2317
+ }
2318
+ function isNumber(token) {
2319
+ return token.kind === TokenKind.NUMBER;
2320
+ }
2321
+ function isBoolean(token) {
2322
+ if (token.kind === TokenKind.IDENTIFIER) {
2323
+ const v = token.value;
2324
+ return v === 'true' || v === 'false' || v === 'null' || v === 'undefined';
2325
+ }
2326
+ return false;
2327
+ }
2328
+ function isTemplateLiteral(token) {
2329
+ return token.kind === TokenKind.TEMPLATE;
2330
+ }
2331
+ function isQueryReference(token) {
2332
+ return token.kind === TokenKind.SELECTOR && token.value.startsWith('<');
2333
+ }
2334
+ function isIdSelector(token) {
2335
+ return token.kind === TokenKind.SELECTOR && token.value.startsWith('#');
2336
+ }
2337
+ function isClassSelector(token) {
2338
+ return token.kind === TokenKind.SELECTOR && token.value.startsWith('.');
2339
+ }
2340
+ function isCssSelector(token) {
2341
+ if (token.kind !== TokenKind.SELECTOR)
2342
+ return false;
2343
+ return !token.value.startsWith('#') && !token.value.startsWith('.');
2344
+ }
2345
+ function isBasicOperator(token) {
2346
+ if (token.kind !== TokenKind.OPERATOR)
2347
+ return false;
2348
+ const lowerValue = token.value.toLowerCase();
2349
+ return !LOGICAL_OPERATORS.has(lowerValue) && !COMPARISON_OPERATORS.has(lowerValue);
2350
+ }
2351
+ function isCommandTerminator(token) {
2352
+ const value = token.value.toLowerCase();
2353
+ return (value === 'then' || value === 'and' || value === 'else' || value === 'end' || value === 'on');
2354
+ }
2355
+
2356
+ function createLiteral(value, raw, pos) {
2357
+ return {
2358
+ type: 'literal',
2359
+ value,
2360
+ raw,
2361
+ start: pos.start,
2362
+ end: pos.end,
2363
+ line: pos.line,
2364
+ column: pos.column,
2365
+ };
2366
+ }
2367
+ function createIdentifier(name, pos) {
2368
+ return {
2369
+ type: 'identifier',
2370
+ name,
2371
+ start: pos.start,
2372
+ end: pos.end,
2373
+ line: pos.line,
2374
+ column: pos.column,
2375
+ };
2376
+ }
2377
+ function createBinaryExpression(operator, left, right, pos) {
2378
+ return {
2379
+ type: 'binaryExpression',
2380
+ operator,
2381
+ left,
2382
+ right,
2383
+ start: pos.start,
2384
+ end: pos.end,
2385
+ line: pos.line,
2386
+ column: pos.column,
2387
+ };
2388
+ }
2389
+ function createUnaryExpression(operator, argument, prefix, pos) {
2390
+ return {
2391
+ type: 'unaryExpression',
2392
+ operator,
2393
+ argument,
2394
+ prefix,
2395
+ start: pos.start,
2396
+ end: pos.end,
2397
+ line: pos.line,
2398
+ column: pos.column,
2399
+ };
2400
+ }
2401
+ function createCallExpression(callee, args, pos) {
2402
+ return {
2403
+ type: 'callExpression',
2404
+ callee,
2405
+ arguments: args,
2406
+ start: pos.start,
2407
+ end: pos.end,
2408
+ line: pos.line,
2409
+ column: pos.column,
2410
+ };
2411
+ }
2412
+ function createMemberExpression(object, property, computed, pos) {
2413
+ return {
2414
+ type: 'memberExpression',
2415
+ object,
2416
+ property,
2417
+ computed,
2418
+ start: pos.start,
2419
+ end: pos.end,
2420
+ line: pos.line,
2421
+ column: pos.column,
2422
+ };
2423
+ }
2424
+ function createSelector(value, pos) {
2425
+ return {
2426
+ type: 'selector',
2427
+ value,
2428
+ start: pos.start,
2429
+ end: pos.end,
2430
+ line: pos.line,
2431
+ column: pos.column,
2432
+ };
2433
+ }
2434
+ function createPossessiveExpression(object, property, pos) {
2435
+ return {
2436
+ type: 'possessiveExpression',
2437
+ object,
2438
+ property,
2439
+ start: pos.start,
2440
+ end: pos.end,
2441
+ line: pos.line,
2442
+ column: pos.column,
2443
+ };
2444
+ }
2445
+ function createBlock(commands, pos) {
2446
+ return {
2447
+ type: 'block',
2448
+ commands,
2449
+ start: pos.start,
2450
+ end: pos.end,
1988
2451
  line: pos.line,
1989
2452
  column: pos.column,
1990
2453
  };
@@ -2070,6 +2533,18 @@ function createErrorCommandNode(pos, message, source) {
2070
2533
  column: pos.column,
2071
2534
  };
2072
2535
  }
2536
+ function createPartialCommandNode(name, pos) {
2537
+ return {
2538
+ type: 'command',
2539
+ name,
2540
+ args: [],
2541
+ partial: true,
2542
+ start: pos.start,
2543
+ end: pos.end,
2544
+ line: pos.line,
2545
+ column: pos.column,
2546
+ };
2547
+ }
2073
2548
  function createProgramNode(statements) {
2074
2549
  debug.parse(`✅ createProgramNode: Called with ${statements.length} statements`);
2075
2550
  if (statements.length === 0) {
@@ -2182,6 +2657,23 @@ function consumeOptionalKeyword(ctx, keyword) {
2182
2657
  }
2183
2658
  return false;
2184
2659
  }
2660
+ function parseMaybeNamedArgument(ctx) {
2661
+ const checkpoint = ctx.savePosition();
2662
+ let name;
2663
+ if (ctx.checkIdentifierLike()) {
2664
+ const possible = ctx.peek().value;
2665
+ ctx.advance();
2666
+ if (ctx.check(':')) {
2667
+ ctx.advance();
2668
+ name = possible;
2669
+ }
2670
+ else {
2671
+ ctx.restorePosition(checkpoint);
2672
+ }
2673
+ }
2674
+ const value = ctx.parseExpression();
2675
+ return name !== undefined ? { name, value } : { value };
2676
+ }
2185
2677
 
2186
2678
  class CommandNodeBuilder {
2187
2679
  constructor(name) {
@@ -2329,26 +2821,13 @@ function parseTriggerCommand(ctx, identifierNode) {
2329
2821
  ctx.advance();
2330
2822
  const detailArgs = [];
2331
2823
  while (!ctx.isAtEnd() && !ctx.check(')')) {
2332
- const checkpoint = ctx.savePosition();
2333
- let paramName;
2334
- if (ctx.checkIdentifierLike()) {
2335
- const possibleName = ctx.peek().value;
2336
- ctx.advance();
2337
- if (ctx.check(':')) {
2338
- ctx.advance();
2339
- paramName = possibleName;
2340
- }
2341
- else {
2342
- ctx.restorePosition(checkpoint);
2343
- }
2344
- }
2345
- const value = ctx.parseExpression();
2346
- if (paramName !== undefined) {
2824
+ const { name, value } = parseMaybeNamedArgument(ctx);
2825
+ if (name !== undefined) {
2347
2826
  detailArgs.push({
2348
2827
  type: 'objectLiteral',
2349
2828
  properties: [
2350
2829
  {
2351
- key: { type: 'identifier', name: paramName },
2830
+ key: { type: 'identifier', name },
2352
2831
  value: value,
2353
2832
  },
2354
2833
  ],
@@ -2393,12 +2872,13 @@ function parseTriggerCommand(ctx, identifierNode) {
2393
2872
  }
2394
2873
  }
2395
2874
  const finalArgs = [...allArgs];
2396
- if (ctx.check('on') || ctx.check('to')) {
2397
- const keyword = ctx.advance().value;
2398
- finalArgs.push(ctx.createIdentifier(keyword));
2399
- while (!isCommandBoundary(ctx)) {
2400
- finalArgs.push(ctx.parsePrimary());
2875
+ while (!isCommandBoundary(ctx)) {
2876
+ if (ctx.check('on') || ctx.check('to')) {
2877
+ const keyword = ctx.advance().value;
2878
+ finalArgs.push(ctx.createIdentifier(keyword));
2879
+ continue;
2401
2880
  }
2881
+ finalArgs.push(ctx.parsePrimary());
2402
2882
  }
2403
2883
  return CommandNodeBuilder.fromIdentifier(identifierNode)
2404
2884
  .withArgs(...finalArgs)
@@ -2543,7 +3023,35 @@ function parseRepeatCommand(ctx, commandToken) {
2543
3023
  indexVariable = KEYWORDS.INDEX;
2544
3024
  }
2545
3025
  }
2546
- const commands = ctx.parseCommandListUntilEnd();
3026
+ let commands;
3027
+ let elseCommands = null;
3028
+ let bottomTested = false;
3029
+ if (loopType === KEYWORDS.FOREVER) {
3030
+ const result = ctx.parseRepeatBody();
3031
+ commands = result.commands;
3032
+ if (result.terminator === 'else') {
3033
+ ctx.advance();
3034
+ elseCommands = ctx.parseCommandListUntilEnd();
3035
+ }
3036
+ else if (result.terminator === 'until' || result.terminator === 'while') {
3037
+ bottomTested = true;
3038
+ loopType = result.terminator;
3039
+ ctx.advance();
3040
+ condition = ctx.parseExpression();
3041
+ if (!ctx.check('end')) {
3042
+ throw new Error('Expected "end" to close repeat block');
3043
+ }
3044
+ ctx.advance();
3045
+ }
3046
+ }
3047
+ else {
3048
+ const result = ctx.parseCommandListUntilEndOrElse();
3049
+ commands = result.commands;
3050
+ if (result.hasElse) {
3051
+ ctx.advance();
3052
+ elseCommands = ctx.parseCommandListUntilEnd();
3053
+ }
3054
+ }
2547
3055
  args.push({
2548
3056
  type: 'identifier',
2549
3057
  name: loopType,
@@ -2575,11 +3083,19 @@ function parseRepeatCommand(ctx, commandToken) {
2575
3083
  if (indexVariable) {
2576
3084
  args.push(createStringLiteral(indexVariable, pos));
2577
3085
  }
2578
- args.push(createBlock(commands, { ...pos, end: pos.end || 0 }));
2579
- return CommandNodeBuilder.from(commandToken)
2580
- .withArgs(...args)
2581
- .endingAt(ctx.getPosition())
2582
- .build();
3086
+ args.push(createBlock(commands, { ...pos, end: pos.end || 0 }));
3087
+ if (elseCommands !== null) {
3088
+ args.push(createBlock(elseCommands, { ...pos, end: pos.end || 0 }));
3089
+ }
3090
+ const builder = CommandNodeBuilder.from(commandToken).withArgs(...args);
3091
+ if (bottomTested) {
3092
+ builder.withModifier('bottomTested', {
3093
+ type: 'literal',
3094
+ value: true,
3095
+ ...pos,
3096
+ });
3097
+ }
3098
+ return builder.endingAt(ctx.getPosition()).build();
2583
3099
  }
2584
3100
  function parseIfCommand(ctx, commandToken) {
2585
3101
  const args = [];
@@ -2930,6 +3446,25 @@ function parseTransitionCommand(ctx, commandToken) {
2930
3446
  .endingAt(ctx.getPosition())
2931
3447
  .build();
2932
3448
  }
3449
+ function parseStartCommand(ctx, identifierNode) {
3450
+ if (!ctx.match('view')) {
3451
+ throw new Error("start: expected 'view transition' (only `start view transition ... end` is supported)");
3452
+ }
3453
+ if (!ctx.match('transition')) {
3454
+ throw new Error("start view: expected 'transition'");
3455
+ }
3456
+ const modifiers = {};
3457
+ if (ctx.match('using')) {
3458
+ const nameExpr = ctx.parsePrimary();
3459
+ modifiers.transitionName = nameExpr;
3460
+ }
3461
+ const body = ctx.parseCommandListUntilEnd();
3462
+ return CommandNodeBuilder.fromIdentifier(identifierNode)
3463
+ .withArgs(...body)
3464
+ .withModifiers(modifiers)
3465
+ .endingAt(ctx.getPosition())
3466
+ .build();
3467
+ }
2933
3468
 
2934
3469
  function parseRemoveCommand(ctx, identifierNode) {
2935
3470
  const args = [];
@@ -3071,7 +3606,6 @@ const SWAP_STRATEGY_KEYWORDS = [
3071
3606
  'morphouter',
3072
3607
  ];
3073
3608
  function parseSwapCommand(ctx, identifierNode) {
3074
- console.log('[PARSER DEBUG] parseSwapCommand called');
3075
3609
  const args = [];
3076
3610
  let strategyKeyword = null;
3077
3611
  if (!ctx.isAtEnd()) {
@@ -3222,21 +3756,7 @@ function parseInstallCommand(ctx, commandToken) {
3222
3756
  ctx.advance();
3223
3757
  const params = [];
3224
3758
  while (!ctx.isAtEnd() && !ctx.check(')')) {
3225
- const checkpoint = ctx.savePosition();
3226
- let paramName;
3227
- if (ctx.checkIdentifierLike()) {
3228
- const possibleName = ctx.peek().value;
3229
- ctx.advance();
3230
- if (ctx.check(':')) {
3231
- ctx.advance();
3232
- paramName = possibleName;
3233
- }
3234
- else {
3235
- ctx.restorePosition(checkpoint);
3236
- }
3237
- }
3238
- const value = ctx.parseExpression();
3239
- params.push(paramName !== undefined ? { name: paramName, value } : { value });
3759
+ params.push(parseMaybeNamedArgument(ctx));
3240
3760
  if (ctx.check(',')) {
3241
3761
  ctx.advance();
3242
3762
  }
@@ -3514,6 +4034,10 @@ function parseCompoundCommand(ctx, identifierNode) {
3514
4034
  return parseJsCommand(ctx, identifierNode);
3515
4035
  case 'tell':
3516
4036
  return parseTellCommand(ctx, identifierNode);
4037
+ case 'pick':
4038
+ return parsePickCommand(ctx, identifierNode);
4039
+ case 'start':
4040
+ return parseStartCommand(ctx, identifierNode);
3517
4041
  case 'swap':
3518
4042
  case 'morph':
3519
4043
  return parseSwapCommand(ctx, identifierNode);
@@ -3621,7 +4145,7 @@ function parseFetchCommand(ctx, commandToken) {
3621
4145
  if (!ctx.isAtEnd() && ctx.check('{')) {
3622
4146
  modifiers['with'] = ctx.parsePrimary();
3623
4147
  }
3624
- for (let i = 0; i < 2 && !ctx.isAtEnd(); i++) {
4148
+ for (let i = 0; i < 3 && !ctx.isAtEnd(); i++) {
3625
4149
  if (ctx.check('as') && !modifiers['as']) {
3626
4150
  ctx.advance();
3627
4151
  if (!ctx.isAtEnd()) {
@@ -3640,491 +4164,335 @@ function parseFetchCommand(ctx, commandToken) {
3640
4164
  }
3641
4165
  continue;
3642
4166
  }
3643
- break;
3644
- }
3645
- const builder = CommandNodeBuilder.from(commandToken).withArgs(url).endingAt(ctx.getPosition());
3646
- if (Object.keys(modifiers).length > 0) {
3647
- builder.withModifiers(modifiers);
3648
- }
3649
- return builder.build();
3650
- }
3651
- function isFetchNakedNamedArgStart(ctx) {
3652
- if (ctx.check('{'))
3653
- return false;
3654
- if (!ctx.checkIdentifierLike())
3655
- return false;
3656
- const next = ctx.peekAt(1);
3657
- return next !== null && next.value === ':';
3658
- }
3659
- function parseFetchNakedNamedArgs(ctx) {
3660
- const properties = [];
3661
- const startPos = ctx.getPosition();
3662
- do {
3663
- if (!ctx.checkIdentifierLike())
3664
- break;
3665
- const keyToken = ctx.advance();
3666
- const key = {
3667
- type: 'identifier',
3668
- name: keyToken.value,
3669
- start: keyToken.start,
3670
- end: keyToken.end,
3671
- line: keyToken.line,
3672
- column: keyToken.column,
3673
- };
3674
- ctx.consume(':', "Expected ':' after property name in fetch named arguments");
3675
- const value = ctx.parsePrimary();
3676
- if (value) {
3677
- properties.push({ key, value });
3678
- }
3679
- } while (ctx.match(',') && !ctx.isAtEnd());
3680
- const endPos = ctx.getPosition();
3681
- return {
3682
- type: 'objectLiteral',
3683
- properties,
3684
- start: startPos.start,
3685
- end: endPos.end,
3686
- line: startPos.line,
3687
- column: startPos.column,
3688
- };
3689
- }
3690
- function findJsEndBoundary(ctx, startPos) {
3691
- const input = ctx.getInputSlice(startPos);
3692
- if (!input) {
3693
- return startPos;
3694
- }
3695
- let i = 0;
3696
- while (i < input.length) {
3697
- const ch = input[i];
3698
- if (ch === "'" || ch === '\u2019' || ch === '\u2018') {
3699
- i++;
3700
- while (i < input.length && input[i] !== ch) {
3701
- if (input[i] === '\\')
3702
- i++;
3703
- i++;
3704
- }
3705
- i++;
3706
- continue;
3707
- }
3708
- if (ch === '"') {
3709
- i++;
3710
- while (i < input.length && input[i] !== '"') {
3711
- if (input[i] === '\\')
3712
- i++;
3713
- i++;
3714
- }
3715
- i++;
3716
- continue;
3717
- }
3718
- if (ch === '`') {
3719
- i++;
3720
- while (i < input.length && input[i] !== '`') {
3721
- if (input[i] === '\\')
3722
- i++;
3723
- i++;
3724
- }
3725
- i++;
3726
- continue;
3727
- }
3728
- if (ch === '/' && i + 1 < input.length && input[i + 1] === '/') {
3729
- i += 2;
3730
- while (i < input.length && input[i] !== '\n')
3731
- i++;
3732
- continue;
3733
- }
3734
- if (ch === '/' && i + 1 < input.length && input[i + 1] === '*') {
3735
- i += 2;
3736
- while (i < input.length &&
3737
- !(input[i] === '*' && i + 1 < input.length && input[i + 1] === '/'))
3738
- i++;
3739
- i += 2;
3740
- continue;
3741
- }
3742
- if ((ch === 'e' || ch === 'E') &&
3743
- i + 3 <= input.length &&
3744
- input.slice(i, i + 3).toLowerCase() === 'end') {
3745
- const before = i === 0 || !/[a-zA-Z0-9_]/.test(input[i - 1]);
3746
- const after = i + 3 >= input.length || !/[a-zA-Z0-9_]/.test(input[i + 3]);
3747
- if (before && after) {
3748
- return startPos + i;
3749
- }
3750
- }
3751
- i++;
3752
- }
3753
- return startPos + input.length;
3754
- }
3755
- function parseJsCommand(ctx, identifierNode) {
3756
- const parameters = [];
3757
- if (ctx.match('(')) {
3758
- while (!ctx.check(')') && !ctx.isAtEnd()) {
3759
- if (ctx.checkIdentifierLike()) {
3760
- parameters.push(ctx.advance().value);
3761
- }
3762
- ctx.match(',');
3763
- }
3764
- ctx.consume(')', 'Expected ) after js parameters');
3765
- }
3766
- const jsCodeStart = ctx.peek().start;
3767
- const jsCodeEnd = findJsEndBoundary(ctx, jsCodeStart);
3768
- while (!ctx.isAtEnd() && !ctx.check(KEYWORDS.END)) {
3769
- if (ctx.peek().start >= jsCodeEnd)
3770
- break;
3771
- ctx.advance();
3772
- }
3773
- ctx.consume(KEYWORDS.END, 'Expected end after js code body');
3774
- const rawSlice = ctx.getInputSlice(jsCodeStart, jsCodeEnd);
3775
- const code = rawSlice.trim();
3776
- const codeNode = {
3777
- type: 'literal',
3778
- value: code,
3779
- start: identifierNode.start,
3780
- end: ctx.getPosition().end,
3781
- };
3782
- const paramsNode = {
3783
- type: 'arrayLiteral',
3784
- elements: parameters.map(p => ({
3785
- type: 'literal',
3786
- value: p,
3787
- start: identifierNode.start,
3788
- end: identifierNode.end,
3789
- })),
3790
- start: identifierNode.start,
3791
- end: ctx.getPosition().end,
3792
- };
3793
- return CommandNodeBuilder.fromIdentifier(identifierNode)
3794
- .withArgs(codeNode, paramsNode)
3795
- .endingAt(ctx.getPosition())
3796
- .build();
3797
- }
3798
- function parseTellCommand(ctx, identifierNode) {
3799
- const target = ctx.parseExpression();
3800
- if (!target) {
3801
- throw new Error('tell command requires a target expression');
3802
- }
3803
- const commands = [];
3804
- while (!ctx.isAtEnd()) {
3805
- if (ctx.checkIsCommand()) {
3806
- try {
4167
+ if (ctx.check('do') && !modifiers['doNotThrow']) {
4168
+ const n1 = ctx.peekAt(1);
4169
+ const n2 = ctx.peekAt(2);
4170
+ if (n1?.value === 'not' && n2?.value === 'throw') {
4171
+ const doToken = ctx.advance();
3807
4172
  ctx.advance();
3808
- const cmd = ctx.parseCommand();
3809
- if (cmd) {
3810
- commands.push(cmd);
3811
- }
3812
- else {
3813
- break;
3814
- }
3815
- }
3816
- catch {
3817
- break;
3818
- }
3819
- if (ctx.match(KEYWORDS.AND)) {
3820
- continue;
3821
- }
3822
- if (ctx.check(KEYWORDS.THEN) || ctx.check(KEYWORDS.ELSE) || ctx.check(KEYWORDS.END)) {
3823
- break;
3824
- }
3825
- if (ctx.checkIsCommand()) {
3826
- continue;
3827
- }
3828
- break;
3829
- }
3830
- else {
3831
- break;
3832
- }
3833
- }
3834
- if (commands.length === 0) {
3835
- throw new Error('tell command requires at least one command after the target');
3836
- }
3837
- return CommandNodeBuilder.fromIdentifier(identifierNode)
3838
- .withArgs(target, ...commands)
3839
- .endingAt(ctx.getPosition())
3840
- .build();
3841
- }
3842
-
3843
- const STOP_TOKENS = new Set([
3844
- 'then',
3845
- 'end',
3846
- 'to',
3847
- 'into',
3848
- 'on',
3849
- 'with',
3850
- 'from',
3851
- 'in',
3852
- 'by',
3853
- 'for',
3854
- 'while',
3855
- 'until',
3856
- 'unless',
3857
- 'else',
3858
- 'catch',
3859
- 'finally',
3860
- ]);
3861
- const STOP_DELIMITERS = new Set([')', ']', '}', ',']);
3862
- function mergeFragments(...fragments) {
3863
- const merged = new Map();
3864
- for (const fragment of fragments) {
3865
- for (const [key, entry] of fragment) {
3866
- const existing = merged.get(key);
3867
- if (existing) {
3868
- merged.set(key, {
3869
- prefix: entry.prefix ?? existing.prefix,
3870
- infix: entry.infix ?? existing.infix,
3871
- });
3872
- }
3873
- else {
3874
- merged.set(key, { ...entry });
4173
+ const throwToken = ctx.advance();
4174
+ modifiers['doNotThrow'] = {
4175
+ type: 'literal',
4176
+ value: true,
4177
+ start: doToken.start,
4178
+ end: throwToken.end,
4179
+ line: doToken.line,
4180
+ column: doToken.column,
4181
+ };
4182
+ continue;
3875
4183
  }
3876
4184
  }
4185
+ break;
3877
4186
  }
3878
- return merged;
3879
- }
3880
- function leftAssoc(bp, handler) {
3881
- return {
3882
- infix: {
3883
- bp: [bp, bp + 1],
3884
- handler: handler ??
3885
- ((left, token, ctx) => ({
3886
- type: 'binaryExpression',
3887
- operator: token.value,
3888
- left,
3889
- right: ctx.parseExpr(bp + 1),
3890
- start: left.start,
3891
- })),
3892
- },
3893
- };
4187
+ const builder = CommandNodeBuilder.from(commandToken).withArgs(url).endingAt(ctx.getPosition());
4188
+ if (Object.keys(modifiers).length > 0) {
4189
+ builder.withModifiers(modifiers);
4190
+ }
4191
+ return builder.build();
3894
4192
  }
3895
- function rightAssoc(bp, handler) {
3896
- return {
3897
- infix: {
3898
- bp: [bp + 1, bp],
3899
- handler: ((left, token, ctx) => ({
3900
- type: 'binaryExpression',
3901
- operator: token.value,
3902
- left,
3903
- right: ctx.parseExpr(bp),
3904
- start: left.start,
3905
- })),
3906
- },
3907
- };
4193
+ function isFetchNakedNamedArgStart(ctx) {
4194
+ if (ctx.check('{'))
4195
+ return false;
4196
+ if (!ctx.checkIdentifierLike())
4197
+ return false;
4198
+ const next = ctx.peekAt(1);
4199
+ return next !== null && next.value === ':';
3908
4200
  }
3909
- function prefix(bp, handler) {
4201
+ function parseFetchNakedNamedArgs(ctx) {
4202
+ const properties = [];
4203
+ const startPos = ctx.getPosition();
4204
+ do {
4205
+ if (!ctx.checkIdentifierLike())
4206
+ break;
4207
+ const keyToken = ctx.advance();
4208
+ const key = {
4209
+ type: 'identifier',
4210
+ name: keyToken.value,
4211
+ start: keyToken.start,
4212
+ end: keyToken.end,
4213
+ line: keyToken.line,
4214
+ column: keyToken.column,
4215
+ };
4216
+ ctx.consume(':', "Expected ':' after property name in fetch named arguments");
4217
+ const value = ctx.parsePrimary();
4218
+ if (value) {
4219
+ properties.push({ key, value });
4220
+ }
4221
+ } while (ctx.match(',') && !ctx.isAtEnd());
4222
+ const endPos = ctx.getPosition();
3910
4223
  return {
3911
- prefix: {
3912
- bp,
3913
- handler: handler ??
3914
- ((token, ctx) => ({
3915
- type: 'unaryExpression',
3916
- operator: token.value,
3917
- operand: ctx.parseExpr(bp),
3918
- start: token.start,
3919
- })),
3920
- },
4224
+ type: 'objectLiteral',
4225
+ properties,
4226
+ start: startPos.start,
4227
+ end: endPos.end,
4228
+ line: startPos.line,
4229
+ column: startPos.column,
3921
4230
  };
3922
4231
  }
3923
- const CORE_FRAGMENT = new Map([
3924
- ['or', leftAssoc(10)],
3925
- ['||', leftAssoc(10)],
3926
- ['and', leftAssoc(20)],
3927
- ['&&', leftAssoc(20)],
3928
- ['==', leftAssoc(30)],
3929
- ['!=', leftAssoc(30)],
3930
- ['<', leftAssoc(30)],
3931
- ['>', leftAssoc(30)],
3932
- ['<=', leftAssoc(30)],
3933
- ['>=', leftAssoc(30)],
3934
- ['is', leftAssoc(30)],
3935
- ['matches', leftAssoc(30)],
3936
- ['contains', leftAssoc(30)],
3937
- ['+', { ...leftAssoc(40), ...prefix(80) }],
3938
- ['-', { ...leftAssoc(40), ...prefix(80) }],
3939
- ['*', leftAssoc(50)],
3940
- ['/', leftAssoc(50)],
3941
- ['%', leftAssoc(50)],
3942
- ['mod', leftAssoc(50)],
3943
- ['^', rightAssoc(60)],
3944
- ['**', rightAssoc(60)],
3945
- [
3946
- 'as',
3947
- leftAssoc(70, (left, _token, ctx) => ({
3948
- type: 'asExpression',
3949
- expression: left,
3950
- targetType: ctx.parseExpr(71),
3951
- start: left.start,
3952
- })),
3953
- ],
3954
- ['not', prefix(80)],
3955
- ['!', prefix(80)],
3956
- ['no', prefix(80)],
3957
- ]);
3958
- const POSITIONAL_FRAGMENT = new Map([
3959
- [
3960
- 'first',
3961
- prefix(85, (token, ctx) => ({
3962
- type: 'positionalExpression',
3963
- position: 'first',
3964
- operand: ctx.parseExpr(85),
3965
- start: token.start,
3966
- })),
3967
- ],
3968
- [
3969
- 'last',
3970
- prefix(85, (token, ctx) => ({
3971
- type: 'positionalExpression',
3972
- position: 'last',
3973
- operand: ctx.parseExpr(85),
3974
- start: token.start,
3975
- })),
3976
- ],
3977
- ]);
3978
- const PROPERTY_FRAGMENT = new Map([
3979
- [
3980
- '.',
3981
- {
3982
- infix: {
3983
- bp: [90, 91],
3984
- handler: (left, _token, ctx) => {
3985
- const propToken = ctx.advance();
3986
- return {
3987
- type: 'propertyAccess',
3988
- object: left,
3989
- property: propToken.value,
3990
- start: left.start,
3991
- };
3992
- },
3993
- },
3994
- },
3995
- ],
3996
- [
3997
- '?.',
3998
- {
3999
- infix: {
4000
- bp: [90, 91],
4001
- handler: (left, _token, ctx) => {
4002
- const propToken = ctx.advance();
4003
- return {
4004
- type: 'optionalChain',
4005
- object: left,
4006
- property: propToken.value,
4007
- start: left.start,
4008
- };
4009
- },
4010
- },
4011
- },
4012
- ],
4013
- [
4014
- "'s",
4015
- {
4016
- infix: {
4017
- bp: [95, 96],
4018
- handler: (left, _token, ctx) => {
4019
- const propToken = ctx.advance();
4020
- return {
4021
- type: 'possessiveExpression',
4022
- object: left,
4023
- property: propToken.value,
4024
- start: left.start,
4025
- };
4026
- },
4027
- },
4028
- },
4029
- ],
4030
- ]);
4031
- const PARSER_COMPARISON_FRAGMENT = new Map([
4032
- ['===', leftAssoc(30)],
4033
- ['!==', leftAssoc(30)],
4034
- ['is not', leftAssoc(30)],
4035
- ['is a', leftAssoc(30)],
4036
- ['is an', leftAssoc(30)],
4037
- ['is not a', leftAssoc(30)],
4038
- ['is not an', leftAssoc(30)],
4039
- ['is in', leftAssoc(30)],
4040
- ['is not in', leftAssoc(30)],
4041
- ['has', leftAssoc(30)],
4042
- ['have', leftAssoc(30)],
4043
- ['match', leftAssoc(30)],
4044
- ['include', leftAssoc(30)],
4045
- ['includes', leftAssoc(30)],
4046
- ['equals', leftAssoc(30)],
4047
- ['does not contain', leftAssoc(30)],
4048
- ['does not include', leftAssoc(30)],
4049
- ['in', leftAssoc(30)],
4050
- ['of', leftAssoc(30)],
4051
- ['really', leftAssoc(30)],
4052
- [
4053
- 'exists',
4054
- {
4055
- prefix: {
4056
- bp: 80,
4057
- handler: (token, ctx) => ({
4058
- type: 'unaryExpression',
4059
- operator: token.value,
4060
- operand: ctx.parseExpr(80),
4061
- start: token.start,
4062
- }),
4063
- },
4064
- infix: {
4065
- bp: [30, 31],
4066
- handler: (left, token) => ({
4067
- type: 'unaryExpression',
4068
- operator: token.value,
4069
- operand: left,
4070
- prefix: false,
4071
- start: left.start,
4072
- }),
4073
- },
4074
- },
4075
- ],
4076
- [
4077
- 'does not exist',
4078
- {
4079
- infix: {
4080
- bp: [30, 31],
4081
- handler: (left, token) => ({
4082
- type: 'unaryExpression',
4083
- operator: token.value,
4084
- operand: left,
4085
- prefix: false,
4086
- start: left.start,
4087
- }),
4088
- },
4089
- },
4090
- ],
4091
- [
4092
- 'is empty',
4093
- {
4094
- infix: {
4095
- bp: [30, 31],
4096
- handler: (left, token) => ({
4097
- type: 'unaryExpression',
4098
- operator: token.value,
4099
- operand: left,
4100
- prefix: false,
4101
- start: left.start,
4102
- }),
4103
- },
4104
- },
4105
- ],
4106
- [
4107
- 'is not empty',
4108
- {
4109
- infix: {
4110
- bp: [30, 31],
4111
- handler: (left, token) => ({
4112
- type: 'unaryExpression',
4113
- operator: token.value,
4114
- operand: left,
4115
- prefix: false,
4116
- start: left.start,
4117
- }),
4118
- },
4119
- },
4120
- ],
4121
- ['some', prefix(80)],
4122
- ]);
4123
- const ASSIGNMENT_FRAGMENT = new Map([
4124
- ['=', rightAssoc(5)],
4125
- ]);
4126
- mergeFragments(CORE_FRAGMENT, POSITIONAL_FRAGMENT, PROPERTY_FRAGMENT);
4127
- const PARSER_TABLE = mergeFragments(CORE_FRAGMENT, PARSER_COMPARISON_FRAGMENT, ASSIGNMENT_FRAGMENT);
4232
+ function findJsEndBoundary(ctx, startPos) {
4233
+ const input = ctx.getInputSlice(startPos);
4234
+ if (!input) {
4235
+ return startPos;
4236
+ }
4237
+ let i = 0;
4238
+ while (i < input.length) {
4239
+ const ch = input[i];
4240
+ if (ch === "'" || ch === '\u2019' || ch === '\u2018') {
4241
+ i++;
4242
+ while (i < input.length && input[i] !== ch) {
4243
+ if (input[i] === '\\')
4244
+ i++;
4245
+ i++;
4246
+ }
4247
+ i++;
4248
+ continue;
4249
+ }
4250
+ if (ch === '"') {
4251
+ i++;
4252
+ while (i < input.length && input[i] !== '"') {
4253
+ if (input[i] === '\\')
4254
+ i++;
4255
+ i++;
4256
+ }
4257
+ i++;
4258
+ continue;
4259
+ }
4260
+ if (ch === '`') {
4261
+ i++;
4262
+ while (i < input.length && input[i] !== '`') {
4263
+ if (input[i] === '\\')
4264
+ i++;
4265
+ i++;
4266
+ }
4267
+ i++;
4268
+ continue;
4269
+ }
4270
+ if (ch === '/' && i + 1 < input.length && input[i + 1] === '/') {
4271
+ i += 2;
4272
+ while (i < input.length && input[i] !== '\n')
4273
+ i++;
4274
+ continue;
4275
+ }
4276
+ if (ch === '/' && i + 1 < input.length && input[i + 1] === '*') {
4277
+ i += 2;
4278
+ while (i < input.length &&
4279
+ !(input[i] === '*' && i + 1 < input.length && input[i + 1] === '/'))
4280
+ i++;
4281
+ i += 2;
4282
+ continue;
4283
+ }
4284
+ if ((ch === 'e' || ch === 'E') &&
4285
+ i + 3 <= input.length &&
4286
+ input.slice(i, i + 3).toLowerCase() === 'end') {
4287
+ const before = i === 0 || !/[a-zA-Z0-9_]/.test(input[i - 1]);
4288
+ const after = i + 3 >= input.length || !/[a-zA-Z0-9_]/.test(input[i + 3]);
4289
+ if (before && after) {
4290
+ return startPos + i;
4291
+ }
4292
+ }
4293
+ i++;
4294
+ }
4295
+ return startPos + input.length;
4296
+ }
4297
+ function parseJsCommand(ctx, identifierNode) {
4298
+ const parameters = [];
4299
+ if (ctx.match('(')) {
4300
+ while (!ctx.check(')') && !ctx.isAtEnd()) {
4301
+ if (ctx.checkIdentifierLike()) {
4302
+ parameters.push(ctx.advance().value);
4303
+ }
4304
+ ctx.match(',');
4305
+ }
4306
+ ctx.consume(')', 'Expected ) after js parameters');
4307
+ }
4308
+ const jsCodeStart = ctx.peek().start;
4309
+ const jsCodeEnd = findJsEndBoundary(ctx, jsCodeStart);
4310
+ while (!ctx.isAtEnd() && !ctx.check(KEYWORDS.END)) {
4311
+ if (ctx.peek().start >= jsCodeEnd)
4312
+ break;
4313
+ ctx.advance();
4314
+ }
4315
+ ctx.consume(KEYWORDS.END, 'Expected end after js code body');
4316
+ const rawSlice = ctx.getInputSlice(jsCodeStart, jsCodeEnd);
4317
+ const code = rawSlice.trim();
4318
+ const codeNode = {
4319
+ type: 'literal',
4320
+ value: code,
4321
+ start: identifierNode.start,
4322
+ end: ctx.getPosition().end,
4323
+ };
4324
+ const paramsNode = {
4325
+ type: 'arrayLiteral',
4326
+ elements: parameters.map(p => ({
4327
+ type: 'literal',
4328
+ value: p,
4329
+ start: identifierNode.start,
4330
+ end: identifierNode.end,
4331
+ })),
4332
+ start: identifierNode.start,
4333
+ end: ctx.getPosition().end,
4334
+ };
4335
+ return CommandNodeBuilder.fromIdentifier(identifierNode)
4336
+ .withArgs(codeNode, paramsNode)
4337
+ .endingAt(ctx.getPosition())
4338
+ .build();
4339
+ }
4340
+ function parseTellCommand(ctx, identifierNode) {
4341
+ const target = ctx.parseExpression();
4342
+ if (!target) {
4343
+ throw new Error('tell command requires a target expression');
4344
+ }
4345
+ const commands = [];
4346
+ while (!ctx.isAtEnd()) {
4347
+ if (ctx.checkIsCommand()) {
4348
+ try {
4349
+ ctx.advance();
4350
+ const cmd = ctx.parseCommand();
4351
+ if (cmd) {
4352
+ commands.push(cmd);
4353
+ }
4354
+ else {
4355
+ break;
4356
+ }
4357
+ }
4358
+ catch {
4359
+ break;
4360
+ }
4361
+ if (ctx.match(KEYWORDS.AND)) {
4362
+ continue;
4363
+ }
4364
+ if (ctx.check(KEYWORDS.THEN) || ctx.check(KEYWORDS.ELSE) || ctx.check(KEYWORDS.END)) {
4365
+ break;
4366
+ }
4367
+ if (ctx.checkIsCommand()) {
4368
+ continue;
4369
+ }
4370
+ break;
4371
+ }
4372
+ else {
4373
+ break;
4374
+ }
4375
+ }
4376
+ if (commands.length === 0) {
4377
+ throw new Error('tell command requires at least one command after the target');
4378
+ }
4379
+ return CommandNodeBuilder.fromIdentifier(identifierNode)
4380
+ .withArgs(target, ...commands)
4381
+ .endingAt(ctx.getPosition())
4382
+ .build();
4383
+ }
4384
+ function parsePickCommand(ctx, identifierNode) {
4385
+ const builder = CommandNodeBuilder.fromIdentifier(identifierNode);
4386
+ consumeOptionalKeyword(ctx, KEYWORDS.THE);
4387
+ const variantToken = ctx.peek();
4388
+ const variantName = variantToken.value;
4389
+ const makeStringLiteral = (value) => ({
4390
+ type: 'literal',
4391
+ value,
4392
+ start: identifierNode.start,
4393
+ end: identifierNode.end,
4394
+ });
4395
+ const consumeSource = () => {
4396
+ if (!ctx.match('of', 'from')) {
4397
+ throw new Error(`pick: expected 'of' or 'from' before source expression`);
4398
+ }
4399
+ return ctx.parseExpression();
4400
+ };
4401
+ if (variantName === 'first') {
4402
+ ctx.advance();
4403
+ const count = ctx.parsePrimary();
4404
+ const source = consumeSource();
4405
+ return builder
4406
+ .withArgs(source)
4407
+ .withModifier('variant', makeStringLiteral('first'))
4408
+ .withModifier('count', count)
4409
+ .endingAt(ctx.getPosition())
4410
+ .build();
4411
+ }
4412
+ if (variantName === 'last') {
4413
+ ctx.advance();
4414
+ const count = ctx.parsePrimary();
4415
+ const source = consumeSource();
4416
+ return builder
4417
+ .withArgs(source)
4418
+ .withModifier('variant', makeStringLiteral('last'))
4419
+ .withModifier('count', count)
4420
+ .endingAt(ctx.getPosition())
4421
+ .build();
4422
+ }
4423
+ if (variantName === 'random') {
4424
+ ctx.advance();
4425
+ let countNode;
4426
+ if (!ctx.check('of') && !ctx.check('from')) {
4427
+ countNode = ctx.parsePrimary();
4428
+ }
4429
+ const source = consumeSource();
4430
+ const b = builder.withArgs(source).withModifier('variant', makeStringLiteral('random'));
4431
+ if (countNode)
4432
+ b.withModifier('count', countNode);
4433
+ return b.endingAt(ctx.getPosition()).build();
4434
+ }
4435
+ if (variantName === 'item' ||
4436
+ variantName === 'items' ||
4437
+ variantName === 'character' ||
4438
+ variantName === 'characters') {
4439
+ ctx.advance();
4440
+ ctx.match('at', 'from');
4441
+ let rangeStart;
4442
+ if (ctx.match('start')) {
4443
+ rangeStart = makeStringLiteral('start');
4444
+ }
4445
+ else {
4446
+ rangeStart = ctx.parsePrimary();
4447
+ }
4448
+ let rangeEnd;
4449
+ let endIsEndKeyword = false;
4450
+ if (ctx.match('to') || ctx.match('..')) {
4451
+ if (ctx.match('end')) {
4452
+ endIsEndKeyword = true;
4453
+ }
4454
+ else {
4455
+ rangeEnd = ctx.parsePrimary();
4456
+ }
4457
+ }
4458
+ let mode = 'default';
4459
+ if (ctx.match('inclusive'))
4460
+ mode = 'inclusive';
4461
+ else if (ctx.match('exclusive'))
4462
+ mode = 'exclusive';
4463
+ const source = consumeSource();
4464
+ const b = builder
4465
+ .withArgs(source)
4466
+ .withModifier('variant', makeStringLiteral('range'))
4467
+ .withModifier('rangeStart', rangeStart)
4468
+ .withModifier('rangeMode', makeStringLiteral(mode));
4469
+ if (endIsEndKeyword) {
4470
+ b.withModifier('rangeEnd', makeStringLiteral('end'));
4471
+ }
4472
+ else if (rangeEnd) {
4473
+ b.withModifier('rangeEnd', rangeEnd);
4474
+ }
4475
+ return b.endingAt(ctx.getPosition()).build();
4476
+ }
4477
+ if (variantName === 'match' || variantName === 'matches') {
4478
+ ctx.advance();
4479
+ ctx.match('of');
4480
+ const regex = ctx.parsePrimary();
4481
+ let flags;
4482
+ if (ctx.matchOperator('|')) {
4483
+ flags = ctx.advance().value;
4484
+ }
4485
+ const source = consumeSource();
4486
+ const b = builder
4487
+ .withArgs(source)
4488
+ .withModifier('variant', makeStringLiteral(variantName === 'match' ? 'match' : 'matches'))
4489
+ .withModifier('regex', regex);
4490
+ if (flags)
4491
+ b.withModifier('flags', makeStringLiteral(flags));
4492
+ return b.endingAt(ctx.getPosition()).build();
4493
+ }
4494
+ return parseRegularCommand(ctx, identifierNode);
4495
+ }
4128
4496
 
4129
4497
  function parseTimeToMs(timeStr) {
4130
4498
  if (timeStr.endsWith('ms'))
@@ -4216,7 +4584,13 @@ class Parser {
4216
4584
  warnings: this.warnings,
4217
4585
  };
4218
4586
  }
4219
- if (this.check('init') || this.check('on') || this.check('def') || this.checkComment()) {
4587
+ const topToken = this.peek();
4588
+ const topPluginFeature = topToken && getRegisteredFeature(topToken.value) ? topToken.value : null;
4589
+ if (this.check('init') ||
4590
+ this.check('on') ||
4591
+ this.check('def') ||
4592
+ this.checkComment() ||
4593
+ topPluginFeature !== null) {
4220
4594
  const statements = [];
4221
4595
  while (!this.isAtEnd()) {
4222
4596
  if (this.checkComment()) {
@@ -4245,6 +4619,16 @@ class Parser {
4245
4619
  }
4246
4620
  }
4247
4621
  else {
4622
+ const tok = this.peek();
4623
+ const pluginParse = tok ? getRegisteredFeature(tok.value) : undefined;
4624
+ if (pluginParse) {
4625
+ const featureToken = this.advance();
4626
+ const featureNode = pluginParse(this.getContext(), featureToken);
4627
+ if (featureNode) {
4628
+ statements.push(featureNode);
4629
+ }
4630
+ continue;
4631
+ }
4248
4632
  break;
4249
4633
  }
4250
4634
  }
@@ -4364,7 +4748,14 @@ class Parser {
4364
4748
  };
4365
4749
  }
4366
4750
  if (!this.isAtEnd()) {
4367
- this.addError(`Unexpected token: ${this.peek().value}`);
4751
+ const next = this.peek();
4752
+ const valueLikeKinds = ['number', 'string', 'identifier'];
4753
+ if (ast && valueLikeKinds.includes(next.kind)) {
4754
+ this.addError(`Unexpected token: ${next.value} (missing operator between values? expected one of +, -, *, /, etc.)`);
4755
+ }
4756
+ else {
4757
+ this.addError(`Unexpected token: ${next.value}`);
4758
+ }
4368
4759
  return {
4369
4760
  success: false,
4370
4761
  node: ast || this.createErrorNode(),
@@ -4442,7 +4833,8 @@ class Parser {
4442
4833
  try {
4443
4834
  this.parseExpressionPratt(0);
4444
4835
  }
4445
- catch {
4836
+ catch (recoveryErr) {
4837
+ debug.parse('arrow body discarded after error:', recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr));
4446
4838
  }
4447
4839
  }
4448
4840
  return this.createErrorNode();
@@ -4501,143 +4893,8 @@ class Parser {
4501
4893
  atEnd: () => self.current >= self.tokens.length,
4502
4894
  };
4503
4895
  }
4504
- parseAssignment() {
4505
- let expr = this.parseLogicalOr();
4506
- if (this.match('=')) {
4507
- if (this.check('>')) {
4508
- this.advance();
4509
- this.addError('Arrow functions (=>) are not supported in hyperscript. ' +
4510
- 'Use "js ... end" blocks for JavaScript callbacks.');
4511
- if (!this.isAtEnd()) {
4512
- try {
4513
- this.parseExpression();
4514
- }
4515
- catch {
4516
- }
4517
- }
4518
- return this.createErrorNode();
4519
- }
4520
- const operator = this.previous().value;
4521
- const right = this.parseAssignment();
4522
- expr = this.createBinaryExpression(operator, expr, right);
4523
- }
4524
- return expr;
4525
- }
4526
- parseLogicalOr() {
4527
- let expr = this.parseLogicalAnd();
4528
- while (this.match('or')) {
4529
- const operator = this.previous().value;
4530
- const right = this.parseLogicalAnd();
4531
- expr = this.createBinaryExpression(operator, expr, right);
4532
- }
4533
- return expr;
4534
- }
4535
4896
  parseLogicalAnd() {
4536
- let expr = this.parseEquality();
4537
- while (this.match('and')) {
4538
- const operator = this.previous().value;
4539
- const right = this.parseEquality();
4540
- expr = this.createBinaryExpression(operator, expr, right);
4541
- }
4542
- return expr;
4543
- }
4544
- parseEquality() {
4545
- let expr = this.parseComparison();
4546
- while (this.matchComparisonOperator() ||
4547
- this.match('is', 'match', 'matches', 'contains', 'include', 'includes', 'in', 'of', 'as', 'really')) {
4548
- const operator = this.previous().value;
4549
- if (Parser.POSTFIX_UNARY_OPERATORS.has(operator)) {
4550
- expr = this.createUnaryExpression(operator, expr, false);
4551
- continue;
4552
- }
4553
- const right = this.parseComparison();
4554
- expr = this.createBinaryExpression(operator, expr, right);
4555
- }
4556
- return expr;
4557
- }
4558
- parseComparison() {
4559
- let expr = this.parseAddition();
4560
- while (this.matchComparisonOperator()) {
4561
- const operator = this.previous().value;
4562
- if (Parser.POSTFIX_UNARY_OPERATORS.has(operator)) {
4563
- expr = this.createUnaryExpression(operator, expr, false);
4564
- continue;
4565
- }
4566
- const right = this.parseAddition();
4567
- expr = this.createBinaryExpression(operator, expr, right);
4568
- }
4569
- return expr;
4570
- }
4571
- parseAddition() {
4572
- let expr = this.parseMultiplication();
4573
- while (this.match('+', '-') || this.matchOperator('+') || this.matchOperator('-')) {
4574
- const operator = this.previous().value;
4575
- if (this.check('+') || this.check('-')) {
4576
- this.addError(`Invalid operator combination: ${operator}${this.peek().value}`);
4577
- return expr;
4578
- }
4579
- if (this.isAtEnd()) {
4580
- this.addError(`Expected expression after '${operator}' operator`);
4581
- return expr;
4582
- }
4583
- const right = this.parseMultiplication();
4584
- expr = this.createBinaryExpression(operator, expr, right);
4585
- }
4586
- return expr;
4587
- }
4588
- parseMultiplication() {
4589
- let expr = this.parseUnary();
4590
- while (this.match('*', '/', '%', 'mod')) {
4591
- const operator = this.previous().value;
4592
- if (this.check('*') ||
4593
- this.check('/') ||
4594
- this.check('%') ||
4595
- this.check('+') ||
4596
- this.check('-')) {
4597
- const nextOp = this.peek().value;
4598
- if (operator === '*' && nextOp === '*') {
4599
- this.addError(`Unexpected token: ${nextOp}`);
4600
- }
4601
- else {
4602
- this.addError(`Invalid operator combination: ${operator}${nextOp}`);
4603
- }
4604
- return expr;
4605
- }
4606
- if (this.isAtEnd()) {
4607
- this.addError(`Expected expression after '${operator}' operator`);
4608
- return expr;
4609
- }
4610
- const right = this.parseUnary();
4611
- expr = this.createBinaryExpression(operator, expr, right);
4612
- }
4613
- return expr;
4614
- }
4615
- parseUnary() {
4616
- if (this.match('not', 'no', 'exists', 'some', '-', '+')) {
4617
- const operator = this.previous().value;
4618
- if (this.isAtEnd()) {
4619
- this.addError(`Expected expression after '${operator}' operator`);
4620
- return this.createErrorNode();
4621
- }
4622
- const expr = this.parseUnary();
4623
- return this.createUnaryExpression(operator, expr, true);
4624
- }
4625
- if (this.check('does') &&
4626
- this.current + 1 < this.tokens.length &&
4627
- this.tokens[this.current + 1].value === 'not' &&
4628
- this.current + 2 < this.tokens.length &&
4629
- this.tokens[this.current + 2].value === 'exist') {
4630
- this.advance();
4631
- this.advance();
4632
- this.advance();
4633
- if (this.isAtEnd()) {
4634
- this.addError(`Expected expression after 'does not exist' operator`);
4635
- return this.createErrorNode();
4636
- }
4637
- const expr = this.parseUnary();
4638
- return this.createUnaryExpression('does not exist', expr, true);
4639
- }
4640
- return this.parseImplicitBinary();
4897
+ return this.parseExpressionPratt(11);
4641
4898
  }
4642
4899
  parseImplicitBinary() {
4643
4900
  let expr = this.parseCall();
@@ -4785,10 +5042,11 @@ class Parser {
4785
5042
  parseTriggerCommand(identifierNode) {
4786
5043
  return parseTriggerCommand(this.getContext(), identifierNode);
4787
5044
  }
4788
- parseCommandListUntilEnd() {
5045
+ parseCommandListUntilTerminator(extraStops) {
4789
5046
  const commands = [];
4790
- debug.parse('🔄 parseCommandListUntilEnd: Starting to parse command list');
4791
- while (!this.isAtEnd() && !this.check('end')) {
5047
+ const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
5048
+ debug.parse('🔄 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
5049
+ while (!this.isAtEnd() && !isStop()) {
4792
5050
  debug.parse('📍 Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
4793
5051
  let parsedCommand = false;
4794
5052
  const isCommandToken = this.checkIsCommand();
@@ -4800,7 +5058,7 @@ class Parser {
4800
5058
  try {
4801
5059
  const cmd = this.parseCommand();
4802
5060
  if (this.error && this.error !== savedError) {
4803
- debug.parse('⚠️ parseCommandListUntilEnd: Command parsing added error, restoring error state. Error was:', this.error.message);
5061
+ debug.parse('⚠️ parseCommandListUntilTerminator: Command parsing added error, restoring. Error was:', this.error.message);
4804
5062
  this.error = savedError;
4805
5063
  }
4806
5064
  if (cmd) {
@@ -4810,7 +5068,7 @@ class Parser {
4810
5068
  }
4811
5069
  }
4812
5070
  catch (error) {
4813
- debug.parse('⚠️ parseCommandListUntilEnd: Command parsing threw exception, restoring error state:', error instanceof Error ? error.message : String(error));
5071
+ debug.parse('⚠️ parseCommandListUntilTerminator: Command parsing threw, restoring:', error instanceof Error ? error.message : String(error));
4814
5072
  this.error = savedError;
4815
5073
  }
4816
5074
  }
@@ -4823,7 +5081,7 @@ class Parser {
4823
5081
  }
4824
5082
  debug.parse('📍 After parsing command, current token:', this.peek().value);
4825
5083
  while (!this.isAtEnd() &&
4826
- !this.check('end') &&
5084
+ !isStop() &&
4827
5085
  !this.checkIsCommand() &&
4828
5086
  !this.isCommand(this.peek().value) &&
4829
5087
  !this.check('then') &&
@@ -4845,6 +5103,21 @@ class Parser {
4845
5103
  break;
4846
5104
  }
4847
5105
  }
5106
+ let terminator = 'end';
5107
+ for (const s of extraStops) {
5108
+ if (this.check(s)) {
5109
+ terminator = s;
5110
+ break;
5111
+ }
5112
+ }
5113
+ if (!this.check('end') && !extraStops.some(s => this.check(s))) {
5114
+ terminator = '';
5115
+ }
5116
+ debug.parse('✅ parseCommandListUntilTerminator: parsed', commands.length, 'commands (terminator:', terminator, ')');
5117
+ return { commands, terminator };
5118
+ }
5119
+ parseCommandListUntilEnd() {
5120
+ const { commands } = this.parseCommandListUntilTerminator([]);
4848
5121
  debug.parse('🔍 After loop, checking for "end". Current token:', this.peek().value);
4849
5122
  if (this.check('end')) {
4850
5123
  debug.parse('✅ Found "end", consuming it');
@@ -4854,9 +5127,35 @@ class Parser {
4854
5127
  debug.parse('❌ ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
4855
5128
  throw new Error('Expected "end" to close repeat block');
4856
5129
  }
4857
- debug.parse('✅ parseCommandListUntilEnd: Successfully parsed', commands.length, 'commands');
4858
5130
  return commands;
4859
5131
  }
5132
+ parseCommandListUntilEndOrElse() {
5133
+ const { commands, terminator } = this.parseCommandListUntilTerminator(['else']);
5134
+ const hasElse = terminator === 'else';
5135
+ if (!hasElse) {
5136
+ if (this.check('end')) {
5137
+ this.advance();
5138
+ }
5139
+ else {
5140
+ throw new Error('Expected "end" to close repeat block');
5141
+ }
5142
+ }
5143
+ return { commands, hasElse };
5144
+ }
5145
+ parseRepeatBody() {
5146
+ const { commands, terminator } = this.parseCommandListUntilTerminator([
5147
+ 'else',
5148
+ 'until',
5149
+ 'while',
5150
+ ]);
5151
+ if (terminator === 'end') {
5152
+ this.advance();
5153
+ }
5154
+ else if (terminator === '') {
5155
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5156
+ }
5157
+ return { commands, terminator };
5158
+ }
4860
5159
  parseRepeatCommand(commandToken) {
4861
5160
  return parseRepeatCommand(this.getContext(), commandToken);
4862
5161
  }
@@ -4894,9 +5193,15 @@ class Parser {
4894
5193
  expr = this.finishCall(expr);
4895
5194
  }
4896
5195
  else if (this.match('.')) {
4897
- const name = this.consumeIdentifier("Expected property name after '.' - malformed member access");
5196
+ const name = this.consumeIdentifierLike("Expected property name after '.' - malformed member access");
4898
5197
  expr = this.createMemberExpression(expr, this.createIdentifier(name.value), false);
4899
5198
  }
5199
+ else if (this.match('?.')) {
5200
+ const name = this.consumeIdentifierLike("Expected property name after '?.' - malformed optional access");
5201
+ const memberNode = this.createMemberExpression(expr, this.createIdentifier(name.value), false);
5202
+ memberNode.optional = true;
5203
+ expr = memberNode;
5204
+ }
4900
5205
  else if (this.match('[')) {
4901
5206
  const index = this.parseExpression();
4902
5207
  this.consume(']', "Expected ']' after array index");
@@ -4996,7 +5301,9 @@ class Parser {
4996
5301
  if (this.matchQueryReference()) {
4997
5302
  const queryValue = this.previous().value;
4998
5303
  const selector = queryValue.slice(1, -2).trim();
4999
- return this.createSelector(selector);
5304
+ const node = this.createSelector(selector);
5305
+ node.fromQuery = true;
5306
+ return node;
5000
5307
  }
5001
5308
  if (this.matchSelector()) {
5002
5309
  return this.createSelector(this.previous().value);
@@ -5107,15 +5414,23 @@ class Parser {
5107
5414
  }
5108
5415
  this.current = hashPos;
5109
5416
  }
5417
+ if (!this.isAtEnd() &&
5418
+ !this.checkBasicOperator() &&
5419
+ !this.check('then') &&
5420
+ !this.check('else') &&
5421
+ !this.check('end')) {
5422
+ return this.parseNavigationFunction(token.value);
5423
+ }
5110
5424
  return this.createIdentifier(token.value);
5111
5425
  }
5112
- if (token.value === 'my' && !this.check('.')) {
5426
+ const nextIsDotChain = this.check('.') || this.check('?.');
5427
+ if (token.value === 'my' && !nextIsDotChain) {
5113
5428
  return this.parseContextPropertyAccess('me');
5114
5429
  }
5115
- if (token.value === 'its' && !this.check('.')) {
5430
+ if (token.value === 'its' && !nextIsDotChain) {
5116
5431
  return this.parseContextPropertyAccess('it');
5117
5432
  }
5118
- if (token.value === 'your' && !this.check('.')) {
5433
+ if (token.value === 'your' && !nextIsDotChain) {
5119
5434
  return this.parseContextPropertyAccess('you');
5120
5435
  }
5121
5436
  if (token.value === 'the') {
@@ -5329,18 +5644,11 @@ class Parser {
5329
5644
  this.advance();
5330
5645
  const args = [];
5331
5646
  if (!this.check(')')) {
5332
- let depth = 1;
5333
- while (depth > 0 && !this.isAtEnd()) {
5334
- const token = this.advance();
5335
- if (token.value === '(')
5336
- depth++;
5337
- if (token.value === ')')
5338
- depth--;
5339
- }
5340
- }
5341
- else {
5342
- this.advance();
5647
+ do {
5648
+ args.push(this.parseExpression());
5649
+ } while (this.match(','));
5343
5650
  }
5651
+ this.consume(')', "Expected ')' after constructor arguments");
5344
5652
  return {
5345
5653
  type: 'callExpression',
5346
5654
  callee: {
@@ -5599,6 +5907,16 @@ class Parser {
5599
5907
  parseEventHandler() {
5600
5908
  debug.parse(`🔧 parseEventHandler: ENTRY - parsing event handler`);
5601
5909
  const eventNames = [];
5910
+ let firstOnceAlias = false;
5911
+ if (this.check('first') && !this.checkIsCommand() && this.current + 1 < this.tokens.length) {
5912
+ const peek2 = this.tokens[this.current + 1];
5913
+ const peek2Value = peek2?.value?.toLowerCase();
5914
+ if (peek2Value && peek2Value !== 'of' && peek2Value !== 'in' && peek2Value !== 'from') {
5915
+ this.advance();
5916
+ firstOnceAlias = true;
5917
+ debug.parse(`🔧 parseEventHandler: Parsed 'first' as .once alias`);
5918
+ }
5919
+ }
5602
5920
  const event = this.parseEventNameWithNamespace("Expected event name after 'on'");
5603
5921
  eventNames.push(event);
5604
5922
  debug.parse(`🔧 parseEventHandler: Parsed first event name: ${event}`);
@@ -5697,6 +6015,9 @@ class Parser {
5697
6015
  this.addError(`Expected 'at' after '${modName}'`);
5698
6016
  }
5699
6017
  }
6018
+ if (firstOnceAlias) {
6019
+ modifiers.once = true;
6020
+ }
5700
6021
  if (Object.keys(modifiers).length > 0) {
5701
6022
  debug.parse(`🔧 parseEventHandler: Parsed modifiers:`, modifiers);
5702
6023
  }
@@ -6195,7 +6516,8 @@ class Parser {
6195
6516
  column: commandToken.column,
6196
6517
  };
6197
6518
  const result = this.parseCompoundCommand(identifierNode);
6198
- return result || this.createErrorNode();
6519
+ return (result ||
6520
+ createPartialCommandNode(lowerName, this.getPosition()));
6199
6521
  }
6200
6522
  const args = [];
6201
6523
  if ((commandName === 'increment' || commandName === 'decrement') && !this.isAtEnd()) {
@@ -6705,6 +7027,34 @@ class Parser {
6705
7027
  column: startColumn,
6706
7028
  };
6707
7029
  }
7030
+ const ATTR_OPS = new Set(['=', '~=', '|=', '^=', '$=', '*=']);
7031
+ const lookhead = this.tokens[this.current];
7032
+ const lookhead2 = this.tokens[this.current + 1];
7033
+ const isAttrSelector = lookhead?.kind === 'identifier' &&
7034
+ (lookhead2?.value === ']' ||
7035
+ (lookhead2 &&
7036
+ ATTR_OPS.has(lookhead2.value) &&
7037
+ this.tokens[this.current + 2]?.kind === 'string'));
7038
+ if (isAttrSelector) {
7039
+ const attrName = this.advance().value;
7040
+ let css = '[' + attrName;
7041
+ if (this.check(']')) {
7042
+ this.advance();
7043
+ css += ']';
7044
+ }
7045
+ else {
7046
+ const op = this.advance().value;
7047
+ const stringTok = this.advance();
7048
+ const raw = stringTok.value;
7049
+ const unquoted = (raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))
7050
+ ? raw.slice(1, -1)
7051
+ : raw;
7052
+ css += op + '"' + unquoted + '"';
7053
+ this.consume(']', "Expected ']' after attribute selector");
7054
+ css += ']';
7055
+ }
7056
+ return this.createSelector(css);
7057
+ }
6708
7058
  const elements = [];
6709
7059
  if (!this.check(']')) {
6710
7060
  do {
@@ -6754,41 +7104,15 @@ class Parser {
6754
7104
  matchOperator: this.matchOperator.bind(this),
6755
7105
  isAtEnd: this.isAtEnd.bind(this),
6756
7106
  createIdentifier: this.createIdentifier.bind(this),
6757
- createLiteral: this.createLiteral.bind(this),
6758
- createSelector: this.createSelector.bind(this),
6759
- createBinaryExpression: this.createBinaryExpression.bind(this),
6760
- createUnaryExpression: this.createUnaryExpression.bind(this),
6761
- createMemberExpression: this.createMemberExpression.bind(this),
6762
- createPossessiveExpression: this.createPossessiveExpression.bind(this),
6763
- createCallExpression: this.createCallExpression.bind(this),
6764
- createErrorNode: this.createErrorNode.bind(this),
6765
- createProgramNode: this.createProgramNode.bind(this),
6766
- createCommandFromIdentifier: this.createCommandFromIdentifier.bind(this),
6767
7107
  parseExpression: this.parseExpression.bind(this),
6768
7108
  parsePrimary: this.parsePrimary.bind(this),
6769
- parseCall: this.parseCall.bind(this),
6770
- parseAssignment: this.parseAssignment.bind(this),
6771
- parseLogicalOr: this.parseLogicalOr.bind(this),
6772
7109
  parseLogicalAnd: this.parseLogicalAnd.bind(this),
6773
- parseEquality: this.parseEquality.bind(this),
6774
- parseComparison: this.parseComparison.bind(this),
6775
- parseAddition: this.parseAddition.bind(this),
6776
- parseMultiplication: this.parseMultiplication.bind(this),
6777
- parseImplicitBinary: this.parseImplicitBinary.bind(this),
6778
- parseConditional: this.parseConditional.bind(this),
6779
- parseConditionalBranch: this.parseConditionalBranch.bind(this),
6780
- parseEventHandler: this.parseEventHandler.bind(this),
6781
- parseBehaviorDefinition: this.parseBehaviorDefinition.bind(this),
6782
- parseNavigationFunction: this.parseNavigationFunction.bind(this),
6783
- parseMyPropertyAccess: this.parseMyPropertyAccess.bind(this),
6784
- parseDollarExpression: this.parseDollarExpression.bind(this),
6785
- parseHyperscriptSelector: this.parseHyperscriptSelector.bind(this),
6786
- parseAttributeOrArrayLiteral: this.parseAttributeOrArrayLiteral.bind(this),
6787
- parseObjectLiteral: this.parseObjectLiteral.bind(this),
6788
7110
  parseCSSObjectLiteral: this.parseCSSObjectLiteral.bind(this),
6789
7111
  parseCommand: this.parseCommand.bind(this),
6790
7112
  parseCommandSequence: this.parseCommandSequence.bind(this),
6791
7113
  parseCommandListUntilEnd: this.parseCommandListUntilEnd.bind(this),
7114
+ parseCommandListUntilEndOrElse: this.parseCommandListUntilEndOrElse.bind(this),
7115
+ parseRepeatBody: this.parseRepeatBody.bind(this),
6792
7116
  getPosition: this.getPosition.bind(this),
6793
7117
  addError: this.addError.bind(this),
6794
7118
  addWarning: this.addWarning.bind(this),
@@ -6831,6 +7155,7 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
6831
7155
  'does not exist',
6832
7156
  'is empty',
6833
7157
  'is not empty',
7158
+ 'ignoring case',
6834
7159
  ]);
6835
7160
  Parser.PRATT_TABLE = PARSER_TABLE;
6836
7161
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];