Skip to content

Commit d9ab9ff

Browse files
committed
print parenthesis
1 parent 2a52453 commit d9ab9ff

File tree

2 files changed

+88
-15
lines changed

2 files changed

+88
-15
lines changed

PARSER_RECOMMENDATIONS.md

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,63 @@
22

33
Based on implementing the formatter, here are recommendations for improving the CSS parser to better support formatting and other tooling use cases.
44

5-
## 1. Attribute Selector Flags
5+
## 1. Parentheses in Value Expressions (CRITICAL)
6+
7+
**Current Issue:** Parentheses in value expressions (particularly in `calc()`, `clamp()`, `min()`, `max()`, etc.) are not preserved in the AST. The parser flattens expressions into a simple sequence of values and operators, losing all grouping information.
8+
9+
**Example:**
10+
```css
11+
/* Input */
12+
calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y)))
13+
14+
/* Parser output (flat list) */
15+
100% - var(--x) / 12 * 6 + -1 * var(--y)
16+
```
17+
18+
**Impact:** **CRITICAL** - Without parentheses, the mathematical meaning changes completely due to operator precedence:
19+
- `(100% - var(--x)) / 12``100% - var(--x) / 12`
20+
- Division happens before subtraction, producing incorrect results
21+
- Browsers will compute different values, breaking layouts
22+
23+
**Comparison with csstree:** The csstree parser has a `Parentheses` node type that wraps grouped expressions:
24+
```typescript
25+
if (node.type === 'Parentheses') {
26+
buffer += '(' + print_list(node.children) + ')'
27+
}
28+
```
29+
30+
**Recommendation:** Add a new node type `NODE_VALUE_PARENTHESES` (or `NODE_VALUE_GROUP`) that represents parenthesized expressions:
31+
32+
```typescript
33+
// New node type constant
34+
export const NODE_VALUE_PARENTHESES = 17
35+
36+
// Example AST structure for: calc((100% - 50px) / 2)
37+
{
38+
type: NODE_VALUE_FUNCTION,
39+
name: 'calc',
40+
children: [
41+
{
42+
type: NODE_VALUE_PARENTHESES, // ✅ Parentheses preserved!
43+
children: [
44+
{ type: NODE_VALUE_DIMENSION, value: '100', unit: '%' },
45+
{ type: NODE_VALUE_OPERATOR, text: '-' },
46+
{ type: NODE_VALUE_DIMENSION, value: '50', unit: 'px' }
47+
]
48+
},
49+
{ type: NODE_VALUE_OPERATOR, text: '/' },
50+
{ type: NODE_VALUE_NUMBER, text: '2' }
51+
]
52+
}
53+
```
54+
55+
**Workaround:** Currently impossible. The formatter cannot reconstruct parentheses because the information is lost during parsing. Falling back to raw text defeats the purpose of having a structured AST.
56+
57+
**Priority:** CRITICAL - This is blocking the migration from csstree to wallace-css-parser, as it causes semantic changes to CSS that break user styles.
58+
59+
---
60+
61+
## 2. Attribute Selector Flags
662

763
**Current Issue:** Attribute selector flags (case-insensitive `i` and case-sensitive `s`) are not exposed as a property on `CSSNode`.
864

@@ -39,7 +95,7 @@ let content_match = text.match(/::[^(]+(\([^)]*\))/)
3995

4096
---
4197

42-
## 3. Pseudo-Class Content Type Indication
98+
## 4. Pseudo-Class Content Type Indication
4399

44100
**Current Issue:** No way to distinguish what type of content a pseudo-class contains without hardcoding known pseudo-class names.
45101

@@ -71,7 +127,7 @@ get pseudo_content_type(): PseudoContentType
71127

72128
---
73129

74-
## 4. Empty Parentheses Detection
130+
## 5. Empty Parentheses Detection
75131

76132
**Current Issue:** When a pseudo-class has empty parentheses (e.g., `:nth-child()`), there's no indication in the AST that parentheses exist at all. `first_child` is null, so formatters can't distinguish `:nth-child` from `:nth-child()`.
77133

@@ -93,7 +149,7 @@ get has_parentheses(): boolean // True even if content is empty
93149

94150
---
95151

