mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
9cd54cab20
This avoids double parsing the page content when `enableEmoji=true`. This commit also adds some general improvements to the parser, making it in general much faster: ```bash benchmark old ns/op new ns/op delta BenchmarkShortcodeLexer-4 90258 101730 +12.71% BenchmarkParse-4 148940 15037 -89.90% benchmark old allocs new allocs delta BenchmarkShortcodeLexer-4 456 700 +53.51% BenchmarkParse-4 28 33 +17.86% benchmark old bytes new bytes delta BenchmarkShortcodeLexer-4 69875 81014 +15.94% BenchmarkParse-4 8128 8304 +2.17% ``` Running some site benchmarks with Emoji support turned on: ```bash benchmark old ns/op new ns/op delta BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render-4 924556797 818115620 -11.51% benchmark old allocs new allocs delta BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render-4 4112613 4133787 +0.51% benchmark old bytes new bytes delta BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render-4 426982864 424363832 -0.61% ``` Fixes #5534
833 lines
22 KiB
Go
833 lines
22 KiB
Go
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
|
||
package hugolib
|
||
|
||
import (
|
||
"bytes"
|
||
"errors"
|
||
"fmt"
|
||
"html/template"
|
||
"path"
|
||
|
||
"github.com/gohugoio/hugo/common/herrors"
|
||
|
||
"reflect"
|
||
|
||
"regexp"
|
||
"sort"
|
||
|
||
"github.com/gohugoio/hugo/parser/pageparser"
|
||
|
||
_errors "github.com/pkg/errors"
|
||
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/gohugoio/hugo/common/maps"
|
||
"github.com/gohugoio/hugo/common/text"
|
||
"github.com/gohugoio/hugo/common/urls"
|
||
"github.com/gohugoio/hugo/output"
|
||
|
||
"github.com/gohugoio/hugo/media"
|
||
|
||
bp "github.com/gohugoio/hugo/bufferpool"
|
||
"github.com/gohugoio/hugo/helpers"
|
||
"github.com/gohugoio/hugo/tpl"
|
||
)
|
||
|
||
var (
|
||
_ urls.RefLinker = (*ShortcodeWithPage)(nil)
|
||
_ pageContainer = (*ShortcodeWithPage)(nil)
|
||
_ text.Positioner = (*ShortcodeWithPage)(nil)
|
||
)
|
||
|
||
// ShortcodeWithPage is the "." context in a shortcode template.
|
||
type ShortcodeWithPage struct {
|
||
Params interface{}
|
||
Inner template.HTML
|
||
Page *PageWithoutContent
|
||
Parent *ShortcodeWithPage
|
||
IsNamedParams bool
|
||
|
||
// Zero-based ordinal in relation to its parent. If the parent is the page itself,
|
||
// this ordinal will represent the position of this shortcode in the page content.
|
||
Ordinal int
|
||
|
||
// pos is the position in bytes in the source file. Used for error logging.
|
||
posInit sync.Once
|
||
posOffset int
|
||
pos text.Position
|
||
|
||
scratch *maps.Scratch
|
||
}
|
||
|
||
// Position returns this shortcode's detailed position. Note that this information
|
||
// may be expensive to calculate, so only use this in error situations.
|
||
func (scp *ShortcodeWithPage) Position() text.Position {
|
||
scp.posInit.Do(func() {
|
||
scp.pos = scp.Page.posFromPage(scp.posOffset)
|
||
})
|
||
return scp.pos
|
||
}
|
||
|
||
// Site returns information about the current site.
|
||
func (scp *ShortcodeWithPage) Site() *SiteInfo {
|
||
return scp.Page.Site
|
||
}
|
||
|
||
// Ref is a shortcut to the Ref method on Page. It passes itself as a context
|
||
// to get better error messages.
|
||
func (scp *ShortcodeWithPage) Ref(args map[string]interface{}) (string, error) {
|
||
return scp.Page.ref(args, scp)
|
||
}
|
||
|
||
// RelRef is a shortcut to the RelRef method on Page. It passes itself as a context
|
||
// to get better error messages.
|
||
func (scp *ShortcodeWithPage) RelRef(args map[string]interface{}) (string, error) {
|
||
return scp.Page.relRef(args, scp)
|
||
}
|
||
|
||
// Scratch returns a scratch-pad scoped for this shortcode. This can be used
|
||
// as a temporary storage for variables, counters etc.
|
||
func (scp *ShortcodeWithPage) Scratch() *maps.Scratch {
|
||
if scp.scratch == nil {
|
||
scp.scratch = maps.NewScratch()
|
||
}
|
||
return scp.scratch
|
||
}
|
||
|
||
// Get is a convenience method to look up shortcode parameters by its key.
|
||
func (scp *ShortcodeWithPage) Get(key interface{}) interface{} {
|
||
if scp.Params == nil {
|
||
return nil
|
||
}
|
||
if reflect.ValueOf(scp.Params).Len() == 0 {
|
||
return nil
|
||
}
|
||
|
||
var x reflect.Value
|
||
|
||
switch key.(type) {
|
||
case int64, int32, int16, int8, int:
|
||
if reflect.TypeOf(scp.Params).Kind() == reflect.Map {
|
||
// We treat this as a non error, so people can do similar to
|
||
// {{ $myParam := .Get "myParam" | default .Get 0 }}
|
||
// Without having to do additional checks.
|
||
return nil
|
||
} else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice {
|
||
idx := int(reflect.ValueOf(key).Int())
|
||
ln := reflect.ValueOf(scp.Params).Len()
|
||
if idx > ln-1 {
|
||
return ""
|
||
}
|
||
x = reflect.ValueOf(scp.Params).Index(idx)
|
||
}
|
||
case string:
|
||
if reflect.TypeOf(scp.Params).Kind() == reflect.Map {
|
||
x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key))
|
||
if !x.IsValid() {
|
||
return ""
|
||
}
|
||
} else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice {
|
||
// We treat this as a non error, so people can do similar to
|
||
// {{ $myParam := .Get "myParam" | default .Get 0 }}
|
||
// Without having to do additional checks.
|
||
return nil
|
||
}
|
||
}
|
||
|
||
switch x.Kind() {
|
||
case reflect.String:
|
||
return x.String()
|
||
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
|
||
return x.Int()
|
||
default:
|
||
return x
|
||
}
|
||
|
||
}
|
||
|
||
func (scp *ShortcodeWithPage) page() *Page {
|
||
return scp.Page.Page
|
||
}
|
||
|
||
// Note - this value must not contain any markup syntax
|
||
const shortcodePlaceholderPrefix = "HUGOSHORTCODE"
|
||
|
||
type shortcode struct {
|
||
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 (s shortcode) innerString() string {
|
||
var sb strings.Builder
|
||
|
||
for _, inner := range s.inner {
|
||
sb.WriteString(inner.(string))
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
func (sc shortcode) String() string {
|
||
// for testing (mostly), so any change here will break tests!
|
||
var params interface{}
|
||
switch v := sc.params.(type) {
|
||
case map[string]string:
|
||
// sort the keys so test assertions won't fail
|
||
var keys []string
|
||
for k := range v {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
var tmp = make([]string, len(keys))
|
||
|
||
for i, k := range keys {
|
||
tmp[i] = k + ":" + v[k]
|
||
}
|
||
params = tmp
|
||
|
||
default:
|
||
// use it as is
|
||
params = sc.params
|
||
}
|
||
|
||
return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner)
|
||
}
|
||
|
||
// We may have special shortcode templates for AMP etc.
|
||
// Note that in the below, OutputFormat may be empty.
|
||
// We will try to look for the most specific shortcode template available.
|
||
type scKey struct {
|
||
Lang string
|
||
OutputFormat string
|
||
Suffix string
|
||
ShortcodePlaceholder string
|
||
}
|
||
|
||
func newScKey(m media.Type, shortcodeplaceholder string) scKey {
|
||
return scKey{Suffix: m.Suffix(), ShortcodePlaceholder: shortcodeplaceholder}
|
||
}
|
||
|
||
func newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey {
|
||
return scKey{Lang: lang, Suffix: o.MediaType.Suffix(), OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder}
|
||
}
|
||
|
||
func newDefaultScKey(shortcodeplaceholder string) scKey {
|
||
return newScKey(media.HTMLType, shortcodeplaceholder)
|
||
}
|
||
|
||
type shortcodeHandler struct {
|
||
init sync.Once
|
||
|
||
p *PageWithoutContent
|
||
|
||
// This is all shortcode rendering funcs for all potential output formats.
|
||
contentShortcodes *orderedMap
|
||
|
||
// This map contains the new or changed set of shortcodes that need
|
||
// to be rendered for the current output format.
|
||
contentShortcodesDelta *orderedMap
|
||
|
||
// This maps the shorcode placeholders with the rendered content.
|
||
// We will do (potential) partial re-rendering per output format,
|
||
// so keep this for the unchanged.
|
||
renderedShortcodes map[string]string
|
||
|
||
// Maps the shortcodeplaceholder with the actual shortcode.
|
||
shortcodes *orderedMap
|
||
|
||
// All the shortcode names in this set.
|
||
nameSet map[string]bool
|
||
|
||
placeholderID int
|
||
placeholderFunc func() string
|
||
|
||
enableInlineShortcodes bool
|
||
}
|
||
|
||
func (s *shortcodeHandler) nextPlaceholderID() int {
|
||
s.placeholderID++
|
||
return s.placeholderID
|
||
}
|
||
|
||
func (s *shortcodeHandler) createShortcodePlaceholder() string {
|
||
return s.placeholderFunc()
|
||
}
|
||
|
||
func newShortcodeHandler(p *Page) *shortcodeHandler {
|
||
|
||
s := &shortcodeHandler{
|
||
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
|
||
if placeholderFunc == nil {
|
||
placeholderFunc = func() string {
|
||
return fmt.Sprintf("HAHA%s-%p-%d-HBHB", shortcodePlaceholderPrefix, p, s.nextPlaceholderID())
|
||
}
|
||
|
||
}
|
||
s.placeholderFunc = placeholderFunc
|
||
return s
|
||
}
|
||
|
||
// TODO(bep) make it non-global
|
||
var isInnerShortcodeCache = struct {
|
||
sync.RWMutex
|
||
m map[string]bool
|
||
}{m: make(map[string]bool)}
|
||
|
||
// to avoid potential costly look-aheads for closing tags we look inside the template itself
|
||
// we could change the syntax to self-closing tags, but that would make users cry
|
||
// the value found is cached
|
||
func isInnerShortcode(t tpl.TemplateExecutor) (bool, error) {
|
||
isInnerShortcodeCache.RLock()
|
||
m, ok := isInnerShortcodeCache.m[t.Name()]
|
||
isInnerShortcodeCache.RUnlock()
|
||
|
||
if ok {
|
||
return m, nil
|
||
}
|
||
|
||
isInnerShortcodeCache.Lock()
|
||
defer isInnerShortcodeCache.Unlock()
|
||
match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree())
|
||
isInnerShortcodeCache.m[t.Name()] = match
|
||
|
||
return match, nil
|
||
}
|
||
|
||
func clearIsInnerShortcodeCache() {
|
||
isInnerShortcodeCache.Lock()
|
||
defer isInnerShortcodeCache.Unlock()
|
||
isInnerShortcodeCache.m = make(map[string]bool)
|
||
}
|
||
|
||
const innerNewlineRegexp = "\n"
|
||
const innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
|
||
const innerCleanupExpand = "$1"
|
||
|
||
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)
|
||
m[key] = func() (string, error) {
|
||
return renderShortcode(key, sc, nil, p)
|
||
}
|
||
}
|
||
|
||
return m
|
||
}
|
||
|
||
func renderShortcode(
|
||
tmplKey scKey,
|
||
sc *shortcode,
|
||
parent *ShortcodeWithPage,
|
||
p *PageWithoutContent) (string, error) {
|
||
|
||
var tmpl tpl.Template
|
||
|
||
if sc.isInline {
|
||
templName := path.Join("_inline_shortcode", p.Path(), sc.name)
|
||
if sc.isClosing {
|
||
templStr := sc.innerString()
|
||
|
||
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
|
||
}
|
||
|
||
data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, Params: sc.params, Page: p, Parent: parent}
|
||
if sc.params != nil {
|
||
data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map
|
||
}
|
||
|
||
if len(sc.inner) > 0 {
|
||
var inner string
|
||
for _, innerData := range sc.inner {
|
||
switch innerData.(type) {
|
||
case string:
|
||
inner += innerData.(string)
|
||
case *shortcode:
|
||
s, err := renderShortcode(tmplKey, innerData.(*shortcode), data, p)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
inner += s
|
||
default:
|
||
p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
|
||
sc.name, p.Path(), reflect.TypeOf(innerData))
|
||
return "", nil
|
||
}
|
||
}
|
||
|
||
if sc.doMarkup {
|
||
newInner := p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
|
||
Content: []byte(inner),
|
||
PageFmt: p.Markup,
|
||
Cfg: p.Language(),
|
||
DocumentID: p.UniqueID(),
|
||
DocumentName: p.Path(),
|
||
Config: p.getRenderingConfig()})
|
||
|
||
// If the type is “unknown” or “markdown”, we assume the markdown
|
||
// generation has been performed. Given the input: `a line`, markdown
|
||
// specifies the HTML `<p>a line</p>\n`. When dealing with documents as a
|
||
// whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo,
|
||
// this is not so good. This code does two things:
|
||
//
|
||
// 1. Check to see if inner has a newline in it. If so, the Inner data is
|
||
// unchanged.
|
||
// 2 If inner does not have a newline, strip the wrapping <p> block and
|
||
// the newline. This was previously tricked out by wrapping shortcode
|
||
// substitutions in <div>HUGOSHORTCODE-1</div> which prevents the
|
||
// generation, but means that you can’t use shortcodes inside of
|
||
// markdown structures itself (e.g., `[foo]({{% ref foo.md %}})`).
|
||
switch p.Markup {
|
||
case "unknown", "markdown":
|
||
if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
|
||
cleaner, err := regexp.Compile(innerCleanupRegexp)
|
||
|
||
if err == nil {
|
||
newInner = cleaner.ReplaceAll(newInner, []byte(innerCleanupExpand))
|
||
}
|
||
}
|
||
}
|
||
|
||
// TODO(bep) we may have plain text inner templates.
|
||
data.Inner = template.HTML(newInner)
|
||
} else {
|
||
data.Inner = template.HTML(inner)
|
||
}
|
||
|
||
}
|
||
|
||
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,
|
||
// which, combined with the ones that do not have alternative representations,
|
||
// builds a complete set ready for a full rebuild of the Page content.
|
||
// This method returns false if there are no new shortcode variants in the
|
||
// current rendering context's output format. This mean we can safely reuse
|
||
// the content from the previous output format, if any.
|
||
func (s *shortcodeHandler) updateDelta() bool {
|
||
s.init.Do(func() {
|
||
s.contentShortcodes = s.createShortcodeRenderers(s.p.withoutContent())
|
||
})
|
||
|
||
if !s.p.shouldRenderTo(s.p.s.rc.Format) {
|
||
// TODO(bep) add test for this re translations
|
||
return false
|
||
}
|
||
of := s.p.s.rc.Format
|
||
contentShortcodes := s.contentShortcodesForOutputFormat(of)
|
||
|
||
if s.contentShortcodesDelta == nil || s.contentShortcodesDelta.Len() == 0 {
|
||
s.contentShortcodesDelta = contentShortcodes
|
||
return true
|
||
}
|
||
|
||
delta := newOrderedMap()
|
||
|
||
for _, k := range contentShortcodes.Keys() {
|
||
if !s.contentShortcodesDelta.Contains(k) {
|
||
v, _ := contentShortcodes.Get(k)
|
||
delta.Add(k, v)
|
||
}
|
||
}
|
||
|
||
s.contentShortcodesDelta = delta
|
||
|
||
return delta.Len() > 0
|
||
}
|
||
|
||
func (s *shortcodeHandler) clearDelta() {
|
||
if s == nil {
|
||
return
|
||
}
|
||
s.contentShortcodesDelta = newOrderedMap()
|
||
}
|
||
|
||
func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) *orderedMap {
|
||
contentShortcodesForOuputFormat := newOrderedMap()
|
||
lang := s.p.Lang()
|
||
|
||
for _, key := range s.shortcodes.Keys() {
|
||
shortcodePlaceholder := key.(string)
|
||
|
||
key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)
|
||
renderFn, found := s.contentShortcodes.Get(key)
|
||
|
||
if !found {
|
||
key.OutputFormat = ""
|
||
renderFn, found = s.contentShortcodes.Get(key)
|
||
}
|
||
|
||
// Fall back to HTML
|
||
if !found && key.Suffix != "html" {
|
||
key.Suffix = "html"
|
||
renderFn, found = s.contentShortcodes.Get(key)
|
||
}
|
||
|
||
if !found {
|
||
panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder))
|
||
}
|
||
contentShortcodesForOuputFormat.Add(newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder), renderFn)
|
||
}
|
||
|
||
return contentShortcodesForOuputFormat
|
||
}
|
||
|
||
func (s *shortcodeHandler) executeShortcodesForDelta(p *PageWithoutContent) error {
|
||
|
||
for _, k := range s.contentShortcodesDelta.Keys() {
|
||
render := s.contentShortcodesDelta.getShortcodeRenderer(k)
|
||
renderedShortcode, err := render()
|
||
if err != nil {
|
||
sc := s.shortcodes.getShortcode(k.(scKey).ShortcodePlaceholder)
|
||
if sc != nil {
|
||
err = p.errWithFileContext(p.parseError(_errors.Wrapf(err, "failed to render shortcode %q", sc.name), p.source.parsed.Input(), sc.pos))
|
||
}
|
||
|
||
p.s.SendError(err)
|
||
continue
|
||
}
|
||
|
||
s.renderedShortcodes[k.(scKey).ShortcodePlaceholder] = renderedShortcode
|
||
}
|
||
|
||
return nil
|
||
|
||
}
|
||
|
||
func (s *shortcodeHandler) createShortcodeRenderers(p *PageWithoutContent) *orderedMap {
|
||
|
||
shortcodeRenderers := newOrderedMap()
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
return shortcodeRenderers
|
||
}
|
||
|
||
var errShortCodeIllegalState = errors.New("Illegal shortcode state")
|
||
|
||
// pageTokens state:
|
||
// - before: positioned just before the shortcode start
|
||
// - after: shortcode(s) consumed (plural when they are nested)
|
||
func (s *shortcodeHandler) extractShortcode(ordinal int, pt *pageparser.Iterator, p *Page) (*shortcode, error) {
|
||
sc := &shortcode{ordinal: ordinal}
|
||
var isInner = false
|
||
|
||
var cnt = 0
|
||
var nestedOrdinal = 0
|
||
|
||
fail := func(err error, i pageparser.Item) error {
|
||
return p.parseError(err, pt.Input(), i.Pos)
|
||
}
|
||
|
||
Loop:
|
||
for {
|
||
currItem := pt.Next()
|
||
switch {
|
||
case currItem.IsLeftShortcodeDelim():
|
||
if sc.pos == 0 {
|
||
sc.pos = currItem.Pos
|
||
}
|
||
next := pt.Peek()
|
||
if next.IsShortcodeClose() {
|
||
continue
|
||
}
|
||
|
||
if cnt > 0 {
|
||
// nested shortcode; append it to inner content
|
||
pt.Backup()
|
||
nested, err := s.extractShortcode(nestedOrdinal, pt, p)
|
||
nestedOrdinal++
|
||
if nested.name != "" {
|
||
s.nameSet[nested.name] = true
|
||
}
|
||
if err == nil {
|
||
sc.inner = append(sc.inner, nested)
|
||
} else {
|
||
return sc, err
|
||
}
|
||
|
||
} else {
|
||
sc.doMarkup = currItem.IsShortcodeMarkupDelimiter()
|
||
}
|
||
|
||
cnt++
|
||
|
||
case currItem.IsRightShortcodeDelim():
|
||
// we trust the template on this:
|
||
// if there's no inner, we're done
|
||
if !sc.isInline && !isInner {
|
||
return sc, nil
|
||
}
|
||
|
||
case currItem.IsShortcodeClose():
|
||
next := pt.Peek()
|
||
if !sc.isInline && !isInner {
|
||
if next.IsError() {
|
||
// return that error, more specific
|
||
continue
|
||
}
|
||
|
||
return sc, fail(_errors.Errorf("shortcode %q has no .Inner, yet a closing tag was provided", next.Val), next)
|
||
}
|
||
if next.IsRightShortcodeDelim() {
|
||
// self-closing
|
||
pt.Consume(1)
|
||
} else {
|
||
sc.isClosing = true
|
||
pt.Consume(2)
|
||
}
|
||
|
||
return sc, nil
|
||
case currItem.IsText():
|
||
sc.inner = append(sc.inner, currItem.ValStr())
|
||
case currItem.IsShortcodeName():
|
||
sc.name = currItem.ValStr()
|
||
// We pick the first template for an arbitrary output format
|
||
// if more than one. It is "all inner or no inner".
|
||
tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl)
|
||
if tmpl == nil {
|
||
return sc, fail(_errors.Errorf("template for shortcode %q not found", sc.name), currItem)
|
||
}
|
||
|
||
var err error
|
||
isInner, err = isInnerShortcode(tmpl.(tpl.TemplateExecutor))
|
||
if err != nil {
|
||
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
|
||
} else if pt.Peek().IsShortcodeParamVal() {
|
||
// named params
|
||
if sc.params == nil {
|
||
params := make(map[string]string)
|
||
params[currItem.ValStr()] = pt.Next().ValStr()
|
||
sc.params = params
|
||
} else {
|
||
if params, ok := sc.params.(map[string]string); ok {
|
||
params[currItem.ValStr()] = pt.Next().ValStr()
|
||
} else {
|
||
return sc, errShortCodeIllegalState
|
||
}
|
||
|
||
}
|
||
} else {
|
||
// positional params
|
||
if sc.params == nil {
|
||
var params []string
|
||
params = append(params, currItem.ValStr())
|
||
sc.params = params
|
||
} else {
|
||
if params, ok := sc.params.([]string); ok {
|
||
params = append(params, currItem.ValStr())
|
||
sc.params = params
|
||
} else {
|
||
return sc, errShortCodeIllegalState
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
case currItem.IsDone():
|
||
// handled by caller
|
||
pt.Backup()
|
||
break Loop
|
||
|
||
}
|
||
}
|
||
return sc, nil
|
||
}
|
||
|
||
var shortCodeStart = []byte("{{")
|
||
|
||
// Replace prefixed shortcode tokens (HUGOSHORTCODE-1, HUGOSHORTCODE-2) with the real content.
|
||
// Note: This function will rewrite the input slice.
|
||
func replaceShortcodeTokens(source []byte, prefix string, replacements map[string]string) ([]byte, error) {
|
||
|
||
if len(replacements) == 0 {
|
||
return source, nil
|
||
}
|
||
|
||
start := 0
|
||
|
||
pre := []byte("HAHA" + prefix)
|
||
post := []byte("HBHB")
|
||
pStart := []byte("<p>")
|
||
pEnd := []byte("</p>")
|
||
|
||
k := bytes.Index(source[start:], pre)
|
||
|
||
for k != -1 {
|
||
j := start + k
|
||
postIdx := bytes.Index(source[j:], post)
|
||
if postIdx < 0 {
|
||
// this should never happen, but let the caller decide to panic or not
|
||
return nil, errors.New("illegal state in content; shortcode token missing end delim")
|
||
}
|
||
|
||
end := j + postIdx + 4
|
||
|
||
newVal := []byte(replacements[string(source[j:end])])
|
||
|
||
// Issue #1148: Check for wrapping p-tags <p>
|
||
if j >= 3 && bytes.Equal(source[j-3:j], pStart) {
|
||
if (k+4) < len(source) && bytes.Equal(source[end:end+4], pEnd) {
|
||
j -= 3
|
||
end += 4
|
||
}
|
||
}
|
||
|
||
// This and other cool slice tricks: https://github.com/golang/go/wiki/SliceTricks
|
||
source = append(source[:j], append(newVal, source[end:]...)...)
|
||
start = j
|
||
k = bytes.Index(source[start:], pre)
|
||
|
||
}
|
||
|
||
return source, nil
|
||
}
|
||
|
||
func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.TemplateFinder) tpl.Template {
|
||
isInnerShortcodeCache.RLock()
|
||
defer isInnerShortcodeCache.RUnlock()
|
||
|
||
var names []string
|
||
|
||
suffix := strings.ToLower(key.Suffix)
|
||
outFormat := strings.ToLower(key.OutputFormat)
|
||
lang := strings.ToLower(key.Lang)
|
||
|
||
if outFormat != "" && suffix != "" {
|
||
if lang != "" {
|
||
names = append(names, fmt.Sprintf("%s.%s.%s.%s", shortcodeName, lang, outFormat, suffix))
|
||
}
|
||
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix))
|
||
}
|
||
|
||
if suffix != "" {
|
||
if lang != "" {
|
||
names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, lang, suffix))
|
||
}
|
||
names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix))
|
||
}
|
||
|
||
names = append(names, shortcodeName)
|
||
|
||
for _, name := range names {
|
||
|
||
if x, found := t.Lookup("shortcodes/" + name); found {
|
||
return x
|
||
}
|
||
if x, found := t.Lookup("theme/shortcodes/" + name); found {
|
||
return x
|
||
}
|
||
if x, found := t.Lookup("_internal/shortcodes/" + name); found {
|
||
return x
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
|
||
buffer := bp.GetBuffer()
|
||
defer bp.PutBuffer(buffer)
|
||
|
||
isInnerShortcodeCache.RLock()
|
||
err := tmpl.Execute(buffer, data)
|
||
isInnerShortcodeCache.RUnlock()
|
||
if err != nil {
|
||
return "", _errors.Wrap(err, "failed to process shortcode")
|
||
}
|
||
return buffer.String(), nil
|
||
}
|