github.com/neohugo/neohugo@v0.123.8/hugolib/shortcode.go (about)

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