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
18// + could copy the transformers but idk
19type Plugin struct {
20 Selector func(*element.Node) bool
21 Level int
22 Handler func(*element.Node, *map[string]element.State)
23}
24
25type Transformer struct {
26 Selector func(*element.Node) bool
27 Handler func(element.Node, *CSS) element.Node
28}
29
30type CSS struct {
31 Width float32
32 Height float32
33 StyleSheets []map[string]map[string]string
34 Plugins []Plugin
35 Transformers []Transformer
36 Document *element.Node
37}
38
39func (c *CSS) Transform(n element.Node) element.Node {
40 for _, v := range c.Transformers {
41 if v.Selector(&n) {
42 n = v.Handler(n, c)
43 }
44 }
45 for i := 0; i < len(n.Children); i++ {
46 v := n.Children[i]
47 tc := c.Transform(v)
48 n = *tc.Parent
49 n.Children[i] = tc
50 }
51
52 return n
53}
54
55func (c *CSS) StyleSheet(path string) {
56 // Parse the CSS file
57 dat, _ := os.ReadFile(path)
58 styles := parser.ParseCSS(string(dat))
59
60 c.StyleSheets = append(c.StyleSheets, styles)
61}
62
63func (c *CSS) StyleTag(css string) {
64 styles := parser.ParseCSS(css)
65 c.StyleSheets = append(c.StyleSheets, styles)
66}
67
68var inheritedProps = []string{
69 "color",
70 "cursor",
71 "font",
72 "font-family",
73 "font-size",
74 "font-style",
75 "font-weight",
76 "letter-spacing",
77 "line-height",
78 // "text-align",
79 "text-indent",
80 "text-justify",
81 "text-shadow",
82 "text-transform",
83 "text-decoration",
84 "visibility",
85 "word-spacing",
86 "display",
87}
88
89func (c *CSS) GetStyles(n element.Node) map[string]string {
90 styles := map[string]string{}
91
92 if n.Parent != nil {
93 ps := c.GetStyles(*n.Parent)
94 for _, v := range inheritedProps {
95 if ps[v] != "" {
96 styles[v] = ps[v]
97 }
98 }
99 }
100 for k, v := range n.Style {
101 styles[k] = v
102 }
103 hovered := false
104 if slices.Contains(n.ClassList.Classes, ":hover") {
105 hovered = true
106 }
107
108 for _, styleSheet := range c.StyleSheets {
109 for selector := range styleSheet {
110 // fmt.Println(selector, n.Properties.Id)
111 key := selector
112 if strings.Contains(selector, ":hover") && hovered {
113 selector = strings.Replace(selector, ":hover", "", -1)
114 }
115 if element.TestSelector(selector, &n) {
116 for k, v := range styleSheet[key] {
117 styles[k] = v
118 }
119 }
120
121 }
122 }
123
124 // This is different than node.Style
125 // temp1 = <span style=​"color:​#a6e22e">​CSS​</span>​
126 // temp1.style == CSSStyleDeclaration {0: 'color', accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
127 // temp1.getAttribute("style") == 'color:#a6e22e'
128 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
129 styles = utils.Merge(styles, inline)
130 // add hover and focus css events
131
132 if n.Parent != nil {
133 if styles["z-index"] == "" && n.Parent.Style["z-index"] != "" {
134 z, _ := strconv.Atoi(n.Parent.Style["z-index"])
135 z += 1
136 styles["z-index"] = strconv.Itoa(z)
137 }
138 }
139
140 return styles
141}
142
143func (c *CSS) AddPlugin(plugin Plugin) {
144 c.Plugins = append(c.Plugins, plugin)
145}
146
147func (c *CSS) AddTransformer(transformer Transformer) {
148 c.Transformers = append(c.Transformers, transformer)
149}
150
151func CheckNode(n *element.Node, state *map[string]element.State) {
152 s := *state
153 self := s[n.Properties.Id]
154
155 fmt.Println(n.TagName, n.Properties.Id)
156 fmt.Printf("ID: %v\n", n.Id)
157 fmt.Printf("EM: %v\n", self.EM)
158 fmt.Printf("Parent: %v\n", n.Parent.TagName)
159 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
160 fmt.Printf("Text: %v\n", n.InnerText)
161 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
162 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
163 fmt.Printf("Styles: %v\n", n.Style)
164 fmt.Printf("Margin: %v\n", self.Margin)
165 fmt.Printf("Padding: %v\n", self.Padding)
166 // fmt.Printf("Background: %v\n", self.Background)
167 // fmt.Printf("Border: %v\n\n\n", self.Border)
168}
169
170func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
171
172 // Head is not renderable
173 if utils.IsParent(*n, "head") {
174 return n
175 }
176
177 plugins := c.Plugins
178
179 s := *state
180 self := s[n.Properties.Id]
181 parent := s[n.Parent.Properties.Id]
182
183 self.Background = color.Parse(n.Style, "background")
184 self.Border, _ = CompleteBorder(n.Style, self, parent)
185
186 fs := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
187 self.EM = fs
188
189 if n.Style["display"] == "none" {
190 self.X = 0
191 self.Y = 0
192 self.Width = 0
193 self.Height = 0
194 return n
195 }
196
197 // Set Z index value to be sorted in window
198 if n.Style["z-index"] != "" {
199 z, _ := strconv.Atoi(n.Style["z-index"])
200 self.Z = float32(z)
201 }
202
203 if parent.Z > 0 {
204 self.Z = parent.Z + 1
205 }
206
207 (*state)[n.Properties.Id] = self
208
209 wh := utils.GetWH(*n, state)
210 width := wh.Width
211 height := wh.Height
212
213 x, y := parent.X, parent.Y
214 // !NOTE: Would like to consolidate all XY function into this function like WH
215 offsetX, offsetY := utils.GetXY(n, state)
216 x += offsetX
217 y += offsetY
218
219 var top, left, right, bottom bool = false, false, false, false
220
221 m := utils.GetMP(*n, wh, state, "margin")
222 p := utils.GetMP(*n, wh, state, "padding")
223
224 self.Margin = m
225 self.Padding = p
226
227 if n.Style["position"] == "absolute" {
228 bas := utils.GetPositionOffsetNode(n)
229 base := s[bas.Properties.Id]
230 if n.Style["top"] != "" {
231 v := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
232 y = v + base.Y
233 top = true
234 }
235 if n.Style["left"] != "" {
236 v := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
237 x = v + base.X
238 left = true
239 }
240 if n.Style["right"] != "" {
241 v := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
242 x = (base.Width - width) - v
243 right = true
244 }
245 if n.Style["bottom"] != "" {
246 v := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
247 y = (base.Height - height) - v
248 bottom = true
249 }
250
251 } else {
252 for i, v := range n.Parent.Children {
253 if v.Style["position"] != "absolute" {
254 if v.Properties.Id == n.Properties.Id {
255 if i-1 > -1 {
256 sib := n.Parent.Children[i-1]
257 sibling := s[sib.Properties.Id]
258 if sib.Style["position"] != "absolute" {
259 if n.Style["display"] == "inline" {
260 if sib.Style["display"] == "inline" {
261 y = sibling.Y
262 } else {
263 y = sibling.Y + sibling.Height
264 }
265 } else {
266 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
267 }
268 }
269
270 }
271 break
272 } else if n.Style["display"] != "inline" {
273 vState := s[v.Properties.Id]
274 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
275 }
276 }
277 }
278 }
279
280 // Display modes need to be calculated here
281
282 relPos := !top && !left && !right && !bottom
283
284 if left || relPos {
285 x += m.Left
286 }
287 if top || relPos {
288 y += m.Top
289 }
290 if right {
291 x -= m.Right
292 }
293 if bottom {
294 y -= m.Bottom
295 }
296
297 self.X = x
298 self.Y = y
299 self.Width = width
300 self.Height = height
301 (*state)[n.Properties.Id] = self
302
303 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
304 // Confirm text exists
305 n.InnerText = strings.TrimSpace(n.InnerText)
306 self = genTextNode(n, state)
307 }
308
309 (*state)[n.Properties.Id] = self
310 (*state)[n.Parent.Properties.Id] = parent
311
312 // Call children here
313
314 // Check to see if node is in fov
315 // if self.Y < c.Height {
316 var childYOffset float32
317 for i := 0; i < len(n.Children); i++ {
318 v := n.Children[i]
319 v.Parent = n
320 // This is were the tainting comes from
321 n.Children[i] = *c.ComputeNodeStyle(&v, state)
322
323 cState := (*state)[n.Children[i].Properties.Id]
324 if n.Style["height"] == "" {
325 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
326 childYOffset = cState.Y + cState.Height
327 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
328 self.Height += cState.Margin.Top
329 self.Height += cState.Margin.Bottom
330 self.Height += cState.Padding.Top
331 self.Height += cState.Padding.Bottom
332 }
333 }
334 if cState.Width > self.Width {
335 self.Width = cState.Width
336 }
337 }
338 // } else {
339 // return n
340 // }
341
342 self.Height += self.Padding.Bottom
343
344 (*state)[n.Properties.Id] = self
345
346 // Sorting the array by the Level field
347 sort.Slice(plugins, func(i, j int) bool {
348 return plugins[i].Level < plugins[j].Level
349 })
350
351 for _, v := range plugins {
352 if v.Selector(n) {
353 v.Handler(n, state)
354 }
355 }
356
357 // CheckNode(n, state)
358
359 return n
360}
361
362func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
363 // Split the shorthand into components
364 borderComponents := strings.Fields(cssProperties["border"])
365
366 // Ensure there are at least 1 component (width or style or color)
367 if len(borderComponents) >= 1 {
368 width := "0px" // Default width
369 style := "solid"
370 borderColor := "#000000" // Default color
371
372 // Extract style and color if available
373 if len(borderComponents) >= 1 {
374 width = borderComponents[0]
375 }
376
377 // Extract style and color if available
378 if len(borderComponents) >= 2 {
379 style = borderComponents[1]
380 }
381 if len(borderComponents) >= 3 {
382 borderColor = borderComponents[2]
383 }
384
385 parsedColor, _ := color.Color(borderColor)
386
387 w := utils.ConvertToPixels(width, self.EM, parent.Width)
388
389 return element.Border{
390 Width: w,
391 Style: style,
392 Color: parsedColor,
393 Radius: cssProperties["border-radius"],
394 }, nil
395 }
396
397 return element.Border{}, fmt.Errorf("invalid border shorthand format")
398}
399
400func genTextNode(n *element.Node, state *map[string]element.State) element.State {
401 s := *state
402 self := s[n.Properties.Id]
403 parent := s[n.Parent.Properties.Id]
404
405 text := element.Text{}
406
407 bold, italic := false, false
408
409 if n.Style["font-weight"] == "bold" {
410 bold = true
411 }
412
413 if n.Style["font-style"] == "italic" {
414 italic = true
415 }
416
417 if text.Font == nil {
418 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
419 text.Font = f
420 }
421
422 letterSpacing := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
423 wordSpacing := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
424 lineHeight := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
425 if lineHeight == 0 {
426 lineHeight = self.EM + 3
427 }
428
429 text.LineHeight = int(lineHeight)
430 text.WordSpacing = int(wordSpacing)
431 text.LetterSpacing = int(letterSpacing)
432 wb := " "
433
434 if n.Style["word-wrap"] == "break-word" {
435 wb = ""
436 }
437
438 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
439 wb = ""
440 }
441
442 var dt float32
443
444 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
445 dt = self.EM / 7
446 } else {
447 dt = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
448 }
449
450 col := color.Parse(n.Style, "font")
451
452 self.Color = col
453
454 text.Color = col
455 text.DecorationColor = color.Parse(n.Style, "decoration")
456 text.Align = n.Style["text-align"]
457 text.WordBreak = wb
458 text.WordSpacing = int(wordSpacing)
459 text.LetterSpacing = int(letterSpacing)
460 text.WhiteSpace = n.Style["white-space"]
461 text.DecorationThickness = int(dt)
462 text.Overlined = n.Style["text-decoration"] == "overline"
463 text.Underlined = n.Style["text-decoration"] == "underline"
464 text.LineThrough = n.Style["text-decoration"] == "linethrough"
465 text.EM = int(self.EM)
466 text.Width = int(parent.Width)
467 text.Text = n.InnerText
468
469 if n.Style["word-spacing"] == "" {
470 text.WordSpacing = font.MeasureSpace(&text)
471 }
472
473 img, width := font.Render(&text)
474 self.Texture = img
475
476 if n.Style["height"] == "" {
477 self.Height = float32(text.LineHeight)
478 }
479
480 if n.Style["width"] == "" {
481 self.Width = float32(width)
482 }
483
484 return self
485}