github.com/neohugo/neohugo@v0.123.8/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 "errors" 20 "fmt" 21 "html/template" 22 "strings" 23 "sync" 24 25 "github.com/neohugo/neohugo/common/text" 26 "github.com/neohugo/neohugo/common/types/hstring" 27 "github.com/neohugo/neohugo/identity" 28 "github.com/neohugo/neohugo/parser/pageparser" 29 30 "github.com/mitchellh/mapstructure" 31 "github.com/spf13/cast" 32 33 "github.com/neohugo/neohugo/markup/converter/hooks" 34 "github.com/neohugo/neohugo/markup/highlight/chromalexers" 35 "github.com/neohugo/neohugo/markup/tableofcontents" 36 37 "github.com/neohugo/neohugo/markup/converter" 38 39 bp "github.com/neohugo/neohugo/bufferpool" 40 "github.com/neohugo/neohugo/tpl" 41 42 "github.com/neohugo/neohugo/helpers" 43 "github.com/neohugo/neohugo/output" 44 "github.com/neohugo/neohugo/resources/page" 45 "github.com/neohugo/neohugo/resources/resource" 46 ) 47 48 var ( 49 nopTargetPath = targetPathsHolder{} 50 nopPagePerOutput = struct { 51 resource.ResourceLinksProvider 52 page.ContentProvider 53 page.PageRenderProvider 54 page.PaginatorProvider 55 page.TableOfContentsProvider 56 page.AlternativeOutputFormatsProvider 57 58 targetPather 59 }{ 60 page.NopPage, 61 page.NopPage, 62 page.NopPage, 63 page.NopPage, 64 page.NopPage, 65 page.NopPage, 66 nopTargetPath, 67 } 68 ) 69 70 func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) { 71 cp := &pageContentOutput{ 72 po: po, 73 renderHooks: &renderHooks{}, 74 } 75 return cp, nil 76 } 77 78 type renderHooks struct { 79 getRenderer hooks.GetRendererFunc 80 init sync.Once 81 } 82 83 // pageContentOutput represents the Page content for a given output format. 84 type pageContentOutput struct { 85 po *pageOutput 86 87 contentRenderedVersion int // Incremented on reset. 88 contentRendered bool // Set on content render. 89 90 // Renders Markdown hooks. 91 renderHooks *renderHooks 92 } 93 94 func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) { 95 pco.po.p.dependencyManagerOutput.AddIdentity(idp.GetIdentity()) 96 } 97 98 func (pco *pageContentOutput) Reset() { 99 if pco == nil { 100 return 101 } 102 pco.contentRenderedVersion++ 103 pco.contentRendered = false 104 pco.renderHooks = &renderHooks{} 105 } 106 107 func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { 108 return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents 109 } 110 111 func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) { 112 content := pco.po.p.m.content 113 source, err := content.pi.contentSource(content) 114 if err != nil { 115 return "", err 116 } 117 ct, err := content.contentToC(ctx, pco) 118 if err != nil { 119 return "", err 120 } 121 122 var insertPlaceholders bool 123 var hasVariants bool 124 cb := setGetContentCallbackInContext.Get(ctx) 125 if cb != nil { 126 insertPlaceholders = true 127 } 128 c := make([]byte, 0, len(source)+(len(source)/10)) 129 for _, it := range content.pi.itemsStep2 { 130 switch v := it.(type) { 131 case pageparser.Item: 132 c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) 133 case pageContentReplacement: 134 // Ignore. 135 case *shortcode: 136 if !insertPlaceholders || !v.insertPlaceholder() { 137 // Insert the rendered shortcode. 138 renderedShortcode, found := ct.contentPlaceholders[v.placeholder] 139 if !found { 140 // This should never happen. 141 panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) 142 } 143 144 b, more, err := renderedShortcode.renderShortcode(ctx) 145 if err != nil { 146 return "", fmt.Errorf("failed to render shortcode: %w", err) 147 } 148 hasVariants = hasVariants || more 149 c = append(c, []byte(b)...) 150 151 } else { 152 // Insert the placeholder so we can insert the content after 153 // markdown processing. 154 c = append(c, []byte(v.placeholder)...) 155 } 156 default: 157 panic(fmt.Sprintf("unknown item type %T", it)) 158 } 159 } 160 161 if hasVariants { 162 pco.po.p.pageOutputTemplateVariationsState.Add(1) 163 } 164 165 if cb != nil { 166 cb(pco, ct) 167 } 168 169 return helpers.BytesToHTML(c), nil 170 } 171 172 func (pco *pageContentOutput) Content(ctx context.Context) (any, error) { 173 r, err := pco.po.p.m.content.contentRendered(ctx, pco) 174 return r.content, err 175 } 176 177 func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { 178 return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML 179 } 180 181 func (p *pageContentOutput) Len(ctx context.Context) int { 182 return len(p.mustContentRendered(ctx).content) 183 } 184 185 func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary { 186 r, err := pco.po.p.m.content.contentRendered(ctx, pco) 187 if err != nil { 188 pco.fail(err) 189 } 190 return r 191 } 192 193 func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords { 194 r, err := pco.po.p.m.content.contentPlain(ctx, pco) 195 if err != nil { 196 pco.fail(err) 197 } 198 return r 199 } 200 201 func (pco *pageContentOutput) fail(err error) { 202 pco.po.p.s.h.FatalError(pco.po.p.wrapError(err)) 203 } 204 205 func (pco *pageContentOutput) Plain(ctx context.Context) string { 206 return pco.mustContentPlain(ctx).plain 207 } 208 209 func (pco *pageContentOutput) PlainWords(ctx context.Context) []string { 210 return pco.mustContentPlain(ctx).plainWords 211 } 212 213 func (pco *pageContentOutput) ReadingTime(ctx context.Context) int { 214 return pco.mustContentPlain(ctx).readingTime 215 } 216 217 func (pco *pageContentOutput) WordCount(ctx context.Context) int { 218 return pco.mustContentPlain(ctx).wordCount 219 } 220 221 func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int { 222 return pco.mustContentPlain(ctx).fuzzyWordCount 223 } 224 225 func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML { 226 return pco.mustContentPlain(ctx).summary 227 } 228 229 func (pco *pageContentOutput) Truncated(ctx context.Context) bool { 230 return pco.mustContentPlain(ctx).summaryTruncated 231 } 232 233 func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) { 234 if len(args) < 1 || len(args) > 2 { 235 return "", errors.New("want 1 or 2 arguments") 236 } 237 238 var contentToRender string 239 opts := defaultRenderStringOpts 240 sidx := 1 241 242 if len(args) == 1 { 243 sidx = 0 244 } else { 245 m, ok := args[0].(map[string]any) 246 if !ok { 247 return "", errors.New("first argument must be a map") 248 } 249 250 if err := mapstructure.WeakDecode(m, &opts); err != nil { 251 return "", fmt.Errorf("failed to decode options: %w", err) 252 } 253 } 254 255 contentToRenderv := args[sidx] 256 257 if _, ok := contentToRenderv.(hstring.RenderedString); ok { 258 // This content is already rendered, this is potentially 259 // a infinite recursion. 260 return "", errors.New("text is already rendered, repeating it may cause infinite recursion") 261 } 262 263 var err error 264 contentToRender, err = cast.ToStringE(contentToRenderv) 265 if err != nil { 266 return "", err 267 } 268 269 if err = pco.initRenderHooks(); err != nil { 270 return "", err 271 } 272 273 conv := pco.po.p.getContentConverter() 274 if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.Markup { 275 var err error 276 conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) 277 if err != nil { 278 return "", pco.po.p.wrapError(err) 279 } 280 } 281 282 var rendered []byte 283 284 parseInfo := &contentParseInfo{ 285 h: pco.po.p.s.h, 286 pid: pco.po.p.pid, 287 } 288 289 if pageparser.HasShortcode(contentToRender) { 290 contentToRenderb := []byte(contentToRender) 291 // String contains a shortcode. 292 parseInfo.itemsStep1, err = pageparser.ParseBytesMain(contentToRenderb, pageparser.Config{}) 293 if err != nil { 294 return "", err 295 } 296 297 s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s) 298 if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil { 299 return "", err 300 } 301 302 placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true) 303 if err != nil { 304 return "", err 305 } 306 307 contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders) 308 if err != nil { 309 return "", err 310 } 311 if hasVariants { 312 pco.po.p.pageOutputTemplateVariationsState.Add(1) 313 } 314 b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false) 315 if err != nil { 316 return "", pco.po.p.wrapError(err) 317 } 318 rendered = b.Bytes() 319 320 if parseInfo.hasNonMarkdownShortcode { 321 var hasShortcodeVariants bool 322 323 tokenHandler := func(ctx context.Context, token string) ([]byte, error) { 324 if token == tocShortcodePlaceholder { 325 toc, err := pco.po.p.m.content.contentToC(ctx, pco) 326 if err != nil { 327 return nil, err 328 } 329 // The Page's TableOfContents was accessed in a shortcode. 330 return []byte(toc.tableOfContentsHTML), nil 331 } 332 renderer, found := placeholders[token] 333 if found { 334 repl, more, err := renderer.renderShortcode(ctx) 335 if err != nil { 336 return nil, err 337 } 338 hasShortcodeVariants = hasShortcodeVariants || more 339 return repl, nil 340 } 341 // This should not happen. 342 return nil, fmt.Errorf("unknown shortcode token %q", token) 343 } 344 345 rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler) 346 if err != nil { 347 return "", err 348 } 349 if hasShortcodeVariants { 350 pco.po.p.pageOutputTemplateVariationsState.Add(1) 351 } 352 } 353 354 // We need a consolidated view in $page.HasShortcode 355 pco.po.p.m.content.shortcodeState.transferNames(s) 356 357 } else { 358 c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) 359 if err != nil { 360 return "", pco.po.p.wrapError(err) 361 } 362 363 rendered = c.Bytes() 364 } 365 366 if opts.Display == "inline" { 367 // We may have to rethink this in the future when we get other 368 // renderers. 369 rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered) 370 } 371 372 return template.HTML(string(rendered)), nil 373 } 374 375 func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) { 376 if len(layout) == 0 { 377 return "", errors.New("no layout given") 378 } 379 templ, found, err := pco.po.p.resolveTemplate(layout...) 380 if err != nil { 381 return "", pco.po.p.wrapError(err) 382 } 383 384 if !found { 385 return "", nil 386 } 387 388 // Make sure to send the *pageState and not the *pageContentOutput to the template. 389 res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p) 390 if err != nil { 391 return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err)) 392 } 393 return template.HTML(res), nil 394 } 395 396 func (pco *pageContentOutput) initRenderHooks() error { 397 if pco == nil { 398 return nil 399 } 400 401 pco.renderHooks.init.Do(func() { 402 if pco.po.p.pageOutputTemplateVariationsState.Load() == 0 { 403 pco.po.p.pageOutputTemplateVariationsState.Store(1) 404 } 405 406 type cacheKey struct { 407 tp hooks.RendererType 408 id any 409 f output.Format 410 } 411 412 renderCache := make(map[cacheKey]any) 413 var renderCacheMu sync.Mutex 414 415 resolvePosition := func(ctx any) text.Position { 416 source := pco.po.p.m.content.mustSource() 417 var offset int 418 419 switch v := ctx.(type) { 420 case hooks.CodeblockContext: 421 offset = bytes.Index(source, []byte(v.Inner())) 422 } 423 424 pos := pco.po.p.posFromInput(source, offset) 425 426 if pos.LineNumber > 0 { 427 // Move up to the code fence delimiter. 428 // This is in line with how we report on shortcodes. 429 pos.LineNumber = pos.LineNumber - 1 430 } 431 432 return pos 433 } 434 435 pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any { 436 renderCacheMu.Lock() 437 defer renderCacheMu.Unlock() 438 439 key := cacheKey{tp: tp, id: id, f: pco.po.f} 440 if r, ok := renderCache[key]; ok { 441 return r 442 } 443 444 layoutDescriptor := pco.po.p.getLayoutDescriptor() 445 layoutDescriptor.RenderingHook = true 446 layoutDescriptor.LayoutOverride = false 447 layoutDescriptor.Layout = "" 448 449 switch tp { 450 case hooks.LinkRendererType: 451 layoutDescriptor.Kind = "render-link" 452 case hooks.ImageRendererType: 453 layoutDescriptor.Kind = "render-image" 454 case hooks.HeadingRendererType: 455 layoutDescriptor.Kind = "render-heading" 456 case hooks.CodeBlockRendererType: 457 layoutDescriptor.Kind = "render-codeblock" 458 if id != nil { 459 lang := id.(string) 460 lexer := chromalexers.Get(lang) 461 if lexer != nil { 462 layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",") 463 } else { 464 layoutDescriptor.KindVariants = lang 465 } 466 } 467 } 468 469 getHookTemplate := func(f output.Format) (tpl.Template, bool) { 470 templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f) 471 if err != nil { 472 panic(err) 473 } 474 if found { 475 if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() { 476 renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks 477 switch templ.Name() { 478 case "_default/_markup/render-link.html": 479 if !renderHookConfig.Link.IsEnableDefault() { 480 return nil, false 481 } 482 case "_default/_markup/render-image.html": 483 if !renderHookConfig.Image.IsEnableDefault() { 484 return nil, false 485 } 486 } 487 } 488 } 489 return templ, found 490 } 491 492 templ, found1 := getHookTemplate(pco.po.f) 493 494 if pco.po.p.reusePageOutputContent() { 495 // Check if some of the other output formats would give a different template. 496 for _, f := range pco.po.p.s.renderFormats { 497 if f.Name == pco.po.f.Name { 498 continue 499 } 500 templ2, found2 := getHookTemplate(f) 501 if found2 { 502 if !found1 { 503 templ = templ2 504 found1 = true 505 break 506 } 507 508 if templ != templ2 { 509 pco.po.p.pageOutputTemplateVariationsState.Add(1) 510 break 511 } 512 } 513 } 514 } 515 if !found1 { 516 if tp == hooks.CodeBlockRendererType { 517 // No user provided template for code blocks, so we use the native Go version -- which is also faster. 518 r := pco.po.p.s.ContentSpec.Converters.GetHighlighter() 519 renderCache[key] = r 520 return r 521 } 522 return nil 523 } 524 525 r := hookRendererTemplate{ 526 templateHandler: pco.po.p.s.Tmpl(), 527 templ: templ, 528 resolvePosition: resolvePosition, 529 } 530 renderCache[key] = r 531 return r 532 } 533 }) 534 535 return nil 536 } 537 538 func (pco *pageContentOutput) getContentConverter() (converter.Converter, error) { 539 if err := pco.initRenderHooks(); err != nil { 540 return nil, err 541 } 542 return pco.po.p.getContentConverter(), nil 543 } 544 545 func (cp *pageContentOutput) ParseAndRenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.ResultRender, error) { 546 c, err := cp.getContentConverter() 547 if err != nil { 548 return nil, err 549 } 550 return cp.renderContentWithConverter(ctx, c, content, renderTOC) 551 } 552 553 func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) { 554 c, err := pco.getContentConverter() 555 if err != nil { 556 return nil, false, err 557 } 558 p, ok := c.(converter.ParseRenderer) 559 if !ok { 560 return nil, ok, nil 561 } 562 rctx := converter.RenderContext{ 563 Ctx: ctx, 564 Src: content, 565 RenderTOC: true, 566 GetRenderer: pco.renderHooks.getRenderer, 567 } 568 r, err := p.Parse(rctx) 569 return r, ok, err 570 } 571 572 func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) { 573 c, err := pco.getContentConverter() 574 if err != nil { 575 return nil, false, err 576 } 577 p, ok := c.(converter.ParseRenderer) 578 if !ok { 579 return nil, ok, nil 580 } 581 rctx := converter.RenderContext{ 582 Ctx: ctx, 583 Src: content, 584 RenderTOC: true, 585 GetRenderer: pco.renderHooks.getRenderer, 586 } 587 r, err := p.Render(rctx, doc) 588 return r, ok, err 589 } 590 591 func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) { 592 r, err := c.Convert( 593 converter.RenderContext{ 594 Ctx: ctx, 595 Src: content, 596 RenderTOC: renderTOC, 597 GetRenderer: pco.renderHooks.getRenderer, 598 }) 599 return r, err 600 } 601 602 // these will be shifted out when rendering a given output format. 603 type pagePerOutputProviders interface { 604 targetPather 605 page.PaginatorProvider 606 resource.ResourceLinksProvider 607 } 608 609 type targetPather interface { 610 targetPaths() page.TargetPaths 611 } 612 613 type targetPathsHolder struct { 614 paths page.TargetPaths 615 page.OutputFormat 616 } 617 618 func (t targetPathsHolder) targetPaths() page.TargetPaths { 619 return t.paths 620 } 621 622 func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) { 623 b := bp.GetBuffer() 624 defer bp.PutBuffer(b) 625 if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil { 626 return "", err 627 } 628 return b.String(), nil 629 } 630 631 func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) { 632 defer func() { 633 if r := recover(); r != nil { 634 err = fmt.Errorf("summary split failed: %s", r) 635 } 636 }() 637 638 startDivider := bytes.Index(c, internalSummaryDividerBaseBytes) 639 640 if startDivider == -1 { 641 return 642 } 643 644 startTag := "p" 645 switch markup { 646 case "asciidocext": 647 startTag = "div" 648 } 649 650 // Walk back and forward to the surrounding tags. 651 start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag)) 652 end := bytes.Index(c[startDivider:], []byte("</"+startTag)) 653 654 if start == -1 { 655 start = startDivider 656 } else { 657 start = startDivider - (startDivider - start) 658 } 659 660 if end == -1 { 661 end = startDivider + len(internalSummaryDividerBase) 662 } else { 663 end = startDivider + end + len(startTag) + 3 664 } 665 666 var addDiv bool 667 668 switch markup { 669 case "rst": 670 addDiv = true 671 } 672 673 withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...) 674 675 if len(withoutDivider) > 0 { 676 summary = bytes.TrimSpace(withoutDivider[:start]) 677 } 678 679 if addDiv { 680 // For the rst 681 summary = append(append([]byte(nil), summary...), []byte("</div>")...) 682 } 683 684 if err != nil { 685 return 686 } 687 688 content = bytes.TrimSpace(withoutDivider) 689 690 return 691 }