Merge branch 'master' of https://git.freesoftwareextremist.com/bloat
[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 Page string
15
16 const (
17         SigninPage       = "signin.tmpl"
18         ErrorPage        = "error.tmpl"
19         NavPage          = "nav.tmpl"
20         RootPage         = "root.tmpl"
21         TimelinePage     = "timeline.tmpl"
22         ThreadPage       = "thread.tmpl"
23         NotificationPage = "notification.tmpl"
24         UserPage         = "user.tmpl"
25         UserSearchPage   = "usersearch.tmpl"
26         AboutPage        = "about.tmpl"
27         EmojiPage        = "emoji.tmpl"
28         LikedByPage      = "likedby.tmpl"
29         RetweetedByPage  = "retweetedby.tmpl"
30         SearchPage       = "search.tmpl"
31         SettingsPage     = "settings.tmpl"
32 )
33
34 type TemplateData struct {
35         Data interface{}
36         Ctx  *Context
37 }
38
39 func emojiFilter(content string, emojis []mastodon.Emoji) string {
40         var replacements []string
41         var r string
42         for _, e := range emojis {
43                 r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"24\" />",
44                         e.URL, e.ShortCode, e.ShortCode)
45                 replacements = append(replacements, ":"+e.ShortCode+":", r)
46         }
47         return strings.NewReplacer(replacements...).Replace(content)
48 }
49
50 func statusContentFilter(spoiler string, content string,
51         emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
52
53         var replacements []string
54         var r string
55         if len(spoiler) > 0 {
56                 content = spoiler + "<br />" + content
57         }
58         for _, e := range emojis {
59                 r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"32\" />",
60                         e.URL, e.ShortCode, e.ShortCode)
61                 replacements = append(replacements, ":"+e.ShortCode+":", r)
62         }
63         for _, m := range mentions {
64                 replacements = append(replacements, "\""+m.URL+"\"", "\"/user/"+m.ID+"\"")
65         }
66         return strings.NewReplacer(replacements...).Replace(content)
67 }
68
69 func statusColor(acct string) string {
70         a, b := byte(0), byte(0)
71         for i := 0; i < len(acct); i++ {
72                 if acct[i] == '@' {
73                         a, b = b, a
74                 } else {
75                         a = (a + acct[i]) % 16
76                 }
77         }
78         return fmt.Sprintf("status-color-%x%x", a, b)
79 }
80
81 func displayInteractionCount(c int64) string {
82         if c > 0 {
83                 return strconv.Itoa(int(c))
84         }
85         return ""
86 }
87
88 func DurToStr(dur time.Duration) string {
89         s := dur.Seconds()
90         if s < 60 {
91                 return strconv.Itoa(int(s)) + "s"
92         }
93         m := dur.Minutes()
94         if m < 60*3 {
95                 return strconv.Itoa(int(m)) + "m"
96         }
97         h := dur.Hours()
98         if h < 24*2 {
99                 return strconv.Itoa(int(h)) + "h"
100         }
101         d := h / 24
102         if d < 30*3 {
103                 return strconv.Itoa(int(d)) + "d"
104         }
105         mo := d / 30
106         if mo < 12*3 {
107                 return strconv.Itoa(int(mo)) + "mo"
108         }
109         y := mo / 12
110         return strconv.Itoa(int(y)) + "y"
111 }
112
113 func timeSince(t time.Time) string {
114         d := time.Since(t)
115         if d < 0 {
116                 d = 0
117         }
118         return DurToStr(d)
119 }
120
121 func timeUntil(t time.Time) string {
122         d := time.Until(t)
123         if d < 0 {
124                 d = 0
125         }
126         return DurToStr(d)
127 }
128
129 func formatTimeRFC3339(t time.Time) string {
130         return t.Format(time.RFC3339)
131 }
132
133 func formatTimeRFC822(t time.Time) string {
134         return t.Format(time.RFC822)
135 }
136
137 func withContext(data interface{}, ctx *Context) TemplateData {
138         return TemplateData{data, ctx}
139 }
140
141 type Renderer interface {
142         Render(ctx *Context, writer io.Writer, page string, data interface{}) (err error)
143 }
144
145 type renderer struct {
146         template *template.Template
147 }
148
149 func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
150         t := template.New("default")
151         t, err = t.Funcs(template.FuncMap{
152                 "EmojiFilter":             emojiFilter,
153                 "StatusContentFilter":     statusContentFilter,
154                 "StatusColor":             statusColor,
155                 "DisplayInteractionCount": displayInteractionCount,
156                 "TimeSince":               timeSince,
157                 "TimeUntil":               timeUntil,
158                 "FormatTimeRFC3339":       formatTimeRFC3339,
159                 "FormatTimeRFC822":        formatTimeRFC822,
160                 "WithContext":             withContext,
161         }).ParseGlob(templateGlobPattern)
162         if err != nil {
163                 return
164         }
165         return &renderer{
166                 template: t,
167         }, nil
168 }
169
170 func (r *renderer) Render(ctx *Context, writer io.Writer,
171         page string, data interface{}) (err error) {
172         return r.template.ExecuteTemplate(writer, page, withContext(data, ctx))
173 }