Make the title case style guide configurable

This works for the `title` func and the other places where Hugo makes title case.

* AP style (new default)
* Chicago style
* Go style (what we have today)

Fixes #989
This commit is contained in:
Bjørn Erik Pedersen 2017-07-30 17:46:04 +02:00
parent 9b4170ce76
commit 8fb594bfb0
10 changed files with 77 additions and 7 deletions

View file

@ -156,6 +156,10 @@ themesDir: "themes"
theme: "" theme: ""
title: "" title: ""
# if true, use /filename.html instead of /filename/ # if true, use /filename.html instead of /filename/
# Title Case style guide for the title func and other automatic title casing in Hugo.
// Valid values are "AP" (default), "Chicago" and "Go" (which was what you had in Hugo <= 0.25.1).
// See https://www.apstylebook.com/ and http://www.chicagomanualofstyle.org/home.html
titleCaseStyle: "AP"
uglyURLs: false uglyURLs: false
# verbose output # verbose output
verbose: false verbose: false

View file

@ -26,6 +26,8 @@ import (
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"github.com/jdkato/prose/transform"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
"github.com/spf13/cast" "github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman" jww "github.com/spf13/jwalterweatherman"
@ -194,6 +196,29 @@ func ReaderContains(r io.Reader, subslice []byte) bool {
return false return false
} }
// GetTitleFunc returns a func that can be used to transform a string to
// title case.
//
// The supported styles are
//
// - "Go" (strings.Title)
// - "AP" (see https://www.apstylebook.com/)
// - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
//
// If an unknown or empty style is provided, AP style is what you get.
func GetTitleFunc(style string) func(s string) string {
switch strings.ToLower(style) {
case "go":
return strings.Title
case "chicago":
tc := transform.NewTitleConverter(transform.ChicagoStyle)
return tc.Title
default:
tc := transform.NewTitleConverter(transform.APStyle)
return tc.Title
}
}
// HasStringsPrefix tests whether the string slice s begins with prefix slice s. // HasStringsPrefix tests whether the string slice s begins with prefix slice s.
func HasStringsPrefix(s, prefix []string) bool { func HasStringsPrefix(s, prefix []string) bool {
return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix) return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix)

View file

