rastrillo / fiesta Public

Read leaf rules with the shared CSS scanner, so a quoted brace is text

leafRules found rules with a pattern over braces, which knows nothing
of strings. [rst-snippet] { --rst-x: "}"; cursor: pointer; } ended at
the quoted brace, so its cursor: pointer was lost, and the tap
inventory, which finds controls by that declaration, never saw the
control. A quoted { started a rule whose selector was the text before
it.

It now walks the stylesheet with cssSpan, the scanner the gates use,
and takes a brace only outside a string. On every stylesheet in css/,
the 1.0 seed and both releases it finds exactly the rules the pattern
did. It returns an error for what the scanner refuses; leafRules fails
the test with it, as stripCSSComments does, and the print-shape check
returns it.

Red: TestLeafRulesReadABraceInAStringAsText, on the pattern, reads the
snippet's body as ` --rst-x: "` and the quoted { rule's selector as
`content: '`, and interactiveCSSRules misses [rst-snippet]. Two
mutations each fail it: stepping one byte at a time instead of a span,
and not closing the block at a }. Green: the check package, and the
browser test that reads the floor block's rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev dbd18a65836d140602718cd75a776951a3cbef4e parent 1ab0dfc
3 files changed, +84 −29
  • check/css_test.go +79 −24
  • check/sizing_browser_test.go +1 −1
  • check/sizing_test.go +4 −4
