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