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