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