diff --git a/docs/content/getting-started/configuration.md b/docs/content/getting-started/configuration.md index b81d878b356..2dafdb52b43 100644 --- a/docs/content/getting-started/configuration.md +++ b/docs/content/getting-started/configuration.md @@ -111,6 +111,8 @@ googleAnalytics: "" # if true, auto-detect Chinese/Japanese/Korean Languages in the content. (.Summary and .WordCount can work properly in CJKLanguage) hasCJKLanguage: false languageCode: "" +# the length of text to show in a .Summary +summaryLength: 70 layoutDir: "layouts" # Enable Logging log: false @@ -252,6 +254,8 @@ googleAnalytics = "" # if true, auto-detect Chinese/Japanese/Korean Languages in the content. (.Summary and .WordCount can work properly in CJKLanguage) hasCJKLanguage = false languageCode = "" +# the length of text to show in a .Summary +summaryLength: 70 layoutDir = "layouts" # Enable Logging log = false diff --git a/helpers/content.go b/helpers/content.go index 7f5975869fe..a79da090bd7 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -36,9 +36,6 @@ import ( "strings" ) -// SummaryLength is the length of the summary that Hugo extracts from a content. -var SummaryLength = 70 - // SummaryDivider denotes where content summarization should end. The default is "". var SummaryDivider = []byte("") @@ -47,6 +44,8 @@ type ContentSpec struct { blackfriday map[string]interface{} footnoteAnchorPrefix string footnoteReturnLinkContents string + // SummaryLength is the length of the summary that Hugo extracts from a content. + summaryLength int Highlight func(code, lang, optsStr string) (string, error) defatultPygmentsOpts map[string]string @@ -61,6 +60,7 @@ func NewContentSpec(cfg config.Provider) (*ContentSpec, error) { blackfriday: cfg.GetStringMap("blackfriday"), footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"), footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"), + summaryLength: cfg.GetInt("summaryLength"), cfg: cfg, } @@ -480,20 +480,20 @@ func totalWordsOld(s string) int { } // TruncateWordsByRune truncates words by runes. -func TruncateWordsByRune(words []string, max int) (string, bool) { +func (c *ContentSpec) TruncateWordsByRune(words []string) (string, bool) { count := 0 for index, word := range words { - if count >= max { + if count >= c.summaryLength { return strings.Join(words[:index], " "), true } runeCount := utf8.RuneCountInString(word) if len(word) == runeCount { count++ - } else if count+runeCount < max { + } else if count+runeCount < c.summaryLength { count += runeCount } else { for ri := range word { - if count >= max { + if count >= c.summaryLength { truncatedWords := append(words[:index], word[:ri]) return strings.Join(truncatedWords, " "), true } @@ -507,8 +507,7 @@ func TruncateWordsByRune(words []string, max int) (string, bool) { // TruncateWordsToWholeSentence takes content and truncates to whole sentence // limited by max number of words. It also returns whether it is truncated. -func TruncateWordsToWholeSentence(s string, max int) (string, bool) { - +func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) { var ( wordCount = 0 lastWordIndex = -1 @@ -519,7 +518,7 @@ func TruncateWordsToWholeSentence(s string, max int) (string, bool) { wordCount++ lastWordIndex = i - if wordCount >= max { + if wordCount >= c.summaryLength { break } @@ -551,24 +550,24 @@ func isEndOfSentence(r rune) bool { } // Kept only for benchmark. -func truncateWordsToWholeSentenceOld(content string, max int) (string, bool) { +func (c *ContentSpec) truncateWordsToWholeSentenceOld(content string) (string, bool) { words := strings.Fields(content) - if max >= len(words) { + if c.summaryLength >= len(words) { return strings.Join(words, " "), false } - for counter, word := range words[max:] { + for counter, word := range words[c.summaryLength:] { if strings.HasSuffix(word, ".") || strings.HasSuffix(word, "?") || strings.HasSuffix(word, ".\"") || strings.HasSuffix(word, "!") { - upper := max + counter + 1 + upper := c.summaryLength + counter + 1 return strings.Join(words[:upper], " "), (upper < len(words)) } } - return strings.Join(words[:max], " "), true + return strings.Join(words[:c.summaryLength], " "), true } func getAsciidocExecPath() string { diff --git a/helpers/content_test.go b/helpers/content_test.go index b0afb9cbd9e..8f2d44cd9cf 100644 --- a/helpers/content_test.go +++ b/helpers/content_test.go @@ -76,20 +76,23 @@ func TestBytesToHTML(t *testing.T) { var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20) func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) { + c := newTestContentSpec() b.ResetTimer() for i := 0; i < b.N; i++ { - TruncateWordsToWholeSentence(benchmarkTruncateString, SummaryLength) + c.TruncateWordsToWholeSentence(benchmarkTruncateString) } } func BenchmarkTestTruncateWordsToWholeSentenceOld(b *testing.B) { + c := newTestContentSpec() b.ResetTimer() for i := 0; i < b.N; i++ { - truncateWordsToWholeSentenceOld(benchmarkTruncateString, SummaryLength) + c.truncateWordsToWholeSentenceOld(benchmarkTruncateString) } } func TestTruncateWordsToWholeSentence(t *testing.T) { + c := newTestContentSpec() type test struct { input, expected string max int @@ -104,9 +107,11 @@ func TestTruncateWordsToWholeSentence(t *testing.T) { {"To be. Or not to be. That's the question.", "To be.", 1, true}, {" \nThis is not a sentence\nAnd this is another", "This is not a sentence", 4, true}, {"", "", 10, false}, + {"This... is a more difficult test?", "This... is a more difficult test?", 1, false}, } for i, d := range data { - output, truncated := TruncateWordsToWholeSentence(d.input, d.max) + c.summaryLength = d.max + output, truncated := c.TruncateWordsToWholeSentence(d.input) if d.expected != output { t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) } @@ -118,6 +123,7 @@ func TestTruncateWordsToWholeSentence(t *testing.T) { } func TestTruncateWordsByRune(t *testing.T) { + c := newTestContentSpec() type test struct { input, expected string max int @@ -139,7 +145,8 @@ func TestTruncateWordsByRune(t *testing.T) { {" \nThis is not a sentence\n ", "This is not", 3, true}, } for i, d := range data { - output, truncated := TruncateWordsByRune(strings.Fields(d.input), d.max) + c.summaryLength = d.max + output, truncated := c.TruncateWordsByRune(strings.Fields(d.input)) if d.expected != output { t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) } diff --git a/hugolib/config.go b/hugolib/config.go index 2406ba77159..d0ade018fca 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -135,6 +135,7 @@ func loadDefaultSettingsFor(v *viper.Viper) error { v.SetDefault("newContentEditor", "") v.SetDefault("paginate", 10) v.SetDefault("paginatePath", "page") + v.SetDefault("summaryLength", 70) v.SetDefault("blackfriday", c.NewBlackfriday()) v.SetDefault("rSSUri", "index.xml") v.SetDefault("rssLimit", -1) diff --git a/hugolib/page.go b/hugolib/page.go index d5c444ed665..12bdf312b92 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -677,9 +677,9 @@ func (p *Page) setAutoSummary() error { var summary string var truncated bool if p.isCJKLanguage { - summary, truncated = helpers.TruncateWordsByRune(p.PlainWords(), helpers.SummaryLength) + summary, truncated = p.s.ContentSpec.TruncateWordsByRune(p.PlainWords()) } else { - summary, truncated = helpers.TruncateWordsToWholeSentence(p.Plain(), helpers.SummaryLength) + summary, truncated = p.s.ContentSpec.TruncateWordsToWholeSentence(p.Plain()) } p.Summary = template.HTML(summary) p.Truncated = truncated