Selector
Selector is a implementation of JavaScripts querySelector. It is split between two files this file and the element
package to prevent circular dependancys, however this document will be the source for it. The best way to explain how this works is to start in the element
package with the querySelector
method and then take a look at the parts that make it up.
func (n *Node) QuerySelector(selectString string) *Node {
# QuerySelector
Arguments | Description |
---|---|
n *element.Node | Target *element.Node |
selectString string | CSS querySelector string |
return *element.Node | element.Node matching the query |
QuerySelector
works almost the same as JavaScripts querySelector method with a far limited scope. After a document is loaded from a HTML file it is compiled into element.Node
's which is a custom implementation of net/html.Node
. The reason the net/html
node is not used is it has already defined features that stray away from JavaScripts DOM.
if TestSelector(selectString, n) {return n}
To start out, we check the current element to see if the selectString
matches the element.Node
we called the method on using the TestSelector
function. If it does we can end the function there and return itself. If it does not we can continue and check its children. We do this process recursively to simplify the code.
if cr.Properties.Id != "" {return cr}
We also do a check to see if the element.Node.Properties.Id
has been assigned. This is a importaint step as this id is the the #id
used in html but a unqiue id generated at run time to be used as a internal reference. If it has not been assigned then the element does not exist.
func (n *Node) QuerySelectorAll(selectString string) *[]*Node {
# QuerySelectorAll
Arguments | Description |
---|---|
n *element.Node | Target *element.Node |
selectString string | CSS querySelector string |
return *element.Node | element.Node matching the query |
See QuerySelector. QuerySelectorAll
works the exact same as QuerySelector
with an added collector (results
) to collect all elements that match the selector throughout the recusive execution.
func TestSelector(selectString string, n *Node) bool {
# TestSelector
Arguments | Description |
---|---|
selectString string | CSS querySelector string |
node *element.Node | Target *element.Node |
return bool | returns true if the selector matches the string |
TestSelector
is the foundation of the QuerySelector
and QuerySelectorAll
as seen above.
parts := strings.Split(selectString, ">")
It first starts off by splitting the selectString
in to parts divided by >
this is becuase when you have a selector like blockquote > p
you need to start at the first level (p
) to compare the current node to see if you will need to continue to check the parents of the element with the next selector.
s := []string{} if n.Properties.Focusable { if n.Properties.Focused { s = append(s, ":focus") } } classes := n.ClassList.Classes for _, v := range classes { s = append(s, "."+v) }
Then we need to build the selectors, so we start by creating an array to store them in (s
) and we check to see if the element is focusable and if the element is focused. If so we add the :focus
selector to the list. This is important because when targeting a :focus
ed element with a querySelector that is the text that is past. We then do the same for classes.
selectors := selector.GetCSSSelectors(n.Properties.Node, s)
Next we use the GetCSSSelectors
method in this package to generate any selectors assigned to the net/html
Node.
if n.Id != "" { selectors = append(selectors, "#"+n.Id) }
Then we add the id to the array to complete the current Nodes selectors.
part := selector.SplitSelector(strings.TrimSpace(parts[len(parts)-1])) has := selector.Contains(part, selectors)
After we have the current Nodes selectors we can use the SplitSelector and Contains methods to process the passed query (selectString) and compare the two arrays.
if len(parts) == 1 || !has { return has }
If we are on the last selector in the split selector (parts) or if we have a part that does not match (i.e. has == false) then we can go ahead and return the has value. We return this instead of just the constant false
becuase if we have gotten to this point in the recursive chain that mean every part has been true until now, so the value of has
weather true
or false
we detirmine if the selector matches for the entire selector string.
} else { return TestSelector(strings.Join(parts[0:len(parts)-1], ">"), n.Parent) }
If we are not on the last element and the selector matches for this Node then we can remove the last element from parts
as we have already checked to make sure it matches and join it be >
charectors as that is what it was split by at the beginning. Then we just recall the function passing the parent as the Node.
func GetCSSSelectors(node *html.Node, selectors []string) []string {
# GetCSSSelectors
Arguments | Description |
---|---|
node *html.Node | Target net/html Node |
selectors []string | Previous Selctors |
return []string | Output of selectors |
GetCSSSelectors
purpose is to generate all possible selectors for a net/html
Node. It is used inside of the element package interally to the TestSelector
function. It does this buy taking the classes, id's, and attributes and creating an array of their string equalivents (.class, #id, and [value="somevalue"]).
func SplitSelector(s string) []string {
# SplitSelector
Arguments | Description |
---|---|
s string | Selector string |
return []string | Output of selectors |
SplitSelector
works by simply spliting a CSS selector into it's individual parts see below for an example:
1func main() {
2 fmt.Println(SplitSelector("p.text[name='first']"))
3}
Result
1[p .text [name='first']]
func Contains(selector []string, node []string) bool {
# Contains
Arguments | Description |
---|---|
selector []string | Array of selectors from the target selector |
node []string | Array of selectors from the target element |
return bool | boolean value |
Contains
compares two arrays of selectors, the first argument is the array of the selector that will be use to detirmine if the Node is a match or not. The second argument is the selecter of the targeted Node, the Node need to have all of the selectors of the selector
array, however it can have additional selectors and it will still match.
1package selector
2
3import (
4 "slices"
5 "strings"
6
7 "golang.org/x/net/html"
8)
9
10func GetCSSSelectors(node *html.Node, selectors []string) []string {
11 if node.Type == html.ElementNode {
12 selectors = append(selectors, node.Data)
13 for _, attr := range node.Attr {
14 if attr.Key == "class" {
15 classes := strings.Split(attr.Val, " ")
16 for _, class := range classes {
17 selectors = append(selectors, "."+class)
18 }
19 } else if attr.Key == "id" {
20 selectors = append(selectors, "#"+attr.Val)
21 } else {
22 selectors = append(selectors, "["+attr.Key+"=\""+attr.Val+"\"]")
23 }
24 }
25 }
26
27 return selectors
28}
29
30func SplitSelector(s string) []string {
31 var result []string
32 var current string
33
34 for _, char := range s {
35 switch char {
36 case '.', '#', '[', ']', ':':
37 if current != "" {
38 if string(char) == "]" {
39 current += string(char)
40 }
41 result = append(result, current)
42 }
43 current = ""
44 if string(char) != "]" {
45 current += string(char)
46 }
47 default:
48 current += string(char)
49 }
50 }
51
52 if current != "" && current != "]" {
53 result = append(result, current)
54 }
55
56 return result
57}
58
59func Contains(selector []string, node []string) bool {
60 has := true
61 for _, s := range selector {
62 if !slices.Contains(node, s) {
63 has = false
64 }
65 }
66 return has
67}
68
1func (n *Node) QuerySelectorAll(selectString string) *[]*Node {
2 results := []*Node{}
3 if TestSelector(selectString, n) {
4 results = append(results, n)
5 }
6
7 for i := range n.Children {
8 el := &n.Children[i]
9 cr := el.QuerySelectorAll(selectString)
10 if len(*cr) > 0 {
11 results = append(results, *cr...)
12 }
13 }
14 return &results
15}
16
17func (n *Node) QuerySelector(selectString string) *Node {
18 if TestSelector(selectString, n) {
19 return n
20 }
21
22 for i := range n.Children {
23 el := &n.Children[i]
24 cr := el.QuerySelector(selectString)
25 if cr.Properties.Id != "" {
26 return cr
27 }
28 }
29
30 return &Node{}
31}
32
33func TestSelector(selectString string, n *Node) bool {
34 parts := strings.Split(selectString, ">")
35
36 s := []string{}
37 if n.Properties.Focusable {
38 if n.Properties.Focused {
39 s = append(s, ":focus")
40 }
41 }
42
43 classes := n.ClassList.Classes
44
45 for _, v := range classes {
46 s = append(s, "."+v)
47 }
48 // fmt.Println(n.Properties.Node)
49 selectors := selector.GetCSSSelectors(n.Properties.Node, s)
50 if n.Id != "" {
51 selectors = append(selectors, "#"+n.Id)
52 }
53
54 part := selector.SplitSelector(strings.TrimSpace(parts[len(parts)-1]))
55
56 has := selector.Contains(part, selectors)
57
58 if len(parts) == 1 || !has {
59 return has
60 } else {
61 return TestSelector(strings.Join(parts[0:len(parts)-1], ">"), n.Parent)
62 }
63}