@ -19,6 +19,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGuessType(t *testing.T) { func TestGuessType(t *testing.T) {
@ -173,6 +174,20 @@ func TestReaderContains(t *testing.T) {
assert.False(t, ReaderContains(nil, nil)) assert.False(t, ReaderContains(nil, nil))
} }
func TestGetTitleFunc(t *testing.T) {
title := "somewhere over the rainbow"
assert := require.New(t)
assert.Equal("Somewhere Over The Rainbow", GetTitleFunc("go")(title))
assert.Equal("Somewhere over the Rainbow", GetTitleFunc("chicago")(title), "Chicago style")
assert.Equal("Somewhere over the Rainbow", GetTitleFunc("Chicago")(title), "Chicago style")
assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("ap")(title), "AP style")
assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("ap")(title), "AP style")
assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("")(title), "AP style")
assert.Equal("Somewhere Over the Rainbow", GetTitleFunc("unknown")(title), "AP style")
}
func BenchmarkReaderContains(b *testing.B) { func BenchmarkReaderContains(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {

View file

@ -101,6 +101,7 @@ func loadDefaultSettingsFor(v *viper.Viper) {
v.SetDefault("canonifyURLs", false) v.SetDefault("canonifyURLs", false)
v.SetDefault("relativeURLs", false) v.SetDefault("relativeURLs", false)
v.SetDefault("removePathAccents", false) v.SetDefault("removePathAccents", false)
v.SetDefault("titleCaseStyle", "AP")
v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"}) v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"})
v.SetDefault("permalinks", make(PermalinkOverrides, 0)) v.SetDefault("permalinks", make(PermalinkOverrides, 0))
v.SetDefault("sitemap", Sitemap{Priority: -1, Filename: "sitemap.xml"}) v.SetDefault("sitemap", Sitemap{Priority: -1, Filename: "sitemap.xml"})

View file

@ -132,6 +132,9 @@ type Site struct {
// Logger etc. // Logger etc.
*deps.Deps `json:"-"` *deps.Deps `json:"-"`
// The func used to title case titles.
titleFunc func(s string) string
siteStats *siteStats siteStats *siteStats
} }
@ -172,6 +175,7 @@ func (s *Site) reset() *Site {
return &Site{Deps: s.Deps, return &Site{Deps: s.Deps,
layoutHandler: output.NewLayoutHandler(s.PathSpec.ThemeSet()), layoutHandler: output.NewLayoutHandler(s.PathSpec.ThemeSet()),
disabledKinds: s.disabledKinds, disabledKinds: s.disabledKinds,
titleFunc: s.titleFunc,
outputFormats: s.outputFormats, outputFormats: s.outputFormats,
outputFormatsConfig: s.outputFormatsConfig, outputFormatsConfig: s.outputFormatsConfig,
mediaTypesConfig: s.mediaTypesConfig, mediaTypesConfig: s.mediaTypesConfig,
@ -227,11 +231,14 @@ func newSite(cfg deps.DepsCfg) (*Site, error) {
return nil, err return nil, err
} }
titleFunc := helpers.GetTitleFunc(cfg.Language.GetString("titleCaseStyle"))
s := &Site{ s := &Site{
PageCollections: c, PageCollections: c,
layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""), layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""),
Language: cfg.Language, Language: cfg.Language,
disabledKinds: disabledKinds, disabledKinds: disabledKinds,
titleFunc: titleFunc,
outputFormats: outputFormats, outputFormats: outputFormats,
outputFormatsConfig: siteOutputFormatsConfig, outputFormatsConfig: siteOutputFormatsConfig,
mediaTypesConfig: siteMediaTypesConfig, mediaTypesConfig: siteMediaTypesConfig,
@ -2121,7 +2128,7 @@ func (s *Site) newTaxonomyPage(plural, key string) *Page {
p.Title = helpers.FirstUpper(key) p.Title = helpers.FirstUpper(key)
key = s.PathSpec.MakePathSanitized(key) key = s.PathSpec.MakePathSanitized(key)
} else { } else {
p.Title = strings.Replace(strings.Title(key), "-", " ", -1) p.Title = strings.Replace(s.titleFunc(key), "-", " ", -1)
} }
return p return p
@ -2141,6 +2148,6 @@ func (s *Site) newSectionPage(name string) *Page {
func (s *Site) newTaxonomyTermsPage(plural string) *Page { func (s *Site) newTaxonomyTermsPage(plural string) *Page {
p := s.newNodePage(KindTaxonomyTerm, plural) p := s.newNodePage(KindTaxonomyTerm, plural)
p.Title = strings.Title(plural) p.Title = s.titleFunc(plural)
return p return p
} }

View file

@ -116,6 +116,7 @@ func init() {
[]string{"title"}, []string{"title"},
[][2]string{ [][2]string{
{`{{title "Bat man"}}`, `Bat Man`}, {`{{title "Bat man"}}`, `Bat Man`},
{`{{title "somewhere over the rainbow"}}`, `Somewhere Over the Rainbow`},
}, },
) )

View file

@ -18,6 +18,7 @@ import (
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/internal"
"github.com/spf13/viper"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -26,7 +27,7 @@ func TestInit(t *testing.T) {
var ns *internal.TemplateFuncsNamespace var ns *internal.TemplateFuncsNamespace
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns = nsf(&deps.Deps{}) ns = nsf(&deps.Deps{Cfg: viper.New()})
if ns.Name == name { if ns.Name == name {
found = true found = true
break break

View file

@ -27,13 +27,16 @@ import (
// New returns a new instance of the strings-namespaced template functions. // New returns a new instance of the strings-namespaced template functions.
func New(d *deps.Deps) *Namespace { func New(d *deps.Deps) *Namespace {
return &Namespace{deps: d} titleCaseStyle := d.Cfg.GetString("titleCaseStyle")
titleFunc := helpers.GetTitleFunc(titleCaseStyle)
return &Namespace{deps: d, titleFunc: titleFunc}
} }
// Namespace provides template functions for the "strings" namespace. // Namespace provides template functions for the "strings" namespace.
// Most functions mimic the Go stdlib, but the order of the parameters may be // Most functions mimic the Go stdlib, but the order of the parameters may be
// different to ease their use in the Go template system. // different to ease their use in the Go template system.
type Namespace struct { type Namespace struct {
titleFunc func(s string) string
deps *deps.Deps deps *deps.Deps
} }
@ -303,7 +306,7 @@ func (ns *Namespace) Title(s interface{}) (string, error) {
return "", err return "", err
} }
return _strings.Title(ss), nil return ns.titleFunc(ss), nil
} }
// ToLower returns a copy of the input s with all Unicode letters mapped to their // ToLower returns a copy of the input s with all Unicode letters mapped to their

View file

@ -19,11 +19,12 @@ import (
"testing" "testing"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var ns = New(&deps.Deps{}) var ns = New(&deps.Deps{Cfg: viper.New()})
type tstNoStringer struct{} type tstNoStringer struct{}

12
vendor/vendor.json vendored
View file

@ -159,6 +159,18 @@
"revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75", "revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75",
"revisionTime": "2014-10-17T20:07:13Z" "revisionTime": "2014-10-17T20:07:13Z"
}, },
{
"checksumSHA1": "ywE9KA40kVq0qKcAIqLgpoA0su4=",
"path": "github.com/jdkato/prose/internal/util",
"revision": "c24611cae00c16858e611ef77226dd2f7502759f",
"revisionTime": "2017-07-29T20:17:14Z"
},
{
"checksumSHA1": "SpQ8EpkRvM9fAxEXQAy7Qy/L0Ig=",
"path": "github.com/jdkato/prose/transform",
"revision": "c24611cae00c16858e611ef77226dd2f7502759f",
"revisionTime": "2017-07-29T20:17:14Z"
},
{ {
"checksumSHA1": "gEjGS03N1eysvpQ+FCHTxPcbxXc=", "checksumSHA1": "gEjGS03N1eysvpQ+FCHTxPcbxXc=",
"path": "github.com/kardianos/osext", "path": "github.com/kardianos/osext",