@hyperfixi/core 3.1.1 → 3.2.0

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