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