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