CStyle Plugins
Plugins add the ability to choose what parts of the HTML/CSS spec you add to your application. If you are trying to keep compile sizes small you can remove as many as you need to reach your target size. Here we will go over the basics of how they work and how to use them.
1type Plugin struct {
2 Styles map[string]string
3 Level int
4 Handler func(*element.Node, *map[string]element.State)
5}
A plugin is a struct defined in CStyle, in contains three properties:
- Styles
- A map with CSS properties and values that the plugin should match on. There is also support for wildcard properties by setting the value to "*"
- Level
- Level of priority in which to execute
- All library level should be between 0 and 2
- All others should be greater than 2
- Handler
- Callback function that provides a pointer to a element that matches the properties of Styles
# AddPlugin?(go)
Add Plugin is the CStyle function that add the plugin to the top level cstyle.Plugins array where it is used within the ComputeNodeStyle
function.
# Usage
1css.AddPlugin(block.Init())
The first step in processing the plugins before running there handler functions is to sort them by their levels. We need to sort the plugins by their level because in the example of flex, it relys on a parent element being block position so it can compute styles based of the positioning of its parent elements. If flex was ran before block then it would have nothing to build apon. This is also the reason that if you are building a custom plugin it is reccomended to keep the level above 2 as anything after 2 will have the assumed styles to build apon.
// Sorting the array by the Level field sort.Slice(plugins, func(i, j int) bool { return plugins[i].Level < plugins[j].Level })
After we have the sorted plugins we can check if the current element matches the Styles
of the plugin. The matching is a all of nothing matching system, if one property is missing then the plugin wil not be ran. If it does match then a pointer to the element.Node
(n) is passed to the handler.
for _, v := range plugins { matches := true for name, value := range v.Styles { if styleMap[name] != value && !(value == "*") { matches = false } } if matches { v.Handler(n) } }
# plugins/block
Here is the code for the block styling plugin, it is recommended to keep this standard format.
All other plugins can be found in the cstyle/plugins folder
1package block
2
3import (
4 "gui/cstyle"
5 "gui/element"
6)
7
8func Init() cstyle.Plugin {
9 return cstyle.Plugin{
10 Styles: map[string]string{
11 "display": "block",
12 },
13 Level: 0,
14 Handler: func(n *element.Node, state *map[string]element.State) {
15 s := *state
16 self := s[n.Properties.Id]
17 // parent := s[n.Parent.Properties.Id]
18
19 // If the element is display block and the width is unset then make it 100%
20
21 // if self.Style["width"] == "" {
22 // self.Width, _ = utils.ConvertToPixels("100%", self.EM, parent.Width)
23 // fmt.Println(self.Margin)
24
25 // // self.Width -= (self.Padding.Right + self.Padding.Left)
26 // // self.Height -= (self.Padding.Top + self.Padding.Bottom)
27 // }
28
29 // if self.X+self.Width+(self.Border.Width*2) > parent.Width {
30 // self.Width = parent.Width
31 // self.Width -= (self.Margin.Right + self.Margin.Left)
32 // self.Width -= (self.Border.Width * 2)
33 // self.Height -= (self.Margin.Top + self.Margin.Bottom)
34 // }
35
36 (*state)[n.Properties.Id] = self
37 },
38 }
39}
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
18// + could copy the transformers but idk
19type Plugin struct {
20 Styles map[string]string
21 Level int
22 Handler func(*element.Node, *map[string]element.State)
23}
24
25type Transformer struct {
26 Selector func(*element.Node) bool
27 Handler func(element.Node) element.Node
28}
29
30type CSS struct {
31 Width float32
32 Height float32
33 StyleSheets []map[string]map[string]string
34 Plugins []Plugin
35 Transformers []Transformer
36 Document *element.Node
37}
38
39func (c *CSS) Transform(n element.Node) element.Node {
40 fmt.Println("########")
41 fmt.Println(n.TagName)
42 // for i := 0; i < len(n.Children); i++ {
43 // v := n.Children[i]
44 // fmt.Println(v.TagName)
45 // }
46 for i := 0; i < len(n.Children); i++ {
47 v := n.Children[i]
48 fmt.Println(v.TagName)
49 n.Children[i] = c.Transform(v)
50 }
51 for _, v := range c.Transformers {
52 if v.Selector(&n) {
53 n = v.Handler(n)
54 }
55 }
56
57 return n
58}
59
60func (c *CSS) StyleSheet(path string) {
61 // Parse the CSS file
62 dat, err := os.ReadFile(path)
63 utils.Check(err)
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) GetStyles(n element.Node) map[string]string {
96 styles := map[string]string{}
97
98 if n.Parent != nil {
99 ps := c.GetStyles(*n.Parent)
100 for _, v := range inheritedProps {
101 if ps[v] != "" {
102 styles[v] = ps[v]
103 }
104 }
105 }
106 for k, v := range n.Style {
107 styles[k] = v
108 }
109 hovered := false
110 if slices.Contains(n.ClassList.Classes, ":hover") {
111 hovered = true
112 }
113
114 for _, styleSheet := range c.StyleSheets {
115 for selector := range styleSheet {
116 // fmt.Println(selector, n.Properties.Id)
117 key := selector
118 if strings.Contains(selector, ":hover") && hovered {
119 selector = strings.Replace(selector, ":hover", "", -1)
120 }
121 if element.TestSelector(selector, &n) {
122 for k, v := range styleSheet[key] {
123 styles[k] = v
124 }
125 }
126
127 }
128 }
129
130 // This is different than node.Style
131 // temp1 = <span style=​"color:​#a6e22e">​CSS​</span>​
132 // temp1.style == CSSStyleDeclaration {0: 'color', accentColor: '', additiveSymbols: '', alignContent: '', alignItems: '', alignSelf: '', …}
133 // temp1.getAttribute("style") == 'color:#a6e22e'
134 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
135 styles = utils.Merge(styles, inline)
136 // add hover and focus css events
137
138 if n.Parent != nil {
139 if styles["z-index"] == "" && n.Parent.Style["z-index"] != "" {
140 z, _ := strconv.Atoi(n.Parent.Style["z-index"])
141 z += 1
142 styles["z-index"] = strconv.Itoa(z)
143 }
144 }
145
146 return styles
147}
148
149func (c *CSS) AddPlugin(plugin Plugin) {
150 c.Plugins = append(c.Plugins, plugin)
151}
152
153func (c *CSS) AddTransformer(transformer Transformer) {
154 c.Transformers = append(c.Transformers, transformer)
155}
156
157func CheckNode(n *element.Node, state *map[string]element.State) {
158 s := *state
159 self := s[n.Properties.Id]
160
161 fmt.Println(n.TagName, n.Properties.Id)
162 fmt.Printf("ID: %v\n", n.Id)
163 fmt.Printf("EM: %v\n", self.EM)
164 fmt.Printf("Parent: %v\n", n.Parent.TagName)
165 fmt.Printf("Classes: %v\n", n.ClassList.Classes)
166 fmt.Printf("Text: %v\n", n.InnerText)
167 fmt.Printf("X: %v, Y: %v, Z: %v\n", self.X, self.Y, self.Z)
168 fmt.Printf("Width: %v, Height: %v\n", self.Width, self.Height)
169 fmt.Printf("Styles: %v\n", self.Style)
170 fmt.Printf("Background: %v\n", self.Background)
171 fmt.Printf("Border: %v\n\n\n", self.Border)
172}
173
174func (c *CSS) ComputeNodeStyle(n *element.Node, state *map[string]element.State) *element.Node {
175
176 // Head is not renderable
177 if utils.IsParent(*n, "head") {
178 return n
179 }
180
181 plugins := c.Plugins
182
183 s := *state
184 self := s[n.Properties.Id]
185 parent := s[n.Parent.Properties.Id]
186 self.Style = c.GetStyles(*n)
187
188 self.Background = color.Parse(self.Style, "background")
189 self.Border, _ = CompleteBorder(self.Style, self, parent)
190
191 fs, _ := utils.ConvertToPixels(self.Style["font-size"], parent.EM, parent.Width)
192 self.EM = fs
193
194 if self.Style["display"] == "none" {
195 self.X = 0
196 self.Y = 0
197 self.Width = 0
198 self.Height = 0
199 return n
200 }
201
202 if self.Style["width"] == "" && self.Style["display"] == "block" {
203 self.Style["width"] = "100%"
204 }
205
206 // Set Z index value to be sorted in window
207 if self.Style["z-index"] != "" {
208 z, _ := strconv.Atoi(self.Style["z-index"])
209 self.Z = float32(z)
210 }
211
212 if parent.Z > 0 {
213 self.Z = parent.Z + 1
214 }
215
216 (*state)[n.Properties.Id] = self
217
218 wh := utils.GetWH(*n, state)
219 width := wh.Width
220 height := wh.Height
221
222 x, y := parent.X, parent.Y
223 // !NOTE: Would like to consolidate all XY function into this function like WH
224 offsetX, offsetY := utils.GetXY(n, state)
225 x += offsetX
226 y += offsetY
227
228 var top, left, right, bottom bool = false, false, false, false
229
230 m := utils.GetMP(*n, wh, state, "margin")
231 p := utils.GetMP(*n, wh, state, "padding")
232
233 self.Margin = m
234 self.Padding = p
235
236 if self.Style["position"] == "absolute" {
237 bas := utils.GetPositionOffsetNode(n, state)
238 base := s[bas.Properties.Id]
239 if self.Style["top"] != "" {
240 v, _ := utils.ConvertToPixels(self.Style["top"], self.EM, parent.Width)
241 y = v + base.Y
242 top = true
243 }
244 if self.Style["left"] != "" {
245 v, _ := utils.ConvertToPixels(self.Style["left"], self.EM, parent.Width)
246 x = v + base.X
247 left = true
248 }
249 if self.Style["right"] != "" {
250 v, _ := utils.ConvertToPixels(self.Style["right"], self.EM, parent.Width)
251 x = (base.Width - width) - v
252 right = true
253 }
254 if self.Style["bottom"] != "" {
255 v, _ := utils.ConvertToPixels(self.Style["bottom"], self.EM, parent.Width)
256 y = (base.Height - height) - v
257 bottom = true
258 }
259
260 } else {
261 for i, v := range n.Parent.Children {
262 vState := s[v.Properties.Id]
263 if vState.Style["position"] != "absolute" {
264 if v.Properties.Id == n.Properties.Id {
265 if i-1 > 0 {
266 sib := n.Parent.Children[i-1]
267 sibling := s[sib.Properties.Id]
268 if sibling.Style["position"] != "absolute" {
269 if self.Style["display"] == "inline" {
270 if sibling.Style["display"] == "inline" {
271 y = sibling.Y
272 } else {
273 y = sibling.Y + sibling.Height
274 }
275 } else {
276 y = sibling.Y + sibling.Height + (sibling.Border.Width * 2) + sibling.Margin.Bottom
277 }
278 }
279
280 }
281 break
282 } else if self.Style["display"] != "inline" {
283 vState := s[v.Properties.Id]
284 y += vState.Margin.Top + vState.Margin.Bottom + vState.Padding.Top + vState.Padding.Bottom + vState.Height + (self.Border.Width)
285 }
286 }
287 }
288 }
289
290 // Display modes need to be calculated here
291
292 relPos := !top && !left && !right && !bottom
293
294 if left || relPos {
295 x += m.Left
296 }
297 if top || relPos {
298 y += m.Top
299 }
300 if right {
301 x -= m.Right
302 }
303 if bottom {
304 y -= m.Bottom
305 }
306
307 self.X = x
308 self.Y = y
309 self.Width = width
310 self.Height = height
311 (*state)[n.Properties.Id] = self
312
313 if !utils.ChildrenHaveText(n) && len(n.InnerText) > 0 {
314 // Confirm text exists
315 // words := strings.Split(strings.TrimSpace(n.InnerText), " ")
316 // if len(words) != 1 {
317 // if self.Style["display"] == "inline" {
318 // n.InnerText = words[0]
319 // // for i := 1; i < len(words); i++ {
320 // // el := *n
321 // // el.InnerText = strings.Join(words[1:], " ")
322 // // n.Parent.InsertAfter(el, *n)
323 // // fmt.Println("injetc", el.Properties.Id)
324 // // }
325
326 // } else {
327 // // el := n.CreateElement("notaspan")
328 // // el.InnerText = n.InnerText
329 // // n.AppendChild(el)
330 // // self.Style["font-size"] = parent.Style["font-size"]
331 // // self.EM = parent.EM
332 // // n.InnerText = ""
333 // }
334 // (*state)[n.Properties.Id] = self
335 // }
336 if len(strings.TrimSpace(n.InnerText)) > 0 {
337 n.InnerText = strings.TrimSpace(n.InnerText)
338 self = genTextNode(n, state)
339 }
340 }
341
342 (*state)[n.Properties.Id] = self
343 (*state)[n.Parent.Properties.Id] = parent
344
345 // Call children here
346
347 var childYOffset float32
348 for i := 0; i < len(n.Children); i++ {
349 v := n.Children[i]
350 v.Parent = n
351 // This is were the tainting comes from
352 n.Children[i] = *c.ComputeNodeStyle(&v, state)
353
354 cState := (*state)[n.Children[i].Properties.Id]
355 if self.Style["height"] == "" {
356 if cState.Style["position"] != "absolute" && cState.Y+cState.Height > childYOffset {
357 childYOffset = cState.Y + cState.Height
358 self.Height = (cState.Y - self.Border.Width) - (self.Y) + cState.Height
359 self.Height += cState.Margin.Top
360 self.Height += cState.Margin.Bottom
361 self.Height += cState.Padding.Top
362 self.Height += cState.Padding.Bottom
363 }
364 }
365 if cState.Width > self.Width {
366 self.Width = cState.Width
367 }
368 }
369
370 self.Height += self.Padding.Bottom
371
372 (*state)[n.Properties.Id] = self
373
374 // Sorting the array by the Level field
375 sort.Slice(plugins, func(i, j int) bool {
376 return plugins[i].Level < plugins[j].Level
377 })
378
379 for _, v := range plugins {
380 matches := true
381 for name, value := range v.Styles {
382 if self.Style[name] != value && !(value == "*") && self.Style[name] != "" {
383 matches = false
384 }
385 }
386 if matches {
387 v.Handler(n, state)
388 }
389 }
390
391 // n.InnerHTML = utils.InnerHTML(*n)
392 // tag, closing := utils.NodeToHTML(*n)
393 // n.OuterHTML = tag + n.InnerHTML + closing
394
395 // CheckNode(n, state)
396
397 return n
398}
399
400func CompleteBorder(cssProperties map[string]string, self, parent element.State) (element.Border, error) {
401 // Split the shorthand into components
402 borderComponents := strings.Fields(cssProperties["border"])
403
404 // Ensure there are at least 1 component (width or style or color)
405 if len(borderComponents) >= 1 {
406 width := "0px" // Default width
407 style := "solid"
408 borderColor := "#000000" // Default color
409
410 // Extract style and color if available
411 if len(borderComponents) >= 1 {
412 width = borderComponents[0]
413 }
414
415 // Extract style and color if available
416 if len(borderComponents) >= 2 {
417 style = borderComponents[1]
418 }
419 if len(borderComponents) >= 3 {
420 borderColor = borderComponents[2]
421 }
422
423 parsedColor, _ := color.Color(borderColor)
424
425 w, _ := utils.ConvertToPixels(width, self.EM, parent.Width)
426
427 return element.Border{
428 Width: w,
429 Style: style,
430 Color: parsedColor,
431 Radius: cssProperties["border-radius"],
432 }, nil
433 }
434
435 return element.Border{}, fmt.Errorf("invalid border shorthand format")
436}
437
438func genTextNode(n *element.Node, state *map[string]element.State) element.State {
439 s := *state
440 self := s[n.Properties.Id]
441 parent := s[n.Parent.Properties.Id]
442
443 text := element.Text{}
444
445 bold, italic := false, false
446
447 if self.Style["font-weight"] == "bold" {
448 bold = true
449 }
450
451 if self.Style["font-style"] == "italic" {
452 italic = true
453 }
454
455 if text.Font == nil {
456 f, _ := font.LoadFont(self.Style["font-family"], int(self.EM), bold, italic)
457 text.Font = f
458 }
459
460 letterSpacing, _ := utils.ConvertToPixels(self.Style["letter-spacing"], self.EM, parent.Width)
461 wordSpacing, _ := utils.ConvertToPixels(self.Style["word-spacing"], self.EM, parent.Width)
462 lineHeight, _ := utils.ConvertToPixels(self.Style["line-height"], self.EM, parent.Width)
463 if lineHeight == 0 {
464 lineHeight = self.EM + 3
465 }
466
467 text.LineHeight = int(lineHeight)
468 text.WordSpacing = int(wordSpacing)
469 text.LetterSpacing = int(letterSpacing)
470 wb := " "
471
472 if self.Style["word-wrap"] == "break-word" {
473 wb = ""
474 }
475
476 if self.Style["text-wrap"] == "wrap" || self.Style["text-wrap"] == "balance" {
477 wb = ""
478 }
479
480 var dt float32
481
482 if self.Style["text-decoration-thickness"] == "auto" || self.Style["text-decoration-thickness"] == "" {
483 dt = self.EM / 7
484 } else {
485 dt, _ = utils.ConvertToPixels(self.Style["text-decoration-thickness"], self.EM, parent.Width)
486 }
487
488 col := color.Parse(self.Style, "font")
489
490 self.Color = col
491
492 text.Color = col
493 text.DecorationColor = color.Parse(self.Style, "decoration")
494 text.Align = self.Style["text-align"]
495 text.WordBreak = wb
496 text.WordSpacing = int(wordSpacing)
497 text.LetterSpacing = int(letterSpacing)
498 text.WhiteSpace = self.Style["white-space"]
499 text.DecorationThickness = int(dt)
500 text.Overlined = self.Style["text-decoration"] == "overline"
501 text.Underlined = self.Style["text-decoration"] == "underline"
502 text.LineThrough = self.Style["text-decoration"] == "linethrough"
503 text.EM = int(self.EM)
504 text.Width = int(parent.Width)
505 text.Text = n.InnerText
506
507 if self.Style["word-spacing"] == "" {
508 text.WordSpacing = font.MeasureSpace(&text)
509 }
510
511 img, width := font.Render(&text)
512 self.Texture = img
513
514 if self.Style["height"] == "" {
515 self.Height = float32(text.LineHeight)
516 }
517
518 if self.Style["width"] == "" {
519 self.Width = float32(width)
520 }
521
522 return self
523}