diff options
author | Frédéric Guillot <fred@miniflux.net> | 2018-02-04 15:45:07 -0800 |
---|---|---|
committer | Frédéric Guillot <fred@miniflux.net> | 2018-02-04 15:45:07 -0800 |
commit | 3884a33b3623ee5166f8254a0919e65be9bfb49b (patch) | |
tree | 9155305707f4d834df03391fd4fc0e74ee998eb3 /template | |
parent | b5b1930599190e2d7a6613db8779d9bbb970a1f7 (diff) |
Move template functions outside engine (refactoring)
Diffstat (limited to 'template')
-rw-r--r-- | template/dict.go | 22 | ||||
-rw-r--r-- | template/dict_test.go | 42 | ||||
-rw-r--r-- | template/elapsed.go | 80 | ||||
-rw-r--r-- | template/elapsed_test.go | 38 | ||||
-rw-r--r-- | template/engine.go | 69 | ||||
-rw-r--r-- | template/functions.go | 105 | ||||
-rw-r--r-- | template/template.go | 167 |
7 files changed, 356 insertions, 167 deletions
diff --git a/template/dict.go b/template/dict.go new file mode 100644 index 0000000..3056bcd --- /dev/null +++ b/template/dict.go @@ -0,0 +1,22 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package template + +import "fmt" + +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 +} diff --git a/template/dict_test.go b/template/dict_test.go new file mode 100644 index 0000000..8b236e0 --- /dev/null +++ b/template/dict_test.go @@ -0,0 +1,42 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package template + +import ( + "testing" +) + +func TestDict(t *testing.T) { + d, err := dict("k1", "v1", "k2", "v2") + if err != nil { + t.Fatalf(`The dict should be valid: %v`, err) + } + + if value, found := d["k1"]; found { + if value != "v1" { + t.Fatalf(`Incorrect value for k1: %q`, value) + } + } + + if value, found := d["k2"]; found { + if value != "v2" { + t.Fatalf(`Incorrect value for k2: %q`, value) + } + } +} + +func TestDictWithIncorrectNumberOfPairs(t *testing.T) { + _, err := dict("k1", "v1", "k2") + if err == nil { + t.Fatalf(`The dict should not be valid because the number of keys/values pairs are incorrect`) + } +} + +func TestDictWithInvalidKey(t *testing.T) { + _, err := dict(1, "v1") + if err == nil { + t.Fatalf(`The dict should not be valid because the key is not a string`) + } +} diff --git a/template/elapsed.go b/template/elapsed.go new file mode 100644 index 0000000..273771d --- /dev/null +++ b/template/elapsed.go @@ -0,0 +1,80 @@ +// Copyright (c) 2017 Hervé Gouchet. All rights reserved. +// Use of this source code is governed by the MIT License +// that can be found in the LICENSE file. + +package template + +import ( + "math" + "time" + + "github.com/miniflux/miniflux/locale" +) + +// Texts to be translated if necessary. +var ( + NotYet = `not yet` + JustNow = `just now` + LastMinute = `1 minute ago` + Minutes = `%d minutes ago` + LastHour = `1 hour ago` + Hours = `%d hours ago` + Yesterday = `yesterday` + Days = `%d days ago` + Weeks = `%d weeks ago` + Months = `%d months ago` + Years = `%d years ago` +) + +// ElapsedTime returns in a human readable format the elapsed time +// since the given datetime. +func elapsedTime(language *locale.Language, timezone string, t time.Time) string { + if t.IsZero() { + return language.Get(NotYet) + } + + var now time.Time + loc, err := time.LoadLocation(timezone) + if err != nil { + now = time.Now() + } else { + now = time.Now().In(loc) + + // The provided date is already converted to the user timezone by Postgres, + // but the timezone information is not set in the time struct. + // We cannot use time.In() because the date will be converted a second time. + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc) + } + + if now.Before(t) { + return language.Get(NotYet) + } + + diff := now.Sub(t) + // Duration in seconds + s := diff.Seconds() + // Duration in days + d := int(s / 86400) + switch { + case s < 60: + return language.Get(JustNow) + case s < 120: + return language.Get(LastMinute) + case s < 3600: + return language.Get(Minutes, int(diff.Minutes())) + case s < 7200: + return language.Get(LastHour) + case s < 86400: + return language.Get(Hours, int(diff.Hours())) + case d == 1: + return language.Get(Yesterday) + case d < 7: + return language.Get(Days, d) + case d < 31: + return language.Get(Weeks, int(math.Ceil(float64(d)/7))) + case d < 365: + return language.Get(Months, int(math.Ceil(float64(d)/30))) + default: + return language.Get(Years, int(math.Ceil(float64(d)/365))) + } +} diff --git a/template/elapsed_test.go b/template/elapsed_test.go new file mode 100644 index 0000000..b5fd4fa --- /dev/null +++ b/template/elapsed_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2017 Hervé Gouchet. All rights reserved. +// Use of this source code is governed by the MIT License +// that can be found in the LICENSE file. + +package template + +import ( + "fmt" + "testing" + "time" + + "github.com/miniflux/miniflux/locale" +) + +func TestElapsedTime(t *testing.T) { + var dt = []struct { + in time.Time + out string + }{ + {time.Time{}, NotYet}, + {time.Now().Add(time.Hour), NotYet}, + {time.Now(), JustNow}, + {time.Now().Add(-time.Minute), LastMinute}, + {time.Now().Add(-time.Minute * 40), fmt.Sprintf(Minutes, 40)}, + {time.Now().Add(-time.Hour), LastHour}, + {time.Now().Add(-time.Hour * 3), fmt.Sprintf(Hours, 3)}, + {time.Now().Add(-time.Hour * 32), Yesterday}, + {time.Now().Add(-time.Hour * 24 * 3), fmt.Sprintf(Days, 3)}, + {time.Now().Add(-time.Hour * 24 * 14), fmt.Sprintf(Weeks, 2)}, + {time.Now().Add(-time.Hour * 24 * 60), fmt.Sprintf(Months, 2)}, + {time.Now().Add(-time.Hour * 24 * 365 * 3), fmt.Sprintf(Years, 3)}, + } + for i, tt := range dt { + if out := elapsedTime(&locale.Language{}, "Local", tt.in); out != tt.out { + t.Errorf(`%d. content mismatch for "%v": expected=%q got=%q`, i, tt.in, tt.out, out) + } + } +} diff --git a/template/engine.go b/template/engine.go new file mode 100644 index 0000000..7d19c8f --- /dev/null +++ b/template/engine.go @@ -0,0 +1,69 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package template + +import ( + "bytes" + "html/template" + "io" + + "github.com/miniflux/miniflux/config" + "github.com/miniflux/miniflux/locale" + "github.com/miniflux/miniflux/logger" + + "github.com/gorilla/mux" +) + +// Engine handles the templating system. +type Engine struct { + templates map[string]*template.Template + translator *locale.Translator + funcMap *funcMap +} + +func (e *Engine) parseAll() { + commonTemplates := "" + for _, content := range templateCommonMap { + commonTemplates += content + } + + for name, content := range templateViewsMap { + logger.Debug("[Template] Parsing: %s", name) + e.templates[name] = template.Must(template.New("main").Funcs(e.funcMap.Map()).Parse(commonTemplates + content)) + } +} + +// SetLanguage change the language for template processing. +func (e *Engine) SetLanguage(language string) { + e.funcMap.Language = e.translator.GetLanguage(language) +} + +// Execute process a template. +func (e *Engine) Execute(w io.Writer, name string, data interface{}) { + tpl, ok := e.templates[name] + if !ok { + logger.Fatal("[Template] The template %s does not exists", name) + } + + var b bytes.Buffer + err := tpl.ExecuteTemplate(&b, "base", data) + if err != nil { + logger.Fatal("[Template] Unable to render template: %v", err) + } + + b.WriteTo(w) +} + +// NewEngine returns a new template engine. +func NewEngine(cfg *config.Config, router *mux.Router, translator *locale.Translator) *Engine { + tpl := &Engine{ + templates: make(map[string]*template.Template), + translator: translator, + funcMap: newFuncMap(cfg, router, translator.GetLanguage("en_US")), + } + + tpl.parseAll() + return tpl +} diff --git a/template/functions.go b/template/functions.go new file mode 100644 index 0000000..08a4617 --- /dev/null +++ b/template/functions.go @@ -0,0 +1,105 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package template + +import ( + "html/template" + "net/mail" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/miniflux/miniflux/config" + "github.com/miniflux/miniflux/errors" + "github.com/miniflux/miniflux/filter" + "github.com/miniflux/miniflux/http/route" + "github.com/miniflux/miniflux/locale" + "github.com/miniflux/miniflux/url" +) + +type funcMap struct { + cfg *config.Config + router *mux.Router + Language *locale.Language +} + +func (f *funcMap) Map() template.FuncMap { + return template.FuncMap{ + "baseURL": func() string { + return f.cfg.BaseURL() + }, + "rootURL": func() string { + return f.cfg.RootURL() + }, + "hasOAuth2Provider": func(provider string) bool { + return f.cfg.OAuth2Provider() == provider + }, + "hasKey": func(dict map[string]string, key string) bool { + if value, found := dict[key]; found { + return value != "" + } + return false + }, + "route": func(name string, args ...interface{}) string { + return route.Path(f.router, name, args...) + }, + "noescape": func(str string) template.HTML { + return template.HTML(str) + }, + "proxyFilter": func(data string) string { + return filter.ImageProxyFilter(f.router, data) + }, + "proxyURL": func(link string) string { + if url.IsHTTPS(link) { + return link + } + + return filter.Proxify(f.router, link) + }, + "domain": func(websiteURL string) string { + return url.Domain(websiteURL) + }, + "isEmail": func(str string) bool { + _, err := mail.ParseAddress(str) + if err != nil { + return false + } + return true + }, + "hasPrefix": func(str, prefix string) bool { + return strings.HasPrefix(str, prefix) + }, + "contains": func(str, substr string) bool { + return strings.Contains(str, substr) + }, + "isodate": func(ts time.Time) string { + return ts.Format("2006-01-02 15:04:05") + }, + "elapsed": func(timezone string, t time.Time) string { + return elapsedTime(f.Language, timezone, t) + }, + "t": func(key interface{}, args ...interface{}) string { + switch key.(type) { + case string: + return f.Language.Get(key.(string), args...) + case errors.LocalizedError: + err := key.(errors.LocalizedError) + return err.Localize(f.Language) + case error: + return key.(error).Error() + default: + return "" + } + }, + "plural": func(key string, n int, args ...interface{}) string { + return f.Language.Plural(key, n, args...) + }, + "dict": dict, + } +} + +func newFuncMap(cfg *config.Config, router *mux.Router, language *locale.Language) *funcMap { + return &funcMap{cfg, router, language} +} diff --git a/template/template.go b/template/template.go deleted file mode 100644 index a78f931..0000000 --- a/template/template.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2017 Frédéric Guilloe. All rights reserved. -// Use of this source code is governed by the Apache 2.0 -// license that can be found in the LICENSE file. - -package template - -import ( - "bytes" - "fmt" - "html/template" - "io" - "net/mail" - "strings" - "time" - - "github.com/miniflux/miniflux/config" - "github.com/miniflux/miniflux/duration" - "github.com/miniflux/miniflux/errors" - "github.com/miniflux/miniflux/filter" - "github.com/miniflux/miniflux/http/route" - "github.com/miniflux/miniflux/locale" - "github.com/miniflux/miniflux/logger" - "github.com/miniflux/miniflux/url" - - "github.com/gorilla/mux" -) - -// Engine handles the templating system. -type Engine struct { - templates map[string]*template.Template - router *mux.Router - translator *locale.Translator - currentLocale *locale.Language - cfg *config.Config -} - -func (e *Engine) parseAll() { - funcMap := template.FuncMap{ - "baseURL": func() string { - return e.cfg.BaseURL() - }, - "rootURL": func() string { - return e.cfg.RootURL() - }, - "hasOAuth2Provider": func(provider string) bool { - return e.cfg.OAuth2Provider() == provider - }, - "hasKey": func(dict map[string]string, key string) bool { - if value, found := dict[key]; found { - return value != "" - } - return false - }, - "route": func(name string, args ...interface{}) string { - return route.Path(e.router, name, args...) - }, - "noescape": func(str string) template.HTML { - return template.HTML(str) - }, - "proxyFilter": func(data string) string { - return filter.ImageProxyFilter(e.router, data) - }, - "proxyURL": func(link string) string { - if url.IsHTTPS(link) { - return link - } - - return filter.Proxify(e.router, link) - }, - "domain": func(websiteURL string) string { - return url.Domain(websiteURL) - }, - "isEmail": func(str string) bool { - _, err := mail.ParseAddress(str) - if err != nil { - return false - } - return true - }, - "hasPrefix": func(str, prefix string) bool { - return strings.HasPrefix(str, prefix) - }, - "contains": func(str, substr string) bool { - return strings.Contains(str, substr) - }, - "isodate": func(ts time.Time) string { - return ts.Format("2006-01-02 15:04:05") - }, - "elapsed": func(timezone string, t time.Time) string { - return duration.ElapsedTime(e.currentLocale, timezone, t) - }, - "t": func(key interface{}, args ...interface{}) string { - switch key.(type) { - case string: - return e.currentLocale.Get(key.(string), args...) - case errors.LocalizedError: - err := key.(errors.LocalizedError) - return err.Localize(e.currentLocale) - case error: - return key.(error).Error() - default: - return "" - } - }, - "plural": func(key string, n int, args ...interface{}) string { - return e.currentLocale.Plural(key, n, args...) - }, - "dict": func(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 - }, - } - - commonTemplates := "" - for _, content := range templateCommonMap { - commonTemplates += content - } - - for name, content := range templateViewsMap { - logger.Debug("[Template] Parsing: %s", name) - e.templates[name] = template.Must(template.New("main").Funcs(funcMap).Parse(commonTemplates + content)) - } -} - -// SetLanguage change the language for template processing. -func (e *Engine) SetLanguage(language string) { - e.currentLocale = e.translator.GetLanguage(language) -} - -// Execute process a template. -func (e *Engine) Execute(w io.Writer, name string, data interface{}) { - tpl, ok := e.templates[name] - if !ok { - logger.Fatal("[Template] The template %s does not exists", name) - } - - var b bytes.Buffer - err := tpl.ExecuteTemplate(&b, "base", data) - if err != nil { - logger.Fatal("[Template] Unable to render template: %v", err) - } - - b.WriteTo(w) -} - -// NewEngine returns a new template Engine. -func NewEngine(cfg *config.Config, router *mux.Router, translator *locale.Translator) *Engine { - tpl := &Engine{ - templates: make(map[string]*template.Template), - router: router, - translator: translator, - cfg: cfg, - } - - tpl.parseAll() - return tpl -} |