-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Make HTML template functions support context #24056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2077de4
introduce ScopedTemplate
wxiaoguang d664a4d
Merge remote-tracking branch 'upstream/main' into improve-tmpl-context
wxiaoguang e6589ed
fix merge
wxiaoguang 171e66a
Merge branch 'main' into test-tmpl-clone
wxiaoguang f1e4631
Merge branch 'main' into test-tmpl-clone
silverwind 34a1fc3
Merge branch 'main' into test-tmpl-clone
wxiaoguang 54cffd0
fix merge
wxiaoguang bd920df
Apply suggestions from code review
wxiaoguang 093a909
Merge branch 'main' into test-tmpl-clone
GiteaBot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
// Copyright 2023 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package scopedtmpl | ||
|
||
import ( | ||
"fmt" | ||
"html/template" | ||
"io" | ||
"reflect" | ||
"sync" | ||
texttemplate "text/template" | ||
"text/template/parse" | ||
"unsafe" | ||
) | ||
|
||
type TemplateExecutor interface { | ||
Execute(wr io.Writer, data interface{}) error | ||
} | ||
|
||
type ScopedTemplate struct { | ||
all *template.Template | ||
parseFuncs template.FuncMap // this func map is only used for parsing templates | ||
frozen bool | ||
|
||
scopedMu sync.RWMutex | ||
scopedTemplateSets map[string]*scopedTemplateSet | ||
} | ||
|
||
func NewScopedTemplate() *ScopedTemplate { | ||
return &ScopedTemplate{ | ||
all: template.New(""), | ||
parseFuncs: template.FuncMap{}, | ||
scopedTemplateSets: map[string]*scopedTemplateSet{}, | ||
} | ||
} | ||
|
||
func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) { | ||
if t.frozen { | ||
panic("cannot add new functions to frozen template set") | ||
} | ||
t.all.Funcs(funcMap) | ||
for k, v := range funcMap { | ||
t.parseFuncs[k] = v | ||
} | ||
} | ||
|
||
func (t *ScopedTemplate) New(name string) *template.Template { | ||
if t.frozen { | ||
panic("cannot add new template to frozen template set") | ||
} | ||
return t.all.New(name) | ||
} | ||
|
||
func (t *ScopedTemplate) Freeze() { | ||
t.frozen = true | ||
// reset the exec func map, then `escapeTemplate` is safe to call `Execute` to do escaping | ||
m := template.FuncMap{} | ||
for k := range t.parseFuncs { | ||
m[k] = func(v ...any) any { return nil } | ||
} | ||
t.all.Funcs(m) | ||
} | ||
|
||
func (t *ScopedTemplate) Executor(name string, funcMap template.FuncMap) (TemplateExecutor, error) { | ||
t.scopedMu.RLock() | ||
scopedTmplSet, ok := t.scopedTemplateSets[name] | ||
t.scopedMu.RUnlock() | ||
|
||
if !ok { | ||
var err error | ||
t.scopedMu.Lock() | ||
if scopedTmplSet, ok = t.scopedTemplateSets[name]; !ok { | ||
if scopedTmplSet, err = newScopedTemplateSet(t.all, name); err == nil { | ||
t.scopedTemplateSets[name] = scopedTmplSet | ||
} | ||
} | ||
t.scopedMu.Unlock() | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
if scopedTmplSet == nil { | ||
return nil, fmt.Errorf("template %s not found", name) | ||
} | ||
return scopedTmplSet.newExecutor(funcMap), nil | ||
} | ||
|
||
type scopedTemplateSet struct { | ||
name string | ||
htmlTemplates map[string]*template.Template | ||
textTemplates map[string]*texttemplate.Template | ||
execFuncs map[string]reflect.Value | ||
} | ||
|
||
func escapeTemplate(t *template.Template) error { | ||
// force the Golang HTML template to complete the escaping work | ||
err := t.Execute(io.Discard, nil) | ||
if _, ok := err.(*template.Error); ok { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
//nolint:unused | ||
type htmlTemplate struct { | ||
escapeErr error | ||
text *texttemplate.Template | ||
} | ||
|
||
//nolint:unused | ||
type textTemplateCommon struct { | ||
tmpl map[string]*template.Template // Map from name to defined templates. | ||
muTmpl sync.RWMutex // protects tmpl | ||
option struct { | ||
missingKey int | ||
} | ||
muFuncs sync.RWMutex // protects parseFuncs and execFuncs | ||
parseFuncs texttemplate.FuncMap | ||
execFuncs map[string]reflect.Value | ||
} | ||
|
||
//nolint:unused | ||
type textTemplate struct { | ||
name string | ||
*parse.Tree | ||
*textTemplateCommon | ||
leftDelim string | ||
rightDelim string | ||
} | ||
|
||
func ptr[T, P any](ptr *P) *T { | ||
// https://pkg.go.dev/unsafe#Pointer | ||
// (1) Conversion of a *T1 to Pointer to *T2. | ||
// Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, | ||
// this conversion allows reinterpreting data of one type as data of another type. | ||
return (*T)(unsafe.Pointer(ptr)) | ||
} | ||
|
||
func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateSet, error) { | ||
targetTmpl := all.Lookup(name) | ||
if targetTmpl == nil { | ||
return nil, fmt.Errorf("template %q not found", name) | ||
} | ||
if err := escapeTemplate(targetTmpl); err != nil { | ||
return nil, fmt.Errorf("template %q has an error when escaping: %v", name, err) | ||
} | ||
|
||
ts := &scopedTemplateSet{ | ||
name: name, | ||
htmlTemplates: map[string]*template.Template{}, | ||
textTemplates: map[string]*texttemplate.Template{}, | ||
} | ||
|
||
htmlTmpl := ptr[htmlTemplate](all) | ||
textTmpl := htmlTmpl.text | ||
textTmplPtr := ptr[textTemplate](textTmpl) | ||
|
||
textTmplPtr.muFuncs.Lock() | ||
ts.execFuncs = map[string]reflect.Value{} | ||
for k, v := range textTmplPtr.execFuncs { | ||
ts.execFuncs[k] = v | ||
} | ||
textTmplPtr.muFuncs.Unlock() | ||
|
||
var collectTemplates func(nodes []parse.Node) | ||
var collectErr error // only need to collect the one error | ||
collectTemplates = func(nodes []parse.Node) { | ||
for _, node := range nodes { | ||
if node.Type() == parse.NodeTemplate { | ||
nodeTemplate := node.(*parse.TemplateNode) | ||
subName := nodeTemplate.Name | ||
if ts.htmlTemplates[subName] == nil { | ||
subTmpl := all.Lookup(subName) | ||
if subTmpl == nil { | ||
// HTML template will add some internal templates like "$delimDoubleQuote" into the text template | ||
ts.textTemplates[subName] = textTmpl.Lookup(subName) | ||
} else if subTmpl.Tree == nil || subTmpl.Tree.Root == nil { | ||
collectErr = fmt.Errorf("template %q has no tree, it's usually caused by broken templates", subName) | ||
} else { | ||
ts.htmlTemplates[subName] = subTmpl | ||
if err := escapeTemplate(subTmpl); err != nil { | ||
collectErr = fmt.Errorf("template %q has an error when escaping: %v", subName, err) | ||
wxiaoguang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return | ||
} | ||
collectTemplates(subTmpl.Tree.Root.Nodes) | ||
} | ||
} | ||
} else if node.Type() == parse.NodeList { | ||
nodeList := node.(*parse.ListNode) | ||
collectTemplates(nodeList.Nodes) | ||
} else if node.Type() == parse.NodeIf { | ||
nodeIf := node.(*parse.IfNode) | ||
collectTemplates(nodeIf.BranchNode.List.Nodes) | ||
if nodeIf.BranchNode.ElseList != nil { | ||
collectTemplates(nodeIf.BranchNode.ElseList.Nodes) | ||
} | ||
} else if node.Type() == parse.NodeRange { | ||
nodeRange := node.(*parse.RangeNode) | ||
collectTemplates(nodeRange.BranchNode.List.Nodes) | ||
if nodeRange.BranchNode.ElseList != nil { | ||
collectTemplates(nodeRange.BranchNode.ElseList.Nodes) | ||
} | ||
} else if node.Type() == parse.NodeWith { | ||
nodeWith := node.(*parse.WithNode) | ||
collectTemplates(nodeWith.BranchNode.List.Nodes) | ||
if nodeWith.BranchNode.ElseList != nil { | ||
collectTemplates(nodeWith.BranchNode.ElseList.Nodes) | ||
} | ||
} | ||
} | ||
} | ||
ts.htmlTemplates[name] = targetTmpl | ||
collectTemplates(targetTmpl.Tree.Root.Nodes) | ||
return ts, collectErr | ||
} | ||
|
||
func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecutor { | ||
tmpl := texttemplate.New("") | ||
tmplPtr := ptr[textTemplate](tmpl) | ||
tmplPtr.execFuncs = map[string]reflect.Value{} | ||
for k, v := range ts.execFuncs { | ||
tmplPtr.execFuncs[k] = v | ||
} | ||
if funcMap != nil { | ||
tmpl.Funcs(funcMap) | ||
} | ||
// after escapeTemplate, the html templates are also escaped text templates, so it could be added to the text template directly | ||
for _, t := range ts.htmlTemplates { | ||
_, _ = tmpl.AddParseTree(t.Name(), t.Tree) | ||
} | ||
for _, t := range ts.textTemplates { | ||
_, _ = tmpl.AddParseTree(t.Name(), t.Tree) | ||
} | ||
|
||
// now the text template has all necessary escaped templates, so we can safely execute, just like what the html template does | ||
return tmpl.Lookup(ts.name) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.