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