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  }