templatefuncsRegistry.go (6949B)
1 // Copyright 2017-present The Hugo Authors. All rights reserved. 2 // 3 // Portions Copyright The Go Authors. 4 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package internal 17 18 import ( 19 "bytes" 20 "encoding/json" 21 "fmt" 22 "go/doc" 23 "go/parser" 24 "go/token" 25 "io/ioutil" 26 "log" 27 "os" 28 "path/filepath" 29 "reflect" 30 "runtime" 31 "strings" 32 "sync" 33 34 "github.com/gohugoio/hugo/deps" 35 ) 36 37 // TemplateFuncsNamespaceRegistry describes a registry of functions that provide 38 // namespaces. 39 var TemplateFuncsNamespaceRegistry []func(d *deps.Deps) *TemplateFuncsNamespace 40 41 // AddTemplateFuncsNamespace adds a given function to a registry. 42 func AddTemplateFuncsNamespace(ns func(d *deps.Deps) *TemplateFuncsNamespace) { 43 TemplateFuncsNamespaceRegistry = append(TemplateFuncsNamespaceRegistry, ns) 44 } 45 46 // TemplateFuncsNamespace represents a template function namespace. 47 type TemplateFuncsNamespace struct { 48 // The namespace name, "strings", "lang", etc. 49 Name string 50 51 // This is the method receiver. 52 Context func(v ...any) (any, error) 53 54 // Additional info, aliases and examples, per method name. 55 MethodMappings map[string]TemplateFuncMethodMapping 56 } 57 58 // TemplateFuncsNamespaces is a slice of TemplateFuncsNamespace. 59 type TemplateFuncsNamespaces []*TemplateFuncsNamespace 60 61 // AddMethodMapping adds a method to a template function namespace. 62 func (t *TemplateFuncsNamespace) AddMethodMapping(m any, aliases []string, examples [][2]string) { 63 if t.MethodMappings == nil { 64 t.MethodMappings = make(map[string]TemplateFuncMethodMapping) 65 } 66 67 name := methodToName(m) 68 69 // sanity check 70 for _, e := range examples { 71 if e[0] == "" { 72 panic(t.Name + ": Empty example for " + name) 73 } 74 } 75 for _, a := range aliases { 76 if a == "" { 77 panic(t.Name + ": Empty alias for " + name) 78 } 79 } 80 81 t.MethodMappings[name] = TemplateFuncMethodMapping{ 82 Method: m, 83 Aliases: aliases, 84 Examples: examples, 85 } 86 } 87 88 // TemplateFuncMethodMapping represents a mapping of functions to methods for a 89 // given namespace. 90 type TemplateFuncMethodMapping struct { 91 Method any 92 93 // Any template funcs aliases. This is mainly motivated by keeping 94 // backwards compatibility, but some new template funcs may also make 95 // sense to give short and snappy aliases. 96 // Note that these aliases are global and will be merged, so the last 97 // key will win. 98 Aliases []string 99 100 // A slice of input/expected examples. 101 // We keep it a the namespace level for now, but may find a way to keep track 102 // of the single template func, for documentation purposes. 103 // Some of these, hopefully just a few, may depend on some test data to run. 104 Examples [][2]string 105 } 106 107 func methodToName(m any) string { 108 name := runtime.FuncForPC(reflect.ValueOf(m).Pointer()).Name() 109 name = filepath.Ext(name) 110 name = strings.TrimPrefix(name, ".") 111 name = strings.TrimSuffix(name, "-fm") 112 return name 113 } 114 115 type goDocFunc struct { 116 Name string 117 Description string 118 Args []string 119 Aliases []string 120 Examples [][2]string 121 } 122 123 func (t goDocFunc) toJSON() ([]byte, error) { 124 args, err := json.Marshal(t.Args) 125 if err != nil { 126 return nil, err 127 } 128 aliases, err := json.Marshal(t.Aliases) 129 if err != nil { 130 return nil, err 131 } 132 examples, err := json.Marshal(t.Examples) 133 if err != nil { 134 return nil, err 135 } 136 var buf bytes.Buffer 137 buf.WriteString(fmt.Sprintf(`%q: 138 { "Description": %q, "Args": %s, "Aliases": %s, "Examples": %s } 139 `, t.Name, t.Description, args, aliases, examples)) 140 141 return buf.Bytes(), nil 142 } 143 144 // MarshalJSON returns the JSON encoding of namespaces. 145 func (namespaces TemplateFuncsNamespaces) MarshalJSON() ([]byte, error) { 146 var buf bytes.Buffer 147 148 buf.WriteString("{") 149 150 for i, ns := range namespaces { 151 if i != 0 { 152 buf.WriteString(",") 153 } 154 b, err := ns.toJSON() 155 if err != nil { 156 return nil, err 157 } 158 buf.Write(b) 159 } 160 161 buf.WriteString("}") 162 163 return buf.Bytes(), nil 164 } 165 166 var ignoreFuncs = map[string]bool{ 167 "Reset": true, 168 } 169 170 func (t *TemplateFuncsNamespace) toJSON() ([]byte, error) { 171 var buf bytes.Buffer 172 173 godoc := getGetTplPackagesGoDoc()[t.Name] 174 175 var funcs []goDocFunc 176 177 buf.WriteString(fmt.Sprintf(`%q: {`, t.Name)) 178 179 ctx, err := t.Context() 180 if err != nil { 181 return nil, err 182 } 183 ctxType := reflect.TypeOf(ctx) 184 for i := 0; i < ctxType.NumMethod(); i++ { 185 method := ctxType.Method(i) 186 if ignoreFuncs[method.Name] { 187 continue 188 } 189 f := goDocFunc{ 190 Name: method.Name, 191 } 192 193 methodGoDoc := godoc[method.Name] 194 195 if mapping, ok := t.MethodMappings[method.Name]; ok { 196 f.Aliases = mapping.Aliases 197 f.Examples = mapping.Examples 198 f.Description = methodGoDoc.Description 199 f.Args = methodGoDoc.Args 200 } 201 202 funcs = append(funcs, f) 203 } 204 205 for i, f := range funcs { 206 if i != 0 { 207 buf.WriteString(",") 208 } 209 funcStr, err := f.toJSON() 210 if err != nil { 211 return nil, err 212 } 213 buf.Write(funcStr) 214 } 215 216 buf.WriteString("}") 217 218 return buf.Bytes(), nil 219 } 220 221 type methodGoDocInfo struct { 222 Description string 223 Args []string 224 } 225 226 var ( 227 tplPackagesGoDoc map[string]map[string]methodGoDocInfo 228 tplPackagesGoDocInit sync.Once 229 ) 230 231 func getGetTplPackagesGoDoc() map[string]map[string]methodGoDocInfo { 232 tplPackagesGoDocInit.Do(func() { 233 tplPackagesGoDoc = make(map[string]map[string]methodGoDocInfo) 234 pwd, err := os.Getwd() 235 if err != nil { 236 log.Fatal(err) 237 } 238 239 fset := token.NewFileSet() 240 241 // pwd will be inside one of the namespace packages during tests 242 var basePath string 243 if strings.Contains(pwd, "tpl") { 244 basePath = filepath.Join(pwd, "..") 245 } else { 246 basePath = filepath.Join(pwd, "tpl") 247 } 248 249 files, err := ioutil.ReadDir(basePath) 250 if err != nil { 251 log.Fatal(err) 252 } 253 254 for _, fi := range files { 255 if !fi.IsDir() { 256 continue 257 } 258 259 namespaceDoc := make(map[string]methodGoDocInfo) 260 packagePath := filepath.Join(basePath, fi.Name()) 261 262 d, err := parser.ParseDir(fset, packagePath, nil, parser.ParseComments) 263 if err != nil { 264 log.Fatal(err) 265 } 266 267 for _, f := range d { 268 p := doc.New(f, "./", 0) 269 270 for _, t := range p.Types { 271 if t.Name == "Namespace" { 272 for _, tt := range t.Methods { 273 var args []string 274 for _, p := range tt.Decl.Type.Params.List { 275 for _, pp := range p.Names { 276 args = append(args, pp.Name) 277 } 278 } 279 280 description := strings.TrimSpace(tt.Doc) 281 di := methodGoDocInfo{Description: description, Args: args} 282 namespaceDoc[tt.Name] = di 283 } 284 } 285 } 286 } 287 288 tplPackagesGoDoc[fi.Name()] = namespaceDoc 289 } 290 }) 291 292 return tplPackagesGoDoc 293 }