diff --git a/hugolib/page.go b/hugolib/page.go index 20751c57cee..18bf6875ec4 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -61,6 +61,7 @@ var ( pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType) nopPageOutput = &pageOutput{ pagePerOutputProviders: nopPagePerOutput, + ContentsProvider: page.NopPage, ContentProvider: page.NopPage, } ) @@ -213,11 +214,8 @@ func (p *pageHeadingsFiltered) page() page.Page { // For internal use by the related content feature. func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document { - r, err := p.m.content.contentToC(ctx, p.pageOutput.pco) - if err != nil { - panic(err) - } - headings := r.tableOfContents.Headings.FilterBy(fn) + fragments := p.pageOutput.pco.c().Fragments(ctx) + headings := fragments.Headings.FilterBy(fn) return &pageHeadingsFiltered{ pageState: p, headings: headings, diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 1119a8a95bb..d6950e6c6be 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -29,15 +29,22 @@ import ( "github.com/gohugoio/hugo/common/hcontext" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/types/hstring" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/goldmark/hugocontext" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/parser/pageparser" "github.com/gohugoio/hugo/resources" + "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/tpl" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cast" ) const ( @@ -130,6 +137,7 @@ func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cached shortcodeState: newShortcodeHandler(filename, m.s), pi: pi, enableEmoji: m.s.conf.EnableEmoji, + scopes: maps.NewCache[string, *cachedContentScope](), } source, err := c.pi.contentSource(m) @@ -155,6 +163,20 @@ type cachedContent struct { pi *contentParseInfo enableEmoji bool + + scopes *maps.Cache[string, *cachedContentScope] +} + +func (c *cachedContent) getOrCreateScope(scope string, pco *pageContentOutput) *cachedContentScope { + key := scope + pco.po.f.Name + cs, _ := c.scopes.GetOrCreate(key, func() (*cachedContentScope, error) { + return &cachedContentScope{ + cachedContent: c, + pco: pco, + scope: scope, + }, nil + }) + return cs } type contentParseInfo struct { @@ -504,9 +526,14 @@ type contentPlainPlainWords struct { readingTime int } -func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) { +func (c *cachedContentScope) keyScope() string { + return c.scope + c.pco.po.f.Name +} + +func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummary, error) { + cp := c.pco ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal) - key := c.pi.sourceKey + "/" + cp.po.f.Name + key := c.pi.sourceKey + "/" + c.keyScope() versionv := c.version(cp) v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) { @@ -518,7 +545,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp cp.contentRendered = true po := cp.po - ct, err := c.contentToC(ctx, cp) + ct, err := c.contentToC(ctx) if err != nil { return nil, err } @@ -614,8 +641,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp return v.Value, nil } -func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutput) contentTableOfContents { - ct, err := c.contentToC(ctx, cp) +func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfContents { + ct, err := c.contentToC(ctx) if err != nil { panic(err) } @@ -624,8 +651,9 @@ func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutpu var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback") -func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) { - key := c.pi.sourceKey + "/" + cp.po.f.Name +func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) { + cp := c.pco + key := c.pi.sourceKey + "/" + c.keyScope() versionv := c.version(cp) v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { @@ -749,8 +777,9 @@ func (c *cachedContent) version(cp *pageContentOutput) uint32 { return c.StaleVersion() + cp.contentRenderedVersion } -func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) { - key := c.pi.sourceKey + "/" + cp.po.f.Name +func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlainWords, error) { + cp := c.pco + key := c.pi.sourceKey + "/" + c.keyScope() versionv := c.version(cp) @@ -762,7 +791,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) }, } - rendered, err := c.contentRendered(ctx, cp) + rendered, err := c.contentRendered(ctx) if err != nil { return nil, err } @@ -831,3 +860,328 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) } return v.Value, nil } + +type cachedContentScope struct { + *cachedContent + pco *pageContentOutput + scope string +} + +// var _ page.ContentsProvider = (*cachedContent)(nil) + +func (c *cachedContentScope) Render(ctx context.Context) (any, error) { + r, err := c.contentRendered(ctx) + return r.content, err +} + +func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (template.HTML, error) { + if len(args) < 1 || len(args) > 2 { + return "", errors.New("want 1 or 2 arguments") + } + + pco := c.pco + + var contentToRender string + opts := defaultRenderStringOpts + sidx := 1 + + if len(args) == 1 { + sidx = 0 + } else { + m, ok := args[0].(map[string]any) + if !ok { + return "", errors.New("first argument must be a map") + } + + if err := mapstructure.WeakDecode(m, &opts); err != nil { + return "", fmt.Errorf("failed to decode options: %w", err) + } + if opts.Markup != "" { + opts.Markup = markup.ResolveMarkup(opts.Markup) + } + } + + contentToRenderv := args[sidx] + + if _, ok := contentToRenderv.(hstring.RenderedString); ok { + // This content is already rendered, this is potentially + // a infinite recursion. + return "", errors.New("text is already rendered, repeating it may cause infinite recursion") + } + + var err error + contentToRender, err = cast.ToStringE(contentToRenderv) + if err != nil { + return "", err + } + + if err = pco.initRenderHooks(); err != nil { + return "", err + } + + conv := pco.po.p.getContentConverter() + + if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType { + var err error + conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) + if err != nil { + return "", pco.po.p.wrapError(err) + } + } + + var rendered []byte + + parseInfo := &contentParseInfo{ + h: pco.po.p.s.h, + pid: pco.po.p.pid, + } + + if pageparser.HasShortcode(contentToRender) { + contentToRenderb := []byte(contentToRender) + // String contains a shortcode. + parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{ + NoFrontMatter: true, + NoSummaryDivider: true, + }) + if err != nil { + return "", err + } + + s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s) + if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil { + return "", err + } + + placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true) + if err != nil { + return "", err + } + + contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders) + if err != nil { + return "", err + } + if hasVariants { + pco.po.p.pageOutputTemplateVariationsState.Add(1) + } + b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false) + if err != nil { + return "", pco.po.p.wrapError(err) + } + rendered = b.Bytes() + + if parseInfo.hasNonMarkdownShortcode { + var hasShortcodeVariants bool + + tokenHandler := func(ctx context.Context, token string) ([]byte, error) { + if token == tocShortcodePlaceholder { + toc, err := c.contentToC(ctx) + if err != nil { + return nil, err + } + // The Page's TableOfContents was accessed in a shortcode. + return []byte(toc.tableOfContentsHTML), nil + } + renderer, found := placeholders[token] + if found { + repl, more, err := renderer.renderShortcode(ctx) + if err != nil { + return nil, err + } + hasShortcodeVariants = hasShortcodeVariants || more + return repl, nil + } + // This should not happen. + return nil, fmt.Errorf("unknown shortcode token %q", token) + } + + rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler) + if err != nil { + return "", err + } + if hasShortcodeVariants { + pco.po.p.pageOutputTemplateVariationsState.Add(1) + } + } + + // We need a consolidated view in $page.HasShortcode + pco.po.p.m.content.shortcodeState.transferNames(s) + + } else { + c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) + if err != nil { + return "", pco.po.p.wrapError(err) + } + + rendered = c.Bytes() + } + + if opts.Display == "inline" { + markup := pco.po.p.m.pageConfig.Content.Markup + if opts.Markup != "" { + markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup) + } + rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup) + } + + return template.HTML(string(rendered)), nil +} + +func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) { + if len(layout) == 0 { + return "", errors.New("no layout given") + } + templ, found, err := pco.po.p.resolveTemplate(layout...) + if err != nil { + return "", pco.po.p.wrapError(err) + } + + if !found { + return "", nil + } + + // Make sure to send the *pageState and not the *pageContentOutput to the template. + res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p) + if err != nil { + return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err)) + } + return template.HTML(res), nil +} + +func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTML, error) { + pco := c.pco + content := pco.po.p.m.content + + source, err := content.pi.contentSource(content) + if err != nil { + return "", err + } + ct, err := c.contentToC(ctx) + if err != nil { + return "", err + } + + var insertPlaceholders bool + var hasVariants bool + cb := setGetContentCallbackInContext.Get(ctx) + if cb != nil { + insertPlaceholders = true + } + cc := make([]byte, 0, len(source)+(len(source)/10)) + for _, it := range content.pi.itemsStep2 { + switch v := it.(type) { + case pageparser.Item: + cc = append(cc, source[v.Pos():v.Pos()+len(v.Val(source))]...) + case pageContentReplacement: + // Ignore. + case *shortcode: + if !insertPlaceholders || !v.insertPlaceholder() { + // Insert the rendered shortcode. + renderedShortcode, found := ct.contentPlaceholders[v.placeholder] + if !found { + // This should never happen. + panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) + } + + b, more, err := renderedShortcode.renderShortcode(ctx) + if err != nil { + return "", fmt.Errorf("failed to render shortcode: %w", err) + } + hasVariants = hasVariants || more + cc = append(cc, []byte(b)...) + + } else { + // Insert the placeholder so we can insert the content after + // markdown processing. + cc = append(cc, []byte(v.placeholder)...) + } + default: + panic(fmt.Sprintf("unknown item type %T", it)) + } + } + + if hasVariants { + pco.po.p.pageOutputTemplateVariationsState.Add(1) + } + + if cb != nil { + cb(pco, ct) + } + + if tpl.Context.IsInGoldmark.Get(ctx) { + // This content will be parsed and rendered by Goldmark. + // Wrap it in a special Hugo markup to assign the correct Page from + // the stack. + return template.HTML(hugocontext.Wrap(cc, pco.po.p.pid)), nil + } + + return helpers.BytesToHTML(cc), nil +} + +func (c *cachedContentScope) Plain(ctx context.Context) string { + return c.mustContentPlain(ctx).plain +} + +func (c *cachedContentScope) mustContentPlain(ctx context.Context) contentPlainPlainWords { + r, err := c.contentPlain(ctx) + if err != nil { + c.pco.fail(err) + } + return r +} + +func (c *cachedContentScope) mustContentRendered(ctx context.Context) contentSummary { + r, err := c.contentRendered(ctx) + if err != nil { + c.pco.fail(err) + } + return r +} + +func (c *cachedContentScope) PlainWords(ctx context.Context) []string { + return c.mustContentPlain(ctx).plainWords +} + +func (c *cachedContentScope) CountWords(ctx context.Context) int { + return c.mustContentPlain(ctx).wordCount +} + +func (c *cachedContentScope) CountWordsFuzzy(ctx context.Context) int { + return c.mustContentPlain(ctx).fuzzyWordCount +} + +func (c *cachedContentScope) ReadingTime(ctx context.Context) int { + return c.mustContentPlain(ctx).readingTime +} + +func (c *cachedContentScope) Len(ctx context.Context) int { + return len(c.mustContentRendered(ctx).content) +} + +func (c *cachedContentScope) Fragments(ctx context.Context) *tableofcontents.Fragments { + toc := c.mustContentToC(ctx).tableOfContents + if toc == nil { + return nil + } + return toc +} + +func (c *cachedContentScope) FragmentsHTML(ctx context.Context) template.HTML { + return c.mustContentToC(ctx).tableOfContentsHTML +} + +func (c *cachedContentScope) Summary(ctx context.Context) page.Summary { + return page.Summary{ + // TODO1 move this so we can just call once. + Text: c.mustContentPlain(ctx).summary, + Truncated: c.mustContentPlain(ctx).summaryTruncated, + } +} + +func (c *cachedContentScope) HasShortcode(name string) bool { + if c.shortcodeState == nil { + return false + } + + return c.shortcodeState.hasName(name) +} diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 2f4d6c205d4..ee5051ecc76 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -65,6 +65,7 @@ func newPageOutput( p: ps, f: f, pagePerOutputProviders: providers, + ContentsProvider: page.NopPage, ContentProvider: page.NopPage, PageRenderProvider: page.NopPage, TableOfContentsProvider: page.NopPage, @@ -95,6 +96,7 @@ type pageOutput struct { // output format. contentRenderer page.ContentRenderer pagePerOutputProviders + page.ContentsProvider page.ContentProvider page.PageRenderProvider page.TableOfContentsProvider @@ -139,6 +141,7 @@ func (p *pageOutput) setContentProvider(cp *pageContentOutput) { } p.contentRenderer = cp p.ContentProvider = cp + p.ContentsProvider = cp p.PageRenderProvider = cp p.TableOfContentsProvider = cp p.RenderShortcodesProvider = cp diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 59cb574dfe0..8d126ea2124 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -16,23 +16,17 @@ package hugolib import ( "bytes" "context" - "errors" "fmt" "html/template" "strings" "sync" "github.com/gohugoio/hugo/common/text" - "github.com/gohugoio/hugo/common/types/hstring" "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/markup" "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/mitchellh/mapstructure" "github.com/spf13/cast" "github.com/gohugoio/hugo/markup/converter/hooks" - "github.com/gohugoio/hugo/markup/goldmark/hugocontext" "github.com/gohugoio/hugo/markup/highlight/chromalexers" "github.com/gohugoio/hugo/markup/tableofcontents" @@ -41,7 +35,6 @@ import ( bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/tpl" - "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" @@ -112,104 +105,38 @@ func (pco *pageContentOutput) Reset() { } func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { - return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents + return pco.c().Fragments(ctx) } func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) { - content := pco.po.p.m.content - source, err := content.pi.contentSource(content) - if err != nil { - return "", err - } - ct, err := content.contentToC(ctx, pco) - if err != nil { - return "", err - } - - var insertPlaceholders bool - var hasVariants bool - cb := setGetContentCallbackInContext.Get(ctx) - if cb != nil { - insertPlaceholders = true - } - c := make([]byte, 0, len(source)+(len(source)/10)) - for _, it := range content.pi.itemsStep2 { - switch v := it.(type) { - case pageparser.Item: - c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) - case pageContentReplacement: - // Ignore. - case *shortcode: - if !insertPlaceholders || !v.insertPlaceholder() { - // Insert the rendered shortcode. - renderedShortcode, found := ct.contentPlaceholders[v.placeholder] - if !found { - // This should never happen. - panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) - } - - b, more, err := renderedShortcode.renderShortcode(ctx) - if err != nil { - return "", fmt.Errorf("failed to render shortcode: %w", err) - } - hasVariants = hasVariants || more - c = append(c, []byte(b)...) - - } else { - // Insert the placeholder so we can insert the content after - // markdown processing. - c = append(c, []byte(v.placeholder)...) - } - default: - panic(fmt.Sprintf("unknown item type %T", it)) - } - } - - if hasVariants { - pco.po.p.pageOutputTemplateVariationsState.Add(1) - } + return pco.c().RenderShortcodes(ctx) +} - if cb != nil { - cb(pco, ct) +func (pco *pageContentOutput) Contents(opts ...any) page.Contents { + if len(opts) > 1 { + panic("too many arguments, expected 0 or 1") } - - if tpl.Context.IsInGoldmark.Get(ctx) { - // This content will be parsed and rendered by Goldmark. - // Wrap it in a special Hugo markup to assign the correct Page from - // the stack. - return template.HTML(hugocontext.Wrap(c, pco.po.p.pid)), nil + var scope string + if len(opts) == 1 { + scope = cast.ToString(opts[0]) } + return pco.po.p.m.content.getOrCreateScope(scope, pco) +} - return helpers.BytesToHTML(c), nil +func (pco *pageContentOutput) c() page.Contents { + return pco.po.p.m.content.getOrCreateScope("", pco) } func (pco *pageContentOutput) Content(ctx context.Context) (any, error) { - r, err := pco.po.p.m.content.contentRendered(ctx, pco) - return r.content, err + return pco.c().Render(ctx) } func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { - return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML -} - -func (p *pageContentOutput) Len(ctx context.Context) int { - return len(p.mustContentRendered(ctx).content) + return pco.c().FragmentsHTML(ctx) } -func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary { - r, err := pco.po.p.m.content.contentRendered(ctx, pco) - if err != nil { - pco.fail(err) - } - return r -} - -func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords { - r, err := pco.po.p.m.content.contentPlain(ctx, pco) - if err != nil { - pco.fail(err) - } - return r +func (pco *pageContentOutput) Len(ctx context.Context) int { + return pco.c().Len(ctx) } func (pco *pageContentOutput) fail(err error) { @@ -217,203 +144,35 @@ func (pco *pageContentOutput) fail(err error) { } func (pco *pageContentOutput) Plain(ctx context.Context) string { - return pco.mustContentPlain(ctx).plain + return pco.c().Plain(ctx) } func (pco *pageContentOutput) PlainWords(ctx context.Context) []string { - return pco.mustContentPlain(ctx).plainWords + return pco.c().PlainWords(ctx) } func (pco *pageContentOutput) ReadingTime(ctx context.Context) int { - return pco.mustContentPlain(ctx).readingTime + return pco.c().ReadingTime(ctx) } func (pco *pageContentOutput) WordCount(ctx context.Context) int { - return pco.mustContentPlain(ctx).wordCount + return pco.c().CountWords(ctx) } func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int { - return pco.mustContentPlain(ctx).fuzzyWordCount + return pco.c().CountWordsFuzzy(ctx) } func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML { - return pco.mustContentPlain(ctx).summary + return pco.c().Summary(ctx).Text } func (pco *pageContentOutput) Truncated(ctx context.Context) bool { - return pco.mustContentPlain(ctx).summaryTruncated + return pco.c().Summary(ctx).Truncated } func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) { - if len(args) < 1 || len(args) > 2 { - return "", errors.New("want 1 or 2 arguments") - } - - var contentToRender string - opts := defaultRenderStringOpts - sidx := 1 - - if len(args) == 1 { - sidx = 0 - } else { - m, ok := args[0].(map[string]any) - if !ok { - return "", errors.New("first argument must be a map") - } - - if err := mapstructure.WeakDecode(m, &opts); err != nil { - return "", fmt.Errorf("failed to decode options: %w", err) - } - if opts.Markup != "" { - opts.Markup = markup.ResolveMarkup(opts.Markup) - } - } - - contentToRenderv := args[sidx] - - if _, ok := contentToRenderv.(hstring.RenderedString); ok { - // This content is already rendered, this is potentially - // a infinite recursion. - return "", errors.New("text is already rendered, repeating it may cause infinite recursion") - } - - var err error - contentToRender, err = cast.ToStringE(contentToRenderv) - if err != nil { - return "", err - } - - if err = pco.initRenderHooks(); err != nil { - return "", err - } - - conv := pco.po.p.getContentConverter() - - if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType { - var err error - conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) - if err != nil { - return "", pco.po.p.wrapError(err) - } - } - - var rendered []byte - - parseInfo := &contentParseInfo{ - h: pco.po.p.s.h, - pid: pco.po.p.pid, - } - - if pageparser.HasShortcode(contentToRender) { - contentToRenderb := []byte(contentToRender) - // String contains a shortcode. - parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{ - NoFrontMatter: true, - NoSummaryDivider: true, - }) - if err != nil { - return "", err - } - - s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s) - if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil { - return "", err - } - - placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true) - if err != nil { - return "", err - } - - contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders) - if err != nil { - return "", err - } - if hasVariants { - pco.po.p.pageOutputTemplateVariationsState.Add(1) - } - b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false) - if err != nil { - return "", pco.po.p.wrapError(err) - } - rendered = b.Bytes() - - if parseInfo.hasNonMarkdownShortcode { - var hasShortcodeVariants bool - - tokenHandler := func(ctx context.Context, token string) ([]byte, error) { - if token == tocShortcodePlaceholder { - toc, err := pco.po.p.m.content.contentToC(ctx, pco) - if err != nil { - return nil, err - } - // The Page's TableOfContents was accessed in a shortcode. - return []byte(toc.tableOfContentsHTML), nil - } - renderer, found := placeholders[token] - if found { - repl, more, err := renderer.renderShortcode(ctx) - if err != nil { - return nil, err - } - hasShortcodeVariants = hasShortcodeVariants || more - return repl, nil - } - // This should not happen. - return nil, fmt.Errorf("unknown shortcode token %q", token) - } - - rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler) - if err != nil { - return "", err - } - if hasShortcodeVariants { - pco.po.p.pageOutputTemplateVariationsState.Add(1) - } - } - - // We need a consolidated view in $page.HasShortcode - pco.po.p.m.content.shortcodeState.transferNames(s) - - } else { - c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) - if err != nil { - return "", pco.po.p.wrapError(err) - } - - rendered = c.Bytes() - } - - if opts.Display == "inline" { - markup := pco.po.p.m.pageConfig.Content.Markup - if opts.Markup != "" { - markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup) - } - rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup) - } - - return template.HTML(string(rendered)), nil -} - -func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) { - if len(layout) == 0 { - return "", errors.New("no layout given") - } - templ, found, err := pco.po.p.resolveTemplate(layout...) - if err != nil { - return "", pco.po.p.wrapError(err) - } - - if !found { - return "", nil - } - - // Make sure to send the *pageState and not the *pageContentOutput to the template. - res, err := executeToString(ctx, pco.po.p.s.Tmpl(), templ, pco.po.p) - if err != nil { - return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err)) - } - return template.HTML(res), nil + return pco.c().RenderString(ctx, args...) } func (pco *pageContentOutput) initRenderHooks() error { diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go index 7c32f2ea147..75f6e272f60 100644 --- a/hugolib/shortcode_page.go +++ b/hugolib/shortcode_page.go @@ -65,6 +65,7 @@ var zeroShortcode = prerenderedShortcode{} type pageForShortcode struct { page.PageWithoutContent page.TableOfContentsProvider + page.ContentsProvider page.ContentProvider // We need to replace it after we have rendered it, so provide a @@ -80,6 +81,7 @@ func newPageForShortcode(p *pageState) page.Page { return &pageForShortcode{ PageWithoutContent: p, TableOfContentsProvider: p, + ContentsProvider: page.NopPage, ContentProvider: page.NopPage, toc: template.HTML(tocShortcodePlaceholder), p: p, @@ -105,6 +107,7 @@ var _ types.Unwrapper = (*pageForRenderHooks)(nil) type pageForRenderHooks struct { page.PageWithoutContent page.TableOfContentsProvider + page.ContentsProvider page.ContentProvider p *pageState } @@ -112,6 +115,7 @@ type pageForRenderHooks struct { func newPageForRenderHook(p *pageState) page.Page { return &pageForRenderHooks{ PageWithoutContent: p, + ContentsProvider: page.NopPage, ContentProvider: page.NopPage, TableOfContentsProvider: p, p: p, diff --git a/resources/page/page.go b/resources/page/page.go index 9647a916b6d..b8620463c9d 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -74,6 +74,10 @@ type ChildCareProvider interface { Resources() resource.Resources } +type ContentsProvider interface { + Contents(opts ...any) Contents +} + // ContentProvider provides the content related values for a Page. type ContentProvider interface { Content(context.Context) (any, error) @@ -169,6 +173,7 @@ type PageProvider interface { // Page is the core interface in Hugo and what you get as the top level data context in your templates. type Page interface { + ContentsProvider ContentProvider TableOfContentsProvider PageWithoutContent @@ -260,7 +265,7 @@ type PageMetaInternalProvider interface { type PageRenderProvider interface { // Render renders the given layout with this Page as context. Render(ctx context.Context, layout ...string) (template.HTML, error) - // RenderString renders the first value in args with tPaginatorhe content renderer defined + // RenderString renders the first value in args with the content renderer defined // for this Page. // It takes an optional map as a second argument: // diff --git a/resources/page/page_contents.go b/resources/page/page_contents.go new file mode 100644 index 00000000000..a7a496aa27e --- /dev/null +++ b/resources/page/page_contents.go @@ -0,0 +1,70 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package page + +import ( + "context" + "html/template" + + "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/markup/tableofcontents" +) + +type Contents interface { + // Render renders the content into the current output format. + Render(context.Context) (any, error) + + // RenderString renders the first value in args with the content renderer defined + // for this Page. + // It takes an optional map as a second argument: + // + // display (“inline”): + // - inline or block. If inline (default), surrounding

on short snippets will be trimmed. + // markup (defaults to the Page’s markup) + RenderString(ctx context.Context, args ...any) (template.HTML, error) + + // RenderShortcodes returns raw content with any shortcodes rendered. + // TODO1 RawContent? + RenderShortcodes(context.Context) (template.HTML, error) + + // Plain returns the content as plain text with no markup. + Plain(context.Context) string + + // PlainWords returns the content split into words with no markup. + PlainWords(context.Context) []string + + CountWords(context.Context) int + CountWordsFuzzy(context.Context) int + ReadingTime(context.Context) int + Len(context.Context) int + + Fragments(context.Context) *tableofcontents.Fragments + FragmentsHTML(context.Context) template.HTML + + // HasShortcode returns whether the page has a shortcode with the given name. + HasShortcode(name string) bool + + Summary(context.Context) Summary +} + +var _ types.PrintableValueProvider = (*Summary)(nil) + +type Summary struct { + Text template.HTML + Truncated bool +} + +func (s Summary) PrintableValue() any { + return s.Text +} diff --git a/resources/page/page_contents_integration_test.go b/resources/page/page_contents_integration_test.go new file mode 100644 index 00000000000..3886d3744fd --- /dev/null +++ b/resources/page/page_contents_integration_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package page_test + +import ( + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +func TestPageContents(t *testing.T) { + files := ` +-- hugo.toml -- +-- content/p1.md -- +--- +title: "Post 1" +date: "2020-01-01" +--- + +# This is a test + +-- layouts/index.html -- +Home. +{{ .Content }} +-- layouts/_default/single.html -- +Single. +{{ $c := .Contents }} +Render: {{ $c.Render }}| +Plain: {{ $c.Plain }}| +PlainWords: {{ $c.PlainWords }}| +CountWords: {{ $c.CountWords }}| +CountWordsFuzzy: {{ $c.CountWordsFuzzy }}| +ReadingTime: {{ $c.ReadingTime }}| +Fragments: {{ $c.Fragments }}| +FragmentsHTML: {{ $c.FragmentsHTML }}| +HasShortcode: {{ $c.HasShortcode "figure" }}| + + +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", "Single.\n

This is a test

") +} diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index f745d8622d3..e386ec1affc 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -44,6 +44,7 @@ import ( var ( NopPage Page = new(nopPage) NopContentRenderer ContentRenderer = new(nopContentRenderer) + NopContents Contents = new(nopContents) NopCPageContentRenderer = struct { OutputFormatPageContentProvider ContentRenderer @@ -109,6 +110,10 @@ func (p *nopPage) BundleType() string { return "" } +func (p *nopPage) Contents(...any) Contents { + return NopContents +} + func (p *nopPage) Content(context.Context) (any, error) { return "", nil } @@ -547,3 +552,60 @@ func (r *nopContentRenderer) ParseContent(ctx context.Context, content []byte) ( func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) { return nil, false, nil } + +type nopContents int + +var _ Contents = (*nopContents)(nil) + +func (c *nopContents) Render(context.Context) (any, error) { + return "", nil +} + +func (c *nopContents) RenderString(ctx context.Context, args ...any) (template.HTML, error) { + return "", nil +} + +func (c *nopContents) RenderShortcodes(context.Context) (template.HTML, error) { + return "", nil +} + +func (c *nopContents) Plain(context.Context) string { + return "" +} + +func (c *nopContents) PlainWords(context.Context) []string { + return nil +} + +func (c *nopContents) CountWords(context.Context) int { + return 0 +} + +func (c *nopContents) CountWordsFuzzy(context.Context) int { + return 0 +} + +func (c *nopContents) ReadingTime(context.Context) int { + return 0 +} + +func (c *nopContents) Len(context.Context) int { + return 0 +} + +func (c *nopContents) Fragments(context.Context) *tableofcontents.Fragments { + return nil +} + +func (c *nopContents) FragmentsHTML(context.Context) template.HTML { + return "" +} + +// HasShortcode returns whether the page has a shortcode with the given name. +func (c *nopContents) HasShortcode(name string) bool { + return false +} + +func (c *nopContents) Summary(context.Context) Summary { + return Summary{} +} diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index cedbc74e960..7a189f9eec5 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -149,6 +149,10 @@ func (p *testPage) Content(context.Context) (any, error) { panic("testpage: not implemented") } +func (p *testPage) Contents(...any) Contents { + panic("testpage: not implemented") +} + func (p *testPage) ContentBaseName() string { panic("testpage: not implemented") }