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 }