You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

htmlrenderer.go 8.7KB

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>
1 year ago
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>
1 year ago
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>
1 year ago
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>
1 year ago
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>
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package templates
  4. import (
  5. "bufio"
  6. "bytes"
  7. "context"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "path/filepath"
  13. "regexp"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "sync/atomic"
  18. texttemplate "text/template"
  19. "code.gitea.io/gitea/modules/assetfs"
  20. "code.gitea.io/gitea/modules/graceful"
  21. "code.gitea.io/gitea/modules/log"
  22. "code.gitea.io/gitea/modules/setting"
  23. "code.gitea.io/gitea/modules/templates/scopedtmpl"
  24. "code.gitea.io/gitea/modules/util"
  25. )
  26. type TemplateExecutor scopedtmpl.TemplateExecutor
  27. type HTMLRender struct {
  28. templates atomic.Pointer[scopedtmpl.ScopedTemplate]
  29. }
  30. var (
  31. htmlRender *HTMLRender
  32. htmlRenderOnce sync.Once
  33. )
  34. var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
  35. func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive
  36. if respWriter, ok := w.(http.ResponseWriter); ok {
  37. if respWriter.Header().Get("Content-Type") == "" {
  38. respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
  39. }
  40. respWriter.WriteHeader(status)
  41. }
  42. t, err := h.TemplateLookup(name, ctx)
  43. if err != nil {
  44. return texttemplate.ExecError{Name: name, Err: err}
  45. }
  46. return t.Execute(w, data)
  47. }
  48. func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive
  49. tmpls := h.templates.Load()
  50. if tmpls == nil {
  51. return nil, ErrTemplateNotInitialized
  52. }
  53. m := NewFuncMap()
  54. m["ctx"] = func() any { return ctx }
  55. return tmpls.Executor(name, m)
  56. }
  57. func (h *HTMLRender) CompileTemplates() error {
  58. assets := AssetFS()
  59. extSuffix := ".tmpl"
  60. tmpls := scopedtmpl.NewScopedTemplate()
  61. tmpls.Funcs(NewFuncMap())
  62. files, err := ListWebTemplateAssetNames(assets)
  63. if err != nil {
  64. return nil
  65. }
  66. for _, file := range files {
  67. if !strings.HasSuffix(file, extSuffix) {
  68. continue
  69. }
  70. name := strings.TrimSuffix(file, extSuffix)
  71. tmpl := tmpls.New(filepath.ToSlash(name))
  72. buf, err := assets.ReadFile(file)
  73. if err != nil {
  74. return err
  75. }
  76. if _, err = tmpl.Parse(string(buf)); err != nil {
  77. return err
  78. }
  79. }
  80. tmpls.Freeze()
  81. h.templates.Store(tmpls)
  82. return nil
  83. }
  84. // HTMLRenderer init once and returns the globally shared html renderer
  85. func HTMLRenderer() *HTMLRender {
  86. htmlRenderOnce.Do(initHTMLRenderer)
  87. return htmlRender
  88. }
  89. func ReloadHTMLTemplates() error {
  90. log.Trace("Reloading HTML templates")
  91. if err := htmlRender.CompileTemplates(); err != nil {
  92. log.Error("Template error: %v\n%s", err, log.Stack(2))
  93. return err
  94. }
  95. return nil
  96. }
  97. func initHTMLRenderer() {
  98. rendererType := "static"
  99. if !setting.IsProd {
  100. rendererType = "auto-reloading"
  101. }
  102. log.Debug("Creating %s HTML Renderer", rendererType)
  103. htmlRender = &HTMLRender{}
  104. if err := htmlRender.CompileTemplates(); err != nil {
  105. p := &templateErrorPrettier{assets: AssetFS()}
  106. wrapTmplErrMsg(p.handleFuncNotDefinedError(err))
  107. wrapTmplErrMsg(p.handleUnexpectedOperandError(err))
  108. wrapTmplErrMsg(p.handleExpectedEndError(err))
  109. wrapTmplErrMsg(p.handleGenericTemplateError(err))
  110. wrapTmplErrMsg(fmt.Sprintf("CompileTemplates error: %v", err))
  111. }
  112. if !setting.IsProd {
  113. go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() {
  114. _ = ReloadHTMLTemplates()
  115. })
  116. }
  117. }
  118. func wrapTmplErrMsg(msg string) {
  119. if msg == "" {
  120. return
  121. }
  122. if setting.IsProd {
  123. // in prod mode, Gitea must have correct templates to run
  124. log.Fatal("Gitea can't run with template errors: %s", msg)
  125. }
  126. // in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded
  127. log.Error("There are template errors but Gitea continues to run in dev mode: %s", msg)
  128. }
  129. type templateErrorPrettier struct {
  130. assets *assetfs.LayeredFS
  131. }
  132. var reGenericTemplateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
  133. func (p *templateErrorPrettier) handleGenericTemplateError(err error) string {
  134. groups := reGenericTemplateError.FindStringSubmatch(err.Error())
  135. if len(groups) != 4 {
  136. return ""
  137. }
  138. tmplName, lineStr, message := groups[1], groups[2], groups[3]
  139. return p.makeDetailedError(message, tmplName, lineStr, -1, "")
  140. }
  141. var reFuncNotDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): (function "(.*)" not defined)`)
  142. func (p *templateErrorPrettier) handleFuncNotDefinedError(err error) string {
  143. groups := reFuncNotDefinedError.FindStringSubmatch(err.Error())
  144. if len(groups) != 5 {
  145. return ""
  146. }
  147. tmplName, lineStr, message, funcName := groups[1], groups[2], groups[3], groups[4]
  148. funcName, _ = strconv.Unquote(`"` + funcName + `"`)
  149. return p.makeDetailedError(message, tmplName, lineStr, -1, funcName)
  150. }
  151. var reUnexpectedOperandError = regexp.MustCompile(`^template: (.*):([0-9]+): (unexpected "(.*)" in operand)`)
  152. func (p *templateErrorPrettier) handleUnexpectedOperandError(err error) string {
  153. groups := reUnexpectedOperandError.FindStringSubmatch(err.Error())
  154. if len(groups) != 5 {
  155. return ""
  156. }
  157. tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
  158. unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
  159. return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
  160. }
  161. var reExpectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): (expected end; found (.*))`)
  162. func (p *templateErrorPrettier) handleExpectedEndError(err error) string {
  163. groups := reExpectedEndError.FindStringSubmatch(err.Error())
  164. if len(groups) != 5 {
  165. return ""
  166. }
  167. tmplName, lineStr, message, unexpected := groups[1], groups[2], groups[3], groups[4]
  168. return p.makeDetailedError(message, tmplName, lineStr, -1, unexpected)
  169. }
  170. var (
  171. reTemplateExecutingError = regexp.MustCompile(`^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)`)
  172. reTemplateExecutingErrorMsg = regexp.MustCompile(`^executing "(.*)" at <(.*)>: `)
  173. )
  174. func (p *templateErrorPrettier) handleTemplateRenderingError(err error) string {
  175. if groups := reTemplateExecutingError.FindStringSubmatch(err.Error()); len(groups) > 0 {
  176. tmplName, lineStr, posStr, msgPart := groups[1], groups[2], groups[3], groups[4]
  177. target := ""
  178. if groups = reTemplateExecutingErrorMsg.FindStringSubmatch(msgPart); len(groups) > 0 {
  179. target = groups[2]
  180. }
  181. return p.makeDetailedError(msgPart, tmplName, lineStr, posStr, target)
  182. } else if execErr, ok := err.(texttemplate.ExecError); ok {
  183. layerName := p.assets.GetFileLayerName(execErr.Name + ".tmpl")
  184. return fmt.Sprintf("asset from: %s, %s", layerName, err.Error())
  185. }
  186. return err.Error()
  187. }
  188. func HandleTemplateRenderingError(err error) string {
  189. p := &templateErrorPrettier{assets: AssetFS()}
  190. return p.handleTemplateRenderingError(err)
  191. }
  192. const dashSeparator = "----------------------------------------------------------------------"
  193. func (p *templateErrorPrettier) makeDetailedError(errMsg, tmplName string, lineNum, posNum any, target string) string {
  194. code, layer, err := p.assets.ReadLayeredFile(tmplName + ".tmpl")
  195. if err != nil {
  196. return fmt.Sprintf("template error: %s, and unable to find template file %q", errMsg, tmplName)
  197. }
  198. line, err := util.ToInt64(lineNum)
  199. if err != nil {
  200. return fmt.Sprintf("template error: %s, unable to parse template %q line number %q", errMsg, tmplName, lineNum)
  201. }
  202. pos, err := util.ToInt64(posNum)
  203. if err != nil {
  204. return fmt.Sprintf("template error: %s, unable to parse template %q pos number %q", errMsg, tmplName, posNum)
  205. }
  206. detail := extractErrorLine(code, int(line), int(pos), target)
  207. var msg string
  208. if pos >= 0 {
  209. msg = fmt.Sprintf("template error: %s:%s:%d:%d : %s", layer, tmplName, line, pos, errMsg)
  210. } else {
  211. msg = fmt.Sprintf("template error: %s:%s:%d : %s", layer, tmplName, line, errMsg)
  212. }
  213. return msg + "\n" + dashSeparator + "\n" + detail + "\n" + dashSeparator
  214. }
  215. func extractErrorLine(code []byte, lineNum, posNum int, target string) string {
  216. b := bufio.NewReader(bytes.NewReader(code))
  217. var line []byte
  218. var err error
  219. for i := 0; i < lineNum; i++ {
  220. if line, err = b.ReadBytes('\n'); err != nil {
  221. if i == lineNum-1 && errors.Is(err, io.EOF) {
  222. err = nil
  223. }
  224. break
  225. }
  226. }
  227. if err != nil {
  228. return fmt.Sprintf("unable to find target line %d", lineNum)
  229. }
  230. line = bytes.TrimRight(line, "\r\n")
  231. var indicatorLine []byte
  232. targetBytes := []byte(target)
  233. targetLen := len(targetBytes)
  234. for i := 0; i < len(line); {
  235. if posNum == -1 && target != "" && bytes.HasPrefix(line[i:], targetBytes) {
  236. for j := 0; j < targetLen && i < len(line); j++ {
  237. indicatorLine = append(indicatorLine, '^')
  238. i++
  239. }
  240. } else if i == posNum {
  241. indicatorLine = append(indicatorLine, '^')
  242. i++
  243. } else {
  244. if line[i] == '\t' {
  245. indicatorLine = append(indicatorLine, '\t')
  246. } else {
  247. indicatorLine = append(indicatorLine, ' ')
  248. }
  249. i++
  250. }
  251. }
  252. // if the indicatorLine only contains spaces, trim it together
  253. return strings.TrimRight(string(line)+"\n"+string(indicatorLine), " \t\r\n")
  254. }