CStyle
daslk
# StyleSheet?(go)
# StyleTag?(go)
# GetStyles?(go)
# Render?(go)
# AddPlugin?(go)
See /cstyle/plugins/
# hash?(go)
# ComputeNodeStyle?(go)
# InitNode?(go)
# parseBorderShorthand?(go)
# CompleteBorder?(go)
# flatten?(go)
# genTextNode?(go)
1package cstyle
2
3// package aui/goldie
4// https://pkg.go.dev/automated.sh/goldie
5// https://pkg.go.dev/automated.sh/aui
6// https://pkg.go.dev/automated.sh/oat
7
8import (
9 "crypto/md5"
10 "encoding/hex"
11 "fmt"
12 "gui/color"
13 "gui/element"
14 "gui/font"
15 "gui/parser"
16 "gui/utils"
17 "os"
18 "slices"
19 "sort"
20 "strings"
21)
22
23type Plugin struct {
24 Styles map[string]string
25 Level int
26 Handler func(*element.Node)
27}
28
29type CSS struct {
30 Width float32
31 Height float32
32 StyleSheets []map[string]map[string]string
33 Plugins []Plugin
34 Document *element.Node
35}
36
37func (c *CSS) StyleSheet(path string) {
38 // Parse the CSS file
39 dat, err := os.ReadFile(path)
40 utils.Check(err)
41 styles := parser.ParseCSS(string(dat))
42
43 c.StyleSheets = append(c.StyleSheets, styles)
44}
45
46func (c *CSS) StyleTag(css string) {
47 styles := parser.ParseCSS(css)
48 c.StyleSheets = append(c.StyleSheets, styles)
49}
50
51var inheritedProps = []string{
52 "color",
53 "cursor",
54 "font",
55 "font-family",
56 "font-size",
57 "font-style",
58 "font-weight",
59 "letter-spacing",
60 "line-height",
61 "text-align",
62 "text-indent",
63 "text-justify",
64 "text-shadow",
65 "text-transform",
66 "visibility",
67 "word-spacing",
68 "display",
69}
70
71// need to get rid of the .props for the most part all styles should be computed dynamically
72// can keep like focusable and stuff that describes the element
73
74// currently the append child does not work due to the props and other stuff not existing so it fails
75// moving to a real time style compute would fix that
76
77// :hover is parsed correctly but because the hash func doesn't invalidate it becuase the val
78// is updated in the props. change to append :hover to style to create the effect
79// or merge the class with the styles? idk have to think more
80
81func (c *CSS) GetStyles(n element.Node) map[string]string {
82 styles := map[string]string{}
83 for k, v := range n.Style {
84 styles[k] = v
85 }
86 if n.Parent != nil {
87 ps := c.GetStyles(*n.Parent)
88 for _, v := range inheritedProps {
89 if ps[v] != "" {
90 styles[v] = ps[v]
91 }
92 }
93
94 }
95 hovered := false
96 if slices.Contains(n.ClassList.Classes, ":hover") {
97 hovered = true
98 }
99
100 for _, styleSheet := range c.StyleSheets {
101 for selector := range styleSheet {
102 // fmt.Println(selector, n.Properties.Id)
103 key := selector
104 if strings.Contains(selector, ":hover") && hovered {
105 selector = strings.Replace(selector, ":hover", "", -1)
106 }
107 if element.TestSelector(selector, &n) {
108 for k, v := range styleSheet[key] {
109 styles[k] = v
110 }
111 }
112
113 }
114 }
115 inline := parser.ParseStyleAttribute(n.GetAttribute("style") + ";")
116 styles = utils.Merge(styles, inline)
117 // add hover and focus css events
118
119 return styles
120}
121
122func (c *CSS) Render(doc element.Node) []element.Node {
123 return flatten(doc)
124}
125
126func (c *CSS) AddPlugin(plugin Plugin) {
127 c.Plugins = append(c.Plugins, plugin)
128}
129
130func hash(n *element.Node) string {
131 // Create a new FNV-1a hash
132 hasher := md5.New()
133
134 // Extract and sort the keys
135 var keys []string
136 for key := range n.Style {
137 keys = append(keys, key)
138 }
139 sort.Strings(keys)
140
141 // Concatenate all values into a single string
142 var concatenatedValues string
143 for _, key := range keys {
144 concatenatedValues += key + n.Style[key]
145 }
146 concatenatedValues += n.ClassList.Value
147 concatenatedValues += n.Id
148 hasher.Write([]byte(concatenatedValues))
149 sum := hasher.Sum(nil)
150 str := hex.EncodeToString(sum)
151 if n.Properties.Hash != str {
152 fmt.Println(n.Properties.Id)
153 fmt.Println(concatenatedValues)
154 fmt.Println(n.Properties.Hash, str)
155 }
156
157 return str
158}
159
160func (c *CSS) ComputeNodeStyle(n *element.Node) *element.Node {
161 plugins := c.Plugins
162 hv := hash(n)
163 if n.Properties.Hash != hv {
164 fmt.Println("RELOAD")
165 // this is kinda a sloppy way to do this but it works ig
166 n.Style = c.GetStyles(*n)
167 n.Properties.Hash = hv
168 }
169 styleMap := n.Style
170
171 if styleMap["display"] == "none" {
172 n.Properties.X = 0
173 n.Properties.Y = 0
174 n.Properties.Width = 0
175 n.Properties.Height = 0
176 return n
177 }
178
179 width, height := n.Properties.Width, n.Properties.Height
180 x, y := n.Parent.Properties.X, n.Parent.Properties.Y
181
182 var top, left, right, bottom bool = false, false, false, false
183
184 m := utils.GetMP(*n, "margin")
185 p := utils.GetMP(*n, "padding")
186
187 if styleMap["position"] == "absolute" {
188 base := utils.GetPositionOffsetNode(n)
189 if styleMap["top"] != "" {
190 v, _ := utils.ConvertToPixels(styleMap["top"], float32(n.Properties.EM), n.Parent.Properties.Width)
191 y = v + base.Properties.Y
192 top = true
193 }
194 if styleMap["left"] != "" {
195 v, _ := utils.ConvertToPixels(styleMap["left"], float32(n.Properties.EM), n.Parent.Properties.Width)
196 x = v + base.Properties.X
197 left = true
198 }
199 if styleMap["right"] != "" {
200 v, _ := utils.ConvertToPixels(styleMap["right"], float32(n.Properties.EM), n.Parent.Properties.Width)
201 x = (base.Properties.Width - width) - v
202 right = true
203 }
204 if styleMap["bottom"] != "" {
205 v, _ := utils.ConvertToPixels(styleMap["bottom"], float32(n.Properties.EM), n.Parent.Properties.Width)
206 y = (base.Properties.Height - height) - v
207 bottom = true
208 }
209 } else {
210 for i, v := range n.Parent.Children {
211 if v.Properties.Id == n.Properties.Id {
212 if i-1 > 0 {
213 sibling := n.Parent.Children[i-1]
214 if styleMap["display"] == "inline" {
215 if sibling.Style["display"] == "inline" {
216 y = sibling.Properties.Y
217 } else {
218 y = sibling.Properties.Y + sibling.Properties.Height
219 }
220 } else {
221 y = sibling.Properties.Y + sibling.Properties.Height
222 }
223 }
224 break
225 } else if styleMap["display"] != "inline" {
226 mc := utils.GetMP(v, "margin")
227 pc := utils.GetMP(v, "padding")
228 y += mc.Top + mc.Bottom + pc.Top + pc.Bottom + v.Properties.Height
229 }
230 }
231 }
232
233 // Display modes need to be calculated here
234
235 relPos := !top && !left && !right && !bottom
236
237 if left || relPos {
238 x += m.Left
239 }
240 if top || relPos {
241 y += m.Top
242 }
243 if right {
244 x -= m.Right
245 }
246 if bottom {
247 y -= m.Bottom
248 }
249
250 bold, italic := false, false
251
252 if n.Style["font-weight"] == "bold" {
253 bold = true
254 }
255
256 if n.Style["font-style"] == "italic" {
257 italic = true
258 }
259
260 if n.Properties.Text.Font == nil {
261 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
262 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
263 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
264 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
265 if lineHeight == 0 {
266 lineHeight = n.Properties.EM + 3
267 }
268
269 n.Properties.Text.LineHeight = int(lineHeight)
270 n.Properties.Text.Font = f
271 n.Properties.Text.WordSpacing = int(wordSpacing)
272 n.Properties.Text.LetterSpacing = int(letterSpacing)
273 }
274
275 if len(n.Children) == 0 {
276 // Confirm text exists
277 if len(n.InnerText) > 0 && !utils.IsParent(*n, "head") {
278 innerWidth := width
279 innerHeight := height
280 genTextNode(n, &innerWidth, &innerHeight, p)
281 width = innerWidth + p.Left + p.Right
282 height = innerHeight
283 }
284 }
285
286 n.Properties.X = x
287 n.Properties.Y = y
288 n.Properties.Width = width
289 n.Properties.Height = height
290
291 // Call children here
292
293 var childYOffset float32
294 for i, v := range n.Children {
295 v.Parent = n
296 n.Children[i] = *c.ComputeNodeStyle(&v)
297 if styleMap["height"] == "" {
298 if n.Children[i].Style["position"] != "absolute" && n.Children[i].Properties.Y > childYOffset {
299 childYOffset = n.Children[i].Properties.Y
300 m := utils.GetMP(n.Children[i], "margin")
301 p := utils.GetMP(n.Children[i], "padding")
302 n.Properties.Height += n.Children[i].Properties.Height
303 n.Properties.Height += m.Top
304 n.Properties.Height += m.Bottom
305 n.Properties.Height += p.Top
306 n.Properties.Height += p.Bottom
307 }
308
309 }
310 }
311
312 // Sorting the array by the Level field
313 sort.Slice(plugins, func(i, j int) bool {
314 return plugins[i].Level < plugins[j].Level
315 })
316
317 for _, v := range plugins {
318 matches := true
319 for name, value := range v.Styles {
320 if styleMap[name] != value && !(value == "*") {
321 matches = false
322 }
323 }
324 if matches {
325 v.Handler(n)
326 }
327 }
328
329 return n
330}
331
332func InitNode(n *element.Node, c CSS) *element.Node {
333 n.Style = c.GetStyles(*n)
334 border, err := CompleteBorder(n.Style)
335 if err == nil {
336 n.Properties.Border = border
337 }
338
339 fs, _ := utils.ConvertToPixels(n.Style["font-size"], n.Parent.Properties.EM, n.Parent.Properties.Width)
340 n.Properties.EM = fs
341
342 width, _ := utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Width)
343 if n.Style["min-width"] != "" {
344 minWidth, _ := utils.ConvertToPixels(n.Style["min-width"], n.Properties.EM, n.Parent.Properties.Width)
345 width = utils.Max(width, minWidth)
346 }
347
348 if n.Style["max-width"] != "" {
349 maxWidth, _ := utils.ConvertToPixels(n.Style["max-width"], n.Properties.EM, n.Parent.Properties.Width)
350 width = utils.Min(width, maxWidth)
351 }
352
353 height, _ := utils.ConvertToPixels(n.Style["height"], n.Properties.EM, n.Parent.Properties.Height)
354 if n.Style["min-height"] != "" {
355 minHeight, _ := utils.ConvertToPixels(n.Style["min-height"], n.Properties.EM, n.Parent.Properties.Height)
356 height = utils.Max(height, minHeight)
357 }
358
359 if n.Style["max-height"] != "" {
360 maxHeight, _ := utils.ConvertToPixels(n.Style["max-height"], n.Properties.EM, n.Parent.Properties.Height)
361 height = utils.Min(height, maxHeight)
362 }
363
364 n.Properties.Width = width
365 n.Properties.Height = height
366
367 bold, italic := false, false
368
369 if n.Style["font-weight"] == "bold" {
370 bold = true
371 }
372
373 if n.Style["font-style"] == "italic" {
374 italic = true
375 }
376
377 f, _ := font.LoadFont(n.Style["font-family"], int(n.Properties.EM), bold, italic)
378 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, width)
379 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, width)
380 lineHeight, _ := utils.ConvertToPixels(n.Style["line-height"], n.Properties.EM, width)
381 if lineHeight == 0 {
382 lineHeight = n.Properties.EM + 3
383 }
384
385 n.Properties.Text.LineHeight = int(lineHeight)
386 n.Properties.Text.Font = f
387 n.Properties.Text.WordSpacing = int(wordSpacing)
388 n.Properties.Text.LetterSpacing = int(letterSpacing)
389 return n
390}
391
392func parseBorderShorthand(borderShorthand string) (element.Border, error) {
393 // Split the shorthand into components
394 borderComponents := strings.Fields(borderShorthand)
395
396 // Ensure there are at least 1 component (width or style or color)
397 if len(borderComponents) >= 1 {
398 width := "0px" // Default width
399 style := "solid"
400 borderColor := "#000000" // Default color
401
402 // Extract style and color if available
403 if len(borderComponents) >= 1 {
404 width = borderComponents[0]
405 }
406
407 // Extract style and color if available
408 if len(borderComponents) >= 2 {
409 style = borderComponents[1]
410 }
411 if len(borderComponents) >= 3 {
412 borderColor = borderComponents[2]
413 }
414
415 parsedColor, _ := color.Color(borderColor)
416
417 return element.Border{
418 Width: width,
419 Style: style,
420 Color: parsedColor,
421 Radius: "", // Default radius
422 }, nil
423 }
424
425 return element.Border{}, fmt.Errorf("invalid border shorthand format")
426}
427
428func CompleteBorder(cssProperties map[string]string) (element.Border, error) {
429 border, err := parseBorderShorthand(cssProperties["border"])
430 border.Radius = cssProperties["border-radius"]
431
432 return border, err
433}
434
435func flatten(n element.Node) []element.Node {
436 var nodes []element.Node
437 nodes = append(nodes, n)
438
439 children := n.Children
440 if len(children) > 0 {
441 for _, ch := range children {
442 chNodes := flatten(ch)
443 nodes = append(nodes, chNodes...)
444 }
445 }
446 return nodes
447}
448
449func genTextNode(n *element.Node, width, height *float32, p utils.MarginPadding) {
450 wb := " "
451
452 if n.Style["word-wrap"] == "break-word" {
453 wb = ""
454 }
455
456 if n.Style["text-wrap"] == "wrap" || n.Style["text-wrap"] == "balance" {
457 wb = ""
458 }
459
460 letterSpacing, _ := utils.ConvertToPixels(n.Style["letter-spacing"], n.Properties.EM, *width)
461 wordSpacing, _ := utils.ConvertToPixels(n.Style["word-spacing"], n.Properties.EM, *width)
462
463 var dt float32
464
465 if n.Style["text-decoration-thickness"] == "auto" || n.Style["text-decoration-thickness"] == "" {
466 dt = 2
467 } else {
468 dt, _ = utils.ConvertToPixels(n.Style["text-decoration-thickness"], n.Properties.EM, *width)
469 }
470
471 col := color.Parse(n.Style, "font")
472
473 n.Properties.Text.Color = col
474 n.Properties.Text.Align = n.Style["text-align"]
475 n.Properties.Text.WordBreak = wb
476 n.Properties.Text.WordSpacing = int(wordSpacing)
477 n.Properties.Text.LetterSpacing = int(letterSpacing)
478 n.Properties.Text.WhiteSpace = n.Style["white-space"]
479 n.Properties.Text.DecorationThickness = int(dt)
480 n.Properties.Text.Overlined = n.Style["text-decoration"] == "overline"
481 n.Properties.Text.Underlined = n.Style["text-decoration"] == "underline"
482 n.Properties.Text.LineThrough = n.Style["text-decoration"] == "linethrough"
483 n.Properties.Text.EM = int(n.Properties.EM)
484 n.Properties.Text.Width = int(n.Parent.Properties.Width)
485
486 if n.Style["word-spacing"] == "" {
487 n.Properties.Text.WordSpacing = font.MeasureSpace(&n.Properties.Text)
488 }
489 if n.Parent.Properties.Width != 0 && n.Style["display"] != "inline" && n.Style["width"] == "" {
490 *width = (n.Parent.Properties.Width - p.Right) - p.Left
491 } else if n.Style["width"] == "" {
492 *width = utils.Max(*width, float32(font.MeasureLongest(n)))
493 } else if n.Style["width"] != "" {
494 *width, _ = utils.ConvertToPixels(n.Style["width"], n.Properties.EM, n.Parent.Properties.Width)
495 }
496
497 n.Properties.Text.Width = int(*width)
498 h := font.Render(n)
499 if n.Style["height"] == "" {
500 *height = h
501 }
502
503}