@hyperfixi/core 3.1.1 → 3.2.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 (97) hide show
  1. package/README.md +28 -36
  2. package/dist/ast/command-slots.d.ts +1 -1
  3. package/dist/ast/nodes.d.ts +2 -0
  4. package/dist/ast-utils/index.cjs +193 -48
  5. package/dist/ast-utils/index.mjs +193 -48
  6. package/dist/ast-utils/interchange/types.d.ts +19 -1
  7. package/dist/behaviors/index.cjs +19 -0
  8. package/dist/behaviors/index.mjs +19 -0
  9. package/dist/bundle-generator/index.cjs +24 -9
  10. package/dist/bundle-generator/index.mjs +24 -9
  11. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  12. package/dist/chunks/bridge-Cnj8dQFV.js +2 -0
  13. package/dist/chunks/browser-modular-BNDYF6Dq.js +2 -0
  14. package/dist/chunks/index-DQi2o8Lk.js +2 -0
  15. package/dist/chunks/index-LLuwEQp0.js +2 -0
  16. package/dist/chunks/index-uCOQ8Qn0.js +2 -0
  17. package/dist/commands/animation/transition.d.ts +12 -6
  18. package/dist/commands/helpers/loop-executor.d.ts +2 -2
  19. package/dist/commands/helpers/numeric-target-parser.d.ts +1 -1
  20. package/dist/commands/helpers/variable-access.d.ts +5 -2
  21. package/dist/commands/helpers/write-target.d.ts +5 -0
  22. package/dist/commands/index.cjs +929 -425
  23. package/dist/commands/index.mjs +929 -425
  24. package/dist/commands/templates/render.d.ts +4 -4
  25. package/dist/commands/utility/beep.d.ts +0 -3
  26. package/dist/expressions/index.cjs +7 -13
  27. package/dist/expressions/index.mjs +7 -13
  28. package/dist/htmx/i18n-hooks.d.ts +4 -1
  29. package/dist/hyperfixi-browser-classic-i18n.js +1 -1
  30. package/dist/hyperfixi-browser.js +1 -1
  31. package/dist/hyperfixi-classic-i18n.js +1 -1
  32. package/dist/hyperfixi-hx-v4.js +1 -1
  33. package/dist/hyperfixi-hx.js +1 -1
  34. package/dist/hyperfixi-hybrid-complete.js +1 -1
  35. package/dist/hyperfixi-hybrid-hx.js +1 -1
  36. package/dist/hyperfixi-multilingual.js +1 -1
  37. package/dist/hyperfixi.js +1 -1
  38. package/dist/hyperfixi.mjs +1 -1
  39. package/dist/index.cjs +1140 -485
  40. package/dist/index.min.js +1 -1
  41. package/dist/index.mjs +1140 -485
  42. package/dist/lib/swap-executor.d.ts +1 -1
  43. package/dist/lokascript-browser-classic-i18n.js +1 -1
  44. package/dist/lokascript-browser.js +1 -1
  45. package/dist/lokascript-hybrid-complete.js +1 -1
  46. package/dist/lokascript-hybrid-hx.js +1 -1
  47. package/dist/lokascript-multilingual.js +1 -1
  48. package/dist/metadata.cjs +9 -9
  49. package/dist/metadata.d.ts +1 -1
  50. package/dist/metadata.mjs +9 -9
  51. package/dist/parser/command-grammar.d.ts +1 -0
  52. package/dist/parser/full-parser.cjs +387 -153
  53. package/dist/parser/full-parser.mjs +387 -153
  54. package/dist/parser/helpers/parsing-helpers.d.ts +3 -0
  55. package/dist/parser/hybrid/index.cjs +3 -0
  56. package/dist/parser/hybrid/index.mjs +3 -0
  57. package/dist/parser/hybrid/parser-core.cjs +3 -0
  58. package/dist/parser/hybrid/parser-core.mjs +3 -0
  59. package/dist/parser/hybrid-parser.cjs +3 -0
  60. package/dist/parser/hybrid-parser.mjs +3 -0
  61. package/dist/parser/parser-types.d.ts +2 -0
  62. package/dist/parser/parser.d.ts +9 -0
  63. package/dist/parser/pratt-parser.d.ts +2 -0
  64. package/dist/registry/index.cjs +526 -210
  65. package/dist/registry/index.mjs +526 -210
  66. package/dist/utils/beep.d.ts +8 -0
  67. package/dist/version.d.ts +1 -1
  68. package/package.json +21 -18
  69. package/vocab/htmx/ar.js +22 -20
  70. package/vocab/htmx/bn.js +21 -5
  71. package/vocab/htmx/de.js +33 -10
  72. package/vocab/htmx/en.js +4 -1
  73. package/vocab/htmx/es.js +28 -9
  74. package/vocab/htmx/fr.js +28 -8
  75. package/vocab/htmx/he.js +22 -1
  76. package/vocab/htmx/hi.js +28 -7
  77. package/vocab/htmx/id.js +29 -10
  78. package/vocab/htmx/it.js +26 -5
  79. package/vocab/htmx/ja.js +36 -11
  80. package/vocab/htmx/ko.js +27 -6
  81. package/vocab/htmx/ms.js +24 -1
  82. package/vocab/htmx/pl.js +27 -7
  83. package/vocab/htmx/pt.js +30 -9
  84. package/vocab/htmx/qu.js +32 -9
  85. package/vocab/htmx/ru.js +28 -6
  86. package/vocab/htmx/sw.js +28 -8
  87. package/vocab/htmx/th.js +19 -1
  88. package/vocab/htmx/tl.js +24 -1
  89. package/vocab/htmx/tr.js +34 -8
  90. package/vocab/htmx/uk.js +28 -6
  91. package/vocab/htmx/vi.js +27 -19
  92. package/vocab/htmx/zh.js +28 -9
  93. package/dist/chunks/bridge-D7qrHhV0.js +0 -2
  94. package/dist/chunks/browser-modular-C4LFxfoS.js +0 -2
  95. package/dist/chunks/index-C7lqD4mo.js +0 -2
  96. package/dist/chunks/index-CAPhqA6k.js +0 -2
  97. package/dist/chunks/index-CLdRyZaP.js +0 -2
@@ -899,6 +899,39 @@ function makeTypeCheckHandler(negated) {
899
899
  };
900
900
  };
901
901
  }
