Skip to content

Commit

Permalink
Make the title case style guide configurable
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bep committed Jul 31, 2017
1 parent 9b4170c commit 8fb594b
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 7 deletions.
4 changes: 4 additions & 0 deletions docs/content/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions helpers/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions helpers/general_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGuessType(t *testing.T) {
Expand Down Expand Up @@ -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++ {
Expand Down
1 change: 1 addition & 0 deletions hugolib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down
11 changes: 9 additions & 2 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
}
1 change: 1 addition & 0 deletions tpl/strings/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func init() {
[]string{"title"},
[][2]string{
{`{{title "Bat man"}}`, `Bat Man`},
{`{{title "somewhere over the rainbow"}}`, `Somewhere Over the Rainbow`},
},
)

Expand Down
3 changes: 2 additions & 1 deletion tpl/strings/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions tpl/strings/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tpl/strings/strings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down
12 changes: 12 additions & 0 deletions vendor/vendor.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 8fb594b

Please sign in to comment.