Skip to content

Commit

Permalink
Make string sorting (e.g. ByTitle, ByLinkTitle and ByParam) language …
Browse files Browse the repository at this point in the history
…aware

Fixes #2180
  • Loading branch information
bep committed Apr 12, 2022
1 parent 82ba634 commit 627eed1
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 28 deletions.
5 changes: 5 additions & 0 deletions hugolib/site.go
Expand Up @@ -739,7 +739,12 @@ func (s *SiteInfo) Sites() page.Sites {
}

// Current returns the currently rendered Site.
// If that isn't set yet, which is the situation before we start rendering,
// if will return the Site itself.
func (s *SiteInfo) Current() page.Site {
if s.s.h.currentSite == nil {
return s
}
return s.s.h.currentSite.Info
}

Expand Down
23 changes: 20 additions & 3 deletions hugolib/taxonomy.go
Expand Up @@ -18,6 +18,7 @@ import (
"sort"

"github.com/gohugoio/hugo/compare"
"github.com/gohugoio/hugo/langs"

"github.com/gohugoio/hugo/resources/page"
)
Expand All @@ -40,6 +41,15 @@ type Taxonomy map[string]page.WeightedPages
// Important because you can't order a map.
type OrderedTaxonomy []OrderedTaxonomyEntry

// getOneOPage returns one page in the taxonomy,
// nil if there is none.
func (t OrderedTaxonomy) getOneOPage() page.Page {
if len(t) == 0 {
return nil
}
return t[0].Pages()[0]
}

// OrderedTaxonomyEntry is similar to an element of a Taxonomy, but with the key embedded (as name)
// e.g: {Name: Technology, page.WeightedPages: TaxonomyPages}
type OrderedTaxonomyEntry struct {
Expand Down Expand Up @@ -72,11 +82,18 @@ func (i Taxonomy) TaxonomyArray() OrderedTaxonomy {

// Alphabetical returns an ordered taxonomy sorted by key name.
func (i Taxonomy) Alphabetical() OrderedTaxonomy {
ia := i.TaxonomyArray()
p := ia.getOneOPage()
if p == nil {
return ia
}
currentSite := p.Site().Current()
coll := langs.GetCollator(currentSite.Language())
coll.Lock()
defer coll.Unlock()
name := func(i1, i2 *OrderedTaxonomyEntry) bool {
return compare.LessStrings(i1.Name, i2.Name)
return coll.CompareStrings(i1.Name, i2.Name) < 0
}

ia := i.TaxonomyArray()
oiBy(name).Sort(ia)
return ia
}
Expand Down
39 changes: 37 additions & 2 deletions langs/language.go
Expand Up @@ -19,6 +19,9 @@ import (
"sync"
"time"

"golang.org/x/text/collate"
"golang.org/x/text/language"

"github.com/pkg/errors"

"github.com/gohugoio/hugo/common/htime"
Expand Down Expand Up @@ -80,8 +83,9 @@ type Language struct {
// TODO(bep) do the same for some of the others.
translator locales.Translator
timeFormatter htime.TimeFormatter

location *time.Location
tag language.Tag
collator *Collator
location *time.Location

// Error during initialization. Will fail the buld.
initErr error
Expand Down Expand Up @@ -111,6 +115,18 @@ func NewLanguage(lang string, cfg config.Provider) *Language {
}
}

var coll *Collator
tag, err := language.Parse(lang)
if err == nil {
coll = &Collator{
c: collate.New(tag),
}
} else {
coll = &Collator{
c: collate.New(language.English),
}
}

l := &Language{
Lang: lang,
ContentDir: cfg.GetString("contentDir"),
Expand All @@ -119,6 +135,8 @@ func NewLanguage(lang string, cfg config.Provider) *Language {
params: params,
translator: translator,
timeFormatter: htime.NewTimeFormatter(translator),
tag: tag,
collator: coll,
}

if err := l.loadLocation(cfg.GetString("timeZone")); err != nil {
Expand Down Expand Up @@ -275,6 +293,10 @@ func GetLocation(l *Language) *time.Location {
return l.location
}

func GetCollator(l *Language) *Collator {
return l.collator
}

func (l *Language) loadLocation(tzStr string) error {
location, err := time.LoadLocation(tzStr)
if err != nil {
Expand All @@ -284,3 +306,16 @@ func (l *Language) loadLocation(tzStr string) error {

return nil
}

type Collator struct {
sync.Mutex
c *collate.Collator
}

// CompareStrings compares a and b.
// It returns -1 if a < b, 1 if a > b and 0 if a == b.
// Note that the Collator is not thread safe, so you may want
// to aquire a lock on it before calling this method.
func (c *Collator) CompareStrings(a, b string) int {
return c.c.CompareString(a, b)
}
59 changes: 59 additions & 0 deletions langs/language_test.go
Expand Up @@ -14,10 +14,13 @@
package langs

import (
"sync"
"testing"

qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"golang.org/x/text/collate"
"golang.org/x/text/language"
)

func TestGetGlobalOnlySetting(t *testing.T) {
Expand Down Expand Up @@ -47,3 +50,59 @@ func TestLanguageParams(t *testing.T) {
c.Assert(lang.Params()["p1"], qt.Equals, "p1p")
c.Assert(lang.Get("p1"), qt.Equals, "p1cfg")
}

func TestCollator(t *testing.T) {

c := qt.New(t)

var wg sync.WaitGroup

coll := &Collator{c: collate.New(language.English, collate.Loose)}

for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
coll.Lock()
defer coll.Unlock()
defer wg.Done()
for j := 0; j < 10; j++ {
k := coll.CompareStrings("abc", "def")
c.Assert(k, qt.Equals, -1)
}
}()
}
wg.Wait()

}

func BenchmarkCollator(b *testing.B) {
s := []string{"foo", "bar", "éntre", "baz", "qux", "quux", "corge", "grault", "garply", "waldo", "fred", "plugh", "xyzzy", "thud"}

doWork := func(coll *Collator) {
for i := 0; i < len(s); i++ {
for j := i + 1; j < len(s); j++ {
_ = coll.CompareStrings(s[i], s[j])
}
}
}

b.Run("Single", func(b *testing.B) {
coll := &Collator{c: collate.New(language.English, collate.Loose)}
for i := 0; i < b.N; i++ {
doWork(coll)
}
})

b.Run("Para", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
coll := &Collator{c: collate.New(language.English, collate.Loose)}

for pb.Next() {
coll.Lock()
doWork(coll)
coll.Unlock()
}
})
})

}
66 changes: 66 additions & 0 deletions resources/page/integration_test.go
Expand Up @@ -70,3 +70,69 @@ date: "2020-02-01"
b.AssertFileContent("public/en/index.html", "0|February, 2020|Pages(1)1|January, 2020|Pages(1)")
b.AssertFileContent("public/fr/index.html", "0|février, 2020|Pages(1)1|janvier, 2020|Pages(1)")
}

func TestPagesSortCollation(t *testing.T) {

files := `
-- config.toml --
defaultContentLanguage = 'en'
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
title = 'My blog'
weight = 1
[languages.fr]
title = 'Mon blogue'
weight = 2
[languages.nn]
title = 'Bloggen min'
weight = 3
-- content/p1.md --
---
title: "zulu"
date: "2020-01-01"
param1: "xylophone"
tags: ["xylophone", "éclair", "zulu", "emma"]
---
-- content/p2.md --
---
title: "émotion"
date: "2020-01-01"
param1: "violin"
---
-- content/p3.md --
---
title: "alpha"
date: "2020-01-01"
param1: "éclair"
---
-- layouts/index.html --
ByTitle: {{ range site.RegularPages.ByTitle }}{{ .Title }}|{{ end }}
ByLinkTitle: {{ range site.RegularPages.ByLinkTitle }}{{ .Title }}|{{ end }}
ByParam: {{ range site.RegularPages.ByParam "param1" }}{{ .Params.param1 }}|{{ end }}
Tags Alphabetical: {{ range site.Taxonomies.tags.Alphabetical }}{{ .Term }}|{{ end }}
GroupBy: {{ range site.RegularPages.GroupBy "Title" }}{{ .Key }}|{{ end }}
{{ with (site.GetPage "p1").Params.tags }}
Sort: {{ sort . }}
ByWeight: {{ range site.RegularPages.ByWeight }}{{ .Title }}|{{ end }}
{{ end }}
`

b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
}).Build()

b.AssertFileContent("public/en/index.html", `
ByTitle: alpha|émotion|zulu|
ByLinkTitle: alpha|émotion|zulu|
ByParam: éclair|violin|xylophone
Tags Alphabetical: éclair|emma|xylophone|zulu|
GroupBy: alpha|émotion|zulu|
Sort: [éclair emma xylophone zulu]
ByWeight: alpha|émotion|zulu|
`)
}
19 changes: 12 additions & 7 deletions resources/page/pagegroup.go
Expand Up @@ -53,13 +53,16 @@ type mapKeyByInt struct{ mapKeyValues }

