1// Package cascadia is an implementation of CSS selectors.
12// a parser for CSS selectors
14 s string // the source text
15 i int // the current position
17 // if `false`, parsing a pseudo-element
19 acceptPseudoElements bool
22// parseEscape parses a backslash escape.
23func (p *parser) parseEscape() (result string, err error) {
24 if len(p.s) < p.i+2 || p.s[p.i] != '\\' {
25 return "", errors.New("invalid escape sequence")
31 case c == '\r' || c == '\n' || c == '\f':
32 return "", errors.New("escaped line ending outside string")
34 // unicode escape (hex)
36 for i = start; i < start+6 && i < len(p.s) && hexDigit(p.s[i]); i++ {
39 v, _ := strconv.ParseUint(p.s[start:i], 16, 64)
44 if len(p.s) > i && p.s[i] == '\n' {
47 case ' ', '\t', '\n', '\f':
52 return string(rune(v)), nil
55 // Return the literal character after the backslash.
56 result = p.s[start : start+1]
61// toLowerASCII returns s with all ASCII capital letters lowercased.
62func toLowerASCII(s string) string {
64 for i := 0; i < len(s); i++ {
65 if c := s[i]; 'A' <= c && c <= 'Z' {
67 b = make([]byte, len(s))
70 b[i] = s[i] + ('a' - 'A')
81func hexDigit(c byte) bool {
82 return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
85// nameStart returns whether c can be the first character of an identifier
86// (not counting an initial hyphen, or an escape sequence).
87func nameStart(c byte) bool {
88 return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127
91// nameChar returns whether c can be a character within an identifier
92// (not counting an escape sequence).
93func nameChar(c byte) bool {
94 return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127 ||
95 c == '-' || '0' <= c && c <= '9'
98// parseIdentifier parses an identifier.
99func (p *parser) parseIdentifier() (result string, err error) {
103 for len(p.s) > p.i && p.s[p.i] == prefix {
109 return "", errors.New("expected identifier, found EOF instead")
112 if c := p.s[p.i]; !(nameStart(c) || c == '\\') {
113 return "", fmt.Errorf("expected identifier, found %c instead", c)
116 result, err = p.parseName()
117 if numPrefix > 0 && err == nil {
118 result = strings.Repeat(string(prefix), numPrefix) + result
123// parseName parses a name (which is like an identifier, but doesn't have
124// extra restrictions on the first character).
125func (p *parser) parseName() (result string, err error) {
133 for i < len(p.s) && nameChar(p.s[i]) {
136 result += p.s[start:i]
139 val, err := p.parseEscape()
151 return "", errors.New("expected name, found EOF instead")
158// parseString parses a single- or double-quoted string.
159func (p *parser) parseString() (result string, err error) {
162 return "", errors.New("expected string, found EOF instead")
173 switch c := p.s[i+1]; c {
175 if len(p.s) > i+2 && p.s[i+2] == '\n' {
186 val, err := p.parseEscape()
194 case '\r', '\n', '\f':
195 return "", errors.New("unexpected end of line in string")
199 if c := p.s[i]; c == quote || c == '\\' || c == '\r' || c == '\n' || c == '\f' {
204 result += p.s[start:i]
209 return "", errors.New("EOF in string")
212 // Consume the final quote.
219// parseRegex parses a regular expression; the end is defined by encountering an
220// unmatched closing ')' or ']' which is not consumed
221func (p *parser) parseRegex() (rx *regexp.Regexp, err error) {
224 return nil, errors.New("expected regular expression, found EOF instead")
227 // number of open parens or brackets;
228 // when it becomes negative, finished parsing regex
246 return nil, errors.New("EOF in regular expression")
248 rx, err = regexp.Compile(p.s[p.i:i])
253// skipWhitespace consumes whitespace characters and comments.
254// It returns true if there was actually anything to skip.
255func (p *parser) skipWhitespace() bool {
259 case ' ', '\t', '\r', '\n', '\f':
263 if strings.HasPrefix(p.s[i:], "/*") {
264 end := strings.Index(p.s[i+len("/*"):], "*/")
266 i += end + len("/**/")
282// consumeParenthesis consumes an opening parenthesis and any following
283// whitespace. It returns true if there was actually a parenthesis to skip.
284func (p *parser) consumeParenthesis() bool {
285 if p.i < len(p.s) && p.s[p.i] == '(' {
293// consumeClosingParenthesis consumes a closing parenthesis and any preceding
294// whitespace. It returns true if there was actually a parenthesis to skip.
295func (p *parser) consumeClosingParenthesis() bool {
298 if p.i < len(p.s) && p.s[p.i] == ')' {
306// parseTypeSelector parses a type selector (one that matches by tag name).
307func (p *parser) parseTypeSelector() (result tagSelector, err error) {
308 tag, err := p.parseIdentifier()
312 return tagSelector{tag: toLowerASCII(tag)}, nil
315// parseIDSelector parses a selector that matches by id attribute.
316func (p *parser) parseIDSelector() (idSelector, error) {
318 return idSelector{}, fmt.Errorf("expected id selector (#id), found EOF instead")
321 return idSelector{}, fmt.Errorf("expected id selector (#id), found '%c' instead", p.s[p.i])
325 id, err := p.parseName()
327 return idSelector{}, err
330 return idSelector{id: id}, nil
333// parseClassSelector parses a selector that matches by class attribute.
334func (p *parser) parseClassSelector() (classSelector, error) {
336 return classSelector{}, fmt.Errorf("expected class selector (.class), found EOF instead")
339 return classSelector{}, fmt.Errorf("expected class selector (.class), found '%c' instead", p.s[p.i])
343 class, err := p.parseIdentifier()
345 return classSelector{}, err
348 return classSelector{class: class}, nil
351// parseAttributeSelector parses a selector that matches by attribute value.
352func (p *parser) parseAttributeSelector() (attrSelector, error) {
354 return attrSelector{}, fmt.Errorf("expected attribute selector ([attribute]), found EOF instead")
357 return attrSelector{}, fmt.Errorf("expected attribute selector ([attribute]), found '%c' instead", p.s[p.i])
362 key, err := p.parseIdentifier()
364 return attrSelector{}, err
366 key = toLowerASCII(key)
370 return attrSelector{}, errors.New("unexpected EOF in attribute selector")
375 return attrSelector{key: key, operation: ""}, nil
378 if p.i+2 >= len(p.s) {
379 return attrSelector{}, errors.New("unexpected EOF in attribute selector")
382 op := p.s[p.i : p.i+2]
385 } else if op[1] != '=' {
386 return attrSelector{}, fmt.Errorf(`expected equality operator, found "%s" instead`, op)
392 return attrSelector{}, errors.New("unexpected EOF in attribute selector")
395 var rx *regexp.Regexp
397 rx, err = p.parseRegex()
401 val, err = p.parseString()
403 val, err = p.parseIdentifier()
407 return attrSelector{}, err
412 return attrSelector{}, errors.New("unexpected EOF in attribute selector")
415 // check if the attribute contains an ignore case flag
417 if p.s[p.i] == 'i' || p.s[p.i] == 'I' {
424 return attrSelector{}, errors.New("unexpected EOF in attribute selector")
428 return attrSelector{}, fmt.Errorf("expected ']', found '%c' instead", p.s[p.i])
433 case "=", "!=", "~=", "|=", "^=", "$=", "*=", "#=":
434 return attrSelector{key: key, val: val, operation: op, regexp: rx, insensitive: ignoreCase}, nil
436 return attrSelector{}, fmt.Errorf("attribute operator %q is not supported", op)
441 errExpectedParenthesis = errors.New("expected '(' but didn't find it")
442 errExpectedClosingParenthesis = errors.New("expected ')' but didn't find it")
443 errUnmatchedParenthesis = errors.New("unmatched '('")
446// parsePseudoclassSelector parses a pseudoclass selector like :not(p) or a pseudo-element
447// For backwards compatibility, both ':' and '::' prefix are allowed for pseudo-elements.
448// https://drafts.csswg.org/selectors-3/#pseudo-elements
449// Returning a nil `Sel` (and a nil `error`) means we found a pseudo-element.
450func (p *parser) parsePseudoclassSelector() (out Sel, pseudoElement string, err error) {
452 return nil, "", fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead")
455 return nil, "", fmt.Errorf("expected attribute selector (:pseudoclass), found '%c' instead", p.s[p.i])
459 var mustBePseudoElement bool
461 return nil, "", fmt.Errorf("got empty pseudoclass (or pseudoelement)")
463 if p.s[p.i] == ':' { // we found a pseudo-element
464 mustBePseudoElement = true
468 name, err := p.parseIdentifier()
472 name = toLowerASCII(name)
473 if mustBePseudoElement && (name != "after" && name != "backdrop" && name != "before" &&
474 name != "cue" && name != "first-letter" && name != "first-line" && name != "grammar-error" &&
475 name != "marker" && name != "placeholder" && name != "selection" && name != "spelling-error") {
476 return out, "", fmt.Errorf("unknown pseudoelement :%s", name)
480 case "not", "has", "haschild":
481 if !p.consumeParenthesis() {
482 return out, "", errExpectedParenthesis
484 sel, parseErr := p.parseSelectorGroup()
486 return out, "", parseErr
488 if !p.consumeClosingParenthesis() {
489 return out, "", errExpectedClosingParenthesis
492 out = relativePseudoClassSelector{name: name, match: sel}
494 case "contains", "containsown":
495 if !p.consumeParenthesis() {
496 return out, "", errExpectedParenthesis
499 return out, "", errUnmatchedParenthesis
504 val, err = p.parseString()
506 val, err = p.parseIdentifier()
511 val = strings.ToLower(val)
514 return out, "", errors.New("unexpected EOF in pseudo selector")
516 if !p.consumeClosingParenthesis() {
517 return out, "", errExpectedClosingParenthesis
520 out = containsPseudoClassSelector{own: name == "containsown", value: val}
522 case "matches", "matchesown":
523 if !p.consumeParenthesis() {
524 return out, "", errExpectedParenthesis
526 rx, err := p.parseRegex()
531 return out, "", errors.New("unexpected EOF in pseudo selector")
533 if !p.consumeClosingParenthesis() {
534 return out, "", errExpectedClosingParenthesis
537 out = regexpPseudoClassSelector{own: name == "matchesown", regexp: rx}
539 case "nth-child", "nth-last-child", "nth-of-type", "nth-last-of-type":
540 if !p.consumeParenthesis() {
541 return out, "", errExpectedParenthesis
543 a, b, err := p.parseNth()
547 if !p.consumeClosingParenthesis() {
548 return out, "", errExpectedClosingParenthesis
550 last := name == "nth-last-child" || name == "nth-last-of-type"
551 ofType := name == "nth-of-type" || name == "nth-last-of-type"
552 out = nthPseudoClassSelector{a: a, b: b, last: last, ofType: ofType}
555 out = nthPseudoClassSelector{a: 0, b: 1, ofType: false, last: false}
557 out = nthPseudoClassSelector{a: 0, b: 1, ofType: false, last: true}
558 case "first-of-type":
559 out = nthPseudoClassSelector{a: 0, b: 1, ofType: true, last: false}
561 out = nthPseudoClassSelector{a: 0, b: 1, ofType: true, last: true}
563 out = onlyChildPseudoClassSelector{ofType: false}
565 out = onlyChildPseudoClassSelector{ofType: true}
567 out = inputPseudoClassSelector{}
569 out = emptyElementPseudoClassSelector{}
571 out = rootPseudoClassSelector{}
573 out = linkPseudoClassSelector{}
575 if !p.consumeParenthesis() {
576 return out, "", errExpectedParenthesis
579 return out, "", errUnmatchedParenthesis
581 val, err := p.parseIdentifier()
585 val = strings.ToLower(val)
588 return out, "", errors.New("unexpected EOF in pseudo selector")
590 if !p.consumeClosingParenthesis() {
591 return out, "", errExpectedClosingParenthesis
593 out = langPseudoClassSelector{lang: val}
595 out = enabledPseudoClassSelector{}
597 out = disabledPseudoClassSelector{}
599 out = checkedPseudoClassSelector{}
600 case "visited", "hover", "active", "focus", "target":
601 // Not applicable in a static context: never match.
602 out = neverMatchSelector{value: ":" + name}
603 case "after", "backdrop", "before", "cue", "first-letter", "first-line", "grammar-error", "marker", "placeholder", "selection", "spelling-error":
604 return nil, name, nil
606 return out, "", fmt.Errorf("unknown pseudoclass or pseudoelement :%s", name)
611// parseInteger parses a decimal integer.
612func (p *parser) parseInteger() (int, error) {
615 for i < len(p.s) && '0' <= p.s[i] && p.s[i] <= '9' {
619 return 0, errors.New("expected integer, but didn't find it")
623 val, err := strconv.Atoi(p.s[start:i])
631// parseNth parses the argument for :nth-child (normally of the form an+b).
632func (p *parser) parseNth() (a, b int, err error) {
644 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
650 case 'o', 'O', 'e', 'E':
651 id, nameErr := p.parseName()
655 id = toLowerASCII(id)
662 return 0, 0, fmt.Errorf("expected 'odd' or 'even', but found '%s' instead", id)
672 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
673 a, err = p.parseInteger()
691 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
692 a, err = p.parseInteger()
715 // The number we read as a is actually b.
728 b, err = p.parseInteger()
736 b, err = p.parseInteger()
746 return 0, 0, errors.New("unexpected EOF while attempting to parse expression of form an+b")
749 return 0, 0, errors.New("unexpected character while attempting to parse expression of form an+b")
752// parseSimpleSelectorSequence parses a selector sequence that applies to
754func (p *parser) parseSimpleSelectorSequence() (Sel, error) {
758 return nil, errors.New("expected selector, found EOF instead")
763 // It's the universal selector. Just skip over it, since it doesn't affect the meaning.
765 if p.i+2 < len(p.s) && p.s[p.i:p.i+2] == "|*" { // other version of universal selector
768 case '#', '.', '[', ':':
769 // There's no type selector. Wait to process the other till the main loop.
771 r, err := p.parseTypeSelector()
775 selectors = append(selectors, r)
778 var pseudoElement string
783 newPseudoElement string
788 ns, err = p.parseIDSelector()
790 ns, err = p.parseClassSelector()
792 ns, err = p.parseAttributeSelector()
794 ns, newPseudoElement, err = p.parsePseudoclassSelector()
801 // From https://drafts.csswg.org/selectors-3/#pseudo-elements :
802 // "Only one pseudo-element may appear per selector, and if present
803 // it must appear after the sequence of simple selectors that
804 // represents the subjects of the selector.""
805 if ns == nil { // we found a pseudo-element
806 if pseudoElement != "" {
807 return nil, fmt.Errorf("only one pseudo-element is accepted per selector, got %s and %s", pseudoElement, newPseudoElement)
809 if !p.acceptPseudoElements {
810 return nil, fmt.Errorf("pseudo-element %s found, but pseudo-elements support is disabled", newPseudoElement)
812 pseudoElement = newPseudoElement
814 if pseudoElement != "" {
815 return nil, fmt.Errorf("pseudo-element %s must be at the end of selector", pseudoElement)
817 selectors = append(selectors, ns)
821 if len(selectors) == 1 && pseudoElement == "" { // no need wrap the selectors in compoundSelector
822 return selectors[0], nil
824 return compoundSelector{selectors: selectors, pseudoElement: pseudoElement}, nil
827// parseSelector parses a selector that may include combinators.
828func (p *parser) parseSelector() (Sel, error) {
830 result, err := p.parseSimpleSelectorSequence()
840 if p.skipWhitespace() {
849 combinator = p.s[p.i]
853 // These characters can't begin a selector, but they can legally occur after one.
861 c, err = p.parseSimpleSelectorSequence()
865 result = combinedSelector{first: result, combinator: combinator, second: c}
869// parseSelectorGroup parses a group of selectors, separated by commas.
870func (p *parser) parseSelectorGroup() (SelectorGroup, error) {
871 current, err := p.parseSelector()
875 result := SelectorGroup{current}
882 c, err := p.parseSelector()
886 result = append(result, c)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list