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  }