iris/view/django.go
Gerasimos (Makis) Maropoulos e1f25eb098
Full support of the http.FileSystem on all view engines as requested at #1575
Also, the HandleDir accepts both string and http.FileSystem (interface{}) (like the view's fs)
2020-09-05 08:34:09 +03:00

280 lines
8.1 KiB
Go

package view
import (
"bytes"
"io"
"net/http"
"os"
stdPath "path"
"path/filepath"
"strings"
"sync"
"github.com/kataras/iris/v12/context"
"github.com/iris-contrib/pongo2"
)
type (
// Value type alias for pongo2.Value
Value = pongo2.Value
// Error type alias for pongo2.Error
Error = pongo2.Error
// FilterFunction type alias for pongo2.FilterFunction
FilterFunction = pongo2.FilterFunction
// Parser type alias for pongo2.Parser
Parser = pongo2.Parser
// Token type alias for pongo2.Token
Token = pongo2.Token
// INodeTag type alias for pongo2.InodeTag
INodeTag = pongo2.INodeTag
// TagParser the function signature of the tag's parser you will have
// to implement in order to create a new tag.
//
// 'doc' is providing access to the whole document while 'arguments'
// is providing access to the user's arguments to the tag:
//
// {% your_tag_name some "arguments" 123 %}
//
// start_token will be the *Token with the tag's name in it (here: your_tag_name).
//
// Please see the Parser documentation on how to use the parser.
// See `RegisterTag` for more information about writing a tag as well.
TagParser = pongo2.TagParser
)
// AsValue converts any given value to a pongo2.Value
// Usually being used within own functions passed to a template
// through a Context or within filter functions.
//
// Example:
// AsValue("my string")
//
// Shortcut for `pongo2.AsValue`.
var AsValue = pongo2.AsValue
// AsSafeValue works like AsValue, but does not apply the 'escape' filter.
// Shortcut for `pongo2.AsSafeValue`.
var AsSafeValue = pongo2.AsSafeValue
type tDjangoAssetLoader struct {
rootDir string
fs http.FileSystem
}
// Abs calculates the path to a given template. Whenever a path must be resolved
// due to an import from another template, the base equals the parent template's path.
func (l *tDjangoAssetLoader) Abs(base, name string) string {
if stdPath.IsAbs(name) {
return name
}
return stdPath.Join(l.rootDir, name)
}
// Get returns an io.Reader where the template's content can be read from.
func (l *tDjangoAssetLoader) Get(path string) (io.Reader, error) {
if stdPath.IsAbs(path) {
path = path[1:]
}
res, err := asset(l.fs, path)
if err != nil {
return nil, err
}
return bytes.NewReader(res), nil
}
// DjangoEngine contains the django view engine structure.
type DjangoEngine struct {
fs http.FileSystem
// files configuration
rootDir string
extension string
reload bool
//
rmu sync.RWMutex // locks for filters, globals and `ExecuteWiter` when `reload` is true.
// filters for pongo2, map[name of the filter] the filter function . The filters are auto register
filters map[string]FilterFunction
// globals share context fields between templates.
globals map[string]interface{}
templateCache map[string]*pongo2.Template
}
var (
_ Engine = (*DjangoEngine)(nil)
_ EngineFuncer = (*DjangoEngine)(nil)
)
// Django creates and returns a new django view engine.
// The given "extension" MUST begin with a dot.
//
// Usage:
// Django("./views", ".html") or
// Django(iris.Dir("./views"), ".html") or
// Django(AssetFile(), ".html") for embedded data.
func Django(fs interface{}, extension string) *DjangoEngine {
s := &DjangoEngine{
fs: getFS(fs),
rootDir: "/",
extension: extension,
globals: make(map[string]interface{}),
filters: make(map[string]FilterFunction),
templateCache: make(map[string]*pongo2.Template),
}
return s
}
// RootDir sets the directory to be used as a starting point
// to load templates from the provided file system.
func (s *DjangoEngine) RootDir(root string) *DjangoEngine {
s.rootDir = filepath.ToSlash(root)
return s
}
// Ext returns the file extension which this view engine is responsible to render.
func (s *DjangoEngine) Ext() string {
return s.extension
}
// Reload if set to true the templates are reloading on each render,
// use it when you're in development and you're boring of restarting
// the whole app when you edit a template file.
//
// Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time,
// no concurrent access across clients, use it only on development status.
// It's good to be used side by side with the https://github.com/kataras/rizla reloader for go source files.
func (s *DjangoEngine) Reload(developmentMode bool) *DjangoEngine {
s.reload = developmentMode
return s
}
// AddFunc adds the function to the template's Globals.
// It is legal to overwrite elements of the default actions:
// - url func(routeName string, args ...string) string
// - urlpath func(routeName string, args ...string) string
// - render func(fullPartialName string) (template.HTML, error).
func (s *DjangoEngine) AddFunc(funcName string, funcBody interface{}) {
s.rmu.Lock()
s.globals[funcName] = funcBody
s.rmu.Unlock()
}
// AddFilter registers a new filter. If there's already a filter with the same
// name, RegisterFilter will panic. You usually want to call this
// function in the filter's init() function:
// http://golang.org/doc/effective_go.html#init
//
// Same as `RegisterFilter`.
func (s *DjangoEngine) AddFilter(filterName string, filterBody FilterFunction) *DjangoEngine {
return s.registerFilter(filterName, filterBody)
}
// RegisterFilter registers a new filter. If there's already a filter with the same
// name, RegisterFilter will panic. You usually want to call this
// function in the filter's init() function:
// http://golang.org/doc/effective_go.html#init
//
// See http://www.florian-schlachter.de/post/pongo2/ for more about
// writing filters and tags.
func (s *DjangoEngine) RegisterFilter(filterName string, filterBody FilterFunction) *DjangoEngine {
return s.registerFilter(filterName, filterBody)
}
func (s *DjangoEngine) registerFilter(filterName string, fn FilterFunction) *DjangoEngine {
pongo2.RegisterFilter(filterName, fn)
return s
}
// RegisterTag registers a new tag. You usually want to call this
// function in the tag's init() function:
// http://golang.org/doc/effective_go.html#init
//
// See http://www.florian-schlachter.de/post/pongo2/ for more about
// writing filters and tags.
func (s *DjangoEngine) RegisterTag(tagName string, fn TagParser) error {
return pongo2.RegisterTag(tagName, fn)
}
// Load parses the templates to the engine.
// It is responsible to add the necessary global functions.
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *DjangoEngine) Load() error {
set := pongo2.NewSet("", &tDjangoAssetLoader{fs: s.fs, rootDir: s.rootDir})
set.Globals = getPongoContext(s.globals)
s.rmu.Lock()
defer s.rmu.Unlock()
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
if info == nil || info.IsDir() {
return nil
}
if s.extension != "" {
if !strings.HasSuffix(path, s.extension) {
return nil
}
}
buf, err := asset(s.fs, path)
if err != nil {
return err
}
name := strings.TrimPrefix(path, "/")
s.templateCache[name], err = set.FromBytes(buf)
return err
})
}
// getPongoContext returns the pongo2.Context from map[string]interface{} or from pongo2.Context, used internaly
func getPongoContext(templateData interface{}) pongo2.Context {
if templateData == nil {
return nil
}
if contextData, isPongoContext := templateData.(pongo2.Context); isPongoContext {
return contextData
}
if contextData, isContextViewData := templateData.(context.Map); isContextViewData {
return pongo2.Context(contextData)
}
return templateData.(map[string]interface{})
}
func (s *DjangoEngine) fromCache(relativeName string) *pongo2.Template {
if s.reload {
s.rmu.RLock()
defer s.rmu.RUnlock()
}
if tmpl, ok := s.templateCache[relativeName]; ok {
return tmpl
}
return nil
}
// ExecuteWriter executes a templates and write its results to the w writer
// layout here is useless.
func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
// re-parse the templates if reload is enabled.
if s.reload {
if err := s.Load(); err != nil {
return err
}
}
if tmpl := s.fromCache(filename); tmpl != nil {
return tmpl.ExecuteWriter(getPongoContext(bindingData), w)
}
return ErrNotExist{filename, false}
}