1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-17 09:57:01 +00:00

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)
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-09-05 08:34:09 +03:00
parent 7e6d453dad
commit e1f25eb098
50 changed files with 1613 additions and 1226 deletions

View File

@@ -115,11 +115,19 @@ func hi(ctx iris.Context) {
## Embedded
View engine supports bundled(https://github.com/go-bindata/go-bindata) template files too.
`go-bindata` gives you two functions, `Assset` and `AssetNames`,
these can be set to each of the template engines using the `.Binary` function.
View engine supports bundled(https://github.com/go-bindata/go-bindata) template files too. Latest
`go-bindata` release gives you a compatible `http.FileSystem` that can be provided as the first argument of a view engine's initialization, e.g. `HTML(AssetFile(), ".html")`.
Example code:
```sh
$ go get -u github.com/go-bindata/go-bindata
# OR: go get -u github.com/go-bindata/go-bindata/v3/go-bindata
# to save it to your go.mod file
$ go-bindata -fs -prefix "templates" ./templates/...
$ go run .
```
Example Code:
```go
package main
@@ -128,12 +136,7 @@ import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
// $ go get -u github.com/go-bindata/go-bindata/v3/go-bindata
// $ go-bindata ./templates/...
// $ go build
// $ ./embedding-templates-into-app
// html files are not used, you can delete the folder and run the example
app.RegisterView(iris.HTML("./templates", ".html").Binary(Asset, AssetNames))
app.RegisterView(iris.HTML(AssetFile(), ".html"))
app.Get("/", hi)
// http://localhost:8080

View File

@@ -1,7 +1,6 @@
package view
import (
"path"
"sync"
"github.com/yosssi/ace"
@@ -13,8 +12,13 @@ import (
// The given "extension" MUST begin with a dot.
//
// Read more about the Ace Go Parser: https://github.com/yosssi/ace
func Ace(directory, extension string) *HTMLEngine {
s := HTML(directory, extension)
//
// Usage:
// Ace("./views", ".ace") or
// Ace(iris.Dir("./views"), ".ace") or
// Ace(AssetFile(), ".ace") for embedded data.
func Ace(fs interface{}, extension string) *HTMLEngine {
s := HTML(fs, extension)
funcs := make(map[string]interface{}, 0)
@@ -30,7 +34,7 @@ func Ace(directory, extension string) *HTMLEngine {
}
})
name = path.Join(path.Clean(directory), name)
// name = path.Join(path.Clean(directory), name)
src := ace.NewSource(
ace.NewFile(name, text),

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"strings"
@@ -14,11 +15,10 @@ import (
// AmberEngine contains the amber view engine structure.
type AmberEngine struct {
fs http.FileSystem
// files configuration
directory string
rootDir string
extension string
assetFn func(name string) ([]byte, error) // for embedded, in combination with directory & extension
namesFn func() []string // for embedded, in combination with directory & extension
reload bool
//
rmu sync.RWMutex // locks for `ExecuteWiter` when `reload` is true.
@@ -33,9 +33,15 @@ var (
// Amber creates and returns a new amber view engine.
// The given "extension" MUST begin with a dot.
func Amber(directory, extension string) *AmberEngine {
//
// Usage:
// Amber("./views", ".amber") or
// Amber(iris.Dir("./views"), ".amber") or
// Amber(AssetFile(), ".amber") for embedded data.
func Amber(fs interface{}, extension string) *AmberEngine {
s := &AmberEngine{
directory: directory,
fs: getFS(fs),
rootDir: "/",
extension: extension,
templateCache: make(map[string]*template.Template),
funcs: make(map[string]interface{}),
@@ -44,20 +50,18 @@ func Amber(directory, extension string) *AmberEngine {
return s
}
// RootDir sets the directory to be used as a starting point
// to load templates from the provided file system.
func (s *AmberEngine) RootDir(root string) *AmberEngine {
s.rootDir = filepath.ToSlash(root)
return s
}
// Ext returns the file extension which this view engine is responsible to render.
func (s *AmberEngine) Ext() string {
return s.extension
}
// Binary optionally, use it when template files are distributed
// inside the app executable (.go generated files).
//
// The assetFn and namesFn can come from the go-bindata library.
func (s *AmberEngine) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) *AmberEngine {
s.assetFn, s.namesFn = assetFn, namesFn
return s
}
// 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.
@@ -86,32 +90,8 @@ func (s *AmberEngine) AddFunc(funcName string, funcBody interface{}) {
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *AmberEngine) Load() error {
if s.assetFn != nil && s.namesFn != nil {
// embedded
return s.loadAssets()
}
// load from directory, make the dir absolute here too.
dir, err := filepath.Abs(s.directory)
if err != nil {
return err
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
return err
}
// change the directory field configuration, load happens after directory has been set, so we will not have any problems here.
s.directory = dir
return s.loadDirectory()
}
// loadDirectory loads the amber templates from directory.
func (s *AmberEngine) loadDirectory() error {
dir, extension := s.directory, s.extension
opt := amber.DirOptions{}
opt.Recursive = true
s.rmu.Lock()
defer s.rmu.Unlock()
// prepare the global amber funcs
funcs := template.FuncMap{}
@@ -125,84 +105,52 @@ func (s *AmberEngine) loadDirectory() error {
}
amber.FuncMap = funcs // set the funcs
opt.Ext = extension
templates, err := amber.CompileDir(dir, opt, amber.DefaultOptions) // this returns the map with stripped extension, we want extension so we copy the map
if err == nil {
s.templateCache = make(map[string]*template.Template)
for k, v := range templates {
name := filepath.ToSlash(k + opt.Ext)
s.templateCache[name] = v
delete(templates, k)
}
}
return err
}
// loadAssets builds the templates by binary, embedded.
func (s *AmberEngine) loadAssets() error {
virtualDirectory, virtualExtension := s.directory, s.extension
assetFn, namesFn := s.assetFn, s.namesFn
// prepare the global amber funcs
funcs := template.FuncMap{}
for k, v := range amber.FuncMap { // add the amber's default funcs
funcs[k] = v
opts := amber.Options{
PrettyPrint: false,
LineNumbers: false,
VirtualFilesystem: s.fs,
}
for k, v := range s.funcs {
funcs[k] = v
}
if len(virtualDirectory) > 0 {
if virtualDirectory[0] == '.' { // first check for .wrong
virtualDirectory = virtualDirectory[1:]
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
if info == nil || info.IsDir() {
return nil
}
if virtualDirectory[0] == '/' || virtualDirectory[0] == filepath.Separator { // second check for /something, (or ./something if we had dot on 0 it will be removed
virtualDirectory = virtualDirectory[1:]
}
}
amber.FuncMap = funcs // set the funcs
names := namesFn()
for _, path := range names {
if !strings.HasPrefix(path, virtualDirectory) {
continue
}
ext := filepath.Ext(path)
if ext == virtualExtension {
rel, err := filepath.Rel(virtualDirectory, path)
if err != nil {
return err
if s.extension != "" {
if !strings.HasSuffix(path, s.extension) {
return nil
}
buf, err := assetFn(path)
if err != nil {
return err
}
name := filepath.ToSlash(rel)
tmpl, err := amber.CompileData(buf, name, amber.DefaultOptions)
if err != nil {
return err
}
s.templateCache[name] = tmpl
}
}
return nil
buf, err := asset(s.fs, path)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
name := strings.TrimPrefix(path, "/")
tmpl, err := amber.CompileData(buf, name, opts)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
s.templateCache[name] = tmpl
return nil
})
}
func (s *AmberEngine) fromCache(relativeName string) *template.Template {
tmpl, ok := s.templateCache[relativeName]
if ok {
if s.reload {
s.rmu.RLock()
defer s.rmu.RUnlock()
}
if tmpl, ok := s.templateCache[relativeName]; ok {
return tmpl
}
return nil
}
@@ -211,10 +159,6 @@ func (s *AmberEngine) fromCache(relativeName string) *template.Template {
func (s *AmberEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
// re-parse the templates if reload is enabled.
if s.reload {
// locks to fix #872, it's the simplest solution and the most correct,
// to execute writers with "wait list", one at a time.
s.rmu.Lock()
defer s.rmu.Unlock()
if err := s.Load(); err != nil {
return err
}
@@ -224,5 +168,5 @@ func (s *AmberEngine) ExecuteWriter(w io.Writer, filename string, layout string,
return tmpl.Execute(w, bindingData)
}
return fmt.Errorf("Template with name: %s does not exist in the dir: %s", filename, s.directory)
return ErrNotExist{filename, false}
}

View File

@@ -39,8 +39,28 @@ func WrapBlocks(v *blocks.Blocks) *BlocksEngine {
// The given "extension" MUST begin with a dot.
//
// See `WrapBlocks` package-level function too.
func Blocks(directory, extension string) *BlocksEngine {
return WrapBlocks(blocks.New(directory).Extension(extension))
//
// Usage:
// Blocks("./views", ".html") or
// Blocks(iris.Dir("./views"), ".html") or
// Blocks(AssetFile(), ".html") for embedded data.
func Blocks(fs interface{}, extension string) *BlocksEngine {
return WrapBlocks(blocks.New(fs).Extension(extension))
}
// RootDir sets the directory to use as the root one inside the provided File System.
func (s *BlocksEngine) RootDir(root string) *BlocksEngine {
s.Engine.RootDir(root)
return s
}
// LayoutDir sets a custom layouts directory,
// always relative to the "rootDir" one.
// Layouts are recognised by their prefix names.
// Defaults to "layouts".
func (s *BlocksEngine) LayoutDir(relToDirLayoutDir string) *BlocksEngine {
s.Engine.LayoutDir(relToDirLayoutDir)
return s
}
// Ext returns empty ext as this template engine
@@ -66,18 +86,6 @@ func (s *BlocksEngine) AddLayoutFunc(funcName string, funcBody interface{}) *Blo
return s
}
// Binary sets the function which reads contents based on a filename
// and a function that returns all the filenames.
// These functions are used to parse the corresponding files into templates.
// You do not need to set them when the given "rootDir" was a system directory.
// It's mostly useful when the application contains embedded template files,
// e.g. pass go-bindata's `Asset` and `AssetNames` functions
// to load templates from go-bindata generated content.
func (s *BlocksEngine) Binary(asset blocks.AssetFunc, assetNames blocks.AssetNamesFunc) *BlocksEngine {
s.Engine.Assets(asset, assetNames)
return s
}
// Layout sets the default layout which inside should use
// the {{ template "content" . }} to render the main template.
//

View File

@@ -2,9 +2,8 @@ package view
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
stdPath "path"
"path/filepath"
@@ -60,41 +59,40 @@ var AsValue = pongo2.AsValue
var AsSafeValue = pongo2.AsSafeValue
type tDjangoAssetLoader struct {
baseDir string
assetGet func(name string) ([]byte, error)
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 (dal *tDjangoAssetLoader) Abs(base, name string) string {
func (l *tDjangoAssetLoader) Abs(base, name string) string {
if stdPath.IsAbs(name) {
return name
}
return stdPath.Join(dal.baseDir, name)
return stdPath.Join(l.rootDir, name)
}
// Get returns an io.Reader where the template's content can be read from.
func (dal *tDjangoAssetLoader) Get(path string) (io.Reader, error) {
func (l *tDjangoAssetLoader) Get(path string) (io.Reader, error) {
if stdPath.IsAbs(path) {
path = path[1:]
}
res, err := dal.assetGet(path)
res, err := asset(l.fs, path)
if err != nil {
return nil, err
}
return bytes.NewBuffer(res), nil
return bytes.NewReader(res), nil
}
// DjangoEngine contains the django view engine structure.
type DjangoEngine struct {
fs http.FileSystem
// files configuration
directory string
rootDir string
extension string
assetFn func(name string) ([]byte, error) // for embedded, in combination with directory & extension
namesFn func() []string // for embedded, in combination with directory & extension
reload bool
//
rmu sync.RWMutex // locks for filters, globals and `ExecuteWiter` when `reload` is true.
@@ -102,7 +100,6 @@ type DjangoEngine struct {
filters map[string]FilterFunction
// globals share context fields between templates.
globals map[string]interface{}
mu sync.Mutex // locks for template cache
templateCache map[string]*pongo2.Template
}
@@ -113,9 +110,15 @@ var (
// Django creates and returns a new django view engine.
// The given "extension" MUST begin with a dot.
func Django(directory, extension string) *DjangoEngine {
//
// 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{
directory: directory,
fs: getFS(fs),
rootDir: "/",
extension: extension,
globals: make(map[string]interface{}),
filters: make(map[string]FilterFunction),
@@ -125,20 +128,18 @@ func Django(directory, extension string) *DjangoEngine {
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
}
// Binary optionally, use it when template files are distributed
// inside the app executable (.go generated files).
//
// The assetFn and namesFn can come from the go-bindata library.
func (s *DjangoEngine) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) *DjangoEngine {
s.assetFn, s.namesFn = assetFn, namesFn
return s
}
// 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.
@@ -203,133 +204,32 @@ func (s *DjangoEngine) RegisterTag(tagName string, fn TagParser) error {
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *DjangoEngine) Load() error {
if s.assetFn != nil && s.namesFn != nil {
// embedded
return s.loadAssets()
}
// load from directory, make the dir absolute here too.
dir, err := filepath.Abs(s.directory)
if err != nil {
return err
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
return err
}
// change the directory field configuration, load happens after directory has been set, so we will not have any problems here.
s.directory = dir
return s.loadDirectory()
}
// LoadDirectory loads the templates from directory.
func (s *DjangoEngine) loadDirectory() (templateErr error) {
dir, extension := s.directory, s.extension
fsLoader, err := pongo2.NewLocalFileSystemLoader(dir) // I see that this doesn't read the content if already parsed, so do it manually via filepath.Walk
if err != nil {
return err
}
set := pongo2.NewSet("", fsLoader)
set := pongo2.NewSet("", &tDjangoAssetLoader{fs: s.fs, rootDir: s.rootDir})
set.Globals = getPongoContext(s.globals)
s.mu.Lock()
defer s.mu.Unlock()
s.rmu.Lock()
defer s.rmu.Unlock()
// Walk the supplied directory and compile any files that match our extension list.
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
// Fix same-extension-dirs bug: some dir might be named to: "users.tmpl", "local.html".
// These dirs should be excluded as they are not valid golang templates, but files under
// them should be treat as normal.
// If is a dir, return immediately (dir is not a valid golang template).
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
if info == nil || info.IsDir() {
} else {
return nil
}
rel, err := filepath.Rel(dir, path)
if err != nil {
templateErr = err
return err
if s.extension != "" {
if !strings.HasSuffix(path, s.extension) {
return nil
}
ext := filepath.Ext(rel)
if ext == extension {
buf, err := ioutil.ReadFile(path)
if err != nil {
templateErr = err
return err
}
name := filepath.ToSlash(rel)
s.templateCache[name], templateErr = set.FromString(string(buf))
if templateErr != nil {
return templateErr
}
}
}
return nil
})
if err != nil {
return err
}
return
}
// loadAssets loads the templates by binary (go-bindata for embedded).
func (s *DjangoEngine) loadAssets() error {
virtualDirectory, virtualExtension := s.directory, s.extension
assetFn, namesFn := s.assetFn, s.namesFn
// Make a file set with a template loader based on asset function
set := pongo2.NewSet("", &tDjangoAssetLoader{baseDir: s.directory, assetGet: s.assetFn})
set.Globals = getPongoContext(s.globals)
if len(virtualDirectory) > 0 {
if virtualDirectory[0] == '.' { // first check for .wrong
virtualDirectory = virtualDirectory[1:]
}
if virtualDirectory[0] == '/' || virtualDirectory[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed
virtualDirectory = virtualDirectory[1:]
}
}
s.mu.Lock()
defer s.mu.Unlock()
names := namesFn()
for _, path := range names {
if !strings.HasPrefix(path, virtualDirectory) {
continue
}
rel, err := filepath.Rel(virtualDirectory, path)
buf, err := asset(s.fs, path)
if err != nil {
return err
}
ext := filepath.Ext(rel)
if ext == virtualExtension {
buf, err := assetFn(path)
if err != nil {
return err
}
name := filepath.ToSlash(rel)
s.templateCache[name], err = set.FromString(string(buf))
if err != nil {
return err
}
}
}
return nil
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
@@ -350,15 +250,14 @@ func getPongoContext(templateData interface{}) pongo2.Context {
}
func (s *DjangoEngine) fromCache(relativeName string) *pongo2.Template {
s.mu.Lock()
if s.reload {
s.rmu.RLock()
defer s.rmu.RUnlock()
}
tmpl, ok := s.templateCache[relativeName]
if ok {
s.mu.Unlock()
if tmpl, ok := s.templateCache[relativeName]; ok {
return tmpl
}
s.mu.Unlock()
return nil
}
@@ -367,10 +266,6 @@ func (s *DjangoEngine) fromCache(relativeName string) *pongo2.Template {
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 {
// locks to fix #872, it's the simplest solution and the most correct,
// to execute writers with "wait list", one at a time.
s.rmu.Lock()
defer s.rmu.Unlock()
if err := s.Load(); err != nil {
return err
}
@@ -380,5 +275,5 @@ func (s *DjangoEngine) ExecuteWriter(w io.Writer, filename string, layout string
return tmpl.ExecuteWriter(getPongoContext(bindingData), w)
}
return fmt.Errorf("template with name: %s ddoes not exist in the dir: %s", filename, s.directory)
return ErrNotExist{filename, false}
}

106
view/fs.go Normal file
View File

@@ -0,0 +1,106 @@
package view
import (
"fmt"
"io/ioutil"
"net/http"
"path"
"path/filepath"
"sort"
)
// walk recursively in "fs" descends "root" path, calling "walkFn".
func walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error {
names, err := assetNames(fs, root)
if err != nil {
return fmt.Errorf("%s: %w", root, err)
}
for _, name := range names {
fullpath := path.Join(root, name)
f, err := fs.Open(fullpath)
if err != nil {
return fmt.Errorf("%s: %w", fullpath, err)
}
stat, err := f.Stat()
err = walkFn(fullpath, stat, err)
if err != nil {
if err != filepath.SkipDir {
return fmt.Errorf("%s: %w", fullpath, err)
}
continue
}
if stat.IsDir() {
if err := walk(fs, fullpath, walkFn); err != nil {
return fmt.Errorf("%s: %w", fullpath, err)
}
}
}
return nil
}
// assetNames returns the first-level directories and file, sorted, names.
func assetNames(fs http.FileSystem, name string) ([]string, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
infos, err := f.Readdir(-1)
f.Close()
if err != nil {
return nil, err
}
names := make([]string, 0, len(infos))
for _, info := range infos {
// note: go-bindata fs returns full names whether
// the http.Dir returns the base part, so
// we only work with their base names.
name := filepath.ToSlash(info.Name())
name = path.Base(name)
names = append(names, name)
}
sort.Strings(names)
return names, nil
}
func asset(fs http.FileSystem, name string) ([]byte, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
contents, err := ioutil.ReadAll(f)
f.Close()
return contents, err
}
func getFS(fsOrDir interface{}) (fs http.FileSystem) {
switch v := fsOrDir.(type) {
case string:
fs = httpDirWrapper{http.Dir(v)}
case http.FileSystem:
fs = v
default:
panic(fmt.Errorf(`unexpected "fsOrDir" argument type of %T (string or http.FileSystem)`, v))
}
return
}
// fixes: "invalid character in file path"
// on amber engine (it uses the virtual fs directly
// and it uses filepath instead of the path package...).
type httpDirWrapper struct {
http.Dir
}
func (d httpDirWrapper) Open(name string) (http.File, error) {
return d.Dir.Open(filepath.ToSlash(name))
}

View File

@@ -3,7 +3,7 @@ package view
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
@@ -14,15 +14,16 @@ import (
// HandlebarsEngine contains the handlebars view engine structure.
type HandlebarsEngine struct {
fs http.FileSystem
// files configuration
directory string
rootDir string
extension string
assetFn func(name string) ([]byte, error) // for embedded, in combination with directory & extension
namesFn func() []string // for embedded, in combination with directory & extension
reload bool // if true, each time the ExecuteWriter is called the templates will be reloaded.
// parser configuration
layout string
rmu sync.RWMutex // locks for helpers and `ExecuteWiter` when `reload` is true.
rmu sync.RWMutex
helpers map[string]interface{}
templateCache map[string]*raymond.Template
}
@@ -34,9 +35,15 @@ var (
// Handlebars creates and returns a new handlebars view engine.
// The given "extension" MUST begin with a dot.
func Handlebars(directory, extension string) *HandlebarsEngine {
//
// Usage:
// Handlebars("./views", ".html") or
// Handlebars(iris.Dir("./views"), ".html") or
// Handlebars(AssetFile(), ".html") for embedded data.
func Handlebars(fs interface{}, extension string) *HandlebarsEngine {
s := &HandlebarsEngine{
directory: directory,
fs: getFS(fs),
rootDir: "/",
extension: extension,
templateCache: make(map[string]*raymond.Template),
helpers: make(map[string]interface{}),
@@ -54,20 +61,18 @@ func Handlebars(directory, extension string) *HandlebarsEngine {
return s
}
// RootDir sets the directory to be used as a starting point
// to load templates from the provided file system.
func (s *HandlebarsEngine) RootDir(root string) *HandlebarsEngine {
s.rootDir = filepath.ToSlash(root)
return s
}
// Ext returns the file extension which this view engine is responsible to render.
func (s *HandlebarsEngine) Ext() string {
return s.extension
}
// Binary optionally, use it when template files are distributed
// inside the app executable (.go generated files).
//
// The assetFn and namesFn can come from the go-bindata library.
func (s *HandlebarsEngine) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) *HandlebarsEngine {
s.assetFn, s.namesFn = assetFn, namesFn
return s
}
// 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.
@@ -105,135 +110,52 @@ func (s *HandlebarsEngine) AddFunc(funcName string, funcBody interface{}) {
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *HandlebarsEngine) Load() error {
if s.assetFn != nil && s.namesFn != nil {
// embedded
return s.loadAssets()
}
s.rmu.Lock()
defer s.rmu.Unlock()
// load from directory, make the dir absolute here too.
dir, err := filepath.Abs(s.directory)
if err != nil {
return err
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
return err
}
// change the directory field configuration, load happens after directory has been set, so we will not have any problems here.
s.directory = dir
return s.loadDirectory()
}
// loadDirectory builds the handlebars templates from directory.
func (s *HandlebarsEngine) loadDirectory() error {
// register the global helpers on the first load
if len(s.templateCache) == 0 && s.helpers != nil {
raymond.RegisterHelpers(s.helpers)
}
dir, extension := s.directory, s.extension
// the render works like {{ render "myfile.html" theContext.PartialContext}}
// instead of the html/template engine which works like {{ render "myfile.html"}} and accepts the parent binding, with handlebars we can't do that because of lack of runtime helpers (dublicate error)
var templateErr error
err := filepath.Walk(dir, func(path string, info os.FileInfo, _ error) error {
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, _ error) error {
if info == nil || info.IsDir() {
return nil
}
rel, err := filepath.Rel(dir, path)
if s.extension != "" {
if !strings.HasSuffix(path, s.extension) {
return nil
}
}
buf, err := asset(s.fs, path)
if err != nil {
return err
}
ext := filepath.Ext(rel)
if ext == extension {
buf, err := ioutil.ReadFile(path)
contents := string(buf)
if err != nil {
templateErr = err
return err
}
name := filepath.ToSlash(rel)
tmpl, err := raymond.Parse(contents)
if err != nil {
templateErr = err
return err
}
s.templateCache[name] = tmpl
name := strings.TrimPrefix(path, "/")
tmpl, err := raymond.Parse(string(buf))
if err != nil {
return err
}
s.templateCache[name] = tmpl
return nil
})
if err != nil {
return err
}
return templateErr
}
// loadAssets loads the templates by binary, embedded.
func (s *HandlebarsEngine) loadAssets() error {
// register the global helpers
if len(s.templateCache) == 0 && s.helpers != nil {
raymond.RegisterHelpers(s.helpers)
}
virtualDirectory, virtualExtension := s.directory, s.extension
assetFn, namesFn := s.assetFn, s.namesFn
if len(virtualDirectory) > 0 {
if virtualDirectory[0] == '.' { // first check for .wrong
virtualDirectory = virtualDirectory[1:]
}
if virtualDirectory[0] == '/' || virtualDirectory[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed
virtualDirectory = virtualDirectory[1:]
}
}
names := namesFn()
for _, path := range names {
if !strings.HasPrefix(path, virtualDirectory) {
continue
}
ext := filepath.Ext(path)
if ext == virtualExtension {
rel, err := filepath.Rel(virtualDirectory, path)
if err != nil {
return err
}
buf, err := assetFn(path)
if err != nil {
return err
}
contents := string(buf)
name := filepath.ToSlash(rel)
tmpl, err := raymond.Parse(contents)
if err != nil {
return err
}
s.templateCache[name] = tmpl
}
}
return nil
}
func (s *HandlebarsEngine) fromCache(relativeName string) *raymond.Template {
tmpl, ok := s.templateCache[relativeName]
if !ok {
return nil
if s.reload {
s.rmu.RLock()
defer s.rmu.RUnlock()
}
return tmpl
if tmpl, ok := s.templateCache[relativeName]; ok {
return tmpl
}
return nil
}
func (s *HandlebarsEngine) executeTemplateBuf(name string, binding interface{}) (string, error) {
@@ -247,10 +169,6 @@ func (s *HandlebarsEngine) executeTemplateBuf(name string, binding interface{})
func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
// re-parse the templates if reload is enabled.
if s.reload {
// locks to fix #872, it's the simplest solution and the most correct,
// to execute writers with "wait list", one at a time.
s.rmu.Lock()
defer s.rmu.Unlock()
if err := s.Load(); err != nil {
return err
}
@@ -295,5 +213,5 @@ func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout st
return err
}
return fmt.Errorf("template with name: %s[original name = %s] does not exist in the dir: %s", renderFilename, filename, s.directory)
return ErrNotExist{fmt.Sprintf("%s (file: %s)", renderFilename, filename), false}
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"html/template"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
@@ -14,14 +14,16 @@ import (
// HTMLEngine contains the html view engine structure.
type HTMLEngine struct {
// the file system to load from.
fs http.FileSystem
// files configuration
directory string
rootDir string
extension string
assetFn func(name string) ([]byte, error) // for embedded, in combination with directory & extension
namesFn func() []string // for embedded, in combination with directory & extension
reload bool // if true, each time the ExecuteWriter is called the templates will be reloaded, each ExecuteWriter waits to be finished before writing to a new one.
// if true, each time the ExecuteWriter is called the templates will be reloaded,
// each ExecuteWriter waits to be finished before writing to a new one.
reload bool
// parser configuration
options []string // text options
options []string // (text) template options
left string
right string
layout string
@@ -65,12 +67,16 @@ var emptyFuncs = template.FuncMap{
// The html engine used like the "html/template" standard go package
// but with a lot of extra features.
// The given "extension" MUST begin with a dot.
func HTML(directory, extension string) *HTMLEngine {
//
// Usage:
// HTML("./views", ".html") or
// HTML(iris.Dir("./views"), ".html") or
// HTML(AssetFile(), ".html") for embedded data.
func HTML(fs interface{}, extension string) *HTMLEngine {
s := &HTMLEngine{
directory: directory,
fs: getFS(fs),
rootDir: "/",
extension: extension,
assetFn: nil,
namesFn: nil,
reload: false,
left: "{{",
right: "}}",
@@ -82,20 +88,18 @@ func HTML(directory, extension string) *HTMLEngine {
return s
}
// RootDir sets the directory to be used as a starting point
// to load templates from the provided file system.
func (s *HTMLEngine) RootDir(root string) *HTMLEngine {
s.rootDir = filepath.ToSlash(root)
return s
}
// Ext returns the file extension which this view engine is responsible to render.
func (s *HTMLEngine) Ext() string {
return s.extension
}
// Binary optionally, use it when template files are distributed
// inside the app executable (.go generated files).
//
// The assetFn and namesFn can come from the go-bindata library.
func (s *HTMLEngine) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) *HTMLEngine {
s.assetFn, s.namesFn = assetFn, namesFn
return s
}
// 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.
@@ -211,213 +215,49 @@ func (s *HTMLEngine) Funcs(funcMap template.FuncMap) *HTMLEngine {
//
// Returns an error if something bad happens, caller is responsible to handle that.
func (s *HTMLEngine) Load() error {
// No need to make this with a complicated and "pro" way, just add lockers to the `ExecuteWriter`.
// if `Reload(true)` and add a note for non conc access on dev mode.
// atomic.StoreUint32(&s.isLoading, 1)
// s.rmu.Lock()
// defer func() {
// s.rmu.Unlock()
// atomic.StoreUint32(&s.isLoading, 0)
// }()
s.rmu.Lock()
defer s.rmu.Unlock()
if s.assetFn != nil && s.namesFn != nil {
// NOT NECESSARY "fix" of https://github.com/kataras/iris/issues/784,
// IT'S BAD CODE WRITTEN WE KEEP HERE ONLY FOR A REMINDER
// for any future questions.
//
// if strings.HasPrefix(s.directory, "../") {
// // this and some more additions are fixes for https://github.com/kataras/iris/issues/784
// // however, the dev SHOULD
// // run the go-bindata command from the "$dir" parent directory
// // and just use the ./$dir in the declaration,
// // so all these fixes are not really necessary but they are here
// // for the future
// dir, err := filepath.Abs(s.directory)
// // the absolute dir here can be invalid if running from other
// // folder but we really don't care
// // when we're working with the bindata because
// // we only care for its relative directory
// // see `loadAssets` for more.
// if err != nil {
// return err
// }
// s.directory = dir
// }
// embedded
return s.loadAssets()
}
// load from directory, make the dir absolute here too.
dir, err := filepath.Abs(s.directory)
if err != nil {
return err
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
return err
}
// change the directory field configuration, load happens after directory has been set, so we will not have any problems here.
s.directory = dir
return s.loadDirectory()
}
// loadDirectory builds the templates from directory.
func (s *HTMLEngine) loadDirectory() error {
dir, extension := s.directory, s.extension
var templateErr error
s.Templates = template.New(dir)
s.Templates = template.New(s.rootDir)
s.Templates.Delims(s.left, s.right)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
if info == nil || info.IsDir() {
} else {
rel, err := filepath.Rel(dir, path)
return nil
}
if s.extension != "" {
if !strings.HasSuffix(path, s.extension) {
return nil
}
}
buf, err := asset(s.fs, path)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
contents := string(buf)
name := strings.TrimPrefix(path, "/")
tmpl := s.Templates.New(name)
tmpl.Option(s.options...)
if s.middleware != nil {
contents, err = s.middleware(name, buf)
if err != nil {
templateErr = err
return err
}
ext := filepath.Ext(path)
if ext == extension {
buf, err := ioutil.ReadFile(path)
if err != nil {
templateErr = err
return err
}
contents := string(buf)
name := filepath.ToSlash(rel)
tmpl := s.Templates.New(name)
tmpl.Option(s.options...)
if s.middleware != nil {
contents, err = s.middleware(name, buf)
}
if err != nil {
templateErr = err
return err
}
// s.mu.Lock()
// Add our funcmaps.
_, err = tmpl.
Funcs(emptyFuncs).
// Funcs(s.makeDefaultLayoutFuncs(name)).
// Funcs(s.layoutFuncs).
Funcs(s.funcs).
Parse(contents)
// s.mu.Unlock()
if err != nil {
templateErr = err
return err
}
}
}
return nil
})
if err != nil {
// Add our funcmaps.
_, err = tmpl.
Funcs(emptyFuncs).
// Funcs(s.makeDefaultLayoutFuncs(name)).
// Funcs(s.layoutFuncs).
Funcs(s.funcs).
Parse(contents)
return err
}
return templateErr
}
// loadAssets loads the templates by binary (go-bindata for embedded).
func (s *HTMLEngine) loadAssets() error {
virtualDirectory, virtualExtension := s.directory, s.extension
assetFn, namesFn := s.assetFn, s.namesFn
var templateErr error
s.Templates = template.New(virtualDirectory)
s.Templates.Delims(s.left, s.right)
names := namesFn()
if len(virtualDirectory) > 0 {
if virtualDirectory[0] == '.' { // first check for .wrong
virtualDirectory = virtualDirectory[1:]
}
if virtualDirectory[0] == '/' || virtualDirectory[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed
virtualDirectory = virtualDirectory[1:]
}
}
for _, path := range names {
// if filepath.IsAbs(virtualDirectory) {
// // fixes https://github.com/kataras/iris/issues/784
// // we take the absolute fullpath of the template file.
// pathFileAbs, err := filepath.Abs(path)
// if err != nil {
// templateErr = err
// continue
// }
//
// path = pathFileAbs
// }
// bindata may contain more files than the templates
// so keep that check as it's.
if !strings.HasPrefix(path, virtualDirectory) {
continue
}
ext := filepath.Ext(path)
// check if extension matches
if ext == virtualExtension {
// take the relative path of the path as base of
// virtualDirectory (the absolute path of the view engine that dev passed).
rel, err := filepath.Rel(virtualDirectory, path)
if err != nil {
templateErr = err
break
}
// // take the current working directory
// cpath, err := filepath.Abs(".")
// if err == nil {
// // set the path as relative to "path" of the current working dir.
// // fixes https://github.com/kataras/iris/issues/784
// rpath, err := filepath.Rel(cpath, path)
// // fix view: Asset not found for path ''
// if err == nil && rpath != "" {
// path = rpath
// }
// }
buf, err := assetFn(path)
if err != nil {
templateErr = fmt.Errorf("%v for path '%s'", err, path)
break
}
contents := string(buf)
name := filepath.ToSlash(rel)
// name should be the filename of the template.
tmpl := s.Templates.New(name)
tmpl.Option(s.options...)
if s.middleware != nil {
contents, err = s.middleware(name, buf)
if err != nil {
templateErr = fmt.Errorf("%v for name '%s'", err, name)
break
}
}
// Add our funcmaps.
if _, err = tmpl.Funcs(emptyFuncs).Funcs(s.funcs).Parse(contents); err != nil {
templateErr = err
break
}
}
}
return templateErr
})
}
func (s *HTMLEngine) executeTemplateBuf(name string, binding interface{}) (*bytes.Buffer, error) {
@@ -494,10 +334,6 @@ func (s *HTMLEngine) runtimeFuncsFor(t *template.Template, name string, binding
func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bindingData interface{}) error {
// re-parse the templates if reload is enabled.
if s.reload {
// locks to fix #872, it's the simplest solution and the most correct,
// to execute writers with "wait list", one at a time.
s.rmu.Lock()
defer s.rmu.Unlock()
if err := s.Load(); err != nil {
return err
}
@@ -505,14 +341,14 @@ func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bind
t := s.Templates.Lookup(name)
if t == nil {
return fmt.Errorf("template: %s does not exist in the dir: %s", name, s.directory)
return ErrNotExist{name, false}
}
s.runtimeFuncsFor(t, name, bindingData)
if layout = getLayout(layout, s.layout); layout != "" {
lt := s.Templates.Lookup(layout)
if lt == nil {
return fmt.Errorf("layout: %s does not exist in the dir: %s", name, s.directory)
return ErrNotExist{name, true}
}
s.layoutFuncsFor(lt, name, bindingData)

View File

@@ -3,8 +3,7 @@ package view
import (
"fmt"
"io"
"os"
"path"
"net/http"
"path/filepath"
"reflect"
"strings"
@@ -18,9 +17,10 @@ const jetEngineName = "jet"
// JetEngine is the jet template parser's view engine.
type JetEngine struct {
directory string
fs http.FileSystem
rootDir string
extension string
// physical system files or app-embedded, see `Binary(..., ...)`. Defaults to file system on initialization.
loader jet.Loader
developmentMode bool
@@ -51,11 +51,12 @@ var jetExtensions = [...]string{
// Jet creates and returns a new jet view engine.
// The given "extension" MUST begin with a dot.
func Jet(directory, extension string) *JetEngine {
// if _, err := os.Stat(directory); os.IsNotExist(err) {
// panic(err)
// }
//
// Usage:
// Jet("./views", ".jet") or
// Jet(iris.Dir("./views"), ".jet") or
// Jet(AssetFile(), ".jet") for embedded data.
func Jet(fs interface{}, extension string) *JetEngine {
extOK := false
for _, ext := range jetExtensions {
if ext == extension {
@@ -69,9 +70,9 @@ func Jet(directory, extension string) *JetEngine {
}
s := &JetEngine{
directory: directory,
rootDir: "/",
extension: extension,
loader: jet.NewOSFileSystemLoader(directory),
loader: &jetLoader{fs: getFS(fs)},
jetDataContextKey: "_jet",
}
@@ -83,6 +84,13 @@ func (s *JetEngine) String() string {
return jetEngineName
}
// RootDir sets the directory to be used as a starting point
// to load templates from the provided file system.
func (s *JetEngine) RootDir(root string) *JetEngine {
s.rootDir = filepath.ToSlash(root)
return s
}
// Ext should return the final file extension which this view engine is responsible to render.
func (s *JetEngine) Ext() string {
return s.extension
@@ -175,121 +183,27 @@ func (s *JetEngine) Reload(developmentMode bool) *JetEngine {
}
// SetLoader can be used when the caller wants to use something like
// multi.Loader or httpfs.Loader of the jet subpackages,
// overrides any previous loader may set by `Binary` or the default.
// Should act before `Load` or `iris.Application#RegisterView`.
// multi.Loader or httpfs.Loader.
func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine {
s.loader = loader
return s
}
// Binary optionally, use it when template files are distributed
// inside the app executable (.go generated files).
//
// The assetFn and namesFn can come from the go-bindata library.
// Should act before `Load` or `iris.Application#RegisterView`.
func (s *JetEngine) Binary(assetFn func(name string) ([]byte, error), assetNames func() []string) *JetEngine {
// embedded.
vdir := s.directory
if vdir[0] == '.' {
vdir = vdir[1:]
}
// second check for /something, (or ./something if we had dot on 0 it will be removed)
if vdir[0] == '/' || vdir[0] == os.PathSeparator {
vdir = vdir[1:]
}
// check for trailing slashes because new users may be do that by mistake
// although all examples are showing the correct way but you never know
// i.e "./assets/" is not correct, if was inside "./assets".
// remove last "/".
if trailingSlashIdx := len(vdir) - 1; vdir[trailingSlashIdx] == '/' {
vdir = vdir[0:trailingSlashIdx]
}
namesSlice := assetNames()
names := make(map[string]struct{})
for _, name := range namesSlice {
if !strings.HasPrefix(name, vdir) {
continue
}
extOK := false
fileExt := path.Ext(name)
for _, ext := range jetExtensions {
if ext == fileExt {
extOK = true
break
}
}
if !extOK {
continue
}
names[name] = struct{}{}
}
if len(names) == 0 {
panic("JetEngine.Binary: no embedded files found in directory: " + vdir)
}
s.loader = &embeddedLoader{
vdir: vdir,
asset: assetFn,
names: names,
}
return s
type jetLoader struct {
fs http.FileSystem
}
type (
embeddedLoader struct {
vdir string
asset func(name string) ([]byte, error)
names map[string]struct{}
}
embeddedFile struct {
contents []byte // the contents are NOT consumed.
readen int64
}
)
var _ jet.Loader = (*jetLoader)(nil)
var (
_ jet.Loader = (*embeddedLoader)(nil)
_ io.ReadCloser = (*embeddedFile)(nil)
)
func (f *embeddedFile) Close() error { return nil }
func (f *embeddedFile) Read(p []byte) (int, error) {
if f.readen >= int64(len(f.contents)) {
return 0, io.EOF
}
n := copy(p, f.contents[f.readen:])
f.readen += int64(n)
return n, nil
}
// Open opens a file from OS file system.
func (l *embeddedLoader) Open(name string) (io.ReadCloser, error) {
name = path.Join(l.vdir, filepath.ToSlash(name))
contents, err := l.asset(name)
if err != nil {
return nil, err
}
return &embeddedFile{
contents: contents,
}, nil
// Open opens a file from file system.
func (l *jetLoader) Open(name string) (io.ReadCloser, error) {
return l.fs.Open(name)
}
// Exists checks if the template name exists by walking the list of template paths
// returns string with the full path of the template and bool true if the template file was found
func (l *embeddedLoader) Exists(name string) (string, bool) {
name = path.Join(l.vdir, filepath.ToSlash(name))
if _, ok := l.names[name]; ok {
func (l *jetLoader) Exists(name string) (string, bool) {
if _, err := l.fs.Open(name); err == nil {
return name, true
}

View File

@@ -2,9 +2,6 @@ package view
import (
"bytes"
"io/ioutil"
"path"
"strings"
"github.com/iris-contrib/jade"
)
@@ -16,26 +13,23 @@ import (
//
// Read more about the Jade Go Parser: https://github.com/Joker/jade
//
// Usage:
// Pug("./views", ".pug") or
// Pug(iris.Dir("./views"), ".pug") or
// Pug(AssetFile(), ".pug") for embedded data.
//
// Examples:
// https://github.com/kataras/iris/tree/master/_examples/view/template_pug_0
// https://github.com/kataras/iris/tree/master/_examples/view/template_pug_1
// https://github.com/kataras/iris/tree/master/_examples/view/template_pug_2
// https://github.com/kataras/iris/tree/master/_examples/view/template_pug_3
func Pug(directory, extension string) *HTMLEngine {
s := HTML(directory, extension)
func Pug(fs interface{}, extension string) *HTMLEngine {
s := HTML(fs, extension)
s.middleware = func(name string, text []byte) (contents string, err error) {
name = path.Join(path.Clean(directory), name)
tmpl := jade.New(name)
tmpl.ReadFunc = func(name string) ([]byte, error) {
if !strings.HasPrefix(path.Clean(name), path.Clean(directory)) {
name = path.Join(directory, name)
}
if s.assetFn != nil {
return s.assetFn(name)
}
return ioutil.ReadFile(name)
return asset(s.fs, name)
}
// Fixes: https://github.com/kataras/iris/issues/1450
@@ -43,7 +37,6 @@ func Pug(directory, extension string) *HTMLEngine {
// And Also able to use relative paths on "extends" and "include" directives:
// e.g. instead of extends "templates/layout.pug" we use "layout.pug"
// so contents of templates are independent of their root location.
exec, err := tmpl.Parse(text)
if err != nil {
return

View File

@@ -19,6 +19,21 @@ type (
EngineFuncer = context.ViewEngineFuncer
)
// ErrNotExist reports whether a template was not found in the parsed templates tree.
type ErrNotExist struct {
Name string
IsLayout bool
}
// Error implements the `error` interface.
func (e ErrNotExist) Error() string {
title := "template"
if e.IsLayout {
title = "layout"
}
return fmt.Sprintf("%s '%s' does not exist", title, e.Name)
}
// View is responsible to
// load the correct templates
// for each of the registered view engines.