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 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 Last bool
129}
130
131type Shadow struct {
132 X int
133 Y int
134 Blur int
135 Color ic.RGBA
136}
137
138func (n *Node) GetAttribute(name string) string {
139 return n.Attribute[name]
140}
141
142func (n *Node) SetAttribute(key, value string) {
143 n.Attribute[key] = value
144}
145
146func (n *Node) CreateElement(name string) Node {
147 return Node{
148 TagName: name,
149 InnerText: "",
150 OuterHTML: "",
151 InnerHTML: "",
152 Children: []*Node{},
153 Style: make(map[string]string),
154 Id: "",
155 ClassList: ClassList{
156 Classes: []string{},
157 Value: "",
158 },
159 Href: "",
160 Src: "",
161 Title: "",
162 Attribute: make(map[string]string),
163 Value: "",
164 Properties: Properties{
165 Id: "",
166 EventListeners: make(map[string][]func(Event)),
167 Focusable: false,
168 Focused: false,
169 Editable: false,
170 Hover: false,
171 Selected: []float32{},
172 },
173 }
174}
175
176func (n *Node) QuerySelectorAll(selectString string) *[]*Node {
177 results := []*Node{}
178 if TestSelector(selectString, n) {
179 results = append(results, n)
180 }
181
182 for i := range n.Children {
183 el := n.Children[i]
184 cr := el.QuerySelectorAll(selectString)
185 if len(*cr) > 0 {
186 results = append(results, *cr...)
187 }
188 }
189 return &results
190}
191
192func (n *Node) QuerySelector(selectString string) *Node {
193 if TestSelector(selectString, n) {
194 return n
195 }
196
197 for i := range n.Children {
198 el := n.Children[i]
199 cr := el.QuerySelector(selectString)
200 if cr.Properties.Id != "" {
201 return cr
202 }
203 }
204
205 return &Node{}
206}
207
208func TestSelector(selectString string, n *Node) bool {
209 parts := strings.Split(selectString, ">")
210
211 selectors := []string{}
212 if n.Properties.Focusable && n.Properties.Focused {
213 selectors = append(selectors, ":focus")
214 }
215
216 for _, class := range n.ClassList.Classes {
217 selectors = append(selectors, "."+class)
218 }
219
220 if n.Id != "" {
221 selectors = append(selectors, "#"+n.Id)
222 }
223
224 selectors = append(selectors, n.TagName)
225 part := selector.SplitSelector(strings.TrimSpace(parts[len(parts)-1]))
226
227 has := selector.Contains(part, selectors)
228
229 if len(parts) == 1 || !has {
230 return has
231 }
232 return TestSelector(strings.Join(parts[:len(parts)-1], ">"), n.Parent)
233}
234
235var (
236 idCounter int64
237 mu sync.Mutex
238)
239
240func generateUniqueId(tagName string) string {
241 mu.Lock()
242 defer mu.Unlock()
243 if idCounter == math.MaxInt64 {
244 idCounter = 0
245 }
246 idCounter++
247 return tagName + strconv.FormatInt(idCounter, 10)
248}
249
250func (n *Node) AppendChild(c *Node) {
251 c.Parent = n
252 c.Properties.Id = generateUniqueId(c.TagName)
253 n.Children = append(n.Children, c)
254}
255
256func (n *Node) InsertAfter(c, tgt *Node) {
257 c.Parent = n
258 c.Properties.Id = generateUniqueId(c.TagName)
259
260 nodeIndex := -1
261 for i, v := range n.Children {
262 if v.Properties.Id == tgt.Properties.Id {
263 nodeIndex = i
264 break
265 }
266 }
267 if nodeIndex > -1 {
268 n.Children = append(n.Children[:nodeIndex+1], append([]*Node{c}, n.Children[nodeIndex+1:]...)...)
269 } else {
270 n.AppendChild(c)
271 }
272}
273
274func (n *Node) InsertBefore(c, tgt *Node) {
275 c.Parent = n
276 // Set Id
277
278 c.Properties.Id = generateUniqueId(c.TagName)
279
280 nodeIndex := -1
281 for i, v := range n.Children {
282 if v.Properties.Id == tgt.Properties.Id {
283 nodeIndex = i
284 break
285 }
286 }
287 if nodeIndex > 0 {
288 n.Children = append(n.Children[:nodeIndex], append([]*Node{c}, n.Children[nodeIndex:]...)...)
289 // n.Children = append(n.Children, Node{}) // Add a zero value to expand the slice
290 // copy(n.Children[nodeIndex+1:], n.Children[nodeIndex:])
291 // n.Children[nodeIndex] = c
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}