Skip to content

Commit

Permalink
hugolib: Extract date and slug from filename
Browse files Browse the repository at this point in the history
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]
dateFallbacks = ["filename"]
```

Fixes gohugoio#285
Closes gohugoio#3310
Closes gohugoio#3762
  • Loading branch information
bep committed Feb 21, 2018
1 parent 55bd46a commit f2099f0
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 2 deletions.
26 changes: 25 additions & 1 deletion 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.
Expand Down Expand Up @@ -1373,6 +1373,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)
}
Expand Down
70 changes: 70 additions & 0 deletions hugolib/page_frontmatter.go
@@ -0,0 +1,70 @@
// 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"
"strings"

"github.com/gohugoio/hugo/config"
"github.com/spf13/cast"
)

type frontmatterConfig struct {
// Ordered chain.
dateHandlers []frontmatterFieldHandler
}

type frontmatterFieldHandler func(frontmatter map[string]interface{}, p *Page) bool

func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) {
var f frontmatterConfig

handlers := new(frontmatterFieldHandlers)

f.dateHandlers = []frontmatterFieldHandler{handlers.defaultDateHandler}

if cfg.IsSet("frontmatter") {
fm := cfg.GetStringMap("frontmatter")
if fm != nil {
dateFallbacks, found := fm["datefallbacks"]
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 int

func (f *frontmatterFieldHandlers) defaultDateHandler(frontmatter map[string]interface{}, p *Page) bool {
return true
}

func (f *frontmatterFieldHandlers) fileanameFallbackDateHandler(frontmatter map[string]interface{}, p *Page) bool {
return true
}
39 changes: 39 additions & 0 deletions 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{}{
"dateFallbacks": []string{"filename"},
})

assert := require.New(t)

fc, err := newFrontmatterConfig(v)

assert.NoError(err)
assert.Equal(2, len(fc.dateHandlers))

}
58 changes: 57 additions & 1 deletion 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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit f2099f0

Please sign in to comment.