@holoscript/core 1.0.0-alpha.2 → 2.0.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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/HoloScript2DParser.js +227 -0
  3. package/src/HoloScript2DParser.ts +5 -0
  4. package/src/HoloScriptCodeParser.js +1102 -0
  5. package/src/HoloScriptCodeParser.ts +145 -20
  6. package/src/HoloScriptDebugger.js +458 -0
  7. package/src/HoloScriptParser.js +338 -0
  8. package/src/HoloScriptPlusParser.js +371 -0
  9. package/src/HoloScriptPlusParser.ts +543 -0
  10. package/src/HoloScriptRuntime.js +1399 -0
  11. package/src/HoloScriptRuntime.test.js +351 -0
  12. package/src/HoloScriptRuntime.ts +17 -3
  13. package/src/HoloScriptTypeChecker.js +356 -0
  14. package/src/__tests__/GraphicsServices.test.js +357 -0
  15. package/src/__tests__/GraphicsServices.test.ts +427 -0
  16. package/src/__tests__/HoloScriptPlusParser.test.js +317 -0
  17. package/src/__tests__/HoloScriptPlusParser.test.ts +392 -0
  18. package/src/__tests__/integration.test.js +336 -0
  19. package/src/__tests__/performance.bench.js +218 -0
  20. package/src/__tests__/type-checker.test.js +60 -0
  21. package/src/__tests__/type-checker.test.ts +73 -0
  22. package/src/index.js +217 -0
  23. package/src/index.ts +158 -18
  24. package/src/interop/Interoperability.js +413 -0
  25. package/src/interop/Interoperability.ts +494 -0
  26. package/src/logger.js +42 -0
  27. package/src/parser/EnhancedParser.js +205 -0
  28. package/src/parser/EnhancedParser.ts +251 -0
  29. package/src/parser/HoloScriptPlusParser.js +928 -0
  30. package/src/parser/HoloScriptPlusParser.ts +1089 -0
  31. package/src/runtime/HoloScriptPlusRuntime.js +674 -0
  32. package/src/runtime/HoloScriptPlusRuntime.ts +861 -0
  33. package/src/runtime/PerformanceTelemetry.js +323 -0
  34. package/src/runtime/PerformanceTelemetry.ts +467 -0
  35. package/src/runtime/RuntimeOptimization.js +361 -0
  36. package/src/runtime/RuntimeOptimization.ts +416 -0
  37. package/src/services/HololandGraphicsPipelineService.js +506 -0
  38. package/src/services/HololandGraphicsPipelineService.ts +662 -0
  39. package/src/services/PlatformPerformanceOptimizer.js +356 -0
  40. package/src/services/PlatformPerformanceOptimizer.ts +503 -0
  41. package/src/state/ReactiveState.js +427 -0
  42. package/src/state/ReactiveState.ts +572 -0
  43. package/src/tools/DeveloperExperience.js +376 -0
  44. package/src/tools/DeveloperExperience.ts +438 -0
  45. package/src/traits/AIDriverTrait.js +322 -0
  46. package/src/traits/AIDriverTrait.test.js +329 -0
  47. package/src/traits/AIDriverTrait.test.ts +357 -0
  48. package/src/traits/AIDriverTrait.ts +474 -0
  49. package/src/traits/LightingTrait.js +313 -0
  50. package/src/traits/LightingTrait.test.js +410 -0
  51. package/src/traits/LightingTrait.test.ts +462 -0
  52. package/src/traits/LightingTrait.ts +505 -0
  53. package/src/traits/MaterialTrait.js +194 -0
  54. package/src/traits/MaterialTrait.test.js +286 -0
  55. package/src/traits/MaterialTrait.test.ts +329 -0
  56. package/src/traits/MaterialTrait.ts +324 -0
  57. package/src/traits/RenderingTrait.js +356 -0
  58. package/src/traits/RenderingTrait.test.js +363 -0
  59. package/src/traits/RenderingTrait.test.ts +427 -0
  60. package/src/traits/RenderingTrait.ts +555 -0
  61. package/src/traits/VRTraitSystem.js +740 -0
  62. package/src/traits/VRTraitSystem.ts +1040 -0
  63. package/src/traits/VoiceInputTrait.js +284 -0
  64. package/src/traits/VoiceInputTrait.test.js +226 -0
  65. package/src/traits/VoiceInputTrait.test.ts +252 -0
  66. package/src/traits/VoiceInputTrait.ts +401 -0
  67. package/src/types/AdvancedTypeSystem.js +226 -0
  68. package/src/types/AdvancedTypeSystem.ts +494 -0
  69. package/src/types/HoloScriptPlus.d.ts +853 -0
  70. package/src/types.js +6 -0
  71. package/src/types.ts +96 -1
  72. package/tsconfig.json +1 -1
  73. package/tsup.config.d.ts +2 -0
  74. package/tsup.config.js +18 -0
