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}