github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/output/layouts/layout.go (about)

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