page__per_output.go (18992B)
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 "fmt" 20 "html/template" 21 "runtime/debug" 22 "strings" 23 "sync" 24 "unicode/utf8" 25 26 "errors" 27 28 "github.com/gohugoio/hugo/common/text" 29 "github.com/gohugoio/hugo/common/types/hstring" 30 "github.com/gohugoio/hugo/identity" 31 "github.com/gohugoio/hugo/parser/pageparser" 32 "github.com/mitchellh/mapstructure" 33 "github.com/spf13/cast" 34 35 "github.com/gohugoio/hugo/markup/converter/hooks" 36 37 "github.com/gohugoio/hugo/markup/converter" 38 39 "github.com/alecthomas/chroma/v2/lexers" 40 "github.com/gohugoio/hugo/lazy" 41 42 bp "github.com/gohugoio/hugo/bufferpool" 43 "github.com/gohugoio/hugo/tpl" 44 45 "github.com/gohugoio/hugo/helpers" 46 "github.com/gohugoio/hugo/output" 47 "github.com/gohugoio/hugo/resources/page" 48 "github.com/gohugoio/hugo/resources/resource" 49 ) 50 51 var ( 52 nopTargetPath = targetPathsHolder{} 53 nopPagePerOutput = struct { 54 resource.ResourceLinksProvider 55 page.ContentProvider 56 page.PageRenderProvider 57 page.PaginatorProvider 58 page.TableOfContentsProvider 59 page.AlternativeOutputFormatsProvider 60 61 targetPather 62 }{ 63 page.NopPage, 64 page.NopPage, 65 page.NopPage, 66 page.NopPage, 67 page.NopPage, 68 page.NopPage, 69 nopTargetPath, 70 } 71 ) 72 73 var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"} 74 75 func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) { 76 parent := p.init 77 78 var dependencyTracker identity.Manager 79 if p.s.running() { 80 dependencyTracker = identity.NewManager(pageContentOutputDependenciesID) 81 } 82 83 cp := &pageContentOutput{ 84 dependencyTracker: dependencyTracker, 85 p: p, 86 f: po.f, 87 renderHooks: &renderHooks{}, 88 } 89 90 initContent := func() (err error) { 91 p.s.h.IncrContentRender() 92 93 if p.cmap == nil { 94 // Nothing to do. 95 return nil 96 } 97 defer func() { 98 // See https://github.com/gohugoio/hugo/issues/6210 99 if r := recover(); r != nil { 100 err = fmt.Errorf("%s", r) 101 p.s.Log.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack())) 102 } 103 }() 104 105 if err := po.cp.initRenderHooks(); err != nil { 106 return err 107 } 108 109 var hasShortcodeVariants bool 110 111 f := po.f 112 cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f) 113 if err != nil { 114 return err 115 } 116 117 if hasShortcodeVariants { 118 p.pageOutputTemplateVariationsState.Store(2) 119 } 120 121 cp.workContent = p.contentToRender(p.source.parsed, p.cmap, cp.contentPlaceholders) 122 123 isHTML := cp.p.m.markup == "html" 124 125 if !isHTML { 126 r, err := cp.renderContent(cp.workContent, true) 127 if err != nil { 128 return err 129 } 130 131 cp.workContent = r.Bytes() 132 133 if tocProvider, ok := r.(converter.TableOfContentsProvider); ok { 134 cfg := p.s.ContentSpec.Converters.GetMarkupConfig() 135 cp.tableOfContents = template.HTML( 136 tocProvider.TableOfContents().ToHTML( 137 cfg.TableOfContents.StartLevel, 138 cfg.TableOfContents.EndLevel, 139 cfg.TableOfContents.Ordered, 140 ), 141 ) 142 } else { 143 tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent) 144 cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents) 145 cp.workContent = tmpContent 146 } 147 } 148 149 if cp.placeholdersEnabled { 150 // ToC was accessed via .Page.TableOfContents in the shortcode, 151 // at a time when the ToC wasn't ready. 152 cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents) 153 } 154 155 if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled { 156 // There are one or more replacement tokens to be replaced. 157 cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders) 158 if err != nil { 159 return err 160 } 161 } 162 163 if cp.p.source.hasSummaryDivider { 164 if isHTML { 165 src := p.source.parsed.Input() 166 167 // Use the summary sections as they are provided by the user. 168 if p.source.posSummaryEnd != -1 { 169 cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd]) 170 } 171 172 if cp.p.source.posBodyStart != -1 { 173 cp.workContent = src[cp.p.source.posBodyStart:] 174 } 175 176 } else { 177 summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent) 178 if err != nil { 179 cp.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err) 180 } else { 181 cp.workContent = content 182 cp.summary = helpers.BytesToHTML(summary) 183 } 184 } 185 } else if cp.p.m.summary != "" { 186 b, err := cp.renderContent([]byte(cp.p.m.summary), false) 187 if err != nil { 188 return err 189 } 190 html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes()) 191 cp.summary = helpers.BytesToHTML(html) 192 } 193 194 cp.content = helpers.BytesToHTML(cp.workContent) 195 196 return nil 197 } 198 199 // There may be recursive loops in shortcodes and render hooks. 200 cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (any, error) { 201 return nil, initContent() 202 }) 203 204 cp.initPlain = cp.initMain.Branch(func() (any, error) { 205 cp.plain = tpl.StripHTML(string(cp.content)) 206 cp.plainWords = strings.Fields(cp.plain) 207 cp.setWordCounts(p.m.isCJKLanguage) 208 209 if err := cp.setAutoSummary(); err != nil { 210 return err, nil 211 } 212 213 return nil, nil 214 }) 215 216 return cp, nil 217 } 218 219 type renderHooks struct { 220 getRenderer hooks.GetRendererFunc 221 init sync.Once 222 } 223 224 // pageContentOutput represents the Page content for a given output format. 225 type pageContentOutput struct { 226 f output.Format 227 228 p *pageState 229 230 // Lazy load dependencies 231 initMain *lazy.Init 232 initPlain *lazy.Init 233 234 placeholdersEnabled bool 235 placeholdersEnabledInit sync.Once 236 237 // Renders Markdown hooks. 238 renderHooks *renderHooks 239 240 workContent []byte 241 dependencyTracker identity.Manager // Set in server mode. 242 243 // Temporary storage of placeholders mapped to their content. 244 // These are shortcodes etc. Some of these will need to be replaced 245 // after any markup is rendered, so they share a common prefix. 246 contentPlaceholders map[string]string 247 248 // Content sections 249 content template.HTML 250 summary template.HTML 251 tableOfContents template.HTML 252 253 truncated bool 254 255 plainWords []string 256 plain string 257 fuzzyWordCount int 258 wordCount int 259 readingTime int 260 } 261 262 func (p *pageContentOutput) trackDependency(id identity.Provider) { 263 if p.dependencyTracker != nil { 264 p.dependencyTracker.Add(id) 265 } 266 } 267 268 func (p *pageContentOutput) Reset() { 269 if p.dependencyTracker != nil { 270 p.dependencyTracker.Reset() 271 } 272 p.initMain.Reset() 273 p.initPlain.Reset() 274 p.renderHooks = &renderHooks{} 275 } 276 277 func (p *pageContentOutput) Content() (any, error) { 278 if p.p.s.initInit(p.initMain, p.p) { 279 return p.content, nil 280 } 281 return nil, nil 282 } 283 284 func (p *pageContentOutput) FuzzyWordCount() int { 285 p.p.s.initInit(p.initPlain, p.p) 286 return p.fuzzyWordCount 287 } 288 289 func (p *pageContentOutput) Len() int { 290 p.p.s.initInit(p.initMain, p.p) 291 return len(p.content) 292 } 293 294 func (p *pageContentOutput) Plain() string { 295 p.p.s.initInit(p.initPlain, p.p) 296 return p.plain 297 } 298 299 func (p *pageContentOutput) PlainWords() []string { 300 p.p.s.initInit(p.initPlain, p.p) 301 return p.plainWords 302 } 303 304 func (p *pageContentOutput) ReadingTime() int { 305 p.p.s.initInit(p.initPlain, p.p) 306 return p.readingTime 307 } 308 309 func (p *pageContentOutput) Summary() template.HTML { 310 p.p.s.initInit(p.initMain, p.p) 311 if !p.p.source.hasSummaryDivider { 312 p.p.s.initInit(p.initPlain, p.p) 313 } 314 return p.summary 315 } 316 317 func (p *pageContentOutput) TableOfContents() template.HTML { 318 p.p.s.initInit(p.initMain, p.p) 319 return p.tableOfContents 320 } 321 322 func (p *pageContentOutput) Truncated() bool { 323 if p.p.truncated { 324 return true 325 } 326 p.p.s.initInit(p.initPlain, p.p) 327 return p.truncated 328 } 329 330 func (p *pageContentOutput) WordCount() int { 331 p.p.s.initInit(p.initPlain, p.p) 332 return p.wordCount 333 } 334 335 func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) { 336 if len(args) < 1 || len(args) > 2 { 337 return "", errors.New("want 1 or 2 arguments") 338 } 339 340 var contentToRender string 341 opts := defaultRenderStringOpts 342 sidx := 1 343 344 if len(args) == 1 { 345 sidx = 0 346 } else { 347 m, ok := args[0].(map[string]any) 348 if !ok { 349 return "", errors.New("first argument must be a map") 350 } 351 352 if err := mapstructure.WeakDecode(m, &opts); err != nil { 353 return "", fmt.Errorf("failed to decode options: %w", err) 354 } 355 } 356 357 contentToRenderv := args[sidx] 358 359 if _, ok := contentToRenderv.(hstring.RenderedString); ok { 360 // This content is already rendered, this is potentially 361 // a infinite recursion. 362 return "", errors.New("text is already rendered, repeating it may cause infinite recursion") 363 } 364 365 var err error 366 contentToRender, err = cast.ToStringE(contentToRenderv) 367 if err != nil { 368 return "", err 369 } 370 371 if err = p.initRenderHooks(); err != nil { 372 return "", err 373 } 374 375 conv := p.p.getContentConverter() 376 if opts.Markup != "" && opts.Markup != p.p.m.markup { 377 var err error 378 // TODO(bep) consider cache 379 conv, err = p.p.m.newContentConverter(p.p, opts.Markup) 380 if err != nil { 381 return "", p.p.wrapError(err) 382 } 383 } 384 385 var rendered []byte 386 387 if strings.Contains(contentToRender, "{{") { 388 // Probably a shortcode. 389 parsed, err := pageparser.ParseMain(strings.NewReader(contentToRender), pageparser.Config{}) 390 if err != nil { 391 return "", err 392 } 393 pm := &pageContentMap{ 394 items: make([]any, 0, 20), 395 } 396 s := newShortcodeHandler(p.p, p.p.s) 397 398 if err := p.p.mapContentForResult( 399 parsed, 400 s, 401 pm, 402 opts.Markup, 403 nil, 404 ); err != nil { 405 return "", err 406 } 407 408 placeholders, hasShortcodeVariants, err := s.renderShortcodesForPage(p.p, p.f) 409 if err != nil { 410 return "", err 411 } 412 413 if hasShortcodeVariants { 414 p.p.pageOutputTemplateVariationsState.Store(2) 415 } 416 417 b, err := p.renderContentWithConverter(conv, p.p.contentToRender(parsed, pm, placeholders), false) 418 if err != nil { 419 return "", p.p.wrapError(err) 420 } 421 rendered = b.Bytes() 422 423 if p.placeholdersEnabled { 424 // ToC was accessed via .Page.TableOfContents in the shortcode, 425 // at a time when the ToC wasn't ready. 426 if _, err := p.p.Content(); err != nil { 427 return "", err 428 } 429 placeholders[tocShortcodePlaceholder] = string(p.tableOfContents) 430 } 431 432 if pm.hasNonMarkdownShortcode || p.placeholdersEnabled { 433 rendered, err = replaceShortcodeTokens(rendered, placeholders) 434 if err != nil { 435 return "", err 436 } 437 } 438 439 // We need a consolidated view in $page.HasShortcode 440 p.p.shortcodeState.transferNames(s) 441 442 } else { 443 c, err := p.renderContentWithConverter(conv, []byte(contentToRender), false) 444 if err != nil { 445 return "", p.p.wrapError(err) 446 } 447 448 rendered = c.Bytes() 449 } 450 451 if opts.Display == "inline" { 452 // We may have to rethink this in the future when we get other 453 // renderers. 454 rendered = p.p.s.ContentSpec.TrimShortHTML(rendered) 455 } 456 457 return template.HTML(string(rendered)), nil 458 } 459 460 func (p *pageContentOutput) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) { 461 p.p.addDependency(info) 462 return p.Render(layout...) 463 } 464 465 func (p *pageContentOutput) Render(layout ...string) (template.HTML, error) { 466 templ, found, err := p.p.resolveTemplate(layout...) 467 if err != nil { 468 return "", p.p.wrapError(err) 469 } 470 471 if !found { 472 return "", nil 473 } 474 475 p.p.addDependency(templ.(tpl.Info)) 476 477 // Make sure to send the *pageState and not the *pageContentOutput to the template. 478 res, err := executeToString(p.p.s.Tmpl(), templ, p.p) 479 if err != nil { 480 return "", p.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err)) 481 } 482 return template.HTML(res), nil 483 } 484 485 func (p *pageContentOutput) initRenderHooks() error { 486 if p == nil { 487 return nil 488 } 489 490 p.renderHooks.init.Do(func() { 491 if p.p.pageOutputTemplateVariationsState.Load() == 0 { 492 p.p.pageOutputTemplateVariationsState.Store(1) 493 } 494 495 type cacheKey struct { 496 tp hooks.RendererType 497 id any 498 f output.Format 499 } 500 501 renderCache := make(map[cacheKey]any) 502 var renderCacheMu sync.Mutex 503 504 resolvePosition := func(ctx any) text.Position { 505 var offset int 506 507 switch v := ctx.(type) { 508 case hooks.CodeblockContext: 509 offset = bytes.Index(p.p.source.parsed.Input(), []byte(v.Inner())) 510 } 511 512 pos := p.p.posFromInput(p.p.source.parsed.Input(), offset) 513 514 if pos.LineNumber > 0 { 515 // Move up to the code fence delimiter. 516 // This is in line with how we report on shortcodes. 517 pos.LineNumber = pos.LineNumber - 1 518 } 519 520 return pos 521 } 522 523 p.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any { 524 renderCacheMu.Lock() 525 defer renderCacheMu.Unlock() 526 527 key := cacheKey{tp: tp, id: id, f: p.f} 528 if r, ok := renderCache[key]; ok { 529 return r 530 } 531 532 layoutDescriptor := p.p.getLayoutDescriptor() 533 layoutDescriptor.RenderingHook = true 534 layoutDescriptor.LayoutOverride = false 535 layoutDescriptor.Layout = "" 536 537 switch tp { 538 case hooks.LinkRendererType: 539 layoutDescriptor.Kind = "render-link" 540 case hooks.ImageRendererType: 541 layoutDescriptor.Kind = "render-image" 542 case hooks.HeadingRendererType: 543 layoutDescriptor.Kind = "render-heading" 544 case hooks.CodeBlockRendererType: 545 layoutDescriptor.Kind = "render-codeblock" 546 if id != nil { 547 lang := id.(string) 548 lexer := lexers.Get(lang) 549 if lexer != nil { 550 layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",") 551 } else { 552 layoutDescriptor.KindVariants = lang 553 } 554 } 555 } 556 557 getHookTemplate := func(f output.Format) (tpl.Template, bool) { 558 templ, found, err := p.p.s.Tmpl().LookupLayout(layoutDescriptor, f) 559 if err != nil { 560 panic(err) 561 } 562 return templ, found 563 } 564 565 templ, found1 := getHookTemplate(p.f) 566 567 if p.p.reusePageOutputContent() { 568 // Check if some of the other output formats would give a different template. 569 for _, f := range p.p.s.renderFormats { 570 if f.Name == p.f.Name { 571 continue 572 } 573 templ2, found2 := getHookTemplate(f) 574 if found2 { 575 if !found1 { 576 templ = templ2 577 found1 = true 578 break 579 } 580 581 if templ != templ2 { 582 p.p.pageOutputTemplateVariationsState.Store(2) 583 break 584 } 585 } 586 } 587 } 588 if !found1 { 589 if tp == hooks.CodeBlockRendererType { 590 // No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster. 591 r := p.p.s.ContentSpec.Converters.GetHighlighter() 592 renderCache[key] = r 593 return r 594 } 595 return nil 596 } 597 598 r := hookRendererTemplate{ 599 templateHandler: p.p.s.Tmpl(), 600 SearchProvider: templ.(identity.SearchProvider), 601 templ: templ, 602 resolvePosition: resolvePosition, 603 } 604 renderCache[key] = r 605 return r 606 } 607 }) 608 609 return nil 610 } 611 612 func (p *pageContentOutput) setAutoSummary() error { 613 if p.p.source.hasSummaryDivider || p.p.m.summary != "" { 614 return nil 615 } 616 617 var summary string 618 var truncated bool 619 620 if p.p.m.isCJKLanguage { 621 summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords) 622 } else { 623 summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain) 624 } 625 p.summary = template.HTML(summary) 626 627 p.truncated = truncated 628 629 return nil 630 } 631 632 func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) { 633 if err := cp.initRenderHooks(); err != nil { 634 return nil, err 635 } 636 c := cp.p.getContentConverter() 637 return cp.renderContentWithConverter(c, content, renderTOC) 638 } 639 640 func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) { 641 r, err := c.Convert( 642 converter.RenderContext{ 643 Src: content, 644 RenderTOC: renderTOC, 645 GetRenderer: cp.renderHooks.getRenderer, 646 }) 647 648 if err == nil { 649 if ids, ok := r.(identity.IdentitiesProvider); ok { 650 for _, v := range ids.GetIdentities() { 651 cp.trackDependency(v) 652 } 653 } 654 } 655 656 return r, err 657 } 658 659 func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) { 660 if isCJKLanguage { 661 p.wordCount = 0 662 for _, word := range p.plainWords { 663 runeCount := utf8.RuneCountInString(word) 664 if len(word) == runeCount { 665 p.wordCount++ 666 } else { 667 p.wordCount += runeCount 668 } 669 } 670 } else { 671 p.wordCount = helpers.TotalWords(p.plain) 672 } 673 674 // TODO(bep) is set in a test. Fix that. 675 if p.fuzzyWordCount == 0 { 676 p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100 677 } 678 679 if isCJKLanguage { 680 p.readingTime = (p.wordCount + 500) / 501 681 } else { 682 p.readingTime = (p.wordCount + 212) / 213 683 } 684 } 685 686 // A callback to signal that we have inserted a placeholder into the rendered 687 // content. This avoids doing extra replacement work. 688 func (p *pageContentOutput) enablePlaceholders() { 689 p.placeholdersEnabledInit.Do(func() { 690 p.placeholdersEnabled = true 691 }) 692 } 693 694 // these will be shifted out when rendering a given output format. 695 type pagePerOutputProviders interface { 696 targetPather 697 page.PaginatorProvider 698 resource.ResourceLinksProvider 699 } 700 701 type targetPather interface { 702 targetPaths() page.TargetPaths 703 } 704 705 type targetPathsHolder struct { 706 paths page.TargetPaths 707 page.OutputFormat 708 } 709 710 func (t targetPathsHolder) targetPaths() page.TargetPaths { 711 return t.paths 712 } 713 714 func executeToString(h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) { 715 b := bp.GetBuffer() 716 defer bp.PutBuffer(b) 717 if err := h.Execute(templ, b, data); err != nil { 718 return "", err 719 } 720 return b.String(), nil 721 } 722 723 func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) { 724 defer func() { 725 if r := recover(); r != nil { 726 err = fmt.Errorf("summary split failed: %s", r) 727 } 728 }() 729 730 startDivider := bytes.Index(c, internalSummaryDividerBaseBytes) 731 732 if startDivider == -1 { 733 return 734 } 735 736 startTag := "p" 737 switch markup { 738 case "asciidocext": 739 startTag = "div" 740 } 741 742 // Walk back and forward to the surrounding tags. 743 start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag)) 744 end := bytes.Index(c[startDivider:], []byte("</"+startTag)) 745 746 if start == -1 { 747 start = startDivider 748 } else { 749 start = startDivider - (startDivider - start) 750 } 751 752 if end == -1 { 753 end = startDivider + len(internalSummaryDividerBase) 754 } else { 755 end = startDivider + end + len(startTag) + 3 756 } 757 758 var addDiv bool 759 760 switch markup { 761 case "rst": 762 addDiv = true 763 } 764 765 withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...) 766 767 if len(withoutDivider) > 0 { 768 summary = bytes.TrimSpace(withoutDivider[:start]) 769 } 770 771 if addDiv { 772 // For the rst 773 summary = append(append([]byte(nil), summary...), []byte("</div>")...) 774 } 775 776 if err != nil { 777 return 778 } 779 780 content = bytes.TrimSpace(withoutDivider) 781 782 return 783 }