fix(html): scope selector lists correctly
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 20:16:26 +00:00
parent 967182a676
commit b5d170817c
2 changed files with 72 additions and 1 deletions

View file

@ -81,7 +81,7 @@ func ScopeVariant(name, selector string) string {
return scope
}
parts := strings.Split(selector, ",")
parts := splitSelectorList(selector)
scoped := make([]string, 0, len(parts))
for i := range parts {
part := strings.TrimSpace(parts[i])
@ -96,6 +96,69 @@ func ScopeVariant(name, selector string) string {
return strings.Join(scoped, ", ")
}
// splitSelectorList splits a CSS selector list on top-level commas only.
// Commas inside brackets, parentheses, braces, or quoted strings are preserved.
func splitSelectorList(selector string) []string {
if selector == "" {
return nil
}
parts := make([]string, 0, 1)
var b strings.Builder
var quote rune
depthParen := 0
depthBracket := 0
depthBrace := 0
for _, r := range selector {
switch {
case quote != 0:
b.WriteRune(r)
if r == quote {
quote = 0
} else if r == '\\' {
// Keep escaped characters inside quoted strings intact.
continue
}
case r == '"' || r == '\'':
quote = r
b.WriteRune(r)
case r == '(':
depthParen++
b.WriteRune(r)
case r == ')':
if depthParen > 0 {
depthParen--
}
b.WriteRune(r)
case r == '[':
depthBracket++
b.WriteRune(r)
case r == ']':
if depthBracket > 0 {
depthBracket--
}
b.WriteRune(r)
case r == '{':
depthBrace++
b.WriteRune(r)
case r == '}':
if depthBrace > 0 {
depthBrace--
}
b.WriteRune(r)
case r == ',' && depthParen == 0 && depthBracket == 0 && depthBrace == 0:
parts = append(parts, b.String())
b.Reset()
default:
b.WriteRune(r)
}
}
parts = append(parts, b.String())
return parts
}
// responsive.go: Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
// Example: r.Variant("mobile", NewLayout("C").C(Raw("body"))).
// Variants render in insertion order.

View file

@ -215,3 +215,11 @@ func TestScopeVariant_IgnoresEmptySelectorSegments(t *testing.T) {
t.Fatalf("ScopeVariant should skip empty selector segments = %q, want %q", got, want)
}
}
func TestScopeVariant_PreservesNestedCommas(t *testing.T) {
got := ScopeVariant("desktop", `:is(.nav, .sidebar), .footer`)
want := `[data-variant="desktop"] :is(.nav, .sidebar), [data-variant="desktop"] .footer`
if got != want {
t.Fatalf("ScopeVariant should preserve nested commas = %q, want %q", got, want)
}
}