Add `DumpVar` helper function to help debugging templates (#24262)

I guess many contributors might agree that it's really difficult to
write Golang template. The dot syntax `.` confuses everyone: what
variable it is ....

So, we can use a `{{DumpVar .ContextUser}}` to look into every variable
now.


![image](https://user-images.githubusercontent.com/2114189/233692383-f3c8f24d-4465-45f8-839b-b63e00731559.png)


And it can even dump the whole `ctx.Data` by `{{DumpVar .}}`:

```
dumpVar: templates.Vars
{
  "AllLangs": [
    {
      "Lang": "id-ID",
      "Name": "Bahasa Indonesia"
    },
...
      "Context": "[dumped]",
      "ContextUser": {
        "AllowCreateOrganization": true,
        "AllowGitHook": false,
        "AllowImportLocal": false,
...
  "TemplateLoadTimes": "[func() string]",
  "TemplateName": "user/profile",
  "Title": "Full'\u003cspan\u003e Name",
  "Total": 7,
  "UnitActionsGlobalDisabled": false,
  "UnitIssuesGlobalDisabled": false,
  "UnitProjectsGlobalDisabled": false,
  "UnitPullsGlobalDisabled": false,
  "UnitWikiGlobalDisabled": false,
  "locale": {
    "Lang": "en-US",
    "LangName": "English",
    "Locale": {}
  }
...

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
wxiaoguang 2023-04-23 01:28:20 +08:00 committed by GitHub
parent 3cc87370c3
commit c0d105609f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 75 additions and 0 deletions

View File

@ -74,6 +74,7 @@ func NewFuncMap() []template.FuncMap {
"DotEscape": DotEscape,
"HasPrefix": strings.HasPrefix,
"EllipsisString": base.EllipsisString,
"DumpVar": dumpVar,
"Json": func(in interface{}) string {
out, err := json.Marshal(in)

View File

@ -5,7 +5,12 @@ package templates
import (
"fmt"
"html"
"html/template"
"reflect"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
)
func dictMerge(base map[string]any, arg any) bool {
@ -45,3 +50,72 @@ func dict(args ...any) (map[string]any, error) {
}
return m, nil
}
func dumpVarMarshalable(v any, dumped map[uintptr]bool) (ret any, ok bool) {
if v == nil {
return nil, true
}
e := reflect.ValueOf(v)
for e.Kind() == reflect.Pointer {
e = e.Elem()
}
if e.CanAddr() {
addr := e.UnsafeAddr()
if dumped[addr] {
return "[dumped]", false
}
dumped[addr] = true
defer delete(dumped, addr)
}
switch e.Kind() {
case reflect.Bool, reflect.String,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return e.Interface(), true
case reflect.Struct:
m := map[string]any{}
for i := 0; i < e.NumField(); i++ {
k := e.Type().Field(i).Name
if !e.Type().Field(i).IsExported() {
continue
}
v := e.Field(i).Interface()
m[k], _ = dumpVarMarshalable(v, dumped)
}
return m, true
case reflect.Map:
m := map[string]any{}
for _, k := range e.MapKeys() {
m[k.String()], _ = dumpVarMarshalable(e.MapIndex(k).Interface(), dumped)
}
return m, true
case reflect.Array, reflect.Slice:
var m []any
for i := 0; i < e.Len(); i++ {
v, _ := dumpVarMarshalable(e.Index(i).Interface(), dumped)
m = append(m, v)
}
return m, true
default:
return "[" + reflect.TypeOf(v).String() + "]", false
}
}
// dumpVar helps to dump a variable in a template, to help debugging and development.
func dumpVar(v any) template.HTML {
if setting.IsProd {
return "<pre>dumpVar: only available in dev mode</pre>"
}
m, ok := dumpVarMarshalable(v, map[uintptr]bool{})
dumpStr := ""
jsonBytes, err := json.MarshalIndent(m, "", " ")
if err != nil {
dumpStr = fmt.Sprintf("dumpVar: unable to marshal %T: %v", v, err)
} else if ok {
dumpStr = fmt.Sprintf("dumpVar: %T\n%s", v, string(jsonBytes))
} else {
dumpStr = fmt.Sprintf("dumpVar: unmarshalable %T\n%s", v, string(jsonBytes))
}
return template.HTML("<pre>" + html.EscapeString(dumpStr) + "</pre>")
}