pulse-js-framework 1.7.19 → 1.7.20

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/cli/help.js CHANGED
@@ -379,7 +379,7 @@ Creates a new release with:
379
379
  { flag: '--no-push', description: 'Create commit and tag but do not push' },
380
380
  { flag: '--title <text>', description: 'Release title for changelog' },
381
381
  { flag: '--skip-prompt', description: 'Use empty changelog (for automation)' },
382
- { flag: '--from-commits', description: 'Auto-extract changelog from git commits' }
382
+ { flag: '--from-commits, --fc', description: 'Auto-extract changelog from git commits' }
383
383
  ],
384
384
  examples: [
385
385
  { cmd: 'pulse release patch', desc: 'Create patch release (1.0.0 -> 1.0.1)' },
package/cli/release.js CHANGED
@@ -568,7 +568,7 @@ Options:
568
568
  --title <text> Release title (e.g., "Performance Improvements")
569
569
  --skip-prompt Use empty changelog (for automated releases)
570
570
  --skip-docs-test Skip documentation validation before release
571
- --from-commits Auto-extract changelog from git commits since last tag
571
+ --from-commits, --fc Auto-extract changelog from git commits since last tag
572
572
  --yes, -y Auto-confirm all prompts
573
573
  --changes <json> Pass changelog as JSON (e.g., '{"added":["Feature 1"],"fixed":["Bug 1"]}')
574
574
  --added <items> Comma-separated list of added features
@@ -602,7 +602,7 @@ export async function runRelease(args) {
602
602
  const dryRun = args.includes('--dry-run');
603
603
  const noPush = args.includes('--no-push');
604
604
  const skipPrompt = args.includes('--skip-prompt');
605
- const fromCommits = args.includes('--from-commits');
605
+ const fromCommits = args.includes('--from-commits') || args.includes('--fc');
606
606
  const autoConfirm = args.includes('--yes') || args.includes('-y');
607
607
  const skipDocsTest = args.includes('--skip-docs-test');
608
608
 
package/compiler/lexer.js CHANGED
@@ -61,6 +61,7 @@ export const TokenType = {
61
61
  COMMA: 'COMMA', // ,
62
62
  DOT: 'DOT', // .
63
63
  HASH: 'HASH', // #
64
+ AMPERSAND: 'AMPERSAND', // & (CSS parent selector)
64
65
  SEMICOLON: 'SEMICOLON', // ;
65
66
 
66
67
  // Operators
@@ -382,15 +383,25 @@ export class Lexer {
382
383
  }
383
384
  }
384
385
 
385
- // Exponent part
386
+ // Exponent part - only consume 'e' if followed by digits (or +/- then digits)
386
387
  if (this.current() === 'e' || this.current() === 'E') {
387
- value += this.advance();
388
- if (this.current() === '+' || this.current() === '-') {
389
- value += this.advance();
390
- }
391
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
392
- value += this.advance();
388
+ const nextChar = this.peek();
389
+ const nextNextChar = this.peek(2);
390
+ // Check if this is actually scientific notation:
391
+ // e followed by digit, or e followed by +/- then digit
392
+ const isScientific = /[0-9]/.test(nextChar) ||
393
+ ((nextChar === '+' || nextChar === '-') && /[0-9]/.test(nextNextChar));
394
+
395
+ if (isScientific) {
396
+ value += this.advance(); // consume 'e' or 'E'
397
+ if (this.current() === '+' || this.current() === '-') {
398
+ value += this.advance();
399
+ }
400
+ while (!this.isEOF() && /[0-9]/.test(this.current())) {
401
+ value += this.advance();
402
+ }
393
403
  }
404
+ // If not scientific notation, leave 'e' for the next token (e.g., 'em' unit)
394
405
  }
395
406
 
396
407
  return new Token(TokenType.NUMBER, parseFloat(value), startLine, startColumn, value);
@@ -656,6 +667,9 @@ export class Lexer {
656
667
  if (this.current() === '&') {
657
668
  this.advance();
658
669
  this.tokens.push(new Token(TokenType.AND, '&&', startLine, startColumn));
670
+ } else {
671
+ // Single & is the CSS parent selector
672
+ this.tokens.push(new Token(TokenType.AMPERSAND, '&', startLine, startColumn));
659
673
  }
660
674
  continue;
661
675
  case '|':
@@ -1560,12 +1560,43 @@ export class Parser {
1560
1560
  * Parse style rule
1561
1561
  */
1562
1562
  parseStyleRule() {
1563
- // Parse selector
1564
- let selector = '';
1563
+ // Parse selector - preserve spaces between tokens
1564
+ const selectorParts = [];
1565
+ let lastLine = this.current()?.line;
1566
+ let lastToken = null;
1567
+
1565
1568
  while (!this.is(TokenType.LBRACE) && !this.is(TokenType.EOF)) {
1566
- selector += this.advance().value;
1569
+ const token = this.advance();
1570
+ const currentLine = token.line;
1571
+ const tokenValue = String(token.value);
1572
+
1573
+ // Determine if we need a space before this token
1574
+ if (selectorParts.length > 0 && currentLine === lastLine) {
1575
+ const lastPart = selectorParts[selectorParts.length - 1];
1576
+
1577
+ // Don't add space after these (they attach to what follows)
1578
+ const noSpaceAfter = ['.', '#', ':', '[', '(', '>', '+', '~'];
1579
+ // Don't add space before these (they attach to what precedes)
1580
+ const noSpaceBefore = [':', ']', ')', ',', '.', '#'];
1581
+
1582
+ // Special case: . or # after an identifier needs space (descendant selector)
1583
+ // e.g., ".school .date" - need space between "school" and "."
1584
+ const isDescendantSelector = (tokenValue === '.' || tokenValue === '#') &&
1585
+ lastToken?.type === TokenType.IDENT;
1586
+
1587
+ const needsSpace = !noSpaceAfter.includes(lastPart) &&
1588
+ !noSpaceBefore.includes(tokenValue) ||
1589
+ isDescendantSelector;
1590
+
1591
+ if (needsSpace) {
1592
+ selectorParts.push(' ');
1593
+ }
1594
+ }
1595
+ selectorParts.push(tokenValue);
1596
+ lastLine = currentLine;
1597
+ lastToken = token;
1567
1598
  }
1568
- selector = selector.trim();
1599
+ const selector = selectorParts.join('').trim();
1569
1600
 
1570
1601
  this.expect(TokenType.LBRACE);
1571
1602
 
@@ -1587,15 +1618,51 @@ export class Parser {
1587
1618
 
1588
1619
  /**
1589
1620
  * Check if current position is a nested rule
1621
+ * A nested rule starts with a selector followed by { on the same logical line
1590
1622
  */
1591
1623
  isNestedRule() {
1592
- // Look ahead to see if there's a { before a : or newline
1624
+ const currentToken = this.peek(0);
1625
+ if (!currentToken) return false;
1626
+
1627
+ // & is always a CSS parent selector, never a property name
1628
+ // So &:hover, &.class, etc. are always nested rules
1629
+ if (currentToken.type === TokenType.AMPERSAND) {
1630
+ return true;
1631
+ }
1632
+
1633
+ const startLine = currentToken.line;
1593
1634
  let i = 0;
1635
+
1594
1636
  while (this.peek(i) && this.peek(i).type !== TokenType.EOF) {
1595
1637
  const token = this.peek(i);
1638
+
1639
+ // Found { before : - this is a nested rule
1596
1640
  if (token.type === TokenType.LBRACE) return true;
1641
+
1642
+ // Found : - this is a property, not a nested rule
1597
1643
  if (token.type === TokenType.COLON) return false;
1644
+
1645
+ // Found } - end of current rule
1598
1646
  if (token.type === TokenType.RBRACE) return false;
1647
+
1648
+ // If we've moved to a different line and the selector isn't continuing,
1649
+ // check if this new line starts with a selector pattern
1650
+ if (token.line > startLine && i > 0) {
1651
+ // We're on a new line - only continue if we haven't found anything significant
1652
+ // A selector on a new line followed by { is a nested rule
1653
+ // Check next few tokens on this new line
1654
+ const nextLine = token.line;
1655
+ let j = i;
1656
+ while (this.peek(j) && this.peek(j).line === nextLine) {
1657
+ const t = this.peek(j);
1658
+ if (t.type === TokenType.LBRACE) return true;
1659
+ if (t.type === TokenType.COLON) return false;
1660
+ if (t.type === TokenType.RBRACE) return false;
1661
+ j++;
1662
+ }
1663
+ return false;
1664
+ }
1665
+
1599
1666
  i++;
1600
1667
  }
1601
1668
  return false;
@@ -1620,11 +1687,19 @@ export class Parser {
1620
1687
 
1621
1688
  let value = '';
1622
1689
  let lastTokenValue = '';
1690
+ let lastTokenLine = this.current()?.line || 0;
1623
1691
  let afterHash = false; // Track if we're collecting a hex color
1624
1692
  let inCssVar = false; // Track if we're inside var(--...)
1625
1693
 
1626
- while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) &&
1627
- !this.is(TokenType.EOF) && !this.isPropertyStart()) {
1694
+ while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
1695
+ const currentToken = this.current();
1696
+
1697
+ // Check if we're on a new line - if so, check for property start or nested rule
1698
+ if (currentToken && currentToken.line > lastTokenLine) {
1699
+ if (this.isPropertyStart() || this.isNestedRule()) {
1700
+ break;
1701
+ }
1702
+ }
1628
1703
  const token = this.advance();
1629
1704
  // Use raw value if available to preserve original representation
1630
1705
  // This is important for numbers that might be parsed as scientific notation
@@ -1973,12 +2048,21 @@ export class Parser {
1973
2048
  * Check if current position starts a new property
1974
2049
  */
1975
2050
  isPropertyStart() {
1976
- // Check if it looks like: identifier followed by :
1977
- if (!this.is(TokenType.IDENT)) return false;
1978
- let i = 1;
1979
- while (this.peek(i) && this.peek(i).type === TokenType.IDENT) {
1980
- i++;
2051
+ // Check if it looks like: identifier (with possible hyphens) followed by :
2052
+ // CSS properties can be: margin, margin-bottom, -webkit-transform, etc.
2053
+ if (!this.is(TokenType.IDENT) && !this.is(TokenType.MINUS)) return false;
2054
+
2055
+ let i = 0;
2056
+ // Skip over property name tokens (IDENT and MINUS for hyphenated names)
2057
+ while (this.peek(i)) {
2058
+ const token = this.peek(i);
2059
+ if (token.type === TokenType.IDENT || token.type === TokenType.MINUS) {
2060
+ i++;
2061
+ } else {
2062
+ break;
2063
+ }
1981
2064
  }
2065
+
1982
2066
  return this.peek(i)?.type === TokenType.COLON;
1983
2067
  }
1984
2068
  }
@@ -27,7 +27,7 @@ import { transformRouter } from './router.js';
27
27
  import { transformStore } from './store.js';
28
28
  import { transformExpression, transformExpressionString, transformFunctionBody } from './expressions.js';
29
29
  import { transformView, transformViewNode, VIEW_NODE_HANDLERS, addScopeToSelector } from './view.js';
30
- import { transformStyle, transformStyleRule, scopeStyleSelector } from './style.js';
30
+ import { transformStyle, flattenStyleRule, scopeStyleSelector } from './style.js';
31
31
  import { generateExport } from './export.js';
32
32
 
33
33
  /**
@@ -331,8 +331,8 @@ export class Transformer {
331
331
  return transformStyle(this, styleBlock);
332
332
  }
333
333
 
334
- transformStyleRule(rule, indent) {
335
- return transformStyleRule(this, rule, indent);
334
+ flattenStyleRule(rule, parentSelector, output) {
335
+ return flattenStyleRule(this, rule, parentSelector, output);
336
336
  }
337
337
 
338
338
  scopeStyleSelector(selector) {
@@ -19,8 +19,15 @@ export function transformStyle(transformer, styleBlock) {
19
19
 
20
20
  lines.push('const styles = `');
21
21
 
22
+ // Collect all flattened rules (handles nesting)
23
+ const flattenedRules = [];
22
24
  for (const rule of styleBlock.rules) {
23
- lines.push(transformStyleRule(transformer, rule, 0));
25
+ flattenStyleRule(transformer, rule, '', flattenedRules);
26
+ }
27
+
28
+ // Output all flattened rules
29
+ for (const cssRule of flattenedRules) {
30
+ lines.push(cssRule);
24
31
  }
25
32
 
26
33
  lines.push('`;');
@@ -39,36 +46,51 @@ export function transformStyle(transformer, styleBlock) {
39
46
  }
40
47
 
41
48
  /**
42
- * Transform style rule with optional scoping
49
+ * Flatten nested CSS rules by combining selectors
50
+ * Handles CSS nesting by prepending parent selector to nested rules
43
51
  * @param {Object} transformer - Transformer instance
44
52
  * @param {Object} rule - CSS rule from AST
45
- * @param {number} indent - Indentation level
46
- * @returns {string} CSS code
53
+ * @param {string} parentSelector - Parent selector to prepend (empty for top-level)
54
+ * @param {Array<string>} output - Array to collect flattened CSS rules
47
55
  */
48
- export function transformStyleRule(transformer, rule, indent) {
49
- const pad = ' '.repeat(indent);
50
- const lines = [];
56
+ export function flattenStyleRule(transformer, rule, parentSelector, output) {
57
+ // Build the full selector by combining parent and current
58
+ let fullSelector = rule.selector;
59
+
60
+ if (parentSelector) {
61
+ // Handle & (parent reference) in nested selectors
62
+ if (rule.selector.includes('&')) {
63
+ // Replace & with parent selector
64
+ fullSelector = rule.selector.replace(/&/g, parentSelector);
65
+ } else {
66
+ // Combine parent and child with space (descendant combinator)
67
+ fullSelector = `${parentSelector} ${rule.selector}`;
68
+ }
69
+ }
51
70
 
52
71
  // Apply scope to selector if enabled
53
- let selector = rule.selector;
72
+ let scopedSelector = fullSelector;
54
73
  if (transformer.scopeId) {
55
- selector = scopeStyleSelector(transformer, selector);
74
+ scopedSelector = scopeStyleSelector(transformer, fullSelector);
56
75
  }
57
76
 
58
- lines.push(`${pad}${selector} {`);
77
+ // Only output rule if it has properties
78
+ if (rule.properties.length > 0) {
79
+ const lines = [];
80
+ lines.push(` ${scopedSelector} {`);
59
81
 
60
- for (const prop of rule.properties) {
61
- lines.push(`${pad} ${prop.name}: ${prop.value};`);
82
+ for (const prop of rule.properties) {
83
+ lines.push(` ${prop.name}: ${prop.value};`);
84
+ }
85
+
86
+ lines.push(' }');
87
+ output.push(lines.join('\n'));
62
88
  }
63
89
 
90
+ // Recursively flatten nested rules with combined selector
64
91
  for (const nested of rule.nestedRules) {
65
- // For nested rules, combine selectors (simplified nesting)
66
- const nestedLines = transformStyleRule(transformer, nested, indent + 1);
67
- lines.push(nestedLines);
92
+ flattenStyleRule(transformer, nested, fullSelector, output);
68
93
  }
69
-
70
- lines.push(`${pad}}`);
71
- return lines.join('\n');
72
94
  }
73
95
 
74
96
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.19",
3
+ "version": "1.7.20",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",