@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
@@ -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')) {
@@ -3211,6 +3356,15 @@ function parseToggleCommand(ctx, identifierNode) {
3211
3356
  args.push(classArg.expression);
3212
3357
  modifiers['as'] = literalModifier(mode);
3213
3358
  }
3359
+ else if (classArg?.type === 'binaryExpression' &&
3360
+ classArg.operator === 'of' &&
3361
+ classArg.left?.type === 'selector' &&
3362
+ typeof classArg.left.value === 'string' &&
3363
+ classArg.left.value.startsWith('*') &&
3364
+ classArg.right) {
3365
+ args.push(classArg.left);
3366
+ modifiers['on'] = classArg.right;
3367
+ }
3214
3368
  else if (classArg) {
3215
3369
  args.push(classArg);
3216
3370
  }
@@ -3388,11 +3542,17 @@ function parseSwapCommand(ctx, identifierNode) {
3388
3542
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
3389
3543
  if (targetExpr)
3390
3544
  args.push(targetExpr);
3391
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
3545
+ const isMorph = identifierNode.name === 'morph';
3546
+ if (!isDelete &&
3547
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
3548
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
3392
3549
  const contentExpr = parseOneArgument(ctx, ['using']);
3393
3550
  if (contentExpr)
3394
3551
  modifiers['with'] = contentExpr;
3395
3552
  }
3553
+ if (isMorph && modifiers['with'] === undefined) {
3554
+ ctx.addError("Expected 'to' and the content to morph into");
3555
+ }
3396
3556
  return CommandNodeBuilder.fromIdentifier(identifierNode)
3397
3557
  .withArgs(...args)
3398
3558
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -4153,6 +4313,8 @@ function atArgumentBoundary(ctx, grammar) {
4153
4313
  const next = ctx.peek();
4154
4314
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
4155
4315
  return false;
4316
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
4317
+ return false;
4156
4318
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
4157
4319
  return true;
4158
4320
  return isKeyword(next, [...grammar.markers]);
@@ -4184,10 +4346,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
4184
4346
  }
4185
4347
  }
4186
4348
  const commaList = new Set(grammar.commaList ?? []);
4349
+ const namedArgs = new Set(grammar.namedArgs ?? []);
4187
4350
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
4188
4351
  const keyword = ctx.advance().value;
4189
4352
  const key = ctx.resolveKeyword(keyword).toLowerCase();
