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