diff --git a/parser.go b/parser.go index 993145f..4f8810e 100644 --- a/parser.go +++ b/parser.go @@ -423,7 +423,7 @@ var errExpectedClosingParenthesis = errors.New("expected ')' but didn't find it" var errUnmatchedParenthesis = errors.New("unmatched '('") // parsePseudoclassSelector parses a pseudoclass selector like :not(p) -func (p *parser) parsePseudoclassSelector() (out Matcher, err error) { +func (p *parser) parsePseudoclassSelector() (out Sel, err error) { if p.i >= len(p.s) { return nil, fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead") } @@ -685,8 +685,8 @@ invalid: // parseSimpleSelectorSequence parses a selector sequence that applies to // a single element. -func (p *parser) parseSimpleSelectorSequence() (Matcher, error) { - var selectors []Matcher +func (p *parser) parseSimpleSelectorSequence() (Sel, error) { + var selectors []Sel if p.i >= len(p.s) { return nil, errors.New("expected selector, found EOF instead") @@ -709,7 +709,7 @@ func (p *parser) parseSimpleSelectorSequence() (Matcher, error) { loop: for p.i < len(p.s) { var ( - ns Matcher + ns Sel err error ) switch p.s[p.i] { diff --git a/selector.go b/selector.go index 0fda705..18ce116 100644 --- a/selector.go +++ b/selector.go @@ -20,6 +20,7 @@ type Matcher interface { // future. type Sel interface { Matcher + Specificity() Specificity } // Parse parses a selector. @@ -177,6 +178,10 @@ func (t tagSelector) Match(n *html.Node) bool { return n.Type == html.ElementNode && n.Data == t.tag } +func (c tagSelector) Specificity() Specificity { + return Specificity{0, 0, 1} +} + type classSelector struct { class string } @@ -188,6 +193,10 @@ func (t classSelector) Match(n *html.Node) bool { }) } +func (c classSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type idSelector struct { id string } @@ -199,6 +208,10 @@ func (t idSelector) Match(n *html.Node) bool { }) } +func (c idSelector) Specificity() Specificity { + return Specificity{1, 0, 0} +} + type attrSelector struct { key, val, operation string regexp *regexp.Regexp @@ -335,12 +348,16 @@ func attributeRegexMatch(key string, rx *regexp.Regexp, n *html.Node) bool { }) } +func (c attrSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + // ---------------- Pseudo class selectors ---------------- // we use severals concrete types of pseudo-class selectors type relativePseudoClassSelector struct { name string // one of "not", "has", "haschild" - match Matcher + match SelectorGroup } func (s relativePseudoClassSelector) Match(n *html.Node) bool { @@ -384,6 +401,20 @@ func hasDescendantMatch(n *html.Node, a Matcher) bool { return false } +// Specificity returns the specificity of the most specific selectors +// in the pseudo-class arguments. +// See https://www.w3.org/TR/selectors/#specificity-rules +func (s relativePseudoClassSelector) Specificity() Specificity { + var max Specificity + for _, sel := range s.match { + newSpe := sel.Specificity() + if max.Less(newSpe) { + max = newSpe + } + } + return max +} + type containsPseudoClassSelector struct { own bool value string @@ -401,6 +432,10 @@ func (s containsPseudoClassSelector) Match(n *html.Node) bool { return strings.Contains(text, s.value) } +func (s containsPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type regexpPseudoClassSelector struct { own bool regexp *regexp.Regexp @@ -449,6 +484,10 @@ func nodeOwnText(n *html.Node) string { return b.String() } +func (s regexpPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type nthPseudoClassSelector struct { a, b int last, ofType bool @@ -578,6 +617,12 @@ func simpleNthLastChildMatch(b int, ofType bool, n *html.Node) bool { return false } +// Specificity for nth-child pseudo-class. +// Does not support a list of selectors +func (s nthPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type onlyChildPseudoClassSelector struct { ofType bool } @@ -612,6 +657,10 @@ func (s onlyChildPseudoClassSelector) Match(n *html.Node) bool { return count == 1 } +func (s onlyChildPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type inputPseudoClassSelector struct{} // Matches input, select, textarea and button elements. @@ -619,6 +668,10 @@ func (s inputPseudoClassSelector) Match(n *html.Node) bool { return n.Type == html.ElementNode && (n.Data == "input" || n.Data == "select" || n.Data == "textarea" || n.Data == "button") } +func (s inputPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type emptyElementPseudoClassSelector struct{} // Matches empty elements. @@ -637,6 +690,10 @@ func (s emptyElementPseudoClassSelector) Match(n *html.Node) bool { return true } +func (s emptyElementPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type rootPseudoClassSelector struct{} // Match implements :root @@ -650,8 +707,12 @@ func (s rootPseudoClassSelector) Match(n *html.Node) bool { return n.Parent.Type == html.DocumentNode } +func (s rootPseudoClassSelector) Specificity() Specificity { + return Specificity{0, 1, 0} +} + type compoundSelector struct { - selectors []Matcher + selectors []Sel } // Matches elements if each sub-selectors matches. @@ -668,10 +729,18 @@ func (t compoundSelector) Match(n *html.Node) bool { return true } +func (s compoundSelector) Specificity() Specificity { + var out Specificity + for _, sel := range s.selectors { + out = out.Add(sel.Specificity()) + } + return out +} + type combinedSelector struct { - first Matcher + first Sel combinator byte - second Matcher + second Sel } func (t combinedSelector) Match(n *html.Node) bool { @@ -741,6 +810,14 @@ func siblingMatch(s1, s2 Matcher, adjacent bool, n *html.Node) bool { return false } +func (s combinedSelector) Specificity() Specificity { + spec := s.first.Specificity() + if s.second != nil { + spec = spec.Add(s.second.Specificity()) + } + return spec +} + // A SelectorGroup is a list of selectors, which matches if any of the // individual selectors matches. type SelectorGroup []Sel diff --git a/specificity.go b/specificity.go new file mode 100644 index 0000000..8db864f --- /dev/null +++ b/specificity.go @@ -0,0 +1,26 @@ +package cascadia + +// Specificity is the CSS specificity as defined in +// https://www.w3.org/TR/selectors/#specificity-rules +// with the convention Specificity = [A,B,C]. +type Specificity [3]int + +// returns `true` if s < other (strictly), false otherwise +func (s Specificity) Less(other Specificity) bool { + for i := range s { + if s[i] < other[i] { + return true + } + if s[i] > other[i] { + return false + } + } + return false +} + +func (s Specificity) Add(other Specificity) Specificity { + for i, sp := range other { + s[i] += sp + } + return s +} diff --git a/specificity_test.go b/specificity_test.go new file mode 100644 index 0000000..b9d47ed --- /dev/null +++ b/specificity_test.go @@ -0,0 +1,107 @@ +package cascadia + +import ( + "strings" + "testing" + + "golang.org/x/net/html" +) + +type testSpec struct { + // html, css selector + HTML, selector string + // correct specificity + spec Specificity +} + +var testsSpecificity = []testSpec{ + { + HTML: `
`, + selector: ":not(em, strong#foo)", + spec: Specificity{1, 0, 1}, + }, + { + HTML: `
`, + selector: "*", + spec: Specificity{0, 0, 0}, + }, + { + HTML: `
`, + selector: "ul", + spec: Specificity{0, 0, 1}, + }, + { + HTML: `
`, + selector: "ul li", + spec: Specificity{0, 0, 2}, + }, + { + HTML: `
`, + selector: "ul ol+li", + spec: Specificity{0, 0, 3}, + }, + { + HTML: `
`, + selector: "H1 + *[REL=up] ", + spec: Specificity{0, 1, 1}, + }, + { + HTML: ``, + selector: "UL OL LI.red", + spec: Specificity{0, 1, 3}, + }, + { + HTML: ``, + selector: "LI.red.level", + spec: Specificity{0, 2, 1}, + }, + { + HTML: ``, + selector: "#x34y", + spec: Specificity{1, 0, 0}, + }, + { + HTML: ``, + selector: "#s12:not(FOO)", + spec: Specificity{1, 0, 1}, + }, + { + HTML: ``, + selector: "#s12:not(FOO)", + spec: Specificity{1, 0, 1}, + }, + { + HTML: ``, + selector: "#s12:empty", + spec: Specificity{1, 1, 0}, + }, + { + HTML: ``, + selector: "#s12:only-child", + spec: Specificity{1, 1, 0}, + }, +} + +func TestSpecificity(t *testing.T) { + for _, test := range testsSpecificity { + s, err := Parse(test.selector) + if err != nil { + t.Fatalf("error compiling %q: %s", test.selector, err) + } + + doc, err := html.Parse(strings.NewReader(test.HTML)) + if err != nil { + t.Fatalf("error parsing %q: %s", test.HTML, err) + } + body := doc.FirstChild.LastChild + testNode := body.FirstChild.FirstChild.LastChild + if !s.Match(testNode) { + t.Errorf("%s didn't match (html tree : \n %s) \n", test.selector, nodeString(doc)) + continue + } + gotSpec := s.Specificity() + if gotSpec != test.spec { + t.Errorf("wrong specificity : expected %v, got %v", test.spec, gotSpec) + } + } +}