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 }