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 }