-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnesting.go
More file actions
132 lines (119 loc) · 4.02 KB
/
nesting.go
File metadata and controls
132 lines (119 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package csshtml
import (
"strings"
scanner "github.com/speedata/css"
)
// flattenNestedBlocks recursively flattens CSS nesting. Nested selector blocks
// are converted into top-level blocks by combining the parent selector with
// the child selector. The & token is replaced with the parent selector; if no
// & is present, the parent selector is prepended as an ancestor (descendant
// combinator).
//
// Examples:
//
// .card { & > h2 { color: red } } → .card > h2 { color: red }
// .card { h2 { color: red } } → .card h2 { color: red }
// h2 { .card & { color: red } } → .card h2 { color: red }
// .a { &.b { color: red } } → .a.b { color: red }
func flattenNestedBlocks(blocks []*sBlock) []*sBlock {
var result []*sBlock
for _, block := range blocks {
result = append(result, flattenBlock(block, nil)...)
}
return result
}
// flattenBlock flattens a single block. parentSelector is nil for top-level blocks.
func flattenBlock(block *sBlock, parentSelector tokenstream) []*sBlock {
var result []*sBlock
// Build the effective selector for this block.
var effectiveSelector tokenstream
if parentSelector == nil {
effectiveSelector = block.componentValues
} else {
effectiveSelector = combineSelectors(parentSelector, block.componentValues)
}
// This block itself becomes a flat block (with only rules, no nested blocks).
if len(block.rules) > 0 {
flat := &sBlock{
componentValues: effectiveSelector,
rules: block.rules,
}
result = append(result, flat)
}
// Recursively flatten nested blocks.
for _, child := range block.blocks {
result = append(result, flattenBlock(child, effectiveSelector)...)
}
return result
}
// combineSelectors combines a parent selector with a child selector.
// If the child contains &, it is replaced with the parent selector.
// If no & is present, the parent is prepended with a space (descendant combinator).
func combineSelectors(parent, child tokenstream) tokenstream {
parent = trimTrailingSpace(parent)
child = trimTrailingSpace(child)
if hasAmpersand(child) {
return substituteAmpersand(parent, child)
}
// No &: prepend parent with space (descendant combinator).
combined := make(tokenstream, 0, len(parent)+1+len(child))
combined = append(combined, parent...)
combined = append(combined, &scanner.Token{Type: scanner.S, Value: " "})
combined = append(combined, child...)
return combined
}
// trimTrailingSpace removes trailing whitespace tokens.
func trimTrailingSpace(toks tokenstream) tokenstream {
for len(toks) > 0 && toks[len(toks)-1].Type == scanner.S {
toks = toks[:len(toks)-1]
}
return toks
}
// hasAmpersand returns true if the token stream contains an & delimiter.
func hasAmpersand(toks tokenstream) bool {
for _, tok := range toks {
if tok.Type == scanner.Delim && tok.Value == "&" {
return true
}
}
return false
}
// substituteAmpersand replaces each & in child with the parent selector.
// Handles cases like:
//
// & > h2 → parent > h2
// &.active → parent.active (no extra space)
// .wrap & → .wrap parent
func substituteAmpersand(parent, child tokenstream) tokenstream {
var result tokenstream
for _, tok := range child {
if tok.Type == scanner.Delim && tok.Value == "&" {
result = append(result, parent...)
} else {
result = append(result, tok)
}
}
return result
}
// selectorString converts a tokenstream to its string representation,
// suitable for passing to cascadia. It produces correct CSS selector syntax:
// no space before . # : (they attach to the preceding simple selector),
// but preserves whitespace tokens as descendant combinators.
func selectorString(toks tokenstream) string {
var sb strings.Builder
for _, tok := range toks {
switch tok.Type {
case scanner.S:
sb.WriteByte(' ')
case scanner.Function:
sb.WriteString(tok.Value)
sb.WriteByte('(')
case scanner.Hash:
// Value already contains "#" prefix from fixupComponentValues
sb.WriteString(tok.Value)
default:
sb.WriteString(tok.Value)
}
}
return strings.TrimSpace(sb.String())
}