shithub: hugo

Download patch

ref: be7404e3371caa2851111bb3bcdc1c26f7ed47f5
parent: be535832f7f4889351df84bad5059bae4db5a95d
author: bep <[email protected]>
date: Thu May 28 19:05:13 EDT 2015

Support `Fish and Chips` section

Section names are also used as the title of the list pages, but naming section folders as `Fish and Chips` and similar didn't work very well.

This commit fixes that.

This commit also changes the title casing of the section titles. Some may argue that this is a breaking change, but the old behaviour was also pretty broken,
even for languages that use title capitalizations, as it didn't follow any particular style guide, `fish and chips` became `Fish And Chips` etc.

Now it just turns the first letter into upper case, so `Fish and Chips` will be left as `Fish and Chips`.

People wanting the good old behaviour can use the `title` template func.

Fixes #1176

--- a/helpers/general.go
+++ b/helpers/general.go
@@ -25,6 +25,8 @@
 	"reflect"
 	"strings"
 	"sync"
+	"unicode"
+	"unicode/utf8"
 
 	"github.com/spf13/cast"
 	bp "github.com/spf13/hugo/bufferpool"
@@ -76,6 +78,15 @@
 	}
 
 	return "unknown"
+}
+
+// FirstUpper returns a string with the first character as upper case.
+func FirstUpper(s string) string {
+	if s == "" {
+		return ""
+	}
+	r, n := utf8.DecodeRuneInString(s)
+	return string(unicode.ToUpper(r)) + s[n:]
 }
 
 // ReaderToBytes takes an io.Reader argument, reads from it
--- a/helpers/general_test.go
+++ b/helpers/general_test.go
@@ -33,6 +33,24 @@
 	}
 }
 
+func TestFirstUpper(t *testing.T) {
+	for i, this := range []struct {
+		in     string
+		expect string
+	}{
+		{"foo", "Foo"},
+		{"foo bar", "Foo bar"},
+		{"Foo Bar", "Foo Bar"},
+		{"", ""},
+		{"å", "Å"},
+	} {
+		result := FirstUpper(this.in)
+		if result != this.expect {
+			t.Errorf("[%d] got %s but expected %s", i, result, this.expect)
+		}
+	}
+}
+
 func TestBytesToReader(t *testing.T) {
 	asBytes := ReaderToBytes(strings.NewReader("Hello World!"))
 	asReader := BytesToReader(asBytes)
--- a/helpers/path.go
+++ b/helpers/path.go
@@ -92,7 +92,7 @@
 	target := make([]rune, 0, len(source))
 
 	for _, r := range source {
-		if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '/' || r == '_' || r == '-' || r == '#' {
+		if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '/' || r == '\\' || r == '_' || r == '-' || r == '#' {
 			target = append(target, r)
 		}
 	}
@@ -332,8 +332,8 @@
 		return "", err
 	}
 
-	if strings.HasSuffix(path, "/") && !strings.HasSuffix(name, "/") {
-		name += "/"
+	if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
+		name += FilePathSeparator
 	}
 	return name, nil
 }
--- a/hugolib/menu_test.go
+++ b/hugolib/menu_test.go
@@ -92,6 +92,16 @@
 +++
 Front Matter with Menu Pages`)
 
+var MENU_PAGE_4 = []byte(`+++
+title = "Four"
+weight = 4
+[menu]
+	[menu.p_two]
+		Name = "Four"
+		Parent = "Three"
++++
+Front Matter with Menu Pages`)
+
 var MENU_PAGE_SOURCES = []source.ByteSource{
 	{filepath.FromSlash("sect/doc1.md"), MENU_PAGE_1},
 	{filepath.FromSlash("sect/doc2.md"), MENU_PAGE_2},
@@ -102,6 +112,7 @@
 	{filepath.FromSlash("first/doc1.md"), MENU_PAGE_1},
 	{filepath.FromSlash("first/doc2.md"), MENU_PAGE_2},
 	{filepath.FromSlash("second-section/doc3.md"), MENU_PAGE_3},
+	{filepath.FromSlash("Fish and Chips/doc4.md"), MENU_PAGE_4},
 }
 
 func tstCreateMenuPageWithNameTOML(title, menu, name string) []byte {
@@ -332,47 +343,63 @@
 
 // Issue #1114
 func TestSectionPagesMenu(t *testing.T) {
-	viper.Reset()
-	defer viper.Reset()
 
-	viper.Set("SectionPagesMenu", "spm")
-
 	doTestSectionPagesMenu(true, t)
 	doTestSectionPagesMenu(false, t)
 }
 
 func doTestSectionPagesMenu(canonifyUrls bool, t *testing.T) {
+	viper.Reset()
+	defer viper.Reset()
+
+	viper.Set("SectionPagesMenu", "spm")
+
 	viper.Set("CanonifyURLs", canonifyUrls)
 	s := setupMenuTests(t, MENU_PAGE_SECTIONS_SOURCES)
 
-	assert.Equal(t, 2, len(s.Sections))
+	assert.Equal(t, 3, len(s.Sections))
 
 	firstSectionPages := s.Sections["first"]
 	assert.Equal(t, 2, len(firstSectionPages))
 	secondSectionPages := s.Sections["second-section"]
 	assert.Equal(t, 1, len(secondSectionPages))
+	fishySectionPages := s.Sections["fish-and-chips"]
+	assert.Equal(t, 1, len(fishySectionPages))
 
-	nodeFirst := s.newSectionListNode("first", firstSectionPages)
-	nodeSecond := s.newSectionListNode("second-section", secondSectionPages)
-
+	nodeFirst := s.newSectionListNode("First", "first", firstSectionPages)
+	nodeSecond := s.newSectionListNode("Second Section", "second-section", secondSectionPages)
+	nodeFishy := s.newSectionListNode("Fish and Chips", "fish-and-chips", fishySectionPages)
 	firstSectionMenuEntry := findTestMenuEntryByID(s, "spm", "first")
 	secondSectionMenuEntry := findTestMenuEntryByID(s, "spm", "second-section")
+	fishySectionMenuEntry := findTestMenuEntryByID(s, "spm", "Fish and Chips")
 
 	assert.NotNil(t, firstSectionMenuEntry)
 	assert.NotNil(t, secondSectionMenuEntry)
 	assert.NotNil(t, nodeFirst)
 	assert.NotNil(t, nodeSecond)
+	assert.NotNil(t, fishySectionMenuEntry)
+	assert.NotNil(t, nodeFishy)
 
+	assert.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry))
+	assert.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry))
+	assert.False(t, nodeFirst.IsMenuCurrent("spm", fishySectionMenuEntry))
+	assert.True(t, nodeFishy.IsMenuCurrent("spm", fishySectionMenuEntry))
+	assert.Equal(t, "Fish and Chips", fishySectionMenuEntry.Name)
+
 	for _, p := range firstSectionPages {
 		assert.True(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
 		assert.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
-		assert.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry))
-		assert.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry))
 	}
 
 	for _, p := range secondSectionPages {
 		assert.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
 		assert.True(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
+	}
+
+	for _, p := range fishySectionPages {
+		assert.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
+		assert.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
+		assert.True(t, p.Page.HasMenuCurrent("spm", fishySectionMenuEntry))
 	}
 }
 
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -346,7 +346,7 @@
 
 func (p *Page) permalink() (*url.URL, error) {
 	baseURL := string(p.Site.BaseURL)
-	dir := strings.TrimSpace(filepath.ToSlash(p.Source.Dir()))
+	dir := strings.TrimSpace(helpers.MakePath(filepath.ToSlash(strings.ToLower(p.Source.Dir()))))
 	pSlug := strings.TrimSpace(helpers.URLize(p.Slug))
 	pURL := strings.TrimSpace(helpers.URLize(p.URL))
 	var permalink string
@@ -837,5 +837,5 @@
 		outfile = helpers.ReplaceExtension(p.Source.LogicalName(), p.Extension())
 	}
 
-	return filepath.Join(p.Source.Dir(), strings.TrimSpace(outfile))
+	return filepath.Join(strings.ToLower(helpers.MakePath(p.Source.Dir())), strings.TrimSpace(outfile))
 }
--- a/hugolib/page_permalink_test.go
+++ b/hugolib/page_permalink_test.go
@@ -15,7 +15,6 @@
 
 	tests := []struct {
 		file         string
-		dir          string
 		base         template.URL
 		slug         string
 		url          string
@@ -24,27 +23,27 @@
 		expectedAbs  string
 		expectedRel  string
 	}{
-		{"x/y/z/boofar.md", "x/y/z", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z/", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "", "", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"},
 		// Issue #1174
-		{"x/y/z/boofar.md", "x/y/z", "http://gopher.com/", "", "", false, true, "http://gopher.com/x/y/z/boofar/", "/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://gopher.com/", "", "", true, true, "http://gopher.com/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "", "boofar", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z", "http://barnew/", "", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/", "boofar", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "", "boofar", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z", "http://barnew/", "", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/", "boofar", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/boo/", "boofar", "", true, false, "http://barnew/boo/x/y/z/boofar.html", "/boo/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/boo/", "boofar", "", false, true, "http://barnew/boo/x/y/z/boofar/", "/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/boo/", "boofar", "", false, false, "http://barnew/boo/x/y/z/boofar/", "/boo/x/y/z/boofar/"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/boo/", "boofar", "", true, true, "http://barnew/boo/x/y/z/boofar.html", "/x/y/z/boofar.html"},
-		{"x/y/z/boofar.md", "x/y/z/", "http://barnew/boo", "boofar", "", true, true, "http://barnew/boo/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "http://gopher.com/", "", "", false, true, "http://gopher.com/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "http://gopher.com/", "", "", true, true, "http://gopher.com/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "", "boofar", "", false, false, "/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "http://barnew/", "", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "http://barnew/", "boofar", "", false, false, "http://barnew/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "", "", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "", "boofar", "", true, false, "/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "http://barnew/", "", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "http://barnew/", "boofar", "", true, false, "http://barnew/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "http://barnew/boo/", "boofar", "", true, false, "http://barnew/boo/x/y/z/boofar.html", "/boo/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "http://barnew/boo/", "boofar", "", false, true, "http://barnew/boo/x/y/z/boofar/", "/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "http://barnew/boo/", "boofar", "", false, false, "http://barnew/boo/x/y/z/boofar/", "/boo/x/y/z/boofar/"},
+		{"x/y/z/boofar.md", "http://barnew/boo/", "boofar", "", true, true, "http://barnew/boo/x/y/z/boofar.html", "/x/y/z/boofar.html"},
+		{"x/y/z/boofar.md", "http://barnew/boo", "boofar", "", true, true, "http://barnew/boo/x/y/z/boofar.html", "/x/y/z/boofar.html"},
 
 		// test URL overrides
-		{"x/y/z/boofar.md", "x/y/z", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"},
+		{"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"},
 	}
 
 	viper.Set("DefaultExtension", "html")
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -774,7 +774,7 @@
 		if sectionPagesMenu != "" {
 			if _, ok := sectionPagesMenus[p.Section()]; !ok {
 				if p.Section() != "" {
-					me := MenuEntry{Identifier: p.Section(), Name: helpers.MakeTitle(p.Section()), URL: s.Info.createNodeMenuEntryURL("/" + p.Section())}
+					me := MenuEntry{Identifier: p.Section(), Name: helpers.MakeTitle(helpers.FirstUpper(p.Section())), URL: s.Info.createNodeMenuEntryURL("/" + p.Section())}
 					if _, ok := flat[twoD{sectionPagesMenu, me.KeyName()}]; ok {
 						// menu with same id defined in config, let that one win
 						continue
@@ -1160,12 +1160,13 @@
 	return
 }
 
-func (s *Site) newSectionListNode(section string, data WeightedPages) *Node {
+func (s *Site) newSectionListNode(sectionName, section string, data WeightedPages) *Node {
 	n := s.NewNode()
+	sectionName = helpers.FirstUpper(sectionName)
 	if viper.GetBool("PluralizeListTitles") {
-		n.Title = strings.Title(inflect.Pluralize(section))
+		n.Title = inflect.Pluralize(sectionName)
 	} else {
-		n.Title = strings.Title(section)
+		n.Title = sectionName
 	}
 	s.setURLs(n, section)
 	n.Date = data[0].Page.Date
@@ -1179,12 +1180,17 @@
 func (s *Site) RenderSectionLists() error {
 	for section, data := range s.Sections {
 
+		// section keys are lower case
+		// extract the original casing from the first page to get sensible titles.
+		sectionName := section
+		if len(data) > 0 {
+			sectionName = data[0].Page.Section()
+		}
 		layouts := s.appendThemeTemplates(
 			[]string{"section/" + section + ".html", "_default/section.html", "_default/list.html", "indexes/" + section + ".html", "_default/indexes.html"})
 
-		n := s.newSectionListNode(section, data)
-
-		if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), fmt.Sprintf("/%s", section), n, s.appendThemeTemplates(layouts)...); err != nil {
+		n := s.newSectionListNode(sectionName, section, data)
+		if err := s.renderAndWritePage(fmt.Sprintf("section %s", section), section, n, s.appendThemeTemplates(layouts)...); err != nil {
 			return err
 		}
 
@@ -1203,7 +1209,7 @@
 					continue
 				}
 
-				sectionPagerNode := s.newSectionListNode(section, data)
+				sectionPagerNode := s.newSectionListNode(sectionName, section, data)
 				sectionPagerNode.paginator = pager
 				if pager.TotalPages() > 0 {
 					sectionPagerNode.Date = pager.Pages()[0].Date
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -1,6 +1,7 @@
 package hugolib
 
 import (
+	"bitbucket.org/pkg/inflect"
 	"bytes"
 	"fmt"
 	"html/template"
@@ -277,6 +278,7 @@
 
 // Issue #957
 func TestCrossrefs(t *testing.T) {
+	hugofs.DestinationFS = new(afero.MemMapFs)
 	for _, uglyUrls := range []bool{true, false} {
 		for _, relative := range []bool{true, false} {
 			doTestCrossrefs(t, relative, uglyUrls)
@@ -360,6 +362,7 @@
 
 // Issue #939
 func Test404ShouldAlwaysHaveUglyUrls(t *testing.T) {
+	hugofs.DestinationFS = new(afero.MemMapFs)
 	for _, uglyURLs := range []bool{true, false} {
 		doTest404ShouldAlwaysHaveUglyUrls(t, uglyURLs)
 	}
@@ -439,6 +442,87 @@
 
 }
 
+// Issue #1176
+func TestSectionNaming(t *testing.T) {
+
+	for _, canonify := range []bool{true, false} {
+		for _, uglify := range []bool{true, false} {
+			for _, pluralize := range []bool{true, false} {
+				doTestSectionNaming(t, canonify, uglify, pluralize)
+			}
+		}
+	}
+}
+
+func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) {
+	hugofs.DestinationFS = new(afero.MemMapFs)
+	viper.Reset()
+	defer viper.Reset()
+	viper.Set("baseurl", "http://auth/sub/")
+	viper.Set("DefaultExtension", "html")
+	viper.Set("UglyURLs", uglify)
+	viper.Set("PluralizeListTitles", pluralize)
+	viper.Set("CanonifyURLs", canonify)
+
+	var expectedPathSuffix string
+
+	if uglify {
+		expectedPathSuffix = ".html"
+	} else {
+		expectedPathSuffix = "/index.html"
+	}
+
+	sources := []source.ByteSource{
+		{filepath.FromSlash("sect/doc1.html"), []byte("doc1")},
+		{filepath.FromSlash("Fish and Chips/doc2.html"), []byte("doc2")},
+		{filepath.FromSlash("ラーメン/doc3.html"), []byte("doc3")},
+	}
+
+	s := &Site{
+		Source:  &source.InMemorySource{ByteSource: sources},
+		Targets: targetList{Page: &target.PagePub{UglyURLs: uglify}},
+	}
+
+	s.initializeSiteInfo()
+	templatePrep(s)
+
+	must(s.addTemplate("_default/single.html", "{{.Content}}"))
+	must(s.addTemplate("_default/list.html", "{{ .Title }}"))
+
+	createAndRenderPages(t, s)
+	s.RenderSectionLists()
+
+	tests := []struct {
+		doc         string
+		pluralAware bool
+		expected    string
+	}{
+		{filepath.FromSlash(fmt.Sprintf("sect/doc1%s", expectedPathSuffix)), false, "doc1"},
+		{filepath.FromSlash(fmt.Sprintf("sect%s", expectedPathSuffix)), true, "Sect"},
+		{filepath.FromSlash(fmt.Sprintf("fish-and-chips/doc2%s", expectedPathSuffix)), false, "doc2"},
+		{filepath.FromSlash(fmt.Sprintf("fish-and-chips%s", expectedPathSuffix)), true, "Fish and Chips"},
+		{filepath.FromSlash(fmt.Sprintf("ラーメン/doc3%s", expectedPathSuffix)), false, "doc3"},
+		{filepath.FromSlash(fmt.Sprintf("ラーメン%s", expectedPathSuffix)), true, "ラーメン"},
+	}
+
+	for _, test := range tests {
+		file, err := hugofs.DestinationFS.Open(test.doc)
+		if err != nil {
+			t.Fatalf("Did not find %s in target: %s", test.doc, err)
+		}
+
+		content := helpers.ReaderToString(file)
+
+		if test.pluralAware && pluralize {
+			test.expected = inflect.Pluralize(test.expected)
+		}
+
+		if content != test.expected {
+			t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content)
+		}
+	}
+
+}
 func TestSkipRender(t *testing.T) {
 	viper.Reset()
 	defer viper.Reset()