shithub: hugo

Download patch

ref: bc337e6ab5a75f1f1bfe3a83f3786d0afdb6346c
parent: 112461fded0d7970817ce7bf476c4763922ad314
author: Bjørn Erik Pedersen <[email protected]>
date: Mon Nov 26 06:01:27 EST 2018

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

--- a/common/herrors/file_error.go
+++ b/common/herrors/file_error.go
@@ -92,7 +92,13 @@
 // 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}
 }
 
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -482,6 +482,6 @@
 	v.SetDefault("debug", false)
 	v.SetDefault("disableFastRender", false)
 	v.SetDefault("timeout", 10000) // 10 seconds
-
+	v.SetDefault("enableInlineShortcodes", false)
 	return nil
 }
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -18,7 +18,10 @@
 	"errors"
 	"fmt"
 	"html/template"
+	"path"
 
+	"github.com/gohugoio/hugo/common/herrors"
+
 	"reflect"
 
 	"regexp"
@@ -163,13 +166,15 @@
 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 @@
 
 	placeholderID   int
 	placeholderFunc func() string
+
+	enableInlineShortcodes bool
 }
 
 func (s *shortcodeHandler) nextPlaceholderID() int {
@@ -259,11 +266,12 @@
 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 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 @@
 	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 @@
 
 	}
 
-	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 @@
 // 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 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 @@
 		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 @@
 		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 @@
 				// self-closing
 				pt.Consume(1)
 			} else {
+				sc.isClosing = true
 				pt.Consume(2)
 			}
 
@@ -609,6 +671,10 @@
 				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 @@
 	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
 }
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -1062,3 +1062,53 @@
 	)
 
 }
+
+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",
+					)
+				}
+
+			})
+
+	}
+}
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -124,6 +124,8 @@
 
 	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 @@
 // 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 @@
 	}
 
 	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
--- a/parser/pageparser/item.go
+++ b/parser/pageparser/item.go
@@ -42,6 +42,10 @@
 	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 @@
 	tRightDelimScWithMarkup
 	tScClose
 	tScName
+	tScNameInline
 	tScParam
 	tScParamVal
 
--- a/parser/pageparser/pagelexer.go
+++ b/parser/pageparser/pagelexer.go
@@ -32,6 +32,7 @@
 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 @@
 
 	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,8 +280,16 @@
 
 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 {
 	l.summaryDivider = summaryDivider
 
@@ -611,6 +633,9 @@
 	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 @@
 		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 @@
 			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 @@
 }
 
 func lexEndOfShortcode(l *pageLexer) stateFunc {
+	l.isInline = false
 	if l.hasPrefix(l.currentRightShortcodeDelim()) {
 		return lexShortcodeRightDelim
 	}
@@ -745,6 +780,22 @@
 		}
 	}
 	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 {
--- a/parser/pageparser/pageparser_shortcode_test.go
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -23,6 +23,7 @@
 	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")
@@ -29,6 +30,7 @@
 	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 @@
 		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) {