mirror of
https://github.com/gohugoio/hugo.git
synced 2025-03-24 06:12:55 +00:00
hugolib: Handle shortcode per output format
This commit allows shortcode per output format, a typical use case would be the special AMP media tags. Note that this will only re-render the "overridden" shortcodes and only in pages where these are used, so performance in the normal case should not suffer. Closes #3220
This commit is contained in:
parent
e951d65771
commit
af72db806f
10 changed files with 398 additions and 65 deletions
|
@ -80,7 +80,7 @@ func (h htmlHandler) PageConvert(p *Page) HandledResult {
|
||||||
p.createWorkContentCopy()
|
p.createWorkContentCopy()
|
||||||
|
|
||||||
if err := p.processShortcodes(); err != nil {
|
if err := p.processShortcodes(); err != nil {
|
||||||
return HandledResult{err: err}
|
p.s.Log.ERROR.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return HandledResult{err: nil}
|
return HandledResult{err: nil}
|
||||||
|
@ -131,7 +131,7 @@ func commonConvert(p *Page) HandledResult {
|
||||||
p.createWorkContentCopy()
|
p.createWorkContentCopy()
|
||||||
|
|
||||||
if err := p.processShortcodes(); err != nil {
|
if err := p.processShortcodes(); err != nil {
|
||||||
return HandledResult{err: err}
|
p.s.Log.ERROR.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(bep) these page handlers need to be re-evaluated, as it is hard to
|
// TODO(bep) these page handlers need to be re-evaluated, as it is hard to
|
||||||
|
|
|
@ -492,12 +492,7 @@ func (h *HugoSites) setupTranslations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
|
func (s *Site) preparePagesForRender(cfg *BuildCfg) {
|
||||||
|
|
||||||
if outFormatIdx > 0 {
|
|
||||||
// TODO(bep) for now
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pageChan := make(chan *Page)
|
pageChan := make(chan *Page)
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
|
@ -508,8 +503,16 @@ func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
|
||||||
go func(pages <-chan *Page, wg *sync.WaitGroup) {
|
go func(pages <-chan *Page, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for p := range pages {
|
for p := range pages {
|
||||||
|
if !p.shouldRenderTo(s.rc.Format) {
|
||||||
|
// No need to prepare
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var shortcodeUpdate bool
|
||||||
|
if p.shortcodeState != nil {
|
||||||
|
shortcodeUpdate = p.shortcodeState.updateDelta()
|
||||||
|
}
|
||||||
|
|
||||||
if !cfg.whatChanged.other && p.rendered {
|
if !shortcodeUpdate && !cfg.whatChanged.other && p.rendered {
|
||||||
// No need to process it again.
|
// No need to process it again.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -521,10 +524,12 @@ func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
|
||||||
// Mark it as rendered
|
// Mark it as rendered
|
||||||
p.rendered = true
|
p.rendered = true
|
||||||
|
|
||||||
// If in watch mode, we need to keep the original so we can
|
// If in watch mode or if we have multiple output formats,
|
||||||
// repeat this process on rebuild.
|
// we need to keep the original so we can
|
||||||
|
// potentially repeat this process on rebuild.
|
||||||
|
needsACopy := cfg.Watching || len(p.outputFormats) > 1
|
||||||
var workContentCopy []byte
|
var workContentCopy []byte
|
||||||
if cfg.Watching {
|
if needsACopy {
|
||||||
workContentCopy = make([]byte, len(p.workContent))
|
workContentCopy = make([]byte, len(p.workContent))
|
||||||
copy(workContentCopy, p.workContent)
|
copy(workContentCopy, p.workContent)
|
||||||
} else {
|
} else {
|
||||||
|
@ -589,15 +594,15 @@ func (h *HugoSites) Pages() Pages {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleShortcodes(p *Page, rawContentCopy []byte) ([]byte, error) {
|
func handleShortcodes(p *Page, rawContentCopy []byte) ([]byte, error) {
|
||||||
if p.shortcodeState != nil && len(p.shortcodeState.contentShortCodes) > 0 {
|
if p.shortcodeState != nil && len(p.shortcodeState.contentShortcodes) > 0 {
|
||||||
p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.shortcodeState.contentShortCodes), p.BaseFileName())
|
p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.shortcodeState.contentShortcodes), p.BaseFileName())
|
||||||
shortcodes, err := executeShortcodeFuncMap(p.shortcodeState.contentShortCodes)
|
err := p.shortcodeState.executeShortcodesForDelta(p)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rawContentCopy, err
|
return rawContentCopy, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, shortcodes)
|
rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, p.shortcodeState.renderedShortcodes)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error())
|
p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error())
|
||||||
|
|
|
@ -213,7 +213,7 @@ func (h *HugoSites) render(config *BuildCfg) error {
|
||||||
s.initRenderFormats()
|
s.initRenderFormats()
|
||||||
for i, rf := range s.renderFormats {
|
for i, rf := range s.renderFormats {
|
||||||
s.rc = &siteRenderingContext{Format: rf}
|
s.rc = &siteRenderingContext{Format: rf}
|
||||||
s.preparePagesForRender(i, config)
|
s.preparePagesForRender(config)
|
||||||
|
|
||||||
if !config.SkipRender {
|
if !config.SkipRender {
|
||||||
if err := s.render(i); err != nil {
|
if err := s.render(i); err != nil {
|
||||||
|
|
|
@ -1257,6 +1257,11 @@ func (p *Page) Menus() PageMenus {
|
||||||
return p.pageMenus
|
return p.pageMenus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Page) shouldRenderTo(f output.Format) bool {
|
||||||
|
_, found := p.outputFormats.GetByName(f.Name)
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Page) determineMarkupType() string {
|
func (p *Page) determineMarkupType() string {
|
||||||
// Try markup explicitly set in the frontmatter
|
// Try markup explicitly set in the frontmatter
|
||||||
p.Markup = helpers.GuessType(p.Markup)
|
p.Markup = helpers.GuessType(p.Markup)
|
||||||
|
@ -1372,8 +1377,8 @@ func (p *Page) SaveSource() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Page) processShortcodes() error {
|
func (p *Page) processShortcodes() error {
|
||||||
p.shortcodeState = newShortcodeHandler()
|
p.shortcodeState = newShortcodeHandler(p)
|
||||||
tmpContent, err := p.shortcodeState.extractAndRenderShortcodes(string(p.workContent), p)
|
tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -24,6 +24,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/hugo/output"
|
||||||
|
|
||||||
|
"github.com/spf13/hugo/media"
|
||||||
|
|
||||||
bp "github.com/spf13/hugo/bufferpool"
|
bp "github.com/spf13/hugo/bufferpool"
|
||||||
"github.com/spf13/hugo/helpers"
|
"github.com/spf13/hugo/helpers"
|
||||||
"github.com/spf13/hugo/tpl"
|
"github.com/spf13/hugo/tpl"
|
||||||
|
@ -149,9 +153,43 @@ func (sc shortcode) String() string {
|
||||||
return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner)
|
return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We may have special shortcode templates for AMP etc.
|
||||||
|
// Note that in the below, OutputFormat may be empty.
|
||||||
|
// We will try to look for the most specific shortcode template available.
|
||||||
|
type scKey struct {
|
||||||
|
OutputFormat string
|
||||||
|
Suffix string
|
||||||
|
ShortcodePlaceholder string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScKey(m media.Type, shortcodeplaceholder string) scKey {
|
||||||
|
return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScKeyFromOutputFormat(o output.Format, shortcodeplaceholder string) scKey {
|
||||||
|
return scKey{Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultScKey(shortcodeplaceholder string) scKey {
|
||||||
|
return newScKey(media.HTMLType, shortcodeplaceholder)
|
||||||
|
}
|
||||||
|
|
||||||
type shortcodeHandler struct {
|
type shortcodeHandler struct {
|
||||||
// Maps the shortcodeplaceholder with the shortcode rendering func.
|
init sync.Once
|
||||||
contentShortCodes map[string]func() (string, error)
|
|
||||||
|
p *Page
|
||||||
|
|
||||||
|
// This is all shortcode rendering funcs for all potential output formats.
|
||||||
|
contentShortcodes map[scKey]func() (string, error)
|
||||||
|
|
||||||
|
// This map contains the new or changed set of shortcodes that need
|
||||||
|
// to be rendered for the current output format.
|
||||||
|
contentShortcodesDelta map[scKey]func() (string, error)
|
||||||
|
|
||||||
|
// This maps the shorcode placeholders with the rendered content.
|
||||||
|
// We will do (potential) partial re-rendering per output format,
|
||||||
|
// so keep this for the unchanged.
|
||||||
|
renderedShortcodes map[string]string
|
||||||
|
|
||||||
// Maps the shortcodeplaceholder with the actual shortcode.
|
// Maps the shortcodeplaceholder with the actual shortcode.
|
||||||
shortcodes map[string]shortcode
|
shortcodes map[string]shortcode
|
||||||
|
@ -160,11 +198,13 @@ type shortcodeHandler struct {
|
||||||
nameSet map[string]bool
|
nameSet map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newShortcodeHandler() *shortcodeHandler {
|
func newShortcodeHandler(p *Page) *shortcodeHandler {
|
||||||
return &shortcodeHandler{
|
return &shortcodeHandler{
|
||||||
contentShortCodes: make(map[string]func() (string, error)),
|
p: p,
|
||||||
shortcodes: make(map[string]shortcode),
|
contentShortcodes: make(map[scKey]func() (string, error)),
|
||||||
nameSet: make(map[string]bool),
|
shortcodes: make(map[string]shortcode),
|
||||||
|
nameSet: make(map[string]bool),
|
||||||
|
renderedShortcodes: make(map[string]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,11 +248,30 @@ const innerNewlineRegexp = "\n"
|
||||||
const innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
|
const innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
|
||||||
const innerCleanupExpand = "$1"
|
const innerCleanupExpand = "$1"
|
||||||
|
|
||||||
func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
|
func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) {
|
||||||
tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
|
|
||||||
|
|
||||||
|
m := make(map[scKey]func() (string, error))
|
||||||
|
|
||||||
|
for _, f := range p.outputFormats {
|
||||||
|
// The most specific template will win.
|
||||||
|
key := newScKeyFromOutputFormat(f, placeholder)
|
||||||
|
m[key] = func() (string, error) {
|
||||||
|
return renderShortcode(key, sc, nil, p), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderShortcode(
|
||||||
|
tmplKey scKey,
|
||||||
|
sc shortcode,
|
||||||
|
parent *ShortcodeWithPage,
|
||||||
|
p *Page) string {
|
||||||
|
|
||||||
|
tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl)
|
||||||
if tmpl == nil {
|
if tmpl == nil {
|
||||||
p.s.Log.ERROR.Printf("Unable to locate template for shortcode '%s' in page %q", sc.name, p.Path())
|
p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +287,7 @@ func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
|
||||||
case string:
|
case string:
|
||||||
inner += innerData.(string)
|
inner += innerData.(string)
|
||||||
case shortcode:
|
case shortcode:
|
||||||
inner += renderShortcode(innerData.(shortcode), data, p)
|
inner += renderShortcode(tmplKey, innerData.(shortcode), data, p)
|
||||||
default:
|
default:
|
||||||
p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
|
p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
|
||||||
sc.name, p.Path(), reflect.TypeOf(innerData))
|
sc.name, p.Path(), reflect.TypeOf(innerData))
|
||||||
|
@ -268,6 +327,7 @@ func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(bep) we may have plain text inner templates.
|
||||||
data.Inner = template.HTML(newInner)
|
data.Inner = template.HTML(newInner)
|
||||||
} else {
|
} else {
|
||||||
data.Inner = template.HTML(inner)
|
data.Inner = template.HTML(inner)
|
||||||
|
@ -278,51 +338,91 @@ func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
|
||||||
return renderShortcodeWithPage(tmpl, data)
|
return renderShortcodeWithPage(tmpl, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *shortcodeHandler) extractAndRenderShortcodes(stringToParse string, p *Page) (string, error) {
|
// The delta represents new output format-versions of the shortcodes,
|
||||||
content, err := s.extractShortcodes(stringToParse, p)
|
// which, combined with the ones that do not have alternative representations,
|
||||||
|
// builds a complete set ready for a full rebuild of the Page content.
|
||||||
|
// This method returns false if there are no new shortcode variants in the
|
||||||
|
// current rendering context's output format. This mean we can safely reuse
|
||||||
|
// the content from the previous output format, if any.
|
||||||
|
func (s *shortcodeHandler) updateDelta() bool {
|
||||||
|
s.init.Do(func() {
|
||||||
|
s.contentShortcodes = createShortcodeRenderers(s.shortcodes, s.p)
|
||||||
|
})
|
||||||
|
|
||||||
if err != nil {
|
contentShortcodes := s.contentShortcodesForOutputFormat(s.p.s.rc.Format)
|
||||||
// try to render what we have whilst logging the error
|
|
||||||
p.s.Log.ERROR.Println(err.Error())
|
if s.contentShortcodesDelta == nil || len(s.contentShortcodesDelta) == 0 {
|
||||||
|
s.contentShortcodesDelta = contentShortcodes
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
s.contentShortCodes = renderShortcodes(s.shortcodes, p)
|
delta := make(map[scKey]func() (string, error))
|
||||||
|
|
||||||
return content, err
|
for k, v := range contentShortcodes {
|
||||||
|
if _, found := s.contentShortcodesDelta[k]; !found {
|
||||||
|
delta[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.contentShortcodesDelta = delta
|
||||||
|
|
||||||
|
return len(delta) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var emptyShortcodeFn = func() (string, error) { return "", nil }
|
func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) {
|
||||||
|
contentShortcodesForOuputFormat := make(map[scKey]func() (string, error))
|
||||||
|
for shortcodePlaceholder := range s.shortcodes {
|
||||||
|
|
||||||
func executeShortcodeFuncMap(funcs map[string]func() (string, error)) (map[string]string, error) {
|
key := newScKeyFromOutputFormat(f, shortcodePlaceholder)
|
||||||
result := make(map[string]string)
|
renderFn, found := s.contentShortcodes[key]
|
||||||
|
|
||||||
for k, v := range funcs {
|
if !found {
|
||||||
s, err := v()
|
key.OutputFormat = ""
|
||||||
|
renderFn, found = s.contentShortcodes[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to HTML
|
||||||
|
if !found && key.Suffix != "html" {
|
||||||
|
key.Suffix = "html"
|
||||||
|
renderFn, found = s.contentShortcodes[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder))
|
||||||
|
}
|
||||||
|
contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentShortcodesForOuputFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *shortcodeHandler) executeShortcodesForDelta(p *Page) error {
|
||||||
|
|
||||||
|
for k, render := range s.contentShortcodesDelta {
|
||||||
|
renderedShortcode, err := render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to execute shortcode with key %s: %s", k, err)
|
return fmt.Errorf("Failed to execute shortcode in page %q: %s", p.Path(), err)
|
||||||
}
|
}
|
||||||
result[k] = s
|
|
||||||
|
s.renderedShortcodes[k.ShortcodePlaceholder] = renderedShortcode
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderShortcodes(shortcodes map[string]shortcode, p *Page) map[string]func() (string, error) {
|
func createShortcodeRenderers(shortcodes map[string]shortcode, p *Page) map[scKey]func() (string, error) {
|
||||||
|
|
||||||
renderedShortcodes := make(map[string]func() (string, error))
|
shortcodeRenderers := make(map[scKey]func() (string, error))
|
||||||
|
|
||||||
for key, sc := range shortcodes {
|
for k, v := range shortcodes {
|
||||||
if sc.err != nil {
|
prepared := prepareShortcodeForPage(k, v, nil, p)
|
||||||
// need to have something to replace with
|
for kk, vv := range prepared {
|
||||||
renderedShortcodes[key] = emptyShortcodeFn
|
shortcodeRenderers[kk] = vv
|
||||||
} else {
|
|
||||||
shortcode := sc
|
|
||||||
renderedShortcodes[key] = func() (string, error) { return renderShortcode(shortcode, nil, p), nil }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderedShortcodes
|
return shortcodeRenderers
|
||||||
}
|
}
|
||||||
|
|
||||||
var errShortCodeIllegalState = errors.New("Illegal shortcode state")
|
var errShortCodeIllegalState = errors.New("Illegal shortcode state")
|
||||||
|
@ -395,7 +495,9 @@ Loop:
|
||||||
sc.inner = append(sc.inner, currItem.val)
|
sc.inner = append(sc.inner, currItem.val)
|
||||||
case tScName:
|
case tScName:
|
||||||
sc.name = currItem.val
|
sc.name = currItem.val
|
||||||
tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
|
// We pick the first template for an arbitrary output format
|
||||||
|
// if more than one. It is "all inner or no inner".
|
||||||
|
tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl)
|
||||||
if tmpl == nil {
|
if tmpl == nil {
|
||||||
return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
|
return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
|
||||||
}
|
}
|
||||||
|
@ -566,17 +668,38 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin
|
||||||
return source, nil
|
return source, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getShortcodeTemplate(name string, t tpl.TemplateFinder) *tpl.TemplateAdapter {
|
func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.TemplateFinder) *tpl.TemplateAdapter {
|
||||||
isInnerShortcodeCache.RLock()
|
isInnerShortcodeCache.RLock()
|
||||||
defer isInnerShortcodeCache.RUnlock()
|
defer isInnerShortcodeCache.RUnlock()
|
||||||
|
|
||||||
if x := t.Lookup("shortcodes/" + name + ".html"); x != nil {
|
var names []string
|
||||||
return x
|
|
||||||
|
suffix := strings.ToLower(key.Suffix)
|
||||||
|
outFormat := strings.ToLower(key.OutputFormat)
|
||||||
|
|
||||||
|
if outFormat != "" && suffix != "" {
|
||||||
|
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix))
|
||||||
}
|
}
|
||||||
if x := t.Lookup("theme/shortcodes/" + name + ".html"); x != nil {
|
|
||||||
return x
|
if suffix != "" {
|
||||||
|
names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix))
|
||||||
}
|
}
|
||||||
return t.Lookup("_internal/shortcodes/" + name + ".html")
|
|
||||||
|
names = append(names, shortcodeName)
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
|
||||||
|
if x := t.Lookup("shortcodes/" + name); x != nil {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
if x := t.Lookup("theme/shortcodes/" + name); x != nil {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
if x := t.Lookup("_internal/shortcodes/" + name); x != nil {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string {
|
func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string {
|
||||||
|
|
|
@ -22,6 +22,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
|
"github.com/spf13/hugo/output"
|
||||||
|
|
||||||
|
"github.com/spf13/hugo/media"
|
||||||
|
|
||||||
"github.com/spf13/hugo/deps"
|
"github.com/spf13/hugo/deps"
|
||||||
"github.com/spf13/hugo/helpers"
|
"github.com/spf13/hugo/helpers"
|
||||||
"github.com/spf13/hugo/source"
|
"github.com/spf13/hugo/source"
|
||||||
|
@ -353,7 +361,7 @@ func TestExtractShortcodes(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
s := newShortcodeHandler()
|
s := newShortcodeHandler(p)
|
||||||
content, err := s.extractShortcodes(this.input, p)
|
content, err := s.extractShortcodes(this.input, p)
|
||||||
|
|
||||||
if b, ok := this.expect.(bool); ok && !b {
|
if b, ok := this.expect.(bool); ok && !b {
|
||||||
|
@ -563,6 +571,150 @@ tags:
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShortcodeMultipleOutputFormats(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
siteConfig := `
|
||||||
|
baseURL = "http://example.com/blog"
|
||||||
|
|
||||||
|
paginate = 1
|
||||||
|
|
||||||
|
disableKinds = ["section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"]
|
||||||
|
|
||||||
|
[outputs]
|
||||||
|
home = [ "HTML", "AMP", "Calendar" ]
|
||||||
|
page = [ "HTML", "AMP", "JSON" ]
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
pageTemplate := `---
|
||||||
|
title: "%s"
|
||||||
|
---
|
||||||
|
# Doc
|
||||||
|
|
||||||
|
{{< myShort >}}
|
||||||
|
{{< noExt >}}
|
||||||
|
{{%% onlyHTML %%}}
|
||||||
|
|
||||||
|
{{< myInner >}}{{< myShort >}}{{< /myInner >}}
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
pageTemplateCSVOnly := `---
|
||||||
|
title: "%s"
|
||||||
|
outputs: ["CSV"]
|
||||||
|
---
|
||||||
|
# Doc
|
||||||
|
|
||||||
|
CSV: {{< myShort >}}
|
||||||
|
`
|
||||||
|
|
||||||
|
pageTemplateShortcodeNotFound := `---
|
||||||
|
title: "%s"
|
||||||
|
outputs: ["CSV"]
|
||||||
|
---
|
||||||
|
# Doc
|
||||||
|
|
||||||
|
NotFound: {{< thisDoesNotExist >}}
|
||||||
|
`
|
||||||
|
|
||||||
|
mf := afero.NewMemMapFs()
|
||||||
|
|
||||||
|
th, h := newTestSitesFromConfig(t, mf, siteConfig,
|
||||||
|
"layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`,
|
||||||
|
"layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`,
|
||||||
|
"layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`,
|
||||||
|
"layouts/index.html", `Home HTML: {{ .Title }}|{{ .Content }}`,
|
||||||
|
"layouts/index.amp.html", `Home AMP: {{ .Title }}|{{ .Content }}`,
|
||||||
|
"layouts/index.ics", `Home Calendar: {{ .Title }}|{{ .Content }}`,
|
||||||
|
"layouts/shortcodes/myShort.html", `ShortHTML`,
|
||||||
|
"layouts/shortcodes/myShort.amp.html", `ShortAMP`,
|
||||||
|
"layouts/shortcodes/myShort.csv", `ShortCSV`,
|
||||||
|
"layouts/shortcodes/myShort.ics", `ShortCalendar`,
|
||||||
|
"layouts/shortcodes/myShort.json", `ShortJSON`,
|
||||||
|
"layouts/shortcodes/noExt", `ShortNoExt`,
|
||||||
|
"layouts/shortcodes/onlyHTML.html", `ShortOnlyHTML`,
|
||||||
|
"layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`,
|
||||||
|
)
|
||||||
|
|
||||||
|
fs := th.Fs
|
||||||
|
|
||||||
|
writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "Home"))
|
||||||
|
writeSource(t, fs, "content/sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"))
|
||||||
|
writeSource(t, fs, "content/sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"))
|
||||||
|
writeSource(t, fs, "content/sect/notfound.md", fmt.Sprintf(pageTemplateShortcodeNotFound, "Single CSV"))
|
||||||
|
|
||||||
|
require.NoError(t, h.Build(BuildCfg{}))
|
||||||
|
require.Len(t, h.Sites, 1)
|
||||||
|
|
||||||
|
s := h.Sites[0]
|
||||||
|
home := s.getPage(KindHome)
|
||||||
|
require.NotNil(t, home)
|
||||||
|
require.Len(t, home.outputFormats, 3)
|
||||||
|
|
||||||
|
th.assertFileContent("public/index.html",
|
||||||
|
"Home HTML",
|
||||||
|
"ShortHTML",
|
||||||
|
"ShortNoExt",
|
||||||
|
"ShortOnlyHTML",
|
||||||
|
"myInner:--ShortHTML--",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/amp/index.html",
|
||||||
|
"Home AMP",
|
||||||
|
"ShortAMP",
|
||||||
|
"ShortNoExt",
|
||||||
|
"ShortOnlyHTML",
|
||||||
|
"myInner:--ShortAMP--",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/index.ics",
|
||||||
|
"Home Calendar",
|
||||||
|
"ShortCalendar",
|
||||||
|
"ShortNoExt",
|
||||||
|
"ShortOnlyHTML",
|
||||||
|
"myInner:--ShortCalendar--",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/sect/mypage/index.html",
|
||||||
|
"Single HTML",
|
||||||
|
"ShortHTML",
|
||||||
|
"ShortNoExt",
|
||||||
|
"ShortOnlyHTML",
|
||||||
|
"myInner:--ShortHTML--",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/sect/mypage/index.json",
|
||||||
|
"Single JSON",
|
||||||
|
"ShortJSON",
|
||||||
|
"ShortNoExt",
|
||||||
|
"ShortOnlyHTML",
|
||||||
|
"myInner:--ShortJSON--",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/amp/sect/mypage/index.html",
|
||||||
|
// No special AMP template
|
||||||
|
"Single HTML",
|
||||||
|
"ShortAMP",
|
||||||
|
"ShortNoExt",
|
||||||
|
"ShortOnlyHTML",
|
||||||
|
"myInner:--ShortAMP--",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/sect/mycsvpage/index.csv",
|
||||||
|
"Single CSV",
|
||||||
|
"ShortCSV",
|
||||||
|
)
|
||||||
|
|
||||||
|
th.assertFileContent("public/sect/notfound/index.csv",
|
||||||
|
"NotFound:",
|
||||||
|
"thisDoesNotExist",
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func collectAndSortShortcodes(shortcodes map[string]shortcode) []string {
|
func collectAndSortShortcodes(shortcodes map[string]shortcode) []string {
|
||||||
var asArray []string
|
var asArray []string
|
||||||
|
|
||||||
|
@ -681,3 +833,13 @@ func TestReplaceShortcodeTokens(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScKey(t *testing.T) {
|
||||||
|
require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"},
|
||||||
|
newScKey(media.XMLType, "ABCD"))
|
||||||
|
require.Equal(t, scKey{Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"},
|
||||||
|
newScKeyFromOutputFormat(output.AMPFormat, "EFGH"))
|
||||||
|
require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"},
|
||||||
|
newDefaultScKey("IJKL"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -99,6 +99,8 @@ title: "%s"
|
||||||
outputs: %s
|
outputs: %s
|
||||||
---
|
---
|
||||||
# Doc
|
# Doc
|
||||||
|
|
||||||
|
{{< myShort >}}
|
||||||
`
|
`
|
||||||
|
|
||||||
mf := afero.NewMemMapFs()
|
mf := afero.NewMemMapFs()
|
||||||
|
@ -118,6 +120,8 @@ other = "Olboge"
|
||||||
"layouts/partials/GoHugo.html", `Go Hugo Partial`,
|
"layouts/partials/GoHugo.html", `Go Hugo Partial`,
|
||||||
"layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`,
|
"layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`,
|
||||||
"layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`,
|
"layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`,
|
||||||
|
"layouts/shortcodes/myShort.html", `ShortHTML`,
|
||||||
|
"layouts/shortcodes/myShort.json", `ShortJSON`,
|
||||||
|
|
||||||
"layouts/_default/list.json", `{{ define "main" }}
|
"layouts/_default/list.json", `{{ define "main" }}
|
||||||
List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}|
|
List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}|
|
||||||
|
@ -141,6 +145,7 @@ List HTML|{{.Title }}|
|
||||||
{{ .Site.Language.Lang }}: {{ T "elbow" -}}
|
{{ .Site.Language.Lang }}: {{ T "elbow" -}}
|
||||||
Partial Hugo 1: {{ partial "GoHugo.html" . }}
|
Partial Hugo 1: {{ partial "GoHugo.html" . }}
|
||||||
Partial Hugo 2: {{ partial "GoHugo" . -}}
|
Partial Hugo 2: {{ partial "GoHugo" . -}}
|
||||||
|
Content: {{ .Content }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
|
@ -180,6 +185,7 @@ Partial Hugo 2: {{ partial "GoHugo" . -}}
|
||||||
"Output/Rel: JSON/alternate|",
|
"Output/Rel: JSON/alternate|",
|
||||||
"Output/Rel: HTML/canonical|",
|
"Output/Rel: HTML/canonical|",
|
||||||
"en: Elbow",
|
"en: Elbow",
|
||||||
|
"ShortJSON",
|
||||||
)
|
)
|
||||||
|
|
||||||
th.assertFileContent("public/index.html",
|
th.assertFileContent("public/index.html",
|
||||||
|
@ -187,6 +193,7 @@ Partial Hugo 2: {{ partial "GoHugo" . -}}
|
||||||
// parsed with html/template.
|
// parsed with html/template.
|
||||||
`List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html+html" />`,
|
`List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html+html" />`,
|
||||||
"en: Elbow",
|
"en: Elbow",
|
||||||
|
"ShortHTML",
|
||||||
)
|
)
|
||||||
th.assertFileContent("public/nn/index.html",
|
th.assertFileContent("public/nn/index.html",
|
||||||
"List HTML|JSON Nynorsk Heim|",
|
"List HTML|JSON Nynorsk Heim|",
|
||||||
|
@ -196,10 +203,12 @@ Partial Hugo 2: {{ partial "GoHugo" . -}}
|
||||||
"Output/Rel: JSON/canonical|",
|
"Output/Rel: JSON/canonical|",
|
||||||
// JSON is plain text, so no need to safeHTML this and that
|
// JSON is plain text, so no need to safeHTML this and that
|
||||||
`<atom:link href=http://example.com/blog/index.json rel="self" type="application/json+json" />`,
|
`<atom:link href=http://example.com/blog/index.json rel="self" type="application/json+json" />`,
|
||||||
|
"ShortJSON",
|
||||||
)
|
)
|
||||||
th.assertFileContent("public/nn/index.json",
|
th.assertFileContent("public/nn/index.json",
|
||||||
"List JSON|JSON Nynorsk Heim|",
|
"List JSON|JSON Nynorsk Heim|",
|
||||||
"nn: Olboge",
|
"nn: Olboge",
|
||||||
|
"ShortJSON",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,6 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
pageOutput, err = newPageOutput(page, false, outFormat)
|
pageOutput, err = newPageOutput(page, false, outFormat)
|
||||||
page.mainPageOutput = pageOutput
|
page.mainPageOutput = pageOutput
|
||||||
} else {
|
|
||||||
pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if outFormat != page.s.rc.Format {
|
if outFormat != page.s.rc.Format {
|
||||||
|
@ -86,6 +84,10 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pageOutput == nil {
|
||||||
|
pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err)
|
s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -58,6 +58,11 @@ type TemplateExecutor interface {
|
||||||
Tree() string
|
Tree() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemplateDebugger prints some debug info to stdoud.
|
||||||
|
type TemplateDebugger interface {
|
||||||
|
Debug()
|
||||||
|
}
|
||||||
|
|
||||||
// TemplateAdapter implements the TemplateExecutor interface.
|
// TemplateAdapter implements the TemplateExecutor interface.
|
||||||
type TemplateAdapter struct {
|
type TemplateAdapter struct {
|
||||||
Template
|
Template
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
package tplimpl
|
package tplimpl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
texttemplate "text/template"
|
texttemplate "text/template"
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ tpl.TemplateHandler = (*templateHandler)(nil)
|
_ tpl.TemplateHandler = (*templateHandler)(nil)
|
||||||
|
_ tpl.TemplateDebugger = (*templateHandler)(nil)
|
||||||
_ tpl.TemplateFuncsGetter = (*templateHandler)(nil)
|
_ tpl.TemplateFuncsGetter = (*templateHandler)(nil)
|
||||||
_ tpl.TemplateTestMocker = (*templateHandler)(nil)
|
_ tpl.TemplateTestMocker = (*templateHandler)(nil)
|
||||||
_ tpl.TemplateFinder = (*htmlTemplates)(nil)
|
_ tpl.TemplateFinder = (*htmlTemplates)(nil)
|
||||||
|
@ -88,6 +91,11 @@ func (t *templateHandler) addError(name string, err error) {
|
||||||
t.errors = append(t.errors, &templateErr{name, err})
|
t.errors = append(t.errors, &templateErr{name, err})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *templateHandler) Debug() {
|
||||||
|
fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates())
|
||||||
|
fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates())
|
||||||
|
}
|
||||||
|
|
||||||
// PrintErrors prints the accumulated errors as ERROR to the log.
|
// PrintErrors prints the accumulated errors as ERROR to the log.
|
||||||
func (t *templateHandler) PrintErrors() {
|
func (t *templateHandler) PrintErrors() {
|
||||||
for _, e := range t.errors {
|
for _, e := range t.errors {
|
||||||
|
@ -293,6 +301,13 @@ func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(name, "shortcodes") {
|
||||||
|
// We need to keep track of one ot the output format's shortcode template
|
||||||
|
// without knowing the rendering context.
|
||||||
|
withoutExt := strings.TrimSuffix(name, path.Ext(name))
|
||||||
|
tt.AddParseTree(withoutExt, templ.Tree)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,6 +330,13 @@ func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl strin
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(name, "shortcodes") {
|
||||||
|
// We need to keep track of one ot the output format's shortcode template
|
||||||
|
// without knowing the rendering context.
|
||||||
|
withoutExt := strings.TrimSuffix(name, path.Ext(name))
|
||||||
|
tt.AddParseTree(withoutExt, templ.Tree)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue