Skip to content

Commit

Permalink
Add support for pseudo elements (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler authored and andybalholm committed Sep 30, 2019
1 parent b69f6c9 commit dfaf055
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 38 deletions.
91 changes: 64 additions & 27 deletions parser.go
Expand Up @@ -13,6 +13,10 @@ import (
type parser struct {
s string // the source text
i int // the current position

// if `false`, parsing a pseudo-element
// returns an error.
acceptPseudoElements bool
}

// parseEscape parses a backslash escape.
Expand Down Expand Up @@ -422,17 +426,25 @@ var errExpectedParenthesis = errors.New("expected '(' but didn't find it")
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 Sel, err error) {
// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
// https://drafts.csswg.org/selectors-3/#pseudo-elements
// Returning a nil `Sel` (and a nil `error`) means we found a pseudo-element.
func (p *parser) parsePseudoclassSelector() (out Sel, pseudoElement string, err error) {
if p.i >= len(p.s) {
return nil, fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead")
return nil, "", fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead")
}
if p.s[p.i] != ':' {
return nil, fmt.Errorf("expected attribute selector (:pseudoclass), found '%c' instead", p.s[p.i])
return nil, "", fmt.Errorf("expected attribute selector (:pseudoclass), found '%c' instead", p.s[p.i])
}

p.i++
var mustBePseudoElement bool
if p.i >= len(p.s) {
return nil, "", fmt.Errorf("got empty pseudoclass (or pseudoelement)")
}
if p.s[p.i] == ':' { // we found a pseudo-element
mustBePseudoElement = true
p.i++
}

Expand All @@ -441,27 +453,33 @@ func (p *parser) parsePseudoclassSelector() (out Sel, err error) {
return
}
name = toLowerASCII(name)
if mustBePseudoElement && (name != "after" && name != "backdrop" && name != "before" &&
name != "cue" && name != "first-letter" && name != "first-line" && name != "grammar-error" &&
name != "marker" && name != "placeholder" && name != "selection" && name != "spelling-error") {
return out, "", fmt.Errorf("unknown pseudoelement :%s", name)
}

switch name {
case "not", "has", "haschild":
if !p.consumeParenthesis() {
return out, errExpectedParenthesis
return out, "", errExpectedParenthesis
}
sel, parseErr := p.parseSelectorGroup()
if parseErr != nil {
return out, parseErr
return out, "", parseErr
}
if !p.consumeClosingParenthesis() {
return out, errExpectedClosingParenthesis
return out, "", errExpectedClosingParenthesis
}

out = relativePseudoClassSelector{name: name, match: sel}

case "contains", "containsown":
if !p.consumeParenthesis() {
return out, errExpectedParenthesis
return out, "", errExpectedParenthesis
}
if p.i == len(p.s) {
return out, errUnmatchedParenthesis
return out, "", errUnmatchedParenthesis
}
var val string
switch p.s[p.i] {
Expand All @@ -471,46 +489,46 @@ func (p *parser) parsePseudoclassSelector() (out Sel, err error) {
val, err = p.parseIdentifier()
}
if err != nil {
return out, err
return out, "", err
}
val = strings.ToLower(val)
p.skipWhitespace()
if p.i >= len(p.s) {
return out, errors.New("unexpected EOF in pseudo selector")
return out, "", errors.New("unexpected EOF in pseudo selector")
}
if !p.consumeClosingParenthesis() {
return out, errExpectedClosingParenthesis
return out, "", errExpectedClosingParenthesis
}

out = containsPseudoClassSelector{own: name == "containsown", value: val}

case "matches", "matchesown":
if !p.consumeParenthesis() {
return out, errExpectedParenthesis
return out, "", errExpectedParenthesis
}
rx, err := p.parseRegex()
if err != nil {
return out, err
return out, "", err
}
if p.i >= len(p.s) {
return out, errors.New("unexpected EOF in pseudo selector")
return out, "", errors.New("unexpected EOF in pseudo selector")
}
if !p.consumeClosingParenthesis() {
return out, errExpectedClosingParenthesis
return out, "", errExpectedClosingParenthesis
}

out = regexpPseudoClassSelector{own: name == "matchesown", regexp: rx}

case "nth-child", "nth-last-child", "nth-of-type", "nth-last-of-type":
if !p.consumeParenthesis() {
return out, errExpectedParenthesis
return out, "", errExpectedParenthesis
}
a, b, err := p.parseNth()
if err != nil {
return out, err
return out, "", err
}
if !p.consumeClosingParenthesis() {
return out, errExpectedClosingParenthesis
return out, "", errExpectedClosingParenthesis
}
last := name == "nth-last-child" || name == "nth-last-of-type"
ofType := name == "nth-of-type" || name == "nth-last-of-type"
Expand All @@ -535,9 +553,9 @@ func (p *parser) parsePseudoclassSelector() (out Sel, err error) {
case "root":
out = rootPseudoClassSelector{}
case "after", "backdrop", "before", "cue", "first-letter", "first-line", "grammar-error", "marker", "placeholder", "selection", "spelling-error":
return out, errors.New("pseudo-elements are not yet supported")
return nil, name, nil
default:
return out, fmt.Errorf("unknown pseudoclass or pseudoelement :%s", name)
return out, "", fmt.Errorf("unknown pseudoclass or pseudoelement :%s", name)
}
return
}
Expand Down Expand Up @@ -706,11 +724,13 @@ func (p *parser) parseSimpleSelectorSequence() (Sel, error) {
selectors = append(selectors, r)
}

var pseudoElement string
loop:
for p.i < len(p.s) {
var (
ns Sel
err error
ns Sel
newPseudoElement string
err error
)
switch p.s[p.i] {
case '#':
Expand All @@ -720,20 +740,37 @@ loop:
case '[':
ns, err = p.parseAttributeSelector()
case ':':
ns, err = p.parsePseudoclassSelector()
ns, newPseudoElement, err = p.parsePseudoclassSelector()
default:
break loop
}
if err != nil {
return nil, err
}
// From https://drafts.csswg.org/selectors-3/#pseudo-elements :
// "Only one pseudo-element may appear per selector, and if present
// it must appear after the sequence of simple selectors that
// represents the subjects of the selector.""
if ns == nil { // we found a pseudo-element
if pseudoElement != "" {
return nil, fmt.Errorf("only one pseudo-element is accepted per selector, got %s and %s", pseudoElement, newPseudoElement)
}
if !p.acceptPseudoElements {
return nil, fmt.Errorf("pseudo-element %s found, but pseudo-elements support is disabled", newPseudoElement)
}
pseudoElement = newPseudoElement
} else {
if pseudoElement != "" {
return nil, fmt.Errorf("pseudo-element %s must be at the end of selector", pseudoElement)
}
selectors = append(selectors, ns)
}

selectors = append(selectors, ns)
}
if len(selectors) == 1 { // no need wrap the selectors in compoundSelector
if len(selectors) == 1 && pseudoElement == "" { // no need wrap the selectors in compoundSelector
return selectors[0], nil
}
return compoundSelector{selectors: selectors}, nil
return compoundSelector{selectors: selectors, pseudoElement: pseudoElement}, nil
}

// parseSelector parses a selector that may include combinators.
Expand Down

0 comments on commit dfaf055

Please sign in to comment.