diff --git a/hugolib/config.go b/hugolib/config.go index 030a5c945..b9b5d54bd 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -33,6 +33,7 @@ type Config struct { Title string Indexes map[string]string // singular, plural ProcessFilters map[string][]string + Params map[string]interface{} BuildDrafts, UglyUrls, Verbose bool } diff --git a/hugolib/page.go b/hugolib/page.go index ae0a630af..f0ec4063e 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -38,6 +38,7 @@ type Page struct { 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 @@ func (p Page) Plain() string { 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 @@ func (page *Page) convertMarkdown(lines io.Reader) { 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 @@ func (page *Page) convertRestructuredText(lines io.Reader) { 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) { diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 2a62e35ab..a85cb7ada 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -212,6 +212,19 @@ func checkPageDate(t *testing.T, page *Page, time time.Time) { } } +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 @@ func TestCreateNewPage(t *testing.T) { 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 @@ func TestPageWithDelimiter(t *testing.T) { checkPageSummary(t, p, "

Summary Next Line

\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 TestPageWithDate(t *testing.T) { } 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 @@ func TestWordCount(t *testing.T) { 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) { diff --git a/hugolib/site.go b/hugolib/site.go index 5e22fd969..2b7f8d98b 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -70,6 +70,7 @@ type Site struct { Alias target.AliasPublisher Completed chan bool RunMode runmode + params map[string]interface{} } type SiteInfo struct { @@ -79,6 +80,7 @@ type SiteInfo struct { LastChange time.Time Title string Config *Config + Params map[string]interface{} } type runmode struct { @@ -222,6 +224,7 @@ func (s *Site) initializeSiteInfo() { Title: s.Config.Title, Recent: &s.Pages, Config: &s.Config, + Params: s.Config.Params, } } diff --git a/hugolib/siteinfo_test.go b/hugolib/siteinfo_test.go new file mode 100644 index 000000000..f855dd970 --- /dev/null +++ b/hugolib/siteinfo_test.go @@ -0,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()) + } +} diff --git a/template/bundle/template.go b/template/bundle/template.go index 6d1653da8..8e8108272 100644 --- 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 @@ func Gt(a interface{}, b interface{}) bool { 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 @@ func NewTemplate() Template { "isset": IsSet, "echoParam": ReturnWhenSet, "safeHtml": SafeHtml, + "First": First, } templates.Funcs(funcMap) diff --git a/template/bundle/template_test.go b/template/bundle/template_test.go new file mode 100644 index 000000000..1d0fce1fb --- /dev/null +++ b/template/bundle/template_test.go @@ -0,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) + } + } +}