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