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