hugo/hugolib/pages_map.go

475 lines
10 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"
"github.com/gohugoio/hugo/common/maps"
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) initPageMeta(p *pageState, bucket *pagesMapBucket) error {
var err error
p.metaInit.Do(func() {
if p.metaInitFn != nil {
err = p.metaInitFn(bucket)
}
})
return err
}
func (m *pagesMap) initPageMetaFor(prefix string, bucket *pagesMapBucket) error {
parentBucket := m.parentBucket(prefix)
m.mergeCascades(bucket, parentBucket)
if err := m.initPageMeta(bucket.owner, bucket); err != nil {
return err
}
if !bucket.view {
for _, p := range bucket.pages {
ps := p.(*pageState)
if err := m.initPageMeta(ps, bucket); err != nil {
return err
}
for _, p := range ps.resources.ByType(pageResourceType) {
if err := m.initPageMeta(p.(*pageState), bucket); err != nil {
return err
}
}
}
// Now that the metadata is initialized (with dates, draft set etc.)
// we can remove the pages that we for some reason should not include
// in this build.
tmp := bucket.pages[:0]
for _, x := range bucket.pages {
if m.s.shouldBuild(x) {
if x.(*pageState).m.headless {
bucket.headlessPages = append(bucket.headlessPages, x)
} else {
tmp = append(tmp, x)
}
}
}
bucket.pages = tmp
}
return nil
}
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())
var cascade map[string]interface{}
if p.bucket != nil {
cascade = p.bucket.cascade
}
bucket := &pagesMapBucket{
owner: p,
view: isView,
cascade: cascade,
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)
bucket.pages = append(bucket.pages, p)
}
func (m *pagesMap) assemblePageMeta() error {
var walkErr error
m.r.Walk(func(s string, v interface{}) bool {
bucket := v.(*pagesMapBucket)
if err := m.initPageMetaFor(s, bucket); err != nil {
walkErr = err
return true
}
return false
})
return walkErr
}
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 err := m.initPageMetaFor(key, bucket); err != nil {
return err
}
}
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) error {
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)
if err := m.initPageMetaFor(key, b2); err != nil {
return err
}
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)
return nil
}
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 {
if err := addTaxonomy(singular, plural, idx, weight, p); err != nil {
m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err)
}
}
} else if v, ok := vals.(string); ok {
if err := addTaxonomy(singular, plural, v, weight, p); err != nil {
m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err)
}
} 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) mergeCascades(b1, b2 *pagesMapBucket) {
if b1.cascade == nil {
b1.cascade = make(maps.Params)
}
if b2 != nil && b2.cascade != nil {
for k, v := range b2.cascade {
if _, found := b1.cascade[k]; !found {
b1.cascade[k] = v
}
}
}
}
func (m *pagesMap) parentBucket(prefix string) *pagesMapBucket {
if prefix == "/" {
return nil
}
_, parentv, found := m.r.LongestPrefix(path.Dir(prefix))
if !found {
panic(fmt.Sprintf("[BUG] parent bucket not found for %q", prefix))
}
return parentv.(*pagesMapBucket)
}
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
})
}
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{}
// Cascading front matter.
cascade 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
headlessPages 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.headlessPages) == 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.getPages()...)
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
}