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