| 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 |