mirror of
https://github.com/kataras/iris.git
synced 2026-01-10 21:45:57 +00:00
add support for fs.FS, embed.FS (in addition of string and http.FileSystem) for i18n locales and view engine's templates
This commit is contained in:
@@ -154,7 +154,7 @@ func hi(ctx iris.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
A real example can be found here: https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app.
|
||||
Examples can be found here: https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app and https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app-bindata.
|
||||
|
||||
## Reload
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func (s *AceEngine) SetIndent(indent string) *AceEngine {
|
||||
// Usage:
|
||||
// Ace("./views", ".ace") or
|
||||
// Ace(iris.Dir("./views"), ".ace") or
|
||||
// Ace(AssetFile(), ".ace") for embedded data.
|
||||
// Ace(embed.FS, ".ace") or Ace(AssetFile(), ".ace") for embedded data.
|
||||
func Ace(fs interface{}, extension string) *AceEngine {
|
||||
s := &AceEngine{HTMLEngine: HTML(fs, extension), indent: ""}
|
||||
s.name = "Ace"
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
|
||||
// AmberEngine contains the amber view engine structure.
|
||||
type AmberEngine struct {
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
// files configuration
|
||||
rootDir string
|
||||
extension string
|
||||
@@ -43,15 +44,15 @@ var amberOnce = new(uint32)
|
||||
// Usage:
|
||||
// Amber("./views", ".amber") or
|
||||
// Amber(iris.Dir("./views"), ".amber") or
|
||||
// Amber(AssetFile(), ".amber") for embedded data.
|
||||
func Amber(fs interface{}, extension string) *AmberEngine {
|
||||
// Amber(embed.FS, ".amber") or Amber(AssetFile(), ".amber") for embedded data.
|
||||
func Amber(dirOrFS interface{}, extension string) *AmberEngine {
|
||||
if atomic.LoadUint32(amberOnce) > 0 {
|
||||
panic("Amber: cannot be registered twice as its internal implementation share the same template functions across instances.")
|
||||
} else {
|
||||
atomic.StoreUint32(amberOnce, 1)
|
||||
}
|
||||
|
||||
fileSystem := getFS(fs)
|
||||
fileSystem := getFS(dirOrFS)
|
||||
s := &AmberEngine{
|
||||
fs: fileSystem,
|
||||
rootDir: "/",
|
||||
@@ -60,7 +61,7 @@ func Amber(fs interface{}, extension string) *AmberEngine {
|
||||
Options: amber.Options{
|
||||
PrettyPrint: false,
|
||||
LineNumbers: false,
|
||||
VirtualFilesystem: fileSystem,
|
||||
VirtualFilesystem: http.FS(fileSystem),
|
||||
},
|
||||
bufPool: &sync.Pool{New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
@@ -84,6 +85,15 @@ func Amber(fs interface{}, extension string) *AmberEngine {
|
||||
// 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 {
|
||||
if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
|
||||
sub, err := fs.Sub(s.fs, s.rootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.fs = sub // here so the "middleware" can work.
|
||||
}
|
||||
|
||||
s.rootDir = filepath.ToSlash(root)
|
||||
return s
|
||||
}
|
||||
@@ -142,7 +152,7 @@ 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 {
|
||||
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
return walk(s.fs, "", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func WrapBlocks(v *blocks.Blocks) *BlocksEngine {
|
||||
// Usage:
|
||||
// Blocks("./views", ".html") or
|
||||
// Blocks(iris.Dir("./views"), ".html") or
|
||||
// Blocks(AssetFile(), ".html") for embedded data.
|
||||
// Blocks(embed.FS, ".html") or Blocks(AssetFile(), ".html") for embedded data.
|
||||
func Blocks(fs interface{}, extension string) *BlocksEngine {
|
||||
return WrapBlocks(blocks.New(fs).Extension(extension))
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (s *BlocksEngine) Name() string {
|
||||
}
|
||||
|
||||
// RootDir sets the directory to use as the root one inside the provided File System.
|
||||
func (s *BlocksEngine) RootDir(root string) *BlocksEngine {
|
||||
func (s *BlocksEngine) RootDir(root string) *BlocksEngine { // TODO: update blocks for the new fs.FS interface and use it for Sub.
|
||||
s.Engine.RootDir(root)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package view
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"io/fs"
|
||||
"os"
|
||||
stdPath "path"
|
||||
"path/filepath"
|
||||
@@ -62,7 +62,7 @@ var AsSafeValue = pongo2.AsSafeValue
|
||||
|
||||
type tDjangoAssetLoader struct {
|
||||
rootDir string
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
// Abs calculates the path to a given template. Whenever a path must be resolved
|
||||
@@ -91,7 +91,7 @@ func (l *tDjangoAssetLoader) Get(path string) (io.Reader, error) {
|
||||
|
||||
// DjangoEngine contains the django view engine structure.
|
||||
type DjangoEngine struct {
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
// files configuration
|
||||
rootDir string
|
||||
extension string
|
||||
@@ -117,7 +117,7 @@ var (
|
||||
// Usage:
|
||||
// Django("./views", ".html") or
|
||||
// Django(iris.Dir("./views"), ".html") or
|
||||
// Django(AssetFile(), ".html") for embedded data.
|
||||
// Django(embed.FS, ".html") or Django(AssetFile(), ".html") for embedded data.
|
||||
func Django(fs interface{}, extension string) *DjangoEngine {
|
||||
s := &DjangoEngine{
|
||||
fs: getFS(fs),
|
||||
@@ -134,6 +134,15 @@ func Django(fs interface{}, extension string) *DjangoEngine {
|
||||
// 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 {
|
||||
if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
|
||||
sub, err := fs.Sub(s.fs, s.rootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.fs = sub // here so the "middleware" can work.
|
||||
}
|
||||
|
||||
s.rootDir = filepath.ToSlash(root)
|
||||
return s
|
||||
}
|
||||
@@ -213,7 +222,7 @@ 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 {
|
||||
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
return walk(s.fs, "", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
129
view/fs.go
129
view/fs.go
@@ -2,125 +2,54 @@ package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
)
|
||||
|
||||
// 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)
|
||||
// walk recursively in "fileSystem" descends "root" path, calling "walkFn".
|
||||
func walk(fileSystem fs.FS, root string, walkFn filepath.WalkFunc) error {
|
||||
if root != "" && root != "/" && root != "." {
|
||||
sub, err := fs.Sub(fileSystem, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileSystem = sub
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
fullpath := path.Join(root, name)
|
||||
f, err := fs.Open(fullpath)
|
||||
if root == "" {
|
||||
root = "."
|
||||
}
|
||||
|
||||
return fs.WalkDir(fileSystem, root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", fullpath, err)
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
stat, err := f.Stat()
|
||||
err = walkFn(fullpath, stat, err)
|
||||
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
if err != filepath.SkipDir {
|
||||
return fmt.Errorf("%s: %w", fullpath, err)
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
if err := walk(fs, fullpath, walkFn); err != nil {
|
||||
return fmt.Errorf("%s: %w", fullpath, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return walkFn(path, info, err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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(fileSystem fs.FS, name string) ([]byte, error) {
|
||||
return fs.ReadFile(fileSystem, name)
|
||||
}
|
||||
|
||||
func asset(fs http.FileSystem, name string) ([]byte, error) {
|
||||
f, err := fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contents, err := io.ReadAll(f)
|
||||
f.Close()
|
||||
return contents, err
|
||||
}
|
||||
|
||||
func getFS(fsOrDir interface{}) (fs http.FileSystem) {
|
||||
if fsOrDir == nil {
|
||||
return noOpFS{}
|
||||
}
|
||||
|
||||
if v, ok := fsOrDir.(string); ok {
|
||||
if v == "" {
|
||||
fs = noOpFS{}
|
||||
} else {
|
||||
fs = httpDirWrapper{http.Dir(v)}
|
||||
}
|
||||
} else {
|
||||
fs = context.ResolveFS(fsOrDir)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type noOpFS struct{}
|
||||
|
||||
func (fs noOpFS) Open(name string) (http.File, error) { return nil, nil }
|
||||
|
||||
func isNoOpFS(fs http.FileSystem) bool {
|
||||
_, ok := fs.(noOpFS)
|
||||
return ok
|
||||
}
|
||||
|
||||
// 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 (fs httpDirWrapper) Open(name string) (http.File, error) {
|
||||
return fs.Dir.Open(filepath.ToSlash(name))
|
||||
func getFS(fsOrDir interface{}) fs.FS {
|
||||
return context.ResolveFS(fsOrDir)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// HandlebarsEngine contains the handlebars view engine structure.
|
||||
type HandlebarsEngine struct {
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
// files configuration
|
||||
rootDir string
|
||||
extension string
|
||||
@@ -41,7 +41,7 @@ var (
|
||||
// Usage:
|
||||
// Handlebars("./views", ".html") or
|
||||
// Handlebars(iris.Dir("./views"), ".html") or
|
||||
// Handlebars(AssetFile(), ".html") for embedded data.
|
||||
// Handlebars(embed.FS, ".html") or Handlebars(AssetFile(), ".html") for embedded data.
|
||||
func Handlebars(fs interface{}, extension string) *HandlebarsEngine {
|
||||
s := &HandlebarsEngine{
|
||||
fs: getFS(fs),
|
||||
@@ -66,6 +66,15 @@ func Handlebars(fs interface{}, extension string) *HandlebarsEngine {
|
||||
// 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 {
|
||||
if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
|
||||
sub, err := fs.Sub(s.fs, s.rootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.fs = sub // here so the "middleware" can work.
|
||||
}
|
||||
|
||||
s.rootDir = filepath.ToSlash(root)
|
||||
return s
|
||||
}
|
||||
@@ -125,7 +134,7 @@ func (s *HandlebarsEngine) AddGlobalFunc(funcName string, funcBody interface{})
|
||||
//
|
||||
// Returns an error if something bad happens, user is responsible to catch it.
|
||||
func (s *HandlebarsEngine) Load() error {
|
||||
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, _ error) error {
|
||||
return walk(s.fs, "", func(path string, info os.FileInfo, _ error) error {
|
||||
if info == nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
31
view/html.go
31
view/html.go
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
type HTMLEngine struct {
|
||||
name string // the view engine's name, can be HTML, Ace or Pug.
|
||||
// the file system to load from.
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
// files configuration
|
||||
rootDir string
|
||||
extension string
|
||||
@@ -80,11 +80,11 @@ var emptyFuncs = template.FuncMap{
|
||||
// Usage:
|
||||
// HTML("./views", ".html") or
|
||||
// HTML(iris.Dir("./views"), ".html") or
|
||||
// HTML(AssetFile(), ".html") for embedded data.
|
||||
func HTML(fs interface{}, extension string) *HTMLEngine {
|
||||
// HTML(embed.FS, ".html") or HTML(AssetFile(), ".html") for embedded data.
|
||||
func HTML(dirOrFS interface{}, extension string) *HTMLEngine {
|
||||
s := &HTMLEngine{
|
||||
name: "HTML",
|
||||
fs: getFS(fs),
|
||||
fs: getFS(dirOrFS),
|
||||
rootDir: "/",
|
||||
extension: extension,
|
||||
reload: false,
|
||||
@@ -104,6 +104,15 @@ func HTML(fs interface{}, extension string) *HTMLEngine {
|
||||
// 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 {
|
||||
if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
|
||||
sub, err := fs.Sub(s.fs, root)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.fs = sub // here so the "middleware" can work.
|
||||
}
|
||||
|
||||
s.rootDir = filepath.ToSlash(root)
|
||||
return s
|
||||
}
|
||||
@@ -246,7 +255,7 @@ func (s *HTMLEngine) load() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
err := walk(s.fs, "", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,6 +277,16 @@ func (s *HTMLEngine) load() error {
|
||||
|
||||
return s.parseTemplate(path, buf, nil)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Templates == nil {
|
||||
return fmt.Errorf("no templates found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HTMLEngine) reloadCustomTemplates() error {
|
||||
|
||||
27
view/jet.go
27
view/jet.go
@@ -3,7 +3,7 @@ package view
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -19,7 +19,7 @@ const jetEngineName = "jet"
|
||||
|
||||
// JetEngine is the jet template parser's view engine.
|
||||
type JetEngine struct {
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
rootDir string
|
||||
extension string
|
||||
left, right string
|
||||
@@ -59,8 +59,8 @@ var jetExtensions = [...]string{
|
||||
// Usage:
|
||||
// Jet("./views", ".jet") or
|
||||
// Jet(iris.Dir("./views"), ".jet") or
|
||||
// Jet(AssetFile(), ".jet") for embedded data.
|
||||
func Jet(fs interface{}, extension string) *JetEngine {
|
||||
// Jet(embed.FS, ".jet") or Jet(AssetFile(), ".jet") for embedded data.
|
||||
func Jet(dirOrFS interface{}, extension string) *JetEngine {
|
||||
extOK := false
|
||||
for _, ext := range jetExtensions {
|
||||
if ext == extension {
|
||||
@@ -74,10 +74,10 @@ func Jet(fs interface{}, extension string) *JetEngine {
|
||||
}
|
||||
|
||||
s := &JetEngine{
|
||||
fs: getFS(fs),
|
||||
fs: getFS(dirOrFS),
|
||||
rootDir: "/",
|
||||
extension: extension,
|
||||
loader: &jetLoader{fs: getFS(fs)},
|
||||
loader: &jetLoader{fs: getFS(dirOrFS)},
|
||||
jetDataContextKey: "_jet",
|
||||
}
|
||||
|
||||
@@ -92,6 +92,15 @@ func (s *JetEngine) String() string {
|
||||
// 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 {
|
||||
if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
|
||||
sub, err := fs.Sub(s.fs, s.rootDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s.fs = sub
|
||||
}
|
||||
|
||||
s.rootDir = filepath.ToSlash(root)
|
||||
return s
|
||||
}
|
||||
@@ -199,7 +208,7 @@ func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine {
|
||||
}
|
||||
|
||||
type jetLoader struct {
|
||||
fs http.FileSystem
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
var _ jet.Loader = (*jetLoader)(nil)
|
||||
@@ -217,7 +226,7 @@ func (l *jetLoader) Exists(name string) bool {
|
||||
|
||||
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
|
||||
func (s *JetEngine) Load() error {
|
||||
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
return walk(s.fs, "", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -256,7 +265,7 @@ func (s *JetEngine) initSet() {
|
||||
var opts = []jet.Option{
|
||||
jet.WithDelims(s.left, s.right),
|
||||
}
|
||||
if s.developmentMode && !isNoOpFS(s.fs) {
|
||||
if s.developmentMode && !context.IsNoOpFS(s.fs) {
|
||||
// this check is made to avoid jet's fs lookup on noOp fs (nil passed by the developer).
|
||||
// This can be produced when nil fs passed
|
||||
// and only `ParseTemplate` is used.
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// Usage:
|
||||
// Pug("./views", ".pug") or
|
||||
// Pug(iris.Dir("./views"), ".pug") or
|
||||
// Pug(AssetFile(), ".pug") for embedded data.
|
||||
// Pug(embed.FS, ".pug") or Pug(AssetFile(), ".pug") for embedded data.
|
||||
//
|
||||
// Examples:
|
||||
// https://github.com/kataras/iris/tree/master/_examples/view/template_pug_0
|
||||
|
||||
Reference in New Issue
Block a user