hugo/tpl/tplimpl/template_ast_transformers.go

335 lines
8.5 KiB
Go

// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl
import (
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
type templateType int
const (
templateUndefined templateType = iota
templateShortcode
templatePartial
)
type templateContext struct {
visited map[string]bool
notFound map[string]bool
lookupFn func(name string) *parse.Tree
// The last error encountered.
err error
typ templateType
// Set when we're done checking for config header.
configChecked bool
// Contains some info about the template
tpl.Info
// Store away the return node in partials.
returnNode *parse.CommandNode
}
func (c templateContext) getIfNotVisited(name string) *parse.Tree {
if c.visited[name] {
return nil
}
c.visited[name] = true
templ := c.lookupFn(name)
if templ == nil {
// This may be a inline template defined outside of this file
// and not yet parsed. Unusual, but it happens.
// Store the name to try again later.
c.notFound[name] = true
}
return templ
}
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
return &templateContext{
Info: tpl.Info{Config: tpl.DefaultConfig},
lookupFn: lookupFn,
visited: make(map[string]bool),
notFound: make(map[string]bool)}
}
func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
return func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
return tt.Tree
}
return nil
}
}
func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ))
}
func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
return applyTemplateTransformers(typ, templ.Tree,
func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
return tt.Tree
}
return nil
})
}
func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) {
if templ == nil {
return nil, errors.New("expected template, but none provided")
}
c := newTemplateContext(lookupFn)
c.typ = typ
_, err := c.applyTransformations(templ.Root)
if err == nil && c.returnNode != nil {
// This is a partial with a return statement.
c.Info.HasReturn = true
templ.Root = c.wrapInPartialReturnWrapper(templ.Root)
}
return c, err
}
const (
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
)
var partialReturnWrapper *parse.ListNode
func init() {
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
if err != nil {
panic(err)
}
partialReturnWrapper = templ.Tree.Root
}
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
wrapper := partialReturnWrapper.CopyList()
withNode := wrapper.Nodes[2].(*parse.WithNode)
retn := withNode.List.Nodes[0]
setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
setPipe := setCmd.Args[1].(*parse.PipeNode)
// Replace PLACEHOLDER with the real return value.
// Note that this is a PipeNode, so it will be wrapped in parens.
setPipe.Cmds = []*parse.CommandNode{c.returnNode}
withNode.List.Nodes = append(n.Nodes, retn)
return wrapper
}
// The truth logic in Go's template package is broken for certain values
// for the if and with keywords. This works around that problem by wrapping
// the node passed to if/with in a getif conditional.
// getif works slightly different than the Go built-in in that it also
// considers any IsZero methods on the values (as in time.Time).
// See https://github.com/gohugoio/hugo/issues/5738
func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
if len(p.Cmds) == 0 {
return
}
// getif will return an empty string if not evaluated as truthful,
// which is when we need the value in the with clause.
firstArg := parse.NewIdentifier("getif")
secondArg := p.CopyPipe()
newCmd := p.Cmds[0].Copy().(*parse.CommandNode)
// secondArg is a PipeNode and will behave as it was wrapped in parens, e.g:
// {{ getif (len .Params | eq 2) }}
newCmd.Args = []parse.Node{firstArg, secondArg}
p.Cmds = []*parse.CommandNode{newCmd}
}
// applyTransformations do 3 things:
// 1) Make all .Params.CamelCase and similar into lowercase.
// 2) Wraps every with and if pipe in getif
// 3) Collects some information about the template content.
func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
switch x := n.(type) {
case *parse.ListNode:
if x != nil {
c.applyTransformationsToNodes(x.Nodes...)
}
case *parse.ActionNode:
c.applyTransformationsToNodes(x.Pipe)
case *parse.IfNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
c.wrapWithGetIf(x.Pipe)
case *parse.WithNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
c.wrapWithGetIf(x.Pipe)
case *parse.RangeNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.TemplateNode:
subTempl := c.getIfNotVisited(x.Name)
if subTempl != nil {
c.applyTransformationsToNodes(subTempl.Root)
}
case *parse.PipeNode:
c.collectConfig(x)
for i, cmd := range x.Cmds {
keep, _ := c.applyTransformations(cmd)
if !keep {
x.Cmds = append(x.Cmds[:i], x.Cmds[i+1:]...)
}
}
case *parse.CommandNode:
c.collectInner(x)
keep := c.collectReturnNode(x)
for _, elem := range x.Args {
switch an := elem.(type) {
case *parse.PipeNode:
c.applyTransformations(an)
}
}
return keep, c.err
}
return true, c.err
}
func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
for _, node := range nodes {
c.applyTransformations(node)
}
}
func (c *templateContext) hasIdent(idents []string, ident string) bool {
for _, id := range idents {
if id == ident {
return true
}
}
return false
}
// collectConfig collects and parses any leading template config variable declaration.
// This will be the first PipeNode in the template, and will be a variable declaration
// on the form:
// {{ $_hugo_config:= `{ "version": 1 }` }}
func (c *templateContext) collectConfig(n *parse.PipeNode) {
if c.typ != templateShortcode {
return
}
if c.configChecked {
return
}
c.configChecked = true
if len(n.Decl) != 1 || len(n.Cmds) != 1 {
// This cannot be a config declaration
return
}
v := n.Decl[0]
if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
return
}
cmd := n.Cmds[0]
if len(cmd.Args) == 0 {
return
}
if s, ok := cmd.Args[0].(*parse.StringNode); ok {
errMsg := "failed to decode $_hugo_config in template"
m, err := maps.ToStringMapE(s.Text)
if err != nil {
c.err = errors.Wrap(err, errMsg)
return
}
if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil {
c.err = errors.Wrap(err, errMsg)
}
}
}
// collectInner determines if the given CommandNode represents a
// shortcode call to its .Inner.
func (c *templateContext) collectInner(n *parse.CommandNode) {
if c.typ != templateShortcode {
return
}
if c.Info.IsInner || len(n.Args) == 0 {
return
}
for _, arg := range n.Args {
var idents []string
switch nt := arg.(type) {
case *parse.FieldNode:
idents = nt.Ident
case *parse.VariableNode:
idents = nt.Ident
}
if c.hasIdent(idents, "Inner") {
c.Info.IsInner = true
break
}
}
}
func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
if c.typ != templatePartial || c.returnNode != nil {
return true
}
if len(n.Args) < 2 {
return true
}
ident, ok := n.Args[0].(*parse.IdentifierNode)
if !ok || ident.Ident != "return" {
return true
}
c.returnNode = n
// Remove the "return" identifiers
c.returnNode.Args = c.returnNode.Args[1:]
return false
}