902
+ const OF_OPERAND_BP = 70;
903
+ function parseOfOperand(ctx, minBp = OF_OPERAND_BP) {
904
+ let right = ctx.parseExpr(minBp);
905
+ if (ctx.peek()?.value === 'in') {
906
+ const inToken = ctx.advance();
907
+ const scope = ctx.parseExpr(minBp);
908
+ right = {
909
+ type: 'binaryExpression',
910
+ operator: 'in',
911
+ left: right,
912
+ right: scope,
913
+ start: right.start,
914
+ end: scope.end ?? inToken.end,
915
+ line: right.line ?? inToken.line,
916
+ column: right.column ?? inToken.column,
917
+ };
918
+ }
919
+ return right;
920
+ }
921
+ const THE_OF_TARGET_BP = OF_OPERAND_BP + 1;
922
+ function ofHandler(left, token, ctx) {
923
+ const right = parseOfOperand(ctx);
924
+ return {
925
+ type: 'binaryExpression',
926
+ operator: token.value,
927
+ left,
928
+ right,
929
+ start: left.start,
930
+ end: right.end ?? token.end,
931
+ line: left.line ?? token.line,
932
+ column: left.column ?? token.column,
933
+ };
934
+ }
902
935
  const PARSER_COMPARISON_FRAGMENT = new Map([
903
936
  ['===', leftAssoc(30)],
904
937
  ['!==', leftAssoc(30)],
@@ -943,7 +976,7 @@ const PARSER_COMPARISON_FRAGMENT = new Map([
943
976
  ['is greater than or equal to', leftAssoc(30)],
944
977
  ['is less than or equal to', leftAssoc(30)],
945
978
  ['in', leftAssoc(30)],
946
- ['of', rightAssoc(30)],
979
+ ['of', { infix: { bp: [90, OF_OPERAND_BP], handler: ofHandler } }],
947
980
  ['really', leftAssoc(30)],
948
981
  [
949
982
  'exists',
@@ -1119,36 +1152,108 @@ function setElementVar(context, name, value) {
1119
1152
  notifyLocalWrite(name, value, context);
1120
1153
  }
1121
1154
 
1122
- function convertToNumber(value) {
1123
- if (value === null || value === undefined) {
1124
- return 0;
1125
- }
1126
- if (typeof value === 'number') {
1127
- return isFinite(value) ? value : 0;
1128
- }
1129
- if (typeof value === 'string') {
1130
- const parsed = parseFloat(value);
1131
- return parsed;
1132
- }
1133
- if (typeof value === 'boolean') {
1134
- return value ? 1 : 0;
1135
- }
1155
+ function isHTMLElement$2(value) {
1156
+ return (value !== null &&
1157
+ typeof value === 'object' &&
1158
+ 'nodeType' in value &&
1159
+ value.nodeType === 1 &&
1160
+ 'tagName' in value &&
1161
+ typeof value.tagName === 'string' &&
1162
+ 'classList' in value);
1163
+ }
1164
+
1165
+ function beepValues(me, values) {
1166
+ const outputs = values.map(describeBeepValue);
1167
+ const shown = outputs.filter(output => announce(me, output.value));
1168
+ if (shown.length === 0)
1169
+ return outputs;
1170
+ console.group('\ud83d\udd14 beep! Debug Output');
1171
+ for (const output of shown) {
1172
+ console.log(`Value:`, output.value);
1173
+ console.log(`Type:`, output.type);
1174
+ console.log(`Representation:`, output.representation);
1175
+ console.log('---');
1176
+ }
1177
+ console.groupEnd();
1178
+ return outputs;
1179
+ }
1180
+ function announce(me, value) {
1181
+ const target = me;
1182
+ if (typeof CustomEvent !== 'function' || typeof target?.dispatchEvent !== 'function')
1183
+ return true;
1184
+ return target.dispatchEvent(new CustomEvent('hyperscript:beep', {
1185
+ bubbles: true,
1186
+ cancelable: true,
1187
+ detail: { element: me, value },
1188
+ }));
1189
+ }
1190
+ function describeBeepValue(value) {
1191
+ return { value, type: typeOf(value), representation: representationOf(value) };
1192
+ }
1193
+ function typeOf(value) {
1194
+ if (value === null)
1195
+ return 'null';
1196
+ if (value === undefined)
1197
+ return 'undefined';
1198
+ if (Array.isArray(value))
1199
+ return 'array';
1200
+ if (isHTMLElement$2(value))
1201
+ return 'HTMLElement';
1202
+ if (typeof Element !== 'undefined' && value instanceof Element)
1203
+ return 'Element';
1204
+ if (typeof Node !== 'undefined' && value instanceof Node)
1205
+ return 'Node';
1206
+ if (value instanceof Error)
1207
+ return 'Error';
1208
+ if (value instanceof Date)
1209
+ return 'Date';
1210
+ if (value instanceof RegExp)
1211
+ return 'RegExp';
1212
+ return typeof value;
1213
+ }
1214
+ function representationOf(value) {
1215
+ if (value === null)
1216
+ return 'null';
1217
+ if (value === undefined)
1218
+ return 'undefined';
1136
1219
  if (Array.isArray(value)) {
1137
- return value.length;
1138
- }
1220
+ return `Array(${value.length}) [${value
1221
+ .slice(0, 3)
1222
+ .map(v => representationOf(v))
1223
+ .join(', ')}${value.length > 3 ? '...' : ''}]`;
1224
+ }
1225
+ if (isHTMLElement$2(value)) {
1226
+ const el = value;
1227
+ const tag = el.tagName.toLowerCase();
1228
+ const id = el.id ? `#${el.id}` : '';
1229
+ const classes = el.className ? `.${el.className.split(' ').join('.')}` : '';
1230
+ return `<${tag}${id}${classes}/>`;
1231
+ }
1232
+ if (value instanceof Error)
1233
+ return `Error: ${value.message}`;
1234
+ if (typeof value === 'string')
1235
+ return value.length > 50 ? `"${value.substring(0, 47)}..."` : `"${value}"`;
1139
1236
  if (typeof value === 'object') {
1140
- if ('length' in value && typeof value.length === 'number') {
1141
- return value.length;
1237
+ try {
1238
+ const keys = Object.keys(value);
1239
+ return `Object {${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}}`;
1142
1240
  }
1143
- if (typeof value.valueOf === 'function') {
1144
- const result = value.valueOf();
1145
- if (typeof result === 'number') {
1146
- return result;
1147
- }
1241
+ catch {
1242
+ return '[Object]';
1148
1243
  }
1149
- return NaN;
1150
1244
  }
1151
- return 0;
1245
+ return String(value);
1246
+ }
1247
+
1248
+ function counterAmount(value) {
1249
+ return typeof value === 'number' ? value : parseFloat(String(value));
1250
+ }
1251
+ function counterValue(value) {
1252
+ if (isHTMLElement$2(value)) {
1253
+ const element = value;
1254
+ return counterValue(element.value || element.textContent);
1255
+ }
1256
+ return value ? counterAmount(value) : 0;
1152
1257
  }
1153
1258
 
1154
1259
  function resolveTargetElements(matches, selector, fromQuery) {
@@ -1514,13 +1619,6 @@ function isFunction(value) {
1514
1619
  function isInputElement(el) {
1515
1620
  return (el !== null && typeof el === 'object' && 'tagName' in el && el.tagName === 'INPUT');
1516
1621
  }
1517
- function isFormElement(el) {
1518
- if (el === null || typeof el !== 'object' || !('tagName' in el)) {
1519
- return false;
1520
- }
1521
- const tag = el.tagName;
1522
- return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA';
1523
- }
1524
1622
  function isHTMLElement$1(el) {
1525
1623
  return (el !== null &&
1526
1624
  typeof el === 'object' &&
@@ -1580,7 +1678,7 @@ const SPECIAL_DOM_PROPERTIES = {
1580
1678
  innertext: el => el.textContent?.trim(),
1581
1679
  innerHTML: el => el.innerHTML,
1582
1680
  outerhtml: el => el.outerHTML,
1583
- value: el => (isFormElement(el) ? el.value : undefined),
1681
+ value: el => ('value' in el ? el.value : undefined),
1584
1682
  checked: el => (isInputElement(el) ? el.checked : undefined),
1585
1683
  disabled: el => ('disabled' in el ? el.disabled : undefined),
1586
1684
  selected: el => (isOptionElement(el) ? el.selected : undefined),
@@ -2248,14 +2346,15 @@ const closestExpression = {
2248
2346
  if (typeof selector !== 'string') {
2249
2347
  throw new Error('closest requires a string selector');
2250
2348
  }
2251
- if (!context.me) {
2349
+ const start = args.length > 1 ? args[1] : context.me;
2350
+ if (!(start instanceof Element)) {
2252
2351
  return null;
2253
2352
  }
2254
- return context.me.closest(selector);
2353
+ return start.closest(selector);
2255
2354
  },
2256
2355
  validate(args) {
2257
- if (args.length !== 1) {
2258
- return 'closest requires exactly 1 argument (selector)';
2356
+ if (args.length !== 1 && args.length !== 2) {
2357
+ return 'closest requires a selector and, optionally, the element to start from';
2259
2358
  }
2260
2359
  const arg = args[0];
2261
2360
  if (typeof arg === 'string')
@@ -4641,7 +4740,7 @@ const isEmptyExpression = {
4641
4740
  else if (value instanceof Node || value instanceof Element)
4642
4741
  result = false;
4643
4742
  else if (isObject(value))
4644
- result = Object.keys(value).length === 0;
4743
+ result = value.length === 0;
4645
4744
  else
4646
4745
  result = false;
4647
4746
  return result;
@@ -5718,6 +5817,48 @@ function parseMaybeNamedArgument(ctx) {
5718
5817
  const value = ctx.parseExpression();
5719
5818
  return name !== undefined ? { name, value } : { value };
5720
5819
  }
5820
+ function isNakedNamedArgStart(ctx) {
5821
+ if (ctx.check('{'))
5822
+ return false;
5823
+ if (!ctx.checkIdentifierLike())
5824
+ return false;
5825
+ const next = ctx.peekAt(1);
5826
+ return next !== null && next.value === ':';
5827
+ }
5828
+ function parseNakedNamedArgs(ctx, parseValue, command) {
5829
+ const properties = [];
5830
+ const startPos = ctx.getPosition();
5831
+ do {
5832
+ if (!ctx.checkIdentifierLike())
5833
+ break;
5834
+ const keyToken = ctx.advance();
5835
+ const key = {
5836
+ type: 'identifier',
5837
+ name: keyToken.value,
5838
+ start: keyToken.start,
5839
+ end: keyToken.end,
5840
+ line: keyToken.line,
5841
+ column: keyToken.column,
5842
+ };
5843
+ ctx.consume(':', `Expected ':' after property name in ${command} named arguments`);
5844
+ const value = parseValue();
5845
+ if (value) {
5846
+ properties.push({ key, value });
5847
+ }
5848
+ } while (ctx.match(',') && !ctx.isAtEnd());
5849
+ const endPos = ctx.getPosition();
5850
+ return {
5851
+ type: 'objectLiteral',
5852
+ properties,
5853
+ start: startPos.start,
5854
+ end: endPos.end,
5855
+ line: startPos.line,
5856
+ column: startPos.column,
5857
+ };
5858
+ }
5859
+ function isGluedBeepBang(beep, bang) {
5860
+ return beep?.value === 'beep' && bang?.value === '!' && bang.start === beep.end;
5861
+ }
5721
5862
 
5722
5863
  class CommandNodeBuilder {
5723
5864
  constructor(name) {
@@ -5919,6 +6060,8 @@ function parseTriggerCommand(ctx, identifierNode) {
5919
6060
  const modifiers = {};
5920
6061
  while (!isCommandBoundary(ctx)) {
5921
6062
  if (ctx.check(KEYWORDS.ON) || ctx.check(KEYWORDS.TO)) {
6063
+ if (modifiers['on'])
6064
+ break;
5922
6065
  ctx.advance();
5923
6066
  const target = parseOneArgument(ctx, [KEYWORDS.WITH]);
5924
6067
  if (target)
@@ -6059,6 +6202,9 @@ function parseRepeatCommand(ctx, commandToken) {
6059
6202
  ctx.advance();
6060
6203
  loopType = KEYWORDS.FOREVER;
6061
6204
  }
6205
+ else if (ctx.checkIsCommand() || ctx.isCommand(ctx.peek().value)) {
6206
+ loopType = KEYWORDS.FOREVER;
6207
+ }
6062
6208
  else {
6063
6209
  times = ctx.parseExpression();
6064
6210
  if (ctx.check(KEYWORDS.TIMES)) {
@@ -6101,10 +6247,12 @@ function parseRepeatCommand(ctx, commandToken) {
6101
6247
  loopType = result.terminator;
6102
6248
  ctx.advance();
6103
6249
  condition = ctx.parseExpression();
6104
- if (!ctx.check('end')) {
6250
+ if (ctx.check('end')) {
6251
+ ctx.advance();
6252
+ }
6253
+ else if (!ctx.atEndOfInput()) {
6105
6254
  throw new Error('Expected "end" to close repeat block');
6106
6255
  }
6107
- ctx.advance();
6108
6256
  }
6109
6257
  }
6110
6258
  else {
@@ -6350,7 +6498,7 @@ function parseIfCommand(ctx, commandToken) {
6350
6498
  }));
6351
6499
  }
6352
6500
  }
6353
- if (!consumedElseIf && !ctx.isAtEnd()) {
6501
+ if (!consumedElseIf && !ctx.atEndOfInput()) {
6354
6502
  ctx.consume(KEYWORDS.END, "Expected 'end' after if block");
6355
6503
  }
6356
6504
  }
@@ -6375,7 +6523,6 @@ function parseIfCommand(ctx, commandToken) {
6375
6523
  .build();
6376
6524
  }
6377
6525
  function parseForCommand(ctx, commandToken) {
6378
- const args = [];
6379
6526
  let variable = null;
6380
6527
  let collection = null;
6381
6528
  if (ctx.check(KEYWORDS.EACH)) {
@@ -6418,31 +6565,22 @@ function parseForCommand(ctx, commandToken) {
6418
6565
  }
6419
6566
  }
6420
6567
  const commands = ctx.parseCommandListUntilEnd('for');
6421
- args.push({
6422
- type: 'identifier',
6423
- name: 'for',
6424
- start: commandToken.start,
6425
- end: commandToken.end,
6426
- line: commandToken.line,
6427
- column: commandToken.column,
6428
- });
6429
- const forPos = {
6568
+ const pos = {
6430
6569
  start: commandToken.start,
6431
6570
  end: commandToken.end,
6432
6571
  line: commandToken.line,
6433
6572
  column: commandToken.column,
6434
6573
  };
6435
- args.push(createStringLiteral(variable, forPos));
6436
- args.push(collection);
6437
- if (indexVariable) {
6438
- args.push(createStringLiteral(indexVariable, forPos));
6439
- }
6440
- args.push(createBlock(commands, { ...forPos, end: forPos.end || 0 }));
6441
- return CommandNodeBuilder.from({
6442
- ...commandToken,
6443
- value: 'repeat',
6444
- })
6445
- .withArgs(...args)
6574
+ const modifiers = {
6575
+ loopType: toLegacyExpression(createStringLiteral(KEYWORDS.FOR, pos)),
6576
+ for: toLegacyExpression(createStringLiteral(variable, pos)),
6577
+ in: collection,
6578
+ };
6579
+ if (indexVariable)
6580
+ modifiers['index'] = toLegacyExpression(createStringLiteral(indexVariable, pos));
6581
+ return CommandNodeBuilder.from({ ...commandToken, value: 'repeat' })
6582
+ .withArgs(createBlock(commands, { ...pos, end: pos.end || 0 }))
6583
+ .withModifiers(modifiers)
6446
6584
  .endingAt(ctx.getPosition())
6447
6585
  .build();
6448
6586
  }
@@ -6498,86 +6636,159 @@ function parseMeasureCommand(ctx, identifierNode) {
6498
6636
  }
6499
6637
  return builder.build();
6500
6638
  }
6501
- const CONTEXT_POSSESSIVES = new Set(['my', 'its', 'your']);
6639
+ const CONTEXT_REFS = new Set(['me', 'it', 'you', 'result']);
6502
6640
  function parseTransitionCommand(ctx, commandToken) {
6503
- const args = [];
6504
- const modifiers = {};
6505
- let property = null;
6506
- let target = null;
6507
- const leadToken = ctx.peek();
6508
- const startsTarget = ctx.checkAnySelector() ||
6509
- ctx.checkContextVar() ||
6510
- ctx.check('<') ||
6511
- (isIdentifierLike(leadToken) && CONTEXT_POSSESSIVES.has(leadToken.value));
6512
- if (startsTarget) {
6513
- const parsed = ctx.parsePrimary();
6514
- if ((parsed.type === 'possessiveExpression' ||
6515
- (parsed.type === 'memberExpression' && !parsed.computed)) &&
6516
- parsed.object &&
6517
- parsed.property?.name) {
6518
- target = parsed.object;
6519
- property = {
6520
- type: 'string',
6521
- value: parsed.property.name,
6522
- start: leadToken.start || 0,
6523
- end: ctx.getPosition().end,
6524
- line: leadToken.line,
6525
- column: leadToken.column,
6526
- };
6641
+ const pairs = [];
6642
+ do {
6643
+ pairs.push(parseTransitionPair(ctx));
6644
+ } while (startsAnotherPair(ctx));
6645
+ const [first, ...rest] = pairs;
6646
+ const args = first.owner ? [first.owner, first.property] : [first.property];
6647
+ const modifiers = { to: first.to };
6648
+ if (first.from)
6649
+ modifiers['from'] = first.from;
6650
+ if (rest.length > 0)
6651
+ modifiers['pairs'] = pairsNode(rest);
6652
+ for (let i = 0; i < 3 && !ctx.isAtEnd(); i++) {
6653
+ if (ctx.check('over') && !modifiers['over']) {
6654
+ ctx.advance();
6655
+ modifiers['over'] = ctx.parseExpression();
6527
6656
  }
6528
- else {
6529
- target = parsed;
6530
- if (ctx.check("'s"))
6531
- ctx.advance();
6657
+ else if (ctx.check('using') && !modifiers['using']) {
6658
+ ctx.advance();
6659
+ modifiers['using'] = ctx.parseExpression();
6532
6660
  }
6533
- }
6534
- const firstToken = ctx.peek();
6535
- if (!property && (isIdentifierLike(firstToken) || firstToken.value === '*')) {
6536
- let propertyValue = '';
6537
- if (ctx.check('*')) {
6538
- propertyValue = '*';
6661
+ else if (ctx.check(KEYWORDS.WITH) && !modifiers['with']) {
6539
6662
  ctx.advance();
6663
+ modifiers['with'] = ctx.parsePrimary();
6540
6664
  }
6541
- const hyphenatedName = parseHyphenatedName(ctx);
6542
- if (hyphenatedName) {
6543
- propertyValue += hyphenatedName;
6544
- property = {
6545
- type: 'string',
6546
- value: propertyValue,
6547
- start: firstToken.start || 0,
6548
- end: ctx.getPosition().end,
6549
- line: firstToken.line,
6550
- column: firstToken.column,
6551
- };
6665
+ else {
6666
+ break;
6552
6667
  }
6553
6668
  }
6554
- if (!property) {
6555
- throw new Error('Transition command requires a CSS property');
6669
+ return CommandNodeBuilder.from(commandToken)
6670
+ .withArgs(...args)
6671
+ .withModifiers(modifiers)
6672
+ .endingAt(ctx.getPosition())
6673
+ .build();
6674
+ }
6675
+ function parseTransitionPair(ctx) {
6676
+ const { owner, property } = parseTransitionProperty(ctx);
6677
+ let from = null;
6678
+ if (ctx.check('from')) {
6679
+ ctx.advance();
6680
+ from = ctx.parseExpression();
6556
6681
  }
6557
- if (target)
6558
- args.push(target);
6559
- args.push(property);
6560
6682
  if (!ctx.check(KEYWORDS.TO)) {
6561
6683
  throw new Error('Expected "to" keyword after property in transition command');
6562
6684
  }
6563
6685
  ctx.advance();
6564
- const value = ctx.parseExpression();
6565
- modifiers['to'] = value;
6566
- if (ctx.check('over')) {
6567
- ctx.advance();
6568
- const duration = ctx.parseExpression();
6569
- modifiers['over'] = duration;
6686
+ const to = ctx.parseExpression();
6687
+ return { owner, property, from, to };
6688
+ }
6689
+ function parseTransitionProperty(ctx) {
6690
+ const lead = ctx.peek();
6691
+ if (!lead || isKeyword(lead, [KEYWORDS.TO, 'from'])) {
6692
+ throw new Error('Transition command requires a CSS property');
6570
6693
  }
6571
- if (ctx.check(KEYWORDS.WITH)) {
6694
+ const expr = ctx.parseExpression();
6695
+ const split = splitStyleExpression(expr);
6696
+ if (split)
6697
+ return { owner: split.owner, property: propertyNode(split.name, lead, ctx) };
6698
+ let name = '';
6699
+ if (ctx.check('*')) {
6700
+ name = '*';
6572
6701
  ctx.advance();
6573
- const timingFunction = ctx.parsePrimary();
6574
- modifiers['with'] = timingFunction;
6575
6702
  }
6576
- return CommandNodeBuilder.from(commandToken)
6577
- .withArgs(...args)
6578
- .withModifiers(modifiers)
6579
- .endingAt(ctx.getPosition())
6580
- .build();
6703
+ const hyphenated = parseHyphenatedName(ctx);
6704
+ if (!hyphenated)
6705
+ throw new Error('Transition command requires a CSS property');
6706
+ return { owner: expr, property: propertyNode(name + hyphenated, lead, ctx) };
6707
+ }
6708
+ function splitStyleExpression(node) {
6709
+ const n = node;
6710
+ switch (n.type) {
6711
+ case 'selector':
6712
+ return typeof n.value === 'string' && n.value.startsWith('*')
6713
+ ? { owner: null, name: n.value }
6714
+ : null;
6715
+ case 'identifier':
6716
+ return typeof n.name === 'string' && !CONTEXT_REFS.has(n.name)
6717
+ ? { owner: null, name: n.name }
6718
+ : null;
6719
+ case 'possessiveExpression':
6720
+ case 'memberExpression':
6721
+ return !n.computed && n.object && typeof n.property?.name === 'string'
6722
+ ? { owner: n.object, name: n.property.name }
6723
+ : null;
6724
+ case 'binaryExpression': {
6725
+ if (n.operator !== 'of' || !n.left || !n.right)
6726
+ return null;
6727
+ const inner = splitStyleExpression(n.left);
6728
+ return inner && !inner.owner ? { owner: n.right, name: inner.name } : null;
6729
+ }
6730
+ case 'propertyOfExpression':
6731
+ return n.target && typeof n.property?.name === 'string'
6732
+ ? { owner: n.target, name: n.property.name }
6733
+ : null;
6734
+ default:
6735
+ return null;
6736
+ }
6737
+ }
6738
+ function propertyNode(name, lead, ctx) {
6739
+ return {
6740
+ type: 'string',
6741
+ value: name,
6742
+ start: lead.start || 0,
6743
+ end: ctx.getPosition().end,
6744
+ line: lead.line,
6745
+ column: lead.column,
6746
+ };
6747
+ }
6748
+ function startsAnotherPair(ctx) {
6749
+ if (ctx.isAtEnd() || isComment(ctx.peek()))
6750
+ return false;
6751
+ if (isKeyword(ctx.peek(), ['over', 'using', KEYWORDS.WITH]))
6752
+ return false;
6753
+ return !isCommandBoundary(ctx, ['catch', 'finally', 'on']);
6754
+ }
6755
+ function pairsNode(pairs) {
6756
+ const field = (name, value) => ({
6757
+ key: {
6758
+ type: 'identifier',
6759
+ name,
6760
+ start: value.start,
6761
+ end: value.end,
6762
+ line: value.line,
6763
+ column: value.column,
6764
+ },
6765
+ value,
6766
+ });
6767
+ const elements = pairs.map(pair => {
6768
+ const properties = [field('property', pair.property), field('to', pair.to)];
6769
+ if (pair.owner)
6770
+ properties.push(field('owner', pair.owner));
6771
+ if (pair.from)
6772
+ properties.push(field('from', pair.from));
6773
+ return {
6774
+ type: 'objectLiteral',
6775
+ properties,
6776
+ start: (pair.owner ?? pair.property).start,
6777
+ end: pair.to.end,
6778
+ line: (pair.owner ?? pair.property).line,
6779
+ column: (pair.owner ?? pair.property).column,
6780
+ };
6781
+ });
6782
+ const firstEl = elements[0];
6783
+ const lastEl = elements[elements.length - 1];
6784
+ return {
6785
+ type: 'arrayLiteral',
6786
+ elements,
6787
+ start: firstEl.start,
6788
+ end: lastEl.end,
6789
+ line: firstEl.line,
6790
+ column: firstEl.column,
6791
+ };
6581
6792
  }
6582
6793
  function parseStartCommand(ctx, identifierNode) {
6583
6794
  if (!ctx.match('view')) {
@@ -6874,11 +7085,17 @@ function parseSwapCommand(ctx, identifierNode) {
6874
7085
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
6875
7086
  if (targetExpr)
6876
7087
  args.push(targetExpr);
6877
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
7088
+ const isMorph = identifierNode.name === 'morph';
7089
+ if (!isDelete &&
7090
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
7091
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
6878
7092
  const contentExpr = parseOneArgument(ctx, ['using']);
6879
7093
  if (contentExpr)
6880
7094
  modifiers['with'] = contentExpr;
6881
7095
  }
7096
+ if (isMorph && modifiers['with'] === undefined) {
7097
+ ctx.addError("Expected 'to' and the content to morph into");
7098
+ }
6882
7099
  return CommandNodeBuilder.fromIdentifier(identifierNode)
6883
7100
  .withArgs(...args)
6884
7101
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -7639,6 +7856,8 @@ function atArgumentBoundary(ctx, grammar) {
7639
7856
  const next = ctx.peek();
7640
7857
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
7641
7858
  return false;
7859
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
7860
+ return false;
7642
7861
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
7643
7862
  return true;
7644
7863
  return isKeyword(next, [...grammar.markers]);
@@ -7670,10 +7889,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
7670
7889
  }
7671
7890
  }
7672
7891
  const commaList = new Set(grammar.commaList ?? []);
7892
+ const namedArgs = new Set(grammar.namedArgs ?? []);
7673
7893
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
7674
7894
  const keyword = ctx.advance().value;
7675
7895
  const key = ctx.resolveKeyword(keyword).toLowerCase();
7676
- let value = ctx.parseExpression();
7896
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
7897
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
7898
+ : ctx.parseExpression();
7677
7899
  if (value && commaList.has(key) && ctx.check(',')) {
7678
7900
  const elements = [value];
7679
7901
  while (ctx.match(',')) {
@@ -7838,8 +8060,8 @@ function parseFetchCommand(ctx, commandToken) {
7838
8060
  }
7839
8061
  if (ctx.check('with') && !modifiers['with']) {
7840
8062
  ctx.advance();
7841
- if (isFetchNakedNamedArgStart(ctx)) {
7842
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
8063
+ if (isNakedNamedArgStart(ctx)) {
8064
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
7843
8065
  }
7844
8066
  else {
7845
8067
  modifiers['with'] = ctx.parsePrimary();
@@ -7874,45 +8096,6 @@ function parseFetchCommand(ctx, commandToken) {
7874
8096
  }
7875
8097
  return builder.build();
7876
8098
  }
7877
- function isFetchNakedNamedArgStart(ctx) {
7878
- if (ctx.check('{'))
7879
- return false;
7880
- if (!ctx.checkIdentifierLike())
7881
- return false;
7882
- const next = ctx.peekAt(1);
7883
- return next !== null && next.value === ':';
7884
- }
7885
- function parseFetchNakedNamedArgs(ctx) {
7886
- const properties = [];
7887
- const startPos = ctx.getPosition();
7888
- do {
7889
- if (!ctx.checkIdentifierLike())
7890
- break;
7891
- const keyToken = ctx.advance();
7892
- const key = {
7893
- type: 'identifier',
7894
- name: keyToken.value,
7895
- start: keyToken.start,
7896
- end: keyToken.end,
7897
- line: keyToken.line,
7898
- column: keyToken.column,
7899
- };
7900
- ctx.consume(':', "Expected ':' after property name in fetch named arguments");
7901
- const value = ctx.parsePrimary();
7902
- if (value) {
7903
- properties.push({ key, value });
7904
- }
7905
- } while (ctx.match(',') && !ctx.isAtEnd());
7906
- const endPos = ctx.getPosition();
7907
- return {
7908
- type: 'objectLiteral',
7909
- properties,
7910
- start: startPos.start,
7911
- end: endPos.end,
7912
- line: startPos.line,
7913
- column: startPos.column,
7914
- };
7915
- }
7916
8099
  function findJsEndBoundary(ctx, startPos) {
7917
8100
  const input = ctx.getInputSlice(startPos);
7918
8101
  if (!input) {
@@ -8032,6 +8215,7 @@ function parseTellCommand(ctx, identifierNode) {
8032
8215
  const commands = [];
8033
8216
  while (!ctx.isAtEnd()) {
8034
8217
  if (ctx.checkIsCommand()) {
8218
+ const word = ctx.peek().value;
8035
8219
  try {
8036
8220
  ctx.advance();
8037
8221
  const cmd = ctx.parseCommand();
@@ -8042,7 +8226,8 @@ function parseTellCommand(ctx, identifierNode) {
8042
8226
  break;
8043
8227
  }
8044
8228
  }
8045
- catch {
8229
+ catch (error) {
8230
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
8046
8231
  break;
8047
8232
  }
8048
8233
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -8191,7 +8376,6 @@ const ONE_EXPR = (syntax) => ({
8191
8376
  markers: [],
8192
8377
  syntax,
8193
8378
  });
8194
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
8195
8379
  const COMMAND_GRAMMAR = {
8196
8380
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
8197
8381
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -8202,8 +8386,7 @@ const COMMAND_GRAMMAR = {
8202
8386
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
8203
8387
  },
8204
8388
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
8205
- 'beep!': BEEP,
8206
- beep: BEEP,
8389
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
8207
8390
  blur: ONE_EXPR('blur [<target>]'),
8208
8391
  break: NONE,
8209
8392
  breakpoint: NONE,
@@ -8227,7 +8410,8 @@ const COMMAND_GRAMMAR = {
8227
8410
  render: {
8228
8411
  positional: 'expression',
8229
8412
  markers: ['with'],
8230
- syntax: 'render <template> [with <variables>]',
8413
+ namedArgs: ['with'],
8414
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
8231
8415
  },
8232
8416
  reset: ONE_EXPR('reset [<form>]'),
8233
8417
  return: ONE_EXPR('return [<value>]'),
@@ -8582,7 +8766,10 @@ class Parser {
8582
8766
  const firstToken = this.peek();
8583
8767
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
8584
8768
  let left;
8585
- if (prefixEntry?.prefix) {
8769
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
8770
+ left = this.parseBeepExpression();
8771
+ }
8772
+ else if (prefixEntry?.prefix) {
8586
8773
  const token = this.advance();
8587
8774
  if (this.isAtEnd()) {
8588
8775
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -8602,6 +8789,9 @@ class Parser {
8602
8789
  continue;
8603
8790
  }
8604
8791
  }
8792
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
8793
+ break;
8794
+ }
8605
8795
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
8606
8796
  if (!infixEntry?.infix) {
8607
8797
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -8663,6 +8853,35 @@ class Parser {
8663
8853
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
8664
8854
  return left;
8665
8855
  }
8856
+ parseBeepExpression() {
8857
+ const beepToken = this.advance();
8858
+ this.advance();
8859
+ if (this.isAtEnd()) {
8860
+ this.addError("Expected expression after 'beep!'");
8861
+ return this.createErrorNode();
8862
+ }
8863
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
8864
+ return {
8865
+ type: 'unaryExpression',
8866
+ operator: 'beep!',
8867
+ operand,
8868
+ argument: operand,
8869
+ prefix: true,
8870
+ start: beepToken.start,
8871
+ end: this.previous().end,
8872
+ line: beepToken.line,
8873
+ column: beepToken.column,
8874
+ };
8875
+ }
8876
+ isStyleRefStar(i) {
8877
+ const star = this.tokens[i];
8878
+ const name = this.tokens[i + 1];
8879
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
8880
+ }
8881
+ isSpacedStyleRef(i) {
8882
+ const prev = this.tokens[i - 1];
8883
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
8884
+ }
8666
8885
  tryParseStringPostfix(left, nextToken) {
8667
8886
  const v = nextToken.value;
8668
8887
  let unit = null;
@@ -8865,6 +9084,10 @@ class Parser {
8865
9084
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
8866
9085
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
8867
9086
  while (!this.isAtEnd() && !isStop()) {
9087
+ if (this.checkComment()) {
9088
+ this.advance();
9089
+ continue;
9090
+ }
8868
9091
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
8869
9092
  let parsedCommand = false;
8870
9093
  const isCommandToken = this.checkIsCommand();
@@ -8948,7 +9171,7 @@ class Parser {
8948
9171
  debug.parse('\u2705 Found "end", consuming it');
8949
9172
  this.advance();
8950
9173
  }
8951
- else {
9174
+ else if (!this.atEndOfInput()) {
8952
9175
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
8953
9176
  throw new Error(`Expected "end" to close ${construct} block`);
8954
9177
  }
@@ -8961,7 +9184,7 @@ class Parser {
8961
9184
  if (this.check('end')) {
8962
9185
  this.advance();
8963
9186
  }
8964
- else {
9187
+ else if (!this.atEndOfInput()) {
8965
9188
  throw new Error('Expected "end" to close repeat block');
8966
9189
  }
8967
9190
  }
@@ -8977,7 +9200,10 @@ class Parser {
8977
9200
  this.advance();
8978
9201
  }
8979
9202
  else if (terminator === '') {
8980
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
9203
+ if (!this.atEndOfInput()) {
9204
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
9205
+ }
9206
+ return { commands, terminator: 'end' };
8981
9207
  }
8982
9208
  return { commands, terminator };
8983
9209
  }
@@ -9561,7 +9787,9 @@ class Parser {
9561
9787
  parseTopLevelInitBlock() {
9562
9788
  const pos = this.getPosition();
9563
9789
  const initCommands = this.parseCommandBlock(['end']);
9564
- this.consume('end', "Expected 'end' after init block");
9790
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
9791
+ this.consume('end', "Expected 'end' after init block");
9792
+ }
9565
9793
  return {
9566
9794
  type: 'initBlock',
9567
9795
  commands: initCommands,
@@ -9737,10 +9965,29 @@ class Parser {
9737
9965
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
9738
9966
  };
9739
9967
  }
9968
+ isArgumentListOnly(open) {
9969
+ let depth = 0;
9970
+ for (let i = open; i < this.tokens.length; i++) {
9971
+ const token = this.tokens[i];
9972
+ if (token.kind !== 'operator')
9973
+ continue;
9974
+ if (token.value === '(' || token.value === '[' || token.value === '{')
9975
+ depth++;
9976
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
9977
+ if (--depth === 0)
9978
+ return i === open + 1;
9979
+ }
9980
+ else if (token.value === ',' && depth === 1)
9981
+ return true;
9982
+ }
9983
+ return false;
9984
+ }
9740
9985
  tryParsePseudoCommand(callExpr, sourceExpr) {
9741
9986
  const methodName = callExpr.callee.name;
9742
9987
  const nextToken = this.peek();
9743
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
9988
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
9989
+ const hasPseudoCommandPattern = nextIsArticle ||
9990
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
9744
9991
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
9745
9992
  isContextVar(nextToken);
9746
9993
  if (!hasPseudoCommandPattern) {
@@ -9766,7 +10013,10 @@ class Parser {
9766
10013
  });
9767
10014
  }
9768
10015
  let preposition;
9769
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
10016
+ if (nextIsArticle) {
10017
+ this.advance();
10018
+ }
10019
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
9770
10020
  preposition = this.advance().value.toLowerCase();
9771
10021
  }
9772
10022
  let targetExpr;
@@ -9930,10 +10180,22 @@ class Parser {
9930
10180
  this.consume(']', "Expected ']' after event condition");
9931
10181
  }
9932
10182
  let target;
10183
+ let targetExpression;
9933
10184
  if (this.match('from')) {
9934
- const targetToken = this.advance();
9935
- target = targetToken.value;
9936
- debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target} (kind: ${targetToken.kind})`);
10185
+ if (this.check('the'))
10186
+ this.advance();
10187
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
10188
+ const startIndex = this.current;
10189
+ targetExpression = this.parseExpression();
10190
+ target = this.tokens
10191
+ .slice(startIndex, this.current)
10192
+ .map(t => t.value)
10193
+ .join(' ');
10194
+ }
10195
+ else {
10196
+ target = this.advance().value;
10197
+ }
10198
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
9937
10199
  this.parseTemporalKeywordModifiers(modifiers);
9938
10200
  }
9939
10201
  let attributeName;
@@ -9979,7 +10241,7 @@ class Parser {
9979
10241
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
9980
10242
  const commandName = this.peek().value.toLowerCase();
9981
10243
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
9982
- if (nextIsOpenParen && !isSpecialBodyCommand) {
10244
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
9983
10245
  let expr;
9984
10246
  const savedError = this.error;
9985
10247
  try {
@@ -10011,7 +10273,7 @@ class Parser {
10011
10273
  }
10012
10274
  }
10013
10275
  }
10014
- else if (this.checkIdentifier()) {
10276
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
10015
10277
  const token = this.peek();
10016
10278
  if (this.isCommand(token.value)) {
10017
10279
  const cmd = this.parseCommandWithErrorRecovery();
@@ -10120,6 +10382,7 @@ class Parser {
10120
10382
  ...(eventParams.length > 0 && { args: eventParams }),
10121
10383
  ...(condition && { condition: fromLegacyExpression(condition) }),
10122
10384
  ...(target && { target }),
10385
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
10123
10386
  ...(attributeName && { attributeName }),
10124
10387
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
10125
10388
  ...(customEventSource && { customEventSource }),
@@ -10375,10 +10638,9 @@ class Parser {
10375
10638
  }
10376
10639
  parseCommandCore() {
10377
10640
  const commandToken = this.previous();
10378
- let commandName = commandToken.value;
10641
+ const commandName = commandToken.value;
10379
10642
  if (commandName === 'beep' && this.check('!')) {
10380
10643
  this.advance();
10381
- commandName = 'beep!';
10382
10644
  }
10383
10645
  if (commandName === 'fetch') {
10384
10646
  return parseFetchCommand(this.getContext(), commandToken);
@@ -10477,6 +10739,9 @@ class Parser {
10477
10739
  args.push(this.parsePrimary());
10478
10740
  }
10479
10741
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
10742
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
10743
+ callNode.closestTo = this.parseExpression();
10744
+ }
10480
10745
  if (funcName === 'next' || funcName === 'previous') {
10481
10746
  const relativePositional = this.parseRelativePositionalModifiers();
10482
10747
  if (relativePositional) {
@@ -10516,6 +10781,9 @@ class Parser {
10516
10781
  }
10517
10782
  parseTheXofY() {
10518
10783
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
10784
+ if (this.isStyleRefStar(this.current)) {
10785
+ return this.parsePrimary();
10786
+ }
10519
10787
  if (!this.checkIdentifier()) {
10520
10788
  return this.createIdentifier('the');
10521
10789
  }
@@ -10531,7 +10799,7 @@ class Parser {
10531
10799
  return this.createIdentifier(propertyName);
10532
10800
  }
10533
10801
  this.advance();
10534
- const target = this.parseCall();
10802
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
10535
10803
  return {
10536
10804
  type: 'propertyOfExpression',
10537
10805
  property: { type: 'identifier', name: propertyName },
@@ -10729,6 +10997,13 @@ class Parser {
10729
10997
  checkLiteral() {
10730
10998
  return this.checkPredicate(isLiteral);
10731
10999
  }
11000
+ checkEventNamedCall() {
11001
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
11002
+ return false;
11003
+ const token = this.peek();
11004
+ const next = this.tokens[this.current + 1];
11005
+ return next?.value === '(' && next.start === token.end;
11006
+ }
10732
11007
  checkIdentifierLike() {
10733
11008
  return this.checkPredicate(isIdentifierLike);
10734
11009
  }
@@ -10837,6 +11112,22 @@ class Parser {
10837
11112
  isAtEnd() {
10838
11113
  return this.current >= this.tokens.length;
10839
11114
  }
11115
+ atEndOfInput() {
11116
+ for (let i = this.current; i < this.tokens.length; i++) {
11117
+ if (!isComment(this.tokens[i]))
11118
+ return false;
11119
+ }
11120
+ return true;
11121
+ }
11122
+ checkFeatureStart() {
11123
+ if (this.isAtEnd())
11124
+ return false;
11125
+ const value = this.peek().value;
11126
+ return (value === 'init' ||
11127
+ value === 'on' ||
11128
+ value === 'def' ||
11129
+ getRegisteredFeature(value) !== undefined);
11130
+ }
10840
11131
  peek() {
10841
11132
  if (this.isAtEnd()) {
10842
11133
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -11048,6 +11339,7 @@ class Parser {
11048
11339
  match: this.match.bind(this),
11049
11340
  matchOperator: this.matchOperator.bind(this),
11050
11341
  isAtEnd: this.isAtEnd.bind(this),
11342
+ atEndOfInput: this.atEndOfInput.bind(this),
11051
11343
  createIdentifier: this.createIdentifier.bind(this),
11052
11344
  parseExpression: this.parseExpression.bind(this),
11053
11345
  parsePrimary: this.parsePrimary.bind(this),
@@ -11098,7 +11390,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
11098
11390
  'ignoring case',
11099
11391
  ]);
11100
11392
  Parser.PRATT_TABLE = PARSER_TABLE;
11393
+ Parser.BEEP_OPERAND_BP = 80;
11101
11394
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
11395
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
11396
+ 'closest',
11397
+ 'next',
11398
+ 'previous',
11399
+ 'first',
11400
+ 'last',
11401
+ ]);
11102
11402
  function parse(input, options) {
11103
11403
  const tokens = tokenize(input);
11104
11404
  const parser = new Parser(tokens, options, input);
@@ -11540,11 +11840,16 @@ async function evaluateBinaryExpression(node, context) {
11540
11840
  }
11541
11841
  }
11542
11842
  }
11543
- if (leftNode?.type === 'selector' && leftNode.fromQuery && typeof leftNode.value === 'string') {
11843
+ if (leftNode?.type === 'selector' &&
11844
+ typeof leftNode.value === 'string' &&
11845
+ (leftNode.fromQuery || (operator === 'in' && leftNode.value.startsWith('.')))) {
11544
11846
  const root = await evaluateAST(node.right, context);
11545
11847
  const scope = root && typeof root.querySelectorAll === 'function' ? root : null;
11546
11848
  if (scope) {
11547
- return Array.from(scope.querySelectorAll(leftNode.value));
11849
+ const query = leftNode.fromQuery
11850
+ ? leftNode.value
11851
+ : escapeSelectorForQuery(leftNode, leftNode.value);
11852
+ return Array.from(scope.querySelectorAll(query));
11548
11853
  }
11549
11854
  }
11550
11855
  }
@@ -11556,6 +11861,12 @@ async function evaluateBinaryExpression(node, context) {
11556
11861
  return getExpr(context, 'styleRef').evaluate(context, leftNode.value.slice(1), el);
11557
11862
  }
11558
11863
  if (operator === 'of') {
11864
+ if (leftNode?.type === 'attributeAccess') {
11865
+ const target = await evaluateAST(node.right, context);
11866
+ if (target == null)
11867
+ return undefined;
11868
+ return getExpr(context, 'possessive').evaluate(context, target, `@${leftNode.attributeName}`);
11869
+ }
11559
11870
  const path = propertyPathOf(node.left);
11560
11871
  if (path) {
11561
11872
  const target = await evaluateAST(node.right, context);
@@ -11563,6 +11874,10 @@ async function evaluateBinaryExpression(node, context) {
11563
11874
  for (const seg of path) {
11564
11875
  if (cur == null)
11565
11876
  return undefined;
11877
+ if (isMappableCollection(cur) && !(seg in cur)) {
11878
+ cur = await getExpr(context, 'possessive').evaluate(context, cur, seg);
11879
+ continue;
11880
+ }
11566
11881
  cur = isElement(cur) ? getElementProperty(cur, seg) : cur[seg];
11567
11882
  }
11568
11883
  return typeof cur === 'function' ? cur.bind(target) : cur;
@@ -11587,26 +11902,21 @@ async function evaluateBinaryExpression(node, context) {
11587
11902
  const R = node.ignoringCase ? applyCI(right) : right;
11588
11903
  switch (operator) {
11589
11904
  case '+': {
11590
- let addLeft = left;
11591
- let addRight = right;
11592
11905
  if (node.coerceNumeric) {
11593
- if (typeof addLeft === 'string')
11594
- addLeft = convertToNumber(addLeft);
11595
- if (typeof addRight === 'string')
11596
- addRight = convertToNumber(addRight);
11906
+ return counterValue(left) + counterAmount(right);
11597
11907
  }
11598
- if (typeof addLeft === 'string' || typeof addRight === 'string') {
11599
- return String(addLeft ?? '') + String(addRight ?? '');
11908
+ if (typeof left === 'string' || typeof right === 'string') {
11909
+ return String(left ?? '') + String(right ?? '');
11600
11910
  }
11601
- if (Array.isArray(addLeft)) {
11602
- return Array.isArray(addRight) ? [...addLeft, ...addRight] : [...addLeft, addRight];
11911
+ if (Array.isArray(left)) {
11912
+ return Array.isArray(right) ? [...left, ...right] : [...left, right];
11603
11913
  }
11604
- return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, {
11605
- left: addLeft,
11606
- right: addRight,
11607
- }));
11914
+ return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, { left, right }));
11608
11915
  }
11609
11916
  case '-':
11917
+ if (node.coerceNumeric) {
11918
+ return counterValue(left) - counterAmount(right);
11919
+ }
11610
11920
  return unwrapTypedResult(await getExpr(context, 'subtraction').evaluate(context, { left, right }));
11611
11921
  case '*':
11612
11922
  return unwrapTypedResult(await getExpr(context, 'multiplication').evaluate(context, { left, right }));
@@ -11864,9 +12174,8 @@ async function evaluatePropertyOfExpressionNode(node, context) {
11864
12174
  }
11865
12175
  const propertyName = propertyNode.name;
11866
12176
  const target = await evaluateAST(node.target, context);
11867
- if (target == null) {
11868
- throw new Error(`Cannot access property "${propertyName}" of ${target}`);
11869
- }
12177
+ if (target == null)
12178
+ return undefined;
11870
12179
  return getExpr(context, 'possessive').evaluate(context, target, propertyName);
11871
12180
  }
11872
12181
  async function evaluateTemplateLiteralNode(node, context) {
@@ -11984,6 +12293,9 @@ async function evaluateUnaryExpression(node, context) {
11984
12293
  case 'not':
11985
12294
  case '!':
11986
12295
  return logicalExpressions.not.evaluate(context, value);
12296
+ case 'beep!':
12297
+ beepValues(context.me, [value]);
12298
+ return value;
11987
12299
  case 'no':
11988
12300
  return logicalExpressions.no.evaluate(context, value);
11989
12301
  case '-':
@@ -12073,6 +12385,10 @@ async function evaluateCallExpression(node, context) {
12073
12385
  const args = await resolveCallArgs(node.arguments, funcName, context);
12074
12386
  switch (funcName) {
12075
12387
  case 'closest':
12388
+ if (node.closestTo) {
12389
+ const start = unwrapElement(await evaluateAST(node.closestTo, context)) ?? null;
12390
+ return referencesExpressions.closest.evaluate(context, args[0], start);
12391
+ }
12076
12392
  return referencesExpressions.closest.evaluate(context, ...args);
12077
12393
  case 'previous':
12078
12394
  case 'next': {