Make HTML template functions support context (#24056)

# Background

Golang template is not friendly for large projects, and Golang template
team is quite slow, related:
* `https://github.com/golang/go/issues/54450`

Without upstream support, we can also have our solution to make HTML
template functions support context.

It helps a lot, the above Golang template issue `#54450` explains a lot:

1. It makes `{{Locale.Tr}}` could be used in any template, without
passing unclear `(dict "root" . )` anymore.
2. More and more functions need `context`, like `avatar`, etc, we do not
need to do `(dict "Context" $.Context)` anymore.
3. Many request-related functions could be shared by parent&children
templates, like "user setting" / "system setting"

See the test `TestScopedTemplateSetFuncMap`, one template set, two
`Execute` calls with different `CtxFunc`.

# The Solution

Instead of waiting for upstream, this PR re-uses the escaped HTML
template trees, use `AddParseTree` to add related templates/trees to a
new template instance, then the new template instance can have its own
FuncMap , the function calls in the template trees will always use the
new template's FuncMap.

`template.New` / `template.AddParseTree` / `adding-FuncMap` are all
quite fast, so the performance is not affected.

The details:

1. Make a new `html/template/Template` for `all` templates
2. Add template code to the `all` template
3. Freeze the `all` template, reset its exec func map, it shouldn't
execute any template.
4. When a router wants to render a template by its `name`
    1. Find the `name` in `all`
    2. Find all its related sub templates
3. Escape all related templates (just like what the html template
package does)
4. Add the escaped parse-trees of related templates into a new (scoped)
`text/template/Template`
    5. Add context-related func map into the new (scoped) text template
    6. Execute the new (scoped) text template
7. To improve performance, the escaped templates are cached to `template
sets`

# FAQ

## There is a `unsafe` call, is this PR unsafe?

This PR is safe. Golang has strict language definition, it's safe to do
so: https://pkg.go.dev/unsafe#Pointer (1) Conversion of a *T1 to Pointer
to *T2


## What if Golang template supports such feature in the future?

The public structs/interfaces/functions introduced by this PR is quite
simple, the code of `HTMLRender` is not changed too much. It's very easy
to switch to the official mechanism if there would be one.

## Does this PR change the template execution behavior?

No, see the tests (welcome to design more tests if it's necessary)

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
wxiaoguang 2023-04-20 16:08:58 +08:00 committed by GitHub
parent de2268ffab
commit 722dab5286
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 351 additions and 16 deletions

View file

@ -9,7 +9,6 @@ import (
"context"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"path/filepath"
@ -22,13 +21,16 @@ import (
"code.gitea.io/gitea/modules/assetfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates/scopedtmpl"
"code.gitea.io/gitea/modules/util"
)
var rendererKey interface{} = "templatesHtmlRenderer"
type TemplateExecutor scopedtmpl.TemplateExecutor
type HTMLRender struct {
templates atomic.Pointer[template.Template]
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
}
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
@ -47,22 +49,20 @@ func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}
return t.Execute(w, data)
}
func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) {
func (h *HTMLRender) TemplateLookup(name string) (TemplateExecutor, error) {
tmpls := h.templates.Load()
if tmpls == nil {
return nil, ErrTemplateNotInitialized
}
tmpl := tmpls.Lookup(name)
if tmpl == nil {
return nil, util.ErrNotExist
}
return tmpl, nil
return tmpls.Executor(name, NewFuncMap()[0])
}
func (h *HTMLRender) CompileTemplates() error {
extSuffix := ".tmpl"
tmpls := template.New("")
assets := AssetFS()
extSuffix := ".tmpl"
tmpls := scopedtmpl.NewScopedTemplate()
tmpls.Funcs(NewFuncMap()[0])
files, err := ListWebTemplateAssetNames(assets)
if err != nil {
return nil
@ -73,9 +73,6 @@ func (h *HTMLRender) CompileTemplates() error {
}
name := strings.TrimSuffix(file, extSuffix)
tmpl := tmpls.New(filepath.ToSlash(name))
for _, fm := range NewFuncMap() {
tmpl.Funcs(fm)
}
buf, err := assets.ReadFile(file)
if err != nil {
return err
@ -84,6 +81,7 @@ func (h *HTMLRender) CompileTemplates() error {
return err
}
}
tmpls.Freeze()
h.templates.Store(tmpls)
return nil
}