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, *map[string]element.State)
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
69func (c *CSS) GetStyles(n element.Node) map[string]string {
70 styles := map[string]string{}
71
72 if n.Parent != nil {
73 ps := c.GetStyles(*n.Parent)
74 for _, v := range inheritedProps {
75 if ps[v] != "" {
76 styles[v] = ps[v]
77 }
78 }
79 }
80 for k, v := range n.Style {
81 styles[k] = v
82 }
83 hovered := false
84 if slices.Contains(n.ClassList.Classes, ":hover") {
85 hovered = true
86 }
87
88 for _, styleSheet := range c.StyleSheets {
89 for selector := range styleSheet {
90 // fmt.Println(selector, n.Properties.Id)
91 key := selector
92 if strings.Contains(selector, ":hover") && hovered {
93 selector = strings.Replace(selector, ":hover", "", -1)
94 }
95 if element.TestSelector(selector, &n) {
96 for k, v := range styleSheet[key] {
97 styles[k] = v
98 }
99 }
100
101 }
102 }
103
104 // !ISSUE: why is this needed, the "attribute" is n.Style that should be mapped during init
105 // + when a user adds a style via the style attirbute it will just be in the .Style prop...
106 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
107 styles = utils.Merge(styles, inline)
108 // add hover and focus css events
109
110 return styles
111}
112
113func (c *CSS) AddPlugin(plugin Plugin) {
114 c.Plugins = append(c.Plugins, plugin)
115}
116
117func CheckNode(n *element.Node, state *map[string]element.State) {
118 s := *state
119 self := s[n.Properties.Id]
120
121 fmt.Println(n.TagName, n.Properties.Id)
122 fmt.Printf("ID: %v\n", n.Id)
123 fmt.Printf("Parent: %v\n", n.Parent.TagName)
124 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
125 fmt.Printf("Text: %v\n", n.InnerText)
126 fmt.Printf("X: %v, Y: %v\n", self.X, self.Y)
127 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
128 fmt.Printf("Styles: %v\n", n.Style)
129 fmt.Printf("Background: %v\n", self.Background)
130 fmt.Printf("Border: %v\n\n\n", self.Border)
131}
132
133func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
134 // Head is not renderable
135 if utils.IsParent(*n, "head") {
136 return n
137 }
138 plugins := c.Plugins
139 // !ISSUE: This should add to state.Style instead as the element.Node should be un effected by the engine
140 // + currently this adds styles to the style attribute that the use did not explisitly set
141 // + this also applies to the margin/padding and border completer functions
142
143 n.Style = c.GetStyles(*n)
144 s := *state
145 self := s[n.Properties.Id]
146 parent := s[n.Parent.Properties.Id]
147
148 self.Background = color.Parse(n.Style, "background")
149 self.Border, _ = CompleteBorder(n.Style)
150
151 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
152 self.EM = fs
153
154 if n.Style["display"] == "none" {
155 self.X = 0
156 self.Y = 0
157 self.Width = 0
158 self.Height = 0
159 return n
160 }
161
162 wh := utils.GetWH(*n)
163 width := wh.Width
164 height := wh.Height
165
166 x, y := parent.X, parent.Y
167
168 var top, left, right, bottom bool = false, false, false, false
169
170 m := utils.GetMP(*n, "margin")
171 p := utils.GetMP(*n, "padding")
172
173 self.Margin = m
174 self.Padding = p
175
176 if n.Style["position"] == "absolute" {
177 bas := utils.GetPositionOffsetNode(n)
178 base := s[bas.Properties.Id]
179 if n.Style["top"] != "" {
180 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
181 y = v + base.Y
182 top = true
183 }
184 if n.Style["left"] != "" {
185 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
186 x = v + base.X
187 left = true
188 }
189 if n.Style["right"] != "" {
190 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
191 x = (base.Width - width) - v
192 right = true
193 }
194 if n.Style["bottom"] != "" {
195 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
196 y = (base.Height - height) - v
197 bottom = true
198 }
199 } else {
200 // !ISSUE: Blue square goes under a absoulutly positioned element when it should pass through
201 for i, v := range n.Parent.Children {
202 if v.Properties.Id == n.Properties.Id {
203 if i-1 > 0 {
204 sib := n.Parent.Children[i-1]
205 sibling := s[sib.Properties.Id]
206 if n.Style["display"] == "inline" {
207 if sib.Style["display"] == "inline" {
208 y = sibling.Y
209 } else {
210 y = sibling.Y + sibling.Height
211 }
212 } else {
213 y = sibling.Y + sibling.Height
214 }
215 }
216 break
217 } else if n.Style["display"] != "inline" {
218 vState := s[v.Properties.Id]
219 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
220 }
221 }
222 }
223
224 // Display modes need to be calculated here
225
226 relPos := !top && !left && !right && !bottom
227
228 if left || relPos {
229 x += m.Left
230 }
231 if top || relPos {
232 y += m.Top
233 }
234 if right {
235 x -= m.Right
236 }
237 if bottom {
238 y -= m.Bottom
239 }
240
241 // fmt.Println(n.InnerText, len(n.Children))
242 self.X = x + parent.Padding.Left
243 self.Y = y
244 self.Width = width
245 self.Height = height + self.Padding.Bottom
246 (*state)[n.Properties.Id] = self
247
248 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
249 // Confirm text exists
250 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
251 if len(words) != 1 {
252 if n.Style["display"] == "inline" {
253 // !ISSUE: this works great and it is how I want it to work but it does modifiy the dom which is a no go
254 n.InnerText = words[0]
255 el := *n
256 el.InnerText = strings.Join(words[1:], " ")
257 n.Parent.InsertAfter(el, *n)
258 }
259 }
260 if len(strings.TrimSpace(n.InnerText)) > 0 {
261 n.InnerText = strings.TrimSpace(n.InnerText)
262 self = genTextNode(n, state)
263 }
264 }
265
266 (*state)[n.Properties.Id] = self
267 (*state)[n.Parent.Properties.Id] = parent
268
269 // Call children here
270
271 var childYOffset float32
272 for i := 0; i < len(n.Children); i++ {
273 v := n.Children[i]
274 v.Parent = n
275 n.Children[i] = *c.ComputeNodeStyle(&v, state)
276 cState := (*state)[n.Children[i].Properties.Id]
277 if n.Style["height"] == "" {
278 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
279 childYOffset = cState.Y
280 self.Height += cState.Height
281 self.Height += cState.Margin.Top
282 self.Height += cState.Margin.Bottom
283 self.Height += cState.Padding.Top
284 self.Height += cState.Padding.Bottom
285 }
286 }
287 // fmt.Println(n.TagName, self.Width, v.TagName, cState.Width)
288 if cState.Width > self.Width {
289 self.Width = cState.Width
290 }
291 }
292
293 (*state)[n.Properties.Id] = self
294
295 // Sorting the array by the Level field
296 sort.Slice(plugins, func(i, j int) bool {
297 return plugins[i].Level < plugins[j].Level
298 })
299
300 for _, v := range plugins {
301 matches := true
302 for name, value := range v.Styles {
303 if n.Style[name] != value && !(value == "*") {
304 matches = false
305 }
306 }
307 if matches {
308 v.Handler(n, state)
309 }
310 }
311
312 for i := range n.Children {
313 cState := (*state)[n.Children[i].Properties.Id]
314 cState.Y += self.Padding.Top
315 (*state)[n.Children[i].Properties.Id] = cState
316 }
317
318 // CheckNode(n, state)
319
320 return n
321}
322
323func parseBorderShorthand(borderShorthand string) (element.Border, error) {
324 // Split the shorthand into components
325 borderComponents := strings.Fields(borderShorthand)
326
327 // Ensure there are at least 1 component (width or style or color)
328 if len(borderComponents) >= 1 {
329 width := "0px" // Default width
330 style := "solid"
331 borderColor := "#000000" // Default color
332
333 // Extract style and color if available
334 if len(borderComponents) >= 1 {
335 width = borderComponents[0]
336 }
337
338 // Extract style and color if available
339 if len(borderComponents) >= 2 {
340 style = borderComponents[1]
341 }
342 if len(borderComponents) >= 3 {
343 borderColor = borderComponents[2]
344 }
345
346 parsedColor, _ := color.Color(borderColor)
347
348 return element.Border{
349 Width: width,
350 Style: style,
351 Color: parsedColor,
352 Radius: "", // Default radius
353 }, nil
354 }
355
356 return element.Border{}, fmt.Errorf("invalid border shorthand format")
357}
358
359func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
360 border, err := parseBorderShorthand(cssProperties["border"])
361 border.Radius = cssProperties["border-radius"]
362
363 return border, err
364}
365
366func genTextNode(n *element.Node, state *map[string]element.State) element.State {
367 s := *state
368 self := s[n.Properties.Id]
369 parent := s[n.Parent.Properties.Id]
370
371 text := element.Text{}
372
373 bold, italic := false, false
374
375 if n.Style["font-weight"] == "bold" {
376 bold = true
377 }
378
379 if n.Style["font-style"] == "italic" {
380 italic = true
381 }
382
383 if text.Font == nil {
384 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
385 text.Font = f
386 }
387
388 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, parent.Width)
389 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, parent.Width)
390 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, parent.Width)
391 if lineHeight == 0 {
392 lineHeight = self.EM + 3
393 }
394
395 text.LineHeight = int(lineHeight)
396 text.WordSpacing = int(wordSpacing)
397 text.LetterSpacing = int(letterSpacing)
398 wb := " "
399
400 if n.Style["word-wrap"] == "break-word" {
401 wb = ""
402 }
403
404 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
405 wb = ""
406 }
407
408 var dt float32
409
410 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
411 dt = 3
412 } else {
413 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, parent.Width)
414 }
415
416 col := color.Parse(n.Style, "font")
417
418 self.Color = col
419
420 text.Color = col
421 text.DecorationColor = color.Parse(n.Style, "decoration")
422 text.Align = n.Style["text-align"]
423 text.WordBreak = wb
424 text.WordSpacing = int(wordSpacing)
425 text.LetterSpacing = int(letterSpacing)
426 text.WhiteSpace = n.Style["white-space"]
427 text.DecorationThickness = int(dt)
428 text.Overlined = n.Style["text-decoration"] == "overline"
429 text.Underlined = n.Style["text-decoration"] == "underline"
430 text.LineThrough = n.Style["text-decoration"] == "linethrough"
431 text.EM = int(self.EM)
432 text.Width = int(parent.Width)
433 text.Text = n.InnerText
434
435 if n.Style["word-spacing"] == "" {
436 text.WordSpacing = font.MeasureSpace(&text)
437 }
438
439 img, width := font.Render(&text)
440 self.Texture = img
441 // self.Text.Width = int(width)
442 self.Width = float32(width)
443
444 if n.Style["height"] == "" {
445 self.Height = float32(text.LineHeight)
446 }
447
448 return self
449}