@hyperfixi/core 2.9.0 → 2.9.2

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 (65) hide show
  1. package/dist/ast-utils/index.js +905 -882
  2. package/dist/ast-utils/index.mjs +905 -882
  3. package/dist/bundle-generator/index.js +6 -0
  4. package/dist/bundle-generator/index.mjs +6 -0
  5. package/dist/bundle-generator/parser-templates.d.ts +1 -1
  6. package/dist/chunks/{bridge-D9JLmPkk.js → bridge-DwtNLKK3.js} +2 -2
  7. package/dist/chunks/browser-modular-6HT1r2AH.js +2 -0
  8. package/dist/chunks/feature-eventsource-Bxg_UNZo.js +2 -0
  9. package/dist/chunks/feature-sockets-CcTxD5sT.js +2 -0
  10. package/dist/chunks/feature-webworker-DoqZ0Grg.js +2 -0
  11. package/dist/chunks/{index-6DUg7Qjm.js → index-B8jwvuj_.js} +2 -2
  12. package/dist/commands/index.js +299 -184
  13. package/dist/commands/index.mjs +299 -184
  14. package/dist/hyperfixi-browser-classic-i18n.js +1 -1
  15. package/dist/hyperfixi-browser-minimal.js +1 -1
  16. package/dist/hyperfixi-browser-standard.js +1 -1
  17. package/dist/hyperfixi-browser.js +1 -1
  18. package/dist/hyperfixi-classic-i18n.js +1 -1
  19. package/dist/hyperfixi-hx-v4.js +1 -1
  20. package/dist/hyperfixi-hx.js +1 -1
  21. package/dist/hyperfixi-hybrid-complete.js +1 -1
  22. package/dist/hyperfixi-hybrid-hx.js +1 -1
  23. package/dist/hyperfixi-minimal.js +1 -1
  24. package/dist/hyperfixi-multilingual.js +1 -1
  25. package/dist/hyperfixi-standard.js +1 -1
  26. package/dist/hyperfixi.js +1 -1
  27. package/dist/hyperfixi.mjs +1 -1
  28. package/dist/index.js +1731 -1474
  29. package/dist/index.min.js +1 -1
  30. package/dist/index.mjs +1731 -1474
  31. package/dist/lokascript-browser-classic-i18n.js +1 -1
  32. package/dist/lokascript-browser-minimal.js +1 -1
  33. package/dist/lokascript-browser-standard.js +1 -1
  34. package/dist/lokascript-browser.js +1 -1
  35. package/dist/lokascript-hybrid-complete.js +1 -1
  36. package/dist/lokascript-hybrid-hx.js +1 -1
  37. package/dist/lokascript-multilingual.js +1 -1
  38. package/dist/lsp-metadata.js +5 -5
  39. package/dist/lsp-metadata.mjs +5 -5
  40. package/dist/metadata.js +25 -25
  41. package/dist/metadata.mjs +25 -25
  42. package/dist/multilingual/index.js +17 -17
  43. package/dist/multilingual/index.mjs +17 -17
  44. package/dist/parser/command-parsers/utility-commands.d.ts +2 -1
  45. package/dist/parser/full-parser.js +286 -178
  46. package/dist/parser/full-parser.mjs +286 -178
  47. package/dist/parser/hybrid/index.js +3 -0
  48. package/dist/parser/hybrid/index.mjs +3 -0
  49. package/dist/parser/hybrid/parser-core.js +3 -0
  50. package/dist/parser/hybrid/parser-core.mjs +3 -0
  51. package/dist/parser/hybrid-parser.js +3 -0
  52. package/dist/parser/hybrid-parser.mjs +3 -0
  53. package/dist/parser/parser.d.ts +1 -0
  54. package/dist/reference/index.js +2 -0
  55. package/dist/reference/index.mjs +2 -0
  56. package/dist/registry/index.js +286 -178
  57. package/dist/registry/index.mjs +286 -178
  58. package/dist/runtime/runtime-base.d.ts +2 -1
  59. package/dist/types/base-types.d.ts +3 -0
  60. package/dist/types/core.d.ts +1 -0
  61. package/package.json +6 -6
  62. package/dist/chunks/browser-modular-CPiVQXM0.js +0 -2
  63. package/dist/chunks/feature-eventsource-BpZvPy_K.js +0 -2
  64. package/dist/chunks/feature-sockets-CrYvjZ4j.js +0 -2
  65. package/dist/chunks/feature-webworker-BSYguEIW.js +0 -2
