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