shithub: hugo

Download patch

ref: 40d05f12a7669f349d231448eaefe907b795a35b
parent: 6017599a3c0160d3f4daad671c1d6b1df47a4b3e
author: Phil Pennock <[email protected]>
date: Sun Nov 10 07:04:51 EST 2013

Truncated; .Site.Params; First function

* Add `.Truncated` bool to each page; will be set true if the
  `.Summary` is truncated and it's worth showing a "more" link of some
  kind.
* Add `Params` to the site config, defining `.Site.Params` accessible
  to each page; this lets the site maintainer associate arbitrary data
  with names, on a site-wide basis.
* Provide a `First` function to templates:
  * Use-case: `{{range First 5 .Site.Recent}}` or anything else which
    is a simple iterable provided by hugolib
* Tests by me for `.Truncated` and `First`

Also @noahcampbell contributed towards this:

* Add UnitTest for `.Site.Params`:
> Digging into this test case a bit more, I'm realizing that we need
> to create a param test case to ensure that for each type we render
> (page, index, homepage, rss, etc.) that the proper fields are
> represented.  This will help us refactor without fear in the
> future.

Sample config.yaml:

```yaml
title: "Test site"
params:
  Subtitle: "More tests always good"
  AuthorName: "John Doe"
  SidebarRecentLimit: 5
```

Signed-off-by: Noah Campbell <[email protected]>

--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -33,6 +33,7 @@
 	Title                                      string
 	Indexes                                    map[string]string // singular, plural
 	ProcessFilters                             map[string][]string
+	Params                                     map[string]interface{}
 	BuildDrafts, UglyUrls, Verbose             bool
 }
 
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -38,6 +38,7 @@
 	Images      []string
 	Content     template.HTML
 	Summary     template.HTML
+	Truncated   bool
 	plain       string // TODO should be []byte
 	Params      map[string]interface{}
 	contentType string
@@ -94,21 +95,26 @@
 	return p.plain
 }
 
-func getSummaryString(content []byte, fmt string) []byte {
+// nb: this is only called for recognised types; so while .html might work for
+// creating posts, it results in missing summaries.
+func getSummaryString(content []byte, pagefmt string) (summary []byte, truncates bool) {
 	if bytes.Contains(content, summaryDivider) {
 		// If user defines split:
 		// Split then render
-		return renderBytes(bytes.Split(content, summaryDivider)[0], fmt)
+		truncates = true // by definition
+		summary = renderBytes(bytes.Split(content, summaryDivider)[0], pagefmt)
 	} else {
 		// If hugo defines split:
 		// render, strip html, then split
-		plain := StripHTML(StripShortcodes(string(renderBytes(content, fmt))))
-		return []byte(TruncateWordsToWholeSentence(plain, summaryLength))
+		plain := strings.TrimSpace(StripHTML(StripShortcodes(string(renderBytes(content, pagefmt)))))
+		summary = []byte(TruncateWordsToWholeSentence(plain, summaryLength))
+		truncates = len(summary) != len(plain)
 	}
+	return
 }
 
-func renderBytes(content []byte, fmt string) []byte {
-	switch fmt {
+func renderBytes(content []byte, pagefmt string) []byte {
+	switch pagefmt {
 	default:
 		return blackfriday.MarkdownCommon(content)
 	case "markdown":
@@ -522,8 +528,9 @@
 	b.ReadFrom(lines)
 	content := b.Bytes()
 	page.Content = template.HTML(string(blackfriday.MarkdownCommon(RemoveSummaryDivider(content))))
-	summary := getSummaryString(content, "markdown")
+	summary, truncated := getSummaryString(content, "markdown")
 	page.Summary = template.HTML(string(summary))
+	page.Truncated = truncated
 }
 
 func (page *Page) convertRestructuredText(lines io.Reader) {
@@ -531,8 +538,9 @@
 	b.ReadFrom(lines)
 	content := b.Bytes()
 	page.Content = template.HTML(getRstContent(content))
-	summary := getSummaryString(content, "rst")
+	summary, truncated := getSummaryString(content, "rst")
 	page.Summary = template.HTML(string(summary))
+	page.Truncated = truncated
 }
 
 func (p *Page) TargetPath() (outfile string) {
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -212,6 +212,19 @@
 	}
 }
 
+func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) {
+	if page.Summary == "" {
+		t.Fatal("page has no summary, can not check truncation")
+	}
+	if page.Truncated != shouldBe {
+		if shouldBe {
+			t.Fatalf("page wasn't truncated: %s", msg)
+		} else {
+			t.Fatalf("page was truncated: %s", msg)
+		}
+	}
+}
+
 func TestCreateNewPage(t *testing.T) {
 	p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE), "simple.md")
 	if err != nil {
@@ -222,6 +235,7 @@
 	checkPageSummary(t, p, "Simple Page")
 	checkPageType(t, p, "page")
 	checkPageLayout(t, p, "page/single.html", "single.html")
+	checkTruncation(t, p, false, "simple short page")
 }
 
 func TestPageWithDelimiter(t *testing.T) {
@@ -234,6 +248,7 @@
 	checkPageSummary(t, p, "<p>Summary Next Line</p>\n")
 	checkPageType(t, p, "page")
 	checkPageLayout(t, p, "page/single.html", "single.html")
+	checkTruncation(t, p, true, "page with summary delimiter")
 }
 
 func TestPageWithShortCodeInSummary(t *testing.T) {
@@ -273,7 +288,7 @@
 }
 
 func TestWordCount(t *testing.T) {
-	p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_WITH_LONG_CONTENT), "simple")
+	p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE_WITH_LONG_CONTENT), "simple.md")
 	if err != nil {
 		t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
 	}
