Skip to content

Commit

Permalink
Add inline shortcode support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bep committed Nov 27, 2018
1 parent 94ab125 commit 07c078a
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 54 deletions.
8 changes: 7 additions & 1 deletion common/herrors/file_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ func UnwrapFileError(err error) FileError {
// with the given offset from the original.
func ToFileErrorWithOffset(fe FileError, offset int) FileError {
pos := fe.Position()
pos.LineNumber = pos.LineNumber + offset
return ToFileErrorWithLineNumber(fe, pos.LineNumber+offset)
}

// ToFileErrorWithOffset will return a new FileError with the given line number.
func ToFileErrorWithLineNumber(fe FileError, lineNumber int) FileError {
pos := fe.Position()
pos.LineNumber = lineNumber
return &fileError{cause: fe, fileType: fe.Type(), position: pos}
}

Expand Down
2 changes: 1 addition & 1 deletion hugolib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions hugolib/page_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (p *Page) errorf(err error, format string, a ...interface{}) error {
errors.Errorf(format, args...)
return fmt.Errorf(format, args...)
}

return errors.Wrapf(err, format, args...)
}

Expand Down
116 changes: 91 additions & 25 deletions hugolib/shortcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
"errors"
"fmt"
"html/template"
"path"

"github.com/gohugoio/hugo/common/herrors"

"reflect"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -245,6 +250,8 @@ type shortcodeHandler struct {

placeholderID int
placeholderFunc func() string

enableInlineShortcodes bool
}

func (s *shortcodeHandler) nextPlaceholderID() int {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -588,6 +649,7 @@ Loop:
// self-closing
pt.Consume(1)
} else {
sc.isClosing = true
pt.Consume(2)
}

Expand All @@ -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
Expand Down Expand Up @@ -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
}
50 changes: 50 additions & 0 deletions hugolib/shortcode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
}

})

}
}
56 changes: 30 additions & 26 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()}

}

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 07c078a

Please sign in to comment.