diff --git a/docs/content/getting-started/configuration.md b/docs/content/getting-started/configuration.md index 5aa5160e091..55e2f1cdce1 100644 --- a/docs/content/getting-started/configuration.md +++ b/docs/content/getting-started/configuration.md @@ -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 diff --git a/helpers/general.go b/helpers/general.go index 552e4d0bf83..a064309d344 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -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) diff --git a/helpers/general_test.go b/helpers/general_test.go index 4d82bc0cf27..561f59522dd 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -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++ { diff --git a/hugolib/config.go b/hugolib/config.go index e70d07756b1..8f36253313d 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -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"}) diff --git a/hugolib/site.go b/hugolib/site.go index 47c2af4538a..62988185633 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -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 } diff --git a/tpl/strings/init.go b/tpl/strings/init.go index 45d694b9744..4f240415aef 100644 --- a/tpl/strings/init.go +++ b/tpl/strings/init.go @@ -116,6 +116,7 @@ func init() { []string{"title"}, [][2]string{ {`{{title "Bat man"}}`, `Bat Man`}, + {`{{title "somewhere over the rainbow"}}`, `Somewhere Over the Rainbow`}, }, ) diff --git a/tpl/strings/init_test.go b/tpl/strings/init_test.go index a8ad8ffdf39..904e486f78a 100644 --- a/tpl/strings/init_test.go +++ b/tpl/strings/init_test.go @@ -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 diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go index ec95be730e8..5fe92043388 100644 --- a/tpl/strings/strings.go +++ b/tpl/strings/strings.go @@ -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 diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go index ee10ac7594d..64ec0864f04 100644 --- a/tpl/strings/strings_test.go +++ b/tpl/strings/strings_test.go @@ -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{} diff --git a/vendor/vendor.json b/vendor/vendor.json index 936f3d62094..8533e32d22c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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",