diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-05-06 20:15:28 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-05-13 22:44:15 +0300 |
commit | af72db806f2c1c0bf1dfe5832275c41eeba89906 (patch) | |
tree | a1bc9c7d09836073a811a1b395b7756727fdb210 /hugolib/shortcode.go | |
parent | e951d65771ca299aa899e91bfe00411a5ada8f19 (diff) |
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
Diffstat (limited to 'hugolib/shortcode.go')
-rw-r--r-- | hugolib/shortcode.go | 211 |
1 files changed, 167 insertions, 44 deletions
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index abe445d71..963ebd0b5 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -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 { |