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}
-