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 }