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 "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 rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 147 return template.HTML(rctx.RenderMarkdown(text)) 148 }, 149 "isNil": func(t any) bool { 150 // returns false for other "zero" values 151 return t == nil 152 }, 153 "list": func(args ...any) []any { 154 return args 155 }, 156 "dict": func(values ...any) (map[string]any, error) { 157 if len(values)%2 != 0 { 158 return nil, errors.New("invalid dict call") 159 } 160 dict := make(map[string]any, len(values)/2) 161 for i := 0; i < len(values); i += 2 { 162 key, ok := values[i].(string) 163 if !ok { 164 return nil, errors.New("dict keys must be strings") 165 } 166 dict[key] = values[i+1] 167 } 168 return dict, nil 169 }, 170 "i": func(name string, classes ...string) template.HTML { 171 data, err := icon(name, classes) 172 if err != nil { 173 log.Printf("icon %s does not exist", name) 174 data, _ = icon("airplay", classes) 175 } 176 return template.HTML(data) 177 }, 178 "cssContentHash": CssContentHash, 179 "fileTree": filetree.FileTree, 180 } 181} 182 183func icon(name string, classes []string) (template.HTML, error) { 184 iconPath := filepath.Join("static", "icons", name) 185 186 if filepath.Ext(name) == "" { 187 iconPath += ".svg" 188 } 189 190 data, err := Files.ReadFile(iconPath) 191 if err != nil { 192 return "", fmt.Errorf("icon %s not found: %w", name, err) 193 } 194 195 // Convert SVG data to string 196 svgStr := string(data) 197 198 svgTagEnd := strings.Index(svgStr, ">") 199 if svgTagEnd == -1 { 200 return "", fmt.Errorf("invalid SVG format for icon %s", name) 201 } 202 203 classTag := ` class="` + strings.Join(classes, " ") + `"` 204 205 modifiedSVG := svgStr[:svgTagEnd] + classTag + svgStr[svgTagEnd:] 206 return template.HTML(modifiedSVG), nil 207}