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("Background: %v\n", self.Background)
165 // fmt.Printf("Border: %v\n\n\n", self.Border)
166}
167
168func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
169
170 // Head is not renderable
171 if utils.IsParent(*n, "head") {
172 return n
173 }
174
175 plugins := c.Plugins
176
177 s := *state
178 self := s[n.Properties.Id]
179 parent := s[n.Parent.Properties.Id]
180
181 self.Background = color.Parse(n.Style, "background")
182 self.Border, _ = CompleteBorder(n.Style, self, parent)
183
184 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
185 self.EM = fs
186
187 if n.Style["display"] == "none" {
188 self.X = 0
189 self.Y = 0
190 self.Width = 0
191 self.Height = 0
192 return n
193 }
194
195 // Set Z index value to be sorted in window
196 if n.Style["z-index"] != "" {
197 z, _ := strconv.Atoi(n.Style["z-index"])
198 self.Z = float32(z)
199 }
200
201 if parent.Z > 0 {
202 self.Z = parent.Z + 1
203 }
204
205 (*state)[n.Properties.Id] = self
206
207 wh := utils.GetWH(*n, state)
208 width := wh.Width
209 height := wh.Height
210
211 x, y := parent.X, parent.Y
212 // !NOTE: Would like to consolidate all XY function into this function like WH
213 offsetX, offsetY := utils.GetXY(n, state)
214 x += offsetX
215 y += offsetY
216
217 var top, left, right, bottom bool = false, false, false, false
218
219 m := utils.GetMP(*n, wh, state, "margin")
220 p := utils.GetMP(*n, wh, state, "padding")
221
222 self.Margin = m
223 self.Padding = p
224
225 if n.Style["position"] == "absolute" {
226 bas := utils.GetPositionOffsetNode(n)
227 base := s[bas.Properties.Id]
228 if n.Style["top"] != "" {
229 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
230 y = v + base.Y
231 top = true
232 }
233 if n.Style["left"] != "" {
234 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
235 x = v + base.X
236 left = true
237 }
238 if n.Style["right"] != "" {
239 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
240 x = (base.Width - width) - v
241 right = true
242 }
243 if n.Style["bottom"] != "" {
244 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
245 y = (base.Height - height) - v
246 bottom = true
247 }
248
249 } else {
250 for i, v := range n.Parent.Children {
251 if v.Style["position"] != "absolute" {
252 if v.Properties.Id == n.Properties.Id {
253 if i-1 > 0 {
254 sib := n.Parent.Children[i-1]
255 sibling := s[sib.Properties.Id]
256 if sib.Style["position"] != "absolute" {
257 if n.Style["display"] == "inline" {
258 if sib.Style["display"] == "inline" {
259 y = sibling.Y
260 } else {
261 y = sibling.Y + sibling.Height
262 }
263 } else {
264 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
265 }
266 }
267
268 }
269 break
270 } else if n.Style["display"] != "inline" {
271 vState := s[v.Properties.Id]
272 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
273 }
274 }
275 }
276 }
277
278 // Display modes need to be calculated here
279
280 relPos := !top && !left && !right && !bottom
281
282 if left || relPos {
283 x += m.Left
284 }
285 if top || relPos {
286 y += m.Top
287 }
288 if right {
289 x -= m.Right
290 }
291 if bottom {
292 y -= m.Bottom
293 }
294
295 self.X = x
296 self.Y = y
297 self.Width = width
298 self.Height = height
299 (*state)[n.Properties.Id] = self
300
301 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
302 // Confirm text exists
303 if len(strings.TrimSpace(n.InnerText)) > 0 {
304 n.InnerText = strings.TrimSpace(n.InnerText)
305 self = genTextNode(n, state)
306 }
307 }
308
309 (*state)[n.Properties.Id] = self
310 (*state)[n.Parent.Properties.Id] = parent
311
312 // Call children here
313 // !TODO: Make something that stops rendering things out of fov
314 fmt.Println("!HER", n.Properties.Id, self.Width, self.Height)
315
316 if self.Y < c.Height {
317 var childYOffset float32
318 for i := 0; i < len(n.Children); i++ {
319 v := n.Children[i]
320 v.Parent = n
321 // This is were the tainting comes from
322 n.Children[i] = *c.ComputeNodeStyle(&v, state)
323
324 cState := (*state)[n.Children[i].Properties.Id]
325 if n.Style["height"] == "" {
326 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
327 childYOffset = cState.Y + cState.Height
328 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
329 self.Height += cState.Margin.Top
330 self.Height += cState.Margin.Bottom
331 self.Height += cState.Padding.Top
332 self.Height += cState.Padding.Bottom
333 }
334 }
335 if cState.Width > self.Width {
336 self.Width = cState.Width
337 }
338 }
339 } else {
340 return n
341 }
342
343 self.Height += self.Padding.Bottom
344
345 (*state)[n.Properties.Id] = self
346
347 // Sorting the array by the Level field
348 sort.Slice(plugins, func(i, j int) bool {
349 return plugins[i].Level < plugins[j].Level
350 })
351
352 for _, v := range plugins {
353 if v.Selector(n) {
354 v.Handler(n, state)
355 }
356 }
357
358 CheckNode(n, state)
359
360 return n
361}
362
363func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
364 // Split the shorthand into components
365 borderComponents := strings.Fields(cssProperties["border"])
366
367 // Ensure there are at least 1 component (width or style or color)
368 if len(borderComponents) >= 1 {
369 width := "0px" // Default width
370 style := "solid"
371 borderColor := "#000000" // Default color
372
373 // Extract style and color if available
374 if len(borderComponents) >= 1 {
375 width = borderComponents[0]
376 }
377
378 // Extract style and color if available
379 if len(borderComponents) >= 2 {
380 style = borderComponents[1]
381 }
382 if len(borderComponents) >= 3 {
383 borderColor = borderComponents[2]
384 }
385
386 parsedColor, _ := color.Color(borderColor)
387
388 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
389
390 return element.Border{
391 Width: w,
392 Style: style,
393 Color: parsedColor,
394 Radius: cssProperties["border-radius"],
395 }, nil
396 }
397
398 return element.Border{}, fmt.Errorf("invalid border shorthand format")
399}
400
401func genTextNode(n *element.Node, state *map[string]element.State) element.State {
402 s := *state
403 self := s[n.Properties.Id]
404 parent := s[n.Parent.Properties.Id]
405
406 text := element.Text{}
407
408 bold, italic := false, false
409
410 if n.Style["font-weight"] == "bold" {
411 bold = true
412 }
413
414 if n.Style["font-style"] == "italic" {
415 italic = true
416 }
417
418 if text.Font == nil {
419 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
420 text.Font = f
421 }
422
423 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
424 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
425 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
426 if lineHeight == 0 {
427 lineHeight = self.EM + 3
428 }
429
430 text.LineHeight = int(lineHeight)
431 text.WordSpacing = int(wordSpacing)
432 text.LetterSpacing = int(letterSpacing)
433 wb := " "
434
435 if n.Style["word-wrap"] == "break-word" {
436 wb = ""
437 }
438
439 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
440 wb = ""
441 }
442
443 var dt float32
444
445 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
446 dt = self.EM / 7
447 } else {
448 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
449 }
450
451 col := color.Parse(n.Style, "font")
452
453 self.Color = col
454
455 text.Color = col
456 text.DecorationColor = color.Parse(n.Style, "decoration")
457 text.Align = n.Style["text-align"]
458 text.WordBreak = wb
459 text.WordSpacing = int(wordSpacing)
460 text.LetterSpacing = int(letterSpacing)
461 text.WhiteSpace = n.Style["white-space"]
462 text.DecorationThickness = int(dt)
463 text.Overlined = n.Style["text-decoration"] == "overline"
464 text.Underlined = n.Style["text-decoration"] == "underline"
465 text.LineThrough = n.Style["text-decoration"] == "linethrough"
466 text.EM = int(self.EM)
467 text.Width = int(parent.Width)
468 text.Text = n.InnerText
469
470 if n.Style["word-spacing"] == "" {
471 text.WordSpacing = font.MeasureSpace(&text)
472 }
473
474 img, width := font.Render(&text)
475 self.Texture = img
476
477 if n.Style["height"] == "" {
478 self.Height = float32(text.LineHeight)
479 }
480
481 if n.Style["width"] == "" {
482 self.Width = float32(width)
483 }
484
485 return self
486}