page__content.go - hugo - [fork] hugo port for 9front
HTML git clone https://git.drkhsh.at/hugo.git
DIR Log
DIR Files
DIR Refs
DIR Submodules
DIR README
DIR LICENSE
---
page__content.go (30685B)
---
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 "context"
18 "errors"
19 "fmt"
20 "html/template"
21 "io"
22 "path/filepath"
23 "strconv"
24 "strings"
25 "unicode/utf8"
26
27 maps0 "maps"
28
29 "github.com/bep/logg"
30 "github.com/gohugoio/hugo/common/hcontext"
31 "github.com/gohugoio/hugo/common/herrors"
32 "github.com/gohugoio/hugo/common/hugio"
33 "github.com/gohugoio/hugo/common/hugo"
34 "github.com/gohugoio/hugo/common/maps"
35 "github.com/gohugoio/hugo/common/types/hstring"
36 "github.com/gohugoio/hugo/helpers"
37 "github.com/gohugoio/hugo/markup"
38 "github.com/gohugoio/hugo/markup/converter"
39 "github.com/gohugoio/hugo/markup/goldmark/hugocontext"
40 "github.com/gohugoio/hugo/markup/tableofcontents"
41 "github.com/gohugoio/hugo/parser/metadecoders"
42 "github.com/gohugoio/hugo/parser/pageparser"
43 "github.com/gohugoio/hugo/resources"
44 "github.com/gohugoio/hugo/resources/page"
45 "github.com/gohugoio/hugo/resources/resource"
46 "github.com/gohugoio/hugo/tpl"
47 "github.com/mitchellh/mapstructure"
48 "github.com/spf13/cast"
49 )
50
51 const (
52 internalSummaryDividerBase = "HUGOMORE42"
53 )
54
55 var (
56 internalSummaryDividerPreString = "\n\n" + internalSummaryDividerBase + "\n\n"
57 internalSummaryDividerPre = []byte(internalSummaryDividerPreString)
58 )
59
60 type pageContentReplacement struct {
61 val []byte
62
63 source pageparser.Item
64 }
65
66 func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) {
67 var (
68 sourceKey string
69 openSource hugio.OpenReadSeekCloser
70 isFromContentAdapter = m.pageConfig.IsFromContentAdapter
71 )
72
73 if m.f != nil && !isFromContentAdapter {
74 sourceKey = filepath.ToSlash(m.f.Filename())
75 if !isFromContentAdapter {
76 meta := m.f.FileInfo().Meta()
77 openSource = func() (hugio.ReadSeekCloser, error) {
78 r, err := meta.Open()
79 if err != nil {
80 return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
81 }
82 return r, nil
83 }
84 }
85 } else if isFromContentAdapter {
86 openSource = m.pageConfig.Content.ValueAsOpenReadSeekCloser()
87 }
88
89 if sourceKey == "" {
90 sourceKey = strconv.FormatUint(pid, 10)
91 }
92
93 pi := &contentParseInfo{
94 h: h,
95 pid: pid,
96 sourceKey: sourceKey,
97 openSource: openSource,
98 }
99
100 source, err := pi.contentSource(m)
101 if err != nil {
102 return nil, err
103 }
104
105 items, err := pageparser.ParseBytes(
106 source,
107 pageparser.Config{
108 NoFrontMatter: isFromContentAdapter,
109 },
110 )
111 if err != nil {
112 return nil, err
113 }
114
115 pi.itemsStep1 = items
116
117 if isFromContentAdapter {
118 // No front matter.
119 return pi, nil
120 }
121
122 if err := pi.mapFrontMatter(source); err != nil {
123 return nil, err
124 }
125
126 return pi, nil
127 }
128
129 func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) {
130 var filename string
131 if m.f != nil {
132 filename = m.f.Filename()
133 }
134
135 c := &cachedContent{
136 pm: m.s.pageMap,
137 StaleInfo: m,
138 shortcodeState: newShortcodeHandler(filename, m.s),
139 pi: pi,
140 enableEmoji: m.s.conf.EnableEmoji,
141 scopes: maps.NewCache[string, *cachedContentScope](),
142 }
143
144 source, err := c.pi.contentSource(m)
145 if err != nil {
146 return nil, err
147 }
148
149 if err := c.parseContentFile(source); err != nil {
150 return nil, err
151 }
152
153 return c, nil
154 }
155
156 type cachedContent struct {
157 pm *pageMap
158
159 resource.StaleInfo
160
161 shortcodeState *shortcodeHandler
162
163 // Parsed content.
164 pi *contentParseInfo
165
166 enableEmoji bool
167
168 scopes *maps.Cache[string, *cachedContentScope]
169 }
170
171 func (c *cachedContent) getOrCreateScope(scope string, pco *pageContentOutput) *cachedContentScope {
172 key := scope + pco.po.f.Name
173 cs, _ := c.scopes.GetOrCreate(key, func() (*cachedContentScope, error) {
174 return &cachedContentScope{
175 cachedContent: c,
176 pco: pco,
177 scope: scope,
178 }, nil
179 })
180 return cs
181 }
182
183 type contentParseInfo struct {
184 h *HugoSites
185
186 pid uint64
187 sourceKey string
188
189 // The source bytes.
190 openSource hugio.OpenReadSeekCloser
191
192 frontMatter map[string]any
193
194 // Whether the parsed content contains a summary separator.
195 hasSummaryDivider bool
196
197 // Returns the position in bytes after any front matter.
198 posMainContent int
199
200 // Indicates whether we must do placeholder replacements.
201 hasNonMarkdownShortcode bool
202
203 // Items from the page parser.
204 // These maps directly to the source
205 itemsStep1 pageparser.Items
206
207 // *shortcode, pageContentReplacement or pageparser.Item
208 itemsStep2 []any
209 }
210
211 func (p *contentParseInfo) AddBytes(item pageparser.Item) {
212 p.itemsStep2 = append(p.itemsStep2, item)
213 }
214
215 func (p *contentParseInfo) AddReplacement(val []byte, source pageparser.Item) {
216 p.itemsStep2 = append(p.itemsStep2, pageContentReplacement{val: val, source: source})
217 }
218
219 func (p *contentParseInfo) AddShortcode(s *shortcode) {
220 p.itemsStep2 = append(p.itemsStep2, s)
221 if s.insertPlaceholder() {
222 p.hasNonMarkdownShortcode = true
223 }
224 }
225
226 // contentToRenderForItems returns the content to be processed by Goldmark or similar.
227 func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) {
228 var hasVariants bool
229 c := make([]byte, 0, len(source)+(len(source)/10))
230
231 for _, it := range pi.itemsStep2 {
232 switch v := it.(type) {
233 case pageparser.Item:
234 c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
235 case pageContentReplacement:
236 c = append(c, v.val...)
237 case *shortcode:
238 if !v.insertPlaceholder() {
239 // Insert the rendered shortcode.
240 renderedShortcode, found := renderedShortcodes[v.placeholder]
241 if !found {
242 // This should never happen.
243 panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
244 }
245
246 b, more, err := renderedShortcode.renderShortcode(ctx)
247 if err != nil {
248 return nil, false, fmt.Errorf("failed to render shortcode: %w", err)
249 }
250 hasVariants = hasVariants || more
251 c = append(c, []byte(b)...)
252
253 } else {
254 // Insert the placeholder so we can insert the content after
255 // markdown processing.
256 c = append(c, []byte(v.placeholder)...)
257 }
258 default:
259 panic(fmt.Sprintf("unknown item type %T", it))
260 }
261 }
262
263 return c, hasVariants, nil
264 }
265
266 func (c *cachedContent) IsZero() bool {
267 return len(c.pi.itemsStep2) == 0
268 }
269
270 func (c *cachedContent) parseContentFile(source []byte) error {
271 if source == nil || c.pi.openSource == nil {
272 return nil
273 }
274
275 return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState)
276 }
277
278 func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error {
279 if c.frontMatter != nil {
280 return nil
281 }
282
283 f := pageparser.FormatFromFrontMatterType(it.Type)
284 var err error
285 c.frontMatter, err = metadecoders.Default.UnmarshalToMap(it.Val(source), f)
286 if err != nil {
287 if fe, ok := err.(herrors.FileError); ok {
288 pos := fe.Position()
289
290 // Offset the starting position of front matter.
291 offset := iter.LineNumber(source) - 1
292 if f == metadecoders.YAML {
293 offset -= 1
294 }
295 pos.LineNumber += offset
296
297 fe.UpdatePosition(pos)
298 fe.SetFilename("") // It will be set later.
299
300 return fe
301 } else {
302 return err
303 }
304 }
305
306 return nil
307 }
308
309 func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error {
310 if fe, ok := err.(herrors.FileError); ok {
311 return fe
312 }
313
314 pos := posFromInput("", source, i.Pos())
315
316 return herrors.NewFileErrorFromPos(err, pos)
317 }
318
319 func (rn *contentParseInfo) mapFrontMatter(source []byte) error {
320 if len(rn.itemsStep1) == 0 {
321 return nil
322 }
323 iter := pageparser.NewIterator(rn.itemsStep1)
324
325 Loop:
326 for {
327 it := iter.Next()
328 switch {
329 case it.IsFrontMatter():
330 if err := rn.parseFrontMatter(it, iter, source); err != nil {
331 return err
332 }
333 next := iter.Peek()
334 if !next.IsDone() {
335 rn.posMainContent = next.Pos()
336 }
337 // Done.
338 break Loop
339 case it.IsEOF():
340 break Loop
341 case it.IsError():
342 return rn.failMap(source, it.Err, it)
343 default:
344
345 }
346 }
347
348 return nil
349 }
350
351 func (rn *contentParseInfo) mapItemsAfterFrontMatter(
352 source []byte,
353 s *shortcodeHandler,
354 ) error {
355 if len(rn.itemsStep1) == 0 {
356 return nil
357 }
358
359 fail := func(err error, i pageparser.Item) error {
360 if fe, ok := err.(herrors.FileError); ok {
361 return fe
362 }
363
364 pos := posFromInput("", source, i.Pos())
365
366 return herrors.NewFileErrorFromPos(err, pos)
367 }
368
369 iter := pageparser.NewIterator(rn.itemsStep1)
370
371 // the parser is guaranteed to return items in proper order or fail, so …
372 // … it's safe to keep some "global" state
373 var ordinal int
374
375 Loop:
376 for {
377 it := iter.Next()
378
379 switch {
380 case it.Type == pageparser.TypeIgnore:
381 case it.IsFrontMatter():
382 // Ignore.
383 case it.Type == pageparser.TypeLeadSummaryDivider:
384 posBody := -1
385 f := func(item pageparser.Item) bool {
386 if posBody == -1 && !item.IsDone() {
387 posBody = item.Pos()
388 }
389
390 if item.IsNonWhitespace(source) {
391 // Done
392 return false
393 }
394 return true
395 }
396 iter.PeekWalk(f)
397
398 rn.hasSummaryDivider = true
399
400 // The content may be rendered by Goldmark or similar,
401 // and we need to track the summary.
402 rn.AddReplacement(internalSummaryDividerPre, it)
403
404 // Handle shortcode
405 case it.IsLeftShortcodeDelim():
406 // let extractShortcode handle left delim (will do so recursively)
407 iter.Backup()
408
409 currShortcode, err := s.extractShortcode(ordinal, 0, source, iter)
410 if err != nil {
411 return fail(err, it)
412 }
413
414 currShortcode.pos = it.Pos()
415 currShortcode.length = iter.Current().Pos() - it.Pos()
416 if currShortcode.placeholder == "" {
417 currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, currShortcode.ordinal)
418 }
419
420 if currShortcode.name != "" {
421 s.addName(currShortcode.name)
422 }
423
424 if currShortcode.params == nil {
425 var s []string
426 currShortcode.params = s
427 }
428
429 currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, ordinal)
430 ordinal++
431 s.shortcodes = append(s.shortcodes, currShortcode)
432
433 rn.AddShortcode(currShortcode)
434
435 case it.IsEOF():
436 break Loop
437 case it.IsError():
438 return fail(it.Err, it)
439 default:
440 rn.AddBytes(it)
441 }
442 }
443
444 return nil
445 }
446
447 func (c *cachedContent) mustSource() []byte {
448 source, err := c.pi.contentSource(c)
449 if err != nil {
450 panic(err)
451 }
452 return source
453 }
454
455 func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
456 key := c.sourceKey
457 versionv := s.StaleVersion()
458
459 v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
460 b, err := c.readSourceAll()
461 if err != nil {
462 return nil, err
463 }
464
465 return &resources.StaleValue[[]byte]{
466 Value: b,
467 StaleVersionFunc: func() uint32 {
468 return s.StaleVersion() - versionv
469 },
470 }, nil
471 })
472 if err != nil {
473 return nil, err
474 }
475
476 return v.Value, nil
477 }
478
479 func (c *contentParseInfo) readSourceAll() ([]byte, error) {
480 if c.openSource == nil {
481 return []byte{}, nil
482 }
483 r, err := c.openSource()
484 if err != nil {
485 return nil, err
486 }
487 defer r.Close()
488
489 return io.ReadAll(r)
490 }
491
492 type contentTableOfContents struct {
493 // For Goldmark we split Parse and Render.
494 astDoc any
495
496 tableOfContents *tableofcontents.Fragments
497 tableOfContentsHTML template.HTML
498
499 // Temporary storage of placeholders mapped to their content.
500 // These are shortcodes etc. Some of these will need to be replaced
501 // after any markup is rendered, so they share a common prefix.
502 contentPlaceholders map[string]shortcodeRenderer
503
504 contentToRender []byte
505 }
506
507 type contentSummary struct {
508 content template.HTML
509 contentWithoutSummary template.HTML
510 summary page.Summary
511 }
512
513 type contentPlainPlainWords struct {
514 plain string
515 plainWords []string
516
517 wordCount int
518 fuzzyWordCount int
519 readingTime int
520 }
521
522 func (c *cachedContentScope) keyScope(ctx context.Context) string {
523 return hugo.GetMarkupScope(ctx) + c.pco.po.f.Name
524 }
525
526 func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummary, error) {
527 cp := c.pco
528 ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
529 key := c.pi.sourceKey + "/" + c.keyScope(ctx)
530 versionv := c.version(cp)
531
532 v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
533 cp.po.p.s.Log.Trace(logg.StringFunc(func() string {
534 return fmt.Sprintln("contentRendered", key)
535 }))
536
537 cp.po.p.s.h.contentRenderCounter.Add(1)
538 cp.contentRendered.Store(true)
539 po := cp.po
540
541 ct, err := c.contentToC(ctx)
542 if err != nil {
543 return nil, err
544 }
545
546 rs, err := func() (*resources.StaleValue[contentSummary], error) {
547 rs := &resources.StaleValue[contentSummary]{
548 StaleVersionFunc: func() uint32 {
549 return c.version(cp) - versionv
550 },
551 }
552
553 if len(c.pi.itemsStep2) == 0 {
554 // Nothing to do.
555 return rs, nil
556 }
557
558 var b []byte
559
560 if ct.astDoc != nil {
561 // The content is parsed, but not rendered.
562 r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
563 if err != nil {
564 return nil, err
565 }
566
567 if !ok {
568 return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
569 }
570
571 b = r.Bytes()
572
573 } else {
574 // Copy the content to be rendered.
575 b = make([]byte, len(ct.contentToRender))
576 copy(b, ct.contentToRender)
577 }
578
579 // There are one or more replacement tokens to be replaced.
580 var hasShortcodeVariants bool
581 tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
582 if token == tocShortcodePlaceholder {
583 return []byte(ct.tableOfContentsHTML), nil
584 }
585 renderer, found := ct.contentPlaceholders[token]
586 if found {
587 repl, more, err := renderer.renderShortcode(ctx)
588 if err != nil {
589 return nil, err
590 }
591 hasShortcodeVariants = hasShortcodeVariants || more
592 return repl, nil
593 }
594 // This should never happen.
595 panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders)))
596 }
597
598 b, err = expandShortcodeTokens(ctx, b, tokenHandler)
599 if err != nil {
600 return nil, err
601 }
602 if hasShortcodeVariants {
603 cp.po.p.incrPageOutputTemplateVariation()
604 }
605
606 var result contentSummary
607 if c.pi.hasSummaryDivider {
608 s := string(b)
609 summarized := page.ExtractSummaryFromHTMLWithDivider(cp.po.p.m.pageConfig.ContentMediaType, s, internalSummaryDividerBase)
610 result.summary = page.Summary{
611 Text: template.HTML(summarized.Summary()),
612 Type: page.SummaryTypeManual,
613 Truncated: summarized.Truncated(),
614 }
615 result.contentWithoutSummary = template.HTML(summarized.ContentWithoutSummary())
616 result.content = template.HTML(summarized.Content())
617 } else {
618 result.content = template.HTML(string(b))
619 }
620
621 if !c.pi.hasSummaryDivider && cp.po.p.m.pageConfig.Summary == "" {
622 numWords := cp.po.p.s.conf.SummaryLength
623 isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
624 summary := page.ExtractSummaryFromHTML(cp.po.p.m.pageConfig.ContentMediaType, string(result.content), numWords, isCJKLanguage)
625 result.summary = page.Summary{
626 Text: template.HTML(summary.Summary()),
627 Type: page.SummaryTypeAuto,
628 Truncated: summary.Truncated(),
629 }
630 result.contentWithoutSummary = template.HTML(summary.ContentWithoutSummary())
631 }
632 rs.Value = result
633
634 return rs, nil
635 }()
636 if err != nil {
637 return rs, cp.po.p.wrapError(err)
638 }
639
640 if rs.Value.summary.IsZero() {
641 b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
642 if err != nil {
643 return nil, err
644 }
645 html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup)
646 rs.Value.summary = page.Summary{
647 Text: helpers.BytesToHTML(html),
648 Type: page.SummaryTypeFrontMatter,
649 }
650 rs.Value.contentWithoutSummary = rs.Value.content
651 }
652
653 return rs, err
654 })
655 if err != nil {
656 return contentSummary{}, cp.po.p.wrapError(err)
657 }
658
659 return v.Value, nil
660 }
661
662 func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfContents {
663 ct, err := c.contentToC(ctx)
664 if err != nil {
665 panic(err)
666 }
667 return ct
668 }
669
670 type contextKey uint8
671
672 const (
673 contextKeyContentCallback contextKey = iota
674 )
675
676 var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)](contextKeyContentCallback)
677
678 func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) {
679 cp := c.pco
680 key := c.pi.sourceKey + "/" + c.keyScope(ctx)
681 versionv := c.version(cp)
682
683 v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
684 source, err := c.pi.contentSource(c)
685 if err != nil {
686 return nil, err
687 }
688
689 var ct contentTableOfContents
690 if err := cp.initRenderHooks(); err != nil {
691 return nil, err
692 }
693 po := cp.po
694 p := po.p
695 ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false)
696 if err != nil {
697 return nil, err
698 }
699
700 // Callback called from below (e.g. in .RenderString)
701 ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
702 cp.otherOutputs.Set(cp2.po.p.pid, cp2)
703
704 // Merge content placeholders
705 maps0.Copy(ct.contentPlaceholders, ct2.contentPlaceholders)
706
707 if p.s.conf.Internal.Watch {
708 for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes {
709 cp.trackDependency(s.templ)
710 }
711 }
712
713 // Transfer shortcode names so HasShortcode works for shortcodes from included pages.
714 cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState)
715 if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 {
716 cp.po.p.incrPageOutputTemplateVariation()
717 }
718 }
719
720 ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
721
722 var hasVariants bool
723 ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders)
724 if err != nil {
725 return nil, err
726 }
727
728 if hasVariants {
729 p.incrPageOutputTemplateVariation()
730 }
731
732 isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
733
734 if !isHTML {
735 createAndSetToC := func(tocProvider converter.TableOfContentsProvider) error {
736 cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
737 ct.tableOfContents = tocProvider.TableOfContents()
738 ct.tableOfContentsHTML, err = ct.tableOfContents.ToHTML(
739 cfg.TableOfContents.StartLevel,
740 cfg.TableOfContents.EndLevel,
741 cfg.TableOfContents.Ordered,
742 )
743 return err
744 }
745
746 // If the converter supports doing the parsing separately, we do that.
747 parseResult, ok, err := po.contentRenderer.ParseContent(ctx, ct.contentToRender)
748 if err != nil {
749 return nil, err
750 }
751 if ok {
752 // This is Goldmark.
753 // Store away the parse result for later use.
754 createAndSetToC(parseResult)
755
756 ct.astDoc = parseResult.Doc()
757
758 } else {
759
760 // This is Asciidoctor etc.
761 r, err := po.contentRenderer.ParseAndRenderContent(ctx, ct.contentToRender, true)
762 if err != nil {
763 return nil, err
764 }
765
766 ct.contentToRender = r.Bytes()
767
768 if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
769 createAndSetToC(tocProvider)
770 } else {
771 tmpContent, tmpTableOfContents := helpers.ExtractTOC(ct.contentToRender)
772 ct.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents)
773 ct.tableOfContents = tableofcontents.Empty
774 ct.contentToRender = tmpContent
775 }
776 }
777 }
778
779 return &resources.StaleValue[contentTableOfContents]{
780 Value: ct,
781 StaleVersionFunc: func() uint32 {
782 return c.version(cp) - versionv
783 },
784 }, nil
785 })
786 if err != nil {
787 return contentTableOfContents{}, err
788 }
789
790 return v.Value, nil
791 }
792
793 func (c *cachedContent) version(cp *pageContentOutput) uint32 {
794 // Both of these gets incremented on change.
795 return c.StaleVersion() + cp.contentRenderedVersion
796 }
797
798 func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlainWords, error) {
799 cp := c.pco
800 key := c.pi.sourceKey + "/" + c.keyScope(ctx)
801
802 versionv := c.version(cp)
803
804 v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) {
805 var result contentPlainPlainWords
806 rs := &resources.StaleValue[contentPlainPlainWords]{
807 StaleVersionFunc: func() uint32 {
808 return c.version(cp) - versionv
809 },
810 }
811
812 rendered, err := c.contentRendered(ctx)
813 if err != nil {
814 return nil, err
815 }
816
817 result.plain = tpl.StripHTML(string(rendered.content))
818 result.plainWords = strings.Fields(result.plain)
819
820 isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
821
822 if isCJKLanguage {
823 result.wordCount = 0
824 for _, word := range result.plainWords {
825 runeCount := utf8.RuneCountInString(word)
826 if len(word) == runeCount {
827 result.wordCount++
828 } else {
829 result.wordCount += runeCount
830 }
831 }
832 } else {
833 result.wordCount = helpers.TotalWords(result.plain)
834 }
835
836 // TODO(bep) is set in a test. Fix that.
837 if result.fuzzyWordCount == 0 {
838 result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100
839 }
840
841 if isCJKLanguage {
842 result.readingTime = (result.wordCount + 500) / 501
843 } else {
844 result.readingTime = (result.wordCount + 212) / 213
845 }
846
847 rs.Value = result
848
849 return rs, nil
850 })
851 if err != nil {
852 if herrors.IsTimeoutError(err) {
853 err = fmt.Errorf("timed out rendering the page content. Extend the `timeout` limit in your Hugo config file: %w", err)
854 }
855 return contentPlainPlainWords{}, err
856 }
857 return v.Value, nil
858 }
859
860 type cachedContentScope struct {
861 *cachedContent
862 pco *pageContentOutput
863 scope string
864 }
865
866 func (c *cachedContentScope) prepareContext(ctx context.Context) context.Context {
867 // A regular page's shortcode etc. may be rendered by e.g. the home page,
868 // so we need to track any changes to this content's page.
869 ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, c.pco.po.p)
870
871 // The markup scope is recursive, so if already set to a non zero value, preserve that value.
872 if s := hugo.GetMarkupScope(ctx); s != "" || s == c.scope {
873 return ctx
874 }
875 return hugo.SetMarkupScope(ctx, c.scope)
876 }
877
878 func (c *cachedContentScope) Render(ctx context.Context) (page.Content, error) {
879 return c, nil
880 }
881
882 func (c *cachedContentScope) Content(ctx context.Context) (template.HTML, error) {
883 ctx = c.prepareContext(ctx)
884 cr, err := c.contentRendered(ctx)
885 if err != nil {
886 return "", err
887 }
888 return cr.content, nil
889 }
890
891 func (c *cachedContentScope) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
892 ctx = c.prepareContext(ctx)
893 cr, err := c.contentRendered(ctx)
894 if err != nil {
895 return "", err
896 }
897 return cr.contentWithoutSummary, nil
898 }
899
900 func (c *cachedContentScope) Summary(ctx context.Context) (page.Summary, error) {
901 ctx = c.prepareContext(ctx)
902 rendered, err := c.contentRendered(ctx)
903 return rendered.summary, err
904 }
905
906 func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
907 ctx = c.prepareContext(ctx)
908
909 if len(args) < 1 || len(args) > 2 {
910 return "", errors.New("want 1 or 2 arguments")
911 }
912
913 pco := c.pco
914
915 var contentToRender string
916 opts := defaultRenderStringOpts
917 sidx := 1
918
919 if len(args) == 1 {
920 sidx = 0
921 } else {
922 m, ok := args[0].(map[string]any)
923 if !ok {
924 return "", errors.New("first argument must be a map")
925 }
926
927 if err := mapstructure.WeakDecode(m, &opts); err != nil {
928 return "", fmt.Errorf("failed to decode options: %w", err)
929 }
930 if opts.Markup != "" {
931 opts.Markup = markup.ResolveMarkup(opts.Markup)
932 }
933 }
934
935 contentToRenderv := args[sidx]
936
937 if _, ok := contentToRenderv.(hstring.HTML); ok {
938 // This content is already rendered, this is potentially
939 // a infinite recursion.
940 return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
941 }
942
943 var err error
944 contentToRender, err = cast.ToStringE(contentToRenderv)
945 if err != nil {
946 return "", err
947 }
948
949 if err = pco.initRenderHooks(); err != nil {
950 return "", err
951 }
952
953 conv := pco.po.p.getContentConverter()
954
955 if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType {
956 var err error
957 conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
958 if err != nil {
959 return "", pco.po.p.wrapError(err)
960 }
961 }
962
963 var rendered []byte
964
965 parseInfo := &contentParseInfo{
966 h: pco.po.p.s.h,
967 pid: pco.po.p.pid,
968 }
969
970 if pageparser.HasShortcode(contentToRender) {
971 contentToRenderb := []byte(contentToRender)
972 // String contains a shortcode.
973 parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
974 NoFrontMatter: true,
975 NoSummaryDivider: true,
976 })
977 if err != nil {
978 return "", err
979 }
980
981 s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s)
982 if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
983 return "", err
984 }
985
986 placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true)
987 if err != nil {
988 return "", err
989 }
990
991 contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
992 if err != nil {
993 return "", err
994 }
995 if hasVariants {
996 pco.po.p.incrPageOutputTemplateVariation()
997 }
998 b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
999 if err != nil {
1000 return "", pco.po.p.wrapError(err)
1001 }
1002 rendered = b.Bytes()
1003
1004 if parseInfo.hasNonMarkdownShortcode {
1005 var hasShortcodeVariants bool
1006
1007 tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
1008 if token == tocShortcodePlaceholder {
1009 toc, err := c.contentToC(ctx)
1010 if err != nil {
1011 return nil, err
1012 }
1013 // The Page's TableOfContents was accessed in a shortcode.
1014 return []byte(toc.tableOfContentsHTML), nil
1015 }
1016 renderer, found := placeholders[token]
1017 if found {
1018 repl, more, err := renderer.renderShortcode(ctx)
1019 if err != nil {
1020 return nil, err
1021 }
1022 hasShortcodeVariants = hasShortcodeVariants || more
1023 return repl, nil
1024 }
1025 // This should not happen.
1026 return nil, fmt.Errorf("unknown shortcode token %q", token)
1027 }
1028
1029 rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler)
1030 if err != nil {
1031 return "", err
1032 }
1033 if hasShortcodeVariants {
1034 pco.po.p.incrPageOutputTemplateVariation()
1035 }
1036 }
1037
1038 // We need a consolidated view in $page.HasShortcode
1039 pco.po.p.m.content.shortcodeState.transferNames(s)
1040
1041 } else {
1042 c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
1043 if err != nil {
1044 return "", pco.po.p.wrapError(err)
1045 }
1046
1047 rendered = c.Bytes()
1048 }
1049
1050 if opts.Display == "inline" {
1051 markup := pco.po.p.m.pageConfig.Content.Markup
1052 if opts.Markup != "" {
1053 markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup)
1054 }
1055 rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup)
1056 }
1057
1058 return template.HTML(string(rendered)), nil
1059 }
1060
1061 func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTML, error) {
1062 ctx = c.prepareContext(ctx)
1063
1064 pco := c.pco
1065 content := pco.po.p.m.content
1066
1067 source, err := content.pi.contentSource(content)
1068 if err != nil {
1069 return "", err
1070 }
1071 ct, err := c.contentToC(ctx)
1072 if err != nil {
1073 return "", err
1074 }
1075
1076 var insertPlaceholders bool
1077 var hasVariants bool
1078 cb := setGetContentCallbackInContext.Get(ctx)
1079 if cb != nil {
1080 insertPlaceholders = true
1081 }
1082 cc := make([]byte, 0, len(source)+(len(source)/10))
1083 for _, it := range content.pi.itemsStep2 {
1084 switch v := it.(type) {
1085 case pageparser.Item:
1086 cc = append(cc, source[v.Pos():v.Pos()+len(v.Val(source))]...)
1087 case pageContentReplacement:
1088 // Ignore.
1089 case *shortcode:
1090 if !insertPlaceholders || !v.insertPlaceholder() {
1091 // Insert the rendered shortcode.
1092 renderedShortcode, found := ct.contentPlaceholders[v.placeholder]
1093 if !found {
1094 // This should never happen.
1095 panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
1096 }
1097
1098 b, more, err := renderedShortcode.renderShortcode(ctx)
1099 if err != nil {
1100 return "", fmt.Errorf("failed to render shortcode: %w", err)
1101 }
1102 hasVariants = hasVariants || more
1103 cc = append(cc, []byte(b)...)
1104
1105 } else {
1106 // Insert the placeholder so we can insert the content after
1107 // markdown processing.
1108 cc = append(cc, []byte(v.placeholder)...)
1109 }
1110 default:
1111 panic(fmt.Sprintf("unknown item type %T", it))
1112 }
1113 }
1114
1115 if hasVariants {
1116 pco.po.p.incrPageOutputTemplateVariation()
1117 }
1118
1119 if cb != nil {
1120 cb(pco, ct)
1121 }
1122
1123 if tpl.Context.IsInGoldmark.Get(ctx) {
1124 // This content will be parsed and rendered by Goldmark.
1125 // Wrap it in a special Hugo markup to assign the correct Page from
1126 // the stack.
1127 return template.HTML(hugocontext.Wrap(cc, pco.po.p.pid)), nil
1128 }
1129
1130 return helpers.BytesToHTML(cc), nil
1131 }
1132
1133 func (c *cachedContentScope) Plain(ctx context.Context) string {
1134 ctx = c.prepareContext(ctx)
1135 return c.mustContentPlain(ctx).plain
1136 }
1137
1138 func (c *cachedContentScope) PlainWords(ctx context.Context) []string {
1139 ctx = c.prepareContext(ctx)
1140 return c.mustContentPlain(ctx).plainWords
1141 }
1142
1143 func (c *cachedContentScope) WordCount(ctx context.Context) int {
1144 ctx = c.prepareContext(ctx)
1145 return c.mustContentPlain(ctx).wordCount
1146 }
1147
1148 func (c *cachedContentScope) FuzzyWordCount(ctx context.Context) int {
1149 ctx = c.prepareContext(ctx)
1150 return c.mustContentPlain(ctx).fuzzyWordCount
1151 }
1152
1153 func (c *cachedContentScope) ReadingTime(ctx context.Context) int {
1154 ctx = c.prepareContext(ctx)
1155 return c.mustContentPlain(ctx).readingTime
1156 }
1157
1158 func (c *cachedContentScope) Len(ctx context.Context) int {
1159 ctx = c.prepareContext(ctx)
1160 return len(c.mustContentRendered(ctx).content)
1161 }
1162
1163 func (c *cachedContentScope) Fragments(ctx context.Context) *tableofcontents.Fragments {
1164 ctx = c.prepareContext(ctx)
1165 toc := c.mustContentToC(ctx).tableOfContents
1166 if toc == nil {
1167 return nil
1168 }
1169 return toc
1170 }
1171
1172 func (c *cachedContentScope) fragmentsHTML(ctx context.Context) template.HTML {
1173 ctx = c.prepareContext(ctx)
1174 return c.mustContentToC(ctx).tableOfContentsHTML
1175 }
1176
1177 func (c *cachedContentScope) mustContentPlain(ctx context.Context) contentPlainPlainWords {
1178 r, err := c.contentPlain(ctx)
1179 if err != nil {
1180 c.pco.fail(err)
1181 }
1182 return r
1183 }
1184
1185 func (c *cachedContentScope) mustContentRendered(ctx context.Context) contentSummary {
1186 r, err := c.contentRendered(ctx)
1187 if err != nil {
1188 c.pco.fail(err)
1189 }
1190 return r
1191 }