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("EM: %v\n", self.EM)
131 fmt.Printf("Parent: %v\n", n.Parent.TagName)
132 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
133 fmt.Printf("Text: %v\n", n.InnerText)
134 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
135 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
136 fmt.Printf("Styles: %v\n", self.Style)
137 fmt.Printf("Background: %v\n", self.Background)
138 fmt.Printf("Border: %v\n\n\n", self.Border)
139}
140
141func (c *CSS) ComputeNodeStyle(node *element.Node, state *map[string]element.State) *element.Node {
142
143 // Head is not renderable
144 if utils.IsParent(*node, "head") {
145 return node
146 }
147
148 // !TODO: Make a plugin type system that can rewrite nodes and matches by more than just tagname
149 // + should be ran here once a node is loaded
150 plugins := c.Plugins
151
152 s := *state
153 self := s[node.Properties.Id]
154 parent := s[node.Parent.Properties.Id]
155
156 var n *element.Node
157
158 // !ISSUE: For some reason node is still being tainted
159 // + if the user changes the innerText of the swap parent then how does the swap get updated????
160 // + in theory it should be invalided when the main invalidator runs
161 if self.Swap.Properties.Id != "" {
162 n = &self.Swap
163 // fmt.Println("Swapped: ", n.Properties.Id, n.InnerText)
164 // CheckNode(node, state)
165 // CheckNode(&self.Swap, state)
166 } else {
167 n = node
168 // fmt.Println("Back: ", n.Properties.Id, n.InnerText)
169 self.Style = c.GetStyles(*n)
170 }
171
172 self.Background = color.Parse(self.Style, "background")
173 self.Border, _ = CompleteBorder(self.Style, self, parent)
174
175 fs, _ := utils.ConvertToPixels(self.Style["font-size"], parent.EM, parent.Width)
176 self.EM = fs
177
178 if self.Style["display"] == "none" {
179 self.X = 0
180 self.Y = 0
181 self.Width = 0
182 self.Height = 0
183 return n
184 }
185
186 if self.Style["width"] == "" && self.Style["display"] == "block" {
187 self.Style["width"] = "100%"
188 }
189
190 // Set Z index value to be sorted in window
191 if self.Style["z-index"] != "" {
192 z, _ := strconv.Atoi(self.Style["z-index"])
193 self.Z = float32(z)
194 }
195
196 if parent.Z > 0 {
197 self.Z = parent.Z + 1
198 }
199
200 (*state)[n.Properties.Id] = self
201
202 wh := utils.GetWH(*n, state)
203 width := wh.Width
204 height := wh.Height
205
206 x, y := parent.X, parent.Y
207 offsetX, offsetY := utils.GetXY(n, state)
208 x += offsetX
209 y += offsetY
210
211 // !NOTE: Maybe make a get xy offset function...
212
213 var top, left, right, bottom bool = false, false, false, false
214
215 m := utils.GetMP(*n, wh, state, "margin")
216 p := utils.GetMP(*n, wh, state, "padding")
217
218 self.Margin = m
219 self.Padding = p
220
221 if self.Style["position"] == "absolute" {
222 bas := utils.GetPositionOffsetNode(n, state)
223 base := s[bas.Properties.Id]
224 if self.Style["top"] != "" {
225 v, _ := utils.ConvertToPixels(self.Style["top"], self.EM, parent.Width)
226 y = v + base.Y
227 top = true
228 }
229 if self.Style["left"] != "" {
230 v, _ := utils.ConvertToPixels(self.Style["left"], self.EM, parent.Width)
231 x = v + base.X
232 left = true
233 }
234 if self.Style["right"] != "" {
235 v, _ := utils.ConvertToPixels(self.Style["right"], self.EM, parent.Width)
236 x = (base.Width - width) - v
237 right = true
238 }
239 if self.Style["bottom"] != "" {
240 v, _ := utils.ConvertToPixels(self.Style["bottom"], self.EM, parent.Width)
241 y = (base.Height - height) - v
242 bottom = true
243 }
244
245 } else {
246 for i, v := range n.Parent.Children {
247 vState := s[v.Properties.Id]
248 if vState.Style["position"] != "absolute" {
249 if v.Properties.Id == n.Properties.Id {
250 if i-1 > 0 {
251 sib := n.Parent.Children[i-1]
252 sibling := s[sib.Properties.Id]
253 if sibling.Style["position"] != "absolute" {
254 if self.Style["display"] == "inline" {
255 if sibling.Style["display"] == "inline" {
256 y = sibling.Y
257 } else {
258 y = sibling.Y + sibling.Height
259 }
260 } else {
261 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
262 }
263 }
264
265 }
266 break
267 } else if self.Style["display"] != "inline" {
268 vState := s[v.Properties.Id]
269 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
270 }
271 }
272 }
273 }
274
275 // Display modes need to be calculated here
276
277 relPos := !top && !left && !right && !bottom
278
279 if left || relPos {
280 x += m.Left
281 }
282 if top || relPos {
283 y += m.Top
284 }
285 if right {
286 x -= m.Right
287 }
288 if bottom {
289 y -= m.Bottom
290 }
291
292 self.X = x
293 self.Y = y
294 self.Width = width
295 self.Height = height
296 (*state)[n.Properties.Id] = self
297
298 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
299 // Confirm text exists
300 words := strings.Split(strings.TrimSpace(n.InnerText), " ")
301 if len(words) != 1 {
302 // !ISSUE: Still doesn't work great
303 if self.Swap.Properties.Id == "" {
304 self.Swap = *n
305 n = &self.Swap
306 }
307 if self.Style["display"] == "inline" {
308 n.InnerText = words[0]
309 el := *n
310 el.InnerText = strings.Join(words[1:], " ")
311 n.Parent.InsertAfter(el, *n)
312 } else {
313 el := n.CreateElement("notaspan")
314 el.InnerText = n.InnerText
315 n.AppendChild(el)
316 self.Style["font-size"] = parent.Style["font-size"]
317 self.EM = parent.EM
318 n.InnerText = ""
319 }
320 (*state)[n.Properties.Id] = self
321 }
322 if len(strings.TrimSpace(n.InnerText)) > 0 {
323 n.InnerText = strings.TrimSpace(n.InnerText)
324 self = genTextNode(n, state)
325 }
326 }
327
328 (*state)[n.Properties.Id] = self
329 (*state)[n.Parent.Properties.Id] = parent
330
331 // Call children here
332
333 var childYOffset float32
334 for i := 0; i < len(n.Children); i++ {
335 v := n.Children[i]
336 v.Parent = n
337 n.Children[i] = *c.ComputeNodeStyle(&v, state)
338
339 cState := (*state)[n.Children[i].Properties.Id]
340 if self.Style["height"] == "" {
341 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
342 childYOffset = cState.Y + cState.Height
343 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
344 self.Height += cState.Margin.Top
345 self.Height += cState.Margin.Bottom
346 self.Height += cState.Padding.Top
347 self.Height += cState.Padding.Bottom
348 }
349 }
350 if cState.Width > self.Width {
351 self.Width = cState.Width
352 }
353 }
354
355 self.Height += self.Padding.Bottom
356
357 (*state)[n.Properties.Id] = self
358
359 // Sorting the array by the Level field
360 sort.Slice(plugins, func(i, j int) bool {
361 return plugins[i].Level < plugins[j].Level
362 })
363
364 for _, v := range plugins {
365 matches := true
366 for name, value := range v.Styles {
367 if self.Style[name] != value && !(value == "*") {
368 matches = false
369 }
370 }
371 if matches {
372 // !NOTE: Might save memory by making a state map tree and passing that instead of the node it's self
373 v.Handler(n, state)
374 }
375 }
376
377 // CheckNode(n, state)
378
379 return n
380}
381
382func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
383 // Split the shorthand into components
384 borderComponents := strings.Fields(cssProperties["border"])
385
386 // Ensure there are at least 1 component (width or style or color)
387 if len(borderComponents) >= 1 {
388 width := "0px" // Default width
389 style := "solid"
390 borderColor := "#000000" // Default color
391
392 // Extract style and color if available
393 if len(borderComponents) >= 1 {
394 width = borderComponents[0]
395 }
396
397 // Extract style and color if available
398 if len(borderComponents) >= 2 {
399 style = borderComponents[1]
400 }
401 if len(borderComponents) >= 3 {
402 borderColor = borderComponents[2]
403 }
404
405 parsedColor, _ := color.Color(borderColor)
406
407 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
408
409 return element.Border{
410 Width: w,
411 Style: style,
412 Color: parsedColor,
413 Radius: cssProperties["border-radius"],
414 }, nil
415 }
416
417 return element.Border{}, fmt.Errorf("invalid border shorthand format")
418}
419
420func genTextNode(n *element.Node, state *map[string]element.State) element.State {
421 s := *state
422 self := s[n.Properties.Id]
423 parent := s[n.Parent.Properties.Id]
424
425 text := element.Text{}
426
427 bold, italic := false, false
428
429 if self.Style["font-weight"] == "bold" {
430 bold = true
431 }
432
433 if self.Style["font-style"] == "italic" {
434 italic = true
435 }
436
437 if text.Font == nil {
438 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
439 text.Font = f
440 }
441
442 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
443 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
444 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
445 if lineHeight == 0 {
446 lineHeight = self.EM + 3
447 }
448
449 text.LineHeight = int(lineHeight)
450 text.WordSpacing = int(wordSpacing)
451 text.LetterSpacing = int(letterSpacing)
452 wb := " "
453
454 if self.Style["word-wrap"] == "break-word" {
455 wb = ""
456 }
457
458 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
459 wb = ""
460 }
461
462 var dt float32
463
464 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
465 dt = self.EM / 7
466 } else {
467 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
468 }
469
470 col := color.Parse(self.Style, "font")
471
472 self.Color = col
473
474 text.Color = col
475 text.DecorationColor = color.Parse(self.Style, "decoration")
476 text.Align = self.Style["text-align"]
477 text.WordBreak = wb
478 text.WordSpacing = int(wordSpacing)
479 text.LetterSpacing = int(letterSpacing)
480 text.WhiteSpace = self.Style["white-space"]
481 text.DecorationThickness = int(dt)
482 text.Overlined = self.Style["text-decoration"] == "overline"
483 text.Underlined = self.Style["text-decoration"] == "underline"
484 text.LineThrough = self.Style["text-decoration"] == "linethrough"
485 text.EM = int(self.EM)
486 text.Width = int(parent.Width)
487 text.Text = n.InnerText
488
489 if self.Style["word-spacing"] == "" {
490 text.WordSpacing = font.MeasureSpace(&text)
491 }
492
493 img, width := font.Render(&text)
494 self.Texture = img
495
496 if self.Style["height"] == "" {
497 self.Height = float32(text.LineHeight)
498 }
499
500 if self.Style["width"] == "" {
501 self.Width = float32(width)
502 }
503
504 return self
505}