diff options
Diffstat (limited to 'hugolib')
-rw-r--r-- | hugolib/config.go | 11 | ||||
-rw-r--r-- | hugolib/page.go | 129 | ||||
-rw-r--r-- | hugolib/page_paths.go | 2 | ||||
-rw-r--r-- | hugolib/page_test.go | 221 | ||||
-rw-r--r-- | hugolib/pagemeta/page_frontmatter.go | 412 | ||||
-rw-r--r-- | hugolib/pagemeta/page_frontmatter_test.go | 279 | ||||
-rw-r--r-- | hugolib/pagemeta/pagemeta.go | 32 | ||||
-rw-r--r-- | hugolib/site.go | 11 |
8 files changed, 836 insertions, 261 deletions
diff --git a/hugolib/config.go b/hugolib/config.go index fc9aeb5ae..678ebf669 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -249,9 +249,16 @@ func loadDefaultSettingsFor(v *viper.Viper) error { v.SetDefault("debug", false) v.SetDefault("disableFastRender", false) - // Remove in Hugo 0.37 + // Remove in Hugo 0.39 + if v.GetBool("useModTimeAsFallback") { - helpers.Deprecated("Site config", "useModTimeAsFallback", "Try --enableGitInfo or set lastMod in front matter", false) + + helpers.Deprecated("Site config", "useModTimeAsFallback", `Replace with this in your config.toml: + +[frontmatter] +date = [ "date",":fileModTime", ":default"] +lastmod = ["lastmod" ,":fileModTime", ":default"] +`, false) } diff --git a/hugolib/page.go b/hugolib/page.go index fd6278bb4..2274aa84a 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. @@ -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" @@ -140,9 +141,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,11 +221,12 @@ type Page struct { Keywords []string Data map[string]interface{} - Date time.Time - Lastmod time.Time + pagemeta.PageDates Sitemap Sitemap - URLPath + pagemeta.URLPath + frontMatterURL string + permalink string relPermalink string @@ -1115,12 +1114,44 @@ func (p *Page) update(frontmatter map[string]interface{}) error { // Needed for case insensitive fetching of params values helpers.ToLowerMap(frontmatter) - var modified time.Time + var mtime time.Time + if p.Source.FileInfo() != nil { + mtime = p.Source.FileInfo().ModTime() + } + + 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) + if err != nil { + p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.Path(), err) + } - 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.frontmatterHandler.IsDateKey(loki) { + continue + } + switch loki { case "title": p.title = cast.ToString(v) @@ -1139,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) @@ -1150,12 +1181,6 @@ func (p *Page) update(frontmatter map[string]interface{}) error { case "keywords": 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 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. @@ -1163,19 +1188,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 { @@ -1190,34 +1202,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 @@ -1333,32 +1320,6 @@ 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") { @@ -1865,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 ce8a700b1..4d64f4c14 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_test.go b/hugolib/page_test.go index 814556c6c..905793ca6 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. @@ -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" @@ -728,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 := `--- @@ -737,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 { @@ -749,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)) } } @@ -905,186 +904,68 @@ func TestPageWithDate(t *testing.T) { checkPageDate(t, p, d) } -const ( - s = "fs mod timestamp" // signifies filesystem's modification timestamp - P = "1969-01-10T09:17:42Z" - D = "2013-10-15T06:16:13Z" - L = "2017-09-03T22:22:22Z" - M = "2018-01-24T12:21:39Z" - E = "2025-12-31T23:59:59Z" - o = "0001-01-01T00:00:00Z" // zero value of type Time, default for some date fields - x = "" // nil date value, default for some date fields - - p_D____ = `--- -title: Simple -date: '2013-10-15T06:16:13' ---- -Page With Date only` - - p__P___ = `--- -title: Simple -publishdate: '1969-01-10T09:17:42' ---- -Page With PublishDate only` - - p_DP___ = `--- -title: Simple -date: '2013-10-15T06:16:13' -publishdate: '1969-01-10T09:17:42' ---- -Page With Date and PublishDate` - - p__PL__ = `--- -title: Simple -publishdate: '1969-01-10T09:17:42' -lastmod: '2017-09-03T22:22:22' ---- -Page With Date and PublishDate` - - p_DPL__ = `--- -title: Simple -date: '2013-10-15T06:16:13' -publishdate: '1969-01-10T09:17:42' -lastmod: '2017-09-03T22:22:22' ---- -Page With Date, PublishDate and LastMod` +func TestPageWithFrontMatterConfig(t *testing.T) { + t.Parallel() - p_DPL_E = `--- -title: Simple -date: '2013-10-15T06:16:13' -publishdate: '1969-01-10T09:17:42' -lastmod: '2017-09-03T22:22:22' -expirydate: '2025-12-31T23:59:59' ---- -Page With Date, PublishDate and LastMod` + for _, dateHandler := range []string{":filename", ":fileModTime"} { + t.Run(fmt.Sprintf("dateHandler=%q", dateHandler), func(t *testing.T) { + assrt := require.New(t) + cfg, fs := newTestCfg() - p_DP_ME = `--- -title: Simple -date: '2013-10-15T06:16:13' -publishdate: '1969-01-10T09:17:42' -modified: '2018-01-24T12:21:39' -expirydate: '2025-12-31T23:59:59' + pageTemplate := ` --- -Page With Date, PublishDate and LastMod` - - p_DPLME = `--- -title: Simple -date: '2013-10-15T06:16:13' -publishdate: '1969-01-10T09:17:42' -lastmod: '2017-09-03T22:22:22' -modified: '2018-01-24T12:21:39' -expirydate: '2025-12-31T23:59:59' +title: Page +weight: %d +lastMod: 2018-02-28 +%s --- -Page With Date, PublishDate and LastMod` +Content +` - emptyFM = `--- + cfg.Set("frontmatter", map[string]interface{}{ + "date": []string{dateHandler, "date"}, + }) ---- -Page With empty front matter` + c1 := filepath.Join("content", "section", "2012-02-21-noslug.md") + c2 := filepath.Join("content", "section", "2012-02-22-slug.md") - zero_FM = "Page With empty front matter" -) + writeSource(t, fs, c1, fmt.Sprintf(pageTemplate, 1, "")) + writeSource(t, fs, c2, fmt.Sprintf(pageTemplate, 2, "slug: aslug")) -func TestMetadataDates(t *testing.T) { - t.Parallel() - var tests = []struct { - text string - filename string - modFallback bool - expDate string - expPub string - expLast string - expMod string - expExp string - }{ - // The three columns on the left are the test case inputs: - // page content: The name indicates which dates are set in the front matter, - // (D)ate, (P)ublishDate, (L)astModified - // (M)odified, (E)xpiryDate. So, for example, - // p__PL__ is content with PublishDate and LastModified - // specified in the front matter. - // file path: For when we start deriving metadata from it - // modFallback: Whether or not useModTimeAsFallback is enabled. - // - // The single character columns on the right are the expected outputs - // for each metadata date given by the column heading. - // Since each date type (D/P/L/M/E) in the input is always set - // to the same value (the constants referenced in these columns), it - // is easy to visualize and test which input date gets copied to which - // output date fields. "s" signifies the file's filesystem time stamp, - // "x" signifies a nil value, and "o" the "zero date". - // - // ------- 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__P___, "test.md", false, P, P, P, x, x}, // pubdate copied across - {p__P___, "testy.md", true, P, P, P, x, x}, - {p_DP___, "test.md", false, D, P, D, x, x}, // date -> lastMod - {p_DP___, "testy.md", true, D, P, D, x, x}, - {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 - {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?) - {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}, - } + c1fi, err := fs.Source.Stat(c1) + assrt.NoError(err) + c2fi, err := fs.Source.Stat(c2) + assrt.NoError(err) - for i, test := range tests { - s := newTestSite(t) - s.Cfg.Set("useModTimeAsFallback", test.modFallback) - fs := hugofs.NewMem(s.Cfg) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - 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) + assrt.Len(s.RegularPages, 2) - sp := source.NewSourceSpec(s.Cfg, fs) - p := s.newPageFromFile(newFileInfo(sp, "", test.filename, fi, bundleNot)) - p.ReadFrom(file) + noSlug := s.RegularPages[0] + slug := s.RegularPages[1] - // check Page Variables - checkDate(t, i+1, "Date", p.Date, test.expDate, fi) - checkDate(t, i+1, "PubDate", p.PublishDate, test.expPub, fi) - checkDate(t, i+1, "LastMod", p.Lastmod, test.expLast, fi) - checkDate(t, i+1, "LastMod", p.ExpiryDate, test.expExp, fi) + assrt.Equal(28, noSlug.Lastmod.Day()) - // check Page Params - 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) - checkDate(t, i+1, "param expirydate", cast.ToTime(p.params["expirydate"]), test.expExp, fi) - } -} + switch strings.ToLower(dateHandler) { + case ":filename": + assrt.False(noSlug.Date.IsZero()) + assrt.False(slug.Date.IsZero()) + assrt.Equal(2012, noSlug.Date.Year()) + assrt.Equal(2012, slug.Date.Year()) + assrt.Equal("noslug", noSlug.Slug) + assrt.Equal("aslug", slug.Slug) + case ":filemodtime": + assrt.Equal(c1fi.ModTime().Year(), noSlug.Date.Year()) + assrt.Equal(c2fi.ModTime().Year(), slug.Date.Year()) + fallthrough + default: + assrt.Equal("", noSlug.Slug) + assrt.Equal("aslug", slug.Slug) -func checkDate(t *testing.T, testId int, dateType string, given time.Time, expected string, fi os.FileInfo) { - var expectedTime time.Time - if expected == s { - expectedTime = fi.ModTime() - } else if expected != x { - expectedTime = parseTime(expected, t) - } - - if given != expectedTime { - t.Errorf("test %d, %s is: %s. Expected: %s", testId, dateType, given, expectedTime) + } + }) } -} -func parseTime(s string, t *testing.T) time.Time { - time, err := time.Parse(time.RFC3339, s) - if err != nil { - t.Fatalf("bad test data: failed to parse date: '%s'", s) - } - return time } func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { diff --git a/hugolib/pagemeta/page_frontmatter.go b/hugolib/pagemeta/page_frontmatter.go new file mode 100644 index 000000000..5e60a47d0 --- /dev/null +++ b/hugolib/pagemeta/page_frontmatter.go @@ -0,0 +1,412 @@ +// 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 ( + "io/ioutil" + "log" + "os" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + + "github.com/gohugoio/hugo/config" + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" +) + +// FrontMatterHandler maps front matter into Page fields and .Params. +// Note that we currently have only extracted the date logic. +type FrontMatterHandler struct { + fmConfig frontmatterConfig + + dateHandler frontMatterFieldHandler + lastModHandler frontMatterFieldHandler + publishDateHandler frontMatterFieldHandler + expiryDateHandler frontMatterFieldHandler + + // A map of all date keys configured, including any custom. + allDateKeys map[string]bool + + logger *jww.Notepad +} + +// FrontMatterDescriptor describes how to handle front matter for a given Page. +// It has pointers to values in the receiving page which gets updated. +type FrontMatterDescriptor struct { + + // This the Page's front matter. + Frontmatter 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 + + // The below are pointers to values on Page and will be modified. + + // This is the Page's params. + Params map[string]interface{} + + // This is the Page's dates. + Dates *PageDates + + // This is the Page's Slug etc. + PageURLs *URLPath +} + +var ( + dateFieldAliases = map[string][]string{ + fmDate: []string{}, + fmLastmod: []string{"modified"}, + fmPubDate: []string{"pubdate", "published"}, + fmExpiryDate: []string{"unpublishdate"}, + } +) + +// HandleDates updates all the dates given the current configuration and the +// supplied front matter params. Note that this requires all lower-case keys +// in the params map. +func (f FrontMatterHandler) HandleDates(d *FrontMatterDescriptor) error { + if d.Dates == nil { + panic("missing dates") + } + + if f.dateHandler == nil { + panic("missing date handler") + } + + if _, err := f.dateHandler(d); err != nil { + return err + } + + if _, err := f.lastModHandler(d); err != nil { + return err + } + + if _, err := f.publishDateHandler(d); err != nil { + return err + } + + if _, err := f.expiryDateHandler(d); err != nil { + return err + } + + return nil +} + +// IsDateKey returns whether the given front matter key is considered a date by the current +// configuration. +func (f FrontMatterHandler) IsDateKey(key string) bool { + return f.allDateKeys[key] +} + +// 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 +} + +type frontMatterFieldHandler 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) + if err != nil { + f.logger.ERROR.Println(err) + } else if success { + return true, nil + } + } + return false, nil + } +} + +type frontmatterConfig struct { + date []string + lastmod []string + publishDate []string + expiryDate []string +} + +const ( + // These are all the date handler identifiers + // All identifiers not starting with a ":" maps to a front matter parameter. + fmDate = "date" + fmPubDate = "publishdate" + fmLastmod = "lastmod" + fmExpiryDate = "expirydate" + + // Gets date from filename, e.g 218-02-22-mypage.md + fmFilename = ":filename" + + // Gets date from file OS mod time. + fmModTime = ":filemodtime" +) + +// This is the config you get when doing nothing. +func newDefaultFrontmatterConfig() frontmatterConfig { + return frontmatterConfig{ + date: []string{fmDate, fmPubDate, fmLastmod}, + lastmod: []string{fmLastmod, fmDate, fmPubDate}, + publishDate: []string{fmPubDate, fmDate}, + expiryDate: []string{fmExpiryDate}, + } +} + +func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) { + c := newDefaultFrontmatterConfig() + defaultConfig := c + + 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) + } + } + } + } + + expander := func(c, d []string) []string { + out := expandDefaultValues(c, d) + out = addDateFieldAliases(out) + return out + } + + c.date = expander(c.date, defaultConfig.date) + c.publishDate = expander(c.publishDate, defaultConfig.publishDate) + c.lastmod = expander(c.lastmod, defaultConfig.lastmod) + c.expiryDate = expander(c.expiryDate, defaultConfig.expiryDate) + + return c, nil +} + +func addDateFieldAliases(values []string) []string { + var complete []string + + for _, v := range values { + complete = append(complete, v) + if aliases, found := dateFieldAliases[v]; found { + complete = append(complete, aliases...) + } + } + return helpers.UniqueStrings(complete) +} + +func expandDefaultValues(values []string, defaults []string) []string { + var out []string + for _, v := range values { + if v == ":default" { + out = append(out, defaults...) + } else { + out = append(out, v) + } + } + return out +} + +func toLowerSlice(in interface{}) []string { + out := cast.ToStringSlice(in) + for i := 0; i < len(out); i++ { + out[i] = strings.ToLower(out[i]) + } + + return out +} + +// NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration. +// If no logger is provided, one will be created. +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) + } + + frontMatterConfig, err := newFrontmatterConfig(cfg) + if err != nil { + return FrontMatterHandler{}, err + } + + allDateKeys := make(map[string]bool) + addKeys := func(vals []string) { + for _, k := range vals { + if !strings.HasPrefix(k, ":") { + allDateKeys[k] = true + } + } + } + + addKeys(frontMatterConfig.date) + addKeys(frontMatterConfig.expiryDate) + addKeys(frontMatterConfig.lastmod) + addKeys(frontMatterConfig.publishDate) + + f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys} + + if err := f.createHandlers(); err != nil { + return f, err + } + + return f, nil +} + +func (f *FrontMatterHandler) createHandlers() error { + var err error + + if f.dateHandler, err = f.createDateHandler(f.fmConfig.date, + func(d *FrontMatterDescriptor, t time.Time) { + d.Dates.Date = t + setParamIfNotSet(fmDate, t, d) + }); err != nil { + return err + } + + if f.lastModHandler, err = f.createDateHandler(f.fmConfig.lastmod, + func(d *FrontMatterDescriptor, t time.Time) { + setParamIfNotSet(fmLastmod, t, d) + d.Dates.Lastmod = t + }); err != nil { + return err + } + + if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.publishDate, + func(d *FrontMatterDescriptor, t time.Time) { + setParamIfNotSet(fmPubDate, t, d) + d.Dates.PublishDate = t + }); err != nil { + return err + } + + if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.expiryDate, + func(d *FrontMatterDescriptor, t time.Time) { + setParamIfNotSet(fmExpiryDate, t, d) + d.Dates.ExpiryDate = t + }); err != nil { + return err + } + + return nil +} + +func setParamIfNotSet(key string, value interface{}, d *FrontMatterDescriptor) { + if _, found := d.Params[key]; found { + return + } + d.Params[key] = value +} + +func (f FrontMatterHandler) createDateHandler(identifiers []string, setter func(d *FrontMatterDescriptor, t time.Time)) (frontMatterFieldHandler, error) { + var h *frontmatterFieldHandlers + var handlers []frontMatterFieldHandler + + for _, identifier := range identifiers { + switch identifier { + case fmFilename: + handlers = append(handlers, h.newDateFilenameHandler(setter)) + case fmModTime: + handlers = append(handlers, h.newDateModTimeHandler(setter)) + default: + handlers = append(handlers, h.newDateFieldHandler(identifier, setter)) + } + } |