Skip to content

Commit 716233c

Browse files
authored
feat: add clone() method to cssnode (#29)
1 parent 3c05c49 commit 716233c

File tree

4 files changed

+454
-40
lines changed

4 files changed

+454
-40
lines changed

API.md

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function parse(source: string, options?: ParserOptions): CSSNode
6363
- `all_compounds` - Array of compound arrays split by combinators (for NODE_SELECTOR)
6464
- `is_compound` - Whether selector has no combinators (for NODE_SELECTOR)
6565
- `first_compound_text` - Text of first compound selector (for NODE_SELECTOR)
66+
- `clone(options?)` - Clone node as a mutable plain object with children as arrays
6667
6768
### Example 1: Basic Parsing
6869
@@ -281,7 +282,7 @@ const ast = parse('.foo { color: red; }')
281282

282283
// Using type_name directly on nodes
283284
for (let node of ast) {
284-
console.log(`${node.type_name}: ${node.text}`)
285+
console.log(`${node.type_name}: ${node.text}`)
285286
}
286287
// Output:
287288
// style_rule: .foo { color: red; }
@@ -301,7 +302,7 @@ console.log(TYPE_NAMES[NODE_DECLARATION]) // 'declaration'
301302

302303
// Compare strings instead of numeric constants
303304
if (node.type_name === 'declaration') {
304-
console.log(`Property: ${node.property}, Value: ${node.value}`)
305+
console.log(`Property: ${node.property}, Value: ${node.value}`)
305306
}
306307
```
307308
@@ -327,14 +328,14 @@ const nthOf = nthPseudo.first_child // NODE_SELECTOR_NTH_OF
327328

328329
// Direct access to formula
329330
console.log(nthOf.nth.type === NODE_SELECTOR_NTH) // true
330-
console.log(nthOf.nth.nth_a) // "2n"
331-
console.log(nthOf.nth.nth_b) // "+1"
331+
console.log(nthOf.nth.nth_a) // "2n"
332+
console.log(nthOf.nth.nth_b) // "+1"
332333

333334
// Direct access to selector list from :nth-child(of)
334-
console.log(nthOf.selector.text) // ".foo"
335+
console.log(nthOf.selector.text) // ".foo"
335336

336337
// Or use the unified helper on the pseudo-class
337-
console.log(nthPseudo.selector_list.text) // ".foo"
338+
console.log(nthPseudo.selector_list.text) // ".foo"
338339
```
339340
340341
**Before (nested loops required):**
@@ -343,18 +344,18 @@ console.log(nthPseudo.selector_list.text) // ".foo"
343344
// Had to manually traverse to find selector list
344345
let child = pseudo.first_child
345346
while (child) {
346-
if (child.type === NODE_SELECTOR_NTH_OF) {
347-
let inner = child.first_child
348-
while (inner) {
349-
if (inner.type === NODE_SELECTOR_LIST) {
350-
processSelectors(inner)
351-
break
352-
}
353-
inner = inner.next_sibling
354-
}
355-
break
356-
}
357-
child = child.next_sibling
347+
if (child.type === NODE_SELECTOR_NTH_OF) {
348+
let inner = child.first_child
349+
while (inner) {
350+
if (inner.type === NODE_SELECTOR_LIST) {
351+
processSelectors(inner)
352+
break
353+
}
354+
inner = inner.next_sibling
355+
}
356+
break
357+
}
358+
child = child.next_sibling
358359
}
359360
```
360361
@@ -363,7 +364,7 @@ while (child) {
363364
```typescript
364365
// Simple and clear
365366
if (pseudo.selector_list) {
366-
processSelectors(pseudo.selector_list)
367+
processSelectors(pseudo.selector_list)
367368
}
368369
```
369370
@@ -380,9 +381,9 @@ const selector = root.first_child
380381
// Hot path: Calculate specificity (zero allocations)
381382
let [id, cls, type] = [0, 0, 0]
382383
for (let part of selector.compound_parts()) {
383-
if (part.type === NODE_SELECTOR_ID) id++
384-
else if (part.type === NODE_SELECTOR_CLASS) cls++
385-
else if (part.type === NODE_SELECTOR_TYPE) type++
384+
if (part.type === NODE_SELECTOR_ID) id++
385+
else if (part.type === NODE_SELECTOR_CLASS) cls++
386+
else if (part.type === NODE_SELECTOR_TYPE) type++
386387
}
387388
console.log('Specificity:', [id, cls, type]) // [1, 1, 1]
388389

@@ -398,7 +399,7 @@ console.log('Compounds:', all.length) // 3
398399
// [[div, .container, #app], [p, .text], [span]]
399400

400401
for (let compound of all) {
401-
console.log('Compound:', compound.map(n => n.text).join(''))
402+
console.log('Compound:', compound.map((n) => n.text).join(''))
402403
}
403404
// Output:
404405
// Compound: div.container#app
@@ -416,12 +417,12 @@ console.log('First text:', selector.first_compound_text) // "div.container#app"
416417
const compoundParts = []
417418
let selectorPart = selector.first_child
418419
while (selectorPart) {
419-
if (selectorPart.type === NODE_SELECTOR_COMBINATOR) break
420-
compoundParts.push(selectorPart)
421-
selectorPart = selectorPart.next_sibling
420+
if (selectorPart.type === NODE_SELECTOR_COMBINATOR) break
421+
compoundParts.push(selectorPart)
422+
selectorPart = selectorPart.next_sibling
422423
}
423424
// Then... REPARSING! ❌
424-
const text = compoundParts.map(n => n.text).join('')
425+
const text = compoundParts.map((n) => n.text).join('')
425426
const result = parse_selector(text) // Expensive!
426427
```
427428
@@ -434,11 +435,69 @@ for (let part of selector.compound_parts()) { ... } // Zero allocations
434435
```
435436
436437
**Performance Benefits**:
438+
437439
- `compound_parts()` iterator: 0 allocations, lazy evaluation
438440
- `first_compound`: Small array allocation (~40-200 bytes typical)
439441
- **10-20x faster** than reparsing approach
440442
- All operations O(n) where n = number of child nodes
441443
444+
### Example 11: Node Cloning
445+
446+
Convert arena-backed immutable nodes into mutable plain JavaScript objects for manipulation:
447+
448+
```typescript
449+
import { parse } from '@projectwallace/css-parser'
450+
451+
const ast = parse('div { margin: 10px 20px; padding: 5px; }')
452+
const rule = ast.first_child
453+
const block = rule.block
454+
const marginDecl = block.first_child
455+
456+
// Shallow clone (no children)
457+
const shallow = marginDecl.clone({ deep: false })
458+
console.log(shallow.type) // NODE_DECLARATION
459+
console.log(shallow.type_name) // "declaration"
460+
console.log(shallow.property) // "margin"
461+
console.log(shallow.children) // [] (empty array)
462+
463+
// Deep clone (includes all children)
464+
const deep = marginDecl.clone({ deep: true })
465+
console.log(deep.children.length) // 2 (dimension nodes)
466+
console.log(deep.children[0].value) // 10
467+
console.log(deep.children[0].unit) // "px"
468+
console.log(deep.children[1].value) // 20
469+
470+
// Clone with location information
471+
const withLocation = marginDecl.clone({ locations: true })
472+
console.log(withLocation.line) // 1
473+
console.log(withLocation.column) // 6
474+
console.log(withLocation.offset) // 6
475+
476+
// Cloned objects are mutable
477+
const clone = marginDecl.clone()
478+
clone.value = '0'
479+
clone.children.push({ type: 99, text: 'test', children: [] })
480+
// Original node unchanged ✅
481+
```
482+
483+
**Use Cases**:
484+
485+
- Convert nodes to plain objects for modification
486+
- Create synthetic AST nodes for tools
487+
- Extract and manipulate selector parts
488+
- Build custom transformations
489+
490+
**Options**:
491+
492+
- `deep?: boolean` (default: `true`) - Recursively clone children
493+
- `locations?: boolean` (default: `false`) - Include line/column/offset/length
494+
495+
**Return Type**: Plain object with:
496+
497+
- All node properties extracted (including `type_name`)
498+
- `children` as array (no linked lists)
499+
- Mutable - can be freely modified
500+
442501
---
443502
444503
## `parse_selector(source)`
@@ -626,10 +685,12 @@ For formatters and tools that need to reconstruct CSS, the parser distinguishes
626685
- `:lang(en)``has_children = true` (function syntax with content)
627686
628687
The `has_children` property on pseudo-class and pseudo-element nodes returns `true` if:
688+
629689
1. The node has actual child nodes (parsed content), OR
630690
2. The node uses function syntax (has parentheses), indicated by the `FLAG_HAS_PARENS` flag
631691
632692
This allows formatters to correctly reconstruct selectors:
693+
633694
- `:hover` → no parentheses needed
634695
- `:lang()` → parentheses needed (even though empty)
635696
@@ -674,19 +735,14 @@ Use these constants with the `node.attr_flags` property to identify case sensiti
674735
#### Example
675736
676737
```javascript
677-
import {
678-
parse_selector,
679-
NODE_SELECTOR_ATTRIBUTE,
680-
ATTR_OPERATOR_EQUAL,
681-
ATTR_FLAG_CASE_INSENSITIVE
682-
} from '@projectwallace/css-parser'
738+
import { parse_selector, NODE_SELECTOR_ATTRIBUTE, ATTR_OPERATOR_EQUAL, ATTR_FLAG_CASE_INSENSITIVE } from '@projectwallace/css-parser'
683739

684740
const ast = parse_selector('[type="text" i]')
685741

686742
for (let node of ast) {
687-
if (node.type === NODE_SELECTOR_ATTRIBUTE) {
688-
console.log(node.attr_operator === ATTR_OPERATOR_EQUAL) // true
689-
console.log(node.attr_flags === ATTR_FLAG_CASE_INSENSITIVE) // true
690-
}
743+
if (node.type === NODE_SELECTOR_ATTRIBUTE) {
744+
console.log(node.attr_operator === ATTR_OPERATOR_EQUAL) // true
745+
console.log(node.attr_flags === ATTR_FLAG_CASE_INSENSITIVE) // true
746+
}
691747
}
692748
```

0 commit comments

Comments
 (0)