@hyperfixi/core 3.1.1 → 3.3.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 +36 -17
  10. package/dist/bundle-generator/index.mjs +36 -17
  11. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  12. package/dist/chunks/bridge-CI9SOl8u.js +2 -0
  13. package/dist/chunks/browser-modular-7CpI5VF2.js +2 -0
  14. package/dist/chunks/index-BwXDEK0m.js +2 -0
  15. package/dist/chunks/index-DQi2o8Lk.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 +938 -425
  23. package/dist/commands/index.mjs +938 -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 +1149 -485
  40. package/dist/index.min.js +1 -1
  41. package/dist/index.mjs +1149 -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 +396 -153
  53. package/dist/parser/full-parser.mjs +396 -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 +535 -210
  65. package/dist/registry/index.mjs +535 -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')) {
@@ -6697,6 +6908,15 @@ function parseToggleCommand(ctx, identifierNode) {
6697
6908
  args.push(classArg.expression);
6698
6909
  modifiers['as'] = literalModifier(mode);
6699
6910
  }
6911
+ else if (classArg?.type === 'binaryExpression' &&
6912
+ classArg.operator === 'of' &&
6913
+ classArg.left?.type === 'selector' &&
6914
+ typeof classArg.left.value === 'string' &&
6915
+ classArg.left.value.startsWith('*') &&
6916
+ classArg.right) {
6917
+ args.push(classArg.left);
6918
+ modifiers['on'] = classArg.right;
6919
+ }
6700
6920
  else if (classArg) {
6701
6921
  args.push(classArg);
6702
6922
  }
@@ -6874,11 +7094,17 @@ function parseSwapCommand(ctx, identifierNode) {
6874
7094
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
6875
7095
  if (targetExpr)
6876
7096
  args.push(targetExpr);
6877
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
7097
+ const isMorph = identifierNode.name === 'morph';
7098
+ if (!isDelete &&
7099
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
7100
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
6878
7101
  const contentExpr = parseOneArgument(ctx, ['using']);
6879
7102
  if (contentExpr)
6880
7103
  modifiers['with'] = contentExpr;
6881
7104
  }
7105
+ if (isMorph && modifiers['with'] === undefined) {
7106
+ ctx.addError("Expected 'to' and the content to morph into");
7107
+ }
6882
7108
  return CommandNodeBuilder.fromIdentifier(identifierNode)
6883
7109
  .withArgs(...args)
6884
7110
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -7639,6 +7865,8 @@ function atArgumentBoundary(ctx, grammar) {
7639
7865
  const next = ctx.peek();
7640
7866
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
7641
7867
  return false;
7868
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
7869
+ return false;
7642
7870
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
7643
7871
  return true;
7644
7872
  return isKeyword(next, [...grammar.markers]);
@@ -7670,10 +7898,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
7670
7898
  }
7671
7899
  }
7672
7900
  const commaList = new Set(grammar.commaList ?? []);
7901
+ const namedArgs = new Set(grammar.namedArgs ?? []);
7673
7902
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
7674
7903
  const keyword = ctx.advance().value;
7675
7904
  const key = ctx.resolveKeyword(keyword).toLowerCase();
