layout.go (7336B)
1 // Copyright 2017-present 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 output 15 16 import ( 17 "strings" 18 "sync" 19 20 "github.com/gohugoio/hugo/helpers" 21 ) 22 23 // These may be used as content sections with potential conflicts. Avoid that. 24 var reservedSections = map[string]bool{ 25 "shortcodes": true, 26 "partials": true, 27 } 28 29 // LayoutDescriptor describes how a layout should be chosen. This is 30 // typically built from a Page. 31 type LayoutDescriptor struct { 32 Type string 33 Section string 34 35 // E.g. "page", but also used for the _markup render kinds, e.g. "render-image". 36 Kind string 37 38 // Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html" 39 KindVariants string 40 41 Lang string 42 Layout string 43 // LayoutOverride indicates what we should only look for the above layout. 44 LayoutOverride bool 45 46 RenderingHook bool 47 Baseof bool 48 } 49 50 func (d LayoutDescriptor) isList() bool { 51 return !d.RenderingHook && d.Kind != "page" && d.Kind != "404" 52 } 53 54 // LayoutHandler calculates the layout template to use to render a given output type. 55 type LayoutHandler struct { 56 mu sync.RWMutex 57 cache map[layoutCacheKey][]string 58 } 59 60 type layoutCacheKey struct { 61 d LayoutDescriptor 62 f string 63 } 64 65 // NewLayoutHandler creates a new LayoutHandler. 66 func NewLayoutHandler() *LayoutHandler { 67 return &LayoutHandler{cache: make(map[layoutCacheKey][]string)} 68 } 69 70 // For returns a layout for the given LayoutDescriptor and options. 71 // Layouts are rendered and cached internally. 72 func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) { 73 // We will get lots of requests for the same layouts, so avoid recalculations. 74 key := layoutCacheKey{d, f.Name} 75 l.mu.RLock() 76 if cacheVal, found := l.cache[key]; found { 77 l.mu.RUnlock() 78 return cacheVal, nil 79 } 80 l.mu.RUnlock() 81 82 layouts := resolvePageTemplate(d, f) 83 84 layouts = helpers.UniqueStringsReuse(layouts) 85 86 l.mu.Lock() 87 l.cache[key] = layouts 88 l.mu.Unlock() 89 90 return layouts, nil 91 } 92 93 type layoutBuilder struct { 94 layoutVariations []string 95 typeVariations []string 96 d LayoutDescriptor 97 f Format 98 } 99 100 func (l *layoutBuilder) addLayoutVariations(vars ...string) { 101 for _, layoutVar := range vars { 102 if l.d.Baseof && layoutVar != "baseof" { 103 l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof") 104 continue 105 } 106 if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout { 107 continue 108 } 109 l.layoutVariations = append(l.layoutVariations, layoutVar) 110 } 111 } 112 113 func (l *layoutBuilder) addTypeVariations(vars ...string) { 114 for _, typeVar := range vars { 115 if !reservedSections[typeVar] { 116 if l.d.RenderingHook { 117 typeVar = typeVar + renderingHookRoot 118 } 119 l.typeVariations = append(l.typeVariations, typeVar) 120 } 121 } 122 } 123 124 func (l *layoutBuilder) addSectionType() { 125 if l.d.Section != "" { 126 l.addTypeVariations(l.d.Section) 127 } 128 } 129 130 func (l *layoutBuilder) addKind() { 131 l.addLayoutVariations(l.d.Kind) 132 l.addTypeVariations(l.d.Kind) 133 } 134 135 const renderingHookRoot = "/_markup" 136 137 func resolvePageTemplate(d LayoutDescriptor, f Format) []string { 138 b := &layoutBuilder{d: d, f: f} 139 140 if !d.RenderingHook && d.Layout != "" { 141 b.addLayoutVariations(d.Layout) 142 } 143 if d.Type != "" { 144 b.addTypeVariations(d.Type) 145 } 146 147 if d.RenderingHook { 148 if d.KindVariants != "" { 149 // Add the more specific variants first. 150 for _, variant := range strings.Split(d.KindVariants, ",") { 151 b.addLayoutVariations(d.Kind + "-" + variant) 152 } 153 } 154 b.addLayoutVariations(d.Kind) 155 b.addSectionType() 156 } 157 158 switch d.Kind { 159 case "page": 160 b.addLayoutVariations("single") 161 b.addSectionType() 162 case "home": 163 b.addLayoutVariations("index", "home") 164 // Also look in the root 165 b.addTypeVariations("") 166 case "section": 167 if d.Section != "" { 168 b.addLayoutVariations(d.Section) 169 } 170 b.addSectionType() 171 b.addKind() 172 case "term": 173 b.addKind() 174 if d.Section != "" { 175 b.addLayoutVariations(d.Section) 176 } 177 b.addLayoutVariations("taxonomy") 178 b.addTypeVariations("taxonomy") 179 b.addSectionType() 180 case "taxonomy": 181 if d.Section != "" { 182 b.addLayoutVariations(d.Section + ".terms") 183 } 184 b.addSectionType() 185 b.addLayoutVariations("terms") 186 // For legacy reasons this is deliberately put last. 187 b.addKind() 188 case "404": 189 b.addLayoutVariations("404") 190 b.addTypeVariations("") 191 } 192 193 isRSS := f.Name == RSSFormat.Name 194 if !d.RenderingHook && !d.Baseof && isRSS { 195 // The historic and common rss.xml case 196 b.addLayoutVariations("") 197 } 198 199 if d.Baseof || d.Kind != "404" { 200 // Most have _default in their lookup path 201 b.addTypeVariations("_default") 202 } 203 204 if d.isList() { 205 // Add the common list type 206 b.addLayoutVariations("list") 207 } 208 209 if d.Baseof { 210 b.addLayoutVariations("baseof") 211 } 212 213 layouts := b.resolveVariations() 214 215 if !d.RenderingHook && !d.Baseof && isRSS { 216 layouts = append(layouts, "_internal/_default/rss.xml") 217 } 218 219 return layouts 220 } 221 222 func (l *layoutBuilder) resolveVariations() []string { 223 var layouts []string 224 225 var variations []string 226 name := strings.ToLower(l.f.Name) 227 228 if l.d.Lang != "" { 229 // We prefer the most specific type before language. 230 variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...) 231 } else { 232 variations = append(variations, name) 233 } 234 235 variations = append(variations, "") 236 237 for _, typeVar := range l.typeVariations { 238 for _, variation := range variations { 239 for _, layoutVar := range l.layoutVariations { 240 if variation == "" && layoutVar == "" { 241 continue 242 } 243 244 s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.FirstSuffix.Suffix) 245 if s != "" { 246 layouts = append(layouts, s) 247 } 248 } 249 } 250 } 251 252 return layouts 253 } 254 255 // constructLayoutPath constructs a layout path given a type, layout, 256 // variations, and extension. The path constructed follows the pattern of 257 // type/layout.variations.extension. If any value is empty, it will be left out 258 // of the path construction. 259 // 260 // Path construction requires at least 2 of 3 out of layout, variations, and extension. 261 // If more than one of those is empty, an empty string is returned. 262 func constructLayoutPath(typ, layout, variations, extension string) string { 263 // we already know that layout and variations are not both empty because of 264 // checks in resolveVariants(). 265 if extension == "" && (layout == "" || variations == "") { 266 return "" 267 } 268 269 // Commence valid path construction... 270 271 var ( 272 p strings.Builder 273 needDot bool 274 ) 275 276 if typ != "" { 277 p.WriteString(typ) 278 p.WriteString("/") 279 } 280 281 if layout != "" { 282 p.WriteString(layout) 283 needDot = true 284 } 285 286 if variations != "" { 287 if needDot { 288 p.WriteString(".") 289 } 290 p.WriteString(variations) 291 needDot = true 292 } 293 294 if extension != "" { 295 if needDot { 296 p.WriteString(".") 297 } 298 p.WriteString(extension) 299 } 300 301 return p.String() 302 }