diff --git a/check/css_test.go b/check/css_test.go
index 3494a20..a678209 100644
--- a/check/css_test.go
+++ b/check/css_test.go
@@ -78,7 +78,11 @@ func shapeLeaks(css string) ([]string, error) {
}
start := idx + i + len(header)
end := braceMatchEnd(css, start)
- for _, r := range leafRules(css[start:end]) {
+ rules, err := readLeafRules(css[start:end])
+ if err != nil {
+ return nil, err
+ }
+ for _, r := range rules {
if r.selector == "html:root" && strings.Contains(r.body, flatten) {
atRoot = true
}
@@ -361,26 +365,77 @@ func reducedMotionBlocks(css string) (blocks []string, rest string) {
// declaration body.
type leafRule struct{ selector, body string }
-// leafRulePattern extracts every innermost selector{...} rule in css —
-// this naturally includes rules nested inside an @media block (matched
-// before the wrapping @media's own, still-open brace), since none of
-// them nest further themselves.
-var leafRulePattern = regexp.MustCompile(`([^{}]+)\{([^{}]*)\}`)
-
-// leafRules is every innermost rule in css, which must have had its
-// comments taken out first, with stripCSSComments: a comment between
-// two rules would otherwise be read as part of the next one's
-// selector, and a comment reader of its own here would be one more
-// that a /* inside a string could fool.
-func leafRules(css string) []leafRule {
+// readLeafRules is every innermost rule in css, a rule nested in an
+// @media block included, its selector being everything since the brace
+// before it. It finds strings with cssSpan, as the gates do, so a
+// brace inside one is text: a pattern over braces cut
+// [rst-snippet] { --rst-x: "}"; cursor: pointer; } at the quoted
+// brace, and the size gate never saw the pointer. css must have had
+// its comments taken out first, with stripCSSComments: a comment
+// between two rules would otherwise be read as part of the next one's
+// selector.
+func readLeafRules(css string) ([]leafRule, error) {
var out []leafRule
- for _, m := range leafRulePattern.FindAllStringSubmatch(css, -1) {
- // Fields+Join collapses the newlines and indentation between
- // rules down to single spaces, on top of trimming the ends.
- sel := strings.Join(strings.Fields(m[1]), " ")
- out = append(out, leafRule{selector: sel, body: m[2]})
+ // open is the { of the block being read, or -1 once a block has
+ // closed inside it, which makes it no leaf. prelude starts just
+ // past the brace before it.
+ open, prelude := -1, 0
+ selector := ""
+ for i := 0; i < len(css); {
+ end, _, err := cssSpan(css, i)
+ if err != nil {
+ return nil, err
+ }
+ switch css[i] {
+ case '{':
+ open, selector, prelude = i, css[prelude:i], i+1
+ case '}':
+ if open >= 0 && selector != "" {
+ // Fields+Join collapses the newlines and indentation
+ // between rules down to single spaces, on top of
+ // trimming the ends.
+ sel := strings.Join(strings.Fields(selector), " ")
+ out = append(out, leafRule{selector: sel, body: css[open+1 : i]})
+ }
+ open, prelude = -1, i+1
+ }
+ i = end
+ }
+ return out, nil
+}
+
+// leafRules is readLeafRules failing the test on a stylesheet it
+// refuses.
+func leafRules(t testing.TB, css string) []leafRule {
+ t.Helper()
+ rules, err := readLeafRules(css)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return rules
+}
+
+// A brace inside a string is text to a browser, so it neither ends a
+// rule nor starts one. Cut at the quoted } instead, the first rule
+// below loses its cursor: pointer, and the tap inventory never sees
+// the control it styles.
+func TestLeafRulesReadABraceInAStringAsText(t *testing.T) {
+ const css = `[rst-snippet] { --rst-x: "}"; cursor: pointer; } [rst-a] { content: '{'; color: red } @media (x) { [rst-b] { cursor: pointer } }`
+ want := []leafRule{
+ {selector: "[rst-snippet]", body: ` --rst-x: "}"; cursor: pointer; `},
+ {selector: "[rst-a]", body: ` content: '{'; color: red `},
+ {selector: "[rst-b]", body: " cursor: pointer "},
+ }
+ if got := leafRules(t, css); !reflect.DeepEqual(got, want) {
+ t.Errorf("leafRules(%q) =\n%q\nwant\n%q", css, got, want)
+ }
+ var controls []string
+ for _, r := range interactiveCSSRules(t, css) {
+ controls = append(controls, r.selector)
+ }
+ if want := []string{"[rst-snippet]", "[rst-b]"}; !reflect.DeepEqual(controls, want) {
+ t.Errorf("interactiveCSSRules(%q) finds %q, want %q", css, controls, want)
}
- return out
}
// selectorList splits a (possibly comma-separated) selector into its
@@ -424,10 +479,10 @@ func motionDecls(body string) []motionDecl {
// that exact selector.
type reduceIndex map[string]map[string]bool
-func buildReduceIndex(blocks []string) reduceIndex {
+func buildReduceIndex(t testing.TB, blocks []string) reduceIndex {
idx := reduceIndex{}
for _, block := range blocks {
- for _, rule := range leafRules(block) {
+ for _, rule := range leafRules(t, block) {
for _, sel := range selectorList(rule.selector) {
for _, d := range motionDecls(rule.body) {
if d.val != "none" {
@@ -476,10 +531,10 @@ func assertReducedMotion(t *testing.T, name, css string) {
if len(reduceBlocks) == 0 {
t.Fatal(name + " declares no @media (prefers-reduced-motion: reduce) block at all")
}
- idx := buildReduceIndex(reduceBlocks)
+ idx := buildReduceIndex(t, reduceBlocks)
tested := 0
- for _, rule := range leafRules(rest) {
+ for _, rule := range leafRules(t, rest) {
decls := motionDecls(rule.body)
if len(decls) == 0 {
continue
@@ -772,7 +827,7 @@ func TestSrOnlyUtilityComesAfterTheRulesItMustBeat(t *testing.T) {
// are checked, and the width is checked for being non-zero.
func TestEveryMenuSurfaceStylesItsSeparator(t *testing.T) {
styled := map[string]bool{}
- for _, rule := range leafRules(stripCSSComments(t, string(fiesta.TokensCSS()))) {
+ for _, rule := range leafRules(t, stripCSSComments(t, string(fiesta.TokensCSS()))) {
decls := declarations(rule.body)
if !resetsBorder(decls["border"]) || !drawsARule(decls["border-top"]) {
continue
diff --git a/check/sizing_browser_test.go b/check/sizing_browser_test.go
index 05eae2d..cd54c14 100644
--- a/check/sizing_browser_test.go
+++ b/check/sizing_browser_test.go
@@ -1025,7 +1025,7 @@ var mayShrink = map[string]string{
func TestTheFloorChangesNothingButSizeOnADesktop(t *testing.T) {
without, block := floorBlock(t)
var sels []string
- for _, r := range leafRules(stripCSSComments(t, block)) {
+ for _, r := range leafRules(t, stripCSSComments(t, block)) {
for _, s := range splitSelectorList(r.selector) {
if !strings.HasPrefix(s, ":where(") {
sels = append(sels, s)
diff --git a/check/sizing_test.go b/check/sizing_test.go
index 7e3252c..6ddef17 100644
--- a/check/sizing_test.go
+++ b/check/sizing_test.go
@@ -165,7 +165,7 @@ func TestTheTargetFloorIsOneToken(t *testing.T) {
continue
}
found := false
- for _, r := range leafRules(everyWidth) {
+ for _, r := range leafRules(t, everyWidth) {
if !sized.MatchString(r.body) {
continue
}
@@ -235,7 +235,7 @@ var floorDeclarations = map[string]struct {
// either here with its reason, or it goes in the touch query.
func TestTheFloorBlockCarriesOnlySizes(t *testing.T) {
_, block := floorBlock(t)
- rules := leafRules(stripCSSComments(t, block))
+ rules := leafRules(t, stripCSSComments(t, block))
if len(rules) < 30 {
t.Fatalf("read %d rules in the floor block; the parse no longer finds them", len(rules))
}
@@ -402,7 +402,7 @@ var iconFree = map[string]string{
// rule exists to stop.
func TestEveryControlSizesTheIconInIt(t *testing.T) {
var sel string
- for _, r := range leafRules(stripCSSComments(t, string(fiesta.TokensCSS()))) {
+ for _, r := range leafRules(t, stripCSSComments(t, string(fiesta.TokensCSS()))) {
if strings.HasPrefix(r.selector, ":where(") && strings.Contains(r.selector, ") .icon") && strings.Contains(r.body, "block-size: 1rem") {
sel = collapseSpace(r.selector)
}
@@ -453,7 +453,7 @@ func endsOnControlElement(sel string) bool {
func interactiveCSSRules(t testing.TB, css string) []leafRule {
t.Helper()
var out []leafRule
- for _, r := range leafRules(stripCSSComments(t, css)) {
+ for _, r := range leafRules(t, stripCSSComments(t, css)) {
hit := strings.Contains(r.body, "cursor: pointer")
for _, s := range splitSelectorList(r.selector) {
hit = hit || endsOnControlElement(s)