github.com/SDLMoe/hugo@v0.47.1/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 "html/template" 19 "strings" 20 texttemplate "text/template" 21 "text/template/parse" 22 ) 23 24 // decl keeps track of the variable mappings, i.e. $mysite => .Site etc. 25 type decl map[string]string 26 27 const ( 28 paramsIdentifier = "Params" 29 ) 30 31 // Containers that may contain Params that we will not touch. 32 var reservedContainers = map[string]bool{ 33 // Aka .Site.Data.Params which must stay case sensitive. 34 "Data": true, 35 } 36 37 type templateContext struct { 38 decl decl 39 visited map[string]bool 40 lookupFn func(name string) *parse.Tree 41 } 42 43 func (c templateContext) getIfNotVisited(name string) *parse.Tree { 44 if c.visited[name] { 45 return nil 46 } 47 c.visited[name] = true 48 return c.lookupFn(name) 49 } 50 51 func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { 52 return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)} 53 54 } 55 56 func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { 57 return func(nn string) *parse.Tree { 58 tt := templ.Lookup(nn) 59 if tt != nil { 60 return tt.Tree 61 } 62 return nil 63 } 64 } 65 66 func applyTemplateTransformersToHMLTTemplate(templ *template.Template) error { 67 return applyTemplateTransformers(templ.Tree, createParseTreeLookup(templ)) 68 } 69 70 func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error { 71 return applyTemplateTransformers(templ.Tree, 72 func(nn string) *parse.Tree { 73 tt := templ.Lookup(nn) 74 if tt != nil { 75 return tt.Tree 76 } 77 return nil 78 }) 79 } 80 81 func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *parse.Tree) error { 82 if templ == nil { 83 return errors.New("expected template, but none provided") 84 } 85 86 c := newTemplateContext(lookupFn) 87 88 c.paramsKeysToLower(templ.Root) 89 90 return nil 91 } 92 93 // paramsKeysToLower is made purposely non-generic to make it not so tempting 94 // to do more of these hard-to-maintain AST transformations. 95 func (c *templateContext) paramsKeysToLower(n parse.Node) { 96 switch x := n.(type) { 97 case *parse.ListNode: 98 if x != nil { 99 c.paramsKeysToLowerForNodes(x.Nodes...) 100 } 101 case *parse.ActionNode: 102 c.paramsKeysToLowerForNodes(x.Pipe) 103 case *parse.IfNode: 104 c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) 105 case *parse.WithNode: 106 c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) 107 case *parse.RangeNode: 108 c.paramsKeysToLowerForNodes(x.Pipe, x.List, x.ElseList) 109 case *parse.TemplateNode: 110 subTempl := c.getIfNotVisited(x.Name) 111 if subTempl != nil { 112 c.paramsKeysToLowerForNodes(subTempl.Root) 113 } 114 case *parse.PipeNode: 115 for i, elem := range x.Decl { 116 if len(x.Cmds) > i { 117 // maps $site => .Site etc. 118 c.decl[elem.Ident[0]] = x.Cmds[i].String() 119 } 120 } 121 122 for _, cmd := range x.Cmds { 123 c.paramsKeysToLower(cmd) 124 } 125 126 case *parse.CommandNode: 127 for _, elem := range x.Args { 128 switch an := elem.(type) { 129 case *parse.FieldNode: 130 c.updateIdentsIfNeeded(an.Ident) 131 case *parse.VariableNode: 132 c.updateIdentsIfNeeded(an.Ident) 133 case *parse.PipeNode: 134 c.paramsKeysToLower(an) 135 } 136 137 } 138 } 139 } 140 141 func (c *templateContext) paramsKeysToLowerForNodes(nodes ...parse.Node) { 142 for _, node := range nodes { 143 c.paramsKeysToLower(node) 144 } 145 } 146 147 func (c *templateContext) updateIdentsIfNeeded(idents []string) { 148 index := c.decl.indexOfReplacementStart(idents) 149 150 if index == -1 { 151 return 152 } 153 154 for i := index; i < len(idents); i++ { 155 idents[i] = strings.ToLower(idents[i]) 156 } 157 158 } 159 160 // indexOfReplacementStart will return the index of where to start doing replacement, 161 // -1 if none needed. 162 func (d decl) indexOfReplacementStart(idents []string) int { 163 164 l := len(idents) 165 166 if l == 0 { 167 return -1 168 } 169 170 if l == 1 { 171 first := idents[0] 172 if first == "" || first == paramsIdentifier || first[0] == '$' { 173 // This can not be a Params.x 174 return -1 175 } 176 } 177 178 var lookFurther bool 179 var needsVarExpansion bool 180 for _, ident := range idents { 181 if ident[0] == '$' { 182 lookFurther = true 183 needsVarExpansion = true 184 break 185 } else if ident == paramsIdentifier { 186 lookFurther = true 187 break 188 } 189 } 190 191 if !lookFurther { 192 return -1 193 } 194 195 var resolvedIdents []string 196 197 if !needsVarExpansion { 198 resolvedIdents = idents 199 } else { 200 var ok bool 201 resolvedIdents, ok = d.resolveVariables(idents) 202 if !ok { 203 return -1 204 } 205 } 206 207 var paramFound bool 208 for i, ident := range resolvedIdents { 209 if ident == paramsIdentifier { 210 if i > 0 { 211 container := resolvedIdents[i-1] 212 if reservedContainers[container] { 213 // .Data.Params.someKey 214 return -1 215 } 216 } 217 218 paramFound = true 219 break 220 } 221 } 222 223 if !paramFound { 224 return -1 225 } 226 227 var paramSeen bool 228 idx := -1 229 for i, ident := range idents { 230 if ident == "" || ident[0] == '$' { 231 continue 232 } 233 234 if ident == paramsIdentifier { 235 paramSeen = true 236 idx = -1 237 238 } else { 239 if paramSeen { 240 return i 241 } 242 if idx == -1 { 243 idx = i 244 } 245 } 246 } 247 return idx 248 249 } 250 251 func (d decl) resolveVariables(idents []string) ([]string, bool) { 252 var ( 253 replacements []string 254 replaced []string 255 ) 256 257 // An Ident can start out as one of 258 // [Params] [$blue] [$colors.Blue] 259 // We need to resolve the variables, so 260 // $blue => [Params Colors Blue] 261 // etc. 262 replacements = []string{idents[0]} 263 264 // Loop until there are no more $vars to resolve. 265 for i := 0; i < len(replacements); i++ { 266 267 if i > 20 { 268 // bail out 269 return nil, false 270 } 271 272 potentialVar := replacements[i] 273 274 if potentialVar == "$" { 275 continue 276 } 277 278 if potentialVar == "" || potentialVar[0] != '$' { 279 // leave it as is 280 replaced = append(replaced, strings.Split(potentialVar, ".")...) 281 continue 282 } 283 284 replacement, ok := d[potentialVar] 285 286 if !ok { 287 // Temporary range vars. We do not care about those. 288 return nil, false 289 } 290 291 if !d.isKeyword(replacement) { 292 // This can not be .Site.Params etc. 293 return nil, false 294 } 295 296 replacement = strings.TrimPrefix(replacement, ".") 297 298 if replacement == "" { 299 continue 300 } 301 302 if replacement[0] == '$' { 303 // Needs further expansion 304 replacements = append(replacements, strings.Split(replacement, ".")...) 305 } else { 306 replaced = append(replaced, strings.Split(replacement, ".")...) 307 } 308 } 309 310 return append(replaced, idents[1:]...), true 311 312 } 313 314 func (d decl) isKeyword(s string) bool { 315 return !strings.ContainsAny(s, " -\"") 316 }