template_ast_transformers.go (8869B)
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 // {{ $_hugo_config:= `{ "version": 1 }` }} 217 func (c *templateContext) collectConfig(n *parse.PipeNode) { 218 if c.t.typ != templateShortcode { 219 return 220 } 221 if c.configChecked { 222 return 223 } 224 c.configChecked = true 225 226 if len(n.Decl) != 1 || len(n.Cmds) != 1 { 227 // This cannot be a config declaration 228 return 229 } 230 231 v := n.Decl[0] 232 233 if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" { 234 return 235 } 236 237 cmd := n.Cmds[0] 238 239 if len(cmd.Args) == 0 { 240 return 241 } 242 243 if s, ok := cmd.Args[0].(*parse.StringNode); ok { 244 errMsg := "failed to decode $_hugo_config in template: %w" 245 m, err := maps.ToStringMapE(s.Text) 246 if err != nil { 247 c.err = fmt.Errorf(errMsg, err) 248 return 249 } 250 if err := mapstructure.WeakDecode(m, &c.t.parseInfo.Config); err != nil { 251 c.err = fmt.Errorf(errMsg, err) 252 } 253 } 254 } 255 256 // collectInner determines if the given CommandNode represents a 257 // shortcode call to its .Inner. 258 func (c *templateContext) collectInner(n *parse.CommandNode) { 259 if c.t.typ != templateShortcode { 260 return 261 } 262 if c.t.parseInfo.IsInner || len(n.Args) == 0 { 263 return 264 } 265 266 for _, arg := range n.Args { 267 var idents []string 268 switch nt := arg.(type) { 269 case *parse.FieldNode: 270 idents = nt.Ident 271 case *parse.VariableNode: 272 idents = nt.Ident 273 } 274 275 if c.hasIdent(idents, "Inner") || c.hasIdent(idents, "InnerDeindent") { 276 c.t.parseInfo.IsInner = true 277 break 278 } 279 } 280 } 281 282 var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`) 283 284 func (c *templateContext) collectPartialInfo(x *parse.CommandNode) { 285 if len(x.Args) < 2 { 286 return 287 } 288 289 first := x.Args[0] 290 var id string 291 switch v := first.(type) { 292 case *parse.IdentifierNode: 293 id = v.Ident 294 case *parse.ChainNode: 295 id = v.String() 296 } 297 298 if partialRe.MatchString(id) { 299 partialName := strings.Trim(x.Args[1].String(), "\"") 300 if !strings.Contains(partialName, ".") { 301 partialName += ".html" 302 } 303 partialName = "partials/" + partialName 304 info := c.lookupFn(partialName) 305 306 if info != nil { 307 c.t.Add(info) 308 } else { 309 // Delay for later 310 c.identityNotFound[partialName] = true 311 } 312 } 313 } 314 315 func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool { 316 if c.t.typ != templatePartial || c.returnNode != nil { 317 return true 318 } 319 320 if len(n.Args) < 2 { 321 return true 322 } 323 324 ident, ok := n.Args[0].(*parse.IdentifierNode) 325 if !ok || ident.Ident != "return" { 326 return true 327 } 328 329 c.returnNode = n 330 // Remove the "return" identifiers 331 c.returnNode.Args = c.returnNode.Args[1:] 332 333 return false 334 } 335 336 func findTemplateIn(name string, in tpl.Template) (tpl.Template, bool) { 337 in = unwrap(in) 338 if text, ok := in.(*texttemplate.Template); ok { 339 if templ := text.Lookup(name); templ != nil { 340 return templ, true 341 } 342 return nil, false 343 } 344 if templ := in.(*htmltemplate.Template).Lookup(name); templ != nil { 345 return templ, true 346 } 347 return nil, false 348 }