├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── css.go
├── css_test.go
├── fuzz_test.go
├── go.mod
├── go.sum
├── lex.go
├── lex_test.go
├── parse.go
├── parse_test.go
├── queue.go
├── queue_test.go
└── testdata
└── fuzz
├── FuzzParse
├── 771e938e4458e983a736261a702e27c7a414fd660a15b63034f290b146d2f217
├── 78c19c977ebc4bbd1a8d570c4661a0bd5803195e4c805f7e32109d930b1ac85b
└── ee189d6aaeb573cafd396e75348f04c44b83416d61cfca61c8446c5a41317cf8
└── FuzzSelector
└── 00e15d22123489fd
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | test:
12 | strategy:
13 | matrix:
14 | os: [ubuntu-latest]
15 | go-version: [1.22.x, 1.23.x]
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - name: Install Go
19 | uses: actions/setup-go@v2
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 | - name: Checkout code
23 | uses: actions/checkout@v2
24 | - name: Build
25 | run: go build ./...
26 | - name: Test
27 | run: go test -v ./...
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Eric Chiang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: cover
2 | cover:
3 | go test -coverprofile=bin/coverage.out
4 | go tool cover -html=bin/coverage.out
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSS selectors in Go
2 |
3 | [](https://pkg.go.dev/github.com/ericchiang/css)
4 |
5 | This package implements a CSS selector compiler for Go's HTML parsing package golang.org/x/net/html.
6 |
7 | ```go
8 | package main
9 |
10 | import (
11 | "fmt"
12 | "os"
13 | "strings"
14 |
15 | "github.com/ericchiang/css"
16 | "golang.org/x/net/html"
17 | )
18 |
19 | var data = `
20 |
44 | ```
45 |
--------------------------------------------------------------------------------
/css.go:
--------------------------------------------------------------------------------
1 | // Package css implements CSS selector HTML search.
2 | //
3 | // Selectors compiled by this package search through golang.org/x/net/html nodes and should
4 | // be used in conjunction with that package.
5 | //
6 | // data := `
7 | //
a header
8 | //
another header
9 | // `
10 | //
11 | // sel, err := css.Parse("h2#foo")
12 | // if err != nil {
13 | // // handle error
14 | // }
15 | // node, err := html.Parse(strings.NewReader(data))
16 | // if err != nil {
17 | // // handle error
18 | // }
19 | // elements := sel.Select(node)
20 | //
21 | // This package aims to support Selectors Level 4 https://www.w3.org/TR/selectors-4/.
22 | //
23 | // The universal selector (*) is supported, along with:
24 | //
25 | // a // Type selector
26 | // ns|a // Type selector with namespace
27 | // .red // Class selector
28 | // #demo // ID selector
29 | // [attr] // Attribute selector
30 | // [attr=value] // Attribute selector value
31 | // [attr~=value] // Attribute selector element of list
32 | // [attr|=value] // Attribute selector value or "{value}-" prefix
33 | // [attr^=value] // Attribute selector prefix
34 | // [attr$=value] // Attribute selector suffix
35 | // [attr*=value] // Attribute selector contains value
36 | // [attr=value i] // Attribute selector case insensitive modifier
37 | // foo, bar // Selector list
38 | // foo bar // Descendant combinator
39 | // foo > bar // Child combinator
40 | // foo ~ bar // General sibling combinator
41 | // foo + bar // Adjacent sibling combinator
42 | // :empty // Element with no children
43 | // :first-child // First child of parent
44 | // :first-of-type // First child of its type of parent
45 | // :last-child // Last child of parent
46 | // :last-of-type // Last child of its type of parent
47 | // :only-child // Only child of parent
48 | // :only-of-type // Only child of its type parent
49 | // :root // Root element
50 | // :nth-child(An+B) // Positional child matcher
51 | // :nth-last-child(An+B) // Reverse positional child matcher
52 | // :nth-last-of-type(An+B) // Reverse positional child matcher of type
53 | // :nth-of-type(An+B) // Positional child matcher of type
54 | package css
55 |
56 | import (
57 | "errors"
58 | "fmt"
59 | "strings"
60 |
61 | "golang.org/x/net/html"
62 | "golang.org/x/net/html/atom"
63 | )
64 |
65 | // ParseError is returned indicating an lex, parse, or compilation error with
66 | // the associated position in the string the error occurred.
67 | type ParseError struct {
68 | Pos int
69 | Msg string
70 | }
71 |
72 | // Error returns a formatted version of the error.
73 | func (p *ParseError) Error() string {
74 | return fmt.Sprintf("css: %s at position %d", p.Msg, p.Pos)
75 | }
76 |
77 | func errorf(pos int, msg string, v ...interface{}) error {
78 | return &ParseError{pos, fmt.Sprintf(msg, v...)}
79 | }
80 |
81 | // Selector is a compiled CSS selector.
82 | type Selector struct {
83 | s []*selector
84 | }
85 |
86 | // Select returns any matches from a parsed HTML document.
87 | func (s *Selector) Select(n *html.Node) []*html.Node {
88 | selected := []*html.Node{}
89 | for _, sel := range s.s {
90 | selected = append(selected, sel.find(n)...)
91 | }
92 | return selected
93 | }
94 |
95 | func findAll(n *html.Node, fn func(n *html.Node) bool) []*html.Node {
96 | var m []*html.Node
97 | if fn(n) {
98 | m = append(m, n)
99 | }
100 | for c := n.FirstChild; c != nil; c = c.NextSibling {
101 | if c.Type != html.ElementNode {
102 | continue
103 | }
104 | m = append(m, findAll(c, fn)...)
105 | }
106 | return m
107 | }
108 |
109 | // MustParse is like Parse but panics on errors.
110 | func MustParse(s string) *Selector {
111 | sel, err := Parse(s)
112 | if err != nil {
113 | panic(err)
114 | }
115 | return sel
116 | }
117 |
118 | // Parse compiles a complex selector list from a string. The parser supports
119 | // Selectors Level 4.
120 | //
121 | // Multiple selectors are supported through comma separated values. For example
122 | // "h1, h2".
123 | //
124 | // Parse reports the first error hit when compiling.
125 | func Parse(s string) (*Selector, error) {
126 | p := newParser(s)
127 | list, err := p.parse()
128 | if err != nil {
129 | var perr *parseErr
130 | if errors.As(err, &perr) {
131 | return nil, &ParseError{perr.t.pos, perr.msg}
132 | }
133 | var lerr *lexErr
134 | if errors.As(err, &lerr) {
135 | return nil, &ParseError{lerr.last, lerr.msg}
136 | }
137 | return nil, err
138 | }
139 | sel := &Selector{}
140 |
141 | c := compiler{maxErrs: 1}
142 | for _, s := range list {
143 | m := c.compile(&s)
144 | if m == nil {
145 | continue
146 | }
147 | sel.s = append(sel.s, m)
148 | }
149 | if err := c.err(); err != nil {
150 | return nil, err
151 | }
152 | return sel, nil
153 | }
154 |
155 | type compiler struct {
156 | sels []complexSelector
157 | maxErrs int
158 | errs []error
159 | }
160 |
161 | func (c *compiler) err() error {
162 | if len(c.errs) == 0 {
163 | return nil
164 | }
165 | return c.errs[0]
166 | }
167 |
168 | func (c *compiler) errorf(pos int, msg string, v ...interface{}) bool {
169 | err := &ParseError{pos, fmt.Sprintf(msg, v...)}
170 | c.errs = append(c.errs, err)
171 | if len(c.errs) >= c.maxErrs {
172 | return true
173 | }
174 | return false
175 | }
176 |
177 | type combinator interface {
178 | find(n *html.Node) []*html.Node
179 | }
180 |
181 | type selector struct {
182 | m *compoundSelectorMatcher
183 |
184 | combinators []combinator
185 | }
186 |
187 | func (s selector) find(n *html.Node) []*html.Node {
188 | nodes := findAll(n, s.m.match)
189 | for _, c := range s.combinators {
190 | var ns []*html.Node
191 | for _, n := range nodes {
192 | ns = append(ns, c.find(n)...)
193 | }
194 | nodes = ns
195 | }
196 | return nodes
197 | }
198 |
199 | type descendantCombinator struct {
200 | m *compoundSelectorMatcher
201 | }
202 |
203 | func (c *descendantCombinator) find(n *html.Node) []*html.Node {
204 | var nodes []*html.Node
205 | for n := n.FirstChild; n != nil; n = n.NextSibling {
206 | if n.Type != html.ElementNode {
207 | continue
208 | }
209 | nodes = append(nodes, findAll(n, c.m.match)...)
210 | }
211 | return nodes
212 | }
213 |
214 | type childCombinator struct {
215 | m *compoundSelectorMatcher
216 | }
217 |
218 | func (c *childCombinator) find(n *html.Node) []*html.Node {
219 | var nodes []*html.Node
220 | for n := n.FirstChild; n != nil; n = n.NextSibling {
221 | if n.Type != html.ElementNode {
222 | continue
223 | }
224 | if c.m.match(n) {
225 | nodes = append(nodes, n)
226 | }
227 | }
228 | return nodes
229 | }
230 |
231 | type adjacentCombinator struct {
232 | m *compoundSelectorMatcher
233 | }
234 |
235 | func (c *adjacentCombinator) find(n *html.Node) []*html.Node {
236 | var (
237 | nodes []*html.Node
238 | prev *html.Node
239 | next *html.Node
240 | )
241 | for prev = n.PrevSibling; prev != nil; prev = prev.PrevSibling {
242 | if prev.Type == html.ElementNode {
243 | break
244 | }
245 | }
246 | for next = n.NextSibling; next != nil; next = next.NextSibling {
247 | if next.Type == html.ElementNode {
248 | break
249 | }
250 | }
251 | if prev != nil && c.m.match(prev) {
252 | nodes = append(nodes, prev)
253 | }
254 | if next != nil && c.m.match(next) {
255 | nodes = append(nodes, next)
256 | }
257 | return nodes
258 | }
259 |
260 | type siblingCombinator struct {
261 | m *compoundSelectorMatcher
262 | }
263 |
264 | func (c *siblingCombinator) find(n *html.Node) []*html.Node {
265 | var nodes []*html.Node
266 | for n := n.PrevSibling; n != nil; n = n.PrevSibling {
267 | if n.Type != html.ElementNode {
268 | continue
269 | }
270 | if c.m.match(n) {
271 | nodes = append(nodes, n)
272 | }
273 | }
274 | for n := n.NextSibling; n != nil; n = n.NextSibling {
275 | if n.Type != html.ElementNode {
276 | continue
277 | }
278 | if c.m.match(n) {
279 | nodes = append(nodes, n)
280 | }
281 | }
282 | return nodes
283 | }
284 |
285 | func (c *compiler) compile(s *complexSelector) *selector {
286 | m := &selector{
287 | m: c.compoundSelector(&s.sel),
288 | }
289 | curr := s
290 | for {
291 | if curr.next == nil {
292 | return m
293 | }
294 | sel := c.compoundSelector(&curr.next.sel)
295 | comb := curr.combinator
296 |
297 | curr = curr.next
298 |
299 | var cm combinator
300 | switch comb {
301 | case "":
302 | cm = &descendantCombinator{sel}
303 | case ">":
304 | cm = &childCombinator{sel}
305 | case "+":
306 | cm = &adjacentCombinator{sel}
307 | case "~":
308 | cm = &siblingCombinator{sel}
309 | default:
310 | c.errorf(curr.pos, "unexpected combinator: %s", comb)
311 | continue
312 | }
313 | m.combinators = append(m.combinators, cm)
314 | }
315 | return m
316 | }
317 |
318 | type compoundSelectorMatcher struct {
319 | m *typeSelectorMatcher
320 | scm []subclassSelectorMatcher
321 | }
322 |
323 | func (c *compoundSelectorMatcher) match(n *html.Node) bool {
324 | if c.m != nil {
325 | if !c.m.match(n) {
326 | return false
327 | }
328 | }
329 | for _, m := range c.scm {
330 | if !m.match(n) {
331 | return false
332 | }
333 | }
334 | return true
335 | }
336 |
337 | func (c *compiler) compoundSelector(s *compoundSelector) *compoundSelectorMatcher {
338 | m := &compoundSelectorMatcher{}
339 | if s.typeSelector != nil {
340 | m.m = c.typeSelector(s.typeSelector)
341 | }
342 | for _, sc := range s.subClasses {
343 | scm := c.subclassSelector(&sc)
344 | if scm != nil {
345 | m.scm = append(m.scm, *scm)
346 | }
347 | }
348 | if len(s.pseudoSelectors) != 0 {
349 | // It's not clear that it makes sense for us to support pseudo elements,
350 | // since this is more about modifying added elements than selecting elements.
351 | //
352 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
353 | if c.errorf(s.pos, "pseudo element selectors not supported") {
354 | return nil
355 | }
356 | }
357 | return m
358 | }
359 |
360 | type subclassSelectorMatcher struct {
361 | idSelector string
362 | classSelector string
363 | attributeSelector *attributeSelectorMatcher
364 | pseudoSelector func(*html.Node) bool
365 | }
366 |
367 | func (s *subclassSelectorMatcher) match(n *html.Node) bool {
368 | if s.idSelector != "" {
369 | for _, a := range n.Attr {
370 | if a.Key == "id" && a.Val == s.idSelector {
371 | return true
372 | }
373 | }
374 | return false
375 | }
376 |
377 | if s.classSelector != "" {
378 | for _, a := range n.Attr {
379 | if a.Key == "class" {
380 | for _, val := range strings.Fields(a.Val) {
381 | if val == s.classSelector {
382 | return true
383 | }
384 | }
385 | }
386 | }
387 | return false
388 | }
389 |
390 | if s.attributeSelector != nil {
391 | return s.attributeSelector.match(n)
392 | }
393 |
394 | if s.pseudoSelector != nil {
395 | return s.pseudoSelector(n)
396 | }
397 | return false
398 | }
399 |
400 | func (c *compiler) subclassSelector(s *subclassSelector) *subclassSelectorMatcher {
401 | m := &subclassSelectorMatcher{
402 | idSelector: s.idSelector,
403 | classSelector: s.classSelector,
404 | }
405 | if s.attributeSelector != nil {
406 | m.attributeSelector = c.attributeSelector(s.attributeSelector)
407 | }
408 | if s.pseudoClassSelector != nil {
409 | m.pseudoSelector = c.pseudoClassSelector(s.pseudoClassSelector)
410 | }
411 | return m
412 | }
413 |
414 | type pseudoClassSelectorMatcher struct {
415 | matcher func(*html.Node) bool
416 | }
417 |
418 | func (c *compiler) pseudoClassSelector(s *pseudoClassSelector) func(*html.Node) bool {
419 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
420 | switch s.ident {
421 | case "empty":
422 | return emptyMatcher
423 | case "first-child":
424 | return firstChildMatcher
425 | case "first-of-type":
426 | return firstOfTypeMatcher
427 | case "last-child":
428 | return lastChildMatcher
429 | case "last-of-type":
430 | return lastOfTypeMatcher
431 | case "only-child":
432 | return onlyChildMatcher
433 | case "only-of-type":
434 | return onlyOfTypeMatcher
435 | case "root":
436 | return rootMatcher
437 | case "":
438 | default:
439 | c.errorf(s.pos, "unsupported pseudo-class selector: %s", s.ident)
440 | return nil
441 | }
442 |
443 | switch s.function {
444 | case "nth-child(":
445 | return c.nthChild(s)
446 | case "nth-last-child(":
447 | return c.nthLastChild(s)
448 | case "nth-last-of-type(":
449 | return c.nthLastOfType(s)
450 | case "nth-of-type(":
451 | return c.nthOfType(s)
452 | default:
453 | c.errorf(s.pos, "unsupported pseudo-class selector: %s", s.function)
454 | return nil
455 | }
456 |
457 | return nil
458 | }
459 |
460 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child
461 | func (c *compiler) nthChild(s *pseudoClassSelector) func(n *html.Node) bool {
462 | nth := c.compileNth(s)
463 | if nth == nil {
464 | return nil
465 | }
466 | return func(n *html.Node) bool {
467 | var i int64 = 1
468 | for s := n.PrevSibling; s != nil; s = s.PrevSibling {
469 | if s.Type == html.ElementNode {
470 | i++
471 | }
472 | }
473 | return nth.matches(i)
474 | }
475 | }
476 |
477 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type
478 | func (c *compiler) nthOfType(s *pseudoClassSelector) func(n *html.Node) bool {
479 | nth := c.compileNth(s)
480 | if nth == nil {
481 | return nil
482 | }
483 | return func(n *html.Node) bool {
484 | var i int64 = 1
485 | for s := n.PrevSibling; s != nil; s = s.PrevSibling {
486 | if s.Type == html.ElementNode && s.DataAtom == n.DataAtom {
487 | i++
488 | }
489 | }
490 | return nth.matches(i)
491 | }
492 | }
493 |
494 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child
495 | func (c *compiler) nthLastChild(s *pseudoClassSelector) func(n *html.Node) bool {
496 | nth := c.compileNth(s)
497 | if nth == nil {
498 | return nil
499 | }
500 | return func(n *html.Node) bool {
501 | var i int64 = 1
502 | for s := n.NextSibling; s != nil; s = s.NextSibling {
503 | if s.Type == html.ElementNode {
504 | i++
505 | }
506 | }
507 | return nth.matches(i)
508 | }
509 | }
510 |
511 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type
512 | func (c *compiler) nthLastOfType(s *pseudoClassSelector) func(n *html.Node) bool {
513 | nth := c.compileNth(s)
514 | if nth == nil {
515 | return nil
516 | }
517 | return func(n *html.Node) bool {
518 | var i int64 = 1
519 | for s := n.NextSibling; s != nil; s = s.NextSibling {
520 | if s.Type == html.ElementNode && n.DataAtom == s.DataAtom {
521 | i++
522 | }
523 | }
524 | return nth.matches(i)
525 | }
526 | }
527 |
528 | // nth holds a computed An+B value for :nth-child() and its associated selectors.
529 | type nth struct {
530 | a int64
531 | b int64
532 | }
533 |
534 | func (nth nth) matches(val int64) bool {
535 | // Is there a value for "n" given "An+B=val" where "n" is non-negative?
536 |
537 | // An + B = val
538 | // An = val - B
539 | // n = (val - B) / A
540 | if nth.a == 0 {
541 | return val == nth.b
542 | }
543 | return (val-nth.b)%nth.a == 0 && (val-nth.b)/nth.a >= 0
544 | }
545 |
546 | func (c *compiler) compileNth(s *pseudoClassSelector) *nth {
547 | p := newParserFromTokens(s.args)
548 | a, err := p.aNPlusB()
549 | if err != nil {
550 | c.errorf(s.pos, "failed to parse expression: %v", err)
551 | return nil
552 | }
553 | if err := p.expectWhitespaceOrEOF(); err != nil {
554 | c.errorf(s.pos, "failed to parse expression: %v", err)
555 | return nil
556 | }
557 | return a
558 | }
559 |
560 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:empty
561 | func emptyMatcher(n *html.Node) bool {
562 | for c := n.FirstChild; c != nil; c = c.NextSibling {
563 | if c.Type == html.ElementNode {
564 | return false
565 | }
566 | }
567 | return true
568 | }
569 |
570 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child
571 | func firstChildMatcher(n *html.Node) bool {
572 | for s := n.PrevSibling; s != nil; s = s.PrevSibling {
573 | if s.Type == html.ElementNode {
574 | return false
575 | }
576 | }
577 | return true
578 | }
579 |
580 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type
581 | func firstOfTypeMatcher(n *html.Node) bool {
582 | for s := n.PrevSibling; s != nil; s = s.PrevSibling {
583 | if s.Type != html.ElementNode {
584 | continue
585 | }
586 | if s.DataAtom == n.DataAtom {
587 | return false
588 | }
589 | }
590 | return true
591 | }
592 |
593 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child
594 | func lastChildMatcher(n *html.Node) bool {
595 | for s := n.NextSibling; s != nil; s = s.NextSibling {
596 | if s.Type == html.ElementNode {
597 | return false
598 | }
599 | }
600 | return true
601 | }
602 |
603 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type
604 | func lastOfTypeMatcher(n *html.Node) bool {
605 | for s := n.NextSibling; s != nil; s = s.NextSibling {
606 | if s.Type != html.ElementNode {
607 | continue
608 | }
609 | if s.DataAtom == n.DataAtom {
610 | return false
611 | }
612 | }
613 | return true
614 | }
615 |
616 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child
617 | func onlyChildMatcher(n *html.Node) bool {
618 | return firstChildMatcher(n) && lastChildMatcher(n)
619 | }
620 |
621 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type
622 | func onlyOfTypeMatcher(n *html.Node) bool {
623 | return firstOfTypeMatcher(n) && lastOfTypeMatcher(n)
624 | }
625 |
626 | // https://developer.mozilla.org/en-US/docs/Web/CSS/:root
627 | func rootMatcher(n *html.Node) bool {
628 | return n.Parent == nil
629 | }
630 |
631 | type attributeSelectorMatcher struct {
632 | ns namespaceMatcher
633 | fn func(key, val string) bool
634 | }
635 |
636 | func (a *attributeSelectorMatcher) match(n *html.Node) bool {
637 | for _, attr := range n.Attr {
638 | if a.ns.match(attr.Namespace) && a.fn(attr.Key, attr.Val) {
639 | return true
640 | }
641 | }
642 | return false
643 | }
644 |
645 | func (c *compiler) attributeSelector(s *attributeSelector) *attributeSelectorMatcher {
646 | m := &attributeSelectorMatcher{
647 | ns: newNamespaceMatcher(s.wqName.hasPrefix, s.wqName.prefix),
648 | }
649 | key := s.wqName.value
650 | val := s.val
651 |
652 | if s.modifier {
653 | key = strings.ToLower(key)
654 | val = strings.ToLower(val)
655 | }
656 |
657 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
658 | switch s.matcher {
659 | case "=":
660 | m.fn = func(k, v string) bool { return k == key && v == val }
661 | case "~=":
662 | m.fn = func(k, v string) bool {
663 | if k != key {
664 | return false
665 | }
666 | for _, f := range strings.Fields(v) {
667 | if f == val {
668 | return true
669 | }
670 | }
671 | return false
672 | }
673 | case "|=":
674 | // "Represents elements with an attribute name of attr whose value can be
675 | // exactly value or can begin with value immediately followed by a hyphen,
676 | // - (U+002D). It is often used for language subcode matches."
677 | m.fn = func(k, v string) bool {
678 | return k == key && (v == val || strings.HasPrefix(v, val+"-"))
679 | }
680 | case "^=":
681 | m.fn = func(k, v string) bool {
682 | return k == key && strings.HasPrefix(v, val)
683 | }
684 | case "$=":
685 | m.fn = func(k, v string) bool {
686 | return k == key && strings.HasSuffix(v, val)
687 | }
688 | case "*=":
689 | m.fn = func(k, v string) bool {
690 | return k == key && strings.Contains(v, val)
691 | }
692 | case "":
693 | m.fn = func(k, v string) bool { return k == key }
694 | default:
695 | c.errorf(s.pos, "unsupported attribute matcher: %s", s.matcher)
696 | return nil
697 | }
698 | if s.modifier {
699 | fn := m.fn
700 | m.fn = func(k, v string) bool {
701 | k = strings.ToLower(k)
702 | v = strings.ToLower(v)
703 | return fn(k, v)
704 | }
705 | }
706 | return m
707 | }
708 |
709 | // namespaceMatcher performs matching for elements and attributes.
710 | type namespaceMatcher struct {
711 | noNamespace bool
712 | namespace string
713 | }
714 |
715 | func newNamespaceMatcher(hasPrefix bool, prefix string) namespaceMatcher {
716 | if !hasPrefix {
717 | return namespaceMatcher{}
718 | }
719 | if prefix == "" {
720 | return namespaceMatcher{noNamespace: true}
721 | }
722 | if prefix == "*" {
723 | return namespaceMatcher{}
724 | }
725 | return namespaceMatcher{namespace: prefix}
726 | }
727 |
728 | func (n *namespaceMatcher) match(ns string) bool {
729 | if n.noNamespace {
730 | return ns == ""
731 | }
732 | if n.namespace == "" {
733 | return true
734 | }
735 | return n.namespace == ns
736 | }
737 |
738 | type typeSelectorMatcher struct {
739 | allAtoms bool
740 | atom atom.Atom
741 | ns namespaceMatcher
742 | }
743 |
744 | func (t *typeSelectorMatcher) match(n *html.Node) (ok bool) {
745 | if !(t.allAtoms || t.atom == n.DataAtom) {
746 | return false
747 | }
748 | return t.ns.match(n.Namespace)
749 | }
750 |
751 | func (c *compiler) typeSelector(s *typeSelector) *typeSelectorMatcher {
752 | m := &typeSelectorMatcher{}
753 | if s.value == "*" {
754 | m.allAtoms = true
755 | } else {
756 | a := atom.Lookup([]byte(s.value))
757 | if a == 0 {
758 | if c.errorf(s.pos, "unrecognized node name: %s", s.value) {
759 | return nil
760 | }
761 | }
762 | m.atom = a
763 | }
764 | m.ns = newNamespaceMatcher(s.hasPrefix, s.prefix)
765 | return m
766 | }
767 |
--------------------------------------------------------------------------------
/css_test.go:
--------------------------------------------------------------------------------
1 | package css
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "reflect"
8 | "strings"
9 | "testing"
10 |
11 | "github.com/google/go-cmp/cmp"
12 | "golang.org/x/net/html"
13 | )
14 |
15 | func (s *Selector) String() string {
16 | var b strings.Builder
17 | formatValue(reflect.ValueOf(s), &b, "")
18 | return b.String()
19 | }
20 |
21 | func formatValue(v reflect.Value, b *strings.Builder, ident string) {
22 | switch v.Kind() {
23 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
24 | fmt.Fprintf(b, "%d", v.Int())
25 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
26 | fmt.Fprintf(b, "%d", v.Uint())
27 | case reflect.Float32, reflect.Float64:
28 | fmt.Fprintf(b, "%f", v.Float())
29 | case reflect.Bool:
30 | fmt.Fprintf(b, "%t", v.Bool())
31 | case reflect.Array, reflect.Slice:
32 | if v.IsNil() {
33 | b.WriteString("[]")
34 | return
35 | }
36 | fmt.Fprintf(b, "[\n")
37 | for i := 0; i < v.Len(); i++ {
38 | b.WriteString(ident)
39 | b.WriteString("\t")
40 | formatValue(v.Index(i), b, ident+"\t")
41 | fmt.Fprintf(b, ",\n")
42 | }
43 | b.WriteString(ident)
44 | b.WriteString("]")
45 | case reflect.Func:
46 | if v.IsNil() {
47 | b.WriteString("")
48 | return
49 | }
50 | fmt.Fprintf(b, "")
51 | case reflect.Interface:
52 | if v.IsNil() {
53 | b.WriteString("")
54 | return
55 | }
56 | formatValue(v.Elem(), b, ident)
57 | case reflect.Map:
58 | if v.IsNil() {
59 | b.WriteString("")
60 | return
61 | }
62 | iter := v.MapRange()
63 | fmt.Fprintf(b, "{\n")
64 | for iter.Next() {
65 | b.WriteString(ident)
66 | formatValue(iter.Key(), b, ident+"\n")
67 | fmt.Fprintf(b, ", ")
68 | formatValue(iter.Value(), b, ident)
69 | }
70 | fmt.Fprintf(b, "}")
71 | case reflect.Ptr:
72 | if v.IsNil() {
73 | b.WriteString("")
74 | return
75 | }
76 | fmt.Fprintf(b, "*")
77 | formatValue(reflect.Indirect(v), b, ident)
78 | case reflect.String:
79 | fmt.Fprintf(b, "%q", v.String())
80 | case reflect.Struct:
81 | t := v.Type()
82 | fmt.Fprintf(b, "%s{\n", t.Name())
83 | for i := 0; i < v.NumField(); i++ {
84 | b.WriteString(ident + "\t")
85 | b.WriteString(t.Field(i).Name)
86 | b.WriteString(": ")
87 | formatValue(v.Field(i), b, ident+"\t")
88 | b.WriteString(",\n")
89 | }
90 | b.WriteString(ident)
91 | b.WriteString("}")
92 | }
93 | }
94 |
95 | type selectorTest struct {
96 | sel string
97 | in string
98 | want []string
99 | }
100 |
101 | var selectorTests = []selectorTest{
102 | {
103 | "a",
104 | `