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