pulse-js-framework 1.7.26 → 1.7.29

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.
package/compiler/lexer.js CHANGED
@@ -85,13 +85,24 @@ export const TokenType = {
85
85
  PLUSPLUS: 'PLUSPLUS', // ++
86
86
  MINUSMINUS: 'MINUSMINUS', // --
87
87
  QUESTION: 'QUESTION', // ?
88
+ NULLISH: 'NULLISH', // ??
89
+ OPTIONAL_CHAIN: 'OPTIONAL_CHAIN', // ?.
88
90
  ARROW: 'ARROW', // =>
89
91
  SPREAD: 'SPREAD', // ...
92
+ // Logical/Nullish Assignment Operators (ES2021)
93
+ OR_ASSIGN: 'OR_ASSIGN', // ||=
94
+ AND_ASSIGN: 'AND_ASSIGN', // &&=
95
+ NULLISH_ASSIGN: 'NULLISH_ASSIGN', // ??=
96
+ PLUS_ASSIGN: 'PLUS_ASSIGN', // +=
97
+ MINUS_ASSIGN: 'MINUS_ASSIGN', // -=
98
+ STAR_ASSIGN: 'STAR_ASSIGN', // *=
99
+ SLASH_ASSIGN: 'SLASH_ASSIGN', // /=
90
100
 
91
101
  // Literals
92
102
  STRING: 'STRING',
93
103
  TEMPLATE: 'TEMPLATE', // Template literal `...`
94
104
  NUMBER: 'NUMBER',
105
+ BIGINT: 'BIGINT', // BigInt literal 123n
95
106
  TRUE: 'TRUE',
96
107
  FALSE: 'FALSE',
97
108
  NULL: 'NULL',
@@ -365,22 +376,96 @@ export class Lexer {
365
376
 
366
377
  /**
367
378
  * Read a number literal
379
+ * Supports:
380
+ * - Integers: 42
381
+ * - Decimals: 3.14
382
+ * - Scientific notation: 1e10, 1.5e-3
383
+ * - Numeric separators (ES2021): 1_000_000, 0xFF_FF_FF
384
+ * - BigInt literals (ES2020): 123n
385
+ * - Hex: 0xFF, Binary: 0b101, Octal: 0o777
368
386
  */
369
387
  readNumber() {
370
388
  const startLine = this.line;
371
389
  const startColumn = this.column;
372
390
  let value = '';
391
+ let rawValue = '';
392
+ let isBigInt = false;
393
+
394
+ // Check for hex, binary, or octal prefixes
395
+ if (this.current() === '0') {
396
+ rawValue += this.advance();
397
+ value += '0';
398
+
399
+ if (this.current() === 'x' || this.current() === 'X') {
400
+ // Hexadecimal
401
+ rawValue += this.advance();
402
+ value += 'x';
403
+ while (!this.isEOF() && /[0-9a-fA-F_]/.test(this.current())) {
404
+ const char = this.advance();
405
+ rawValue += char;
406
+ if (char !== '_') value += char; // Skip separators in actual value
407
+ }
408
+ // Check for BigInt suffix
409
+ if (this.current() === 'n') {
410
+ rawValue += this.advance();
411
+ isBigInt = true;
412
+ }
413
+ if (isBigInt) {
414
+ return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
415
+ }
416
+ return new Token(TokenType.NUMBER, parseInt(value, 16), startLine, startColumn, rawValue);
417
+ } else if (this.current() === 'b' || this.current() === 'B') {
418
+ // Binary
419
+ rawValue += this.advance();
420
+ value += 'b';
421
+ while (!this.isEOF() && /[01_]/.test(this.current())) {
422
+ const char = this.advance();
423
+ rawValue += char;
424
+ if (char !== '_') value += char;
425
+ }
426
+ if (this.current() === 'n') {
427
+ rawValue += this.advance();
428
+ isBigInt = true;
429
+ }
430
+ if (isBigInt) {
431
+ return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
432
+ }
433
+ return new Token(TokenType.NUMBER, parseInt(value.slice(2), 2), startLine, startColumn, rawValue);
434
+ } else if (this.current() === 'o' || this.current() === 'O') {
435
+ // Octal
436
+ rawValue += this.advance();
437
+ value += 'o';
438
+ while (!this.isEOF() && /[0-7_]/.test(this.current())) {
439
+ const char = this.advance();
440
+ rawValue += char;
441
+ if (char !== '_') value += char;
442
+ }
443
+ if (this.current() === 'n') {
444
+ rawValue += this.advance();
445
+ isBigInt = true;
446
+ }
447
+ if (isBigInt) {
448
+ return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
449
+ }
450
+ return new Token(TokenType.NUMBER, parseInt(value.slice(2), 8), startLine, startColumn, rawValue);
451
+ }
452
+ }
373
453
 
374
- // Integer part
375
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
376
- value += this.advance();
454
+ // Regular decimal number (or continuation of '0')
455
+ while (!this.isEOF() && /[0-9_]/.test(this.current())) {
456
+ const char = this.advance();
457
+ rawValue += char;
458
+ if (char !== '_') value += char;
377
459
  }
378
460
 
379
461
  // Decimal part
380
462
  if (this.current() === '.' && /[0-9]/.test(this.peek())) {
381
- value += this.advance(); // .
382
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
383
- value += this.advance();
463
+ rawValue += this.advance();
464
+ value += '.';
465
+ while (!this.isEOF() && /[0-9_]/.test(this.current())) {
466
+ const char = this.advance();
467
+ rawValue += char;
468
+ if (char !== '_') value += char;
384
469
  }
385
470
  }
386
471
 
@@ -394,18 +479,32 @@ export class Lexer {
394
479
  ((nextChar === '+' || nextChar === '-') && /[0-9]/.test(nextNextChar));
395
480
 
396
481
  if (isScientific) {
397
- value += this.advance(); // consume 'e' or 'E'
482
+ rawValue += this.advance();
483
+ value += 'e';
398
484
  if (this.current() === '+' || this.current() === '-') {
399
- value += this.advance();
485
+ const sign = this.advance();
486
+ rawValue += sign;
487
+ value += sign;
400
488
  }
401
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
402
- value += this.advance();
489
+ while (!this.isEOF() && /[0-9_]/.test(this.current())) {
490
+ const char = this.advance();
491
+ rawValue += char;
492
+ if (char !== '_') value += char;
403
493
  }
404
494
  }
405
495
  // If not scientific notation, leave 'e' for the next token (e.g., 'em' unit)
406
496
  }
407
497
 
408
- return new Token(TokenType.NUMBER, parseFloat(value), startLine, startColumn, value);
498
+ // Check for BigInt suffix 'n'
499
+ if (this.current() === 'n') {
500
+ rawValue += this.advance();
501
+ isBigInt = true;
502
+ }
503
+
504
+ if (isBigInt) {
505
+ return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
506
+ }
507
+ return new Token(TokenType.NUMBER, parseFloat(value), startLine, startColumn, rawValue);
409
508
  }
410
509
 
411
510
  /**
@@ -586,6 +685,9 @@ export class Lexer {
586
685
  if (this.current() === '+') {
587
686
  this.advance();
588
687
  this.tokens.push(new Token(TokenType.PLUSPLUS, '++', startLine, startColumn));
688
+ } else if (this.current() === '=') {
689
+ this.advance();
690
+ this.tokens.push(new Token(TokenType.PLUS_ASSIGN, '+=', startLine, startColumn));
589
691
  } else {
590
692
  this.tokens.push(new Token(TokenType.PLUS, '+', startLine, startColumn));
591
693
  }
@@ -595,17 +697,30 @@ export class Lexer {
595
697
  if (this.current() === '-') {
596
698
  this.advance();
597
699
  this.tokens.push(new Token(TokenType.MINUSMINUS, '--', startLine, startColumn));
700
+ } else if (this.current() === '=') {
701
+ this.advance();
702
+ this.tokens.push(new Token(TokenType.MINUS_ASSIGN, '-=', startLine, startColumn));
598
703
  } else {
599
704
  this.tokens.push(new Token(TokenType.MINUS, '-', startLine, startColumn));
600
705
  }
601
706
  continue;
602
707
  case '*':
603
708
  this.advance();
604
- this.tokens.push(new Token(TokenType.STAR, '*', startLine, startColumn));
709
+ if (this.current() === '=') {
710
+ this.advance();
711
+ this.tokens.push(new Token(TokenType.STAR_ASSIGN, '*=', startLine, startColumn));
712
+ } else {
713
+ this.tokens.push(new Token(TokenType.STAR, '*', startLine, startColumn));
714
+ }
605
715
  continue;
606
716
  case '/':
607
717
  this.advance();
608
- this.tokens.push(new Token(TokenType.SLASH, '/', startLine, startColumn));
718
+ if (this.current() === '=') {
719
+ this.advance();
720
+ this.tokens.push(new Token(TokenType.SLASH_ASSIGN, '/=', startLine, startColumn));
721
+ } else {
722
+ this.tokens.push(new Token(TokenType.SLASH, '/', startLine, startColumn));
723
+ }
609
724
  continue;
610
725
  case '=':
611
726
  this.advance();
@@ -626,7 +741,25 @@ export class Lexer {
626
741
  continue;
627
742
  case '?':
628
743
  this.advance();
629
- this.tokens.push(new Token(TokenType.QUESTION, '?', startLine, startColumn));
744
+ if (this.current() === '?') {
745
+ this.advance();
746
+ if (this.current() === '=') {
747
+ this.advance();
748
+ this.tokens.push(new Token(TokenType.NULLISH_ASSIGN, '??=', startLine, startColumn));
749
+ } else {
750
+ this.tokens.push(new Token(TokenType.NULLISH, '??', startLine, startColumn));
751
+ }
752
+ } else if (this.current() === '.') {
753
+ // Optional chaining ?. but only if not followed by a digit (to avoid ?.5)
754
+ if (!/[0-9]/.test(this.peek())) {
755
+ this.advance();
756
+ this.tokens.push(new Token(TokenType.OPTIONAL_CHAIN, '?.', startLine, startColumn));
757
+ } else {
758
+ this.tokens.push(new Token(TokenType.QUESTION, '?', startLine, startColumn));
759
+ }
760
+ } else {
761
+ this.tokens.push(new Token(TokenType.QUESTION, '?', startLine, startColumn));
762
+ }
630
763
  continue;
631
764
  case '%':
632
765
  this.advance();
@@ -668,7 +801,12 @@ export class Lexer {
668
801
  this.advance();
669
802
  if (this.current() === '&') {
670
803
  this.advance();
671
- this.tokens.push(new Token(TokenType.AND, '&&', startLine, startColumn));
804
+ if (this.current() === '=') {
805
+ this.advance();
806
+ this.tokens.push(new Token(TokenType.AND_ASSIGN, '&&=', startLine, startColumn));
807
+ } else {
808
+ this.tokens.push(new Token(TokenType.AND, '&&', startLine, startColumn));
809
+ }
672
810
  } else {
673
811
  // Single & is the CSS parent selector
674
812
  this.tokens.push(new Token(TokenType.AMPERSAND, '&', startLine, startColumn));
@@ -678,7 +816,12 @@ export class Lexer {
678
816
  this.advance();
679
817
  if (this.current() === '|') {
680
818
  this.advance();
681
- this.tokens.push(new Token(TokenType.OR, '||', startLine, startColumn));
819
+ if (this.current() === '=') {
820
+ this.advance();
821
+ this.tokens.push(new Token(TokenType.OR_ASSIGN, '||=', startLine, startColumn));
822
+ } else {
823
+ this.tokens.push(new Token(TokenType.OR, '||', startLine, startColumn));
824
+ }
682
825
  }
683
826
  continue;
684
827
  }
@@ -169,8 +169,13 @@ export function transformExpressionString(transformer, exprStr) {
169
169
  `${stateVar}.get()`
170
170
  );
171
171
  }
172
- // Add optional chaining after function calls followed by property access
173
- result = result.replace(/(\w+\([^)]*\))\.(\w)/g, '$1?.$2');
172
+
173
+ // NOTE: Removed aggressive optional chaining regex that was adding ?.
174
+ // after ALL function calls. This caused false positives like:
175
+ // "User.name" -> "User?.name" in string literals.
176
+ // Optional chaining should be explicitly written by developers, not auto-added.
177
+ // The lexer now properly tokenizes ?. as OPTIONAL_CHAIN for explicit usage.
178
+
174
179
  return result;
175
180
  }
176
181
 
@@ -63,6 +63,49 @@ function isKeyframesRule(selector) {
63
63
  return selector.trim().startsWith('@keyframes');
64
64
  }
65
65
 
66
+ /**
67
+ * Check if selector is @layer (CSS Cascade Layers)
68
+ * @param {string} selector - CSS selector
69
+ * @returns {boolean}
70
+ */
71
+ function isLayerRule(selector) {
72
+ return selector.trim().startsWith('@layer');
73
+ }
74
+
75
+ /**
76
+ * Check if selector is @supports (CSS Feature Queries)
77
+ * @param {string} selector - CSS selector
78
+ * @returns {boolean}
79
+ */
80
+ function isSupportsRule(selector) {
81
+ return selector.trim().startsWith('@supports');
82
+ }
83
+
84
+ /**
85
+ * Check if selector is @container (CSS Container Queries)
86
+ * @param {string} selector - CSS selector
87
+ * @returns {boolean}
88
+ */
89
+ function isContainerRule(selector) {
90
+ return selector.trim().startsWith('@container');
91
+ }
92
+
93
+ /**
94
+ * Check if selector is a conditional group at-rule that can contain nested rules
95
+ * These include @media, @supports, @container, @layer
96
+ * @param {string} selector - CSS selector
97
+ * @returns {boolean}
98
+ */
99
+ function isConditionalGroupAtRule(selector) {
100
+ const trimmed = selector.trim();
101
+ return trimmed.startsWith('@media') ||
102
+ trimmed.startsWith('@supports') ||
103
+ trimmed.startsWith('@container') ||
104
+ trimmed.startsWith('@layer') ||
105
+ trimmed.startsWith('@scope') ||
106
+ trimmed.startsWith('@document');
107
+ }
108
+
66
109
  /**
67
110
  * Check if a selector is a keyframe step (from, to, or percentage)
68
111
  * @param {string} selector - CSS selector
@@ -76,7 +119,7 @@ function isKeyframeStep(selector) {
76
119
  /**
77
120
  * Flatten nested CSS rules by combining selectors
78
121
  * Handles CSS nesting by prepending parent selector to nested rules
79
- * Special handling for @-rules (media queries, keyframes, etc.)
122
+ * Special handling for @-rules (media queries, keyframes, supports, container, layer, etc.)
80
123
  * @param {Object} transformer - Transformer instance
81
124
  * @param {Object} rule - CSS rule from AST
82
125
  * @param {string} parentSelector - Parent selector to prepend (empty for top-level)
@@ -90,27 +133,96 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
90
133
  // Check if this is an @-rule
91
134
  if (isAtRule(selector)) {
92
135
  const isKeyframes = isKeyframesRule(selector);
136
+ const isLayer = isLayerRule(selector);
137
+ const isConditionalGroup = isConditionalGroupAtRule(selector);
93
138
 
94
139
  // @keyframes should be output as a complete block, not flattened
95
140
  if (isKeyframes) {
96
141
  const lines = [];
97
- lines.push(` ${selector} {`);
98
-
99
- // Output all keyframe steps
100
- for (const nested of rule.nestedRules) {
101
- lines.push(` ${nested.selector} {`);
102
- for (const prop of nested.properties) {
103
- lines.push(` ${prop.name}: ${prop.value};`);
142
+ // Wrap in existing @-rule if present
143
+ if (atRuleWrapper) {
144
+ lines.push(` ${atRuleWrapper} {`);
145
+ lines.push(` ${selector} {`);
146
+ for (const nested of rule.nestedRules) {
147
+ lines.push(` ${nested.selector} {`);
148
+ for (const prop of nested.properties) {
149
+ lines.push(` ${prop.name}: ${prop.value};`);
150
+ }
151
+ lines.push(' }');
104
152
  }
105
153
  lines.push(' }');
154
+ lines.push(' }');
155
+ } else {
156
+ lines.push(` ${selector} {`);
157
+ for (const nested of rule.nestedRules) {
158
+ lines.push(` ${nested.selector} {`);
159
+ for (const prop of nested.properties) {
160
+ lines.push(` ${prop.name}: ${prop.value};`);
161
+ }
162
+ lines.push(' }');
163
+ }
164
+ lines.push(' }');
165
+ }
166
+ output.push(lines.join('\n'));
167
+ return;
168
+ }
169
+
170
+ // @layer - output with its content, support both named layers and anonymous layer blocks
171
+ if (isLayer) {
172
+ // Check if it's just a layer statement (@layer name;) or a layer block (@layer name { ... })
173
+ if (rule.nestedRules.length === 0 && rule.properties.length === 0) {
174
+ // Layer order statement: @layer base, components, utilities;
175
+ output.push(` ${selector};`);
176
+ return;
106
177
  }
107
178
 
108
- lines.push(' }');
179
+ // Layer block with content
180
+ const lines = [];
181
+
182
+ if (atRuleWrapper) {
183
+ lines.push(` ${atRuleWrapper} {`);
184
+ lines.push(` ${selector} {`);
185
+ } else {
186
+ lines.push(` ${selector} {`);
187
+ }
188
+
189
+ // Process nested rules within the layer
190
+ const nestedOutput = [];
191
+ for (const nested of rule.nestedRules) {
192
+ flattenStyleRule(transformer, nested, '', nestedOutput, '', false);
193
+ }
194
+
195
+ // Add nested output with proper indentation
196
+ const baseIndent = atRuleWrapper ? ' ' : ' ';
197
+ for (const nestedRule of nestedOutput) {
198
+ // Adjust indentation for nested rules
199
+ const reindented = nestedRule.split('\n').map(line => baseIndent + line.trim()).join('\n');
200
+ lines.push(reindented);
201
+ }
202
+
203
+ if (atRuleWrapper) {
204
+ lines.push(' }');
205
+ lines.push(' }');
206
+ } else {
207
+ lines.push(' }');
208
+ }
109
209
  output.push(lines.join('\n'));
110
210
  return;
111
211
  }
112
212
 
113
- // Other @-rules (@media, @supports) wrap their nested rules
213
+ // Conditional group @-rules (@media, @supports, @container) wrap their nested rules
214
+ // They can be nested inside each other
215
+ if (isConditionalGroup) {
216
+ // Combine with existing wrapper if present
217
+ const combinedWrapper = atRuleWrapper ? `${atRuleWrapper} { ${selector}` : selector;
218
+
219
+ for (const nested of rule.nestedRules) {
220
+ flattenStyleRule(transformer, nested, parentSelector, output, combinedWrapper, false);
221
+ }
222
+ return;
223
+ }
224
+
225
+ // Other @-rules (unknown) - output as-is with nested content
114
226
  for (const nested of rule.nestedRules) {
115
227
  flattenStyleRule(transformer, nested, '', output, selector, false);
116
228
  }
@@ -172,6 +284,9 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
172
284
  * .container -> .container.p123abc
173
285
  * div -> div.p123abc
174
286
  * .a .b -> .a.p123abc .b.p123abc
287
+ * .a > .b -> .a.p123abc > .b.p123abc (preserves combinators)
288
+ * .a + .b -> .a.p123abc + .b.p123abc
289
+ * .a ~ .b -> .a.p123abc ~ .b.p123abc
175
290
  * @media (max-width: 900px) -> @media (max-width: 900px) (unchanged)
176
291
  * :root, body, *, html -> unchanged (global selectors)
177
292
  * @param {Object} transformer - Transformer instance
@@ -196,31 +311,81 @@ export function scopeStyleSelector(transformer, selector) {
196
311
  return selector;
197
312
  }
198
313
 
314
+ // CSS combinators that should be preserved
315
+ const combinators = new Set(['>', '+', '~']);
316
+
199
317
  // Split by comma for multiple selectors
200
318
  return selector.split(',').map(part => {
201
319
  part = part.trim();
202
320
 
203
- // Split by space for descendant selectors
204
- return part.split(/\s+/).map(segment => {
321
+ // Split by whitespace but preserve combinators
322
+ // This regex splits on whitespace but keeps combinators as separate tokens
323
+ const tokens = part.split(/(\s*[>+~]\s*|\s+)/).filter(t => t.trim());
324
+ const result = [];
325
+
326
+ for (let i = 0; i < tokens.length; i++) {
327
+ const token = tokens[i].trim();
328
+
329
+ // Check if this is a combinator
330
+ if (combinators.has(token)) {
331
+ result.push(` ${token} `);
332
+ continue;
333
+ }
334
+
335
+ // Skip empty tokens
336
+ if (!token) continue;
337
+
205
338
  // Check if this segment is a global selector
206
- const segmentBase = segment.split(/[.#\[]/)[0];
207
- if (globalSelectors.has(segmentBase) || globalSelectors.has(segment)) {
208
- return segment;
339
+ const segmentBase = token.split(/[.#\[]/)[0];
340
+ if (globalSelectors.has(segmentBase) || globalSelectors.has(token)) {
341
+ result.push(token);
342
+ continue;
343
+ }
344
+
345
+ // Handle :has(), :is(), :where(), :not() - scope selectors inside
346
+ if (token.includes(':has(') || token.includes(':is(') ||
347
+ token.includes(':where(') || token.includes(':not(')) {
348
+ result.push(scopePseudoClassSelector(transformer, token));
349
+ continue;
209
350
  }
210
351
 
211
352
  // Skip pseudo-elements and pseudo-classes at the end
212
- const pseudoMatch = segment.match(/^([^:]+)(:.+)?$/);
353
+ const pseudoMatch = token.match(/^([^:]+)(:.+)?$/);
213
354
  if (pseudoMatch) {
214
355
  const base = pseudoMatch[1];
215
356
  const pseudo = pseudoMatch[2] || '';
216
357
 
217
358
  // Skip if it's just a pseudo selector (like :root)
218
- if (!base || globalSelectors.has(`:${pseudo.slice(1)}`)) return segment;
359
+ if (!base || globalSelectors.has(`:${pseudo.slice(1)}`)) {
360
+ result.push(token);
361
+ continue;
362
+ }
219
363
 
220
364
  // Add scope class
221
- return `${base}.${transformer.scopeId}${pseudo}`;
365
+ result.push(`${base}.${transformer.scopeId}${pseudo}`);
366
+ continue;
222
367
  }
223
- return `${segment}.${transformer.scopeId}`;
224
- }).join(' ');
368
+ result.push(`${token}.${transformer.scopeId}`);
369
+ }
370
+
371
+ return result.join('');
225
372
  }).join(', ');
226
373
  }
374
+
375
+ /**
376
+ * Scope selectors inside functional pseudo-classes like :has(), :is(), :where(), :not()
377
+ * @param {Object} transformer - Transformer instance
378
+ * @param {string} selector - Selector containing functional pseudo-class
379
+ * @returns {string} Scoped selector
380
+ */
381
+ function scopePseudoClassSelector(transformer, selector) {
382
+ // Match functional pseudo-classes: :has(), :is(), :where(), :not()
383
+ return selector.replace(
384
+ /:(has|is|where|not)\(([^)]+)\)/g,
385
+ (_match, pseudoClass, inner) => {
386
+ // Recursively scope the inner selector
387
+ const scopedInner = scopeStyleSelector(transformer, inner);
388
+ return `:${pseudoClass}(${scopedInner})`;
389
+ }
390
+ );
391
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.26",
3
+ "version": "1.7.29",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",