diff options
author | Bjørn Erik Pedersen <[email protected]> | 2024-08-13 15:49:56 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2024-08-29 16:45:21 +0200 |
commit | 37609262dcddac6d3358412b10214111b4d4dc3d (patch) | |
tree | 60f1370ec79454742c7eb727ca1bb9156aecb296 /hugolib | |
parent | 2b5c335e933cbd8e4e8569f206add5ec1bccd8e9 (diff) | |
download | hugo-37609262dcddac6d3358412b10214111b4d4dc3d.tar.gz hugo-37609262dcddac6d3358412b10214111b4d4dc3d.zip |
Add Page.Contents with scope support
Note that this also adds a new `.ContentWithoutSummary` method, and to do that we had to unify the different summary types:
Both `auto` and `manual` now returns HTML. Before this commit, `auto` would return plain text. This could be considered to be a slightly breaking change, but for the better: Now you can treat the `.Summary` the same without thinking about where it comes from, and if you want plain text, pipe it into `{{ .Summary | plainify }}`.
Fixes #8680
Fixes #12761
Fixes #12778
Fixes #716
Diffstat (limited to 'hugolib')
-rw-r--r-- | hugolib/page.go | 9 | ||||
-rw-r--r-- | hugolib/page__content.go | 578 | ||||
-rw-r--r-- | hugolib/page__meta.go | 2 | ||||
-rw-r--r-- | hugolib/page__output.go | 5 | ||||
-rw-r--r-- | hugolib/page__per_output.go | 392 | ||||
-rw-r--r-- | hugolib/page_test.go | 204 | ||||
-rw-r--r-- | hugolib/shortcode_page.go | 4 | ||||
-rw-r--r-- | hugolib/shortcode_test.go | 31 |
8 files changed, 565 insertions, 660 deletions
diff --git a/hugolib/page.go b/hugolib/page.go index 20751c57c..7525ab672 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, + MarkupProvider: 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, @@ -719,6 +717,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { }) p.pageOutput.contentRenderer = lcp p.pageOutput.ContentProvider = lcp + p.pageOutput.MarkupProvider = lcp p.pageOutput.PageRenderProvider = lcp p.pageOutput.TableOfContentsProvider = lcp } diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 1119a8a95..30caebed0 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -14,7 +14,6 @@ package hugolib import ( - "bytes" "context" "errors" "fmt" @@ -29,15 +28,23 @@ import ( "github.com/gohugoio/hugo/common/hcontext" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/common/hugo" + "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 ( @@ -45,8 +52,8 @@ const ( ) var ( - internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase) - internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n") + internalSummaryDividerPreString = "\n\n" + internalSummaryDividerBase + "\n\n" + internalSummaryDividerPre = []byte(internalSummaryDividerPreString) ) type pageContentReplacement struct { @@ -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 { @@ -171,9 +193,6 @@ type contentParseInfo struct { // Whether the parsed content contains a summary separator. hasSummaryDivider bool - // Whether there are more content after the summary divider. - summaryTruncated bool - // Returns the position in bytes after any front matter. posMainContent int @@ -368,8 +387,6 @@ Loop: } if item.IsNonWhitespace(source) { - rn.summaryTruncated = true - // Done return false } @@ -487,26 +504,28 @@ type contentTableOfContents struct { } type contentSummary struct { - content template.HTML - summary template.HTML - summaryTruncated bool + content string + contentWithoutSummary template.HTML + summary page.Summary } type contentPlainPlainWords struct { plain string plainWords []string - summary template.HTML - summaryTruncated bool - wordCount int fuzzyWordCount int readingTime int } -func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) { +func (c *cachedContentScope) keyScope(ctx context.Context) string { + return hugo.GetMarkupScope(ctx) + 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(ctx) versionv := c.version(cp) v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) { @@ -515,97 +534,121 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp })) cp.po.p.s.h.contentRenderCounter.Add(1) - cp.contentRendered = true + cp.contentRendered.Store(true) po := cp.po - ct, err := c.contentToC(ctx, cp) + ct, err := c.contentToC(ctx) if err != nil { return nil, err } - rs := &resources.StaleValue[contentSummary]{ - StaleVersionFunc: func() uint32 { - return c.version(cp) - versionv - }, - } + rs, err := func() (*resources.StaleValue[contentSummary], error) { + rs := &resources.StaleValue[contentSummary]{ + StaleVersionFunc: func() uint32 { + return c.version(cp) - versionv + }, + } - if len(c.pi.itemsStep2) == 0 { - // Nothing to do. - return rs, nil - } + if len(c.pi.itemsStep2) == 0 { + // Nothing to do. + return rs, nil + } - var b []byte + var b []byte - if ct.astDoc != nil { - // The content is parsed, but not rendered. - r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc) - if err != nil { - return nil, err - } + if ct.astDoc != nil { + // The content is parsed, but not rendered. + r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc) + if err != nil { + return nil, err + } - if !ok { - return nil, errors.New("invalid state: astDoc is set but RenderContent returned false") + if !ok { + return nil, errors.New("invalid state: astDoc is set but RenderContent returned false") + } + + b = r.Bytes() + + } else { + // Copy the content to be rendered. + b = make([]byte, len(ct.contentToRender)) + copy(b, ct.contentToRender) } - b = r.Bytes() + // There are one or more replacement tokens to be replaced. + var hasShortcodeVariants bool + tokenHandler := func(ctx context.Context, token string) ([]byte, error) { + if token == tocShortcodePlaceholder { + return []byte(ct.tableOfContentsHTML), nil + } + renderer, found := ct.contentPlaceholders[token] + if found { + repl, more, err := renderer.renderShortcode(ctx) + if err != nil { + return nil, err + } + hasShortcodeVariants = hasShortcodeVariants || more + return repl, nil + } + // This should never happen. + panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders))) + } - } else { - // Copy the content to be rendered. - b = make([]byte, len(ct.contentToRender)) - copy(b, ct.contentToRender) - } + b, err = expandShortcodeTokens(ctx, b, tokenHandler) + if err != nil { + return nil, err + } + if hasShortcodeVariants { + cp.po.p.pageOutputTemplateVariationsState.Add(1) + } - // There are one or more replacement tokens to be replaced. - var hasShortcodeVariants bool - tokenHandler := func(ctx context.Context, token string) ([]byte, error) { - if token == tocShortcodePlaceholder { - return []byte(ct.tableOfContentsHTML), nil + var result contentSummary + if c.pi.hasSummaryDivider { + s := string(b) + summarized := page.ExtractSummaryFromHTMLWithDivider(cp.po.p.m.pageConfig.ContentMediaType, s, internalSummaryDividerBase) + result.summary = page.Summary{ + Text: template.HTML(summarized.Summary()), + Type: page.SummaryTypeManual, + Truncated: summarized.Truncated(), + } + result.contentWithoutSummary = template.HTML(summarized.ContentWithoutSummary()) + result.content = summarized.Content() + } else { + result.content = string(b) } - renderer, found := ct.contentPlaceholders[token] - if found { - repl, more, err := renderer.renderShortcode(ctx) - if err != nil { - return nil, err + + if !c.pi.hasSummaryDivider && cp.po.p.m.pageConfig.Summary == "" { + numWords := cp.po.p.s.conf.SummaryLength + isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage + summary := page.ExtractSummaryFromHTML(cp.po.p.m.pageConfig.ContentMediaType, string(result.content), numWords, isCJKLanguage) + result.summary = page.Summary{ + Text: template.HTML(summary.Summary()), + Type: page.SummaryTypeAuto, + Truncated: summary.Truncated(), } - hasShortcodeVariants = hasShortcodeVariants || more - return repl, nil + result.contentWithoutSummary = template.HTML(summary.ContentWithoutSummary()) } - // This should never happen. - panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders))) - } + rs.Value = result - b, err = expandShortcodeTokens(ctx, b, tokenHandler) + return rs, nil + }() if err != nil { - return nil, err - } - if hasShortcodeVariants { - cp.po.p.pageOutputTemplateVariationsState.Add(1) + return rs, cp.po.p.wrapError(err) } - var result contentSummary // hasVariants bool - - if c.pi.hasSummaryDivider { - if cp.po.p.m.pageConfig.ContentMediaType.IsHTML() { - // Use the summary sections as provided by the user. - i := bytes.Index(b, internalSummaryDividerPre) - result.summary = helpers.BytesToHTML(b[:i]) - b = b[i+len(internalSummaryDividerPre):] - - } else { - summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Content.Markup, b) - if err != nil { - cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err) - } else { - b = content - result.summary = helpers.BytesToHTML(summary) - } + if rs.Value.summary.IsZero() { + b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false) + if err != nil { + return nil, err + } + html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup) + rs.Value.summary = page.Summary{ + Text: helpers.BytesToHTML(html), + Type: page.SummaryTypeFrontMatter, } - result.summaryTruncated = c.pi.summaryTruncated } - result.content = helpers.BytesToHTML(b) - rs.Value = result - return rs, nil + return rs, err }) if err != nil { return contentSummary{}, cp.po.p.wrapError(err) @@ -614,8 +657,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 +667,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(ctx) versionv := c.version(cp) v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { @@ -648,7 +692,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) ( // Callback called from below (e.g. in .RenderString) ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) { - cp.otherOutputs[cp2.po.p.pid] = cp2 + cp.otherOutputs.Set(cp2.po.p.pid, cp2) // Merge content placeholders for k, v := range ct2.contentPlaceholders { @@ -749,8 +793,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(ctx) versionv := c.version(cp) @@ -762,7 +807,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 } @@ -797,28 +842,6 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) result.readingTime = (result.wordCount + 212) / 213 } - if c.pi.hasSummaryDivider || rendered.summary != "" { - result.summary = rendered.summary - result.summaryTruncated = rendered.summaryTruncated - } else if cp.po.p.m.pageConfig.Summary != "" { - b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false) - if err != nil { - return nil, err - } - html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup) - result.summary = helpers.BytesToHTML(html) - } else { - var summary string - var truncated bool - if isCJKLanguage { - summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsByRune(result.plainWords) - } else { - summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsToWholeSentence(result.plain) - } - result.summary = template.HTML(summary) - result.summaryTruncated = truncated - } - rs.Value = result return rs, nil @@ -831,3 +854,332 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) } return v.Value, nil } + +type cachedContentScope struct { + *cachedContent + pco *pageContentOutput + scope string +} + +func (c *cachedContentScope) prepareContext(ctx context.Context) context.Context { + // The markup scope is recursive, so if already set to a non zero value, preserve that value. + if s := hugo.GetMarkupScope(ctx); s != "" || s == c.scope { + return ctx + } + return hugo.SetMarkupScope(ctx, c.scope) +} + +func (c *cachedContentScope) Render(ctx context.Context) (page.Content, error) { + return c, nil +} + +func (c *cachedContentScope) Content(ctx context.Context) (template.HTML, error) { + ctx = c.prepareContext(ctx) + cr, err := c.contentRendered(ctx) + if err != nil { + return "", err + } + return template.HTML(cr.content), nil +} + +func (c *cachedContentScope) ContentWithoutSummary(ctx context.Context) (template.HTML, error) { + ctx = c.prepareContext(ctx) + cr, err := c.contentRendered(ctx) + if err != nil { + return "", err + } + return cr.contentWithoutSummary, nil +} + +func (c *cachedContentScope) Summary(ctx context.Context) (page.Summary, error) { + ctx = c.prepareContext(ctx) + rendered, err := c.contentRendered(ctx) + return rendered.summary, err +} + +func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (template.HTML, error) { + ctx = c.prepareContext(ctx) + + 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 (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTML, error) { + ctx = c.prepareContext(ctx) + + 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 { + ctx = c.prepareContext(ctx) + return c.mustContentPlain(ctx).plain +} + +func (c *cachedContentScope) PlainWords(ctx context.Context) []string { + ctx = c.prepareContext(ctx) + return c.mustContentPlain(ctx).plainWords +} + +func (c *cachedContentScope) WordCount(ctx context.Context) int { + ctx = c.prepareContext(ctx) + return c.mustContentPlain(ctx).wordCount +} + +func (c *cachedContentScope) FuzzyWordCount(ctx context.Context) int { + ctx = c.prepareContext(ctx) + return c.mustContentPlain(ctx).fuzzyWordCount +} + +func (c *cachedContentScope) ReadingTime(ctx context.Context) int { + ctx = c.prepareContext(ctx) + return c.mustContentPlain(ctx).readingTime +} + +func (c *cachedContentScope) Len(ctx context.Context) int { + ctx = c.prepareContext(ctx) + return len(c.mustContentRendered(ctx).content) +} + +func (c *cachedContentScope) Fragments(ctx context.Context) *tableofcontents.Fragments { + ctx = c.prepareContext(ctx) + toc := c.mustContentToC(ctx).tableOfContents + if toc == nil { + return nil + } + return toc +} + +func (c *cachedContentScope) fragmentsHTML(ctx context.Context) template.HTML { + ctx = c.prepareContext(ctx) + return c.mustContentToC(ctx).tableOfContentsHTML +} + +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 +} diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index b23084a47..56e26ecdb 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -821,7 +821,7 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter. // This prevents infinite recursion in some cases. return doc } - if v, ok := ps.pageOutput.pco.otherOutputs[id]; ok { + if v, ok := ps.pageOutput.pco.otherOutputs.Get(id); ok { return v.po.p } return nil diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 2f4d6c205..b8086bb48 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -65,6 +65,7 @@ func newPageOutput( p: ps, f: f, pagePerOutputProviders: providers, + MarkupProvider: 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.MarkupProvider page.ContentProvider page.PageRenderProvider page.TableOfContentsProvider @@ -119,7 +121,7 @@ func (po *pageOutput) isRendered() bool { if po.renderState > 0 { return true } - if po.pco != nil && po.pco.contentRendered { + if po.pco != nil && po.pco.contentRendered.Load() { return true } return false @@ -139,6 +141,7 @@ func (p *pageOutput) setContentProvider(cp *pageContentOutput) { } p.contentRenderer = cp p.ContentProvider = cp + p.MarkupProvider = 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 59cb574df..f074e8db7 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -21,18 +21,14 @@ import ( "html/template" "strings" "sync" + "sync/atomic" + "github.com/gohugoio/hugo/common/maps" "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 +37,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" @@ -73,7 +68,7 @@ func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) { cp := &pageContentOutput{ po: po, renderHooks: &renderHooks{}, - otherOutputs: make(map[uint64]*pageContentOutput), + otherOutputs: maps.NewCache[uint64, *pageContentOutput](), } return cp, nil } @@ -89,10 +84,10 @@ type pageContentOutput struct { // Other pages involved in rendering of this page, // typically included with .RenderShortcodes. - otherOutputs map[uint64]*pageContentOutput + otherOutputs *maps.Cache[uint64, *pageContentOutput] - contentRenderedVersion uint32 // Incremented on reset. - contentRendered bool // Set on content render. + contentRenderedVersion uint32 // Incremented on reset. + contentRendered atomic.Bool // Set on content render. // Renders Markdown hooks. renderHooks *renderHooks @@ -107,109 +102,84 @@ func (pco *pageContentOutput) Reset() { return } pco.contentRenderedVersion++ - pco.contentRendered = false + pco.contentRendered.Store(false) pco.renderHooks = &renderHooks{} } -func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { - return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents -} - -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 +func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) { + if len(layout) == 0 { + return "", errors.New("no layout given") } - ct, err := content.contentToC(ctx, pco) + templ, found, err := pco.po.p.resolveTemplate(layout...) if err != nil { - return "", err + return "", pco.po.p.wrapError(err) } - var insertPlaceholders bool - var hasVariants bool - cb := setGetContentCallbackInContext.Get(ctx) - if cb != nil { - insertPlaceholders = true + if !found { + return "", nil } - 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)) - } + // 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 +} - if hasVariants { - pco.po.p.pageOutputTemplateVariationsState.Add(1) - } +func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { + return pco.c().Fragments(ctx) +} - if cb != nil { - cb(pco, ct) - } +func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) { + return pco.c().RenderShortcodes(ctx) +} - 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 +func (pco *pageContentOutput) Markup(opts ...any) page.Markup { + if len(opts) > 1 { + panic("too many arguments, expected 0 or 1") + } + 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.Markup { + 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 + r, err := pco.c().Render(ctx) + if err != nil { + return nil, err + } + return r.Content(ctx) } -func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { - return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML +func (pco *pageContentOutput) ContentWithoutSummary(ctx context.Context) (template.HTML, error) { + r, err := pco.c().Render(ctx) + if err != nil { + return "", err + } + return r.ContentWithoutSummary(ctx) } -func (p *pageContentOutput) Len(ctx context.Context) int { - return len(p.mustContentRendered(ctx).content) +func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { + return pco.c().(*cachedContentScope).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) Len(ctx context.Context) int { + return pco.mustRender(ctx).Len(ctx) } -func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords { - r, err := pco.po.p.m.content.contentPlain(ctx, pco) +func (pco *pageContentOutput) mustRender(ctx context.Context) page.Content { + c, err := pco.c().Render(ctx) if err != nil { pco.fail(err) } - return r + return c } func (pco *pageContentOutput) fail(err error) { @@ -217,203 +187,43 @@ func (pco *pageContentOutput) fail(err error) { } func (pco *pageContentOutput) Plain(ctx context.Context) string { - return pco.mustContentPlain(ctx).plain + return pco.mustRender(ctx).Plain(ctx) } func (pco *pageContentOutput) PlainWords(ctx context.Context) []string { - return pco.mustContentPlain(ctx).plainWords + return pco.mustRender(ctx).PlainWords(ctx) } func (pco *pageContentOutput) ReadingTime(ctx context.Context) int { - return pco.mustContentPlain(ctx).readingTime + return pco.mustRender(ctx).ReadingTime(ctx) } func (pco *pageContentOutput) WordCount(ctx context.Context) int { - return pco.mustContentPlain(ctx).wordCount + return pco.mustRender(ctx).WordCount(ctx) } func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int { - return pco.mustContentPlain(ctx).fuzzyWordCount + return pco.mustRender(ctx).FuzzyWordCount(ctx) } func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML { - return pco.mustContentPlain(ctx).summary -} - -func (pco *pageContentOutput) Truncated(ctx context.Context) bool { - return pco.mustContentPlain(ctx).summaryTruncated -} - -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) + summary, err := pco.mustRender(ctx).Summary(ctx) 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) + pco.fail(err) } - - return template.HTML(string(rendered)), nil + return summary.Text } -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...) +func (pco *pageContentOutput) Truncated(ctx context.Context) bool { + summary, err := pco.mustRender(ctx).Summary(ctx) if err != nil { - return "", pco.po.p.wrapError(err) - } - - if !found { - return "", nil + pco.fail(err) } + return summary.Truncated +} - // 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 (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) { + return pco.c().RenderString(ctx, args...) } func (pco *pageContentOutput) initRenderHooks() error { @@ -660,65 +470,3 @@ func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Templ } return b.String(), nil } - -func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("summary split failed: %s", r) - } - }() - - startDivider := bytes.Index(c, internalSummaryDividerBaseBytes) - - if startDivider == -1 { - return - } - - startTag := "p" - switch markup { - case media.DefaultContentTypes.AsciiDoc.SubType: - startTag = "div" - } - - // Walk back and forward to the surrounding tags. - start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag)) - end := bytes.Index(c[startDivider:], []byte("</"+startTag)) - - if start == -1 { - start = startDivider - } else { - start = startDivider - (startDivider - start) - } - - if end == -1 { - end = startDivider + len(internalSummaryDividerBase) - } else { - end = startDivider + end + len(startTag) + 3 - } - - var addDiv bool - - switch markup { - case "rst": - addDiv = true - } - - withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...) - - if len(withoutDivider) > 0 { - summary = bytes.TrimSpace(withoutDivider[:start]) - } - - if addDiv { - // For the rst - summary = append(append([]byte(nil), summary...), []byte("</div>")...) - } - - if err != nil { - return - } - - content = bytes.TrimSpace(withoutDivider) - - return -} diff --git a/hugolib/page_test.go b/hugolib/page_test.go index a1eb43430..47f7c59c9 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -65,15 +65,6 @@ Summary Next Line Some more text ` - simplePageWithBlankSummary = `--- -title: SimpleWithBlankSummary ---- - -<!--more--> - -Some text. -` - simplePageWithSummaryParameter = `--- title: SimpleWithSummaryParameter summary: "Page with summary parameter and [a link](http://www.example.com/)" @@ -322,7 +313,8 @@ func checkPageTOC(t *testing.T, page page.Page, toc string) { } func checkPageSummary(t *testing.T, page page.Page, summary string, msg ...any) { - a := normalizeContent(string(page.Summary(context.Background()))) + s := string(page.Summary(context.Background())) + a := normalizeContent(s) b := normalizeContent(summary) if a != b { t.Fatalf("Page summary is:\n%q.\nExpected\n%q (%q)", a, b, msg) @@ -593,26 +585,6 @@ date: 2012-01-12 b.Assert(s.Site().Lastmod().Year(), qt.Equals, 2018) } -func TestCreateNewPage(t *testing.T) { - t.Parallel() - c := qt.New(t) - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - - // issue #2290: Path is relative to the content dir and will continue to be so. - c.Assert(p.File().Path(), qt.Equals, fmt.Sprintf("p0.%s", ext)) - c.Assert(p.IsHome(), qt.Equals, false) - checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "<p>Simple Page</p>\n")) - checkPageSummary(t, p, "Simple Page") - checkPageType(t, p, "page") - } - - settings := map[string]any{} - - testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage) -} - func TestPageSummary(t *testing.T) { t.Parallel() assertFunc := func(t *testing.T, ext string, pages page.Pages) { @@ -621,7 +593,7 @@ func TestPageSummary(t *testing.T) { // Source is not Asciidoctor- or RST-compatible so don't test them if ext != "ad" && ext != "rst" { checkPageContent(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>\n\n<p>Additional text.</p>\n\n<p>Further text.</p>\n"), ext) - checkPageSummary(t, p, normalizeExpected(ext, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Additional text."), ext) + checkPageSummary(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>"), ext) } checkPageType(t, p, "page") } @@ -642,19 +614,6 @@ func TestPageWithDelimiter(t *testing.T) { testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter) } -func TestPageWithBlankSummary(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - checkPageTitle(t, p, "SimpleWithBlankSummary") - checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n"), ext) - checkPageSummary(t, p, normalizeExpected(ext, ""), ext) - checkPageType(t, p, "page") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithBlankSummary) -} - func TestPageWithSummaryParameter(t *testing.T) { t.Parallel() assertFunc := func(t *testing.T, ext string, pages page.Pages) { @@ -729,19 +688,6 @@ title: "empty" b.AssertFileContent("public/empty/index.html", "! title") } -func TestPageWithShortCodeInSummary(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line. <figure><img src=\"/not/real\"> </figure> . More text here.</p><p>Some more text</p>")) - checkPageSummary(t, p, "Summary Next Line. . More text here. Some more text") - checkPageType(t, p, "page") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary) -} - func TestTableOfContents(t *testing.T) { c := qt.New(t) cfg, fs := newTestCfg() @@ -853,7 +799,7 @@ Summary: {{ .Summary }}|Truncated: {{ .Truncated }}| Content: {{ .Content }}| `).AssertFileContent("public/simple/index.html", - "Summary: This is summary. This is more summary. This is even more summary*.|", + "Summary: <p>This is <strong>summary</strong>.\nThis is <strong>more summary</strong>.\nThis is <em>even more summary</em>*.\nThis is <strong>more summary</strong>.</p>|", "Truncated: true|", "Content: <p>This is <strong>summary</strong>.") } @@ -1242,11 +1188,6 @@ func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { if p.WordCount(context.Background()) != 74 { t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 74, p.WordCount(context.Background())) } - - if p.Summary(context.Background()) != simplePageWithMainEnglishWithCJKRunesSummary { - t.Fatalf("[%s] incorrect Summary for content '%s'. expected\n%v, got\n%v", ext, p.Plain(context.Background()), - simplePageWithMainEnglishWithCJKRunesSummary, p.Summary(context.Background())) - } } testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithMainEnglishWithCJKRunes) @@ -1263,11 +1204,6 @@ func TestWordCountWithIsCJKLanguageFalse(t *testing.T) { if p.WordCount(context.Background()) != 75 { t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), 74, p.WordCount(context.Background())) } - - if p.Summary(context.Background()) != simplePageWithIsCJKLanguageFalseSummary { - t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), - simplePageWithIsCJKLanguageFalseSummary, p.Summary(context.Background())) - } } testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithIsCJKLanguageFalse) @@ -1485,42 +1421,6 @@ func TestChompBOM(t *testing.T) { checkPageTitle(t, p, "Simple") } -func TestPageHTMLContent(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile() - - frontmatter := `--- -title: "HTML Content" ---- -` - b.WithContent("regular.html", frontmatter+`<h1>Hugo</h1>`) - b.WithContent("nomarkdownforyou.html", frontmatter+`**Hugo!**`) - b.WithContent("manualsummary.html", frontmatter+` -<p>This is summary</p> -<!--more--> -<p>This is the main content.</p>`) - - b.Build(BuildCfg{}) - - b.AssertFileContent( - "public/regular/index.html", - "Single: HTML Content|Hello|en|RelPermalink: /regular/|", - "Summary: Hugo|Truncated: false") - - b.AssertFileContent( - "public/nomarkdownforyou/index.html", - "Permalink: http://example.com/nomarkdownforyou/|**Hugo!**|", - ) - - // https://github.com/gohugoio/hugo/issues/5723 - b.AssertFileContent( - "public/manualsummary/index.html", - "Single: HTML Content|Hello|en|RelPermalink: /manualsummary/|", - "Summary: \n<p>This is summary</p>\n|Truncated: true", - "|<p>This is the main content.</p>|", - ) -} - // https://github.com/gohugoio/hugo/issues/5381 func TestPageManualSummary(t *testing.T) { b := newTestSitesBuilder(t) @@ -1761,102 +1661,6 @@ Single: {{ .Title}}|{{ .RelPermalink }}|{{ .Path }}| b.AssertFileContent("public/sect3/Pag.E4/index.html", "Single: Pag.E4|/sect3/Pag.E4/|/sect3/p4|") } -// https://github.com/gohugoio/hugo/issues/4675 -func TestWordCountAndSimilarVsSummary(t *testing.T) { - t.Parallel() - c := qt.New(t) - - single := []string{"_default/single.html", ` -WordCount: {{ .WordCount }} -FuzzyWordCount: {{ .FuzzyWordCount }} -ReadingTime: {{ .ReadingTime }} -Len Plain: {{ len .Plain }} -Len PlainWords: {{ len .PlainWords }} -Truncated: {{ .Truncated }} -Len Summary: {{ len .Summary }} -Len Content: {{ len .Content }} - -SUMMARY:{{ .Summary }}:{{ len .Summary }}:END - -`} - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded(single...).WithContent("p1.md", fmt.Sprintf(`--- -title: p1 ---- - -%s - -`, strings.Repeat("word ", 510)), - - "p2.md", fmt.Sprintf(`--- -title: p2 ---- -This is a summary. - -<!--more--> - -%s - -`, strings.Repeat("word ", 310)), - "p3.md", fmt.Sprintf(`--- -title: p3 -isCJKLanguage: true ---- -Summary: In Chinese, 好 means good. - -<!--more--> - -%s - -`, strings.Repeat("好", 200)), - "p4.md", fmt.Sprintf(`--- -title: p4 -isCJKLanguage: false ---- -Summary: In Chinese, 好 means good. - -<!--more--> - -%s - -`, strings.Repeat("好", 200)), - - "p5.md", fmt.Sprintf(`--- -title: p4 -isCJKLanguage: true ---- -Summary: In Chinese, 好 means good. - -%s - -`, strings.Repeat("好", 200)), - "p6.md", fmt.Sprintf(`--- -title: p4 -isCJKLanguage: false ---- -Summary: In Chinese, 好 means good. - -%s - -`, strings.Repeat("好", 200)), - ) - - b.CreateSites().Build(BuildCfg{}) - - c.Assert(len(b.H.Sites), qt.Equals, 1) - c.Assert(len(b.H.Sites[0].RegularPages()), qt.Equals, 6) - - b.AssertFileContent("public/p1/index.html", "WordCount: 510\nFuzzyWordCount: 600\nReadingTime: 3\nLen Plain: 2550\nLen PlainWords: 510\nTruncated: false\nLen Summary: 2549\nLen Content: 2557") - - b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1582") - - b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651") - b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651") - b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 652") - b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 652") -} - func TestScratch(t *testing.T) { t.Parallel() diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go index 7c32f2ea1..8030b0285 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.MarkupProvider 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, + MarkupProvider: 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.MarkupProvider page.ContentProvider p *pageState } @@ -112,6 +115,7 @@ type pageForRenderHooks struct { func newPageForRenderHook(p *pageState) page.Page { return &pageForRenderHooks{ PageWithoutContent: p, + MarkupProvider: page.NopPage, ContentProvider: page.NopPage, TableOfContentsProvider: p, p: p, diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index a1c5c0aea..21436d980 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -756,12 +756,15 @@ title: "Hugo Rocks!" func TestShortcodeParams(t *testing.T) { t.Parallel() - c := qt.New(t) - - builder := newTestSitesBuilder(t).WithSimpleConfigFile() - builder.WithContent("page.md", `--- + files := ` +-- hugo.toml -- +baseURL = "https://example.org" +-- layouts/shortcodes/hello.html -- +{{ range $i, $v := .Params }}{{ printf "- %v: %v (%T) " $i $v $v -}}{{ end }} +-- content/page.md -- title: "Hugo Rocks!" +summary: "Foo" --- # doc @@ -770,23 +773,15 @@ types positional: {{< hello true false 33 3.14 >}} types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}} types string: {{< hello "true" trues "33" "3.14" >}} escaped quoute: {{< hello "hello \"world\"." >}} +-- layouts/_default/single.html -- +Content: {{ .Content }}| +` + b := Test(t, files) -`).WithTemplatesAdded( - "layouts/shortcodes/hello.html", - `{{ range $i, $v := .Params }} -- {{ printf "%v: %v (%T)" $i $v $v }} -{{ end }} -{{ $b1 := .Get "b1" }} -Get: {{ printf "%v (%T)" $b1 $b1 | safeHTML }} -`).Build(BuildCfg{}) - - s := builder.H.Sites[0] - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - builder.AssertFileContent("public/page/index.html", + b.AssertFileContent("public/page/index.html", "types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)", - "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int) Get: true (bool) ", + "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int)", "types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ", "hello "world". (string)", ) |