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