github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/tpl/internal/templatefuncsRegistry.go (about) 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 ...interface{}) (interface{}, 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 interface{}, 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 interface{} 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 interface{}) 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 func (t *TemplateFuncsNamespace) toJSON() ([]byte, error) { 167 var buf bytes.Buffer 168 169 godoc := getGetTplPackagesGoDoc()[t.Name] 170 171 var funcs []goDocFunc 172 173 buf.WriteString(fmt.Sprintf(`%q: {`, t.Name)) 174 175 ctx, err := t.Context() 176 if err != nil { 177 return nil, err 178 } 179 ctxType := reflect.TypeOf(ctx) 180 for i := 0; i < ctxType.NumMethod(); i++ { 181 method := ctxType.Method(i) 182 f := goDocFunc{ 183 Name: method.Name, 184 } 185 186 methodGoDoc := godoc[method.Name] 187 188 if mapping, ok := t.MethodMappings[method.Name]; ok { 189 f.Aliases = mapping.Aliases 190 f.Examples = mapping.Examples 191 f.Description = methodGoDoc.Description 192 f.Args = methodGoDoc.Args 193 } 194 195 funcs = append(funcs, f) 196 } 197 198 for i, f := range funcs { 199 if i != 0 { 200 buf.WriteString(",") 201 } 202 funcStr, err := f.toJSON() 203 if err != nil { 204 return nil, err 205 } 206 buf.Write(funcStr) 207 } 208 209 buf.WriteString("}") 210 211 return buf.Bytes(), nil 212 } 213 214 type methodGoDocInfo struct { 215 Description string 216 Args []string 217 } 218 219 var ( 220 tplPackagesGoDoc map[string]map[string]methodGoDocInfo 221 tplPackagesGoDocInit sync.Once 222 ) 223 224 func getGetTplPackagesGoDoc() map[string]map[string]methodGoDocInfo { 225 tplPackagesGoDocInit.Do(func() { 226 tplPackagesGoDoc = make(map[string]map[string]methodGoDocInfo) 227 pwd, err := os.Getwd() 228 if err != nil { 229 log.Fatal(err) 230 } 231 232 fset := token.NewFileSet() 233 234 // pwd will be inside one of the namespace packages during tests 235 var basePath string 236 if strings.Contains(pwd, "tpl") { 237 basePath = filepath.Join(pwd, "..") 238 } else { 239 basePath = filepath.Join(pwd, "tpl") 240 } 241 242 files, err := ioutil.ReadDir(basePath) 243 if err != nil { 244 log.Fatal(err) 245 } 246 247 for _, fi := range files { 248 if !fi.IsDir() { 249 continue 250 } 251 252 namespaceDoc := make(map[string]methodGoDocInfo) 253 packagePath := filepath.Join(basePath, fi.Name()) 254 255 d, err := parser.ParseDir(fset, packagePath, nil, parser.ParseComments) 256 if err != nil { 257 log.Fatal(err) 258 } 259 260 for _, f := range d { 261 p := doc.New(f, "./", 0) 262 263 for _, t := range p.Types { 264 if t.Name == "Namespace" { 265 for _, tt := range t.Methods { 266 var args []string 267 for _, p := range tt.Decl.Type.Params.List { 268 for _, pp := range p.Names { 269 args = append(args, pp.Name) 270 } 271 } 272 273 description := strings.TrimSpace(tt.Doc) 274 di := methodGoDocInfo{Description: description, Args: args} 275 namespaceDoc[tt.Name] = di 276 } 277 } 278 } 279 } 280 281 tplPackagesGoDoc[fi.Name()] = namespaceDoc 282 } 283 }) 284 285 return tplPackagesGoDoc 286 }