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