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