@lokascript/core 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1158 @@
1
+ /**
2
+ * Parser Templates for Bundle Generation
3
+ *
4
+ * These templates contain self-contained parser code that can be embedded
5
+ * directly into generated bundles, eliminating external parser imports.
6
+ *
7
+ * LITE_PARSER_TEMPLATE: Regex-based parser (~200 lines, ~2 KB gzipped)
8
+ * - Supports: event handlers, command sequences, simple conditions
9
+ * - Commands: toggle, add, remove, put, set, log, send, wait, show/hide
10
+ *
11
+ * HYBRID_PARSER_TEMPLATE: Full AST parser (~1100 lines, ~7 KB gzipped)
12
+ * - Supports: blocks (if/repeat/for/while/fetch), positional expressions
13
+ * - Full operator precedence, function calls, object/array literals
14
+ */
15
+
16
+ // =============================================================================
17
+ // LITE PARSER TEMPLATE
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Minimal regex-based parser for simple hyperscript patterns.
22
+ * Use when only basic commands are detected (no blocks, no expressions).
23
+ */
24
+ export const LITE_PARSER_TEMPLATE = `
25
+ // Lite Parser - Regex-based for minimal bundle size
26
+
27
+ function parseLite(code) {
28
+ const trimmed = code.trim();
29
+
30
+ // Handle event handlers: "on click toggle .active"
31
+ const onMatch = trimmed.match(/^on\\s+(\\w+)(?:\\s+from\\s+([^\\s]+))?\\s+(.+)$/i);
32
+ if (onMatch) {
33
+ return {
34
+ type: 'event',
35
+ event: onMatch[1],
36
+ filter: onMatch[2] ? { type: 'selector', value: onMatch[2] } : undefined,
37
+ modifiers: {},
38
+ body: parseCommands(onMatch[3]),
39
+ };
40
+ }
41
+
42
+ // Handle "every Nms" event pattern
43
+ const everyMatch = trimmed.match(/^every\\s+(\\d+)(ms|s)?\\s+(.+)$/i);
44
+ if (everyMatch) {
45
+ const ms = everyMatch[2] === 's' ? parseInt(everyMatch[1]) * 1000 : parseInt(everyMatch[1]);
46
+ return {
47
+ type: 'event',
48
+ event: 'interval:' + ms,
49
+ modifiers: {},
50
+ body: parseCommands(everyMatch[3]),
51
+ };
52
+ }
53
+
54
+ // Handle "init" pattern
55
+ const initMatch = trimmed.match(/^init\\s+(.+)$/i);
56
+ if (initMatch) {
57
+ return {
58
+ type: 'event',
59
+ event: 'init',
60
+ modifiers: {},
61
+ body: parseCommands(initMatch[1]),
62
+ };
63
+ }
64
+
65
+ return { type: 'sequence', commands: parseCommands(trimmed) };
66
+ }
67
+
68
+ function parseCommands(code) {
69
+ const parts = code.split(/\\s+(?:then|and)\\s+/i);
70
+ return parts.map(parseCommand).filter(Boolean);
71
+ }
72
+
73
+ function parseCommand(code) {
74
+ const trimmed = code.trim();
75
+ if (!trimmed) return null;
76
+
77
+ let match;
78
+
79
+ // toggle .class [on target]
80
+ match = trimmed.match(/^toggle\\s+(\\.\\w+|\\w+)(?:\\s+on\\s+(.+))?$/i);
81
+ if (match) {
82
+ return {
83
+ type: 'command',
84
+ name: 'toggle',
85
+ args: [{ type: 'selector', value: match[1] }],
86
+ target: match[2] ? parseTarget(match[2]) : undefined,
87
+ };
88
+ }
89
+
90
+ // add .class [to target]
91
+ match = trimmed.match(/^add\\s+(\\.\\w+|\\w+)(?:\\s+to\\s+(.+))?$/i);
92
+ if (match) {
93
+ return {
94
+ type: 'command',
95
+ name: 'add',
96
+ args: [{ type: 'selector', value: match[1] }],
97
+ target: match[2] ? parseTarget(match[2]) : undefined,
98
+ };
99
+ }
100
+
101
+ // remove .class [from target] | remove [target]
102
+ match = trimmed.match(/^remove\\s+(\\.\\w+)(?:\\s+from\\s+(.+))?$/i);
103
+ if (match) {
104
+ return {
105
+ type: 'command',
106
+ name: 'removeClass',
107
+ args: [{ type: 'selector', value: match[1] }],
108
+ target: match[2] ? parseTarget(match[2]) : undefined,
109
+ };
110
+ }
111
+ match = trimmed.match(/^remove\\s+(.+)$/i);
112
+ if (match) {
113
+ return {
114
+ type: 'command',
115
+ name: 'remove',
116
+ args: [],
117
+ target: parseTarget(match[1]),
118
+ };
119
+ }
120
+
121
+ // put "content" into target
122
+ match = trimmed.match(/^put\\s+(?:"([^"]+)"|'([^']+)'|(\\S+))\\s+(into|before|after)\\s+(.+)$/i);
123
+ if (match) {
124
+ const content = match[1] || match[2] || match[3];
125
+ return {
126
+ type: 'command',
127
+ name: 'put',
128
+ args: [{ type: 'literal', value: content }],
129
+ modifier: match[4],
130
+ target: parseTarget(match[5]),
131
+ };
132
+ }
133
+
134
+ // set target to value | set :var to value
135
+ match = trimmed.match(/^set\\s+(.+?)\\s+to\\s+(.+)$/i);
136
+ if (match) {
137
+ return {
138
+ type: 'command',
139
+ name: 'set',
140
+ args: [parseTarget(match[1]), parseLiteValue(match[2])],
141
+ };
142
+ }
143
+
144
+ // log message
145
+ match = trimmed.match(/^log\\s+(.+)$/i);
146
+ if (match) {
147
+ return {
148
+ type: 'command',
149
+ name: 'log',
150
+ args: [parseLiteValue(match[1])],
151
+ };
152
+ }
153
+
154
+ // send event [to target]
155
+ match = trimmed.match(/^send\\s+(\\w+)(?:\\s+to\\s+(.+))?$/i);
156
+ if (match) {
157
+ return {
158
+ type: 'command',
159
+ name: 'send',
160
+ args: [{ type: 'literal', value: match[1] }],
161
+ target: match[2] ? parseTarget(match[2]) : undefined,
162
+ };
163
+ }
164
+
165
+ // wait Nms | wait Ns
166
+ match = trimmed.match(/^wait\\s+(\\d+)(ms|s)?$/i);
167
+ if (match) {
168
+ const ms = match[2] === 's' ? parseInt(match[1]) * 1000 : parseInt(match[1]);
169
+ return {
170
+ type: 'command',
171
+ name: 'wait',
172
+ args: [{ type: 'literal', value: ms }],
173
+ };
174
+ }
175
+
176
+ // show/hide shortcuts
177
+ match = trimmed.match(/^(show|hide)(?:\\s+(.+))?$/i);
178
+ if (match) {
179
+ return {
180
+ type: 'command',
181
+ name: match[1].toLowerCase(),
182
+ args: [],
183
+ target: match[2] ? parseTarget(match[2]) : undefined,
184
+ };
185
+ }
186
+
187
+ // Unknown command - try generic parsing
188
+ const parts = trimmed.split(/\\s+/);
189
+ if (parts.length > 0) {
190
+ return {
191
+ type: 'command',
192
+ name: parts[0],
193
+ args: parts.slice(1).map(p => ({ type: 'literal', value: p })),
194
+ };
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ function parseTarget(str) {
201
+ const s = str.trim();
202
+ if (s === 'me') return { type: 'identifier', value: 'me' };
203
+ if (s === 'body') return { type: 'identifier', value: 'body' };
204
+ if (s.startsWith('#') || s.startsWith('.') || s.startsWith('[')) {
205
+ return { type: 'selector', value: s };
206
+ }
207
+ if (s.startsWith(':')) {
208
+ return { type: 'variable', name: s, scope: 'local' };
209
+ }
210
+ return { type: 'identifier', value: s };
211
+ }
212
+
213
+ function parseLiteValue(str) {
214
+ const s = str.trim();
215
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
216
+ return { type: 'literal', value: s.slice(1, -1) };
217
+ }
218
+ if (/^-?\\d+(\\.\\d+)?$/.test(s)) {
219
+ return { type: 'literal', value: parseFloat(s) };
220
+ }
221
+ if (s === 'true') return { type: 'literal', value: true };
222
+ if (s === 'false') return { type: 'literal', value: false };
223
+ if (s === 'null') return { type: 'literal', value: null };
224
+ if (s.startsWith(':')) return { type: 'variable', name: s, scope: 'local' };
225
+ if (s === 'me') return { type: 'identifier', value: 'me' };
226
+ return { type: 'identifier', value: s };
227
+ }
228
+ `;
229
+
230
+ // =============================================================================
231
+ // HYBRID PARSER TEMPLATE
232
+ // =============================================================================
233
+
234
+ /**
235
+ * Full recursive descent parser with operator precedence.
236
+ * Use when blocks, positional expressions, or complex features are detected.
237
+ */
238
+ export const HYBRID_PARSER_TEMPLATE = `
239
+ // Hybrid Parser - Full AST with operator precedence
240
+
241
+ // Tokenizer
242
+ const KEYWORDS = new Set([
243
+ 'on', 'from', 'to', 'into', 'before', 'after', 'in', 'of', 'at', 'with',
244
+ 'if', 'else', 'unless', 'end', 'then', 'and', 'or', 'not',
245
+ 'repeat', 'times', 'for', 'each', 'while', 'until',
246
+ 'toggle', 'add', 'remove', 'put', 'set', 'get', 'call', 'return', 'append',
247
+ 'log', 'send', 'trigger', 'wait', 'settle', 'fetch', 'as',
248
+ 'show', 'hide', 'take', 'increment', 'decrement', 'focus', 'blur', 'go', 'transition', 'over',
249
+ 'the', 'a', 'an', 'my', 'its', 'me', 'it', 'you',
250
+ 'first', 'last', 'next', 'previous', 'closest', 'parent',
251
+ 'true', 'false', 'null', 'undefined',
252
+ 'is', 'matches', 'contains', 'includes', 'exists', 'has', 'init', 'every', 'by',
253
+ ]);
254
+
255
+ const COMMAND_ALIASES = {
256
+ flip: 'toggle', switch: 'toggle', display: 'show', reveal: 'show',
257
+ conceal: 'hide', increase: 'increment', decrease: 'decrement',
258
+ fire: 'trigger', dispatch: 'send', navigate: 'go', goto: 'go',
259
+ };
260
+
261
+ const EVENT_ALIASES = {
262
+ clicked: 'click', pressed: 'keydown', changed: 'change',
263
+ submitted: 'submit', loaded: 'load',
264
+ };
265
+
266
+ function normalizeCommand(name) {
267
+ const lower = name.toLowerCase();
268
+ return COMMAND_ALIASES[lower] || lower;
269
+ }
270
+
271
+ function normalizeEvent(name) {
272
+ const lower = name.toLowerCase();
273
+ return EVENT_ALIASES[lower] || lower;
274
+ }
275
+
276
+ function tokenize(code) {
277
+ const tokens = [];
278
+ let pos = 0;
279
+
280
+ while (pos < code.length) {
281
+ if (/\\s/.test(code[pos])) { pos++; continue; }
282
+ if (code.slice(pos, pos + 2) === '--') {
283
+ while (pos < code.length && code[pos] !== '\\n') pos++;
284
+ continue;
285
+ }
286
+
287
+ const start = pos;
288
+
289
+ // HTML selector <tag/>
290
+ if (code[pos] === '<' && /[a-zA-Z]/.test(code[pos + 1] || '')) {
291
+ pos++;
292
+ while (pos < code.length && code[pos] !== '>') pos++;
293
+ if (code[pos] === '>') pos++;
294
+ const val = code.slice(start, pos);
295
+ if (val.endsWith('/>') || val.endsWith('>')) {
296
+ const normalized = val.slice(1).replace(/\\/?>$/, '');
297
+ tokens.push({ type: 'selector', value: normalized, pos: start });
298
+ continue;
299
+ }
300
+ }
301
+
302
+ // Possessive 's
303
+ if (code.slice(pos, pos + 2) === "'s" && !/[a-zA-Z]/.test(code[pos + 2] || '')) {
304
+ tokens.push({ type: 'operator', value: "'s", pos: start });
305
+ pos += 2;
306
+ continue;
307
+ }
308
+
309
+ // String literals
310
+ if (code[pos] === '"' || code[pos] === "'") {
311
+ const quote = code[pos++];
312
+ while (pos < code.length && code[pos] !== quote) {
313
+ if (code[pos] === '\\\\') pos++;
314
+ pos++;
315
+ }
316
+ pos++;
317
+ tokens.push({ type: 'string', value: code.slice(start, pos), pos: start });
318
+ continue;
319
+ }
320
+
321
+ // Numbers with units
322
+ if (/\\d/.test(code[pos]) || (code[pos] === '-' && /\\d/.test(code[pos + 1] || ''))) {
323
+ if (code[pos] === '-') pos++;
324
+ while (pos < code.length && /[\\d.]/.test(code[pos])) pos++;
325
+ if (code.slice(pos, pos + 2) === 'ms') pos += 2;
326
+ else if (code[pos] === 's' && !/[a-zA-Z]/.test(code[pos + 1] || '')) pos++;
327
+ else if (code.slice(pos, pos + 2) === 'px') pos += 2;
328
+ tokens.push({ type: 'number', value: code.slice(start, pos), pos: start });
329
+ continue;
330
+ }
331
+
332
+ // Local variable :name
333
+ if (code[pos] === ':') {
334
+ pos++;
335
+ while (pos < code.length && /[\\w]/.test(code[pos])) pos++;
336
+ tokens.push({ type: 'localVar', value: code.slice(start, pos), pos: start });
337
+ continue;
338
+ }
339
+
340
+ // Global variable $name
341
+ if (code[pos] === '$') {
342
+ pos++;
343
+ while (pos < code.length && /[\\w]/.test(code[pos])) pos++;
344
+ tokens.push({ type: 'globalVar', value: code.slice(start, pos), pos: start });
345
+ continue;
346
+ }
347
+
348
+ // CSS selectors: #id, .class
349
+ if (code[pos] === '#' || code[pos] === '.') {
350
+ if (code[pos] === '.') {
351
+ const afterDot = code.slice(pos + 1).match(/^(once|prevent|stop|debounce|throttle)\\b/i);
352
+ if (afterDot) {
353
+ tokens.push({ type: 'symbol', value: '.', pos: start });
354
+ pos++;
355
+ continue;
356
+ }
357
+ }
358
+ pos++;
359
+ while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;
360
+ tokens.push({ type: 'selector', value: code.slice(start, pos), pos: start });
361
+ continue;
362
+ }
363
+
364
+ // Array literal vs Attribute selector
365
+ if (code[pos] === '[') {
366
+ let lookahead = pos + 1;
367
+ while (lookahead < code.length && /\\s/.test(code[lookahead])) lookahead++;
368
+ const nextChar = code[lookahead] || '';
369
+ const isArrayLiteral = /['"\\d\\[\\]:\\$\\-]/.test(nextChar) || nextChar === '';
370
+ if (isArrayLiteral) {
371
+ tokens.push({ type: 'symbol', value: '[', pos: start });
372
+ pos++;
373
+ continue;
374
+ } else {
375
+ pos++;
376
+ let depth = 1;
377
+ while (pos < code.length && depth > 0) {
378
+ if (code[pos] === '[') depth++;
379
+ if (code[pos] === ']') depth--;
380
+ pos++;
381
+ }
382
+ tokens.push({ type: 'selector', value: code.slice(start, pos), pos: start });
383
+ continue;
384
+ }
385
+ }
386
+
387
+ if (code[pos] === ']') {
388
+ tokens.push({ type: 'symbol', value: ']', pos: start });
389
+ pos++;
390
+ continue;
391
+ }
392
+
393
+ // Multi-char operators
394
+ const twoChar = code.slice(pos, pos + 2);
395
+ if (['==', '!=', '<=', '>=', '&&', '||'].includes(twoChar)) {
396
+ tokens.push({ type: 'operator', value: twoChar, pos: start });
397
+ pos += 2;
398
+ continue;
399
+ }
400
+
401
+ // Style property *opacity
402
+ if (code[pos] === '*' && /[a-zA-Z]/.test(code[pos + 1] || '')) {
403
+ pos++;
404
+ while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;
405
+ tokens.push({ type: 'styleProperty', value: code.slice(start, pos), pos: start });
406
+ continue;
407
+ }
408
+
409
+ if ('+-*/%<>!'.includes(code[pos])) {
410
+ tokens.push({ type: 'operator', value: code[pos], pos: start });
411
+ pos++;
412
+ continue;
413
+ }
414
+
415
+ if ('()[]{},.'.includes(code[pos])) {
416
+ tokens.push({ type: 'symbol', value: code[pos], pos: start });
417
+ pos++;
418
+ continue;
419
+ }
420
+
421
+ if (/[a-zA-Z_]/.test(code[pos])) {
422
+ while (pos < code.length && /[\\w-]/.test(code[pos])) pos++;
423
+ const value = code.slice(start, pos);
424
+ const type = KEYWORDS.has(value.toLowerCase()) ? 'keyword' : 'identifier';
425
+ tokens.push({ type, value, pos: start });
426
+ continue;
427
+ }
428
+
429
+ pos++;
430
+ }
431
+
432
+ tokens.push({ type: 'eof', value: '', pos: code.length });
433
+ return tokens;
434
+ }
435
+
436
+ // Parser
437
+ class HybridParser {
438
+ constructor(code) {
439
+ this.tokens = tokenize(code);
440
+ this.pos = 0;
441
+ }
442
+
443
+ peek(offset = 0) {
444
+ return this.tokens[Math.min(this.pos + offset, this.tokens.length - 1)];
445
+ }
446
+
447
+ advance() {
448
+ return this.tokens[this.pos++];
449
+ }
450
+
451
+ match(...values) {
452
+ const token = this.peek();
453
+ return values.some(v => token.value.toLowerCase() === v.toLowerCase());
454
+ }
455
+
456
+ matchType(...types) {
457
+ return types.includes(this.peek().type);
458
+ }
459
+
460
+ expect(value) {
461
+ if (!this.match(value) && normalizeCommand(this.peek().value) !== value) {
462
+ throw new Error("Expected '" + value + "', got '" + this.peek().value + "'");
463
+ }
464
+ return this.advance();
465
+ }
466
+
467
+ isAtEnd() {
468
+ return this.peek().type === 'eof';
469
+ }
470
+
471
+ parse() {
472
+ if (this.match('on')) return this.parseEventHandler();
473
+ if (this.match('init')) {
474
+ this.advance();
475
+ return { type: 'event', event: 'init', modifiers: {}, body: this.parseCommandSequence() };
476
+ }
477
+ if (this.match('every')) return this.parseEveryHandler();
478
+ return { type: 'sequence', commands: this.parseCommandSequence() };
479
+ }
480
+
481
+ parseEventHandler() {
482
+ this.expect('on');
483
+ const eventName = this.advance().value;
484
+ const modifiers = {};
485
+ let filter;
486
+
487
+ while (this.peek().value === '.') {
488
+ this.advance();
489
+ const mod = this.advance().value.toLowerCase();
490
+ if (mod === 'once') modifiers.once = true;
491
+ else if (mod === 'prevent') modifiers.prevent = true;
492
+ else if (mod === 'stop') modifiers.stop = true;
493
+ else if (mod === 'debounce' || mod === 'throttle') {
494
+ if (this.peek().value === '(') {
495
+ this.advance();
496
+ const num = this.advance().value;
497
+ this.expect(')');
498
+ if (mod === 'debounce') modifiers.debounce = parseInt(num) || 100;
499
+ else modifiers.throttle = parseInt(num) || 100;
500
+ }
501
+ }
502
+ }
503
+
504
+ if (this.match('from')) {
505
+ this.advance();
506
+ filter = this.parseExpression();
507
+ }
508
+
509
+ return { type: 'event', event: normalizeEvent(eventName), filter, modifiers, body: this.parseCommandSequence() };
510
+ }
511
+
512
+ parseEveryHandler() {
513
+ this.expect('every');
514
+ const interval = this.advance().value;
515
+ return { type: 'event', event: 'interval:' + interval, modifiers: {}, body: this.parseCommandSequence() };
516
+ }
517
+
518
+ parseCommandSequence() {
519
+ const commands = [];
520
+ while (!this.isAtEnd() && !this.match('end', 'else')) {
521
+ const cmd = this.parseCommand();
522
+ if (cmd) commands.push(cmd);
523
+ if (this.match('then', 'and')) this.advance();
524
+ }
525
+ return commands;
526
+ }
527
+
528
+ parseCommand() {
529
+ if (this.match('if', 'unless')) return this.parseIf();
530
+ if (this.match('repeat')) return this.parseRepeat();
531
+ if (this.match('for')) return this.parseFor();
532
+ if (this.match('while')) return this.parseWhile();
533
+ if (this.match('fetch')) return this.parseFetchBlock();
534
+
535
+ const cmdMap = {
536
+ toggle: () => this.parseToggle(),
537
+ add: () => this.parseAdd(),
538
+ remove: () => this.parseRemove(),
539
+ put: () => this.parsePut(),
540
+ append: () => this.parseAppend(),
541
+ set: () => this.parseSet(),
542
+ get: () => this.parseGet(),
543
+ call: () => this.parseCall(),
544
+ log: () => this.parseLog(),
545
+ send: () => this.parseSend(),
546
+ trigger: () => this.parseSend(),
547
+ wait: () => this.parseWait(),
548
+ show: () => this.parseShow(),
549
+ hide: () => this.parseHide(),
550
+ take: () => this.parseTake(),
551
+ increment: () => this.parseIncDec('increment'),
552
+ decrement: () => this.parseIncDec('decrement'),
553
+ focus: () => this.parseFocusBlur('focus'),
554
+ blur: () => this.parseFocusBlur('blur'),
555
+ go: () => this.parseGo(),
556
+ return: () => this.parseReturn(),
557
+ transition: () => this.parseTransition(),
558
+ };
559
+
560
+ const normalized = normalizeCommand(this.peek().value);
561
+ if (cmdMap[normalized]) return cmdMap[normalized]();
562
+
563
+ if (!this.isAtEnd() && !this.match('then', 'and', 'end', 'else')) this.advance();
564
+ return null;
565
+ }
566
+
567
+ parseIf() {
568
+ const isUnless = this.match('unless');
569
+ this.advance();
570
+ const condition = this.parseExpression();
571
+ const body = this.parseCommandSequence();
572
+ let elseBody;
573
+
574
+ if (this.match('else')) {
575
+ this.advance();
576
+ elseBody = this.parseCommandSequence();
577
+ }
578
+ if (this.match('end')) this.advance();
579
+
580
+ return {
581
+ type: 'if',
582
+ condition: isUnless ? { type: 'unary', operator: 'not', operand: condition } : condition,
583
+ body,
584
+ elseBody,
585
+ };
586
+ }
587
+
588
+ parseRepeat() {
589
+ this.expect('repeat');
590
+ let count;
591
+ if (!this.match('until', 'while', 'forever')) {
592
+ count = this.parseExpression();
593
+ if (this.match('times')) this.advance();
594
+ }
595
+ const body = this.parseCommandSequence();
596
+ if (this.match('end')) this.advance();
597
+ return { type: 'repeat', condition: count, body };
598
+ }
599
+
600
+ parseFor() {
601
+ this.expect('for');
602
+ if (this.match('each')) this.advance();
603
+ const variable = this.advance().value;
604
+ this.expect('in');
605
+ const iterable = this.parseExpression();
606
+ const body = this.parseCommandSequence();
607
+ if (this.match('end')) this.advance();
608
+ return { type: 'for', condition: { type: 'forCondition', variable, iterable }, body };
609
+ }
610
+
611
+ parseWhile() {
612
+ this.expect('while');
613
+ const condition = this.parseExpression();
614
+ const body = this.parseCommandSequence();
615
+ if (this.match('end')) this.advance();
616
+ return { type: 'while', condition, body };
617
+ }
618
+
619
+ parseFetchBlock() {
620
+ this.expect('fetch');
621
+ const url = this.parseExpression();
622
+ let responseType = { type: 'literal', value: 'text' };
623
+ if (this.match('as')) {
624
+ this.advance();
625
+ responseType = this.parseExpression();
626
+ }
627
+ if (this.match('then')) this.advance();
628
+ const body = this.parseCommandSequence();
629
+ return { type: 'fetch', condition: { type: 'fetchConfig', url, responseType }, body };
630
+ }
631
+
632
+ parseToggle() {
633
+ this.expect('toggle');
634
+ const what = this.parseExpression();
635
+ let target;
636
+ if (this.match('on')) {
637
+ this.advance();
638
+ target = this.parseExpression();
639
+ }
640
+ return { type: 'command', name: 'toggle', args: [what], target };
641
+ }
642
+
643
+ parseAdd() {
644
+ this.expect('add');
645
+ const what = this.parseExpression();
646
+ let target;
647
+ if (this.match('to')) {
648
+ this.advance();
649
+ target = this.parseExpression();
650
+ }
651
+ return { type: 'command', name: 'add', args: [what], target };
652
+ }
653
+
654
+ parseRemove() {
655
+ this.expect('remove');
656
+ if (this.matchType('selector')) {
657
+ const what = this.parseExpression();
658
+ let target;
659
+ if (this.match('from')) {
660
+ this.advance();
661
+ target = this.parseExpression();
662
+ }
663
+ return { type: 'command', name: 'removeClass', args: [what], target };
664
+ }
665
+ const target = this.parseExpression();
666
+ return { type: 'command', name: 'remove', args: [], target };
667
+ }
668
+
669
+ parsePut() {
670
+ this.expect('put');
671
+ const content = this.parseExpression();
672
+ let modifier = 'into';
673
+ if (this.match('into', 'before', 'after', 'at')) {
674
+ modifier = this.advance().value;
675
+ if (modifier === 'at') {
676
+ const pos = this.advance().value;
677
+ this.expect('of');
678
+ modifier = 'at ' + pos + ' of';
679
+ }
680
+ }
681
+ const target = this.parseExpression();
682
+ return { type: 'command', name: 'put', args: [content], target, modifier };
683
+ }
684
+
685
+ parseAppend() {
686
+ this.expect('append');
687
+ const content = this.parseExpression();
688
+ let target;
689
+ if (this.match('to')) {
690
+ this.advance();
691
+ target = this.parseExpression();
692
+ }
693
+ return { type: 'command', name: 'append', args: [content], target };
694
+ }
695
+
696
+ parseSet() {
697
+ this.expect('set');
698
+ const target = this.parseExpression();
699
+ if (this.match('to')) {
700
+ this.advance();
701
+ const value = this.parseExpression();
702
+ return { type: 'command', name: 'set', args: [target, value] };
703
+ }
704
+ return { type: 'command', name: 'set', args: [target] };
705
+ }
706
+
707
+ parseGet() {
708
+ this.expect('get');
709
+ return { type: 'command', name: 'get', args: [this.parseExpression()] };
710
+ }
711
+
712
+ parseCall() {
713
+ this.expect('call');
714
+ return { type: 'command', name: 'call', args: [this.parseExpression()] };
715
+ }
716
+
717
+ parseLog() {
718
+ this.expect('log');
719
+ const args = [];
720
+ while (!this.isAtEnd() && !this.match('then', 'and', 'end', 'else')) {
721
+ args.push(this.parseExpression());
722
+ if (this.match(',')) this.advance();
723
+ else break;
724
+ }
725
+ return { type: 'command', name: 'log', args };
726
+ }
727
+
728
+ parseSend() {
729
+ this.advance();
730
+ const event = this.advance().value;
731
+ let target;
732
+ if (this.match('to')) {
733
+ this.advance();
734
+ target = this.parseExpression();
735
+ }
736
+ return { type: 'command', name: 'send', args: [{ type: 'literal', value: event }], target };
737
+ }
738
+
739
+ parseWait() {
740
+ this.expect('wait');
741
+ if (this.match('for')) {
742
+ this.advance();
743
+ const event = this.advance().value;
744
+ let target;
745
+ if (this.match('from')) {
746
+ this.advance();
747
+ target = this.parseExpression();
748
+ }
749
+ return { type: 'command', name: 'waitFor', args: [{ type: 'literal', value: event }], target };
750
+ }
751
+ return { type: 'command', name: 'wait', args: [this.parseExpression()] };
752
+ }
753
+
754
+ parseShow() {
755
+ this.expect('show');
756
+ let target;
757
+ const modifiers = {};
758
+ if (!this.isAtEnd() && !this.match('then', 'and', 'end', 'else', 'when', 'where')) {
759
+ target = this.parseExpression();
760
+ }
761
+ if (!this.isAtEnd() && this.match('when', 'where')) {
762
+ const keyword = this.advance().value;
763
+ modifiers[keyword] = this.parseExpression();
764
+ }
765
+ return { type: 'command', name: 'show', args: [], target, modifiers };
766
+ }
767
+
768
+ parseHide() {
769
+ this.expect('hide');
770
+ let target;
771
+ const modifiers = {};
772
+ if (!this.isAtEnd() && !this.match('then', 'and', 'end', 'else', 'when', 'where')) {
773
+ target = this.parseExpression();
774
+ }
775
+ if (!this.isAtEnd() && this.match('when', 'where')) {
776
+ const keyword = this.advance().value;
777
+ modifiers[keyword] = this.parseExpression();
778
+ }
779
+ return { type: 'command', name: 'hide', args: [], target, modifiers };
780
+ }
781
+
782
+ parseTake() {
783
+ this.expect('take');
784
+ const what = this.parseExpression();
785
+ let from;
786
+ if (this.match('from')) {
787
+ this.advance();
788
+ from = this.parseExpression();
789
+ }
790
+ return { type: 'command', name: 'take', args: [what], target: from };
791
+ }
792
+
793
+ parseIncDec(name) {
794
+ this.advance();
795
+ const target = this.parseExpression();
796
+ let amount = { type: 'literal', value: 1 };
797
+ if (this.match('by')) {
798
+ this.advance();
799
+ amount = this.parseExpression();
800
+ }
801
+ return { type: 'command', name, args: [target, amount] };
802
+ }
803
+
804
+ parseFocusBlur(name) {
805
+ this.advance();
806
+ let target;
807
+ if (!this.isAtEnd() && !this.match('then', 'and', 'end', 'else')) {
808
+ target = this.parseExpression();
809
+ }
810
+ return { type: 'command', name, args: [], target };
811
+ }
812
+
813
+ parseGo() {
814
+ this.expect('go');
815
+ if (this.match('to')) this.advance();
816
+ if (this.match('url')) this.advance();
817
+ const dest = this.parseExpression();
818
+ return { type: 'command', name: 'go', args: [dest] };
819
+ }
820
+
821
+ parseReturn() {
822
+ this.expect('return');
823
+ let value;
824
+ if (!this.isAtEnd() && !this.match('then', 'and', 'end', 'else')) {
825
+ value = this.parseExpression();
826
+ }
827
+ return { type: 'command', name: 'return', args: value ? [value] : [] };
828
+ }
829
+
830
+ parseTransition() {
831
+ this.expect('transition');
832
+ let target;
833
+ if (this.match('my', 'its')) {
834
+ const ref = this.advance().value;
835
+ target = { type: 'identifier', value: ref === 'my' ? 'me' : 'it' };
836
+ } else if (this.matchType('selector')) {
837
+ const expr = this.parseExpression();
838
+ if (expr.type === 'possessive') {
839
+ return this.parseTransitionRest(expr.object, expr.property);
840
+ }
841
+ target = expr;
842
+ }
843
+
844
+ const propToken = this.peek();
845
+ let property;
846
+ if (propToken.type === 'styleProperty') {
847
+ property = this.advance().value;
848
+ } else if (propToken.type === 'identifier' || propToken.type === 'keyword') {
849
+ property = this.advance().value;
850
+ } else {
851
+ property = 'opacity';
852
+ }
853
+
854
+ return this.parseTransitionRest(target, property);
855
+ }
856
+
857
+ parseTransitionRest(target, property) {
858
+ let toValue = { type: 'literal', value: 1 };
859
+ if (this.match('to')) {
860
+ this.advance();
861
+ toValue = this.parseExpression();
862
+ }
863
+ let duration = { type: 'literal', value: 300 };
864
+ if (this.match('over')) {
865
+ this.advance();
866
+ duration = this.parseExpression();
867
+ }
868
+ return { type: 'command', name: 'transition', args: [{ type: 'literal', value: property }, toValue, duration], target };
869
+ }
870
+
871
+ parseExpression() { return this.parseOr(); }
872
+
873
+ parseOr() {
874
+ let left = this.parseAnd();
875
+ while (this.match('or', '||')) {
876
+ this.advance();
877
+ left = { type: 'binary', operator: 'or', left, right: this.parseAnd() };
878
+ }
879
+ return left;
880
+ }
881
+
882
+ parseAnd() {
883
+ let left = this.parseEquality();
884
+ while (this.match('and', '&&') && !this.isCommandKeyword(this.peek(1))) {
885
+ this.advance();
886
+ left = { type: 'binary', operator: 'and', left, right: this.parseEquality() };
887
+ }
888
+ return left;
889
+ }
890
+
891
+ isCommandKeyword(token) {
892
+ const cmds = ['toggle', 'add', 'remove', 'set', 'put', 'log', 'send', 'wait', 'show', 'hide', 'increment', 'decrement', 'focus', 'blur', 'go'];
893
+ return cmds.includes(normalizeCommand(token.value));
894
+ }
895
+
896
+ parseEquality() {
897
+ let left = this.parseComparison();
898
+ while (this.match('==', '!=', 'is', 'matches', 'contains', 'includes', 'has')) {
899
+ const op = this.advance().value;
900
+ if (op.toLowerCase() === 'is' && this.match('not')) {
901
+ this.advance();
902
+ left = { type: 'binary', operator: 'is not', left, right: this.parseComparison() };
903
+ } else {
904
+ left = { type: 'binary', operator: op, left, right: this.parseComparison() };
905
+ }
906
+ }
907
+ return left;
908
+ }
909
+
910
+ parseComparison() {
911
+ let left = this.parseAdditive();
912
+ while (this.match('<', '>', '<=', '>=')) {
913
+ const op = this.advance().value;
914
+ left = { type: 'binary', operator: op, left, right: this.parseAdditive() };
915
+ }
916
+ return left;
917
+ }
918
+
919
+ parseAdditive() {
920
+ let left = this.parseMultiplicative();
921
+ while (this.match('+', '-')) {
922
+ const op = this.advance().value;
923
+ left = { type: 'binary', operator: op, left, right: this.parseMultiplicative() };
924
+ }
925
+ return left;
926
+ }
927
+
928
+ parseMultiplicative() {
929
+ let left = this.parseUnary();
930
+ while (this.match('*', '/', '%')) {
931
+ const op = this.advance().value;
932
+ left = { type: 'binary', operator: op, left, right: this.parseUnary() };
933
+ }
934
+ return left;
935
+ }
936
+
937
+ parseUnary() {
938
+ if (this.match('not', '!')) {
939
+ this.advance();
940
+ return { type: 'unary', operator: 'not', operand: this.parseUnary() };
941
+ }
942
+ if (this.match('-') && this.peek(1).type === 'number') {
943
+ this.advance();
944
+ const num = this.advance();
945
+ return { type: 'literal', value: -parseFloat(num.value) };
946
+ }
947
+ return this.parsePostfix();
948
+ }
949
+
950
+ parsePostfix() {
951
+ let left = this.parsePrimary();
952
+ while (true) {
953
+ if (this.match("'s")) {
954
+ this.advance();
955
+ const next = this.peek();
956
+ const prop = next.type === 'styleProperty' ? this.advance().value : this.advance().value;
957
+ left = { type: 'possessive', object: left, property: prop };
958
+ } else if (this.peek().type === 'styleProperty') {
959
+ const prop = this.advance().value;
960
+ left = { type: 'possessive', object: left, property: prop };
961
+ } else if (this.peek().value === '.') {
962
+ this.advance();
963
+ const prop = this.advance().value;
964
+ left = { type: 'member', object: left, property: prop };
965
+ } else if (this.peek().type === 'selector' && this.peek().value.startsWith('.')) {
966
+ const prop = this.advance().value.slice(1);
967
+ left = { type: 'member', object: left, property: prop };
968
+ } else if (this.peek().value === '(') {
969
+ this.advance();
970
+ const args = [];
971
+ while (!this.match(')')) {
972
+ args.push(this.parseExpression());
973
+ if (this.match(',')) this.advance();
974
+ }
975
+ this.expect(')');
976
+ left = { type: 'call', callee: left, args };
977
+ } else if (this.peek().value === '[' && left.type !== 'selector') {
978
+ this.advance();
979
+ const index = this.parseExpression();
980
+ this.expect(']');
981
+ left = { type: 'member', object: left, property: index, computed: true };
982
+ } else {
983
+ break;
984
+ }
985
+ }
986
+ return left;
987
+ }
988
+
989
+ parsePrimary() {
990
+ const token = this.peek();
991
+
992
+ if (token.value === '(') {
993
+ this.advance();
994
+ const expr = this.parseExpression();
995
+ this.expect(')');
996
+ return expr;
997
+ }
998
+
999
+ if (token.value === '{') return this.parseObjectLiteral();
1000
+ if (token.value === '[') return this.parseArrayLiteral();
1001
+
1002
+ if (token.type === 'number') {
1003
+ this.advance();
1004
+ const val = token.value;
1005
+ if (val.endsWith('ms')) return { type: 'literal', value: parseInt(val), unit: 'ms' };
1006
+ if (val.endsWith('s')) return { type: 'literal', value: parseFloat(val) * 1000, unit: 'ms' };
1007
+ return { type: 'literal', value: parseFloat(val) };
1008
+ }
1009
+
1010
+ if (token.type === 'string') {
1011
+ this.advance();
1012
+ return { type: 'literal', value: token.value.slice(1, -1) };
1013
+ }
1014
+
1015
+ if (this.match('true')) { this.advance(); return { type: 'literal', value: true }; }
1016
+ if (this.match('false')) { this.advance(); return { type: 'literal', value: false }; }
1017
+ if (this.match('null')) { this.advance(); return { type: 'literal', value: null }; }
1018
+ if (this.match('undefined')) { this.advance(); return { type: 'literal', value: undefined }; }
1019
+
1020
+ if (token.type === 'localVar') {
1021
+ this.advance();
1022
+ return { type: 'variable', name: token.value, scope: 'local' };
1023
+ }
1024
+ if (token.type === 'globalVar') {
1025
+ this.advance();
1026
+ return { type: 'variable', name: token.value, scope: 'global' };
1027
+ }
1028
+ if (token.type === 'selector') {
1029
+ this.advance();
1030
+ return { type: 'selector', value: token.value };
1031
+ }
1032
+
1033
+ if (this.match('my')) {
1034
+ this.advance();
1035
+ const next = this.peek();
1036
+ if ((next.type === 'identifier' || next.type === 'keyword') && !this.isCommandKeyword(next)) {
1037
+ const prop = this.advance().value;
1038
+ return { type: 'possessive', object: { type: 'identifier', value: 'me' }, property: prop };
1039
+ }
1040
+ return { type: 'identifier', value: 'me' };
1041
+ }
1042
+ if (this.match('its')) {
1043
+ this.advance();
1044
+ const next = this.peek();
1045
+ if ((next.type === 'identifier' || next.type === 'keyword') && !this.isCommandKeyword(next)) {
1046
+ const prop = this.advance().value;
1047
+ return { type: 'possessive', object: { type: 'identifier', value: 'it' }, property: prop };
1048
+ }
1049
+ return { type: 'identifier', value: 'it' };
1050
+ }
1051
+ if (this.match('me')) { this.advance(); return { type: 'identifier', value: 'me' }; }
1052
+ if (this.match('it')) { this.advance(); return { type: 'identifier', value: 'it' }; }
1053
+ if (this.match('you')) { this.advance(); return { type: 'identifier', value: 'you' }; }
1054
+
1055
+ if (this.match('the', 'a', 'an')) {
1056
+ this.advance();
1057
+ if (this.match('first', 'last', 'next', 'previous', 'closest', 'parent')) {
1058
+ const position = this.advance().value;
1059
+ const target = this.parsePositionalTarget();
1060
+ return { type: 'positional', position, target };
1061
+ }
1062
+ return this.parsePrimary();
1063
+ }
1064
+
1065
+ if (this.match('first', 'last', 'next', 'previous', 'closest', 'parent')) {
1066
+ const position = this.advance().value;
1067
+ const target = this.parsePositionalTarget();
1068
+ return { type: 'positional', position, target };
1069
+ }
1070
+
1071
+ if (token.type === 'identifier' || token.type === 'keyword') {
1072
+ this.advance();
1073
+ return { type: 'identifier', value: token.value };
1074
+ }
1075
+
1076
+ this.advance();
1077
+ return { type: 'identifier', value: token.value };
1078
+ }
1079
+
1080
+ parseObjectLiteral() {
1081
+ this.expect('{');
1082
+ const properties = [];
1083
+ while (!this.match('}')) {
1084
+ const key = this.advance().value;
1085
+ this.expect(':');
1086
+ const value = this.parseExpression();
1087
+ properties.push({ key, value });
1088
+ if (this.match(',')) this.advance();
1089
+ }
1090
+ this.expect('}');
1091
+ return { type: 'object', properties };
1092
+ }
1093
+
1094
+ parseArrayLiteral() {
1095
+ this.expect('[');
1096
+ const elements = [];
1097
+ while (!this.match(']')) {
1098
+ elements.push(this.parseExpression());
1099
+ if (this.match(',')) this.advance();
1100
+ }
1101
+ this.expect(']');
1102
+ return { type: 'array', elements };
1103
+ }
1104
+
1105
+ parsePositionalTarget() {
1106
+ const token = this.peek();
1107
+ if (token.type === 'selector') {
1108
+ return { type: 'selector', value: this.advance().value };
1109
+ }
1110
+ if (token.type === 'identifier' || token.type === 'keyword') {
1111
+ return { type: 'identifier', value: this.advance().value };
1112
+ }
1113
+ return this.parseExpression();
1114
+ }
1115
+ }
1116
+ `;
1117
+
1118
+ // =============================================================================
1119
+ // HELPER FUNCTIONS
1120
+ // =============================================================================
1121
+
1122
+ /**
1123
+ * Get the appropriate parser template based on detected features.
1124
+ */
1125
+ export function getParserTemplate(type: 'lite' | 'hybrid'): string {
1126
+ return type === 'lite' ? LITE_PARSER_TEMPLATE : HYBRID_PARSER_TEMPLATE;
1127
+ }
1128
+
1129
+ /**
1130
+ * Commands supported by the lite parser.
1131
+ */
1132
+ export const LITE_PARSER_COMMANDS = [
1133
+ 'toggle',
1134
+ 'add',
1135
+ 'remove',
1136
+ 'put',
1137
+ 'set',
1138
+ 'log',
1139
+ 'send',
1140
+ 'wait',
1141
+ 'show',
1142
+ 'hide',
1143
+ ] as const;
1144
+
1145
+ /**
1146
+ * Check if a set of commands can be handled by the lite parser.
1147
+ */
1148
+ export function canUseLiteParser(
1149
+ commands: string[],
1150
+ blocks: string[],
1151
+ positional: boolean
1152
+ ): boolean {
1153
+ if (blocks.length > 0) return false;
1154
+ if (positional) return false;
1155
+
1156
+ const liteCommands = new Set(LITE_PARSER_COMMANDS);
1157
+ return commands.every(cmd => liteCommands.has(cmd as (typeof LITE_PARSER_COMMANDS)[number]));
1158
+ }