pulse-js-framework 1.7.19 → 1.7.21
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 +1 -1
- package/cli/release.js +2 -2
- package/compiler/lexer.js +21 -7
- package/compiler/parser.js +118 -22
- package/compiler/transformer/index.js +3 -3
- package/compiler/transformer/style.js +40 -18
- package/package.json +1 -1
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
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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 '|':
|
package/compiler/parser.js
CHANGED
|
@@ -1560,12 +1560,43 @@ export class Parser {
|
|
|
1560
1560
|
* Parse style rule
|
|
1561
1561
|
*/
|
|
1562
1562
|
parseStyleRule() {
|
|
1563
|
-
// Parse selector
|
|
1564
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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;
|
|
@@ -1615,16 +1682,25 @@ export class Parser {
|
|
|
1615
1682
|
|
|
1616
1683
|
// Tokens that should not have space after them in CSS values
|
|
1617
1684
|
const noSpaceAfter = new Set(['#', '(', '.', '/', 'rgba', 'rgb', 'hsl', 'hsla', 'var', 'calc', 'url', 'linear-gradient', 'radial-gradient']);
|
|
1618
|
-
// Tokens that should not have space before them
|
|
1619
|
-
const noSpaceBefore = new Set([')', ',', '%', 'px', 'em', 'rem', 'vh', 'vw', 'fr', 's', 'ms', '(']);
|
|
1685
|
+
// Tokens that should not have space before them (units and punctuation)
|
|
1686
|
+
const noSpaceBefore = new Set([')', ',', '%', 'px', 'em', 'rem', 'vh', 'vw', 'vmin', 'vmax', 'fr', 's', 'ms', '(', 'deg', 'rad', 'turn', 'grad', 'ex', 'ch', 'pt', 'pc', 'in', 'cm', 'mm']);
|
|
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
|
|
1692
|
+
let hexColorLength = 0; // Track hex color length (max 8 for RRGGBBAA)
|
|
1624
1693
|
let inCssVar = false; // Track if we're inside var(--...)
|
|
1625
1694
|
|
|
1626
|
-
while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) &&
|
|
1627
|
-
|
|
1695
|
+
while (!this.is(TokenType.SEMICOLON) && !this.is(TokenType.RBRACE) && !this.is(TokenType.EOF)) {
|
|
1696
|
+
const currentToken = this.current();
|
|
1697
|
+
|
|
1698
|
+
// Check if we're on a new line - if so, check for property start or nested rule
|
|
1699
|
+
if (currentToken && currentToken.line > lastTokenLine) {
|
|
1700
|
+
if (this.isPropertyStart() || this.isNestedRule()) {
|
|
1701
|
+
break;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1628
1704
|
const token = this.advance();
|
|
1629
1705
|
// Use raw value if available to preserve original representation
|
|
1630
1706
|
// This is important for numbers that might be parsed as scientific notation
|
|
@@ -1638,12 +1714,24 @@ export class Parser {
|
|
|
1638
1714
|
}
|
|
1639
1715
|
|
|
1640
1716
|
// For hex colors (#abc123), collect tokens without spacing after #
|
|
1717
|
+
// Hex colors are 3, 4, 6, or 8 characters long
|
|
1641
1718
|
if (tokenValue === '#') {
|
|
1642
1719
|
afterHash = true;
|
|
1720
|
+
hexColorLength = 0;
|
|
1643
1721
|
} else if (afterHash) {
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1722
|
+
// Check if this token is a valid hex color continuation
|
|
1723
|
+
const tokenStr = String(tokenValue);
|
|
1724
|
+
const isHexChar = /^[0-9a-fA-F]+$/.test(tokenStr);
|
|
1725
|
+
if (isHexChar) {
|
|
1726
|
+
const newLength = hexColorLength + tokenStr.length;
|
|
1727
|
+
// Valid hex color lengths are 3, 4, 6, or 8
|
|
1728
|
+
// If adding this token would exceed a valid length, stop
|
|
1729
|
+
if (hexColorLength >= 6 || newLength > 8) {
|
|
1730
|
+
afterHash = false;
|
|
1731
|
+
} else {
|
|
1732
|
+
hexColorLength = newLength;
|
|
1733
|
+
}
|
|
1734
|
+
} else {
|
|
1647
1735
|
afterHash = false;
|
|
1648
1736
|
}
|
|
1649
1737
|
}
|
|
@@ -1652,16 +1740,15 @@ export class Parser {
|
|
|
1652
1740
|
// - It's the first token
|
|
1653
1741
|
// - Last token was in noSpaceAfter
|
|
1654
1742
|
// - This token is in noSpaceBefore
|
|
1655
|
-
// - We're collecting a hex color (afterHash
|
|
1743
|
+
// - We're collecting a hex color (afterHash is true)
|
|
1656
1744
|
// - We're inside var(--...) and this is part of the variable name
|
|
1657
1745
|
// - Last was '-' and current is an identifier (hyphenated name)
|
|
1658
|
-
const skipSpace = noSpaceAfter.has(lastTokenValue) ||
|
|
1746
|
+
const skipSpace = noSpaceAfter.has(String(lastTokenValue)) ||
|
|
1659
1747
|
noSpaceBefore.has(tokenValue) ||
|
|
1660
|
-
|
|
1661
|
-
(afterHash && /^[0-9a-fA-F]+$/.test(tokenValue)) ||
|
|
1748
|
+
afterHash ||
|
|
1662
1749
|
inCssVar ||
|
|
1663
1750
|
(lastTokenValue === '-' || lastTokenValue === '--') ||
|
|
1664
|
-
(tokenValue === '-' && /^[a-zA-Z]/.test(this.current()?.value || ''));
|
|
1751
|
+
(tokenValue === '-' && /^[a-zA-Z]/.test(String(this.current()?.value || '')));
|
|
1665
1752
|
|
|
1666
1753
|
if (value.length > 0 && !skipSpace) {
|
|
1667
1754
|
value += ' ';
|
|
@@ -1973,12 +2060,21 @@ export class Parser {
|
|
|
1973
2060
|
* Check if current position starts a new property
|
|
1974
2061
|
*/
|
|
1975
2062
|
isPropertyStart() {
|
|
1976
|
-
// Check if it looks like: identifier followed by :
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2063
|
+
// Check if it looks like: identifier (with possible hyphens) followed by :
|
|
2064
|
+
// CSS properties can be: margin, margin-bottom, -webkit-transform, etc.
|
|
2065
|
+
if (!this.is(TokenType.IDENT) && !this.is(TokenType.MINUS)) return false;
|
|
2066
|
+
|
|
2067
|
+
let i = 0;
|
|
2068
|
+
// Skip over property name tokens (IDENT and MINUS for hyphenated names)
|
|
2069
|
+
while (this.peek(i)) {
|
|
2070
|
+
const token = this.peek(i);
|
|
2071
|
+
if (token.type === TokenType.IDENT || token.type === TokenType.MINUS) {
|
|
2072
|
+
i++;
|
|
2073
|
+
} else {
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
1981
2076
|
}
|
|
2077
|
+
|
|
1982
2078
|
return this.peek(i)?.type === TokenType.COLON;
|
|
1983
2079
|
}
|
|
1984
2080
|
}
|
|
@@ -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,
|
|
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
|
-
|
|
335
|
-
return
|
|
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
|
-
|
|
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
|
-
*
|
|
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 {
|
|
46
|
-
* @
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
72
|
+
let scopedSelector = fullSelector;
|
|
54
73
|
if (transformer.scopeId) {
|
|
55
|
-
|
|
74
|
+
scopedSelector = scopeStyleSelector(transformer, fullSelector);
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
// Only output rule if it has properties
|
|
78
|
+
if (rule.properties.length > 0) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
lines.push(` ${scopedSelector} {`);
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
/**
|