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