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
3// package aui/goldie
4// https://pkg.go.dev/automated.sh/goldie
5// https://pkg.go.dev/automated.sh/aui
6// https://pkg.go.dev/automated.sh/oat
7
8import (
9 "crypto/md5"
10 "encoding/hex"
11 "fmt"
12 "gui/color"
13 "gui/element"
14 "gui/font"
15 "gui/parser"
16 "gui/utils"
17 "os"
18 "slices"
19 "sort"
20 "strings"
21)
22
23type Plugin struct {
24 Styles map[string]string
25 Level int
26 Handler func(*element.Node)
27}
28
29type CSS struct {
30 Width float32
31 Height float32
32 StyleSheets []map[string]map[string]string
33 Plugins []Plugin
34 Document *element.Node
35}
36
37func (c *CSS) StyleSheet(path string) {
38 // Parse the CSS file
39 dat, err := os.ReadFile(path)
40 utils.Check(err)
41 styles := parser.ParseCSS(string(dat))
42
43 c.StyleSheets = append(c.StyleSheets, styles)
44}
45
46func (c *CSS) StyleTag(css string) {
47 styles := parser.ParseCSS(css)
48 c.StyleSheets = append(c.StyleSheets, styles)
49}
50
51var inheritedProps = []string{
52 "color",
53 "cursor",
54 "font",
55 "font-family",
56 "font-size",
57 "font-style",
58 "font-weight",
59 "letter-spacing",
60 "line-height",
61 "text-align",
62 "text-indent",
63 "text-justify",
64 "text-shadow",
65 "text-transform",
66 "visibility",
67 "word-spacing",
68 "display",
69}
70
71// need to get rid of the .props for the most part all styles should be computed dynamically
72// can keep like focusable and stuff that describes the element
73
74// currently the append child does not work due to the props and other stuff not existing so it fails
75// moving to a real time style compute would fix that
76
77// :hover is parsed correctly but because the hash func doesn't invalidate it becuase the val
78// is updated in the props. change to append :hover to style to create the effect
79// or merge the class with the styles? idk have to think more
80
81func (c *CSS) GetStyles(n element.Node) map[string]string {
82 styles := map[string]string{}
83 for k, v := range n.Style {
84 styles[k] = v
85 }
86 if n.Parent != nil {
87 ps := c.GetStyles(*n.Parent)
88 for _, v := range inheritedProps {
89 if ps[v] != "" {
90 styles[v] = ps[v]
91 }
92 }
93
94 }
95 hovered := false
96 if slices.Contains(n.ClassList.Classes, ":hover") {
97 hovered = true
98 }
99
100 for _, styleSheet := range c.StyleSheets {
101 for selector := range styleSheet {
102 // fmt.Println(selector, n.Properties.Id)
103 key := selector
104 if strings.Contains(selector, ":hover") && hovered {
105 selector = strings.Replace(selector, ":hover", "", -1)
106 }
107 if element.TestSelector(selector, &n) {
108 for k, v := range styleSheet[key] {
109 styles[k] = v
110 }
111 }
112
113 }
114 }
115 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
116 styles = utils.Merge(styles, inline)
117 // add hover and focus css events
118
119 return styles
120}
121
122func (c *CSS) Render(doc element.Node) []element.Node {
123 return flatten(doc)
124}
125
126func (c *CSS) AddPlugin(plugin Plugin) {
127 c.Plugins = append(c.Plugins, plugin)
128}
129
130func hash(n *element.Node) string {
131 // Create a new FNV-1a hash
132 hasher := md5.New()
133
134 // Extract and sort the keys
135 var keys []string
136 for key := range n.Style {
137 keys = append(keys, key)
138 }
139 sort.Strings(keys)
140
141 // Concatenate all values into a single string
142 var concatenatedValues string
143 for _, key := range keys {
144 concatenatedValues += key + n.Style[key]
145 }
146 concatenatedValues += n.ClassList.Value
147 concatenatedValues += n.Id
148 hasher.Write([]byte(concatenatedValues))
149 sum := hasher.Sum(nil)
150 str := hex.EncodeToString(sum)
151 if n.Properties.Hash != str {
152 fmt.Println(n.Properties.Id)
153 fmt.Println(concatenatedValues)
154 fmt.Println(n.Properties.Hash, str)
155 }
156
157 return str
158}
159
160func (c *CSS) ComputeNodeStyle(n *element.Node) *element.Node {
161 plugins := c.Plugins
162 hv := hash(n)
163 if n.Properties.Hash != hv {
164 fmt.Println("RELOAD")
165 // this is kinda a sloppy way to do this but it works ig
166 n.Style = c.GetStyles(*n)
167 n.Properties.Hash = hv
168 }
169 styleMap := n.Style
170
171 if styleMap["display"] == "none" {
172 n.Properties.X = 0
173 n.Properties.Y = 0
174 n.Properties.Width = 0
175 n.Properties.Height = 0
176 return n
177 }
178
179 width, height := n.Properties.Width, n.Properties.Height
180 x, y := n.Parent.Properties.X, n.Parent.Properties.Y
181
182 var top, left, right, bottom bool = false, false, false, false
183
184 m := utils.GetMP(*n, "margin")
185 p := utils.GetMP(*n, "padding")
186
187 if styleMap["position"] == "absolute" {
188 base := utils.GetPositionOffsetNode(n)
189 if styleMap["top"] != "" {
190 v, _ := utils.ConvertToPixels(styleMap["top"], float32(n.Properties.EM), n.Parent.Properties.Width)
191 y = v + base.Properties.Y
192 top = true
193 }
194 if styleMap["left"] != "" {
195 v, _ := utils.ConvertToPixels(styleMap["left"], float32(n.Properties.EM), n.Parent.Properties.Width)
196 x = v + base.Properties.X
197 left = true
198 }
199 if styleMap["right"] != "" {
200 v, _ := utils.ConvertToPixels(styleMap["right"], float32(n.Properties.EM), n.Parent.Properties.Width)
201 x = (base.Properties.Width - width) - v
202 right = true
203 }
204 if styleMap["bottom"] != "" {
205 v, _ := utils.ConvertToPixels(styleMap["bottom"], float32(n.Properties.EM), n.Parent.Properties.Width)
206 y = (base.Properties.Height - height) - v
207 bottom = true
208 }
209 } else {
210 for i, v := range n.Parent.Children {
211 if v.Properties.Id == n.Properties.Id {
212 if i-1 > 0 {
213 sibling := n.Parent.Children[i-1]
214 if styleMap["display"] == "inline" {
215 if sibling.Style["display"] == "inline" {
216 y = sibling.Properties.Y
217 } else {
218 y = sibling.Properties.Y + sibling.Properties.Height
219 }
220 } else {
221 y = sibling.Properties.Y + sibling.Properties.Height
222 }
223 }
224 break
225 } else if styleMap["display"] != "inline" {
226 mc := utils.GetMP(v, "margin")
227 pc := utils.GetMP(v, "padding")
228 y += mc.Top + mc.Bottom + pc.Top + pc.Bottom + v.Properties.Height
229 }
230 }
231 }
232
233 // Display modes need to be calculated here
234
235 relPos := !top && !left && !right && !bottom
236
237 if left || relPos {
238 x += m.Left
239 }
240 if top || relPos {
241 y += m.Top
242 }
243 if right {
244 x -= m.Right
245 }
246 if bottom {
247 y -= m.Bottom
248 }
249
250 bold, italic := false, false
251
252 if n.Style["font-weight"] == "bold" {
253 bold = true
254 }
255
256 if n.Style["font-style"] == "italic" {
257 italic = true
258 }
259
260 if n.Properties.Text.Font == nil {
261 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
262 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
263 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
264 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
265 if lineHeight == 0 {
266 lineHeight = n.Properties.EM + 3
267 }
268
269 n.Properties.Text.LineHeight = int(lineHeight)
270 n.Properties.Text.Font = f
271 n.Properties.Text.WordSpacing = int(wordSpacing)
272 n.Properties.Text.LetterSpacing = int(letterSpacing)
273 }
274
275 if len(n.Children) == 0 {
276 // Confirm text exists
277 if len(n.InnerText) > 0 && !utils.IsParent(*n, "head") {
278 innerWidth := width
279 innerHeight := height
280 genTextNode(n, &innerWidth, &innerHeight, p)
281 width = innerWidth + p.Left + p.Right
282 height = innerHeight
283 }
284 }
285
286 n.Properties.X = x
287 n.Properties.Y = y
288 n.Properties.Width = width
289 n.Properties.Height = height
290
291 // Call children here
292
293 var childYOffset float32
294 for i, v := range n.Children {
295 v.Parent = n
296 n.Children[i] = *c.ComputeNodeStyle(&v)
297 if styleMap["height"] == "" {
298 if n.Children[i].Style["position"] != "absolute" && n.Children[i].Properties.Y > childYOffset {
299 childYOffset = n.Children[i].Properties.Y
300 m := utils.GetMP(n.Children[i], "margin")
301 p := utils.GetMP(n.Children[i], "padding")
302 n.Properties.Height += n.Children[i].Properties.Height
303 n.Properties.Height += m.Top
304 n.Properties.Height += m.Bottom
305 n.Properties.Height += p.Top
306 n.Properties.Height += p.Bottom
307 }
308
309 }
310 }
311
312 // Sorting the array by the Level field
313 sort.Slice(plugins, func(i, j int) bool {
314 return plugins[i].Level < plugins[j].Level
315 })
316
317 for _, v := range plugins {
318 matches := true
319 for name, value := range v.Styles {
320 if styleMap[name] != value && !(value == "*") {
321 matches = false
322 }
323 }
324 if matches {
325 v.Handler(n)
326 }
327 }
328
329 return n
330}
331
332func InitNode(n *element.Node, c CSS) *element.Node {
333 n.Style = c.GetStyles(*n)
334 border, err := CompleteBorder(n.Style)
335 if err == nil {
336 n.Properties.Border = border
337 }
338
339 fs, _ := utils.ConvertToPixels(n.Style["font-size"], n.Parent.Properties.EM, n.Parent.Properties.Width)
340 n.Properties.EM = fs
341
342 width, _ := utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Width)
343 if n.Style["min-width"] != "" {
344 minWidth, _ := utils.ConvertToPixels(n.Style["min-width"], n.Properties.EM, n.Parent.Properties.Width)
345 width = utils.Max(width, minWidth)
346 }
347
348 if n.Style["max-width"] != "" {
349 maxWidth, _ := utils.ConvertToPixels(n.Style["max-width"], n.Properties.EM, n.Parent.Properties.Width)
350 width = utils.Min(width, maxWidth)
351 }
352
353 height, _ := utils.ConvertToPixels(n.Style["height"], n.Properties.EM, n.Parent.Properties.Height)
354 if n.Style["min-height"] != "" {
355 minHeight, _ := utils.ConvertToPixels(n.Style["min-height"], n.Properties.EM, n.Parent.Properties.Height)
356 height = utils.Max(height, minHeight)
357 }
358
359 if n.Style["max-height"] != "" {
360 maxHeight, _ := utils.ConvertToPixels(n.Style["max-height"], n.Properties.EM, n.Parent.Properties.Height)
361 height = utils.Min(height, maxHeight)
362 }
363
364 n.Properties.Width = width
365 n.Properties.Height = height
366
367 bold, italic := false, false
368
369 if n.Style["font-weight"] == "bold" {
370 bold = true
371 }
372
373 if n.Style["font-style"] == "italic" {
374 italic = true
375 }
376
377 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
378 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
379 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
380 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
381 if lineHeight == 0 {
382 lineHeight = n.Properties.EM + 3
383 }
384
385 n.Properties.Text.LineHeight = int(lineHeight)
386 n.Properties.Text.Font = f
387 n.Properties.Text.WordSpacing = int(wordSpacing)
388 n.Properties.Text.LetterSpacing = int(letterSpacing)
389 return n
390}
391
392func parseBorderShorthand(borderShorthand string) (element.Border, error) {
393 // Split the shorthand into components
394 borderComponents := strings.Fields(borderShorthand)
395
396 // Ensure there are at least 1 component (width or style or color)
397 if len(borderComponents) >= 1 {
398 width := "0px" // Default width
399 style := "solid"
400 borderColor := "#000000" // Default color
401
402 // Extract style and color if available
403 if len(borderComponents) >= 1 {
404 width = borderComponents[0]
405 }
406
407 // Extract style and color if available
408 if len(borderComponents) >= 2 {
409 style = borderComponents[1]
410 }
411 if len(borderComponents) >= 3 {
412 borderColor = borderComponents[2]
413 }
414
415 parsedColor, _ := color.Color(borderColor)
416
417 return element.Border{
418 Width: width,
419 Style: style,
420 Color: parsedColor,
421 Radius: "", // Default radius
422 }, nil
423 }
424
425 return element.Border{}, fmt.Errorf("invalid border shorthand format")
426}
427
428func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
429 border, err := parseBorderShorthand(cssProperties["border"])
430 border.Radius = cssProperties["border-radius"]
431
432 return border, err
433}
434
435func flatten(n element.Node) []element.Node {
436 var nodes []element.Node
437 nodes = append(nodes, n)
438
439 children := n.Children
440 if len(children) > 0 {
441 for _, ch := range children {
442 chNodes := flatten(ch)
443 nodes = append(nodes, chNodes...)
444 }
445 }
446 return nodes
447}
448
449func genTextNode(n *element.Node, width, height *float32, p utils.MarginPadding) {
450 wb := " "
451
452 if n.Style["word-wrap"] == "break-word" {
453 wb = ""
454 }
455
456 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
457 wb = ""
458 }
459
460 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, *width)
461 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, *width)
462
463 var dt float32
464
465 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
466 dt = 2
467 } else {
468 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], n.Properties.EM, *width)
469 }
470
471 col := color.Parse(n.Style, "font")
472
473 n.Properties.Text.Color = col
474 n.Properties.Text.Align = n.Style["text-align"]
475 n.Properties.Text.WordBreak = wb
476 n.Properties.Text.WordSpacing = int(wordSpacing)
477 n.Properties.Text.LetterSpacing = int(letterSpacing)
478 n.Properties.Text.WhiteSpace = n.Style["white-space"]
479 n.Properties.Text.DecorationThickness = int(dt)
480 n.Properties.Text.Overlined = n.Style["text-decoration"] == "overline"
481 n.Properties.Text.Underlined = n.Style["text-decoration"] == "underline"
482 n.Properties.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
483 n.Properties.Text.EM = int(n.Properties.EM)
484 n.Properties.Text.Width = int(n.Parent.Properties.Width)
485
486 if n.Style["word-spacing"] == "" {
487 n.Properties.Text.WordSpacing = font.MeasureSpace(&n.Properties.Text)
488 }
489 if n.Parent.Properties.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
490 *width = (n.Parent.Properties.Width - p.Right) - p.Left
491 } else if n.Style["width"] == "" {
492 *width = utils.Max(*width, float32(font.MeasureLongest(n)))
493 } else if n.Style["width"] != "" {
494 *width, _ = utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Width)
495 }
496
497 n.Properties.Text.Width = int(*width)
498 h := font.Render(n)
499 if n.Style["height"] == "" {
500 *height = h
501 }
502
503}