mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
7285e74090
There are some breaking changes in this commit, see #11455. Closes #11455 Closes #11549 This fixes a set of bugs (see issue list) and it is also paying some technical debt accumulated over the years. We now build with Staticcheck enabled in the CI build. The performance should be about the same as before for regular sized Hugo sites, but it should perform and scale much better to larger data sets, as objects that uses lots of memory (e.g. rendered Markdown, big JSON files read into maps with transform.Unmarshal etc.) will now get automatically garbage collected if needed. Performance on partial rebuilds when running the server in fast render mode should be the same, but the change detection should be much more accurate. A list of the notable new features: * A new dependency tracker that covers (almost) all of Hugo's API and is used to do fine grained partial rebuilds when running the server. * A new and simpler tree document store which allows fast lookups and prefix-walking in all dimensions (e.g. language) concurrently. * You can now configure an upper memory limit allowing for much larger data sets and/or running on lower specced PCs. We have lifted the "no resources in sub folders" restriction for branch bundles (e.g. sections). Memory Limit * Hugos will, by default, set aside a quarter of the total system memory, but you can set this via the OS environment variable HUGO_MEMORYLIMIT (in gigabytes). This is backed by a partitioned LRU cache used throughout Hugo. A cache that gets dynamically resized in low memory situations, allowing Go's Garbage Collector to free the memory. New Dependency Tracker: Hugo has had a rule based coarse grained approach to server rebuilds that has worked mostly pretty well, but there have been some surprises (e.g. stale content). This is now revamped with a new dependency tracker that can quickly calculate the delta given a changed resource (e.g. a content file, template, JS file etc.). This handles transitive relations, e.g. $page -> js.Build -> JS import, or $page1.Content -> render hook -> site.GetPage -> $page2.Title, or $page1.Content -> shortcode -> partial -> site.RegularPages -> $page2.Content -> shortcode ..., and should also handle changes to aggregated values (e.g. site.Lastmod) effectively. This covers all of Hugo's API with 2 known exceptions (a list that may not be fully exhaustive): Changes to files loaded with template func os.ReadFile may not be handled correctly. We recommend loading resources with resources.Get Changes to Hugo objects (e.g. Page) passed in the template context to lang.Translate may not be detected correctly. We recommend having simple i18n templates without too much data context passed in other than simple types such as strings and numbers. Note that the cachebuster configuration (when A changes then rebuild B) works well with the above, but we recommend that you revise that configuration, as it in most situations should not be needed. One example where it is still needed is with TailwindCSS and using changes to hugo_stats.json to trigger new CSS rebuilds. Document Store: Previously, a little simplified, we split the document store (where we store pages and resources) in a tree per language. This worked pretty well, but the structure made some operations harder than they needed to be. We have now restructured it into one Radix tree for all languages. Internally the language is considered to be a dimension of that tree, and the tree can be viewed in all dimensions concurrently. This makes some operations re. language simpler (e.g. finding translations is just a slice range), but the idea is that it should also be relatively inexpensive to add more dimensions if needed (e.g. role). Fixes #10169 Fixes #10364 Fixes #10482 Fixes #10630 Fixes #10656 Fixes #10694 Fixes #10918 Fixes #11262 Fixes #11439 Fixes #11453 Fixes #11457 Fixes #11466 Fixes #11540 Fixes #11551 Fixes #11556 Fixes #11654 Fixes #11661 Fixes #11663 Fixes #11664 Fixes #11669 Fixes #11671 Fixes #11807 Fixes #11808 Fixes #11809 Fixes #11815 Fixes #11840 Fixes #11853 Fixes #11860 Fixes #11883 Fixes #11904 Fixes #7388 Fixes #7425 Fixes #7436 Fixes #7544 Fixes #7882 Fixes #7960 Fixes #8255 Fixes #8307 Fixes #8863 Fixes #8927 Fixes #9192 Fixes #9324
1891 lines
49 KiB
Go
1891 lines
49 KiB
Go
// Copyright 2024 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 (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/bep/logg"
|
|
"github.com/gohugoio/hugo/cache/dynacache"
|
|
"github.com/gohugoio/hugo/common/loggers"
|
|
"github.com/gohugoio/hugo/common/paths"
|
|
"github.com/gohugoio/hugo/common/predicate"
|
|
"github.com/gohugoio/hugo/common/rungroup"
|
|
"github.com/gohugoio/hugo/common/types"
|
|
"github.com/gohugoio/hugo/hugofs/files"
|
|
"github.com/gohugoio/hugo/hugolib/doctree"
|
|
"github.com/gohugoio/hugo/identity"
|
|
"github.com/gohugoio/hugo/output"
|
|
"github.com/gohugoio/hugo/resources"
|
|
"github.com/spf13/cast"
|
|
|
|
"github.com/gohugoio/hugo/common/maps"
|
|
|
|
"github.com/gohugoio/hugo/resources/kinds"
|
|
"github.com/gohugoio/hugo/resources/page"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
)
|
|
|
|
var pagePredicates = struct {
|
|
KindPage predicate.P[*pageState]
|
|
KindSection predicate.P[*pageState]
|
|
KindHome predicate.P[*pageState]
|
|
KindTerm predicate.P[*pageState]
|
|
ShouldListLocal predicate.P[*pageState]
|
|
ShouldListGlobal predicate.P[*pageState]
|
|
ShouldListAny predicate.P[*pageState]
|
|
ShouldLink predicate.P[page.Page]
|
|
}{
|
|
KindPage: func(p *pageState) bool {
|
|
return p.Kind() == kinds.KindPage
|
|
},
|
|
KindSection: func(p *pageState) bool {
|
|
return p.Kind() == kinds.KindSection
|
|
},
|
|
KindHome: func(p *pageState) bool {
|
|
return p.Kind() == kinds.KindHome
|
|
},
|
|
KindTerm: func(p *pageState) bool {
|
|
return p.Kind() == kinds.KindTerm
|
|
},
|
|
ShouldListLocal: func(p *pageState) bool {
|
|
return p.m.shouldList(false)
|
|
},
|
|
ShouldListGlobal: func(p *pageState) bool {
|
|
return p.m.shouldList(true)
|
|
},
|
|
ShouldListAny: func(p *pageState) bool {
|
|
return p.m.shouldListAny()
|
|
},
|
|
ShouldLink: func(p page.Page) bool {
|
|
return !p.(*pageState).m.noLink()
|
|
},
|
|
}
|
|
|
|
type pageMap struct {
|
|
i int
|
|
s *Site
|
|
|
|
// Main storage for all pages.
|
|
*pageTrees
|
|
|
|
// Used for simple page lookups by name, e.g. "mypage.md" or "mypage".
|
|
pageReverseIndex *contentTreeReverseIndex
|
|
|
|
cachePages *dynacache.Partition[string, page.Pages]
|
|
cacheResources *dynacache.Partition[string, resource.Resources]
|
|
cacheContentRendered *dynacache.Partition[string, *resources.StaleValue[contentSummary]]
|
|
cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]]
|
|
contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]]
|
|
cacheContentSource *dynacache.Partition[string, *resources.StaleValue[[]byte]]
|
|
|
|
cfg contentMapConfig
|
|
}
|
|
|
|
// pageTrees holds pages and resources in a tree structure for all sites/languages.
|
|
// Eeach site gets its own tree set via the Shape method.
|
|
type pageTrees struct {
|
|
// This tree contains all Pages.
|
|
// This include regular pages, sections, taxonimies and so on.
|
|
// Note that all of these trees share the same key structure,
|
|
// so you can take a leaf Page key and do a prefix search
|
|
// with key + "/" to get all of its resources.
|
|
treePages *doctree.NodeShiftTree[contentNodeI]
|
|
|
|
// This tree contains Resoures bundled in pages.
|
|
treeResources *doctree.NodeShiftTree[contentNodeI]
|
|
|
|
// All pages and resources.
|
|
treePagesResources doctree.WalkableTrees[contentNodeI]
|
|
|
|
// This tree contains all taxonomy entries, e.g "/tags/blue/page1"
|
|
treeTaxonomyEntries *doctree.TreeShiftTree[*weightedContentNode]
|
|
|
|
// A slice of the resource trees.
|
|
resourceTrees doctree.MutableTrees
|
|
}
|
|
|
|
// collectIdentities collects all identities from in all trees matching the given key.
|
|
// This will at most match in one tree, but may give identies from multiple dimensions (e.g. language).
|
|
func (t *pageTrees) collectIdentities(key string) []identity.Identity {
|
|
var ids []identity.Identity
|
|
if n := t.treePages.Get(key); n != nil {
|
|
n.ForEeachIdentity(func(id identity.Identity) bool {
|
|
ids = append(ids, id)
|
|
return false
|
|
})
|
|
}
|
|
if n := t.treeResources.Get(key); n != nil {
|
|
n.ForEeachIdentity(func(id identity.Identity) bool {
|
|
ids = append(ids, id)
|
|
return false
|
|
})
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
// collectIdentitiesSurrounding collects all identities surrounding the given key.
|
|
func (t *pageTrees) collectIdentitiesSurrounding(key string, maxSamplesPerTree int) []identity.Identity {
|
|
// TODO1 test language coverage from this.
|
|
ids := t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treePages)
|
|
ids = append(ids, t.collectIdentitiesSurroundingIn(key, maxSamplesPerTree, t.treeResources)...)
|
|
return ids
|
|
}
|
|
|
|
func (t *pageTrees) collectIdentitiesSurroundingIn(key string, maxSamples int, tree *doctree.NodeShiftTree[contentNodeI]) []identity.Identity {
|
|
var ids []identity.Identity
|
|
section, ok := tree.LongestPrefixAll(path.Dir(key))
|
|
if ok {
|
|
count := 0
|
|
prefix := section + "/"
|
|
level := strings.Count(prefix, "/")
|
|
tree.WalkPrefixRaw(prefix, func(s string, n contentNodeI) bool {
|
|
if level != strings.Count(s, "/") {
|
|
return true
|
|
}
|
|
n.ForEeachIdentity(func(id identity.Identity) bool {
|
|
ids = append(ids, id)
|
|
return false
|
|
})
|
|
count++
|
|
return count > maxSamples
|
|
})
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
func (t *pageTrees) DeletePageAndResourcesBelow(ss ...string) {
|
|
commit1 := t.resourceTrees.Lock(true)
|
|
defer commit1()
|
|
commit2 := t.treePages.Lock(true)
|
|
defer commit2()
|
|
for _, s := range ss {
|
|
t.resourceTrees.DeletePrefix(paths.AddTrailingSlash(s))
|
|
t.treePages.Delete(s)
|
|
}
|
|
}
|
|
|
|
// Shape shapes all trees in t to the given dimension.
|
|
func (t pageTrees) Shape(d, v int) *pageTrees {
|
|
t.treePages = t.treePages.Shape(d, v)
|
|
t.treeResources = t.treeResources.Shape(d, v)
|
|
t.treeTaxonomyEntries = t.treeTaxonomyEntries.Shape(d, v)
|
|
|
|
return &t
|
|
}
|
|
|
|
var (
|
|
_ resource.Identifier = pageMapQueryPagesInSection{}
|
|
_ resource.Identifier = pageMapQueryPagesBelowPath{}
|
|
)
|
|
|
|
type pageMapQueryPagesInSection struct {
|
|
pageMapQueryPagesBelowPath
|
|
|
|
Recursive bool
|
|
IncludeSelf bool
|
|
}
|
|
|
|
func (q pageMapQueryPagesInSection) Key() string {
|
|
return "gagesInSection" + "/" + q.pageMapQueryPagesBelowPath.Key() + "/" + strconv.FormatBool(q.Recursive) + "/" + strconv.FormatBool(q.IncludeSelf)
|
|
}
|
|
|
|
// This needs to be hashable.
|
|
type pageMapQueryPagesBelowPath struct {
|
|
Path string
|
|
|
|
// Additional identifier for this query.
|
|
// Used as part of the cache key.
|
|
KeyPart string
|
|
|
|
// Page inclusion filter.
|
|
// May be nil.
|
|
Include predicate.P[*pageState]
|
|
}
|
|
|
|
func (q pageMapQueryPagesBelowPath) Key() string {
|
|
return q.Path + "/" + q.KeyPart
|
|
}
|
|
|
|
// Apply fn to all pages in m matching the given predicate.
|
|
// fn may return true to stop the walk.
|
|
func (m *pageMap) forEachPage(include predicate.P[*pageState], fn func(p *pageState) (bool, error)) error {
|
|
if include == nil {
|
|
include = func(p *pageState) bool {
|
|
return true
|
|
}
|
|
}
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: m.treePages,
|
|
LockType: doctree.LockTypeRead,
|
|
Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if p, ok := n.(*pageState); ok && include(p) {
|
|
if terminate, err := fn(p); terminate || err != nil {
|
|
return terminate, err
|
|
}
|
|
}
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
return w.Walk(context.Background())
|
|
}
|
|
|
|
func (m *pageMap) forEeachPageIncludingBundledPages(include predicate.P[*pageState], fn func(p *pageState) (bool, error)) error {
|
|
if include == nil {
|
|
include = func(p *pageState) bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if err := m.forEachPage(include, fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: m.treeResources,
|
|
LockType: doctree.LockTypeRead,
|
|
Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if rs, ok := n.(*resourceSource); ok {
|
|
if p, ok := rs.r.(*pageState); ok && include(p) {
|
|
if terminate, err := fn(p); terminate || err != nil {
|
|
return terminate, err
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
return w.Walk(context.Background())
|
|
}
|
|
|
|
func (m *pageMap) getOrCreatePagesFromCache(
|
|
key string, create func(string) (page.Pages, error),
|
|
) (page.Pages, error) {
|
|
return m.cachePages.GetOrCreate(key, create)
|
|
}
|
|
|
|
func (m *pageMap) getPagesInSection(q pageMapQueryPagesInSection) page.Pages {
|
|
cacheKey := q.Key()
|
|
|
|
pages, err := m.getOrCreatePagesFromCache(cacheKey, func(string) (page.Pages, error) {
|
|
prefix := paths.AddTrailingSlash(q.Path)
|
|
|
|
var (
|
|
pas page.Pages
|
|
otherBranch string
|
|
)
|
|
|
|
include := q.Include
|
|
if include == nil {
|
|
include = pagePredicates.ShouldListLocal
|
|
}
|
|
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: m.treePages,
|
|
Prefix: prefix,
|
|
Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if q.Recursive {
|
|
if p, ok := n.(*pageState); ok && include(p) {
|
|
pas = append(pas, p)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// We store both leafs and branches in the same tree, so for non-recursive walks,
|
|
// we need to walk until the end, but can skip
|
|
// any not belonging to child branches.
|
|
if otherBranch != "" && strings.HasPrefix(key, otherBranch) {
|
|
return false, nil
|
|
}
|
|
|
|
if p, ok := n.(*pageState); ok && include(p) {
|
|
pas = append(pas, p)
|
|
}
|
|
|
|
if n.isContentNodeBranch() {
|
|
otherBranch = key + "/"
|
|
}
|
|
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
err := w.Walk(context.Background())
|
|
|
|
if err == nil {
|
|
if q.IncludeSelf {
|
|
if n := m.treePages.Get(q.Path); n != nil {
|
|
if p, ok := n.(*pageState); ok && include(p) {
|
|
pas = append(pas, p)
|
|
}
|
|
}
|
|
}
|
|
page.SortByDefault(pas)
|
|
}
|
|
|
|
return pas, err
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return pages
|
|
}
|
|
|
|
func (m *pageMap) getPagesWithTerm(q pageMapQueryPagesBelowPath) page.Pages {
|
|
key := q.Key()
|
|
|
|
v, err := m.cachePages.GetOrCreate(key, func(string) (page.Pages, error) {
|
|
var pas page.Pages
|
|
include := q.Include
|
|
if include == nil {
|
|
include = pagePredicates.ShouldListLocal
|
|
}
|
|
|
|
err := m.treeTaxonomyEntries.WalkPrefix(
|
|
doctree.LockTypeNone,
|
|
paths.AddTrailingSlash(q.Path),
|
|
func(s string, n *weightedContentNode) (bool, error) {
|
|
p := n.n.(*pageState)
|
|
if !include(p) {
|
|
return false, nil
|
|
}
|
|
pas = append(pas, pageWithWeight0{n.weight, p})
|
|
return false, nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
page.SortByDefault(pas)
|
|
|
|
return pas, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func (m *pageMap) getTermsForPageInTaxonomy(path, taxonomy string) page.Pages {
|
|
prefix := paths.AddLeadingSlash(taxonomy)
|
|
|
|
v, err := m.cachePages.GetOrCreate(prefix+path, func(string) (page.Pages, error) {
|
|
var pas page.Pages
|
|
|
|
err := m.treeTaxonomyEntries.WalkPrefix(
|
|
doctree.LockTypeNone,
|
|
paths.AddTrailingSlash(prefix),
|
|
func(s string, n *weightedContentNode) (bool, error) {
|
|
if strings.HasSuffix(s, path) {
|
|
pas = append(pas, n.term)
|
|
}
|
|
return false, nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
page.SortByDefault(pas)
|
|
|
|
return pas, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func (m *pageMap) forEachResourceInPage(
|
|
ps *pageState,
|
|
lockType doctree.LockType,
|
|
exact bool,
|
|
handle func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error),
|
|
) error {
|
|
keyPage := ps.Path()
|
|
if keyPage == "/" {
|
|
keyPage = ""
|
|
}
|
|
prefix := paths.AddTrailingSlash(ps.Path())
|
|
isBranch := ps.IsNode()
|
|
|
|
rw := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: m.treeResources,
|
|
Prefix: prefix,
|
|
LockType: lockType,
|
|
Exact: exact,
|
|
}
|
|
|
|
rw.Handle = func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if isBranch {
|
|
ownerKey, _ := m.treePages.LongestPrefixAll(resourceKey)
|
|
if ownerKey != keyPage {
|
|
// Stop walking downwards, someone else owns this resource.
|
|
rw.SkipPrefix(ownerKey + "/")
|
|
return false, nil
|
|
}
|
|
}
|
|
return handle(resourceKey, n, match)
|
|
}
|
|
|
|
return rw.Walk(context.Background())
|
|
}
|
|
|
|
func (m *pageMap) getResourcesForPage(ps *pageState) (resource.Resources, error) {
|
|
var res resource.Resources
|
|
m.forEachResourceInPage(ps, doctree.LockTypeNone, false, func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
rs := n.(*resourceSource)
|
|
if rs.r != nil {
|
|
res = append(res, rs.r)
|
|
}
|
|
return false, nil
|
|
})
|
|
return res, nil
|
|
}
|
|
|
|
func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources {
|
|
keyPage := ps.Path()
|
|
if keyPage == "/" {
|
|
keyPage = ""
|
|
}
|
|
key := keyPage + "/get-resources-for-page"
|
|
v, err := m.cacheResources.GetOrCreate(key, func(string) (resource.Resources, error) {
|
|
res, err := m.getResourcesForPage(ps)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if translationKey := ps.m.translationKey; translationKey != "" {
|
|
// This this should not be a very common case.
|
|
// Merge in resources from the other languages.
|
|
translatedPages, _ := m.s.h.translationKeyPages.Get(translationKey)
|
|
for _, tp := range translatedPages {
|
|
if tp == ps {
|
|
continue
|
|
}
|
|
tps := tp.(*pageState)
|
|
// Make sure we query from the correct language root.
|
|
res2, err := tps.s.pageMap.getResourcesForPage(tps)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Add if Name not already in res.
|
|
for _, r := range res2 {
|
|
var found bool
|
|
for _, r2 := range res {
|
|
if r2.Name() == r.Name() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
res = append(res, r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lessFunc := func(i, j int) bool {
|
|
ri, rj := res[i], res[j]
|
|
if ri.ResourceType() < rj.ResourceType() {
|
|
return true
|
|
}
|
|
|
|
p1, ok1 := ri.(page.Page)
|
|
p2, ok2 := rj.(page.Page)
|
|
|
|
if ok1 != ok2 {
|
|
// Pull pages behind other resources.
|
|
|
|
return ok2
|
|
}
|
|
|
|
if ok1 {
|
|
return page.DefaultPageSort(p1, p2)
|
|
}
|
|
|
|
// Make sure not to use RelPermalink or any of the other methods that
|
|
// trigger lazy publishing.
|
|
return ri.Name() < rj.Name()
|
|
}
|
|
|
|
sort.SliceStable(res, lessFunc)
|
|
|
|
if len(ps.m.resourcesMetadata) > 0 {
|
|
for i, r := range res {
|
|
res[i] = resources.CloneWithMetadataIfNeeded(ps.m.resourcesMetadata, r)
|
|
}
|
|
sort.SliceStable(res, lessFunc)
|
|
}
|
|
|
|
return res, nil
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
type weightedContentNode struct {
|
|
n contentNodeI
|
|
weight int
|
|
term *pageWithOrdinal
|
|
}
|
|
|
|
type buildStateReseter interface {
|
|
resetBuildState()
|
|
}
|
|
|
|
type contentNodeI interface {
|
|
identity.IdentityProvider
|
|
identity.ForEeachIdentityProvider
|
|
Path() string
|
|
isContentNodeBranch() bool
|
|
buildStateReseter
|
|
resource.StaleMarker
|
|
}
|
|
|
|
var _ contentNodeI = (*contentNodeIs)(nil)
|
|
|
|
type contentNodeIs []contentNodeI
|
|
|
|
func (n contentNodeIs) Path() string {
|
|
return n[0].Path()
|
|
}
|
|
|
|
func (n contentNodeIs) isContentNodeBranch() bool {
|
|
return n[0].isContentNodeBranch()
|
|
}
|
|
|
|
func (n contentNodeIs) GetIdentity() identity.Identity {
|
|
return n[0].GetIdentity()
|
|
}
|
|
|
|
func (n contentNodeIs) ForEeachIdentity(f func(identity.Identity) bool) {
|
|
for _, nn := range n {
|
|
if nn != nil {
|
|
nn.ForEeachIdentity(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (n contentNodeIs) resetBuildState() {
|
|
for _, nn := range n {
|
|
if nn != nil {
|
|
nn.resetBuildState()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (n contentNodeIs) MarkStale() {
|
|
for _, nn := range n {
|
|
if nn != nil {
|
|
nn.MarkStale()
|
|
}
|
|
}
|
|
}
|
|
|
|
type contentNodeShifter struct {
|
|
numLanguages int
|
|
}
|
|
|
|
func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (bool, bool) {
|
|
lidx := dimension[0]
|
|
switch v := n.(type) {
|
|
case contentNodeIs:
|
|
resource.MarkStale(v[lidx])
|
|
wasDeleted := v[lidx] != nil
|
|
v[lidx] = nil
|
|
isEmpty := true
|
|
for _, vv := range v {
|
|
if vv != nil {
|
|
isEmpty = false
|
|
break
|
|
}
|
|
}
|
|
return wasDeleted, isEmpty
|
|
case resourceSources:
|
|
resource.MarkStale(v[lidx])
|
|
wasDeleted := v[lidx] != nil
|
|
v[lidx] = nil
|
|
isEmpty := true
|
|
for _, vv := range v {
|
|
if vv != nil {
|
|
isEmpty = false
|
|
break
|
|
}
|
|
}
|
|
return wasDeleted, isEmpty
|
|
case *resourceSource:
|
|
resource.MarkStale(v)
|
|
return true, true
|
|
case *pageState:
|
|
resource.MarkStale(v)
|
|
return true, true
|
|
default:
|
|
panic(fmt.Sprintf("unknown type %T", n))
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) Shift(n contentNodeI, dimension doctree.Dimension, exact bool) (contentNodeI, bool, doctree.DimensionFlag) {
|
|
lidx := dimension[0]
|
|
// How accurate is the match.
|
|
accuracy := doctree.DimensionLanguage
|
|
switch v := n.(type) {
|
|
case contentNodeIs:
|
|
if len(v) == 0 {
|
|
panic("empty contentNodeIs")
|
|
}
|
|
vv := v[lidx]
|
|
if vv != nil {
|
|
return vv, true, accuracy
|
|
}
|
|
return nil, false, 0
|
|
case resourceSources:
|
|
vv := v[lidx]
|
|
if vv != nil {
|
|
return vv, true, doctree.DimensionLanguage
|
|
}
|
|
if exact {
|
|
return nil, false, 0
|
|
}
|
|
// For non content resources, pick the first match.
|
|
for _, vv := range v {
|
|
if vv != nil {
|
|
if vv.isPage() {
|
|
return nil, false, 0
|
|
}
|
|
return vv, true, 0
|
|
}
|
|
}
|
|
case *resourceSource:
|
|
if v.LangIndex() == lidx {
|
|
return v, true, doctree.DimensionLanguage
|
|
}
|
|
if !v.isPage() && !exact {
|
|
return v, true, 0
|
|
}
|
|
case *pageState:
|
|
if v.s.languagei == lidx {
|
|
return n, true, doctree.DimensionLanguage
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unknown type %T", n))
|
|
}
|
|
return nil, false, 0
|
|
}
|
|
|
|
func (s *contentNodeShifter) ForEeachInDimension(n contentNodeI, d int, f func(contentNodeI) bool) {
|
|
if d != doctree.DimensionLanguage.Index() {
|
|
panic("only language dimension supported")
|
|
}
|
|
|
|
switch vv := n.(type) {
|
|
case contentNodeIs:
|
|
for _, v := range vv {
|
|
if v != nil {
|
|
if f(v) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
f(vv)
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) contentNodeI {
|
|
langi := dimension[doctree.DimensionLanguage.Index()]
|
|
switch vv := old.(type) {
|
|
case *pageState:
|
|
newp, ok := new.(*pageState)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown type %T", new))
|
|
}
|
|
if vv.s.languagei == newp.s.languagei && newp.s.languagei == langi {
|
|
return new
|
|
}
|
|
is := make(contentNodeIs, s.numLanguages)
|
|
is[vv.s.languagei] = old
|
|
is[langi] = new
|
|
return is
|
|
case contentNodeIs:
|
|
vv[langi] = new
|
|
return vv
|
|
case resourceSources:
|
|
vv[langi] = new.(*resourceSource)
|
|
return vv
|
|
case *resourceSource:
|
|
newp, ok := new.(*resourceSource)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown type %T", new))
|
|
}
|
|
if vv.LangIndex() == newp.LangIndex() && newp.LangIndex() == langi {
|
|
return new
|
|
}
|
|
rs := make(resourceSources, s.numLanguages)
|
|
rs[vv.LangIndex()] = vv
|
|
rs[langi] = newp
|
|
return rs
|
|
|
|
default:
|
|
panic(fmt.Sprintf("unknown type %T", old))
|
|
}
|
|
}
|
|
|
|
func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI {
|
|
switch vv := old.(type) {
|
|
case *pageState:
|
|
newp, ok := new.(*pageState)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown type %T", new))
|
|
}
|
|
if vv.s.languagei == newp.s.languagei {
|
|
return new
|
|
}
|
|
is := make(contentNodeIs, s.numLanguages)
|
|
is[newp.s.languagei] = new
|
|
is[vv.s.languagei] = old
|
|
return is
|
|
case contentNodeIs:
|
|
newp, ok := new.(*pageState)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown type %T", new))
|
|
}
|
|
vv[newp.s.languagei] = new
|
|
return vv
|
|
case *resourceSource:
|
|
newp, ok := new.(*resourceSource)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown type %T", new))
|
|
}
|
|
if vv.LangIndex() == newp.LangIndex() {
|
|
return new
|
|
}
|
|
rs := make(resourceSources, s.numLanguages)
|
|
rs[newp.LangIndex()] = newp
|
|
rs[vv.LangIndex()] = vv
|
|
return rs
|
|
case resourceSources:
|
|
newp, ok := new.(*resourceSource)
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown type %T", new))
|
|
}
|
|
vv[newp.LangIndex()] = newp
|
|
return vv
|
|
default:
|
|
panic(fmt.Sprintf("unknown type %T", old))
|
|
}
|
|
}
|
|
|
|
func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *pageMap {
|
|
var m *pageMap
|
|
|
|
var taxonomiesConfig taxonomiesConfig = s.conf.Taxonomies
|
|
|
|
m = &pageMap{
|
|
pageTrees: pageTrees.Shape(0, i),
|
|
cachePages: dynacache.GetOrCreatePartition[string, page.Pages](mcache, fmt.Sprintf("/pags/%d", i), dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}),
|
|
cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources](mcache, fmt.Sprintf("/ress/%d", i), dynacache.OptionsPartition{Weight: 60, ClearWhen: dynacache.ClearOnRebuild}),
|
|
cacheContentRendered: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentSummary]](mcache, fmt.Sprintf("/cont/ren/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
|
|
cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
|
|
contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
|
|
cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](mcache, fmt.Sprintf("/cont/src/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
|
|
|
|
cfg: contentMapConfig{
|
|
lang: s.Lang(),
|
|
taxonomyConfig: taxonomiesConfig.Values(),
|
|
taxonomyDisabled: !s.conf.IsKindEnabled(kinds.KindTaxonomy),
|
|
taxonomyTermDisabled: !s.conf.IsKindEnabled(kinds.KindTerm),
|
|
pageDisabled: !s.conf.IsKindEnabled(kinds.KindPage),
|
|
},
|
|
i: i,
|
|
s: s,
|
|
}
|
|
|
|
m.pageReverseIndex = &contentTreeReverseIndex{
|
|
initFn: func(rm map[any]contentNodeI) {
|
|
add := func(k string, n contentNodeI) {
|
|
existing, found := rm[k]
|
|
if found && existing != ambiguousContentNode {
|
|
rm[k] = ambiguousContentNode
|
|
} else if !found {
|
|
rm[k] = n
|
|
}
|
|
}
|
|
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: m.treePages,
|
|
LockType: doctree.LockTypeRead,
|
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
p := n.(*pageState)
|
|
if p.File() != nil {
|
|
add(p.File().FileInfo().Meta().PathInfo.BaseNameNoIdentifier(), p)
|
|
}
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
if err := w.Walk(context.Background()); err != nil {
|
|
panic(err)
|
|
}
|
|
},
|
|
contentTreeReverseIndexMap: &contentTreeReverseIndexMap{},
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
type contentTreeReverseIndex struct {
|
|
initFn func(rm map[any]contentNodeI)
|
|
*contentTreeReverseIndexMap
|
|
}
|
|
|
|
func (c *contentTreeReverseIndex) Reset() {
|
|
c.contentTreeReverseIndexMap = &contentTreeReverseIndexMap{
|
|
m: make(map[any]contentNodeI),
|
|
}
|
|
}
|
|
|
|
func (c *contentTreeReverseIndex) Get(key any) contentNodeI {
|
|
c.init.Do(func() {
|
|
c.m = make(map[any]contentNodeI)
|
|
c.initFn(c.contentTreeReverseIndexMap.m)
|
|
})
|
|
return c.m[key]
|
|
}
|
|
|
|
type contentTreeReverseIndexMap struct {
|
|
init sync.Once
|
|
m map[any]contentNodeI
|
|
}
|
|
|
|
type sitePagesAssembler struct {
|
|
*Site
|
|
watching bool
|
|
incomingChanges *whatChanged
|
|
assembleChanges *whatChanged
|
|
ctx context.Context
|
|
}
|
|
|
|
func (m *pageMap) debugPrint(prefix string, maxLevel int, w io.Writer) {
|
|
noshift := false
|
|
var prevKey string
|
|
|
|
pageWalker := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
NoShift: noshift,
|
|
Tree: m.treePages,
|
|
Prefix: prefix,
|
|
WalkContext: &doctree.WalkContext[contentNodeI]{},
|
|
}
|
|
|
|
resourceWalker := pageWalker.Extend()
|
|
resourceWalker.Tree = m.treeResources
|
|
|
|
pageWalker.Handle = func(keyPage string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
level := strings.Count(keyPage, "/")
|
|
if level > maxLevel {
|
|
return false, nil
|
|
}
|
|
const indentStr = " "
|
|
p := n.(*pageState)
|
|
s := strings.TrimPrefix(keyPage, paths.CommonDir(prevKey, keyPage))
|
|
lenIndent := len(keyPage) - len(s)
|
|
fmt.Fprint(w, strings.Repeat(indentStr, lenIndent))
|
|
info := fmt.Sprintf("%s lm: %s (%s)", s, p.Lastmod().Format("2006-01-02"), p.Kind())
|
|
fmt.Fprintln(w, info)
|
|
switch p.Kind() {
|
|
case kinds.KindTerm:
|
|
m.treeTaxonomyEntries.WalkPrefix(
|
|
doctree.LockTypeNone,
|
|
keyPage+"/",
|
|
func(s string, n *weightedContentNode) (bool, error) {
|
|
fmt.Fprint(w, strings.Repeat(indentStr, lenIndent+4))
|
|
fmt.Fprintln(w, s)
|
|
return false, nil
|
|
},
|
|
)
|
|
}
|
|
|
|
isBranch := n.isContentNodeBranch()
|
|
prevKey = keyPage
|
|
resourceWalker.Prefix = keyPage + "/"
|
|
|
|
resourceWalker.Handle = func(ss string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if isBranch {
|
|
ownerKey, _ := pageWalker.Tree.LongestPrefix(ss, true, nil)
|
|
if ownerKey != keyPage {
|
|
// Stop walking downwards, someone else owns this resource.
|
|
pageWalker.SkipPrefix(ownerKey + "/")
|
|
return false, nil
|
|
}
|
|
}
|
|
fmt.Fprint(w, strings.Repeat(indentStr, lenIndent+8))
|
|
fmt.Fprintln(w, ss+" (resource)")
|
|
return false, nil
|
|
}
|
|
|
|
return false, resourceWalker.Walk(context.Background())
|
|
}
|
|
|
|
err := pageWalker.Walk(context.Background())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (h *HugoSites) resolveAndClearStateForIdentities(
|
|
ctx context.Context,
|
|
l logg.LevelLogger,
|
|
cachebuster func(s string) bool, changes []identity.Identity,
|
|
) error {
|
|
h.Log.Debug().Log(logg.StringFunc(
|
|
func() string {
|
|
var sb strings.Builder
|
|
for _, change := range changes {
|
|
var key string
|
|
if kp, ok := change.(resource.Identifier); ok {
|
|
key = " " + kp.Key()
|
|
}
|
|
sb.WriteString(fmt.Sprintf("Direct dependencies of %q (%T%s) =>\n", change.IdentifierBase(), change, key))
|
|
seen := map[string]bool{
|
|
change.IdentifierBase(): true,
|
|
}
|
|
// Print the top level dependenies.
|
|
identity.WalkIdentitiesDeep(change, func(level int, id identity.Identity) bool {
|
|
if level > 1 {
|
|
return true
|
|
}
|
|
if !seen[id.IdentifierBase()] {
|
|
sb.WriteString(fmt.Sprintf(" %s%s\n", strings.Repeat(" ", level), id.IdentifierBase()))
|
|
}
|
|
seen[id.IdentifierBase()] = true
|
|
return false
|
|
})
|
|
}
|
|
return sb.String()
|
|
}),
|
|
)
|
|
|
|
for _, id := range changes {
|
|
if staler, ok := id.(resource.Staler); ok {
|
|
h.Log.Trace(logg.StringFunc(func() string { return fmt.Sprintf("Marking stale: %s (%T)\n", id, id) }))
|
|
staler.MarkStale()
|
|
}
|
|
}
|
|
|
|
// The order matters here:
|
|
// 1. Handle the cache busters first, as those may produce identities for the page reset step.
|
|
// 2. Then reset the page outputs, which may mark some resources as stale.
|
|
// 3. Then GC the cache.
|
|
if cachebuster != nil {
|
|
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
|
|
ll := l.WithField("substep", "gc dynacache cachebuster")
|
|
|
|
shouldDelete := func(k, v any) bool {
|
|
if cachebuster == nil {
|
|
return false
|
|
}
|
|
var b bool
|
|
if s, ok := k.(string); ok {
|
|
b = cachebuster(s)
|
|
}
|
|
|
|
if b {
|
|
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
|
|
// Add them to the change set so we can reset any page that depends on them.
|
|
changes = append(changes, id)
|
|
return false
|
|
})
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
h.MemCache.ClearMatching(shouldDelete)
|
|
|
|
return ll, nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Remove duplicates
|
|
seen := make(map[identity.Identity]bool)
|
|
var n int
|
|
for _, id := range changes {
|
|
if !seen[id] {
|
|
seen[id] = true
|
|
changes[n] = id
|
|
n++
|
|
}
|
|
}
|
|
changes = changes[:n]
|
|
|
|
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
|
|
// changesLeft: The IDs that the pages is dependent on.
|
|
// changesRight: The IDs that the pages depend on.
|
|
ll := l.WithField("substep", "resolve page output change set").WithField("changes", len(changes))
|
|
|
|
checkedCount, matchCount, err := h.resolveAndResetDependententPageOutputs(ctx, changes)
|
|
ll = ll.WithField("checked", checkedCount).WithField("matches", matchCount)
|
|
return ll, err
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
|
|
ll := l.WithField("substep", "gc dynacache")
|
|
|
|
h.MemCache.ClearOnRebuild(changes...)
|
|
h.Log.Trace(logg.StringFunc(func() string {
|
|
var sb strings.Builder
|
|
sb.WriteString("dynacache keys:\n")
|
|
for _, key := range h.MemCache.Keys(nil) {
|
|
sb.WriteString(fmt.Sprintf(" %s\n", key))
|
|
}
|
|
return sb.String()
|
|
}))
|
|
return ll, nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// The left change set is the IDs that the pages is dependent on.
|
|
// The right change set is the IDs that the pages depend on.
|
|
func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context, changes []identity.Identity) (int, int, error) {
|
|
if changes == nil {
|
|
return 0, 0, nil
|
|
}
|
|
|
|
// This can be shared (many of the same IDs are repeated).
|
|
depsFinder := identity.NewFinder(identity.FinderConfig{})
|
|
|
|
h.Log.Trace(logg.StringFunc(func() string {
|
|
var sb strings.Builder
|
|
sb.WriteString("resolve page dependencies: ")
|
|
for _, id := range changes {
|
|
sb.WriteString(fmt.Sprintf(" %T: %s|", id, id.IdentifierBase()))
|
|
}
|
|
return sb.String()
|
|
}))
|
|
|
|
var (
|
|
resetCounter atomic.Int64
|
|
checkedCounter atomic.Int64
|
|
)
|
|
|
|
resetPo := func(po *pageOutput, r identity.FinderResult) {
|
|
if po.pco != nil {
|
|
po.pco.Reset() // Will invalidate content cache.
|
|
}
|
|
|
|
po.renderState = 0
|
|
po.p.resourcesPublishInit = &sync.Once{}
|
|
if r == identity.FinderFoundOneOfMany {
|
|
// Will force a re-render even in fast render mode.
|
|
po.renderOnce = false
|
|
}
|
|
resetCounter.Add(1)
|
|
h.Log.Trace(logg.StringFunc(func() string {
|
|
p := po.p
|
|
return fmt.Sprintf("Resetting page output %s for %s for output %s\n", p.Kind(), p.Path(), po.f.Name)
|
|
}))
|
|
}
|
|
|
|
// This can be a relativeley expensive operations, so we do it in parallel.
|
|
g := rungroup.Run[*pageState](ctx, rungroup.Config[*pageState]{
|
|
NumWorkers: h.numWorkers,
|
|
Handle: func(ctx context.Context, p *pageState) error {
|
|
if !p.isRenderedAny() {
|
|
// This needs no reset, so no need to check it.
|
|
return nil
|
|
}
|
|
// First check the top level dependency manager.
|
|
for _, id := range changes {
|
|
checkedCounter.Add(1)
|
|
if r := depsFinder.Contains(id, p.dependencyManager, 100); r > identity.FinderFoundOneOfManyRepetition {
|
|
for _, po := range p.pageOutputs {
|
|
resetPo(po, r)
|
|
}
|
|
// Done.
|
|
return nil
|
|
}
|
|
}
|
|
// Then do a more fine grained reset for each output format.
|
|
OUTPUTS:
|
|
for _, po := range p.pageOutputs {
|
|
if !po.isRendered() {
|
|
continue
|
|
}
|
|
for _, id := range changes {
|
|
checkedCounter.Add(1)
|
|
if r := depsFinder.Contains(id, po.dependencyManagerOutput, 2); r > identity.FinderFoundOneOfManyRepetition {
|
|
resetPo(po, r)
|
|
continue OUTPUTS
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
|
|
h.withPage(func(s string, p *pageState) bool {
|
|
var needToCheck bool
|
|
for _, po := range p.pageOutputs {
|
|
if po.isRendered() {
|
|
needToCheck = true
|
|
break
|
|
}
|
|
}
|
|
if needToCheck {
|
|
g.Enqueue(p)
|
|
}
|
|
return false
|
|
})
|
|
|
|
err := g.Wait()
|
|
resetCount := int(resetCounter.Load())
|
|
checkedCount := int(checkedCounter.Load())
|
|
|
|
return checkedCount, resetCount, err
|
|
}
|
|
|
|
// Calculate and apply aggregate values to the page tree (e.g. dates, cascades).
|
|
func (sa *sitePagesAssembler) applyAggregates() error {
|
|
sectionPageCount := map[string]int{}
|
|
|
|
pw := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: sa.pageMap.treePages,
|
|
LockType: doctree.LockTypeRead,
|
|
WalkContext: &doctree.WalkContext[contentNodeI]{},
|
|
}
|
|
rw := pw.Extend()
|
|
rw.Tree = sa.pageMap.treeResources
|
|
sa.lastmod = time.Time{}
|
|
|
|
pw.Handle = func(keyPage string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
pageBundle := n.(*pageState)
|
|
|
|
if pageBundle.Kind() == kinds.KindTerm {
|
|
// Delay this until they're created.
|
|
return false, nil
|
|
}
|
|
|
|
if pageBundle.IsPage() {
|
|
rootSection := pageBundle.Section()
|
|
sectionPageCount[rootSection]++
|
|
}
|
|
|
|
// Handle cascades first to get any default dates set.
|
|
var cascade map[page.PageMatcher]maps.Params
|
|
if keyPage == "" {
|
|
// Home page gets it's cascade from the site config.
|
|
cascade = sa.conf.Cascade.Config
|
|
|
|
if pageBundle.m.cascade == nil {
|
|
// Pass the site cascade downwards.
|
|
pw.WalkContext.Data().Insert(keyPage, cascade)
|
|
}
|
|
} else {
|
|
_, data := pw.WalkContext.Data().LongestPrefix(keyPage)
|
|
if data != nil {
|
|
cascade = data.(map[page.PageMatcher]maps.Params)
|
|
}
|
|
}
|
|
|
|
if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 {
|
|
oldDates := pageBundle.m.dates
|
|
|
|
// We need to wait until after the walk to determine if any of the dates have changed.
|
|
pw.WalkContext.AddPostHook(
|
|
func() error {
|
|
if oldDates != pageBundle.m.dates {
|
|
sa.assembleChanges.Add(pageBundle)
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
}
|
|
|
|
// Combine the cascade map with front matter.
|
|
pageBundle.setMetaPost(cascade)
|
|
|
|
// We receive cascade values from above. If this leads to a change compared
|
|
// to the previous value, we need to mark the page and its dependencies as changed.
|
|
if pageBundle.m.setMetaPostCascadeChanged {
|
|
sa.assembleChanges.Add(pageBundle)
|
|
}
|
|
|
|
const eventName = "dates"
|
|
if n.isContentNodeBranch() {
|
|
if pageBundle.m.cascade != nil {
|
|
// Pass it down.
|
|
pw.WalkContext.Data().Insert(keyPage, pageBundle.m.cascade)
|
|
}
|
|
wasZeroDates := resource.IsZeroDates(pageBundle.m.dates)
|
|
if wasZeroDates || pageBundle.IsHome() {
|
|
pw.WalkContext.AddEventListener(eventName, keyPage, func(e *doctree.Event[contentNodeI]) {
|
|
sp, ok := e.Source.(*pageState)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if wasZeroDates {
|
|
pageBundle.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates)
|
|
}
|
|
|
|
if pageBundle.IsHome() {
|
|
if pageBundle.m.dates.Lastmod().After(pageBundle.s.lastmod) {
|
|
pageBundle.s.lastmod = pageBundle.m.dates.Lastmod()
|
|
}
|
|
if sp.m.dates.Lastmod().After(pageBundle.s.lastmod) {
|
|
pageBundle.s.lastmod = sp.m.dates.Lastmod()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Send the date info up the tree.
|
|
pw.WalkContext.SendEvent(&doctree.Event[contentNodeI]{Source: n, Path: keyPage, Name: eventName})
|
|
|
|
isBranch := n.isContentNodeBranch()
|
|
rw.Prefix = keyPage + "/"
|
|
|
|
rw.Handle = func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if isBranch {
|
|
ownerKey, _ := pw.Tree.LongestPrefix(resourceKey, true, nil)
|
|
if ownerKey != keyPage {
|
|
// Stop walking downwards, someone else owns this resource.
|
|
rw.SkipPrefix(ownerKey + "/")
|
|
return false, nil
|
|
}
|
|
}
|
|
rs := n.(*resourceSource)
|
|
if rs.isPage() {
|
|
pageResource := rs.r.(*pageState)
|
|
relPath := pageResource.m.pathInfo.BaseRel(pageBundle.m.pathInfo)
|
|
pageResource.m.resourcePath = relPath
|
|
var cascade map[page.PageMatcher]maps.Params
|
|
// Apply cascade (if set) to the page.
|
|
_, data := pw.WalkContext.Data().LongestPrefix(resourceKey)
|
|
if data != nil {
|
|
cascade = data.(map[page.PageMatcher]maps.Params)
|
|
}
|
|
pageResource.setMetaPost(cascade)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
return false, rw.Walk(sa.ctx)
|
|
}
|
|
|
|
if err := pw.Walk(sa.ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := pw.WalkContext.HandleEventsAndHooks(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !sa.s.conf.C.IsMainSectionsSet() {
|
|
var mainSection string
|
|
var maxcount int
|
|
for section, counter := range sectionPageCount {
|
|
if section != "" && counter > maxcount {
|
|
mainSection = section
|
|
maxcount = counter
|
|
}
|
|
}
|
|
sa.s.conf.C.SetMainSections([]string{mainSection})
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
|
|
walkContext := &doctree.WalkContext[contentNodeI]{}
|
|
|
|
handlePlural := func(key string) error {
|
|
var pw *doctree.NodeShiftTreeWalker[contentNodeI]
|
|
pw = &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: sa.pageMap.treePages,
|
|
Prefix: key, // We also want to include the root taxonomy nodes, so no trailing slash.
|
|
LockType: doctree.LockTypeRead,
|
|
WalkContext: walkContext,
|
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
p := n.(*pageState)
|
|
if p.Kind() != kinds.KindTerm {
|
|
// The other kinds were handled in applyAggregates.
|
|
if p.m.cascade != nil {
|
|
// Pass it down.
|
|
pw.WalkContext.Data().Insert(s, p.m.cascade)
|
|
}
|
|
}
|
|
|
|
if p.Kind() != kinds.KindTerm && p.Kind() != kinds.KindTaxonomy {
|
|
// Already handled.
|
|
return false, nil
|
|
}
|
|
|
|
const eventName = "dates"
|
|
|
|
if p.Kind() == kinds.KindTerm {
|
|
var cascade map[page.PageMatcher]maps.Params
|
|
_, data := pw.WalkContext.Data().LongestPrefix(s)
|
|
if data != nil {
|
|
cascade = data.(map[page.PageMatcher]maps.Params)
|
|
}
|
|
p.setMetaPost(cascade)
|
|
|
|
if err := sa.pageMap.treeTaxonomyEntries.WalkPrefix(
|
|
doctree.LockTypeRead,
|
|
paths.AddTrailingSlash(s),
|
|
func(ss string, wn *weightedContentNode) (bool, error) {
|
|
// Send the date info up the tree.
|
|
pw.WalkContext.SendEvent(&doctree.Event[contentNodeI]{Source: wn.n, Path: ss, Name: eventName})
|
|
return false, nil
|
|
},
|
|
); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
// Send the date info up the tree.
|
|
pw.WalkContext.SendEvent(&doctree.Event[contentNodeI]{Source: n, Path: s, Name: eventName})
|
|
|
|
if resource.IsZeroDates(p.m.dates) {
|
|
pw.WalkContext.AddEventListener(eventName, s, func(e *doctree.Event[contentNodeI]) {
|
|
sp, ok := e.Source.(*pageState)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
p.m.dates.UpdateDateAndLastmodIfAfter(sp.m.dates)
|
|
})
|
|
}
|
|
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
if err := pw.Walk(sa.ctx); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, viewName := range sa.pageMap.cfg.taxonomyConfig.views {
|
|
if err := handlePlural(viewName.pluralTreeKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := walkContext.HandleEventsAndHooks(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
|
|
var (
|
|
pages = sa.pageMap.treePages
|
|
entries = sa.pageMap.treeTaxonomyEntries
|
|
views = sa.pageMap.cfg.taxonomyConfig.views
|
|
)
|
|
|
|
lockType := doctree.LockTypeWrite
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: pages,
|
|
LockType: lockType,
|
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
ps := n.(*pageState)
|
|
|
|
if ps.m.noLink() {
|
|
return false, nil
|
|
}
|
|
|
|
// This is a little out of place, but is conveniently put here.
|
|
// Check if translationKey is set by user.
|
|
// This is to support the manual way of setting the translationKey in front matter.
|
|
if ps.m.translationKey != "" {
|
|
sa.s.h.translationKeyPages.Append(ps.m.translationKey, ps)
|
|
}
|
|
|
|
if sa.pageMap.cfg.taxonomyTermDisabled {
|
|
return false, nil
|
|
}
|
|
|
|
for _, viewName := range views {
|
|
vals := types.ToStringSlicePreserveString(getParam(ps, viewName.plural, false))
|
|
if vals == nil {
|
|
continue
|
|
}
|
|
|
|
w := getParamToLower(ps, viewName.plural+"_weight")
|
|
weight, err := cast.ToIntE(w)
|
|
if err != nil {
|
|
sa.Log.Warnf("Unable to convert taxonomy weight %#v to int for %q", w, n.Path())
|
|
// weight will equal zero, so let the flow continue
|
|
}
|
|
|
|
for i, v := range vals {
|
|
if v == "" {
|
|
continue
|
|
}
|
|
viewTermKey := "/" + viewName.plural + "/" + v
|
|
pi := sa.Site.Conf.PathParser().Parse(files.ComponentFolderContent, viewTermKey+"/_index.md")
|
|
term := pages.Get(pi.Base())
|
|
if term == nil {
|
|
m := &pageMeta{
|
|
term: v,
|
|
singular: viewName.singular,
|
|
s: sa.Site,
|
|
pathInfo: pi,
|
|
kind: kinds.KindTerm,
|
|
}
|
|
n, err := sa.h.newPage(m)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
pages.InsertIntoValuesDimension(pi.Base(), n)
|
|
term = pages.Get(pi.Base())
|
|
}
|
|
|
|
if s == "" {
|
|
// Consider making this the real value.
|
|
s = "/"
|
|
}
|
|
|
|
key := pi.Base() + s
|
|
|
|
entries.Insert(key, &weightedContentNode{
|
|
weight: weight,
|
|
n: n,
|
|
term: &pageWithOrdinal{pageState: term.(*pageState), ordinal: i},
|
|
})
|
|
}
|
|
}
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
return w.Walk(sa.ctx)
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) assembleResources() error {
|
|
pagesTree := sa.pageMap.treePages
|
|
resourcesTree := sa.pageMap.treeResources
|
|
|
|
lockType := doctree.LockTypeWrite
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: pagesTree,
|
|
LockType: lockType,
|
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
ps := n.(*pageState)
|
|
|
|
// Prepare resources for this page.
|
|
ps.shiftToOutputFormat(true, 0)
|
|
targetPaths := ps.targetPaths()
|
|
baseTarget := targetPaths.SubResourceBaseTarget
|
|
duplicateResourceFiles := true
|
|
if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.markup) {
|
|
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
|
|
}
|
|
|
|
duplicateResourceFiles = duplicateResourceFiles || ps.s.Conf.IsMultihost()
|
|
|
|
sa.pageMap.forEachResourceInPage(
|
|
ps, lockType,
|
|
!duplicateResourceFiles,
|
|
func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
rs := n.(*resourceSource)
|
|
if !match.Has(doctree.DimensionLanguage) {
|
|
// We got an alternative language version.
|
|
// Clone this and insert it into the tree.
|
|
rs = rs.clone()
|
|
resourcesTree.InsertIntoCurrentDimension(resourceKey, rs)
|
|
}
|
|
if rs.r != nil {
|
|
return false, nil
|
|
}
|
|
|
|
relPathOriginal := rs.path.PathRel(ps.m.pathInfo)
|
|
relPath := rs.path.BaseRel(ps.m.pathInfo)
|
|
|
|
var targetBasePaths []string
|
|
if ps.s.Conf.IsMultihost() {
|
|
baseTarget = targetPaths.SubResourceBaseLink
|
|
// In multihost we need to publish to the lang sub folder.
|
|
targetBasePaths = []string{ps.s.GetTargetLanguageBasePath()} // TODO(bep) we don't need this as a slice anymore.
|
|
|
|
}
|
|
|
|
rd := resources.ResourceSourceDescriptor{
|
|
OpenReadSeekCloser: rs.opener,
|
|
Path: rs.path,
|
|
GroupIdentity: rs.path,
|
|
TargetPath: relPathOriginal, // Use the original path for the target path, so the links can be guessed.
|
|
TargetBasePaths: targetBasePaths,
|
|
BasePathRelPermalink: targetPaths.SubResourceBaseLink,
|
|
BasePathTargetPath: baseTarget,
|
|
Name: relPath,
|
|
NameOriginal: relPathOriginal,
|
|
LazyPublish: !ps.m.buildConfig.PublishResources,
|
|
}
|
|
r, err := ps.m.s.ResourceSpec.NewResource(rd)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
rs.r = r
|
|
return false, nil
|
|
},
|
|
)
|
|
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
return w.Walk(sa.ctx)
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) assemblePagesStep1(ctx context.Context) error {
|
|
if err := sa.addMissingTaxonomies(); err != nil {
|
|
return err
|
|
}
|
|
if err := sa.addMissingRootSections(); err != nil {
|
|
return err
|
|
}
|
|
if err := sa.addStandalonePages(); err != nil {
|
|
return err
|
|
}
|
|
if err := sa.applyAggregates(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) assemblePagesStep2() error {
|
|
if err := sa.removeShouldNotBuild(); err != nil {
|
|
return err
|
|
}
|
|
if err := sa.assembleTermsAndTranslations(); err != nil {
|
|
return err
|
|
}
|
|
if err := sa.applyAggregatesToTaxonomiesAndTerms(); err != nil {
|
|
return err
|
|
}
|
|
if err := sa.assembleResources(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove any leftover node that we should not build for some reason (draft, expired, scheduled in the future).
|
|
// Note that for the home and section kinds we just disable the nodes to preserve the structure.
|
|
func (sa *sitePagesAssembler) removeShouldNotBuild() error {
|
|
s := sa.Site
|
|
var keys []string
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
LockType: doctree.LockTypeRead,
|
|
Tree: sa.pageMap.treePages,
|
|
Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
p := n.(*pageState)
|
|
if !s.shouldBuild(p) {
|
|
switch p.Kind() {
|
|
case kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy:
|
|
// We need to keep these for the structure, but disable
|
|
// them so they don't get listed/rendered.
|
|
(&p.m.buildConfig).Disable()
|
|
default:
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
return false, nil
|
|
},
|
|
}
|
|
if err := w.Walk(sa.ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
sa.pageMap.DeletePageAndResourcesBelow(keys...)
|
|
|
|
return nil
|
|
}
|
|
|
|
// // Create the fixed output pages, e.g. sitemap.xml, if not already there.
|
|
func (sa *sitePagesAssembler) addStandalonePages() error {
|
|
s := sa.Site
|
|
m := s.pageMap
|
|
tree := m.treePages
|
|
|
|
commit := tree.Lock(true)
|
|
defer commit()
|
|
|
|
addStandalone := func(key, kind string, f output.Format) {
|
|
if !s.Conf.IsMultihost() {
|
|
switch kind {
|
|
case kinds.KindSitemapIndex, kinds.KindRobotsTXT:
|
|
// Only one for all languages.
|
|
if s.languagei != 0 {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if !sa.Site.conf.IsKindEnabled(kind) || tree.Has(key) {
|
|
return
|
|
}
|
|
|
|
m := &pageMeta{
|
|
s: s,
|
|
pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
|
|
kind: kind,
|
|
standaloneOutputFormat: f,
|
|
}
|
|
|
|
p, _ := s.h.newPage(m)
|
|
|
|
tree.InsertIntoValuesDimension(key, p)
|
|
}
|
|
|
|
addStandalone("/404", kinds.KindStatus404, output.HTTPStatusHTMLFormat)
|
|
|
|
if s.conf.EnableRobotsTXT {
|
|
if m.i == 0 || s.Conf.IsMultihost() {
|
|
addStandalone("/robots", kinds.KindRobotsTXT, output.RobotsTxtFormat)
|
|
}
|
|
}
|
|
|
|
sitemapEnabled := false
|
|
for _, s := range s.h.Sites {
|
|
if s.conf.IsKindEnabled(kinds.KindSitemap) {
|
|
sitemapEnabled = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if sitemapEnabled {
|
|
addStandalone("/sitemap", kinds.KindSitemap, output.SitemapFormat)
|
|
skipSitemapIndex := s.Conf.IsMultihost() || !(s.Conf.DefaultContentLanguageInSubdir() || s.Conf.IsMultiLingual())
|
|
|
|
if !skipSitemapIndex {
|
|
addStandalone("/sitemapindex", kinds.KindSitemapIndex, output.SitemapIndexFormat)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) addMissingRootSections() error {
|
|
var hasHome bool
|
|
|
|
// Add missing root sections.
|
|
seen := map[string]bool{}
|
|
var w *doctree.NodeShiftTreeWalker[contentNodeI]
|
|
w = &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
LockType: doctree.LockTypeWrite,
|
|
Tree: sa.pageMap.treePages,
|
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
if n == nil {
|
|
panic("n is nil")
|
|
}
|
|
|
|
ps := n.(*pageState)
|
|
|
|
if ps.Lang() != sa.Lang() {
|
|
panic(fmt.Sprintf("lang mismatch: %q: %s != %s", s, ps.Lang(), sa.Lang()))
|
|
}
|
|
|
|
if s == "" {
|
|
hasHome = true
|
|
sa.home = ps
|
|
return false, nil
|
|
}
|
|
|
|
p := ps.m.pathInfo
|
|
section := p.Section()
|
|
if section == "" || seen[section] {
|
|
return false, nil
|
|
}
|
|
seen[section] = true
|
|
|
|
// Try to preserve the original casing if possible.
|
|
sectionUnnormalized := p.Unmormalized().Section()
|
|
pth := sa.s.Conf.PathParser().Parse(files.ComponentFolderContent, "/"+sectionUnnormalized+"/_index.md")
|
|
nn := w.Tree.Get(pth.Base())
|
|
|
|
if nn == nil {
|
|
m := &pageMeta{
|
|
s: sa.Site,
|
|
pathInfo: pth,
|
|
}
|
|
|
|
ps, err := sa.h.newPage(m)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
w.Tree.InsertIntoValuesDimension(pth.Base(), ps)
|
|
}
|
|
|
|
// /a/b, we don't need to walk deeper.
|
|
if strings.Count(s, "/") > 1 {
|
|
w.SkipPrefix(s + "/")
|
|
}
|
|
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
if err := w.Walk(sa.ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !hasHome {
|
|
p := sa.Site.Conf.PathParser().Parse(files.ComponentFolderContent, "/_index.md")
|
|
m := &pageMeta{
|
|
s: sa.Site,
|
|
pathInfo: p,
|
|
kind: kinds.KindHome,
|
|
}
|
|
n, err := sa.h.newPage(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.Tree.InsertWithLock(p.Base(), n)
|
|
sa.home = n
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sa *sitePagesAssembler) addMissingTaxonomies() error {
|
|
if sa.pageMap.cfg.taxonomyDisabled && sa.pageMap.cfg.taxonomyTermDisabled {
|
|
return nil
|
|
}
|
|
|
|
tree := sa.pageMap.treePages
|
|
|
|
commit := tree.Lock(true)
|
|
defer commit()
|
|
|
|
for _, viewName := range sa.pageMap.cfg.taxonomyConfig.views {
|
|
key := viewName.pluralTreeKey
|
|
if v := tree.Get(key); v == nil {
|
|
m := &pageMeta{
|
|
s: sa.Site,
|
|
pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"),
|
|
kind: kinds.KindTaxonomy,
|
|
singular: viewName.singular,
|
|
}
|
|
p, _ := sa.h.newPage(m)
|
|
tree.InsertIntoValuesDimension(key, p)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *pageMap) CreateSiteTaxonomies(ctx context.Context) error {
|
|
m.s.taxonomies = make(page.TaxonomyList)
|
|
|
|
if m.cfg.taxonomyDisabled && m.cfg.taxonomyTermDisabled {
|
|
return nil
|
|
}
|
|
|
|
for _, viewName := range m.cfg.taxonomyConfig.views {
|
|
key := viewName.pluralTreeKey
|
|
m.s.taxonomies[viewName.plural] = make(page.Taxonomy)
|
|
w := &doctree.NodeShiftTreeWalker[contentNodeI]{
|
|
Tree: m.treePages,
|
|
Prefix: paths.AddTrailingSlash(key),
|
|
LockType: doctree.LockTypeRead,
|
|
Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
|
|
p := n.(*pageState)
|
|
plural := p.Section()
|
|
|
|
switch p.Kind() {
|
|
case kinds.KindTerm:
|
|
taxonomy := m.s.taxonomies[plural]
|
|
if taxonomy == nil {
|
|
return true, fmt.Errorf("missing taxonomy: %s", plural)
|
|
}
|
|
k := strings.ToLower(p.m.term)
|
|
err := m.treeTaxonomyEntries.WalkPrefix(
|
|
doctree.LockTypeRead,
|
|
paths.AddTrailingSlash(s),
|
|
func(s string, wn *weightedContentNode) (bool, error) {
|
|
taxonomy[k] = append(taxonomy[k], page.NewWeightedPage(wn.weight, wn.n.(page.Page), wn.term.Page()))
|
|
return false, nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
default:
|
|
return false, nil
|
|
}
|
|
|
|
return false, nil
|
|
},
|
|
}
|
|
|
|
if err := w.Walk(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, taxonomy := range m.s.taxonomies {
|
|
for _, v := range taxonomy {
|
|
v.Sort()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type viewName struct {
|
|
singular string // e.g. "category"
|
|
plural string // e.g. "categories"
|
|
pluralTreeKey string
|
|
}
|
|
|
|
func (v viewName) IsZero() bool {
|
|
return v.singular == ""
|
|
}
|