1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-16 18:01:37 +00:00

Move template functions outside engine (refactoring)

This commit is contained in:
Frédéric Guillot 2018-02-04 15:45:07 -08:00
parent b5b1930599
commit 3884a33b36
9 changed files with 254 additions and 214 deletions

22
template/dict.go Normal file
View file

@ -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
}

42
template/dict_test.go Normal file
View file

@ -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`)
}
}

80
template/elapsed.go Normal file
View file

@ -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)))
}
}

38
template/elapsed_test.go Normal file
View file

@ -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)
}
}
}

69
template/engine.go Normal file
View file

@ -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
}

105
template/functions.go Normal file
View file

@ -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}
}

View file

@ -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
}