github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/tpl/tplimpl/template_ast_transformers.go (about)

     1  // Copyright 2016 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package tplimpl
    15  
    16  import (
    17  	"fmt"
    18  	"regexp"
    19  	"strings"
    20  
    21  	htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
    22  	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
    23  
    24  	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
    25  
    26  	"errors"
    27  
    28  	"github.com/gohugoio/hugo/common/maps"
    29  	"github.com/gohugoio/hugo/tpl"
    30  	"github.com/mitchellh/mapstructure"
    31  )
    32  
    33  type templateType int
    34  
    35  const (
    36  	templateUndefined templateType = iota
    37  	templateShortcode
    38  	templatePartial
    39  )
    40  
    41  type templateContext struct {
    42  	visited          map[string]bool
    43  	templateNotFound map[string]bool
    44  	identityNotFound map[string]bool
    45  	lookupFn         func(name string) *templateState
    46  
    47  	// The last error encountered.
    48  	err error
    49  
    50  	// Set when we're done checking for config header.
    51  	configChecked bool
    52  
    53  	t *templateState
    54  
    55  	// Store away the return node in partials.
    56  	returnNode *parse.CommandNode
    57  }
    58  
    59  func (c templateContext) getIfNotVisited(name string) *templateState {
    60  	if c.visited[name] {
    61  		return nil
    62  	}
    63  	c.visited[name] = true
    64  	templ := c.lookupFn(name)
    65  	if templ == nil {
    66  		// This may be a inline template defined outside of this file
    67  		// and not yet parsed. Unusual, but it happens.
    68  		// Store the name to try again later.
    69  		c.templateNotFound[name] = true
    70  	}
    71  
    72  	return templ
    73  }
    74  
    75  func newTemplateContext(
    76  	t *templateState,
    77  	lookupFn func(name string) *templateState) *templateContext {
    78  	return &templateContext{
    79  		t:                t,
    80  		lookupFn:         lookupFn,
    81  		visited:          make(map[string]bool),
    82  		templateNotFound: make(map[string]bool),
    83  		identityNotFound: make(map[string]bool),
    84  	}
    85  }
    86  
    87  func applyTemplateTransformers(
    88  	t *templateState,
    89  	lookupFn func(name string) *templateState) (*templateContext, error) {
    90  	if t == nil {
    91  		return nil, errors.New("expected template, but none provided")
    92  	}
    93  
    94  	c := newTemplateContext(t, lookupFn)
    95  	tree := getParseTree(t.Template)
    96  
    97  	_, err := c.applyTransformations(tree.Root)
    98  
    99  	if err == nil && c.returnNode != nil {
   100  		// This is a partial with a return statement.
   101  		c.t.parseInfo.HasReturn = true
   102  		tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
   103  	}
   104  
   105  	return c, err
   106  }
   107  
   108  func getParseTree(templ tpl.Template) *parse.Tree {
   109  	templ = unwrap(templ)
   110  	if text, ok := templ.(*texttemplate.Template); ok {
   111  		return text.Tree
   112  	}
   113  	return templ.(*htmltemplate.Template).Tree
   114  }
   115  
   116  const (
   117  	// We parse this template and modify the nodes in order to assign
   118  	// the return value of a partial to a contextWrapper via Set. We use
   119  	// "range" over a one-element slice so we can shift dot to the
   120  	// partial's argument, Arg, while allowing Arg to be falsy.
   121  	partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
   122  )
   123  
   124  var partialReturnWrapper *parse.ListNode
   125  
   126  func init() {
   127  	templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
   128  	if err != nil {
   129  		panic(err)
   130  	}
   131  	partialReturnWrapper = templ.Tree.Root
   132  }
   133  
   134  // wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
   135  // predefined partial return wrapper to insert those of a user-defined partial.
   136  func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
   137  	wrapper := partialReturnWrapper.CopyList()
   138  	rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
   139  	retn := rangeNode.List.Nodes[0]
   140  	setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
   141  	setPipe := setCmd.Args[1].(*parse.PipeNode)
   142  	// Replace PLACEHOLDER with the real return value.
   143  	// Note that this is a PipeNode, so it will be wrapped in parens.
   144  	setPipe.Cmds = []*parse.CommandNode{c.returnNode}
   145  	rangeNode.List.Nodes = append(n.Nodes, retn)
   146  
   147  	return wrapper
   148  }
   149  
   150  // applyTransformations do 2 things:
   151  // 1) Parses partial return statement.
   152  // 2) Tracks template (partial) dependencies and some other info.
   153  func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
   154  	switch x := n.(type) {
   155  	case *parse.ListNode:
   156  		if x != nil {
   157  			c.applyTransformationsToNodes(x.Nodes...)
   158  		}
   159  	case *parse.ActionNode:
   160  		c.applyTransformationsToNodes(x.Pipe)
   161  	case *parse.IfNode:
   162  		c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
   163  	case *parse.WithNode:
   164  		c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
   165  	case *parse.RangeNode:
   166  		c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
   167  	case *parse.TemplateNode:
   168  		subTempl := c.getIfNotVisited(x.Name)
   169  		if subTempl != nil {
   170  			c.applyTransformationsToNodes(getParseTree(subTempl.Template).Root)
   171  		}
   172  	case *parse.PipeNode:
   173  		c.collectConfig(x)
   174  		for i, cmd := range x.Cmds {
   175  			keep, _ := c.applyTransformations(cmd)
   176  			if !keep {
   177  				x.Cmds = append(x.Cmds[:i], x.Cmds[i+1:]...)
   178  			}
   179  		}
   180  
   181  	case *parse.CommandNode:
   182  		c.collectPartialInfo(x)
   183  		c.collectInner(x)
   184  		keep := c.collectReturnNode(x)
   185  
   186  		for _, elem := range x.Args {
   187  			switch an := elem.(type) {
   188  			case *parse.PipeNode:
   189  				c.applyTransformations(an)
   190  			}
   191  		}
   192  		return keep, c.err
   193  	}
   194  
   195  	return true, c.err
   196  }
   197  
   198  func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
   199  	for _, node := range nodes {
   200  		c.applyTransformations(node)
   201  	}
   202  }
   203  
   204  func (c *templateContext) hasIdent(idents []string, ident string) bool {
   205  	for _, id := range idents {
   206  		if id == ident {
   207  			return true
   208  		}
   209  	}
   210  	return false
   211  }
   212  
   213  // collectConfig collects and parses any leading template config variable declaration.
   214  // This will be the first PipeNode in the template, and will be a variable declaration
   215  // on the form:
   216  //
   217  //	{{ $_hugo_config:= `{ "version": 1 }` }}
   218  func (c *templateContext) collectConfig(n *parse.PipeNode) {
   219  	if c.t.typ != templateShortcode {
   220  		return
   221  	}
   222  	if c.configChecked {
   223  		return
   224  	}
   225  	c.configChecked = true
   226  
   227  	if len(n.Decl) != 1 || len(n.Cmds) != 1 {
   228  		// This cannot be a config declaration
   229  		return
   230  	}
   231  
   232  	v := n.Decl[0]
   233  
   234  	if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
   235  		return
   236  	}
   237  
   238  	cmd := n.Cmds[0]
   239  
   240  	if len(cmd.Args) == 0 {
   241  		return
   242  	}
   243  
   244  	if s, ok := cmd.Args[0].(*parse.StringNode); ok {
   245  		errMsg := "failed to decode $_hugo_config in template: %w"
   246  		m, err := maps.ToStringMapE(s.Text)
   247  		if err != nil {
   248  			c.err = fmt.Errorf(errMsg, err)
   249  			return
   250  		}
   251  		if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
   252  			c.err = fmt.Errorf(errMsg, err)
   253  		}
   254  	}
   255  }
   256  
   257  // collectInner determines if the given CommandNode represents a
   258  // shortcode call to its .Inner.
   259  func (c *templateContext) collectInner(n *parse.CommandNode) {
   260  	if c.t.typ != templateShortcode {
   261  		return
   262  	}
   263  	if c.t.parseInfo.IsInner || len(n.Args) == 0 {
   264  		return
   265  	}
   266  
   267  	for _, arg := range n.Args {
   268  		var idents []string
   269  		switch nt := arg.(type) {
   270  		case *parse.FieldNode:
   271  			idents = nt.Ident
   272  		case *parse.VariableNode:
   273  			idents = nt.Ident
   274  		}
   275  
   276  		if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") {
   277  			c.t.parseInfo.IsInner = true
   278  			break
   279  		}
   280  	}
   281  }
   282  
   283  var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`)
   284  
   285  func (c *templateContext) collectPartialInfo(x *parse.CommandNode) {
   286  	if len(x.Args) < 2 {
   287  		return
   288  	}
   289  
   290  	first := x.Args[0]
   291  	var id string
   292  	switch v := first.(type) {
   293  	case *parse.IdentifierNode:
   294  		id = v.Ident
   295  	case *parse.ChainNode:
   296  		id = v.String()
   297  	}
   298  
   299  	if partialRe.MatchString(id) {
   300  		partialName := strings.Trim(x.Args[1].String(), "\"")
   301  		if !strings.Contains(partialName, ".") {
   302  			partialName += ".html"
   303  		}
   304  		partialName = "partials/" + partialName
   305  		info := c.lookupFn(partialName)
   306  
   307  		if info != nil {
   308  			c.t.Add(info)
   309  		} else {
   310  			// Delay for later
   311  			c.identityNotFound[partialName] = true
   312  		}
   313  	}
   314  }
   315  
   316  func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
   317  	if c.t.typ != templatePartial || c.returnNode != nil {
   318  		return true
   319  	}
   320  
   321  	if len(n.Args) < 2 {
   322  		return true
   323  	}
   324  
   325  	ident, ok := n.Args[0].(*parse.IdentifierNode)
   326  	if !ok || ident.Ident != "return" {
   327  		return true
   328  	}
   329  
   330  	c.returnNode = n
   331  	// Remove the "return" identifiers
   332  	c.returnNode.Args = c.returnNode.Args[1:]
   333  
   334  	return false
   335  }
   336  
   337  func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
   338  	in = unwrap(in)
   339  	if text, ok := in.(*texttemplate.Template); ok {
   340  		if templ := text.Lookup(name); templ != nil {
   341  			return templ, true
   342  		}
   343  		return nil, false
   344  	}
   345  	if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
   346  		return templ, true
   347  	}
   348  	return nil, false
   349  }