Parser
Parser is the CSS parser for this project, it is made up of two primary functions ParseCSS
, ParseStyleAttribute
, and a few other functions designed to help with the parsing.
# ParseCSS?(go)
ParseCSS
is the function for reading CSS files. It is a RegExp based parser which it converts CSS definitions into a 2d map of strings. If you want to convert the values into absoulte values there are helper functions in the utils documentation.
matches := selectorRegex.FindAllStringSubmatch(css, -1)
First we start off by using a RegExp to find the individual CSS blocks and to sort them into the block selector and the styles for the selector.
selectors := parseSelectors(selectorBlock)
The mapped values are defined by the selector pulled from the parseSelectors function and will include the entire name (this includes the symbol ".","#", and ",")
selectorMap[selector] = parseStyles(styleBlock)
Once the selectors of the file have been parsed, the styles are mapped to the second level of the map with their respective key and value pulled from parseStyles.
NOTE: When parsing duplicate selectors and styles will be merged with the last conflicting selector/style overriding the prevous.
# Implementation
styles := parser.ParseCSS(string(dat))
The only time ParseCSS
is used is in the cstyle
package, and it used to add css files in the first example
styles := parser.ParseCSS(css)
and style tags in the next. As you can see in both examples those functions are for appending the new styles to the current global CSS stylesheet held within the instance of the CSS struct (CSS.StyleSheets
).
NOTE: Style tag is refering to the below
1<style>
2 table td.r,
3 table th.r {
4 text-align: center;
5 }
6</style>
# parseSelectors?(go)
parseSelectors
takes the first output of the RegExp match in ParseCSS and splits it up by commas.
# parseSelectors Example
1
2selectorBlock := `table td.r,
3table th.r`
4
5parseSelectors(selectorBlock)
6
7// Output
8[table td.r table th.r]
9
# parseStyles?(go)
styleRegex := regexp.MustCompile
parseStyles
takes the second output of the RegExp match in ParseCSS and splits it up using this RegExp:
styleMap := make(map[string]string) for _, match := range matches { propName := strings.TrimSpace(match[1]) propValue := strings.TrimSpace(match[2]) styleMap[propName] = propValue }
It then takes the split styles and inserts them into a map[string]string
.
# parseStyles Example
1
2selectorBlock := `text-align: center;
3color: red;`
4
5parseStyles(selectorBlock)
6
7// Output
8map[string]string=map[text-align:center color:red]
9
# ParseStyleAttribute?(go)
inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
ParseStyleAttribute
is for parsing inline styles from elements in the html document on the inital load. It is also used to parse the local styles applied by the "script" via the .style
attribute. It will only be applied to a element.Node
's local styles and will not be add to the global stylesheets. It is used with the cstyle.GetStyles
function that is ran on every cycle.
# ParseStyleAttribute Example
1
2styleAttribute := "color:#f8f8f2;background-color:#272822;"
3
4ParseStyleAttribute(styleAttribute)
5
6//Output
7map[string]string=map[color:#f8f8f2 background-color:#272822]
8
# removeComments?(go)
1package parser
2
3import (
4 "regexp"
5 "strings"
6)
7
8func ParseCSS(css string) map[string]map[string]string {
9 selectorMap := make(map[string]map[string]string)
10
11 // Remove comments
12 css = removeComments(css)
13
14 // Parse regular selectors and styles
15 selectorRegex := regexp.MustCompile(`([^{]+){([^}]+)}`)
16 matches := selectorRegex.FindAllStringSubmatch(css, -1)
17
18 for _, match := range matches {
19 selectorBlock := strings.TrimSpace(match[1])
20 styleBlock := match[2]
21
22 selectors := parseSelectors(selectorBlock)
23 for _, selector := range selectors {
24 selectorMap[selector] = parseStyles(styleBlock)
25 }
26 }
27
28 return selectorMap
29}
30
31func parseSelectors(selectorBlock string) []string {
32 // Split by comma and trim each selector
33 selectors := strings.Split(selectorBlock, ",")
34 for i, selector := range selectors {
35 selectors[i] = strings.TrimSpace(selector)
36 }
37 return selectors
38}
39
40func parseStyles(styleBlock string) map[string]string {
41 styleRegex := regexp.MustCompile(`([a-zA-Z-]+)\s*:\s*([^;]+);`)
42 matches := styleRegex.FindAllStringSubmatch(styleBlock, -1)
43
44 styleMap := make(map[string]string)
45 for _, match := range matches {
46 propName := strings.TrimSpace(match[1])
47 propValue := strings.TrimSpace(match[2])
48 styleMap[propName] = propValue
49 }
50
51 return styleMap
52}
53
54func ParseStyleAttribute(styleValue string) map[string]string {
55 styleMap := make(map[string]string)
56
57 // Regular expression to match key-value pairs in the style attribute
58 re := regexp.MustCompile(`\s*([\w-]+)\s*:\s*([^;]+)\s*;`)
59
60 // Find all matches in the style attribute value
61 matches := re.FindAllStringSubmatch(styleValue, -1)
62
63 // Populate the map with key-value pairs
64 for _, match := range matches {
65 if len(match) == 3 {
66 key := strings.TrimSpace(match[1])
67 value := strings.TrimSpace(match[2])
68 styleMap[key] = value
69 }
70 }
71
72 return styleMap
73}
74
75func removeComments(css string) string {
76 commentRegex := regexp.MustCompile(`(?s)/\*.*?\*/`)
77 return commentRegex.ReplaceAllString(css, "")
78}
1package cstyle
2
3import (
4 "fmt"
5 "gui/color"
6 "gui/element"
7 "gui/font"
8 "gui/parser"
9 "gui/utils"
10 "os"
11 "slices"
12 "sort"
13 "strconv"
14 "strings"
15)
16
17// !TODO: Make a fine selector to target tags and if it has children or not etc
18type Plugin struct {
19 Styles map[string]string
20 Level int
21 Handler func(*element.Node, *map[string]element.State)
22}
23
24type CSS struct {
25 Width float32
26 Height float32
27 StyleSheets []map[string]map[string]string
28 Plugins []Plugin
29 Document *element.Node
30}
31
32func (c *CSS) StyleSheet(path string) {
33 // Parse the CSS file
34 dat, err := os.ReadFile(path)
35 utils.Check(err)
36 styles := parser.ParseCSS(string(dat))
37
38 c.StyleSheets = append(c.StyleSheets, styles)
39}
40
41func (c *CSS) StyleTag(css string) {
42 styles := parser.ParseCSS(css)
43 c.StyleSheets = append(c.StyleSheets, styles)
44}
45
46var inheritedProps = []string{
47 "color",
48 "cursor",
49 "font",
50 "font-family",
51 "font-size",
52 "font-style",
53 "font-weight",
54 "letter-spacing",
55 "line-height",
56 // "text-align",
57 "text-indent",
58 "text-justify",
59 "text-shadow",
60 "text-transform",
61 "text-decoration",
62 "visibility",
63 "word-spacing",
64 "display",
65}
66
67func (c *CSS) GetStyles(n element.Node) map[string]string {
68 styles := map[string]string{}
69
70 if n.Parent != nil {
71 ps := c.GetStyles(*n.Parent)
72 for _, v := range inheritedProps {
73 if ps[v] != "" {
74 styles[v] = ps[v]
75 }
76 }
77 }
78 for k, v := range n.Style {
79 styles[k] = v
80 }
81 hovered := false
82 if slices.Contains(n.ClassList.Classes, ":hover") {
83 hovered = true
84 }
85
86 for _, styleSheet := range c.StyleSheets {
87 for selector := range styleSheet {
88 // fmt.Println(selector, n.Properties.Id)
89 key := selector
90 if strings.Contains(selector, ":hover") && hovered {
91 selector = strings.Replace(selector, ":hover", "", -1)
92 }
93 if element.TestSelector(selector, &n) {
94 for k, v := range styleSheet[key] {
95 styles[k] = v
96 }
97 }
98
99 }
100 }
101
102 // This is different than node.Style
103 // temp1 = <span style=​"color:​#a6e22e">​CSS​</span>​
104 // temp1.style == CSSStyleDeclaration {0: 'color', accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
105 // temp1.getAttribute("style") == 'color:#a6e22e'
106 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
107 styles = utils.Merge(styles, inline)
108 // add hover and focus css events
109
110 if n.Parent != nil {
111 if styles["z-index"] == "" && n.Parent.Style["z-index"] != "" {
112 z, _ := strconv.Atoi(n.Parent.Style["z-index"])
113 z += 1
114 styles["z-index"] = strconv.Itoa(z)
115 }
116 }
117
118 return styles
119}
120
121func (c *CSS) AddPlugin(plugin Plugin) {
122 c.Plugins = append(c.Plugins, plugin)
123}
124
125func CheckNode(n *element.Node, state *map[string]element.State) {
126 s := *state
127 self := s[n.Properties.Id]
128
129 fmt.Println(n.TagName, n.Properties.Id)
130 fmt.Printf("ID: %v\n", n.Id)
131 fmt.Printf("EM: %v\n", self.EM)
132 fmt.Printf("Parent: %v\n", n.Parent.TagName)
133 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
134 fmt.Printf("Text: %v\n", n.InnerText)
135 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
136 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
137 fmt.Printf("Styles: %v\n", self.Style)
138 fmt.Printf("Background: %v\n", self.Background)
139 fmt.Printf("Border: %v\n\n\n", self.Border)
140}
141
142func (c *CSS) ComputeNodeStyle(node *element.Node, state *map[string]element.State) *element.Node {
143
144 // Head is not renderable
145 if utils.IsParent(*node, "head") {
146 return node
147 }
148
149 // !TODO: Make a plugin type system that can rewrite nodes and matches by more than just tagname
150 // + should be ran here once a node is loaded
151 plugins := c.Plugins
152
153 s := *state
154 self := s[node.Properties.Id]
155 parent := s[node.Parent.Properties.Id]
156
157 var n *element.Node
158
159 // !ISSUE: For some reason node is still being tainted
160 // + if the user changes the innerText of the swap parent then how does the swap get updated????
161 // + in theory it should be invalided when the main invalidator runs
162 if self.Swap.Properties.Id != "" {
163 n = &self.Swap
164 // fmt.Println("Swapped: ", n.Properties.Id, n.InnerText)
165 // CheckNode(node, state)
166 // CheckNode(&self.Swap, state)
167 } else {
168 n = node
169 // fmt.Println("Back: ", n.Properties.Id, n.InnerText)
170 self.Style = c.GetStyles(*n)
171 }
172
173 self.Background = color.Parse(self.Style, "background")
174 self.Border, _ = CompleteBorder(self.Style, self, parent)
175
176 fs, _ := utils.ConvertToPixels(self.Style["font-size"], parent.EM, parent.Width)
177 self.EM = fs
178
179 if self.Style["display"] == "none" {
180 self.X = 0
181 self.Y = 0
182 self.Width = 0
183 self.Height = 0
184 return n
185 }
186
187 if self.Style["width"] == "" && self.Style["display"] == "block" {
188 self.Style["width"] = "100%"
189 }
190
191 // Set Z index value to be sorted in window
192 if self.Style["z-index"] != "" {
193 z, _ := strconv.Atoi(self.Style["z-index"])
194 self.Z = float32(z)
195 }
196
197 if parent.Z > 0 {
198 self.Z = parent.Z + 1
199 }
200
201 (*state)[n.Properties.Id] = self
202
203 wh := utils.GetWH(*n, state)
204 width := wh.Width
205 height := wh.Height
206
207 x, y := parent.X, parent.Y
208 // !NOTE: Would like to consolidate all XY function into this function like WH
209 offsetX, offsetY := utils.GetXY(n, state)
210 x += offsetX
211 y += offsetY
212
213 var top, left, right, bottom bool = false, false, false, false
214
215 m := utils.GetMP(*n, wh, state, "margin")
216 p := utils.GetMP(*n, wh, state, "padding")
217
218 self.Margin = m
219 self.Padding = p
220
221 if self.Style["position"] == "absolute" {
222 bas := utils.GetPositionOffsetNode(n, state)
223 base := s[bas.Properties.Id]
224 if self.Style["top"] != "" {
225 v, _ := utils.ConvertToPixels(self.Style["top"], self.EM, parent.Width)
226 y = v + base.Y
227 top = true
228 }
229 if self.Style["left"] != "" {
230 v, _ := utils.ConvertToPixels(self.Style["left"], self.EM, parent.Width)
231 x = v + base.X
232 left = true
233 }
234 if self.Style["right"] != "" {
235 v, _ := utils.ConvertToPixels(self.Style["right"], self.EM, parent.Width)
236 x = (base.Width - width) - v
237 right = true
238 }
239 if self.Style["bottom"] != "" {
240 v, _ := utils.ConvertToPixels(self.Style["bottom"], self.EM, parent.Width)
241 y = (base.Height - height) - v
242 bottom = true
243 }
244
245 } else {
246 for i, v := range n.Parent.Children {
247 vState := s[v.Properties.Id]
248 if vState.Style["position"] != "absolute" {
249 if v.Properties.Id == n.Properties.Id {
250 if i-1 > 0 {
251 sib := n.Parent.Children[i-1]
252 sibling := s[sib.Properties.Id]
253 if sibling.Style["position"] != "absolute" {
254 if self.Style["display"] == "inline" {
255 if sibling.Style["display"] == "inline" {
256 y = sibling.Y
257 } else {
258 y = sibling.Y + sibling.Height
259 }
260 } else {
261 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
262 }
263 }
264
265 }
266 break
267 } else if self.Style["display"] != "inline" {
268 vState := s[v.Properties.Id]
269 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
270 }
271 }
272 }
273 }
274
275 // Display modes need to be calculated here
276
277 relPos := !top && !left && !right && !bottom
278
279 if left || relPos {
280 x += m.Left
281 }
282 if top || relPos {
283 y += m.Top
284 }
285 if right {
286 x -= m.Right
287 }
288 if bottom {
289 y -= m.Bottom
290 }
291
292 self.X = x
293 self.Y = y
294 self.Width = width
295 self.Height = height
296 (*state)[n.Properties.Id] = self
297
298 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
299 // Confirm text exists
300 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
301 if len(words) != 1 {
302 // !ISSUE: Still doesn't work great
303 if self.Swap.Properties.Id == "" {
304 self.Swap = *n
305 n = &self.Swap
306 n.Style["inlineText"] = "true"
307 }
308 if self.Style["display"] == "inline" {
309 n.InnerText = words[0]
310 n.Style["inlineText"] = "true"
311 el := *n
312 el.InnerText = strings.Join(words[1:], " ")
313 n.Parent.InsertAfter(el, *n)
314 } else {
315 el := n.CreateElement("notaspan")
316 el.InnerText = n.InnerText
317 n.AppendChild(el)
318 self.Style["font-size"] = parent.Style["font-size"]
319 self.EM = parent.EM
320 n.InnerText = ""
321 }
322 (*state)[n.Properties.Id] = self
323 }
324 if len(strings.TrimSpace(n.InnerText)) > 0 {
325 n.InnerText = strings.TrimSpace(n.InnerText)
326 self = genTextNode(n, state)
327 }
328 }
329
330 (*state)[n.Properties.Id] = self
331 (*state)[n.Parent.Properties.Id] = parent
332
333 // Call children here
334
335 var childYOffset float32
336 for i := 0; i < len(n.Children); i++ {
337 v := n.Children[i]
338 v.Parent = n
339 // This is were the tainting comes from
340 n.Children[i] = *c.ComputeNodeStyle(&v, state)
341
342 cState := (*state)[n.Children[i].Properties.Id]
343 if self.Style["height"] == "" {
344 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
345 childYOffset = cState.Y + cState.Height
346 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
347 self.Height += cState.Margin.Top
348 self.Height += cState.Margin.Bottom
349 self.Height += cState.Padding.Top
350 self.Height += cState.Padding.Bottom
351 }
352 }
353 if cState.Width > self.Width {
354 self.Width = cState.Width
355 }
356 }
357
358 self.Height += self.Padding.Bottom
359
360 (*state)[n.Properties.Id] = self
361
362 // Sorting the array by the Level field
363 sort.Slice(plugins, func(i, j int) bool {
364 return plugins[i].Level < plugins[j].Level
365 })
366
367 for _, v := range plugins {
368 matches := true
369 for name, value := range v.Styles {
370 if self.Style[name] != value && !(value == "*") && self.Style[name] != "" {
371 matches = false
372 }
373 }
374 if matches {
375 // !NOTE: Might save memory by making a state map tree and passing that instead of the node it's self
376 v.Handler(n, state)
377 }
378 }
379
380 // !IMPORTAINT: Tomorrow the way textt should work is all free standing text should be in text elements, then the words should be notaspan
381 // + so in theory the inner/outerhtml methods can clean those and after the notspans are rendered (I don't know if removing them will do the same)
382 // + (thing as below) but the text should be a text element so it shows childNodes bc just children is repetitive
383 // n.InnerHTML = utils.InnerHTML(*n)
384 // tag, closing := utils.NodeToHTML(*n)
385 // n.OuterHTML = tag + n.InnerHTML + closing
386
387 // !NOTE: I think that .Children can just act like .childNodes but the text needs to be joined into one "text" node for each line
388 // + So it is ok to modifey the DOM but only to make text nodes and do the innerHTML
389 // + also I think innerHTML should be the main source of truth, but if innerHTML == "" then generate the html and if it changes update the node
390 // + but if the DOM under it changes then you would need to update it aswell
391
392 CheckNode(n, state)
393 // toRemove := make([]int, 0)
394 // for i := len(n.Children) - 1; i >= 1; i-- {
395 // v := n.Children[i]
396 // next := n.Children[i-1]
397 // if v.TagName == next.TagName {
398 // matches := true
399 // for k, t := range v.Style {
400 // if next.Style[k] != t {
401 // matches = false
402 // }
403 // }
404 // if matches {
405 // // fmt.Println(n.Properties.Id)
406 // n.Children[i-1].InnerText = n.Children[i-1].InnerText + " " + v.InnerText
407
408 // toRemove = append(toRemove, i)
409 // }
410 // }
411 // }
412 // for _, index := range toRemove {
413 // n.Children = append(n.Children[:index], n.Children[index+1:]...)
414 // }
415 // if len(toRemove) > 0 {
416 // for _, v := range n.Children {
417 // fmt.Println(v.InnerText)
418 // }
419 // }
420
421 return n
422}
423
424func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
425 // Split the shorthand into components
426 borderComponents := strings.Fields(cssProperties["border"])
427
428 // Ensure there are at least 1 component (width or style or color)
429 if len(borderComponents) >= 1 {
430 width := "0px" // Default width
431 style := "solid"
432 borderColor := "#000000" // Default color
433
434 // Extract style and color if available
435 if len(borderComponents) >= 1 {
436 width = borderComponents[0]
437 }
438
439 // Extract style and color if available
440 if len(borderComponents) >= 2 {
441 style = borderComponents[1]
442 }
443 if len(borderComponents) >= 3 {
444 borderColor = borderComponents[2]
445 }
446
447 parsedColor, _ := color.Color(borderColor)
448
449 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
450
451 return element.Border{
452 Width: w,
453 Style: style,
454 Color: parsedColor,
455 Radius: cssProperties["border-radius"],
456 }, nil
457 }
458
459 return element.Border{}, fmt.Errorf("invalid border shorthand format")
460}
461
462func genTextNode(n *element.Node, state *map[string]element.State) element.State {
463 s := *state
464 self := s[n.Properties.Id]
465 parent := s[n.Parent.Properties.Id]
466
467 text := element.Text{}
468
469 bold, italic := false, false
470
471 if self.Style["font-weight"] == "bold" {
472 bold = true
473 }
474
475 if self.Style["font-style"] == "italic" {
476 italic = true
477 }
478
479 if text.Font == nil {
480 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
481 text.Font = f
482 }
483
484 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
485 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
486 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
487 if lineHeight == 0 {
488 lineHeight = self.EM + 3
489 }
490
491 text.LineHeight = int(lineHeight)
492 text.WordSpacing = int(wordSpacing)
493 text.LetterSpacing = int(letterSpacing)
494 wb := " "
495
496 if self.Style["word-wrap"] == "break-word" {
497 wb = ""
498 }
499
500 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
501 wb = ""
502 }
503
504 var dt float32
505
506 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
507 dt = self.EM / 7
508 } else {
509 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
510 }
511
512 col := color.Parse(self.Style, "font")
513
514 self.Color = col
515
516 text.Color = col
517 text.DecorationColor = color.Parse(self.Style, "decoration")
518 text.Align = self.Style["text-align"]
519 text.WordBreak = wb
520 text.WordSpacing = int(wordSpacing)
521 text.LetterSpacing = int(letterSpacing)
522 text.WhiteSpace = self.Style["white-space"]
523 text.DecorationThickness = int(dt)
524 text.Overlined = self.Style["text-decoration"] == "overline"
525 text.Underlined = self.Style["text-decoration"] == "underline"
526 text.LineThrough = self.Style["text-decoration"] == "linethrough"
527 text.EM = int(self.EM)
528 text.Width = int(parent.Width)
529 text.Text = n.InnerText
530
531 if self.Style["word-spacing"] == "" {
532 text.WordSpacing = font.MeasureSpace(&text)
533 }
534
535 img, width := font.Render(&text)
536 self.Texture = img
537
538 if self.Style["height"] == "" {
539 self.Height = float32(text.LineHeight)
540 }
541
542 if self.Style["width"] == "" {
543 self.Width = float32(width)
544 }
545
546 return self
547}