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
3import (
4 "fmt"
5 "gui/color"
6 "gui/element"
7 "gui/font"
8 "gui/parser"
9 "gui/utils"
10 "os"
11 "slices"
12 "sort"
13 "strconv"
14 "strings"
15)
16
17type Plugin struct {
18 Styles map[string]string
19 Level int
20 Handler func(*element.Node, *map[string]element.State)
21}
22
23type CSS struct {
24 Width float32
25 Height float32
26 StyleSheets []map[string]map[string]string
27 Plugins []Plugin
28 Document *element.Node
29}
30
31func (c *CSS) StyleSheet(path string) {
32 // Parse the CSS file
33 dat, err := os.ReadFile(path)
34 utils.Check(err)
35 styles := parser.ParseCSS(string(dat))
36
37 c.StyleSheets = append(c.StyleSheets, styles)
38}
39
40func (c *CSS) StyleTag(css string) {
41 styles := parser.ParseCSS(css)
42 c.StyleSheets = append(c.StyleSheets, styles)
43}
44
45var inheritedProps = []string{
46 "color",
47 "cursor",
48 "font",
49 "font-family",
50 "font-size",
51 "font-style",
52 "font-weight",
53 "letter-spacing",
54 "line-height",
55 "text-align",
56 "text-indent",
57 "text-justify",
58 "text-shadow",
59 "text-transform",
60 "text-decoration",
61 "visibility",
62 "word-spacing",
63 "display",
64}
65
66func (c *CSS) GetStyles(n element.Node) map[string]string {
67 styles := map[string]string{}
68
69 if n.Parent != nil {
70 ps := c.GetStyles(*n.Parent)
71 for _, v := range inheritedProps {
72 if ps[v] != "" {
73 styles[v] = ps[v]
74 }
75 }
76 }
77 for k, v := range n.Style {
78 styles[k] = v
79 }
80 hovered := false
81 if slices.Contains(n.ClassList.Classes, ":hover") {
82 hovered = true
83 }
84
85 for _, styleSheet := range c.StyleSheets {
86 for selector := range styleSheet {
87 // fmt.Println(selector, n.Properties.Id)
88 key := selector
89 if strings.Contains(selector, ":hover") && hovered {
90 selector = strings.Replace(selector, ":hover", "", -1)
91 }
92 if element.TestSelector(selector, &n) {
93 for k, v := range styleSheet[key] {
94 styles[k] = v
95 }
96 }
97
98 }
99 }
100
101 // This is different than node.Style
102 // temp1 = <span style=​"color:​#a6e22e">​CSS​</span>​
103 // temp1.style == CSSStyleDeclaration {0: 'color', accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
104 // temp1.getAttribute("style") == 'color:#a6e22e'
105 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
106 styles = utils.Merge(styles, inline)
107 // add hover and focus css events
108
109 if n.Parent != nil {
110 if styles["z-index"] == "" && n.Parent.Style["z-index"] != "" {
111 z, _ := strconv.Atoi(n.Parent.Style["z-index"])
112 z += 1
113 styles["z-index"] = strconv.Itoa(z)
114 }
115 }
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", n.InnerText)
133 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
134 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
135 fmt.Printf("Styles: %v\n", self.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 // !ISSUE: This should add to state.Style instead as the element.Node should be un effected by the engine
147 // + currently this adds styles to the style attribute that the use did not explisitly set
148 // + this also applies to the margin/padding and border completer functions
149
150 s := *state
151 self := s[n.Properties.Id]
152 parent := s[n.Parent.Properties.Id]
153
154 // !ISSUE: BREAKING
155 self.Style = c.GetStyles(*n)
156
157 self.Background = color.Parse(self.Style, "background")
158 self.Border, _ = CompleteBorder(self.Style, self, parent)
159
160 fs, _ := utils.ConvertToPixels(self.Style["font-size"], parent.EM, parent.Width)
161 self.EM = fs
162
163 if self.Style["display"] == "none" {
164 self.X = 0
165 self.Y = 0
166 self.Width = 0
167 self.Height = 0
168 return n
169 }
170
171 if self.Style["width"] == "" && self.Style["display"] == "block" {
172 self.Style["width"] = "100%"
173 }
174
175 // Set Z index value to be sorted in window
176 if self.Style["z-index"] != "" {
177 z, _ := strconv.Atoi(self.Style["z-index"])
178 self.Z = float32(z)
179 }
180
181 if parent.Z > 0 {
182 self.Z = parent.Z + 1
183 }
184
185 (*state)[n.Properties.Id] = self
186
187 wh := utils.GetWH(*n, state)
188 width := wh.Width
189 height := wh.Height
190
191 x, y := parent.X, parent.Y
192
193 var top, left, right, bottom bool = false, false, false, false
194
195 m := utils.GetMP(*n, wh, state, "margin")
196 p := utils.GetMP(*n, wh, state, "padding")
197
198 self.Margin = m
199 self.Padding = p
200
201 if self.Style["position"] == "absolute" {
202 bas := utils.GetPositionOffsetNode(n, state)
203 base := s[bas.Properties.Id]
204 if self.Style["top"] != "" {
205 v, _ := utils.ConvertToPixels(self.Style["top"], self.EM, parent.Width)
206 y = v + base.Y
207 top = true
208 }
209 if self.Style["left"] != "" {
210 v, _ := utils.ConvertToPixels(self.Style["left"], self.EM, parent.Width)
211 x = v + base.X
212 left = true
213 }
214 if self.Style["right"] != "" {
215 v, _ := utils.ConvertToPixels(self.Style["right"], self.EM, parent.Width)
216 x = (base.Width - width) - v
217 right = true
218 }
219 if self.Style["bottom"] != "" {
220 v, _ := utils.ConvertToPixels(self.Style["bottom"], self.EM, parent.Width)
221 y = (base.Height - height) - v
222 bottom = true
223 }
224
225 } else {
226 for i, v := range n.Parent.Children {
227 vState := s[v.Properties.Id]
228 if vState.Style["position"] != "absolute" {
229 if v.Properties.Id == n.Properties.Id {
230 if i-1 > 0 {
231 sib := n.Parent.Children[i-1]
232 sibling := s[sib.Properties.Id]
233 if sibling.Style["position"] != "absolute" {
234 if self.Style["display"] == "inline" {
235 if sibling.Style["display"] == "inline" {
236 y = sibling.Y
237 } else {
238 y = sibling.Y + sibling.Height
239 }
240 } else {
241 y = sibling.Y + sibling.Height
242 }
243 }
244
245 }
246 break
247 } else if self.Style["display"] != "inline" {
248 vState := s[v.Properties.Id]
249 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (vState.Border.Width * 2)
250 }
251 }
252
253 }
254 }
255
256 // Display modes need to be calculated here
257
258 relPos := !top && !left && !right && !bottom
259
260 if left || relPos {
261 x += m.Left
262 }
263 if top || relPos {
264 y += m.Top
265 }
266 if right {
267 x -= m.Right
268 }
269 if bottom {
270 y -= m.Bottom
271 }
272
273 self.X = x + parent.Padding.Left
274 self.Y = y
275 self.Width = width
276 self.Height = height
277 (*state)[n.Properties.Id] = self
278
279 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
280 // Confirm text exists
281 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
282 // !ISSUE: Doesn't break the h1 element for some reason
283 if len(words) != 1 {
284 if self.Style["display"] == "inline" {
285 // !ISSUE: this works great and it is how I want it to work but it does modifiy the dom which is a no go
286 n.InnerText = words[0]
287 el := *n
288 el.InnerText = strings.Join(words[1:], " ")
289 n.Parent.InsertAfter(el, *n)
290 }
291 }
292 if len(strings.TrimSpace(n.InnerText)) > 0 {
293 n.InnerText = strings.TrimSpace(n.InnerText)
294 self = genTextNode(n, state)
295 }
296 }
297
298 (*state)[n.Properties.Id] = self
299 (*state)[n.Parent.Properties.Id] = parent
300
301 // Call children here
302
303 var childYOffset float32
304 for i := 0; i < len(n.Children); i++ {
305 v := n.Children[i]
306 v.Parent = n
307 n.Children[i] = *c.ComputeNodeStyle(&v, state)
308 cState := (*state)[n.Children[i].Properties.Id]
309 if self.Style["height"] == "" {
310 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
311 childYOffset = cState.Y + cState.Height
312 self.Height = (cState.Y - self.Y) + cState.Height
313 self.Height += cState.Margin.Top
314 self.Height += cState.Margin.Bottom
315 self.Height += cState.Padding.Top
316 self.Height += cState.Padding.Bottom
317 }
318 }
319 if cState.Width > self.Width {
320 self.Width = cState.Width
321 }
322 }
323
324 (*state)[n.Properties.Id] = self
325
326 // !ISSUE: Align Text
327 // if n.Style["text-align"] == "center" {
328 // minX := float32(9e15)
329 // maxXW := float32(0)
330 // fmt.Println(n.Properties.Id, len(n.Children))
331 // for _, v := range n.Children {
332 // cState := (*state)[v.Properties.Id]
333 // if cState.X < minX {
334 // minX = cState.X
335 // }
336 // if (cState.X + cState.Width) > maxXW {
337 // maxXW = cState.X + cState.Width
338 // }
339 // }
340 // for _, v := range n.Children {
341 // cState := (*state)[v.Properties.Id]
342 // cState.X += (self.Width - (maxXW - minX)) / 2
343 // (*state)[v.Properties.Id] = cState
344 // }
345 // }
346
347 // (*state)[n.Properties.Id] = self
348
349 // Sorting the array by the Level field
350 sort.Slice(plugins, func(i, j int) bool {
351 return plugins[i].Level < plugins[j].Level
352 })
353
354 for _, v := range plugins {
355 matches := true
356 for name, value := range v.Styles {
357 if self.Style[name] != value && !(value == "*") {
358 matches = false
359 }
360 }
361 if matches {
362 // !ISSUE: Might save memory by making a state map tree and passing that instead of the node it's self
363 v.Handler(n, state)
364 }
365 }
366
367 for i := range n.Children {
368 cState := (*state)[n.Children[i].Properties.Id]
369 cState.Y += self.Padding.Top
370 (*state)[n.Children[i].Properties.Id] = cState
371 }
372
373 // CheckNode(n, state)
374
375 return n
376}
377
378func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
379 // Split the shorthand into components
380 borderComponents := strings.Fields(cssProperties["border"])
381
382 // Ensure there are at least 1 component (width or style or color)
383 if len(borderComponents) >= 1 {
384 width := "0px" // Default width
385 style := "solid"
386 borderColor := "#000000" // Default color
387
388 // Extract style and color if available
389 if len(borderComponents) >= 1 {
390 width = borderComponents[0]
391 }
392
393 // Extract style and color if available
394 if len(borderComponents) >= 2 {
395 style = borderComponents[1]
396 }
397 if len(borderComponents) >= 3 {
398 borderColor = borderComponents[2]
399 }
400
401 parsedColor, _ := color.Color(borderColor)
402
403 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
404
405 return element.Border{
406 Width: w,
407 Style: style,
408 Color: parsedColor,
409 Radius: cssProperties["border-radius"],
410 }, nil
411 }
412
413 return element.Border{}, fmt.Errorf("invalid border shorthand format")
414}
415
416func genTextNode(n *element.Node, state *map[string]element.State) element.State {
417 s := *state
418 self := s[n.Properties.Id]
419 parent := s[n.Parent.Properties.Id]
420
421 text := element.Text{}
422
423 bold, italic := false, false
424
425 if self.Style["font-weight"] == "bold" {
426 bold = true
427 }
428
429 if self.Style["font-style"] == "italic" {
430 italic = true
431 }
432
433 if text.Font == nil {
434 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
435 text.Font = f
436 }
437
438 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
439 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
440 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
441 if lineHeight == 0 {
442 lineHeight = self.EM + 3
443 }
444
445 text.LineHeight = int(lineHeight)
446 text.WordSpacing = int(wordSpacing)
447 text.LetterSpacing = int(letterSpacing)
448 wb := " "
449
450 if self.Style["word-wrap"] == "break-word" {
451 wb = ""
452 }
453
454 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
455 wb = ""
456 }
457
458 var dt float32
459
460 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
461 dt = self.EM / 7
462 } else {
463 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
464 }
465
466 col := color.Parse(self.Style, "font")
467
468 self.Color = col
469
470 text.Color = col
471 text.DecorationColor = color.Parse(self.Style, "decoration")
472 text.Align = self.Style["text-align"]
473 text.WordBreak = wb
474 text.WordSpacing = int(wordSpacing)
475 text.LetterSpacing = int(letterSpacing)
476 text.WhiteSpace = self.Style["white-space"]
477 text.DecorationThickness = int(dt)
478 text.Overlined = self.Style["text-decoration"] == "overline"
479 text.Underlined = self.Style["text-decoration"] == "underline"
480 text.LineThrough = self.Style["text-decoration"] == "linethrough"
481 text.EM = int(self.EM)
482 text.Width = int(parent.Width)
483 text.Text = n.InnerText
484
485 if self.Style["word-spacing"] == "" {
486 text.WordSpacing = font.MeasureSpace(&text)
487 }
488
489 img, width := font.Render(&text)
490 self.Texture = img
491
492 if self.Style["height"] == "" {
493 self.Height = float32(text.LineHeight)
494 }
495
496 if self.Style["width"] == "" {
497 self.Width = float32(width)
498 }
499
500 return self
501}