ref: 40d05f12a7669f349d231448eaefe907b795a35b
parent: 6017599a3c0160d3f4daad671c1d6b1df47a4b3e
author: Phil Pennock <pdp@spodhuis.org>
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 <noahcampbell@gmail.com>
--- 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)+ }
+ }
+}
--
⑨