Additional text.
\n\nFurther text.
\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, "Some text.
\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, "Summary Next Line. . More text here.
This is the main content.
`)
-
- 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: \nThis is the main content.
|",
- )
-}
-
// 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.
-
-
-
-%s
-
-`, strings.Repeat("word ", 310)),
- "p3.md", fmt.Sprintf(`---
-title: p3
-isCJKLanguage: true
----
-Summary: In Chinese, 好 means good.
-
-
-
-%s
-
-`, strings.Repeat("好", 200)),
- "p4.md", fmt.Sprintf(`---
-title: p4
-isCJKLanguage: false
----
-Summary: In Chinese, 好 means good.
-
-
-
-%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)",
)
diff --git a/resources/page/page.go b/resources/page/page.go
index 9647a916b..4cda8d31f 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -74,10 +74,17 @@ type ChildCareProvider interface {
Resources() resource.Resources
}
+type MarkupProvider interface {
+ Markup(opts ...any) Markup
+}
+
// ContentProvider provides the content related values for a Page.
type ContentProvider interface {
Content(context.Context) (any, error)
+ // ContentWithoutSummary returns the Page Content stripped of the summary.
+ ContentWithoutSummary(ctx context.Context) (template.HTML, error)
+
// Plain returns the Page Content stripped of HTML markup.
Plain(context.Context) string
@@ -169,6 +176,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 {
+ MarkupProvider
ContentProvider
TableOfContentsProvider
PageWithoutContent
@@ -260,7 +268,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_lazy_contentprovider.go b/resources/page/page_lazy_contentprovider.go
index 665b2d003..8e66a03e4 100644
--- a/resources/page/page_lazy_contentprovider.go
+++ b/resources/page/page_lazy_contentprovider.go
@@ -35,6 +35,7 @@ type OutputFormatContentProvider interface {
// OutputFormatPageContentProvider holds the exported methods from Page that are "outputFormat aware".
type OutputFormatPageContentProvider interface {
+ MarkupProvider
ContentProvider
TableOfContentsProvider
PageRenderProvider
@@ -74,6 +75,11 @@ func (lcp *LazyContentProvider) Reset() {
lcp.init.Reset()
}
+func (lcp *LazyContentProvider) Markup(opts ...any) Markup {
+ lcp.init.Do(context.Background())
+ return lcp.cp.Markup(opts...)
+}
+
func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
lcp.init.Do(ctx)
return lcp.cp.TableOfContents(ctx)
@@ -89,6 +95,11 @@ func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) {
return lcp.cp.Content(ctx)
}
+func (lcp *LazyContentProvider) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
+ lcp.init.Do(ctx)
+ return lcp.cp.ContentWithoutSummary(ctx)
+}
+
func (lcp *LazyContentProvider) Plain(ctx context.Context) string {
lcp.init.Do(ctx)
return lcp.cp.Plain(ctx)
diff --git a/resources/page/page_markup.go b/resources/page/page_markup.go
new file mode 100644
index 000000000..ef4a56e3a
--- /dev/null
+++ b/resources/page/page_markup.go
@@ -0,0 +1,344 @@
+// 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"
+ "regexp"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/markup/tableofcontents"
+ "github.com/gohugoio/hugo/media"
+ "github.com/gohugoio/hugo/tpl"
+)
+
+type Content interface {
+ Content(context.Context) (template.HTML, error)
+ ContentWithoutSummary(context.Context) (template.HTML, error)
+ Summary(context.Context) (Summary, error)
+ Plain(context.Context) string
+ PlainWords(context.Context) []string
+ WordCount(context.Context) int
+ FuzzyWordCount(context.Context) int
+ ReadingTime(context.Context) int
+ Len(context.Context) int
+}
+
+type Markup interface {
+ Render(context.Context) (Content, error)
+ RenderString(ctx context.Context, args ...any) (template.HTML, error)
+ RenderShortcodes(context.Context) (template.HTML, error)
+ Fragments(context.Context) *tableofcontents.Fragments
+}
+
+var _ types.PrintableValueProvider = Summary{}
+
+const (
+ SummaryTypeAuto = "auto"
+ SummaryTypeManual = "manual"
+ SummaryTypeFrontMatter = "frontmatter"
+)
+
+type Summary struct {
+ Text template.HTML
+ Type string // "auto", "manual" or "frontmatter"
+ Truncated bool
+}
+
+func (s Summary) IsZero() bool {
+ return s.Text == ""
+}
+
+func (s Summary) PrintableValue() any {
+ return s.Text
+}
+
+var _ types.PrintableValueProvider = (*Summary)(nil)
+
+type HtmlSummary struct {
+ source string
+ SummaryLowHigh types.LowHigh[string]
+ SummaryEndTag types.LowHigh[string]
+ WrapperStart types.LowHigh[string]
+ WrapperEnd types.LowHigh[string]
+ Divider types.LowHigh[string]
+}
+
+func (s HtmlSummary) wrap(ss string) string {
+ if s.WrapperStart.IsZero() {
+ return ss
+ }
+ return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss + s.source[s.WrapperEnd.Low:s.WrapperEnd.High]
+}
+
+func (s HtmlSummary) wrapLeft(ss string) string {
+ if s.WrapperStart.IsZero() {
+ return ss
+ }
+
+ return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss
+}
+
+func (s HtmlSummary) Value(l types.LowHigh[string]) string {
+ return s.source[l.Low:l.High]
+}
+
+func (s HtmlSummary) trimSpace(ss string) string {
+ return strings.TrimSpace(ss)
+}
+
+func (s HtmlSummary) Content() string {
+ if s.Divider.IsZero() {
+ return s.source
+ }
+ ss := s.source[:s.Divider.Low]
+ ss += s.source[s.Divider.High:]
+ return s.trimSpace(ss)
+}
+
+func (s HtmlSummary) Summary() string {
+ if s.Divider.IsZero() {
+ return s.trimSpace(s.wrap(s.Value(s.SummaryLowHigh)))
+ }
+ ss := s.source[s.SummaryLowHigh.Low:s.Divider.Low]
+ if s.SummaryLowHigh.High > s.Divider.High {
+ ss += s.source[s.Divider.High:s.SummaryLowHigh.High]
+ }
+ if !s.SummaryEndTag.IsZero() {
+ ss += s.Value(s.SummaryEndTag)
+ }
+ return s.trimSpace(s.wrap(ss))
+}
+
+func (s HtmlSummary) ContentWithoutSummary() string {
+ if s.Divider.IsZero() {
+ if s.SummaryLowHigh.Low == s.WrapperStart.High && s.SummaryLowHigh.High == s.WrapperEnd.Low {
+ return ""
+ }
+ return s.trimSpace(s.wrapLeft(s.source[s.SummaryLowHigh.High:]))
+ }
+ if s.SummaryEndTag.IsZero() {
+ return s.trimSpace(s.wrapLeft(s.source[s.Divider.High:]))
+ }
+ return s.trimSpace(s.wrapLeft(s.source[s.SummaryEndTag.High:]))
+}
+
+func (s HtmlSummary) Truncated() bool {
+ return s.SummaryLowHigh.High < len(s.source)
+}
+
+func (s *HtmlSummary) resolveParagraphTagAndSetWrapper(mt media.Type) tagReStartEnd {
+ ptag := startEndP
+
+ switch mt.SubType {
+ case media.DefaultContentTypes.AsciiDoc.SubType:
+ ptag = startEndDiv
+ case media.DefaultContentTypes.ReStructuredText.SubType:
+ const markerStart = "]?>$`)
+
+ startEndDiv = tagReStartEnd{
+ startEndOfString: regexp.MustCompile(`
]*?>$`),
+ endEndOfString: regexp.MustCompile(`
$`),
+ tagName: "div",
+ }
+
+ startEndP = tagReStartEnd{
+ startEndOfString: regexp.MustCompile(`
]*?>$`),
+ endEndOfString: regexp.MustCompile(`
$`),
+ tagName: "p",
+ }
+)
+
+type tagReStartEnd struct {
+ startEndOfString *regexp.Regexp
+ endEndOfString *regexp.Regexp
+ tagName string
+}
+
+func expandSummaryDivider(s string, re tagReStartEnd, divider types.LowHigh[string]) (types.LowHigh[string], types.LowHigh[string]) {
+ var endMarkup types.LowHigh[string]
+
+ if divider.IsZero() {
+ return divider, endMarkup
+ }
+
+ lo, hi := divider.Low, divider.High
+
+ var preserveEndMarkup bool
+
+ // Find the start of the paragraph.
+
+ for i := lo - 1; i >= 0; i-- {
+ if s[i] == '>' {
+ if match := re.startEndOfString.FindString(s[:i+1]); match != "" {
+ lo = i - len(match) + 1
+ break
+ }
+ if match := pOrDiv.FindString(s[:i+1]); match != "" {
+ i -= len(match) - 1
+ continue
+ }
+ }
+
+ r, _ := utf8.DecodeRuneInString(s[i:])
+ if !unicode.IsSpace(r) {
+ preserveEndMarkup = true
+ break
+ }
+ }
+
+ divider.Low = lo
+
+ // Now walk forward to the end of the paragraph.
+ for ; hi < len(s); hi++ {
+ if s[hi] != '>' {
+ continue
+ }
+ if match := re.endEndOfString.FindString(s[:hi+1]); match != "" {
+ hi++
+ break
+ }
+ }
+
+ if preserveEndMarkup {
+ endMarkup.Low = divider.High
+ endMarkup.High = hi
+ } else {
+ divider.High = hi
+ }
+
+ // Consume trailing newline if any.
+ if divider.High < len(s) && s[divider.High] == '\n' {
+ divider.High++
+ }
+
+ return divider, endMarkup
+}
diff --git a/resources/page/page_markup_integration_test.go b/resources/page/page_markup_integration_test.go
new file mode 100644
index 000000000..fc3c6a569
--- /dev/null
+++ b/resources/page/page_markup_integration_test.go
@@ -0,0 +1,337 @@
+// 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"
+ "github.com/gohugoio/hugo/markup/asciidocext"
+ "github.com/gohugoio/hugo/markup/rst"
+)
+
+func TestPageMarkupMethods(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+summaryLength=2
+-- content/p1.md --
+---
+title: "Post 1"
+date: "2020-01-01"
+---
+{{% foo %}}
+-- layouts/shortcodes/foo.html --
+Two *words*.
+{{/* Test that markup scope is set in all relevant constructs. */}}
+{{ if eq hugo.Context.MarkupScope "foo" }}
+
+## Heading 1
+Sint ad mollit qui Lorem ut occaecat culpa officia. Et consectetur aute voluptate non sit ullamco adipisicing occaecat. Sunt deserunt amet sit ad. Deserunt enim voluptate proident ipsum dolore dolor ut sit velit esse est mollit irure esse. Mollit incididunt veniam laboris magna et excepteur sit duis. Magna adipisicing reprehenderit tempor irure.
+### Heading 2
+Exercitation quis est consectetur occaecat nostrud. Ullamco aute mollit aliqua est amet. Exercitation ullamco consectetur dolor labore et non irure eu cillum Lorem.
+{{ end }}
+-- layouts/index.html --
+Home.
+{{ .Content }}
+-- layouts/_default/single.html --
+Single.
+Page.ContentWithoutSummmary: {{ .ContentWithoutSummary }}|
+{{ template "render-scope" (dict "page" . "scope" "main") }}
+{{ template "render-scope" (dict "page" . "scope" "foo") }}
+{{ define "render-scope" }}
+{{ $c := .page.Markup .scope }}
+{{ with $c.Render }}
+{{ $.scope }}: Content: {{ .Content }}|
+ {{ $.scope }}: ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+{{ $.scope }}: Plain: {{ .Plain }}|
+{{ $.scope }}: PlainWords: {{ .PlainWords }}|
+{{ $.scope }}: WordCount: {{ .WordCount }}|
+{{ $.scope }}: FuzzyWordCount: {{ .FuzzyWordCount }}|
+{{ $.scope }}: ReadingTime: {{ .ReadingTime }}|
+{{ $.scope }}: Len: {{ .Len }}|
+{{ $.scope }}: Summary: {{ with .Summary }}{{ . }}{{ else }}nil{{ end }}|
+{{ end }}
+{{ $.scope }}: Fragments: {{ $c.Fragments.Identifiers }}|
+{{ end }}
+
+
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Main scope.
+ b.AssertFileContent("public/p1/index.html",
+ "Page.ContentWithoutSummmary: |",
+ "main: Content:
Two words .
\n|",
+ "main: ContentWithoutSummary: |",
+ "main: Plain: Two words.\n|",
+ "PlainWords: [Two words.]|\nmain: WordCount: 2|\nmain: FuzzyWordCount: 100|\nmain: ReadingTime: 1|",
+ "main: Summary:
Two words .
|\n\nmain: Fragments: []|",
+ "main: Len: 27|",
+ )
+
+ // Foo scope (has more content).
+ b.AssertFileContent("public/p1/index.html",
+ "foo: Content:
Two words .
\n
Two words .|",
+ "foo: Fragments: [heading-1 heading-2]|",
+ )
+}
+
+func TestPageMarkupScope(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "rss", "section"]
+-- content/p1.md --
+---
+title: "Post 1"
+date: "2020-01-01"
+---
+
+# P1
+
+{{< foo >}}
+
+Begin:{{% includerendershortcodes "p2" %}}:End
+Begin:{{< includecontent "p3" >}}:End
+
+-- content/p2.md --
+---
+title: "Post 2"
+date: "2020-01-02"
+---
+
+# P2
+-- content/p3.md --
+---
+title: "Post 3"
+date: "2020-01-03"
+---
+
+# P3
+
+{{< foo >}}
+
+-- layouts/index.html --
+Home.
+{{ with site.GetPage "p1" }}
+ {{ with .Markup "home" }}
+ {{ .Render.Content }}
+ {{ end }}
+{{ end }}
+-- layouts/_default/single.html --
+Single.
+{{ with .Markup }}
+ {{ with .Render }}
+ {{ .Content }}
+ {{ end }}
+{{ end }}
+-- layouts/_default/_markup/render-heading.html --
+Render heading: title: {{ .Text}} scope: {{ hugo.Context.MarkupScope }}|
+-- layouts/shortcodes/foo.html --
+Foo scope: {{ hugo.Context.MarkupScope }}|
+-- layouts/shortcodes/includerendershortcodes.html --
+{{ $p := site.GetPage (.Get 0) }}
+includerendershortcodes: {{ hugo.Context.MarkupScope }}|{{ $p.Markup.RenderShortcodes }}|
+-- layouts/shortcodes/includecontent.html --
+{{ $p := site.GetPage (.Get 0) }}
+includecontent: {{ hugo.Context.MarkupScope }}|{{ $p.Markup.Render.Content }}|
+
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/p1/index.html", "Render heading: title: P1 scope: |", "Foo scope: |")
+
+ b.AssertFileContent("public/index.html",
+ "Render heading: title: P1 scope: home|",
+ "Foo scope: home|",
+ "Begin:\nincluderendershortcodes: home|\nRender heading: title: P2 scope: home| |:End",
+ "Begin:\nincludecontent: home|Render heading: title: P3 scope: home|Foo scope: home|\n|\n:End",
+ )
+}
+
+func TestPageMarkupWithoutSummary(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+summaryLength=5
+-- content/p1.md --
+---
+title: "Post 1"
+date: "2020-01-01"
+---
+This is summary.
+
+This is content.
+-- content/p2.md --
+---
+title: "Post 2"
+date: "2020-01-01"
+---
+This is some content about a summary and more.
+
+Another paragraph.
+
+Third paragraph.
+-- layouts/_default/single.html --
+Single.
+Page.Summary: {{ .Summary }}|
+{{ with .Markup.Render }}
+Content: {{ .Content }}|
+ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+WordCount: {{ .WordCount }}|
+FuzzyWordCount: {{ .FuzzyWordCount }}|
+{{ with .Summary }}
+Summary: {{ . }}|
+Summary Type: {{ .Type }}|
+Summary Truncated: {{ .Truncated }}|
+{{ end }}
+{{ end }}
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContentExact("public/p1/index.html",
+ "Content:
This is summary.
\n
This is content.
",
+ "ContentWithoutSummary:
This is content.
|",
+ "WordCount: 6|",
+ "FuzzyWordCount: 100|",
+ "Summary:
This is summary.
|",
+ "Summary Type: manual|",
+ "Summary Truncated: true|",
+ )
+ b.AssertFileContent("public/p2/index.html",
+ "Summary:
This is some content about a summary and more.
|",
+ "WordCount: 13|",
+ "FuzzyWordCount: 100|",
+ "Summary Type: auto",
+ "Summary Truncated: true",
+ )
+}
+
+func TestPageMarkupWithoutSummaryRST(t *testing.T) {
+ t.Parallel()
+ if !rst.Supports() {
+ t.Skip("Skip RST test as not supported")
+ }
+
+ files := `
+-- hugo.toml --
+summaryLength=5
+[security.exec]
+allow = ["rst", "python"]
+
+-- content/p1.rst --
+This is a story about a summary and more.
+
+Another paragraph.
+-- content/p2.rst --
+This is summary.
+
+This is content.
+-- layouts/_default/single.html --
+Single.
+Page.Summary: {{ .Summary }}|
+{{ with .Markup.Render }}
+Content: {{ .Content }}|
+ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+{{ with .Summary }}
+Summary: {{ . }}|
+Summary Type: {{ .Type }}|
+Summary Truncated: {{ .Truncated }}|
+{{ end }}
+{{ end }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Auto summary.
+ b.AssertFileContentExact("public/p1/index.html",
+ "Content:
\n\n\n
This is a story about a summary and more.
\n
Another paragraph.
\n
|",
+ "Summary:
\n\n\n
This is a story about a summary and more.
|\nSummary Type: auto|\nSummary Truncated: true|",
+ "ContentWithoutSummary:
|",
+ )
+
+ // Manual summary.
+ b.AssertFileContentExact("public/p2/index.html",
+ "Content:
\n\n\n
This is summary.
\n
This is content.
\n
|",
+ "ContentWithoutSummary:
|",
+ "Summary:
|\nSummary Type: manual|\nSummary Truncated: true|",
+ )
+}
+
+func TestPageMarkupWithoutSummaryAsciidoc(t *testing.T) {
+ t.Parallel()
+ if !asciidocext.Supports() {
+ t.Skip("Skip asiidoc test as not supported")
+ }
+
+ files := `
+-- hugo.toml --
+summaryLength=5
+[security.exec]
+allow = ["asciidoc", "python"]
+
+-- content/p1.ad --
+This is a story about a summary and more.
+
+Another paragraph.
+-- content/p2.ad --
+This is summary.
+
+This is content.
+-- layouts/_default/single.html --
+Single.
+Page.Summary: {{ .Summary }}|
+{{ with .Markup.Render }}
+Content: {{ .Content }}|
+ContentWithoutSummary: {{ .ContentWithoutSummary }}|
+{{ with .Summary }}
+Summary: {{ . }}|
+Summary Type: {{ .Type }}|
+Summary Truncated: {{ .Truncated }}|
+{{ end }}
+{{ end }}
+
+`
+
+ b := hugolib.Test(t, files)
+
+ // Auto summary.
+ b.AssertFileContentExact("public/p1/index.html",
+ "Content:
\n
This is a story about a summary and more.
\n
\n
\n|",
+ "Summary:
\n
This is a story about a summary and more.
\n
|",
+ "Summary Type: auto|\nSummary Truncated: true|",
+ "ContentWithoutSummary:
|",
+ )
+
+ // Manual summary.
+ b.AssertFileContentExact("public/p2/index.html",
+ "Content:
\n
|",
+ "ContentWithoutSummary:
|",
+ "Summary:
|\nSummary Type: manual|\nSummary Truncated: true|",
+ )
+}
diff --git a/resources/page/page_markup_test.go b/resources/page/page_markup_test.go
new file mode 100644
index 000000000..b7d363f8f
--- /dev/null
+++ b/resources/page/page_markup_test.go
@@ -0,0 +1,151 @@
+// 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 (
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/media"
+)
+
+func TestExtractSummaryFromHTML(t *testing.T) {
+ c := qt.New(t)
+
+ tests := []struct {
+ mt media.Type
+ input string
+ isCJK bool
+ numWords int
+ expectSummary string
+ expectContentWithoutSummary string
+ }{
+ {media.Builtin.ReStructuredTextType, "
", false, 70, "
", ""},
+ {media.Builtin.ReStructuredTextType, "
First paragraph
Second paragraph
", false, 2, `
`, "
"},
+ {media.Builtin.MarkdownType, "
First paragraph
", false, 10, "
First paragraph
", ""},
+ {media.Builtin.MarkdownType, "
First paragraph
Second paragraph
", false, 2, "
First paragraph
", "
Second paragraph
"},
+ {media.Builtin.MarkdownType, "
First paragraph
Second paragraph
Third paragraph
", false, 3, "
First paragraph
Second paragraph
", "
Third paragraph
"},
+ {media.Builtin.AsciiDocType, "
", false, 2, "
", "
"},
+ {media.Builtin.MarkdownType, "
这是中文,全中文
a这是中文,全中文
", true, 5, "
这是中文,全中文
", "
a这是中文,全中文
"},
+ }
+
+ for i, test := range tests {
+ summary := ExtractSummaryFromHTML(test.mt, test.input, test.numWords, test.isCJK)
+ c.Assert(summary.Summary(), qt.Equals, test.expectSummary, qt.Commentf("Summary %d", i))
+ c.Assert(summary.ContentWithoutSummary(), qt.Equals, test.expectContentWithoutSummary, qt.Commentf("ContentWithoutSummary %d", i))
+ }
+}
+
+func TestExtractSummaryFromHTMLWithDivider(t *testing.T) {
+ c := qt.New(t)
+
+ const divider = "FOOO"
+
+ tests := []struct {
+ mt media.Type
+ input string
+ expectSummary string
+ expectContentWithoutSummary string
+ expectContent string
+ }{
+ {media.Builtin.MarkdownType, "
First paragraph
FOOO
Second paragraph
", "
First paragraph
", "
Second paragraph
", "
First paragraph
Second paragraph
"},
+ {media.Builtin.MarkdownType, "
First paragraph
\n
FOOO
\n
Second paragraph
", "
First paragraph
", "
Second paragraph
", "
First paragraph
\n
Second paragraph
"},
+ {media.Builtin.MarkdownType, "
FOOO
\n
First paragraph
", "", "
First paragraph
", "
First paragraph
"},
+ {media.Builtin.MarkdownType, "
First paragraph
Second paragraphFOOO
Third paragraph
", "
First paragraph
Second paragraph
", "
Third paragraph
", "
First paragraph
Second paragraph
Third paragraph
"},
+ {media.Builtin.MarkdownType, "
这是中文,全中文FOOO
a这是中文,全中文
", "
这是中文,全中文
", "
a这是中文,全中文
", "
这是中文,全中文
a这是中文,全中文
"},
+ {media.Builtin.MarkdownType, `
a b ` + "\v" + ` c
` + "\n
FOOO
", "
a b \v c
", "", "
a b \v c
"},
+
+ {media.Builtin.HTMLType, "
First paragraph
FOOO
Second paragraph
", "
First paragraph
", "
Second paragraph
", "
First paragraph
Second paragraph
"},
+
+ {media.Builtin.ReStructuredTextType, "
\n\n\n
This is summary.
\n
FOOO
\n
This is content.
\n
", "
", "
", "
\n\n\n
This is summary.
\n
This is content.
\n
"},
+ {media.Builtin.ReStructuredTextType, "
First paragraphFOOO
Second paragraph
", "
", "
", `
First paragraph
Second paragraph
`},
+
+ {media.Builtin.AsciiDocType, "
", "
", "
", "
"},
+ {media.Builtin.AsciiDocType, "
\n
\n
\n", "
", "
", "
\n
"},
+ {media.Builtin.AsciiDocType, "
", "", "
", "
"},
+ {media.Builtin.AsciiDocType, "
", "
", "
", "
"},
+ }
+
+ for i, test := range tests {
+ summary := ExtractSummaryFromHTMLWithDivider(test.mt, test.input, divider)
+ c.Assert(summary.Summary(), qt.Equals, test.expectSummary, qt.Commentf("Summary %d", i))
+ c.Assert(summary.ContentWithoutSummary(), qt.Equals, test.expectContentWithoutSummary, qt.Commentf("ContentWithoutSummary %d", i))
+ c.Assert(summary.Content(), qt.Equals, test.expectContent, qt.Commentf("Content %d", i))
+ }
+}
+
+func TestExpandDivider(t *testing.T) {
+ c := qt.New(t)
+
+ for i, test := range []struct {
+ input string
+ divider string
+ ptag tagReStartEnd
+ expect string
+ expectEndMarkup string
+ }{
+ {"
First paragraph
\n
FOOO
\n
Second paragraph
", "FOOO", startEndP, "
FOOO
\n", ""},
+ {"
", "FOOO", startEndDiv, "
", ""},
+ {"
", "FOOO", startEndDiv, "
", ""},
+ {"
", "FOOO", startEndDiv, "FOOO", "
"},
+ {"