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
10// !ISSUE: Create :not and other selectors
11
12func GetInitCSSSelectors(node *html.Node, selectors []string) []string {
13 if node.Type == html.ElementNode {
14 selectors = append(selectors, node.Data)
15 for _, attr := range node.Attr {
16 if attr.Key == "class" {
17 classes := strings.Split(attr.Val, " ")
18 for _, class := range classes {
19 selectors = append(selectors, "."+class)
20 }
21 } else if attr.Key == "id" {
22 selectors = append(selectors, "#"+attr.Val)
23 } else {
24 selectors = append(selectors, "["+attr.Key+"=\""+attr.Val+"\"]")
25 }
26 }
27 }
28
29 return selectors
30}
31
32func SplitSelector(s string) []string {
33 var result []string
34 var current strings.Builder
35
36 for _, char := range s {
37 switch char {
38 case '.', '#', '[', ']', ':':
39 if current.Len() > 0 {
40 if char == ']' {
41 current.WriteRune(char)
42 }
43 result = append(result, current.String())
44 current.Reset()
45 }
46 if char != ']' {
47 current.WriteRune(char)
48 }
49 default:
50 current.WriteRune(char)
51 }
52 }
53
54 if current.Len() > 0 {
55 result = append(result, current.String())
56 }
57
58 return result
59}
60
61func Contains(selector []string, node []string) bool {
62 has := true
63 for _, s := range selector {
64 if !slices.Contains(node, s) {
65 has = false
66 }
67 }
68 return has
69}
1package element
2
3import (
4 "gui/selector"
5 "image"
6 ic "image/color"
7 "math"
8 "strconv"
9 "strings"
10 "sync"
11
12 "golang.org/x/image/font"
13)
14
15type Node struct {
16 TagName string
17 InnerText string
18 InnerHTML string
19 OuterHTML string
20 Parent *Node `json:"-"`
21 Children []*Node
22 Style map[string]string
23 Id string
24 ClassList ClassList
25 Href string
26 Src string
27 Title string
28 Attribute map[string]string
29
30 ScrollY float32
31 Value string
32 OnClick func(Event) `json:"-"`
33 OnContextMenu func(Event) `json:"-"`
34 OnMouseDown func(Event) `json:"-"`
35 OnMouseUp func(Event) `json:"-"`
36 OnMouseEnter func(Event) `json:"-"`
37 OnMouseLeave func(Event) `json:"-"`
38 OnMouseOver func(Event) `json:"-"`
39 OnMouseMove func(Event) `json:"-"`
40 OnScroll func(Event) `json:"-"`
41 Properties Properties
42}
43
44type State struct {
45 // Id string
46 X float32
47 Y float32
48 Z float32
49 Width float32
50 Height float32
51 Border Border
52 Texture *image.RGBA
53 EM float32
54 Background ic.RGBA
55 Color ic.RGBA
56 Margin MarginPadding
57 Padding MarginPadding
58}
59
60// !FLAG: Plan to get rid of this
61
62type Properties struct {
63 Id string
64 EventListeners map[string][]func(Event) `json:"-"`
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 Top BorderSide
102 Right BorderSide
103 Bottom BorderSide
104 Left BorderSide
105 Radius BorderRadius
106 Texture *image.RGBA
107}
108
109type BorderSide struct {
110 Width float32
111 Style string
112 Color ic.RGBA
113}
114
115type BorderRadius struct {
116 TopLeft float32
117 TopRight float32
118 BottomLeft float32
119 BottomRight float32
120}
121
122type Text struct {
123 Font *font.Face
124 Color ic.RGBA
125 Text string
126 Underlined bool
127 Overlined bool
128 LineThrough bool
129 DecorationColor ic.RGBA
130 DecorationThickness int
131 Align string
132 Indent int // very low priority
133 LetterSpacing int
134 LineHeight int
135 WordSpacing int
136 WhiteSpace string
137 Shadows []Shadow // need
138 Width int
139 WordBreak string
140 EM int
141 X int
142 LoadedFont string
143 Last bool
144}
145
146type Shadow struct {
147 X int
148 Y int
149 Blur int
150 Color ic.RGBA
151}
152
153func (n *Node) GetAttribute(name string) string {
154 return n.Attribute[name]
155}
156
157func (n *Node) SetAttribute(key, value string) {
158 n.Attribute[key] = value
159}
160
161func (n *Node) CreateElement(name string) Node {
162 return Node{
163 TagName: name,
164 InnerText: "",
165 OuterHTML: "",
166 InnerHTML: "",
167 Children: []*Node{},
168 Style: make(map[string]string),
169 Id: "",
170 ClassList: ClassList{
171 Classes: []string{},
172 Value: "",
173 },
174 Href: "",
175 Src: "",
176 Title: "",
177 Attribute: make(map[string]string),
178 Value: "",
179 Properties: Properties{
180 Id: "",
181 EventListeners: make(map[string][]func(Event)),
182 Focusable: false,
183 Focused: false,
184 Editable: false,
185 Hover: false,
186 Selected: []float32{},
187 },
188 }
189}
190
191func (n *Node) QuerySelectorAll(selectString string) *[]*Node {
192 results := []*Node{}
193 if TestSelector(selectString, n) {
194 results = append(results, n)
195 }
196
197 for i := range n.Children {
198 el := n.Children[i]
199 cr := el.QuerySelectorAll(selectString)
200 if len(*cr) > 0 {
201 results = append(results, *cr...)
202 }
203 }
204 return &results
205}
206
207func (n *Node) QuerySelector(selectString string) *Node {
208 if TestSelector(selectString, n) {
209 return n
210 }
211
212 for i := range n.Children {
213 el := n.Children[i]
214 cr := el.QuerySelector(selectString)
215 if cr.Properties.Id != "" {
216 return cr
217 }
218 }
219
220 return &Node{}
221}
222
223func TestSelector(selectString string, n *Node) bool {
224 parts := strings.Split(selectString, ">")
225
226 selectors := []string{}
227 if n.Properties.Focusable && n.Properties.Focused {
228 selectors = append(selectors, ":focus")
229 }
230
231 for _, class := range n.ClassList.Classes {
232 selectors = append(selectors, "."+class)
233 }
234
235 if n.Id != "" {
236 selectors = append(selectors, "#"+n.Id)
237 }
238
239 selectors = append(selectors, n.TagName)
240 part := selector.SplitSelector(strings.TrimSpace(parts[len(parts)-1]))
241
242 has := selector.Contains(part, selectors)
243
244 if len(parts) == 1 || !has {
245 return has
246 }
247 return TestSelector(strings.Join(parts[:len(parts)-1], ">"), n.Parent)
248}
249
250var (
251 idCounter int64
252 mu sync.Mutex
253)
254
255func generateUniqueId(tagName string) string {
256 mu.Lock()
257 defer mu.Unlock()
258 if idCounter == math.MaxInt64 {
259 idCounter = 0
260 }
261 idCounter++
262 return tagName + strconv.FormatInt(idCounter, 10)
263}
264
265func (n *Node) AppendChild(c *Node) {
266 c.Parent = n
267 c.Properties.Id = generateUniqueId(c.TagName)
268 n.Children = append(n.Children, c)
269}
270
271func (n *Node) InsertAfter(c, tgt *Node) {
272 c.Parent = n
273 c.Properties.Id = generateUniqueId(c.TagName)
274
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 > -1 {
283 n.Children = append(n.Children[:nodeIndex+1], append([]*Node{c}, n.Children[nodeIndex+1:]...)...)
284 } else {
285 n.AppendChild(c)
286 }
287}
288
289func (n *Node) InsertBefore(c, tgt *Node) {
290 c.Parent = n
291 // Set Id
292
293 c.Properties.Id = generateUniqueId(c.TagName)
294
295 nodeIndex := -1
296 for i, v := range n.Children {
297 if v.Properties.Id == tgt.Properties.Id {
298 nodeIndex = i
299 break
300 }
301 }
302 if nodeIndex > 0 {
303 n.Children = append(n.Children[:nodeIndex], append([]*Node{c}, n.Children[nodeIndex:]...)...)
304 // n.Children = append(n.Children, Node{}) // Add a zero value to expand the slice
305 // copy(n.Children[nodeIndex+1:], n.Children[nodeIndex:])
306 // n.Children[nodeIndex] = c
307 } else {
308 n.AppendChild(c)
309 }
310
311}
312
313func (n *Node) Remove() {
314 nodeIndex := -1
315 for i, v := range n.Parent.Children {
316 if v.Properties.Id == n.Properties.Id {
317 nodeIndex = i
318 break
319 }
320 }
321 if nodeIndex > 0 {
322 n.Parent.Children = append(n.Parent.Children[:nodeIndex], n.Parent.Children[nodeIndex+1:]...)
323 }
324}
325
326func (n *Node) Focus() {
327 if n.Properties.Focusable {
328 n.Properties.Focused = true
329 n.ClassList.Add(":focus")
330 }
331}
332
333func (n *Node) Blur() {
334 if n.Properties.Focusable {
335 n.Properties.Focused = false
336 n.ClassList.Remove(":focus")
337 }
338}
339
340type Event struct {
341 X int
342 Y int
343 KeyCode int
344 Key string
345 CtrlKey bool
346 MetaKey bool
347 ShiftKey bool
348 AltKey bool
349 Click bool
350 ContextMenu bool
351 MouseDown bool
352 MouseUp bool
353 MouseEnter bool
354 MouseLeave bool
355 MouseOver bool
356 KeyUp bool
357 KeyDown bool
358 KeyPress bool
359 Input bool
360 Target Node
361}
362
363type EventList struct {
364 Event Event
365 List []string
366}
367
368func (node *Node) AddEventListener(name string, callback func(Event)) {
369 if node.Properties.EventListeners == nil {
370 node.Properties.EventListeners = make(map[string][]func(Event))
371 }
372 if node.Properties.EventListeners[name] == nil {
373 node.Properties.EventListeners[name] = []func(Event){}
374 }
375 node.Properties.EventListeners[name] = append(node.Properties.EventListeners[name], callback)
376}