hugo/hugolib/pages_map.go
Bjørn Erik Pedersen 7ff0a8ee9f Simplify page tree logic
This is preparation for #6041.

For historic reasons, the code for bulding the section tree and the taxonomies were very much separate.

This works, but makes it hard to extend, maintain, and possibly not so fast as it could be.

This simplification also introduces 3 slightly breaking changes, which I suspect most people will be pleased about. See referenced issues:

This commit also switches the radix tree dependency to a mutable implementation: github.com/armon/go-radix.

Fixes #6154
Fixes #6153
Fixes #6152
2019-08-08 20:13:39 +02:00

368 lines
7.8 KiB
Go

// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"path"
"path/filepath"
"strings"
"sync"
radix "github.com/armon/go-radix"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/resources/page"
)
func newPagesMap(s *Site) *pagesMap {
return &pagesMap{
r: radix.New(),
s: s,
}
}
type pagesMap struct {
r *radix.Tree
s *Site
}
func (m *pagesMap) Get(key string) *pagesMapBucket {
key = m.cleanKey(key)
v, found := m.r.Get(key)
if !found {
return nil
}
return v.(*pagesMapBucket)
}
func (m *pagesMap) getKey(p *pageState) string {
if !p.File().IsZero() {
return m.cleanKey(p.File().Dir())
}
return m.cleanKey(p.SectionsPath())
}
func (m *pagesMap) getOrCreateHome() *pageState {
var home *pageState
b, found := m.r.Get("/")
if !found {
home = m.s.newPage(page.KindHome)
m.addBucketFor("/", home, nil)
} else {
home = b.(*pagesMapBucket).owner
}
return home
}
func (m *pagesMap) createSectionIfNotExists(section string) {
key := m.cleanKey(section)
_, found := m.r.Get(key)
if !found {
kind := m.s.kindFromSectionPath(section)
p := m.s.newPage(kind, section)
m.addBucketFor(key, p, nil)
}
}
func (m *pagesMap) addBucket(p *pageState) {
key := m.getKey(p)
m.addBucketFor(key, p, nil)
}
func (m *pagesMap) addBucketFor(key string, p *pageState, meta map[string]interface{}) *pagesMapBucket {
var isView bool
switch p.Kind() {
case page.KindTaxonomy, page.KindTaxonomyTerm:
isView = true
}
disabled := !m.s.isEnabled(p.Kind())
bucket := &pagesMapBucket{owner: p, view: isView, meta: meta, disabled: disabled}
p.bucket = bucket
m.r.Insert(key, bucket)
return bucket
}
func (m *pagesMap) addPage(p *pageState) {
if !p.IsPage() {
m.addBucket(p)
return
}
if !m.s.isEnabled(page.KindPage) {
return
}
key := m.getKey(p)
var bucket *pagesMapBucket
_, v, found := m.r.LongestPrefix(key)
if !found {
panic(fmt.Sprintf("[BUG] bucket with key %q not found", key))
}
bucket = v.(*pagesMapBucket)
p.bucket = bucket
bucket.pages = append(bucket.pages, p)
}
func (m *pagesMap) withEveryPage(f func(p *pageState)) {
m.r.Walk(func(k string, v interface{}) bool {
b := v.(*pagesMapBucket)
f(b.owner)
if !b.view {
for _, p := range b.pages {
f(p.(*pageState))
}
}
return false
})
}
func (m *pagesMap) assembleTaxonomies(s *Site) error {
s.Taxonomies = make(TaxonomyList)
type bucketKey struct {
plural string
termKey string
}
// Temporary cache.
taxonomyBuckets := make(map[bucketKey]*pagesMapBucket)
for singular, plural := range s.siteCfg.taxonomiesConfig {
s.Taxonomies[plural] = make(Taxonomy)
bkey := bucketKey{
plural: plural,
}
bucket := m.Get(plural)
if bucket == nil {
// Create the page and bucket
n := s.newPage(page.KindTaxonomyTerm, plural)
key := m.cleanKey(plural)
bucket = m.addBucketFor(key, n, nil)
}
if bucket.meta == nil {
bucket.meta = map[string]interface{}{
"singular": singular,
"plural": plural,
}
}
// Add it to the temporary cache.
taxonomyBuckets[bkey] = bucket
// Taxonomy entries used in page front matter will be picked up later,
// but there may be some yet to be used.
pluralPrefix := m.cleanKey(plural) + "/"
m.r.WalkPrefix(pluralPrefix, func(k string, v interface{}) bool {
tb := v.(*pagesMapBucket)
termKey := strings.TrimPrefix(k, pluralPrefix)
if tb.meta == nil {
tb.meta = map[string]interface{}{
"singular": singular,
"plural": plural,
"term": tb.owner.Title(),
"termKey": termKey,
}
}
bucket.pages = append(bucket.pages, tb.owner)
bkey.termKey = termKey
taxonomyBuckets[bkey] = tb
return false
})
}
addTaxonomy := func(singular, plural, term string, weight int, p page.Page) {
bkey := bucketKey{
plural: plural,
}
termKey := s.getTaxonomyKey(term)
b1 := taxonomyBuckets[bkey]
var b2 *pagesMapBucket
bkey.termKey = termKey
b, found := taxonomyBuckets[bkey]
if found {
b2 = b
} else {
// Create the page and bucket
n := s.newTaxonomyPage(term, plural, termKey)
meta := map[string]interface{}{
"singular": singular,
"plural": plural,
"term": term,
"termKey": termKey,
}
key := m.cleanKey(path.Join(plural, termKey))
b2 = m.addBucketFor(key, n, meta)
b1.pages = append(b1.pages, b2.owner)
taxonomyBuckets[bkey] = b2
}
w := page.NewWeightedPage(weight, p, b2.owner)
s.Taxonomies[plural].add(termKey, w)
b1.owner.m.Dates.UpdateDateAndLastmodIfAfter(p)
b2.owner.m.Dates.UpdateDateAndLastmodIfAfter(p)
}
m.r.Walk(func(k string, v interface{}) bool {
b := v.(*pagesMapBucket)
if b.view {
return false
}
for singular, plural := range s.siteCfg.taxonomiesConfig {
for _, p := range b.pages {
vals := getParam(p, plural, false)
w := getParamToLower(p, plural+"_weight")
weight, err := cast.ToIntE(w)
if err != nil {
m.s.Log.ERROR.Printf("Unable to convert taxonomy weight %#v to int for %q", w, p.Path())
// weight will equal zero, so let the flow continue
}
if vals != nil {
if v, ok := vals.([]string); ok {
for _, idx := range v {
addTaxonomy(singular, plural, idx, weight, p)
}
} else if v, ok := vals.(string); ok {
addTaxonomy(singular, plural, v, weight, p)
} else {
m.s.Log.ERROR.Printf("Invalid %s in %q\n", plural, p.Path())
}
}
}
}
return false
})
for _, plural := range s.siteCfg.taxonomiesConfig {
for k := range s.Taxonomies[plural] {
s.Taxonomies[plural][k].Sort()
}
}
return nil
}
func (m *pagesMap) cleanKey(key string) string {
key = filepath.ToSlash(strings.ToLower(key))
key = strings.Trim(key, "/")
return "/" + key
}
func (m *pagesMap) dump() {
m.r.Walk(func(s string, v interface{}) bool {
b := v.(*pagesMapBucket)
fmt.Println("-------\n", s, ":", b.owner.Kind(), ":")
if b.owner != nil {
fmt.Println("Owner:", b.owner.Path())
}
for _, p := range b.pages {
fmt.Println(p.Path())
}
return false
})
}
type pagesMapBucket struct {
// Set if the pages in this bucket is also present in another bucket.
view bool
// Some additional metatadata attached to this node.
meta map[string]interface{}
owner *pageState // The branch node
// When disableKinds is enabled for this node.
disabled bool
// Used to navigate the sections tree
parent *pagesMapBucket
bucketSections []*pagesMapBucket
pagesInit sync.Once
pages page.Pages
pagesAndSectionsInit sync.Once
pagesAndSections page.Pages
sectionsInit sync.Once
sections page.Pages
}
func (b *pagesMapBucket) isEmpty() bool {
return len(b.pages) == 0 && len(b.bucketSections) == 0
}
func (b *pagesMapBucket) getPages() page.Pages {
b.pagesInit.Do(func() {
page.SortByDefault(b.pages)
})
return b.pages
}
func (b *pagesMapBucket) getPagesAndSections() page.Pages {
b.pagesAndSectionsInit.Do(func() {
var pas page.Pages
pas = append(pas, b.pages...)
for _, p := range b.bucketSections {
pas = append(pas, p.owner)
}
b.pagesAndSections = pas
page.SortByDefault(b.pagesAndSections)
})
return b.pagesAndSections
}
func (b *pagesMapBucket) getSections() page.Pages {
b.sectionsInit.Do(func() {
for _, p := range b.bucketSections {
b.sections = append(b.sections, p.owner)
}
page.SortByDefault(b.sections)
})
return b.sections
}