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