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