4190
- let value = ctx.parseExpression();
4353
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
4354
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
4355
+ : ctx.parseExpression();
4191
4356
  if (value && commaList.has(key) && ctx.check(',')) {
4192
4357
  const elements = [value];
4193
4358
  while (ctx.match(',')) {
@@ -4352,8 +4517,8 @@ function parseFetchCommand(ctx, commandToken) {
4352
4517
  }
4353
4518
  if (ctx.check('with') && !modifiers['with']) {
4354
4519
  ctx.advance();
4355
- if (isFetchNakedNamedArgStart(ctx)) {
4356
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
4520
+ if (isNakedNamedArgStart(ctx)) {
4521
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
4357
4522
  }
4358
4523
  else {
4359
4524
  modifiers['with'] = ctx.parsePrimary();
@@ -4388,45 +4553,6 @@ function parseFetchCommand(ctx, commandToken) {
4388
4553
  }
4389
4554
  return builder.build();
4390
4555
  }
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
4556
  function findJsEndBoundary(ctx, startPos) {
4431
4557
  const input = ctx.getInputSlice(startPos);
4432
4558
  if (!input) {
@@ -4546,6 +4672,7 @@ function parseTellCommand(ctx, identifierNode) {
4546
4672
  const commands = [];
4547
4673
  while (!ctx.isAtEnd()) {
4548
4674
  if (ctx.checkIsCommand()) {
4675
+ const word = ctx.peek().value;
4549
4676
  try {
4550
4677
  ctx.advance();
4551
4678
  const cmd = ctx.parseCommand();
@@ -4556,7 +4683,8 @@ function parseTellCommand(ctx, identifierNode) {
4556
4683
  break;
4557
4684
  }
4558
4685
  }
4559
- catch {
4686
+ catch (error) {
4687
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
4560
4688
  break;
4561
4689
  }
4562
4690
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -4705,7 +4833,6 @@ const ONE_EXPR = (syntax) => ({
4705
4833
  markers: [],
4706
4834
  syntax,
4707
4835
  });
4708
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
4709
4836
  const COMMAND_GRAMMAR = {
4710
4837
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
4711
4838
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -4716,8 +4843,7 @@ const COMMAND_GRAMMAR = {
4716
4843
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
4717
4844
  },
4718
4845
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
4719
- 'beep!': BEEP,
4720
- beep: BEEP,
4846
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
4721
4847
  blur: ONE_EXPR('blur [<target>]'),
4722
4848
  break: NONE,
4723
4849
  breakpoint: NONE,
@@ -4741,7 +4867,8 @@ const COMMAND_GRAMMAR = {
4741
4867
  render: {
4742
4868
  positional: 'expression',
4743
4869
  markers: ['with'],
4744
- syntax: 'render <template> [with <variables>]',
4870
+ namedArgs: ['with'],
4871
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
4745
4872
  },
4746
4873
  reset: ONE_EXPR('reset [<form>]'),
4747
4874
  return: ONE_EXPR('return [<value>]'),
@@ -5096,7 +5223,10 @@ class Parser {
5096
5223
  const firstToken = this.peek();
5097
5224
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
5098
5225
  let left;
5099
- if (prefixEntry?.prefix) {
5226
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
5227
+ left = this.parseBeepExpression();
5228
+ }
5229
+ else if (prefixEntry?.prefix) {
5100
5230
  const token = this.advance();
5101
5231
  if (this.isAtEnd()) {
5102
5232
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -5116,6 +5246,9 @@ class Parser {
5116
5246
  continue;
5117
5247
  }
5118
5248
  }
5249
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
5250
+ break;
5251
+ }
5119
5252
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
5120
5253
  if (!infixEntry?.infix) {
5121
5254
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -5177,6 +5310,35 @@ class Parser {
5177
5310
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
5178
5311
  return left;
5179
5312
  }
5313
+ parseBeepExpression() {
5314
+ const beepToken = this.advance();
5315
+ this.advance();
5316
+ if (this.isAtEnd()) {
5317
+ this.addError("Expected expression after 'beep!'");
5318
+ return this.createErrorNode();
5319
+ }
5320
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
5321
+ return {
5322
+ type: 'unaryExpression',
5323
+ operator: 'beep!',
5324
+ operand,
5325
+ argument: operand,
5326
+ prefix: true,
5327
+ start: beepToken.start,
5328
+ end: this.previous().end,
5329
+ line: beepToken.line,
5330
+ column: beepToken.column,
5331
+ };
5332
+ }
5333
+ isStyleRefStar(i) {
5334
+ const star = this.tokens[i];
5335
+ const name = this.tokens[i + 1];
5336
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
5337
+ }
5338
+ isSpacedStyleRef(i) {
5339
+ const prev = this.tokens[i - 1];
5340
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
5341
+ }
5180
5342
  tryParseStringPostfix(left, nextToken) {
5181
5343
  const v = nextToken.value;
5182
5344
  let unit = null;
@@ -5379,6 +5541,10 @@ class Parser {
5379
5541
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
5380
5542
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
5381
5543
  while (!this.isAtEnd() && !isStop()) {
5544
+ if (this.checkComment()) {
5545
+ this.advance();
5546
+ continue;
5547
+ }
5382
5548
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
5383
5549
  let parsedCommand = false;
5384
5550
  const isCommandToken = this.checkIsCommand();
@@ -5462,7 +5628,7 @@ class Parser {
5462
5628
  debug.parse('\u2705 Found "end", consuming it');
5463
5629
  this.advance();
5464
5630
  }
5465
- else {
5631
+ else if (!this.atEndOfInput()) {
5466
5632
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
5467
5633
  throw new Error(`Expected "end" to close ${construct} block`);
5468
5634
  }
@@ -5475,7 +5641,7 @@ class Parser {
5475
5641
  if (this.check('end')) {
5476
5642
  this.advance();
5477
5643
  }
5478
- else {
5644
+ else if (!this.atEndOfInput()) {
5479
5645
  throw new Error('Expected "end" to close repeat block');
5480
5646
  }
5481
5647
  }
@@ -5491,7 +5657,10 @@ class Parser {
5491
5657
  this.advance();
5492
5658
  }
5493
5659
  else if (terminator === '') {
5494
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5660
+ if (!this.atEndOfInput()) {
5661
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5662
+ }
5663
+ return { commands, terminator: 'end' };
5495
5664
  }
5496
5665
  return { commands, terminator };
5497
5666
  }
@@ -6075,7 +6244,9 @@ class Parser {
6075
6244
  parseTopLevelInitBlock() {
6076
6245
  const pos = this.getPosition();
6077
6246
  const initCommands = this.parseCommandBlock(['end']);
6078
- this.consume('end', "Expected 'end' after init block");
6247
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
6248
+ this.consume('end', "Expected 'end' after init block");
6249
+ }
6079
6250
  return {
6080
6251
  type: 'initBlock',
6081
6252
  commands: initCommands,
@@ -6251,10 +6422,29 @@ class Parser {
6251
6422
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
6252
6423
  };
6253
6424
  }
6425
+ isArgumentListOnly(open) {
6426
+ let depth = 0;
6427
+ for (let i = open; i < this.tokens.length; i++) {
6428
+ const token = this.tokens[i];
6429
+ if (token.kind !== 'operator')
6430
+ continue;
6431
+ if (token.value === '(' || token.value === '[' || token.value === '{')
6432
+ depth++;
6433
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
6434
+ if (--depth === 0)
6435
+ return i === open + 1;
6436
+ }
6437
+ else if (token.value === ',' && depth === 1)
6438
+ return true;
6439
+ }
6440
+ return false;
6441
+ }
6254
6442
  tryParsePseudoCommand(callExpr, sourceExpr) {
6255
6443
  const methodName = callExpr.callee.name;
6256
6444
  const nextToken = this.peek();
6257
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
6445
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
6446
+ const hasPseudoCommandPattern = nextIsArticle ||
6447
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
6258
6448
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
6259
6449
  isContextVar(nextToken);
6260
6450
  if (!hasPseudoCommandPattern) {
@@ -6280,7 +6470,10 @@ class Parser {
6280
6470
  });
6281
6471
  }
6282
6472
  let preposition;
6283
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
6473
+ if (nextIsArticle) {
6474
+ this.advance();
6475
+ }
6476
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
6284
6477
  preposition = this.advance().value.toLowerCase();
6285
6478
  }
6286
6479
  let targetExpr;
@@ -6444,10 +6637,22 @@ class Parser {
6444
6637
  this.consume(']', "Expected ']' after event condition");
6445
6638
  }
6446
6639
  let target;
6640
+ let targetExpression;
6447
6641
  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})`);
6642
+ if (this.check('the'))
6643
+ this.advance();
6644
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
6645
+ const startIndex = this.current;
6646
+ targetExpression = this.parseExpression();
6647
+ target = this.tokens
6648
+ .slice(startIndex, this.current)
6649
+ .map(t => t.value)
6650
+ .join(' ');
6651
+ }
6652
+ else {
6653
+ target = this.advance().value;
6654
+ }
6655
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
6451
6656
  this.parseTemporalKeywordModifiers(modifiers);
6452
6657
  }
6453
6658
  let attributeName;
@@ -6493,7 +6698,7 @@ class Parser {
6493
6698
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
6494
6699
  const commandName = this.peek().value.toLowerCase();
6495
6700
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
6496
- if (nextIsOpenParen && !isSpecialBodyCommand) {
6701
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
6497
6702
  let expr;
6498
6703
  const savedError = this.error;
6499
6704
  try {
@@ -6525,7 +6730,7 @@ class Parser {
6525
6730
  }
6526
6731
  }
6527
6732
  }
6528
- else if (this.checkIdentifier()) {
6733
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
6529
6734
  const token = this.peek();
6530
6735
  if (this.isCommand(token.value)) {
6531
6736
  const cmd = this.parseCommandWithErrorRecovery();
@@ -6634,6 +6839,7 @@ class Parser {
6634
6839
  ...(eventParams.length > 0 && { args: eventParams }),
6635
6840
  ...(condition && { condition: fromLegacyExpression(condition) }),
6636
6841
  ...(target && { target }),
6842
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
6637
6843
  ...(attributeName && { attributeName }),
6638
6844
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
6639
6845
  ...(customEventSource && { customEventSource }),
@@ -6889,10 +7095,9 @@ class Parser {
6889
7095
  }
6890
7096
  parseCommandCore() {
6891
7097
  const commandToken = this.previous();
6892
- let commandName = commandToken.value;
7098
+ const commandName = commandToken.value;
6893
7099
  if (commandName === 'beep' && this.check('!')) {
6894
7100
  this.advance();
6895
- commandName = 'beep!';
6896
7101
  }
6897
7102
  if (commandName === 'fetch') {
6898
7103
  return parseFetchCommand(this.getContext(), commandToken);
@@ -6991,6 +7196,9 @@ class Parser {
6991
7196
  args.push(this.parsePrimary());
6992
7197
  }
6993
7198
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
7199
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
7200
+ callNode.closestTo = this.parseExpression();
7201
+ }
6994
7202
  if (funcName === 'next' || funcName === 'previous') {
6995
7203
  const relativePositional = this.parseRelativePositionalModifiers();
6996
7204
  if (relativePositional) {
@@ -7030,6 +7238,9 @@ class Parser {
7030
7238
  }
7031
7239
  parseTheXofY() {
7032
7240
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
7241
+ if (this.isStyleRefStar(this.current)) {
7242
+ return this.parsePrimary();
7243
+ }
7033
7244
  if (!this.checkIdentifier()) {
7034
7245
  return this.createIdentifier('the');
7035
7246
  }
@@ -7045,7 +7256,7 @@ class Parser {
7045
7256
  return this.createIdentifier(propertyName);
7046
7257
  }
7047
7258
  this.advance();
7048
- const target = this.parseCall();
7259
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
7049
7260
  return {
7050
7261
  type: 'propertyOfExpression',
7051
7262
  property: { type: 'identifier', name: propertyName },
@@ -7243,6 +7454,13 @@ class Parser {
7243
7454
  checkLiteral() {
7244
7455
  return this.checkPredicate(isLiteral);
7245
7456
  }
7457
+ checkEventNamedCall() {
7458
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
7459
+ return false;
7460
+ const token = this.peek();
7461
+ const next = this.tokens[this.current + 1];
7462
+ return next?.value === '(' && next.start === token.end;
7463
+ }
7246
7464
  checkIdentifierLike() {
7247
7465
  return this.checkPredicate(isIdentifierLike);
7248
7466
  }
@@ -7351,6 +7569,22 @@ class Parser {
7351
7569
  isAtEnd() {
7352
7570
  return this.current >= this.tokens.length;
7353
7571
  }
7572
+ atEndOfInput() {
7573
+ for (let i = this.current; i < this.tokens.length; i++) {
7574
+ if (!isComment(this.tokens[i]))
7575
+ return false;
7576
+ }
7577
+ return true;
7578
+ }
7579
+ checkFeatureStart() {
7580
+ if (this.isAtEnd())
7581
+ return false;
7582
+ const value = this.peek().value;
7583
+ return (value === 'init' ||
7584
+ value === 'on' ||
7585
+ value === 'def' ||
7586
+ getRegisteredFeature(value) !== undefined);
7587
+ }
7354
7588
  peek() {
7355
7589
  if (this.isAtEnd()) {
7356
7590
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -7562,6 +7796,7 @@ class Parser {
7562
7796
  match: this.match.bind(this),
7563
7797
  matchOperator: this.matchOperator.bind(this),
7564
7798
  isAtEnd: this.isAtEnd.bind(this),
7799
+ atEndOfInput: this.atEndOfInput.bind(this),
7565
7800
  createIdentifier: this.createIdentifier.bind(this),
7566
7801
  parseExpression: this.parseExpression.bind(this),
7567
7802
  parsePrimary: this.parsePrimary.bind(this),
@@ -7612,7 +7847,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
7612
7847
  'ignoring case',
7613
7848
  ]);
7614
7849
  Parser.PRATT_TABLE = PARSER_TABLE;
7850
+ Parser.BEEP_OPERAND_BP = 80;
7615
7851
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
7852
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
7853
+ 'closest',
7854
+ 'next',
7855
+ 'previous',
7856
+ 'first',
7857
+ 'last',
7858
+ ]);
7616
7859
 
7617
7860
  class FullParserImpl {
7618
7861
  constructor() {