github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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  	"regexp"
    18  	"strings"
    19  
    20  	htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
    21  	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
    22  
    23  	"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
    24  
    25  	"github.com/gohugoio/hugo/common/maps"
    26  	"github.com/gohugoio/hugo/tpl"
    27  	"github.com/mitchellh/mapstructure"
    28  	"github.com/pkg/errors"
    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  	identityNotFound map[string]bool
    43  	lookupFn         func(name string) *templateState
    44  
    45  	// The last error encountered.
    46  	err error
    47  
    48  	// Set when we're done checking for config header.
    49  	configChecked bool
    50  
    51  	t *templateState
    52  
    53  	// Store away the return node in partials.
    54  	returnNode *parse.CommandNode
    55  }
    56  
    57  func (c templateContext) getIfNotVisited(name string) *templateState {
    58  	if c.visited[name] {
    59  		return nil
    60  	}
    61  	c.visited[name] = true
    62  	templ := c.lookupFn(name)
    63  	if templ == nil {
    64  		// This may be a inline template defined outside of this file
    65  		// and not yet parsed. Unusual, but it happens.
    66  		// Store the name to try again later.
    67  		c.templateNotFound[name] = true
    68  	}
    69  
    70  	return templ
    71  }
    72  
    73  func newTemplateContext(
    74  	t *templateState,
    75  	lookupFn func(name string) *templateState) *templateContext {
    76  	return &templateContext{
    77  		t:                t,
    78  		lookupFn:         lookupFn,
    79  		visited:          make(map[string]bool),
    80  		templateNotFound: make(map[string]bool),
    81  		identityNotFound: make(map[string]bool),
    82  	}
    83  }
    84  
    85  func applyTemplateTransformers(
    86  	t *templateState,
    87  	lookupFn func(name string) *templateState) (*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  	partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
   116  )
   117  
   118  var partialReturnWrapper *parse.ListNode
   119  
   120  func init() {
   121  	templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
   122  	if err != nil {
   123  		panic(err)
   124  	}
   125  	partialReturnWrapper = templ.Tree.Root
   126  }
   127  
   128  func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
   129  	wrapper := partialReturnWrapper.CopyList()
   130  	withNode := wrapper.Nodes[2].(*parse.WithNode)
   131  	retn := withNode.List.Nodes[0]
   132  	setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
   133  	setPipe := setCmd.Args[1].(*parse.PipeNode)
   134  	// Replace PLACEHOLDER with the real return value.
   135  	// Note that this is a PipeNode, so it will be wrapped in parens.
   136  	setPipe.Cmds = []*parse.CommandNode{c.returnNode}
   137  	withNode.List.Nodes = append(n.Nodes, retn)
   138  
   139  	return wrapper
   140  }
   141  
   142  // applyTransformations do 2 things:
   143  // 1) Parses partial return statement.
   144  // 2) Tracks template (partial) dependencies and some other info.
   145  func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
   146  	switch x := n.(type) {
   147  	case *parse.ListNode:
   148  		if x != nil {
   149  			c.applyTransformationsToNodes(x.Nodes...)
   150  		}
   151  	case *parse.ActionNode:
   152  		c.applyTransformationsToNodes(x.Pipe)
   153  	case *parse.IfNode:
   154  		c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
   155  	case *parse.WithNode:
   156  		c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
   157  	case *parse.RangeNode:
   158  		c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
   159  	case *parse.TemplateNode:
   160  		subTempl := c.getIfNotVisited(x.Name)
   161  		if subTempl != nil {
   162  			c.applyTransformationsToNodes(getParseTree(subTempl.Template).Root)
   163  		}
   164  	case *parse.PipeNode:
   165  		c.collectConfig(x)
   166  		for i, cmd := range x.Cmds {
   167  			keep, _ := c.applyTransformations(cmd)
   168  			if !keep {
   169  				x.Cmds = append(x.Cmds[:i], x.Cmds[i+1:]...)
   170  			}
   171  		}
   172  
   173  	case *parse.CommandNode:
   174  		c.collectPartialInfo(x)
   175  		c.collectInner(x)
   176  		keep := c.collectReturnNode(x)
   177  
   178  		for _, elem := range x.Args {
   179  			switch an := elem.(type) {
   180  			case *parse.PipeNode:
   181  				c.applyTransformations(an)
   182  			}
   183  		}
   184  		return keep, c.err
   185  	}
   186  
   187  	return true, c.err
   188  }
   189  
   190  func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
   191  	for _, node := range nodes {
   192  		c.applyTransformations(node)
   193  	}
   194  }
   195  
   196  func (c *templateContext) hasIdent(idents []string, ident string) bool {
   197  	for _, id := range idents {
   198  		if id == ident {
   199  			return true
   200  		}
   201  	}
   202  	return false
   203  }
   204  
   205  // collectConfig collects and parses any leading template config variable declaration.
   206  // This will be the first PipeNode in the template, and will be a variable declaration
   207  // on the form:
   208  //    {{ $_hugo_config:= `{ "version": 1 }` }}
   209  func (c *templateContext) collectConfig(n *parse.PipeNode) {
   210  	if c.t.typ != templateShortcode {
   211  		return
   212  	}
   213  	if c.configChecked {
   214  		return
   215  	}
   216  	c.configChecked = true
   217  
   218  	if len(n.Decl) != 1 || len(n.Cmds) != 1 {
   219  		// This cannot be a config declaration
   220  		return
   221  	}
   222  
   223  	v := n.Decl[0]
   224  
   225  	if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
   226  		return
   227  	}
   228  
   229  	cmd := n.Cmds[0]
   230  
   231  	if len(cmd.Args) == 0 {
   232  		return
   233  	}
   234  
   235  	if s, ok := cmd.Args[0].(*parse.StringNode); ok {
   236  		errMsg := "failed to decode $_hugo_config in template"
   237  		m, err := maps.ToStringMapE(s.Text)
   238  		if err != nil {
   239  			c.err = errors.Wrap(err, errMsg)
   240  			return
   241  		}
   242  		if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil {
   243  			c.err = errors.Wrap(err, errMsg)
   244  		}
   245  	}
   246  }
   247  
   248  // collectInner determines if the given CommandNode represents a
   249  // shortcode call to its .Inner.
   250  func (c *templateContext) collectInner(n *parse.CommandNode) {
   251  	if c.t.typ != templateShortcode {
   252  		return
   253  	}
   254  	if c.t.parseInfo.IsInner || len(n.Args) == 0 {
   255  		return
   256  	}
   257  
   258  	for _, arg := range n.Args {
   259  		var idents []string
   260  		switch nt := arg.(type) {
   261  		case *parse.FieldNode:
   262  			idents = nt.Ident
   263  		case *parse.VariableNode:
   264  			idents = nt.Ident
   265  		}
   266  
   267  		if c.hasIdent(idents, "Inner") {
   268  			c.t.parseInfo.IsInner = true
   269  			break
   270  		}
   271  	}
   272  }
   273  
   274  var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`)
   275  
   276  func (c *templateContext) collectPartialInfo(x *parse.CommandNode) {
   277  	if len(x.Args) < 2 {
   278  		return
   279  	}
   280  
   281  	first := x.Args[0]
   282  	var id string
   283  	switch v := first.(type) {
   284  	case *parse.IdentifierNode:
   285  		id = v.Ident
   286  	case *parse.ChainNode:
   287  		id = v.String()
   288  	}
   289  
   290  	if partialRe.MatchString(id) {
   291  		partialName := strings.Trim(x.Args[1].String(), "\"")
   292  		if !strings.Contains(partialName, ".") {
   293  			partialName += ".html"
   294  		}
   295  		partialName = "partials/" + partialName
   296  		info := c.lookupFn(partialName)
   297  
   298  		if info != nil {
   299  			c.t.Add(info)
   300  		} else {
   301  			// Delay for later
   302  			c.identityNotFound[partialName] = true
   303  		}
   304  	}
   305  }
   306  
   307  func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
   308  	if c.t.typ != templatePartial || c.returnNode != nil {
   309  		return true
   310  	}
   311  
   312  	if len(n.Args) < 2 {
   313  		return true
   314  	}
   315  
   316  	ident, ok := n.Args[0].(*parse.IdentifierNode)
   317  	if !ok || ident.Ident != "return" {
   318  		return true
   319  	}
   320  
   321  	c.returnNode = n
   322  	// Remove the "return" identifiers
   323  	c.returnNode.Args = c.returnNode.Args[1:]
   324  
   325  	return false
   326  }
   327  
   328  func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) {
   329  	in = unwrap(in)
   330  	if text, ok := in.(*texttemplate.Template); ok {
   331  		if templ := text.Lookup(name); templ != nil {
   332  			return templ, true
   333  		}
   334  		return nil, false
   335  	}
   336  	if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil {
   337  		return templ, true
   338  	}
   339  	return nil, false
   340  }