2d227c4b15299ed4d47c84dc463caa938ea5608a
[bloat] / renderer / renderer.go
1 package renderer
2
3 import (
4         "fmt"
5         "io"
6         "strconv"
7         "strings"
8         "text/template"
9         "time"
10
11         "mastodon"
12 )
13
14 var (
15         icons = map[string]string{
16                 "envelope":          "/static/icons/envelope.png",
17                 "dark-envelope":     "/static/icons/dark-envelope.png",
18                 "globe":             "/static/icons/globe.png",
19                 "dark-globe":        "/static/icons/dark-globe.png",
20                 "liked":             "/static/icons/liked.png",
21                 "dark-liked":        "/static/icons/liked.png",
22                 "link":              "/static/icons/link.png",
23                 "dark-link":         "/static/icons/dark-link.png",
24                 "lock":              "/static/icons/lock.png",
25                 "dark-lock":         "/static/icons/dark-lock.png",
26                 "mail-forward":      "/static/icons/mail-forward.png",
27                 "dark-mail-forward": "/static/icons/dark-mail-forward.png",
28                 "reply":             "/static/icons/reply.png",
29                 "dark-reply":        "/static/icons/dark-reply.png",
30                 "retweet":           "/static/icons/retweet.png",
31                 "dark-retweet":      "/static/icons/dark-retweet.png",
32                 "retweeted":         "/static/icons/retweeted.png",
33                 "dark-retweeted":    "/static/icons/retweeted.png",
34                 "smile-o":           "/static/icons/smile-o.png",
35                 "dark-smile-o":      "/static/icons/dark-smile-o.png",
36                 "star-o":            "/static/icons/star-o.png",
37                 "dark-star-o":       "/static/icons/dark-star-o.png",
38                 "star":              "/static/icons/star.png",
39                 "dark-star":         "/static/icons/dark-star.png",
40                 "unlock-alt":        "/static/icons/unlock-alt.png",
41                 "dark-unlock-alt":   "/static/icons/dark-unlock-alt.png",
42                 "user-plus":         "/static/icons/user-plus.png",
43                 "dark-user-plus":    "/static/icons/dark-user-plus.png",
44         }
45 )
46
47 type TemplateData struct {
48         Data interface{}
49         Ctx  *Context
50 }
51
52 type Renderer interface {
53         RenderSigninPage(ctx *Context, writer io.Writer, data *SigninData) (err error)
54         RenderErrorPage(ctx *Context, writer io.Writer, data *ErrorData)
55         RenderTimelinePage(ctx *Context, writer io.Writer, data *TimelineData) (err error)
56         RenderThreadPage(ctx *Context, writer io.Writer, data *ThreadData) (err error)
57         RenderNotificationPage(ctx *Context, writer io.Writer, data *NotificationData) (err error)
58         RenderUserPage(ctx *Context, writer io.Writer, data *UserData) (err error)
59         RenderAboutPage(ctx *Context, writer io.Writer, data *AboutData) (err error)
60         RenderEmojiPage(ctx *Context, writer io.Writer, data *EmojiData) (err error)
61         RenderLikedByPage(ctx *Context, writer io.Writer, data *LikedByData) (err error)
62         RenderRetweetedByPage(ctx *Context, writer io.Writer, data *RetweetedByData) (err error)
63         RenderFollowingPage(ctx *Context, writer io.Writer, data *FollowingData) (err error)
64         RenderFollowersPage(ctx *Context, writer io.Writer, data *FollowersData) (err error)
65         RenderSearchPage(ctx *Context, writer io.Writer, data *SearchData) (err error)
66         RenderSettingsPage(ctx *Context, writer io.Writer, data *SettingsData) (err error)
67 }
68
69 type renderer struct {
70         template *template.Template
71 }
72
73 func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
74         t := template.New("default")
75         t, err = t.Funcs(template.FuncMap{
76                 "EmojiFilter":             EmojiFilter,
77                 "StatusContentFilter":     StatusContentFilter,
78                 "DisplayInteractionCount": DisplayInteractionCount,
79                 "TimeSince":               TimeSince,
80                 "FormatTimeRFC3339":       FormatTimeRFC3339,
81                 "FormatTimeRFC822":        FormatTimeRFC822,
82                 "GetIcon":                 GetIcon,
83                 "WithContext":             WithContext,
84         }).ParseGlob(templateGlobPattern)
85         if err != nil {
86                 return
87         }
88         return &renderer{
89                 template: t,
90         }, nil
91 }
92
93 func (r *renderer) RenderSigninPage(ctx *Context, writer io.Writer,
94         signinData *SigninData) (err error) {
95         return r.template.ExecuteTemplate(writer, "signin.tmpl", WithContext(signinData, ctx))
96 }
97
98 func (r *renderer) RenderErrorPage(ctx *Context, writer io.Writer,
99         errorData *ErrorData) {
100         r.template.ExecuteTemplate(writer, "error.tmpl", WithContext(errorData, ctx))
101         return
102 }
103
104 func (r *renderer) RenderTimelinePage(ctx *Context, writer io.Writer,
105         data *TimelineData) (err error) {
106         return r.template.ExecuteTemplate(writer, "timeline.tmpl", WithContext(data, ctx))
107 }
108
109 func (r *renderer) RenderThreadPage(ctx *Context, writer io.Writer,
110         data *ThreadData) (err error) {
111         return r.template.ExecuteTemplate(writer, "thread.tmpl", WithContext(data, ctx))
112 }
113
114 func (r *renderer) RenderNotificationPage(ctx *Context, writer io.Writer,
115         data *NotificationData) (err error) {
116         return r.template.ExecuteTemplate(writer, "notification.tmpl", WithContext(data, ctx))
117 }
118
119 func (r *renderer) RenderUserPage(ctx *Context, writer io.Writer,
120         data *UserData) (err error) {
121         return r.template.ExecuteTemplate(writer, "user.tmpl", WithContext(data, ctx))
122 }
123
124 func (r *renderer) RenderAboutPage(ctx *Context, writer io.Writer,
125         data *AboutData) (err error) {
126         return r.template.ExecuteTemplate(writer, "about.tmpl", WithContext(data, ctx))
127 }
128
129 func (r *renderer) RenderEmojiPage(ctx *Context, writer io.Writer,
130         data *EmojiData) (err error) {
131         return r.template.ExecuteTemplate(writer, "emoji.tmpl", WithContext(data, ctx))
132 }
133
134 func (r *renderer) RenderLikedByPage(ctx *Context, writer io.Writer,
135         data *LikedByData) (err error) {
136         return r.template.ExecuteTemplate(writer, "likedby.tmpl", WithContext(data, ctx))
137 }
138
139 func (r *renderer) RenderRetweetedByPage(ctx *Context, writer io.Writer,
140         data *RetweetedByData) (err error) {
141         return r.template.ExecuteTemplate(writer, "retweetedby.tmpl", WithContext(data, ctx))
142 }
143
144 func (r *renderer) RenderFollowingPage(ctx *Context, writer io.Writer,
145         data *FollowingData) (err error) {
146         return r.template.ExecuteTemplate(writer, "following.tmpl", WithContext(data, ctx))
147 }
148
149 func (r *renderer) RenderFollowersPage(ctx *Context, writer io.Writer,
150         data *FollowersData) (err error) {
151         return r.template.ExecuteTemplate(writer, "followers.tmpl", WithContext(data, ctx))
152 }
153
154 func (r *renderer) RenderSearchPage(ctx *Context, writer io.Writer,
155         data *SearchData) (err error) {
156         return r.template.ExecuteTemplate(writer, "search.tmpl", WithContext(data, ctx))
157 }
158
159 func (r *renderer) RenderSettingsPage(ctx *Context, writer io.Writer,
160         data *SettingsData) (err error) {
161         return r.template.ExecuteTemplate(writer, "settings.tmpl", WithContext(data, ctx))
162 }
163
164 func EmojiFilter(content string, emojis []mastodon.Emoji) string {
165         var replacements []string
166         var r string
167         for _, e := range emojis {
168                 r = fmt.Sprintf("<img class=\"status-emoji\" src=\"%s\" alt=\"%s\" title=\"%s\" />",
169                         e.URL, e.ShortCode, e.ShortCode)
170                 replacements = append(replacements, ":"+e.ShortCode+":", r)
171         }
172         return strings.NewReplacer(replacements...).Replace(content)
173 }
174
175 func StatusContentFilter(spoiler string, content string,
176         emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
177
178         var replacements []string
179         var r string
180         if len(spoiler) > 0 {
181                 content = spoiler + "<br />" + content
182         }
183         for _, e := range emojis {
184                 r = fmt.Sprintf("<img class=\"status-emoji\" src=\"%s\" alt=\"%s\" title=\"%s\" />",
185                         e.URL, e.ShortCode, e.ShortCode)
186                 replacements = append(replacements, ":"+e.ShortCode+":", r)
187         }
188         for _, m := range mentions {
189                 replacements = append(replacements, "\""+m.URL+"\"", "\"/user/"+m.ID+"\"")
190         }
191         return strings.NewReplacer(replacements...).Replace(content)
192 }
193
194 func DisplayInteractionCount(c int64) string {
195         if c > 0 {
196                 return strconv.Itoa(int(c))
197         }
198         return ""
199 }
200
201 func TimeSince(t time.Time) string {
202         dur := time.Since(t)
203         s := dur.Seconds()
204         if s < 60 {
205                 return strconv.Itoa(int(s)) + "s"
206         }
207         m := dur.Minutes()
208         if m < 60 {
209                 return strconv.Itoa(int(m)) + "m"
210         }
211         h := dur.Hours()
212         if h < 24 {
213                 return strconv.Itoa(int(h)) + "h"
214         }
215         d := h / 24
216         if d < 30 {
217                 return strconv.Itoa(int(d)) + "d"
218         }
219         mo := d / 30
220         if mo < 12 {
221                 return strconv.Itoa(int(mo)) + "mo"
222         }
223         y := mo / 12
224         return strconv.Itoa(int(y)) + "y"
225 }
226
227 func FormatTimeRFC3339(t time.Time) string {
228         return t.Format(time.RFC3339)
229 }
230
231 func FormatTimeRFC822(t time.Time) string {
232         return t.Format(time.RFC822)
233 }
234
235 func GetIcon(name string, darkMode bool) (icon string) {
236         if darkMode {
237                 name = "dark-" + name
238         }
239         icon, _ = icons[name]
240         return
241 }
242
243 func WithContext(data interface{}, ctx *Context) TemplateData {
244         return TemplateData{data, ctx}
245 }