2023-04-07 17:39:08 +03:00
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"fmt"
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>
2023-04-22 20:28:20 +03:00
"html"
"html/template"
2023-04-07 17:39:08 +03:00
"reflect"
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>
2023-04-22 20:28:20 +03:00
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
2023-04-07 17:39:08 +03:00
)
func dictMerge ( base map [ string ] any , arg any ) bool {
if arg == nil {
return true
}
rv := reflect . ValueOf ( arg )
if rv . Kind ( ) == reflect . Map {
for _ , k := range rv . MapKeys ( ) {
base [ k . String ( ) ] = rv . MapIndex ( k ) . Interface ( )
}
return true
}
return false
}
// dict is a helper function for creating a map[string]any from a list of key-value pairs.
// If the key is dot ".", the value is merged into the base map, just like Golang template's dot syntax: dot means current
// The dot syntax is highly discouraged because it might cause unclear key conflicts. It's always good to use explicit keys.
func dict ( args ... any ) ( map [ string ] any , error ) {
if len ( args ) % 2 != 0 {
return nil , fmt . Errorf ( "invalid dict constructor syntax: must have key-value pairs" )
}
m := make ( map [ string ] any , len ( args ) / 2 )
for i := 0 ; i < len ( args ) ; i += 2 {
key , ok := args [ i ] . ( string )
if ! ok {
return nil , fmt . Errorf ( "invalid dict constructor syntax: unable to merge args[%d]" , i )
}
if key == "." {
if ok = dictMerge ( m , args [ i + 1 ] ) ; ! ok {
return nil , fmt . Errorf ( "invalid dict constructor syntax: dot arg[%d] must be followed by a dict" , i )
}
} else {
m [ key ] = args [ i + 1 ]
}
}
return m , nil
}
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>
2023-04-22 20:28:20 +03:00
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>" )
}