summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--common/herrors/file_error.go8
-rw-r--r--hugolib/config.go2
-rw-r--r--hugolib/shortcode.go116
-rw-r--r--hugolib/shortcode_test.go50
-rw-r--r--hugolib/site.go56
-rw-r--r--parser/pageparser/item.go5
-rw-r--r--parser/pageparser/pagelexer.go53
-rw-r--r--parser/pageparser/pageparser_shortcode_test.go8
8 files changed, 244 insertions, 54 deletions
diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go
index 929cc800f..5af84adf5 100644
--- a/common/herrors/file_error.go
+++ b/common/herrors/file_error.go
@@ -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}
}
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
diff --git a/parser/pageparser/item.go b/parser/pageparser/item.go
index 0567bd8b9..644c20e87 100644
--- a/parser/pageparser/item.go
+++ b/parser/pageparser/item.go
@@ -42,6 +42,10 @@ func (i Item) IsShortcodeName() bool {
return i.Type == tScName
}
+func (i Item) IsInlineShortcodeName() bool {
+ return i.Type == tScNameInline
+}
+
func (i Item) IsLeftShortcodeDelim() bool {
return i.Type == tLeftDelimScWithMarkup || i.Type == tLeftDelimScNoMarkup
}
@@ -119,6 +123,7 @@ const (
tRightDelimScWithMarkup
tScClose
tScName
+ tScNameInline
tScParam
tScParamVal
diff --git a/parser/pageparser/pagelexer.go b/parser/pageparser/pagelexer.go
index 8106758a9..94c1ff26b 100644
--- a/parser/pageparser/pagelexer.go
+++ b/parser/pageparser/pagelexer.go
@@ -32,6 +32,7 @@ type stateFunc func(*pageLexer) stateFunc
type lexerShortcodeState struct {
currLeftDelimItem ItemType
currRightDelimItem ItemType
+ isInline bool
currShortcodeName string // is only set when a shortcode is in opened state
closingState int // > 0 = on its way to be closed
elementStepNum int // step number in element
@@ -224,6 +225,19 @@ func lexMainSection(l *pageLexer) stateFunc {
for {
if l.isShortCodeStart() {
+ if l.isInline {
+ // If we're inside an inline shortcode, the only valid shortcode markup is
+ // the markup which closes it.
+ b := l.input[l.pos+3:]
+ end := indexNonWhiteSpace(b, '/')
+ if end != len(l.input)-1 {
+ b = bytes.TrimSpace(b[end+1:])
+ if end == -1 || !bytes.HasPrefix(b, []byte(l.currShortcodeName+" ")) {
+ return l.errorf("inline shortcodes do not support nesting")
+ }
+ }
+ }
+
if l.pos > l.start {
l.emit(tText)
}
@@ -266,6 +280,14 @@ func lexMainSection(l *pageLexer) stateFunc {
func (l *pageLexer) isShortCodeStart() bool {
return l.hasPrefix(leftDelimScWithMarkup) || l.hasPrefix(leftDelimScNoMarkup)
+
+}
+
+func (l *pageLexer) posFirstNonWhiteSpace() int {
+ f := func(c rune) bool {
+ return !unicode.IsSpace(c)
+ }
+ return bytes.IndexFunc(l.input[l.pos:], f)
}
func lexIntroSection(l *pageLexer) stateFunc {
@@ -611,6 +633,9 @@ Loop:
return lexInsideShortcode
}
+// Inline shortcodes has the form {{< myshortcode.inline >}}
+var inlineIdentifier = []byte("inline ")
+
// scans an alphanumeric inside shortcode
func lexIdentifierInShortcode(l *pageLexer) stateFunc {
lookForEnd := false
@@ -620,6 +645,11 @@ Loop:
case isAlphaNumericOrHyphen(r):
// Allow forward slash inside names to make it possible to create namespaces.
case r == '/':
+ case r == '.':
+ l.isInline = l.hasPrefix(inlineIdentifier)
+ if !l.isInline {
+ return l.errorf("period in shortcode name only allowed for inline identifiers")
+ }
default:
l.backup()
word := string(l.input[l.start:l.pos])
@@ -634,7 +664,11 @@ Loop:
l.currShortcodeName = word
l.openShortcodes[word] = true
l.elementStepNum++
- l.emit(tScName)
+ if l.isInline {
+ l.emit(tScNameInline)
+ } else {
+ l.emit(tScName)
+ }
break Loop
}
}
@@ -646,6 +680,7 @@ Loop:
}
func lexEndOfShortcode(l *pageLexer) stateFunc {
+ l.isInline = false
if l.hasPrefix(l.currentRightShortcodeDelim()) {
return lexShortcodeRightDelim
}
@@ -747,6 +782,22 @@ func minIndex(indices ...int) int {
return min
}
+func indexNonWhiteSpace(s []byte, in rune) int {
+ idx := bytes.IndexFunc(s, func(r rune) bool {
+ return !unicode.IsSpace(r)
+ })
+
+ if idx == -1 {
+ return -1
+ }
+
+ r, _ := utf8.DecodeRune(s[idx:])
+ if r == in {
+ return idx
+ }
+ return -1
+}
+
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
diff --git a/parser/pageparser/pageparser_shortcode_test.go b/parser/pageparser/pageparser_shortcode_test.go
index efef6fca2..c52840b58 100644
--- a/parser/pageparser/pageparser_shortcode_test.go
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -23,12 +23,14 @@ var (
tstRightMD = nti(tRightDelimScWithMarkup, "%}}")
tstSCClose = nti(tScClose, "/")
tstSC1 = nti(tScName, "sc1")
+ tstSC1Inline = nti(tScNameInline, "sc1.inline")
tstSC2 = nti(tScName, "sc2")
tstSC3 = nti(tScName, "sc3")
tstSCSlash = nti(tScName, "sc/sub")
tstParam1 = nti(tScParam, "param1")
tstParam2 = nti(tScParam, "param2")
tstVal = nti(tScParamVal, "Hello World")
+ tstText = nti(tText, "Hello World")
)
var shortCodeLexerTests = []lexerTest{
@@ -146,6 +148,12 @@ var shortCodeLexerTests = []lexerTest{
nti(tError, "comment must be closed")}},
{"commented out, misplaced close", `{{</* sc1 >}}*/`, []Item{
nti(tError, "comment must be closed")}},
+ // Inline shortcodes
+ {"basic inline", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
+ {"basic inline with space", `{{< sc1.inline >}}Hello World{{< / sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
+ {"inline self closing", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}Hello World{{< sc1.inline />}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSC1Inline, tstSCClose, tstRightNoMD, tstEOF}},
+ {"inline with nested shortcode (not supported)", `{{< sc1.inline >}}Hello World{{< sc1 >}}{{< /sc1.inline >}}`, []Item{tstLeftNoMD, tstSC1Inline, tstRightNoMD, nti(tError, "inline shortcodes do not support nesting")}},
+ {"inline case mismatch", `{{< sc1.Inline >}}Hello World{{< /sc1.Inline >}}`, []Item{tstLeftNoMD, nti(tError, "period in shortcode name only allowed for inline identifiers")}},
}
func TestShortcodeLexer(t *testing.T) {