1package pages
2
3import (
4 "errors"
5 "fmt"
6 "html"
7 "html/template"
8 "log"
9 "math"
10 "path/filepath"
11 "reflect"
12 "strings"
13 "time"
14
15 "github.com/dustin/go-humanize"
16)
17
18func funcMap() template.FuncMap {
19 return template.FuncMap{
20 "split": func(s string) []string {
21 return strings.Split(s, "\n")
22 },
23 "truncateAt30": func(s string) string {
24 if len(s) <= 30 {
25 return s
26 }
27 return s[:30] + "…"
28 },
29 "splitOn": func(s, sep string) []string {
30 return strings.Split(s, sep)
31 },
32 "add": func(a, b int) int {
33 return a + b
34 },
35 "sub": func(a, b int) int {
36 return a - b
37 },
38 "cond": func(cond interface{}, a, b string) string {
39 if cond == nil {
40 return b
41 }
42
43 if boolean, ok := cond.(bool); boolean && ok {
44 return a
45 }
46
47 return b
48 },
49 "didOrHandle": func(did, handle string) string {
50 if handle != "" {
51 return fmt.Sprintf("@%s", handle)
52 } else {
53 return did
54 }
55 },
56 "assoc": func(values ...string) ([][]string, error) {
57 if len(values)%2 != 0 {
58 return nil, fmt.Errorf("invalid assoc call, must have an even number of arguments")
59 }
60 pairs := make([][]string, 0)
61 for i := 0; i < len(values); i += 2 {
62 pairs = append(pairs, []string{values[i], values[i+1]})
63 }
64 return pairs, nil
65 },
66 "append": func(s []string, values ...string) []string {
67 s = append(s, values...)
68 return s
69 },
70 "timeFmt": humanize.Time,
71 "longTimeFmt": func(t time.Time) string {
72 return t.Format("2006-01-02 * 3:04 PM")
73 },
74 "shortTimeFmt": func(t time.Time) string {
75 return humanize.CustomRelTime(t, time.Now(), "", "", []humanize.RelTimeMagnitude{
76 {time.Second, "now", time.Second},
77 {2 * time.Second, "1s %s", 1},
78 {time.Minute, "%ds %s", time.Second},
79 {2 * time.Minute, "1min %s", 1},
80 {time.Hour, "%dmin %s", time.Minute},
81 {2 * time.Hour, "1hr %s", 1},
82 {humanize.Day, "%dhrs %s", time.Hour},
83 {2 * humanize.Day, "1d %s", 1},
84 {20 * humanize.Day, "%dd %s", humanize.Day},
85 {8 * humanize.Week, "%dw %s", humanize.Week},
86 {humanize.Year, "%dmo %s", humanize.Month},
87 {18 * humanize.Month, "1y %s", 1},
88 {2 * humanize.Year, "2y %s", 1},
89 {humanize.LongTime, "%dy %s", humanize.Year},
90 {math.MaxInt64, "a long while %s", 1},
91 })
92 },
93 "byteFmt": humanize.Bytes,
94 "length": func(slice any) int {
95 v := reflect.ValueOf(slice)
96 if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
97 return v.Len()
98 }
99 return 0
100 },
101 "splitN": func(s, sep string, n int) []string {
102 return strings.SplitN(s, sep, n)
103 },
104 "escapeHtml": func(s string) template.HTML {
105 if s == "" {
106 return template.HTML("<br>")
107 }
108 return template.HTML(s)
109 },
110 "unescapeHtml": func(s string) string {
111 return html.UnescapeString(s)
112 },
113 "nl2br": func(text string) template.HTML {
114 return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
115 },
116 "unwrapText": func(text string) string {
117 paragraphs := strings.Split(text, "\n\n")
118
119 for i, p := range paragraphs {
120 lines := strings.Split(p, "\n")
121 paragraphs[i] = strings.Join(lines, " ")
122 }
123
124 return strings.Join(paragraphs, "\n\n")
125 },
126 "sequence": func(n int) []struct{} {
127 return make([]struct{}, n)
128 },
129 "subslice": func(slice any, start, end int) any {
130 v := reflect.ValueOf(slice)
131 if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
132 return nil
133 }
134 if start < 0 || start > v.Len() || end > v.Len() || start > end {
135 return nil
136 }
137 return v.Slice(start, end).Interface()
138 },
139 "markdown": func(text string) template.HTML {
140 return template.HTML(renderMarkdown(text))
141 },
142 "isNil": func(t any) bool {
143 // returns false for other "zero" values
144 return t == nil
145 },
146 "list": func(args ...any) []any {
147 return args
148 },
149 "dict": func(values ...any) (map[string]any, error) {
150 if len(values)%2 != 0 {
151 return nil, errors.New("invalid dict call")
152 }
153 dict := make(map[string]any, len(values)/2)
154 for i := 0; i < len(values); i += 2 {
155 key, ok := values[i].(string)
156 if !ok {
157 return nil, errors.New("dict keys must be strings")
158 }
159 dict[key] = values[i+1]
160 }
161 return dict, nil
162 },
163 "i": func(name string, classes ...string) template.HTML {
164 data, err := icon(name, classes)
165 if err != nil {
166 log.Printf("icon %s does not exist", name)
167 data, _ = icon("airplay", classes)
168 }
169 return template.HTML(data)
170 },
171 "cssContentHash": CssContentHash,
172 }
173}
174
175func icon(name string, classes []string) (template.HTML, error) {
176 iconPath := filepath.Join("static", "icons", name)
177
178 if filepath.Ext(name) == "" {
179 iconPath += ".svg"
180 }
181
182 data, err := Files.ReadFile(iconPath)
183 if err != nil {
184 return "", fmt.Errorf("icon %s not found: %w", name, err)
185 }
186
187 // Convert SVG data to string
188 svgStr := string(data)
189
190 svgTagEnd := strings.Index(svgStr, ">")
191 if svgTagEnd == -1 {
192 return "", fmt.Errorf("invalid SVG format for icon %s", name)
193 }
194
195 classTag := ` class="` + strings.Join(classes, " ") + `"`
196
197 modifiedSVG := svgStr[:svgTagEnd] + classTag + svgStr[svgTagEnd:]
198 return template.HTML(modifiedSVG), nil
199}