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