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