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