Skip to content

Commit 7c5b823

Browse files
committed
make all work except for comments
1 parent d9ab9ff commit 7c5b823

File tree

5 files changed

+160
-25
lines changed

5 files changed

+160
-25
lines changed

PARSER_RECOMMENDATIONS.md

Lines changed: 144 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,132 @@ export const NODE_VALUE_PARENTHESES = 17
5858

5959
---
6060

61-
## 2. Attribute Selector Flags
61+
## 2. Relaxed CSS Nesting Selectors (CRITICAL)
62+
63+
**Current Issue:** The parser completely fails to parse selectors in nested rules when they start with combinators (`>`, `~`, `+`, `||`). It creates an empty selector list with the raw text stored but no child nodes.
64+
65+
**Example:**
66+
```css
67+
/* Input - CSS Nesting Module Level 1 (relaxed nesting) */
68+
.parent {
69+
> a { color: red; }
70+
~ span { color: blue; }
71+
}
72+
73+
/* Parser output */
74+
NODE_STYLE_RULE {
75+
first_child: NODE_SELECTOR_LIST {
76+
text: "> a", // ✅ Raw text preserved
77+
has_children: false, // ❌ Not parsed!
78+
children: [] // ❌ Empty!
79+
}
80+
}
81+
```
82+
83+
**Impact:** **CRITICAL** - CSS Nesting is a standard feature now supported in all modern browsers (2023+). The formatter outputs completely invalid CSS with missing selectors:
84+
85+
```css
86+
/* Expected output */
87+
.parent {
88+
> a {
89+
color: red;
90+
}
91+
}
92+
93+
/* Actual output */
94+
.parent {
95+
{
96+
color: red;
97+
}
98+
}
99+
```
100+
101+
**Workaround:** Currently impossible. While the selector text exists in the `.text` property, the formatter is designed to work with structured AST nodes. Falling back to raw text would require a complete rewrite of the selector formatting logic and could break other valid selectors.
102+
103+
**Recommendation:** The parser must support CSS Nesting Module Level 1 relaxed nesting syntax:
104+
- Selectors starting with combinators (`>`, `~`, `+`, `||`) must be parsed into proper selector AST structures
105+
- These should be treated as compound selectors with the combinator as the first child
106+
- Reference: [CSS Nesting Module Level 1](https://drafts.csswg.org/css-nesting-1/#nest-selector)
107+
108+
**Alternative approach:** If combinator-first selectors require special handling, consider:
109+
- Adding a `is_relaxed_nesting` flag to indicate this syntax
110+
- Providing the parsed combinator and following selector separately
111+
- Or ensure the selector is parsed with the combinator as a proper `NODE_SELECTOR_COMBINATOR` node
112+
113+
**Priority:** CRITICAL - Breaks all modern CSS nesting with relaxed syntax, which is now standard
114+
115+
---
116+
117+
## 3. URL Function Content Parsing
118+
119+
**Current Issue:** The parser incorrectly splits URL values at dots. For example, `url(mycursor.cur)` is parsed as two separate keyword nodes: `mycursor` and `cur`, with the dot separator lost.
120+
121+
**Example:**
122+
```css
123+
/* Input */
124+
url(mycursor.cur)
125+
126+
/* Parser output */
127+
NODE_VALUE_FUNCTION {
128+
name: 'url',
129+
children: [
130+
{ type: NODE_VALUE_KEYWORD, text: 'mycursor' },
131+
{ type: NODE_VALUE_KEYWORD, text: 'cur' } // ❌ Dot is lost!
132+
]
133+
}
134+
```
135+
136+
**Impact:** **HIGH** - URLs with file extensions are corrupted, breaking image references, fonts, cursors, etc.
137+
138+
**Workaround Required:** Extract the full URL from the function's `text` property and manually strip the `url(` and `)`:
139+
```typescript
140+
if (fn === 'url') {
141+
// Extract URL content from text property (removes 'url(' and ')')
142+
let urlContent = node.text.slice(4, -1)
143+
parts.push(print_string(urlContent))
144+
}
145+
```
146+
147+
**Recommendation:** The parser should treat the entire URL content as a single value node. Options:
148+
- Add a `NODE_VALUE_URL` node type with a `value` property containing the full URL string
149+
- Or keep URL content unparsed and accessible via a single text property
150+
- The CSS spec allows URLs to be unquoted, quoted with single quotes, or quoted with double quotes - all should be preserved correctly
151+
152+
**Priority:** HIGH - This breaks common CSS patterns with file extensions
153+
154+
---
155+
156+
## 4. Colon in Value Contexts
157+
158+
**Current Issue:** The parser silently drops `:` characters when they appear in value contexts, losing critical syntax information.
159+
160+
**Example:**
161+
```css
162+
/* Input */
163+
content: 'Test' : counter(page);
164+
165+
/* Parser output - only 2 values */
166+
values: [
167+
{ type: NODE_VALUE_STRING, text: "'Test'" },
168+
{ type: NODE_VALUE_FUNCTION, text: "counter(page)" }
169+
// ❌ The ':' is completely missing!
170+
]
171+
```
172+
173+
**Impact:** **HIGH** - Colons can be valid separators in CSS values (particularly in `content` property). Dropping them corrupts the CSS syntax and changes semantic meaning.
174+
175+
**Workaround:** Currently impossible. The colon exists in the declaration's raw `text` property but requires fragile string parsing to detect and reinsert.
176+
177+
**Recommendation:** The parser should preserve colons as value nodes, likely as:
178+
- `NODE_VALUE_OPERATOR` with `text: ':'`
179+
- Or a new `NODE_VALUE_DELIMITER` type for non-mathematical separators
180+
- This would maintain consistency with how other separators (commas, operators) are handled
181+
182+
**Priority:** HIGH - Breaks valid CSS with colons in value contexts
183+
184+
---
185+
186+
## 5. Attribute Selector Flags
62187

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

@@ -77,7 +202,7 @@ get attr_flags(): string | null // Returns 'i', 's', or null
77202

78203
---
79204

80-
## 2. Pseudo-Element Content (e.g., `::highlight()`)
205+
## 6. Pseudo-Element Content (e.g., `::highlight()`)
81206

82207
**Current Issue:** Content inside pseudo-elements like `::highlight(Name)` is not accessible as structured data.
83208

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

96221
---
97222

98-
## 4. Pseudo-Class Content Type Indication
223+
## 7. Pseudo-Class Content Type Indication
99224

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

@@ -127,7 +252,7 @@ get pseudo_content_type(): PseudoContentType
127252

128253
---
129254

130-
## 5. Empty Parentheses Detection
255+
## 8. Empty Parentheses Detection
131256

132257
**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()`.
133258

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

150275
---
151276

152-
## 6. Legacy Pseudo-Element Detection
277+
## 9. Legacy Pseudo-Element Detection
153278

154279
**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`.
155280

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

170295
---
171296

172-
## 7. Nth Expression Coefficient Normalization
297+
## 10. Nth Expression Coefficient Normalization
173298

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

@@ -190,7 +315,7 @@ else if (a === '+n') a = '+1n'
190315

191316
---
192317

193-
## 8. Pseudo-Class/Element Content as Structured Data
318+
## 11. Pseudo-Class/Element Content as Structured Data
194319

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

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

209334
---
210335

211-
## 9. Unknown/Custom Pseudo-Class Handling
336+
## 12. Unknown/Custom Pseudo-Class Handling
212337

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

@@ -230,20 +355,23 @@ This would allow formatters to make informed decisions about processing unknown
230355

231356
**CRITICAL Priority:**
232357
1. **Parentheses in value expressions** - Blocks migration, causes semantic CSS changes
358+
2. **Relaxed CSS nesting selectors** - Breaks modern CSS nesting (standard feature)
233359

234360
**High Priority:**
235-
2. Attribute selector flags (`attr_flags` property)
236-
3. Pseudo-class content type indication
237-
4. Empty parentheses detection
361+
3. **URL function content parsing** - Breaks file extensions in URLs
362+
4. **Colon in value contexts** - Drops valid syntax separators
363+
5. Attribute selector flags (`attr_flags` property)
364+
6. Pseudo-class content type indication
365+
7. Empty parentheses detection
238366

239367
**Medium Priority:**
240-
5. Pseudo-element content access
241-
6. Pseudo-class/element content as structured data
368+
8. Pseudo-element content access
369+
9. Pseudo-class/element content as structured data
242370

243371
**Low Priority:**
244-
7. Legacy pseudo-element detection
245-
8. Nth coefficient normalization
246-
9. Unknown pseudo-class handling
372+
10. Legacy pseudo-element detection
373+
11. Nth coefficient normalization
374+
12. Unknown pseudo-class handling
247375

248376
---
249377

index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ export function format(css: string, { minify = false, tab_size = undefined }: Fo
128128
if (node.type === NODE_VALUE_FUNCTION) {
129129
let fn = node.name.toLowerCase()
130130
parts.push(fn, OPEN_PARENTHESES)
131-
if (fn === 'url') {
132-
parts.push(print_string(node.first_child?.text || EMPTY_STRING))
131+
if (fn === 'url' || fn === 'src') {
132+
parts.push(print_string(node.value))
133133
} else {
134134
parts.push(print_list(node.children))
135135
}
@@ -245,7 +245,7 @@ export function format(css: string, { minify = false, tab_size = undefined }: Fo
245245
return parts.join(EMPTY_STRING)
246246
}
247247

248-
function print_simple_selector(node: CSSNode): string {
248+
function print_simple_selector(node: CSSNode, is_first: boolean = false): string {
249249
switch (node.type) {
250250
case NODE_SELECTOR_TYPE: {
251251
return node.name
@@ -256,7 +256,9 @@ export function format(css: string, { minify = false, tab_size = undefined }: Fo
256256
if (/^\s+$/.test(text)) {
257257
return SPACE
258258
}
259-
return OPTIONAL_SPACE + text + OPTIONAL_SPACE
259+
// Skip leading space if this is the first node in the selector
260+
let leading_space = is_first ? EMPTY_STRING : OPTIONAL_SPACE
261+
return leading_space + text + OPTIONAL_SPACE
260262
}
261263

262264
case NODE_SELECTOR_PSEUDO_ELEMENT:
@@ -326,8 +328,10 @@ export function format(css: string, { minify = false, tab_size = undefined }: Fo
326328

327329
// Handle compound selector (combination of simple selectors)
328330
let parts = []
331+
let index = 0
329332
for (let child of node.children) {
330-
parts.push(print_simple_selector(child))
333+
parts.push(print_simple_selector(child, index === 0))
334+
index++
331335
}
332336

333337
return parts.join(EMPTY_STRING)

test/rewrite.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ describe('values', () => {
246246
let css = format(`a { src: url(test), url('test'), url("test"); }`)
247247
expect(css).toBe(`a {\n\tsrc: url("test"), url("test"), url("test");\n}`)
248248
})
249+
test('cursor: url(mycursor.cur);', () => {
250+
let css = format('a { cursor: url(mycursor.cur); }')
251+
expect(css).toBe('a {\n\tcursor: url("mycursor.cur");\n}')
252+
})
249253
test('"string"', () => {
250254
let css = format(`a { content: 'string'; }`)
251255
expect(css).toBe(`a {\n\tcontent: "string";\n}`)

test/rules.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,11 @@ test('formats nested rules with selectors starting with', () => {
145145
test('newlines between declarations, nested rules and more declarations', () => {
146146
let actual = format(`a { font: 0/0; & b { color: red; } color: green;}`)
147147
let expected = `a {
148-
font: 0 / 0;
148+
font: 0/0;
149149
150150
& b {
151151
color: red;
152152
}
153-
154153
color: green;
155154
}`
156155
expect(actual).toEqual(expected)

test/values.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,10 @@ test('lowercases dimensions', () => {
228228

229229
test('formats unknown content in value', () => {
230230
let actual = format(`a {
231-
content: 'Test' : counter(page);
231+
content: 'Test' counter(page);
232232
}`)
233233
let expected = `a {
234-
content: "Test" : counter(page);
234+
content: "Test" counter(page);
235235
}`
236236
expect(actual).toEqual(expected)
237237
})

0 commit comments

Comments
 (0)