forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
Monorepo for Tangled
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package pages
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "html"
10 "html/template"
11 "log"
12 "math"
13 "net/url"
14 "path/filepath"
15 "reflect"
16 "strings"
17 "time"
18
19 "github.com/dustin/go-humanize"
20 "github.com/microcosm-cc/bluemonday"
21 "tangled.sh/tangled.sh/core/appview/filetree"
22 "tangled.sh/tangled.sh/core/appview/pages/markup"
23)
24
25func (p *Pages) funcMap() template.FuncMap {
26 return template.FuncMap{
27 "split": func(s string) []string {
28 return strings.Split(s, "\n")
29 },
30 "truncateAt30": func(s string) string {
31 if len(s) <= 30 {
32 return s
33 }
34 return s[:30] + "…"
35 },
36 "splitOn": func(s, sep string) []string {
37 return strings.Split(s, sep)
38 },
39 "int64": func(a int) int64 {
40 return int64(a)
41 },
42 "add": func(a, b int) int {
43 return a + b
44 },
45 "now": func() time.Time {
46 return time.Now()
47 },
48 // the absolute state of go templates
49 "add64": func(a, b int64) int64 {
50 return a + b
51 },
52 "sub": func(a, b int) int {
53 return a - b
54 },
55 "f64": func(a int) float64 {
56 return float64(a)
57 },
58 "addf64": func(a, b float64) float64 {
59 return a + b
60 },
61 "subf64": func(a, b float64) float64 {
62 return a - b
63 },
64 "mulf64": func(a, b float64) float64 {
65 return a * b
66 },
67 "divf64": func(a, b float64) float64 {
68 if b == 0 {
69 return 0
70 }
71 return a / b
72 },
73 "negf64": func(a float64) float64 {
74 return -a
75 },
76 "cond": func(cond interface{}, a, b string) string {
77 if cond == nil {
78 return b
79 }
80
81 if boolean, ok := cond.(bool); boolean && ok {
82 return a
83 }
84
85 return b
86 },
87 "didOrHandle": func(did, handle string) string {
88 if handle != "" {
89 return fmt.Sprintf("@%s", handle)
90 } else {
91 return did
92 }
93 },
94 "assoc": func(values ...string) ([][]string, error) {
95 if len(values)%2 != 0 {
96 return nil, fmt.Errorf("invalid assoc call, must have an even number of arguments")
97 }
98 pairs := make([][]string, 0)
99 for i := 0; i < len(values); i += 2 {
100 pairs = append(pairs, []string{values[i], values[i+1]})
101 }
102 return pairs, nil
103 },
104 "append": func(s []string, values ...string) []string {
105 s = append(s, values...)
106 return s
107 },
108 "commaFmt": humanize.Comma,
109 "relTimeFmt": humanize.Time,
110 "shortRelTimeFmt": func(t time.Time) string {
111 return humanize.CustomRelTime(t, time.Now(), "", "", []humanize.RelTimeMagnitude{
112 {time.Second, "now", time.Second},
113 {2 * time.Second, "1s %s", 1},
114 {time.Minute, "%ds %s", time.Second},
115 {2 * time.Minute, "1min %s", 1},
116 {time.Hour, "%dmin %s", time.Minute},
117 {2 * time.Hour, "1hr %s", 1},
118 {humanize.Day, "%dhrs %s", time.Hour},
119 {2 * humanize.Day, "1d %s", 1},
120 {20 * humanize.Day, "%dd %s", humanize.Day},
121 {8 * humanize.Week, "%dw %s", humanize.Week},
122 {humanize.Year, "%dmo %s", humanize.Month},
123 {18 * humanize.Month, "1y %s", 1},
124 {2 * humanize.Year, "2y %s", 1},
125 {humanize.LongTime, "%dy %s", humanize.Year},
126 {math.MaxInt64, "a long while %s", 1},
127 })
128 },
129 "longTimeFmt": func(t time.Time) string {
130 return t.Format("Jan 2, 2006, 3:04 PM MST")
131 },
132 "iso8601DateTimeFmt": func(t time.Time) string {
133 return t.Format("2006-01-02T15:04:05-07:00")
134 },
135 "iso8601DurationFmt": func(duration time.Duration) string {
136 days := int64(duration.Hours() / 24)
137 hours := int64(math.Mod(duration.Hours(), 24))
138 minutes := int64(math.Mod(duration.Minutes(), 60))
139 seconds := int64(math.Mod(duration.Seconds(), 60))
140 return fmt.Sprintf("P%dD%dH%dM%dS", days, hours, minutes, seconds)
141 },
142 "durationFmt": func(duration time.Duration) string {
143 return durationFmt(duration, [4]string{"d", "hr", "min", "s"})
144 },
145 "longDurationFmt": func(duration time.Duration) string {
146 return durationFmt(duration, [4]string{"days", "hours", "minutes", "seconds"})
147 },
148 "byteFmt": humanize.Bytes,
149 "length": func(slice any) int {
150 v := reflect.ValueOf(slice)
151 if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
152 return v.Len()
153 }
154 return 0
155 },
156 "splitN": func(s, sep string, n int) []string {
157 return strings.SplitN(s, sep, n)
158 },
159 "escapeHtml": func(s string) template.HTML {
160 if s == "" {
161 return template.HTML("<br>")
162 }
163 return template.HTML(s)
164 },
165 "unescapeHtml": func(s string) string {
166 return html.UnescapeString(s)
167 },
168 "nl2br": func(text string) template.HTML {
169 return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
170 },
171 "unwrapText": func(text string) string {
172 paragraphs := strings.Split(text, "\n\n")
173
174 for i, p := range paragraphs {
175 lines := strings.Split(p, "\n")
176 paragraphs[i] = strings.Join(lines, " ")
177 }
178
179 return strings.Join(paragraphs, "\n\n")
180 },
181 "sequence": func(n int) []struct{} {
182 return make([]struct{}, n)
183 },
184 // take atmost N items from this slice
185 "take": func(slice any, n int) any {
186 v := reflect.ValueOf(slice)
187 if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
188 return nil
189 }
190 if v.Len() == 0 {
191 return nil
192 }
193 return v.Slice(0, min(n, v.Len()-1)).Interface()
194 },
195
196 "markdown": func(text string) template.HTML {
197 rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault}
198 return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text)))
199 },
200 "isNil": func(t any) bool {
201 // returns false for other "zero" values
202 return t == nil
203 },
204 "list": func(args ...any) []any {
205 return args
206 },
207 "dict": func(values ...any) (map[string]any, error) {
208 if len(values)%2 != 0 {
209 return nil, errors.New("invalid dict call")
210 }
211 dict := make(map[string]any, len(values)/2)
212 for i := 0; i < len(values); i += 2 {
213 key, ok := values[i].(string)
214 if !ok {
215 return nil, errors.New("dict keys must be strings")
216 }
217 dict[key] = values[i+1]
218 }
219 return dict, nil
220 },
221 "deref": func(v any) any {
222 val := reflect.ValueOf(v)
223 if val.Kind() == reflect.Ptr && !val.IsNil() {
224 return val.Elem().Interface()
225 }
226 return nil
227 },
228 "i": func(name string, classes ...string) template.HTML {
229 data, err := icon(name, classes)
230 if err != nil {
231 log.Printf("icon %s does not exist", name)
232 data, _ = icon("airplay", classes)
233 }
234 return template.HTML(data)
235 },
236 "cssContentHash": CssContentHash,
237 "fileTree": filetree.FileTree,
238 "pathUnescape": func(s string) string {
239 u, _ := url.PathUnescape(s)
240 return u
241 },
242
243 "tinyAvatar": p.tinyAvatar,
244 }
245}
246
247func (p *Pages) tinyAvatar(handle string) string {
248 handle = strings.TrimPrefix(handle, "@")
249 secret := p.avatar.SharedSecret
250 h := hmac.New(sha256.New, []byte(secret))
251 h.Write([]byte(handle))
252 signature := hex.EncodeToString(h.Sum(nil))
253 return fmt.Sprintf("%s/%s/%s?size=tiny", p.avatar.Host, signature, handle)
254}
255
256func icon(name string, classes []string) (template.HTML, error) {
257 iconPath := filepath.Join("static", "icons", name)
258
259 if filepath.Ext(name) == "" {
260 iconPath += ".svg"
261 }
262
263 data, err := Files.ReadFile(iconPath)
264 if err != nil {
265 return "", fmt.Errorf("icon %s not found: %w", name, err)
266 }
267
268 // Convert SVG data to string
269 svgStr := string(data)
270
271 svgTagEnd := strings.Index(svgStr, ">")
272 if svgTagEnd == -1 {
273 return "", fmt.Errorf("invalid SVG format for icon %s", name)
274 }
275
276 classTag := ` class="` + strings.Join(classes, " ") + `"`
277
278 modifiedSVG := svgStr[:svgTagEnd] + classTag + svgStr[svgTagEnd:]
279 return template.HTML(modifiedSVG), nil
280}
281
282func durationFmt(duration time.Duration, names [4]string) string {
283 days := int64(duration.Hours() / 24)
284 hours := int64(math.Mod(duration.Hours(), 24))
285 minutes := int64(math.Mod(duration.Minutes(), 60))
286 seconds := int64(math.Mod(duration.Seconds(), 60))
287
288 chunks := []struct {
289 name string
290 amount int64
291 }{
292 {names[0], days},
293 {names[1], hours},
294 {names[2], minutes},
295 {names[3], seconds},
296 }
297
298 parts := []string{}
299
300 for _, chunk := range chunks {
301 if chunk.amount != 0 {
302 parts = append(parts, fmt.Sprintf("%d%s", chunk.amount, chunk.name))
303 }
304 }
305
306 return strings.Join(parts, " ")
307}