mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	You can now create custom hook templates for code blocks, either one for all (`render-codeblock.html`) or for a given code language (e.g. `render-codeblock-go.html`). We also used this new hook to add support for diagrams in Hugo: * Goat (Go ASCII Tool) is built-in and enabled by default; just create a fenced code block with the language `goat` and start draw your Ascii diagrams. * Another popular alternative for diagrams in Markdown, Mermaid (supported by GitHub), can also be implemented with a simple template. See the Hugo documentation for more information. Updates #7765 Closes #9538 Fixes #9553 Fixes #8520 Fixes #6702 Fixes #9558
		
			
				
	
	
		
			303 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2017-present The Hugo Authors. All rights reserved.
 | 
						|
//
 | 
						|
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
// http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
 | 
						|
package output
 | 
						|
 | 
						|
import (
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/helpers"
 | 
						|
)
 | 
						|
 | 
						|
// These may be used as content sections with potential conflicts. Avoid that.
 | 
						|
var reservedSections = map[string]bool{
 | 
						|
	"shortcodes": true,
 | 
						|
	"partials":   true,
 | 
						|
}
 | 
						|
 | 
						|
// LayoutDescriptor describes how a layout should be chosen. This is
 | 
						|
// typically built from a Page.
 | 
						|
type LayoutDescriptor struct {
 | 
						|
	Type    string
 | 
						|
	Section string
 | 
						|
 | 
						|
	// E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
 | 
						|
	Kind string
 | 
						|
 | 
						|
	// Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
 | 
						|
	KindVariants string
 | 
						|
 | 
						|
	Lang   string
 | 
						|
	Layout string
 | 
						|
	// LayoutOverride indicates what we should only look for the above layout.
 | 
						|
	LayoutOverride bool
 | 
						|
 | 
						|
	RenderingHook bool
 | 
						|
	Baseof        bool
 | 
						|
}
 | 
						|
 | 
						|
func (d LayoutDescriptor) isList() bool {
 | 
						|
	return !d.RenderingHook && d.Kind != "page" && d.Kind != "404"
 | 
						|
}
 | 
						|
 | 
						|
// LayoutHandler calculates the layout template to use to render a given output type.
 | 
						|
type LayoutHandler struct {
 | 
						|
	mu    sync.RWMutex
 | 
						|
	cache map[layoutCacheKey][]string
 | 
						|
}
 | 
						|
 | 
						|
type layoutCacheKey struct {
 | 
						|
	d LayoutDescriptor
 | 
						|
	f string
 | 
						|
}
 | 
						|
 | 
						|
// NewLayoutHandler creates a new LayoutHandler.
 | 
						|
func NewLayoutHandler() *LayoutHandler {
 | 
						|
	return &LayoutHandler{cache: make(map[layoutCacheKey][]string)}
 | 
						|
}
 | 
						|
 | 
						|
// For returns a layout for the given LayoutDescriptor and options.
 | 
						|
// Layouts are rendered and cached internally.
 | 
						|
func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) {
 | 
						|
	// We will get lots of requests for the same layouts, so avoid recalculations.
 | 
						|
	key := layoutCacheKey{d, f.Name}
 | 
						|
	l.mu.RLock()
 | 
						|
	if cacheVal, found := l.cache[key]; found {
 | 
						|
		l.mu.RUnlock()
 | 
						|
		return cacheVal, nil
 | 
						|
	}
 | 
						|
	l.mu.RUnlock()
 | 
						|
 | 
						|
	layouts := resolvePageTemplate(d, f)
 | 
						|
 | 
						|
	layouts = helpers.UniqueStringsReuse(layouts)
 | 
						|
 | 
						|
	l.mu.Lock()
 | 
						|
	l.cache[key] = layouts
 | 
						|
	l.mu.Unlock()
 | 
						|
 | 
						|
	return layouts, nil
 | 
						|
}
 | 
						|
 | 
						|
