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