Add poll support
[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         "bloat/mastodon"
12 )
13
14 type TemplateData struct {
15         Data interface{}
16         Ctx  *Context
17 }
18
19 type Renderer interface {
20         RenderSigninPage(ctx *Context, writer io.Writer, data *SigninData) (err error)
21         RenderErrorPage(ctx *Context, writer io.Writer, data *ErrorData)
22         RenderTimelinePage(ctx *Context, writer io.Writer, data *TimelineData) (err error)
23         RenderThreadPage(ctx *Context, writer io.Writer, data *ThreadData) (err error)
24         RenderNotificationPage(ctx *Context, writer io.Writer, data *NotificationData) (err error)
25         RenderUserPage(ctx *Context, writer io.Writer, data *UserData) (err error)
26         RenderUserSearchPage(ctx *Context, writer io.Writer, data *UserSearchData) (err error)
27         RenderAboutPage(ctx *Context, writer io.Writer, data *AboutData) (err error)
28         RenderEmojiPage(ctx *Context, writer io.Writer, data *EmojiData) (err error)
29         RenderLikedByPage(ctx *Context, writer io.Writer, data *LikedByData) (err error)
30         RenderRetweetedByPage(ctx *Context, writer io.Writer, data *RetweetedByData) (err error)
31         RenderSearchPage(ctx *Context, writer io.Writer, data *SearchData) (err error)
32         RenderSettingsPage(ctx *Context, writer io.Writer, data *SettingsData) (err error)
33 }
34
35 type renderer struct {
36         template *template.Template
37 }
38
39 func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
40         t := template.New("default")
41         t, err = t.Funcs(template.FuncMap{
42                 "EmojiFilter":             EmojiFilter,
43                 "StatusContentFilter":     StatusContentFilter,
44                 "DisplayInteractionCount": DisplayInteractionCount,
45                 "TimeSince":               TimeSince,
46                 "TimeUntil":               TimeUntil,
47                 "FormatTimeRFC3339":       FormatTimeRFC3339,
48                 "FormatTimeRFC822":        FormatTimeRFC822,
49                 "WithContext":             WithContext,
50         }).ParseGlob(templateGlobPattern)
51         if err != nil {
52                 return
53         }
54         return &renderer{
55                 template: t,
56         }, nil
57 }
58
59 func (r *renderer) RenderSigninPage(ctx *Context, writer io.Writer,
60         signinData *SigninData) (err error) {
61         return r.template.ExecuteTemplate(writer, "signin.tmpl", WithContext(signinData, ctx))
62 }
63
64 func (r *renderer) RenderErrorPage(ctx *Context, writer io.Writer,
65         errorData *ErrorData) {
66         r.template.ExecuteTemplate(writer, "error.tmpl", WithContext(errorData, ctx))
67         return
68 }
69
70 func (r *renderer) RenderTimelinePage(ctx *Context, writer io.Writer,
71         data *TimelineData) (err error) {
72         return r.template.ExecuteTemplate(writer, "timeline.tmpl", WithContext(data, ctx))
73 }
74
75 func (r *renderer) RenderThreadPage(ctx *Context, writer io.Writer,
76         data *ThreadData) (err error) {
77         return r.template.ExecuteTemplate(writer, "thread.tmpl", WithContext(data, ctx))
78 }
79
80 func (r *renderer) RenderNotificationPage(ctx *Context, writer io.Writer,
81         data *NotificationData) (err error) {
82         return r.template.ExecuteTemplate(writer, "notification.tmpl", WithContext(data, ctx))
83 }
84
85 func (r *renderer) RenderUserPage(ctx *Context, writer io.Writer,
86         data *UserData) (err error) {
87         return r.template.ExecuteTemplate(writer, "user.tmpl", WithContext(data, ctx))
88 }
89
90 func (r *renderer) RenderUserSearchPage(ctx *Context, writer io.Writer,
91         data *UserSearchData) (err error) {
92         return r.template.ExecuteTemplate(writer, "usersearch.tmpl", WithContext(data, ctx))
93 }
94
95 func (r *renderer) RenderAboutPage(ctx *Context, writer io.Writer,
96         data *AboutData) (err error) {
97         return r.template.ExecuteTemplate(writer, "about.tmpl", WithContext(data, ctx))
98 }
99
100 func (r *renderer) RenderEmojiPage(ctx *Context, writer io.Writer,
101         data *EmojiData) (err error) {
102         return r.template.ExecuteTemplate(writer, "emoji.tmpl", WithContext(data, ctx))
103 }
104
105 func (r *renderer) RenderLikedByPage(ctx *Context, writer io.Writer,
106         data *LikedByData) (err error) {
107         return r.template.ExecuteTemplate(writer, "likedby.tmpl", WithContext(data, ctx))
108 }
109
110 func (r *renderer) RenderRetweetedByPage(ctx *Context, writer io.Writer,
111         data *RetweetedByData) (err error) {
112         return r.template.ExecuteTemplate(writer, "retweetedby.tmpl", WithContext(data, ctx))
113 }
114
115 func (r *renderer) RenderSearchPage(ctx *Context, writer io.Writer,
116         data *SearchData) (err error) {
117         return r.template.ExecuteTemplate(writer, "search.tmpl", WithContext(data, ctx))
118 }
119
120 func (r *renderer) RenderSettingsPage(ctx *Context, writer io.Writer,
121         data *SettingsData) (err error) {
122         return r.template.ExecuteTemplate(writer, "settings.tmpl", WithContext(data, ctx))
123 }
124
125 func EmojiFilter(content string, emojis []mastodon.Emoji) string {
126         var replacements []string
127         var r string
128         for _, e := range emojis {
129                 r = fmt.Sprintf("<img class=\"status-emoji\" src=\"%s\" alt=\"%s\" title=\"%s\" />",
130                         e.URL, e.ShortCode, e.ShortCode)
131                 replacements = append(replacements, ":"+e.ShortCode+":", r)
132         }
133         return strings.NewReplacer(replacements...).Replace(content)
134 }
135
136 func StatusContentFilter(spoiler string, content string,
137         emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
138
139         var replacements []string
140         var r string
141         if len(spoiler) > 0 {
142                 content = spoiler + "<br />" + content
143         }
144         for _, e := range emojis {
145                 r = fmt.Sprintf("<img class=\"status-emoji\" src=\"%s\" alt=\"%s\" title=\"%s\" />",
146                         e.URL, e.ShortCode, e.ShortCode)
147                 replacements = append(replacements, ":"+e.ShortCode+":", r)
148         }
149         for _, m := range mentions {
150                 replacements = append(replacements, "\""+m.URL+"\"", "\"/user/"+m.ID+"\"")
151         }
152         return strings.NewReplacer(replacements...).Replace(content)
153 }
154
155 func DisplayInteractionCount(c int64) string {
156         if c > 0 {
157                 return strconv.Itoa(int(c))
158         }
159         return ""
160 }
161
162 func DurToStr(dur time.Duration) string {
163         s := dur.Seconds()
164         if s < 60 {
165                 return strconv.Itoa(int(s)) + "s"
166         }
167         m := dur.Minutes()
168         if m < 60 {
169                 return strconv.Itoa(int(m)) + "m"
170         }
171         h := dur.Hours()
172         if h < 24 {
173                 return strconv.Itoa(int(h)) + "h"
174         }
175         d := h / 24
176         if d < 30 {
177                 return strconv.Itoa(int(d)) + "d"
178         }
179         mo := d / 30
180         if mo < 12 {
181                 return strconv.Itoa(int(mo)) + "mo"
182         }
183         y := mo / 12
184         return strconv.Itoa(int(y)) + "y"
185 }
186
187 func TimeSince(t time.Time) string {
188         return DurToStr(time.Since(t))
189 }
190
191 func TimeUntil(t time.Time) string {
192         return DurToStr(time.Until(t))
193 }
194
195 func FormatTimeRFC3339(t time.Time) string {
196         return t.Format(time.RFC3339)
197 }
198
199 func FormatTimeRFC822(t time.Time) string {
200         return t.Format(time.RFC822)
201 }
202
203 func WithContext(data interface{}, ctx *Context) TemplateData {
204         return TemplateData{data, ctx}
205 }