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