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