github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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  	"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(ctx context.Context, 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  	// Rewrite §§ to ` in example commands.
    70  	for i, e := range examples {
    71  		examples[i][0] = strings.ReplaceAll(e[0], "§§", "`")
    72  	}
    73  
    74  	// sanity check
    75  	for _, e := range examples {
    76  		if e[0] == "" {
    77  			panic(t.Name + ": Empty example for " + name)
    78  		}
    79  	}
    80  	for _, a := range aliases {
    81  		if a == "" {
    82  			panic(t.Name + ": Empty alias for " + name)
    83  		}
    84  	}
    85  
    86  	t.MethodMappings[name] = TemplateFuncMethodMapping{
    87  		Method:   m,
    88  		Aliases:  aliases,
    89  		Examples: examples,
    90  	}
    91  }
    92  
    93  // TemplateFuncMethodMapping represents a mapping of functions to methods for a
    94  // given namespace.
    95  type TemplateFuncMethodMapping struct {
    96  	Method any
    97  
    98  	// Any template funcs aliases. This is mainly motivated by keeping
    99  	// backwards compatibility, but some new template funcs may also make
   100  	// sense to give short and snappy aliases.
   101  	// Note that these aliases are global and will be merged, so the last
   102  	// key will win.
   103  	Aliases []string
   104  
   105  	// A slice of input/expected examples.
   106  	// We keep it a the namespace level for now, but may find a way to keep track
   107  	// of the single template func, for documentation purposes.
   108  	// Some of these, hopefully just a few, may depend on some test data to run.
   109  	Examples [][2]string
   110  }
   111  
   112  func methodToName(m any) string {
   113  	name := runtime.FuncForPC(reflect.ValueOf(m).Pointer()).Name()
   114  	name = filepath.Ext(name)
   115  	name = strings.TrimPrefix(name, ".")
   116  	name = strings.TrimSuffix(name, "-fm")
   117  	return name
   118  }
   119  
   120  type goDocFunc struct {
   121  	Name        string
   122  	Description string
   123  	Args        []string
   124  	Aliases     []string
   125  	Examples    [][2]string
   126  }
   127  
   128  func (t goDocFunc) toJSON() ([]byte, error) {
   129  	args, err := json.Marshal(t.Args)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	aliases, err := json.Marshal(t.Aliases)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	examples, err := json.Marshal(t.Examples)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	var buf bytes.Buffer
   142  	buf.WriteString(fmt.Sprintf(`%q:
   143      { "Description": %q, "Args": %s, "Aliases": %s, "Examples": %s }	
   144  `, t.Name, t.Description, args, aliases, examples))
   145  
   146  	return buf.Bytes(), nil
   147  }
   148  
   149  // ToMap returns a limited map representation of the namespaces.
   150  func (namespaces TemplateFuncsNamespaces) ToMap() map[string]any {
   151  	m := make(map[string]any)
   152  	for _, ns := range namespaces {
   153  		mm := make(map[string]any)
   154  		for name, mapping := range ns.MethodMappings {
   155  			mm[name] = map[string]any{
   156  				"Examples": mapping.Examples,
   157  				"Aliases":  mapping.Aliases,
   158  			}
   159  		}
   160  		m[ns.Name] = mm
   161  	}
   162  	return m
   163  }
   164  
   165  // MarshalJSON returns the JSON encoding of namespaces.
   166  func (namespaces TemplateFuncsNamespaces) MarshalJSON() ([]byte, error) {
   167  	var buf bytes.Buffer
   168  
   169  	buf.WriteString("{")
   170  
   171  	for i, ns := range namespaces {
   172  
   173  		b, err := ns.toJSON(context.TODO())
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  		if b != nil {
   178  			if i != 0 {
   179  				buf.WriteString(",")
   180  			}
   181  			buf.Write(b)
   182  		}
   183  	}
   184  
   185  	buf.WriteString("}")
   186  
   187  	return buf.Bytes(), nil
   188  }
   189  
   190  var ignoreFuncs = map[string]bool{
   191  	"Reset": true,
   192  }
   193  
   194  func (t *TemplateFuncsNamespace) toJSON(ctx context.Context) ([]byte, error) {
   195  	var buf bytes.Buffer
   196  
   197  	godoc := getGetTplPackagesGoDoc()[t.Name]
   198  
   199  	var funcs []goDocFunc
   200  
   201  	buf.WriteString(fmt.Sprintf(`%q: {`, t.Name))
   202  
   203  	tctx, err := t.Context(ctx)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	if tctx == nil {
   208  		// E.g. page.
   209  		// We should fix this, but we're going to abandon this construct in a little while.
   210  		return nil, nil
   211  	}
   212  	ctxType := reflect.TypeOf(tctx)
   213  	for i := 0; i < ctxType.NumMethod(); i++ {
   214  		method := ctxType.Method(i)
   215  		if ignoreFuncs[method.Name] {
   216  			continue
   217  		}
   218  		f := goDocFunc{
   219  			Name: method.Name,
   220  		}
   221  
   222  		methodGoDoc := godoc[method.Name]
   223  
   224  		if mapping, ok := t.MethodMappings[method.Name]; ok {
   225  			f.Aliases = mapping.Aliases
   226  			f.Examples = mapping.Examples
   227  			f.Description = methodGoDoc.Description
   228  			f.Args = methodGoDoc.Args
   229  		}
   230  
   231  		funcs = append(funcs, f)
   232  	}
   233  
   234  	for i, f := range funcs {
   235  		if i != 0 {
   236  			buf.WriteString(",")
   237  		}
   238  		funcStr, err := f.toJSON()
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		buf.Write(funcStr)
   243  	}
   244  
   245  	buf.WriteString("}")
   246  
   247  	return buf.Bytes(), nil
   248  }
   249  
   250  type methodGoDocInfo struct {
   251  	Description string
   252  	Args        []string
   253  }
   254  
   255  var (
   256  	tplPackagesGoDoc     map[string]map[string]methodGoDocInfo
   257  	tplPackagesGoDocInit sync.Once
   258  )
   259  
   260  func getGetTplPackagesGoDoc() map[string]map[string]methodGoDocInfo {
   261  	tplPackagesGoDocInit.Do(func() {
   262  		tplPackagesGoDoc = make(map[string]map[string]methodGoDocInfo)
   263  		pwd, err := os.Getwd()
   264  		if err != nil {
   265  			log.Fatal(err)
   266  		}
   267  
   268  		fset := token.NewFileSet()
   269  
   270  		// pwd will be inside one of the namespace packages during tests
   271  		var basePath string
   272  		if strings.Contains(pwd, "tpl") {
   273  			basePath = filepath.Join(pwd, "..")
   274  		} else {
   275  			basePath = filepath.Join(pwd, "tpl")
   276  		}
   277  
   278  		files, err := os.ReadDir(basePath)
   279  		if err != nil {
   280  			log.Fatal(err)
   281  		}
   282  
   283  		for _, fi := range files {
   284  			if !fi.IsDir() {
   285  				continue
   286  			}
   287  
   288  			namespaceDoc := make(map[string]methodGoDocInfo)
   289  			packagePath := filepath.Join(basePath, fi.Name())
   290  
   291  			d, err := parser.ParseDir(fset, packagePath, nil, parser.ParseComments)
   292  			if err != nil {
   293  				log.Fatal(err)
   294  			}
   295  
   296  			for _, f := range d {
   297  				p := doc.New(f, "./", 0)
   298  
   299  				for _, t := range p.Types {
   300  					if t.Name == "Namespace" {
   301  						for _, tt := range t.Methods {
   302  							var args []string
   303  							for _, p := range tt.Decl.Type.Params.List {
   304  								for _, pp := range p.Names {
   305  									args = append(args, pp.Name)
   306  								}
   307  							}
   308  
   309  							description := strings.TrimSpace(tt.Doc)
   310  							di := methodGoDocInfo{Description: description, Args: args}
   311  							namespaceDoc[tt.Name] = di
   312  						}
   313  					}
   314  				}
   315  			}
   316  
   317  			tplPackagesGoDoc[fi.Name()] = namespaceDoc
   318  		}
   319  	})
   320  
   321  	return tplPackagesGoDoc
   322  }