diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-11-26 11:01:27 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-11-27 16:14:09 +0100 |
commit | bc337e6ab5a75f1f1bfe3a83f3786d0afdb6346c (patch) | |
tree | 1f3b087822337acbde696147f18e86b2c9f1d8eb /hugolib | |
parent | 112461fded0d7970817ce7bf476c4763922ad314 (diff) |
Add inline shortcode support
An inline shortcode's name must end with `.inline`, all lowercase.
E.g.:
```bash
{{< time.inline >}}{{ now }}{{< /time.inline >}}
```
The above will print the current date and time.
Note that an inline shortcode's inner content is parsed and executed as a Go text template with the same context as a regular shortcode template.
This means that the current page can be accessed via `.Page.Title` etc. This also means that there are no concept of "nested inline shortcodes".
The same inline shortcode can be reused later in the same content file, with different params if needed, using the self-closing syntax:
```
{{< time.inline />}}
```
Fixes #4011
Diffstat (limited to 'hugolib')
-rw-r--r-- | hugolib/config.go | 2 | ||||
-rw-r--r-- | hugolib/shortcode.go | 116 | ||||
-rw-r--r-- | hugolib/shortcode_test.go | 50 | ||||
-rw-r--r-- | hugolib/site.go | 56 |
4 files changed, 172 insertions, 52 deletions
diff --git a/hugolib/config.go b/hugolib/config.go index 388069047..77ebb42ae 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -482,6 +482,6 @@ func loadDefaultSettingsFor(v *viper.Viper) error { v.SetDefault("debug", false) v.SetDefault("disableFastRender", false) v.SetDefault("timeout", 10000) // 10 seconds - + v.SetDefault("enableInlineShortcodes", false) return nil } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 1860a5e90..8be312f83 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -18,6 +18,9 @@ import ( "errors" "fmt" "html/template" + "path" + + "github.com/gohugoio/hugo/common/herrors" "reflect" @@ -163,13 +166,15 @@ func (scp *ShortcodeWithPage) page() *Page { const shortcodePlaceholderPrefix = "HUGOSHORTCODE" type shortcode struct { - name string - inner []interface{} // string or nested shortcode - params interface{} // map or array - ordinal int - err error - doMarkup bool - pos int // the position in bytes in the source file + name string + isInline bool // inline shortcode. Any inner will be a Go template. + isClosing bool // whether a closing tag was provided + inner []interface{} // string or nested shortcode + params interface{} // map or array + ordinal int + err error + doMarkup bool + pos int // the position in bytes in the source file } func (sc shortcode) String() string { @@ -245,6 +250,8 @@ type shortcodeHandler struct { placeholderID int placeholderFunc func() string + + enableInlineShortcodes bool } func (s *shortcodeHandler) nextPlaceholderID() int { @@ -259,11 +266,12 @@ func (s *shortcodeHandler) createShortcodePlaceholder() string { func newShortcodeHandler(p *Page) *shortcodeHandler { s := &shortcodeHandler{ - p: p.withoutContent(), - contentShortcodes: newOrderedMap(), - shortcodes: newOrderedMap(), - nameSet: make(map[string]bool), - renderedShortcodes: make(map[string]string), + p: p.withoutContent(), + enableInlineShortcodes: p.s.enableInlineShortcodes, + contentShortcodes: newOrderedMap(), + shortcodes: newOrderedMap(), + nameSet: make(map[string]bool), + renderedShortcodes: make(map[string]string), } placeholderFunc := p.s.shortcodePlaceholderFunc @@ -313,11 +321,26 @@ const innerNewlineRegexp = "\n" const innerCleanupRegexp = `\A<p>(.*)</p>\n\z` const innerCleanupExpand = "$1" -func prepareShortcodeForPage(placeholder string, sc *shortcode, parent *ShortcodeWithPage, p *PageWithoutContent) map[scKey]func() (string, error) { - +func (s *shortcodeHandler) prepareShortcodeForPage(placeholder string, sc *shortcode, parent *ShortcodeWithPage, p *PageWithoutContent) map[scKey]func() (string, error) { m := make(map[scKey]func() (string, error)) lang := p.Lang() + if sc.isInline { + key := newScKeyFromLangAndOutputFormat(lang, p.outputFormats[0], placeholder) + if !s.enableInlineShortcodes { + m[key] = func() (string, error) { + return "", nil + } + } else { + m[key] = func() (string, error) { + return renderShortcode(key, sc, nil, p) + } + } + + return m + + } + for _, f := range p.outputFormats { // The most specific template will win. key := newScKeyFromLangAndOutputFormat(lang, f, placeholder) @@ -335,7 +358,34 @@ func renderShortcode( parent *ShortcodeWithPage, p *PageWithoutContent) (string, error) { - tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl) + var tmpl tpl.Template + + if sc.isInline { + templName := path.Join("_inline_shortcode", p.Path(), sc.name) + if sc.isClosing { + templStr := sc.inner[0].(string) + + var err error + tmpl, err = p.s.TextTmpl.Parse(templName, templStr) + if err != nil { + fe := herrors.ToFileError("html", err) + l1, l2 := p.posFromPage(sc.pos).LineNumber, fe.Position().LineNumber + fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1) + return "", p.errWithFileContext(fe) + } + + } else { + // Re-use of shortcode defined earlier in the same page. + var found bool + tmpl, found = p.s.TextTmpl.Lookup(templName) + if !found { + return "", _errors.Errorf("no earlier definition of shortcode %q found", sc.name) + } + } + } else { + tmpl = getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl) + } + if tmpl == nil { p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) return "", nil @@ -406,7 +456,16 @@ func renderShortcode( } - return renderShortcodeWithPage(tmpl, data) + s, err := renderShortcodeWithPage(tmpl, data) + + if err != nil && sc.isInline { + fe := herrors.ToFileError("html", err) + l1, l2 := p.posFromPage(sc.pos).LineNumber, fe.Position().LineNumber + fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1) + return "", fe + } + + return s, err } // The delta represents new output format-versions of the shortcodes, @@ -417,7 +476,7 @@ func renderShortcode( // 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.withoutContent()) + s.contentShortcodes = s.createShortcodeRenderers(s.p.withoutContent()) }) if !s.p.shouldRenderTo(s.p.s.rc.Format) { @@ -505,13 +564,13 @@ func (s *shortcodeHandler) executeShortcodesForDelta(p *PageWithoutContent) erro } -func createShortcodeRenderers(shortcodes *orderedMap, p *PageWithoutContent) *orderedMap { +func (s *shortcodeHandler) createShortcodeRenderers(p *PageWithoutContent) *orderedMap { shortcodeRenderers := newOrderedMap() - for _, k := range shortcodes.Keys() { - v := shortcodes.getShortcode(k) - prepared := prepareShortcodeForPage(k.(string), v, nil, p) + for _, k := range s.shortcodes.Keys() { + v := s.shortcodes.getShortcode(k) + prepared := s.prepareShortcodeForPage(k.(string), v, nil, p) for kk, vv := range prepared { shortcodeRenderers.Add(kk, vv) } @@ -541,7 +600,9 @@ Loop: currItem := pt.Next() switch { case currItem.IsLeftShortcodeDelim(): - sc.pos = currItem.Pos + if sc.pos == 0 { + sc.pos = currItem.Pos + } next := pt.Peek() if next.IsShortcodeClose() { continue @@ -570,13 +631,13 @@ Loop: case currItem.IsRightShortcodeDelim(): // we trust the template on this: // if there's no inner, we're done - if !isInner { + if !sc.isInline && !isInner { return sc, nil } case currItem.IsShortcodeClose(): next := pt.Peek() - if !isInner { + if !sc.isInline && !isInner { if next.IsError() { // return that error, more specific continue @@ -588,6 +649,7 @@ Loop: // self-closing pt.Consume(1) } else { + sc.isClosing = true pt.Consume(2) } @@ -609,6 +671,10 @@ Loop: return sc, fail(_errors.Wrapf(err, "failed to handle template for shortcode %q", sc.name), currItem) } + case currItem.IsInlineShortcodeName(): + sc.name = currItem.ValStr() + sc.isInline = true + case currItem.IsShortcodeParam(): if !pt.IsValueNext() { continue @@ -751,7 +817,7 @@ func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string err := tmpl.Execute(buffer, data) isInnerShortcodeCache.RUnlock() if err != nil { - return "", data.Page.errorf(err, "failed to process shortcode") + return "", _errors.Wrap(err, "failed to process shortcode") } return buffer.String(), nil } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 30fdbead3..3a1656e26 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -1062,3 +1062,53 @@ String: {{ . | safeHTML }} ) } + +func TestInlineShortcodes(t *testing.T) { + for _, enableInlineShortcodes := range []bool{true, false} { + t.Run(fmt.Sprintf("enableInlineShortcodes=%t", enableInlineShortcodes), + func(t *testing.T) { + conf := fmt.Sprintf(` +baseURL = "https://example.com" +enableInlineShortcodes = %t +`, enableInlineShortcodes) + + b := newTestSitesBuilder(t) + b.WithConfigFile("toml", conf) + b.WithContent("page-md-shortcode.md", `--- +title: "Hugo" +--- + +FIRST:{{< myshort.inline "first" >}} +Page: {{ .Page.Title }} +Seq: {{ seq 3 }} +Param: {{ .Get 0 }} +{{< /myshort.inline >}}:END: + +SECOND:{{< myshort.inline "second" />}}:END + +`) + + b.WithTemplatesAdded("layouts/_default/single.html", ` +CONTENT:{{ .Content }} +`) + + b.CreateSites().Build(BuildCfg{}) + + if enableInlineShortcodes { + b.AssertFileContent("public/page-md-shortcode/index.html", + "Page: Hugo", + "Seq: [1 2 3]", + "Param: first", + "Param: second", + ) + } else { + b.AssertFileContent("public/page-md-shortcode/index.html", + "FIRST::END", + "SECOND::END", + ) + } + + }) + + } +} diff --git a/hugolib/site.go b/hugolib/site.go index fb32853e3..25eb34f05 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -124,6 +124,8 @@ type Site struct { disabledKinds map[string]bool + enableInlineShortcodes bool + // Output formats defined in site config per Page Kind, or some defaults // if not set. // Output formats defined in Page front matter will override these. @@ -194,21 +196,22 @@ func (s *Site) isEnabled(kind string) bool { // reset returns a new Site prepared for rebuild. func (s *Site) reset() *Site { return &Site{Deps: s.Deps, - layoutHandler: output.NewLayoutHandler(), - disabledKinds: s.disabledKinds, - titleFunc: s.titleFunc, - relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg), - siteRefLinker: s.siteRefLinker, - outputFormats: s.outputFormats, - rc: s.rc, - outputFormatsConfig: s.outputFormatsConfig, - frontmatterHandler: s.frontmatterHandler, - mediaTypesConfig: s.mediaTypesConfig, - Language: s.Language, - owner: s.owner, - publisher: s.publisher, - siteConfig: s.siteConfig, - PageCollections: newPageCollections()} + layoutHandler: output.NewLayoutHandler(), + disabledKinds: s.disabledKinds, + titleFunc: s.titleFunc, + relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg), + siteRefLinker: s.siteRefLinker, + outputFormats: s.outputFormats, + rc: s.rc, + outputFormatsConfig: s.outputFormatsConfig, + frontmatterHandler: s.frontmatterHandler, + mediaTypesConfig: s.mediaTypesConfig, + Language: s.Language, + owner: s.owner, + publisher: s.publisher, + siteConfig: s.siteConfig, + enableInlineShortcodes: s.enableInlineShortcodes, + PageCollections: newPageCollections()} } @@ -282,17 +285,18 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { } s := &Site{ - PageCollections: c, - layoutHandler: output.NewLayoutHandler(), - Language: cfg.Language, - disabledKinds: disabledKinds, - titleFunc: titleFunc, - relatedDocsHandler: newSearchIndexHandler(relatedContentConfig), - outputFormats: outputFormats, - rc: &siteRenderingContext{output.HTMLFormat}, - outputFormatsConfig: siteOutputFormatsConfig, - mediaTypesConfig: siteMediaTypesConfig, - frontmatterHandler: frontMatterHandler, + PageCollections: c, + layoutHandler: output.NewLayoutHandler(), + Language: cfg.Language, + disabledKinds: disabledKinds, + titleFunc: titleFunc, + relatedDocsHandler: newSearchIndexHandler(relatedContentConfig), + outputFormats: outputFormats, + rc: &siteRenderingContext{output.HTMLFormat}, + outputFormatsConfig: siteOutputFormatsConfig, + mediaTypesConfig: siteMediaTypesConfig, + frontmatterHandler: frontMatterHandler, + enableInlineShortcodes: cfg.Language.GetBool("enableInlineShortcodes"), } return s, nil |