Utils
dsa
# MapToInline?(go)
# InlineToMap?(go)
# GetWH?(go)
# GetMP?(go)
# convertMarginToIndividualProperties?(go)
# ConvertToPixels?(go)
# evaluateCalcExpression?(go)
# GetTextBounds?(go)
# Merge?(go)
# ExMerge?(go)
# Max?(go)
# Min?(go)
# FindRelative?(go)
# ParseFloat?(go)
# GetStructField?(go)
# SetStructFieldValue?(go)
# Check?(go)
# GetInnerText?(go)
# GetPositionOffsetNode?(go)
# IsParent?(go)
1package utils
2
3import (
4 "fmt"
5 "gui/element"
6 "gui/font"
7 "math"
8 "reflect"
9 "regexp"
10 "strconv"
11 "strings"
12
13 "golang.org/x/net/html"
14)
15
16// MapToInlineCSS converts a map[string]string to a string formatted like inline CSS style
17func MapToInline(m map[string]string) string {
18 var cssStrings []string
19 for key, value := range m {
20 cssStrings = append(cssStrings, fmt.Sprintf("%s: %s;", key, value))
21 }
22 return strings.Join(cssStrings, " ")
23}
24
25// InlineCSSToMap converts a string formatted like inline CSS style to a map[string]string
26func InlineToMap(cssString string) map[string]string {
27 cssMap := make(map[string]string)
28 declarations := strings.Split(cssString, ";")
29 for _, declaration := range declarations {
30 parts := strings.Split(strings.TrimSpace(declaration), ":")
31 if len(parts) == 2 {
32 key := strings.TrimSpace(parts[0])
33 value := strings.TrimSpace(parts[1])
34 cssMap[key] = value
35 }
36 }
37 return cssMap
38}
39
40type WidthHeight struct {
41 Width float32
42 Height float32
43}
44
45// !ISSUE: Need to apply the margin and padding to the width/height
46// + right now the body width is 834 and the div inside is 850 bc the margin 8 isn't applied
47func GetWH(n element.Node) WidthHeight {
48 fs := font.GetFontSize(n.Style)
49
50 var pwh WidthHeight
51 if n.Parent != nil {
52 pwh = GetWH(*n.Parent)
53 } else {
54 pwh = WidthHeight{}
55 // if n.Properties.Computed["width"] == 0 {
56 if n.Style["width"] != "" {
57 str := strings.TrimSuffix(n.Style["width"], "px")
58 // Convert the string to float32
59 f, _ := strconv.ParseFloat(str, 32)
60 pwh.Width = float32(f)
61 }
62 // } else {
63 // pwh.Width = n.Properties.Computed["width"]
64 // }
65 // if n.Properties.Computed["width"] == 0 {
66
67 if n.Style["height"] != "" {
68 str := strings.TrimSuffix(n.Style["height"], "px")
69 // Convert the string to float32
70 f, _ := strconv.ParseFloat(str, 32)
71 pwh.Height = float32(f)
72 }
73 // } else {
74 // pwh.Height = n.Properties.Computed["height"]
75 // }
76 }
77 var width float32
78 // if n.Properties.Computed["width"] == 0 {
79 if n.Style["width"] == "" && n.Style["display"] != "inline" {
80 n.Style["width"] = "100%"
81
82 }
83 width, _ = ConvertToPixels(n.Style["width"], fs, pwh.Width)
84 // n.Properties.Computed["width"] = width
85 // } else {
86 // width, _ = ConvertToPixels(n.Style["width"], fs, pwh.Width)
87 // n.Properties.Computed["width"] = width
88
89 // // width = n.Properties.Computed["width"]
90 // }
91
92 var height float32
93 // if n.Properties.Computed["height"] == 0 {
94 height, _ = ConvertToPixels(n.Style["height"], fs, pwh.Height)
95 // n.Properties.Computed["height"] = height
96 // } else {
97 // height = n.Properties.Computed["height"]
98 // }
99
100 if n.Style["min-width"] != "" {
101 minWidth, _ := ConvertToPixels(n.Style["min-width"], fs, pwh.Width)
102 width = Max(width, minWidth)
103 }
104
105 if n.Style["max-width"] != "" {
106 maxWidth, _ := ConvertToPixels(n.Style["max-width"], fs, pwh.Width)
107 width = Min(width, maxWidth)
108 }
109 if n.Style["min-height"] != "" {
110 minHeight, _ := ConvertToPixels(n.Style["min-height"], fs, pwh.Height)
111 height = Max(height, minHeight)
112 }
113
114 if n.Style["max-height"] != "" {
115 maxHeight, _ := ConvertToPixels(n.Style["max-height"], fs, pwh.Height)
116 height = Min(height, maxHeight)
117 }
118 return WidthHeight{
119 Width: width,
120 Height: height,
121 }
122}
123
124func GetMP(n element.Node, t string) element.MarginPadding {
125 fs := font.GetFontSize(n.Style)
126 m := element.MarginPadding{}
127
128 wh := GetWH(n)
129
130 if n.Style[t] != "" {
131 left, right, top, bottom := convertMarginToIndividualProperties(n.Style[t])
132 if n.Style[t+"-left"] == "" {
133 n.Style[t+"-left"] = left
134 }
135 if n.Style[t+"-right"] == "" {
136 n.Style[t+"-right"] = right
137 }
138 if n.Style[t+"-top"] == "" {
139 n.Style[t+"-top"] = top
140 }
141 if n.Style[t+"-bottom"] == "" {
142 n.Style[t+"-bottom"] = bottom
143 }
144 }
145 if n.Style[t+"-left"] != "" || n.Style[t+"-right"] != "" {
146 l, _ := ConvertToPixels(n.Style[t+"-left"], fs, wh.Width)
147 r, _ := ConvertToPixels(n.Style[t+"-right"], fs, wh.Width)
148 m.Left = l
149 m.Right = r
150 }
151 if n.Style[t+"-top"] != "" || n.Style[t+"-bottom"] != "" {
152 top, _ := ConvertToPixels(n.Style[t+"-top"], fs, wh.Height)
153 b, _ := ConvertToPixels(n.Style[t+"-bottom"], fs, wh.Height)
154 m.Top = top
155 m.Bottom = b
156 }
157 if t == "margin" {
158 if n.Style["margin"] == "auto" && n.Style["margin-left"] == "" && n.Style["margin-right"] == "" {
159 pwh := GetWH(*n.Parent)
160 m.Left = Max((pwh.Width-wh.Width)/2, 0)
161 m.Right = m.Left
162 }
163 }
164
165 return m
166}
167
168func convertMarginToIndividualProperties(margin string) (string, string, string, string) {
169 // Remove extra whitespace
170 margin = strings.TrimSpace(margin)
171
172 if margin == "" {
173 return "0px", "0px", "0px", "0px"
174 }
175
176 // Regular expression to match values with optional units
177 re := regexp.MustCompile(`(-?\d+(\.\d+)?)(\w*|\%)?`)
178
179 // Extract numerical values from the margin property
180 matches := re.FindAllStringSubmatch(margin, -1)
181
182 // Initialize variables for individual margins
183 var left, right, top, bottom string
184
185 switch len(matches) {
186 case 1:
187 // If only one value is provided, apply it to all margins
188 left = matches[0][0]
189 right = matches[0][0]
190 top = matches[0][0]
191 bottom = matches[0][0]
192 case 2:
193 // If two values are provided, apply the first to top and bottom, and the second to left and right
194 top = matches[0][0]
195 bottom = matches[0][0]
196 left = matches[1][0]
197 right = matches[1][0]
198 case 3:
199 // If three values are provided, apply the first to top, the second to left and right, and the third to bottom
200 top = matches[0][0]
201 left = matches[1][0]
202 right = matches[1][0]
203 bottom = matches[2][0]
204 case 4:
205 // If four values are provided, apply them to top, right, bottom, and left, respectively
206 top = matches[0][0]
207 right = matches[1][0]
208 bottom = matches[2][0]
209 left = matches[3][0]
210 }
211
212 return left, right, top, bottom
213}
214
215// ConvertToPixels converts a CSS measurement to pixels.
216func ConvertToPixels(value string, em, max float32) (float32, error) {
217 unitFactors := map[string]float32{
218 "px": 1,
219 "em": em,
220 "pt": 1.33,
221 "pc": 16.89,
222 "%": max / 100,
223 "vw": max / 100,
224 "vh": max / 100,
225 "cm": 37.79527559,
226 "in": 96,
227 }
228
229 if strings.HasPrefix(value, "calc(") {
230 // Handle calculation expression
231 calcResult, err := evaluateCalcExpression(value[5:len(value)-1], em, max)
232 if err != nil {
233 return 0, err
234 }
235 return calcResult, nil
236 } else {
237 // Extract numeric value and unit
238 for k, v := range unitFactors {
239 if strings.HasSuffix(value, k) {
240 cutStr, _ := strings.CutSuffix(value, k)
241 numericValue, _ := strconv.ParseFloat(cutStr, 64)
242 return float32(numericValue) * v, nil
243 }
244 }
245 return 0, fmt.Errorf("unable to parse value")
246 }
247
248}
249
250// evaluateCalcExpression recursively evaluates 'calc()' expressions
251func evaluateCalcExpression(expression string, em, max float32) (float32, error) {
252 terms := strings.FieldsFunc(expression, func(c rune) bool {
253 return c == '+' || c == '-' || c == '*' || c == '/'
254 })
255
256 operators := strings.FieldsFunc(expression, func(c rune) bool {
257 return c != '+' && c != '-' && c != '*' && c != '/'
258 })
259
260 var result float32
261
262 for i, term := range terms {
263 value, err := ConvertToPixels(strings.TrimSpace(term), em, max)
264 if err != nil {
265 return 0, err
266 }
267
268 if i > 0 {
269 switch operators[i-1] {
270 case "+":
271 result += value
272 case "-":
273 result -= value
274 case "*":
275 result *= value
276 case "/":
277 if value != 0 {
278 result /= value
279 } else {
280 return 0, fmt.Errorf("division by zero in 'calc()' expression")
281 }
282 }
283 } else {
284 result = value
285 }
286 }
287
288 return result, nil
289}
290
291func GetTextBounds(text string, fontSize, width, height float32) (float32, float32) {
292 w := float32(len(text) * int(fontSize))
293 h := fontSize
294 if width > 0 && height > 0 {
295 if w > width {
296 height = Max(height, float32(math.Ceil(float64(w/width)))*h)
297 }
298 return width, height
299 } else {
300 return w, h
301 }
302
303}
304
305func Merge(m1, m2 map[string]string) map[string]string {
306 // Create a new map and copy m1 into it
307 result := make(map[string]string)
308 for k, v := range m1 {
309 result[k] = v
310 }
311
312 // Merge m2 into the new map
313 for k, v := range m2 {
314 result[k] = v
315 }
316
317 return result
318}
319
320func ExMerge(m1, m2 map[string]string) map[string]string {
321 // Create a new map and copy m1 into it
322 result := make(map[string]string)
323 for k, v := range m1 {
324 result[k] = v
325 }
326
327 // Merge m2 into the new map only if the key is not already present
328 for k, v := range m2 {
329 if result[k] == "" {
330 result[k] = v
331 }
332 }
333
334 return result
335}
336
337func Max(a, b float32) float32 {
338 if a > b {
339 return a
340 } else {
341 return b
342 }
343}
344
345func Min(a, b float32) float32 {
346 if a < b {
347 return a
348 } else {
349 return b
350 }
351}
352
353func FindRelative(n *element.Node, styleMap map[string]map[string]string) (float32, float32) {
354 pos := styleMap[n.Properties.Id]["position"]
355
356 if pos == "relative" {
357 x, _ := strconv.ParseFloat(styleMap[n.Properties.Id]["x"], 32)
358 y, _ := strconv.ParseFloat(styleMap[n.Properties.Id]["y"], 32)
359 return float32(x), float32(y)
360 } else {
361 if n.Parent != nil {
362 x, y := FindRelative(n.Parent, styleMap)
363 return x, y
364 } else {
365 return 0, 0
366 }
367 }
368}
369
370func ParseFloat(str string, def float32) float32 {
371 var a float32
372 if str == "" {
373 a = 0
374 } else {
375 v, _ := strconv.ParseFloat(str, 32)
376 a = float32(v)
377 }
378 return a
379}
380
381// getStructField uses reflection to get the value of a struct field by name
382func GetStructField(data interface{}, fieldName string) (interface{}, error) {
383 val := reflect.ValueOf(data)
384
385 // Make sure we have a pointer to a struct
386 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
387 return nil, fmt.Errorf("expected a pointer to a struct")
388 }
389
390 // Get the struct field by name
391 field := val.Elem().FieldByName(fieldName)
392
393 // Check if the field exists
394 if !field.IsValid() {
395 return nil, fmt.Errorf("field not found: %s", fieldName)
396 }
397
398 return field.Interface(), nil
399}
400
401func SetStructFieldValue(data interface{}, fieldName string, newValue interface{}) error {
402 val := reflect.ValueOf(data)
403
404 // Make sure we have a pointer to a struct
405 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
406 return fmt.Errorf("expected a pointer to a struct")
407 }
408
409 // Get the struct field by name
410 field := val.Elem().FieldByName(fieldName)
411
412 // Check if the field exists
413 if !field.IsValid() {
414 return fmt.Errorf("field not found: %s", fieldName)
415 }
416
417 // Check if the new value type is assignable to the field type
418 if !reflect.ValueOf(newValue).Type().AssignableTo(field.Type()) {
419 return fmt.Errorf("incompatible types for field %s", fieldName)
420 }
421
422 // Set the new value
423 field.Set(reflect.ValueOf(newValue))
424
425 return nil
426}
427
428func Check(e error) {
429 if e != nil {
430 panic(e)
431 }
432}
433
434func GetInnerText(n *html.Node) string {
435 var result strings.Builder
436
437 var getText func(*html.Node)
438 getText = func(n *html.Node) {
439 if n.Type == html.TextNode {
440 result.WriteString(n.Data)
441 }
442
443 for c := n.FirstChild; c != nil; c = c.NextSibling {
444 getText(c)
445 }
446 }
447
448 getText(n)
449
450 return result.String()
451}
452
453func GetPositionOffsetNode(n *element.Node) *element.Node {
454 pos := n.Style["position"]
455
456 if pos == "relative" {
457 return n
458 } else {
459 if n.Parent.TagName != "ROOT" {
460 if n.Parent.Style != nil {
461 return GetPositionOffsetNode(n.Parent)
462 } else {
463 return nil
464 }
465 } else {
466 return n.Parent
467 }
468 }
469}
470
471func IsParent(n element.Node, name string) bool {
472 if n.Parent.TagName != "ROOT" {
473 if n.Parent.TagName == name {
474 return true
475 } else {
476 return IsParent(*n.Parent, name)
477 }
478 } else {
479 return false
480 }
481}
482
483func ChildrenHaveText(n *element.Node) bool {
484 for _, child := range n.Children {
485 if len(strings.TrimSpace(child.InnerText)) != 0 {
486 return true
487 }
488 // Recursively check if any child nodes have text
489 if ChildrenHaveText(&child) {
490 return true
491 }
492 }
493 return false
494}