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}
1package element
2
3import (
4 "fmt"
5 "gui/selector"
6 "image"
7 ic "image/color"
8 "math/rand"
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 State struct {
42 // Id string
43 X float32
44 Y float32
45 Z float32
46 Width float32
47 Height float32
48 Border Border
49 Texture *image.RGBA
50 EM float32
51 Background ic.RGBA
52 Color ic.RGBA
53 Hash string
54 Margin MarginPadding
55 Padding MarginPadding
56 RenderCount int
57 Style map[string]string
58}
59
60// !FLAG: Plan to get rid of this
61
62type Properties struct {
63 Id string
64 EventListeners map[string][]func(Event)
65 Focusable bool
66 Focused bool
67 Editable bool
68 Hover bool
69 Selected []float32
70}
71
72type ClassList struct {
73 Classes []string
74 Value string
75}
76
77type MarginPadding struct {
78 Top float32
79 Left float32
80 Right float32
81 Bottom float32
82}
83
84func (c *ClassList) Add(class string) {
85 c.Classes = append(c.Classes, class)
86 c.Value = strings.Join(c.Classes, " ")
87}
88
89func (c *ClassList) Remove(class string) {
90 for i, v := range c.Classes {
91 if v == class {
92 c.Classes = append(c.Classes[:i], c.Classes[i+1:]...)
93 break
94 }
95 }
96
97 c.Value = strings.Join(c.Classes, " ")
98}
99
100type Border struct {
101 Width float32
102 Style string
103 Color ic.RGBA
104 Radius string
105}
106
107type Text struct {
108 Font font.Face
109 Color ic.RGBA
110 Text string
111 Underlined bool
112 Overlined bool
113 LineThrough bool
114 DecorationColor ic.RGBA
115 DecorationThickness int
116 Align string
117 Indent int // very low priority
118 LetterSpacing int
119 LineHeight int
120 WordSpacing int
121 WhiteSpace string
122 Shadows []Shadow // need
123 Width int
124 WordBreak string
125 EM int
126 X int
127 LoadedFont string
128}
129
130type Shadow struct {
131 X int
132 Y int
133 Blur int
134 Color ic.RGBA
135}
136
137func (n *Node) GetAttribute(name string) string {
138 return n.Attribute[name]
139}
140
141func (n *Node) SetAttribute(key, value string) {
142 n.Attribute[key] = value
143}
144
145func (n *Node) CreateElement(name string) Node {
146 return Node{
147 TagName: name,
148 InnerText: "",
149 Children: []Node{},
150 Style: make(map[string]string),
151 Id: "",
152 ClassList: ClassList{
153 Classes: []string{},
154 Value: "",
155 },
156 Href: "",
157 Src: "",
158 Title: "",
159 Attribute: make(map[string]string),
160 Value: "",
161 Properties: Properties{
162 Id: "",
163 EventListeners: make(map[string][]func(Event)),
164 Focusable: false,
165 Focused: false,
166 Editable: false,
167 Hover: false,
168 Selected: []float32{},
169 },
170 }
171}
172
173func (n *Node) QuerySelectorAll(selectString string) *[]*Node {
174 results := []*Node{}
175 if TestSelector(selectString, n) {
176 results = append(results, n)
177 }
178
179 for i := range n.Children {
180 el := &n.Children[i]
181 cr := el.QuerySelectorAll(selectString)
182 if len(*cr) > 0 {
183 results = append(results, *cr...)
184 }
185 }
186 return &results
187}
188
189func (n *Node) QuerySelector(selectString string) *Node {
190 if TestSelector(selectString, n) {
191 return n
192 }
193
194 for i := range n.Children {
195 el := &n.Children[i]
196 cr := el.QuerySelector(selectString)
197 if cr.Properties.Id != "" {
198 return cr
199 }
200 }
201
202 return &Node{}
203}
204
205func TestSelector(selectString string, n *Node) bool {
206 parts := strings.Split(selectString, ">")
207
208 selectors := []string{}
209 if n.Properties.Focusable {
210 if n.Properties.Focused {
211 selectors = append(selectors, ":focus")
212 }
213 }
214
215 classes := n.ClassList.Classes
216
217 for _, v := range classes {
218 selectors = append(selectors, "."+v)
219 }
220
221 if n.Id != "" {
222 selectors = append(selectors, "#"+n.Id)
223 }
224
225 selectors = append(selectors, n.TagName)
226
227 part := selector.SplitSelector(strings.TrimSpace(parts[len(parts)-1]))
228
229 has := selector.Contains(part, selectors)
230
231 if len(parts) == 1 || !has {
232 return has
233 } else {
234 return TestSelector(strings.Join(parts[0:len(parts)-1], ">"), n.Parent)
235 }
236}
237
238func (n *Node) AppendChild(c Node) {
239 c.Parent = n
240 // Set Id
241 randomInt := rand.Intn(10000)
242
243 c.Properties.Id = c.TagName + fmt.Sprint(randomInt+len(c.Parent.Children))
244
245 n.Children = append(n.Children, c)
246}
247
248func (n *Node) InsertAfter(c, tgt Node) {
249 c.Parent = n
250 // Set Id
251 randomInt := rand.Intn(10000)
252
253 c.Properties.Id = c.TagName + fmt.Sprint(randomInt+len(c.Parent.Children))
254 nodeIndex := -1
255 for i, v := range n.Children {
256
257 if v.Properties.Id == tgt.Properties.Id {
258 nodeIndex = i
259 break
260 }
261 }
262 if nodeIndex > -1 {
263 n.Children = append(n.Children[:nodeIndex+1], append([]Node{c}, n.Children[nodeIndex+1:]...)...)
264 } else {
265 n.AppendChild(c)
266 }
267}
268
269func (n *Node) InsertBefore(c, tgt Node) {
270 c.Parent = n
271 // Set Id
272 randomInt := rand.Intn(10000)
273
274 c.Properties.Id = c.TagName + fmt.Sprint(randomInt+len(c.Parent.Children))
275 nodeIndex := -1
276 for i, v := range n.Children {
277 if v.Properties.Id == tgt.Properties.Id {
278 nodeIndex = i
279 break
280 }
281 }
282 if nodeIndex > 0 {
283 n.Children = append(n.Children[:nodeIndex], append([]Node{c}, n.Children[nodeIndex:]...)...)
284 } else {
285 n.AppendChild(c)
286 }
287
288}
289
290func (n *Node) Remove() {
291 nodeIndex := -1
292 for i, v := range n.Parent.Children {
293 if v.Properties.Id == n.Properties.Id {
294 nodeIndex = i
295 break
296 }
297 }
298 if nodeIndex > 0 {
299 n.Parent.Children = append(n.Parent.Children[:nodeIndex], n.Parent.Children[nodeIndex+1:]...)
300 }
301}
302
303func (n *Node) Focus() {
304 if n.Properties.Focusable {
305 n.Properties.Focused = true
306 n.ClassList.Add(":focus")
307 }
308}
309
310func (n *Node) Blur() {
311 if n.Properties.Focusable {
312 n.Properties.Focused = false
313 n.ClassList.Remove(":focus")
314 }
315}
316
317type Event struct {
318 X int
319 Y int
320 KeyCode int
321 Key string
322 CtrlKey bool
323 MetaKey bool
324 ShiftKey bool
325 AltKey bool
326 Click bool
327 ContextMenu bool
328 MouseDown bool
329 MouseUp bool
330 MouseEnter bool
331 MouseLeave bool
332 MouseOver bool
333 KeyUp bool
334 KeyDown bool
335 KeyPress bool
336 Input bool
337 Target Node
338}
339
340type EventList struct {
341 Event Event
342 List []string
343}
344
345func (node *Node) AddEventListener(name string, callback func(Event)) {
346 if node.Properties.EventListeners == nil {
347 node.Properties.EventListeners = make(map[string][]func(Event))
348 }
349 if node.Properties.EventListeners[name] == nil {
350 node.Properties.EventListeners[name] = []func(Event){}
351 }
352 node.Properties.EventListeners[name] = append(node.Properties.EventListeners[name], callback)
353}