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