From 22c493fac4103b70b1e236e5a9f84160f21d95a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 21 Feb 2018 11:09:08 +0100 Subject: [PATCH 01/13] hugolib: Extract date and slug from filename This commit adds a new config option which, when enabled and no date is set in front matter, will make Hugo try to parse the date from the content filename. Also, the filenames in these cases will make for very poor permalinks, so we will also use the remaining part as the page `slug` if that value is not set in front matter. This should make it easier to move content from Jekyll to Hugo. To enable, put this in your `config.toml`: ```toml [frontmatter] defaultDate = ["filename"] ``` Fixes #285 Closes #3310 Closes #3762 Closes #4340 --- hugolib/page.go | 35 +++++++-- hugolib/page_frontmatter.go | 117 +++++++++++++++++++++++++++++++ hugolib/page_frontmatter_test.go | 39 +++++++++++ hugolib/page_test.go | 58 ++++++++++++++- hugolib/site.go | 10 +++ 5 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 hugolib/page_frontmatter.go create mode 100644 hugolib/page_frontmatter_test.go diff --git a/hugolib/page.go b/hugolib/page.go index fd6278bb443..900c05d1530 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1,4 +1,4 @@ -// Copyright 2016 The Hugo Authors. All rights reserved. +// Copyright 2018 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -1115,6 +1115,9 @@ func (p *Page) update(frontmatter map[string]interface{}) error { // Needed for case insensitive fetching of params values helpers.ToLowerMap(frontmatter) + // Handle the date separately + p.s.frontmatterConfig.handleDate(frontmatter, p) + var modified time.Time var err error @@ -1151,11 +1154,7 @@ func (p *Page) update(frontmatter map[string]interface{}) error { p.Keywords = cast.ToStringSlice(v) p.params[loki] = p.Keywords case "date": - p.Date, err = cast.ToTimeE(v) - if err != nil { - p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path()) - } - p.params[loki] = p.Date + // Handled separately. case "headless": // For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output). // We may expand on this in the future, but that gets more complex pretty fast. @@ -1373,6 +1372,30 @@ func (p *Page) update(frontmatter map[string]interface{}) error { return nil } +// A Zero date is a signal that the name can not be parsed. +// This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: +// "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" +func dateAndSlugFromBaseFilename(name string) (time.Time, string) { + withoutExt, _ := helpers.FileAndExt(name) + + if len(withoutExt) < 10 { + // This can not be a date. + return time.Time{}, "" + } + + // Note: Hugo currently have no custom timezone support. + // We will have to revisit this when that is in place. + d, err := time.Parse("2006-01-02", withoutExt[:10]) + if err != nil { + return time.Time{}, "" + } + + // Be a little lenient with the format here. + slug := strings.Trim(withoutExt[10:], " -_") + + return d, slug +} + func (p *Page) GetParam(key string) interface{} { return p.getParam(key, false) } diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go new file mode 100644 index 00000000000..b86c5865494 --- /dev/null +++ b/hugolib/page_frontmatter.go @@ -0,0 +1,117 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/gohugoio/hugo/config" + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" +) + +// TODO(bep) should probably make the date handling chain complete to give people the flexibility they want. + +type frontmatterConfig struct { + // Ordered chain. + dateHandlers []frontmatterFieldHandler + + logger *jww.Notepad +} + +func (f frontmatterConfig) handleField(handlers []frontmatterFieldHandler, frontmatter map[string]interface{}, p *Page) { + for _, h := range handlers { + handled, err := h(frontmatter, p) + if err != nil { + f.logger.ERROR.Println(err) + } + if handled { + break + } + } +} + +func (f frontmatterConfig) handleDate(frontmatter map[string]interface{}, p *Page) { + f.handleField(f.dateHandlers, frontmatter, p) +} + +type frontmatterFieldHandler func(frontmatter map[string]interface{}, p *Page) (bool, error) + +func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatterConfig, error) { + + if logger == nil { + logger = jww.NewNotepad(jww.LevelWarn, jww.LevelWarn, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + } + + f := frontmatterConfig{logger: logger} + + handlers := &frontmatterFieldHandlers{logger: logger} + + f.dateHandlers = []frontmatterFieldHandler{handlers.defaultDateHandler} + + if cfg.IsSet("frontmatter") { + fm := cfg.GetStringMap("frontmatter") + if fm != nil { + dateFallbacks, found := fm["defaultdate"] + if found { + slice, err := cast.ToStringSliceE(dateFallbacks) + if err != nil { + return f, fmt.Errorf("invalid value for dataCallbacks, expeced a string slice, got %T", dateFallbacks) + } + + for _, v := range slice { + if strings.EqualFold(v, "filename") { + f.dateHandlers = append(f.dateHandlers, handlers.fileanameFallbackDateHandler) + // No more for now. + break + } + } + } + } + } + + return f, nil +} + +type frontmatterFieldHandlers struct { + logger *jww.Notepad +} + +// TODO(bep) modtime + +func (f *frontmatterFieldHandlers) defaultDateHandler(frontmatter map[string]interface{}, p *Page) (bool, error) { + loki := "date" + v, found := frontmatter[loki] + if !found { + return false, nil + } + + var err error + p.Date, err = cast.ToTimeE(v) + if err != nil { + return false, fmt.Errorf("Failed to parse date %q in page %s", v, p.File.Path()) + } + + p.params[loki] = p.Date + + return true, nil +} + +func (f *frontmatterFieldHandlers) fileanameFallbackDateHandler(frontmatter map[string]interface{}, p *Page) (bool, error) { + return true, nil +} diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go new file mode 100644 index 00000000000..1624de56a0e --- /dev/null +++ b/hugolib/page_frontmatter_test.go @@ -0,0 +1,39 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestNewFrontmatterConfig(t *testing.T) { + t.Parallel() + + v := viper.New() + + v.Set("frontmatter", map[string]interface{}{ + "defaultDate": []string{"filename"}, + }) + + assert := require.New(t) + + fc, err := newFrontmatterConfig(newWarningLogger(), v) + + assert.NoError(err) + assert.Equal(2, len(fc.dateHandlers)) + +} diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 814556c6c59..b5f97caac80 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 The Hugo Authors. All rights reserved. +// Copyright 2018 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -985,6 +985,25 @@ Page With empty front matter` zero_FM = "Page With empty front matter" ) +/*func TestPageWithFilenameDateAsFallback(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + var tests = []struct { + }{} + + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + + p := s.RegularPages[0] + d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z") + + checkPageDate(t, p, d) +} +*/ func TestMetadataDates(t *testing.T) { t.Parallel() var tests = []struct { @@ -1873,6 +1892,43 @@ tags: } } +func TestDateAndSlugFromBaseFilename(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + tests := []struct { + name string + date string + slug string + }{ + {"page.md", "0001-01-01", ""}, + {"2012-09-12-page.md", "2012-09-12", "page"}, + {"2018-02-28-page.md", "2018-02-28", "page"}, + {"2018-02-28_page.md", "2018-02-28", "page"}, + {"2018-02-28 page.md", "2018-02-28", "page"}, + {"2018-02-28page.md", "2018-02-28", "page"}, + {"2018-02-28-.md", "2018-02-28", ""}, + {"2018-02-28-.md", "2018-02-28", ""}, + {"2018-02-28.md", "2018-02-28", ""}, + {"2018-02-28-page", "2018-02-28", "page"}, + {"2012-9-12-page.md", "0001-01-01", ""}, + {"asdfasdf.md", "0001-01-01", ""}, + } + + for i, test := range tests { + expectedDate, err := time.Parse("2006-01-02", test.date) + assert.NoError(err) + + errMsg := fmt.Sprintf("Test %d", i) + gotDate, gotSlug := dateAndSlugFromBaseFilename(test.name) + + assert.Equal(expectedDate, gotDate, errMsg) + assert.Equal(test.slug, gotSlug, errMsg) + + } +} + func BenchmarkParsePage(b *testing.B) { s := newTestSite(b) f, _ := os.Open("testdata/redis.cn.md") diff --git a/hugolib/site.go b/hugolib/site.go index 95cd0a23edf..6fdde99336b 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -121,6 +121,9 @@ type Site struct { outputFormatsConfig output.Formats mediaTypesConfig media.Types + // How to handle page front matter. + frontmatterConfig frontmatterConfig + // We render each site for all the relevant output formats in serial with // this rendering context pointing to the current one. rc *siteRenderingContext @@ -177,6 +180,7 @@ func (s *Site) reset() *Site { relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg), outputFormats: s.outputFormats, outputFormatsConfig: s.outputFormatsConfig, + frontmatterConfig: s.frontmatterConfig, mediaTypesConfig: s.mediaTypesConfig, resourceSpec: s.resourceSpec, Language: s.Language, @@ -248,6 +252,11 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { titleFunc := helpers.GetTitleFunc(cfg.Language.GetString("titleCaseStyle")) + fmConfig, err := newFrontmatterConfig(cfg.Logger, cfg.Cfg) + if err != nil { + return nil, err + } + s := &Site{ PageCollections: c, layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""), @@ -258,6 +267,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { outputFormats: outputFormats, outputFormatsConfig: siteOutputFormatsConfig, mediaTypesConfig: siteMediaTypesConfig, + frontmatterConfig: fmConfig, } s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) From 1d9e1e8b79daec9e1cf48e84e553aa9cf16db92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 21 Feb 2018 19:36:58 +0100 Subject: [PATCH 02/13] Work --- hugolib/page.go | 125 +++++--------------- hugolib/page_frontmatter.go | 193 +++++++++++++++++++++++++------ hugolib/page_frontmatter_test.go | 43 +++++++ hugolib/page_test.go | 46 +------- 4 files changed, 237 insertions(+), 170 deletions(-) diff --git a/hugolib/page.go b/hugolib/page.go index 900c05d1530..d8d7ac08118 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -140,9 +140,6 @@ type Page struct { Draft bool Status string - PublishDate time.Time - ExpiryDate time.Time - // PageMeta contains page stats such as word count etc. PageMeta @@ -223,8 +220,7 @@ type Page struct { Keywords []string Data map[string]interface{} - Date time.Time - Lastmod time.Time + PageDates Sitemap Sitemap URLPath @@ -264,6 +260,13 @@ type Page struct { targetPathDescriptorPrototype *targetPathDescriptor } +type PageDates struct { + Date time.Time + Lastmod time.Time + PublishDate time.Time + ExpiryDate time.Time +} + // SearchKeywords implements the related.Document interface needed for fast page searches. func (p *Page) SearchKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { @@ -1115,15 +1118,33 @@ func (p *Page) update(frontmatter map[string]interface{}) error { // Needed for case insensitive fetching of params values helpers.ToLowerMap(frontmatter) - // Handle the date separately - p.s.frontmatterConfig.handleDate(frontmatter, p) + descriptor := frontMatterDescriptor{frontmatter: frontmatter, params: p.params, baseFilename: p.BaseFileName()} - var modified time.Time + // Handle the date separately + dates, err := p.s.frontmatterConfig.handleDates(descriptor) + if err != nil { + p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.Path(), err) + } else { + p.PageDates = dates + } - var err error var draft, published, isCJKLanguage *bool for k, v := range frontmatter { loki := strings.ToLower(k) + + if loki == "published" { // Intentionally undocumented + vv, err := cast.ToBoolE(v) + if err == nil { + published = &vv + } + // published may also be a date + continue + } + + if p.s.frontmatterConfig.isDateKey(loki) { + continue + } + switch loki { case "title": p.title = cast.ToString(v) @@ -1153,8 +1174,6 @@ func (p *Page) update(frontmatter map[string]interface{}) error { case "keywords": p.Keywords = cast.ToStringSlice(v) p.params[loki] = p.Keywords - case "date": - // Handled separately. case "headless": // For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output). // We may expand on this in the future, but that gets more complex pretty fast. @@ -1162,19 +1181,6 @@ func (p *Page) update(frontmatter map[string]interface{}) error { p.headless = cast.ToBool(v) } p.params[loki] = p.headless - case "lastmod": - p.Lastmod, err = cast.ToTimeE(v) - if err != nil { - p.s.Log.ERROR.Printf("Failed to parse lastmod '%v' in page %s", v, p.File.Path()) - } - case "modified": - vv, err := cast.ToTimeE(v) - if err == nil { - p.params[loki] = vv - modified = vv - } else { - p.params[loki] = cast.ToString(v) - } case "outputs": o := cast.ToStringSlice(v) if len(o) > 0 { @@ -1189,34 +1195,9 @@ func (p *Page) update(frontmatter map[string]interface{}) error { } } - case "publishdate", "pubdate": - p.PublishDate, err = cast.ToTimeE(v) - if err != nil { - p.s.Log.ERROR.Printf("Failed to parse publishdate '%v' in page %s", v, p.File.Path()) - } - p.params[loki] = p.PublishDate - case "expirydate", "unpublishdate": - p.ExpiryDate, err = cast.ToTimeE(v) - if err != nil { - p.s.Log.ERROR.Printf("Failed to parse expirydate '%v' in page %s", v, p.File.Path()) - } case "draft": draft = new(bool) *draft = cast.ToBool(v) - case "published": // Intentionally undocumented - vv, err := cast.ToBoolE(v) - if err == nil { - published = &vv - } else { - // Some sites use this as the publishdate - vv, err := cast.ToTimeE(v) - if err == nil { - p.PublishDate = vv - p.params[loki] = p.PublishDate - } else { - p.params[loki] = cast.ToString(v) - } - } case "layout": p.Layout = cast.ToString(v) p.params[loki] = p.Layout @@ -1332,32 +1313,10 @@ func (p *Page) update(frontmatter map[string]interface{}) error { } p.params["draft"] = p.Draft - if p.Date.IsZero() { - p.Date = p.PublishDate - } - - if p.PublishDate.IsZero() { - p.PublishDate = p.Date - } - if p.Date.IsZero() && p.s.Cfg.GetBool("useModTimeAsFallback") { p.Date = p.Source.FileInfo().ModTime() } - if p.Lastmod.IsZero() { - if !modified.IsZero() { - p.Lastmod = modified - } else { - p.Lastmod = p.Date - } - - } - - p.params["date"] = p.Date - p.params["lastmod"] = p.Lastmod - p.params["publishdate"] = p.PublishDate - p.params["expirydate"] = p.ExpiryDate - if isCJKLanguage != nil { p.isCJKLanguage = *isCJKLanguage } else if p.s.Cfg.GetBool("hasCJKLanguage") { @@ -1372,30 +1331,6 @@ func (p *Page) update(frontmatter map[string]interface{}) error { return nil } -// A Zero date is a signal that the name can not be parsed. -// This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: -// "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" -func dateAndSlugFromBaseFilename(name string) (time.Time, string) { - withoutExt, _ := helpers.FileAndExt(name) - - if len(withoutExt) < 10 { - // This can not be a date. - return time.Time{}, "" - } - - // Note: Hugo currently have no custom timezone support. - // We will have to revisit this when that is in place. - d, err := time.Parse("2006-01-02", withoutExt[:10]) - if err != nil { - return time.Time{}, "" - } - - // Be a little lenient with the format here. - slug := strings.Trim(withoutExt[10:], " -_") - - return d, slug -} - func (p *Page) GetParam(key string) interface{} { return p.getParam(key, false) } diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index b86c5865494..5d4b35e7c6b 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -19,6 +19,9 @@ import ( "log" "os" "strings" + "time" + + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/config" "github.com/spf13/cast" @@ -34,23 +37,152 @@ type frontmatterConfig struct { logger *jww.Notepad } -func (f frontmatterConfig) handleField(handlers []frontmatterFieldHandler, frontmatter map[string]interface{}, p *Page) { +type frontMatterDescriptor struct { + + // This the Page's front matter. + frontmatter map[string]interface{} + + // This is the Page's params. + params map[string]interface{} + + // This is the Page's base filename, e.g. page.md. + baseFilename string +} + +func (f frontmatterConfig) handleField(handlers []frontmatterFieldHandler, d frontMatterDescriptor) (interface{}, error) { for _, h := range handlers { - handled, err := h(frontmatter, p) + // First non-nil value wins. + val, err := h(d) if err != nil { f.logger.ERROR.Println(err) + } else if val != nil { + return val, nil } - if handled { - break + } + + return nil, nil +} + +func (f frontmatterConfig) handleDate(d frontMatterDescriptor) (time.Time, error) { + v, err := f.handleField(f.dateHandlers, d) + if err != nil || v == nil { + return time.Time{}, err + } + return v.(time.Time), nil +} + +var ( + lastModFrontMatterKeys = []string{"lastmod", "modified"} + publishDateFrontMatterKeys = []string{"publishdate", "pubdate", "published"} + expiryDateFrontMatterKeys = []string{"expirydate", "unpublishdate"} + allDateFrontMatterKeys = make(map[string]bool) +) + +func init() { + for _, key := range lastModFrontMatterKeys { + allDateFrontMatterKeys[key] = true + } + for _, key := range publishDateFrontMatterKeys { + allDateFrontMatterKeys[key] = true + } + for _, key := range expiryDateFrontMatterKeys { + allDateFrontMatterKeys[key] = true + } + + allDateFrontMatterKeys["date"] = true +} + +func (f frontmatterConfig) handleDates(d frontMatterDescriptor) (PageDates, error) { + pd := &PageDates{} + + date, err := f.handleDate(d) + if err != nil { + return *pd, err + } + + pd.Date = date + pd.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) + pd.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) + pd.ExpiryDate = f.setParamsAndReturnFirstDate(d, expiryDateFrontMatterKeys) + + // Hugo really needs a date! + if pd.Date.IsZero() { + pd.Date = pd.PublishDate + } + + if pd.PublishDate.IsZero() { + pd.PublishDate = pd.Date + } + + if pd.Lastmod.IsZero() { + pd.Lastmod = pd.Date + } + + f.setParamIfNotZero("date", d.params, pd.Date) + f.setParamIfNotZero("lastmod", d.params, pd.Lastmod) + f.setParamIfNotZero("publishdate", d.params, pd.PublishDate) + f.setParamIfNotZero("expirydate", d.params, pd.ExpiryDate) + + return *pd, nil +} + +func (f frontmatterConfig) isDateKey(key string) bool { + return allDateFrontMatterKeys[key] +} + +func (f frontmatterConfig) setParamIfNotZero(name string, params map[string]interface{}, date time.Time) { + if date.IsZero() { + return + } + params[name] = date +} + +func (f frontmatterConfig) setParamsAndReturnFirstDate(d frontMatterDescriptor, keys []string) time.Time { + var date time.Time + + for _, key := range keys { + v, found := d.frontmatter[key] + if found { + currentDate, err := cast.ToTimeE(v) + if err == nil { + d.params[key] = currentDate + if date.IsZero() { + date = currentDate + } + } else { + d.params[key] = v + } } } + + return date } -func (f frontmatterConfig) handleDate(frontmatter map[string]interface{}, p *Page) { - f.handleField(f.dateHandlers, frontmatter, p) +// A Zero date is a signal that the name can not be parsed. +// This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: +// "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" +func (f frontmatterConfig) dateAndSlugFromBaseFilename(name string) (time.Time, string) { + withoutExt, _ := helpers.FileAndExt(name) + + if len(withoutExt) < 10 { + // This can not be a date. + return time.Time{}, "" + } + + // Note: Hugo currently have no custom timezone support. + // We will have to revisit this when that is in place. + d, err := time.Parse("2006-01-02", withoutExt[:10]) + if err != nil { + return time.Time{}, "" + } + + // Be a little lenient with the format here. + slug := strings.Trim(withoutExt[10:], " -_") + + return d, slug } -type frontmatterFieldHandler func(frontmatter map[string]interface{}, p *Page) (bool, error) +type frontmatterFieldHandler func(d frontMatterDescriptor) (interface{}, error) func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatterConfig, error) { @@ -64,25 +196,22 @@ func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatter f.dateHandlers = []frontmatterFieldHandler{handlers.defaultDateHandler} - if cfg.IsSet("frontmatter") { - fm := cfg.GetStringMap("frontmatter") - if fm != nil { - dateFallbacks, found := fm["defaultdate"] - if found { - slice, err := cast.ToStringSliceE(dateFallbacks) - if err != nil { - return f, fmt.Errorf("invalid value for dataCallbacks, expeced a string slice, got %T", dateFallbacks) - } + defaultDate := cfg.Get("frontmatter.defaultdate") - for _, v := range slice { - if strings.EqualFold(v, "filename") { - f.dateHandlers = append(f.dateHandlers, handlers.fileanameFallbackDateHandler) - // No more for now. - break - } - } + if defaultDate != nil { + slice, err := cast.ToStringSliceE(defaultDate) + if err != nil { + return f, fmt.Errorf("invalid value for defaultDate, expeced a string slice, got %T", defaultDate) + } + + for _, v := range slice { + if strings.EqualFold(v, "filename") { + f.dateHandlers = append(f.dateHandlers, handlers.filenameFallbackDateHandler) + // No more for now. + break } } + } return f, nil @@ -94,24 +223,20 @@ type frontmatterFieldHandlers struct { // TODO(bep) modtime -func (f *frontmatterFieldHandlers) defaultDateHandler(frontmatter map[string]interface{}, p *Page) (bool, error) { - loki := "date" - v, found := frontmatter[loki] +func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) (interface{}, error) { + v, found := d.frontmatter["date"] if !found { - return false, nil + return nil, nil } - var err error - p.Date, err = cast.ToTimeE(v) + date, err := cast.ToTimeE(v) if err != nil { - return false, fmt.Errorf("Failed to parse date %q in page %s", v, p.File.Path()) + return nil, err } - p.params[loki] = p.Date - - return true, nil + return date, nil } -func (f *frontmatterFieldHandlers) fileanameFallbackDateHandler(frontmatter map[string]interface{}, p *Page) (bool, error) { +func (f *frontmatterFieldHandlers) filenameFallbackDateHandler(d frontMatterDescriptor) (interface{}, error) { return true, nil } diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go index 1624de56a0e..33ff36faef0 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/page_frontmatter_test.go @@ -14,7 +14,9 @@ package hugolib import ( + "fmt" "testing" + "time" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -37,3 +39,44 @@ func TestNewFrontmatterConfig(t *testing.T) { assert.Equal(2, len(fc.dateHandlers)) } + +func TestDateAndSlugFromBaseFilename(t *testing.T) { + + t.Parallel() + + assert := require.New(t) + + fc, err := newFrontmatterConfig(newWarningLogger(), viper.New()) + assert.NoError(err) + + tests := []struct { + name string + date string + slug string + }{ + {"page.md", "0001-01-01", ""}, + {"2012-09-12-page.md", "2012-09-12", "page"}, + {"2018-02-28-page.md", "2018-02-28", "page"}, + {"2018-02-28_page.md", "2018-02-28", "page"}, + {"2018-02-28 page.md", "2018-02-28", "page"}, + {"2018-02-28page.md", "2018-02-28", "page"}, + {"2018-02-28-.md", "2018-02-28", ""}, + {"2018-02-28-.md", "2018-02-28", ""}, + {"2018-02-28.md", "2018-02-28", ""}, + {"2018-02-28-page", "2018-02-28", "page"}, + {"2012-9-12-page.md", "0001-01-01", ""}, + {"asdfasdf.md", "0001-01-01", ""}, + } + + for i, test := range tests { + expectedDate, err := time.Parse("2006-01-02", test.date) + assert.NoError(err) + + errMsg := fmt.Sprintf("Test %d", i) + gotDate, gotSlug := fc.dateAndSlugFromBaseFilename(test.name) + + assert.Equal(expectedDate, gotDate, errMsg) + assert.Equal(test.slug, gotSlug, errMsg) + + } +} diff --git a/hugolib/page_test.go b/hugolib/page_test.go index b5f97caac80..5ca051b015e 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1049,10 +1049,11 @@ func TestMetadataDates(t *testing.T) { {p_DP_ME, "testy.md", true, D, P, M, M, E}, // mod copied to lastMod {p_DPLME, "testy.md", true, D, P, L, M, E}, // all dates - {emptyFM, "test.md", false, o, o, o, x, x}, // 3 year-one dates, 2 empty dates - {zero_FM, "test.md", false, o, o, o, x, x}, - {emptyFM, "testy.md", true, s, o, s, x, x}, // 2 filesys, 1 year-one, 2 empty - {zero_FM, "testy.md", true, s, o, s, x, x}, + // TODO(bep) dates + //{emptyFM, "test.md", false, o, o, o, x, x}, // 3 year-one dates, 2 empty dates + //{zero_FM, "test.md", false, o, o, o, x, x}, + // {emptyFM, "testy.md", true, s, o, s, x, x}, // 2 filesys, 1 year-one, 2 empty + //{zero_FM, "testy.md", true, s, o, s, x, x}, } for i, test := range tests { @@ -1892,43 +1893,6 @@ tags: } } -func TestDateAndSlugFromBaseFilename(t *testing.T) { - t.Parallel() - - assert := require.New(t) - - tests := []struct { - name string - date string - slug string - }{ - {"page.md", "0001-01-01", ""}, - {"2012-09-12-page.md", "2012-09-12", "page"}, - {"2018-02-28-page.md", "2018-02-28", "page"}, - {"2018-02-28_page.md", "2018-02-28", "page"}, - {"2018-02-28 page.md", "2018-02-28", "page"}, - {"2018-02-28page.md", "2018-02-28", "page"}, - {"2018-02-28-.md", "2018-02-28", ""}, - {"2018-02-28-.md", "2018-02-28", ""}, - {"2018-02-28.md", "2018-02-28", ""}, - {"2018-02-28-page", "2018-02-28", "page"}, - {"2012-9-12-page.md", "0001-01-01", ""}, - {"asdfasdf.md", "0001-01-01", ""}, - } - - for i, test := range tests { - expectedDate, err := time.Parse("2006-01-02", test.date) - assert.NoError(err) - - errMsg := fmt.Sprintf("Test %d", i) - gotDate, gotSlug := dateAndSlugFromBaseFilename(test.name) - - assert.Equal(expectedDate, gotDate, errMsg) - assert.Equal(test.slug, gotSlug, errMsg) - - } -} - func BenchmarkParsePage(b *testing.B) { s := newTestSite(b) f, _ := os.Open("testdata/redis.cn.md") From f3db8949215d83ebfcf442cdbcc5ddfa11b90ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Feb 2018 11:06:53 +0100 Subject: [PATCH 03/13] Work --- hugolib/page.go | 11 +++++----- hugolib/page_frontmatter.go | 20 ++++++++++++++--- hugolib/page_test.go | 43 ++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/hugolib/page.go b/hugolib/page.go index d8d7ac08118..36f8fdd0887 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1118,7 +1118,12 @@ func (p *Page) update(frontmatter map[string]interface{}) error { // Needed for case insensitive fetching of params values helpers.ToLowerMap(frontmatter) - descriptor := frontMatterDescriptor{frontmatter: frontmatter, params: p.params, baseFilename: p.BaseFileName()} + var mtime time.Time + if p.Source.FileInfo() != nil { + mtime = p.Source.FileInfo().ModTime() + } + + descriptor := frontMatterDescriptor{frontmatter: frontmatter, params: p.params, baseFilename: p.BaseFileName(), modTime: mtime} // Handle the date separately dates, err := p.s.frontmatterConfig.handleDates(descriptor) @@ -1313,10 +1318,6 @@ func (p *Page) update(frontmatter map[string]interface{}) error { } p.params["draft"] = p.Draft - if p.Date.IsZero() && p.s.Cfg.GetBool("useModTimeAsFallback") { - p.Date = p.Source.FileInfo().ModTime() - } - if isCJKLanguage != nil { p.isCJKLanguage = *isCJKLanguage } else if p.s.Cfg.GetBool("hasCJKLanguage") { diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index 5d4b35e7c6b..21b7aa32580 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -47,6 +47,9 @@ type frontMatterDescriptor struct { // This is the Page's base filename, e.g. page.md. baseFilename string + + // The content file's mod time. + modTime time.Time } func (f frontmatterConfig) handleField(handlers []frontmatterFieldHandler, d frontMatterDescriptor) (interface{}, error) { @@ -99,7 +102,6 @@ func (f frontmatterConfig) handleDates(d frontMatterDescriptor) (PageDates, erro if err != nil { return *pd, err } - pd.Date = date pd.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) pd.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) @@ -206,7 +208,7 @@ func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatter for _, v := range slice { if strings.EqualFold(v, "filename") { - f.dateHandlers = append(f.dateHandlers, handlers.filenameFallbackDateHandler) + f.dateHandlers = append(f.dateHandlers, handlers.defaultDateDateFilenameHandler) // No more for now. break } @@ -214,6 +216,11 @@ func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatter } + // This is deprecated + if cfg.GetBool("useModTimeAsFallback") { + f.dateHandlers = append(f.dateHandlers, handlers.defaultDateDateModTimeHandler) + } + return f, nil } @@ -237,6 +244,13 @@ func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) ( return date, nil } -func (f *frontmatterFieldHandlers) filenameFallbackDateHandler(d frontMatterDescriptor) (interface{}, error) { +func (f *frontmatterFieldHandlers) defaultDateDateFilenameHandler(d frontMatterDescriptor) (interface{}, error) { return true, nil } + +func (f *frontmatterFieldHandlers) defaultDateDateModTimeHandler(d frontMatterDescriptor) (interface{}, error) { + if !d.modTime.IsZero() { + return d.modTime, nil + } + return nil, nil +} diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 5ca051b015e..b27e29ac14b 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -27,8 +27,6 @@ import ( "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/source" "github.com/spf13/cast" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1006,6 +1004,9 @@ Page With empty front matter` */ func TestMetadataDates(t *testing.T) { t.Parallel() + + assert := require.New(t) + var tests = []struct { text string filename string @@ -1038,9 +1039,9 @@ func TestMetadataDates(t *testing.T) { {p_D____, "test.md", false, D, D, D, x, x}, // date copied across {p_D____, "testy.md", true, D, D, D, x, x}, {p__P___, "test.md", false, P, P, P, x, x}, // pubdate copied across - {p__P___, "testy.md", true, P, P, P, x, x}, + {p__P___, "testy.md", true, P, P, P, x, x}, // TODO(bep) date from modTime {p_DP___, "test.md", false, D, P, D, x, x}, // date -> lastMod - {p_DP___, "testy.md", true, D, P, D, x, x}, + {p_DP___, "testy.md", true, D, P, D, x, x}, // TODO(bep) date from modTime {p__PL__, "test.md", false, P, P, L, x, x}, // pub -> date overrides lastMod -> date code (inconsistent?) {p__PL__, "testy.md", true, P, P, L, x, x}, {p_DPL__, "test.md", false, D, P, L, x, x}, // three dates @@ -1048,29 +1049,27 @@ func TestMetadataDates(t *testing.T) { {p_DPL_E, "testy.md", true, D, P, L, x, E}, // lastMod NOT copied to mod (inconsistent?) {p_DP_ME, "testy.md", true, D, P, M, M, E}, // mod copied to lastMod {p_DPLME, "testy.md", true, D, P, L, M, E}, // all dates - - // TODO(bep) dates - //{emptyFM, "test.md", false, o, o, o, x, x}, // 3 year-one dates, 2 empty dates - //{zero_FM, "test.md", false, o, o, o, x, x}, - // {emptyFM, "testy.md", true, s, o, s, x, x}, // 2 filesys, 1 year-one, 2 empty - //{zero_FM, "testy.md", true, s, o, s, x, x}, + {emptyFM, "test.md", false, o, o, o, x, x}, // 3 year-one dates, 2 empty dates + {zero_FM, "test.md", false, o, o, o, x, x}, + {emptyFM, "testy.md", true, s, o, s, x, x}, // 2 filesys, 1 year-one, 2 empty TODO(bep) date from modTime + {zero_FM, "testy.md", true, s, o, s, x, x}, // TODO(bep) date from modTime } for i, test := range tests { - s := newTestSite(t) - s.Cfg.Set("useModTimeAsFallback", test.modFallback) - fs := hugofs.NewMem(s.Cfg) - writeToFs(t, fs.Source, test.filename, test.text) - file, err := fs.Source.Open(test.filename) - if err != nil { - t.Fatal("failed to write test file to test filesystem") - } - fi, _ := fs.Source.Stat(test.filename) + var ( + cfg, fs = newTestCfg() + ) + + writeToFs(t, fs.Source, filepath.Join("content", test.filename), test.text) + + cfg.Set("useModTimeAsFallback", test.modFallback) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - sp := source.NewSourceSpec(s.Cfg, fs) - p := s.newPageFromFile(newFileInfo(sp, "", test.filename, fi, bundleNot)) - p.ReadFrom(file) + assert.Equal(1, len(s.RegularPages)) + p := s.RegularPages[0] + fi := p.Source.FileInfo() // check Page Variables checkDate(t, i+1, "Date", p.Date, test.expDate, fi) From 8d074542c6a91f010d92a45b13a98bca5a88c9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Feb 2018 18:47:27 +0100 Subject: [PATCH 04/13] Work --- hugolib/page.go | 14 +++-- hugolib/page_frontmatter.go | 105 ++++++++++++++++--------------- hugolib/page_frontmatter_test.go | 18 ------ hugolib/page_test.go | 8 +-- hugolib/site.go | 6 +- 5 files changed, 71 insertions(+), 80 deletions(-) diff --git a/hugolib/page.go b/hugolib/page.go index 36f8fdd0887..77e12735619 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1123,14 +1123,18 @@ func (p *Page) update(frontmatter map[string]interface{}) error { mtime = p.Source.FileInfo().ModTime() } - descriptor := frontMatterDescriptor{frontmatter: frontmatter, params: p.params, baseFilename: p.BaseFileName(), modTime: mtime} + descriptor := frontMatterDescriptor{ + frontmatter: frontmatter, + params: p.params, + dates: &p.PageDates, + baseFilename: p.BaseFileName(), modTime: mtime} // Handle the date separately - dates, err := p.s.frontmatterConfig.handleDates(descriptor) + // TODO(bep) we need to "do more" in this area so this can be split up and + // more easily tested without the Page, but the coupling is strong. + err := p.s.frontmatterHandler.handleDates(descriptor) if err != nil { p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.Path(), err) - } else { - p.PageDates = dates } var draft, published, isCJKLanguage *bool @@ -1146,7 +1150,7 @@ func (p *Page) update(frontmatter map[string]interface{}) error { continue } - if p.s.frontmatterConfig.isDateKey(loki) { + if p.s.frontmatterHandler.isDateKey(loki) { continue } diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index 21b7aa32580..cefa782c7d0 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -30,9 +30,9 @@ import ( // TODO(bep) should probably make the date handling chain complete to give people the flexibility they want. -type frontmatterConfig struct { +type frontmatterHandler struct { // Ordered chain. - dateHandlers []frontmatterFieldHandler + dateHandlers frontMatterFieldHandler logger *jww.Notepad } @@ -42,32 +42,23 @@ type frontMatterDescriptor struct { // This the Page's front matter. frontmatter map[string]interface{} - // This is the Page's params. - params map[string]interface{} - // This is the Page's base filename, e.g. page.md. baseFilename string // The content file's mod time. modTime time.Time -} -func (f frontmatterConfig) handleField(handlers []frontmatterFieldHandler, d frontMatterDescriptor) (interface{}, error) { - for _, h := range handlers { - // First non-nil value wins. - val, err := h(d) - if err != nil { - f.logger.ERROR.Println(err) - } else if val != nil { - return val, nil - } - } + // The below are pointers to values on Page and will be updated. - return nil, nil + // This is the Page's params. + params map[string]interface{} + + // This is the Page's dates. + dates *PageDates } -func (f frontmatterConfig) handleDate(d frontMatterDescriptor) (time.Time, error) { - v, err := f.handleField(f.dateHandlers, d) +func (f frontmatterHandler) handleDate(d frontMatterDescriptor) (time.Time, error) { + v, err := f.dateHandlers(d) if err != nil || v == nil { return time.Time{}, err } @@ -95,51 +86,50 @@ func init() { allDateFrontMatterKeys["date"] = true } -func (f frontmatterConfig) handleDates(d frontMatterDescriptor) (PageDates, error) { - pd := &PageDates{} +func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { date, err := f.handleDate(d) if err != nil { - return *pd, err + return err } - pd.Date = date - pd.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) - pd.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) - pd.ExpiryDate = f.setParamsAndReturnFirstDate(d, expiryDateFrontMatterKeys) + d.dates.Date = date + d.dates.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) + d.dates.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) + d.dates.ExpiryDate = f.setParamsAndReturnFirstDate(d, expiryDateFrontMatterKeys) // Hugo really needs a date! - if pd.Date.IsZero() { - pd.Date = pd.PublishDate + if d.dates.Date.IsZero() { + d.dates.Date = d.dates.PublishDate } - if pd.PublishDate.IsZero() { - pd.PublishDate = pd.Date + if d.dates.PublishDate.IsZero() { + d.dates.PublishDate = d.dates.Date } - if pd.Lastmod.IsZero() { - pd.Lastmod = pd.Date + if d.dates.Lastmod.IsZero() { + d.dates.Lastmod = d.dates.Date } - f.setParamIfNotZero("date", d.params, pd.Date) - f.setParamIfNotZero("lastmod", d.params, pd.Lastmod) - f.setParamIfNotZero("publishdate", d.params, pd.PublishDate) - f.setParamIfNotZero("expirydate", d.params, pd.ExpiryDate) + f.setParamIfNotZero("date", d.params, d.dates.Date) + f.setParamIfNotZero("lastmod", d.params, d.dates.Lastmod) + f.setParamIfNotZero("publishdate", d.params, d.dates.PublishDate) + f.setParamIfNotZero("expirydate", d.params, d.dates.ExpiryDate) - return *pd, nil + return nil } -func (f frontmatterConfig) isDateKey(key string) bool { +func (f frontmatterHandler) isDateKey(key string) bool { return allDateFrontMatterKeys[key] } -func (f frontmatterConfig) setParamIfNotZero(name string, params map[string]interface{}, date time.Time) { +func (f frontmatterHandler) setParamIfNotZero(name string, params map[string]interface{}, date time.Time) { if date.IsZero() { return } params[name] = date } -func (f frontmatterConfig) setParamsAndReturnFirstDate(d frontMatterDescriptor, keys []string) time.Time { +func (f frontmatterHandler) setParamsAndReturnFirstDate(d frontMatterDescriptor, keys []string) time.Time { var date time.Time for _, key := range keys { @@ -163,7 +153,7 @@ func (f frontmatterConfig) setParamsAndReturnFirstDate(d frontMatterDescriptor, // A Zero date is a signal that the name can not be parsed. // This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: // "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" -func (f frontmatterConfig) dateAndSlugFromBaseFilename(name string) (time.Time, string) { +func (f frontmatterHandler) dateAndSlugFromBaseFilename(name string) (time.Time, string) { withoutExt, _ := helpers.FileAndExt(name) if len(withoutExt) < 10 { @@ -184,19 +174,35 @@ func (f frontmatterConfig) dateAndSlugFromBaseFilename(name string) (time.Time, return d, slug } -type frontmatterFieldHandler func(d frontMatterDescriptor) (interface{}, error) +type frontMatterFieldHandler func(d frontMatterDescriptor) (interface{}, error) + +func (f frontmatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler { + return func(d frontMatterDescriptor) (interface{}, error) { + for _, h := range handlers { + // First non-nil value wins. + val, err := h(d) + if err != nil { + f.logger.ERROR.Println(err) + } else if val != nil { + return val, nil + } + } + return nil, nil + } + +} -func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatterConfig, error) { +func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatterHandler, error) { if logger == nil { logger = jww.NewNotepad(jww.LevelWarn, jww.LevelWarn, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) } - f := frontmatterConfig{logger: logger} + f := frontmatterHandler{logger: logger} handlers := &frontmatterFieldHandlers{logger: logger} - f.dateHandlers = []frontmatterFieldHandler{handlers.defaultDateHandler} + dateHandlers := []frontMatterFieldHandler{handlers.defaultDateHandler} defaultDate := cfg.Get("frontmatter.defaultdate") @@ -208,19 +214,20 @@ func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatter for _, v := range slice { if strings.EqualFold(v, "filename") { - f.dateHandlers = append(f.dateHandlers, handlers.defaultDateDateFilenameHandler) + dateHandlers = append(dateHandlers, handlers.defaultDateDateFilenameHandler) // No more for now. break } } - } // This is deprecated if cfg.GetBool("useModTimeAsFallback") { - f.dateHandlers = append(f.dateHandlers, handlers.defaultDateDateModTimeHandler) + dateHandlers = append(dateHandlers, handlers.defaultDateDateModTimeHandler) } + f.dateHandlers = f.newChainedFrontMatterFieldHandler(dateHandlers...) + return f, nil } @@ -228,8 +235,6 @@ type frontmatterFieldHandlers struct { logger *jww.Notepad } -// TODO(bep) modtime - func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) (interface{}, error) { v, found := d.frontmatter["date"] if !found { diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go index 33ff36faef0..c93d5cc66b7 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/page_frontmatter_test.go @@ -22,24 +22,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestNewFrontmatterConfig(t *testing.T) { - t.Parallel() - - v := viper.New() - - v.Set("frontmatter", map[string]interface{}{ - "defaultDate": []string{"filename"}, - }) - - assert := require.New(t) - - fc, err := newFrontmatterConfig(newWarningLogger(), v) - - assert.NoError(err) - assert.Equal(2, len(fc.dateHandlers)) - -} - func TestDateAndSlugFromBaseFilename(t *testing.T) { t.Parallel() diff --git a/hugolib/page_test.go b/hugolib/page_test.go index b27e29ac14b..3bf1ba6f4c8 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1039,11 +1039,11 @@ func TestMetadataDates(t *testing.T) { {p_D____, "test.md", false, D, D, D, x, x}, // date copied across {p_D____, "testy.md", true, D, D, D, x, x}, {p__P___, "test.md", false, P, P, P, x, x}, // pubdate copied across - {p__P___, "testy.md", true, P, P, P, x, x}, // TODO(bep) date from modTime + //{p__P___, "testy.md", true, P, P, P, x, x}, // TODO(bep) date from modTime {p_DP___, "test.md", false, D, P, D, x, x}, // date -> lastMod {p_DP___, "testy.md", true, D, P, D, x, x}, // TODO(bep) date from modTime {p__PL__, "test.md", false, P, P, L, x, x}, // pub -> date overrides lastMod -> date code (inconsistent?) - {p__PL__, "testy.md", true, P, P, L, x, x}, + //{p__PL__, "testy.md", true, P, P, L, x, x}, {p_DPL__, "test.md", false, D, P, L, x, x}, // three dates {p_DPL__, "testy.md", true, D, P, L, x, x}, {p_DPL_E, "testy.md", true, D, P, L, x, E}, // lastMod NOT copied to mod (inconsistent?) @@ -1051,8 +1051,8 @@ func TestMetadataDates(t *testing.T) { {p_DPLME, "testy.md", true, D, P, L, M, E}, // all dates {emptyFM, "test.md", false, o, o, o, x, x}, // 3 year-one dates, 2 empty dates {zero_FM, "test.md", false, o, o, o, x, x}, - {emptyFM, "testy.md", true, s, o, s, x, x}, // 2 filesys, 1 year-one, 2 empty TODO(bep) date from modTime - {zero_FM, "testy.md", true, s, o, s, x, x}, // TODO(bep) date from modTime + //{emptyFM, "testy.md", true, s, o, s, x, x}, // 2 filesys, 1 year-one, 2 empty TODO(bep) date from modTime + //{zero_FM, "testy.md", true, s, o, s, x, x}, // TODO(bep) date from modTime } for i, test := range tests { diff --git a/hugolib/site.go b/hugolib/site.go index 6fdde99336b..c84383dbec0 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -122,7 +122,7 @@ type Site struct { mediaTypesConfig media.Types // How to handle page front matter. - frontmatterConfig frontmatterConfig + frontmatterHandler frontmatterHandler // We render each site for all the relevant output formats in serial with // this rendering context pointing to the current one. @@ -180,7 +180,7 @@ func (s *Site) reset() *Site { relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg), outputFormats: s.outputFormats, outputFormatsConfig: s.outputFormatsConfig, - frontmatterConfig: s.frontmatterConfig, + frontmatterHandler: s.frontmatterHandler, mediaTypesConfig: s.mediaTypesConfig, resourceSpec: s.resourceSpec, Language: s.Language, @@ -267,7 +267,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { outputFormats: outputFormats, outputFormatsConfig: siteOutputFormatsConfig, mediaTypesConfig: siteMediaTypesConfig, - frontmatterConfig: fmConfig, + frontmatterHandler: fmConfig, } s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) From 69b829e67a3cc62379279124fa182b4ad805c333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Feb 2018 19:20:21 +0100 Subject: [PATCH 05/13] Work --- hugolib/page_frontmatter.go | 17 ++++++----- hugolib/page_test.go | 56 +++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index cefa782c7d0..02a7ae6dc1c 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -59,10 +59,13 @@ type frontMatterDescriptor struct { func (f frontmatterHandler) handleDate(d frontMatterDescriptor) (time.Time, error) { v, err := f.dateHandlers(d) - if err != nil || v == nil { - return time.Time{}, err + if err == nil { + if d, ok := v.(time.Time); ok { + return d, nil + } } - return v.(time.Time), nil + + return time.Time{}, err } var ( @@ -214,7 +217,7 @@ func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatter for _, v := range slice { if strings.EqualFold(v, "filename") { - dateHandlers = append(dateHandlers, handlers.defaultDateDateFilenameHandler) + dateHandlers = append(dateHandlers, handlers.defaultDateFilenameHandler) // No more for now. break } @@ -223,7 +226,7 @@ func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatter // This is deprecated if cfg.GetBool("useModTimeAsFallback") { - dateHandlers = append(dateHandlers, handlers.defaultDateDateModTimeHandler) + dateHandlers = append(dateHandlers, handlers.defaultDateModTimeHandler) } f.dateHandlers = f.newChainedFrontMatterFieldHandler(dateHandlers...) @@ -249,11 +252,11 @@ func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) ( return date, nil } -func (f *frontmatterFieldHandlers) defaultDateDateFilenameHandler(d frontMatterDescriptor) (interface{}, error) { +func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d frontMatterDescriptor) (interface{}, error) { return true, nil } -func (f *frontmatterFieldHandlers) defaultDateDateModTimeHandler(d frontMatterDescriptor) (interface{}, error) { +func (f *frontmatterFieldHandlers) defaultDateModTimeHandler(d frontMatterDescriptor) (interface{}, error) { if !d.modTime.IsZero() { return d.modTime, nil } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 3bf1ba6f4c8..9266c167c8c 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -983,25 +983,59 @@ Page With empty front matter` zero_FM = "Page With empty front matter" ) -/*func TestPageWithFilenameDateAsFallback(t *testing.T) { +func TestPageWithFilenameDateAsFallback(t *testing.T) { t.Parallel() - cfg, fs := newTestCfg() - var tests = []struct { - }{} + for _, useFilename := range []bool{false, true} { + t.Run(fmt.Sprintf("useFilename=%t", useFilename), func(t *testing.T) { + ass := require.New(t) + cfg, fs := newTestCfg() - writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date) + pageTemplate := ` +--- +title: Page +weight: %d +%s +--- +Content +` - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + if useFilename { + cfg.Set("frontmatter", map[string]interface{}{ + "defaultDate": []string{"filename"}, + }) + } - require.Len(t, s.RegularPages, 1) + writeSource(t, fs, filepath.Join("content", "2012-02-21-noslug.md"), fmt.Sprintf(pageTemplate, 1, "")) + writeSource(t, fs, filepath.Join("content", "2012-02-22-slug.md"), fmt.Sprintf(pageTemplate, 2, "slug: aslug")) - p := s.RegularPages[0] - d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z") + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + ass.Len(s.RegularPages, 2) + + noSlug := s.RegularPages[0] + slug := s.RegularPages[1] + + if useFilename { + ass.False(noSlug.Date.IsZero()) + ass.False(slug.Date.IsZero()) + ass.Equal(2012, noSlug.Date.Year()) + ass.Equal(2012, slug.Date.Year()) + ass.Equal("noslug", noSlug.Slug) + ass.Equal("aslug", slug.Slug) + + } else { + ass.True(noSlug.Date.IsZero()) + ass.True(slug.Date.IsZero()) + ass.Equal("", noSlug.Slug) + ass.Equal("aslug", slug.Slug) + } + + }) + } - checkPageDate(t, p, d) } -*/ + func TestMetadataDates(t *testing.T) { t.Parallel() From 51574fdceb5cbac1e780a2cc8adbe1ab53e8b412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Feb 2018 19:26:53 +0100 Subject: [PATCH 06/13] Work --- hugolib/page.go | 1 + hugolib/page_frontmatter.go | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/hugolib/page.go b/hugolib/page.go index 77e12735619..9151a599013 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1127,6 +1127,7 @@ func (p *Page) update(frontmatter map[string]interface{}) error { frontmatter: frontmatter, params: p.params, dates: &p.PageDates, + pageURLs: &p.URLPath, baseFilename: p.BaseFileName(), modTime: mtime} // Handle the date separately diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index 02a7ae6dc1c..78ae4a76e37 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -55,6 +55,9 @@ type frontMatterDescriptor struct { // This is the Page's dates. dates *PageDates + + // This is the Page's Slug etc. + pageURLs *URLPath } func (f frontmatterHandler) handleDate(d frontMatterDescriptor) (time.Time, error) { @@ -156,7 +159,7 @@ func (f frontmatterHandler) setParamsAndReturnFirstDate(d frontMatterDescriptor, // A Zero date is a signal that the name can not be parsed. // This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: // "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" -func (f frontmatterHandler) dateAndSlugFromBaseFilename(name string) (time.Time, string) { +func (f frontmatterFieldHandlers) dateAndSlugFromBaseFilename(name string) (time.Time, string) { withoutExt, _ := helpers.FileAndExt(name) if len(withoutExt) < 10 { @@ -253,6 +256,19 @@ func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) ( } func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d frontMatterDescriptor) (interface{}, error) { + + date, slug := f.dateAndSlugFromBaseFilename(d.baseFilename) + if date.IsZero() { + return nil, nil + } + + d.dates.Date = date + + if _, found := d.frontmatter["slug"]; !found { + // Use slug from filename + d.pageURLs.Slug = slug + } + return true, nil } From 3d6d4eb291baaeb53e9d812ad3dfb0a47413cf47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Feb 2018 19:34:32 +0100 Subject: [PATCH 07/13] Working --- hugolib/page_frontmatter.go | 56 ++++++++++++++------------------ hugolib/page_frontmatter_test.go | 6 +--- hugolib/page_test.go | 4 +-- hugolib/site.go | 4 +-- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index 78ae4a76e37..d4ec33935c3 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -60,15 +60,9 @@ type frontMatterDescriptor struct { pageURLs *URLPath } -func (f frontmatterHandler) handleDate(d frontMatterDescriptor) (time.Time, error) { - v, err := f.dateHandlers(d) - if err == nil { - if d, ok := v.(time.Time); ok { - return d, nil - } - } - - return time.Time{}, err +func (f frontmatterHandler) handleDate(d frontMatterDescriptor) error { + _, err := f.dateHandlers(d) + return err } var ( @@ -94,11 +88,10 @@ func init() { func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { - date, err := f.handleDate(d) + err := f.handleDate(d) if err != nil { return err } - d.dates.Date = date d.dates.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) d.dates.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) d.dates.ExpiryDate = f.setParamsAndReturnFirstDate(d, expiryDateFrontMatterKeys) @@ -159,7 +152,7 @@ func (f frontmatterHandler) setParamsAndReturnFirstDate(d frontMatterDescriptor, // A Zero date is a signal that the name can not be parsed. // This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: // "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" -func (f frontmatterFieldHandlers) dateAndSlugFromBaseFilename(name string) (time.Time, string) { +func dateAndSlugFromBaseFilename(name string) (time.Time, string) { withoutExt, _ := helpers.FileAndExt(name) if len(withoutExt) < 10 { @@ -180,22 +173,21 @@ func (f frontmatterFieldHandlers) dateAndSlugFromBaseFilename(name string) (time return d, slug } -type frontMatterFieldHandler func(d frontMatterDescriptor) (interface{}, error) +type frontMatterFieldHandler func(d frontMatterDescriptor) (bool, error) func (f frontmatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler { - return func(d frontMatterDescriptor) (interface{}, error) { + return func(d frontMatterDescriptor) (bool, error) { for _, h := range handlers { - // First non-nil value wins. - val, err := h(d) + // First successful handler wins. + success, err := h(d) if err != nil { f.logger.ERROR.Println(err) - } else if val != nil { - return val, nil + } else if success { + return true, nil } } - return nil, nil + return false, nil } - } func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatterHandler, error) { @@ -241,25 +233,26 @@ type frontmatterFieldHandlers struct { logger *jww.Notepad } -func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) (interface{}, error) { +func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) (bool, error) { v, found := d.frontmatter["date"] if !found { - return nil, nil + return false, nil } date, err := cast.ToTimeE(v) if err != nil { - return nil, err + return false, err } - return date, nil -} + d.dates.Date = date -func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d frontMatterDescriptor) (interface{}, error) { + return true, nil +} - date, slug := f.dateAndSlugFromBaseFilename(d.baseFilename) +func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d frontMatterDescriptor) (bool, error) { + date, slug := dateAndSlugFromBaseFilename(d.baseFilename) if date.IsZero() { - return nil, nil + return false, nil } d.dates.Date = date @@ -272,9 +265,10 @@ func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d frontMatterDescr return true, nil } -func (f *frontmatterFieldHandlers) defaultDateModTimeHandler(d frontMatterDescriptor) (interface{}, error) { +func (f *frontmatterFieldHandlers) defaultDateModTimeHandler(d frontMatterDescriptor) (bool, error) { if !d.modTime.IsZero() { - return d.modTime, nil + d.dates.Date = d.modTime + return true, nil } - return nil, nil + return false, nil } diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go index c93d5cc66b7..ae87a1ca879 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/page_frontmatter_test.go @@ -18,7 +18,6 @@ import ( "testing" "time" - "github.com/spf13/viper" "github.com/stretchr/testify/require" ) @@ -28,9 +27,6 @@ func TestDateAndSlugFromBaseFilename(t *testing.T) { assert := require.New(t) - fc, err := newFrontmatterConfig(newWarningLogger(), viper.New()) - assert.NoError(err) - tests := []struct { name string date string @@ -55,7 +51,7 @@ func TestDateAndSlugFromBaseFilename(t *testing.T) { assert.NoError(err) errMsg := fmt.Sprintf("Test %d", i) - gotDate, gotSlug := fc.dateAndSlugFromBaseFilename(test.name) + gotDate, gotSlug := dateAndSlugFromBaseFilename(test.name) assert.Equal(expectedDate, gotDate, errMsg) assert.Equal(test.slug, gotSlug, errMsg) diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 9266c167c8c..b024a69a5c4 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1006,8 +1006,8 @@ Content }) } - writeSource(t, fs, filepath.Join("content", "2012-02-21-noslug.md"), fmt.Sprintf(pageTemplate, 1, "")) - writeSource(t, fs, filepath.Join("content", "2012-02-22-slug.md"), fmt.Sprintf(pageTemplate, 2, "slug: aslug")) + writeSource(t, fs, filepath.Join("content", "section", "2012-02-21-noslug.md"), fmt.Sprintf(pageTemplate, 1, "")) + writeSource(t, fs, filepath.Join("content", "section", "2012-02-22-slug.md"), fmt.Sprintf(pageTemplate, 2, "slug: aslug")) s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) diff --git a/hugolib/site.go b/hugolib/site.go index c84383dbec0..184b1e187a3 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -180,7 +180,7 @@ func (s *Site) reset() *Site { relatedDocsHandler: newSearchIndexHandler(s.relatedDocsHandler.cfg), outputFormats: s.outputFormats, outputFormatsConfig: s.outputFormatsConfig, - frontmatterHandler: s.frontmatterHandler, + frontmatterHandler: s.frontmatterHandler, mediaTypesConfig: s.mediaTypesConfig, resourceSpec: s.resourceSpec, Language: s.Language, @@ -267,7 +267,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { outputFormats: outputFormats, outputFormatsConfig: siteOutputFormatsConfig, mediaTypesConfig: siteMediaTypesConfig, - frontmatterHandler: fmConfig, + frontmatterHandler: fmConfig, } s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) From 2b61ea79116774183e4bc10a9b7eb6ce1790c960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 22 Feb 2018 20:55:14 +0100 Subject: [PATCH 08/13] Rename --- hugolib/page_frontmatter.go | 2 +- hugolib/site.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index d4ec33935c3..3f9775573a5 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -190,7 +190,7 @@ func (f frontmatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontM } } -func newFrontmatterConfig(logger *jww.Notepad, cfg config.Provider) (frontmatterHandler, error) { +func newFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (frontmatterHandler, error) { if logger == nil { logger = jww.NewNotepad(jww.LevelWarn, jww.LevelWarn, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) diff --git a/hugolib/site.go b/hugolib/site.go index 184b1e187a3..2f20a35d9c5 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -252,7 +252,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { titleFunc := helpers.GetTitleFunc(cfg.Language.GetString("titleCaseStyle")) - fmConfig, err := newFrontmatterConfig(cfg.Logger, cfg.Cfg) + frontMatterHandler, err := newFrontmatterHandler(cfg.Logger, cfg.Cfg) if err != nil { return nil, err } @@ -267,7 +267,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { outputFormats: outputFormats, outputFormatsConfig: siteOutputFormatsConfig, mediaTypesConfig: siteMediaTypesConfig, - frontmatterHandler: fmConfig, + frontmatterHandler: frontMatterHandler, } s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) From 69b97b237d4bf52d61ca24873e7cfd9c1fc9de66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 23 Feb 2018 09:44:40 +0100 Subject: [PATCH 09/13] Adjust pubdate Ref #3977 --- hugolib/page_frontmatter.go | 8 ++++---- hugolib/page_test.go | 14 +++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index 3f9775573a5..b8f3a4dc3ae 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -101,14 +101,14 @@ func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { d.dates.Date = d.dates.PublishDate } - if d.dates.PublishDate.IsZero() { - d.dates.PublishDate = d.dates.Date - } - if d.dates.Lastmod.IsZero() { d.dates.Lastmod = d.dates.Date } + if d.dates.Date.IsZero() { + d.dates.Date = d.dates.Lastmod + } + f.setParamIfNotZero("date", d.params, d.dates.Date) f.setParamIfNotZero("lastmod", d.params, d.dates.Lastmod) f.setParamIfNotZero("publishdate", d.params, d.dates.PublishDate) diff --git a/hugolib/page_test.go b/hugolib/page_test.go index b024a69a5c4..dc4fcb60eeb 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -726,6 +726,7 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { } // Issue #3854 +// Also see https://github.com/gohugoio/hugo/issues/3977 func TestPageWithDateFields(t *testing.T) { assert := require.New(t) pageWithDate := `--- @@ -735,8 +736,8 @@ weight: %d --- Simple Page With Some Date` - hasBothDates := func(p *Page) bool { - return p.Date.Year() == 2017 && p.PublishDate.Year() == 2017 + hasDate := func(p *Page) bool { + return p.Date.Year() == 2017 } datePage := func(field string, weight int) string { @@ -747,7 +748,7 @@ Simple Page With Some Date` assertFunc := func(t *testing.T, ext string, pages Pages) { assert.True(len(pages) > 0) for _, p := range pages { - assert.True(hasBothDates(p)) + assert.True(hasDate(p)) } } @@ -1070,8 +1071,8 @@ func TestMetadataDates(t *testing.T) { // // ------- inputs --------|--- outputs ---| //content filename modfb? D P L M E - {p_D____, "test.md", false, D, D, D, x, x}, // date copied across - {p_D____, "testy.md", true, D, D, D, x, x}, + {p_D____, "test.md", false, D, o, D, x, x}, + {p_D____, "testy.md", true, D, o, D, x, x}, {p__P___, "test.md", false, P, P, P, x, x}, // pubdate copied across //{p__P___, "testy.md", true, P, P, P, x, x}, // TODO(bep) date from modTime {p_DP___, "test.md", false, D, P, D, x, x}, // date -> lastMod @@ -1112,6 +1113,9 @@ func TestMetadataDates(t *testing.T) { checkDate(t, i+1, "LastMod", p.ExpiryDate, test.expExp, fi) // check Page Params + // TODO(bep) we need to rewrite these date tests to more unit style. + // The params checks below are currently flawed, as they don't check for the + // absense (nil) of a date. checkDate(t, i+1, "param date", cast.ToTime(p.params["date"]), test.expDate, fi) checkDate(t, i+1, "param publishdate", cast.ToTime(p.params["publishdate"]), test.expPub, fi) checkDate(t, i+1, "param modified", cast.ToTime(p.params["modified"]), test.expMod, fi) From 247a82fa4d9634504d37013c8285ce5cb7977329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 23 Feb 2018 10:38:35 +0100 Subject: [PATCH 10/13] Test work --- hugolib/page_frontmatter.go | 3 + hugolib/page_frontmatter_test.go | 133 +++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index b8f3a4dc3ae..c88d482a10f 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -87,6 +87,9 @@ func init() { } func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { + if d.dates == nil { + panic("missing dates") + } err := f.handleDate(d) if err != nil { diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go index ae87a1ca879..a0ac7e65c03 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/page_frontmatter_test.go @@ -18,6 +18,8 @@ import ( "testing" "time" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) @@ -58,3 +60,134 @@ func TestDateAndSlugFromBaseFilename(t *testing.T) { } } + +func TestFrontMatterDates(t *testing.T) { + t.Parallel() + + defaultDateSettings := []string{"none", "file"} + + for _, defaultDateSetting := range defaultDateSettings { + t.Run(fmt.Sprintf("defaultDate=%s", defaultDateSetting), func(t *testing.T) { + doTestFrontMatterDates(t, defaultDateSetting) + }) + } +} + +func doTestFrontMatterDates(t *testing.T, defaultDateSetting string) { + assert := require.New(t) + + cfg := viper.New() + + cfg.Set("frontmatter", map[string]interface{}{ + "defaultDate": []string{defaultDateSetting}, + }) + + handler, err := newFrontmatterHandler(newWarningLogger(), cfg) + assert.NoError(err) + + testDate, err := time.Parse("2006-01-02", "2018-02-01") + assert.NoError(err) + + sentinel := (time.Time{}).Add(1 * time.Hour) + zero := time.Time{} + + // See http://www.imdb.com/title/tt0133093/ + for _, lastModKey := range []string{"lastmod", "modified"} { + for _, lastModDate := range []time.Time{testDate, sentinel} { + for _, dateKey := range []string{"date"} { + testDate = testDate.Add(24 * time.Hour) + for _, dateDate := range []time.Time{testDate, sentinel} { + for _, pubDateKey := range []string{"publishdate", "pubdate", "published"} { + testDate = testDate.Add(24 * time.Hour) + for _, pubDateDate := range []time.Time{testDate, sentinel} { + for _, expiryDateKey := range []string{"expirydate", "unpublishdate"} { + testDate = testDate.Add(24 * time.Hour) + for _, expiryDateDate := range []time.Time{testDate, sentinel} { + d := frontMatterDescriptor{ + frontmatter: make(map[string]interface{}), + params: make(map[string]interface{}), + dates: &PageDates{}, + pageURLs: &URLPath{}, + } + + var ( + // expLastModP, expDateP, expPubDateP, expExiryDateP = sentinel, sentinel, sentinel, sentinel + expLastMod, expDate, expPubDate, expExiryDate = zero, zero, zero, zero + ) + + if lastModDate != sentinel { + d.frontmatter[lastModKey] = lastModDate + expLastMod = lastModDate + expDate = lastModDate + } + + if dateDate != sentinel { + d.frontmatter[dateKey] = dateDate + expDate = dateDate + } + + if pubDateDate != sentinel { + d.frontmatter[pubDateKey] = pubDateDate + expPubDate = pubDateDate + } + + if expiryDateDate != sentinel { + d.frontmatter[expiryDateKey] = expiryDateDate + expExiryDate = expiryDateDate + } + + assert.NoError(handler.handleDates(d)) + + assertFrontMatterDate(assert, d, expDate, "date") + assertFrontMatterDate(assert, d, expLastMod, "lastmod") + assertFrontMatterDate(assert, d, expPubDate, "publishdate") + assertFrontMatterDate(assert, d, expExiryDate, "expirydate") + + } + } + } + } + } + } + } + } +} + +func assertFrontMatterDate(assert *require.Assertions, d frontMatterDescriptor, expected time.Time, dateField string) { + switch dateField { + case "date": + case "lastmod": + case "publishdate": + case "expirydate": + default: + assert.Failf("Unknown datefield %s", dateField) + } + + param, found := d.params[dateField] + + message := fmt.Sprintf("[%s] Found: %t Expected: %v Params: %v Front matter: %v", dateField, found, expected, d.params, d.frontmatter) + + assert.True(found != expected.IsZero(), message) + + if found { + if expected != param { + assert.Fail("Params check failed", "[%s] Expected:\n%q\nGot:\n%q", dateField, expected, param) + } + assert.Equal(expected, param) + + } +} + +type dateTestHelper struct { + name string + + dates PageDates +} + +func (d dateTestHelper) descriptor() frontMatterDescriptor { + return frontMatterDescriptor{dates: &d.dates} +} + +func (d dateTestHelper) assert(t *testing.T) { + +} From 0bb9de753885fb1370b096172e29ccded3d8e195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 23 Feb 2018 14:50:31 +0100 Subject: [PATCH 11/13] More --- hugolib/page_frontmatter.go | 5 ++++ hugolib/page_frontmatter_test.go | 42 +++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index c88d482a10f..c5f50f7ef5e 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -108,6 +108,11 @@ func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { d.dates.Lastmod = d.dates.Date } + // TODO(bep) date decide vs https://github.com/gohugoio/hugo/issues/3977 + if d.dates.PublishDate.IsZero() { + //d.dates.PublishDate = d.dates.Date + } + if d.dates.Date.IsZero() { d.dates.Date = d.dates.Lastmod } diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go index a0ac7e65c03..317e1f26a7f 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/page_frontmatter_test.go @@ -93,15 +93,20 @@ func doTestFrontMatterDates(t *testing.T, defaultDateSetting string) { // See http://www.imdb.com/title/tt0133093/ for _, lastModKey := range []string{"lastmod", "modified"} { + testDate = testDate.Add(24 * time.Hour) + t.Log(lastModKey, testDate) for _, lastModDate := range []time.Time{testDate, sentinel} { for _, dateKey := range []string{"date"} { testDate = testDate.Add(24 * time.Hour) + t.Log(dateKey, testDate) for _, dateDate := range []time.Time{testDate, sentinel} { for _, pubDateKey := range []string{"publishdate", "pubdate", "published"} { testDate = testDate.Add(24 * time.Hour) + t.Log(pubDateKey, testDate) for _, pubDateDate := range []time.Time{testDate, sentinel} { for _, expiryDateKey := range []string{"expirydate", "unpublishdate"} { testDate = testDate.Add(24 * time.Hour) + t.Log(expiryDateKey, testDate) for _, expiryDateDate := range []time.Time{testDate, sentinel} { d := frontMatterDescriptor{ frontmatter: make(map[string]interface{}), @@ -110,16 +115,7 @@ func doTestFrontMatterDates(t *testing.T, defaultDateSetting string) { pageURLs: &URLPath{}, } - var ( - // expLastModP, expDateP, expPubDateP, expExiryDateP = sentinel, sentinel, sentinel, sentinel - expLastMod, expDate, expPubDate, expExiryDate = zero, zero, zero, zero - ) - - if lastModDate != sentinel { - d.frontmatter[lastModKey] = lastModDate - expLastMod = lastModDate - expDate = lastModDate - } + var expLastMod, expDate, expPubDate, expExiryDate = zero, zero, zero, zero if dateDate != sentinel { d.frontmatter[dateKey] = dateDate @@ -129,6 +125,18 @@ func doTestFrontMatterDates(t *testing.T, defaultDateSetting string) { if pubDateDate != sentinel { d.frontmatter[pubDateKey] = pubDateDate expPubDate = pubDateDate + if expDate.IsZero() { + expDate = expPubDate + } + } + + if lastModDate != sentinel { + d.frontmatter[lastModKey] = lastModDate + expLastMod = lastModDate + + if expDate.IsZero() { + expDate = lastModDate + } } if expiryDateDate != sentinel { @@ -136,13 +144,16 @@ func doTestFrontMatterDates(t *testing.T, defaultDateSetting string) { expExiryDate = expiryDateDate } + if expLastMod.IsZero() { + expLastMod = expDate + } + assert.NoError(handler.handleDates(d)) assertFrontMatterDate(assert, d, expDate, "date") assertFrontMatterDate(assert, d, expLastMod, "lastmod") assertFrontMatterDate(assert, d, expPubDate, "publishdate") assertFrontMatterDate(assert, d, expExiryDate, "expirydate") - } } } @@ -165,7 +176,12 @@ func assertFrontMatterDate(assert *require.Assertions, d frontMatterDescriptor, param, found := d.params[dateField] - message := fmt.Sprintf("[%s] Found: %t Expected: %v Params: %v Front matter: %v", dateField, found, expected, d.params, d.frontmatter) + if found && param.(time.Time).IsZero() { + assert.Fail("Zero time in params", dateField) + } + + message := fmt.Sprintf("[%s] Found: %t Expected: %v (%t) Param: %v Params: %v Front matter: %v", + dateField, found, expected, expected.IsZero(), param, d.params, d.frontmatter) assert.True(found != expected.IsZero(), message) @@ -173,8 +189,6 @@ func assertFrontMatterDate(assert *require.Assertions, d frontMatterDescriptor, if expected != param { assert.Fail("Params check failed", "[%s] Expected:\n%q\nGot:\n%q", dateField, expected, param) } - assert.Equal(expected, param) - } } From 352e78739217f77e8fb8cb1a96c49236da7d9dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 24 Feb 2018 11:39:06 +0100 Subject: [PATCH 12/13] More --- hugolib/page_frontmatter.go | 70 ++++++++++++++++++++++++++++++++ hugolib/page_frontmatter_test.go | 50 +++++++++++++++++------ 2 files changed, 108 insertions(+), 12 deletions(-) diff --git a/hugolib/page_frontmatter.go b/hugolib/page_frontmatter.go index c5f50f7ef5e..532d33047db 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/page_frontmatter.go @@ -21,6 +21,8 @@ import ( "strings" "time" + "github.com/mitchellh/mapstructure" + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/config" @@ -198,6 +200,64 @@ func (f frontmatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontM } } +type frontmatterConfig struct { + date []string + lastMod []string + publishDate []string + expiryDate []string +} + +const ( + fmDate = "date" + fmPubDate = "publishdate" + fmLastMod = "lastmod" + fmExpiryDate = "expirydate" +) + +func newDefaultFrontmatterConfig() frontmatterConfig { + return frontmatterConfig{ + date: []string{fmDate, fmPubDate, fmLastMod}, + lastMod: []string{fmLastMod, fmDate}, + publishDate: []string{fmPubDate, fmDate}, + expiryDate: []string{fmExpiryDate}, + } +} + +func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) { + c := newDefaultFrontmatterConfig() + + if cfg.IsSet("frontmatter") { + fm := cfg.GetStringMap("frontmatter") + if fm != nil { + for k, v := range fm { + loki := strings.ToLower(k) + switch loki { + case fmDate: + c.date = toLowerSlice(v) + case fmPubDate: + c.publishDate = toLowerSlice(v) + case fmLastMod: + c.lastMod = toLowerSlice(v) + case fmExpiryDate: + c.expiryDate = toLowerSlice(v) + } + } + } + err := mapstructure.WeakDecode(fm, &c) + return c, err + } + return c, nil +} + +func toLowerSlice(in interface{}) []string { + out := cast.ToStringSlice(in) + for i := 0; i < len(out); i++ { + out[i] = strings.ToLower(out[i]) + } + + return out +} + func newFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (frontmatterHandler, error) { if logger == nil { @@ -208,6 +268,16 @@ func newFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (frontmatte handlers := &frontmatterFieldHandlers{logger: logger} + /* + + [frontmatter] + date = ["date", "publishDate", "lastMod"] + lastMod = ["lastMod", "date"] + publishDate = ["publishDate", "date"] + expiryDate = ["expiryDate"] + + */ + dateHandlers := []frontMatterFieldHandler{handlers.defaultDateHandler} defaultDate := cfg.Get("frontmatter.defaultdate") diff --git a/hugolib/page_frontmatter_test.go b/hugolib/page_frontmatter_test.go index 317e1f26a7f..38f486e6c07 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/page_frontmatter_test.go @@ -61,27 +61,53 @@ func TestDateAndSlugFromBaseFilename(t *testing.T) { } } -func TestFrontMatterDates(t *testing.T) { - t.Parallel() +func TestFrontMatterNewConfig(t *testing.T) { + assert := require.New(t) - defaultDateSettings := []string{"none", "file"} + cfg := viper.New() - for _, defaultDateSetting := range defaultDateSettings { - t.Run(fmt.Sprintf("defaultDate=%s", defaultDateSetting), func(t *testing.T) { - doTestFrontMatterDates(t, defaultDateSetting) - }) - } -} + cfg.Set("frontmatter", map[string]interface{}{ + "date": []string{"publishDate", "LastMod"}, + "Lastmod": []string{"publishDate"}, + "expiryDate": []string{"lastMod"}, + "publishDate": []string{"date"}, + }) -func doTestFrontMatterDates(t *testing.T, defaultDateSetting string) { - assert := require.New(t) + fc, err := newFrontmatterConfig(cfg) + assert.NoError(err) + assert.Equal([]string{"publishdate", "lastmod"}, fc.date) + assert.Equal([]string{"publishdate"}, fc.lastMod) + assert.Equal([]string{"lastmod"}, fc.expiryDate) + assert.Equal([]string{"date"}, fc.publishDate) + + // Default + cfg = viper.New() + fc, err = newFrontmatterConfig(cfg) + assert.NoError(err) + assert.Equal(3, len(fc.date)) + assert.Equal(2, len(fc.lastMod)) + assert.Equal(2, len(fc.publishDate)) + assert.Equal(1, len(fc.expiryDate)) +} + +func TestFrontMatterDatesConfigVariations(t *testing.T) { cfg := viper.New() cfg.Set("frontmatter", map[string]interface{}{ - "defaultDate": []string{defaultDateSetting}, + "defaultDate": []string{"date"}, }) + fmt.Println(">>", cfg) +} + +func TestFrontMatterDates(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + cfg := viper.New() + handler, err := newFrontmatterHandler(newWarningLogger(), cfg) assert.NoError(err) From dbd5c97eb97a6d1a9ac1fafa1068ae6b2736bb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 24 Feb 2018 15:17:14 +0100 Subject: [PATCH 13/13] Move to pagemeta package --- hugolib/page.go | 41 +++----- hugolib/page_paths.go | 2 +- hugolib/{ => pagemeta}/page_frontmatter.go | 99 +++++++++---------- .../{ => pagemeta}/page_frontmatter_test.go | 43 ++++---- hugolib/pagemeta/pagemeta.go | 30 ++++++ hugolib/site.go | 5 +- 6 files changed, 115 insertions(+), 105 deletions(-) rename hugolib/{ => pagemeta}/page_frontmatter.go (75%) rename hugolib/{ => pagemeta}/page_frontmatter_test.go (85%) create mode 100644 hugolib/pagemeta/pagemeta.go diff --git a/hugolib/page.go b/hugolib/page.go index 9151a599013..170c15dddf6 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -25,6 +25,7 @@ import ( "github.com/bep/gitmap" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib/pagemeta" "github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/output" @@ -220,10 +221,12 @@ type Page struct { Keywords []string Data map[string]interface{} - PageDates + pagemeta.PageDates Sitemap Sitemap - URLPath + pagemeta.URLPath + frontMatterURL string + permalink string relPermalink string @@ -260,13 +263,6 @@ type Page struct { targetPathDescriptorPrototype *targetPathDescriptor } -type PageDates struct { - Date time.Time - Lastmod time.Time - PublishDate time.Time - ExpiryDate time.Time -} - // SearchKeywords implements the related.Document interface needed for fast page searches. func (p *Page) SearchKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { @@ -1123,17 +1119,18 @@ func (p *Page) update(frontmatter map[string]interface{}) error { mtime = p.Source.FileInfo().ModTime() } - descriptor := frontMatterDescriptor{ - frontmatter: frontmatter, - params: p.params, - dates: &p.PageDates, - pageURLs: &p.URLPath, - baseFilename: p.BaseFileName(), modTime: mtime} + descriptor := pagemeta.FrontMatterDescriptor{ + Frontmatter: frontmatter, + Params: p.params, + Dates: &p.PageDates, + PageURLs: &p.URLPath, + BaseFilename: p.BaseFileName(), + ModTime: mtime} // Handle the date separately // TODO(bep) we need to "do more" in this area so this can be split up and // more easily tested without the Page, but the coupling is strong. - err := p.s.frontmatterHandler.handleDates(descriptor) + err := p.s.frontmatterHandler.HandleDates(descriptor) if err != nil { p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.Path(), err) } @@ -1151,7 +1148,7 @@ func (p *Page) update(frontmatter map[string]interface{}) error { continue } - if p.s.frontmatterHandler.isDateKey(loki) { + if p.s.frontmatterHandler.IsDateKey(loki) { continue } @@ -1173,7 +1170,7 @@ func (p *Page) update(frontmatter map[string]interface{}) error { return fmt.Errorf("Only relative URLs are supported, %v provided", url) } p.URLPath.URL = cast.ToString(v) - p.URLPath.frontMatterURL = p.URLPath.URL + p.frontMatterURL = p.URLPath.URL p.params[loki] = p.URLPath.URL case "type": p.contentType = cast.ToString(v) @@ -1829,14 +1826,6 @@ func (p *Page) String() string { return fmt.Sprintf("Page(%q)", p.title) } -type URLPath struct { - URL string - frontMatterURL string - Permalink string - Slug string - Section string -} - // Scratch returns the writable context associated with this Page. func (p *Page) Scratch() *Scratch { if p.scratch == nil { diff --git a/hugolib/page_paths.go b/hugolib/page_paths.go index 5f45f7adf5a..13aebcd9120 100644 --- a/hugolib/page_paths.go +++ b/hugolib/page_paths.go @@ -88,7 +88,7 @@ func (p *Page) initTargetPathDescriptor() error { Sections: p.sections, UglyURLs: p.s.Info.uglyURLs(p), Dir: filepath.ToSlash(p.Source.Dir()), - URL: p.URLPath.frontMatterURL, + URL: p.frontMatterURL, IsMultihost: p.s.owner.IsMultihost(), } diff --git a/hugolib/page_frontmatter.go b/hugolib/pagemeta/page_frontmatter.go similarity index 75% rename from hugolib/page_frontmatter.go rename to hugolib/pagemeta/page_frontmatter.go index 532d33047db..135c7d6c6b4 100644 --- a/hugolib/page_frontmatter.go +++ b/hugolib/pagemeta/page_frontmatter.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package hugolib +package pagemeta import ( "fmt" @@ -32,37 +32,37 @@ import ( // TODO(bep) should probably make the date handling chain complete to give people the flexibility they want. -type frontmatterHandler struct { +type FrontmatterHandler struct { // Ordered chain. dateHandlers frontMatterFieldHandler logger *jww.Notepad } -type frontMatterDescriptor struct { +type FrontMatterDescriptor struct { // This the Page's front matter. - frontmatter map[string]interface{} + Frontmatter map[string]interface{} // This is the Page's base filename, e.g. page.md. - baseFilename string + BaseFilename string // The content file's mod time. - modTime time.Time + ModTime time.Time // The below are pointers to values on Page and will be updated. // This is the Page's params. - params map[string]interface{} + Params map[string]interface{} // This is the Page's dates. - dates *PageDates + Dates *PageDates // This is the Page's Slug etc. - pageURLs *URLPath + PageURLs *URLPath } -func (f frontmatterHandler) handleDate(d frontMatterDescriptor) error { +func (f FrontmatterHandler) handleDate(d FrontMatterDescriptor) error { _, err := f.dateHandlers(d) return err } @@ -88,8 +88,8 @@ func init() { allDateFrontMatterKeys["date"] = true } -func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { - if d.dates == nil { +func (f FrontmatterHandler) HandleDates(d FrontMatterDescriptor) error { + if d.Dates == nil { panic("missing dates") } @@ -97,61 +97,61 @@ func (f frontmatterHandler) handleDates(d frontMatterDescriptor) error { if err != nil { return err } - d.dates.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) - d.dates.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) - d.dates.ExpiryDate = f.setParamsAndReturnFirstDate(d, expiryDateFrontMatterKeys) + d.Dates.Lastmod = f.setParamsAndReturnFirstDate(d, lastModFrontMatterKeys) + d.Dates.PublishDate = f.setParamsAndReturnFirstDate(d, publishDateFrontMatterKeys) + d.Dates.ExpiryDate = f.setParamsAndReturnFirstDate(d, expiryDateFrontMatterKeys) // Hugo really needs a date! - if d.dates.Date.IsZero() { - d.dates.Date = d.dates.PublishDate + if d.Dates.Date.IsZero() { + d.Dates.Date = d.Dates.PublishDate } - if d.dates.Lastmod.IsZero() { - d.dates.Lastmod = d.dates.Date + if d.Dates.Lastmod.IsZero() { + d.Dates.Lastmod = d.Dates.Date } // TODO(bep) date decide vs https://github.com/gohugoio/hugo/issues/3977 - if d.dates.PublishDate.IsZero() { + if d.Dates.PublishDate.IsZero() { //d.dates.PublishDate = d.dates.Date } - if d.dates.Date.IsZero() { - d.dates.Date = d.dates.Lastmod + if d.Dates.Date.IsZero() { + d.Dates.Date = d.Dates.Lastmod } - f.setParamIfNotZero("date", d.params, d.dates.Date) - f.setParamIfNotZero("lastmod", d.params, d.dates.Lastmod) - f.setParamIfNotZero("publishdate", d.params, d.dates.PublishDate) - f.setParamIfNotZero("expirydate", d.params, d.dates.ExpiryDate) + f.setParamIfNotZero("date", d.Params, d.Dates.Date) + f.setParamIfNotZero("lastmod", d.Params, d.Dates.Lastmod) + f.setParamIfNotZero("publishdate", d.Params, d.Dates.PublishDate) + f.setParamIfNotZero("expirydate", d.Params, d.Dates.ExpiryDate) return nil } -func (f frontmatterHandler) isDateKey(key string) bool { +func (f FrontmatterHandler) IsDateKey(key string) bool { return allDateFrontMatterKeys[key] } -func (f frontmatterHandler) setParamIfNotZero(name string, params map[string]interface{}, date time.Time) { +func (f FrontmatterHandler) setParamIfNotZero(name string, params map[string]interface{}, date time.Time) { if date.IsZero() { return } params[name] = date } -func (f frontmatterHandler) setParamsAndReturnFirstDate(d frontMatterDescriptor, keys []string) time.Time { +func (f FrontmatterHandler) setParamsAndReturnFirstDate(d FrontMatterDescriptor, keys []string) time.Time { var date time.Time for _, key := range keys { - v, found := d.frontmatter[key] + v, found := d.Frontmatter[key] if found { currentDate, err := cast.ToTimeE(v) if err == nil { - d.params[key] = currentDate + d.Params[key] = currentDate if date.IsZero() { date = currentDate } } else { - d.params[key] = v + d.Params[key] = v } } } @@ -183,10 +183,10 @@ func dateAndSlugFromBaseFilename(name string) (time.Time, string) { return d, slug } -type frontMatterFieldHandler func(d frontMatterDescriptor) (bool, error) +type frontMatterFieldHandler func(d FrontMatterDescriptor) (bool, error) -func (f frontmatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler { - return func(d frontMatterDescriptor) (bool, error) { +func (f FrontmatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontMatterFieldHandler) frontMatterFieldHandler { + return func(d FrontMatterDescriptor) (bool, error) { for _, h := range handlers { // First successful handler wins. success, err := h(d) @@ -258,15 +258,15 @@ func toLowerSlice(in interface{}) []string { return out } -func newFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (frontmatterHandler, error) { +func NewFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (FrontmatterHandler, error) { if logger == nil { logger = jww.NewNotepad(jww.LevelWarn, jww.LevelWarn, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) } - f := frontmatterHandler{logger: logger} + f := FrontmatterHandler{logger: logger} - handlers := &frontmatterFieldHandlers{logger: logger} + handlers := &frontmatterFieldHandlers{} /* @@ -308,11 +308,10 @@ func newFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (frontmatte } type frontmatterFieldHandlers struct { - logger *jww.Notepad } -func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) (bool, error) { - v, found := d.frontmatter["date"] +func (f *frontmatterFieldHandlers) defaultDateHandler(d FrontMatterDescriptor) (bool, error) { + v, found := d.Frontmatter["date"] if !found { return false, nil } @@ -322,30 +321,30 @@ func (f *frontmatterFieldHandlers) defaultDateHandler(d frontMatterDescriptor) ( return false, err } - d.dates.Date = date + d.Dates.Date = date return true, nil } -func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d frontMatterDescriptor) (bool, error) { - date, slug := dateAndSlugFromBaseFilename(d.baseFilename) +func (f *frontmatterFieldHandlers) defaultDateFilenameHandler(d FrontMatterDescriptor) (bool, error) { + date, slug := dateAndSlugFromBaseFilename(d.BaseFilename) if date.IsZero() { return false, nil } - d.dates.Date = date + d.Dates.Date = date - if _, found := d.frontmatter["slug"]; !found { + if _, found := d.Frontmatter["slug"]; !found { // Use slug from filename - d.pageURLs.Slug = slug + d.PageURLs.Slug = slug } return true, nil } -func (f *frontmatterFieldHandlers) defaultDateModTimeHandler(d frontMatterDescriptor) (bool, error) { - if !d.modTime.IsZero() { - d.dates.Date = d.modTime +func (f *frontmatterFieldHandlers) defaultDateModTimeHandler(d FrontMatterDescriptor) (bool, error) { + if !d.ModTime.IsZero() { + d.Dates.Date = d.ModTime return true, nil } return false, nil diff --git a/hugolib/page_frontmatter_test.go b/hugolib/pagemeta/page_frontmatter_test.go similarity index 85% rename from hugolib/page_frontmatter_test.go rename to hugolib/pagemeta/page_frontmatter_test.go index 38f486e6c07..a6e17b407b7 100644 --- a/hugolib/page_frontmatter_test.go +++ b/hugolib/pagemeta/page_frontmatter_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package hugolib +package pagemeta import ( "fmt" @@ -108,7 +108,7 @@ func TestFrontMatterDates(t *testing.T) { cfg := viper.New() - handler, err := newFrontmatterHandler(newWarningLogger(), cfg) + handler, err := NewFrontmatterHandler(nil, cfg) assert.NoError(err) testDate, err := time.Parse("2006-01-02", "2018-02-01") @@ -134,22 +134,22 @@ func TestFrontMatterDates(t *testing.T) { testDate = testDate.Add(24 * time.Hour) t.Log(expiryDateKey, testDate) for _, expiryDateDate := range []time.Time{testDate, sentinel} { - d := frontMatterDescriptor{ - frontmatter: make(map[string]interface{}), - params: make(map[string]interface{}), - dates: &PageDates{}, - pageURLs: &URLPath{}, + d := FrontMatterDescriptor{ + Frontmatter: make(map[string]interface{}), + Params: make(map[string]interface{}), + Dates: &PageDates{}, + PageURLs: &URLPath{}, } var expLastMod, expDate, expPubDate, expExiryDate = zero, zero, zero, zero if dateDate != sentinel { - d.frontmatter[dateKey] = dateDate + d.Frontmatter[dateKey] = dateDate expDate = dateDate } if pubDateDate != sentinel { - d.frontmatter[pubDateKey] = pubDateDate + d.Frontmatter[pubDateKey] = pubDateDate expPubDate = pubDateDate if expDate.IsZero() { expDate = expPubDate @@ -157,7 +157,7 @@ func TestFrontMatterDates(t *testing.T) { } if lastModDate != sentinel { - d.frontmatter[lastModKey] = lastModDate + d.Frontmatter[lastModKey] = lastModDate expLastMod = lastModDate if expDate.IsZero() { @@ -166,7 +166,7 @@ func TestFrontMatterDates(t *testing.T) { } if expiryDateDate != sentinel { - d.frontmatter[expiryDateKey] = expiryDateDate + d.Frontmatter[expiryDateKey] = expiryDateDate expExiryDate = expiryDateDate } @@ -174,7 +174,7 @@ func TestFrontMatterDates(t *testing.T) { expLastMod = expDate } - assert.NoError(handler.handleDates(d)) + assert.NoError(handler.HandleDates(d)) assertFrontMatterDate(assert, d, expDate, "date") assertFrontMatterDate(assert, d, expLastMod, "lastmod") @@ -190,7 +190,7 @@ func TestFrontMatterDates(t *testing.T) { } } -func assertFrontMatterDate(assert *require.Assertions, d frontMatterDescriptor, expected time.Time, dateField string) { +func assertFrontMatterDate(assert *require.Assertions, d FrontMatterDescriptor, expected time.Time, dateField string) { switch dateField { case "date": case "lastmod": @@ -200,14 +200,14 @@ func assertFrontMatterDate(assert *require.Assertions, d frontMatterDescriptor, assert.Failf("Unknown datefield %s", dateField) } - param, found := d.params[dateField] + param, found := d.Params[dateField] if found && param.(time.Time).IsZero() { assert.Fail("Zero time in params", dateField) } message := fmt.Sprintf("[%s] Found: %t Expected: %v (%t) Param: %v Params: %v Front matter: %v", - dateField, found, expected, expected.IsZero(), param, d.params, d.frontmatter) + dateField, found, expected, expected.IsZero(), param, d.Params, d.Frontmatter) assert.True(found != expected.IsZero(), message) @@ -218,16 +218,7 @@ func assertFrontMatterDate(assert *require.Assertions, d frontMatterDescriptor, } } -type dateTestHelper struct { - name string - - dates PageDates -} - -func (d dateTestHelper) descriptor() frontMatterDescriptor { - return frontMatterDescriptor{dates: &d.dates} -} - -func (d dateTestHelper) assert(t *testing.T) { +func TestFrontMatterFieldHandlers(t *testing.T) { + //handlers := &frontmatterFieldHandlers{} } diff --git a/hugolib/pagemeta/pagemeta.go b/hugolib/pagemeta/pagemeta.go new file mode 100644 index 00000000000..57cef9efab8 --- /dev/null +++ b/hugolib/pagemeta/pagemeta.go @@ -0,0 +1,30 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pagemeta + +import "time" + +type URLPath struct { + URL string + Permalink string + Slug string + Section string +} + +type PageDates struct { + Date time.Time + Lastmod time.Time + PublishDate time.Time + ExpiryDate time.Time +} diff --git a/hugolib/site.go b/hugolib/site.go index 2f20a35d9c5..a6704363dcf 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -42,6 +42,7 @@ import ( bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib/pagemeta" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/related" @@ -122,7 +123,7 @@ type Site struct { mediaTypesConfig media.Types // How to handle page front matter. - frontmatterHandler frontmatterHandler + frontmatterHandler pagemeta.FrontmatterHandler // We render each site for all the relevant output formats in serial with // this rendering context pointing to the current one. @@ -252,7 +253,7 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { titleFunc := helpers.GetTitleFunc(cfg.Language.GetString("titleCaseStyle")) - frontMatterHandler, err := newFrontmatterHandler(cfg.Logger, cfg.Cfg) + frontMatterHandler, err := pagemeta.NewFrontmatterHandler(cfg.Logger, cfg.Cfg) if err != nil { return nil, err }