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 }