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 }