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 }