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 }