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.
flowchart LR; QuerySelector-->TestSelector; TestSelector-->GetCSSSelectors; GetCSSSelectors-->SplitSelector; SplitSelector-->Contains; Contains-->True; Contains-->False; False-->Children; True-->QuerySelector; EOL-->Yes; EOL-->No; No-->TestSelector Yes-->QuerySelector; Children-->TestSelector; Children-->EOL;
flowchart LR; QuerySelectorAll-->TestSelector; TestSelector-->GetCSSSelectors; GetCSSSelectors-->SplitSelector; SplitSelector-->Contains; Contains-->True; Contains-->False; False-->QuerySelectorAll; True-->EOL; EOL-->Yes; EOL-->No; No-->TestSelector Yes-->QuerySelectorAll;
# QuerySelector?(go)
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 (n) 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.
# QuerySelectorAll?(go)
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.
# TestSelector?(go)
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.
selectors := []string{} if n.Properties.Focusable { if n.Properties.Focused { selectors = append(selectors, ":focus") } } classes := n.ClassList.Classes for _, v := range classes { selectors = append(selectors, "."+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.
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.
# SplitSelector?(go)
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']]
# Contains?(go)
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 GetInitCSSSelectors(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
69// func Query(selector string, n *Node) bool {
70// parts := strings.Split(selector, ">")
71
72// selectors := getCSSSelectors(n.Node, []string{})
73
74// part := splitSelector(strings.TrimSpace(parts[len(parts)-1]))
75
76// fmt.Println(part, selectors)
77
78// has := contains(part, selectors)
79
80// if len(parts) == 1 || !has {
81// return has
82// } else {
83// return Query(strings.Join(parts[0:len(parts)-1], ">"), n.Parent)
84// }
85// }
86
87// func main() {
88// selector := "div.class#id[attr=\"value\"] > div"
89
90// node := &html.Node{
91// Type: html.ElementNode,
92// Data: "div",
93// Attr: []html.Attribute{
94// {Key: "class", Val: "class"},
95// {Key: "id", Val: "id"},
96// {Key: "attr", Val: "value"},
97// },
98// }
99
100// nodeparent := &html.Node{
101// Type: html.ElementNode,
102// Data: "div",
103// Attr: []html.Attribute{
104// {Key: "class", Val: "class"},
105// {Key: "id", Val: "id"},
106// {Key: "attr", Val: "value"},
107// },
108// }
109
110// n := Node{
111// Node: node,
112// Parent: &Node{
113// Node: nodeparent,
114// },
115// }
116
117// fmt.Println(Query(selector, &n))
118// }
1package element
2
3import (
4 "fmt"
5 "gui/selector"
6 "image"
7 ic "image/color"
8 "regexp"
9 "strings"
10
11 "golang.org/x/image/font"
12)
13
14type Node struct {
15 TagName string
16 InnerText string
17 Parent *Node
18 Children []Node
19 Style map[string]string
20 Id string
21 ClassList ClassList
22 Href string
23 Src string
24 Title string
25 Attribute map[string]string
26
27 ScrollY float32
28 Value string
29 OnClick func(Event)
30 OnContextMenu func(Event)
31 OnMouseDown func(Event)
32 OnMouseUp func(Event)
33 OnMouseEnter func(Event)
34 OnMouseLeave func(Event)
35 OnMouseOver func(Event)
36 OnMouseMove func(Event)
37 OnScroll func(Event)
38 Properties Properties
39}
40
41type Properties struct {
42 Id string
43 X float32
44 Y float32
45 Hash string
46 Width float32
47 Height float32
48 Border Border
49 EventListeners map[string][]func(Event)
50 EM float32
51 Text Text
52 Focusable bool
53 Focused bool
54 Editable bool
55 Hover bool
56 Selected []float32
57 Test string
58}
59
60type ClassList struct {
61 Classes []string
62 Value string
63}
64
65func (c *ClassList) Add(class string) {
66 c.Classes = append(c.Classes, class)
67 c.Value = strings.Join(c.Classes, " ")
68}
69
70func (c *ClassList) Remove(class string) {
71 for i, v := range c.Classes {
72 if v == class {
73 c.Classes = append(c.Classes[:i], c.Classes[i+1:]...)
74 break
75 }
76 }
77
78 c.Value = strings.Join(c.Classes, " ")
79}
80
81type Border struct {
82 Width string
83 Style string
84 Color ic.RGBA
85 Radius string
86}
87
88type Text struct {
89 Font font.Face
90 Color ic.RGBA
91 Image *image.RGBA
92 Underlined bool
93 Overlined bool
94 LineThrough bool
95 DecorationColor ic.RGBA
96 DecorationThickness int
97 Align string
98 Indent int // very low priority
99 LetterSpacing int
100 LineHeight int
101 WordSpacing int
102 WhiteSpace string
103 Shadows []Shadow // need
104 Width int
105 WordBreak string
106 EM int
107 X int
108 LoadedFont string
109}
110
111type Shadow struct {
112 X int
113 Y int
114 Blur int
115 Color ic.RGBA
116}
117
118func (n *Node) GetAttribute(name string) string {
119 return n.Attribute[name]
120}
121
122func (n *Node) SetAttribute(key, value string) {
123 n.Attribute[key] = value
124}
125
126func (n *Node) CreateElement(name string) Node {
127 return Node{
128 TagName: name,
129 InnerText: "",
130 Children: []Node{},
131 Style: map[string]string{},
132 Id: "",
133 ClassList: ClassList{
134 Classes: []string{},
135 Value: "",
136 },
137 Href: "",
138 Src: "",
139 Title: "",
140 Attribute: map[string]string{},
141 Value: "",
142 Properties: Properties{
143 Id: "",
144 X: 0,
145 Y: 0,
146 Hash: "",
147 Width: 0,
148 Height: 0,
149 Border: Border{
150 Width: "0px",
151 Style: "solid",
152 Color: ic.RGBA{
153 R: 0,
154 G: 0,
155 B: 0,
156 A: 0,
157 },
158 Radius: "0px",
159 },
160 EventListeners: map[string][]func(Event){},
161 EM: 16,
162 Text: Text{},
163 Focusable: false,
164 Focused: false,
165 Editable: false,
166 Hover: false,
167 Selected: []float32{},
168 },
169 }
170}
171
172func (n *Node) QuerySelectorAll(selectString string) *[]*Node {
173 results := []*Node{}
174 if TestSelector(selectString, n) {
175 results = append(results, n)
176 }
177
178 for i := range n.Children {
179 el := &n.Children[i]
180 cr := el.QuerySelectorAll(selectString)
181 if len(*cr) > 0 {
182 results = append(results, *cr...)
183 }
184 }
185 return &results
186}
187
188func (n *Node) QuerySelector(selectString string) *Node {
189 if TestSelector(selectString, n) {
190 return n
191 }
192
193 for i := range n.Children {
194 el := &n.Children[i]
195 cr := el.QuerySelector(selectString)
196 if cr.Properties.Id != "" {
197 return cr
198 }
199 }
200
201 return &Node{}
202}
203
204func TestSelector(selectString string, n *Node) bool {
205 parts := strings.Split(selectString, ">")
206
207 selectors := []string{}
208 if n.Properties.Focusable {
209 if n.Properties.Focused {
210 selectors = append(selectors, ":focus")
211 }
212 }
213
214 classes := n.ClassList.Classes
215
216 for _, v := range classes {
217 selectors = append(selectors, "."+v)
218 }
219
220 if n.Id != "" {
221 selectors = append(selectors, "#"+n.Id)
222 }
223
224 part := selector.SplitSelector(strings.TrimSpace(parts[len(parts)-1]))
225
226 has := selector.Contains(part, selectors)
227
228 if len(parts) == 1 || !has {
229 return has
230 } else {
231 return TestSelector(strings.Join(parts[0:len(parts)-1], ">"), n.Parent)
232 }
233}
234
235func (n *Node) AppendChild(c Node) {
236 c.Parent = n
237 // Set ID
238 // Define a regular expression to match numeric digits
239 re := regexp.MustCompile(`\d+`)
240
241 // Find all matches of numeric digits in the input string
242 matches := re.FindAllString(c.Parent.Properties.Id, -1)
243
244 // Concatenate all matches into a single string
245 numericPart := ""
246 for _, match := range matches {
247 numericPart += match
248 }
249
250 c.Properties.Id = c.TagName + numericPart + fmt.Sprint(len(c.Parent.Children))
251
252 n.Children = append(n.Children, c)
253}
254
255func (n *Node) Focus() {
256 if n.Properties.Focusable {
257 n.Properties.Focused = true
258 n.ClassList.Add(":focus")
259 }
260}
261
262func (n *Node) Blur() {
263 if n.Properties.Focusable {
264 n.Properties.Focused = false
265 n.ClassList.Remove(":focus")
266 }
267}
268
269type Event struct {
270 X int
271 Y int
272 KeyCode int
273 Key string
274 CtrlKey bool
275 MetaKey bool
276 ShiftKey bool
277 AltKey bool
278 Click bool
279 ContextMenu bool
280 MouseDown bool
281 MouseUp bool
282 MouseEnter bool
283 MouseLeave bool
284 MouseOver bool
285 KeyUp bool
286 KeyDown bool
287 KeyPress bool
288 Input bool
289 Target Node
290}
291
292type EventList struct {
293 Event Event
294 List []string
295}
296
297func (node *Node) AddEventListener(name string, callback func(Event)) {
298 if node.Properties.EventListeners == nil {
299 node.Properties.EventListeners = make(map[string][]func(Event))
300 }
301 if node.Properties.EventListeners[name] == nil {
302 node.Properties.EventListeners[name] = []func(Event){}
303 }
304 node.Properties.EventListeners[name] = append(node.Properties.EventListeners[name], callback)
305}