@@ -424,6 +424,7 @@ function tokenize(input) {
424
424
  const prevToken = tokenizer.tokens[tokenizer.tokens.length - 1];
425
425
  const isPossessive = nextChar === 's' &&
426
426
  prevToken &&
427
+ !/\s/.test(tokenizer.input[tokenizer.position - 1] ?? ' ') &&
427
428
  (prevToken.kind === TokenKind.IDENTIFIER ||
428
429
  prevToken.kind === TokenKind.SELECTOR ||
429
430
  prevToken.value === ']' ||
@@ -515,13 +516,13 @@ function tokenize(input) {
515
516
  continue;
516
517
  }
517
518
  if (char === '{') {
518
- addToken(tokenizer, TokenKind.OPERATOR, '{');
519
519
  advance(tokenizer);
520
+ addToken(tokenizer, TokenKind.OPERATOR, '{');
520
521
  continue;
521
522
  }
522
523
  if (char === '}') {
523
- addToken(tokenizer, TokenKind.OPERATOR, '}');
524
524
  advance(tokenizer);
525
+ addToken(tokenizer, TokenKind.OPERATOR, '}');
525
526
  continue;
526
527
  }
527
528
  if (char === '[') {
@@ -537,24 +538,24 @@ function tokenize(input) {
537
538
  prevToken.value === ']') &&
538
539
  !isEventCondition;
539
540
  if (isMemberAccess) {
540
- addToken(tokenizer, TokenKind.OPERATOR, '[');
541
541
  advance(tokenizer);
542
+ addToken(tokenizer, TokenKind.OPERATOR, '[');
542
543
  }
543
544
  else {
544
545
  if (isEventCondition) {
545
- addToken(tokenizer, TokenKind.SYMBOL, '[');
546
546
  advance(tokenizer);
547
+ addToken(tokenizer, TokenKind.SYMBOL, '[');
547
548
  }
548
549
  else {
549
- addToken(tokenizer, TokenKind.OPERATOR, '[');
550
550
  advance(tokenizer);
551
+ addToken(tokenizer, TokenKind.OPERATOR, '[');
551
552
  }
552
553
  }
553
554
  continue;
554
555
  }
555
556
  if (char === ']') {
556
- addToken(tokenizer, TokenKind.OPERATOR, ']');
557
557
  advance(tokenizer);
558
+ addToken(tokenizer, TokenKind.OPERATOR, ']');
558
559
  continue;
559
560
  }
560
561
  if (char === '$') {
@@ -1296,39 +1297,39 @@ const DEBUG = {
1296
1297
  const debug = {
1297
1298
  command: (...args) => {
1298
1299
  if (DEBUG.commands)
1299
- console.log('🔧', ...args);
1300
+ console.log('\ud83d\udd27', ...args);
1300
1301
  },
1301
1302
  event: (...args) => {
1302
1303
  if (DEBUG.events)
1303
- console.log('🎯', ...args);
1304
+ console.log('\ud83c\udfaf', ...args);
1304
1305
  },
1305
1306
  parse: (...args) => {
1306
1307
  if (DEBUG.parsing)
1307
- console.log('📝', ...args);
1308
+ console.log('\ud83d\udcdd', ...args);
1308
1309
  },
1309
1310
  expr: (...args) => {
1310
1311
  if (DEBUG.expressions)
1311
- console.log('🔍', ...args);
1312
+ console.log('\ud83d\udd0d', ...args);
1312
1313
  },
1313
1314
  expressions: (...args) => {
1314
1315
  if (DEBUG.expressions)
1315
- console.log('🔍', ...args);
1316
+ console.log('\ud83d\udd0d', ...args);
1316
1317
  },
1317
1318
  style: (...args) => {
1318
1319
  if (DEBUG.styles)
1319
- console.log('🎨', ...args);
1320
+ console.log('\ud83c\udfa8', ...args);
1320
1321
  },
1321
1322
  runtime: (...args) => {
1322
1323
  if (DEBUG.runtime)
1323
- console.log('🚀', ...args);
1324
+ console.log('\ud83d\ude80', ...args);
1324
1325
  },
1325
1326
  loop: (...args) => {
1326
1327
  if (DEBUG.loops)
1327
- console.log('🔁', ...args);
1328
+ console.log('\ud83d\udd01', ...args);
1328
1329
  },
1329
1330
  async: (...args) => {
1330
1331
  if (DEBUG.async)
1331
- console.log('⏳', ...args);
1332
+ console.log('\u23f3', ...args);
1332
1333
  },
1333
1334
  };
1334
1335
 
@@ -2726,9 +2727,9 @@ function createPartialCommandNode(name, pos) {
2726
2727
  };
2727
2728
  }
2728
2729
  function createProgramNode(statements) {
2729
- debug.parse(`✅ createProgramNode: Called with ${statements.length} statements`);
2730
+ debug.parse(`\u2705 createProgramNode: Called with ${statements.length} statements`);
2730
2731
  if (statements.length === 0) {
2731
- debug.parse(`✅ createProgramNode: Returning error node (0 statements)`);
2732
+ debug.parse(`\u2705 createProgramNode: Returning error node (0 statements)`);
2732
2733
  return {
2733
2734
  type: 'identifier',
2734
2735
  name: '__ERROR__',
@@ -2739,7 +2740,7 @@ function createProgramNode(statements) {
2739
2740
  };
2740
2741
  }
2741
2742
  if (statements.length === 1) {
2742
- debug.parse(`✅ createProgramNode: Returning single statement (type=${statements[0].type})`);
2743
+ debug.parse(`\u2705 createProgramNode: Returning single statement (type=${statements[0].type})`);
2743
2744
  return statements[0];
2744
2745
  }
2745
2746
  const programNode = {
@@ -2750,7 +2751,7 @@ function createProgramNode(statements) {
2750
2751
  line: statements[0]?.line || 1,
2751
2752
  column: statements[0]?.column || 1,
2752
2753
  };
2753
- debug.parse(`✅ createProgramNode: Returning Program node with ${statements.length} statements, type=${programNode.type}`);
2754
+ debug.parse(`\u2705 createProgramNode: Returning Program node with ${statements.length} statements, type=${programNode.type}`);
2754
2755
  return programNode;
2755
2756
  }
2756
2757
 
@@ -2761,7 +2762,6 @@ const MULTI_WORD_PATTERNS = [
2761
2762
  keywords: ['a', 'an', 'from', 'called'],
2762
2763
  syntax: 'make (a|an) <type> [from <args>] [called <name>]',
2763
2764
  },
2764
- { command: 'send', keywords: ['to'], syntax: 'send <event> to <target>' },
2765
2765
  { command: 'throw', keywords: [], syntax: 'throw <error>' },
2766
2766
  ];
2767
2767
  function getMultiWordPattern(commandName) {
@@ -3062,7 +3062,13 @@ function parseTriggerCommand(ctx, identifierNode) {
3062
3062
  finalArgs.push(ctx.createIdentifier(keyword));
3063
3063
  continue;
3064
3064
  }
3065
- finalArgs.push(ctx.parsePrimary());
3065
+ const before = ctx.savePosition();
3066
+ const expr = ctx.parsePrimary();
3067
+ if (expr)
3068
+ finalArgs.push(expr);
3069
+ if (ctx.savePosition() === before) {
3070
+ ctx.advance();
3071
+ }
3066
3072
  }
3067
3073
  return CommandNodeBuilder.fromIdentifier(identifierNode)
3068
3074
  .withArgs(...finalArgs)
@@ -3139,37 +3145,37 @@ function parseRepeatCommand(ctx, commandToken) {
3139
3145
  ctx.advance();
3140
3146
  loopType = 'until-event';
3141
3147
  const eventToken = ctx.peek();
3142
- debug.parse('📍 Parsing event name, current token:', {
3148
+ debug.parse('\ud83d\udccd Parsing event name, current token:', {
3143
3149
  value: eventToken.value,
3144
3150
  kind: eventToken.kind,
3145
3151
  });
3146
3152
  if (isIdentifierLike(eventToken) || isEvent(eventToken)) {
3147
3153
  eventName = eventToken.value;
3148
3154
  ctx.advance();
3149
- debug.parse('✅ Got event name:', eventName, 'Next token:', ctx.peek().value);
3155
+ debug.parse('\u2705 Got event name:', eventName, 'Next token:', ctx.peek().value);
3150
3156
  }
3151
3157
  else {
3152
3158
  throw new Error('Expected event name after "event"');
3153
3159
  }
3154
- debug.parse('🔍 Checking for "from", current token:', ctx.peek().value);
3160
+ debug.parse('\ud83d\udd0d Checking for "from", current token:', ctx.peek().value);
3155
3161
  if (ctx.check(KEYWORDS.FROM)) {
3156
- debug.parse('✅ Found "from", advancing...');
3162
+ debug.parse('\u2705 Found "from", advancing...');
3157
3163
  ctx.advance();
3158
- debug.parse('📍 After consuming "from", current token:', ctx.peek().value);
3164
+ debug.parse('\ud83d\udccd After consuming "from", current token:', ctx.peek().value);
3159
3165
  if (consumeOptionalKeyword(ctx, KEYWORDS.THE)) {
3160
- debug.parse('✅ Found "the", advancing...');
3166
+ debug.parse('\u2705 Found "the", advancing...');
3161
3167
  }
3162
3168
  const beforePrimary = ctx.peek();
3163
- debug.parse('🔍 Before parsePrimary for event target:', {
3169
+ debug.parse('\ud83d\udd0d Before parsePrimary for event target:', {
3164
3170
  value: beforePrimary.value,
3165
3171
  kind: beforePrimary.kind,
3166
3172
  position: beforePrimary.start,
3167
3173
  });
3168
3174
  eventTarget = ctx.parsePrimary();
3169
- debug.parse('✅ After parsePrimary, eventTarget:', eventTarget);
3175
+ debug.parse('\u2705 After parsePrimary, eventTarget:', eventTarget);
3170
3176
  }
3171
3177
  else {
3172
- debug.parse('❌ No "from" found, skipping target parsing');
3178
+ debug.parse('\u274c No "from" found, skipping target parsing');
3173
3179
  }
3174
3180
  }
3175
3181
  else {
@@ -3281,11 +3287,64 @@ function parseRepeatCommand(ctx, commandToken) {
3281
3287
  }
3282
3288
  return builder.endingAt(ctx.getPosition()).build();
3283
3289
  }
3290
+ function parseIfBranchCommands(ctx, stopAtElse) {
3291
+ const commands = [];
3292
+ const atTerminator = () => ctx.check(KEYWORDS.END) || (stopAtElse && ctx.check(KEYWORDS.ELSE));
3293
+ while (!ctx.isAtEnd() && !atTerminator()) {
3294
+ if (isComment(ctx.peek())) {
3295
+ ctx.advance();
3296
+ continue;
3297
+ }
3298
+ if (!ctx.checkIsCommand() && !ctx.isCommand(ctx.peek().value))
3299
+ break;
3300
+ ctx.advance();
3301
+ const cmd = ctx.parseCommand();
3302
+ if (cmd) {
3303
+ commands.push(cmd);
3304
+ }
3305
+ if (ctx.check(KEYWORDS.THEN) || ctx.check(KEYWORDS.AND) || ctx.check(',')) {
3306
+ ctx.advance();
3307
+ }
3308
+ }
3309
+ return commands;
3310
+ }
3311
+ const OPERAND_INTRODUCERS = new Set([
3312
+ 'is',
3313
+ 'am',
3314
+ 'are',
3315
+ 'not',
3316
+ 'no',
3317
+ 'and',
3318
+ 'or',
3319
+ '==',
3320
+ '===',
3321
+ '!=',
3322
+ '!==',
3323
+ '<',
3324
+ '>',
3325
+ '<=',
3326
+ '>=',
3327
+ '+',
3328
+ '-',
3329
+ '*',
3330
+ '/',
3331
+ 'mod',
3332
+ ]);
3333
+ function isBodyCommandStart(ctx, prevToken, token) {
3334
+ if (prevToken === null) {
3335
+ return false;
3336
+ }
3337
+ if (OPERAND_INTRODUCERS.has(prevToken.value?.toLowerCase?.() ?? '')) {
3338
+ return false;
3339
+ }
3340
+ return ctx.checkIsCommand() || ctx.isCommand(token.value?.toLowerCase());
3341
+ }
3284
3342
  function parseIfCommand(ctx, commandToken) {
3285
3343
  const args = [];
3286
3344
  let hasThen = false;
3287
3345
  const savedPosForThen = ctx.savePosition();
3288
3346
  const maxThenLookahead = 500;
3347
+ let prevForThen = null;
3289
3348
  for (let i = 0; i < maxThenLookahead && !ctx.isAtEnd(); i++) {
3290
3349
  const token = ctx.peek();
3291
3350
  if (token.value === KEYWORDS.THEN) {
@@ -3298,6 +3357,14 @@ function parseIfCommand(ctx, commandToken) {
3298
3357
  token.value === KEYWORDS.ON) {
3299
3358
  break;
3300
3359
  }
3360
+ if (token.line !== undefined &&
3361
+ token.line !== commandToken.line &&
3362
+ isBodyCommandStart(ctx, prevForThen, token)) {
3363
+ break;
3364
+ }
3365
+ if (!isComment(token)) {
3366
+ prevForThen = token;
3367
+ }
3301
3368
  ctx.advance();
3302
3369
  }
3303
3370
  ctx.restorePosition(savedPosForThen);
@@ -3306,9 +3373,14 @@ function parseIfCommand(ctx, commandToken) {
3306
3373
  const savedPosition = ctx.savePosition();
3307
3374
  const ifStatementLine = commandToken.line;
3308
3375
  const maxLookahead = 100;
3376
+ let firstCommandOnIfLine = false;
3377
+ let prevToken = null;
3309
3378
  while (!ctx.isAtEnd() && ctx.current - savedPosition < maxLookahead) {
3310
3379
  const token = ctx.peek();
3311
3380
  const tokenValue = token.value?.toLowerCase();
3381
+ if (firstCommandOnIfLine && token.line !== undefined && token.line !== ifStatementLine) {
3382
+ break;
3383
+ }
3312
3384
  if (tokenValue === KEYWORDS.BEHAVIOR ||
3313
3385
  tokenValue === KEYWORDS.DEF ||
3314
3386
  tokenValue === KEYWORDS.ON) {
@@ -3320,11 +3392,15 @@ function parseIfCommand(ctx, commandToken) {
3320
3392
  }
3321
3393
  break;
3322
3394
  }
3323
- if (ctx.checkIsCommand() || ctx.isCommand(tokenValue)) {
3395
+ if (isBodyCommandStart(ctx, prevToken, token)) {
3324
3396
  if (token.line !== undefined && token.line !== ifStatementLine) {
3325
3397
  hasImplicitMultiLineEnd = true;
3326
3398
  break;
3327
3399
  }
3400
+ firstCommandOnIfLine = true;
3401
+ }
3402
+ if (!isComment(token)) {
3403
+ prevToken = token;
3328
3404
  }
3329
3405
  ctx.advance();
3330
3406
  }
@@ -3339,11 +3415,12 @@ function parseIfCommand(ctx, commandToken) {
3339
3415
  const conditionTokens = [];
3340
3416
  const maxIterations = 20;
3341
3417
  let iterations = 0;
3418
+ let firstConditionParse = true;
3342
3419
  while (!ctx.isAtEnd() &&
3343
- !ctx.checkIsCommand() &&
3344
- !ctx.isCommand(ctx.peek().value) &&
3345
3420
  !ctx.check(KEYWORDS.THEN) &&
3421
+ (firstConditionParse || (!ctx.checkIsCommand() && !ctx.isCommand(ctx.peek().value))) &&
3346
3422
  iterations < maxIterations) {
3423
+ firstConditionParse = false;
3347
3424
  const beforePos = ctx.savePosition();
3348
3425
  conditionTokens.push(ctx.parseLogicalAnd());
3349
3426
  if (ctx.savePosition() === beforePos) {
@@ -3370,22 +3447,8 @@ function parseIfCommand(ctx, commandToken) {
3370
3447
  }
3371
3448
  args.push(condition);
3372
3449
  if (isMultiLine) {
3373
- if (hasThen) {
3374
- ctx.advance();
3375
- }
3376
- const thenCommands = [];
3377
- while (!ctx.isAtEnd() && !ctx.check(KEYWORDS.ELSE) && !ctx.check(KEYWORDS.END)) {
3378
- if (ctx.checkIsCommand() || ctx.isCommand(ctx.peek().value)) {
3379
- ctx.advance();
3380
- const cmd = ctx.parseCommand();
3381
- if (cmd) {
3382
- thenCommands.push(cmd);
3383
- }
3384
- }
3385
- else {
3386
- break;
3387
- }
3388
- }
3450
+ consumeOptionalKeyword(ctx, KEYWORDS.THEN);
3451
+ const thenCommands = parseIfBranchCommands(ctx, true);
3389
3452
  if (thenCommands.length === 0 && ctx.isAtEnd()) {
3390
3453
  throw new Error("Expected command after 'then' in if statement - incomplete conditional");
3391
3454
  }
@@ -3411,19 +3474,7 @@ function parseIfCommand(ctx, commandToken) {
3411
3474
  consumedElseIf = true;
3412
3475
  }
3413
3476
  else {
3414
- const elseCommands = [];
3415
- while (!ctx.isAtEnd() && !ctx.check(KEYWORDS.END)) {
3416
- if (ctx.checkIsCommand() || ctx.isCommand(ctx.peek().value)) {
3417
- ctx.advance();
3418
- const cmd = ctx.parseCommand();
3419
- if (cmd) {
3420
- elseCommands.push(cmd);
3421
- }
3422
- }
3423
- else {
3424
- break;
3425
- }
3426
- }
3477
+ const elseCommands = parseIfBranchCommands(ctx, false);
3427
3478
  args.push(createBlock(elseCommands, {
3428
3479
  start: commandToken.start,
3429
3480
  end: ctx.getPosition().end,
@@ -4245,21 +4296,6 @@ const GO_KEYWORDS = new Set([
4245
4296
  'back',
4246
4297
  'forward',
4247
4298
  ]);
4248
- const GO_URL_STOP = new Set([
4249
- 'in',
4250
- 'then',
4251
- 'and',
4252
- 'else',
4253
- 'end',
4254
- 'otherwise',
4255
- 'when',
4256
- 'where',
4257
- 'catch',
4258
- 'finally',
4259
- ]);
4260
- function isGoURLTerminator(ctx) {
4261
- return ctx.isAtEnd() || GO_URL_STOP.has(ctx.peek().value);
4262
- }
4263
4299
  function stringNode(value, tok) {
4264
4300
  return {
4265
4301
  type: 'string',
@@ -4270,16 +4306,6 @@ function stringNode(value, tok) {
4270
4306
  column: tok.column,
4271
4307
  };
4272
4308
  }
4273
- function isNakedURLStart(ctx) {
4274
- const tok = ctx.peek();
4275
- if (tok.value === '/')
4276
- return true;
4277
- if (ctx.checkIdentifierLike()) {
4278
- const next = ctx.peekAt(1);
4279
- return !!next && next.value === ':' && next.start === tok.end;
4280
- }
4281
- return false;
4282
- }
4283
4309
  function parseGoCommand(ctx, identifierNode) {
4284
4310
  const args = [];
4285
4311
  while (!isCommandBoundary(ctx, ['when', 'where', 'catch', 'finally'])) {
@@ -4291,7 +4317,7 @@ function parseGoCommand(ctx, identifierNode) {
4291
4317
  continue;
4292
4318
  }
4293
4319
  if (isNakedURLStart(ctx)) {
4294
- const url = parseBareURLPath(ctx, isGoURLTerminator);
4320
+ const url = parseBareURLPath(ctx);
4295
4321
  if (url) {
4296
4322
  args.push(url);
4297
4323
  continue;
@@ -4431,35 +4457,65 @@ function parseMultiWordCommand(ctx, commandToken, commandName) {
4431
4457
  }
4432
4458
  return builder.build();
4433
4459
  }
4434
- function isFetchURLTerminator(ctx) {
4435
- if (ctx.isAtEnd())
4460
+ function isNakedURLStart(ctx) {
4461
+ const tok = ctx.peek();
4462
+ if (tok.value === '/')
4436
4463
  return true;
4437
- const val = ctx.peek().value;
4438
- return (['as', 'with', '{', 'then', 'end', 'else', 'otherwise', ')'].includes(val) ||
4439
- isCommandBoundary(ctx));
4464
+ if (ctx.checkIdentifierLike()) {
4465
+ const next = ctx.peekAt(1);
4466
+ return !!next && next.value === ':' && next.start === tok.end;
4467
+ }
4468
+ return false;
4440
4469
  }
4441
- function parseBareURLPath(ctx, isTerminator = isFetchURLTerminator) {
4470
+ const URL_HARD_STOPS = new Set(['{', ')']);
4471
+ function parseBareURLPath(ctx) {
4442
4472
  const startPos = ctx.savePosition();
4473
+ const firstTok = ctx.peek();
4443
4474
  let path = '';
4444
- while (!ctx.isAtEnd() && !isTerminator(ctx)) {
4445
- path += ctx.advance().value;
4475
+ let prev = null;
4476
+ while (!ctx.isAtEnd()) {
4477
+ const tok = ctx.peek();
4478
+ if (prev && tok.start !== prev.end)
4479
+ break;
4480
+ if (tok.value === '$' && ctx.peekAt(1)?.value === '{') {
4481
+ ctx.advance();
4482
+ let last = ctx.advance();
4483
+ let depth = 1;
4484
+ while (!ctx.isAtEnd() && depth > 0) {
4485
+ last = ctx.advance();
4486
+ if (last.value === '{')
4487
+ depth++;
4488
+ else if (last.value === '}')
4489
+ depth--;
4490
+ }
4491
+ path += ctx.getInputSlice(tok.start, last.end);
4492
+ prev = last;
4493
+ continue;
4494
+ }
4495
+ if (URL_HARD_STOPS.has(tok.value))
4496
+ break;
4497
+ prev = ctx.advance();
4498
+ path += prev.value;
4446
4499
  }
4447
4500
  if (!path || path === '/') {
4448
4501
  ctx.restorePosition(startPos);
4449
4502
  return null;
4450
4503
  }
4451
- return {
4452
- type: 'literal',
4453
- value: path,
4454
- raw: path,
4455
- start: startPos,
4456
- end: ctx.savePosition(),
4504
+ const span = {
4505
+ start: firstTok.start,
4506
+ end: prev ? prev.end : firstTok.end,
4507
+ line: firstTok.line,
4508
+ column: firstTok.column,
4457
4509
  };
4510
+ if (path.includes('${')) {
4511
+ return { type: 'templateLiteral', value: path, ...span };
4512
+ }
4513
+ return { type: 'literal', value: path, raw: path, ...span };
4458
4514
  }
4459
4515
  function parseFetchCommand(ctx, commandToken) {
4460
4516
  const modifiers = {};
4461
4517
  let url = null;
4462
- if (!ctx.isAtEnd() && ctx.check('/')) {
4518
+ if (!ctx.isAtEnd() && isNakedURLStart(ctx)) {
4463
4519
  url = parseBareURLPath(ctx);
4464
4520
  }
4465
4521
  if (!url) {
@@ -4685,10 +4741,10 @@ function parseTellCommand(ctx, identifierNode) {
4685
4741
  catch {
4686
4742
  break;
4687
4743
  }
4688
- if (ctx.match(KEYWORDS.AND)) {
4744
+ if (ctx.match(KEYWORDS.AND) || ctx.match(KEYWORDS.THEN)) {
4689
4745
  continue;
4690
4746
  }
4691
- if (ctx.check(KEYWORDS.THEN) || ctx.check(KEYWORDS.ELSE) || ctx.check(KEYWORDS.END)) {
4747
+ if (ctx.check(KEYWORDS.ELSE) || ctx.check(KEYWORDS.END)) {
4692
4748
  break;
4693
4749
  }
4694
4750
  if (ctx.checkIsCommand()) {
@@ -4891,6 +4947,7 @@ class Parser {
4891
4947
  const result = this.parseInternal();
4892
4948
  if (this.errors.length > 0) {
4893
4949
  result.errors = [...this.errors];
4950
+ result.recovered = true;
4894
4951
  }
4895
4952
  return result;
4896
4953
  }
@@ -5025,26 +5082,26 @@ class Parser {
5025
5082
  const commandSequence = this.parseCommandSequence();
5026
5083
  if (commandSequence) {
5027
5084
  if (!this.isAtEnd() && this.check('on')) {
5028
- debug.parse('✅ PARSER: Found event handlers after command sequence, parsing as program');
5085
+ debug.parse('\u2705 PARSER: Found event handlers after command sequence, parsing as program');
5029
5086
  const statements = [commandSequence];
5030
- debug.parse(`✅ PARSER: Starting with ${statements.length} statement(s) from command sequence`);
5087
+ debug.parse(`\u2705 PARSER: Starting with ${statements.length} statement(s) from command sequence`);
5031
5088
  let eventHandlerCount = 0;
5032
5089
  while (!this.isAtEnd() && this.check('on')) {
5033
- debug.parse(`✅ PARSER: Parsing event handler #${eventHandlerCount + 1}, current token: ${this.peek().value}`);
5090
+ debug.parse(`\u2705 PARSER: Parsing event handler #${eventHandlerCount + 1}, current token: ${this.peek().value}`);
5034
5091
  this.advance();
5035
5092
  const eventHandler = this.parseEventHandler();
5036
- debug.parse(`✅ PARSER: parseEventHandler returned:`, eventHandler
5093
+ debug.parse(`\u2705 PARSER: parseEventHandler returned:`, eventHandler
5037
5094
  ? `type=${eventHandler.type}, event=${eventHandler.event}`
5038
5095
  : 'null');
5039
5096
  if (eventHandler) {
5040
5097
  statements.push(eventHandler);
5041
5098
  eventHandlerCount++;
5042
- debug.parse(`✅ PARSER: Added event handler, now have ${statements.length} statements total`);
5099
+ debug.parse(`\u2705 PARSER: Added event handler, now have ${statements.length} statements total`);
5043
5100
  }
5044
- debug.parse(`✅ PARSER: After parsing event handler, current token: ${this.isAtEnd() ? 'END' : this.peek().value}, isAtEnd=${this.isAtEnd()}, check('on')=${this.check('on')}`);
5101
+ debug.parse(`\u2705 PARSER: After parsing event handler, current token: ${this.isAtEnd() ? 'END' : this.peek().value}, isAtEnd=${this.isAtEnd()}, check('on')=${this.check('on')}`);
5045
5102
  if (!this.isAtEnd() && !this.check('on')) {
5046
5103
  if (!this.isAtEnd()) {
5047
- debug.parse(`⚠️ PARSER: Unexpected token after event handlers: ${this.peek().value}`);
5104
+ debug.parse(`\u26a0\ufe0f PARSER: Unexpected token after event handlers: ${this.peek().value}`);
5048
5105
  this.addError(`Unexpected token after event handlers: ${this.peek().value}`);
5049
5106
  return {
5050
5107
  success: false,
@@ -5053,11 +5110,11 @@ class Parser {
5053
5110
  error: this.error,
5054
5111
  };
5055
5112
  }
5056
- debug.parse(`✅ PARSER: No more event handlers, at end of input`);
5113
+ debug.parse(`\u2705 PARSER: No more event handlers, at end of input`);
5057
5114
  break;
5058
5115
  }
5059
5116
  }
5060
- debug.parse(`✅ PARSER: Finished parsing, creating Program node with ${statements.length} statements`);
5117
+ debug.parse(`\u2705 PARSER: Finished parsing, creating Program node with ${statements.length} statements`);
5061
5118
  if (this.error) {
5062
5119
  return {
5063
5120
  success: false,
@@ -5452,41 +5509,41 @@ class Parser {
5452
5509
  parseCommandListUntilTerminator(extraStops) {
5453
5510
  const commands = [];
5454
5511
  const isStop = () => this.check('end') || extraStops.some(s => this.check(s));
5455
- debug.parse('🔄 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
5512
+ debug.parse('\ud83d\udd04 parseCommandListUntilTerminator: Starting (extraStops:', extraStops.join(','), ')');
5456
5513
  while (!this.isAtEnd() && !isStop()) {
5457
- debug.parse('📍 Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
5514
+ debug.parse('\ud83d\udccd Loop iteration, current token:', this.peek().value, 'kind:', this.peek().kind);
5458
5515
  let parsedCommand = false;
5459
5516
  const isCommandToken = this.checkIsCommand();
5460
5517
  const isCommandIdentifier = !isCommandToken && this.checkIdentifier() && this.isCommand(this.peek().value);
5461
5518
  if (isCommandToken || isCommandIdentifier) {
5462
- debug.parse(isCommandToken ? '✅ Found COMMAND token:' : '✅ Found IDENTIFIER that is a command:', this.peek().value);
5519
+ debug.parse(isCommandToken ? '\u2705 Found COMMAND token:' : '\u2705 Found IDENTIFIER that is a command:', this.peek().value);
5463
5520
  this.advance();
5464
5521
  const savedError = this.error;
5465
5522
  try {
5466
5523
  const cmd = this.parseCommand();
5467
5524
  if (this.error && this.error !== savedError) {
5468
- debug.parse('⚠️ parseCommandListUntilTerminator: Command parsing added error, restoring. Error was:', this.error.message);
5525
+ debug.parse('\u26a0\ufe0f parseCommandListUntilTerminator: Command parsing added error, restoring. Error was:', this.error.message);
5469
5526
  this.error = savedError;
5470
5527
  }
5471
5528
  if (cmd) {
5472
- debug.parse('✅ Parsed command:', cmd.name);
5529
+ debug.parse('\u2705 Parsed command:', cmd.name);
5473
5530
  commands.push(cmd);
5474
5531
  parsedCommand = true;
5475
5532
  }
5476
5533
  }
5477
5534
  catch (error) {
5478
- debug.parse('⚠️ parseCommandListUntilTerminator: Command parsing threw, restoring:', error instanceof Error ? error.message : String(error));
5535
+ debug.parse('\u26a0\ufe0f parseCommandListUntilTerminator: Command parsing threw, restoring:', error instanceof Error ? error.message : String(error));
5479
5536
  this.error = savedError;
5480
5537
  }
5481
5538
  }
5482
5539
  else if (this.checkIdentifier()) {
5483
- debug.parse('❌ IDENTIFIER is not a command:', this.peek().value);
5540
+ debug.parse('\u274c IDENTIFIER is not a command:', this.peek().value);
5484
5541
  }
5485
5542
  if (!parsedCommand) {
5486
- debug.parse('❌ No command parsed, breaking. Current token:', this.peek().value);
5543
+ debug.parse('\u274c No command parsed, breaking. Current token:', this.peek().value);
5487
5544
  break;
5488
5545
  }
5489
- debug.parse('📍 After parsing command, current token:', this.peek().value);
5546
+ debug.parse('\ud83d\udccd After parsing command, current token:', this.peek().value);
5490
5547
  while (!this.isAtEnd() &&
5491
5548
  !isStop() &&
5492
5549
  !this.checkIsCommand() &&
@@ -5494,19 +5551,19 @@ class Parser {
5494
5551
  !this.check('then') &&
5495
5552
  !this.check('and') &&
5496
5553
  !this.check(',')) {
5497
- debug.parse('⚠️ Skipping unexpected token:', this.peek().value);
5554
+ debug.parse('\u26a0\ufe0f Skipping unexpected token:', this.peek().value);
5498
5555
  this.advance();
5499
5556
  }
5500
5557
  if (this.match('then', 'and', ',')) {
5501
- debug.parse('✅ Found separator, continuing');
5558
+ debug.parse('\u2705 Found separator, continuing');
5502
5559
  continue;
5503
5560
  }
5504
5561
  else if (this.checkIsCommand() || this.isCommand(this.peek().value)) {
5505
- debug.parse('✅ Next token is a command, continuing without separator');
5562
+ debug.parse('\u2705 Next token is a command, continuing without separator');
5506
5563
  continue;
5507
5564
  }
5508
5565
  else {
5509
- debug.parse('📍 No separator and no command, breaking. Current token:', this.peek().value);
5566
+ debug.parse('\ud83d\udccd No separator and no command, breaking. Current token:', this.peek().value);
5510
5567
  break;
5511
5568
  }
5512
5569
  }
@@ -5520,18 +5577,18 @@ class Parser {
5520
5577
  if (!this.check('end') && !extraStops.some(s => this.check(s))) {
5521
5578
  terminator = '';
5522
5579
  }
5523
- debug.parse('✅ parseCommandListUntilTerminator: parsed', commands.length, 'commands (terminator:', terminator, ')');
5580
+ debug.parse('\u2705 parseCommandListUntilTerminator: parsed', commands.length, 'commands (terminator:', terminator, ')');
5524
5581
  return { commands, terminator };
5525
5582
  }
5526
5583
  parseCommandListUntilEnd() {
5527
5584
  const { commands } = this.parseCommandListUntilTerminator([]);
5528
- debug.parse('🔍 After loop, checking for "end". Current token:', this.peek().value);
5585
+ debug.parse('\ud83d\udd0d After loop, checking for "end". Current token:', this.peek().value);
5529
5586
  if (this.check('end')) {
5530
- debug.parse('✅ Found "end", consuming it');
5587
+ debug.parse('\u2705 Found "end", consuming it');
5531
5588
  this.advance();
5532
5589
  }
5533
5590
  else {
5534
- debug.parse('❌ ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
5591
+ debug.parse('\u274c ERROR: Expected "end" but got:', this.peek().value, 'at position:', this.peek().start);
5535
5592
  throw new Error('Expected "end" to close repeat block');
5536
5593
  }
5537
5594
  return commands;
@@ -6204,6 +6261,23 @@ class Parser {
6204
6261
  this.consume(')', "Expected ')' after parameter list");
6205
6262
  }
6206
6263
  const bodyCommands = this.parseCommandBlock(['end', 'catch', 'finally']);
6264
+ const { errorSymbol, errorHandler, finallyHandler } = this.parseErrorAndFinallyBlocks();
6265
+ this.consume('end', "Expected 'end' after function definition");
6266
+ return {
6267
+ type: 'def',
6268
+ name: funcName,
6269
+ params,
6270
+ body: bodyCommands,
6271
+ ...(errorSymbol !== undefined && { errorSymbol }),
6272
+ ...(errorHandler !== undefined && { errorHandler }),
6273
+ ...(finallyHandler !== undefined && { finallyHandler }),
6274
+ start: pos.start,
6275
+ end: this.getPosition().end,
6276
+ line: pos.line,
6277
+ column: pos.column,
6278
+ };
6279
+ }
6280
+ parseErrorAndFinallyBlocks() {
6207
6281
  let errorSymbol;
6208
6282
  let errorHandler;
6209
6283
  if (this.check('catch')) {
@@ -6218,19 +6292,10 @@ class Parser {
6218
6292
  this.advance();
6219
6293
  finallyHandler = this.parseCommandBlock(['end']);
6220
6294
  }
6221
- this.consume('end', "Expected 'end' after function definition");
6222
6295
  return {
6223
- type: 'def',
6224
- name: funcName,
6225
- params,
6226
- body: bodyCommands,
6227
6296
  ...(errorSymbol !== undefined && { errorSymbol }),
6228
6297
  ...(errorHandler !== undefined && { errorHandler }),
6229
6298
  ...(finallyHandler !== undefined && { finallyHandler }),
6230
- start: pos.start,
6231
- end: this.getPosition().end,
6232
- line: pos.line,
6233
- column: pos.column,
6234
6299
  };
6235
6300
  }
6236
6301
  parseEventNameWithNamespace(errorMessage) {
@@ -6375,13 +6440,13 @@ class Parser {
6375
6440
  try {
6376
6441
  const cmd = this.parseCommand();
6377
6442
  if (this.error && this.error !== savedError) {
6378
- debug.parse('⚠️ Command parsing added error, restoring error state. Error was:', this.error.message);
6443
+ debug.parse('\u26a0\ufe0f Command parsing added error, restoring error state. Error was:', this.error.message);
6379
6444
  this.error = savedError;
6380
6445
  }
6381
6446
  return cmd;
6382
6447
  }
6383
6448
  catch (error) {
6384
- debug.parse('⚠️ Command parsing threw exception, restoring error state:', error instanceof Error ? error.message : String(error));
6449
+ debug.parse('\u26a0\ufe0f Command parsing threw exception, restoring error state:', error instanceof Error ? error.message : String(error));
6385
6450
  this.error = savedError;
6386
6451
  return null;
6387
6452
  }
@@ -6398,12 +6463,12 @@ class Parser {
6398
6463
  const timeToken = this.advance();
6399
6464
  const delayMs = parseTimeToMs(timeToken.value);
6400
6465
  modifiers[modifierKey] = delayMs;
6401
- debug.parse(`🔧 parseEventHandler: Parsed '${modName} at ${timeToken.value}' (${delayMs}ms)`);
6466
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed '${modName} at ${timeToken.value}' (${delayMs}ms)`);
6402
6467
  }
6403
6468
  else if (this.matchNumber()) {
6404
6469
  const delayMs = parseInt(this.previous().value, 10);
6405
6470
  modifiers[modifierKey] = delayMs;
6406
- debug.parse(`🔧 parseEventHandler: Parsed '${modName} at ${delayMs}'`);
6471
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed '${modName} at ${delayMs}'`);
6407
6472
  }
6408
6473
  else {
6409
6474
  this.addError(`Expected time expression after '${modName} at', got: ${this.peek().value}`);
@@ -6414,7 +6479,7 @@ class Parser {
6414
6479
  }
6415
6480
  }
6416
6481
  parseEventHandler() {
6417
- debug.parse(`🔧 parseEventHandler: ENTRY - parsing event handler`);
6482
+ debug.parse(`\ud83d\udd27 parseEventHandler: ENTRY - parsing event handler`);
6418
6483
  const eventNames = [];
6419
6484
  let firstOnceAlias = false;
6420
6485
  if (this.check('first') && !this.checkIsCommand() && this.current + 1 < this.tokens.length) {
@@ -6423,27 +6488,27 @@ class Parser {
6423
6488
  if (peek2Value && peek2Value !== 'of' && peek2Value !== 'in' && peek2Value !== 'from') {
6424
6489
  this.advance();
6425
6490
  firstOnceAlias = true;
6426
- debug.parse(`🔧 parseEventHandler: Parsed 'first' as .once alias`);
6491
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'first' as .once alias`);
6427
6492
  }
6428
6493
  }
6429
6494
  const event = this.parseEventNameWithNamespace("Expected event name after 'on'");
6430
6495
  eventNames.push(event);
6431
- debug.parse(`🔧 parseEventHandler: Parsed first event name: ${event}`);
6496
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed first event name: ${event}`);
6432
6497
  while (this.check('or')) {
6433
6498
  this.advance();
6434
- debug.parse(`🔧 parseEventHandler: Found 'or', parsing additional event name`);
6499
+ debug.parse(`\ud83d\udd27 parseEventHandler: Found 'or', parsing additional event name`);
6435
6500
  const additionalEvent = this.parseEventNameWithNamespace("Expected event name after 'or'");
6436
6501
  eventNames.push(additionalEvent);
6437
- debug.parse(`🔧 parseEventHandler: Parsed additional event name: ${additionalEvent}`);
6502
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed additional event name: ${additionalEvent}`);
6438
6503
  }
6439
- debug.parse(`🔧 parseEventHandler: Total events parsed: ${eventNames.join(', ')}`);
6504
+ debug.parse(`\ud83d\udd27 parseEventHandler: Total events parsed: ${eventNames.join(', ')}`);
6440
6505
  let customEventSource;
6441
6506
  if (this.registryIntegration) {
6442
6507
  const firstEvent = eventNames[0];
6443
6508
  if (this.registryIntegration.hasEventSource(firstEvent)) {
6444
6509
  const source = this.registryIntegration.getEventSource(firstEvent);
6445
6510
  customEventSource = source?.name;
6446
- debug.parse(`🔧 parseEventHandler: Detected custom event source '${customEventSource}' for event '${firstEvent}'`);
6511
+ debug.parse(`\ud83d\udd27 parseEventHandler: Detected custom event source '${customEventSource}' for event '${firstEvent}'`);
6447
6512
  }
6448
6513
  }
6449
6514
  const eventParams = [];
@@ -6455,7 +6520,7 @@ class Parser {
6455
6520
  } while (this.match(','));
6456
6521
  }
6457
6522
  this.consume(')', "Expected ')' after event parameters");
6458
- debug.parse(`🔧 parseEventHandler: Parsed event parameters: ${eventParams.join(', ')}`);
6523
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed event parameters: ${eventParams.join(', ')}`);
6459
6524
  }
6460
6525
  const modifiers = {};
6461
6526
  while (this.check('.')) {
@@ -6464,15 +6529,15 @@ class Parser {
6464
6529
  const modName = modToken.value.toLowerCase();
6465
6530
  if (modName === 'once') {
6466
6531
  modifiers.once = true;
6467
- debug.parse(`🔧 parseEventHandler: Parsed modifier '.once'`);
6532
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed modifier '.once'`);
6468
6533
  }
6469
6534
  else if (modName === 'prevent') {
6470
6535
  modifiers.prevent = true;
6471
- debug.parse(`🔧 parseEventHandler: Parsed modifier '.prevent'`);
6536
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed modifier '.prevent'`);
6472
6537
  }
6473
6538
  else if (modName === 'stop') {
6474
6539
  modifiers.stop = true;
6475
- debug.parse(`🔧 parseEventHandler: Parsed modifier '.stop'`);
6540
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed modifier '.stop'`);
6476
6541
  }
6477
6542
  else if (modName === 'debounce' || modName === 'throttle') {
6478
6543
  if (this.check('(')) {
@@ -6484,11 +6549,11 @@ class Parser {
6484
6549
  }
6485
6550
  if (modName === 'debounce') {
6486
6551
  modifiers.debounce = delayMs;
6487
- debug.parse(`🔧 parseEventHandler: Parsed modifier '.debounce(${delayMs})'`);
6552
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed modifier '.debounce(${delayMs})'`);
6488
6553
  }
6489
6554
  else {
6490
6555
  modifiers.throttle = delayMs;
6491
- debug.parse(`🔧 parseEventHandler: Parsed modifier '.throttle(${delayMs})'`);
6556
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed modifier '.throttle(${delayMs})'`);
6492
6557
  }
6493
6558
  this.consume(')', `Expected ')' after ${modName} delay`);
6494
6559
  }
@@ -6497,7 +6562,7 @@ class Parser {
6497
6562
  }
6498
6563
  }
6499
6564
  else {
6500
- debug.parse(`🔧 parseEventHandler: Warning - unknown modifier '.${modName}'`);
6565
+ debug.parse(`\ud83d\udd27 parseEventHandler: Warning - unknown modifier '.${modName}'`);
6501
6566
  }
6502
6567
  }
6503
6568
  this.parseTemporalKeywordModifiers(modifiers);
@@ -6505,7 +6570,7 @@ class Parser {
6505
6570
  modifiers.once = true;
6506
6571
  }
6507
6572
  if (Object.keys(modifiers).length > 0) {
6508
- debug.parse(`🔧 parseEventHandler: Parsed modifiers:`, modifiers);
6573
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed modifiers:`, modifiers);
6509
6574
  }
6510
6575
  let condition;
6511
6576
  if (this.match('[')) {
@@ -6516,7 +6581,7 @@ class Parser {
6516
6581
  if (this.match('from')) {
6517
6582
  const targetToken = this.advance();
6518
6583
  target = targetToken.value;
6519
- debug.parse(`🔧 parseEventHandler: Parsed 'from' target: ${target} (kind: ${targetToken.kind})`);
6584
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed 'from' target: ${target} (kind: ${targetToken.kind})`);
6520
6585
  this.parseTemporalKeywordModifiers(modifiers);
6521
6586
  }
6522
6587
  let attributeName;
@@ -6528,32 +6593,36 @@ class Parser {
6528
6593
  else {
6529
6594
  attributeName = attrToken.value;
6530
6595
  }
6531
- debug.parse(`🔧 parseEventHandler: Parsed attribute name: ${attributeName}`);
6596
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed attribute name: ${attributeName}`);
6532
6597
  }
6533
6598
  let watchTarget;
6534
6599
  if (this.match('in')) {
6535
- debug.parse(`🔧 parseEventHandler: Found 'in' keyword, parsing watch target`);
6600
+ debug.parse(`\ud83d\udd27 parseEventHandler: Found 'in' keyword, parsing watch target`);
6536
6601
  watchTarget = this.parseExpression();
6537
- debug.parse(`🔧 parseEventHandler: Parsed watch target expression`);
6602
+ debug.parse(`\ud83d\udd27 parseEventHandler: Parsed watch target expression`);
6538
6603
  }
6539
6604
  const commands = [];
6540
- debug.parse(`✅ parseEventHandler: About to parse commands, current token: ${this.isAtEnd() ? 'END' : this.peek().value}, isAtEnd: ${this.isAtEnd()}`);
6605
+ debug.parse(`\u2705 parseEventHandler: About to parse commands, current token: ${this.isAtEnd() ? 'END' : this.peek().value}, isAtEnd: ${this.isAtEnd()}`);
6541
6606
  while (!this.isAtEnd()) {
6542
- debug.parse(`✅ parseEventHandler: Loop iteration, current token: ${this.peek().value}, kind: ${this.peek().kind}`);
6607
+ debug.parse(`\u2705 parseEventHandler: Loop iteration, current token: ${this.peek().value}, kind: ${this.peek().kind}`);
6543
6608
  if (this.check('on')) {
6544
- debug.parse(`✅ parseEventHandler: Stopping command parsing, found next event handler 'on'`);
6609
+ debug.parse(`\u2705 parseEventHandler: Stopping command parsing, found next event handler 'on'`);
6545
6610
  break;
6546
6611
  }
6547
6612
  if (this.checkComment()) {
6548
- debug.parse(`✅ parseEventHandler: Skipping comment token: ${this.peek().value}`);
6613
+ debug.parse(`\u2705 parseEventHandler: Skipping comment token: ${this.peek().value}`);
6549
6614
  this.advance();
6550
6615
  continue;
6551
6616
  }
6552
6617
  if (this.check('end')) {
6553
- debug.parse(`✅ parseEventHandler: Stopping command parsing, found 'end' keyword`);
6618
+ debug.parse(`\u2705 parseEventHandler: Stopping command parsing, found 'end' keyword`);
6554
6619
  this.advance();
6555
6620
  break;
6556
6621
  }
6622
+ if (this.check('catch') || this.check('finally')) {
6623
+ debug.parse(`\u2705 parseEventHandler: Stopping command parsing, found '${this.peek().value}'`);
6624
+ break;
6625
+ }
6557
6626
  if (this.checkIsCommand()) {
6558
6627
  const nextIsOpenParen = this.tokens[this.current + 1]?.value === '(';
6559
6628
  const commandName = this.peek().value.toLowerCase();
@@ -6582,7 +6651,7 @@ class Parser {
6582
6651
  const cmd = this.parseCommandWithErrorRecovery();
6583
6652
  if (cmd) {
6584
6653
  commands.push(cmd);
6585
- debug.parse(`✅ parseEventHandler: Parsed command, next token: ${this.isAtEnd() ? 'END' : this.peek().value}`);
6654
+ debug.parse(`\u2705 parseEventHandler: Parsed command, next token: ${this.isAtEnd() ? 'END' : this.peek().value}`);
6586
6655
  }
6587
6656
  }
6588
6657
  }
@@ -6600,7 +6669,7 @@ class Parser {
6600
6669
  expr = this.parseExpression();
6601
6670
  }
6602
6671
  catch (error) {
6603
- debug.parse('⚠️ Expression parsing error, restoring error state:', error instanceof Error ? error.message : String(error));
6672
+ debug.parse('\u26a0\ufe0f Expression parsing error, restoring error state:', error instanceof Error ? error.message : String(error));
6604
6673
  this.error = savedError;
6605
6674
  break;
6606
6675
  }
@@ -6647,7 +6716,9 @@ class Parser {
6647
6716
  !this.check('then') &&
6648
6717
  !this.check('and') &&
6649
6718
  !this.check(',') &&
6650
- !this.check('on')) {
6719
+ !this.check('on') &&
6720
+ !this.check('catch') &&
6721
+ !this.check('finally')) {
6651
6722
  this.advance();
6652
6723
  }
6653
6724
  if (this.match('then', 'and', ',')) {
@@ -6660,12 +6731,24 @@ class Parser {
6660
6731
  break;
6661
6732
  }
6662
6733
  }
6734
+ let errorSymbol;
6735
+ let errorHandler;
6736
+ let finallyHandler;
6737
+ if (!this.isAtEnd() && (this.check('catch') || this.check('finally'))) {
6738
+ ({ errorSymbol, errorHandler, finallyHandler } = this.parseErrorAndFinallyBlocks());
6739
+ if (this.check('end')) {
6740
+ this.advance();
6741
+ }
6742
+ }
6663
6743
  const pos = this.getPosition();
6664
6744
  const node = {
6665
6745
  type: 'eventHandler',
6666
6746
  event: eventNames.length === 1 ? eventNames[0] : eventNames.join('|'),
6667
6747
  events: eventNames,
6668
6748
  commands,
6749
+ ...(errorSymbol !== undefined && { errorSymbol }),
6750
+ ...(errorHandler !== undefined && { errorHandler }),
6751
+ ...(finallyHandler !== undefined && { finallyHandler }),
6669
6752
  ...(eventParams.length > 0 && { args: eventParams }),
6670
6753
  ...(condition && { condition }),
6671
6754
  ...(target && { target }),
@@ -6678,7 +6761,7 @@ class Parser {
6678
6761
  line: pos.line,
6679
6762
  column: pos.column,
6680
6763
  };
6681
- debug.parse(`🔧 parseEventHandler: Created node with events: ${eventNames.join(', ')}, attributeName: ${attributeName || 'none'}, watchTarget: ${watchTarget ? 'yes' : 'none'}`);
6764
+ debug.parse(`\ud83d\udd27 parseEventHandler: Created node with events: ${eventNames.join(', ')}, attributeName: ${attributeName || 'none'}, watchTarget: ${watchTarget ? 'yes' : 'none'}`);
6682
6765
  return node;
6683
6766
  }
6684
6767
  parseBehaviorDefinition() {
@@ -6743,11 +6826,33 @@ class Parser {
6743
6826
  }
6744
6827
  }
6745
6828
  }
6746
- const handlerCommands = this.parseCommandListUntilEnd();
6829
+ const { commands: handlerCommands, terminator } = this.parseCommandListUntilTerminator([
6830
+ 'catch',
6831
+ 'finally',
6832
+ ]);
6833
+ let handlerErrorSymbol;
6834
+ let handlerErrorHandler;
6835
+ let handlerFinallyHandler;
6836
+ if (terminator === 'catch' || terminator === 'finally') {
6837
+ ({
6838
+ errorSymbol: handlerErrorSymbol,
6839
+ errorHandler: handlerErrorHandler,
6840
+ finallyHandler: handlerFinallyHandler,
6841
+ } = this.parseErrorAndFinallyBlocks());
6842
+ }
6843
+ if (this.check('end')) {
6844
+ this.advance();
6845
+ }
6846
+ else {
6847
+ throw new Error('Expected "end" to close behavior event handler');
6848
+ }
6747
6849
  const handlerNode = {
6748
6850
  type: 'eventHandler',
6749
6851
  event: eventName,
6750
6852
  commands: handlerCommands,
6853
+ ...(handlerErrorSymbol !== undefined && { errorSymbol: handlerErrorSymbol }),
6854
+ ...(handlerErrorHandler !== undefined && { errorHandler: handlerErrorHandler }),
6855
+ ...(handlerFinallyHandler !== undefined && { finallyHandler: handlerFinallyHandler }),
6751
6856
  ...(eventSource !== undefined && { target: eventSource }),
6752
6857
  ...(eventArgs.length > 0 && { args: eventArgs }),
6753
6858
  start: handlerPos.start,
@@ -6802,7 +6907,7 @@ class Parser {
6802
6907
  this.error.message.includes('unclosed parenthes') ||
6803
6908
  this.error.message.includes('Unclosed parenthes');
6804
6909
  if (!isCriticalError) {
6805
- debug.parse('⚠️ parseCommandSequence: Command parsing added non-critical error, restoring error state. Error was:', this.error.message);
6910
+ debug.parse('\u26a0\ufe0f parseCommandSequence: Command parsing added non-critical error, restoring error state. Error was:', this.error.message);
6806
6911
  this.error = savedError;
6807
6912
  }
6808
6913
  }
@@ -6812,11 +6917,11 @@ class Parser {
6812
6917
  !this.isCommand(this.peek().value) &&
6813
6918
  !this.check('then') &&
6814
6919
  !this.check('on')) {
6815
- debug.parse('⚠️ parseCommandSequence: Skipping unexpected token:', this.peek().value);
6920
+ debug.parse('\u26a0\ufe0f parseCommandSequence: Skipping unexpected token:', this.peek().value);
6816
6921
  this.advance();
6817
6922
  }
6818
6923
  if (this.check('on')) {
6819
- debug.parse('✅ parseCommandSequence: Found "on" token, stopping command sequence to allow event handler parsing');
6924
+ debug.parse('\u2705 parseCommandSequence: Found "on" token, stopping command sequence to allow event handler parsing');
6820
6925
  break;
6821
6926
  }
6822
6927
  if (this.match('then')) {
@@ -6874,6 +6979,9 @@ class Parser {
6874
6979
  this.advance();
6875
6980
  const cmd = this.parseCommand();
6876
6981
  commands.push(cmd);
6982
+ if (this.match('then', 'and', ',')) {
6983
+ continue;
6984
+ }
6877
6985
  }
6878
6986
  else {
6879
6987
  break;
@@ -7260,7 +7368,7 @@ class Parser {
7260
7368
  if (!this.check(')') && callee.type === 'identifier') {
7261
7369
  const funcName = callee.name;
7262
7370
  if (CommandClassification.isCSSFunction(funcName)) {
7263
- debug.parse(`💡 Tip: CSS functions like ${funcName}() should be quoted for clean parsing. ` +
7371
+ debug.parse(`\ud83d\udca1 Tip: CSS functions like ${funcName}() should be quoted for clean parsing. ` +
7264
7372
  `Use '${funcName}(...)' or \`${funcName}(...)\` instead.`);
7265
7373
  }
7266
7374
  }
@@ -7562,7 +7670,7 @@ class Parser {
7562
7670
  if (this.checkIsCommand() || (this.isCommand(token.value) && !this.isKeyword(token.value))) {
7563
7671
  return;
7564
7672
  }
7565
- debug.parse('⚠️ synchronize: Skipping token:', token.value);
7673
+ debug.parse('\u26a0\ufe0f synchronize: Skipping token:', token.value);
7566
7674
  this.advance();
7567
7675
  }
7568
7676
  }