mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
hugolib: Consider summary in front matter for .Summary
Add the ability to have a `summary` page variable that overrides the auto-generated summary. Logic for obtaining summary becomes: * if summary divider is present in content, use the text above it * if summary variables is present in page metadata, use that * auto-generate summary from first _x_ words of the content Fixes #5800
This commit is contained in:
parent
ebab291c0e
commit
3a62d54745
8 changed files with 115 additions and 15 deletions
|
@ -42,6 +42,12 @@ import (
|
||||||
// SummaryDivider denotes where content summarization should end. The default is "<!--more-->".
|
// SummaryDivider denotes where content summarization should end. The default is "<!--more-->".
|
||||||
var SummaryDivider = []byte("<!--more-->")
|
var SummaryDivider = []byte("<!--more-->")
|
||||||
|
|
||||||
|
var (
|
||||||
|
openingPTag = []byte("<p>")
|
||||||
|
closingPTag = []byte("</p>")
|
||||||
|
paragraphIndicator = []byte("<p")
|
||||||
|
)
|
||||||
|
|
||||||
// ContentSpec provides functionality to render markdown content.
|
// ContentSpec provides functionality to render markdown content.
|
||||||
type ContentSpec struct {
|
type ContentSpec struct {
|
||||||
BlackFriday *BlackFriday
|
BlackFriday *BlackFriday
|
||||||
|
@ -580,6 +586,21 @@ func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
|
||||||
return strings.TrimSpace(s[:endIndex]), endIndex < len(s)
|
return strings.TrimSpace(s[:endIndex]), endIndex < len(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TrimShortHTML removes the <p>/</p> tags from HTML input in the situation
|
||||||
|
// where said tags are the only <p> tags in the input and enclose the content
|
||||||
|
// of the input (whitespace excluded).
|
||||||
|
func (c *ContentSpec) TrimShortHTML(input []byte) []byte {
|
||||||
|
first := bytes.Index(input, paragraphIndicator)
|
||||||
|
last := bytes.LastIndex(input, paragraphIndicator)
|
||||||
|
if first == last {
|
||||||
|
input = bytes.TrimSpace(input)
|
||||||
|
input = bytes.TrimPrefix(input, openingPTag)
|
||||||
|
input = bytes.TrimSuffix(input, closingPTag)
|
||||||
|
input = bytes.TrimSpace(input)
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
func isEndOfSentence(r rune) bool {
|
func isEndOfSentence(r rune) bool {
|
||||||
return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n'
|
return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n'
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,28 @@ import (
|
||||||
|
|
||||||
const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>"
|
const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>"
|
||||||
|
|
||||||
|
func TestTrimShortHTML(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input, output []byte
|
||||||
|
}{
|
||||||
|
{[]byte(""), []byte("")},
|
||||||
|
{[]byte("Plain text"), []byte("Plain text")},
|
||||||
|
{[]byte(" \t\n Whitespace text\n\n"), []byte("Whitespace text")},
|
||||||
|
{[]byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")},
|
||||||
|
{[]byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")},
|
||||||
|
{[]byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")},
|
||||||
|
{[]byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")},
|
||||||
|
}
|
||||||
|
|
||||||
|
c := newTestContentSpec()
|
||||||
|
for i, test := range tests {
|
||||||
|
output := c.TrimShortHTML(test.input)
|
||||||
|
if bytes.Compare(test.output, output) != 0 {
|
||||||
|
t.Errorf("Test %d failed. Expected %q got %q", i, test.output, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStripHTML(t *testing.T) {
|
func TestStripHTML(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
input, expected string
|
input, expected string
|
||||||
|
|
|
@ -64,6 +64,8 @@ type pageMeta struct {
|
||||||
title string
|
title string
|
||||||
linkTitle string
|
linkTitle string
|
||||||
|
|
||||||
|
summary string
|
||||||
|
|
||||||
resourcePath string
|
resourcePath string
|
||||||
|
|
||||||
weight int
|
weight int
|
||||||
|
@ -361,6 +363,9 @@ func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}
|
||||||
case "linktitle":
|
case "linktitle":
|
||||||
pm.linkTitle = cast.ToString(v)
|
pm.linkTitle = cast.ToString(v)
|
||||||
pm.params[loki] = pm.linkTitle
|
pm.params[loki] = pm.linkTitle
|
||||||
|
case "summary":
|
||||||
|
pm.summary = cast.ToString(v)
|
||||||
|
pm.params[loki] = pm.summary
|
||||||
case "description":
|
case "description":
|
||||||
pm.description = cast.ToString(v)
|
pm.description = cast.ToString(v)
|
||||||
pm.params[loki] = pm.description
|
pm.params[loki] = pm.description
|
||||||
|
|
|
@ -128,6 +128,14 @@ func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutpu
|
||||||
cp.summary = helpers.BytesToHTML(summary)
|
cp.summary = helpers.BytesToHTML(summary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if cp.p.m.summary != "" {
|
||||||
|
html := cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
|
||||||
|
Content: []byte(cp.p.m.summary), RenderTOC: false, PageFmt: cp.p.m.markup,
|
||||||
|
Cfg: p.Language(),
|
||||||
|
DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
|
||||||
|
Config: cp.p.getRenderingConfig()})
|
||||||
|
html = cp.p.s.ContentSpec.TrimShortHTML(html)
|
||||||
|
cp.summary = helpers.BytesToHTML(html)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +279,7 @@ func (p *pageContentOutput) WordCount() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageContentOutput) setAutoSummary() error {
|
func (p *pageContentOutput) setAutoSummary() error {
|
||||||
if p.p.source.hasSummaryDivider {
|
if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,16 @@ const (
|
||||||
|
|
||||||
simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content"
|
simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content"
|
||||||
|
|
||||||
|
simplePageWithoutSummaryDelimiter = `---
|
||||||
|
title: SimpleWithoutSummaryDelimiter
|
||||||
|
---
|
||||||
|
[Lorem ipsum](https://lipsum.com/) 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.
|
||||||
|
|
||||||
|
Further text.
|
||||||
|
`
|
||||||
|
|
||||||
simplePageWithSummaryDelimiter = `---
|
simplePageWithSummaryDelimiter = `---
|
||||||
title: Simple
|
title: Simple
|
||||||
---
|
---
|
||||||
|
@ -52,6 +62,16 @@ Summary Next Line
|
||||||
|
|
||||||
<!--more-->
|
<!--more-->
|
||||||
Some more text
|
Some more text
|
||||||
|
`
|
||||||
|
|
||||||
|
simplePageWithSummaryParameter = `---
|
||||||
|
title: SimpleWithSummaryParameter
|
||||||
|
summary: "Page with summary parameter and [a link](http://www.example.com/)"
|
||||||
|
---
|
||||||
|
|
||||||
|
Some text.
|
||||||
|
|
||||||
|
Some more text.
|
||||||
`
|
`
|
||||||
|
|
||||||
simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder = `---
|
simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder = `---
|
||||||
|
@ -519,6 +539,22 @@ func TestCreateNewPage(t *testing.T) {
|
||||||
testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage)
|
testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPageSummary(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||||
|
p := pages[0]
|
||||||
|
checkPageTitle(t, p, "SimpleWithoutSummaryDelimiter")
|
||||||
|
// Source is not Asciidoctor- or RST-compatibile 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)
|
||||||
|
}
|
||||||
|
checkPageType(t, p, "page")
|
||||||
|
}
|
||||||
|
|
||||||
|
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithoutSummaryDelimiter)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPageWithDelimiter(t *testing.T) {
|
func TestPageWithDelimiter(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||||
|
@ -532,6 +568,22 @@ func TestPageWithDelimiter(t *testing.T) {
|
||||||
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter)
|
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPageWithSummaryParameter(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||||
|
p := pages[0]
|
||||||
|
checkPageTitle(t, p, "SimpleWithSummaryParameter")
|
||||||
|
checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n\n<p>Some more text.</p>\n"), ext)
|
||||||
|
// Summary is not Asciidoctor- or RST-compatibile so don't test them
|
||||||
|
if ext != "ad" && ext != "rst" {
|
||||||
|
checkPageSummary(t, p, normalizeExpected(ext, "Page with summary parameter and <a href=\"http://www.example.com/\">a link</a>"), ext)
|
||||||
|
}
|
||||||
|
checkPageType(t, p, "page")
|
||||||
|
}
|
||||||
|
|
||||||
|
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryParameter)
|
||||||
|
}
|
||||||
|
|
||||||
// Issue #3854
|
// Issue #3854
|
||||||
// Also see https://github.com/gohugoio/hugo/issues/3977
|
// Also see https://github.com/gohugoio/hugo/issues/3977
|
||||||
func TestPageWithDateFields(t *testing.T) {
|
func TestPageWithDateFields(t *testing.T) {
|
||||||
|
|
|
@ -55,6 +55,9 @@ func TestRSSOutput(t *testing.T) {
|
||||||
if c != rssLimit {
|
if c != rssLimit {
|
||||||
t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c)
|
t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encoded summary
|
||||||
|
th.assertFileContent(filepath.Join("public", rssURI), "<?xml", "description", "A <em>custom</em> summary")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before Hugo 0.49 we set the pseudo page kind RSS on the page when output to RSS.
|
// Before Hugo 0.49 we set the pseudo page kind RSS on the page when output to RSS.
|
||||||
|
|
|
@ -586,6 +586,7 @@ date = "2012-01-01"
|
||||||
publishdate = "2012-01-01"
|
publishdate = "2012-01-01"
|
||||||
my_param = "baz"
|
my_param = "baz"
|
||||||
my_date = 2010-05-27T07:32:00Z
|
my_date = 2010-05-27T07:32:00Z
|
||||||
|
summary = "A _custom_ summary"
|
||||||
categories = [ "hugo" ]
|
categories = [ "hugo" ]
|
||||||
+++
|
+++
|
||||||
Front Matter with Ordered Pages 4. This is longer content`
|
Front Matter with Ordered Pages 4. This is longer content`
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
package transform
|
package transform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
|
@ -91,12 +90,6 @@ func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) {
|
||||||
return html.UnescapeString(ss), nil
|
return html.UnescapeString(ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
markdownTrimPrefix = []byte("<p>")
|
|
||||||
markdownTrimSuffix = []byte("</p>\n")
|
|
||||||
markdownParagraphIndicator = []byte("<p")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Markdownify renders a given input from Markdown to HTML.
|
// Markdownify renders a given input from Markdown to HTML.
|
||||||
func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
|
func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
|
||||||
ss, err := cast.ToStringE(s)
|
ss, err := cast.ToStringE(s)
|
||||||
|
@ -114,14 +107,9 @@ func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Strip if this is a short inline type of text.
|
// Strip if this is a short inline type of text.
|
||||||
first := bytes.Index(m, markdownParagraphIndicator)
|
m = ns.deps.ContentSpec.TrimShortHTML(m)
|
||||||
last := bytes.LastIndex(m, markdownParagraphIndicator)
|
|
||||||
if first == last {
|
|
||||||
m = bytes.TrimPrefix(m, markdownTrimPrefix)
|
|
||||||
m = bytes.TrimSuffix(m, markdownTrimSuffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
return template.HTML(m), nil
|
return helpers.BytesToHTML(m), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plainify returns a copy of s with all HTML tags removed.
|
// Plainify returns a copy of s with all HTML tags removed.
|
||||||
|
|
Loading…
Reference in a new issue