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