diff --git a/docs/doc.md b/docs/doc.md
index b76011f2ab..303da647a3 100644
--- a/docs/doc.md
+++ b/docs/doc.md
@@ -1341,13 +1341,19 @@ func main() {
### HTML rendering
-Using LoadHTMLGlob() or LoadHTMLFiles()
+Using LoadHTMLGlob() or LoadHTMLFiles() or LoadHTMLFS()
```go
+//go:embed templates/*
+var templates embed.FS
+
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
+ //router.LoadHTMLFS(http.Dir("templates"), "template1.html", "template2.html")
+ //or
+ //router.LoadHTMLFS(http.FS(templates), "templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
diff --git a/gin.go b/gin.go
index 48cc15c985..10b6ed957a 100644
--- a/gin.go
+++ b/gin.go
@@ -16,6 +16,7 @@ import (
"sync"
"github.com/gin-gonic/gin/internal/bytesconv"
+ filesystem "github.com/gin-gonic/gin/internal/fs"
"github.com/gin-gonic/gin/render"
"github.com/quic-go/quic-go/http3"
@@ -285,6 +286,19 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ)
}
+// LoadHTMLFS loads an http.FileSystem and a slice of patterns
+// and associates the result with HTML renderer.
+func (engine *Engine) LoadHTMLFS(fs http.FileSystem, patterns ...string) {
+ if IsDebugging() {
+ engine.HTMLRender = render.HTMLDebug{FileSystem: fs, Patterns: patterns, FuncMap: engine.FuncMap, Delims: engine.delims}
+ return
+ }
+
+ templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFS(
+ filesystem.FileSystem{FileSystem: fs}, patterns...))
+ engine.SetHTMLTemplate(templ)
+}
+
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
diff --git a/ginS/gins.go b/ginS/gins.go
index ea38c613ce..a7d6e92ac1 100644
--- a/ginS/gins.go
+++ b/ginS/gins.go
@@ -32,6 +32,11 @@ func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...)
}
+// LoadHTMLFS is a wrapper for Engine.LoadHTMLFS.
+func LoadHTMLFS(fs http.FileSystem, patterns ...string) {
+ engine().LoadHTMLFS(fs, patterns...)
+}
+
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ)
diff --git a/gin_test.go b/gin_test.go
index 719f63e454..9f81900346 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -325,6 +325,115 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
assert.Equal(t, "Date: 2017/07/01", string(resp))
}
+var tmplFS = http.Dir("testdata/template")
+
+func TestLoadHTMLFSTestMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
+ if err != nil {
+ t.Error(err)
+ }
+
+ resp, _ := io.ReadAll(res.Body)
+ assert.Equal(t, "
Hello world
", string(resp))
+}
+
+func TestLoadHTMLFSDebugMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ DebugMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
+ if err != nil {
+ t.Error(err)
+ }
+
+ resp, _ := io.ReadAll(res.Body)
+ assert.Equal(t, "Hello world
", string(resp))
+}
+
+func TestLoadHTMLFSReleaseMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ ReleaseMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
+ if err != nil {
+ t.Error(err)
+ }
+
+ resp, _ := io.ReadAll(res.Body)
+ assert.Equal(t, "Hello world
", string(resp))
+}
+
+func TestLoadHTMLFSUsingTLS(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ true,
+ func(router *Engine) {
+ router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ }
+ client := &http.Client{Transport: tr}
+ res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
+ if err != nil {
+ t.Error(err)
+ }
+
+ resp, _ := io.ReadAll(res.Body)
+ assert.Equal(t, "Hello world
", string(resp))
+}
+
+func TestLoadHTMLFSFuncMap(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFS(tmplFS, "hello.tmpl", "raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
+ if err != nil {
+ t.Error(err)
+ }
+
+ resp, _ := io.ReadAll(res.Body)
+ assert.Equal(t, "Date: 2017/07/01", string(resp))
+}
+
func TestAddRoute(t *testing.T) {
router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 0000000000..524ac08b0d
--- /dev/null
+++ b/internal/fs/fs.go
@@ -0,0 +1,22 @@
+package fs
+
+import (
+ "io/fs"
+ "net/http"
+)
+
+// FileSystem implements an [fs.FS].
+type FileSystem struct {
+ http.FileSystem
+}
+
+// Open passes `Open` to the upstream implementation and return an [fs.File].
+func (o FileSystem) Open(name string) (fs.File, error) {
+ f, err := o.FileSystem.Open(name)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return fs.File(f), nil
+}
diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go
new file mode 100644
index 0000000000..113e92b65c
--- /dev/null
+++ b/internal/fs/fs_test.go
@@ -0,0 +1,49 @@
+package fs
+
+import (
+ "errors"
+ "net/http"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type mockFileSystem struct {
+ open func(name string) (http.File, error)
+}
+
+func (m *mockFileSystem) Open(name string) (http.File, error) {
+ return m.open(name)
+}
+
+func TesFileSystem_Open(t *testing.T) {
+ var testFile *os.File
+ mockFS := &mockFileSystem{
+ open: func(name string) (http.File, error) {
+ return testFile, nil
+ },
+ }
+ fs := &FileSystem{mockFS}
+
+ file, err := fs.Open("foo")
+
+ require.NoError(t, err)
+ assert.Equal(t, testFile, file)
+}
+
+func TestFileSystem_Open_err(t *testing.T) {
+ testError := errors.New("mock")
+ mockFS := &mockFileSystem{
+ open: func(_ string) (http.File, error) {
+ return nil, testError
+ },
+ }
+ fs := &FileSystem{mockFS}
+
+ file, err := fs.Open("foo")
+
+ require.ErrorIs(t, err, testError)
+ assert.Nil(t, file)
+}
diff --git a/render/html.go b/render/html.go
index c308408d2a..f5e7455a02 100644
--- a/render/html.go
+++ b/render/html.go
@@ -7,6 +7,8 @@ package render
import (
"html/template"
"net/http"
+
+ "github.com/gin-gonic/gin/internal/fs"
)
// Delims represents a set of Left and Right delimiters for HTML template rendering.
@@ -31,10 +33,12 @@ type HTMLProduction struct {
// HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct {
- Files []string
- Glob string
- Delims Delims
- FuncMap template.FuncMap
+ Files []string
+ Glob string
+ FileSystem http.FileSystem
+ Patterns []string
+ Delims Delims
+ FuncMap template.FuncMap
}
// HTML contains template reference and its name with given interface object.
@@ -73,7 +77,11 @@ func (r HTMLDebug) loadTemplate() *template.Template {
if r.Glob != "" {
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
}
- panic("the HTML debug render was created without files or glob pattern")
+ if r.FileSystem != nil && len(r.Patterns) > 0 {
+ return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(
+ fs.FileSystem{FileSystem: r.FileSystem}, r.Patterns...))
+ }
+ panic("the HTML debug render was created without files or glob pattern or file system with patterns")
}
// Render (HTML) executes template and writes its result with custom ContentType for response.
diff --git a/render/render_test.go b/render/render_test.go
index 27a5065be7..6dabb8a8c5 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -489,10 +489,12 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
- Files: []string{"../testdata/template/hello.tmpl"},
- Glob: "",
- Delims: Delims{Left: "{[{", Right: "}]}"},
- FuncMap: nil,
+ Files: []string{"../testdata/template/hello.tmpl"},
+ Glob: "",
+ FileSystem: nil,
+ Patterns: nil,
+ Delims: Delims{Left: "{[{", Right: "}]}"},
+ FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
@@ -508,10 +510,33 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{
- Files: nil,
- Glob: "../testdata/template/hello*",
- Delims: Delims{Left: "{[{", Right: "}]}"},
- FuncMap: nil,
+ Files: nil,
+ Glob: "../testdata/template/hello*",
+ FileSystem: nil,
+ Patterns: nil,
+ Delims: Delims{Left: "{[{", Right: "}]}"},
+ FuncMap: nil,
+ }
+ instance := htmlRender.Instance("hello.tmpl", map[string]any{
+ "name": "thinkerou",
+ })
+
+ err := instance.Render(w)
+
+ require.NoError(t, err)
+ assert.Equal(t, "Hello thinkerou
", w.Body.String())
+ assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
+func TestRenderHTMLDebugFS(t *testing.T) {
+ w := httptest.NewRecorder()
+ htmlRender := HTMLDebug{
+ Files: nil,
+ Glob: "",
+ FileSystem: http.Dir("../testdata/template"),
+ Patterns: []string{"hello.tmpl"},
+ Delims: Delims{Left: "{[{", Right: "}]}"},
+ FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]any{
"name": "thinkerou",
@@ -526,10 +551,12 @@ func TestRenderHTMLDebugGlob(t *testing.T) {
func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{
- Files: nil,
- Glob: "",
- Delims: Delims{"{{", "}}"},
- FuncMap: nil,
+ Files: nil,
+ Glob: "",
+ FileSystem: nil,
+ Patterns: nil,
+ Delims: Delims{"{{", "}}"},
+ FuncMap: nil,
}
assert.Panics(t, func() { htmlRender.Instance("", nil) })
}