96-
## 5. Legacy Pseudo-Element Detection
152+
## 6. Legacy Pseudo-Element Detection
97153

98154
**Current Issue:** Legacy pseudo-elements (`:before`, `:after`, `:first-letter`, `:first-line`) can be written with single colons but should be normalized to double colons. Parser treats them as `NODE_SELECTOR_PSEUDO_CLASS` rather than `NODE_SELECTOR_PSEUDO_ELEMENT`.
99155

@@ -113,7 +169,7 @@ if (name === 'before' || name === 'after' || name === 'first-letter' || name ===
113169

114170
---
115171

116-
## 6. Nth Expression Coefficient Normalization
172+
## 7. Nth Expression Coefficient Normalization
117173

118174
**Current Issue:** Nth expressions like `-n` need to be normalized to `-1n` for consistency, but parser returns raw text.
119175

@@ -134,7 +190,7 @@ else if (a === '+n') a = '+1n'
134190

135191
---
136192

137-
## 7. Pseudo-Class/Element Content as Structured Data
193+
## 8. Pseudo-Class/Element Content as Structured Data
138194

139195
**Current Issue:** Content inside pseudo-classes like `:lang("en", "fr")` is not parsed into structured data. Must preserve as raw text.
140196

@@ -152,7 +208,7 @@ parts.push(content_match[1]) // "(\"en\", \"fr\")"
152208

153209
---
154210

155-
## 8. Unknown/Custom Pseudo-Class Handling
211+
## 9. Unknown/Custom Pseudo-Class Handling
156212

157213
**Current Issue:** For unknown or custom pseudo-classes, there's no way to know if they should be formatted or preserved as-is.
158214

@@ -172,19 +228,22 @@ This would allow formatters to make informed decisions about processing unknown
172228

173229
## Priority Summary
174230

231+
**CRITICAL Priority:**
232+
1. **Parentheses in value expressions** - Blocks migration, causes semantic CSS changes
233+
175234
**High Priority:**
176-
1. Attribute selector flags (`attr_flags` property)
177-
2. Pseudo-class content type indication
178-
3. Empty parentheses detection
235+
2. Attribute selector flags (`attr_flags` property)
236+
3. Pseudo-class content type indication
237+
4. Empty parentheses detection
179238

180239
**Medium Priority:**
181-
4. Pseudo-element content access
182-
5. Pseudo-class/element content as structured data
240+
5. Pseudo-element content access
241+
6. Pseudo-class/element content as structured data
183242

184243
**Low Priority:**
185-
6. Legacy pseudo-element detection
186-
7. Nth coefficient normalization
187-
8. Unknown pseudo-class handling
244+
7. Legacy pseudo-element detection
245+
8. Nth coefficient normalization
246+
9. Unknown pseudo-class handling
188247

189248
---
190249

index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
ATTR_FLAG_NONE,
2828
ATTR_FLAG_CASE_INSENSITIVE,
2929
ATTR_FLAG_CASE_SENSITIVE,
30+
NODE_VALUE_PARENTHESIS,
3031
} from '../css-parser'
3132

3233
const SPACE = ' '
@@ -139,6 +140,8 @@ export function format(css: string, { minify = false, tab_size = undefined }: Fo
139140
parts.push(print_string(node.text))
140141
} else if (node.type === NODE_VALUE_OPERATOR) {
141142
parts.push(print_operator(node))
143+
} else if (node.type === NODE_VALUE_PARENTHESIS) {
144+
parts.push(OPEN_PARENTHESES, print_list(node.children), CLOSE_PARENTHESES)
142145
} else {
143146
parts.push(node.text)
144147
}
@@ -171,6 +174,17 @@ export function format(css: string, { minify = false, tab_size = undefined }: Fo
171174
}
172175
let value = print_values(node.values)
173176
let property = node.property
177+
178+
// Special case for `font` shorthand: remove whitespace around /
179+
if (property === 'font') {
180+
value = value.replace(/\s*\/\s*/, '/')
181+
}
182+
183+
// Hacky: add a space in case of a `space toggle` during minification
184+
if (value === EMPTY_STRING && minify === true) {
185+
value += SPACE
186+
}
187+
174188
if (!property.startsWith('--')) {
175189
property = property.toLowerCase()
176190
}

0 commit comments

Comments
 (0)