@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
@@ -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')) {
@@ -3213,6 +3358,15 @@ function parseToggleCommand(ctx, identifierNode) {
3213
3358
  args.push(classArg.expression);
3214
3359
  modifiers['as'] = literalModifier(mode);
3215
3360
  }
3361
+ else if (classArg?.type === 'binaryExpression' &&
3362
+ classArg.operator === 'of' &&
3363
+ classArg.left?.type === 'selector' &&
3364
+ typeof classArg.left.value === 'string' &&
3365
+ classArg.left.value.startsWith('*') &&
3366
+ classArg.right) {
3367
+ args.push(classArg.left);
3368
+ modifiers['on'] = classArg.right;
3369
+ }
3216
3370
  else if (classArg) {
3217
3371
  args.push(classArg);
3218
3372
  }
@@ -3390,11 +3544,17 @@ function parseSwapCommand(ctx, identifierNode) {
3390
3544
  const targetExpr = isDelete ? ctx.parseExpression() : parseOneArgument(ctx, [KEYWORDS.WITH]);
3391
3545
  if (targetExpr)
3392
3546
  args.push(targetExpr);
3393
- if (!isDelete && consumeOptionalKeyword(ctx, KEYWORDS.WITH)) {
3547
+ const isMorph = identifierNode.name === 'morph';
3548
+ if (!isDelete &&
3549
+ (consumeOptionalKeyword(ctx, KEYWORDS.WITH) ||
3550
+ (isMorph && consumeOptionalKeyword(ctx, KEYWORDS.TO)))) {
3394
3551
  const contentExpr = parseOneArgument(ctx, ['using']);
3395
3552
  if (contentExpr)
3396
3553
  modifiers['with'] = contentExpr;
3397
3554
  }
3555
+ if (isMorph && modifiers['with'] === undefined) {
3556
+ ctx.addError("Expected 'to' and the content to morph into");
3557
+ }
3398
3558
  return CommandNodeBuilder.fromIdentifier(identifierNode)
3399
3559
  .withArgs(...args)
3400
3560
  .withModifiers({ ...modifiers, ...parseViewTransitionTail(ctx) })
@@ -4155,6 +4315,8 @@ function atArgumentBoundary(ctx, grammar) {
4155
4315
  const next = ctx.peek();
4156
4316
  if (ctx.checkIsCommand() && ctx.peekAt(1)?.value === '(')
4157
4317
  return false;
4318
+ if (isGluedBeepBang(next, ctx.peekAt(1)))
4319
+ return false;
4158
4320
  if (isCommandBoundary(ctx, ['catch', 'finally', 'on']))
4159
4321
  return true;
4160
4322
  return isKeyword(next, [...grammar.markers]);
@@ -4186,10 +4348,13 @@ function parseDeclaredCommand(ctx, commandToken, name, grammar) {
4186
4348
  }
4187
4349
  }
4188
4350
  const commaList = new Set(grammar.commaList ?? []);
4351
+ const namedArgs = new Set(grammar.namedArgs ?? []);
4189
4352
  while (!ctx.isAtEnd() && isKeyword(ctx.peek(), [...grammar.markers])) {
4190
4353
  const keyword = ctx.advance().value;
4191
4354
  const key = ctx.resolveKeyword(keyword).toLowerCase();
4192
- let value = ctx.parseExpression();
4355
+ let value = namedArgs.has(key) && isNakedNamedArgStart(ctx)
4356
+ ? parseNakedNamedArgs(ctx, () => ctx.parseExpression(), name)
4357
+ : ctx.parseExpression();
4193
4358
  if (value && commaList.has(key) && ctx.check(',')) {
4194
4359
  const elements = [value];
4195
4360
  while (ctx.match(',')) {
@@ -4354,8 +4519,8 @@ function parseFetchCommand(ctx, commandToken) {
4354
4519
  }
4355
4520
  if (ctx.check('with') && !modifiers['with']) {
4356
4521
  ctx.advance();
4357
- if (isFetchNakedNamedArgStart(ctx)) {
4358
- modifiers['with'] = parseFetchNakedNamedArgs(ctx);
4522
+ if (isNakedNamedArgStart(ctx)) {
4523
+ modifiers['with'] = parseNakedNamedArgs(ctx, () => ctx.parsePrimary(), 'fetch');
4359
4524
  }
4360
4525
  else {
4361
4526
  modifiers['with'] = ctx.parsePrimary();
@@ -4390,45 +4555,6 @@ function parseFetchCommand(ctx, commandToken) {
4390
4555
  }
4391
4556
  return builder.build();
4392
4557
  }
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
4558
  function findJsEndBoundary(ctx, startPos) {
4433
4559
  const input = ctx.getInputSlice(startPos);
4434
4560
  if (!input) {
@@ -4548,6 +4674,7 @@ function parseTellCommand(ctx, identifierNode) {
4548
4674
  const commands = [];
4549
4675
  while (!ctx.isAtEnd()) {
4550
4676
  if (ctx.checkIsCommand()) {
4677
+ const word = ctx.peek().value;
4551
4678
  try {
4552
4679
  ctx.advance();
4553
4680
  const cmd = ctx.parseCommand();
@@ -4558,7 +4685,8 @@ function parseTellCommand(ctx, identifierNode) {
4558
4685
  break;
4559
4686
  }
4560
4687
  }
4561
- catch {
4688
+ catch (error) {
4689
+ ctx.addError(`'${word}' in a tell body failed to parse: ${error instanceof Error ? error.message : String(error)}`);
4562
4690
  break;
4563
4691
  }
4564
4692
  if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
@@ -4707,7 +4835,6 @@ const ONE_EXPR = (syntax) => ({
4707
4835
  markers: [],
4708
4836
  syntax,
4709
4837
  });
4710
- const BEEP = ONE_EXPR('beep! [<expression>, \u2026]');
4711
4838
  const COMMAND_GRAMMAR = {
4712
4839
  append: { positional: 'expression', markers: ['to'], syntax: 'append <value> [to <target>]' },
4713
4840
  prepend: { positional: 'expression', markers: ['to'], syntax: 'prepend <value> [to <target>]' },
@@ -4718,8 +4845,7 @@ const COMMAND_GRAMMAR = {
4718
4845
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
4719
4846
  },
4720
4847
  throw: { positional: 'primary', markers: [], syntax: 'throw <error>' },
4721
- 'beep!': BEEP,
4722
- beep: BEEP,
4848
+ beep: ONE_EXPR('beep! [<expression>, \u2026]'),
4723
4849
  blur: ONE_EXPR('blur [<target>]'),
4724
4850
  break: NONE,
4725
4851
  breakpoint: NONE,
@@ -4743,7 +4869,8 @@ const COMMAND_GRAMMAR = {
4743
4869
  render: {
4744
4870
  positional: 'expression',
4745
4871
  markers: ['with'],
4746
- syntax: 'render <template> [with <variables>]',
4872
+ namedArgs: ['with'],
4873
+ syntax: 'render <template> [with <name>: <value>, \u2026]',
4747
4874
  },
4748
4875
  reset: ONE_EXPR('reset [<form>]'),
4749
4876
  return: ONE_EXPR('return [<value>]'),
@@ -5098,7 +5225,10 @@ class Parser {
5098
5225
  const firstToken = this.peek();
5099
5226
  const prefixEntry = Parser.PRATT_TABLE.get(firstToken.value);
5100
5227
  let left;
5101
- if (prefixEntry?.prefix) {
5228
+ if (isGluedBeepBang(firstToken, this.tokens[this.current + 1])) {
5229
+ left = this.parseBeepExpression();
5230
+ }
5231
+ else if (prefixEntry?.prefix) {
5102
5232
  const token = this.advance();
5103
5233
  if (this.isAtEnd()) {
5104
5234
  this.addError(`Expected expression after '${token.value}' operator`);
@@ -5118,6 +5248,9 @@ class Parser {
5118
5248
  continue;
5119
5249
  }
5120
5250
  }
5251
+ if (nextToken.value === '*' && this.isSpacedStyleRef(this.current)) {
5252
+ break;
5253
+ }
5121
5254
  const infixEntry = Parser.PRATT_TABLE.get(nextToken.value);
5122
5255
  if (!infixEntry?.infix) {
5123
5256
  if (STOP_TOKENS.has(nextToken.value) || STOP_DELIMITERS.has(nextToken.value)) {
@@ -5179,6 +5312,35 @@ class Parser {
5179
5312
  this.prattCache.set(cacheKey, { node: left, endPos: this.current });
5180
5313
  return left;
5181
5314
  }
5315
+ parseBeepExpression() {
5316
+ const beepToken = this.advance();
5317
+ this.advance();
5318
+ if (this.isAtEnd()) {
5319
+ this.addError("Expected expression after 'beep!'");
5320
+ return this.createErrorNode();
5321
+ }
5322
+ const operand = this.parseExpressionPratt(Parser.BEEP_OPERAND_BP);
5323
+ return {
5324
+ type: 'unaryExpression',
5325
+ operator: 'beep!',
5326
+ operand,
5327
+ argument: operand,
5328
+ prefix: true,
5329
+ start: beepToken.start,
5330
+ end: this.previous().end,
5331
+ line: beepToken.line,
5332
+ column: beepToken.column,
5333
+ };
5334
+ }
5335
+ isStyleRefStar(i) {
5336
+ const star = this.tokens[i];
5337
+ const name = this.tokens[i + 1];
5338
+ return star?.value === '*' && !!name && name.start === star.end && isIdentifierLike(name);
5339
+ }
5340
+ isSpacedStyleRef(i) {
5341
+ const prev = this.tokens[i - 1];
5342
+ return this.isStyleRefStar(i) && !!prev && prev.end < this.tokens[i].start;
5343
+ }
5182
5344
  tryParseStringPostfix(left, nextToken) {
5183
5345
  const v = nextToken.value;
5184
5346
  let unit = null;
@@ -5381,6 +5543,10 @@ class Parser {
5381
5543
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
5382
5544
  debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
5383
5545
  while (!this.isAtEnd() && !isStop()) {
5546
+ if (this.checkComment()) {
5547
+ this.advance();
5548
+ continue;
5549
+ }
5384
5550
  debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
5385
5551
  let parsedCommand = false;
5386
5552
  const isCommandToken = this.checkIsCommand();
@@ -5464,7 +5630,7 @@ class Parser {
5464
5630
  debug.parse('\u2705 Found "end", consuming it');
5465
5631
  this.advance();
5466
5632
  }
5467
- else {
5633
+ else if (!this.atEndOfInput()) {
5468
5634
  debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
5469
5635
  throw new Error(`Expected "end" to close ${construct} block`);
5470
5636
  }
@@ -5477,7 +5643,7 @@ class Parser {
5477
5643
  if (this.check('end')) {
5478
5644
  this.advance();
5479
5645
  }
5480
- else {
5646
+ else if (!this.atEndOfInput()) {
5481
5647
  throw new Error('Expected "end" to close repeat block');
5482
5648
  }
5483
5649
  }
@@ -5493,7 +5659,10 @@ class Parser {
5493
5659
  this.advance();
5494
5660
  }
5495
5661
  else if (terminator === '') {
5496
- throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5662
+ if (!this.atEndOfInput()) {
5663
+ throw new Error('Expected "end", "else", "until", or "while" to close repeat block');
5664
+ }
5665
+ return { commands, terminator: 'end' };
5497
5666
  }
5498
5667
  return { commands, terminator };
5499
5668
  }
@@ -6077,7 +6246,9 @@ class Parser {
6077
6246
  parseTopLevelInitBlock() {
6078
6247
  const pos = this.getPosition();
6079
6248
  const initCommands = this.parseCommandBlock(['end']);
6080
- this.consume('end', "Expected 'end' after init block");
6249
+ if (!this.match('end') && !this.atEndOfInput() && !this.checkFeatureStart()) {
6250
+ this.consume('end', "Expected 'end' after init block");
6251
+ }
6081
6252
  return {
6082
6253
  type: 'initBlock',
6083
6254
  commands: initCommands,
@@ -6253,10 +6424,29 @@ class Parser {
6253
6424
  ...(sourceExpr.column !== undefined && { column: sourceExpr.column }),
6254
6425
  };
6255
6426
  }
6427
+ isArgumentListOnly(open) {
6428
+ let depth = 0;
6429
+ for (let i = open; i < this.tokens.length; i++) {
6430
+ const token = this.tokens[i];
6431
+ if (token.kind !== 'operator')
6432
+ continue;
6433
+ if (token.value === '(' || token.value === '[' || token.value === '{')
6434
+ depth++;
6435
+ else if (token.value === ')' || token.value === ']' || token.value === '}') {
6436
+ if (--depth === 0)
6437
+ return i === open + 1;
6438
+ }
6439
+ else if (token.value === ',' && depth === 1)
6440
+ return true;
6441
+ }
6442
+ return false;
6443
+ }
6256
6444
  tryParsePseudoCommand(callExpr, sourceExpr) {
6257
6445
  const methodName = callExpr.callee.name;
6258
6446
  const nextToken = this.peek();
6259
- const hasPseudoCommandPattern = Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
6447
+ const nextIsArticle = nextToken.value.toLowerCase() === 'the';
6448
+ const hasPseudoCommandPattern = nextIsArticle ||
6449
+ Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(nextToken.value.toLowerCase()) ||
6260
6450
  (isIdentifier(nextToken) && !this.isCommand(nextToken.value)) ||
6261
6451
  isContextVar(nextToken);
6262
6452
  if (!hasPseudoCommandPattern) {
@@ -6282,7 +6472,10 @@ class Parser {
6282
6472
  });
6283
6473
  }
6284
6474
  let preposition;
6285
- if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
6475
+ if (nextIsArticle) {
6476
+ this.advance();
6477
+ }
6478
+ else if (Parser.PSEUDO_COMMAND_PREPOSITIONS.includes(this.peek().value.toLowerCase())) {
6286
6479
  preposition = this.advance().value.toLowerCase();
6287
6480
  }
6288
6481
  let targetExpr;
@@ -6446,10 +6639,22 @@ class Parser {
6446
6639
  this.consume(']', "Expected ']' after event condition");
6447
6640
  }
6448
6641
  let target;
6642
+ let targetExpression;
6449
6643
  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})`);
6644
+ if (this.check('the'))
6645
+ this.advance();
6646
+ if (Parser.POSITIONAL_SOURCE_HEADS.has(this.peek().value.toLowerCase())) {
6647
+ const startIndex = this.current;
6648
+ targetExpression = this.parseExpression();
6649
+ target = this.tokens
6650
+ .slice(startIndex, this.current)
6651
+ .map(t => t.value)
6652
+ .join(' ');
6653
+ }
6654
+ else {
6655
+ target = this.advance().value;
6656
+ }
6657
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target}`);
6453
6658
  this.parseTemporalKeywordModifiers(modifiers);
6454
6659
  }
6455
6660
  let attributeName;
@@ -6495,7 +6700,7 @@ class Parser {
6495
6700
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
6496
6701
  const commandName = this.peek().value.toLowerCase();
6497
6702
  const isSpecialBodyCommand = commandName === 'js' || commandName === 'tell';
6498
- if (nextIsOpenParen && !isSpecialBodyCommand) {
6703
+ if (nextIsOpenParen && !isSpecialBodyCommand && this.isArgumentListOnly(this.current + 1)) {
6499
6704
  let expr;
6500
6705
  const savedError = this.error;
6501
6706
  try {
@@ -6527,7 +6732,7 @@ class Parser {
6527
6732
  }
6528
6733
  }
6529
6734
  }
6530
- else if (this.checkIdentifier()) {
6735
+ else if (this.checkIdentifier() || this.checkEventNamedCall()) {
6531
6736
  const token = this.peek();
6532
6737
  if (this.isCommand(token.value)) {
6533
6738
  const cmd = this.parseCommandWithErrorRecovery();
@@ -6636,6 +6841,7 @@ class Parser {
6636
6841
  ...(eventParams.length > 0 && { args: eventParams }),
6637
6842
  ...(condition && { condition: fromLegacyExpression(condition) }),
6638
6843
  ...(target && { target }),
6844
+ ...(targetExpression && { targetExpression: fromLegacyExpression(targetExpression) }),
6639
6845
  ...(attributeName && { attributeName }),
6640
6846
  ...(watchTarget && { watchTarget: fromLegacyExpression(watchTarget) }),
6641
6847
  ...(customEventSource && { customEventSource }),
@@ -6891,10 +7097,9 @@ class Parser {
6891
7097
  }
6892
7098
  parseCommandCore() {
6893
7099
  const commandToken = this.previous();
6894
- let commandName = commandToken.value;
7100
+ const commandName = commandToken.value;
6895
7101
  if (commandName === 'beep' && this.check('!')) {
6896
7102
  this.advance();
6897
- commandName = 'beep!';
6898
7103
  }
6899
7104
  if (commandName === 'fetch') {
6900
7105
  return parseFetchCommand(this.getContext(), commandToken);
@@ -6993,6 +7198,9 @@ class Parser {
6993
7198
  args.push(this.parsePrimary());
6994
7199
  }
6995
7200
  const callNode = this.createCallExpression(createIdentifier(funcName, namePos), args);
7201
+ if (funcName === 'closest' && args.length > 0 && this.match('to')) {
7202
+ callNode.closestTo = this.parseExpression();
7203
+ }
6996
7204
  if (funcName === 'next' || funcName === 'previous') {
6997
7205
  const relativePositional = this.parseRelativePositionalModifiers();
6998
7206
  if (relativePositional) {
@@ -7032,6 +7240,9 @@ class Parser {
7032
7240
  }
7033
7241
  parseTheXofY() {
7034
7242
  const positionalKeywords = ['first', 'last', 'next', 'previous', 'random', 'closest'];
7243
+ if (this.isStyleRefStar(this.current)) {
7244
+ return this.parsePrimary();
7245
+ }
7035
7246
  if (!this.checkIdentifier()) {
7036
7247
  return this.createIdentifier('the');
7037
7248
  }
@@ -7047,7 +7258,7 @@ class Parser {
7047
7258
  return this.createIdentifier(propertyName);
7048
7259
  }
7049
7260
  this.advance();
7050
- const target = this.parseCall();
7261
+ const target = parseOfOperand(this.makePrattContext(), THE_OF_TARGET_BP);
7051
7262
  return {
7052
7263
  type: 'propertyOfExpression',
7053
7264
  property: { type: 'identifier', name: propertyName },
@@ -7245,6 +7456,13 @@ class Parser {
7245
7456
  checkLiteral() {
7246
7457
  return this.checkPredicate(isLiteral);
7247
7458
  }
7459
+ checkEventNamedCall() {
7460
+ if (this.isAtEnd() || !this.checkIdentifierLike() || this.checkIsCommand())
7461
+ return false;
7462
+ const token = this.peek();
7463
+ const next = this.tokens[this.current + 1];
7464
+ return next?.value === '(' && next.start === token.end;
7465
+ }
7248
7466
  checkIdentifierLike() {
7249
7467
  return this.checkPredicate(isIdentifierLike);
7250
7468
  }
@@ -7353,6 +7571,22 @@ class Parser {
7353
7571
  isAtEnd() {
7354
7572
  return this.current >= this.tokens.length;
7355
7573
  }
7574
+ atEndOfInput() {
7575
+ for (let i = this.current; i < this.tokens.length; i++) {
7576
+ if (!isComment(this.tokens[i]))
7577
+ return false;
7578
+ }
7579
+ return true;
7580
+ }
7581
+ checkFeatureStart() {
7582
+ if (this.isAtEnd())
7583
+ return false;
7584
+ const value = this.peek().value;
7585
+ return (value === 'init' ||
7586
+ value === 'on' ||
7587
+ value === 'def' ||
7588
+ getRegisteredFeature(value) !== undefined);
7589
+ }
7356
7590
  peek() {
7357
7591
  if (this.isAtEnd()) {
7358
7592
  return { kind: 'unknown', value: '', start: 0, end: 0, line: 1, column: 1 };
@@ -7564,6 +7798,7 @@ class Parser {
7564
7798
  match: this.match.bind(this),
7565
7799
  matchOperator: this.matchOperator.bind(this),
7566
7800
  isAtEnd: this.isAtEnd.bind(this),
7801
+ atEndOfInput: this.atEndOfInput.bind(this),
7567
7802
  createIdentifier: this.createIdentifier.bind(this),
7568
7803
  parseExpression: this.parseExpression.bind(this),
7569
7804
  parsePrimary: this.parsePrimary.bind(this),
@@ -7614,7 +7849,15 @@ Parser.POSTFIX_UNARY_OPERATORS = new Set([
7614
7849
  'ignoring case',
7615
7850
  ]);
7616
7851
  Parser.PRATT_TABLE = PARSER_TABLE;
7852
+ Parser.BEEP_OPERAND_BP = 80;
7617
7853
  Parser.PSEUDO_COMMAND_PREPOSITIONS = ['from', 'on', 'with', 'into', 'at', 'to'];
7854
+ Parser.POSITIONAL_SOURCE_HEADS = new Set([
7855
+ 'closest',
7856
+ 'next',
7857
+ 'previous',
7858
+ 'first',
7859
+ 'last',
7860
+ ]);
7618
7861
 
7619
7862
  class FullParserImpl {
7620
7863
  constructor() {