gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
78.84k stars 8.02k forks source link

If method loading html files from embed.FS type could be helpful?? #2795

Open j1mmyson opened 3 years ago

j1mmyson commented 3 years ago

First of all, thank you for creating a very useful framework !!

Recently, I found out that the gin framework cannot load template with embed.FS type and I have solved it in the following way.

//embed:go web/templates/*.html
var templatesFS embed.FS

func main(){
    r := gin.Default()
    LoadHTMLFromEmbedFS(r, templatesFS, "web/templates/*")
    ...
    r.Run()
}

func LoadHTMLFromEmbedFS(engine *gin.Engine, embedFS embed.FS, pattern string){
    templ := template.Must(template.ParseFS(embedFS, pattern))
    engine.SetHTMLTemplate(templ)
}

Is it convenient to use this function in the form of a method?

func (engine *Engine) LoadHTMLFromFS(embedFS embed.FS, pattern string){
    templ := template.Must(template.ParseFS(embedFS, pattern))
    engine.SetHTMLTemplate(templ)
}

and users use it like

//embed:go web/templates/*.html
var templatesFS embed.FS

func main(){
    r := gin.Default()
    r.LoadHTMLFromFS(templatesFS, "web/templates/*")
    ...
    r.Run()
}

I don't know whether my code is safe or not, so leave it as an issue for now. Thank you!

sesopenko commented 3 years ago

The workaround from @j1mmyson above doesn't handle templates in recursive directories. Combining this solution from stack exchange with the above lets you attach templates from recursive embedded directories:

package main

import (
    "embed"
    "github.com/gin-gonic/gin"
    "html/template"
    "io/fs"
    "net/http"
    "strings"
)

// template files in directory:
// templates/index.html
// templates/test/nested.html

//go:embed templates/**
var f embed.FS

func main() {
    r := gin.Default()
    root := template.New("")
    LoadAndAddToRoot(r.FuncMap, root)
    r.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "templates/index.html", gin.H{})
    })
    r.GET("/embed", func(c *gin.Context) {
        c.HTML(http.StatusOK, "templates/test/nested.html", gin.H{})
    })
    r.Run("127.0.0.1:8080")
}

func LoadAndAddToRoot(funcMap template.FuncMap, rootTemplate *template.Template) error {
    // This solution is CC by share alike 4.0
    // Copyright Rik-777
    // https://stackoverflow.com/a/50581032
    err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, walkErr error) error {
        if walkErr != nil {
            return walkErr
        }
        if !d.IsDir() && strings.HasSuffix(path, ".html") {
            data, readErr := f.ReadFile(path)
            if readErr != nil {
                return readErr
            }
            t := rootTemplate.New(path).Funcs(funcMap)
            if _, parseErr := t.Parse(string(data)); parseErr != nil {
                return parseErr
            }
        }
        return nil
    })
    return err
}
j1mmyson commented 3 years ago

@sesopenko Thank you for comments! according to your comments, it finally attach templates from recursive embedded directories.

func LoadHTMLFromEmbedFS(engine *gin.Engine, embedFS embed.FS, pattern string) {
    root := template.New("")
    tmpl := template.Must(root, LoadAndAddToRoot(engine.FuncMap, root, embedFS, pattern))
    engine.SetHTMLTemplate(tmpl)
}

// Method version
// func (engine *gin.Engine) LoadHTMLFromFS(embedFS embed.FS, pattern string) {
//  root := template.New("")
//  tmpl := template.Must(root, LoadAndAddToRoot(engine.FuncMap, root, embedFS, pattern))
//  engine.SetHTMLTemplate(tmpl)
// }

func LoadAndAddToRoot(funcMap template.FuncMap, rootTemplate *template.Template, embedFS embed.FS, pattern string) error {
    pattern = strings.ReplaceAll(pattern, ".", "\\.")
    pattern = strings.ReplaceAll(pattern, "*", ".*")

    err := fs.WalkDir(embedFS, ".", func(path string, d fs.DirEntry, walkErr error) error {
        if walkErr != nil {
            return walkErr
        }

        if matched, _ := regexp.MatchString(pattern, path); !d.IsDir() && matched {
            data, readErr := embedFS.ReadFile(path)
            if readErr != nil {
                return readErr
            }
            t := rootTemplate.New(path).Funcs(funcMap)
            if _, parseErr := t.Parse(string(data)); parseErr != nil {
                return parseErr
            }
        }
        return nil
    })
    return err
}

and use it like

package main

import (
    "embed"
    "html/template"
    "io/fs"
    "net/http"
    "regexp"
    "strings"

    "github.com/gin-gonic/gin"
)

//go:embed web/templates
var templatesFS embed.FS

func main() {
    r := gin.Default()
    LoadHTMLFromEmbedFS(r, templatesFS, "web/templates/*html")
        // if method
        // r.LoadHTMLFromEmbedFS(templatesFS, "web/templates/*html")

        ...

    r.Run()
}

following is an example: https://github.com/j1mmyson/LoadHTMLFromEmbedFS-example

from now on, is there any improvement point or problems? Thanks again for your comments!

walnut-tom commented 1 year ago

the template has method

func ParseFS(fs fs.FS, patterns ...string) (*Template, error) 
func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error)

can add a method like this:

func (engine *Engine) LoadHTMLFS(fs fs.FS, patterns ...string) {
    if IsDebugging() {
        engine.HTMLRender = render.HTMLDebug{Files: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
        return
    }

    templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(fs, patterns...))
    engine.SetHTMLTemplate(templ)
}