hugolib: Consider summary in front matter for .Summary
authorJim McDonald <Jim@mcdee.net>
Fri, 5 Apr 2019 17:11:04 +0000 (18:11 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 5 Apr 2019 17:11:04 +0000 (19:11 +0200)
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

helpers/content.go
helpers/content_test.go
hugolib/page__meta.go
hugolib/page__per_output.go
hugolib/page_test.go
hugolib/rss_test.go
hugolib/site_test.go
tpl/transform/transform.go

index be5090c211dcec2a1415a15d8f80ac65c342cf2e..3892647bb29d58480d4cb5372af94f34307e5230 100644 (file)
@@ -42,6 +42,12 @@ import (
 // SummaryDivider denotes where content summarization should end. The default is "<!--more-->".
 var SummaryDivider = []byte("<!--more-->")
 
+var (
+       openingPTag        = []byte("<p>")
+       closingPTag        = []byte("</p>")
+       paragraphIndicator = []byte("<p")
+)
+
 // ContentSpec provides functionality to render markdown content.
 type ContentSpec struct {
        BlackFriday                *BlackFriday
@@ -580,6 +586,21 @@ func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
        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 {
        return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n'
 }
index 1dd4a2fb83b3287c57c7698dca7858242a4240e4..709c811420c77b8b7ca275848f21877736c6c4c3 100644 (file)
@@ -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>"
 
+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) {
        type test struct {
                input, expected string
index 1e013db663783bf0655a99f80100ce2153ded9ec..eefecbe4a650fc6ac9826c891d299babadfc99c3 100644 (file)
@@ -64,6 +64,8 @@ type pageMeta struct {
        title     string
        linkTitle string
 
+       summary string
+
        resourcePath string
 
        weight int
@@ -361,6 +363,9 @@ func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}
                case "linktitle":
                        pm.linkTitle = cast.ToString(v)
                        pm.params[loki] = pm.linkTitle
+               case "summary":
+                       pm.summary = cast.ToString(v)
+                       pm.params[loki] = pm.summary
                case "description":
                        pm.description = cast.ToString(v)
                        pm.params[loki] = pm.description
index 05b35cc8703c22600abbf2936486cd8194afdc71..177e0420a8b347a8002d4e4fe81eaa15a8c54604 100644 (file)
@@ -128,6 +128,14 @@ func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutpu
                                                        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 {
-       if p.p.source.hasSummaryDivider {
+       if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
                return nil
        }
 
index 6d9d337eb374fb1f9ec24ffb3e1ef9e45b416650..a3b86ef2aea4eac069490c626063119522b00129 100644 (file)
@@ -45,6 +45,16 @@ const (
 
        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 = `---
 title: Simple
 ---
@@ -52,6 +62,16 @@ Summary Next Line
 
 <!--more-->
 Some more text
+`
+
+       simplePageWithSummaryParameter = `---
+title: SimpleWithSummaryParameter
+summary: "Page with summary parameter and [a link](http://www.example.com/)"
+---
+
+Some text.
+
+Some more text.
 `
 
        simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder = `---
@@ -519,6 +539,22 @@ func TestCreateNewPage(t *testing.T) {
        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) {
        t.Parallel()
        assertFunc := func(t *testing.T, ext string, pages page.Pages) {
@@ -532,6 +568,22 @@ func TestPageWithDelimiter(t *testing.T) {
        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
 // Also see https://github.com/gohugoio/hugo/issues/3977
 func TestPageWithDateFields(t *testing.T) {
index 683a737c59b4d89321d4e70ed35d4f460cbfb6df..38f0f1effb373b6f847d7778996ce70716952e03 100644 (file)
@@ -55,6 +55,9 @@ func TestRSSOutput(t *testing.T) {
        if c != rssLimit {
                t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c)
        }
+
+       // Encoded summary
+       th.assertFileContent(filepath.Join("public", rssURI), "<?xml", "description", "A &lt;em&gt;custom&lt;/em&gt; summary")
 }
 
 // Before Hugo 0.49 we set the pseudo page kind RSS on the page when output to RSS.
index 21575072dcc349bdad81069080497a50e8a999a7..5912abbc9c0224bdb6b9ceed4ecdd91c6c5b0e31 100644 (file)
@@ -586,6 +586,7 @@ date = "2012-01-01"
 publishdate = "2012-01-01"
 my_param = "baz"
 my_date = 2010-05-27T07:32:00Z
+summary = "A _custom_ summary"
 categories = [ "hugo" ]
 +++
 Front Matter with Ordered Pages 4. This is longer content`
index 42e36eb0f26285b6be9f556e4bbd80f8eac0dd0e..2aa0c1959407b70bdf91b2799c40d981b618ea15 100644 (file)
@@ -15,7 +15,6 @@
 package transform
 
 import (
-       "bytes"
        "html"
        "html/template"
 
@@ -91,12 +90,6 @@ func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) {
        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.
 func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
        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.
-       first := bytes.Index(m, markdownParagraphIndicator)
-       last := bytes.LastIndex(m, markdownParagraphIndicator)
-       if first == last {
-               m = bytes.TrimPrefix(m, markdownTrimPrefix)
-               m = bytes.TrimSuffix(m, markdownTrimSuffix)
-       }
+       m = ns.deps.ContentSpec.TrimShortHTML(m)
 
-       return template.HTML(m), nil
+       return helpers.BytesToHTML(m), nil
 }
 
 // Plainify returns a copy of s with all HTML tags removed.