github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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  
    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  }