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 > 0 {
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 break
304 } else if n.Style["display"] != "inline" {
305 vState := s[v.Properties.Id]
306 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
307 }
308 }
309 }
310 }
311
312 // Display modes need to be calculated here
313
314 relPos := !top && !left && !right && !bottom
315
316 if left || relPos {
317 x += m.Left
318 }
319 if top || relPos {
320 y += m.Top
321 }
322 if right {
323 x -= m.Right
324 }
325 if bottom {
326 y -= m.Bottom
327 }
328
329 self.X = x
330 self.Y = y
331 self.Width = width
332 self.Height = height
333 (*state)[n.Properties.Id] = self
334
335 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
336 // Confirm text exists
337 n.InnerText = strings.TrimSpace(n.InnerText)
338 self = genTextNode(n, state, c)
339 }
340
341 (*state)[n.Properties.Id] = self
342 (*state)[n.Parent.Properties.Id] = parent
343 // Call children here
344
345 var childYOffset float32
346 for i := 0; i < len(n.Children); i++ {
347 v := n.Children[i]
348 v.Parent = n
349 // This is were the tainting comes from
350 n.Children[i] = *c.ComputeNodeStyle(&v, state)
351
352 cState := (*state)[n.Children[i].Properties.Id]
353 if n.Style["height"] == "" && n.Style["min-height"] == "" {
354 if v.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
355 childYOffset = cState.Y + cState.Height
356 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
357 self.Height += cState.Margin.Top
358 self.Height += cState.Margin.Bottom
359 self.Height += cState.Padding.Top
360 self.Height += cState.Padding.Bottom
361 self.Height += cState.Border.Width * 2
362 }
363 }
364 if cState.Width > self.Width {
365 self.Width = cState.Width
366 }
367 }
368
369 if n.Style["height"] == "" {
370 self.Height += self.Padding.Bottom
371 }
372
373 (*state)[n.Properties.Id] = self
374
375 // Sorting the array by the Level field
376 sort.Slice(plugins, func(i, j int) bool {
377 return plugins[i].Level < plugins[j].Level
378 })
379
380 for _, v := range plugins {
381 if v.Selector(n) {
382 v.Handler(n, state)
383 }
384 }
385
386 // CheckNode(n, state)
387 return n
388}
389
390func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
391 // Split the shorthand into components
392 borderComponents := strings.Fields(cssProperties["border"])
393
394 // Default values
395 width := "0px" // Default width
396 style := "solid"
397 borderColor := "#000000" // Default color
398
399 // Suffixes for width properties
400 widthSuffixes := []string{"px", "em", "pt", "pc", "%", "vw", "vh", "cm", "in"}
401
402 // Identify each component regardless of order
403 for _, component := range borderComponents {
404 if isWidthComponent(component, widthSuffixes) {
405 width = component
406 } else {
407 switch component {
408 case "thin", "medium", "thick":
409 width = component
410 case "none", "hidden", "dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset":
411 style = component
412 default:
413 // Handle colors
414 borderColor = component
415 }
416 }
417 }
418
419 parsedColor, _ := color.Color(borderColor)
420 w := utils.ConvertToPixels(width, self.EM, parent.Width)
421
422 return element.Border{
423 Width: w,
424 Style: style,
425 Color: parsedColor,
426 Radius: cssProperties["border-radius"],
427 }, nil
428}
429
430// Helper function to determine if a component is a width value
431func isWidthComponent(component string, suffixes []string) bool {
432 for _, suffix := range suffixes {
433 if strings.HasSuffix(component, suffix) {
434 return true
435 }
436 }
437 return false
438}
439
440func genTextNode(n *element.Node, state *map[string]element.State, css *CSS) element.State {
441 s := *state
442 self := s[n.Properties.Id]
443 parent := s[n.Parent.Properties.Id]
444
445 text := element.Text{}
446
447 bold, italic := false, false
448 // !ISSUE: needs bolder and the 100 -> 900
449 if n.Style["font-weight"] == "bold" {
450 bold = true
451 }
452
453 if n.Style["font-style"] == "italic" {
454 italic = true
455 }
456
457 if text.Font == nil {
458 if css.Fonts == nil {
459 css.Fonts = map[string]imgFont.Face{}
460 }
461 fid := n.Style["font-family"] + fmt.Sprint(self.EM, bold, italic)
462 if css.Fonts[fid] == nil {
463 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
464 css.Fonts[fid] = f
465 }
466 fnt := css.Fonts[fid]
467 text.Font = &fnt
468 }
469
470 letterSpacing := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
471 wordSpacing := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
472 lineHeight := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
473 if lineHeight == 0 {
474 lineHeight = self.EM + 3
475 }
476
477 text.LineHeight = int(lineHeight)
478 text.WordSpacing = int(wordSpacing)
479 text.LetterSpacing = int(letterSpacing)
480 wb := " "
481
482 if n.Style["word-wrap"] == "break-word" {
483 wb = ""
484 }
485
486 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
487 wb = ""
488 }
489
490 var dt float32
491
492 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
493 dt = self.EM / 7
494 } else {
495 dt = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
496 }
497
498 col := color.Parse(n.Style, "font")
499
500 self.Color = col
501
502 text.Color = col
503 text.DecorationColor = color.Parse(n.Style, "decoration")
504 text.Align = n.Style["text-align"]
505 text.WordBreak = wb
506 text.WordSpacing = int(wordSpacing)
507 text.LetterSpacing = int(letterSpacing)
508 text.WhiteSpace = n.Style["white-space"]
509 text.DecorationThickness = int(dt)
510 text.Overlined = n.Style["text-decoration"] == "overline"
511 text.Underlined = n.Style["text-decoration"] == "underline"
512 text.LineThrough = n.Style["text-decoration"] == "linethrough"
513 text.EM = int(self.EM)
514 text.Width = int(parent.Width)
515 text.Text = n.InnerText
516 text.Last = n.GetAttribute("last") == "true"
517
518 if n.Style["word-spacing"] == "" {
519 text.WordSpacing = font.MeasureSpace(&text)
520 }
521
522 img, width := font.Render(&text)
523 self.Texture = img
524
525 if n.Style["height"] == "" && n.Style["min-height"] == "" {
526 self.Height = float32(text.LineHeight)
527 }
528
529 if n.Style["width"] == "" && n.Style["min-width"] == "" {
530 self.Width = float32(width)
531 }
532
533 return self
534}