Skip to content

Commit

Permalink
Add specificity support (#39)
Browse files Browse the repository at this point in the history
add support for specificity for simple selectors
  • Loading branch information
benoitkugler authored and andybalholm committed Sep 21, 2019
1 parent 522016b commit b69f6c9
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 8 deletions.
8 changes: 4 additions & 4 deletions parser.go
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
Expand All @@ -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] {
Expand Down
85 changes: 81 additions & 4 deletions selector.go
Expand Up @@ -20,6 +20,7 @@ type Matcher interface {
// future.
type Sel interface {
Matcher
Specificity() Specificity
}

// Parse parses a selector.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -612,13 +657,21 @@ 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.
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.
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions 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
}
107 changes: 107 additions & 0 deletions 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: `<html><body><div><div><a href="http://www.foo.com"></a></div></div></body></html>`,
selector: ":not(em, strong#foo)",
spec: Specificity{1, 0, 1},
},
{
HTML: `<html><body><div><div><a href="http://www.foo.com"></a></div></div></body></html>`,
selector: "*",
spec: Specificity{0, 0, 0},
},
{
HTML: `<html><body><div><div><ul></ul></div></div></body></html>`,
selector: "ul",
spec: Specificity{0, 0, 1},
},
{
HTML: `<html><body><div><ul><li></li></ul></div></body></html>`,
selector: "ul li",
spec: Specificity{0, 0, 2},
},
{
HTML: `<html><body><div><ul><ol></ol><li></li></ul></div></body></html>`,
selector: "ul ol+li",
spec: Specificity{0, 0, 3},
},
{
HTML: `<html><body><div><ul><h1></h1><li rel="up"></li></ul></div></body></html>`,
selector: "H1 + *[REL=up] ",
spec: Specificity{0, 1, 1},
},
{
HTML: `<html><body><ul><ol><li class="red"></li></ol></ul></body></html>`,
selector: "UL OL LI.red",
spec: Specificity{0, 1, 3},
},
{
HTML: `<html><body><ul><ol><li class="red level"></li></ol></ul></body></html>`,
selector: "LI.red.level",
spec: Specificity{0, 2, 1},
},
{
HTML: `<html><body><ul><ol><li id="x34y"></li></ol></ul></body></html>`,
selector: "#x34y",
spec: Specificity{1, 0, 0},
},
{
HTML: `<html><body><ul><ol><li id="s12"></li></ol></ul></body></html>`,
selector: "#s12:not(FOO)",
spec: Specificity{1, 0, 1},
},
{
HTML: `<html><body><ul><ol><li id="s12"></li></ol></ul></body></html>`,
selector: "#s12:not(FOO)",
spec: Specificity{1, 0, 1},
},
{
HTML: `<html><body><ul><ol><li id="s12"></li></ol></ul></body></html>`,
selector: "#s12:empty",
spec: Specificity{1, 1, 0},
},
{
HTML: `<html><body><ul><ol><li id="s12"></li></ol></ul></body></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)
}
}
}

0 comments on commit b69f6c9

Please sign in to comment.