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