func (s mapKeyByInt) Less(i, j int) bool { return s.mapKeyValues[i].Int() < s.mapKeyValues[j].Int() }

type mapKeyByStr struct{ mapKeyValues }
type mapKeyByStr struct {
less func(a, b string) bool
mapKeyValues
}

func (s mapKeyByStr) Less(i, j int) bool {
return compare.LessStrings(s.mapKeyValues[i].String(), s.mapKeyValues[j].String())
return s.less(s.mapKeyValues[i].String(), s.mapKeyValues[j].String())
}

func sortKeys(v []reflect.Value, order string) []reflect.Value {
func sortKeys(examplePage Page, v []reflect.Value, order string) []reflect.Value {
if len(v) <= 1 {
return v
}
Expand All @@ -72,10 +75,12 @@ func sortKeys(v []reflect.Value, order string) []reflect.Value {
sort.Sort(mapKeyByInt{v})
}
case reflect.String:
stringLess, close := collatorStringLess(examplePage)
defer close()
if order == "desc" {
sort.Sort(sort.Reverse(mapKeyByStr{v}))
sort.Sort(sort.Reverse(mapKeyByStr{stringLess, v}))
} else {
sort.Sort(mapKeyByStr{v})
sort.Sort(mapKeyByStr{stringLess, v})
}
}
return v
Expand Down Expand Up @@ -161,7 +166,7 @@ func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv))
}

sortedKeys := sortKeys(tmp.MapKeys(), direction)
sortedKeys := sortKeys(p[0], tmp.MapKeys(), direction)
r := make([]PageGroup, len(sortedKeys))
for i, k := range sortedKeys {
r[i] = PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)}
Expand Down Expand Up @@ -213,7 +218,7 @@ func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) {
}

var r []PageGroup
for _, k := range sortKeys(tmp.MapKeys(), direction) {
for _, k := range sortKeys(p[0], tmp.MapKeys(), direction) {
r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().(Pages)})
}

Expand Down

0 comments on commit 627eed1

Please sign in to comment.