hugolib: Handle shortcode per output format
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 6 May 2017 18:15:28 +0000 (20:15 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 13 May 2017 19:44:15 +0000 (22:44 +0300)
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

hugolib/handler_page.go
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/page.go
hugolib/shortcode.go
hugolib/shortcode_test.go
hugolib/site_output_test.go
hugolib/site_render.go
tpl/template.go
tpl/tplimpl/template.go

index a7dbff2e608a690f0974f0cea3b61d9a8001f58d..70266fb2190527c21e7719f0f3de730c54bb966c 100644 (file)
@@ -80,7 +80,7 @@ func (h htmlHandler) PageConvert(p *Page) HandledResult {
        p.createWorkContentCopy()
 
        if err := p.processShortcodes(); err != nil {
-               return HandledResult{err: err}
+               p.s.Log.ERROR.Println(err)
        }
 
        return HandledResult{err: nil}
@@ -131,7 +131,7 @@ func commonConvert(p *Page) HandledResult {
        p.createWorkContentCopy()
 
        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
index 8a8fc2223a78d359ff8230dd283d7c32c8750cd2..6e7034bd2bec2edaf9828b368aa25f50eb424f12 100644 (file)
@@ -492,12 +492,7 @@ func (h *HugoSites) setupTranslations() {
        }
 }
 
-func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
-
-       if outFormatIdx > 0 {
-               // TODO(bep) for now
-               return
-       }
+func (s *Site) preparePagesForRender(cfg *BuildCfg) {
 
        pageChan := make(chan *Page)
        wg := &sync.WaitGroup{}
@@ -508,8 +503,16 @@ func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
                go func(pages <-chan *Page, wg *sync.WaitGroup) {
                        defer wg.Done()
                        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.
                                        continue
                                }
@@ -521,10 +524,12 @@ func (s *Site) preparePagesForRender(outFormatIdx int, cfg *BuildCfg) {
                                // Mark it as rendered
                                p.rendered = true
 
-                               // If in watch mode, we need to keep the original so we can
-                               // repeat this process on rebuild.
+                               // If in watch mode or if we have multiple output formats,
+                               // 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
-                               if cfg.Watching {
+                               if needsACopy {
                                        workContentCopy = make([]byte, len(p.workContent))
                                        copy(workContentCopy, p.workContent)
                                } else {
@@ -589,15 +594,15 @@ func (h *HugoSites) Pages() Pages {
 }
 
 func handleShortcodes(p *Page, rawContentCopy []byte) ([]byte, error) {
-       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())
-               shortcodes, err := executeShortcodeFuncMap(p.shortcodeState.contentShortCodes)
+       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())
+               err := p.shortcodeState.executeShortcodesForDelta(p)
 
                if err != nil {
                        return rawContentCopy, err
                }
 
-               rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, shortcodes)
+               rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, p.shortcodeState.renderedShortcodes)
 
                if err != nil {
                        p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error())
index 12689f6de0aaa668523e29534d2e5022e5e7396f..61e2ac337486cb3a309da0ac2cc40ca9f18448a5 100644 (file)
@@ -213,7 +213,7 @@ func (h *HugoSites) render(config *BuildCfg) error {
                s.initRenderFormats()
                for i, rf := range s.renderFormats {
                        s.rc = &siteRenderingContext{Format: rf}
-                       s.preparePagesForRender(i, config)
+                       s.preparePagesForRender(config)
 
                        if !config.SkipRender {
                                if err := s.render(i); err != nil {
index 21603c3dcf98d9f2bf5eab5a58c753e8320a8465..8de4ad924f4e80abd693c269595c640a028d3293 100644 (file)
@@ -1257,6 +1257,11 @@ func (p *Page) Menus() 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 {
        // Try markup explicitly set in the frontmatter
        p.Markup = helpers.GuessType(p.Markup)
@@ -1372,8 +1377,8 @@ func (p *Page) SaveSource() error {
 }
 
 func (p *Page) processShortcodes() error {
-       p.shortcodeState = newShortcodeHandler()
-       tmpContent, err := p.shortcodeState.extractAndRenderShortcodes(string(p.workContent), p)
+       p.shortcodeState = newShortcodeHandler(p)
+       tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p)
        if err != nil {
                return err
        }
index abe445d71dcabcb9e936b844534dea1625c911a4..963ebd0b52e752aa2dc3c236da3aad2f2ceedb43 100644 (file)
@@ -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");
 // you may not use this file except in compliance with the License.
@@ -24,6 +24,10 @@ import (
        "strings"
        "sync"
 
+       "github.com/spf13/hugo/output"
+
+       "github.com/spf13/hugo/media"
+
        bp "github.com/spf13/hugo/bufferpool"
        "github.com/spf13/hugo/helpers"
        "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)
 }
 
+// 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 {
-       // Maps the shortcodeplaceholder with the shortcode rendering func.
-       contentShortCodes map[string]func() (string, error)
+       init sync.Once
+
+       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.
        shortcodes map[string]shortcode
@@ -160,11 +198,13 @@ type shortcodeHandler struct {
        nameSet map[string]bool
 }
 
-func newShortcodeHandler() *shortcodeHandler {
+func newShortcodeHandler(p *Page) *shortcodeHandler {
        return &shortcodeHandler{
-               contentShortCodes: make(map[string]func() (string, error)),
-               shortcodes:        make(map[string]shortcode),
-               nameSet:           make(map[string]bool),
+               p:                  p,
+               contentShortcodes:  make(map[scKey]func() (string, error)),
+               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 innerCleanupExpand = "$1"
 
-func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
-       tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
+func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) {
+
+       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 {
-               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 ""
        }
 
@@ -228,7 +287,7 @@ func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
                        case string:
                                inner += innerData.(string)
                        case shortcode:
-                               inner += renderShortcode(innerData.(shortcode), data, p)
+                               inner += renderShortcode(tmplKey, innerData.(shortcode), data, p)
                        default:
                                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))
@@ -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)
                } else {
                        data.Inner = template.HTML(inner)
@@ -278,51 +338,91 @@ func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page) string {
        return renderShortcodeWithPage(tmpl, data)
 }
 
-func (s *shortcodeHandler) extractAndRenderShortcodes(stringToParse string, p *Page) (string, error) {
-       content, err := s.extractShortcodes(stringToParse, p)
-
-       if err != nil {
-               //  try to render what we have whilst logging the error
-               p.s.Log.ERROR.Println(err.Error())
+// The delta represents new output format-versions of the shortcodes,
+// 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)
+       })
+
+       contentShortcodes := s.contentShortcodesForOutputFormat(s.p.s.rc.Format)
+
+       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))
+
+       for k, v := range contentShortcodes {
+               if _, found := s.contentShortcodesDelta[k]; !found {
+                       delta[k] = v
+               }
+       }
 
-       return content, err
+       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) {
-       result := make(map[string]string)
+               key := newScKeyFromOutputFormat(f, shortcodePlaceholder)
+               renderFn, found := s.contentShortcodes[key]
 
-       for k, v := range funcs {
-               s, err := v()
+               if !found {
+                       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 {
-                       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 {
-               if sc.err != nil {
-                       // need to have something to replace with
-                       renderedShortcodes[key] = emptyShortcodeFn
-               } else {
-                       shortcode := sc
-                       renderedShortcodes[key] = func() (string, error) { return renderShortcode(shortcode, nil, p), nil }
+       for k, v := range shortcodes {
+               prepared := prepareShortcodeForPage(k, v, nil, p)
+               for kk, vv := range prepared {
+                       shortcodeRenderers[kk] = vv
                }
        }
 
-       return renderedShortcodes
+       return shortcodeRenderers
 }
 
 var errShortCodeIllegalState = errors.New("Illegal shortcode state")
@@ -395,7 +495,9 @@ Loop:
                        sc.inner = append(sc.inner, currItem.val)
                case tScName:
                        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 {
                                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
 }
 
-func getShortcodeTemplate(name string, t tpl.TemplateFinder) *tpl.TemplateAdapter {
+func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.TemplateFinder) *tpl.TemplateAdapter {
        isInnerShortcodeCache.RLock()
        defer isInnerShortcodeCache.RUnlock()
 
-       if x := t.Lookup("shortcodes/" + name + ".html"); x != nil {
-               return x
+       var names []string
+
+       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))
+       }
+
+       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 t.Lookup("_internal/shortcodes/" + name + ".html")
+       return nil
 }
 
 func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string {
index 3d192246270148bf9d0d214183b93cc63ae0645b..2c1e887c9aeb6a6af80c63f6bbcff4f27e912963 100644 (file)
@@ -22,6 +22,14 @@ import (
        "strings"
        "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/helpers"
        "github.com/spf13/hugo/source"
@@ -353,7 +361,7 @@ func TestExtractShortcodes(t *testing.T) {
                        return nil
                })
 
-               s := newShortcodeHandler()
+               s := newShortcodeHandler(p)
                content, err := s.extractShortcodes(this.input, p)
 
                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 {
        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"))
+
+}
index ef2cdf9ad173f75b432ab904d70044071a96dfc5..2935f32576166815f0ca6d2208c1620f2a380d54 100644 (file)
@@ -99,6 +99,8 @@ title: "%s"
 outputs: %s
 ---
 # Doc
+
+{{< myShort >}}
 `
 
        mf := afero.NewMemMapFs()
@@ -118,6 +120,8 @@ other = "Olboge"
                "layouts/partials/GoHugo.html", `Go Hugo Partial`,
                "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/shortcodes/myShort.html", `ShortHTML`,
+               "layouts/shortcodes/myShort.json", `ShortJSON`,
 
                "layouts/_default/list.json", `{{ define "main" }}
 List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}|
@@ -141,6 +145,7 @@ List HTML|{{.Title }}|
 {{ .Site.Language.Lang }}: {{ T "elbow" -}}
 Partial Hugo 1: {{ partial "GoHugo.html" . }}
 Partial Hugo 2: {{ partial "GoHugo" . -}}
+Content: {{ .Content }}
 {{ end }}
 `,
        )
@@ -180,6 +185,7 @@ Partial Hugo 2: {{ partial "GoHugo" . -}}
                        "Output/Rel: JSON/alternate|",
                        "Output/Rel: HTML/canonical|",
                        "en: Elbow",
+                       "ShortJSON",
                )
 
                th.assertFileContent("public/index.html",
@@ -187,6 +193,7 @@ Partial Hugo 2: {{ partial "GoHugo" . -}}
                        // parsed with html/template.
                        `List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html&#43;html" />`,
                        "en: Elbow",
+                       "ShortHTML",
                )
                th.assertFileContent("public/nn/index.html",
                        "List HTML|JSON Nynorsk Heim|",
@@ -196,10 +203,12 @@ Partial Hugo 2: {{ partial "GoHugo" . -}}
                        "Output/Rel: JSON/canonical|",
                        // 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" />`,
+                       "ShortJSON",
                )
                th.assertFileContent("public/nn/index.json",
                        "List JSON|JSON Nynorsk Heim|",
                        "nn: Olboge",
+                       "ShortJSON",
                )
        }
 
index ac7b25da7da2b2882a257d7113fca2a732479a02..5bb2401614a50e5352d2256b06bdc61180ac0b8f 100644 (file)
@@ -77,8 +77,6 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa
                        if i == 0 {
                                pageOutput, err = newPageOutput(page, false, outFormat)
                                page.mainPageOutput = pageOutput
-                       } else {
-                               pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
                        }
 
                        if outFormat != page.s.rc.Format {
@@ -86,6 +84,10 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa
                                continue
                        }
 
+                       if pageOutput == nil {
+                               pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat)
+                       }
+
                        if err != nil {
                                s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err)
                                continue
index 9fbf6b7b88682e5adf3b27086e64fc24e1cf5d99..aa46a8ac2a1babba232bb6a7e7a78052655ce87e 100644 (file)
@@ -58,6 +58,11 @@ type TemplateExecutor interface {
        Tree() string
 }
 
+// TemplateDebugger prints some debug info to stdoud.
+type TemplateDebugger interface {
+       Debug()
+}
+
 // TemplateAdapter implements the TemplateExecutor interface.
 type TemplateAdapter struct {
        Template
index 77826e0b085e977bba82237c1e0917faddeda0fc..ae1ae6820f5e5a59e0528a12346cba3f2e570402 100644 (file)
@@ -14,7 +14,9 @@
 package tplimpl
 
 import (
+       "fmt"
        "html/template"
+       "path"
        "strings"
        texttemplate "text/template"
 
@@ -39,6 +41,7 @@ const (
 
 var (
        _ tpl.TemplateHandler       = (*templateHandler)(nil)
+       _ tpl.TemplateDebugger      = (*templateHandler)(nil)
        _ tpl.TemplateFuncsGetter   = (*templateHandler)(nil)
        _ tpl.TemplateTestMocker    = (*templateHandler)(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})
 }
 
+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.
 func (t *templateHandler) PrintErrors() {
        for _, e := range t.errors {
@@ -293,6 +301,13 @@ func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) e
                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
 }
 
@@ -315,6 +330,13 @@ func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl strin
                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
 }