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