@@ -289,6 +304,8 @@
 	if p.MinRead != 3 {
 		t.Fatalf("incorrect min read. expected %v, got %v", 3, p.MinRead)
 	}
+
+	checkTruncation(t, p, true, "long page")
 }
 
 func TestCreatePage(t *testing.T) {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -70,6 +70,7 @@
 	Alias      target.AliasPublisher
 	Completed  chan bool
 	RunMode    runmode
+	params     map[string]interface{}
 }
 
 type SiteInfo struct {
@@ -79,6 +80,7 @@
 	LastChange time.Time
 	Title      string
 	Config     *Config
+	Params     map[string]interface{}
 }
 
 type runmode struct {
@@ -222,6 +224,7 @@
 		Title:   s.Config.Title,
 		Recent:  &s.Pages,
 		Config:  &s.Config,
+		Params:  s.Config.Params,
 	}
 }
 
--- /dev/null
+++ b/hugolib/siteinfo_test.go
@@ -1,0 +1,32 @@
+package hugolib
+
+import (
+	"testing"
+	"bytes"
+)
+
+const SITE_INFO_PARAM_TEMPLATE = `{{ .Site.Params.MyGlobalParam }}`
+
+
+func TestSiteInfoParams(t *testing.T) {
+	s := &Site{
+		Config: Config{Params: map[string]interface{}{"MyGlobalParam": "FOOBAR_PARAM"}},
+	}
+
+	s.initialize()
+	if s.Info.Params["MyGlobalParam"] != "FOOBAR_PARAM" {
+		t.Errorf("Unable to set site.Info.Param")
+	}
+	s.prepTemplates()
+	s.addTemplate("template", SITE_INFO_PARAM_TEMPLATE)
+	buf := new(bytes.Buffer)
+
+	err := s.renderThing(s.NewNode(), "template", buf)
+	if err != nil {
+		t.Errorf("Unable to render template: %s", err)
+	}
+
+	if buf.String() != "FOOBAR_PARAM" {
+		t.Errorf("Expected FOOBAR_PARAM: got %s", buf.String())
+	}
+}
--- a/template/bundle/template.go
+++ b/template/bundle/template.go
@@ -1,6 +1,7 @@
 package bundle
 
 import (
+	"errors"
 	"github.com/eknkc/amber"
 	helpers "github.com/spf13/hugo/template"
 	"html/template"
@@ -40,6 +41,36 @@
 	return left > right
 }
 
+// First is exposed to templates, to iterate over the first N items in a
+// rangeable list.
+func First(limit int, seq interface{}) (interface{}, error) {
+	if limit < 1 {
+		return nil, errors.New("can't return negative/empty count of items from sequence")
+	}
+
+	seqv := reflect.ValueOf(seq)
+	// this is better than my first pass; ripped from text/template/exec.go indirect():
+	for ; seqv.Kind() == reflect.Ptr || seqv.Kind() == reflect.Interface; seqv = seqv.Elem() {
+		if seqv.IsNil() {
+			return nil, errors.New("can't iterate over a nil value")
+		}
+		if seqv.Kind() == reflect.Interface && seqv.NumMethod() > 0 {
+			break
+		}
+	}
+
+	switch seqv.Kind() {
+	case reflect.Array, reflect.Slice, reflect.String:
+		// okay
+	default:
+		return nil, errors.New("can't iterate over " + reflect.ValueOf(seq).Type().String())
+	}
+	if limit > seqv.Len() {
+		limit = seqv.Len()
+	}
+	return seqv.Slice(0, limit).Interface(), nil
+}
+
 func IsSet(a interface{}, key interface{}) bool {
 	av := reflect.ValueOf(a)
 	kv := reflect.ValueOf(key)
@@ -113,6 +144,7 @@
 		"isset":     IsSet,
 		"echoParam": ReturnWhenSet,
 		"safeHtml":  SafeHtml,
+		"First":     First,
 	}
 
 	templates.Funcs(funcMap)
--- /dev/null
+++ b/template/bundle/template_test.go
@@ -1,0 +1,55 @@
+package bundle
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestGt(t *testing.T) {
+	for i, this := range []struct{
+		left interface{}
+		right interface{}
+		leftShouldWin bool
+	}{
+		{ 5, 8, false },
+		{ 8, 5, true },
+		{ 5, 5, false },
+		{ -2, 1, false },
+		{ 2, -5, true },
+		{ "8", "5", true },
+		{ "5", "0001", true },
+		{ []int{100,99}, []int{1,2,3,4}, false },
+	} {
+		leftIsBigger := Gt(this.left, this.right)
+		if leftIsBigger != this.leftShouldWin {
+			var which string
+			if leftIsBigger {
+				which = "expected right to be bigger, but left was"
+			} else {
+				which = "expected left to be bigger, but right was"
+			}
+			t.Errorf("[%d] %v compared to %v: %s", i, this.left, this.right, which)
+		}
+	}
+}
+
+func TestFirst(t *testing.T) {
+	for i, this := range []struct{
+		count int
+		sequence interface{}
+		expect interface{}
+	} {
+		{ 2, []string{"a", "b", "c"}, []string{"a", "b"} },
+		{ 3, []string{"a", "b"}, []string{"a", "b"} },
+		{ 2, []int{100, 200, 300}, []int{100, 200} },
+	} {
+		results, err := First(this.count, this.sequence)
+		if err != nil {
+			t.Errorf("[%d] failed: %s", i, err)
+			continue
+		}
+		if !reflect.DeepEqual(results, this.expect) {
+			t.Errorf("[%d] First %d items, got %v but expected %v", i, this.count, results, this.expect)
+		}
+	}
+}