mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
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
This commit is contained in:
parent
112461fded
commit
bc337e6ab5
8 changed files with 244 additions and 54 deletions
|
@ -92,7 +92,13 @@ func UnwrapFileError(err error) FileError {
|
||||||
// with the given offset from the original.
|
// with the given offset from the original.
|
||||||
func ToFileErrorWithOffset(fe FileError, offset int) FileError {
|
func ToFileErrorWithOffset(fe FileError, offset int) FileError {
|
||||||
pos := fe.Position()
|
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}
|
return &fileError{cause: fe, fileType: fe.Type(), position: pos}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -482,6 +482,6 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
|
||||||
v.SetDefault("debug", false)
|
v.SetDefault("debug", false)
|
||||||
v.SetDefault("disableFastRender", false)
|
v.SetDefault("disableFastRender", false)
|
||||||
v.SetDefault("timeout", 10000) // 10 seconds
|
v.SetDefault("timeout", 10000) // 10 seconds
|
||||||
|
v.SetDefault("enableInlineShortcodes", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/common/herrors"
|
||||||
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
@ -163,13 +166,15 @@ func (scp *ShortcodeWithPage) page() *Page {
|
||||||
const shortcodePlaceholderPrefix = "HUGOSHORTCODE"
|
const shortcodePlaceholderPrefix = "HUGOSHORTCODE"
|
||||||
|
|
||||||
type shortcode struct {
|
type shortcode struct {
|
||||||
name string
|
name string
|
||||||
inner []interface{} // string or nested shortcode
|
isInline bool // inline shortcode. Any inner will be a Go template.
|
||||||
params interface{} // map or array
|
isClosing bool // whether a closing tag was provided
|
||||||
ordinal int
|
inner []interface{} // string or nested shortcode
|
||||||
err error
|
params interface{} // map or array
|
||||||
doMarkup bool
|
ordinal int
|
||||||
pos int // the position in bytes in the source file
|
err error
|
||||||
|
doMarkup bool
|
||||||
|
pos int // the position in bytes in the source file
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc shortcode) String() string {
|
func (sc shortcode) String() string {
|
||||||
|
@ -245,6 +250,8 @@ type shortcodeHandler struct {
|
||||||
|
|
||||||
placeholderID int
|
placeholderID int
|
||||||
placeholderFunc func() string
|
placeholderFunc func() string
|
||||||
|
|
||||||
|
enableInlineShortcodes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *shortcodeHandler) nextPlaceholderID() int {
|
func (s *shortcodeHandler) nextPlaceholderID() int {
|
||||||
|
@ -259,11 +266,12 @@ func (s *shortcodeHandler) createShortcodePlaceholder() string {
|
||||||
func newShortcodeHandler(p *Page) *shortcodeHandler {
|
func newShortcodeHandler(p *Page) *shortcodeHandler {
|
||||||
|
|
||||||
s := &shortcodeHandler{
|
s := &shortcodeHandler{
|
||||||
p: p.withoutContent(),
|
p: p.withoutContent(),
|
||||||
contentShortcodes: newOrderedMap(),
|
enableInlineShortcodes: p.s.enableInlineShortcodes,
|
||||||
shortcodes: newOrderedMap(),
|
contentShortcodes: newOrderedMap(),
|
||||||
nameSet: make(map[string]bool),
|
shortcodes: newOrderedMap(),
|
||||||
renderedShortcodes: make(map[string]string),
|
nameSet: make(map[string]bool),
|
||||||
|
renderedShortcodes: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
placeholderFunc := p.s.shortcodePlaceholderFunc
|
placeholderFunc := p.s.shortcodePlaceholderFunc
|
||||||
|
@ -313,11 +321,26 @@ const innerNewlineRegexp = "\n"
|
||||||
const innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
|
const innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
|
||||||
const innerCleanupExpand = "$1"
|
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))
|
m := make(map[scKey]func() (string, error))
|
||||||
lang := p.Lang()
|
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 {
|
for _, f := range p.outputFormats {
|
||||||
// The most specific template will win.
|
// The most specific template will win.
|
||||||
key := newScKeyFromLangAndOutputFormat(lang, f, placeholder)
|
key := newScKeyFromLangAndOutputFormat(lang, f, placeholder)
|
||||||
|
@ -335,7 +358,34 @@ func renderShortcode(
|
||||||
parent *ShortcodeWithPage,
|
parent *ShortcodeWithPage,
|
||||||
p *PageWithoutContent) (string, error) {
|
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 {
|
if tmpl == nil {
|
||||||
p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q 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 "", nil
|
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,
|
// 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.
|
// the content from the previous output format, if any.
|
||||||
func (s *shortcodeHandler) updateDelta() bool {
|
func (s *shortcodeHandler) updateDelta() bool {
|
||||||
s.init.Do(func() {
|
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) {
|
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()
|
shortcodeRenderers := newOrderedMap()
|
||||||
|
|
||||||
for _, k := range shortcodes.Keys() {
|
for _, k := range s.shortcodes.Keys() {
|
||||||
v := shortcodes.getShortcode(k)
|
v := s.shortcodes.getShortcode(k)
|
||||||
prepared := prepareShortcodeForPage(k.(string), v, nil, p)
|
prepared := s.prepareShortcodeForPage(k.(string), v, nil, p)
|
||||||
for kk, vv := range prepared {
|
for kk, vv := range prepared {
|
||||||
shortcodeRenderers.Add(kk, vv)
|
shortcodeRenderers.Add(kk, vv)
|
||||||
}
|
}
|
||||||
|
@ -541,7 +600,9 @@ Loop:
|
||||||
currItem := pt.Next()
|
currItem := pt.Next()
|
||||||
switch {
|
switch {
|
||||||
case currItem.IsLeftShortcodeDelim():
|
case currItem.IsLeftShortcodeDelim():
|
||||||
sc.pos = currItem.Pos
|
if sc.pos == 0 {
|
||||||
|
sc.pos = currItem.Pos
|
||||||
|
}
|
||||||
next := pt.Peek()
|
next := pt.Peek()
|
||||||
if next.IsShortcodeClose() {
|
if next.IsShortcodeClose() {
|
||||||
continue
|
continue
|
||||||
|
@ -570,13 +631,13 @@ Loop:
|
||||||
case currItem.IsRightShortcodeDelim():
|
case currItem.IsRightShortcodeDelim():
|
||||||
// we trust the template on this:
|
// we trust the template on this:
|
||||||
// if there's no inner, we're done
|
// if there's no inner, we're done
|
||||||
if !isInner {
|
if !sc.isInline && !isInner {
|
||||||
return sc, nil
|
return sc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case currItem.IsShortcodeClose():
|
case currItem.IsShortcodeClose():
|
||||||
next := pt.Peek()
|
next := pt.Peek()
|
||||||
if !isInner {
|
if !sc.isInline && !isInner {
|
||||||
if next.IsError() {
|
if next.IsError() {
|
||||||
// return that error, more specific
|
// return that error, more specific
|
||||||
continue
|
continue
|
||||||
|
@ -588,6 +649,7 @@ Loop:
|
||||||
// self-closing
|
// self-closing
|
||||||
pt.Consume(1)
|
pt.Consume(1)
|
||||||
} else {
|
} else {
|
||||||
|
sc.isClosing = true
|
||||||
pt.Consume(2)
|
pt.Consume(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -609,6 +671,10 @@ Loop:
|
||||||
return sc, fail(_errors.Wrapf(err, "failed to handle template for shortcode %q", sc.name), currItem)
|
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():
|
case currItem.IsShortcodeParam():
|
||||||
if !pt.IsValueNext() {
|
if !pt.IsValueNext() {
|
||||||
continue
|
continue
|
||||||
|
@ -751,7 +817,7 @@ func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string
|
||||||
err := tmpl.Execute(buffer, data)
|
err := tmpl.Execute(buffer, data)
|
||||||
isInnerShortcodeCache.RUnlock()
|
isInnerShortcodeCache.RUnlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", data.Page.errorf(err, "failed to process shortcode")
|
return "", _errors.Wrap(err, "failed to process shortcode")
|
||||||
}
|
}
|
||||||
return buffer.String(), nil
|
return buffer.String(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -124,6 +124,8 @@ type Site struct {
|
||||||
|
|
||||||
disabledKinds map[string]bool
|
disabledKinds map[string]bool
|
||||||
|
|
||||||
|
enableInlineShortcodes bool
|
||||||
|
|
||||||
// Output formats defined in site config per Page Kind, or some defaults
|
// Output formats defined in site config per Page Kind, or some defaults
|
||||||
// if not set.
|
// if not set.
|
||||||
// Output formats defined in Page front matter will override these.
|
// 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.
|
// reset returns a new Site prepared for rebuild.
|
||||||
func (s *Site) reset() *Site {
|
func (s *Site) reset() *Site {
|
||||||
return &Site{Deps: s.Deps,
|
return &Site{Deps: s.Deps,
|
||||||
layoutHandler: output.NewLayoutHandler(),
|
layoutHandler: output.NewLayoutHandler(),
|
||||||
disabledKinds: s.disabledKinds,
|
disabledKinds: s.disabledKinds,
|
||||||
titleFunc: s.titleFunc,
|
titleFunc: s.titleFunc,
|
||||||
relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg),
|
relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg),
|
||||||
siteRefLinker: s.siteRefLinker,
|
siteRefLinker: s.siteRefLinker,
|
||||||
outputFormats: s.outputFormats,
|
outputFormats: s.outputFormats,
|
||||||
rc: s.rc,
|
rc: s.rc,
|
||||||
outputFormatsConfig: s.outputFormatsConfig,
|
outputFormatsConfig: s.outputFormatsConfig,
|
||||||
frontmatterHandler: s.frontmatterHandler,
|
frontmatterHandler: s.frontmatterHandler,
|
||||||
mediaTypesConfig: s.mediaTypesConfig,
|
mediaTypesConfig: s.mediaTypesConfig,
|
||||||
Language: s.Language,
|
Language: s.Language,
|
||||||
owner: s.owner,
|
owner: s.owner,
|
||||||
publisher: s.publisher,
|
publisher: s.publisher,
|
||||||
siteConfig: s.siteConfig,
|
siteConfig: s.siteConfig,
|
||||||
PageCollections: newPageCollections()}
|
enableInlineShortcodes: s.enableInlineShortcodes,
|
||||||
|
PageCollections: newPageCollections()}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,17 +285,18 @@ func newSite(cfg deps.DepsCfg) (*Site, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Site{
|
s := &Site{
|
||||||
PageCollections: c,
|
PageCollections: c,
|
||||||
layoutHandler: output.NewLayoutHandler(),
|
layoutHandler: output.NewLayoutHandler(),
|
||||||
Language: cfg.Language,
|
Language: cfg.Language,
|
||||||
disabledKinds: disabledKinds,
|
disabledKinds: disabledKinds,
|
||||||
titleFunc: titleFunc,
|
titleFunc: titleFunc,
|
||||||
relatedDocsHandler: newSearchIndexHandler(relatedContentConfig),
|
relatedDocsHandler: newSearchIndexHandler(relatedContentConfig),
|
||||||
outputFormats: outputFormats,
|
outputFormats: outputFormats,
|
||||||
rc: &siteRenderingContext{output.HTMLFormat},
|
rc: &siteRenderingContext{output.HTMLFormat},
|
||||||
outputFormatsConfig: siteOutputFormatsConfig,
|
outputFormatsConfig: siteOutputFormatsConfig,
|
||||||
mediaTypesConfig: siteMediaTypesConfig,
|
mediaTypesConfig: siteMediaTypesConfig,
|
||||||
frontmatterHandler: frontMatterHandler,
|
frontmatterHandler: frontMatterHandler,
|
||||||
|
enableInlineShortcodes: cfg.Language.GetBool("enableInlineShortcodes"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|
|
@ -42,6 +42,10 @@ func (i Item) IsShortcodeName() bool {
|
||||||
return i.Type == tScName
|
return i.Type == tScName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i Item) IsInlineShortcodeName() bool {
|
||||||
|
return i.Type == tScNameInline
|
||||||
|
}
|
||||||
|
|
||||||
func (i Item) IsLeftShortcodeDelim() bool {
|
func (i Item) IsLeftShortcodeDelim() bool {
|
||||||
return i.Type == tLeftDelimScWithMarkup || i.Type == tLeftDelimScNoMarkup
|
return i.Type == tLeftDelimScWithMarkup || i.Type == tLeftDelimScNoMarkup
|
||||||
}
|
}
|
||||||
|
@ -119,6 +123,7 @@ const (
|
||||||
tRightDelimScWithMarkup
|
tRightDelimScWithMarkup
|
||||||
tScClose
|
tScClose
|
||||||
tScName
|
tScName
|
||||||
|
tScNameInline
|
||||||
tScParam
|
tScParam
|
||||||
tScParamVal
|
tScParamVal
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ type stateFunc func(*pageLexer) stateFunc
|
||||||
type lexerShortcodeState struct {
|
type lexerShortcodeState struct {
|
||||||
currLeftDelimItem ItemType
|
currLeftDelimItem ItemType
|
||||||
currRightDelimItem ItemType
|
currRightDelimItem ItemType
|
||||||
|
isInline bool
|
||||||
currShortcodeName string // is only set when a shortcode is in opened state
|
currShortcodeName string // is only set when a shortcode is in opened state
|
||||||
closingState int // > 0 = on its way to be closed
|
closingState int // > 0 = on its way to be closed
|
||||||
elementStepNum int // step number in element
|
elementStepNum int // step number in element
|
||||||
|
@ -224,6 +225,19 @@ func lexMainSection(l *pageLexer) stateFunc {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if l.isShortCodeStart() {
|
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 {
|
if l.pos > l.start {
|
||||||
l.emit(tText)
|
l.emit(tText)
|
||||||
}
|
}
|
||||||
|
@ -266,6 +280,14 @@ func lexMainSection(l *pageLexer) stateFunc {
|
||||||
|
|
||||||
func (l *pageLexer) isShortCodeStart() bool {
|
func (l *pageLexer) isShortCodeStart() bool {
|
||||||
return l.hasPrefix(leftDelimScWithMarkup) || l.hasPrefix(leftDelimScNoMarkup)
|
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 {
|
func lexIntroSection(l *pageLexer) stateFunc {
|
||||||
|
@ -611,6 +633,9 @@ Loop:
|
||||||
return lexInsideShortcode
|
return lexInsideShortcode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline shortcodes has the form {{< myshortcode.inline >}}
|
||||||
|
var inlineIdentifier = []byte("inline ")
|
||||||
|
|
||||||
// scans an alphanumeric inside shortcode
|
// scans an alphanumeric inside shortcode
|
||||||
func lexIdentifierInShortcode(l *pageLexer) stateFunc {
|
func lexIdentifierInShortcode(l *pageLexer) stateFunc {
|
||||||
lookForEnd := false
|
lookForEnd := false
|
||||||
|
@ -620,6 +645,11 @@ Loop:
|
||||||
case isAlphaNumericOrHyphen(r):
|
case isAlphaNumericOrHyphen(r):
|
||||||
// Allow forward slash inside names to make it possible to create namespaces.
|
// Allow forward slash inside names to make it possible to create namespaces.
|
||||||
case r == '/':
|
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:
|
default:
|
||||||
l.backup()
|
l.backup()
|
||||||
word := string(l.input[l.start:l.pos])
|
word := string(l.input[l.start:l.pos])
|
||||||
|
@ -634,7 +664,11 @@ Loop:
|
||||||
l.currShortcodeName = word
|
l.currShortcodeName = word
|
||||||
l.openShortcodes[word] = true
|
l.openShortcodes[word] = true
|
||||||
l.elementStepNum++
|
l.elementStepNum++
|
||||||
l.emit(tScName)
|
if l.isInline {
|
||||||
|
l.emit(tScNameInline)
|
||||||
|
} else {
|
||||||
|
l.emit(tScName)
|
||||||
|
}
|
||||||
break Loop
|
break Loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -646,6 +680,7 @@ Loop:
|
||||||
}
|
}
|
||||||
|
|
||||||
func lexEndOfShortcode(l *pageLexer) stateFunc {
|
func lexEndOfShortcode(l *pageLexer) stateFunc {
|
||||||
|
l.isInline = false
|
||||||
if l.hasPrefix(l.currentRightShortcodeDelim()) {
|
if l.hasPrefix(l.currentRightShortcodeDelim()) {
|
||||||
return lexShortcodeRightDelim
|
return lexShortcodeRightDelim
|
||||||
}
|
}
|
||||||
|
@ -747,6 +782,22 @@ func minIndex(indices ...int) int {
|
||||||
return min
|
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 {
|
func isSpace(r rune) bool {
|
||||||
return r == ' ' || r == '\t'
|
return r == ' ' || r == '\t'
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,14 @@ var (
|
||||||
tstRightMD = nti(tRightDelimScWithMarkup, "%}}")
|
tstRightMD = nti(tRightDelimScWithMarkup, "%}}")
|
||||||
tstSCClose = nti(tScClose, "/")
|
tstSCClose = nti(tScClose, "/")
|
||||||
tstSC1 = nti(tScName, "sc1")
|
tstSC1 = nti(tScName, "sc1")
|
||||||
|
tstSC1Inline = nti(tScNameInline, "sc1.inline")
|
||||||
tstSC2 = nti(tScName, "sc2")
|
tstSC2 = nti(tScName, "sc2")
|
||||||
tstSC3 = nti(tScName, "sc3")
|
tstSC3 = nti(tScName, "sc3")
|
||||||
tstSCSlash = nti(tScName, "sc/sub")
|
tstSCSlash = nti(tScName, "sc/sub")
|
||||||
tstParam1 = nti(tScParam, "param1")
|
tstParam1 = nti(tScParam, "param1")
|
||||||
tstParam2 = nti(tScParam, "param2")
|
tstParam2 = nti(tScParam, "param2")
|
||||||
tstVal = nti(tScParamVal, "Hello World")
|
tstVal = nti(tScParamVal, "Hello World")
|
||||||
|
tstText = nti(tText, "Hello World")
|
||||||
)
|
)
|
||||||
|
|
||||||
var shortCodeLexerTests = []lexerTest{
|
var shortCodeLexerTests = []lexerTest{
|
||||||
|
@ -146,6 +148,12 @@ var shortCodeLexerTests = []lexerTest{
|
||||||
nti(tError, "comment must be closed")}},
|
nti(tError, "comment must be closed")}},
|
||||||
{"commented out, misplaced close", `{{</* sc1 >}}*/`, []Item{
|
{"commented out, misplaced close", `{{</* sc1 >}}*/`, []Item{
|
||||||
nti(tError, "comment must be closed")}},
|
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) {
|
func TestShortcodeLexer(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue