diff --git a/duration/LICENSE b/duration/LICENSE deleted file mode 100644 index 036a2a16..00000000 --- a/duration/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Hervé GOUCHET - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/duration/doc.go b/duration/doc.go deleted file mode 100644 index 8fc52969..00000000 --- a/duration/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2018 Frédéric Guillot. All rights reserved. -// Use of this source code is governed by the MIT license -// that can be found in the LICENSE file. - -/* - -Package duration implements helpers to calculate time duration. - -*/ -package duration diff --git a/template/dict.go b/template/dict.go new file mode 100644 index 00000000..3056bcd5 --- /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 00000000..8b236e04 --- /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/duration/duration.go b/template/elapsed.go similarity index 69% rename from duration/duration.go rename to template/elapsed.go index fe84026c..273771db 100644 --- a/duration/duration.go +++ b/template/elapsed.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License // that can be found in the LICENSE file. -package duration +package template import ( "math" @@ -28,9 +28,9 @@ var ( // ElapsedTime returns in a human readable format the elapsed time // since the given datetime. -func ElapsedTime(translator *locale.Language, timezone string, t time.Time) string { +func elapsedTime(language *locale.Language, timezone string, t time.Time) string { if t.IsZero() { - return translator.Get(NotYet) + return language.Get(NotYet) } var now time.Time @@ -47,7 +47,7 @@ func ElapsedTime(translator *locale.Language, timezone string, t time.Time) stri } if now.Before(t) { - return translator.Get(NotYet) + return language.Get(NotYet) } diff := now.Sub(t) @@ -57,24 +57,24 @@ func ElapsedTime(translator *locale.Language, timezone string, t time.Time) stri d := int(s / 86400) switch { case s < 60: - return translator.Get(JustNow) + return language.Get(JustNow) case s < 120: - return translator.Get(LastMinute) + return language.Get(LastMinute) case s < 3600: - return translator.Get(Minutes, int(diff.Minutes())) + return language.Get(Minutes, int(diff.Minutes())) case s < 7200: - return translator.Get(LastHour) + return language.Get(LastHour) case s < 86400: - return translator.Get(Hours, int(diff.Hours())) + return language.Get(Hours, int(diff.Hours())) case d == 1: - return translator.Get(Yesterday) + return language.Get(Yesterday) case d < 7: - return translator.Get(Days, d) + return language.Get(Days, d) case d < 31: - return translator.Get(Weeks, int(math.Ceil(float64(d)/7))) + return language.Get(Weeks, int(math.Ceil(float64(d)/7))) case d < 365: - return translator.Get(Months, int(math.Ceil(float64(d)/30))) + return language.Get(Months, int(math.Ceil(float64(d)/30))) default: - return translator.Get(Years, int(math.Ceil(float64(d)/365))) + return language.Get(Years, int(math.Ceil(float64(d)/365))) } } diff --git a/duration/duration_test.go b/template/elapsed_test.go similarity index 93% rename from duration/duration_test.go rename to template/elapsed_test.go index d1aae217..b5fd4fa8 100644 --- a/duration/duration_test.go +++ b/template/elapsed_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by the MIT License // that can be found in the LICENSE file. -package duration +package template import ( "fmt" @@ -31,7 +31,7 @@ func TestElapsedTime(t *testing.T) { {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 { + 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 00000000..7d19c8f9 --- /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 00000000..08a46175 --- /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 a78f931c..00000000 --- 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 -}