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 }