79494047b0
* Show commit status icon in commits table * Add comments * Fix icons * Few more places where commit table is displayed * Change integration test to use goquery for parsing html * Add integration tests for commit table and status icons * Fix status to return lates status correctly on all databases * Rewrote lates commit status selects
622 lines
14 KiB
Go
622 lines
14 KiB
Go
package cascadia
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
// the Selector type, and functions for creating them
|
|
|
|
// A Selector is a function which tells whether a node matches or not.
|
|
type Selector func(*html.Node) bool
|
|
|
|
// hasChildMatch returns whether n has any child that matches a.
|
|
func hasChildMatch(n *html.Node, a Selector) bool {
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
if a(c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasDescendantMatch performs a depth-first search of n's descendants,
|
|
// testing whether any of them match a. It returns true as soon as a match is
|
|
// found, or false if no match is found.
|
|
func hasDescendantMatch(n *html.Node, a Selector) bool {
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
if a(c) || (c.Type == html.ElementNode && hasDescendantMatch(c, a)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Compile parses a selector and returns, if successful, a Selector object
|
|
// that can be used to match against html.Node objects.
|
|
func Compile(sel string) (Selector, error) {
|
|
p := &parser{s: sel}
|
|
compiled, err := p.parseSelectorGroup()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.i < len(sel) {
|
|
return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
|
|
}
|
|
|
|
return compiled, nil
|
|
}
|
|
|
|
// MustCompile is like Compile, but panics instead of returning an error.
|
|
func MustCompile(sel string) Selector {
|
|
compiled, err := Compile(sel)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return compiled
|
|
}
|
|
|
|
// MatchAll returns a slice of the nodes that match the selector,
|
|
// from n and its children.
|
|
func (s Selector) MatchAll(n *html.Node) []*html.Node {
|
|
return s.matchAllInto(n, nil)
|
|
}
|
|
|
|
func (s Selector) matchAllInto(n *html.Node, storage []*html.Node) []*html.Node {
|
|
if s(n) {
|
|
storage = append(storage, n)
|
|
}
|
|
|
|
for child := n.FirstChild; child != nil; child = child.NextSibling {
|
|
storage = s.matchAllInto(child, storage)
|
|
}
|
|
|
|
return storage
|
|
}
|
|
|
|
// Match returns true if the node matches the selector.
|
|
func (s Selector) Match(n *html.Node) bool {
|
|
return s(n)
|
|
}
|
|
|
|
// MatchFirst returns the first node that matches s, from n and its children.
|
|
func (s Selector) MatchFirst(n *html.Node) *html.Node {
|
|
if s.Match(n) {
|
|
return n
|
|
}
|
|
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
m := s.MatchFirst(c)
|
|
if m != nil {
|
|
return m
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Filter returns the nodes in nodes that match the selector.
|
|
func (s Selector) Filter(nodes []*html.Node) (result []*html.Node) {
|
|
for _, n := range nodes {
|
|
if s(n) {
|
|
result = append(result, n)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// typeSelector returns a Selector that matches elements with a given tag name.
|
|
func typeSelector(tag string) Selector {
|
|
tag = toLowerASCII(tag)
|
|
return func(n *html.Node) bool {
|
|
return n.Type == html.ElementNode && n.Data == tag
|
|
}
|
|
}
|
|
|
|
// toLowerASCII returns s with all ASCII capital letters lowercased.
|
|
func toLowerASCII(s string) string {
|
|
var b []byte
|
|
for i := 0; i < len(s); i++ {
|
|
if c := s[i]; 'A' <= c && c <= 'Z' {
|
|
if b == nil {
|
|
b = make([]byte, len(s))
|
|
copy(b, s)
|
|
}
|
|
b[i] = s[i] + ('a' - 'A')
|
|
}
|
|
}
|
|
|
|
if b == nil {
|
|
return s
|
|
}
|
|
|
|
return string(b)
|
|
}
|
|
|
|
// attributeSelector returns a Selector that matches elements
|
|
// where the attribute named key satisifes the function f.
|
|
func attributeSelector(key string, f func(string) bool) Selector {
|
|
key = toLowerASCII(key)
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
for _, a := range n.Attr {
|
|
if a.Key == key && f(a.Val) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// attributeExistsSelector returns a Selector that matches elements that have
|
|
// an attribute named key.
|
|
func attributeExistsSelector(key string) Selector {
|
|
return attributeSelector(key, func(string) bool { return true })
|
|
}
|
|
|
|
// attributeEqualsSelector returns a Selector that matches elements where
|
|
// the attribute named key has the value val.
|
|
func attributeEqualsSelector(key, val string) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
return s == val
|
|
})
|
|
}
|
|
|
|
// attributeNotEqualSelector returns a Selector that matches elements where
|
|
// the attribute named key does not have the value val.
|
|
func attributeNotEqualSelector(key, val string) Selector {
|
|
key = toLowerASCII(key)
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
for _, a := range n.Attr {
|
|
if a.Key == key && a.Val == val {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// attributeIncludesSelector returns a Selector that matches elements where
|
|
// the attribute named key is a whitespace-separated list that includes val.
|
|
func attributeIncludesSelector(key, val string) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
for s != "" {
|
|
i := strings.IndexAny(s, " \t\r\n\f")
|
|
if i == -1 {
|
|
return s == val
|
|
}
|
|
if s[:i] == val {
|
|
return true
|
|
}
|
|
s = s[i+1:]
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
// attributeDashmatchSelector returns a Selector that matches elements where
|
|
// the attribute named key equals val or starts with val plus a hyphen.
|
|
func attributeDashmatchSelector(key, val string) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
if s == val {
|
|
return true
|
|
}
|
|
if len(s) <= len(val) {
|
|
return false
|
|
}
|
|
if s[:len(val)] == val && s[len(val)] == '-' {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
// attributePrefixSelector returns a Selector that matches elements where
|
|
// the attribute named key starts with val.
|
|
func attributePrefixSelector(key, val string) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
if strings.TrimSpace(s) == "" {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(s, val)
|
|
})
|
|
}
|
|
|
|
// attributeSuffixSelector returns a Selector that matches elements where
|
|
// the attribute named key ends with val.
|
|
func attributeSuffixSelector(key, val string) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
if strings.TrimSpace(s) == "" {
|
|
return false
|
|
}
|
|
return strings.HasSuffix(s, val)
|
|
})
|
|
}
|
|
|
|
// attributeSubstringSelector returns a Selector that matches nodes where
|
|
// the attribute named key contains val.
|
|
func attributeSubstringSelector(key, val string) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
if strings.TrimSpace(s) == "" {
|
|
return false
|
|
}
|
|
return strings.Contains(s, val)
|
|
})
|
|
}
|
|
|
|
// attributeRegexSelector returns a Selector that matches nodes where
|
|
// the attribute named key matches the regular expression rx
|
|
func attributeRegexSelector(key string, rx *regexp.Regexp) Selector {
|
|
return attributeSelector(key,
|
|
func(s string) bool {
|
|
return rx.MatchString(s)
|
|
})
|
|
}
|
|
|
|
// intersectionSelector returns a selector that matches nodes that match
|
|
// both a and b.
|
|
func intersectionSelector(a, b Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
return a(n) && b(n)
|
|
}
|
|
}
|
|
|
|
// unionSelector returns a selector that matches elements that match
|
|
// either a or b.
|
|
func unionSelector(a, b Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
return a(n) || b(n)
|
|
}
|
|
}
|
|
|
|
// negatedSelector returns a selector that matches elements that do not match a.
|
|
func negatedSelector(a Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
return !a(n)
|
|
}
|
|
}
|
|
|
|
// writeNodeText writes the text contained in n and its descendants to b.
|
|
func writeNodeText(n *html.Node, b *bytes.Buffer) {
|
|
switch n.Type {
|
|
case html.TextNode:
|
|
b.WriteString(n.Data)
|
|
case html.ElementNode:
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
writeNodeText(c, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
// nodeText returns the text contained in n and its descendants.
|
|
func nodeText(n *html.Node) string {
|
|
var b bytes.Buffer
|
|
writeNodeText(n, &b)
|
|
return b.String()
|
|
}
|
|
|
|
// nodeOwnText returns the contents of the text nodes that are direct
|
|
// children of n.
|
|
func nodeOwnText(n *html.Node) string {
|
|
var b bytes.Buffer
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
if c.Type == html.TextNode {
|
|
b.WriteString(c.Data)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// textSubstrSelector returns a selector that matches nodes that
|
|
// contain the given text.
|
|
func textSubstrSelector(val string) Selector {
|
|
return func(n *html.Node) bool {
|
|
text := strings.ToLower(nodeText(n))
|
|
return strings.Contains(text, val)
|
|
}
|
|
}
|
|
|
|
// ownTextSubstrSelector returns a selector that matches nodes that
|
|
// directly contain the given text
|
|
func ownTextSubstrSelector(val string) Selector {
|
|
return func(n *html.Node) bool {
|
|
text := strings.ToLower(nodeOwnText(n))
|
|
return strings.Contains(text, val)
|
|
}
|
|
}
|
|
|
|
// textRegexSelector returns a selector that matches nodes whose text matches
|
|
// the specified regular expression
|
|
func textRegexSelector(rx *regexp.Regexp) Selector {
|
|
return func(n *html.Node) bool {
|
|
return rx.MatchString(nodeText(n))
|
|
}
|
|
}
|
|
|
|
// ownTextRegexSelector returns a selector that matches nodes whose text
|
|
// directly matches the specified regular expression
|
|
func ownTextRegexSelector(rx *regexp.Regexp) Selector {
|
|
return func(n *html.Node) bool {
|
|
return rx.MatchString(nodeOwnText(n))
|
|
}
|
|
}
|
|
|
|
// hasChildSelector returns a selector that matches elements
|
|
// with a child that matches a.
|
|
func hasChildSelector(a Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
return hasChildMatch(n, a)
|
|
}
|
|
}
|
|
|
|
// hasDescendantSelector returns a selector that matches elements
|
|
// with any descendant that matches a.
|
|
func hasDescendantSelector(a Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
return hasDescendantMatch(n, a)
|
|
}
|
|
}
|
|
|
|
// nthChildSelector returns a selector that implements :nth-child(an+b).
|
|
// If last is true, implements :nth-last-child instead.
|
|
// If ofType is true, implements :nth-of-type instead.
|
|
func nthChildSelector(a, b int, last, ofType bool) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
|
|
parent := n.Parent
|
|
if parent == nil {
|
|
return false
|
|
}
|
|
|
|
if parent.Type == html.DocumentNode {
|
|
return false
|
|
}
|
|
|
|
i := -1
|
|
count := 0
|
|
for c := parent.FirstChild; c != nil; c = c.NextSibling {
|
|
if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
|
|
continue
|
|
}
|
|
count++
|
|
if c == n {
|
|
i = count
|
|
if !last {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if i == -1 {
|
|
// This shouldn't happen, since n should always be one of its parent's children.
|
|
return false
|
|
}
|
|
|
|
if last {
|
|
i = count - i + 1
|
|
}
|
|
|
|
i -= b
|
|
if a == 0 {
|
|
return i == 0
|
|
}
|
|
|
|
return i%a == 0 && i/a >= 0
|
|
}
|
|
}
|
|
|
|
// simpleNthChildSelector returns a selector that implements :nth-child(b).
|
|
// If ofType is true, implements :nth-of-type instead.
|
|
func simpleNthChildSelector(b int, ofType bool) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
|
|
parent := n.Parent
|
|
if parent == nil {
|
|
return false
|
|
}
|
|
|
|
if parent.Type == html.DocumentNode {
|
|
return false
|
|
}
|
|
|
|
count := 0
|
|
for c := parent.FirstChild; c != nil; c = c.NextSibling {
|
|
if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
|
|
continue
|
|
}
|
|
count++
|
|
if c == n {
|
|
return count == b
|
|
}
|
|
if count >= b {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// simpleNthLastChildSelector returns a selector that implements
|
|
// :nth-last-child(b). If ofType is true, implements :nth-last-of-type
|
|
// instead.
|
|
func simpleNthLastChildSelector(b int, ofType bool) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
|
|
parent := n.Parent
|
|
if parent == nil {
|
|
return false
|
|
}
|
|
|
|
if parent.Type == html.DocumentNode {
|
|
return false
|
|
}
|
|
|
|
count := 0
|
|
for c := parent.LastChild; c != nil; c = c.PrevSibling {
|
|
if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
|
|
continue
|
|
}
|
|
count++
|
|
if c == n {
|
|
return count == b
|
|
}
|
|
if count >= b {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// onlyChildSelector returns a selector that implements :only-child.
|
|
// If ofType is true, it implements :only-of-type instead.
|
|
func onlyChildSelector(ofType bool) Selector {
|
|
return func(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
|
|
parent := n.Parent
|
|
if parent == nil {
|
|
return false
|
|
}
|
|
|
|
if parent.Type == html.DocumentNode {
|
|
return false
|
|
}
|
|
|
|
count := 0
|
|
for c := parent.FirstChild; c != nil; c = c.NextSibling {
|
|
if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
|
|
continue
|
|
}
|
|
count++
|
|
if count > 1 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return count == 1
|
|
}
|
|
}
|
|
|
|
// inputSelector is a Selector that matches input, select, textarea and button elements.
|
|
func inputSelector(n *html.Node) bool {
|
|
return n.Type == html.ElementNode && (n.Data == "input" || n.Data == "select" || n.Data == "textarea" || n.Data == "button")
|
|
}
|
|
|
|
// emptyElementSelector is a Selector that matches empty elements.
|
|
func emptyElementSelector(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
switch c.Type {
|
|
case html.ElementNode, html.TextNode:
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// descendantSelector returns a Selector that matches an element if
|
|
// it matches d and has an ancestor that matches a.
|
|
func descendantSelector(a, d Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
if !d(n) {
|
|
return false
|
|
}
|
|
|
|
for p := n.Parent; p != nil; p = p.Parent {
|
|
if a(p) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
// childSelector returns a Selector that matches an element if
|
|
// it matches d and its parent matches a.
|
|
func childSelector(a, d Selector) Selector {
|
|
return func(n *html.Node) bool {
|
|
return d(n) && n.Parent != nil && a(n.Parent)
|
|
}
|
|
}
|
|
|
|
// siblingSelector returns a Selector that matches an element
|
|
// if it matches s2 and in is preceded by an element that matches s1.
|
|
// If adjacent is true, the sibling must be immediately before the element.
|
|
func siblingSelector(s1, s2 Selector, adjacent bool) Selector {
|
|
return func(n *html.Node) bool {
|
|
if !s2(n) {
|
|
return false
|
|
}
|
|
|
|
if adjacent {
|
|
for n = n.PrevSibling; n != nil; n = n.PrevSibling {
|
|
if n.Type == html.TextNode || n.Type == html.CommentNode {
|
|
continue
|
|
}
|
|
return s1(n)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Walk backwards looking for element that matches s1
|
|
for c := n.PrevSibling; c != nil; c = c.PrevSibling {
|
|
if s1(c) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
// rootSelector implements :root
|
|
func rootSelector(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
if n.Parent == nil {
|
|
return false
|
|
}
|
|
return n.Parent.Type == html.DocumentNode
|
|
}
|