@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
@@ -897,6 +897,39 @@ function makeTypeCheckHandler(negated) {
897
897
  };
898
898
  };
899
899
  }
900
+ const OF_OPERAND_BP = 70;
901
+ function parseOfOperand(ctx, minBp = OF_OPERAND_BP) {
902
+ let right = ctx.parseExpr(minBp);
903
+ if (ctx.peek()?.value === 'in') {
904
+ const inToken = ctx.advance();
905
+ const scope = ctx.parseExpr(minBp);
906
+ right = {
907
+ type: 'binaryExpression',
908
+ operator: 'in',
909
+ left: right,
910
+ right: scope,
911
+ start: right.start,
912
+ end: scope.end ?? inToken.end,
913
+ line: right.line ?? inToken.line,
914
+ column: right.column ?? inToken.column,
915
+ };
916
+ }
917
+ return right;
918
+ }
919
+ const THE_OF_TARGET_BP = OF_OPERAND_BP + 1;
920
+ function ofHandler(left, token, ctx) {
921
+ const right = parseOfOperand(ctx);
922
+ return {
923
+ type: 'binaryExpression',
924
+ operator: token.value,
925
+ left,
926
+ right,
927
+ start: left.start,
928
+ end: right.end ?? token.end,
929
+ line: left.line ?? token.line,
930
+ column: left.column ?? token.column,
931
+ };
932
+ }
900
933
  const PARSER_COMPARISON_FRAGMENT = new Map([
901
934
  ['===', leftAssoc(30)],
902
935
  ['!==', leftAssoc(30)],
@@ -941,7 +974,7 @@ const PARSER_COMPARISON_FRAGMENT = new Map([
941
974
  ['is greater than or equal to', leftAssoc(30)],
942
975
  ['is less than or equal to', leftAssoc(30)],
943
976
  ['in', leftAssoc(30)],
944
- ['of', rightAssoc(30)],
977
+ ['of', { infix: { bp: [90, OF_OPERAND_BP], handler: ofHandler } }],
945
978
  ['really', leftAssoc(30)],
946
979
  [
947
980
  'exists',
@@ -1117,36 +1150,108 @@ function setElementVar(context, name, value) {
1117
1150
  notifyLocalWrite(name, value, context);
1118
1151
  }
1119
1152
 
1120
- function convertToNumber(value) {
1121
- if (value === null || value === undefined) {
1122
- return 0;
1123
- }
1124
- if (typeof value === 'number') {
1125
- return isFinite(value) ? value : 0;
1126
- }
1127
- if (typeof value === 'string') {
1128
- const parsed = parseFloat(value);
1129
- return parsed;
1130
- }
1131
- if (typeof value === 'boolean') {
1132
- return value ? 1 : 0;
1133
- }
1153
+ function isHTMLElement$2(value) {
1154
+ return (value !== null &&
1155
+ typeof value === 'object' &&
1156
+ 'nodeType' in value &&
1157
+ value.nodeType === 1 &&
1158
+ 'tagName' in value &&
1159
+ typeof value.tagName === 'string' &&
1160
+ 'classList' in value);
1161
+ }
1162
+
1163
+ function beepValues(me, values) {
1164
+ const outputs = values.map(describeBeepValue);
1165
+ const shown = outputs.filter(output => announce(me, output.value));
1166
+ if (shown.length === 0)
1167
+ return outputs;
1168
+ console.group('\ud83d\udd14 beep! Debug Output');
1169
+ for (const output of shown) {
1170
+ console.log(`Value:`, output.value);
1171
+ console.log(`Type:`, output.type);
1172
+ console.log(`Representation:`, output.representation);
1173
+ console.log('---');
1174
+ }
1175
+ console.groupEnd();
1176
+ return outputs;
1177
+ }
1178
+ function announce(me, value) {
1179
+ const target = me;
1180
+ if (typeof CustomEvent !== 'function' || typeof target?.dispatchEvent !== 'function')
1181
+ return true;
1182
+ return target.dispatchEvent(new CustomEvent('hyperscript:beep', {
1183
+ bubbles: true,
1184
+ cancelable: true,
1185
+ detail: { element: me, value },
1186
+ }));
1187
+ }
1188
+ function describeBeepValue(value) {
1189
+ return { value, type: typeOf(value), representation: representationOf(value) };
1190
+ }
1191
+ function typeOf(value) {
1192
+ if (value === null)
1193
+ return 'null';
1194
+ if (value === undefined)
1195
+ return 'undefined';
1196
+ if (Array.isArray(value))
1197
+ return 'array';
1198
+ if (isHTMLElement$2(value))
1199
+ return 'HTMLElement';
1200
+ if (typeof Element !== 'undefined' && value instanceof Element)
1201
+ return 'Element';
1202
+ if (typeof Node !== 'undefined' && value instanceof Node)
1203
+ return 'Node';
1204
+ if (value instanceof Error)
1205
+ return 'Error';
1206
+ if (value instanceof Date)
1207
+ return 'Date';
1208
+ if (value instanceof RegExp)
1209
+ return 'RegExp';
1210
+ return typeof value;
1211
+ }
1212
+ function representationOf(value) {
1213
+ if (value === null)
1214
+ return 'null';
1215
+ if (value === undefined)
1216
+ return 'undefined';
1134
1217
  if (Array.isArray(value)) {
1135
- return value.length;
1136
- }
1218
+ return `Array(${value.length}) [${value
1219
+ .slice(0, 3)
1220
+ .map(v => representationOf(v))
1221
+ .join(', ')}${value.length > 3 ? '...' : ''}]`;
1222
+ }
1223
+ if (isHTMLElement$2(value)) {
1224
+ const el = value;
1225
+ const tag = el.tagName.toLowerCase();
1226
+ const id = el.id ? `#${el.id}` : '';
1227
+ const classes = el.className ? `.${el.className.split(' ').join('.')}` : '';
1228
+ return `<${tag}${id}${classes}/>`;
1229
+ }
1230
+ if (value instanceof Error)
1231
+ return `Error: ${value.message}`;
1232
+ if (typeof value === 'string')
1233
+ return value.length > 50 ? `"${value.substring(0, 47)}..."` : `"${value}"`;
1137
1234
  if (typeof value === 'object') {
1138
- if ('length' in value && typeof value.length === 'number') {
1139
- return value.length;
1235
+ try {
1236
+ const keys = Object.keys(value);
1237
+ return `Object {${keys.slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''}}`;
1140
1238
  }
1141
- if (typeof value.valueOf === 'function') {
1142
- const result = value.valueOf();
1143
- if (typeof result === 'number') {
1144
- return result;
1145
- }
1239
+ catch {
1240
+ return '[Object]';
1146
1241
  }
1147
- return NaN;
1148
1242
  }
1149
- return 0;
1243
+ return String(value);
1244
+ }
1245
+
1246
+ function counterAmount(value) {
1247
+ return typeof value === 'number' ? value : parseFloat(String(value));
1248
+ }
1249
+ function counterValue(value) {
1250
+ if (isHTMLElement$2(value)) {
1251
+ const element = value;
1252
+ return counterValue(element.value || element.textContent);
1253
+ }
1254
+ return value ? counterAmount(value) : 0;
1150
1255
  }
1151
1256
 
1152
1257
  function resolveTargetElements(matches, selector, fromQuery) {
@@ -1512,13 +1617,6 @@ function isFunction(value) {
1512
1617
  function isInputElement(el) {
1513
1618
  return (el !== null && typeof el === 'object' && 'tagName' in el && el.tagName === 'INPUT');
1514
1619
  }
1515
- function isFormElement(el) {
1516
- if (el === null || typeof el !== 'object' || !('tagName' in el)) {
1517
- return false;
1518
- }
1519
- const tag = el.tagName;
1520
- return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA';
1521
- }
1522
1620
  function isHTMLElement$1(el) {
1523
1621
  return (el !== null &&
1524
1622
  typeof el === 'object' &&
@@ -1578,7 +1676,7 @@ const SPECIAL_DOM_PROPERTIES = {
1578
1676
  innertext: el => el.textContent?.trim(),
1579
1677
  innerHTML: el => el.innerHTML,
1580
1678
  outerhtml: el => el.outerHTML,
1581
- value: el => (isFormElement(el) ? el.value : undefined),
1679
+ value: el => ('value' in el ? el.value : undefined),
1582
1680
  checked: el => (isInputElement(el) ? el.checked : undefined),
1583
1681
  disabled: el => ('disabled' in el ? el.disabled : undefined),
1584
1682
  selected: el => (isOptionElement(el) ? el.selected : undefined),
@@ -2246,14 +2344,15 @@ const closestExpression = {
2246
2344
  if (typeof selector !== 'string') {
2247
2345
  throw new Error('closest requires a string selector');
2248
2346
  }
2249
- if (!context.me) {
2347
+ const start = args.length > 1 ? args[1] : context.me;
2348
+ if (!(start instanceof Element)) {
2250
2349
  return null;
2251
2350
  }
2252
- return context.me.closest(selector);
2351
+ return start.closest(selector);
2253
2352
  },
2254
2353
  validate(args) {
2255
- if (args.length !== 1) {
2256
- return 'closest requires exactly 1 argument (selector)';
2354
+ if (args.length !== 1 && args.length !== 2) {
2355
+ return 'closest requires a selector and, optionally, the element to start from';
2257
2356
  }
2258
2357
  const arg = args[0];
2259
2358
  if (typeof arg === 'string')
@@ -4639,7 +4738,7 @@ const isEmptyExpression = {
4639
4738
  else if (value instanceof Node || value instanceof Element)
4640
4739
  result = false;
4641
4740
  else if (isObject(value))
4642
- result = Object.keys(value).length === 0;
4741
+ result = value.length === 0;
4643
4742
  else
4644
4743
  result = false;
4645
4744
  return result;
@@ -5716,6 +5815,48 @@ function parseMaybeNamedArgument(ctx) {
5716
5815
  const value = ctx.parseExpression();
5717
5816
  return name !== undefined ? { name, value } : { value };
5718
5817
  }
5818
+ function isNakedNamedArgStart(ctx) {
5819
+ if (ctx.check('{'))
5820
+ return false;
5821
+ if (!ctx.checkIdentifierLike())
5822
+ return false;
5823
+ const next = ctx.peekAt(1);
5824
+ return next !== null && next.value === ':';
5825
+ }
5826
+ function parseNakedNamedArgs(ctx, parseValue, command) {
5827
+ const properties = [];
5828
+ const startPos = ctx.getPosition();
5829
+ do {
5830
+ if (!ctx.checkIdentifierLike())
5831
+ break;
5832
+ const keyToken = ctx.advance();
5833
+ const key = {
5834
+ type: 'identifier',
5835
+ name: keyToken.value,
5836
+ start: keyToken.start,
5837
+ end: keyToken.end,
5838
+ line: keyToken.line,
5839
+ column: keyToken.column,
5840
+ };
5841
+ ctx.consume(':', `Expected ':' after property name in ${command} named arguments`);
5842
+ const value = parseValue();
5843
+ if (value) {
5844
+ properties.push({ key, value });
5845
+ }
5846
+ } while (ctx.match(',') && !ctx.isAtEnd());
5847
+ const endPos = ctx.getPosition();
5848
+ return {
5849
+ type: 'objectLiteral',
5850
+ properties,
5851
+ start: startPos.start,
5852
+ end: endPos.end,
5853
+ line: startPos.line,
5854
+ column: startPos.column,
5855
+ };
5856
+ }
5857
+ function isGluedBeepBang(beep, bang) {
5858
+ return beep?.value === 'beep' && bang?.value === '!' && bang.start === beep.end;
5859
+ }
5719
5860
 
5720
5861
  class CommandNodeBuilder {
5721
5862
  constructor(name) {
@@ -5917,6 +6058,8 @@ function parseTriggerCommand(ctx, identifierNode) {
5917
6058
  const modifiers = {};
5918
6059
  while (!isCommandBoundary(ctx)) {
5919
6060
  if (ctx.check(KEYWORDS.ON) || ctx.check(KEYWORDS.TO)) {
6061
+ if (modifiers['on'])
6062
+ break;
5920
6063
  ctx.advance();
5921
6064
  const target = parseOneArgument(ctx, [KEYWORDS.WITH]);
5922
6065
  if (target)
@@ -6057,6 +6200,9 @@ function parseRepeatCommand(ctx, commandToken) {
6057
6200
  ctx.advance();
6058
6201
  loopType = KEYWORDS.FOREVER;
6059
6202
  }
6203
+ else if (ctx.checkIsCommand() || ctx.isCommand(ctx.peek().value)) {
6204
+ loopType = KEYWORDS.FOREVER;
6205
+ }
6060
6206
  else {
6061
6207
  times = ctx.parseExpression();
6062
6208
  if (ctx.check(KEYWORDS.TIMES)) {
@@ -6099,10 +6245,12 @@ function parseRepeatCommand(ctx, commandToken) {
6099
6245
  loopType = result.terminator;
6100
6246
  ctx.advance();
6101
6247
  condition = ctx.parseExpression();
6102
- if (!ctx.check('end')) {
6248
+ if (ctx.check('end')) {
6249
+ ctx.advance();
6250
+ }
6251
+ else if (!ctx.atEndOfInput()) {
6103
6252
  throw new Error('Expected "end" to close repeat block');
6104
6253
  }
6105
- ctx.advance();
6106
6254
  }
6107
6255
  }
6108
6256
  else {
@@ -6348,7 +6496,7 @@ function parseIfCommand(ctx, commandToken) {
6348
6496
  }));
6349
6497
  }
6350
6498
  }
6351
- if (!consumedElseIf && !ctx.isAtEnd()) {
6499
+ if (!consumedElseIf && !ctx.atEndOfInput()) {
6352
6500
  ctx.consume(KEYWORDS.END, "Expected 'end' after if block");
6353
6501
  }
6354
6502
  }
@@ -6373,7 +6521,6 @@ function parseIfCommand(ctx, commandToken) {
6373
6521
  .build();
6374
6522
  }
6375
6523
  function parseForCommand(ctx, commandToken) {
6376
- const args = [];
6377
6524
  let variable = null;
6378
6525
  let collection = null;
6379
6526
  if (ctx.check(KEYWORDS.EACH)) {
@@ -6416,31 +6563,22 @@ function parseForCommand(ctx, commandToken) {
6416
6563
  }
6417
6564
  }
6418
6565
  const commands = ctx.parseCommandListUntilEnd('for');
6419
- args.push({
6420
- type: 'identifier',
6421
- name: 'for',
6422
- start: commandToken.start,
6423
- end: commandToken.end,
6424
- line: commandToken.line,
6425
- column: commandToken.column,
6426
- });
6427
- const forPos = {
6566
+ const pos = {
6428
6567
  start: commandToken.start,
6429
6568
  end: commandToken.end,
6430
6569
  line: commandToken.line,
6431
6570
  column: commandToken.column,
6432
6571
  };
6433
- args.push(createStringLiteral(variable, forPos));
6434
- args.push(collection);
6435
- if (indexVariable) {
6436
- args.push(createStringLiteral(indexVariable, forPos));
6437
- }
6438
- args.push(createBlock(commands, { ...forPos, end: forPos.end || 0 }));
6439
- return CommandNodeBuilder.from({
6440
- ...commandToken,
6441
- value: 'repeat',
6442
- })
6443
- .withArgs(...args)
6572
+ const modifiers = {
6573
+ loopType: toLegacyExpression(createStringLiteral(KEYWORDS.FOR, pos)),
6574
+ for: toLegacyExpression(createStringLiteral(variable, pos)),
6575
+ in: collection,
6576
+ };
6577
+ if (indexVariable)
6578
+ modifiers['index'] = toLegacyExpression(createStringLiteral(indexVariable, pos));
6579
+ return CommandNodeBuilder.from({ ...commandToken, value: 'repeat' })
6580
+ .withArgs(createBlock(commands, { ...pos, end: pos.end || 0 }))
6581
+ .withModifiers(modifiers)
6444
6582
  .endingAt(ctx.getPosition())
6445
6583
  .build();
6446
6584
  }
@@ -6496,86 +6634,159 @@ function parseMeasureCommand(ctx, identifierNode) {
6496
6634
  }
6497
6635
  return builder.build();
6498
6636
  }
6499
- const CONTEXT_POSSESSIVES = new Set(['my', 'its', 'your']);
6637
+ const CONTEXT_REFS = new Set(['me', 'it', 'you', 'result']);
6500
6638
  function parseTransitionCommand(ctx, commandToken) {
6501
- const args = [];
6502
- const modifiers = {};
6503
- let property = null;
6504
- let target = null;
6505
- const leadToken = ctx.peek();
6506
- const startsTarget = ctx.checkAnySelector() ||
6507
- ctx.checkContextVar() ||
6508
- ctx.check('<') ||
6509
- (isIdentifierLike(leadToken) && CONTEXT_POSSESSIVES.has(leadToken.value));
6510
- if (startsTarget) {
6511
- const parsed = ctx.parsePrimary();
6512
- if ((parsed.type === 'possessiveExpression' ||
6513
- (parsed.type === 'memberExpression' && !parsed.computed)) &&
6514
- parsed.object &&
6515
- parsed.property?.name) {
6516
- target = parsed.object;
6517
- property = {
6518
- type: 'string',
6519
- value: parsed.property.name,
6520
- start: leadToken.start || 0,
6521
- end: ctx.getPosition().end,
6522
- line: leadToken.line,
6523
- column: leadToken.column,
6524
- };
6639
+ const pairs = [];
6640
+ do {
6641
+ pairs.push(parseTransitionPair(ctx));
6642
+ } while (startsAnotherPair(ctx));
6643
+ const [first, ...rest] = pairs;
6644
+ const args = first.owner ? [first.owner, first.property] : [first.property];
6645
+ const modifiers = { to: first.to };
6646
+ if (first.from)
6647
+ modifiers['from'] = first.from;
6648
+ if (rest.length > 0)
6649
+ modifiers['pairs'] = pairsNode(rest);
6650
+ for (let i = 0; i < 3 && !ctx.isAtEnd(); i++) {
6651
+ if (ctx.check('over') && !modifiers['over']) {
6652
+ ctx.advance();
6653
+ modifiers['over'] = ctx.parseExpression();
6525
6654
  }
6526
- else {
6527
- target = parsed;
6528
- if (ctx.check("'s"))
6529
- ctx.advance();
6655
+ else if (ctx.check('using') && !modifiers['using']) {
6656
+ ctx.advance();
6657
+ modifiers['using'] = ctx.parseExpression();
6530
6658
  }
6531
- }
6532
- const firstToken = ctx.peek();
6533
- if (!property && (isIdentifierLike(firstToken) || firstToken.value === '*')) {
6534
- let propertyValue = '';
6535
- if (ctx.check('*')) {
6536
- propertyValue = '*';
6659
+ else if (ctx.check(KEYWORDS.WITH) && !modifiers['with']) {
6537
6660
  ctx.advance();
6661
+ modifiers['with'] = ctx.parsePrimary();
6538
6662
  }
6539
- const hyphenatedName = parseHyphenatedName(ctx);
6540
- if (hyphenatedName) {
6541
- propertyValue += hyphenatedName;
6542
- property = {
6543
- type: 'string',
6544
- value: propertyValue,
6545
- start: firstToken.start || 0,
6546
- end: ctx.getPosition().end,
6547
- line: firstToken.line,
6548
- column: firstToken.column,
6549
- };
6663
+ else {
6664
+ break;
6550
6665
  }
6551
6666
  }
6552
- if (!property) {
6553
- throw new Error('Transition command requires a CSS property');
6667
+ return CommandNodeBuilder.from(commandToken)
6668
+ .withArgs(...args)
6669
+ .withModifiers(modifiers)
6670
+ .endingAt(ctx.getPosition())
6671
+ .build();
6672
+ }
6673
+ function parseTransitionPair(ctx) {
6674
+ const { owner, property } = parseTransitionProperty(ctx);
6675
+ let from = null;
6676
+ if (ctx.check('from')) {
6677
+ ctx.advance();
6678
+ from = ctx.parseExpression();
6554
6679
  }
6555
- if (target)
6556
- args.push(target);
6557
- args.push(property);
6558
6680
  if (!ctx.check(KEYWORDS.TO)) {
6559
6681
  throw new Error('Expected "to" keyword after property in transition command');
6560
6682
  }
6561
6683
  ctx.advance();
6562
- const value = ctx.parseExpression();
6563
- modifiers['to'] = value;
6564
- if (ctx.check('over')) {
6565
- ctx.advance();
6566
- const duration = ctx.parseExpression();
6567
- modifiers['over'] = duration;
6684
+ const to = ctx.parseExpression();
6685
+ return { owner, property, from, to };
6686
+ }
6687
+ function parseTransitionProperty(ctx) {
6688
+ const lead = ctx.peek();
6689
+ if (!lead || isKeyword(lead, [KEYWORDS.TO, 'from'])) {
6690
+ throw new Error('Transition command requires a CSS property');
6568
6691
  }
6569
- if (ctx.check(KEYWORDS.WITH)) {
6692
+ const expr = ctx.parseExpression();
6693
+ const split = splitStyleExpression(expr);
6694
+ if (split)
6695
+ return { owner: split.owner, property: propertyNode(split.name, lead, ctx) };
6696
+ let name = '';
6697
+ if (ctx.check('*')) {
6698
+ name = '*';
6570
6699
  ctx.advance();
6571
- const timingFunction = ctx.parsePrimary();
6572
- modifiers['with'] = timingFunction;
6573
6700
  }
6574
- return CommandNodeBuilder.from(commandToken)
6575
- .withArgs(...args)
6576
- .withModifiers(modifiers)
6577
- .endingAt(ctx.getPosition())
6578
- .build();
6701
+ const hyphenated = parseHyphenatedName(ctx);
6702
+ if (!hyphenated)
6703
+ throw new Error('Transition command requires a CSS property');
6704
+ return { owner: expr, property: propertyNode(name + hyphenated, lead, ctx) };
6705
+ }
6706
+ function splitStyleExpression(node) {
6707
+ const n = node;
6708
+ switch (n.type) {
6709
+ case 'selector':
6710
+ return typeof n.value === 'string' && n.value.startsWith('*')
6711
+ ? { owner: null, name: n.value }
6712
+ : null;
6713
+ case 'identifier':
6714
+ return typeof n.name === 'string' && !CONTEXT_REFS.has(n.name)
6715
+ ? { owner: null, name: n.name }
6716
+ : null;
6717
+ case 'possessiveExpression':
6718
+ case 'memberExpression':
6719
+ return !n.computed && n.object && typeof n.property?.name === 'string'
6720
+ ? { owner: n.object, name: n.property.name }
6721
+ : null;
6722
+ case 'binaryExpression': {
6723
+ if (n.operator !== 'of' || !n.left || !n.right)
6724
+ return null;
6725
+ const inner = splitStyleExpression(n.left);
6726
+ return inner && !inner.owner ? { owner: n.right, name: inner.name } : null;
6727
+ }
6728
+ case 'propertyOfExpression':
6729
+ return n.target && typeof n.property?.name === 'string'
6730
+ ? { owner: n.target, name: n.property.name }
6731
+ : null;
6732
+ default:
6733
+ return null;
6734
+ }
6735
+ }
6736
+ function propertyNode(name, lead, ctx) {
6737
+ return {
6738
+ type: 'string',
6739
+ value: name,
6740
+ start: lead.start || 0,
6741
+ end: ctx.getPosition().end,
6742
+ line: lead.line,
6743
+ column: lead.column,
6744
+ };
6745
+ }
6746
+ function startsAnotherPair(ctx) {
6747
+ if (ctx.isAtEnd() || isComment(ctx.peek()))
6748
+ return false;
6749
+ if (isKeyword(ctx.peek(), ['over', 'using', KEYWORDS.WITH]))
6750
+ return false;
6751
+ return !isCommandBoundary(ctx, ['catch', 'finally', 'on']);
6752
+ }
6753
+ function pairsNode(pairs) {
6754
+ const field = (name, value) => ({
6755
+ key: {
6756
+ type: 'identifier',
6757
+ name,
6758
+ start: value.start,
6759
+ end: value.end,
6760
+ line: value.line,
6761
+ column: value.column,
6762
+ },
6763
+ value,
6764
+ });
6765
+ const elements = pairs.map(pair => {
6766
+ const properties = [field('property', pair.property), field('to', pair.to)];
6767
+ if (pair.owner)
6768
+ properties.push(field('owner', pair.owner));
6769
+ if (pair.from)
6770
+ properties.push(field('from', pair.from));
6771
+ return {
6772
+ type: 'objectLiteral',
6773
+ properties,
6774
+ start: (pair.owner ?? pair.property).start,
6775
+ end: pair.to.end,
6776
+ line: (pair.owner ?? pair.property).line,
6777
+ column: (pair.owner ?? pair.property).column,
6778
+ };
6779
+ });
6780
+ const firstEl = elements[0];
6781
+ const lastEl = elements[elements.length - 1];
6782
+ return {
6783
+ type: 'arrayLiteral',
6784
+ elements,
6785
+ start: firstEl.start,
6786
+ end: lastEl.end,
6787
+ line: firstEl.line,
6788
+ column: firstEl.column,
6789
+ };
6579
6790
  }
6580
6791
  function parseStartCommand(ctx, identifierNode) {
6581
6792
  if (!ctx.match('view')) {
@@ -6695,6 +6906,15 @@ function parseToggleCommand(ctx, identifierNode) {
6695
6906
  args.push(classArg.expression);
6696
6907
  modifiers['as'] = literalModifier(mode);
6697
6908
  }
6909
+ else if (classArg?.type === 'binaryExpression' &&
6910
+ classArg.operator === 'of' &&
6911
+ classArg.left?.type === 'selector' &&
6912
+ typeof classArg.left.value === 'string' &&
6913
+ classArg.left.value.startsWith('*') &&
6914
+ classArg.right) {
6915
+ args.push(classArg.left);
6916
+ modifiers['on'] = classArg.right;
6917
+ }
6698
6918
  else if (classArg) {
6699
6919
  args.push(classArg);
6700
6920
  }
@@ -6872,11 +7092,17 @@ function parseSwapCommand(ctx, identifierNode) {
6872
7092
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
6873
7093
  if (targetExpr)
6874
7094
  args.push(targetExpr);
6875
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
7095
+ const isMorph = identifierNode.name === 'morph';
7096
+ if (!isDelete &&
7097
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
7098
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
6876
7099
  const contentExpr = parseOneArgument(ctx, ['using']);
6877
7100
  if (contentExpr)
6878
7101
  modifiers['with'] = contentExpr;
6879
7102
  }
7103
+ if (isMorph && modifiers['with'] === undefined) {
7104
+ ctx.addError("Expected 'to' and the content to morph into");
7105
+ }
6880
7106
  return CommandNodeBuilder.fromIdentifier(identifierNode)
6881
7107
  .withArgs(...args)
6882
7108
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -7637,6 +7863,8 @@ function atArgumentBoundary(ctx, grammar) {
7637
7863
  const next = ctx.peek();
7638
7864
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
7639
7865
  return false;
7866
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
7867
+ return false;
7640
7868
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
7641
7869
  return true;
7642
7870
  return isKeyword(next, [...grammar.markers]);
@@ -7668,10 +7896,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
7668
7896
  }
7669
7897
  }
7670
7898
  const commaList = new Set(grammar.commaList ?? []);
7899
+ const namedArgs = new Set(grammar.namedArgs ?? []);
7671
7900
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
7672
7901
  const keyword = ctx.advance().value;
7673
7902
  const key = ctx.resolveKeyword(keyword).toLowerCase();
7674
- let value = ctx.parseExpression();
7903
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
7904
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
7905
+ : ctx.parseExpression();
7675
7906
  if (value && commaList.has(key) && ctx.check(',')) {
7676
7907
  const elements = [value];
7677
7908
  while (ctx.match(',')) {
@@ -7836,8 +8067,8 @@ function parseFetchCommand(ctx, commandToken) {
7836
8067
  }
7837
8068
  if (ctx.check('with') && !modifiers['with']) {
7838
8069
  ctx.advance();
7839
- if (isFetchNakedNamedArgStart(ctx)) {
7840
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
8070
+ if (isNakedNamedArgStart(ctx)) {
8071
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
7841
8072
  }
7842
8073
  else {
7843
8074
  modifiers['with'] = ctx.parsePrimary();
@@ -7872,45 +8103,6 @@ function parseFetchCommand(ctx, commandToken) {
7872
8103
  }
7873
8104
  return builder.build();
7874
8105
  }
7875
- function isFetchNakedNamedArgStart(ctx) {
7876
- if (ctx.check('{'))
7877
- return false;
7878
- if (!ctx.checkIdentifierLike())
7879
- return false;
7880
- const next = ctx.peekAt(1);
7881
- return next !== null && next.value === ':';
7882
- }
7883
- function parseFetchNakedNamedArgs(ctx) {
7884
- const properties = [];
7885
- const startPos = ctx.getPosition();
7886
- do {
7887
- if (!ctx.checkIdentifierLike())
7888
- break;
7889
- const keyToken = ctx.advance();
7890
- const key = {
7891
- type: 'identifier',
7892
- name: keyToken.value,
7893
- start: keyToken.start,
7894
- end: keyToken.end,
7895
- line: keyToken.line,
7896
- column: keyToken.column,
7897
- };
7898
- ctx.consume(':', "Expected ':' after property name in fetch named arguments");
7899
- const value = ctx.parsePrimary();
7900
- if (value) {
7901
- properties.push({ key, value });
7902
- }
7903
- } while (ctx.match(',') && !ctx.isAtEnd());
7904
- const endPos = ctx.getPosition();
7905
- return {
7906
- type: 'objectLiteral',
7907
- properties,
7908
- start: startPos.start,
7909
- end: endPos.end,
7910
- line: startPos.line,
7911
- column: startPos.column,
7912
- };
7913
- }
7914
8106
  function findJsEndBoundary(ctx, startPos) {
7915
8107
  const input = ctx.getInputSlice(startPos);
7916
8108
  if (!input) {
@@ -8030,6 +8222,7 @@ function parseTellCommand(ctx, identifierNode) {
8030
8222
  const commands = [];
8031
8223
  while (!ctx.isAtEnd()) {
8032
8224
  if (ctx.checkIsCommand()) {
8225
+ const word = ctx.peek().value;
8033
8226
  try {
8034
8227
  ctx.advance();
8035
8228
  const cmd = ctx.parseCommand();
@@ -8040,7 +8233,8 @@ function parseTellCommand(ctx, identifierNode) {
8040
8233
  break;
8041
8234
  }
8042
8235
  }
8043
- catch {
8236
+ catch (error) {
8237
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
8044
8238
  break;
8045
8239
  }
8046
8240
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -8189,7 +8383,6 @@ const ONE_EXPR = (syntax) => ({
8189
8383
  markers: [],
8190
8384
  syntax,
8191
8385
  });
8192
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
8193
8386
  const COMMAND_GRAMMAR = {
8194
8387
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
8195
8388
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -8200,8 +8393,7 @@ const COMMAND_GRAMMAR = {
8200
8393
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
8201
8394
  },
8202
8395
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
8203
- 'beep!': BEEP,
8204
- beep: BEEP,
8396
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
8205
8397
  blur: ONE_EXPR('blur [<target>]'),
8206
8398
  break: NONE,
8207
8399
  breakpoint: NONE,
@@ -8225,7 +8417,8 @@ const COMMAND_GRAMMAR = {
8225
8417
  render: {
8226
8418
  positional: 'expression',
8227
8419
  markers: ['with'],
8228
- syntax: 'render <template> [with <variables>]',
8420
+ namedArgs: ['with'],
8421
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
8229
8422
  },
8230
8423
  reset: ONE_EXPR('reset [<form>]'),
8231
8424
  return: ONE_EXPR('return [<value>]'),
@@ -8580,7 +8773,10 @@ class Parser {
8580
8773
  const firstToken = this.peek();
8581
8774
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
8582
8775
  let left;
8583
- if (prefixEntry?.prefix) {
8776
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
8777
+ left = this.parseBeepExpression();
8778
+ }
8779
+ else if (prefixEntry?.prefix) {
8584
8780
  const token = this.advance();
8585
8781
  if (this.isAtEnd()) {
8586
8782
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -8600,6 +8796,9 @@ class Parser {
8600
8796
  continue;
8601
8797
  }
8602
8798
  }
8799
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
8800
+ break;
8801
+ }
8603
8802
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
8604
8803
  if (!infixEntry?.infix) {
8605
8804
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -8661,6 +8860,35 @@ class Parser {
8661
8860
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
8662
8861
  return left;
8663
8862
  }
8863
+ parseBeepExpression() {
8864
+ const beepToken = this.advance();
8865
+ this.advance();
8866
+ if (this.isAtEnd()) {
8867
+ this.addError("Expected expression after 'beep!'");
8868
+ return this.createErrorNode();
8869
+ }
8870
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
8871
+ return {
8872
+ type: 'unaryExpression',
8873
+ operator: 'beep!',
8874
+ operand,
8875
+ argument: operand,
8876
+ prefix: true,
8877
+ start: beepToken.start,
8878
+ end: this.previous().end,
8879
+ line: beepToken.line,
8880
+ column: beepToken.column,
8881
+ };
8882
+ }
8883
+ isStyleRefStar(i) {
8884
+ const star = this.tokens[i];
8885
+ const name = this.tokens[i + 1];
8886
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
8887
+ }
8888
+ isSpacedStyleRef(i) {
8889
+ const prev = this.tokens[i - 1];
8890
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
8891
+ }
8664
8892
  tryParseStringPostfix(left, nextToken) {
8665
8893
  const v = nextToken.value;
8666
8894
  let unit = null;
@@ -8863,6 +9091,10 @@ class Parser {
8863
9091
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
8864
9092
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
8865
9093
  while (!this.isAtEnd() && !isStop()) {
9094
+ if (this.checkComment()) {
9095
+ this.advance();
9096
+ continue;
9097
+ }
8866
9098
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
8867
9099
  let parsedCommand = false;
8868
9100
  const isCommandToken = this.checkIsCommand();
@@ -8946,7 +9178,7 @@ class Parser {
8946
9178
  debug.parse('\u2705 Found "end", consuming it');
8947
9179
  this.advance();
8948
9180
  }
8949
- else {
9181
+ else if (!this.atEndOfInput()) {
8950
9182
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
8951
9183
  throw new Error(`Expected "end" to close ${construct} block`);
8952
9184
  }
@@ -8959,7 +9191,7 @@ class Parser {
8959
9191
  if (this.check('end')) {
8960
9192
  this.advance();
8961
9193
  }
8962
- else {
9194
+ else if (!this.atEndOfInput()) {
8963
9195
  throw new Error('Expected "end" to close repeat block');
8964
9196
  }
8965
9197
  }
@@ -8975,7 +9207,10 @@ class Parser {
8975
9207
  this.advance();
8976
9208
  }
8977
9209
  else if (terminator === '') {
8978
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
9210
+ if (!this.atEndOfInput()) {
9211
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
9212
+ }
9213
+ return { commands, terminator: 'end' };
8979
9214
  }
8980
9215
  return { commands, terminator };
8981
9216
  }
@@ -9559,7 +9794,9 @@ class Parser {
9559
9794
  parseTopLevelInitBlock() {
9560
9795
  const pos = this.getPosition();
9561
9796
  const initCommands = this.parseCommandBlock(['end']);
9562
- this.consume('end', "Expected 'end' after init block");
9797
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
9798
+ this.consume('end', "Expected 'end' after init block");
9799
+ }
9563
9800
  return {
9564
9801
  type: 'initBlock',
9565
9802
  commands: initCommands,
@@ -9735,10 +9972,29 @@ class Parser {
9735
9972
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
9736
9973
  };
9737
9974
  }
9975
+ isArgumentListOnly(open) {
9976
+ let depth = 0;
9977
+ for (let i = open; i < this.tokens.length; i++) {
9978
+ const token = this.tokens[i];
9979
+ if (token.kind !== 'operator')
9980
+ continue;
9981
+ if (token.value === '(' || token.value === '[' || token.value === '{')
9982
+ depth++;
9983
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
9984
+ if (--depth === 0)
9985
+ return i === open + 1;
9986
+ }
9987
+ else if (token.value === ',' && depth === 1)
9988
+ return true;
9989
+ }
9990
+ return false;
9991
+ }
9738
9992
  tryParsePseudoCommand(callExpr, sourceExpr) {
9739
9993
  const methodName = callExpr.callee.name;
9740
9994
  const nextToken = this.peek();
9741
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
9995
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
9996
+ const hasPseudoCommandPattern = nextIsArticle ||
9997
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
9742
9998
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
9743
9999
  isContextVar(nextToken);
9744
10000
  if (!hasPseudoCommandPattern) {
@@ -9764,7 +10020,10 @@ class Parser {
9764
10020
  });
9765
10021
  }
9766
10022
  let preposition;
9767
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
10023
+ if (nextIsArticle) {
10024
+ this.advance();
10025
+ }
10026
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
9768
10027
  preposition = this.advance().value.toLowerCase();
9769
10028
  }
9770
10029
  let targetExpr;
@@ -9928,10 +10187,22 @@ class Parser {
9928
10187
  this.consume(']', "Expected ']' after event condition");
9929
10188
  }
9930
10189
  let target;
10190
+ let targetExpression;
9931
10191
  if (this.match('from')) {
9932
- const targetToken = this.advance();
9933
- target = targetToken.value;
9934
- debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target} (kind: ${targetToken.kind})`);
10192
+ if (this.check('the'))
10193
+ this.advance();
10194
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
10195
+ const startIndex = this.current;
10196
+ targetExpression = this.parseExpression();
10197
+ target = this.tokens
10198
+ .slice(startIndex, this.current)
10199
+ .map(t => t.value)
10200
+ .join(' ');
10201
+ }
10202
+ else {
10203
+ target = this.advance().value;
10204
+ }
10205
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
9935
10206
  this.parseTemporalKeywordModifiers(modifiers);
9936
10207
  }
9937
10208
  let attributeName;
@@ -9977,7 +10248,7 @@ class Parser {
9977
10248
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
9978
10249
  const commandName = this.peek().value.toLowerCase();
9979
10250
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
9980
- if (nextIsOpenParen && !isSpecialBodyCommand) {
10251
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
9981
10252
  let expr;
9982
10253
  const savedError = this.error;
9983
10254
  try {
@@ -10009,7 +10280,7 @@ class Parser {
10009
10280
  }
10010
10281
  }
10011
10282
  }
10012
- else if (this.checkIdentifier()) {
10283
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
10013
10284
  const token = this.peek();
10014
10285
  if (this.isCommand(token.value)) {
10015
10286
  const cmd = this.parseCommandWithErrorRecovery();
@@ -10118,6 +10389,7 @@ class Parser {
10118
10389
  ...(eventParams.length > 0 && { args: eventParams }),
10119
10390
  ...(condition && { condition: fromLegacyExpression(condition) }),
10120
10391
  ...(target && { target }),
10392
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
10121
10393
  ...(attributeName && { attributeName }),
10122
10394
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
10123
10395
  ...(customEventSource && { customEventSource }),
@@ -10373,10 +10645,9 @@ class Parser {
10373
10645
  }
10374
10646
  parseCommandCore() {
10375
10647
  const commandToken = this.previous();
10376
- let commandName = commandToken.value;
10648
+ const commandName = commandToken.value;
10377
10649
  if (commandName === 'beep' && this.check('!')) {
10378
10650
  this.advance();
10379
- commandName = 'beep!';
10380
10651
  }
10381
10652
  if (commandName === 'fetch') {
10382
10653
  return parseFetchCommand(this.getContext(), commandToken);
@@ -10475,6 +10746,9 @@ class Parser {
10475
10746
  args.push(this.parsePrimary());
10476
10747
  }
10477
10748
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
10749
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
10750
+ callNode.closestTo = this.parseExpression();
10751
+ }
10478
10752
  if (funcName === 'next' || funcName === 'previous') {
10479
10753
  const relativePositional = this.parseRelativePositionalModifiers();
10480
10754
  if (relativePositional) {
@@ -10514,6 +10788,9 @@ class Parser {
10514
10788
  }
10515
10789
  parseTheXofY() {
10516
10790
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
10791
+ if (this.isStyleRefStar(this.current)) {
10792
+ return this.parsePrimary();
10793
+ }
10517
10794
  if (!this.checkIdentifier()) {
10518
10795
  return this.createIdentifier('the');
10519
10796
  }
@@ -10529,7 +10806,7 @@ class Parser {
10529
10806
  return this.createIdentifier(propertyName);
10530
10807
  }
10531
10808
  this.advance();
10532
- const target = this.parseCall();
10809
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
10533
10810
  return {
10534
10811
  type: 'propertyOfExpression',
10535
10812
  property: { type: 'identifier', name: propertyName },
@@ -10727,6 +11004,13 @@ class Parser {
10727
11004
  checkLiteral() {
10728
11005
  return this.checkPredicate(isLiteral);
10729
11006
  }
11007
+ checkEventNamedCall() {
11008
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
11009
+ return false;
11010
+ const token = this.peek();
11011
+ const next = this.tokens[this.current + 1];
11012
+ return next?.value === '(' && next.start === token.end;
11013
+ }
10730
11014
  checkIdentifierLike() {
10731
11015
  return this.checkPredicate(isIdentifierLike);
10732
11016
  }
@@ -10835,6 +11119,22 @@ class Parser {
10835
11119
  isAtEnd() {
10836
11120
  return this.current >= this.tokens.length;
10837
11121
  }
11122
+ atEndOfInput() {
11123
+ for (let i = this.current; i < this.tokens.length; i++) {
11124
+ if (!isComment(this.tokens[i]))
11125
+ return false;
11126
+ }
11127
+ return true;
11128
+ }
11129
+ checkFeatureStart() {
11130
+ if (this.isAtEnd())
11131
+ return false;
11132
+ const value = this.peek().value;
11133
+ return (value === 'init' ||
11134
+ value === 'on' ||
11135
+ value === 'def' ||
11136
+ getRegisteredFeature(value) !== undefined);
11137
+ }
10838
11138
  peek() {
10839
11139
  if (this.isAtEnd()) {
10840
11140
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -11046,6 +11346,7 @@ class Parser {
11046
11346
  match: this.match.bind(this),
11047
11347
  matchOperator: this.matchOperator.bind(this),
11048
11348
  isAtEnd: this.isAtEnd.bind(this),
11349
+ atEndOfInput: this.atEndOfInput.bind(this),
11049
11350
  createIdentifier: this.createIdentifier.bind(this),
11050
11351
  parseExpression: this.parseExpression.bind(this),
11051
11352
  parsePrimary: this.parsePrimary.bind(this),
@@ -11096,7 +11397,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
11096
11397
  'ignoring case',
11097
11398
  ]);
11098
11399
  Parser.PRATT_TABLE = PARSER_TABLE;
11400
+ Parser.BEEP_OPERAND_BP = 80;
11099
11401
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
11402
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
11403
+ 'closest',
11404
+ 'next',
11405
+ 'previous',
11406
+ 'first',
11407
+ 'last',
11408
+ ]);
11100
11409
  function parse(input, options) {
11101
11410
  const tokens = tokenize(input);
11102
11411
  const parser = new Parser(tokens, options, input);
@@ -11538,11 +11847,16 @@ async function evaluateBinaryExpression(node, context) {
11538
11847
  }
11539
11848
  }
11540
11849
  }
11541
- if (leftNode?.type === 'selector' && leftNode.fromQuery && typeof leftNode.value === 'string') {
11850
+ if (leftNode?.type === 'selector' &&
11851
+ typeof leftNode.value === 'string' &&
11852
+ (leftNode.fromQuery || (operator === 'in' && leftNode.value.startsWith('.')))) {
11542
11853
  const root = await evaluateAST(node.right, context);
11543
11854
  const scope = root && typeof root.querySelectorAll === 'function' ? root : null;
11544
11855
  if (scope) {
11545
- return Array.from(scope.querySelectorAll(leftNode.value));
11856
+ const query = leftNode.fromQuery
11857
+ ? leftNode.value
11858
+ : escapeSelectorForQuery(leftNode, leftNode.value);
11859
+ return Array.from(scope.querySelectorAll(query));
11546
11860
  }
11547
11861
  }
11548
11862
  }
@@ -11554,6 +11868,12 @@ async function evaluateBinaryExpression(node, context) {
11554
11868
  return getExpr(context, 'styleRef').evaluate(context, leftNode.value.slice(1), el);
11555
11869
  }
11556
11870
  if (operator === 'of') {
11871
+ if (leftNode?.type === 'attributeAccess') {
11872
+ const target = await evaluateAST(node.right, context);
11873
+ if (target == null)
11874
+ return undefined;
11875
+ return getExpr(context, 'possessive').evaluate(context, target, `@${leftNode.attributeName}`);
11876
+ }
11557
11877
  const path = propertyPathOf(node.left);
11558
11878
  if (path) {
11559
11879
  const target = await evaluateAST(node.right, context);
@@ -11561,6 +11881,10 @@ async function evaluateBinaryExpression(node, context) {
11561
11881
  for (const seg of path) {
11562
11882
  if (cur == null)
11563
11883
  return undefined;
11884
+ if (isMappableCollection(cur) && !(seg in cur)) {
11885
+ cur = await getExpr(context, 'possessive').evaluate(context, cur, seg);
11886
+ continue;
11887
+ }
11564
11888
  cur = isElement(cur) ? getElementProperty(cur, seg) : cur[seg];
11565
11889
  }
11566
11890
  return typeof cur === 'function' ? cur.bind(target) : cur;
@@ -11585,26 +11909,21 @@ async function evaluateBinaryExpression(node, context) {
11585
11909
  const R = node.ignoringCase ? applyCI(right) : right;
11586
11910
  switch (operator) {
11587
11911
  case '+': {
11588
- let addLeft = left;
11589
- let addRight = right;
11590
11912
  if (node.coerceNumeric) {
11591
- if (typeof addLeft === 'string')
11592
- addLeft = convertToNumber(addLeft);
11593
- if (typeof addRight === 'string')
11594
- addRight = convertToNumber(addRight);
11913
+ return counterValue(left) + counterAmount(right);
11595
11914
  }
11596
- if (typeof addLeft === 'string' || typeof addRight === 'string') {
11597
- return String(addLeft ?? '') + String(addRight ?? '');
11915
+ if (typeof left === 'string' || typeof right === 'string') {
11916
+ return String(left ?? '') + String(right ?? '');
11598
11917
  }
11599
- if (Array.isArray(addLeft)) {
11600
- return Array.isArray(addRight) ? [...addLeft, ...addRight] : [...addLeft, addRight];
11918
+ if (Array.isArray(left)) {
11919
+ return Array.isArray(right) ? [...left, ...right] : [...left, right];
11601
11920
  }
11602
- return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, {
11603
- left: addLeft,
11604
- right: addRight,
11605
- }));
11921
+ return unwrapTypedResult(await getExpr(context, 'addition').evaluate(context, { left, right }));
11606
11922
  }
11607
11923
  case '-':
11924
+ if (node.coerceNumeric) {
11925
+ return counterValue(left) - counterAmount(right);
11926
+ }
11608
11927
  return unwrapTypedResult(await getExpr(context, 'subtraction').evaluate(context, { left, right }));
11609
11928
  case '*':
11610
11929
  return unwrapTypedResult(await getExpr(context, 'multiplication').evaluate(context, { left, right }));
@@ -11862,9 +12181,8 @@ async function evaluatePropertyOfExpressionNode(node, context) {
11862
12181
  }
11863
12182
  const propertyName = propertyNode.name;
11864
12183
  const target = await evaluateAST(node.target, context);
11865
- if (target == null) {
11866
- throw new Error(`Cannot access property "${propertyName}" of ${target}`);
11867
- }
12184
+ if (target == null)
12185
+ return undefined;
11868
12186
  return getExpr(context, 'possessive').evaluate(context, target, propertyName);
11869
12187
  }
11870
12188
  async function evaluateTemplateLiteralNode(node, context) {
@@ -11982,6 +12300,9 @@ async function evaluateUnaryExpression(node, context) {
11982
12300
  case 'not':
11983
12301
  case '!':
11984
12302
  return logicalExpressions.not.evaluate(context, value);
12303
+ case 'beep!':
12304
+ beepValues(context.me, [value]);
12305
+ return value;
11985
12306
  case 'no':
11986
12307
  return logicalExpressions.no.evaluate(context, value);
11987
12308
  case '-':
@@ -12071,6 +12392,10 @@ async function evaluateCallExpression(node, context) {
12071
12392
  const args = await resolveCallArgs(node.arguments, funcName, context);
12072
12393
  switch (funcName) {
12073
12394
  case 'closest':
12395
+ if (node.closestTo) {
12396
+ const start = unwrapElement(await evaluateAST(node.closestTo, context)) ?? null;
12397
+ return referencesExpressions.closest.evaluate(context, args[0], start);
12398
+ }
12074
12399
  return referencesExpressions.closest.evaluate(context, ...args);
12075
12400
  case 'previous':
12076
12401
  case 'next': {