t

thomas / gistfile1.txt

Last active 2 hours ago

Like 0
go ok
render.go Raw
1package server
2
3import (
4 gojson "encoding/json"
5 "errors"
6 "fmt"
7 htmlpkg "html"
8 "html/template"
9 "io"
10 "io/fs"
11 "net/http"
12 "net/url"
13 "path/filepath"
14 "regexp"
15 "strconv"
16 "strings"
17 "time"
18
19 "github.com/dustin/go-humanize"
20 "github.com/labstack/echo/v4"
21 "github.com/rs/zerolog/log"
22 "github.com/thomiceli/opengist/internal/config"
23 "github.com/thomiceli/opengist/internal/db"
24 "github.com/thomiceli/opengist/internal/index"
25 "github.com/thomiceli/opengist/internal/web/context"
26 "github.com/thomiceli/opengist/internal/web/handlers"
27 "github.com/thomiceli/opengist/public"
28 "github.com/thomiceli/opengist/templates"
29)
30
31type Template struct {
32 templates *template.Template
33 // pages holds the new layout-based templates, keyed by file name (e.g. "all.html").
34 // Each entry is a clone of the base layout with that page's "content" block parsed in.
35 pages map[string]*template.Template
36}
37
38func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
39 if tmpl, ok := t.pages[name]; ok {
40 return tmpl.ExecuteTemplate(w, "base", data)
41 }
42 return t.templates.ExecuteTemplate(w, name, data)
43}
44
45var re = regexp.MustCompile("[^a-z0-9]+")
46
47func (s *Server) setFuncMap() {
48 fm := template.FuncMap{
49 "split": strings.Split,
50 "indexByte": strings.IndexByte,
51 "toInt": func(i string) int {
52 val, _ := strconv.Atoi(i)
53 return val
54 },
55 "inc": func(i int) int {
56 return i + 1
57 },
58 "splitGit": func(i string) []string {
59 return strings.FieldsFunc(i, func(r rune) bool {
60 return r == ',' || r == ' '
61 })
62 },
63 "lines": func(i string) []string {
64 return strings.Split(i, "\n")
65 },
66 "isMarkdown": func(i string) bool {
67 return strings.ToLower(filepath.Ext(i)) == ".md"
68 },
69 "isMermaid": func(i string) bool {
70 return strings.ToLower(filepath.Ext(i)) == ".mmd"
71 },
72 "isJupyter": func(i string) bool {
73 return strings.ToLower(filepath.Ext(i)) == ".ipynb"
74 },
75 "httpStatusText": http.StatusText,
76 "loadedTime": func(startTime time.Time) string {
77 return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
78 },
79 "slug": func(s string) string {
80 return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
81 },
82 "avatarUrl": func(user *db.User, noGravatar bool) string {
83 if user.HasUploadedAvatar() {
84 return fmt.Sprintf("%s/avatar/%s", config.C.ExternalUrl, user.AvatarURL)
85 }
86
87 if user.AvatarURL != "" {
88 return user.AvatarURL
89 }
90
91 if user.MD5Hash != "" && !noGravatar {
92 return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
93 }
94
95 return ""
96 },
97 "shouldGenerateAvatar": func(user *db.User, noGravatar bool) bool {
98 if user == nil {
99 return true
100 }
101 return user.AvatarURL == "" && (user.MD5Hash == "" || noGravatar)
102 },
103 "asset": func(file string) string {
104 if s.dev {
105 return "http://localhost:16157/" + file
106 }
107 return config.C.ExternalUrl + "/" + context.ManifestEntries[file].File
108 },
109 "assetCss": func(file string) string {
110 if s.dev {
111 return "http://localhost:16157/" + file
112 }
113 return config.C.ExternalUrl + "/" + context.ManifestEntries[file].Css[0]
114 },
115 "custom": func(file string) string {
116 assetpath, err := url.JoinPath("/", "assets", file)
117 if err != nil {
118 log.Error().Err(err).Msgf("Failed to join path for custom file %s", file)
119 }
120 return config.C.ExternalUrl + assetpath
121 },
122 "dev": func() bool {
123 return s.dev
124 },
125 "visibilityStr": func(visibility db.Visibility, lowercase bool) string {
126 s := "Public"
127 switch visibility {
128 case 1:
129 s = "Unlisted"
130 case 2:
131 s = "Private"
132 }
133
134 if lowercase {
135 return strings.ToLower(s)
136 }
137 return s
138 },
139 "unescape": htmlpkg.UnescapeString,
140 "join": func(s ...string) string {
141 return strings.Join(s, "")
142 },
143 "toStr": func(i interface{}) string {
144 return fmt.Sprint(i)
145 },
146 "safe": func(s string) template.HTML {
147 return template.HTML(s)
148 },
149 "dict": func(values ...interface{}) (map[string]interface{}, error) {
150 if len(values)%2 != 0 {
151 return nil, errors.New("invalid dict call")
152 }
153 dict := make(map[string]interface{})
154 for i := 0; i < len(values); i += 2 {
155 key, ok := values[i].(string)
156 if !ok {
157 return nil, errors.New("dict keys must be strings")
158 }
159 dict[key] = values[i+1]
160 }
161 return dict, nil
162 },
163 "addMetadataToSearchQuery": func(input, key, value string) string {
164 metadata := handlers.ParseSearchQueryStr(input)
165 // extract free-text content (stored under "all") and remove it from metadata
166 content := metadata["all"]
167 delete(metadata, "all")
168
169 metadata[key] = value
170
171 var resultBuilder strings.Builder
172 resultBuilder.WriteString(content)
173
174 for k, v := range metadata {
175 resultBuilder.WriteString(" ")
176 resultBuilder.WriteString(k)
177 resultBuilder.WriteString(":")
178 resultBuilder.WriteString(v)
179 }
180
181 return strings.TrimSpace(resultBuilder.String())
182 },
183 "indexEnabled": index.IndexEnabled,
184 "isUrl": func(s string) bool {
185 _, err := url.ParseRequestURI(s)
186 return err == nil
187 },
188 "topicsToStr": func(topics []db.GistTopic) string {
189 str := ""
190 for i, topic := range topics {
191 if i > 0 {
192 str += " "
193 }
194 str += topic.Topic
195 }
196 return str
197 },
198 "hexToRgb": func(hex string) string {
199 h, _ := strconv.ParseUint(strings.TrimPrefix(hex, "#"), 16, 32)
200 return fmt.Sprintf("%d, %d, %d,", (h>>16)&0xFF, (h>>8)&0xFF, h&0xFF)
201 },
202 "humanTimeDiff": func(t int64) string {
203 return humanize.Time(time.Unix(t, 0))
204 },
205 "humanTimeDiffStr": func(timestamp string) string {
206 t, _ := strconv.ParseInt(timestamp, 10, 64)
207 return humanize.Time(time.Unix(t, 0))
208 },
209 "humanDate": func(t int64) string {
210 return time.Unix(t, 0).Format("02/01/2006 15:04")
211 },
212 "humanDateOnly": func(t int64) string {
213 return time.Unix(t, 0).Format("02/01/2006")
214 },
215 "mainTheme": func(theme *db.UserStyleDTO) string {
216 if theme == nil {
217 return "auto"
218 }
219
220 if theme.Theme == "" {
221 return "auto"
222 }
223
224 return theme.Theme
225 },
226 }
227
228 base := template.Must(template.New("base").Funcs(fm).ParseFS(templates.Files, "layouts/*.html", "partials/*.html"))
229 pagePaths, err := fs.Glob(templates.Files, "pages/*.html")
230 if err != nil {
231 log.Fatal().Err(err).Msg("Failed to glob new page templates")
232 }
233 pages := make(map[string]*template.Template, len(pagePaths))
234 for _, p := range pagePaths {
235 cloned := template.Must(base.Clone())
236 pages[filepath.Base(p)] = template.Must(cloned.ParseFS(templates.Files, p))
237 }
238
239 s.echo.Renderer = &Template{
240 templates: base,
241 pages: pages,
242 }
243}
244
245func (s *Server) parseManifestEntries() {
246 file, err := public.Files.Open(".vite/manifest.json")
247 if err != nil {
248 log.Fatal().Err(err).Msg("Failed to open manifest.json")
249 }
250 byteValue, err := io.ReadAll(file)
251 if err != nil {
252 log.Fatal().Err(err).Msg("Failed to read manifest.json")
253 }
254 if err = gojson.Unmarshal(byteValue, &context.ManifestEntries); err != nil {
255 log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
256 }
257}
258