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
This commit is contained in:
Vas Sudanagunta 2018-01-25 22:54:15 -05:00 committed by Bjørn Erik Pedersen
parent 3f0379adb7
commit 91bb774ae4
8 changed files with 56 additions and 42 deletions

View file

@ -1231,9 +1231,7 @@ func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinV
return return
} }
config := tomlMeta.(map[string]interface{}) if minVersion, ok := tomlMeta["min_version"]; ok {
if minVersion, ok := config["min_version"]; ok {
return helpers.CompareVersion(minVersion) > 0, fmt.Sprint(minVersion) return helpers.CompareVersion(minVersion) > 0, fmt.Sprint(minVersion)
} }

View file

@ -255,7 +255,7 @@ func loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} {
return nil return nil
} }
return c.(map[string]interface{}) return c
} }
func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) { func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) {

View file

@ -99,7 +99,7 @@ func undraftContent(p parser.Page) (bytes.Buffer, error) {
var isDraft, gotDate bool var isDraft, gotDate bool
var date string var date string
L: L:
for k, v := range meta.(map[string]interface{}) { for k, v := range meta {
switch k { switch k {
case "draft": case "draft":
if !v.(bool) { if !v.(bool) {

View file

@ -69,7 +69,7 @@ func TestUndraftContent(t *testing.T) {
t.Errorf("[%d] unexpected error %q", i, err) t.Errorf("[%d] unexpected error %q", i, err)
continue continue
} }
for k, v := range meta.(map[string]interface{}) { for k, v := range meta {
if k == "draft" { if k == "draft" {
if v.(bool) { if v.(bool) {
t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k) t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k)

View file

@ -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") var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter")
func (p *Page) update(f interface{}) error { func (p *Page) update(frontmatter map[string]interface{}) error {
if f == nil { if frontmatter == nil {
return errors.New("no metadata found") return errors.New("missing frontmatter data")
} }
m := f.(map[string]interface{})
// Needed for case insensitive fetching of params values // Needed for case insensitive fetching of params values
helpers.ToLowerMap(m) helpers.ToLowerMap(frontmatter)
var modified time.Time var modified time.Time
var err error var err error
var draft, published, isCJKLanguage *bool var draft, published, isCJKLanguage *bool
for k, v := range m { for k, v := range frontmatter {
loki := strings.ToLower(k) loki := strings.ToLower(k)
switch loki { switch loki {
case "title": case "title":
@ -1371,7 +1370,6 @@ func (p *Page) update(f interface{}) error {
p.params["iscjklanguage"] = p.isCJKLanguage p.params["iscjklanguage"] = p.isCJKLanguage
return nil return nil
} }
func (p *Page) GetParam(key string) interface{} { func (p *Page) GetParam(key string) interface{} {
@ -1614,14 +1612,12 @@ func (p *Page) parse(reader io.Reader) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err)
} }
if meta == nil {
if meta != nil { // missing frontmatter equivalent to empty frontmatter
if err = p.update(meta); err != nil { meta = map[string]interface{}{}
return err
}
} }
return nil return p.update(meta)
} }
func (p *Page) RawContent() string { func (p *Page) RawContent() string {

View file

@ -912,8 +912,8 @@ const (
L = "2017-09-03T22:22:22Z" L = "2017-09-03T22:22:22Z"
M = "2018-01-24T12:21:39Z" M = "2018-01-24T12:21:39Z"
E = "2025-12-31T23:59:59Z" E = "2025-12-31T23:59:59Z"
o = "0001-01-01T00:00:00Z" o = "0001-01-01T00:00:00Z" // zero value of type Time, default for some date fields
x = "" x = "" // nil date value, default for some date fields
p_D____ = `--- p_D____ = `---
title: Simple title: Simple
@ -981,20 +981,41 @@ Page With Date, PublishDate and LastMod`
--- ---
Page With empty front matter` Page With empty front matter`
zero_FM = "Page With empty front matter"
) )
func TestMetadataDates(t *testing.T) { func TestMetadataDates(t *testing.T) {
t.Parallel() t.Parallel()
var tests = []struct { var tests = []struct {
text string text string
filename string filename string
fallback bool modFallback bool
expDate string expDate string
expPub string expPub string
expLast string expLast string
expMod string expMod string
expExp string expExp string
}{ // D P L M E }{
// 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____, "test.md", false, D, D, D, x, x}, // date copied across
{p_D____, "testy.md", true, D, D, D, x, x}, {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___, "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 {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 {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 {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 { for i, test := range tests {
s := newTestSite(t) s := newTestSite(t)
s.Cfg.Set("useModTimeAsFallback", test.fallback) s.Cfg.Set("useModTimeAsFallback", test.modFallback)
fs := hugofs.NewMem(s.Cfg) fs := hugofs.NewMem(s.Cfg)
writeToFs(t, fs.Source, test.filename, test.text) writeToFs(t, fs.Source, test.filename, test.text)

View file

@ -31,7 +31,7 @@ import (
// FrontmatterType represents a type of frontmatter. // FrontmatterType represents a type of frontmatter.
type FrontmatterType struct { type FrontmatterType struct {
// Parse decodes content into a Go interface. // 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 markstart, markend []byte // starting and ending delimiters
includeMark bool // include start and end mark in output 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 // HandleTOMLMetaData unmarshals TOML-encoded datum and returns a Go interface
// representing the encoded data structure. // representing the encoded data structure.
func HandleTOMLMetaData(datum []byte) (interface{}, error) { func HandleTOMLMetaData(datum []byte) (map[string]interface{}, error) {
m := map[string]interface{}{} m := map[string]interface{}{}
datum = removeTOMLIdentifier(datum) datum = removeTOMLIdentifier(datum)
@ -198,7 +198,7 @@ func removeTOMLIdentifier(datum []byte) []byte {
// HandleYAMLMetaData unmarshals YAML-encoded datum and returns a Go interface // HandleYAMLMetaData unmarshals YAML-encoded datum and returns a Go interface
// representing the encoded data structure. // representing the encoded data structure.
func HandleYAMLMetaData(datum []byte) (interface{}, error) { func HandleYAMLMetaData(datum []byte) (map[string]interface{}, error) {
m := map[string]interface{}{} m := map[string]interface{}{}
err := yaml.Unmarshal(datum, &m) err := yaml.Unmarshal(datum, &m)
return m, err return m, err
@ -206,7 +206,7 @@ func HandleYAMLMetaData(datum []byte) (interface{}, error) {
// HandleJSONMetaData unmarshals JSON-encoded datum and returns a Go interface // HandleJSONMetaData unmarshals JSON-encoded datum and returns a Go interface
// representing the encoded data structure. // representing the encoded data structure.
func HandleJSONMetaData(datum []byte) (interface{}, error) { func HandleJSONMetaData(datum []byte) (map[string]interface{}, error) {
if datum == nil { if datum == nil {
// Package json returns on error on nil input. // Package json returns on error on nil input.
// Return an empty map to be consistent with our other supported // 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 return make(map[string]interface{}), nil
} }
var f interface{} var f map[string]interface{}
err := json.Unmarshal(datum, &f) err := json.Unmarshal(datum, &f)
return f, err return f, err
} }
// HandleOrgMetaData unmarshals org-mode encoded datum and returns a Go // HandleOrgMetaData unmarshals org-mode encoded datum and returns a Go
// interface representing the encoded data structure. // interface representing the encoded data structure.
func HandleOrgMetaData(datum []byte) (interface{}, error) { func HandleOrgMetaData(datum []byte) (map[string]interface{}, error) {
return goorgeous.OrgHeaders(datum) return goorgeous.OrgHeaders(datum)
} }

View file

@ -74,7 +74,7 @@ type Page interface {
IsRenderable() bool IsRenderable() bool
// Metadata returns the unmarshalled frontmatter data. // Metadata returns the unmarshalled frontmatter data.
Metadata() (interface{}, error) Metadata() (map[string]interface{}, error)
} }
// page implements the Page interface. // page implements the Page interface.
@ -100,16 +100,13 @@ func (p *page) IsRenderable() bool {
} }
// Metadata returns the unmarshalled frontmatter data. // 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() frontmatter := p.FrontMatter()
if len(frontmatter) != 0 { if len(frontmatter) != 0 {
fm := DetectFrontMatter(rune(frontmatter[0])) fm := DetectFrontMatter(rune(frontmatter[0]))
if fm != nil { if fm != nil {
meta, err = fm.Parse(frontmatter) meta, err = fm.Parse(frontmatter)
if err != nil {
return
}
} }
} }
return return