diff --git a/README.md b/README.md
index aa8ada4e..9f548cc0 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,7 @@ See the [API documentation on go.dev](https://pkg.go.dev/github.com/gin-gonic/gi
The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
-- [English](https://gin-gonic.com/docs/)
+- [English](https://gin-gonic.com/en/docs/)
- [简体中文](https://gin-gonic.com/zh-cn/docs/)
- [繁體中文](https://gin-gonic.com/zh-tw/docs/)
- [日本語](https://gin-gonic.com/ja/docs/)
diff --git a/docs/doc.md b/docs/doc.md
index cd651390..ce466652 100644
--- a/docs/doc.md
+++ b/docs/doc.md
@@ -1393,13 +1393,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 0761c14d..f9813e1d 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 1dcb1919..3e6a92eb 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 850ae09b..a80b690e 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(http.MethodGet, "/", HandlersChain{func(_ *Context) {}})
diff --git a/go.mod b/go.mod
index 1223267c..3a7e1ba6 100644
--- a/go.mod
+++ b/go.mod
@@ -4,16 +4,16 @@ go 1.23.0
require (
github.com/bytedance/sonic v1.13.1
- github.com/gin-contrib/sse v0.1.0
- github.com/go-playground/validator/v10 v10.22.1
+ github.com/gin-contrib/sse v1.1.0
+ github.com/go-playground/validator/v10 v10.26.0
github.com/goccy/go-json v0.10.2
github.com/json-iterator/go v1.1.12
github.com/mattn/go-isatty v0.0.20
github.com/pelletier/go-toml/v2 v2.2.2
- github.com/quic-go/quic-go v0.48.2
- github.com/stretchr/testify v1.9.0
+ github.com/quic-go/quic-go v0.51.0
+ github.com/stretchr/testify v1.10.0
github.com/ugorji/go/codec v1.2.12
- golang.org/x/net v0.37.0
+ golang.org/x/net v0.38.0
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1
)
@@ -22,7 +22,7 @@ require (
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -35,12 +35,12 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
- go.uber.org/mock v0.4.0 // indirect
+ go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.36.0 // indirect
- golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
- golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/mod v0.18.0 // indirect
+ golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
- golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+ golang.org/x/tools v0.22.0 // indirect
)
diff --git a/go.sum b/go.sum
index 2e1c30ad..5a7f2adf 100644
--- a/go.sum
+++ b/go.sum
@@ -12,10 +12,10 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
-github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
+github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -24,8 +24,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
-github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@@ -61,8 +61,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
-github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
+github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
+github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -74,24 +74,23 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
-go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
-golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -100,10 +99,8 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 00000000..524ac08b
--- /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 00000000..113e92b6
--- /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 c308408d..f5e7455a 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/json.go b/render/json.go
index fc8dea45..a6b54dc3 100644
--- a/render/json.go
+++ b/render/json.go
@@ -151,7 +151,7 @@ func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
}
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
-func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
+func (r AsciiJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
if err != nil {
@@ -159,12 +159,15 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
}
var buffer bytes.Buffer
+ escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
+
for _, r := range bytesconv.BytesToString(ret) {
- cvt := string(r)
if r >= 128 {
- cvt = fmt.Sprintf("\\u%04x", int64(r))
+ escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
+ buffer.Write(escapeBuf)
+ } else {
+ buffer.WriteByte(byte(r))
}
- buffer.WriteString(cvt)
}
_, err = w.Write(buffer.Bytes())
diff --git a/render/render_test.go b/render/render_test.go
index ad633b00..4dd2a3af 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) })
}