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: ""
title: ""
# 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
# verbose output
verbose: false

View file

@ -26,6 +26,8 @@ import (
"unicode"
"unicode/utf8"
"github.com/jdkato/prose/transform"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
@ -194,6 +196,29 @@ func ReaderContains(r io.Reader, subslice []byte) bool {
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.
func HasStringsPrefix(s, prefix []string) bool {
return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix)

View file

@ -19,6 +19,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGuessType(t *testing.T) {
@ -173,6 +174,20 @@ func TestReaderContains(t *testing.T) {
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) {
b.ResetTimer()
for i := 0; i < b.N; i++ {

View file

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

View file

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

View file

@ -116,6 +116,7 @@ func init() {
[]string{"title"},
[][2]string{
{`{{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/tpl/internal"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
@ -26,7 +27,7 @@ func TestInit(t *testing.T) {
var ns *internal.TemplateFuncsNamespace
for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns = nsf(&deps.Deps{})
ns = nsf(&deps.Deps{Cfg: viper.New()})
if ns.Name == name {
found = true
break

View file

@ -27,14 +27,17 @@ import (
// New returns a new instance of the strings-namespaced template functions.
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.
// Most functions mimic the Go stdlib, but the order of the parameters may be
// different to ease their use in the Go template system.
type Namespace struct {
deps *deps.Deps
titleFunc func(s string) string
deps *deps.Deps
}
// CountRunes returns the number of runes in s, excluding whitepace.
@ -303,7 +306,7 @@ func (ns *Namespace) Title(s interface{}) (string, error) {
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

View file

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

12
vendor/vendor.json vendored
View file

@ -159,6 +159,18 @@
"revision": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75",
"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=",
"path": "github.com/kardianos/osext",