mirror of
https://github.com/gohugoio/hugo.git
synced 2025-03-21 21:22:17 +00:00
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>
This commit is contained in:
parent
6017599a3c
commit
40d05f12a7
7 changed files with 157 additions and 9 deletions
|
@ -33,6 +33,7 @@ type Config struct {
|
||||||
Title string
|
Title string
|
||||||
Indexes map[string]string // singular, plural
|
Indexes map[string]string // singular, plural
|
||||||
ProcessFilters map[string][]string
|
ProcessFilters map[string][]string
|
||||||
|
Params map[string]interface{}
|
||||||
BuildDrafts, UglyUrls, Verbose bool
|
BuildDrafts, UglyUrls, Verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ type Page struct {
|
||||||
Images []string
|
Images []string
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
Summary template.HTML
|
Summary template.HTML
|
||||||
|
Truncated bool
|
||||||
plain string // TODO should be []byte
|
plain string // TODO should be []byte
|
||||||
Params map[string]interface{}
|
Params map[string]interface{}
|
||||||
contentType string
|
contentType string
|
||||||
|
@ -94,21 +95,26 @@ func (p Page) Plain() string {
|
||||||
return p.plain
|
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 bytes.Contains(content, summaryDivider) {
|
||||||
// If user defines split:
|
// If user defines split:
|
||||||
// Split then render
|
// Split then render
|
||||||
return renderBytes(bytes.Split(content, summaryDivider)[0], fmt)
|
truncates = true // by definition
|
||||||
|
summary = renderBytes(bytes.Split(content, summaryDivider)[0], pagefmt)
|
||||||
} else {
|
} else {
|
||||||
// If hugo defines split:
|
// If hugo defines split:
|
||||||
// render, strip html, then split
|
// render, strip html, then split
|
||||||
plain := StripHTML(StripShortcodes(string(renderBytes(content, fmt))))
|
plain := strings.TrimSpace(StripHTML(StripShortcodes(string(renderBytes(content, pagefmt)))))
|
||||||
return []byte(TruncateWordsToWholeSentence(plain, summaryLength))
|
summary = []byte(TruncateWordsToWholeSentence(plain, summaryLength))
|
||||||
|
truncates = len(summary) != len(plain)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderBytes(content []byte, fmt string) []byte {
|
func renderBytes(content []byte, pagefmt string) []byte {
|
||||||
switch fmt {
|
switch pagefmt {
|
||||||
default:
|
default:
|
||||||
return blackfriday.MarkdownCommon(content)
|
return blackfriday.MarkdownCommon(content)
|
||||||
case "markdown":
|
case "markdown":
|
||||||
|
@ -522,8 +528,9 @@ func (page *Page) convertMarkdown(lines io.Reader) {
|
||||||
b.ReadFrom(lines)
|
b.ReadFrom(lines)
|
||||||
content := b.Bytes()
|
content := b.Bytes()
|
||||||
page.Content = template.HTML(string(blackfriday.MarkdownCommon(RemoveSummaryDivider(content))))
|
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.Summary = template.HTML(string(summary))
|
||||||
|
page.Truncated = truncated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (page *Page) convertRestructuredText(lines io.Reader) {
|
func (page *Page) convertRestructuredText(lines io.Reader) {
|
||||||
|
@ -531,8 +538,9 @@ func (page *Page) convertRestructuredText(lines io.Reader) {
|
||||||
b.ReadFrom(lines)
|
b.ReadFrom(lines)
|
||||||
content := b.Bytes()
|
content := b.Bytes()
|
||||||
page.Content = template.HTML(getRstContent(content))
|
page.Content = template.HTML(getRstContent(content))
|
||||||
summary := getSummaryString(content, "rst")
|
summary, truncated := getSummaryString(content, "rst")
|
||||||
page.Summary = template.HTML(string(summary))
|
page.Summary = template.HTML(string(summary))
|
||||||
|
page.Truncated = truncated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Page) TargetPath() (outfile string) {
|
func (p *Page) TargetPath() (outfile string) {
|
||||||
|
|
|
@ -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) {
|
func TestCreateNewPage(t *testing.T) {
|
||||||
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE), "simple.md")
|
p, err := ReadFrom(strings.NewReader(SIMPLE_PAGE), "simple.md")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -222,6 +235,7 @@ func TestCreateNewPage(t *testing.T) {
|
||||||
checkPageSummary(t, p, "Simple Page")
|
checkPageSummary(t, p, "Simple Page")
|
||||||
checkPageType(t, p, "page")
|
checkPageType(t, p, "page")
|
||||||
checkPageLayout(t, p, "page/single.html", "single.html")
|
checkPageLayout(t, p, "page/single.html", "single.html")
|
||||||
|
checkTruncation(t, p, false, "simple short page")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPageWithDelimiter(t *testing.T) {
|
func TestPageWithDelimiter(t *testing.T) {
|
||||||
|
@ -234,6 +248,7 @@ func TestPageWithDelimiter(t *testing.T) {
|
||||||
checkPageSummary(t, p, "<p>Summary Next Line</p>\n")
|
checkPageSummary(t, p, "<p>Summary Next Line</p>\n")
|
||||||
checkPageType(t, p, "page")
|
checkPageType(t, p, "page")
|
||||||
checkPageLayout(t, p, "page/single.html", "single.html")
|
checkPageLayout(t, p, "page/single.html", "single.html")
|
||||||
|
checkTruncation(t, p, true, "page with summary delimiter")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPageWithShortCodeInSummary(t *testing.T) {
|
func TestPageWithShortCodeInSummary(t *testing.T) {
|
||||||
|
@ -273,7 +288,7 @@ func TestPageWithDate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWordCount(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 {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create a page with frontmatter and body content: %s", err)
|
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 {
|
if p.MinRead != 3 {
|
||||||
t.Fatalf("incorrect min read. expected %v, got %v", 3, p.MinRead)
|
t.Fatalf("incorrect min read. expected %v, got %v", 3, p.MinRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkTruncation(t, p, true, "long page")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreatePage(t *testing.T) {
|
func TestCreatePage(t *testing.T) {
|
||||||
|
|
|
@ -70,6 +70,7 @@ type Site struct {
|
||||||
Alias target.AliasPublisher
|
Alias target.AliasPublisher
|
||||||
Completed chan bool
|
Completed chan bool
|
||||||
RunMode runmode
|
RunMode runmode
|
||||||
|
params map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SiteInfo struct {
|
type SiteInfo struct {
|
||||||
|
@ -79,6 +80,7 @@ type SiteInfo struct {
|
||||||
LastChange time.Time
|
LastChange time.Time
|
||||||
Title string
|
Title string
|
||||||
Config *Config
|
Config *Config
|
||||||
|
Params map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type runmode struct {
|
type runmode struct {
|
||||||
|
@ -222,6 +224,7 @@ func (s *Site) initializeSiteInfo() {
|
||||||
Title: s.Config.Title,
|
Title: s.Config.Title,
|
||||||
Recent: &s.Pages,
|
Recent: &s.Pages,
|
||||||
Config: &s.Config,
|
Config: &s.Config,
|
||||||
|
Params: s.Config.Params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
hugolib/siteinfo_test.go
Normal file
32
hugolib/siteinfo_test.go
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package bundle
|
package bundle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/eknkc/amber"
|
"github.com/eknkc/amber"
|
||||||
helpers "github.com/spf13/hugo/template"
|
helpers "github.com/spf13/hugo/template"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
@ -40,6 +41,36 @@ func Gt(a interface{}, b interface{}) bool {
|
||||||
return left > right
|
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 {
|
func IsSet(a interface{}, key interface{}) bool {
|
||||||
av := reflect.ValueOf(a)
|
av := reflect.ValueOf(a)
|
||||||
kv := reflect.ValueOf(key)
|
kv := reflect.ValueOf(key)
|
||||||
|
@ -113,6 +144,7 @@ func NewTemplate() Template {
|
||||||
"isset": IsSet,
|
"isset": IsSet,
|
||||||
"echoParam": ReturnWhenSet,
|
"echoParam": ReturnWhenSet,
|
||||||
"safeHtml": SafeHtml,
|
"safeHtml": SafeHtml,
|
||||||
|
"First": First,
|
||||||
}
|
}
|
||||||
|
|
||||||
templates.Funcs(funcMap)
|
templates.Funcs(funcMap)
|
||||||
|
|
55
template/bundle/template_test.go
Normal file
55
template/bundle/template_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue