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