summaryrefslogtreecommitdiff
path: root/internal/template/functions.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/template/functions.go')
-rw-r--r--internal/template/functions.go216
1 files changed, 216 insertions, 0 deletions
diff --git a/internal/template/functions.go b/internal/template/functions.go
new file mode 100644
index 00000000..08b02411
--- /dev/null
+++ b/internal/template/functions.go
@@ -0,0 +1,216 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package template // import "miniflux.app/v2/internal/template"
+
+import (
+ "fmt"
+ "html/template"
+ "math"
+ "net/mail"
+ "strings"
+ "time"
+
+ "miniflux.app/v2/internal/config"
+ "miniflux.app/v2/internal/crypto"
+ "miniflux.app/v2/internal/http/route"
+ "miniflux.app/v2/internal/locale"
+ "miniflux.app/v2/internal/model"
+ "miniflux.app/v2/internal/proxy"
+ "miniflux.app/v2/internal/timezone"
+ "miniflux.app/v2/internal/url"
+
+ "github.com/gorilla/mux"
+)
+
+type funcMap struct {
+ router *mux.Router
+}
+
+// Map returns a map of template functions that are compiled during template parsing.
+func (f *funcMap) Map() template.FuncMap {
+ return template.FuncMap{
+ "formatFileSize": formatFileSize,
+ "dict": dict,
+ "hasKey": hasKey,
+ "truncate": truncate,
+ "isEmail": isEmail,
+ "baseURL": func() string {
+ return config.Opts.BaseURL()
+ },
+ "rootURL": func() string {
+ return config.Opts.RootURL()
+ },
+ "hasOAuth2Provider": func(provider string) bool {
+ return config.Opts.OAuth2Provider() == provider
+ },
+ "hasAuthProxy": func() bool {
+ return config.Opts.AuthProxyHeader() != ""
+ },
+ "route": func(name string, args ...interface{}) string {
+ return route.Path(f.router, name, args...)
+ },
+ "safeURL": func(url string) template.URL {
+ return template.URL(url)
+ },
+ "safeCSS": func(str string) template.CSS {
+ return template.CSS(str)
+ },
+ "noescape": func(str string) template.HTML {
+ return template.HTML(str)
+ },
+ "proxyFilter": func(data string) string {
+ return proxy.ProxyRewriter(f.router, data)
+ },
+ "proxyURL": func(link string) string {
+ proxyOption := config.Opts.ProxyOption()
+
+ if proxyOption == "all" || (proxyOption != "none" && !url.IsHTTPS(link)) {
+ return proxy.ProxifyURL(f.router, link)
+ }
+
+ return link
+ },
+ "mustBeProxyfied": func(mediaType string) bool {
+ for _, t := range config.Opts.ProxyMediaTypes() {
+ if t == mediaType {
+ return true
+ }
+ }
+ return false
+ },
+ "domain": func(websiteURL string) string {
+ return url.Domain(websiteURL)
+ },
+ "hasPrefix": func(str, prefix string) bool {
+ return strings.HasPrefix(str, prefix)
+ },
+ "contains": func(str, substr string) bool {
+ return strings.Contains(str, substr)
+ },
+ "replace": func(str, old, new string) string {
+ return strings.Replace(str, old, new, 1)
+ },
+ "isodate": func(ts time.Time) string {
+ return ts.Format("2006-01-02 15:04:05")
+ },
+ "theme_color": func(theme, colorScheme string) string {
+ return model.ThemeColor(theme, colorScheme)
+ },
+ "icon": func(iconName string) template.HTML {
+ return template.HTML(fmt.Sprintf(
+ `<svg class="icon" aria-hidden="true"><use xlink:href="%s#icon-%s"/></svg>`,
+ route.Path(f.router, "appIcon", "filename", "sprite.svg"),
+ iconName,
+ ))
+ },
+ "nonce": func() string {
+ return crypto.GenerateRandomStringHex(16)
+ },
+ "deRef": func(i *int) int { return *i },
+
+ // These functions are overrode at runtime after the parsing.
+ "elapsed": func(timezone string, t time.Time) string {
+ return ""
+ },
+ "t": func(key interface{}, args ...interface{}) string {
+ return ""
+ },
+ "plural": func(key string, n int, args ...interface{}) string {
+ return ""
+ },
+ }
+}
+
+func dict(values ...interface{}) (map[string]interface{}, error) {
+ if len(values)%2 != 0 {
+ return nil, fmt.Errorf("dict expects an even number of arguments")
+ }
+ dict := make(map[string]interface{}, len(values)/2)
+ for i := 0; i < len(values); i += 2 {
+ key, ok := values[i].(string)
+ if !ok {
+ return nil, fmt.Errorf("dict keys must be strings")
+ }
+ dict[key] = values[i+1]
+ }
+ return dict, nil
+}
+
+func hasKey(dict map[string]string, key string) bool {
+ if value, found := dict[key]; found {
+ return value != ""
+ }
+ return false
+}
+
+func truncate(str string, max int) string {
+ runes := 0
+ for i := range str {
+ runes++
+ if runes > max {
+ return str[:i] + "…"
+ }
+ }
+ return str
+}
+
+func isEmail(str string) bool {
+ _, err := mail.ParseAddress(str)
+ return err == nil
+}
+
+func elapsedTime(printer *locale.Printer, tz string, t time.Time) string {
+ if t.IsZero() {
+ return printer.Printf("time_elapsed.not_yet")
+ }
+
+ now := timezone.Now(tz)
+ t = timezone.Convert(tz, t)
+ if now.Before(t) {
+ return printer.Printf("time_elapsed.not_yet")
+ }
+
+ diff := now.Sub(t)
+ // Duration in seconds
+ s := diff.Seconds()
+ // Duration in days
+ d := int(s / 86400)
+ switch {
+ case s < 60:
+ return printer.Printf("time_elapsed.now")
+ case s < 3600:
+ minutes := int(diff.Minutes())
+ return printer.Plural("time_elapsed.minutes", minutes, minutes)
+ case s < 86400:
+ hours := int(diff.Hours())
+ return printer.Plural("time_elapsed.hours", hours, hours)
+ case d == 1:
+ return printer.Printf("time_elapsed.yesterday")
+ case d < 21:
+ return printer.Plural("time_elapsed.days", d, d)
+ case d < 31:
+ weeks := int(math.Round(float64(d) / 7))
+ return printer.Plural("time_elapsed.weeks", weeks, weeks)
+ case d < 365:
+ months := int(math.Round(float64(d) / 30))
+ return printer.Plural("time_elapsed.months", months, months)
+ default:
+ years := int(math.Round(float64(d) / 365))
+ return printer.Plural("time_elapsed.years", years, years)
+ }
+}
+
+func formatFileSize(b int64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%d B", b)
+ }
+ div, exp := int64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.1f %ciB",
+ float64(b)/float64(div), "KMGTPE"[exp])
+}