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