hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

shortcode.go (19529B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package hugolib
   15 
   16 import (
   17 	"bytes"
   18 	"fmt"
   19 	"html/template"
   20 	"path"
   21 	"reflect"
   22 	"regexp"
   23 	"sort"
   24 	"strconv"
   25 	"strings"
   26 	"sync"
   27 
   28 	"github.com/gohugoio/hugo/helpers"
   29 
   30 	"errors"
   31 
   32 	"github.com/gohugoio/hugo/common/herrors"
   33 
   34 	"github.com/gohugoio/hugo/parser/pageparser"
   35 	"github.com/gohugoio/hugo/resources/page"
   36 
   37 	"github.com/gohugoio/hugo/common/maps"
   38 	"github.com/gohugoio/hugo/common/text"
   39 	"github.com/gohugoio/hugo/common/urls"
   40 	"github.com/gohugoio/hugo/output"
   41 
   42 	bp "github.com/gohugoio/hugo/bufferpool"
   43 	"github.com/gohugoio/hugo/tpl"
   44 )
   45 
   46 var (
   47 	_ urls.RefLinker  = (*ShortcodeWithPage)(nil)
   48 	_ pageWrapper     = (*ShortcodeWithPage)(nil)
   49 	_ text.Positioner = (*ShortcodeWithPage)(nil)
   50 )
   51 
   52 // ShortcodeWithPage is the "." context in a shortcode template.
   53 type ShortcodeWithPage struct {
   54 	Params        any
   55 	Inner         template.HTML
   56 	Page          page.Page
   57 	Parent        *ShortcodeWithPage
   58 	Name          string
   59 	IsNamedParams bool
   60 
   61 	// Zero-based ordinal in relation to its parent. If the parent is the page itself,
   62 	// this ordinal will represent the position of this shortcode in the page content.
   63 	Ordinal int
   64 
   65 	// Indentation before the opening shortcode in the source.
   66 	indentation string
   67 
   68 	innerDeindentInit sync.Once
   69 	innerDeindent     template.HTML
   70 
   71 	// pos is the position in bytes in the source file. Used for error logging.
   72 	posInit   sync.Once
   73 	posOffset int
   74 	pos       text.Position
   75 
   76 	scratch *maps.Scratch
   77 }
   78 
   79 // InnerDeindent returns the (potentially de-indented) inner content of the shortcode.
   80 func (scp *ShortcodeWithPage) InnerDeindent() template.HTML {
   81 	if scp.indentation == "" {
   82 		return scp.Inner
   83 	}
   84 	scp.innerDeindentInit.Do(func() {
   85 		b := bp.GetBuffer()
   86 		text.VisitLinesAfter(string(scp.Inner), func(s string) {
   87 			if strings.HasPrefix(s, scp.indentation) {
   88 				b.WriteString(strings.TrimPrefix(s, scp.indentation))
   89 			} else {
   90 				b.WriteString(s)
   91 			}
   92 		})
   93 		scp.innerDeindent = template.HTML(b.String())
   94 		bp.PutBuffer(b)
   95 	})
   96 
   97 	return scp.innerDeindent
   98 }
   99 
  100 // Position returns this shortcode's detailed position. Note that this information
  101 // may be expensive to calculate, so only use this in error situations.
  102 func (scp *ShortcodeWithPage) Position() text.Position {
  103 	scp.posInit.Do(func() {
  104 		if p, ok := mustUnwrapPage(scp.Page).(pageContext); ok {
  105 			scp.pos = p.posOffset(scp.posOffset)
  106 		}
  107 	})
  108 	return scp.pos
  109 }
  110 
  111 // Site returns information about the current site.
  112 func (scp *ShortcodeWithPage) Site() page.Site {
  113 	return scp.Page.Site()
  114 }
  115 
  116 // Ref is a shortcut to the Ref method on Page. It passes itself as a context
  117 // to get better error messages.
  118 func (scp *ShortcodeWithPage) Ref(args map[string]any) (string, error) {
  119 	return scp.Page.RefFrom(args, scp)
  120 }
  121 
  122 // RelRef is a shortcut to the RelRef method on Page. It passes itself as a context
  123 // to get better error messages.
  124 func (scp *ShortcodeWithPage) RelRef(args map[string]any) (string, error) {
  125 	return scp.Page.RelRefFrom(args, scp)
  126 }
  127 
  128 // Scratch returns a scratch-pad scoped for this shortcode. This can be used
  129 // as a temporary storage for variables, counters etc.
  130 func (scp *ShortcodeWithPage) Scratch() *maps.Scratch {
  131 	if scp.scratch == nil {
  132 		scp.scratch = maps.NewScratch()
  133 	}
  134 	return scp.scratch
  135 }
  136 
  137 // Get is a convenience method to look up shortcode parameters by its key.
  138 func (scp *ShortcodeWithPage) Get(key any) any {
  139 	if scp.Params == nil {
  140 		return nil
  141 	}
  142 	if reflect.ValueOf(scp.Params).Len() == 0 {
  143 		return nil
  144 	}
  145 
  146 	var x reflect.Value
  147 
  148 	switch key.(type) {
  149 	case int64, int32, int16, int8, int:
  150 		if reflect.TypeOf(scp.Params).Kind() == reflect.Map {
  151 			// We treat this as a non error, so people can do similar to
  152 			// {{ $myParam := .Get "myParam" | default .Get 0 }}
  153 			// Without having to do additional checks.
  154 			return nil
  155 		} else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice {
  156 			idx := int(reflect.ValueOf(key).Int())
  157 			ln := reflect.ValueOf(scp.Params).Len()
  158 			if idx > ln-1 {
  159 				return ""
  160 			}
  161 			x = reflect.ValueOf(scp.Params).Index(idx)
  162 		}
  163 	case string:
  164 		if reflect.TypeOf(scp.Params).Kind() == reflect.Map {
  165 			x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key))
  166 			if !x.IsValid() {
  167 				return ""
  168 			}
  169 		} else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice {
  170 			// We treat this as a non error, so people can do similar to
  171 			// {{ $myParam := .Get "myParam" | default .Get 0 }}
  172 			// Without having to do additional checks.
  173 			return nil
  174 		}
  175 	}
  176 
  177 	return x.Interface()
  178 }
  179 
  180 func (scp *ShortcodeWithPage) page() page.Page {
  181 	return scp.Page
  182 }
  183 
  184 // Note - this value must not contain any markup syntax
  185 const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE"
  186 
  187 func createShortcodePlaceholder(id string, ordinal int) string {
  188 	return shortcodePlaceholderPrefix + "-" + id + strconv.Itoa(ordinal) + "-HBHB"
  189 }
  190 
  191 type shortcode struct {
  192 	name      string
  193 	isInline  bool  // inline shortcode. Any inner will be a Go template.
  194 	isClosing bool  // whether a closing tag was provided
  195 	inner     []any // string or nested shortcode
  196 	params    any   // map or array
  197 	ordinal   int
  198 	err       error
  199 
  200 	indentation string // indentation from source.
  201 
  202 	info   tpl.Info       // One of the output formats (arbitrary)
  203 	templs []tpl.Template // All output formats
  204 
  205 	// If set, the rendered shortcode is sent as part of the surrounding content
  206 	// to Goldmark and similar.
  207 	// Before Hug0 0.55 we didn't send any shortcode output to the markup
  208 	// renderer, and this flag told Hugo to process the {{ .Inner }} content
  209 	// separately.
  210 	// The old behaviour can be had by starting your shortcode template with:
  211 	//    {{ $_hugo_config := `{ "version": 1 }`}}
  212 	doMarkup bool
  213 
  214 	// the placeholder in the source when passed to Goldmark etc.
  215 	// This also identifies the rendered shortcode.
  216 	placeholder string
  217 
  218 	pos    int // the position in bytes in the source file
  219 	length int // the length in bytes in the source file
  220 }
  221 
  222 func (s shortcode) insertPlaceholder() bool {
  223 	return !s.doMarkup || s.configVersion() == 1
  224 }
  225 
  226 func (s shortcode) configVersion() int {
  227 	if s.info == nil {
  228 		// Not set for inline shortcodes.
  229 		return 2
  230 	}
  231 
  232 	return s.info.ParseInfo().Config.Version
  233 }
  234 
  235 func (s shortcode) innerString() string {
  236 	var sb strings.Builder
  237 
  238 	for _, inner := range s.inner {
  239 		sb.WriteString(inner.(string))
  240 	}
  241 
  242 	return sb.String()
  243 }
  244 
  245 func (sc shortcode) String() string {
  246 	// for testing (mostly), so any change here will break tests!
  247 	var params any
  248 	switch v := sc.params.(type) {
  249 	case map[string]any:
  250 		// sort the keys so test assertions won't fail
  251 		var keys []string
  252 		for k := range v {
  253 			keys = append(keys, k)
  254 		}
  255 		sort.Strings(keys)
  256 		tmp := make(map[string]any)
  257 
  258 		for _, k := range keys {
  259 			tmp[k] = v[k]
  260 		}
  261 		params = tmp
  262 
  263 	default:
  264 		// use it as is
  265 		params = sc.params
  266 	}
  267 
  268 	return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner)
  269 }
  270 
  271 type shortcodeHandler struct {
  272 	p *pageState
  273 
  274 	s *Site
  275 
  276 	// Ordered list of shortcodes for a page.
  277 	shortcodes []*shortcode
  278 
  279 	// All the shortcode names in this set.
  280 	nameSet   map[string]bool
  281 	nameSetMu sync.RWMutex
  282 
  283 	// Configuration
  284 	enableInlineShortcodes bool
  285 }
  286 
  287 func newShortcodeHandler(p *pageState, s *Site) *shortcodeHandler {
  288 	sh := &shortcodeHandler{
  289 		p:                      p,
  290 		s:                      s,
  291 		enableInlineShortcodes: s.ExecHelper.Sec().EnableInlineShortcodes,
  292 		shortcodes:             make([]*shortcode, 0, 4),
  293 		nameSet:                make(map[string]bool),
  294 	}
  295 
  296 	return sh
  297 }
  298 
  299 const (
  300 	innerNewlineRegexp = "\n"
  301 	innerCleanupRegexp = `\A<p>(.*)</p>\n\z`
  302 	innerCleanupExpand = "$1"
  303 )
  304 
  305 func renderShortcode(
  306 	level int,
  307 	s *Site,
  308 	tplVariants tpl.TemplateVariants,
  309 	sc *shortcode,
  310 	parent *ShortcodeWithPage,
  311 	p *pageState) (string, bool, error) {
  312 	var tmpl tpl.Template
  313 
  314 	// Tracks whether this shortcode or any of its children has template variations
  315 	// in other languages or output formats. We are currently only interested in
  316 	// the output formats, so we may get some false positives -- we
  317 	// should improve on that.
  318 	var hasVariants bool
  319 
  320 	if sc.isInline {
  321 		if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
  322 			return "", false, nil
  323 		}
  324 		templName := path.Join("_inline_shortcode", p.File().Path(), sc.name)
  325 		if sc.isClosing {
  326 			templStr := sc.innerString()
  327 
  328 			var err error
  329 			tmpl, err = s.TextTmpl().Parse(templName, templStr)
  330 			if err != nil {
  331 				fe := herrors.NewFileErrorFromName(err, p.File().Filename())
  332 				pos := fe.Position()
  333 				pos.LineNumber += p.posOffset(sc.pos).LineNumber
  334 				fe = fe.UpdatePosition(pos)
  335 				return "", false, p.wrapError(fe)
  336 			}
  337 
  338 		} else {
  339 			// Re-use of shortcode defined earlier in the same page.
  340 			var found bool
  341 			tmpl, found = s.TextTmpl().Lookup(templName)
  342 			if !found {
  343 				return "", false, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
  344 			}
  345 		}
  346 	} else {
  347 		var found, more bool
  348 		tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
  349 		if !found {
  350 			s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
  351 			return "", false, nil
  352 		}
  353 		hasVariants = hasVariants || more
  354 	}
  355 
  356 	data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, indentation: sc.indentation, Params: sc.params, Page: newPageForShortcode(p), Parent: parent, Name: sc.name}
  357 	if sc.params != nil {
  358 		data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map
  359 	}
  360 
  361 	if len(sc.inner) > 0 {
  362 		var inner string
  363 		for _, innerData := range sc.inner {
  364 			switch innerData := innerData.(type) {
  365 			case string:
  366 				inner += innerData
  367 			case *shortcode:
  368 				s, more, err := renderShortcode(level+1, s, tplVariants, innerData, data, p)
  369 				if err != nil {
  370 					return "", false, err
  371 				}
  372 				hasVariants = hasVariants || more
  373 				inner += s
  374 			default:
  375 				s.Log.Errorf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
  376 					sc.name, p.File().Path(), reflect.TypeOf(innerData))
  377 				return "", false, nil
  378 			}
  379 		}
  380 
  381 		// Pre Hugo 0.55 this was the behaviour even for the outer-most
  382 		// shortcode.
  383 		if sc.doMarkup && (level > 0 || sc.configVersion() == 1) {
  384 			var err error
  385 			b, err := p.pageOutput.cp.renderContent([]byte(inner), false)
  386 			if err != nil {
  387 				return "", false, err
  388 			}
  389 
  390 			newInner := b.Bytes()
  391 
  392 			// If the type is “” (unknown) or “markdown”, we assume the markdown
  393 			// generation has been performed. Given the input: `a line`, markdown
  394 			// specifies the HTML `<p>a line</p>\n`. When dealing with documents as a
  395 			// whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo,
  396 			// this is not so good. This code does two things:
  397 			//
  398 			// 1.  Check to see if inner has a newline in it. If so, the Inner data is
  399 			//     unchanged.
  400 			// 2   If inner does not have a newline, strip the wrapping <p> block and
  401 			//     the newline.
  402 			switch p.m.markup {
  403 			case "", "markdown":
  404 				if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
  405 					cleaner, err := regexp.Compile(innerCleanupRegexp)
  406 
  407 					if err == nil {
  408 						newInner = cleaner.ReplaceAll(newInner, []byte(innerCleanupExpand))
  409 					}
  410 				}
  411 			}
  412 
  413 			// TODO(bep) we may have plain text inner templates.
  414 			data.Inner = template.HTML(newInner)
  415 		} else {
  416 			data.Inner = template.HTML(inner)
  417 		}
  418 
  419 	}
  420 
  421 	result, err := renderShortcodeWithPage(s.Tmpl(), tmpl, data)
  422 
  423 	if err != nil && sc.isInline {
  424 		fe := herrors.NewFileErrorFromName(err, p.File().Filename())
  425 		pos := fe.Position()
  426 		pos.LineNumber += p.posOffset(sc.pos).LineNumber
  427 		fe = fe.UpdatePosition(pos)
  428 		return "", false, fe
  429 	}
  430 
  431 	if len(sc.inner) == 0 && len(sc.indentation) > 0 {
  432 		b := bp.GetBuffer()
  433 		i := 0
  434 		text.VisitLinesAfter(result, func(line string) {
  435 			// The first line is correctly indented.
  436 			if i > 0 {
  437 				b.WriteString(sc.indentation)
  438 			}
  439 			i++
  440 			b.WriteString(line)
  441 		})
  442 
  443 		result = b.String()
  444 		bp.PutBuffer(b)
  445 	}
  446 
  447 	return result, hasVariants, err
  448 }
  449 
  450 func (s *shortcodeHandler) hasShortcodes() bool {
  451 	return s != nil && len(s.shortcodes) > 0
  452 }
  453 
  454 func (s *shortcodeHandler) addName(name string) {
  455 	s.nameSetMu.Lock()
  456 	defer s.nameSetMu.Unlock()
  457 	s.nameSet[name] = true
  458 }
  459 
  460 func (s *shortcodeHandler) transferNames(in *shortcodeHandler) {
  461 	s.nameSetMu.Lock()
  462 	defer s.nameSetMu.Unlock()
  463 	for k := range in.nameSet {
  464 		s.nameSet[k] = true
  465 	}
  466 
  467 }
  468 
  469 func (s *shortcodeHandler) hasName(name string) bool {
  470 	s.nameSetMu.RLock()
  471 	defer s.nameSetMu.RUnlock()
  472 	_, ok := s.nameSet[name]
  473 	return ok
  474 }
  475 
  476 func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) {
  477 	rendered := make(map[string]string)
  478 
  479 	tplVariants := tpl.TemplateVariants{
  480 		Language:     p.Language().Lang,
  481 		OutputFormat: f,
  482 	}
  483 
  484 	var hasVariants bool
  485 
  486 	for _, v := range s.shortcodes {
  487 		s, more, err := renderShortcode(0, s.s, tplVariants, v, nil, p)
  488 		if err != nil {
  489 			err = p.parseError(fmt.Errorf("failed to render shortcode %q: %w", v.name, err), p.source.parsed.Input(), v.pos)
  490 			return nil, false, err
  491 		}
  492 		hasVariants = hasVariants || more
  493 		rendered[v.placeholder] = s
  494 
  495 	}
  496 
  497 	return rendered, hasVariants, nil
  498 }
  499 
  500 var errShortCodeIllegalState = errors.New("Illegal shortcode state")
  501 
  502 func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error {
  503 	if s.p != nil {
  504 		return s.p.parseError(err, input, pos)
  505 	}
  506 	return err
  507 }
  508 
  509 // pageTokens state:
  510 // - before: positioned just before the shortcode start
  511 // - after: shortcode(s) consumed (plural when they are nested)
  512 func (s *shortcodeHandler) extractShortcode(ordinal, level int, pt *pageparser.Iterator) (*shortcode, error) {
  513 	if s == nil {
  514 		panic("handler nil")
  515 	}
  516 	sc := &shortcode{ordinal: ordinal}
  517 
  518 	// Back up one to identify any indentation.
  519 	if pt.Pos() > 0 {
  520 		pt.Backup()
  521 		item := pt.Next()
  522 		if item.IsIndentation() {
  523 			sc.indentation = string(item.Val)
  524 		}
  525 	}
  526 
  527 	cnt := 0
  528 	nestedOrdinal := 0
  529 	nextLevel := level + 1
  530 	const errorPrefix = "failed to extract shortcode"
  531 
  532 	fail := func(err error, i pageparser.Item) error {
  533 		return s.parseError(fmt.Errorf("%s: %w", errorPrefix, err), pt.Input(), i.Pos)
  534 	}
  535 
  536 Loop:
  537 	for {
  538 		currItem := pt.Next()
  539 		switch {
  540 		case currItem.IsLeftShortcodeDelim():
  541 			next := pt.Peek()
  542 			if next.IsRightShortcodeDelim() {
  543 				// no name: {{< >}} or {{% %}}
  544 				return sc, errors.New("shortcode has no name")
  545 			}
  546 			if next.IsShortcodeClose() {
  547 				continue
  548 			}
  549 
  550 			if cnt > 0 {
  551 				// nested shortcode; append it to inner content
  552 				pt.Backup()
  553 				nested, err := s.extractShortcode(nestedOrdinal, nextLevel, pt)
  554 				nestedOrdinal++
  555 				if nested != nil && nested.name != "" {
  556 					s.addName(nested.name)
  557 				}
  558 
  559 				if err == nil {
  560 					sc.inner = append(sc.inner, nested)
  561 				} else {
  562 					return sc, err
  563 				}
  564 
  565 			} else {
  566 				sc.doMarkup = currItem.IsShortcodeMarkupDelimiter()
  567 			}
  568 
  569 			cnt++
  570 
  571 		case currItem.IsRightShortcodeDelim():
  572 			// we trust the template on this:
  573 			// if there's no inner, we're done
  574 			if !sc.isInline {
  575 				if sc.info == nil {
  576 					// This should not happen.
  577 					return sc, fail(errors.New("BUG: template info not set"), currItem)
  578 				}
  579 				if !sc.info.ParseInfo().IsInner {
  580 					return sc, nil
  581 				}
  582 			}
  583 
  584 		case currItem.IsShortcodeClose():
  585 			next := pt.Peek()
  586 			if !sc.isInline {
  587 				if sc.info == nil || !sc.info.ParseInfo().IsInner {
  588 					if next.IsError() {
  589 						// return that error, more specific
  590 						continue
  591 					}
  592 					return sc, fail(fmt.Errorf("shortcode %q has no .Inner, yet a closing tag was provided", next.Val), next)
  593 				}
  594 			}
  595 			if next.IsRightShortcodeDelim() {
  596 				// self-closing
  597 				pt.Consume(1)
  598 			} else {
  599 				sc.isClosing = true
  600 				pt.Consume(2)
  601 			}
  602 
  603 			return sc, nil
  604 		case currItem.IsText():
  605 			sc.inner = append(sc.inner, currItem.ValStr())
  606 		case currItem.Type == pageparser.TypeEmoji:
  607 			// TODO(bep) avoid the duplication of these "text cases", to prevent
  608 			// more of #6504 in the future.
  609 			val := currItem.ValStr()
  610 			if emoji := helpers.Emoji(val); emoji != nil {
  611 				sc.inner = append(sc.inner, string(emoji))
  612 			} else {
  613 				sc.inner = append(sc.inner, val)
  614 			}
  615 		case currItem.IsShortcodeName():
  616 
  617 			sc.name = currItem.ValStr()
  618 
  619 			// Used to check if the template expects inner content.
  620 			templs := s.s.Tmpl().LookupVariants(sc.name)
  621 			if templs == nil {
  622 				return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
  623 			}
  624 
  625 			sc.info = templs[0].(tpl.Info)
  626 			sc.templs = templs
  627 		case currItem.IsInlineShortcodeName():
  628 			sc.name = currItem.ValStr()
  629 			sc.isInline = true
  630 		case currItem.IsShortcodeParam():
  631 			if !pt.IsValueNext() {
  632 				continue
  633 			} else if pt.Peek().IsShortcodeParamVal() {
  634 				// named params
  635 				if sc.params == nil {
  636 					params := make(map[string]any)
  637 					params[currItem.ValStr()] = pt.Next().ValTyped()
  638 					sc.params = params
  639 				} else {
  640 					if params, ok := sc.params.(map[string]any); ok {
  641 						params[currItem.ValStr()] = pt.Next().ValTyped()
  642 					} else {
  643 						return sc, errShortCodeIllegalState
  644 					}
  645 				}
  646 			} else {
  647 				// positional params
  648 				if sc.params == nil {
  649 					var params []any
  650 					params = append(params, currItem.ValTyped())
  651 					sc.params = params
  652 				} else {
  653 					if params, ok := sc.params.([]any); ok {
  654 						params = append(params, currItem.ValTyped())
  655 						sc.params = params
  656 					} else {
  657 						return sc, errShortCodeIllegalState
  658 					}
  659 				}
  660 			}
  661 		case currItem.IsDone():
  662 			// handled by caller
  663 			pt.Backup()
  664 			break Loop
  665 
  666 		}
  667 	}
  668 	return sc, nil
  669 }
  670 
  671 // Replace prefixed shortcode tokens with the real content.
  672 // Note: This function will rewrite the input slice.
  673 func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]byte, error) {
  674 	if len(replacements) == 0 {
  675 		return source, nil
  676 	}
  677 
  678 	start := 0
  679 
  680 	pre := []byte(shortcodePlaceholderPrefix)
  681 	post := []byte("HBHB")
  682 	pStart := []byte("<p>")
  683 	pEnd := []byte("</p>")
  684 
  685 	k := bytes.Index(source[start:], pre)
  686 
  687 	for k != -1 {
  688 		j := start + k
  689 		postIdx := bytes.Index(source[j:], post)
  690 		if postIdx < 0 {
  691 			// this should never happen, but let the caller decide to panic or not
  692 			return nil, errors.New("illegal state in content; shortcode token missing end delim")
  693 		}
  694 
  695 		end := j + postIdx + 4
  696 
  697 		newVal := []byte(replacements[string(source[j:end])])
  698 
  699 		// Issue #1148: Check for wrapping p-tags <p>
  700 		if j >= 3 && bytes.Equal(source[j-3:j], pStart) {
  701 			if (k+4) < len(source) && bytes.Equal(source[end:end+4], pEnd) {
  702 				j -= 3
  703 				end += 4
  704 			}
  705 		}
  706 
  707 		// This and other cool slice tricks: https://github.com/golang/go/wiki/SliceTricks
  708 		source = append(source[:j], append(newVal, source[end:]...)...)
  709 		start = j
  710 		k = bytes.Index(source[start:], pre)
  711 
  712 	}
  713 
  714 	return source, nil
  715 }
  716 
  717 func renderShortcodeWithPage(h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
  718 	buffer := bp.GetBuffer()
  719 	defer bp.PutBuffer(buffer)
  720 
  721 	err := h.Execute(tmpl, buffer, data)
  722 	if err != nil {
  723 		return "", fmt.Errorf("failed to process shortcode: %w", err)
  724 	}
  725 	return buffer.String(), nil
  726 }