From 91bb774ae4e129f7ed0624754b31479c960ef774 Mon Sep 17 00:00:00 2001 From: Vas Sudanagunta Date: Thu, 25 Jan 2018 22:54:15 -0500 Subject: [PATCH] Support pages without front matter * Page without front matter now treated same as a page with empty front matter. * Test cases added to cover this and repro issue #4320. * Type safety of front matter code improved. Fixes #4320 --- commands/hugo.go | 4 +--- commands/import_jekyll.go | 2 +- commands/undraft.go | 2 +- commands/undraft_test.go | 2 +- hugolib/page.go | 22 ++++++++---------- hugolib/page_test.go | 47 +++++++++++++++++++++++++++++---------- parser/frontmatter.go | 12 +++++----- parser/page.go | 7 ++---- 8 files changed, 56 insertions(+), 42 deletions(-) diff --git a/commands/hugo.go b/commands/hugo.go index 758106faf29..c9f07348341 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -1231,9 +1231,7 @@ func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinV return } - config := tomlMeta.(map[string]interface{}) - - if minVersion, ok := config["min_version"]; ok { + if minVersion, ok := tomlMeta["min_version"]; ok { return helpers.CompareVersion(minVersion) > 0, fmt.Sprint(minVersion) } diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go index 98094dbb7be..327bf6095b9 100644 --- a/commands/import_jekyll.go +++ b/commands/import_jekyll.go @@ -255,7 +255,7 @@ func loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} { return nil } - return c.(map[string]interface{}) + return c } func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) { diff --git a/commands/undraft.go b/commands/undraft.go index fbd2d4c3aec..53861f45606 100644 --- a/commands/undraft.go +++ b/commands/undraft.go @@ -99,7 +99,7 @@ func undraftContent(p parser.Page) (bytes.Buffer, error) { var isDraft, gotDate bool var date string L: - for k, v := range meta.(map[string]interface{}) { + for k, v := range meta { switch k { case "draft": if !v.(bool) { diff --git a/commands/undraft_test.go b/commands/undraft_test.go index 259e3479bcc..889f3656737 100644 --- a/commands/undraft_test.go +++ b/commands/undraft_test.go @@ -69,7 +69,7 @@ func TestUndraftContent(t *testing.T) { t.Errorf("[%d] unexpected error %q", i, err) continue } - for k, v := range meta.(map[string]interface{}) { + for k, v := range meta { if k == "draft" { if v.(bool) { t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k) diff --git a/hugolib/page.go b/hugolib/page.go index fe998219c3c..468353ebc61 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1108,19 +1108,18 @@ func (p *Page) prepareForRender(cfg *BuildCfg) error { var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter") -func (p *Page) update(f interface{}) error { - if f == nil { - return errors.New("no metadata found") +func (p *Page) update(frontmatter map[string]interface{}) error { + if frontmatter == nil { + return errors.New("missing frontmatter data") } - m := f.(map[string]interface{}) // Needed for case insensitive fetching of params values - helpers.ToLowerMap(m) + helpers.ToLowerMap(frontmatter) var modified time.Time var err error var draft, published, isCJKLanguage *bool - for k, v := range m { + for k, v := range frontmatter { loki := strings.ToLower(k) switch loki { case "title": @@ -1371,7 +1370,6 @@ func (p *Page) update(f interface{}) error { p.params["iscjklanguage"] = p.isCJKLanguage return nil - } func (p *Page) GetParam(key string) interface{} { @@ -1614,14 +1612,12 @@ func (p *Page) parse(reader io.Reader) error { if err != nil { return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) } - - if meta != nil { - if err = p.update(meta); err != nil { - return err - } + if meta == nil { + // missing frontmatter equivalent to empty frontmatter + meta = map[string]interface{}{} } - return nil + return p.update(meta) } func (p *Page) RawContent() string { diff --git a/hugolib/page_test.go b/hugolib/page_test.go index cfaf13406de..03305149849 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -912,8 +912,8 @@ const ( L = "2017-09-03T22:22:22Z" M = "2018-01-24T12:21:39Z" E = "2025-12-31T23:59:59Z" - o = "0001-01-01T00:00:00Z" - x = "" + 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 @@ -981,20 +981,41 @@ Page With Date, PublishDate and LastMod` --- Page With empty front matter` + + zero_FM = "Page With empty front matter" ) func TestMetadataDates(t *testing.T) { t.Parallel() var tests = []struct { - text string - filename string - fallback bool - expDate string - expPub string - expLast string - expMod string - expExp string - }{ // D P L M E + 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 @@ -1010,12 +1031,14 @@ 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 + {zero_FM, "testy.md", true, s, o, s, x, x}, // Issue #4320 } for i, test := range tests { s := newTestSite(t) - s.Cfg.Set("useModTimeAsFallback", test.fallback) + s.Cfg.Set("useModTimeAsFallback", test.modFallback) fs := hugofs.NewMem(s.Cfg) writeToFs(t, fs.Source, test.filename, test.text) diff --git a/parser/frontmatter.go b/parser/frontmatter.go index ab56b14d14f..7560a734af6 100644 --- a/parser/frontmatter.go +++ b/parser/frontmatter.go @@ -31,7 +31,7 @@ import ( // FrontmatterType represents a type of frontmatter. type FrontmatterType struct { // Parse decodes content into a Go interface. - Parse func([]byte) (interface{}, error) + Parse func([]byte) (map[string]interface{}, error) markstart, markend []byte // starting and ending delimiters includeMark bool // include start and end mark in output @@ -168,7 +168,7 @@ func DetectFrontMatter(mark rune) (f *FrontmatterType) { // HandleTOMLMetaData unmarshals TOML-encoded datum and returns a Go interface // representing the encoded data structure. -func HandleTOMLMetaData(datum []byte) (interface{}, error) { +func HandleTOMLMetaData(datum []byte) (map[string]interface{}, error) { m := map[string]interface{}{} datum = removeTOMLIdentifier(datum) @@ -198,7 +198,7 @@ func removeTOMLIdentifier(datum []byte) []byte { // HandleYAMLMetaData unmarshals YAML-encoded datum and returns a Go interface // representing the encoded data structure. -func HandleYAMLMetaData(datum []byte) (interface{}, error) { +func HandleYAMLMetaData(datum []byte) (map[string]interface{}, error) { m := map[string]interface{}{} err := yaml.Unmarshal(datum, &m) return m, err @@ -206,7 +206,7 @@ func HandleYAMLMetaData(datum []byte) (interface{}, error) { // HandleJSONMetaData unmarshals JSON-encoded datum and returns a Go interface // representing the encoded data structure. -func HandleJSONMetaData(datum []byte) (interface{}, error) { +func HandleJSONMetaData(datum []byte) (map[string]interface{}, error) { if datum == nil { // Package json returns on error on nil input. // Return an empty map to be consistent with our other supported @@ -214,13 +214,13 @@ func HandleJSONMetaData(datum []byte) (interface{}, error) { return make(map[string]interface{}), nil } - var f interface{} + var f map[string]interface{} err := json.Unmarshal(datum, &f) return f, err } // HandleOrgMetaData unmarshals org-mode encoded datum and returns a Go // interface representing the encoded data structure. -func HandleOrgMetaData(datum []byte) (interface{}, error) { +func HandleOrgMetaData(datum []byte) (map[string]interface{}, error) { return goorgeous.OrgHeaders(datum) } diff --git a/parser/page.go b/parser/page.go index 1537915f492..17378840dc8 100644 --- a/parser/page.go +++ b/parser/page.go @@ -74,7 +74,7 @@ type Page interface { IsRenderable() bool // Metadata returns the unmarshalled frontmatter data. - Metadata() (interface{}, error) + Metadata() (map[string]interface{}, error) } // page implements the Page interface. @@ -100,16 +100,13 @@ func (p *page) IsRenderable() bool { } // Metadata returns the unmarshalled frontmatter data. -func (p *page) Metadata() (meta interface{}, err error) { +func (p *page) Metadata() (meta map[string]interface{}, err error) { frontmatter := p.FrontMatter() if len(frontmatter) != 0 { fm := DetectFrontMatter(rune(frontmatter[0])) if fm != nil { meta, err = fm.Parse(frontmatter) - if err != nil { - return - } } } return