type layoutBuilder struct {
 | 
						|
	layoutVariations []string
 | 
						|
	typeVariations   []string
 | 
						|
	d                LayoutDescriptor
 | 
						|
	f                Format
 | 
						|
}
 | 
						|
 | 
						|
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
 | 
						|
	for _, layoutVar := range vars {
 | 
						|
		if l.d.Baseof && layoutVar != "baseof" {
 | 
						|
			l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		l.layoutVariations = append(l.layoutVariations, layoutVar)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (l *layoutBuilder) addTypeVariations(vars ...string) {
 | 
						|
	for _, typeVar := range vars {
 | 
						|
		if !reservedSections[typeVar] {
 | 
						|
			if l.d.RenderingHook {
 | 
						|
				typeVar = typeVar + renderingHookRoot
 | 
						|
			}
 | 
						|
			l.typeVariations = append(l.typeVariations, typeVar)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (l *layoutBuilder) addSectionType() {
 | 
						|
	if l.d.Section != "" {
 | 
						|
		l.addTypeVariations(l.d.Section)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (l *layoutBuilder) addKind() {
 | 
						|
	l.addLayoutVariations(l.d.Kind)
 | 
						|
	l.addTypeVariations(l.d.Kind)
 | 
						|
}
 | 
						|
 | 
						|
const renderingHookRoot = "/_markup"
 | 
						|
 | 
						|
func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
 | 
						|
	b := &layoutBuilder{d: d, f: f}
 | 
						|
 | 
						|
	if !d.RenderingHook && d.Layout != "" {
 | 
						|
		b.addLayoutVariations(d.Layout)
 | 
						|
	}
 | 
						|
	if d.Type != "" {
 | 
						|
		b.addTypeVariations(d.Type)
 | 
						|
	}
 | 
						|
 | 
						|
	if d.RenderingHook {
 | 
						|
		if d.KindVariants != "" {
 | 
						|
			// Add the more specific variants first.
 | 
						|
			for _, variant := range strings.Split(d.KindVariants, ",") {
 | 
						|
				b.addLayoutVariations(d.Kind + "-" + variant)
 | 
						|
			}
 | 
						|
		}
 | 
						|
		b.addLayoutVariations(d.Kind)
 | 
						|
		b.addSectionType()
 | 
						|
	}
 | 
						|
 | 
						|
	switch d.Kind {
 | 
						|
	case "page":
 | 
						|
		b.addLayoutVariations("single")
 | 
						|
		b.addSectionType()
 | 
						|
	case "home":
 | 
						|
		b.addLayoutVariations("index", "home")
 | 
						|
		// Also look in the root
 | 
						|
		b.addTypeVariations("")
 | 
						|
	case "section":
 | 
						|
		if d.Section != "" {
 | 
						|
			b.addLayoutVariations(d.Section)
 | 
						|
		}
 | 
						|
		b.addSectionType()
 | 
						|
		b.addKind()
 | 
						|
	case "term":
 | 
						|
		b.addKind()
 | 
						|
		if d.Section != "" {
 | 
						|
			b.addLayoutVariations(d.Section)
 | 
						|
		}
 | 
						|
		b.addLayoutVariations("taxonomy")
 | 
						|
		b.addTypeVariations("taxonomy")
 | 
						|
		b.addSectionType()
 | 
						|
	case "taxonomy":
 | 
						|
		if d.Section != "" {
 | 
						|
			b.addLayoutVariations(d.Section + ".terms")
 | 
						|
		}
 | 
						|
		b.addSectionType()
 | 
						|
		b.addLayoutVariations("terms")
 | 
						|
		// For legacy reasons this is deliberately put last.
 | 
						|
		b.addKind()
 | 
						|
	case "404":
 | 
						|
		b.addLayoutVariations("404")
 | 
						|
		b.addTypeVariations("")
 | 
						|
	}
 | 
						|
 | 
						|
	isRSS := f.Name == RSSFormat.Name
 | 
						|
	if !d.RenderingHook && !d.Baseof && isRSS {
 | 
						|
		// The historic and common rss.xml case
 | 
						|
		b.addLayoutVariations("")
 | 
						|
	}
 | 
						|
 | 
						|
	if d.Baseof || d.Kind != "404" {
 | 
						|
		// Most have _default in their lookup path
 | 
						|
		b.addTypeVariations("_default")
 | 
						|
	}
 | 
						|
 | 
						|
	if d.isList() {
 | 
						|
		// Add the common list type
 | 
						|
		b.addLayoutVariations("list")
 | 
						|
	}
 | 
						|
 | 
						|
	if d.Baseof {
 | 
						|
		b.addLayoutVariations("baseof")
 | 
						|
	}
 | 
						|
 | 
						|
	layouts := b.resolveVariations()
 | 
						|
 | 
						|
	if !d.RenderingHook && !d.Baseof && isRSS {
 | 
						|
		layouts = append(layouts, "_internal/_default/rss.xml")
 | 
						|
	}
 | 
						|
 | 
						|
	return layouts
 | 
						|
}
 | 
						|
 | 
						|
func (l *layoutBuilder) resolveVariations() []string {
 | 
						|
	var layouts []string
 | 
						|
 | 
						|
	var variations []string
 | 
						|
	name := strings.ToLower(l.f.Name)
 | 
						|
 | 
						|
	if l.d.Lang != "" {
 | 
						|
		// We prefer the most specific type before language.
 | 
						|
		variations = append(variations, []string{l.d.Lang + "." + name, name, l.d.Lang}...)
 | 
						|
	} else {
 | 
						|
		variations = append(variations, name)
 | 
						|
	}
 | 
						|
 | 
						|
	variations = append(variations, "")
 | 
						|
 | 
						|
	for _, typeVar := range l.typeVariations {
 | 
						|
		for _, variation := range variations {
 | 
						|
			for _, layoutVar := range l.layoutVariations {
 | 
						|
				if variation == "" && layoutVar == "" {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.FirstSuffix.Suffix)
 | 
						|
				if s != "" {
 | 
						|
					layouts = append(layouts, s)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return layouts
 | 
						|
}
 | 
						|
 | 
						|
// constructLayoutPath constructs a layout path given a type, layout,
 | 
						|
// variations, and extension.  The path constructed follows the pattern of
 | 
						|
// type/layout.variations.extension.  If any value is empty, it will be left out
 | 
						|
// of the path construction.
 | 
						|
//
 | 
						|
// Path construction requires at least 2 of 3 out of layout, variations, and extension.
 | 
						|
// If more than one of those is empty, an empty string is returned.
 | 
						|
func constructLayoutPath(typ, layout, variations, extension string) string {
 | 
						|
	// we already know that layout and variations are not both empty because of
 | 
						|
	// checks in resolveVariants().
 | 
						|
	if extension == "" && (layout == "" || variations == "") {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	// Commence valid path construction...
 | 
						|
 | 
						|
	var (
 | 
						|
		p       strings.Builder
 | 
						|
		needDot bool
 | 
						|
	)
 | 
						|
 | 
						|
	if typ != "" {
 | 
						|
		p.WriteString(typ)
 | 
						|
		p.WriteString("/")
 | 
						|
	}
 | 
						|
 | 
						|
	if layout != "" {
 | 
						|
		p.WriteString(layout)
 | 
						|
		needDot = true
 | 
						|
	}
 | 
						|
 | 
						|
	if variations != "" {
 | 
						|
		if needDot {
 | 
						|
			p.WriteString(".")
 | 
						|
		}
 | 
						|
		p.WriteString(variations)
 | 
						|
		needDot = true
 | 
						|
	}
 | 
						|
 | 
						|
	if extension != "" {
 | 
						|
		if needDot {
 | 
						|
			p.WriteString(".")
 | 
						|
		}
 | 
						|
		p.WriteString(extension)
 | 
						|
	}
 | 
						|
 | 
						|
	return p.String()
 | 
						|
}
 |