7676
- let value = ctx.parseExpression();
7905
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
7906
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
7907
+ : ctx.parseExpression();
7677
7908
  if (value && commaList.has(key) && ctx.check(',')) {
7678
7909
  const elements = [value];
7679
7910
  while (ctx.match(',')) {
@@ -7838,8 +8069,8 @@ function parseFetchCommand(ctx, commandToken) {
7838
8069
  }
7839
8070
  if (ctx.check('with') && !modifiers['with']) {
7840
8071
  ctx.advance();
7841
- if (isFetchNakedNamedArgStart(ctx)) {
7842
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
8072
+ if (isNakedNamedArgStart(ctx)) {
8073
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
7843
8074
  }
7844
8075
  else {
7845
8076
  modifiers['with'] = ctx.parsePrimary();
@@ -7874,45 +8105,6 @@ function parseFetchCommand(ctx, commandToken) {
7874
8105
  }
7875
8106
  return builder.build();
7876
8107
  }
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
8108
  function findJsEndBoundary(ctx, startPos) {
7917
8109
  const input = ctx.getInputSlice(startPos);
7918
8110
  if (!input) {
@@ -8032,6 +8224,7 @@ function parseTellCommand(ctx, identifierNode) {
8032
8224
  const commands = [];
8033
8225
  while (!ctx.isAtEnd()) {
8034
8226
  if (ctx.checkIsCommand()) {
8227
+ const word = ctx.peek().value;
8035
8228
  try {
8036
8229
  ctx.advance();
8037
8230
  const cmd = ctx.parseCommand();
@@ -8042,7 +8235,8 @@ function parseTellCommand(ctx, identifierNode) {
8042
8235
  break;
8043
8236
  }
8044
8237
  }
8045
- catch {
8238
+ catch (error) {
8239
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
8046
8240
  break;
8047
8241
  }
8048
8242
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -8191,7 +8385,6 @@ const ONE_EXPR = (syntax) => ({
8191
8385
  markers: [],
8192
8386
  syntax,
8193
8387
  });
8194
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
8195
8388
  const COMMAND_GRAMMAR = {
8196
8389
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
8197
8390
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -8202,8 +8395,7 @@ const COMMAND_GRAMMAR = {
8202
8395
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
8203
8396
  },
8204
8397
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
8205
- 'beep!': BEEP,
8206
- beep: BEEP,
8398
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
8207
8399
  blur: ONE_EXPR('blur [<target>]'),
8208
8400
  break: NONE,
8209
8401
  breakpoint: NONE,
@@ -8227,7 +8419,8 @@ const COMMAND_GRAMMAR = {
8227
8419
  render: {
8228
8420
  positional: 'expression',
8229
8421
  markers: ['with'],
8230
- syntax: 'render <template> [with <variables>]',
8422
+ namedArgs: ['with'],
8423
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
8231
8424
  },
8232
8425
  reset: ONE_EXPR('reset [<form>]'),
8233
8426
  return: ONE_EXPR('return [<value>]'),
@@ -8582,7 +8775,10 @@ class Parser {
8582
8775
  const firstToken = this.peek();
8583
8776
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
8584
8777
  let left;
8585
- if (prefixEntry?.prefix) {
8778
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
8779
+ left = this.parseBeepExpression();
8780
+ }
8781
+ else if (prefixEntry?.prefix) {
8586
8782
  const token = this.advance();
8587
8783
  if (this.isAtEnd()) {
8588
8784
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -8602,6 +8798,9 @@ class Parser {
8602
8798
  continue;
8603
8799
  }
8604
8800
  }
8801
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
8802
+ break;
8803
+ }
8605
8804
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
8606
8805
  if (!infixEntry?.infix) {
8607
8806
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -8663,6 +8862,35 @@ class Parser {
8663
8862
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
8664
8863
  return left;
8665
8864
  }
8865
+ parseBeepExpression() {
8866
+ const beepToken = this.advance();
8867
+ this.advance();
8868
+ if (this.isAtEnd()) {
8869
+ this.addError("Expected expression after 'beep!'");
8870
+ return this.createErrorNode();
8871
+ }
8872
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
8873
+ return {
8874
+ type: 'unaryExpression',
8875
+ operator: 'beep!',
8876
+ operand,
8877
+ argument: operand,
8878
+ prefix: true,
8879
+ start: beepToken.start,
8880
+ end: this.previous().end,
8881
+ line: beepToken.line,
8882
+ column: beepToken.column,
8883
+ };
8884
+ }
8885
+ isStyleRefStar(i) {
8886
+ const star = this.tokens[i];
8887
+ const name = this.tokens[i + 1];
8888
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
8889
+ }
8890
+ isSpacedStyleRef(i) {
8891
+ const prev = this.tokens[i - 1];
8892
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
8893
+ }
8666
8894
  tryParseStringPostfix(left, nextToken) {
8667
8895
  const v = nextToken.value;
8668
8896
  let unit = null;
@@ -8865,6 +9093,10 @@ class Parser {
8865
9093
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
8866
9094
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
8867
9095
  while (!this.isAtEnd() && !isStop()) {
9096
+ if (this.checkComment()) {
9097
+ this.advance();
9098
+ continue;
9099
+ }
8868
9100
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
8869
9101
  let parsedCommand = false;
8870
9102
  const isCommandToken = this.checkIsCommand();
@@ -8948,7 +9180,7 @@ class Parser {
8948
9180
  debug.parse('\u2705 Found "end", consuming it');
8949
9181
  this.advance();
8950
9182
  }
8951
- else {
9183
+ else if (!this.atEndOfInput()) {
8952
9184
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
8953
9185
  throw new Error(`Expected "end" to close ${construct} block`);
8954
9186
  }
@@ -8961,7 +9193,7 @@ class Parser {
8961
9193
  if (this.check('end')) {
8962
9194
  this.advance();
8963
9195
  }
8964
- else {
9196
+ else if (!this.atEndOfInput()) {
8965
9197
  throw new Error('Expected "end" to close repeat block');
8966
9198
  }
8967
9199
  }
@@ -8977,7 +9209,10 @@ class Parser {
8977
9209
  this.advance();
8978
9210
  }
8979
9211
  else if (terminator === '') {
8980
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
9212
+ if (!this.atEndOfInput()) {
9213
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
9214
+ }
9215
+ return { commands, terminator: 'end' };
8981
9216
  }
8982
9217
  return { commands, terminator };
8983
9218
  }
@@ -9561,7 +9796,9 @@ class Parser {
9561
9796
  parseTopLevelInitBlock() {
9562
9797
  const pos = this.getPosition();
9563
9798
  const initCommands = this.parseCommandBlock(['end']);
9564
- this.consume('end', "Expected 'end' after init block");
9799
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
9800
+ this.consume('end', "Expected 'end' after init block");
9801
+ }
9565
9802
  return {
9566
9803
  type: 'initBlock',
9567
9804
  commands: initCommands,
@@ -9737,10 +9974,29 @@ class Parser {
9737
9974
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
9738
9975
  };
9739
9976
  }
9977
+ isArgumentListOnly(open) {
9978
+ let depth = 0;
9979
+ for (let i = open; i < this.tokens.length; i++) {
9980
+ const token = this.tokens[i];
9981
+ if (token.kind !== 'operator')
9982
+ continue;
9983
+ if (token.value === '(' || token.value === '[' || token.value === '{')
9984
+ depth++;
9985
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
9986
+ if (--depth === 0)
9987
+ return i === open + 1;
9988
+ }
9989
+ else if (token.value === ',' && depth === 1)
9990
+ return true;
9991
+ }
9992
+ return false;
9993
+ }
9740
9994
  tryParsePseudoCommand(callExpr, sourceExpr) {
9741
9995
  const methodName = callExpr.callee.name;
9742
9996
  const nextToken = this.peek();
9743
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
9997
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
9998
+ const hasPseudoCommandPattern = nextIsArticle ||
9999
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
9744
10000
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
9745
10001
  isContextVar(nextToken);
9746
10002
  if (!hasPseudoCommandPattern) {
@@ -9766,7 +10022,10 @@ class Parser {
9766
10022
  });
9767
10023
  }
9768
10024
  let preposition;
9769
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
10025
+ if (nextIsArticle) {
10026
+ this.advance();
10027
+ }
10028
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
9770
10029
  preposition = this.advance().value.toLowerCase();
9771
10030
  }
9772
10031
  let targetExpr;
@@ -9930,10 +10189,22 @@ class Parser {
9930
10189
  this.consume(']', "Expected ']' after event condition");
9931
10190
  }
9932
10191
  let target;
10192
+ let targetExpression;
9933
10193
  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})`);
10194
+ if (this.check('the'))
10195
+ this.advance();
10196
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
10197
+ const startIndex = this.current;
10198
+ targetExpression = this.parseExpression();
10199
+ target = this.tokens
10200
+ .slice(startIndex, this.current)
10201
+ .map(t => t.value)
10202
+ .join(' ');
10203
+ }
10204
+ else {
10205
+ target = this.advance().value;
10206
+ }
10207
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
9937
10208
  this.parseTemporalKeywordModifiers(modifiers);
9938
10209
  }
9939
10210
  let attributeName;
@@ -9979,7 +10250,7 @@ class Parser {
9979
10250
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
9980
10251
  const commandName = this.peek().value.toLowerCase();
9981
10252
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
9982
- if (nextIsOpenParen && !isSpecialBodyCommand) {
10253
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
9983
10254
  let expr;
9984
10255
  const savedError = this.error;
9985
10256
  try {
@@ -10011,7 +10282,7 @@ class Parser {
10011
10282
  }
10012
10283
  }
10013
10284
  }
10014
- else if (this.checkIdentifier()) {
10285
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
10015
10286
  const token = this.peek();
10016
10287
  if (this.isCommand(token.value)) {
10017
10288
  const cmd = this.parseCommandWithErrorRecovery();
@@ -10120,6 +10391,7 @@ class Parser {
10120
10391
  ...(eventParams.length > 0 && { args: eventParams }),
10121
10392
  ...(condition && { condition: fromLegacyExpression(condition) }),
10122
10393
  ...(target && { target }),
10394
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
10123
10395
  ...(attributeName && { attributeName }),
10124
10396
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
10125
10397
  ...(customEventSource && { customEventSource }),
@@ -10375,10 +10647,9 @@ class Parser {
10375
10647
  }
10376
10648
  parseCommandCore() {
10377
10649
  const commandToken = this.previous();
10378
- let commandName = commandToken.value;
10650
+ const commandName = commandToken.value;
10379
10651
  if (commandName === 'beep' && this.check('!')) {
10380
10652
  this.advance();
10381
- commandName = 'beep!';
10382
10653
  }
10383
10654
  if (commandName === 'fetch') {
10384
10655
  return parseFetchCommand(this.getContext(), commandToken);
@@ -10477,6 +10748,9 @@ class Parser {
10477
10748
  args.push(this.parsePrimary());
10478
10749
  }
10479
10750
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
10751
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
10752
+ callNode.closestTo = this.parseExpression();
10753
+ }
10480
10754
  if (funcName === 'next' || funcName === 'previous') {
10481
10755
  const relativePositional = this.parseRelativePositionalModifiers();
10482
10756
  if (relativePositional) {
@@ -10516,6 +10790,9 @@ class Parser {
10516
10790
  }
10517
10791
  parseTheXofY() {
10518
10792
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
10793
+ if (this.isStyleRefStar(this.current)) {
10794
+ return this.parsePrimary();
10795
+ }
10519
10796
  if (!this.checkIdentifier()) {
10520
10797
  return this.createIdentifier('the');
10521
10798
  }
@@ -10531,7 +10808,7 @@ class Parser {
10531
10808
  return this.createIdentifier(propertyName);
10532
10809
  }
10533
10810
  this.advance();
10534
- const target = this.parseCall();
10811
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
10535
10812
  return {
10536
10813
  type: 'propertyOfExpression',
10537
10814
  property: { type: 'identifier', name: propertyName },
@@ -10729,6 +11006,13 @@ class Parser {
10729
11006
  checkLiteral() {
10730
11007
  return this.checkPredicate(isLiteral);
10731
11008
  }
11009
+ checkEventNamedCall() {
11010
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
11011
+ return false;
11012
+ const token = this.peek();
11013
+ const next = this.tokens[this.current + 1];
11014
+ return next?.value === '(' && next.start === token.end;
11015
+ }
10732
11016
  checkIdentifierLike() {
10733
11017
  return this.checkPredicate(isIdentifierLike);
10734
11018
  }
@@ -10837,6 +11121,22 @@ class Parser {
10837
11121
  isAtEnd() {
10838
11122
  return this.current >= this.tokens.length;
10839
11123
  }
11124
+ atEndOfInput() {
11125
+ for (let i = this.current; i < this.tokens.length; i++) {
11126
+ if (!isComment(this.tokens[i]))
11127
+ return false;
11128
+ }
11129
+ return true;
11130
+ }
11131
+ checkFeatureStart() {
11132
+ if (this.isAtEnd())
11133
+ return false;
11134
+ const value = this.peek().value;
11135
+ return (value === 'init' ||
11136
+ value === 'on' ||
11137
+ value === 'def' ||
11138
+ getRegisteredFeature(value) !== undefined);
11139
+ }
10840
11140
  peek() {
10841
11141
  if (this.isAtEnd()) {
10842
11142
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -11048,6 +11348,7 @@ class Parser {
11048
11348
  match: this.match.bind(this),
11049
11349
  matchOperator: this.matchOperator.bind(this),
11050
11350
  isAtEnd: this.isAtEnd.bind(this),
11351
+ atEndOfInput: this.atEndOfInput.bind(this),
11051
11352
  createIdentifier: this.createIdentifier.bind(this),
11052
11353
  parseExpression: this.parseExpression.bind(this),
11053
11354
  parsePrimary: this.parsePrimary.bind(this),
@@ -11098,7 +11399,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
11098
11399
  'ignoring case',
11099
11400
  ]);
11100
11401
  Parser.PRATT_TABLE = PARSER_TABLE;
11402
+ Parser.BEEP_OPERAND_BP = 80;
11101
11403
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
11404
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
11405
+ 'closest',
11406
+ 'next',
11407
+ 'previous',
11408
+ 'first',
11409
+ 'last',
11410
+ ]);
11102
11411
  function parse(input, options) {
11103
11412
  const tokens = tokenize(input);
11104
11413
  const parser = new Parser(tokens, options, input);
@@ -11540,11 +11849,16 @@ async function evaluateBinaryExpression(node, context) {
11540
11849
  }
11541
11850
  }
11542
11851
  }
11543
- if (leftNode?.type === 'selector' && leftNode.fromQuery && typeof leftNode.value === 'string') {
11852
+ if (leftNode?.type === 'selector' &&
11853
+ typeof leftNode.value === 'string' &&
11854
+ (leftNode.fromQuery || (operator === 'in' && leftNode.value.startsWith('.')))) {
11544
11855
  const root = await evaluateAST(node.right, context);
11545
11856
  const scope = root && typeof root.querySelectorAll === 'function' ? root : null;
11546
11857
  if (scope) {
11547
- return Array.from(scope.querySelectorAll(leftNode.value));
11858
+ const query = leftNode.fromQuery
11859
+ ? leftNode.value
11860
+ : escapeSelectorForQuery(leftNode, leftNode.value);
11861
+ return Array.from(scope.querySelectorAll(query));
11548
11862
  }
11549
11863
  }
11550
11864
  }
@@ -11556,6 +11870,12 @@ async function evaluateBinaryExpression(node, context) {
11556
11870
  return getExpr(context, 'styleRef').evaluate(context, leftNode.value.slice(1), el);
11557
11871
  }
11558
11872
  if (operator === 'of') {
11873
+ if (leftNode?.type === 'attributeAccess') {
11874
+ const target = await evaluateAST(node.right, context);
11875
+ if (target == null)
11876
+ return undefined;
11877
+ return getExpr(context, 'possessive').evaluate(context, target, `@${leftNode.attributeName}`);
11878
+ }
11559
11879
  const path = propertyPathOf(node.left);
11560
11880
  if (path) {
11561
11881
  const target = await evaluateAST(node.right, context);
@@ -11563,6 +11883,10 @@ async function evaluateBinaryExpression(node, context) {
11563
11883
  for (const seg of path) {
11564
11884
  if (cur == null)
11565
11885
  return undefined;
11886
+ if (isMappableCollection(cur) && !(seg in cur)) {
11887
+ cur = await getExpr(context, 'possessive').evaluate(context, cur, seg);
11888
+ continue;
11889
+ }
11566
11890
  cur = isElement(cur) ? getElementProperty(cur, seg) : cur[seg];
11567
11891
  }
11568
11892
  return typeof cur === 'function' ? cur.bind(target) : cur;
@@ -11587,26 +11911,21 @@ async function evaluateBinaryExpression(node, context) {
11587
11911
  const R = node.ignoringCase ? applyCI(right) : right;
11588
11912
  switch (operator) {
11589
11913
  case '+': {
11590
- let addLeft = left;
11591
- let addRight = right;
11592
11914
  if (node.coerceNumeric) {
11593
- if (typeof addLeft === 'string')
11594
- addLeft = convertToNumber(addLeft);
11595
- if (typeof addRight === 'string')
11596
- addRight = convertToNumber(addRight);
11915
+ return counterValue(left) + counterAmount(right);
11597
11916
  }
11598
- if (typeof addLeft === 'string' || typeof addRight === 'string') {
11599
- return String(addLeft ?? '') + String(addRight ?? '');
11917
+ if (typeof left === 'string' || typeof right === 'string') {
11918
+ return String(left ?? '') + String(right ?? '');
11600
11919
  }
11601
- if (Array.isArray(addLeft)) {
11602
- return Array.isArray(addRight) ? [...addLeft, ...addRight] : [...addLeft, addRight];
11920
+ if (Array.isArray(left)) {
11921
+ return Array.isArray(right) ? [...left, ...right] : [...left, right];
11603
11922
  }
11604
- return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, {
11605
- left: addLeft,
11606
- right: addRight,
11607
- }));
11923
+ return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, { left, right }));
11608
11924
  }
11609
11925
  case '-':
11926
+ if (node.coerceNumeric) {
11927
+ return counterValue(left) - counterAmount(right);
11928
+ }
11610
11929
  return unwrapTypedResult(await getExpr(context, 'subtraction').evaluate(context, { left, right }));
11611
11930
  case '*':
11612
11931
  return unwrapTypedResult(await getExpr(context, 'multiplication').evaluate(context, { left, right }));
@@ -11864,9 +12183,8 @@ async function evaluatePropertyOfExpressionNode(node, context) {
11864
12183
  }
11865
12184
  const propertyName = propertyNode.name;
11866
12185
  const target = await evaluateAST(node.target, context);
11867
- if (target == null) {
11868
- throw new Error(`Cannot access property "${propertyName}" of ${target}`);
11869
- }
12186
+ if (target == null)
12187
+ return undefined;
11870
12188
  return getExpr(context, 'possessive').evaluate(context, target, propertyName);
11871
12189
  }
11872
12190
  async function evaluateTemplateLiteralNode(node, context) {
@@ -11984,6 +12302,9 @@ async function evaluateUnaryExpression(node, context) {
11984
12302
  case 'not':
11985
12303
  case '!':
11986
12304
  return logicalExpressions.not.evaluate(context, value);
12305
+ case 'beep!':
12306
+ beepValues(context.me, [value]);
12307
+ return value;
11987
12308
  case 'no':
11988
12309
  return logicalExpressions.no.evaluate(context, value);
11989
12310
  case '-':
@@ -12073,6 +12394,10 @@ async function evaluateCallExpression(node, context) {
12073
12394
  const args = await resolveCallArgs(node.arguments, funcName, context);
12074
12395
  switch (funcName) {
12075
12396
  case 'closest':
12397
+ if (node.closestTo) {
12398
+ const start = unwrapElement(await evaluateAST(node.closestTo, context)) ?? null;
12399
+ return referencesExpressions.closest.evaluate(context, args[0], start);
12400
+ }
12076
12401
  return referencesExpressions.closest.evaluate(context, ...args);
12077
12402
  case 'previous':
12078
12403
  case 'next': {