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