hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }