shithub: hugo

Download patch

ref: 8fb594bfb090c017d4e5cbb2905780221e202c41
parent: 9b4170ce768717adfbe9d97c46e38ceaec2ce994
author: Bjørn Erik Pedersen <[email protected]>
date: Sun Jul 30 13:46:04 EDT 2017

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

--- a/docs/content/getting-started/configuration.md
+++ b/docs/content/getting-started/configuration.md
@@ -156,6 +156,10 @@
 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
--- a/helpers/general.go
+++ b/helpers/general.go
@@ -26,6 +26,8 @@
 	"unicode"
 	"unicode/utf8"
 
+	"github.com/jdkato/prose/transform"
+
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/spf13/cast"
 	jww "github.com/spf13/jwalterweatherman"
@@ -192,6 +194,29 @@
 		}
 	}
 	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.
--- a/helpers/general_test.go
+++ b/helpers/general_test.go
@@ -19,6 +19,7 @@
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestGuessType(t *testing.T) {
@@ -171,6 +172,20 @@
 
 	assert.False(t, ReaderContains(nil, []byte("a")))
 	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) {
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -101,6 +101,7 @@
 	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"})
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -132,6 +132,9 @@
 	// Logger etc.
 	*deps.Deps `json:"-"`
 
+	// The func used to title case titles.
+	titleFunc func(s string) string
+
 	siteStats *siteStats
 }
 
@@ -172,6 +175,7 @@
 	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 @@
 		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 @@
 		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) newTaxonomyTermsPage(plural string) *Page {
 	p := s.newNodePage(KindTaxonomyTerm, plural)
-	p.Title = strings.Title(plural)
+	p.Title = s.titleFunc(plural)
 	return p
 }
--- a/tpl/strings/init.go
+++ b/tpl/strings/init.go
@@ -116,6 +116,7 @@
 			[]string{"title"},
 			[][2]string{
 				{`{{title "Bat man"}}`, `Bat Man`},
+				{`{{title "somewhere over the rainbow"}}`, `Somewhere Over the Rainbow`},
 			},
 		)
 
--- a/tpl/strings/init_test.go
+++ b/tpl/strings/init_test.go
@@ -18,6 +18,7 @@
 
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/tpl/internal"
+	"github.com/spf13/viper"
 	"github.com/stretchr/testify/require"
 )
 
@@ -26,7 +27,7 @@
 	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
--- a/tpl/strings/strings.go
+++ b/tpl/strings/strings.go
@@ -27,7 +27,9 @@
 
 // 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.
@@ -34,7 +36,8 @@
 // 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 @@
 		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
--- a/tpl/strings/strings_test.go
+++ b/tpl/strings/strings_test.go
@@ -19,11 +19,12 @@
 	"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{}
 
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -160,6 +160,18 @@
 			"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",
 			"revision": "ae77be60afb1dcacde03767a8c37337fad28ac14",