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 "regexp"
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 Properties struct {
42 Id string
43 Computed map[string]float32
44 X float32
45 Y float32
46 Hash string
47 // Width float32
48 // Height float32
49 Border Border
50 EventListeners map[string][]func(Event)
51 EM float32
52 Text Text
53 Focusable bool
54 Focused bool
55 Editable bool
56 Hover bool
57 Selected []float32
58 Test string
59}
60
61type ClassList struct {
62 Classes []string
63 Value string
64}
65
66func (c *ClassList) Add(class string) {
67 c.Classes = append(c.Classes, class)
68 c.Value = strings.Join(c.Classes, " ")
69}
70
71func (c *ClassList) Remove(class string) {
72 for i, v := range c.Classes {
73 if v == class {
74 c.Classes = append(c.Classes[:i], c.Classes[i+1:]...)
75 break
76 }
77 }
78
79 c.Value = strings.Join(c.Classes, " ")
80}
81
82type Border struct {
83 Width string
84 Style string
85 Color ic.RGBA
86 Radius string
87}
88
89type Text struct {
90 Font font.Face
91 Color ic.RGBA
92 Image *image.RGBA
93 Underlined bool
94 Overlined bool
95 LineThrough bool
96 DecorationColor ic.RGBA
97 DecorationThickness int
98 Align string
99 Indent int // very low priority
100 LetterSpacing int
101 LineHeight int
102 WordSpacing int
103 WhiteSpace string
104 Shadows []Shadow // need
105 Width int
106 WordBreak string
107 EM int
108 X int
109 LoadedFont string
110}
111
112type Shadow struct {
113 X int
114 Y int
115 Blur int
116 Color ic.RGBA
117}
118
119func (n *Node) GetAttribute(name string) string {
120 return n.Attribute[name]
121}
122
123func (n *Node) SetAttribute(key, value string) {
124 n.Attribute[key] = value
125}
126
127func (n *Node) CreateElement(name string) Node {
128 return Node{
129 TagName: name,
130 InnerText: "",
131 Children: []Node{},
132 Style: make(map[string]string),
133 Id: "",
134 ClassList: ClassList{
135 Classes: []string{},
136 Value: "",
137 },
138 Href: "",
139 Src: "",
140 Title: "",
141 Attribute: make(map[string]string),
142 Value: "",
143 Properties: Properties{
144 Id: "",
145 X: 0,
146 Y: 0,
147 Hash: "",
148 Computed: make(map[string]float32),
149 // Width: 0,
150 // Height: 0,
151 Border: Border{
152 Width: "0px",
153 Style: "solid",
154 Color: ic.RGBA{
155 R: 0,
156 G: 0,
157 B: 0,
158 A: 0,
159 },
160 Radius: "0px",
161 },
162 EventListeners: make(map[string][]func(Event)),
163 EM: 16,
164 Text: Text{},
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 // Define a regular expression to match numeric digits
243 re := regexp.MustCompile(`\d+`)
244
245 // Find all matches of numeric digits in the input string
246 matches := re.FindAllString(c.Parent.Properties.Id, -1)
247
248 // Concatenate all matches into a single string
249 numericPart := ""
250 for _, match := range matches {
251 numericPart += match
252 }
253
254 c.Properties.Id = c.TagName + numericPart + fmt.Sprint(len(c.Parent.Children))
255
256 n.Children = append(n.Children, c)
257}
258
259func (n *Node) Focus() {
260 if n.Properties.Focusable {
261 n.Properties.Focused = true
262 n.ClassList.Add(":focus")
263 }
264}
265
266func (n *Node) Blur() {
267 if n.Properties.Focusable {
268 n.Properties.Focused = false
269 n.ClassList.Remove(":focus")
270 }
271}
272
273type Event struct {
274 X int
275 Y int
276 KeyCode int
277 Key string
278 CtrlKey bool
279 MetaKey bool
280 ShiftKey bool
281 AltKey bool
282 Click bool
283 ContextMenu bool
284 MouseDown bool
285 MouseUp bool
286 MouseEnter bool
287 MouseLeave bool
288 MouseOver bool
289 KeyUp bool
290 KeyDown bool
291 KeyPress bool
292 Input bool
293 Target Node
294}
295
296type EventList struct {
297 Event Event
298 List []string
299}
300
301func (node *Node) AddEventListener(name string, callback func(Event)) {
302 if node.Properties.EventListeners == nil {
303 node.Properties.EventListeners = make(map[string][]func(Event))
304 }
305 if node.Properties.EventListeners[name] == nil {
306 node.Properties.EventListeners[name] = []func(Event){}
307 }
308 node.Properties.EventListeners[name] = append(node.Properties.EventListeners[name], callback)
309}