1package cascadia
2
3import (
4 "bytes"
5 "fmt"
6 "regexp"
7 "strings"
8
9 "golang.org/x/net/html"
10 "golang.org/x/net/html/atom"
11)
12
13// This file implements the pseudo classes selectors,
14// which share the implementation of PseudoElement() and Specificity()
15
16type abstractPseudoClass struct{}
17
18func (s abstractPseudoClass) Specificity() Specificity {
19 return Specificity{0, 1, 0}
20}
21
22func (c abstractPseudoClass) PseudoElement() string {
23 return ""
24}
25
26type relativePseudoClassSelector struct {
27 name string // one of "not", "has", "haschild"
28 match SelectorGroup
29}
30
31func (s relativePseudoClassSelector) Match(n *html.Node) bool {
32 if n.Type != html.ElementNode {
33 return false
34 }
35 switch s.name {
36 case "not":
37 // matches elements that do not match a.
38 return !s.match.Match(n)
39 case "has":
40 // matches elements with any descendant that matches a.
41 return hasDescendantMatch(n, s.match)
42 case "haschild":
43 // matches elements with a child that matches a.
44 return hasChildMatch(n, s.match)
45 default:
46 panic(fmt.Sprintf("unsupported relative pseudo class selector : %s", s.name))
47 }
48}
49
50// hasChildMatch returns whether n has any child that matches a.
51func hasChildMatch(n *html.Node, a Matcher) bool {
52 for c := n.FirstChild; c != nil; c = c.NextSibling {
53 if a.Match(c) {
54 return true
55 }
56 }
57 return false
58}
59
60// hasDescendantMatch performs a depth-first search of n's descendants,
61// testing whether any of them match a. It returns true as soon as a match is
62// found, or false if no match is found.
63func hasDescendantMatch(n *html.Node, a Matcher) bool {
64 for c := n.FirstChild; c != nil; c = c.NextSibling {
65 if a.Match(c) || (c.Type == html.ElementNode && hasDescendantMatch(c, a)) {
66 return true
67 }
68 }
69 return false
70}
71
72// Specificity returns the specificity of the most specific selectors
73// in the pseudo-class arguments.
74// See https://www.w3.org/TR/selectors/#specificity-rules
75func (s relativePseudoClassSelector) Specificity() Specificity {
76 var max Specificity
77 for _, sel := range s.match {
78 newSpe := sel.Specificity()
79 if max.Less(newSpe) {
80 max = newSpe
81 }
82 }
83 return max
84}
85
86func (c relativePseudoClassSelector) PseudoElement() string {
87 return ""
88}
89
90type containsPseudoClassSelector struct {
91 abstractPseudoClass
92 value string
93 own bool
94}
95
96func (s containsPseudoClassSelector) Match(n *html.Node) bool {
97 var text string
98 if s.own {
99 // matches nodes that directly contain the given text
100 text = strings.ToLower(nodeOwnText(n))
101 } else {
102 // matches nodes that contain the given text.
103 text = strings.ToLower(nodeText(n))
104 }
105 return strings.Contains(text, s.value)
106}
107
108type regexpPseudoClassSelector struct {
109 abstractPseudoClass
110 regexp *regexp.Regexp
111 own bool
112}
113
114func (s regexpPseudoClassSelector) Match(n *html.Node) bool {
115 var text string
116 if s.own {
117 // matches nodes whose text directly matches the specified regular expression
118 text = nodeOwnText(n)
119 } else {
120 // matches nodes whose text matches the specified regular expression
121 text = nodeText(n)
122 }
123 return s.regexp.MatchString(text)
124}
125
126// writeNodeText writes the text contained in n and its descendants to b.
127func writeNodeText(n *html.Node, b *bytes.Buffer) {
128 switch n.Type {
129 case html.TextNode:
130 b.WriteString(n.Data)
131 case html.ElementNode:
132 for c := n.FirstChild; c != nil; c = c.NextSibling {
133 writeNodeText(c, b)
134 }
135 }
136}
137
138// nodeText returns the text contained in n and its descendants.
139func nodeText(n *html.Node) string {
140 var b bytes.Buffer
141 writeNodeText(n, &b)
142 return b.String()
143}
144
145// nodeOwnText returns the contents of the text nodes that are direct
146// children of n.
147func nodeOwnText(n *html.Node) string {
148 var b bytes.Buffer
149 for c := n.FirstChild; c != nil; c = c.NextSibling {
150 if c.Type == html.TextNode {
151 b.WriteString(c.Data)
152 }
153 }
154 return b.String()
155}
156
157type nthPseudoClassSelector struct {
158 abstractPseudoClass
159 a, b int
160 last, ofType bool
161}
162
163func (s nthPseudoClassSelector) Match(n *html.Node) bool {
164 if s.a == 0 {
165 if s.last {
166 return simpleNthLastChildMatch(s.b, s.ofType, n)
167 } else {
168 return simpleNthChildMatch(s.b, s.ofType, n)
169 }
170 }
171 return nthChildMatch(s.a, s.b, s.last, s.ofType, n)
172}
173
174// nthChildMatch implements :nth-child(an+b).
175// If last is true, implements :nth-last-child instead.
176// If ofType is true, implements :nth-of-type instead.
177func nthChildMatch(a, b int, last, ofType bool, n *html.Node) bool {
178 if n.Type != html.ElementNode {
179 return false
180 }
181
182 parent := n.Parent
183 if parent == nil {
184 return false
185 }
186
187 i := -1
188 count := 0
189 for c := parent.FirstChild; c != nil; c = c.NextSibling {
190 if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
191 continue
192 }
193 count++
194 if c == n {
195 i = count
196 if !last {
197 break
198 }
199 }
200 }
201
202 if i == -1 {
203 // This shouldn't happen, since n should always be one of its parent's children.
204 return false
205 }
206
207 if last {
208 i = count - i + 1
209 }
210
211 i -= b
212 if a == 0 {
213 return i == 0
214 }
215
216 return i%a == 0 && i/a >= 0
217}
218
219// simpleNthChildMatch implements :nth-child(b).
220// If ofType is true, implements :nth-of-type instead.
221func simpleNthChildMatch(b int, ofType bool, n *html.Node) bool {
222 if n.Type != html.ElementNode {
223 return false
224 }
225
226 parent := n.Parent
227 if parent == nil {
228 return false
229 }
230
231 count := 0
232 for c := parent.FirstChild; c != nil; c = c.NextSibling {
233 if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
234 continue
235 }
236 count++
237 if c == n {
238 return count == b
239 }
240 if count >= b {
241 return false
242 }
243 }
244 return false
245}
246
247// simpleNthLastChildMatch implements :nth-last-child(b).
248// If ofType is true, implements :nth-last-of-type instead.
249func simpleNthLastChildMatch(b int, ofType bool, n *html.Node) bool {
250 if n.Type != html.ElementNode {
251 return false
252 }
253
254 parent := n.Parent
255 if parent == nil {
256 return false
257 }
258
259 count := 0
260 for c := parent.LastChild; c != nil; c = c.PrevSibling {
261 if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
262 continue
263 }
264 count++
265 if c == n {
266 return count == b
267 }
268 if count >= b {
269 return false
270 }
271 }
272 return false
273}
274
275type onlyChildPseudoClassSelector struct {
276 abstractPseudoClass
277 ofType bool
278}
279
280// Match implements :only-child.
281// If `ofType` is true, it implements :only-of-type instead.
282func (s onlyChildPseudoClassSelector) Match(n *html.Node) bool {
283 if n.Type != html.ElementNode {
284 return false
285 }
286
287 parent := n.Parent
288 if parent == nil {
289 return false
290 }
291
292 count := 0
293 for c := parent.FirstChild; c != nil; c = c.NextSibling {
294 if (c.Type != html.ElementNode) || (s.ofType && c.Data != n.Data) {
295 continue
296 }
297 count++
298 if count > 1 {
299 return false
300 }
301 }
302
303 return count == 1
304}
305
306type inputPseudoClassSelector struct {
307 abstractPseudoClass
308}
309
310// Matches input, select, textarea and button elements.
311func (s inputPseudoClassSelector) Match(n *html.Node) bool {
312 return n.Type == html.ElementNode && (n.Data == "input" || n.Data == "select" || n.Data == "textarea" || n.Data == "button")
313}
314
315type emptyElementPseudoClassSelector struct {
316 abstractPseudoClass
317}
318
319// Matches empty elements.
320func (s emptyElementPseudoClassSelector) Match(n *html.Node) bool {
321 if n.Type != html.ElementNode {
322 return false
323 }
324
325 for c := n.FirstChild; c != nil; c = c.NextSibling {
326 switch c.Type {
327 case html.ElementNode:
328 return false
329 case html.TextNode:
330 if strings.TrimSpace(nodeText(c)) == "" {
331 continue
332 } else {
333 return false
334 }
335 }
336 }
337
338 return true
339}
340
341type rootPseudoClassSelector struct {
342 abstractPseudoClass
343}
344
345// Match implements :root
346func (s rootPseudoClassSelector) Match(n *html.Node) bool {
347 if n.Type != html.ElementNode {
348 return false
349 }
350 if n.Parent == nil {
351 return false
352 }
353 return n.Parent.Type == html.DocumentNode
354}
355
356func hasAttr(n *html.Node, attr string) bool {
357 return matchAttribute(n, attr, func(string) bool { return true })
358}
359
360type linkPseudoClassSelector struct {
361 abstractPseudoClass
362}
363
364// Match implements :link
365func (s linkPseudoClassSelector) Match(n *html.Node) bool {
366 return (n.DataAtom == atom.A || n.DataAtom == atom.Area || n.DataAtom == atom.Link) && hasAttr(n, "href")
367}
368
369type langPseudoClassSelector struct {
370 abstractPseudoClass
371 lang string
372}
373
374func (s langPseudoClassSelector) Match(n *html.Node) bool {
375 own := matchAttribute(n, "lang", func(val string) bool {
376 return val == s.lang || strings.HasPrefix(val, s.lang+"-")
377 })
378 if n.Parent == nil {
379 return own
380 }
381 return own || s.Match(n.Parent)
382}
383
384type enabledPseudoClassSelector struct {
385 abstractPseudoClass
386}
387
388func (s enabledPseudoClassSelector) Match(n *html.Node) bool {
389 if n.Type != html.ElementNode {
390 return false
391 }
392 switch n.DataAtom {
393 case atom.A, atom.Area, atom.Link:
394 return hasAttr(n, "href")
395 case atom.Optgroup, atom.Menuitem, atom.Fieldset:
396 return !hasAttr(n, "disabled")
397 case atom.Button, atom.Input, atom.Select, atom.Textarea, atom.Option:
398 return !hasAttr(n, "disabled") && !inDisabledFieldset(n)
399 }
400 return false
401}
402
403type disabledPseudoClassSelector struct {
404 abstractPseudoClass
405}
406
407func (s disabledPseudoClassSelector) Match(n *html.Node) bool {
408 if n.Type != html.ElementNode {
409 return false
410 }
411 switch n.DataAtom {
412 case atom.Optgroup, atom.Menuitem, atom.Fieldset:
413 return hasAttr(n, "disabled")
414 case atom.Button, atom.Input, atom.Select, atom.Textarea, atom.Option:
415 return hasAttr(n, "disabled") || inDisabledFieldset(n)
416 }
417 return false
418}
419
420func hasLegendInPreviousSiblings(n *html.Node) bool {
421 for s := n.PrevSibling; s != nil; s = s.PrevSibling {
422 if s.DataAtom == atom.Legend {
423 return true
424 }
425 }
426 return false
427}
428
429func inDisabledFieldset(n *html.Node) bool {
430 if n.Parent == nil {
431 return false
432 }
433 if n.Parent.DataAtom == atom.Fieldset && hasAttr(n.Parent, "disabled") &&
434 (n.DataAtom != atom.Legend || hasLegendInPreviousSiblings(n)) {
435 return true
436 }
437 return inDisabledFieldset(n.Parent)
438}
439
440type checkedPseudoClassSelector struct {
441 abstractPseudoClass
442}
443
444func (s checkedPseudoClassSelector) Match(n *html.Node) bool {
445 if n.Type != html.ElementNode {
446 return false
447 }
448 switch n.DataAtom {
449 case atom.Input, atom.Menuitem:
450 return hasAttr(n, "checked") && matchAttribute(n, "type", func(val string) bool {
451 t := toLowerASCII(val)
452 return t == "checkbox" || t == "radio"
453 })
454 case atom.Option:
455 return hasAttr(n, "selected")
456 }
457 return false
458}