@@ -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
283284for (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
303304if (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
329330console.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
344345let child = pseudo.first_child
345346while (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
365366if (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)
381382let [id, cls, type] = [0, 0, 0]
382383for (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}
387388console.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
400401for (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"
416417const compoundParts = []
417418let selectorPart = selector.first_child
418419while (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(' ' )
425426const 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
628687The ` has_children ` property on pseudo-class and pseudo-element nodes returns ` true ` if:
688+
6296891. The node has actual child nodes (parsed content), OR
6306902. The node uses function syntax (has parentheses), indicated by the ` FLAG_HAS_PARENS ` flag
631691
632692This 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
684740const ast = parse_selector (' [type="text" i]' )
685741
686742for (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