pulse-js-framework 1.4.3 → 1.4.5

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.
@@ -384,39 +384,36 @@ export class Parser {
384
384
  return new ASTNode(NodeType.Property, { name: name.value, value });
385
385
  }
386
386
 
387
+ /**
388
+ * Try to parse a literal token (STRING, NUMBER, TRUE, FALSE, NULL)
389
+ * Returns the AST node or null if not a literal
390
+ */
391
+ tryParseLiteral() {
392
+ const token = this.current();
393
+ if (!token) return null;
394
+
395
+ const literalMap = {
396
+ [TokenType.STRING]: () => new ASTNode(NodeType.Literal, { value: this.advance().value, raw: token.raw }),
397
+ [TokenType.NUMBER]: () => new ASTNode(NodeType.Literal, { value: this.advance().value }),
398
+ [TokenType.TRUE]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: true })),
399
+ [TokenType.FALSE]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: false })),
400
+ [TokenType.NULL]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: null }))
401
+ };
402
+
403
+ return literalMap[token.type]?.() || null;
404
+ }
405
+
387
406
  /**
388
407
  * Parse a value (literal, object, array, etc.)
389
408
  */
390
409
  parseValue() {
391
- if (this.is(TokenType.LBRACE)) {
392
- return this.parseObjectLiteral();
393
- }
394
- if (this.is(TokenType.LBRACKET)) {
395
- return this.parseArrayLiteral();
396
- }
397
- if (this.is(TokenType.STRING)) {
398
- const token = this.advance();
399
- return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
400
- }
401
- if (this.is(TokenType.NUMBER)) {
402
- const token = this.advance();
403
- return new ASTNode(NodeType.Literal, { value: token.value });
404
- }
405
- if (this.is(TokenType.TRUE)) {
406
- this.advance();
407
- return new ASTNode(NodeType.Literal, { value: true });
408
- }
409
- if (this.is(TokenType.FALSE)) {
410
- this.advance();
411
- return new ASTNode(NodeType.Literal, { value: false });
412
- }
413
- if (this.is(TokenType.NULL)) {
414
- this.advance();
415
- return new ASTNode(NodeType.Literal, { value: null });
416
- }
417
- if (this.is(TokenType.IDENT)) {
418
- return this.parseIdentifierOrExpression();
419
- }
410
+ if (this.is(TokenType.LBRACE)) return this.parseObjectLiteral();
411
+ if (this.is(TokenType.LBRACKET)) return this.parseArrayLiteral();
412
+
413
+ const literal = this.tryParseLiteral();
414
+ if (literal) return literal;
415
+
416
+ if (this.is(TokenType.IDENT)) return this.parseIdentifierOrExpression();
420
417
 
421
418
  throw new Error(
422
419
  `Unexpected token ${this.current()?.type} in value at line ${this.current()?.line}`
@@ -612,32 +609,18 @@ export class Parser {
612
609
 
613
610
  let value;
614
611
  if (this.is(TokenType.LBRACE)) {
615
- // Expression prop: name={expression}
616
- this.advance(); // consume {
612
+ this.advance();
617
613
  value = this.parseExpression();
618
614
  this.expect(TokenType.RBRACE);
619
- } else if (this.is(TokenType.STRING)) {
620
- // String prop: name="value"
621
- const token = this.advance();
622
- value = new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
623
- } else if (this.is(TokenType.NUMBER)) {
624
- // Number prop: name=123
625
- const token = this.advance();
626
- value = new ASTNode(NodeType.Literal, { value: token.value });
627
- } else if (this.is(TokenType.TRUE)) {
628
- this.advance();
629
- value = new ASTNode(NodeType.Literal, { value: true });
630
- } else if (this.is(TokenType.FALSE)) {
631
- this.advance();
632
- value = new ASTNode(NodeType.Literal, { value: false });
633
- } else if (this.is(TokenType.NULL)) {
634
- this.advance();
635
- value = new ASTNode(NodeType.Literal, { value: null });
636
- } else if (this.is(TokenType.IDENT)) {
637
- // Identifier prop: name=someVar
638
- value = this.parseIdentifierOrExpression();
639
615
  } else {
640
- throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
616
+ value = this.tryParseLiteral();
617
+ if (!value) {
618
+ if (this.is(TokenType.IDENT)) {
619
+ value = this.parseIdentifierOrExpression();
620
+ } else {
621
+ throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
622
+ }
623
+ }
641
624
  }
642
625
 
643
626
  return new ASTNode(NodeType.Property, { name: name.value, value });
@@ -885,80 +868,39 @@ export class Parser {
885
868
  }
886
869
 
887
870
  /**
888
- * Parse OR expression
889
- */
890
- parseOrExpression() {
891
- let left = this.parseAndExpression();
892
-
893
- while (this.is(TokenType.OR)) {
894
- this.advance();
895
- const right = this.parseAndExpression();
896
- left = new ASTNode(NodeType.BinaryExpression, { operator: '||', left, right });
897
- }
898
-
899
- return left;
900
- }
901
-
902
- /**
903
- * Parse AND expression
871
+ * Binary operator precedence table (higher = binds tighter)
904
872
  */
905
- parseAndExpression() {
906
- let left = this.parseComparisonExpression();
907
-
908
- while (this.is(TokenType.AND)) {
909
- this.advance();
910
- const right = this.parseComparisonExpression();
911
- left = new ASTNode(NodeType.BinaryExpression, { operator: '&&', left, right });
912
- }
913
-
914
- return left;
915
- }
873
+ static BINARY_OPS = [
874
+ { ops: [TokenType.OR], name: 'or' },
875
+ { ops: [TokenType.AND], name: 'and' },
876
+ { ops: [TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
877
+ TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE], name: 'comparison' },
878
+ { ops: [TokenType.PLUS, TokenType.MINUS], name: 'additive' },
879
+ { ops: [TokenType.STAR, TokenType.SLASH, TokenType.PERCENT], name: 'multiplicative' }
880
+ ];
916
881
 
917
882
  /**
918
- * Parse comparison expression
883
+ * Generic binary expression parser using precedence climbing
919
884
  */
920
- parseComparisonExpression() {
921
- let left = this.parseAdditiveExpression();
922
-
923
- while (this.isAny(TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
924
- TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE)) {
925
- const operator = this.advance().value;
926
- const right = this.parseAdditiveExpression();
927
- left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
885
+ parseBinaryExpr(level = 0) {
886
+ if (level >= Parser.BINARY_OPS.length) {
887
+ return this.parseUnaryExpression();
928
888
  }
929
889
 
930
- return left;
931
- }
932
-
933
- /**
934
- * Parse additive expression
935
- */
936
- parseAdditiveExpression() {
937
- let left = this.parseMultiplicativeExpression();
890
+ let left = this.parseBinaryExpr(level + 1);
891
+ const { ops } = Parser.BINARY_OPS[level];
938
892
 
939
- while (this.isAny(TokenType.PLUS, TokenType.MINUS)) {
893
+ while (this.isAny(...ops)) {
940
894
  const operator = this.advance().value;
941
- const right = this.parseMultiplicativeExpression();
895
+ const right = this.parseBinaryExpr(level + 1);
942
896
  left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
943
897
  }
944
898
 
945
899
  return left;
946
900
  }
947
901
 
948
- /**
949
- * Parse multiplicative expression
950
- */
951
- parseMultiplicativeExpression() {
952
- let left = this.parseUnaryExpression();
953
-
954
- while (this.isAny(TokenType.STAR, TokenType.SLASH, TokenType.PERCENT)) {
955
- const operator = this.advance().value;
956
- const right = this.parseUnaryExpression();
957
- left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
958
- }
959
-
960
- return left;
961
- }
902
+ /** Parse OR expression (entry point for binary expressions) */
903
+ parseOrExpression() { return this.parseBinaryExpr(0); }
962
904
 
963
905
  /**
964
906
  * Parse unary expression
@@ -1048,30 +990,9 @@ export class Parser {
1048
990
  return new ASTNode(NodeType.SpreadElement, { argument });
1049
991
  }
1050
992
 
1051
- if (this.is(TokenType.NUMBER)) {
1052
- const token = this.advance();
1053
- return new ASTNode(NodeType.Literal, { value: token.value });
1054
- }
1055
-
1056
- if (this.is(TokenType.STRING)) {
1057
- const token = this.advance();
1058
- return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
1059
- }
1060
-
1061
- if (this.is(TokenType.TRUE)) {
1062
- this.advance();
1063
- return new ASTNode(NodeType.Literal, { value: true });
1064
- }
1065
-
1066
- if (this.is(TokenType.FALSE)) {
1067
- this.advance();
1068
- return new ASTNode(NodeType.Literal, { value: false });
1069
- }
1070
-
1071
- if (this.is(TokenType.NULL)) {
1072
- this.advance();
1073
- return new ASTNode(NodeType.Literal, { value: null });
1074
- }
993
+ // Try parsing a literal (NUMBER, STRING, TRUE, FALSE, NULL)
994
+ const literal = this.tryParseLiteral();
995
+ if (literal) return literal;
1075
996
 
1076
997
  // In expressions, SELECTOR tokens should be treated as IDENT
1077
998
  // This happens when identifiers like 'selectedCategory' are followed by space in view context
@@ -11,12 +11,17 @@
11
11
 
12
12
  import { NodeType } from './parser.js';
13
13
 
14
- /**
15
- * Generate a unique scope ID for CSS scoping
16
- */
17
- function generateScopeId() {
18
- return 'p' + Math.random().toString(36).substring(2, 8);
19
- }
14
+ /** Generate a unique scope ID for CSS scoping */
15
+ const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 8);
16
+
17
+ // Token spacing constants (shared across methods)
18
+ const NO_SPACE_AFTER = new Set(['DOT', 'LPAREN', 'LBRACKET', 'LBRACE', 'NOT', 'SPREAD', '.', '(', '[', '{', '!', '~', '...']);
19
+ const NO_SPACE_BEFORE = new Set(['DOT', 'RPAREN', 'RBRACKET', 'RBRACE', 'SEMICOLON', 'COMMA', 'INCREMENT', 'DECREMENT', 'LPAREN', 'LBRACKET', '.', ')', ']', '}', ';', ',', '++', '--', '(', '[']);
20
+ const PUNCT_NO_SPACE_BEFORE = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
21
+ const PUNCT_NO_SPACE_AFTER = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
22
+ const STATEMENT_KEYWORDS = new Set(['let', 'const', 'var', 'return', 'if', 'else', 'for', 'while', 'switch', 'throw', 'try', 'catch', 'finally']);
23
+ const BUILTIN_FUNCTIONS = new Set(['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'alert', 'confirm', 'prompt', 'console', 'document', 'window', 'Math', 'JSON', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'fetch']);
24
+ const STATEMENT_TOKEN_TYPES = new Set(['IF', 'FOR', 'EACH']);
20
25
 
21
26
  /**
22
27
  * Transformer class
@@ -24,17 +29,12 @@ function generateScopeId() {
24
29
  export class Transformer {
25
30
  constructor(ast, options = {}) {
26
31
  this.ast = ast;
27
- this.options = {
28
- runtime: 'pulse-js-framework/runtime',
29
- minify: false,
30
- scopeStyles: true, // Enable CSS scoping by default
31
- ...options
32
- };
32
+ this.options = { runtime: 'pulse-js-framework/runtime', minify: false, scopeStyles: true, ...options };
33
33
  this.stateVars = new Set();
34
- this.propVars = new Set(); // Track prop names
35
- this.propDefaults = new Map(); // Track prop default values
34
+ this.propVars = new Set();
35
+ this.propDefaults = new Map();
36
36
  this.actionNames = new Set();
37
- this.importedComponents = new Map(); // Map of local name -> import info
37
+ this.importedComponents = new Map();
38
38
  this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
39
39
  }
40
40
 
@@ -283,30 +283,30 @@ export class Transformer {
283
283
  return lines.join('\n');
284
284
  }
285
285
 
286
- /**
287
- * Transform router guard body - handles store references
288
- */
286
+ /** Helper to emit token value with proper string/template handling */
287
+ emitToken(token) {
288
+ if (token.type === 'STRING') return token.raw || JSON.stringify(token.value);
289
+ if (token.type === 'TEMPLATE') return token.raw || ('`' + token.value + '`');
290
+ return token.value;
291
+ }
292
+
293
+ /** Helper to check if space needed between tokens */
294
+ needsSpace(token, nextToken) {
295
+ if (!nextToken) return false;
296
+ return !PUNCT_NO_SPACE_BEFORE.includes(nextToken.type) && !PUNCT_NO_SPACE_AFTER.includes(token.type);
297
+ }
298
+
299
+ /** Transform router guard body - handles store references */
289
300
  transformRouterGuardBody(tokens) {
290
301
  let code = '';
291
302
  for (let i = 0; i < tokens.length; i++) {
292
303
  const token = tokens[i];
293
- if (token.type === 'STRING') {
294
- code += token.raw || JSON.stringify(token.value);
295
- } else if (token.type === 'TEMPLATE') {
296
- code += token.raw || ('`' + token.value + '`');
297
- } else if (token.value === 'store' && tokens[i + 1]?.type === 'DOT') {
298
- // Transform store.xxx to $store.xxx for accessing combined store
304
+ if (token.value === 'store' && tokens[i + 1]?.type === 'DOT') {
299
305
  code += '$store';
300
306
  } else {
301
- code += token.value;
302
- }
303
- // Add space between tokens unless it's punctuation (before or after)
304
- const nextToken = tokens[i + 1];
305
- const noPunctBefore = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
306
- const noPunctAfter = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
307
- if (nextToken && !noPunctBefore.includes(nextToken.type) && !noPunctAfter.includes(token.type)) {
308
- code += ' ';
307
+ code += this.emitToken(token);
309
308
  }
309
+ if (this.needsSpace(token, tokens[i + 1])) code += ' ';
310
310
  }
311
311
  return code.trim();
312
312
  }
@@ -374,49 +374,22 @@ export class Transformer {
374
374
  return lines.join('\n');
375
375
  }
376
376
 
377
- /**
378
- * Transform store action body (this.x = y -> store.x.set(y))
379
- */
377
+ /** Transform store action body (this.x = y -> store.x.set(y)) */
380
378
  transformStoreActionBody(tokens) {
381
379
  let code = '';
382
380
  for (let i = 0; i < tokens.length; i++) {
383
381
  const token = tokens[i];
384
- // Transform 'this' to 'store'
385
- if (token.value === 'this') {
386
- code += 'store';
387
- } else if (token.type === 'STRING') {
388
- code += token.raw || JSON.stringify(token.value);
389
- } else if (token.type === 'TEMPLATE') {
390
- code += token.raw || ('`' + token.value + '`');
391
- } else if (token.type === 'COLON') {
392
- // Handle colon with proper spacing (ternary operator)
393
- code += ' : ';
394
- } else {
395
- code += token.value;
396
- }
397
- // Add space between tokens unless it's punctuation (before or after)
398
- const nextToken = tokens[i + 1];
399
- const noPunctBefore = ['DOT', 'LPAREN', 'RPAREN', 'LBRACKET', 'RBRACKET', 'SEMICOLON', 'COMMA', 'COLON'];
400
- const noPunctAfter = ['DOT', 'LPAREN', 'LBRACKET', 'NOT', 'COLON'];
401
- if (nextToken && !noPunctBefore.includes(nextToken.type) && !noPunctAfter.includes(token.type)) {
402
- code += ' ';
403
- }
382
+ if (token.value === 'this') code += 'store';
383
+ else if (token.type === 'COLON') code += ' : ';
384
+ else code += this.emitToken(token);
385
+ if (this.needsSpace(token, tokens[i + 1])) code += ' ';
404
386
  }
405
-
406
- // Post-process: store.x = y -> store.x.set(y)
407
- code = code.replace(/store\.(\w+)\s*=\s*([^;]+)/g, 'store.$1.set($2)');
408
-
409
- return code.trim();
387
+ return code.replace(/store\.(\w+)\s*=\s*([^;]+)/g, 'store.$1.set($2)').trim();
410
388
  }
411
389
 
412
- /**
413
- * Transform store getter body (this.x -> store.x.get())
414
- */
390
+ /** Transform store getter body (this.x -> store.x.get()) */
415
391
  transformStoreGetterBody(tokens) {
416
- let code = this.transformStoreActionBody(tokens);
417
- // Transform store.x reads to store.x.get() (but not store.x.set or store.x.get)
418
- code = code.replace(/store\.(\w+)(?!\.(?:get|set)\()/g, 'store.$1.get()');
419
- return code;
392
+ return this.transformStoreActionBody(tokens).replace(/store\.(\w+)(?!\.(?:get|set)\()/g, 'store.$1.get()');
420
393
  }
421
394
 
422
395
  /**
@@ -479,76 +452,20 @@ export class Transformer {
479
452
  let code = '';
480
453
  let lastToken = null;
481
454
  let lastNonSpaceToken = null;
482
- const statementKeywords = ['let', 'const', 'var', 'return', 'if', 'else', 'for', 'while', 'switch', 'throw', 'try', 'catch', 'finally'];
483
- const builtinFunctions = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'alert', 'confirm', 'prompt', 'console', 'document', 'window', 'Math', 'JSON', 'Date', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'fetch'];
484
-
485
- // Tokens that should not have space after them
486
- const noSpaceAfterTypes = new Set(['DOT', 'LPAREN', 'LBRACKET', 'LBRACE', 'NOT', 'SPREAD']);
487
- const noSpaceAfterValues = new Set(['.', '(', '[', '{', '!', '~', '...']);
488
-
489
- // Tokens that should not have space before them
490
- const noSpaceBeforeTypes = new Set(['DOT', 'RPAREN', 'RBRACKET', 'RBRACE', 'SEMICOLON', 'COMMA', 'INCREMENT', 'DECREMENT', 'LPAREN', 'LBRACKET']);
491
- const noSpaceBeforeValues = new Set(['.', ')', ']', '}', ';', ',', '++', '--', '(', '[']);
492
-
493
- // Check if token is a statement starter that the regex won't handle
494
- // (i.e., not a state variable assignment - those are handled by regex)
495
- // Statement keywords that have their own token types
496
- // Note: ELSE is excluded because it follows IF and should not have semicolon before it
497
- const statementTokenTypes = new Set(['IF', 'FOR', 'EACH']);
498
455
 
499
456
  const needsManualSemicolon = (token, nextToken, lastNonSpace) => {
500
- if (!token) return false;
501
-
502
- // Don't add semicolon after 'new' keyword (e.g., new Date())
503
- if (lastNonSpace?.value === 'new') return false;
504
-
505
- // Statement keywords with dedicated token types (if, else, for, etc.)
506
- if (statementTokenTypes.has(token.type)) return true;
507
-
508
- // Only process IDENT tokens from here
457
+ if (!token || lastNonSpace?.value === 'new') return false;
458
+ if (STATEMENT_TOKEN_TYPES.has(token.type)) return true;
509
459
  if (token.type !== 'IDENT') return false;
510
-
511
- // Statement keywords (let, const, var, return, etc.)
512
- if (statementKeywords.includes(token.value)) return true;
513
-
514
- // State variable assignment (stateVar = value)
515
- // These need semicolons BEFORE them when following a statement end
516
- // The regex adds semicolons AFTER, but not before
517
- if (this.stateVars.has(token.value) && nextToken?.type === 'EQ') {
518
- return true;
519
- }
520
-
521
- // Builtin function call or action call (not state var assignment)
522
- if (nextToken?.type === 'LPAREN') {
523
- if (builtinFunctions.includes(token.value)) return true;
524
- if (this.actionNames.has(token.value)) return true;
525
- }
526
-
527
- // Builtin method chain (e.g., document.body.classList.toggle(...))
528
- if (nextToken?.type === 'DOT' && builtinFunctions.includes(token.value)) {
529
- return true;
530
- }
531
-
460
+ if (STATEMENT_KEYWORDS.has(token.value)) return true;
461
+ if (this.stateVars.has(token.value) && nextToken?.type === 'EQ') return true;
462
+ if (nextToken?.type === 'LPAREN' && (BUILTIN_FUNCTIONS.has(token.value) || this.actionNames.has(token.value))) return true;
463
+ if (nextToken?.type === 'DOT' && BUILTIN_FUNCTIONS.has(token.value)) return true;
532
464
  return false;
533
465
  };
534
466
 
535
- // Check if previous context indicates end of statement
536
- const afterStatementEnd = (lastNonSpace) => {
537
- if (!lastNonSpace) return false;
538
- return lastNonSpace.type === 'RBRACE' ||
539
- lastNonSpace.type === 'RPAREN' ||
540
- lastNonSpace.type === 'RBRACKET' ||
541
- lastNonSpace.type === 'SEMICOLON' ||
542
- lastNonSpace.type === 'STRING' ||
543
- lastNonSpace.type === 'NUMBER' ||
544
- lastNonSpace.type === 'TRUE' ||
545
- lastNonSpace.type === 'FALSE' ||
546
- lastNonSpace.type === 'NULL' ||
547
- lastNonSpace.value === 'null' ||
548
- lastNonSpace.value === 'true' ||
549
- lastNonSpace.value === 'false' ||
550
- lastNonSpace.type === 'IDENT'; // Any identifier can end a statement (variables, function results, etc.)
551
- };
467
+ const STATEMENT_END_TYPES = new Set(['RBRACE', 'RPAREN', 'RBRACKET', 'SEMICOLON', 'STRING', 'NUMBER', 'TRUE', 'FALSE', 'NULL', 'IDENT']);
468
+ const afterStatementEnd = (t) => t && STATEMENT_END_TYPES.has(t.type);
552
469
 
553
470
  let afterIfCondition = false; // Track if we just closed an if(...) condition
554
471
 
@@ -604,43 +521,24 @@ export class Transformer {
604
521
  }
605
522
 
606
523
  // Decide whether to add space after this token
607
- let addSpace = true;
608
-
609
- // No space after certain tokens
610
- if (noSpaceAfterTypes.has(token.type) || noSpaceAfterValues.has(token.value)) {
611
- addSpace = false;
612
- }
613
-
614
- // No space before certain tokens (look ahead)
615
- if (nextToken && (noSpaceBeforeTypes.has(nextToken.type) || noSpaceBeforeValues.has(nextToken.value))) {
616
- addSpace = false;
617
- }
618
-
619
- if (addSpace && nextToken) {
620
- code += ' ';
621
- }
524
+ const noSpaceAfter = NO_SPACE_AFTER.has(token.type) || NO_SPACE_AFTER.has(token.value);
525
+ const noSpaceBefore = nextToken && (NO_SPACE_BEFORE.has(nextToken.type) || NO_SPACE_BEFORE.has(nextToken.value));
526
+ if (!noSpaceAfter && !noSpaceBefore && nextToken) code += ' ';
622
527
 
623
528
  lastToken = token;
624
529
  lastNonSpaceToken = token;
625
530
  }
626
531
 
627
- // Build patterns for boundaries
532
+ // Build patterns for state variable transformation
628
533
  const stateVarPattern = [...this.stateVars].join('|');
629
- const funcPattern = [...this.actionNames, ...builtinFunctions].join('|');
534
+ const funcPattern = [...this.actionNames, ...BUILTIN_FUNCTIONS].join('|');
535
+ const keywordsPattern = [...STATEMENT_KEYWORDS].join('|');
630
536
 
631
- // Transform state access - order matters!
632
- // First, replace state var assignments with boundary detection
633
- // Use multiple passes to handle the case where replacements change the boundaries
537
+ // Transform state var assignments: stateVar = value -> stateVar.set(value)
634
538
  for (const stateVar of this.stateVars) {
635
- // Replace standalone state var assignments: stateVar = value -> stateVar.set(value)
636
- // Use negative lookahead (?!=) to avoid matching === or ==
637
- // Stop at: next state var assignment (original or already replaced), function call, statement keyword, or end
638
- const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${statementKeywords.join('|')})\\b|;|$`;
639
- const assignPattern = new RegExp(
640
- `\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`,
641
- 'g'
642
- );
643
- code = code.replace(assignPattern, (_match, value) => `${stateVar}.set(${value.trim()});`);
539
+ const boundaryPattern = `\\s+(?:${stateVarPattern})(?:\\s*=(?!=)|\\s*\\.set\\()|\\s+(?:${funcPattern})\\s*\\(|\\s+(?:${keywordsPattern})\\b|;|$`;
540
+ const assignPattern = new RegExp(`\\b${stateVar}\\s*=(?!=)\\s*(.+?)(?=${boundaryPattern})`, 'g');
541
+ code = code.replace(assignPattern, (_, value) => `${stateVar}.set(${value.trim()});`);
644
542
  }
645
543
 
646
544
  // Clean up any double semicolons
@@ -700,44 +598,24 @@ export class Transformer {
700
598
  return lines.join('\n');
701
599
  }
702
600
 
703
- /**
704
- * Transform a view node (element, directive, slot, text)
705
- */
601
+ /** View node transformers lookup table */
602
+ static VIEW_NODE_HANDLERS = {
603
+ [NodeType.Element]: 'transformElement',
604
+ [NodeType.TextNode]: 'transformTextNode',
605
+ [NodeType.IfDirective]: 'transformIfDirective',
606
+ [NodeType.EachDirective]: 'transformEachDirective',
607
+ [NodeType.EventDirective]: 'transformEventDirective',
608
+ [NodeType.SlotElement]: 'transformSlot',
609
+ [NodeType.LinkDirective]: 'transformLinkDirective',
610
+ [NodeType.OutletDirective]: 'transformOutletDirective',
611
+ [NodeType.NavigateDirective]: 'transformNavigateDirective'
612
+ };
613
+
614
+ /** Transform a view node (element, directive, slot, text) */
706
615
  transformViewNode(node, indent = 0) {
707
- const pad = ' '.repeat(indent);
708
-
709
- switch (node.type) {
710
- case NodeType.Element:
711
- return this.transformElement(node, indent);
712
-
713
- case NodeType.TextNode:
714
- return this.transformTextNode(node, indent);
715
-
716
- case NodeType.IfDirective:
717
- return this.transformIfDirective(node, indent);
718
-
719
- case NodeType.EachDirective:
720
- return this.transformEachDirective(node, indent);
721
-
722
- case NodeType.EventDirective:
723
- return this.transformEventDirective(node, indent);
724
-
725
- case NodeType.SlotElement:
726
- return this.transformSlot(node, indent);
727
-
728
- // Router directives
729
- case NodeType.LinkDirective:
730
- return this.transformLinkDirective(node, indent);
731
-
732
- case NodeType.OutletDirective:
733
- return this.transformOutletDirective(node, indent);
734
-
735
- case NodeType.NavigateDirective:
736
- return this.transformNavigateDirective(node, indent);
737
-
738
- default:
739
- return `${pad}/* unknown node: ${node.type} */`;
740
- }
616
+ const handler = Transformer.VIEW_NODE_HANDLERS[node.type];
617
+ if (handler) return this[handler](node, indent);
618
+ return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
741
619
  }
742
620
 
743
621
  /**
package/index.js CHANGED
@@ -4,14 +4,20 @@
4
4
  * A declarative DOM framework with CSS selector-based structure
5
5
  */
6
6
 
7
+ import { readFileSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
7
11
  // Runtime exports
8
12
  export * from './runtime/index.js';
9
13
 
10
14
  // Compiler exports
11
15
  export { compile, parse, tokenize } from './compiler/index.js';
12
16
 
13
- // Version
14
- export const VERSION = '1.4.3';
17
+ // Version - read dynamically from package.json
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
20
+ export const VERSION = pkg.version;
15
21
 
16
22
  // Default export
17
23
  export default {