1
0
Fork 0
forked from forgejo/forgejo

Start using template context function (#26254)

Before:

* `{{.locale.Tr ...}}`
* `{{$.locale.Tr ...}}`
* `{{$.root.locale.Tr ...}}`
* `{{template "sub" .}}`
* `{{template "sub" (dict "locale" $.locale)}}`
* `{{template "sub" (dict "root" $)}}`
* .....

With context function: only need to `{{ctx.Locale.Tr ...}}`

The "ctx" could be considered as a super-global variable for all
templates including sub-templates.


To avoid potential risks (any bug in the template context function
package), this PR only starts using "ctx" in "head.tmpl" and
"footer.tmpl" and it has a "DataRaceCheck". If there is anything wrong,
the code can be fixed or reverted easily.
This commit is contained in:
wxiaoguang 2023-08-08 09:22:47 +08:00 committed by GitHub
parent 0c6ae61229
commit 6913053223
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 91 additions and 22 deletions

View file

@ -5,6 +5,7 @@
package context
import (
"context"
"html"
"html/template"
"io"
@ -31,14 +32,16 @@ import (
// Render represents a template render
type Render interface {
TemplateLookup(tmpl string) (templates.TemplateExecutor, error)
HTML(w io.Writer, status int, name string, data any) error
TemplateLookup(tmpl string, templateCtx context.Context) (templates.TemplateExecutor, error)
HTML(w io.Writer, status int, name string, data any, templateCtx context.Context) error
}
// Context represents context of a request.
type Context struct {
*Base
TemplateContext TemplateContext
Render Render
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
@ -60,6 +63,8 @@ type Context struct {
Package *Package
}
type TemplateContext map[string]any
func init() {
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(WebContextKey).(*Context)
@ -133,8 +138,12 @@ func Contexter() func(next http.Handler) http.Handler {
}
defer baseCleanUp()
// TODO: "install.go" also shares the same logic, which should be refactored to a general function
ctx.TemplateContext = NewTemplateContext(ctx)
ctx.TemplateContext["Locale"] = ctx.Locale
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data["Context"] = &ctx
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
ctx.Data["Link"] = ctx.Link
ctx.Data["locale"] = ctx.Locale

View file

@ -75,7 +75,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
}
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data)
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
if err == nil {
return
}
@ -93,7 +93,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
// RenderToString renders the template content to a string
func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
var buf strings.Builder
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data)
err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext)
return buf.String(), err
}

View file

@ -0,0 +1,49 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"context"
"errors"
"time"
"code.gitea.io/gitea/modules/log"
)
var _ context.Context = TemplateContext(nil)
func NewTemplateContext(ctx context.Context) TemplateContext {
return TemplateContext{"_ctx": ctx}
}
func (c TemplateContext) parentContext() context.Context {
return c["_ctx"].(context.Context)
}
func (c TemplateContext) Deadline() (deadline time.Time, ok bool) {
return c.parentContext().Deadline()
}
func (c TemplateContext) Done() <-chan struct{} {
return c.parentContext().Done()
}
func (c TemplateContext) Err() error {
return c.parentContext().Err()
}
func (c TemplateContext) Value(key any) any {
return c.parentContext().Value(key)
}
// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context
// as the current template's rendering context (request context), to help to find data race issues as early as possible.
// When the code is proven to be correct and stable, this function should be removed.
func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) {
if c.parentContext() != dataCtx {
log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2))
return "", errors.New("parent context mismatch")
}
return "", nil
}

View file

@ -28,6 +28,8 @@ import (
// NewFuncMap returns functions for injecting to templates
func NewFuncMap() template.FuncMap {
return map[string]any{
"ctx": func() any { return nil }, // template context function
"DumpVar": dumpVar,
// -----------------------------------------------------------------

View file

@ -6,6 +6,7 @@ package templates
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
@ -39,27 +40,28 @@ var (
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any) error {
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive
if respWriter, ok := w.(http.ResponseWriter); ok {
if respWriter.Header().Get("Content-Type") == "" {
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
}
respWriter.WriteHeader(status)
}
t, err := h.TemplateLookup(name)
t, err := h.TemplateLookup(name, ctx)
if err != nil {
return texttemplate.ExecError{Name: name, Err: err}
}
return t.Execute(w, data)
}
func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) {
func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive
tmpls := h.templates.Load()
if tmpls == nil {
return nil, ErrTemplateNotInitialized
}
return tmpls.Executor(name, NewFuncMap())
m := NewFuncMap()
m["ctx"] = func() any { return ctx }
return tmpls.Executor(name, m)
}
func (h *HTMLRender) CompileTemplates() error {

View file

@ -150,11 +150,11 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) {
type mockRender struct{}
func (tr *mockRender) TemplateLookup(tmpl string) (templates.TemplateExecutor, error) {
func (tr *mockRender) TemplateLookup(tmpl string, _ gocontext.Context) (templates.TemplateExecutor, error) {
return nil, nil
}
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ any) error {
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ any, _ gocontext.Context) error {
if resp, ok := w.(http.ResponseWriter); ok {
resp.WriteHeader(status)
}