Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions tpl/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package debug

import (
"encoding/json"
"reflect"
"sort"
"sync"
"time"
Expand Down Expand Up @@ -120,6 +121,67 @@ func (ns *Namespace) VisualizeSpaces(val any) string {
return string(util.VisualizeSpaces([]byte(s)))
}

// List returns a string slice of field names and methods for structs/pointers,
// or keys for maps. This function uses reflection and is non-recursive.
// For structs and pointers to structs, it returns all exported field names and method names.
// Method names include both value and pointer receiver methods.
// For maps, it returns all keys converted to strings.
// For other types, it returns an empty slice.
func (ns *Namespace) List(val any) []string {
if val == nil {
return []string{}
}

v := reflect.ValueOf(val)
t := v.Type()

// Handle pointers by dereferencing them
if t.Kind() == reflect.Ptr {
if v.IsNil() {
return []string{}
}
v = v.Elem()
t = v.Type()
}

var names []string

switch t.Kind() {
case reflect.Struct:
// Get field names
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.IsExported() {
names = append(names, field.Name)
}
}

// Get method names (use pointer type to get all methods)
ptrType := reflect.PointerTo(t)
for i := 0; i < ptrType.NumMethod(); i++ {
method := ptrType.Method(i)
if method.IsExported() {
names = append(names, method.Name)
}
}

case reflect.Map:
// Get map keys as strings
for _, key := range v.MapKeys() {
keyStr := cast.ToString(key.Interface())
names = append(names, keyStr)
}

default:
// For other types, return empty slice
return []string{}
}

// Sort the names for consistent output
sort.Strings(names)
return names
}

func (ns *Namespace) Timer(name string) Timer {
if ns.timers == nil {
return nopTimer
Expand Down
38 changes: 38 additions & 0 deletions tpl/debug/debug_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,41 @@ Dump: {{ debug.Dump . | safeHTML }}
b := hugolib.TestRunning(t, files)
b.AssertFileContent("public/index.html", "Dump: {\n \"Date\": \"2012-03-15T00:00:00Z\"")
}

func TestDebugList(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.org/"
disableLiveReload = true
-- content/_index.md --
---
title: "The Index"
---
-- layouts/_default/list.html --
{{ $dict := dict "name" "Hugo" "version" "0.120" "type" "SSG" }}
Map Keys: {{ debug.List $dict }}

{{ $page := . }}
Page Fields: {{ debug.List $page }}

Nil: {{ debug.List nil }}
String: {{ debug.List "hello" }}
Number: {{ debug.List 42 }}
Slice: {{ debug.List (slice 1 2 3) }}
`
b := hugolib.TestRunning(t, files)

// Test map keys
b.AssertFileContent("public/index.html", "Map Keys: [name type version]")

// Test that page struct returns field names and methods (should include common page fields)
b.AssertFileContent("public/index.html", "Page Fields:")
b.AssertFileContent("public/index.html", "Title")
b.AssertFileContent("public/index.html", "Content")

// Test edge cases
b.AssertFileContent("public/index.html", "Nil: []")
b.AssertFileContent("public/index.html", "String: []")
b.AssertFileContent("public/index.html", "Number: []")
b.AssertFileContent("public/index.html", "Slice: []")
}
8 changes: 8 additions & 0 deletions tpl/debug/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ func init() {
},
)

ns.AddMethodMapping(ctx.List,
nil,
[][2]string{
{`{{ $s := dict "name" "Hugo" "type" "SSG" }}
{{ debug.List $s }}`, `[name type]`},
},
)

return ns
}

Expand Down