@hyperfixi/core 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +28 -36
  2. package/dist/ast/command-slots.d.ts +1 -1
  3. package/dist/ast/nodes.d.ts +2 -0
  4. package/dist/ast-utils/index.cjs +193 -48
  5. package/dist/ast-utils/index.mjs +193 -48
  6. package/dist/ast-utils/interchange/types.d.ts +19 -1
  7. package/dist/behaviors/index.cjs +19 -0
  8. package/dist/behaviors/index.mjs +19 -0
  9. package/dist/bundle-generator/index.cjs +24 -9
  10. package/dist/bundle-generator/index.mjs +24 -9
  11. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  12. package/dist/chunks/bridge-Cnj8dQFV.js +2 -0
  13. package/dist/chunks/browser-modular-BNDYF6Dq.js +2 -0
  14. package/dist/chunks/index-DQi2o8Lk.js +2 -0
  15. package/dist/chunks/index-LLuwEQp0.js +2 -0
  16. package/dist/chunks/index-uCOQ8Qn0.js +2 -0
  17. package/dist/commands/animation/transition.d.ts +12 -6
  18. package/dist/commands/helpers/loop-executor.d.ts +2 -2
  19. package/dist/commands/helpers/numeric-target-parser.d.ts +1 -1
  20. package/dist/commands/helpers/variable-access.d.ts +5 -2
  21. package/dist/commands/helpers/write-target.d.ts +5 -0
  22. package/dist/commands/index.cjs +929 -425
  23. package/dist/commands/index.mjs +929 -425
  24. package/dist/commands/templates/render.d.ts +4 -4
  25. package/dist/commands/utility/beep.d.ts +0 -3
  26. package/dist/expressions/index.cjs +7 -13
  27. package/dist/expressions/index.mjs +7 -13
  28. package/dist/htmx/i18n-hooks.d.ts +4 -1
  29. package/dist/hyperfixi-browser-classic-i18n.js +1 -1
  30. package/dist/hyperfixi-browser.js +1 -1
  31. package/dist/hyperfixi-classic-i18n.js +1 -1
  32. package/dist/hyperfixi-hx-v4.js +1 -1
  33. package/dist/hyperfixi-hx.js +1 -1
  34. package/dist/hyperfixi-hybrid-complete.js +1 -1
  35. package/dist/hyperfixi-hybrid-hx.js +1 -1
  36. package/dist/hyperfixi-multilingual.js +1 -1
  37. package/dist/hyperfixi.js +1 -1
  38. package/dist/hyperfixi.mjs +1 -1
  39. package/dist/index.cjs +1140 -485
  40. package/dist/index.min.js +1 -1
  41. package/dist/index.mjs +1140 -485
  42. package/dist/lib/swap-executor.d.ts +1 -1
  43. package/dist/lokascript-browser-classic-i18n.js +1 -1
  44. package/dist/lokascript-browser.js +1 -1
  45. package/dist/lokascript-hybrid-complete.js +1 -1
  46. package/dist/lokascript-hybrid-hx.js +1 -1
  47. package/dist/lokascript-multilingual.js +1 -1
  48. package/dist/metadata.cjs +9 -9
  49. package/dist/metadata.d.ts +1 -1
  50. package/dist/metadata.mjs +9 -9
  51. package/dist/parser/command-grammar.d.ts +1 -0
  52. package/dist/parser/full-parser.cjs +387 -153
  53. package/dist/parser/full-parser.mjs +387 -153
  54. package/dist/parser/helpers/parsing-helpers.d.ts +3 -0
  55. package/dist/parser/hybrid/index.cjs +3 -0
  56. package/dist/parser/hybrid/index.mjs +3 -0
  57. package/dist/parser/hybrid/parser-core.cjs +3 -0
  58. package/dist/parser/hybrid/parser-core.mjs +3 -0
  59. package/dist/parser/hybrid-parser.cjs +3 -0
  60. package/dist/parser/hybrid-parser.mjs +3 -0
  61. package/dist/parser/parser-types.d.ts +2 -0
  62. package/dist/parser/parser.d.ts +9 -0
  63. package/dist/parser/pratt-parser.d.ts +2 -0
  64. package/dist/registry/index.cjs +526 -210
  65. package/dist/registry/index.mjs +526 -210
  66. package/dist/utils/beep.d.ts +8 -0
  67. package/dist/version.d.ts +1 -1
  68. package/package.json +21 -18
  69. package/vocab/htmx/ar.js +22 -20
  70. package/vocab/htmx/bn.js +21 -5
  71. package/vocab/htmx/de.js +33 -10
  72. package/vocab/htmx/en.js +4 -1
  73. package/vocab/htmx/es.js +28 -9
  74. package/vocab/htmx/fr.js +28 -8
  75. package/vocab/htmx/he.js +22 -1
  76. package/vocab/htmx/hi.js +28 -7
  77. package/vocab/htmx/id.js +29 -10
  78. package/vocab/htmx/it.js +26 -5
  79. package/vocab/htmx/ja.js +36 -11
  80. package/vocab/htmx/ko.js +27 -6
  81. package/vocab/htmx/ms.js +24 -1
  82. package/vocab/htmx/pl.js +27 -7
  83. package/vocab/htmx/pt.js +30 -9
  84. package/vocab/htmx/qu.js +32 -9
  85. package/vocab/htmx/ru.js +28 -6
  86. package/vocab/htmx/sw.js +28 -8
  87. package/vocab/htmx/th.js +19 -1
  88. package/vocab/htmx/tl.js +24 -1
  89. package/vocab/htmx/tr.js +34 -8
  90. package/vocab/htmx/uk.js +28 -6
  91. package/vocab/htmx/vi.js +27 -19
  92. package/vocab/htmx/zh.js +28 -9
  93. package/dist/chunks/bridge-DylVo1oZ.js +0 -2
  94. package/dist/chunks/browser-modular-sLPMdxR7.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
