From cdfaa60817cf06f04e8d4283106d0c873dc13e7b Mon Sep 17 00:00:00 2001 From: zhangmj Date: Thu, 21 Nov 2024 14:18:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/jethtml/functions.go | 1 + components/jethtml/jet.go | 27 +-- context.go | 68 ++++-- render/data.go => data.go | 2 +- gin.go | 53 +---- go.mod | 1 + go.sum | 2 + html.go | 17 ++ jet.go | 129 ++++++++++ render/json.go => json.go | 2 +- render/msgpack.go => msgpack.go | 2 +- render/protobuf.go => protobuf.go | 2 +- render/reader.go => reader.go | 2 +- render/reader_test.go => reader_test.go | 2 +- render/redirect.go => redirect.go | 2 +- render/render.go => render.go | 31 ++- render/html.go | 110 --------- render_functions.go | 221 ++++++++++++++++++ ..._msgpack_test.go => render_msgpack_test.go | 2 +- render/render_test.go => render_test.go | 82 +------ render/text.go => text.go | 2 +- render/toml.go => toml.go | 2 +- render/xml.go => xml.go | 2 +- render/yaml.go => yaml.go | 2 +- 24 files changed, 454 insertions(+), 312 deletions(-) create mode 100644 components/jethtml/functions.go rename render/data.go => data.go (97%) create mode 100644 html.go create mode 100644 jet.go rename render/json.go => json.go (99%) rename render/msgpack.go => msgpack.go (98%) rename render/protobuf.go => protobuf.go (98%) rename render/reader.go => reader.go (98%) rename render/reader_test.go => reader_test.go (97%) rename render/redirect.go => redirect.go (98%) rename render/render.go => render.go (54%) delete mode 100644 render/html.go create mode 100644 render_functions.go rename render/render_msgpack_test.go => render_msgpack_test.go (98%) rename render/render_test.go => render_test.go (85%) rename render/text.go => text.go (98%) rename render/toml.go => toml.go (98%) rename render/xml.go => xml.go (97%) rename render/yaml.go => yaml.go (98%) diff --git a/components/jethtml/functions.go b/components/jethtml/functions.go new file mode 100644 index 0000000000..79fd74a635 --- /dev/null +++ b/components/jethtml/functions.go @@ -0,0 +1 @@ +package jethtml diff --git a/components/jethtml/jet.go b/components/jethtml/jet.go index ac386916ea..e63abf4b5d 100644 --- a/components/jethtml/jet.go +++ b/components/jethtml/jet.go @@ -3,7 +3,6 @@ package jethtml import ( "github.com/CloudyKit/jet/v6" "github.com/make-money-fast/gin" - "github.com/make-money-fast/gin/render" "net/http" ) @@ -16,7 +15,7 @@ type JetInstantRenderOption struct { extensions []string leftDelim string rightDelim string - functions []jet.Func + functions map[string]jet.Func debug bool } @@ -34,7 +33,7 @@ func Debug() Options { } } -func Functions(functions ...jet.Func) Options { +func Functions(functions map[string]jet.Func) Options { return func(o *JetInstantRenderOption) { o.functions = functions } @@ -70,28 +69,22 @@ func NewJetRender(directory string, options ...Options) *JetInstantRender { opts..., ) - return &JetInstantRender{ - views: views, + if len(jetOption.functions) != 0 { + for name, fn := range jetOption.functions { + views.AddGlobalFunc(name, fn) + } } -} -var ( - contextKey = struct{}{} -) - -func NewContext(name string, ctx *gin.Context) *render.Context { - c := &render.Context{ - Name: name, + return &JetInstantRender{ + views: views, } - c.Set(contextKey, ctx) - return c } -func (j *JetInstantRender) Instance(ctx *render.Context) render.Render { +func (j *JetInstantRender) Instance(ctx *gin.RenderContext) gin.Render { return &JetHtmlRender{ views: j.views, name: ctx.Name, - ctx: ctx.ContextValue(contextKey).(*gin.Context), + ctx: ctx.GinContext, } } diff --git a/context.go b/context.go index 1ab660b731..f0c45c9e99 100644 --- a/context.go +++ b/context.go @@ -22,7 +22,6 @@ import ( "time" "github.com/make-money-fast/gin/binding" - "github.com/make-money-fast/gin/render" ) // Content-Type MIME of the most common data formats. @@ -93,6 +92,7 @@ type Context struct { muxVarCache map[string]string templateVariables map[string]any + templateName string } func (c *Context) initMuxVars() { @@ -124,6 +124,7 @@ func (c *Context) reset() { c.sameSite = 0 c.muxVarCache = nil c.templateVariables = nil + c.templateName = "" *c.params = (*c.params)[:0] *c.skippedNodes = (*c.skippedNodes)[:0] } @@ -1039,8 +1040,8 @@ func (c *Context) Cookie(name string) (string, error) { return val, nil } -// Render writes the response headers and calls render.Render to render data. -func (c *Context) Render(code int, r render.Render) { +// Render writes the response headers and calls Render to render data. +func (c *Context) Render(code int, r Render) { c.Status(code) if !bodyAllowedForStatus(code) { @@ -1052,21 +1053,38 @@ func (c *Context) Render(code int, r render.Render) { if err := r.Render(c.Writer); err != nil { // Pushing error to c.Errors _ = c.Error(err) - c.Abort() + log.Println("render failed:" + err.Error()) + c.AbortWithError(400, err) } } // HTML renders the HTTP template specified by its file name. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ -func (c *Context) HTML(ctx *render.Context) { +func (c *Context) HTML(tpl ...string) { + rc := RenderContext{ + GinContext: c, + Name: c.templateName, + DataMap: c.templateVariables, + } + if len(tpl) > 0 { + rc.Name = tpl[0] + } code := 200 - instance := c.engine.HTMLRender.Instance(ctx) + instance := c.engine.HTMLRender.Instance(&rc) c.Render(code, instance) } -func (c *Context) HTMLWithCode(code int, ctx *render.Context) { - instance := c.engine.HTMLRender.Instance(ctx) +func (c *Context) HTMLWithCode(code int, tpl ...string) { + rc := RenderContext{ + GinContext: c, + Name: c.templateName, + DataMap: c.templateVariables, + } + if len(tpl) > 0 { + rc.Name = tpl[0] + } + instance := c.engine.HTMLRender.Instance(&rc) c.Render(code, instance) } @@ -1075,14 +1093,14 @@ func (c *Context) HTMLWithCode(code int, ctx *render.Context) { // WARNING: we recommend using this only for development purposes since printing pretty JSON is // more CPU and bandwidth consuming. Use Context.JSON() instead. func (c *Context) IndentedJSON(code int, obj any) { - c.Render(code, render.IndentedJSON{Data: obj}) + c.Render(code, IndentedJSON{Data: obj}) } // SecureJSON serializes the given struct as Secure JSON into the response body. // Default prepends "while(1)," to response body if the given struct is array values. // It also sets the Content-Type as "application/json". func (c *Context) SecureJSON(code int, obj any) { - c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj}) + c.Render(code, SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj}) } // JSONP serializes the given struct as JSON into the response body. @@ -1091,59 +1109,59 @@ func (c *Context) SecureJSON(code int, obj any) { func (c *Context) JSONP(code int, obj any) { callback := c.DefaultQuery("callback", "") if callback == "" { - c.Render(code, render.JSON{Data: obj}) + c.Render(code, JSON{Data: obj}) return } - c.Render(code, render.JsonpJSON{Callback: callback, Data: obj}) + c.Render(code, JsonpJSON{Callback: callback, Data: obj}) } // JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj any) { - c.Render(code, render.JSON{Data: obj}) + c.Render(code, JSON{Data: obj}) } // AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string. // It also sets the Content-Type as "application/json". func (c *Context) AsciiJSON(code int, obj any) { - c.Render(code, render.AsciiJSON{Data: obj}) + c.Render(code, AsciiJSON{Data: obj}) } // PureJSON serializes the given struct as JSON into the response body. // PureJSON, unlike JSON, does not replace special html characters with their unicode entities. func (c *Context) PureJSON(code int, obj any) { - c.Render(code, render.PureJSON{Data: obj}) + c.Render(code, PureJSON{Data: obj}) } // XML serializes the given struct as XML into the response body. // It also sets the Content-Type as "application/xml". func (c *Context) XML(code int, obj any) { - c.Render(code, render.XML{Data: obj}) + c.Render(code, XML{Data: obj}) } // YAML serializes the given struct as YAML into the response body. func (c *Context) YAML(code int, obj any) { - c.Render(code, render.YAML{Data: obj}) + c.Render(code, YAML{Data: obj}) } // TOML serializes the given struct as TOML into the response body. func (c *Context) TOML(code int, obj any) { - c.Render(code, render.TOML{Data: obj}) + c.Render(code, TOML{Data: obj}) } // ProtoBuf serializes the given struct as ProtoBuf into the response body. func (c *Context) ProtoBuf(code int, obj any) { - c.Render(code, render.ProtoBuf{Data: obj}) + c.Render(code, ProtoBuf{Data: obj}) } // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...any) { - c.Render(code, render.String{Format: format, Data: values}) + c.Render(code, String{Format: format, Data: values}) } // Redirect returns an HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - c.Render(-1, render.Redirect{ + c.Render(-1, Redirect{ Code: code, Location: location, Request: c.Request, @@ -1152,7 +1170,7 @@ func (c *Context) Redirect(code int, location string) { // Data writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { - c.Render(code, render.Data{ + c.Render(code, Data{ ContentType: contentType, Data: data, }) @@ -1160,7 +1178,7 @@ func (c *Context) Data(code int, contentType string, data []byte) { // DataFromReader writes the specified reader into the body stream and updates the HTTP code. func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) { - c.Render(code, render.Reader{ + c.Render(code, Reader{ Headers: extraHeaders, ContentType: contentType, ContentLength: contentLength, @@ -1285,6 +1303,10 @@ func (c *Context) AssignH(h H) { } } +func (c *Context) SetView(s string) { + c.templateName = s +} + func (c *Context) Assign(key string, v any) { if c.templateVariables == nil { c.templateVariables = make(map[string]any) diff --git a/render/data.go b/data.go similarity index 97% rename from render/data.go rename to data.go index a653ea3091..9a30f90350 100644 --- a/render/data.go +++ b/data.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import "net/http" diff --git a/gin.go b/gin.go index dace1212ec..8e927a799d 100644 --- a/gin.go +++ b/gin.go @@ -17,7 +17,6 @@ import ( "sync" "github.com/make-money-fast/gin/internal/bytesconv" - "github.com/make-money-fast/gin/render" "github.com/quic-go/quic-go/http3" "golang.org/x/net/http2" @@ -167,9 +166,8 @@ type Engine struct { // ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil. ContextWithFallback bool - delims render.Delims secureJSONPrefix string - HTMLRender render.HTMLRender + HTMLRender HTMLRender FuncMap template.FuncMap allNoRoute HandlersChain allNoMethod HandlersChain @@ -213,7 +211,6 @@ func New(opts ...OptionFunc) *Engine { UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, trees: make(methodTrees, 0, 9), - delims: render.Delims{Left: "{{", Right: "}}"}, secureJSONPrefix: "while(1);", trustedProxies: []string{"0.0.0.0/0", "::/0"}, trustedCIDRs: defaultTrustedCIDRs, @@ -249,60 +246,12 @@ func (engine *Engine) allocateContext(maxParams uint16) *Context { return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes} } -// Delims sets template left and right delims and returns an Engine instance. -func (engine *Engine) Delims(left, right string) *Engine { - engine.delims = render.Delims{Left: left, Right: right} - return engine -} - // SecureJsonPrefix sets the secureJSONPrefix used in Context.SecureJSON. func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { engine.secureJSONPrefix = prefix return engine } -// LoadHTMLGlob loads HTML files identified by glob pattern -// and associates the result with HTML renderer. -func (engine *Engine) LoadHTMLGlob(pattern string) { - left := engine.delims.Left - right := engine.delims.Right - templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)) - - if IsDebugging() { - debugPrintLoadTemplate(templ) - engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims} - return - } - - engine.SetHTMLTemplate(templ) -} - -// LoadHTMLFiles loads a slice of HTML files -// and associates the result with HTML renderer. -func (engine *Engine) LoadHTMLFiles(files ...string) { - if IsDebugging() { - engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} - return - } - - templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...)) - engine.SetHTMLTemplate(templ) -} - -// SetHTMLTemplate associate a template with HTML renderer. -func (engine *Engine) SetHTMLTemplate(templ *template.Template) { - if len(engine.trees) > 0 { - debugPrintWARNINGSetHTMLTemplate() - } - - engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} -} - -// SetFuncMap sets the FuncMap used for template.FuncMap. -func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { - engine.FuncMap = funcMap -} - // NoRoute adds handlers for NoRoute. It returns a 404 code by default. func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.noRoute = handlers diff --git a/go.mod b/go.mod index 0bc6c23c45..5abb5d7aee 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( 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 + github.com/golang-module/carbon/v2 v2.4.1 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/go.sum b/go.sum index 8a36976d62..78177d9f05 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe 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= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-module/carbon/v2 v2.4.1 h1:cYUD8T+rHeX+qIybGYpnJ8I90F10dvyEF67VNOO+zZM= +github.com/golang-module/carbon/v2 v2.4.1/go.mod h1:1jP9AZ4k2+lmfgY/wZgmtsN52VcHC5YuPM6varKDTkM= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/html.go b/html.go new file mode 100644 index 0000000000..fa75c323f2 --- /dev/null +++ b/html.go @@ -0,0 +1,17 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +type RenderContext struct { + GinContext *Context + Name string + DataMap map[string]any +} + +// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug. +type HTMLRender interface { + // Instance returns an HTML instance. + Instance(*RenderContext) Render +} diff --git a/jet.go b/jet.go new file mode 100644 index 0000000000..d66f99c560 --- /dev/null +++ b/jet.go @@ -0,0 +1,129 @@ +package gin + +import ( + "fmt" + "github.com/CloudyKit/jet/v6" + "html/template" + "io" + "net/http" +) + +type JetInstantRender struct { + views *jet.Set + funcs map[string]jet.Func +} + +type JetInstantRenderOption struct { + extensions []string + leftDelim string + rightDelim string + functions map[string]jet.Func + debug bool +} + +type Options func(*JetInstantRenderOption) + +func Extensions(s ...string) Options { + return func(r *JetInstantRenderOption) { + r.extensions = s + } +} + +func Debug() Options { + return func(option *JetInstantRenderOption) { + option.debug = true + } +} + +func Functions(functions map[string]jet.Func) Options { + return func(o *JetInstantRenderOption) { + o.functions = functions + } +} + +func Delims(left string, right string) Options { + return func(option *JetInstantRenderOption) { + option.leftDelim = left + option.rightDelim = right + } +} + +func NewJetRender(directory string, options ...Options) *JetInstantRender { + var jetOption JetInstantRenderOption + for _, opt := range options { + opt(&jetOption) + } + + var opts []jet.Option + if jetOption.debug { + opts = append(opts, jet.InDevelopmentMode()) + } + if jetOption.leftDelim != "" || jetOption.rightDelim != "" { + opts = append(opts, jet.WithDelims(jetOption.leftDelim, jetOption.rightDelim)) + } + + if len(jetOption.extensions) != 0 { + opts = append(opts, jet.WithTemplateNameExtensions(jetOption.extensions)) + } + + opts = append(opts, htmlEscaper) + + views := jet.NewSet( + jet.NewOSFileSystemLoader(directory), + opts..., + ) + + if len(jetOption.functions) != 0 { + for name, fn := range jetOption.functions { + views.AddGlobalFunc(name, fn) + } + } + + return &JetInstantRender{ + views: views, + } +} + +func htmlEscaper(w io.Writer, b []byte) { + template.HTMLEscape(w, b) +} + +func (j *JetInstantRender) Instance(ctx *RenderContext) Render { + return &JetHtmlRender{ + views: j.views, + name: ctx.Name, + ctx: ctx.GinContext, + } +} + +type JetHtmlRender struct { + views *jet.Set + name string + ctx *Context +} + +func (j *JetHtmlRender) Render(writer http.ResponseWriter) error { + t, err := j.views.GetTemplate(j.name) + if err != nil { + return fmt.Errorf("parse template failed: %+w", err) + } + data := j.ctx.Assigned() + variables := make(jet.VarMap) + for key, item := range data { + variables.Set(key, item) + } + + for _, functionBuilder := range contextBuilders { + name, fn := functionBuilder(j.ctx) + variables.Set(name, fn) + } + + if err := t.Execute(writer, variables, nil); err != nil { + return err + } + return nil +} + +func (j *JetHtmlRender) WriteContentType(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/html") +} diff --git a/render/json.go b/json.go similarity index 99% rename from render/json.go rename to json.go index ab6ad80d29..657eeaf6cf 100644 --- a/render/json.go +++ b/json.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "bytes" diff --git a/render/msgpack.go b/msgpack.go similarity index 98% rename from render/msgpack.go rename to msgpack.go index 2ac7734ebd..7638a039c5 100644 --- a/render/msgpack.go +++ b/msgpack.go @@ -4,7 +4,7 @@ //go:build !nomsgpack -package render +package gin import ( "net/http" diff --git a/render/protobuf.go b/protobuf.go similarity index 98% rename from render/protobuf.go rename to protobuf.go index 9331c40583..9680abcd3c 100644 --- a/render/protobuf.go +++ b/protobuf.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "net/http" diff --git a/render/reader.go b/reader.go similarity index 98% rename from render/reader.go rename to reader.go index 5752d8d859..8f4a394c8a 100644 --- a/render/reader.go +++ b/reader.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "io" diff --git a/render/reader_test.go b/reader_test.go similarity index 97% rename from render/reader_test.go rename to reader_test.go index aaceb9eaac..3a12d22e3b 100644 --- a/render/reader_test.go +++ b/reader_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "net/http/httptest" diff --git a/render/redirect.go b/redirect.go similarity index 98% rename from render/redirect.go rename to redirect.go index 70e3a47e81..36b31aebf1 100644 --- a/render/redirect.go +++ b/redirect.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "fmt" diff --git a/render/render.go b/render.go similarity index 54% rename from render/render.go rename to render.go index 4bdcfa2326..9683dbd38c 100644 --- a/render/render.go +++ b/render.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import "net/http" @@ -15,22 +15,19 @@ type Render interface { } var ( - _ Render = (*JSON)(nil) - _ Render = (*IndentedJSON)(nil) - _ Render = (*SecureJSON)(nil) - _ Render = (*JsonpJSON)(nil) - _ Render = (*XML)(nil) - _ Render = (*String)(nil) - _ Render = (*Redirect)(nil) - _ Render = (*Data)(nil) - _ Render = (*HTML)(nil) - _ HTMLRender = (*HTMLDebug)(nil) - _ HTMLRender = (*HTMLProduction)(nil) - _ Render = (*YAML)(nil) - _ Render = (*Reader)(nil) - _ Render = (*AsciiJSON)(nil) - _ Render = (*ProtoBuf)(nil) - _ Render = (*TOML)(nil) + _ Render = (*JSON)(nil) + _ Render = (*IndentedJSON)(nil) + _ Render = (*SecureJSON)(nil) + _ Render = (*JsonpJSON)(nil) + _ Render = (*XML)(nil) + _ Render = (*String)(nil) + _ Render = (*Redirect)(nil) + _ Render = (*Data)(nil) + _ Render = (*YAML)(nil) + _ Render = (*Reader)(nil) + _ Render = (*AsciiJSON)(nil) + _ Render = (*ProtoBuf)(nil) + _ Render = (*TOML)(nil) ) func writeContentType(w http.ResponseWriter, value []string) { diff --git a/render/html.go b/render/html.go deleted file mode 100644 index 577ebc2cd5..0000000000 --- a/render/html.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2014 Manu Martinez-Almeida. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -package render - -import ( - "golang.org/x/net/context" - "html/template" - "net/http" -) - -// Delims represents a set of Left and Right delimiters for HTML template rendering. -type Delims struct { - // Left delimiter, defaults to {{. - Left string - // Right delimiter, defaults to }}. - Right string -} - -type Context struct { - RequestContext context.Context - Name string - DataMap map[string]any -} - -func (c *Context) ContextValue(key interface{}) any { - return c.RequestContext.Value(key) -} - -func (c *Context) Set(key interface{}, val interface{}) { - if c.RequestContext == nil { - c.RequestContext = context.Background() - } - c.RequestContext = context.WithValue(c.RequestContext, key, val) -} - -// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug. -type HTMLRender interface { - // Instance returns an HTML instance. - Instance(*Context) Render -} - -// HTMLProduction contains template reference and its delims. -type HTMLProduction struct { - Template *template.Template - Delims Delims -} - -// HTMLDebug contains template delims and pattern and function with file list. -type HTMLDebug struct { - Files []string - Glob string - Delims Delims - FuncMap template.FuncMap -} - -// HTML contains template reference and its name with given interface object. -type HTML struct { - Template *template.Template - Name string - Data any -} - -var htmlContentType = []string{"text/html; charset=utf-8"} - -// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface. -func (r HTMLProduction) Instance(ctx *Context) Render { - return HTML{ - Template: r.Template, - Name: ctx.Name, - Data: ctx.DataMap, - } -} - -// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface. -func (r HTMLDebug) Instance(ctx *Context) Render { - return HTML{ - Template: r.loadTemplate(), - Name: ctx.Name, - Data: ctx.DataMap, - } -} -func (r HTMLDebug) loadTemplate() *template.Template { - if r.FuncMap == nil { - r.FuncMap = template.FuncMap{} - } - if len(r.Files) > 0 { - return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...)) - } - 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") -} - -// Render (HTML) executes template and writes its result with custom ContentType for response. -func (r HTML) Render(w http.ResponseWriter) error { - r.WriteContentType(w) - - if r.Name == "" { - return r.Template.Execute(w, r.Data) - } - return r.Template.ExecuteTemplate(w, r.Name, r.Data) -} - -// WriteContentType (HTML) writes HTML ContentType. -func (r HTML) WriteContentType(w http.ResponseWriter) { - writeContentType(w, htmlContentType) -} diff --git a/render_functions.go b/render_functions.go new file mode 100644 index 0000000000..0de3e4be7f --- /dev/null +++ b/render_functions.go @@ -0,0 +1,221 @@ +package gin + +import ( + "fmt" + "github.com/CloudyKit/jet/v6" + "github.com/golang-module/carbon/v2" + "reflect" + "time" +) + +var ( + globalFunctions = make(map[string]jet.Func) + contextBuilders = make([]FunctionsBuilder, 0) +) + +func init() { + fn := new(TemplateFunc) + carbon.SetDefault(carbon.Default{ + Layout: carbon.DateTimeFormat, + Timezone: carbon.Local, + Locale: "zh-CN", + }) + RegisterContextFunctionBuilders( + fn.Route(), + fn.CurrentURI(), + fn.AbsUrl(), + fn.HasQuery(), + fn.Carbon(), + fn.Date(), + fn.DateTime(), + ) +} + +func RegisterGlobalFunctions(name string, p jet.Func) { + globalFunctions[name] = p +} + +func RegisterContextFunctionBuilders(b ...FunctionsBuilder) { + contextBuilders = append(contextBuilders, b...) +} + +type FunctionsBuilder func(c *Context) (string, jet.Func) + +func of(a any) reflect.Value { + return reflect.ValueOf(a) +} + +type TemplateFunc struct { +} + +// === url 辅助函数 + +// Route 用法: {{ route "/index" }} , {{ route "/index" "a=1&b=2" }} +func (c *TemplateFunc) Route() FunctionsBuilder { + return func(c *Context) (string, jet.Func) { + name := "route" + return name, func(a jet.Arguments) reflect.Value { + a.RequireNumOfArguments(name, 1, 2) + path := a.Get(0).String() + + var query string + if a.IsSet(1) { + query = a.Get(1).String() + } + if len(query) == 0 { + return of(fmt.Sprintf("%s", path)) + } + return of(fmt.Sprintf("%s?%s", path, query)) + } + } +} + +// CurrentURI 获取当前路劲 +func (c *TemplateFunc) CurrentURI() FunctionsBuilder { + name := "currentURI" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + return reflect.ValueOf(c.Request.RequestURI) + } + } +} + +// AbsUrl 获取相对url +func (c *TemplateFunc) AbsUrl() FunctionsBuilder { + name := "absUrl" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + a.RequireNumOfArguments(name, 1, 1) + newUrl, _ := c.Request.URL.Parse(a.Get(0).String()) + if newUrl != nil { + return of(newUrl.String()) + } + return of("") + } + } +} + +// HasQuery 获取相对url +func (c *TemplateFunc) HasQuery() FunctionsBuilder { + name := "hasQuery" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + a.RequireNumOfArguments(name, 1, 1) + key := a.Get(0).String() + val := c.Query(key) + if val == "" { + return of(false) + } + return of(true) + } + } +} + +// Query 获取 query 参数. +func (c *TemplateFunc) Query() FunctionsBuilder { + name := "query" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + a.RequireNumOfArguments(name, 1, 1) + key := a.Get(0).String() + val := c.Query(key) + return of(val) + } + } +} + +// == 时间函数 +func (c *TemplateFunc) carbonBase(a jet.Arguments) time.Time { + var ( + ti time.Time + ) + if a.IsSet(0) { + arg := a.Get(0) + numVal, ok := numberValue(arg) + if ok { + ti = time.Unix(numVal, 0) + } else { + if arg.Kind() == reflect.Struct { // time.time + ti = arg.Interface().(time.Time) + } + if arg.Kind() == reflect.String { // string . + var layout string + if a.IsSet(1) { + // layout + layout = a.Get(1).String() + } else { + layout = time.RFC3339 + } + t, err := time.Parse(layout, arg.Interface().(string)) + if err != nil { + panic("failed to parse time: " + err.Error()) + } + ti = t + } + } + } + return ti +} + +func (f *TemplateFunc) Carbon() FunctionsBuilder { + name := "carbon" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + return of(carbon.CreateFromStdTime(f.carbonBase(a)).DiffForHumans(carbon.Now())) + } + } +} + +func (f *TemplateFunc) Date() FunctionsBuilder { + name := "date" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + return of(carbon.CreateFromStdTime(f.carbonBase(a)).ToDateString()) + } + } +} + +func (f *TemplateFunc) DateTime() FunctionsBuilder { + name := "datetime" + return func(c *Context) (string, jet.Func) { + return name, func(a jet.Arguments) reflect.Value { + return of(carbon.CreateFromStdTime(f.carbonBase(a)).ToDateTimeString()) + } + } +} + +func numberValue(value reflect.Value) (int64, bool) { + switch value.Kind() { + case reflect.Int: + v := value.Interface().(int) + return int64(v), true + case reflect.Int8: + v := value.Interface().(int8) + return int64(v), true + case reflect.Int16: + v := value.Interface().(int16) + return int64(v), true + case reflect.Int32: + v := value.Interface().(int32) + return int64(v), true + case reflect.Int64: + v := value.Interface().(int64) + return int64(v), true + case reflect.Uint: + v := value.Interface().(uint) + return int64(v), true + case reflect.Uint8: + v := value.Interface().(uint8) + return int64(v), true + case reflect.Uint16: + v := value.Interface().(uint16) + return int64(v), true + case reflect.Uint32: + v := value.Interface().(uint32) + return int64(v), true + case reflect.Uint64: + v := value.Interface().(uint64) + return int64(v), true + } + return 0, false +} diff --git a/render/render_msgpack_test.go b/render_msgpack_test.go similarity index 98% rename from render/render_msgpack_test.go rename to render_msgpack_test.go index 579897ccc6..5eac219efe 100644 --- a/render/render_msgpack_test.go +++ b/render_msgpack_test.go @@ -4,7 +4,7 @@ //go:build !nomsgpack -package render +package gin import ( "bytes" diff --git a/render/render_test.go b/render_test.go similarity index 85% rename from render/render_test.go rename to render_test.go index 8e17fab9d1..3a253fd57c 100644 --- a/render/render_test.go +++ b/render_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "encoding/xml" @@ -454,86 +454,6 @@ func TestRenderStringLenZero(t *testing.T) { assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type")) } -func TestRenderHTMLTemplate(t *testing.T) { - w := httptest.NewRecorder() - templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) - - htmlRender := HTMLProduction{Template: templ} - instance := htmlRender.Instance("t", map[string]any{ - "name": "alexandernyquist", - }) - - err := instance.Render(w) - - require.NoError(t, err) - assert.Equal(t, "Hello alexandernyquist", w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) -} - -func TestRenderHTMLTemplateEmptyName(t *testing.T) { - w := httptest.NewRecorder() - templ := template.Must(template.New("").Parse(`Hello {{.name}}`)) - - htmlRender := HTMLProduction{Template: templ} - instance := htmlRender.Instance("", map[string]any{ - "name": "alexandernyquist", - }) - - err := instance.Render(w) - - require.NoError(t, err) - assert.Equal(t, "Hello alexandernyquist", w.Body.String()) - assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) -} - -func TestRenderHTMLDebugFiles(t *testing.T) { - w := httptest.NewRecorder() - htmlRender := HTMLDebug{ - Files: []string{"../testdata/template/hello.tmpl"}, - Glob: "", - 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 TestRenderHTMLDebugGlob(t *testing.T) { - w := httptest.NewRecorder() - htmlRender := HTMLDebug{ - Files: nil, - Glob: "../testdata/template/hello*", - 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 TestRenderHTMLDebugPanics(t *testing.T) { - htmlRender := HTMLDebug{ - Files: nil, - Glob: "", - Delims: Delims{"{{", "}}"}, - FuncMap: nil, - } - assert.Panics(t, func() { htmlRender.Instance("", nil) }) -} - func TestRenderReader(t *testing.T) { w := httptest.NewRecorder() diff --git a/render/text.go b/text.go similarity index 98% rename from render/text.go rename to text.go index 2a5ae7ae99..98b8f0bfba 100644 --- a/render/text.go +++ b/text.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "fmt" diff --git a/render/toml.go b/toml.go similarity index 98% rename from render/toml.go rename to toml.go index 40f044c88a..aa18dd1ccb 100644 --- a/render/toml.go +++ b/toml.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "net/http" diff --git a/render/xml.go b/xml.go similarity index 97% rename from render/xml.go rename to xml.go index 6af8901796..8009cc29f5 100644 --- a/render/xml.go +++ b/xml.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "encoding/xml" diff --git a/render/yaml.go b/yaml.go similarity index 98% rename from render/yaml.go rename to yaml.go index 042bb821da..a82aeaeb51 100644 --- a/render/yaml.go +++ b/yaml.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -package render +package gin import ( "net/http"