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