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
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) AddPlugin(plugin Plugin) {
121 c.Plugins = append(c.Plugins, plugin)
122}
123
124func CheckNode(n *element.Node, state *map[string]element.State) {
125 s := *state
126 self := s[n.Properties.Id]
127
128 fmt.Println(n.TagName, n.Properties.Id)
129 fmt.Printf("ID: %v\n", n.Id)
130 fmt.Printf("Parent: %v\n", n.Parent.TagName)
131 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
132 fmt.Printf("Text: %v\n", self.Text.Text)
133 fmt.Printf("X: %v, Y: %v\n", self.X, self.Y)
134 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
135 fmt.Printf("Styles: %v\n", n.Style)
136 fmt.Printf("Background: %v\n", self.Background)
137 fmt.Printf("Border: %v\n\n\n", self.Border)
138}
139
140func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
141 // Head is not renderable
142 if utils.IsParent(*n, "head") {
143 return n
144 }
145 plugins := c.Plugins
146 n.Style = c.GetStyles(*n)
147 s := *state
148 self := s[n.Properties.Id]
149 parent := s[n.Parent.Properties.Id]
150
151 self.Background = color.Parse(n.Style, "background")
152 self.Border, _ = CompleteBorder(n.Style)
153
154 fs, _ := utils.ConvertToPixels(n.Style["font-size"], parent.EM, parent.Width)
155 self.EM = fs
156
157 if n.Style["display"] == "none" {
158 self.X = 0
159 self.Y = 0
160 self.Width = 0
161 self.Height = 0
162 return n
163 }
164
165 wh := utils.GetWH(*n)
166 width := wh.Width
167 height := wh.Height
168
169 x, y := parent.X, parent.Y
170
171 var top, left, right, bottom bool = false, false, false, false
172
173 m := utils.GetMP(*n, "margin")
174 p := utils.GetMP(*n, "padding")
175
176 self.Margin = m
177 self.Padding = p
178
179 if n.Style["position"] == "absolute" {
180 bas := utils.GetPositionOffsetNode(n)
181 base := s[bas.Properties.Id]
182 if n.Style["top"] != "" {
183 v, _ := utils.ConvertToPixels(n.Style["top"], self.EM, parent.Width)
184 y = v + base.Y
185 top = true
186 }
187 if n.Style["left"] != "" {
188 v, _ := utils.ConvertToPixels(n.Style["left"], self.EM, parent.Width)
189 x = v + base.X
190 left = true
191 }
192 if n.Style["right"] != "" {
193 v, _ := utils.ConvertToPixels(n.Style["right"], self.EM, parent.Width)
194 x = (base.Width - width) - v
195 right = true
196 }
197 if n.Style["bottom"] != "" {
198 v, _ := utils.ConvertToPixels(n.Style["bottom"], self.EM, parent.Width)
199 y = (base.Height - height) - v
200 bottom = true
201 }
202 } else {
203 for i, v := range n.Parent.Children {
204 if v.Properties.Id == n.Properties.Id {
205 if i-1 > 0 {
206 sib := n.Parent.Children[i-1]
207 sibling := s[sib.Properties.Id]
208 if n.Style["display"] == "inline" {
209 if sib.Style["display"] == "inline" {
210 y = sibling.Y
211 } else {
212 y = sibling.Y + sibling.Height
213 }
214 } else {
215 y = sibling.Y + sibling.Height
216 }
217 }
218 break
219 } else if n.Style["display"] != "inline" {
220 vState := s[v.Properties.Id]
221 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
222 }
223 }
224 }
225
226 // Display modes need to be calculated here
227
228 relPos := !top && !left && !right && !bottom
229
230 if left || relPos {
231 x += m.Left
232 }
233 if top || relPos {
234 y += m.Top
235 }
236 if right {
237 x -= m.Right
238 }
239 if bottom {
240 y -= m.Bottom
241 }
242
243 if len(n.Children) == 0 {
244 // Confirm text exists
245 if len(n.InnerText) > 0 {
246 innerWidth := width
247 innerHeight := height
248 (*state)[n.Properties.Id] = self
249 self = genTextNode(n, &innerWidth, &innerHeight, p, state)
250 width = innerWidth + p.Left + p.Right
251 height = innerHeight
252 }
253 }
254
255 self.X = x
256 self.Y = y
257 self.Width = width
258 self.Height = height
259
260 (*state)[n.Properties.Id] = self
261 (*state)[n.Parent.Properties.Id] = parent
262
263 // CheckNode(n, state)
264
265 // Call children here
266
267 var childYOffset float32
268 for i, v := range n.Children {
269 v.Parent = n
270 n.Children[i] = *c.ComputeNodeStyle(&v, state)
271 if n.Style["height"] == "" {
272 cState := s[n.Children[i].Properties.Id]
273 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
274 childYOffset = cState.Y
275 self.Height += cState.Height
276 self.Height += cState.Margin.Top
277 self.Height += cState.Margin.Bottom
278 self.Height += cState.Padding.Top
279 self.Height += cState.Padding.Bottom
280 }
281
282 }
283 }
284
285 (*state)[n.Properties.Id] = self
286
287 // Sorting the array by the Level field
288 sort.Slice(plugins, func(i, j int) bool {
289 return plugins[i].Level < plugins[j].Level
290 })
291
292 for _, v := range plugins {
293 matches := true
294 for name, value := range v.Styles {
295 if n.Style[name] != value && !(value == "*") {
296 matches = false
297 }
298 }
299 if matches {
300 v.Handler(n, state)
301 }
302 }
303
304 return n
305}
306
307func parseBorderShorthand(borderShorthand string) (element.Border, error) {
308 // Split the shorthand into components
309 borderComponents := strings.Fields(borderShorthand)
310
311 // Ensure there are at least 1 component (width or style or color)
312 if len(borderComponents) >= 1 {
313 width := "0px" // Default width
314 style := "solid"
315 borderColor := "#000000" // Default color
316
317 // Extract style and color if available
318 if len(borderComponents) >= 1 {
319 width = borderComponents[0]
320 }
321
322 // Extract style and color if available
323 if len(borderComponents) >= 2 {
324 style = borderComponents[1]
325 }
326 if len(borderComponents) >= 3 {
327 borderColor = borderComponents[2]
328 }
329
330 parsedColor, _ := color.Color(borderColor)
331
332 return element.Border{
333 Width: width,
334 Style: style,
335 Color: parsedColor,
336 Radius: "", // Default radius
337 }, nil
338 }
339
340 return element.Border{}, fmt.Errorf("invalid border shorthand format")
341}
342
343func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
344 border, err := parseBorderShorthand(cssProperties["border"])
345 border.Radius = cssProperties["border-radius"]
346
347 return border, err
348}
349
350func genTextNode(n *element.Node, width, height *float32, p element.MarginPadding, state *map[string]element.State) element.State {
351 s := *state
352 self := s[n.Properties.Id]
353 parent := s[n.Parent.Properties.Id]
354
355 bold, italic := false, false
356
357 if n.Style["font-weight"] == "bold" {
358 bold = true
359 }
360
361 if n.Style["font-style"] == "italic" {
362 italic = true
363 }
364
365 if self.Text.Font == nil {
366 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
367 self.Text.Font = f
368 }
369
370 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, *width)
371 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, *width)
372 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, *width)
373 if lineHeight == 0 {
374 lineHeight = self.EM + 3
375 }
376
377 self.Text.LineHeight = int(lineHeight)
378 self.Text.WordSpacing = int(wordSpacing)
379 self.Text.LetterSpacing = int(letterSpacing)
380 wb := " "
381
382 if n.Style["word-wrap"] == "break-word" {
383 wb = ""
384 }
385
386 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
387 wb = ""
388 }
389
390 var dt float32
391
392 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
393 dt = 2
394 } else {
395 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, *width)
396 }
397
398 col := color.Parse(n.Style, "font")
399
400 self.Text.Color = col
401 self.Text.Align = n.Style["text-align"]
402 self.Text.WordBreak = wb
403 self.Text.WordSpacing = int(wordSpacing)
404 self.Text.LetterSpacing = int(letterSpacing)
405 self.Text.WhiteSpace = n.Style["white-space"]
406 self.Text.DecorationThickness = int(dt)
407 self.Text.Overlined = n.Style["text-decoration"] == "overline"
408 self.Text.Underlined = n.Style["text-decoration"] == "underline"
409 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
410 self.Text.EM = int(self.EM)
411 self.Text.Width = int(parent.Width)
412 self.Text.Text = n.InnerText
413
414 if n.Style["word-spacing"] == "" {
415 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
416 }
417 if parent.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
418 *width = (parent.Width - p.Right) - p.Left
419 } else if n.Style["width"] == "" {
420 *width = utils.Max(*width, float32(font.MeasureLongest(&self)))
421 } else if n.Style["width"] != "" {
422 *width, _ = utils.ConvertToPixels(n.Style["width"], self.EM, parent.Width)
423 }
424
425 self.Text.Width = int(*width)
426 h := font.Render(&self)
427 if n.Style["height"] == "" {
428 *height = h
429 }
430
431 return self
432
433}