github.com/whatlly/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  }