@@ -0,0 +1,928 @@
1
+ /**
2
+ * HoloScript+ Parser
3
+ *
4
+ * Parses HoloScript+ source code into an AST with support for:
5
+ * - Standard HoloScript syntax (backward compatible)
6
+ * - @ directive parsing for VR traits, state, control flow
7
+ * - Expression interpolation with ${...}
8
+ * - TypeScript companion imports
9
+ *
10
+ * @version 1.0.0
11
+ */
12
+ // =============================================================================
13
+ // VR TRAITS
14
+ // =============================================================================
15
+ const VR_TRAITS = [
16
+ 'grabbable',
17
+ 'throwable',
18
+ 'pointable',
19
+ 'hoverable',
20
+ 'scalable',
21
+ 'rotatable',
22
+ 'stackable',
23
+ 'snappable',
24
+ 'breakable',
25
+ ];
26
+ // =============================================================================
27
+ // LIFECYCLE HOOKS
28
+ // =============================================================================
29
+ const LIFECYCLE_HOOKS = [
30
+ // Standard lifecycle
31
+ 'on_mount',
32
+ 'on_unmount',
33
+ 'on_update',
34
+ 'on_data_update',
35
+ // VR lifecycle
36
+ 'on_grab',
37
+ 'on_release',
38
+ 'on_hover_enter',
39
+ 'on_hover_exit',
40
+ 'on_point_enter',
41
+ 'on_point_exit',
42
+ 'on_collision',
43
+ 'on_trigger_enter',
44
+ 'on_trigger_exit',
45
+ 'on_click',
46
+ 'on_double_click',
47
+ // Controller hooks
48
+ 'on_controller_button',
49
+ 'on_trigger_hold',
50
+ 'on_trigger_release',
51
+ 'on_grip_hold',
52
+ 'on_grip_release',
53
+ ];
54
+ // =============================================================================
55
+ // LEXER
56
+ // =============================================================================
57
+ class Lexer {
58
+ constructor(source) {
59
+ this.pos = 0;
60
+ this.line = 1;
61
+ this.column = 1;
62
+ this.indentStack = [0];
63
+ this.tokens = [];
64
+ this.pendingDedents = 0;
65
+ this.source = source;
66
+ }
67
+ tokenize() {
68
+ while (this.pos < this.source.length) {
69
+ // Handle pending dedents
70
+ while (this.pendingDedents > 0) {
71
+ this.tokens.push(this.createToken('DEDENT', ''));
72
+ this.pendingDedents--;
73
+ }
74
+ const char = this.source[this.pos];
75
+ // Skip whitespace (but track indentation at line start)
76
+ if (char === ' ' || char === '\t') {
77
+ if (this.column === 1) {
78
+ this.handleIndentation();
79
+ }
80
+ else {
81
+ this.advance();
82
+ }
83
+ continue;
84
+ }
85
+ // Comments
86
+ if (char === '/' && this.peek(1) === '/') {
87
+ this.skipLineComment();
88
+ continue;
89
+ }
90
+ if (char === '/' && this.peek(1) === '*') {
91
+ this.skipBlockComment();
92
+ continue;
93
+ }
94
+ if (char === '#' && this.peek(1) !== '#') {
95
+ if (this.peek(1) === '#') {
96
+ this.skipLineComment();
97
+ continue;
98
+ }
99
+ }
100
+ // Newlines
101
+ if (char === '\n') {
102
+ this.tokens.push(this.createToken('NEWLINE', '\n'));
103
+ this.advance();
104
+ this.line++;
105
+ this.column = 1;
106
+ continue;
107
+ }
108
+ if (char === '\r') {
109
+ this.advance();
110
+ if (this.peek() === '\n') {
111
+ this.advance();
112
+ }
113
+ this.tokens.push(this.createToken('NEWLINE', '\n'));
114
+ this.line++;
115
+ this.column = 1;
116
+ continue;
117
+ }
118
+ // Symbols
119
+ if (char === '{') {
120
+ this.tokens.push(this.createToken('LBRACE', '{'));
121
+ this.advance();
122
+ continue;
123
+ }
124
+ if (char === '}') {
125
+ this.tokens.push(this.createToken('RBRACE', '}'));
126
+ this.advance();
127
+ continue;
128
+ }
129
+ if (char === '[') {
130
+ this.tokens.push(this.createToken('LBRACKET', '['));
131
+ this.advance();
132
+ continue;
133
+ }
134
+ if (char === ']') {
135
+ this.tokens.push(this.createToken('RBRACKET', ']'));
136
+ this.advance();
137
+ continue;
138
+ }
139
+ if (char === '(') {
140
+ this.tokens.push(this.createToken('LPAREN', '('));
141
+ this.advance();
142
+ continue;
143
+ }
144
+ if (char === ')') {
145
+ this.tokens.push(this.createToken('RPAREN', ')'));
146
+ this.advance();
147
+ continue;
148
+ }
149
+ if (char === ':') {
150
+ this.tokens.push(this.createToken('COLON', ':'));
151
+ this.advance();
152
+ continue;
153
+ }
154
+ if (char === ',') {
155
+ this.tokens.push(this.createToken('COMMA', ','));
156
+ this.advance();
157
+ continue;
158
+ }
159
+ if (char === '@') {
160
+ this.tokens.push(this.createToken('AT', '@'));
161
+ this.advance();
162
+ continue;
163
+ }
164
+ if (char === '#') {
165
+ this.tokens.push(this.createToken('HASH', '#'));
166
+ this.advance();
167
+ continue;
168
+ }
169
+ if (char === '.') {
170
+ this.tokens.push(this.createToken('DOT', '.'));
171
+ this.advance();
172
+ continue;
173
+ }
174
+ if (char === '=') {
175
+ if (this.peek(1) === '>') {
176
+ this.tokens.push(this.createToken('ARROW', '=>'));
177
+ this.advance();
178
+ this.advance();
179
+ continue;
180
+ }
181
+ this.tokens.push(this.createToken('EQUALS', '='));
182
+ this.advance();
183
+ continue;
184
+ }
185
+ if (char === '|') {
186
+ this.tokens.push(this.createToken('PIPE', '|'));
187
+ this.advance();
188
+ continue;
189
+ }
190
+ // Strings
191
+ if (char === '"' || char === "'") {
192
+ this.tokens.push(this.readString(char));
193
+ continue;
194
+ }
195
+ // Numbers
196
+ if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek(1)))) {
197
+ this.tokens.push(this.readNumber());
198
+ continue;
199
+ }
200
+ // Expression interpolation ${...}
201
+ if (char === '$' && this.peek(1) === '{') {
202
+ this.tokens.push(this.readExpression());
203
+ continue;
204
+ }
205
+ // Identifiers and keywords
206
+ if (this.isIdentifierStart(char)) {
207
+ this.tokens.push(this.readIdentifier());
208
+ continue;
209
+ }
210
+ // Unknown character - skip
211
+ this.advance();
212
+ }
213
+ // Handle remaining dedents
214
+ while (this.indentStack.length > 1) {
215
+ this.tokens.push(this.createToken('DEDENT', ''));
216
+ this.indentStack.pop();
217
+ }
218
+ this.tokens.push(this.createToken('EOF', ''));
219
+ return this.tokens;
220
+ }
221
+ advance() {
222
+ const char = this.source[this.pos];
223
+ this.pos++;
224
+ this.column++;
225
+ return char;
226
+ }
227
+ peek(offset = 0) {
228
+ const pos = this.pos + offset;
229
+ return pos < this.source.length ? this.source[pos] : '';
230
+ }
231
+ createToken(type, value) {
232
+ return {
233
+ type,
234
+ value,
235
+ line: this.line,
236
+ column: this.column - value.length,
237
+ };
238
+ }
239
+ handleIndentation() {
240
+ let indent = 0;
241
+ while (this.peek() === ' ' || this.peek() === '\t') {
242
+ indent += this.peek() === '\t' ? 4 : 1;
243
+ this.advance();
244
+ }
245
+ if (this.peek() === '\n' || this.peek() === '\r') {
246
+ return;
247
+ }
248
+ const currentIndent = this.indentStack[this.indentStack.length - 1];
249
+ if (indent > currentIndent) {
250
+ this.indentStack.push(indent);
251
+ this.tokens.push(this.createToken('INDENT', ''));
252
+ }
253
+ else if (indent < currentIndent) {
254
+ while (this.indentStack.length > 1 &&
255
+ indent < this.indentStack[this.indentStack.length - 1]) {
256
+ this.indentStack.pop();
257
+ this.pendingDedents++;
258
+ }
259
+ }
260
+ }
261
+ skipLineComment() {
262
+ while (this.peek() !== '\n' && this.pos < this.source.length) {
263
+ this.advance();
264
+ }
265
+ }
266
+ skipBlockComment() {
267
+ this.advance(); // /
268
+ this.advance(); // *
269
+ while (this.pos < this.source.length) {
270
+ if (this.peek() === '*' && this.peek(1) === '/') {
271
+ this.advance();
272
+ this.advance();
273
+ break;
274
+ }
275
+ if (this.peek() === '\n') {
276
+ this.line++;
277
+ this.column = 0;
278
+ }
279
+ this.advance();
280
+ }
281
+ }
282
+ readString(quote) {
283
+ const startLine = this.line;
284
+ const startColumn = this.column;
285
+ this.advance(); // Opening quote
286
+ let value = '';
287
+ while (this.peek() !== quote && this.pos < this.source.length) {
288
+ if (this.peek() === '\\') {
289
+ this.advance();
290
+ const escaped = this.advance();
291
+ switch (escaped) {
292
+ case 'n':
293
+ value += '\n';
294
+ break;
295
+ case 't':
296
+ value += '\t';
297
+ break;
298
+ case 'r':
299
+ value += '\r';
300
+ break;
301
+ case '\\':
302
+ value += '\\';
303
+ break;
304
+ case '"':
305
+ value += '"';
306
+ break;
307
+ case "'":
308
+ value += "'";
309
+ break;
310
+ default:
311
+ value += escaped;
312
+ }
313
+ }
314
+ else if (this.peek() === '\n') {
315
+ this.line++;
316
+ this.column = 0;
317
+ value += this.advance();
318
+ }
319
+ else {
320
+ value += this.advance();
321
+ }
322
+ }
323
+ this.advance(); // Closing quote
324
+ return {
325
+ type: 'STRING',
326
+ value,
327
+ line: startLine,
328
+ column: startColumn,
329
+ };
330
+ }
331
+ readNumber() {
332
+ const startColumn = this.column;
333
+ let value = '';
334
+ if (this.peek() === '-') {
335
+ value += this.advance();
336
+ }
337
+ while (this.isDigit(this.peek())) {
338
+ value += this.advance();
339
+ }
340
+ if (this.peek() === '.' && this.isDigit(this.peek(1))) {
341
+ value += this.advance(); // .
342
+ while (this.isDigit(this.peek())) {
343
+ value += this.advance();
344
+ }
345
+ }
346
+ // Scientific notation
347
+ if (this.peek() === 'e' || this.peek() === 'E') {
348
+ value += this.advance();
349
+ if (this.peek() === '+' || this.peek() === '-') {
350
+ value += this.advance();
351
+ }
352
+ while (this.isDigit(this.peek())) {
353
+ value += this.advance();
354
+ }
355
+ }
356
+ // Unit suffix
357
+ while (this.isAlpha(this.peek()) || this.peek() === '%') {
358
+ value += this.advance();
359
+ }
360
+ return {
361
+ type: 'NUMBER',
362
+ value,
363
+ line: this.line,
364
+ column: startColumn,
365
+ };
366
+ }
367
+ readExpression() {
368
+ const startLine = this.line;
369
+ const startColumn = this.column;
370
+ this.advance(); // $
371
+ this.advance(); // {
372
+ let value = '';
373
+ let braceDepth = 1;
374
+ while (braceDepth > 0 && this.pos < this.source.length) {
375
+ if (this.peek() === '{') {
376
+ braceDepth++;
377
+ }
378
+ else if (this.peek() === '}') {
379
+ braceDepth--;
380
+ if (braceDepth === 0) {
381
+ break;
382
+ }
383
+ }
384
+ if (this.peek() === '\n') {
385
+ this.line++;
386
+ this.column = 0;
387
+ }
388
+ value += this.advance();
389
+ }
390
+ this.advance(); // Closing }
391
+ return {
392
+ type: 'EXPRESSION',
393
+ value: value.trim(),
394
+ line: startLine,
395
+ column: startColumn,
396
+ };
397
+ }
398
+ readIdentifier() {
399
+ const startColumn = this.column;
400
+ let value = '';
401
+ while (this.isIdentifierPart(this.peek())) {
402
+ value += this.advance();
403
+ }
404
+ if (value === 'true' || value === 'false') {
405
+ return {
406
+ type: 'BOOLEAN',
407
+ value,
408
+ line: this.line,
409
+ column: startColumn,
410
+ };
411
+ }
412
+ if (value === 'null' || value === 'none') {
413
+ return {
414
+ type: 'NULL',
415
+ value,
416
+ line: this.line,
417
+ column: startColumn,
418
+ };
419
+ }
420
+ return {
421
+ type: 'IDENTIFIER',
422
+ value,
423
+ line: this.line,
424
+ column: startColumn,
425
+ };
426
+ }
427
+ isDigit(char) {
428
+ return char >= '0' && char <= '9';
429
+ }
430
+ isAlpha(char) {
431
+ return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z');
432
+ }
433
+ isIdentifierStart(char) {
434
+ return this.isAlpha(char) || char === '_';
435
+ }
436
+ isIdentifierPart(char) {
437
+ return this.isIdentifierStart(char) || this.isDigit(char) || char === '-';
438
+ }
439
+ }
440
+ // =============================================================================
441
+ // PARSER
442
+ // =============================================================================
443
+ export class HoloScriptPlusParser {
444
+ constructor(options = {}) {
445
+ this.tokens = [];
446
+ this.pos = 0;
447
+ this.errors = [];
448
+ this.warnings = [];
449
+ this.imports = [];
450
+ this.hasState = false;
451
+ this.hasVRTraits = false;
452
+ this.hasControlFlow = false;
453
+ this.compiledExpressions = new Map();
454
+ this.options = {
455
+ enableVRTraits: true,
456
+ enableTypeScriptImports: true,
457
+ strict: false,
458
+ ...options,
459
+ };
460
+ }
461
+ parse(source) {
462
+ // Reset state
463
+ this.errors = [];
464
+ this.warnings = [];
465
+ this.imports = [];
466
+ this.hasState = false;
467
+ this.hasVRTraits = false;
468
+ this.hasControlFlow = false;
469
+ this.compiledExpressions = new Map();
470
+ this.pos = 0;
471
+ // Tokenize
472
+ const lexer = new Lexer(source);
473
+ this.tokens = lexer.tokenize();
474
+ // Parse root node
475
+ const root = this.parseDocument();
476
+ // Build AST
477
+ const ast = {
478
+ version: '1.0',
479
+ root,
480
+ imports: this.imports,
481
+ hasState: this.hasState,
482
+ hasVRTraits: this.hasVRTraits,
483
+ hasControlFlow: this.hasControlFlow,
484
+ };
485
+ return {
486
+ ast,
487
+ compiledExpressions: this.compiledExpressions,
488
+ requiredCompanions: this.imports.map((i) => i.path),
489
+ features: {
490
+ state: this.hasState,
491
+ vrTraits: this.hasVRTraits,
492
+ loops: this.hasControlFlow,
493
+ conditionals: this.hasControlFlow,
494
+ lifecycleHooks: root.directives.some((d) => d.type === 'lifecycle'),
495
+ },
496
+ warnings: this.warnings,
497
+ errors: this.errors,
498
+ };
499
+ }
500
+ parseDocument() {
501
+ this.skipNewlines();
502
+ const directives = [];
503
+ while (this.check('AT')) {
504
+ const directive = this.parseDirective();
505
+ if (directive) {
506
+ directives.push(directive);
507
+ }
508
+ this.skipNewlines();
509
+ }
510
+ const root = this.parseNode();
511
+ root.directives = [...directives, ...root.directives];
512
+ return root;
513
+ }
514
+ parseNode() {
515
+ const startToken = this.current();
516
+ const type = this.expect('IDENTIFIER', 'Expected element type').value;
517
+ let id;
518
+ if (this.check('HASH')) {
519
+ this.advance();
520
+ id = this.expect('IDENTIFIER', 'Expected ID after #').value;
521
+ }
522
+ const properties = {};
523
+ const directives = [];
524
+ const traits = new Map();
525
+ while (!this.check('LBRACE') && !this.check('NEWLINE') && !this.check('EOF')) {
526
+ if (this.check('AT')) {
527
+ const directive = this.parseDirective();
528
+ if (directive) {
529
+ if (directive.type === 'trait') {
530
+ traits.set(directive.name, directive.config);
531
+ this.hasVRTraits = true;
532
+ }
533
+ else {
534
+ directives.push(directive);
535
+ }
536
+ }
537
+ }
538
+ else if (this.check('IDENTIFIER')) {
539
+ const key = this.advance().value;
540
+ if (this.check('COLON') || this.check('EQUALS')) {
541
+ this.advance();
542
+ properties[key] = this.parseValue();
543
+ }
544
+ else {
545
+ properties[key] = true;
546
+ }
547
+ }
548
+ else {
549
+ break;
550
+ }
551
+ }
552
+ const children = [];
553
+ if (this.check('LBRACE')) {
554
+ this.advance();
555
+ this.skipNewlines();
556
+ while (!this.check('RBRACE') && !this.check('EOF')) {
557
+ if (this.check('AT')) {
558
+ const directive = this.parseDirective();
559
+ if (directive) {
560
+ if (directive.type === 'trait') {
561
+ traits.set(directive.name, directive.config);
562
+ this.hasVRTraits = true;
563
+ }
564
+ else {
565
+ directives.push(directive);
566
+ }
567
+ }
568
+ }
569
+ else if (this.check('IDENTIFIER')) {
570
+ const saved = this.pos;
571
+ const name = this.advance().value;
572
+ if (this.check('COLON') || this.check('EQUALS')) {
573
+ this.advance();
574
+ properties[name] = this.parseValue();
575
+ }
576
+ else {
577
+ this.pos = saved;
578
+ children.push(this.parseNode());
579
+ }
580
+ }
581
+ else {
582
+ this.skipNewlines();
583
+ if (this.check('RBRACE') || this.check('EOF'))
584
+ break;
585
+ this.advance();
586
+ }
587
+ this.skipNewlines();
588
+ }
589
+ this.expect('RBRACE', 'Expected }');
590
+ }
591
+ return {
592
+ type,
593
+ id,
594
+ properties,
595
+ directives,
596
+ children,
597
+ traits,
598
+ loc: {
599
+ start: { line: startToken.line, column: startToken.column },
600
+ end: { line: this.current().line, column: this.current().column },
601
+ },
602
+ };
603
+ }
604
+ parseDirective() {
605
+ this.expect('AT', 'Expected @');
606
+ const name = this.expect('IDENTIFIER', 'Expected directive name').value;
607
+ if (VR_TRAITS.includes(name)) {
608
+ if (!this.options.enableVRTraits) {
609
+ this.warn(`VR trait @${name} is disabled`);
610
+ return null;
611
+ }
612
+ const config = this.parseTraitConfig();
613
+ return { type: 'trait', name: name, config };
614
+ }
615
+ if (LIFECYCLE_HOOKS.includes(name)) {
616
+ const params = [];
617
+ if (this.check('LPAREN')) {
618
+ this.advance();
619
+ while (!this.check('RPAREN') && !this.check('EOF')) {
620
+ params.push(this.expect('IDENTIFIER', 'Expected parameter name').value);
621
+ if (this.check('COMMA'))
622
+ this.advance();
623
+ }
624
+ this.expect('RPAREN', 'Expected )');
625
+ }
626
+ let body = '';
627
+ if (this.check('ARROW')) {
628
+ this.advance();
629
+ body = this.parseInlineExpression();
630
+ }
631
+ else if (this.check('LBRACE')) {
632
+ body = this.parseCodeBlock();
633
+ }
634
+ return {
635
+ type: 'lifecycle',
636
+ hook: name,
637
+ params,
638
+ body,
639
+ };
640
+ }
641
+ if (name === 'state') {
642
+ this.hasState = true;
643
+ const body = this.parseStateBlock();
644
+ return { type: 'state', body };
645
+ }
646
+ if (name === 'for') {
647
+ this.hasControlFlow = true;
648
+ const variable = this.expect('IDENTIFIER', 'Expected variable name').value;
649
+ this.expect('IDENTIFIER', 'Expected "in"');
650
+ const iterable = this.parseInlineExpression();
651
+ const body = this.parseControlFlowBody();
652
+ return { type: 'for', variable, iterable, body };
653
+ }
654
+ if (name === 'if') {
655
+ this.hasControlFlow = true;
656
+ const condition = this.parseInlineExpression();
657
+ const body = this.parseControlFlowBody();
658
+ let elseBody;
659
+ this.skipNewlines();
660
+ if (this.check('AT')) {
661
+ const saved = this.pos;
662
+ this.advance();
663
+ if (this.check('IDENTIFIER') && this.current().value === 'else') {
664
+ this.advance();
665
+ elseBody = this.parseControlFlowBody();
666
+ }
667
+ else {
668
+ this.pos = saved;
669
+ }
670
+ }
671
+ return { type: 'if', condition, body, else: elseBody };
672
+ }
673
+ if (name === 'import') {
674
+ if (!this.options.enableTypeScriptImports) {
675
+ this.warn('@import is disabled');
676
+ return null;
677
+ }
678
+ const path = this.expect('STRING', 'Expected import path').value;
679
+ let alias = path.split('/').pop()?.replace(/\.[^.]+$/, '') || 'import';
680
+ if (this.check('IDENTIFIER') && this.current().value === 'as') {
681
+ this.advance();
682
+ alias = this.expect('IDENTIFIER', 'Expected alias').value;
683
+ }
684
+ this.imports.push({ path, alias });
685
+ return { type: 'import', path, alias };
686
+ }
687
+ if (this.options.strict) {
688
+ this.error(`Unknown directive @${name}`);
689
+ }
690
+ else {
691
+ this.warn(`Unknown directive @${name}`);
692
+ }
693
+ return null;
694
+ }
695
+ parseTraitConfig() {
696
+ const config = {};
697
+ if (this.check('LPAREN')) {
698
+ this.advance();
699
+ while (!this.check('RPAREN') && !this.check('EOF')) {
700
+ const key = this.expect('IDENTIFIER', 'Expected property name').value;
701
+ if (this.check('COLON') || this.check('EQUALS')) {
702
+ this.advance();
703
+ config[key] = this.parseValue();
704
+ }
705
+ else {
706
+ config[key] = true;
707
+ }
708
+ if (this.check('COMMA'))
709
+ this.advance();
710
+ }
711
+ this.expect('RPAREN', 'Expected )');
712
+ }
713
+ return config;
714
+ }
715
+ parseStateBlock() {
716
+ const state = {};
717
+ if (this.check('LBRACE')) {
718
+ this.advance();
719
+ this.skipNewlines();
720
+ while (!this.check('RBRACE') && !this.check('EOF')) {
721
+ const key = this.expect('IDENTIFIER', 'Expected state variable name').value;
722
+ if (this.check('COLON') || this.check('EQUALS')) {
723
+ this.advance();
724
+ state[key] = this.parseValue();
725
+ }
726
+ else {
727
+ state[key] = null;
728
+ }
729
+ this.skipNewlines();
730
+ }
731
+ this.expect('RBRACE', 'Expected }');
732
+ }
733
+ return state;
734
+ }
735
+ parseControlFlowBody() {
736
+ const nodes = [];
737
+ if (this.check('LBRACE')) {
738
+ this.advance();
739
+ this.skipNewlines();
740
+ while (!this.check('RBRACE') && !this.check('EOF')) {
741
+ if (this.check('AT')) {
742
+ const directive = this.parseDirective();
743
+ if (directive && directive.type === 'for') {
744
+ nodes.push({
745
+ type: 'fragment',
746
+ properties: {},
747
+ directives: [directive],
748
+ children: [],
749
+ traits: new Map(),
750
+ });
751
+ }
752
+ }
753
+ else if (this.check('IDENTIFIER')) {
754
+ nodes.push(this.parseNode());
755
+ }
756
+ this.skipNewlines();
757
+ }
758
+ this.expect('RBRACE', 'Expected }');
759
+ }
760
+ return nodes;
761
+ }
762
+ parseCodeBlock() {
763
+ let code = '';
764
+ let braceDepth = 0;
765
+ if (this.check('LBRACE')) {
766
+ this.advance();
767
+ braceDepth = 1;
768
+ while (braceDepth > 0 && !this.check('EOF')) {
769
+ const token = this.advance();
770
+ if (token.type === 'LBRACE') {
771
+ braceDepth++;
772
+ code += '{';
773
+ }
774
+ else if (token.type === 'RBRACE') {
775
+ braceDepth--;
776
+ if (braceDepth > 0) {
777
+ code += '}';
778
+ }
779
+ }
780
+ else {
781
+ code += token.value;
782
+ if (token.type === 'NEWLINE') {
783
+ code += '\n';
784
+ }
785
+ else {
786
+ code += ' ';
787
+ }
788
+ }
789
+ }
790
+ }
791
+ return code.trim();
792
+ }
793
+ parseInlineExpression() {
794
+ let expr = '';
795
+ while (!this.check('NEWLINE') &&
796
+ !this.check('LBRACE') &&
797
+ !this.check('EOF')) {
798
+ const token = this.advance();
799
+ expr += token.value + ' ';
800
+ }
801
+ return expr.trim();
802
+ }
803
+ parseValue() {
804
+ const token = this.current();
805
+ if (token.type === 'STRING') {
806
+ this.advance();
807
+ return token.value;
808
+ }
809
+ if (token.type === 'NUMBER') {
810
+ this.advance();
811
+ const match = token.value.match(/^(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)(.*)?$/i);
812
+ if (match) {
813
+ const num = parseFloat(match[1]);
814
+ const unit = match[2];
815
+ if (unit) {
816
+ return `${num}${unit}`;
817
+ }
818
+ return num;
819
+ }
820
+ return parseFloat(token.value);
821
+ }
822
+ if (token.type === 'BOOLEAN') {
823
+ this.advance();
824
+ return token.value === 'true';
825
+ }
826
+ if (token.type === 'NULL') {
827
+ this.advance();
828
+ return null;
829
+ }
830
+ if (token.type === 'EXPRESSION') {
831
+ this.advance();
832
+ const exprId = `expr_${this.compiledExpressions.size}`;
833
+ this.compiledExpressions.set(exprId, token.value);
834
+ return { __expr: exprId, __raw: token.value };
835
+ }
836
+ if (token.type === 'LBRACKET') {
837
+ return this.parseArray();
838
+ }
839
+ if (token.type === 'LBRACE') {
840
+ return this.parseObject();
841
+ }
842
+ if (token.type === 'IDENTIFIER') {
843
+ this.advance();
844
+ return { __ref: token.value };
845
+ }
846
+ return null;
847
+ }
848
+ parseArray() {
849
+ const arr = [];
850
+ this.expect('LBRACKET', 'Expected [');
851
+ while (!this.check('RBRACKET') && !this.check('EOF')) {
852
+ arr.push(this.parseValue());
853
+ if (this.check('COMMA'))
854
+ this.advance();
855
+ this.skipNewlines();
856
+ }
857
+ this.expect('RBRACKET', 'Expected ]');
858
+ return arr;
859
+ }
860
+ parseObject() {
861
+ const obj = {};
862
+ this.expect('LBRACE', 'Expected {');
863
+ this.skipNewlines();
864
+ while (!this.check('RBRACE') && !this.check('EOF')) {
865
+ const key = this.expect('IDENTIFIER', 'Expected property name').value;
866
+ if (this.check('COLON') || this.check('EQUALS')) {
867
+ this.advance();
868
+ obj[key] = this.parseValue();
869
+ }
870
+ else {
871
+ obj[key] = true;
872
+ }
873
+ if (this.check('COMMA'))
874
+ this.advance();
875
+ this.skipNewlines();
876
+ }
877
+ this.expect('RBRACE', 'Expected }');
878
+ return obj;
879
+ }
880
+ current() {
881
+ return this.tokens[this.pos] || { type: 'EOF', value: '', line: 0, column: 0 };
882
+ }
883
+ check(type) {
884
+ return this.current().type === type;
885
+ }
886
+ advance() {
887
+ const token = this.current();
888
+ if (this.pos < this.tokens.length) {
889
+ this.pos++;
890
+ }
891
+ return token;
892
+ }
893
+ expect(type, message) {
894
+ if (!this.check(type)) {
895
+ this.error(`${message}. Got ${this.current().type} "${this.current().value}"`);
896
+ return { type, value: '', line: this.current().line, column: this.current().column };
897
+ }
898
+ return this.advance();
899
+ }
900
+ skipNewlines() {
901
+ while (this.check('NEWLINE') || this.check('INDENT') || this.check('DEDENT')) {
902
+ this.advance();
903
+ }
904
+ }
905
+ error(message) {
906
+ const token = this.current();
907
+ this.errors.push({
908
+ message,
909
+ line: token.line,
910
+ column: token.column,
911
+ });
912
+ }
913
+ warn(message) {
914
+ const token = this.current();
915
+ this.warnings.push({
916
+ message,
917
+ line: token.line,
918
+ column: token.column,
919
+ });
920
+ }
921
+ }
922
+ export function createParser(options) {
923
+ return new HoloScriptPlusParser(options);
924
+ }
925
+ export function parse(source, options) {
926
+ const parser = createParser(options);
927
+ return parser.parse(source);
928
+ }