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", self.Text.Text)
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 for i, v := range n.Parent.Children {
201 if v.Properties.Id == n.Properties.Id {
202 if i-1 > 0 {
203 sib := n.Parent.Children[i-1]
204 sibling := s[sib.Properties.Id]
205 if n.Style["display"] == "inline" {
206 if sib.Style["display"] == "inline" {
207 y = sibling.Y
208 } else {
209 y = sibling.Y + sibling.Height
210 }
211 } else {
212 y = sibling.Y + sibling.Height
213 }
214 }
215 break
216 } else if n.Style["display"] != "inline" {
217 vState := s[v.Properties.Id]
218 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height
219 }
220 }
221 }
222
223 // Display modes need to be calculated here
224
225 relPos := !top && !left && !right && !bottom
226
227 if left || relPos {
228 x += m.Left
229 }
230 if top || relPos {
231 y += m.Top
232 }
233 if right {
234 x -= m.Right
235 }
236 if bottom {
237 y -= m.Bottom
238 }
239
240 // fmt.Println(n.InnerText, len(n.Children))
241
242 if !utils.ChildrenHaveText(n) {
243 // Confirm text exists
244 if len(n.InnerText) > 0 {
245 words := strings.Split(n.InnerText, " ")
246 if len(words) != 1 && !strings.Contains(n.Properties.Id, "copy") && n.Style["display"] == "inline" {
247 n.InnerText = words[0]
248 n.Properties.Id = "copy" + n.Properties.Id
249 // slices.Reverse(words)
250 // !ISSUE: I thinkthis is a good route to take, might make a style prop for text that a plugins can be used to turn the groups
251 // + into psuedo elements but make them inline
252 // + that would take care of wrapping and it would also work for display block
253 for _, v := range words {
254 if len(strings.TrimSpace(v)) > 0 {
255 el := *n
256 el.InnerText = v
257 for k, val := range n.Style {
258 el.Style[k] = val
259 }
260 n.Parent.InsertAfter(el, *n)
261 }
262 }
263 // !ISSUE: can probally use flex to do it need to group text so it doesn't include block element
264 // + if there is a block element break then go on
265 // fmt.Println(n.Parent.Children)
266 }
267 innerWidth := width
268 innerHeight := height
269 (*state)[n.Properties.Id] = self
270 // !ISSUE: change genTextNode to basically a image generator for a word.
271 // + make it so this api can be used to also do images
272 self = genTextNode(n, &innerWidth, &innerHeight, p, state)
273 width = innerWidth + p.Left + p.Right
274 height = innerHeight
275 }
276 }
277
278 // if !utils.ChildrenHaveText(n) {
279 // // Confirm text exists
280 // if len(n.InnerText) > 0 {
281 // innerWidth := width
282 // innerHeight := height
283 // (*state)[n.Properties.Id] = self
284 // self = genTextNode(n, &innerWidth, &innerHeight, p, state)
285 // width = innerWidth + p.Left + p.Right
286 // height = innerHeight
287 // }
288 // }
289
290 self.X = x
291 self.Y = y
292 self.Width = width
293 self.Height = height
294
295 (*state)[n.Properties.Id] = self
296 (*state)[n.Parent.Properties.Id] = parent
297
298 // Call children here
299
300 var childYOffset float32
301 for i, v := range n.Children {
302 v.Parent = n
303 n.Children[i] = *c.ComputeNodeStyle(&v, state)
304 cState := (*state)[n.Children[i].Properties.Id]
305 if n.Style["height"] == "" {
306 if n.Children[i].Style["position"] != "absolute" && cState.Y > childYOffset {
307 childYOffset = cState.Y
308 self.Height += cState.Height
309 self.Height += cState.Margin.Top
310 self.Height += cState.Margin.Bottom
311 self.Height += cState.Padding.Top
312 self.Height += cState.Padding.Bottom
313 }
314 }
315 // fmt.Println(n.TagName, self.Width, v.TagName, cState.Width)
316 if cState.Width > self.Width {
317 self.Width = cState.Width
318 }
319 }
320
321 (*state)[n.Properties.Id] = self
322
323 // Sorting the array by the Level field
324 sort.Slice(plugins, func(i, j int) bool {
325 return plugins[i].Level < plugins[j].Level
326 })
327
328 for _, v := range plugins {
329 matches := true
330 for name, value := range v.Styles {
331 if n.Style[name] != value && !(value == "*") {
332 matches = false
333 }
334 }
335 if matches {
336 v.Handler(n, state)
337 }
338 }
339
340 CheckNode(n, state)
341
342 return n
343}
344
345func parseBorderShorthand(borderShorthand string) (element.Border, error) {
346 // Split the shorthand into components
347 borderComponents := strings.Fields(borderShorthand)
348
349 // Ensure there are at least 1 component (width or style or color)
350 if len(borderComponents) >= 1 {
351 width := "0px" // Default width
352 style := "solid"
353 borderColor := "#000000" // Default color
354
355 // Extract style and color if available
356 if len(borderComponents) >= 1 {
357 width = borderComponents[0]
358 }
359
360 // Extract style and color if available
361 if len(borderComponents) >= 2 {
362 style = borderComponents[1]
363 }
364 if len(borderComponents) >= 3 {
365 borderColor = borderComponents[2]
366 }
367
368 parsedColor, _ := color.Color(borderColor)
369
370 return element.Border{
371 Width: width,
372 Style: style,
373 Color: parsedColor,
374 Radius: "", // Default radius
375 }, nil
376 }
377
378 return element.Border{}, fmt.Errorf("invalid border shorthand format")
379}
380
381func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
382 border, err := parseBorderShorthand(cssProperties["border"])
383 border.Radius = cssProperties["border-radius"]
384
385 return border, err
386}
387
388func genTextNode(n *element.Node, width, height *float32, p element.MarginPadding, state *map[string]element.State) element.State {
389 s := *state
390 self := s[n.Properties.Id]
391 parent := s[n.Parent.Properties.Id]
392
393 bold, italic := false, false
394
395 if n.Style["font-weight"] == "bold" {
396 bold = true
397 }
398
399 if n.Style["font-style"] == "italic" {
400 italic = true
401 }
402
403 if self.Text.Font == nil {
404 f, _ := font.LoadFont(n.Style["font-family"], int(self.EM), bold, italic)
405 self.Text.Font = f
406 }
407
408 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], self.EM, *width)
409 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], self.EM, *width)
410 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], self.EM, *width)
411 if lineHeight == 0 {
412 lineHeight = self.EM + 3
413 }
414
415 self.Text.LineHeight = int(lineHeight)
416 self.Text.WordSpacing = int(wordSpacing)
417 self.Text.LetterSpacing = int(letterSpacing)
418 wb := " "
419
420 if n.Style["word-wrap"] == "break-word" {
421 wb = ""
422 }
423
424 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
425 wb = ""
426 }
427
428 var dt float32
429
430 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
431 dt = 3
432 } else {
433 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], self.EM, *width)
434 }
435
436 col := color.Parse(n.Style, "font")
437
438 self.Text.Color = col
439 self.Text.DecorationColor = color.Parse(n.Style, "decoration")
440 self.Text.Align = n.Style["text-align"]
441 self.Text.WordBreak = wb
442 self.Text.WordSpacing = int(wordSpacing)
443 self.Text.LetterSpacing = int(letterSpacing)
444 self.Text.WhiteSpace = n.Style["white-space"]
445 self.Text.DecorationThickness = int(dt)
446 self.Text.Overlined = n.Style["text-decoration"] == "overline"
447 self.Text.Underlined = n.Style["text-decoration"] == "underline"
448 self.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
449 self.Text.EM = int(self.EM)
450 self.Text.Width = int(parent.Width)
451 self.Text.Text = n.InnerText
452
453 if n.Style["word-spacing"] == "" {
454 self.Text.WordSpacing = font.MeasureSpace(&self.Text)
455 }
456 if parent.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
457 *width = (parent.Width - p.Right) - p.Left
458 } else if n.Style["width"] == "" {
459 *width = utils.Max(*width, float32(font.MeasureLongest(&self)))
460 } else if n.Style["width"] != "" {
461 *width, _ = utils.ConvertToPixels(n.Style["width"], self.EM, parent.Width)
462 }
463
464 self.Text.Width = int(*width)
465 self.Width = *width
466 h := font.Render(&self)
467 if n.Style["height"] == "" {
468 *height = h
469 }
470
471 return self
472
473}