@@ -1691,6 +1691,39 @@ function makeTypeCheckHandler(negated) {
1691
1691
  };
1692
1692
  };
1693
1693
  }
1694
+ const OF_OPERAND_BP = 70;
1695
+ function parseOfOperand(ctx, minBp = OF_OPERAND_BP) {
1696
+ let right = ctx.parseExpr(minBp);
1697
+ if (ctx.peek()?.value === 'in') {
1698
+ const inToken = ctx.advance();
1699
+ const scope = ctx.parseExpr(minBp);
1700
+ right = {
1701
+ type: 'binaryExpression',
1702
+ operator: 'in',
1703
+ left: right,
1704
+ right: scope,
1705
+ start: right.start,
1706
+ end: scope.end ?? inToken.end,
1707
+ line: right.line ?? inToken.line,
1708
+ column: right.column ?? inToken.column,
1709
+ };
1710
+ }
1711
+ return right;
1712
+ }
1713
+ const THE_OF_TARGET_BP = OF_OPERAND_BP + 1;
1714
+ function ofHandler(left, token, ctx) {
1715
+ const right = parseOfOperand(ctx);
1716
+ return {
1717
+ type: 'binaryExpression',
1718
+ operator: token.value,
1719
+ left,
1720
+ right,
1721
+ start: left.start,
1722
+ end: right.end ?? token.end,
1723
+ line: left.line ?? token.line,
1724
+ column: left.column ?? token.column,
1725
+ };
1726
+ }
1694
1727
  const PARSER_COMPARISON_FRAGMENT = new Map([
1695
1728
  ['===', leftAssoc(30)],
1696
1729
  ['!==', leftAssoc(30)],
@@ -1735,7 +1768,7 @@ const PARSER_COMPARISON_FRAGMENT = new Map([
1735
1768
  ['is greater than or equal to', leftAssoc(30)],
1736
1769
  ['is less than or equal to', leftAssoc(30)],
1737
1770
  ['in', leftAssoc(30)],
1738
- ['of', rightAssoc(30)],
1771
+ ['of', { infix: { bp: [90, OF_OPERAND_BP], handler: ofHandler } }],
1739
1772
  ['really', leftAssoc(30)],
1740
1773
  [
1741
1774
  'exists',
@@ -2232,6 +2265,48 @@ function parseMaybeNamedArgument(ctx) {
2232
2265
  const value = ctx.parseExpression();
2233
2266
  return name !== undefined ? { name, value } : { value };
2234
2267
  }
2268
+ function isNakedNamedArgStart(ctx) {
2269
+ if (ctx.check('{'))
2270
+ return false;
2271
+ if (!ctx.checkIdentifierLike())
2272
+ return false;
2273
+ const next = ctx.peekAt(1);
2274
+ return next !== null && next.value === ':';
2275
+ }
2276
+ function parseNakedNamedArgs(ctx, parseValue, command) {
2277
+ const properties = [];
2278
+ const startPos = ctx.getPosition();
2279
+ do {
2280
+ if (!ctx.checkIdentifierLike())
2281
+ break;
2282
+ const keyToken = ctx.advance();
2283
+ const key = {
2284
+ type: 'identifier',
2285
+ name: keyToken.value,
2286
+ start: keyToken.start,
2287
+ end: keyToken.end,
2288
+ line: keyToken.line,
2289
+ column: keyToken.column,
2290
+ };
2291
+ ctx.consume(':', `Expected ':' after property name in ${command} named arguments`);
2292
+ const value = parseValue();
2293
+ if (value) {
2294
+ properties.push({ key, value });
2295
+ }
2296
+ } while (ctx.match(',') && !ctx.isAtEnd());
2297
+ const endPos = ctx.getPosition();
2298
+ return {
2299
+ type: 'objectLiteral',
2300
+ properties,
2301
+ start: startPos.start,
2302
+ end: endPos.end,
2303
+ line: startPos.line,
2304
+ column: startPos.column,
2305
+ };
2306
+ }
2307
+ function isGluedBeepBang(beep, bang) {
2308
+ return beep?.value === 'beep' && bang?.value === '!' && bang.start === beep.end;
2309
+ }
2235
2310
 
2236
2311
  class CommandNodeBuilder {
2237
2312
  constructor(name) {
@@ -2433,6 +2508,8 @@ function parseTriggerCommand(ctx, identifierNode) {
2433
2508
  const modifiers = {};
2434
2509
  while (!isCommandBoundary(ctx)) {
2435
2510
  if (ctx.check(KEYWORDS.ON) || ctx.check(KEYWORDS.TO)) {
2511
+ if (modifiers['on'])
2512
+ break;
2436
2513
  ctx.advance();
2437
2514
  const target = parseOneArgument(ctx, [KEYWORDS.WITH]);
2438
2515
  if (target)
@@ -2573,6 +2650,9 @@ function parseRepeatCommand(ctx, commandToken) {
2573
2650
  ctx.advance();
2574
2651
  loopType = KEYWORDS.FOREVER;
2575
2652
  }
2653
+ else if (ctx.checkIsCommand() || ctx.isCommand(ctx.peek().value)) {
2654
+ loopType = KEYWORDS.FOREVER;
2655
+ }
2576
2656
  else {
2577
2657
  times = ctx.parseExpression();
2578
2658
  if (ctx.check(KEYWORDS.TIMES)) {
@@ -2615,10 +2695,12 @@ function parseRepeatCommand(ctx, commandToken) {
2615
2695
  loopType = result.terminator;
2616
2696
  ctx.advance();
2617
2697
  condition = ctx.parseExpression();
2618
- if (!ctx.check('end')) {
2698
+ if (ctx.check('end')) {
2699
+ ctx.advance();
2700
+ }
2701
+ else if (!ctx.atEndOfInput()) {
2619
2702
  throw new Error('Expected "end" to close repeat block');
2620
2703
  }
2621
- ctx.advance();
2622
2704
  }
2623
2705
  }
2624
2706
  else {
@@ -2864,7 +2946,7 @@ function parseIfCommand(ctx, commandToken) {
2864
2946
  }));
2865
2947
  }
2866
2948
  }
2867
- if (!consumedElseIf && !ctx.isAtEnd()) {
2949
+ if (!consumedElseIf && !ctx.atEndOfInput()) {
2868
2950
  ctx.consume(KEYWORDS.END, "Expected 'end' after if block");
2869
2951
  }
2870
2952
  }
@@ -2889,7 +2971,6 @@ function parseIfCommand(ctx, commandToken) {
2889
2971
  .build();
2890
2972
  }
2891
2973
  function parseForCommand(ctx, commandToken) {
2892
- const args = [];
2893
2974
  let variable = null;
2894
2975
  let collection = null;
2895
2976
  if (ctx.check(KEYWORDS.EACH)) {
@@ -2932,31 +3013,22 @@ function parseForCommand(ctx, commandToken) {
2932
3013
  }
2933
3014
  }
2934
3015
  const commands = ctx.parseCommandListUntilEnd('for');
2935
- args.push({
2936
- type: 'identifier',
2937
- name: 'for',
2938
- start: commandToken.start,
2939
- end: commandToken.end,
2940
- line: commandToken.line,
2941
- column: commandToken.column,
2942
- });
2943
- const forPos = {
3016
+ const pos = {
2944
3017
  start: commandToken.start,
2945
3018
  end: commandToken.end,
2946
3019
  line: commandToken.line,
2947
3020
  column: commandToken.column,
2948
3021
  };
2949
- args.push(createStringLiteral(variable, forPos));
2950
- args.push(collection);
2951
- if (indexVariable) {
2952
- args.push(createStringLiteral(indexVariable, forPos));
2953
- }
2954
- args.push(createBlock(commands, { ...forPos, end: forPos.end || 0 }));
2955
- return CommandNodeBuilder.from({
2956
- ...commandToken,
2957
- value: 'repeat',
2958
- })
2959
- .withArgs(...args)
3022
+ const modifiers = {
3023
+ loopType: toLegacyExpression(createStringLiteral(KEYWORDS.FOR, pos)),
3024
+ for: toLegacyExpression(createStringLiteral(variable, pos)),
3025
+ in: collection,
3026
+ };
3027
+ if (indexVariable)
3028
+ modifiers['index'] = toLegacyExpression(createStringLiteral(indexVariable, pos));
3029
+ return CommandNodeBuilder.from({ ...commandToken, value: 'repeat' })
3030
+ .withArgs(createBlock(commands, { ...pos, end: pos.end || 0 }))
3031
+ .withModifiers(modifiers)
2960
3032
  .endingAt(ctx.getPosition())
2961
3033
  .build();
2962
3034
  }
@@ -3012,86 +3084,159 @@ function parseMeasureCommand(ctx, identifierNode) {
3012
3084
  }
3013
3085
  return builder.build();
3014
3086
  }
3015
- const CONTEXT_POSSESSIVES = new Set(['my', 'its', 'your']);
3087
+ const CONTEXT_REFS = new Set(['me', 'it', 'you', 'result']);
3016
3088
  function parseTransitionCommand(ctx, commandToken) {
3017
- const args = [];
3018
- const modifiers = {};
3019
- let property = null;
3020
- let target = null;
3021
- const leadToken = ctx.peek();
3022
- const startsTarget = ctx.checkAnySelector() ||
3023
- ctx.checkContextVar() ||
3024
- ctx.check('<') ||
3025
- (isIdentifierLike(leadToken) && CONTEXT_POSSESSIVES.has(leadToken.value));
3026
- if (startsTarget) {
3027
- const parsed = ctx.parsePrimary();
3028
- if ((parsed.type === 'possessiveExpression' ||
3029
- (parsed.type === 'memberExpression' && !parsed.computed)) &&
3030
- parsed.object &&
3031
- parsed.property?.name) {
3032
- target = parsed.object;
3033
- property = {
3034
- type: 'string',
3035
- value: parsed.property.name,
3036
- start: leadToken.start || 0,
3037
- end: ctx.getPosition().end,
3038
- line: leadToken.line,
3039
- column: leadToken.column,
3040
- };
3089
+ const pairs = [];
3090
+ do {
3091
+ pairs.push(parseTransitionPair(ctx));
3092
+ } while (startsAnotherPair(ctx));
3093
+ const [first, ...rest] = pairs;
3094
+ const args = first.owner ? [first.owner, first.property] : [first.property];
3095
+ const modifiers = { to: first.to };
3096
+ if (first.from)
3097
+ modifiers['from'] = first.from;
3098
+ if (rest.length > 0)
3099
+ modifiers['pairs'] = pairsNode(rest);
3100
+ for (let i = 0; i < 3 && !ctx.isAtEnd(); i++) {
3101
+ if (ctx.check('over') && !modifiers['over']) {
3102
+ ctx.advance();
3103
+ modifiers['over'] = ctx.parseExpression();
3041
3104
  }
3042
- else {
3043
- target = parsed;
3044
- if (ctx.check("'s"))
3045
- ctx.advance();
3105
+ else if (ctx.check('using') && !modifiers['using']) {
3106
+ ctx.advance();
3107
+ modifiers['using'] = ctx.parseExpression();
3046
3108
  }
3047
- }
3048
- const firstToken = ctx.peek();
3049
- if (!property && (isIdentifierLike(firstToken) || firstToken.value === '*')) {
3050
- let propertyValue = '';
3051
- if (ctx.check('*')) {
3052
- propertyValue = '*';
3109
+ else if (ctx.check(KEYWORDS.WITH) && !modifiers['with']) {
3053
3110
  ctx.advance();
3111
+ modifiers['with'] = ctx.parsePrimary();
3054
3112
  }
3055
- const hyphenatedName = parseHyphenatedName(ctx);
3056
- if (hyphenatedName) {
3057
- propertyValue += hyphenatedName;
3058
- property = {
3059
- type: 'string',
3060
- value: propertyValue,
3061
- start: firstToken.start || 0,
3062
- end: ctx.getPosition().end,
3063
- line: firstToken.line,
3064
- column: firstToken.column,
3065
- };
3113
+ else {
3114
+ break;
3066
3115
  }
3067
3116
  }
3068
- if (!property) {
3069
- throw new Error('Transition command requires a CSS property');
3117
+ return CommandNodeBuilder.from(commandToken)
3118
+ .withArgs(...args)
3119
+ .withModifiers(modifiers)
3120
+ .endingAt(ctx.getPosition())
3121
+ .build();
3122
+ }
3123
+ function parseTransitionPair(ctx) {
3124
+ const { owner, property } = parseTransitionProperty(ctx);
3125
+ let from = null;
3126
+ if (ctx.check('from')) {
3127
+ ctx.advance();
3128
+ from = ctx.parseExpression();
3070
3129
  }
3071
- if (target)
3072
- args.push(target);
3073
- args.push(property);
3074
3130
  if (!ctx.check(KEYWORDS.TO)) {
3075
3131
  throw new Error('Expected "to" keyword after property in transition command');
3076
3132
  }
3077
3133
  ctx.advance();
3078
- const value = ctx.parseExpression();
3079
- modifiers['to'] = value;
3080
- if (ctx.check('over')) {
3081
- ctx.advance();
3082
- const duration = ctx.parseExpression();
3083
- modifiers['over'] = duration;
3134
+ const to = ctx.parseExpression();
3135
+ return { owner, property, from, to };
3136
+ }
3137
+ function parseTransitionProperty(ctx) {
3138
+ const lead = ctx.peek();
3139
+ if (!lead || isKeyword(lead, [KEYWORDS.TO, 'from'])) {
3140
+ throw new Error('Transition command requires a CSS property');
3084
3141
  }
3085
- if (ctx.check(KEYWORDS.WITH)) {
3142
+ const expr = ctx.parseExpression();
3143
+ const split = splitStyleExpression(expr);
3144
+ if (split)
3145
+ return { owner: split.owner, property: propertyNode(split.name, lead, ctx) };
3146
+ let name = '';
3147
+ if (ctx.check('*')) {
3148
+ name = '*';
3086
3149
  ctx.advance();
3087
- const timingFunction = ctx.parsePrimary();
3088
- modifiers['with'] = timingFunction;
3089
3150
  }
3090
- return CommandNodeBuilder.from(commandToken)
3091
- .withArgs(...args)
3092
- .withModifiers(modifiers)
3093
- .endingAt(ctx.getPosition())
3094
- .build();
3151
+ const hyphenated = parseHyphenatedName(ctx);
3152
+ if (!hyphenated)
3153
+ throw new Error('Transition command requires a CSS property');
3154
+ return { owner: expr, property: propertyNode(name + hyphenated, lead, ctx) };
3155
+ }
3156
+ function splitStyleExpression(node) {
3157
+ const n = node;
3158
+ switch (n.type) {
3159
+ case 'selector':
3160
+ return typeof n.value === 'string' && n.value.startsWith('*')
3161
+ ? { owner: null, name: n.value }
3162
+ : null;
3163
+ case 'identifier':
3164
+ return typeof n.name === 'string' && !CONTEXT_REFS.has(n.name)
3165
+ ? { owner: null, name: n.name }
3166
+ : null;
3167
+ case 'possessiveExpression':
3168
+ case 'memberExpression':
3169
+ return !n.computed && n.object && typeof n.property?.name === 'string'
3170
+ ? { owner: n.object, name: n.property.name }
3171
+ : null;
3172
+ case 'binaryExpression': {
3173
+ if (n.operator !== 'of' || !n.left || !n.right)
3174
+ return null;
3175
+ const inner = splitStyleExpression(n.left);
3176
+ return inner && !inner.owner ? { owner: n.right, name: inner.name } : null;
3177
+ }
3178
+ case 'propertyOfExpression':
3179
+ return n.target && typeof n.property?.name === 'string'
3180
+ ? { owner: n.target, name: n.property.name }
3181
+ : null;
3182
+ default:
3183
+ return null;
3184
+ }
3185
+ }
3186
+ function propertyNode(name, lead, ctx) {
3187
+ return {
3188
+ type: 'string',
3189
+ value: name,
3190
+ start: lead.start || 0,
3191
+ end: ctx.getPosition().end,
3192
+ line: lead.line,
3193
+ column: lead.column,
3194
+ };
3195
+ }
3196
+ function startsAnotherPair(ctx) {
3197
+ if (ctx.isAtEnd() || isComment(ctx.peek()))
3198
+ return false;
3199
+ if (isKeyword(ctx.peek(), ['over', 'using', KEYWORDS.WITH]))
3200
+ return false;
3201
+ return !isCommandBoundary(ctx, ['catch', 'finally', 'on']);
3202
+ }
3203
+ function pairsNode(pairs) {
3204
+ const field = (name, value) => ({
3205
+ key: {
3206
+ type: 'identifier',
3207
+ name,
3208
+ start: value.start,
3209
+ end: value.end,
3210
+ line: value.line,
3211
+ column: value.column,
3212
+ },
3213
+ value,
3214
+ });
3215
+ const elements = pairs.map(pair => {
3216
+ const properties = [field('property', pair.property), field('to', pair.to)];
3217
+ if (pair.owner)
3218
+ properties.push(field('owner', pair.owner));
3219
+ if (pair.from)
3220
+ properties.push(field('from', pair.from));
3221
+ return {
3222
+ type: 'objectLiteral',
3223
+ properties,
3224
+ start: (pair.owner ?? pair.property).start,
3225
+ end: pair.to.end,
3226
+ line: (pair.owner ?? pair.property).line,
3227
+ column: (pair.owner ?? pair.property).column,
3228
+ };
3229
+ });
3230
+ const firstEl = elements[0];
3231
+ const lastEl = elements[elements.length - 1];
3232
+ return {
3233
+ type: 'arrayLiteral',
3234
+ elements,
3235
+ start: firstEl.start,
3236
+ end: lastEl.end,
3237
+ line: firstEl.line,
3238
+ column: firstEl.column,
3239
+ };
3095
3240
  }
3096
3241
  function parseStartCommand(ctx, identifierNode) {
3097
3242
  if (!ctx.match('view')) {
@@ -3388,11 +3533,17 @@ function parseSwapCommand(ctx, identifierNode) {
3388
3533
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
3389
3534
  if (targetExpr)
3390
3535
  args.push(targetExpr);
3391
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
3536
+ const isMorph = identifierNode.name === 'morph';
3537
+ if (!isDelete &&
3538
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
3539
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
3392
3540
  const contentExpr = parseOneArgument(ctx, ['using']);
3393
3541
  if (contentExpr)
3394
3542
  modifiers['with'] = contentExpr;
3395
3543
  }
3544
+ if (isMorph && modifiers['with'] === undefined) {
3545
+ ctx.addError("Expected 'to' and the content to morph into");
3546
+ }
3396
3547
  return CommandNodeBuilder.fromIdentifier(identifierNode)
3397
3548
  .withArgs(...args)
3398
3549
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -4153,6 +4304,8 @@ function atArgumentBoundary(ctx, grammar) {
4153
4304
  const next = ctx.peek();
4154
4305
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
4155
4306
  return false;
4307
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
4308
+ return false;
4156
4309
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
4157
4310
  return true;
4158
4311
  return isKeyword(next, [...grammar.markers]);
@@ -4184,10 +4337,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
4184
4337
  }
4185
4338
  }
4186
4339
  const commaList = new Set(grammar.commaList ?? []);
4340
+ const namedArgs = new Set(grammar.namedArgs ?? []);
4187
4341
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
4188
4342
  const keyword = ctx.advance().value;
4189
4343
  const key = ctx.resolveKeyword(keyword).toLowerCase();
4190
- let value = ctx.parseExpression();
4344
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
4345
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
4346
+ : ctx.parseExpression();
4191
4347
  if (value && commaList.has(key) && ctx.check(',')) {
4192
4348
  const elements = [value];
4193
4349
  while (ctx.match(',')) {
@@ -4352,8 +4508,8 @@ function parseFetchCommand(ctx, commandToken) {
4352
4508
  }
4353
4509
  if (ctx.check('with') && !modifiers['with']) {
4354
4510
  ctx.advance();
4355
- if (isFetchNakedNamedArgStart(ctx)) {
4356
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
4511
+ if (isNakedNamedArgStart(ctx)) {
4512
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
4357
4513
  }
4358
4514
  else {
4359
4515
  modifiers['with'] = ctx.parsePrimary();
@@ -4388,45 +4544,6 @@ function parseFetchCommand(ctx, commandToken) {
4388
4544
  }
4389
4545
  return builder.build();
4390
4546
  }
4391
- function isFetchNakedNamedArgStart(ctx) {
4392
- if (ctx.check('{'))
4393
- return false;
4394
- if (!ctx.checkIdentifierLike())
4395
- return false;
4396
- const next = ctx.peekAt(1);
4397
- return next !== null && next.value === ':';
4398
- }
4399
- function parseFetchNakedNamedArgs(ctx) {
4400
- const properties = [];
4401
- const startPos = ctx.getPosition();
4402
- do {
4403
- if (!ctx.checkIdentifierLike())
4404
- break;
4405
- const keyToken = ctx.advance();
4406
- const key = {
4407
- type: 'identifier',
4408
- name: keyToken.value,
4409
- start: keyToken.start,
4410
- end: keyToken.end,
4411
- line: keyToken.line,
4412
- column: keyToken.column,
4413
- };
4414
- ctx.consume(':', "Expected ':' after property name in fetch named arguments");
4415
- const value = ctx.parsePrimary();
4416
- if (value) {
4417
- properties.push({ key, value });
4418
- }
4419
- } while (ctx.match(',') && !ctx.isAtEnd());
4420
- const endPos = ctx.getPosition();
4421
- return {
4422
- type: 'objectLiteral',
4423
- properties,
4424
- start: startPos.start,
4425
- end: endPos.end,
4426
- line: startPos.line,
4427
- column: startPos.column,
4428
- };
4429
- }
4430
4547
  function findJsEndBoundary(ctx, startPos) {
4431
4548
  const input = ctx.getInputSlice(startPos);
4432
4549
  if (!input) {
@@ -4546,6 +4663,7 @@ function parseTellCommand(ctx, identifierNode) {
4546
4663
  const commands = [];
4547
4664
  while (!ctx.isAtEnd()) {
4548
4665
  if (ctx.checkIsCommand()) {
4666
+ const word = ctx.peek().value;
4549
4667
  try {
4550
4668
  ctx.advance();
4551
4669
  const cmd = ctx.parseCommand();
@@ -4556,7 +4674,8 @@ function parseTellCommand(ctx, identifierNode) {
4556
4674
  break;
4557
4675
  }
4558
4676
  }
4559
- catch {
4677
+ catch (error) {
4678
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
4560
4679
  break;
4561
4680
  }
4562
4681
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -4705,7 +4824,6 @@ const ONE_EXPR = (syntax) => ({
4705
4824
  markers: [],
4706
4825
  syntax,
4707
4826
  });
4708
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
4709
4827
  const COMMAND_GRAMMAR = {
4710
4828
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
4711
4829
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -4716,8 +4834,7 @@ const COMMAND_GRAMMAR = {
4716
4834
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
4717
4835
  },
4718
4836
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
4719
- 'beep!': BEEP,
4720
- beep: BEEP,
4837
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
4721
4838
  blur: ONE_EXPR('blur [<target>]'),
4722
4839
  break: NONE,
4723
4840
  breakpoint: NONE,
@@ -4741,7 +4858,8 @@ const COMMAND_GRAMMAR = {
4741
4858
  render: {
4742
4859
  positional: 'expression',
4743
4860
  markers: ['with'],
4744
- syntax: 'render <template> [with <variables>]',
4861
+ namedArgs: ['with'],
4862
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
4745
4863
  },
4746
4864
  reset: ONE_EXPR('reset [<form>]'),
4747
4865
  return: ONE_EXPR('return [<value>]'),
@@ -5096,7 +5214,10 @@ class Parser {
5096
5214
  const firstToken = this.peek();
5097
5215
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
5098
5216
  let left;
5099
- if (prefixEntry?.prefix) {
5217
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
5218
+ left = this.parseBeepExpression();
5219
+ }
5220
+ else if (prefixEntry?.prefix) {
5100
5221
  const token = this.advance();
5101
5222
  if (this.isAtEnd()) {
5102
5223
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -5116,6 +5237,9 @@ class Parser {
5116
5237
  continue;
5117
5238
  }
5118
5239
  }
5240
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
5241
+ break;
5242
+ }
5119
5243
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
5120
5244
  if (!infixEntry?.infix) {
5121
5245
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -5177,6 +5301,35 @@ class Parser {
5177
5301
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
5178
5302
  return left;
5179
5303
  }
5304
+ parseBeepExpression() {
5305
+ const beepToken = this.advance();
5306
+ this.advance();
5307
+ if (this.isAtEnd()) {
5308
+ this.addError("Expected expression after 'beep!'");
5309
+ return this.createErrorNode();
5310
+ }
5311
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
5312
+ return {
5313
+ type: 'unaryExpression',
5314
+ operator: 'beep!',
5315
+ operand,
5316
+ argument: operand,
5317
+ prefix: true,
5318
+ start: beepToken.start,
5319
+ end: this.previous().end,
5320
+ line: beepToken.line,
5321
+ column: beepToken.column,
5322
+ };
5323
+ }
5324
+ isStyleRefStar(i) {
5325
+ const star = this.tokens[i];
5326
+ const name = this.tokens[i + 1];
5327
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
5328
+ }
5329
+ isSpacedStyleRef(i) {
5330
+ const prev = this.tokens[i - 1];
5331
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
5332
+ }
5180
5333
  tryParseStringPostfix(left, nextToken) {
5181
5334
  const v = nextToken.value;
5182
5335
  let unit = null;
@@ -5379,6 +5532,10 @@ class Parser {
5379
5532
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
5380
5533
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
5381
5534
  while (!this.isAtEnd() && !isStop()) {
5535
+ if (this.checkComment()) {
5536
+ this.advance();
5537
+ continue;
5538
+ }
5382
5539
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
5383
5540
  let parsedCommand = false;
5384
5541
  const isCommandToken = this.checkIsCommand();
@@ -5462,7 +5619,7 @@ class Parser {
5462
5619
  debug.parse('\u2705 Found "end", consuming it');
5463
5620
  this.advance();
5464
5621
  }
5465
- else {
5622
+ else if (!this.atEndOfInput()) {
5466
5623
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
5467
5624
  throw new Error(`Expected "end" to close ${construct} block`);
5468
5625
  }
@@ -5475,7 +5632,7 @@ class Parser {
5475
5632
  if (this.check('end')) {
5476
5633
  this.advance();
5477
5634
  }
5478
- else {
5635
+ else if (!this.atEndOfInput()) {
5479
5636
  throw new Error('Expected "end" to close repeat block');
5480
5637
  }
5481
5638
  }
@@ -5491,7 +5648,10 @@ class Parser {
5491
5648
  this.advance();
5492
5649
  }
5493
5650
  else if (terminator === '') {
5494
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5651
+ if (!this.atEndOfInput()) {
5652
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5653
+ }
5654
+ return { commands, terminator: 'end' };
5495
5655
  }
5496
5656
  return { commands, terminator };
5497
5657
  }
@@ -6075,7 +6235,9 @@ class Parser {
6075
6235
  parseTopLevelInitBlock() {
6076
6236
  const pos = this.getPosition();
6077
6237
  const initCommands = this.parseCommandBlock(['end']);
6078
- this.consume('end', "Expected 'end' after init block");
6238
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
6239
+ this.consume('end', "Expected 'end' after init block");
6240
+ }
6079
6241
  return {
6080
6242
  type: 'initBlock',
6081
6243
  commands: initCommands,
@@ -6251,10 +6413,29 @@ class Parser {
6251
6413
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
6252
6414
  };
6253
6415
  }
6416
+ isArgumentListOnly(open) {
6417
+ let depth = 0;
6418
+ for (let i = open; i < this.tokens.length; i++) {
6419
+ const token = this.tokens[i];
6420
+ if (token.kind !== 'operator')
6421
+ continue;
6422
+ if (token.value === '(' || token.value === '[' || token.value === '{')
6423
+ depth++;
6424
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
6425
+ if (--depth === 0)
6426
+ return i === open + 1;
6427
+ }
6428
+ else if (token.value === ',' && depth === 1)
6429
+ return true;
6430
+ }
6431
+ return false;
6432
+ }
6254
6433
  tryParsePseudoCommand(callExpr, sourceExpr) {
6255
6434
  const methodName = callExpr.callee.name;
6256
6435
  const nextToken = this.peek();
6257
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
6436
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
6437
+ const hasPseudoCommandPattern = nextIsArticle ||
6438
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
6258
6439
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
6259
6440
  isContextVar(nextToken);
6260
6441
  if (!hasPseudoCommandPattern) {
@@ -6280,7 +6461,10 @@ class Parser {
6280
6461
  });
6281
6462
  }
6282
6463
  let preposition;
6283
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
6464
+ if (nextIsArticle) {
6465
+ this.advance();
6466
+ }
6467
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
6284
6468
  preposition = this.advance().value.toLowerCase();
6285
6469
  }
6286
6470
  let targetExpr;
@@ -6444,10 +6628,22 @@ class Parser {
6444
6628
  this.consume(']', "Expected ']' after event condition");
6445
6629
  }
6446
6630
  let target;
6631
+ let targetExpression;
6447
6632
  if (this.match('from')) {
6448
- const targetToken = this.advance();
6449
- target = targetToken.value;
6450
- debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target} (kind: ${targetToken.kind})`);
6633
+ if (this.check('the'))
6634
+ this.advance();
6635
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
6636
+ const startIndex = this.current;
6637
+ targetExpression = this.parseExpression();
6638
+ target = this.tokens
6639
+ .slice(startIndex, this.current)
6640
+ .map(t => t.value)
6641
+ .join(' ');
6642
+ }
6643
+ else {
6644
+ target = this.advance().value;
6645
+ }
6646
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
6451
6647
  this.parseTemporalKeywordModifiers(modifiers);
6452
6648
  }
6453
6649
  let attributeName;
@@ -6493,7 +6689,7 @@ class Parser {
6493
6689
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
6494
6690
  const commandName = this.peek().value.toLowerCase();
6495
6691
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
6496
- if (nextIsOpenParen && !isSpecialBodyCommand) {
6692
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
6497
6693
  let expr;
6498
6694
  const savedError = this.error;
6499
6695
  try {
@@ -6525,7 +6721,7 @@ class Parser {
6525
6721
  }
6526
6722
  }
6527
6723
  }
6528
- else if (this.checkIdentifier()) {
6724
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
6529
6725
  const token = this.peek();
6530
6726
  if (this.isCommand(token.value)) {
6531
6727
  const cmd = this.parseCommandWithErrorRecovery();
@@ -6634,6 +6830,7 @@ class Parser {
6634
6830
  ...(eventParams.length > 0 && { args: eventParams }),
6635
6831
  ...(condition && { condition: fromLegacyExpression(condition) }),
6636
6832
  ...(target && { target }),
6833
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
6637
6834
  ...(attributeName && { attributeName }),
6638
6835
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
6639
6836
  ...(customEventSource && { customEventSource }),
@@ -6889,10 +7086,9 @@ class Parser {
6889
7086
  }
6890
7087
  parseCommandCore() {
6891
7088
  const commandToken = this.previous();
6892
- let commandName = commandToken.value;
7089
+ const commandName = commandToken.value;
6893
7090
  if (commandName === 'beep' && this.check('!')) {
6894
7091
  this.advance();
6895
- commandName = 'beep!';
6896
7092
  }
6897
7093
  if (commandName === 'fetch') {
6898
7094
  return parseFetchCommand(this.getContext(), commandToken);
@@ -6991,6 +7187,9 @@ class Parser {
6991
7187
  args.push(this.parsePrimary());
6992
7188
  }
6993
7189
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
7190
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
7191
+ callNode.closestTo = this.parseExpression();
7192
+ }
6994
7193
  if (funcName === 'next' || funcName === 'previous') {
6995
7194
  const relativePositional = this.parseRelativePositionalModifiers();
6996
7195
  if (relativePositional) {
@@ -7030,6 +7229,9 @@ class Parser {
7030
7229
  }
7031
7230
  parseTheXofY() {
7032
7231
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
7232
+ if (this.isStyleRefStar(this.current)) {
7233
+ return this.parsePrimary();
7234
+ }
7033
7235
  if (!this.checkIdentifier()) {
7034
7236
  return this.createIdentifier('the');
7035
7237
  }
@@ -7045,7 +7247,7 @@ class Parser {
7045
7247
  return this.createIdentifier(propertyName);
7046
7248
  }
7047
7249
  this.advance();
7048
- const target = this.parseCall();
7250
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
7049
7251
  return {
7050
7252
  type: 'propertyOfExpression',
7051
7253
  property: { type: 'identifier', name: propertyName },
@@ -7243,6 +7445,13 @@ class Parser {
7243
7445
  checkLiteral() {
7244
7446
  return this.checkPredicate(isLiteral);
7245
7447
  }
7448
+ checkEventNamedCall() {
7449
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
7450
+ return false;
7451
+ const token = this.peek();
7452
+ const next = this.tokens[this.current + 1];
7453
+ return next?.value === '(' && next.start === token.end;
7454
+ }
7246
7455
  checkIdentifierLike() {
7247
7456
  return this.checkPredicate(isIdentifierLike);
7248
7457
  }
@@ -7351,6 +7560,22 @@ class Parser {
7351
7560
  isAtEnd() {
7352
7561
  return this.current >= this.tokens.length;
7353
7562
  }
7563
+ atEndOfInput() {
7564
+ for (let i = this.current; i < this.tokens.length; i++) {
7565
+ if (!isComment(this.tokens[i]))
7566
+ return false;
7567
+ }
7568
+ return true;
7569
+ }
7570
+ checkFeatureStart() {
7571
+ if (this.isAtEnd())
7572
+ return false;
7573
+ const value = this.peek().value;
7574
+ return (value === 'init' ||
7575
+ value === 'on' ||
7576
+ value === 'def' ||
7577
+ getRegisteredFeature(value) !== undefined);
7578
+ }
7354
7579
  peek() {
7355
7580
  if (this.isAtEnd()) {
7356
7581
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -7562,6 +7787,7 @@ class Parser {
7562
7787
  match: this.match.bind(this),
7563
7788
  matchOperator: this.matchOperator.bind(this),
7564
7789
  isAtEnd: this.isAtEnd.bind(this),
7790
+ atEndOfInput: this.atEndOfInput.bind(this),
7565
7791
  createIdentifier: this.createIdentifier.bind(this),
7566
7792
  parseExpression: this.parseExpression.bind(this),
7567
7793
  parsePrimary: this.parsePrimary.bind(this),
@@ -7612,7 +7838,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
7612
7838
  'ignoring case',
7613
7839
  ]);
7614
7840
  Parser.PRATT_TABLE = PARSER_TABLE;
7841
+ Parser.BEEP_OPERAND_BP = 80;
7615
7842
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
7843
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
7844
+ 'closest',
7845
+ 'next',
7846
+ 'previous',
7847
+ 'first',
7848
+ 'last',
7849
+ ]);
7616
7850
 
7617
7851
  class FullParserImpl {
7618
7852
  constructor() {