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