hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

content_map_page.go (22837B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package hugolib
   15 
   16 import (
   17 	"context"
   18 	"fmt"
   19 	"path"
   20 	"path/filepath"
   21 	"strings"
   22 	"sync"
   23 
   24 	"github.com/gohugoio/hugo/common/maps"
   25 
   26 	"github.com/gohugoio/hugo/common/types"
   27 	"github.com/gohugoio/hugo/resources"
   28 
   29 	"github.com/gohugoio/hugo/common/hugio"
   30 	"github.com/gohugoio/hugo/hugofs"
   31 	"github.com/gohugoio/hugo/hugofs/files"
   32 	"github.com/gohugoio/hugo/parser/pageparser"
   33 	"github.com/gohugoio/hugo/resources/page"
   34 	"github.com/gohugoio/hugo/resources/resource"
   35 	"github.com/spf13/cast"
   36 
   37 	"github.com/gohugoio/hugo/common/para"
   38 )
   39 
   40 func newPageMaps(h *HugoSites) *pageMaps {
   41 	mps := make([]*pageMap, len(h.Sites))
   42 	for i, s := range h.Sites {
   43 		mps[i] = s.pageMap
   44 	}
   45 	return &pageMaps{
   46 		workers: para.New(h.numWorkers),
   47 		pmaps:   mps,
   48 	}
   49 }
   50 
   51 type pageMap struct {
   52 	s *Site
   53 	*contentMap
   54 }
   55 
   56 func (m *pageMap) Len() int {
   57 	l := 0
   58 	for _, t := range m.contentMap.pageTrees {
   59 		l += t.Len()
   60 	}
   61 	return l
   62 }
   63 
   64 func (m *pageMap) createMissingTaxonomyNodes() error {
   65 	if m.cfg.taxonomyDisabled {
   66 		return nil
   67 	}
   68 	m.taxonomyEntries.Walk(func(s string, v any) bool {
   69 		n := v.(*contentNode)
   70 		vi := n.viewInfo
   71 		k := cleanSectionTreeKey(vi.name.plural + "/" + vi.termKey)
   72 
   73 		if _, found := m.taxonomies.Get(k); !found {
   74 			vic := &contentBundleViewInfo{
   75 				name:       vi.name,
   76 				termKey:    vi.termKey,
   77 				termOrigin: vi.termOrigin,
   78 			}
   79 			m.taxonomies.Insert(k, &contentNode{viewInfo: vic})
   80 		}
   81 		return false
   82 	})
   83 
   84 	return nil
   85 }
   86 
   87 func (m *pageMap) newPageFromContentNode(n *contentNode, parentBucket *pagesMapBucket, owner *pageState) (*pageState, error) {
   88 	if n.fi == nil {
   89 		panic("FileInfo must (currently) be set")
   90 	}
   91 
   92 	f, err := newFileInfo(m.s.SourceSpec, n.fi)
   93 	if err != nil {
   94 		return nil, err
   95 	}
   96 
   97 	meta := n.fi.Meta()
   98 	content := func() (hugio.ReadSeekCloser, error) {
   99 		return meta.Open()
  100 	}
  101 
  102 	bundled := owner != nil
  103 	s := m.s
  104 
  105 	sections := s.sectionsFromFile(f)
  106 
  107 	kind := s.kindFromFileInfoOrSections(f, sections)
  108 	if kind == page.KindTerm {
  109 		s.PathSpec.MakePathsSanitized(sections)
  110 	}
  111 
  112 	metaProvider := &pageMeta{kind: kind, sections: sections, bundled: bundled, s: s, f: f}
  113 
  114 	ps, err := newPageBase(metaProvider)
  115 	if err != nil {
  116 		return nil, err
  117 	}
  118 
  119 	if n.fi.Meta().IsRootFile {
  120 		// Make sure that the bundle/section we start walking from is always
  121 		// rendered.
  122 		// This is only relevant in server fast render mode.
  123 		ps.forceRender = true
  124 	}
  125 
  126 	n.p = ps
  127 	if ps.IsNode() {
  128 		ps.bucket = newPageBucket(ps)
  129 	}
  130 
  131 	gi, err := s.h.gitInfoForPage(ps)
  132 	if err != nil {
  133 		return nil, fmt.Errorf("failed to load Git data: %w", err)
  134 	}
  135 	ps.gitInfo = gi
  136 
  137 	owners, err := s.h.codeownersForPage(ps)
  138 	if err != nil {
  139 		return nil, fmt.Errorf("failed to load CODEOWNERS: %w", err)
  140 	}
  141 	ps.codeowners = owners
  142 
  143 	r, err := content()
  144 	if err != nil {
  145 		return nil, err
  146 	}
  147 	defer r.Close()
  148 
  149 	parseResult, err := pageparser.Parse(
  150 		r,
  151 		pageparser.Config{EnableEmoji: s.siteCfg.enableEmoji},
  152 	)
  153 	if err != nil {
  154 		return nil, err
  155 	}
  156 
  157 	ps.pageContent = pageContent{
  158 		source: rawPageContent{
  159 			parsed:         parseResult,
  160 			posMainContent: -1,
  161 			posSummaryEnd:  -1,
  162 			posBodyStart:   -1,
  163 		},
  164 	}
  165 
  166 	if err := ps.mapContent(parentBucket, metaProvider); err != nil {
  167 		return nil, ps.wrapError(err)
  168 	}
  169 
  170 	if err := metaProvider.applyDefaultValues(n); err != nil {
  171 		return nil, err
  172 	}
  173 
  174 	ps.init.Add(func() (any, error) {
  175 		pp, err := newPagePaths(s, ps, metaProvider)
  176 		if err != nil {
  177 			return nil, err
  178 		}
  179 
  180 		outputFormatsForPage := ps.m.outputFormats()
  181 
  182 		// Prepare output formats for all sites.
  183 		// We do this even if this page does not get rendered on
  184 		// its own. It may be referenced via .Site.GetPage and
  185 		// it will then need an output format.
  186 		ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats))
  187 		created := make(map[string]*pageOutput)
  188 		shouldRenderPage := !ps.m.noRender()
  189 
  190 		for i, f := range ps.s.h.renderFormats {
  191 			if po, found := created[f.Name]; found {
  192 				ps.pageOutputs[i] = po
  193 				continue
  194 			}
  195 
  196 			render := shouldRenderPage
  197 			if render {
  198 				_, render = outputFormatsForPage.GetByName(f.Name)
  199 			}
  200 
  201 			po := newPageOutput(ps, pp, f, render)
  202 
  203 			// Create a content provider for the first,
  204 			// we may be able to reuse it.
  205 			if i == 0 {
  206 				contentProvider, err := newPageContentOutput(ps, po)
  207 				if err != nil {
  208 					return nil, err
  209 				}
  210 				po.initContentProvider(contentProvider)
  211 			}
  212 
  213 			ps.pageOutputs[i] = po
  214 			created[f.Name] = po
  215 
  216 		}
  217 
  218 		if err := ps.initCommonProviders(pp); err != nil {
  219 			return nil, err
  220 		}
  221 
  222 		return nil, nil
  223 	})
  224 
  225 	ps.parent = owner
  226 
  227 	return ps, nil
  228 }
  229 
  230 func (m *pageMap) newResource(fim hugofs.FileMetaInfo, owner *pageState) (resource.Resource, error) {
  231 	if owner == nil {
  232 		panic("owner is nil")
  233 	}
  234 	// TODO(bep) consolidate with multihost logic + clean up
  235 	outputFormats := owner.m.outputFormats()
  236 	seen := make(map[string]bool)
  237 	var targetBasePaths []string
  238 	// Make sure bundled resources are published to all of the output formats'
  239 	// sub paths.
  240 	for _, f := range outputFormats {
  241 		p := f.Path
  242 		if seen[p] {
  243 			continue
  244 		}
  245 		seen[p] = true
  246 		targetBasePaths = append(targetBasePaths, p)
  247 
  248 	}
  249 
  250 	meta := fim.Meta()
  251 	r := func() (hugio.ReadSeekCloser, error) {
  252 		return meta.Open()
  253 	}
  254 
  255 	target := strings.TrimPrefix(meta.Path, owner.File().Dir())
  256 
  257 	return owner.s.ResourceSpec.New(
  258 		resources.ResourceSourceDescriptor{
  259 			TargetPaths:        owner.getTargetPaths,
  260 			OpenReadSeekCloser: r,
  261 			FileInfo:           fim,
  262 			RelTargetFilename:  target,
  263 			TargetBasePaths:    targetBasePaths,
  264 			LazyPublish:        !owner.m.buildConfig.PublishResources,
  265 		})
  266 }
  267 
  268 func (m *pageMap) createSiteTaxonomies() error {
  269 	m.s.taxonomies = make(TaxonomyList)
  270 	var walkErr error
  271 	m.taxonomies.Walk(func(s string, v any) bool {
  272 		n := v.(*contentNode)
  273 		t := n.viewInfo
  274 
  275 		viewName := t.name
  276 
  277 		if t.termKey == "" {
  278 			m.s.taxonomies[viewName.plural] = make(Taxonomy)
  279 		} else {
  280 			taxonomy := m.s.taxonomies[viewName.plural]
  281 			if taxonomy == nil {
  282 				walkErr = fmt.Errorf("missing taxonomy: %s", viewName.plural)
  283 				return true
  284 			}
  285 			m.taxonomyEntries.WalkPrefix(s, func(ss string, v any) bool {
  286 				b2 := v.(*contentNode)
  287 				info := b2.viewInfo
  288 				taxonomy.add(info.termKey, page.NewWeightedPage(info.weight, info.ref.p, n.p))
  289 
  290 				return false
  291 			})
  292 		}
  293 
  294 		return false
  295 	})
  296 
  297 	for _, taxonomy := range m.s.taxonomies {
  298 		for _, v := range taxonomy {
  299 			v.Sort()
  300 		}
  301 	}
  302 
  303 	return walkErr
  304 }
  305 
  306 func (m *pageMap) createListAllPages() page.Pages {
  307 	pages := make(page.Pages, 0)
  308 
  309 	m.contentMap.pageTrees.Walk(func(s string, n *contentNode) bool {
  310 		if n.p == nil {
  311 			panic(fmt.Sprintf("BUG: page not set for %q", s))
  312 		}
  313 		if contentTreeNoListAlwaysFilter(s, n) {
  314 			return false
  315 		}
  316 		pages = append(pages, n.p)
  317 		return false
  318 	})
  319 
  320 	page.SortByDefault(pages)
  321 	return pages
  322 }
  323 
  324 func (m *pageMap) assemblePages() error {
  325 	m.taxonomyEntries.DeletePrefix("/")
  326 
  327 	if err := m.assembleSections(); err != nil {
  328 		return err
  329 	}
  330 
  331 	var err error
  332 
  333 	if err != nil {
  334 		return err
  335 	}
  336 
  337 	m.pages.Walk(func(s string, v any) bool {
  338 		n := v.(*contentNode)
  339 
  340 		var shouldBuild bool
  341 
  342 		defer func() {
  343 			// Make sure we always rebuild the view cache.
  344 			if shouldBuild && err == nil && n.p != nil {
  345 				m.attachPageToViews(s, n)
  346 			}
  347 		}()
  348 
  349 		if n.p != nil {
  350 			// A rebuild
  351 			shouldBuild = true
  352 			return false
  353 		}
  354 
  355 		var parent *contentNode
  356 		var parentBucket *pagesMapBucket
  357 
  358 		_, parent = m.getSection(s)
  359 		if parent == nil {
  360 			panic(fmt.Sprintf("BUG: parent not set for %q", s))
  361 		}
  362 		parentBucket = parent.p.bucket
  363 
  364 		n.p, err = m.newPageFromContentNode(n, parentBucket, nil)
  365 		if err != nil {
  366 			return true
  367 		}
  368 
  369 		shouldBuild = !(n.p.Kind() == page.KindPage && m.cfg.pageDisabled) && m.s.shouldBuild(n.p)
  370 		if !shouldBuild {
  371 			m.deletePage(s)
  372 			return false
  373 		}
  374 
  375 		n.p.treeRef = &contentTreeRef{
  376 			m:   m,
  377 			t:   m.pages,
  378 			n:   n,
  379 			key: s,
  380 		}
  381 
  382 		if err = m.assembleResources(s, n.p, parentBucket); err != nil {
  383 			return true
  384 		}
  385 
  386 		return false
  387 	})
  388 
  389 	m.deleteOrphanSections()
  390 
  391 	return err
  392 }
  393 
  394 func (m *pageMap) assembleResources(s string, p *pageState, parentBucket *pagesMapBucket) error {
  395 	var err error
  396 
  397 	m.resources.WalkPrefix(s, func(s string, v any) bool {
  398 		n := v.(*contentNode)
  399 		meta := n.fi.Meta()
  400 		classifier := meta.Classifier
  401 		var r resource.Resource
  402 		switch classifier {
  403 		case files.ContentClassContent:
  404 			var rp *pageState
  405 			rp, err = m.newPageFromContentNode(n, parentBucket, p)
  406 			if err != nil {
  407 				return true
  408 			}
  409 			rp.m.resourcePath = filepath.ToSlash(strings.TrimPrefix(rp.File().Path(), p.File().Dir()))
  410 			r = rp
  411 
  412 		case files.ContentClassFile:
  413 			r, err = m.newResource(n.fi, p)
  414 			if err != nil {
  415 				return true
  416 			}
  417 		default:
  418 			panic(fmt.Sprintf("invalid classifier: %q", classifier))
  419 		}
  420 
  421 		p.resources = append(p.resources, r)
  422 		return false
  423 	})
  424 
  425 	return err
  426 }
  427 
  428 func (m *pageMap) assembleSections() error {
  429 	var sectionsToDelete []string
  430 	var err error
  431 
  432 	m.sections.Walk(func(s string, v any) bool {
  433 		n := v.(*contentNode)
  434 		var shouldBuild bool
  435 
  436 		defer func() {
  437 			// Make sure we always rebuild the view cache.
  438 			if shouldBuild && err == nil && n.p != nil {
  439 				m.attachPageToViews(s, n)
  440 				if n.p.IsHome() {
  441 					m.s.home = n.p
  442 				}
  443 			}
  444 		}()
  445 
  446 		sections := m.splitKey(s)
  447 
  448 		if n.p != nil {
  449 			if n.p.IsHome() {
  450 				m.s.home = n.p
  451 			}
  452 			shouldBuild = true
  453 			return false
  454 		}
  455 
  456 		var parent *contentNode
  457 		var parentBucket *pagesMapBucket
  458 
  459 		if s != "/" {
  460 			_, parent = m.getSection(s)
  461 			if parent == nil || parent.p == nil {
  462 				panic(fmt.Sprintf("BUG: parent not set for %q", s))
  463 			}
  464 		}
  465 
  466 		if parent != nil {
  467 			parentBucket = parent.p.bucket
  468 		} else if s == "/" {
  469 			parentBucket = m.s.siteBucket
  470 		}
  471 
  472 		kind := page.KindSection
  473 		if s == "/" {
  474 			kind = page.KindHome
  475 		}
  476 
  477 		if n.fi != nil {
  478 			n.p, err = m.newPageFromContentNode(n, parentBucket, nil)
  479 			if err != nil {
  480 				return true
  481 			}
  482 		} else {
  483 			n.p = m.s.newPage(n, parentBucket, kind, "", sections...)
  484 		}
  485 
  486 		shouldBuild = m.s.shouldBuild(n.p)
  487 		if !shouldBuild {
  488 			sectionsToDelete = append(sectionsToDelete, s)
  489 			return false
  490 		}
  491 
  492 		n.p.treeRef = &contentTreeRef{
  493 			m:   m,
  494 			t:   m.sections,
  495 			n:   n,
  496 			key: s,
  497 		}
  498 
  499 		if err = m.assembleResources(s+cmLeafSeparator, n.p, parentBucket); err != nil {
  500 			return true
  501 		}
  502 
  503 		return false
  504 	})
  505 
  506 	for _, s := range sectionsToDelete {
  507 		m.deleteSectionByPath(s)
  508 	}
  509 
  510 	return err
  511 }
  512 
  513 func (m *pageMap) assembleTaxonomies() error {
  514 	var taxonomiesToDelete []string
  515 	var err error
  516 
  517 	m.taxonomies.Walk(func(s string, v any) bool {
  518 		n := v.(*contentNode)
  519 
  520 		if n.p != nil {
  521 			return false
  522 		}
  523 
  524 		kind := n.viewInfo.kind()
  525 		sections := n.viewInfo.sections()
  526 
  527 		_, parent := m.getTaxonomyParent(s)
  528 		if parent == nil || parent.p == nil {
  529 			panic(fmt.Sprintf("BUG: parent not set for %q", s))
  530 		}
  531 		parentBucket := parent.p.bucket
  532 
  533 		if n.fi != nil {
  534 			n.p, err = m.newPageFromContentNode(n, parent.p.bucket, nil)
  535 			if err != nil {
  536 				return true
  537 			}
  538 		} else {
  539 			title := ""
  540 			if kind == page.KindTerm {
  541 				title = n.viewInfo.term()
  542 			}
  543 			n.p = m.s.newPage(n, parent.p.bucket, kind, title, sections...)
  544 		}
  545 
  546 		if !m.s.shouldBuild(n.p) {
  547 			taxonomiesToDelete = append(taxonomiesToDelete, s)
  548 			return false
  549 		}
  550 
  551 		n.p.treeRef = &contentTreeRef{
  552 			m:   m,
  553 			t:   m.taxonomies,
  554 			n:   n,
  555 			key: s,
  556 		}
  557 
  558 		if err = m.assembleResources(s+cmLeafSeparator, n.p, parentBucket); err != nil {
  559 			return true
  560 		}
  561 
  562 		return false
  563 	})
  564 
  565 	for _, s := range taxonomiesToDelete {
  566 		m.deleteTaxonomy(s)
  567 	}
  568 
  569 	return err
  570 }
  571 
  572 func (m *pageMap) attachPageToViews(s string, b *contentNode) {
  573 	if m.cfg.taxonomyDisabled {
  574 		return
  575 	}
  576 
  577 	for _, viewName := range m.cfg.taxonomyConfig {
  578 		vals := types.ToStringSlicePreserveString(getParam(b.p, viewName.plural, false))
  579 		if vals == nil {
  580 			continue
  581 		}
  582 		w := getParamToLower(b.p, viewName.plural+"_weight")
  583 		weight, err := cast.ToIntE(w)
  584 		if err != nil {
  585 			m.s.Log.Errorf("Unable to convert taxonomy weight %#v to int for %q", w, b.p.Pathc())
  586 			// weight will equal zero, so let the flow continue
  587 		}
  588 
  589 		for i, v := range vals {
  590 			termKey := m.s.getTaxonomyKey(v)
  591 
  592 			bv := &contentNode{
  593 				viewInfo: &contentBundleViewInfo{
  594 					ordinal:    i,
  595 					name:       viewName,
  596 					termKey:    termKey,
  597 					termOrigin: v,
  598 					weight:     weight,
  599 					ref:        b,
  600 				},
  601 			}
  602 
  603 			var key string
  604 			if strings.HasSuffix(s, "/") {
  605 				key = cleanSectionTreeKey(path.Join(viewName.plural, termKey, s))
  606 			} else {
  607 				key = cleanTreeKey(path.Join(viewName.plural, termKey, s))
  608 			}
  609 			m.taxonomyEntries.Insert(key, bv)
  610 		}
  611 	}
  612 }
  613 
  614 type pageMapQuery struct {
  615 	Prefix string
  616 	Filter contentTreeNodeCallback
  617 }
  618 
  619 func (m *pageMap) collectPages(query pageMapQuery, fn func(c *contentNode)) error {
  620 	if query.Filter == nil {
  621 		query.Filter = contentTreeNoListAlwaysFilter
  622 	}
  623 
  624 	m.pages.WalkQuery(query, func(s string, n *contentNode) bool {
  625 		fn(n)
  626 		return false
  627 	})
  628 
  629 	return nil
  630 }
  631 
  632 func (m *pageMap) collectPagesAndSections(query pageMapQuery, fn func(c *contentNode)) error {
  633 	if err := m.collectSections(query, fn); err != nil {
  634 		return err
  635 	}
  636 
  637 	query.Prefix = query.Prefix + cmBranchSeparator
  638 	if err := m.collectPages(query, fn); err != nil {
  639 		return err
  640 	}
  641 
  642 	return nil
  643 }
  644 
  645 func (m *pageMap) collectSections(query pageMapQuery, fn func(c *contentNode)) error {
  646 	level := strings.Count(query.Prefix, "/")
  647 
  648 	return m.collectSectionsFn(query, func(s string, c *contentNode) bool {
  649 		if strings.Count(s, "/") != level+1 {
  650 			return false
  651 		}
  652 
  653 		fn(c)
  654 
  655 		return false
  656 	})
  657 }
  658 
  659 func (m *pageMap) collectSectionsFn(query pageMapQuery, fn func(s string, c *contentNode) bool) error {
  660 	if !strings.HasSuffix(query.Prefix, "/") {
  661 		query.Prefix += "/"
  662 	}
  663 
  664 	m.sections.WalkQuery(query, func(s string, n *contentNode) bool {
  665 		return fn(s, n)
  666 	})
  667 
  668 	return nil
  669 }
  670 
  671 func (m *pageMap) collectSectionsRecursiveIncludingSelf(query pageMapQuery, fn func(c *contentNode)) error {
  672 	return m.collectSectionsFn(query, func(s string, c *contentNode) bool {
  673 		fn(c)
  674 		return false
  675 	})
  676 }
  677 
  678 func (m *pageMap) collectTaxonomies(prefix string, fn func(c *contentNode)) error {
  679 	m.taxonomies.WalkQuery(pageMapQuery{Prefix: prefix}, func(s string, n *contentNode) bool {
  680 		fn(n)
  681 		return false
  682 	})
  683 	return nil
  684 }
  685 
  686 // withEveryBundlePage applies fn to every Page, including those bundled inside
  687 // leaf bundles.
  688 func (m *pageMap) withEveryBundlePage(fn func(p *pageState) bool) {
  689 	m.bundleTrees.Walk(func(s string, n *contentNode) bool {
  690 		if n.p != nil {
  691 			return fn(n.p)
  692 		}
  693 		return false
  694 	})
  695 }
  696 
  697 type pageMaps struct {
  698 	workers *para.Workers
  699 	pmaps   []*pageMap
  700 }
  701 
  702 // deleteSection deletes the entire section from s.
  703 func (m *pageMaps) deleteSection(s string) {
  704 	m.withMaps(func(pm *pageMap) error {
  705 		pm.deleteSectionByPath(s)
  706 		return nil
  707 	})
  708 }
  709 
  710 func (m *pageMaps) AssemblePages() error {
  711 	return m.withMaps(func(pm *pageMap) error {
  712 		if err := pm.CreateMissingNodes(); err != nil {
  713 			return err
  714 		}
  715 
  716 		if err := pm.assemblePages(); err != nil {
  717 			return err
  718 		}
  719 
  720 		if err := pm.createMissingTaxonomyNodes(); err != nil {
  721 			return err
  722 		}
  723 
  724 		// Handle any new sections created in the step above.
  725 		if err := pm.assembleSections(); err != nil {
  726 			return err
  727 		}
  728 
  729 		if pm.s.home == nil {
  730 			// Home is disabled, everything is.
  731 			pm.bundleTrees.DeletePrefix("")
  732 			return nil
  733 		}
  734 
  735 		if err := pm.assembleTaxonomies(); err != nil {
  736 			return err
  737 		}
  738 
  739 		if err := pm.createSiteTaxonomies(); err != nil {
  740 			return err
  741 		}
  742 
  743 		sw := &sectionWalker{m: pm.contentMap}
  744 		a := sw.applyAggregates()
  745 		_, mainSectionsSet := pm.s.s.Info.Params()["mainsections"]
  746 		if !mainSectionsSet && a.mainSection != "" {
  747 			mainSections := []string{strings.TrimRight(a.mainSection, "/")}
  748 			pm.s.s.Info.Params()["mainSections"] = mainSections
  749 			pm.s.s.Info.Params()["mainsections"] = mainSections
  750 		}
  751 
  752 		pm.s.lastmod = a.datesAll.Lastmod()
  753 		if resource.IsZeroDates(pm.s.home) {
  754 			pm.s.home.m.Dates = a.datesAll
  755 		}
  756 
  757 		return nil
  758 	})
  759 }
  760 
  761 func (m *pageMaps) walkBundles(fn func(n *contentNode) bool) {
  762 	_ = m.withMaps(func(pm *pageMap) error {
  763 		pm.bundleTrees.Walk(func(s string, n *contentNode) bool {
  764 			return fn(n)
  765 		})
  766 		return nil
  767 	})
  768 }
  769 
  770 func (m *pageMaps) walkBranchesPrefix(prefix string, fn func(s string, n *contentNode) bool) {
  771 	_ = m.withMaps(func(pm *pageMap) error {
  772 		pm.branchTrees.WalkPrefix(prefix, func(s string, n *contentNode) bool {
  773 			return fn(s, n)
  774 		})
  775 		return nil
  776 	})
  777 }
  778 
  779 func (m *pageMaps) withMaps(fn func(pm *pageMap) error) error {
  780 	g, _ := m.workers.Start(context.Background())
  781 	for _, pm := range m.pmaps {
  782 		pm := pm
  783 		g.Run(func() error {
  784 			return fn(pm)
  785 		})
  786 	}
  787 	return g.Wait()
  788 }
  789 
  790 type pagesMapBucket struct {
  791 	// Cascading front matter.
  792 	cascade map[page.PageMatcher]maps.Params
  793 
  794 	owner *pageState // The branch node
  795 
  796 	*pagesMapBucketPages
  797 }
  798 
  799 type pagesMapBucketPages struct {
  800 	pagesInit sync.Once
  801 	pages     page.Pages
  802 
  803 	pagesAndSectionsInit sync.Once
  804 	pagesAndSections     page.Pages
  805 
  806 	sectionsInit sync.Once
  807 	sections     page.Pages
  808 }
  809 
  810 func (b *pagesMapBucket) getPages() page.Pages {
  811 	b.pagesInit.Do(func() {
  812 		b.pages = b.owner.treeRef.getPages()
  813 		page.SortByDefault(b.pages)
  814 	})
  815 	return b.pages
  816 }
  817 
  818 func (b *pagesMapBucket) getPagesRecursive() page.Pages {
  819 	pages := b.owner.treeRef.getPagesRecursive()
  820 	page.SortByDefault(pages)
  821 	return pages
  822 }
  823 
  824 func (b *pagesMapBucket) getPagesAndSections() page.Pages {
  825 	b.pagesAndSectionsInit.Do(func() {
  826 		b.pagesAndSections = b.owner.treeRef.getPagesAndSections()
  827 	})
  828 	return b.pagesAndSections
  829 }
  830 
  831 func (b *pagesMapBucket) getSections() page.Pages {
  832 	b.sectionsInit.Do(func() {
  833 		if b.owner.treeRef == nil {
  834 			return
  835 		}
  836 		b.sections = b.owner.treeRef.getSections()
  837 	})
  838 
  839 	return b.sections
  840 }
  841 
  842 func (b *pagesMapBucket) getTaxonomies() page.Pages {
  843 	b.sectionsInit.Do(func() {
  844 		var pas page.Pages
  845 		ref := b.owner.treeRef
  846 		ref.m.collectTaxonomies(ref.key, func(c *contentNode) {
  847 			pas = append(pas, c.p)
  848 		})
  849 		page.SortByDefault(pas)
  850 		b.sections = pas
  851 	})
  852 
  853 	return b.sections
  854 }
  855 
  856 func (b *pagesMapBucket) getTaxonomyEntries() page.Pages {
  857 	var pas page.Pages
  858 	ref := b.owner.treeRef
  859 	viewInfo := ref.n.viewInfo
  860 	prefix := strings.ToLower("/" + viewInfo.name.plural + "/" + viewInfo.termKey + "/")
  861 	ref.m.taxonomyEntries.WalkPrefix(prefix, func(s string, v any) bool {
  862 		n := v.(*contentNode)
  863 		pas = append(pas, n.viewInfo.ref.p)
  864 		return false
  865 	})
  866 	page.SortByDefault(pas)
  867 	return pas
  868 }
  869 
  870 type sectionAggregate struct {
  871 	datesAll             resource.Dates
  872 	datesSection         resource.Dates
  873 	pageCount            int
  874 	mainSection          string
  875 	mainSectionPageCount int
  876 }
  877 
  878 type sectionAggregateHandler struct {
  879 	sectionAggregate
  880 	sectionPageCount int
  881 
  882 	// Section
  883 	b *contentNode
  884 	s string
  885 }
  886 
  887 func (h *sectionAggregateHandler) String() string {
  888 	return fmt.Sprintf("%s/%s - %d - %s", h.sectionAggregate.datesAll, h.sectionAggregate.datesSection, h.sectionPageCount, h.s)
  889 }
  890 
  891 func (h *sectionAggregateHandler) isRootSection() bool {
  892 	return h.s != "/" && strings.Count(h.s, "/") == 2
  893 }
  894 
  895 func (h *sectionAggregateHandler) handleNested(v sectionWalkHandler) error {
  896 	nested := v.(*sectionAggregateHandler)
  897 	h.sectionPageCount += nested.pageCount
  898 	h.pageCount += h.sectionPageCount
  899 	h.datesAll.UpdateDateAndLastmodIfAfter(nested.datesAll)
  900 	h.datesSection.UpdateDateAndLastmodIfAfter(nested.datesAll)
  901 	return nil
  902 }
  903 
  904 func (h *sectionAggregateHandler) handlePage(s string, n *contentNode) error {
  905 	h.sectionPageCount++
  906 
  907 	var d resource.Dated
  908 	if n.p != nil {
  909 		d = n.p
  910 	} else if n.viewInfo != nil && n.viewInfo.ref != nil {
  911 		d = n.viewInfo.ref.p
  912 	} else {
  913 		return nil
  914 	}
  915 
  916 	h.datesAll.UpdateDateAndLastmodIfAfter(d)
  917 	h.datesSection.UpdateDateAndLastmodIfAfter(d)
  918 	return nil
  919 }
  920 
  921 func (h *sectionAggregateHandler) handleSectionPost() error {
  922 	if h.sectionPageCount > h.mainSectionPageCount && h.isRootSection() {
  923 		h.mainSectionPageCount = h.sectionPageCount
  924 		h.mainSection = strings.TrimPrefix(h.s, "/")
  925 	}
  926 
  927 	if resource.IsZeroDates(h.b.p) {
  928 		h.b.p.m.Dates = h.datesSection
  929 	}
  930 
  931 	h.datesSection = resource.Dates{}
  932 
  933 	return nil
  934 }
  935 
  936 func (h *sectionAggregateHandler) handleSectionPre(s string, b *contentNode) error {
  937 	h.s = s
  938 	h.b = b
  939 	h.sectionPageCount = 0
  940 	h.datesAll.UpdateDateAndLastmodIfAfter(b.p)
  941 	return nil
  942 }
  943 
  944 type sectionWalkHandler interface {
  945 	handleNested(v sectionWalkHandler) error
  946 	handlePage(s string, b *contentNode) error
  947 	handleSectionPost() error
  948 	handleSectionPre(s string, b *contentNode) error
  949 }
  950 
  951 type sectionWalker struct {
  952 	err error
  953 	m   *contentMap
  954 }
  955 
  956 func (w *sectionWalker) applyAggregates() *sectionAggregateHandler {
  957 	return w.walkLevel("/", func() sectionWalkHandler {
  958 		return &sectionAggregateHandler{}
  959 	}).(*sectionAggregateHandler)
  960 }
  961 
  962 func (w *sectionWalker) walkLevel(prefix string, createVisitor func() sectionWalkHandler) sectionWalkHandler {
  963 	level := strings.Count(prefix, "/")
  964 
  965 	visitor := createVisitor()
  966 
  967 	w.m.taxonomies.WalkBelow(prefix, func(s string, v any) bool {
  968 		currentLevel := strings.Count(s, "/")
  969 
  970 		if currentLevel > level+1 {
  971 			return false
  972 		}
  973 
  974 		n := v.(*contentNode)
  975 
  976 		if w.err = visitor.handleSectionPre(s, n); w.err != nil {
  977 			return true
  978 		}
  979 
  980 		if currentLevel == 2 {
  981 			nested := w.walkLevel(s, createVisitor)
  982 			if w.err = visitor.handleNested(nested); w.err != nil {
  983 				return true
  984 			}
  985 		} else {
  986 			w.m.taxonomyEntries.WalkPrefix(s, func(ss string, v any) bool {
  987 				n := v.(*contentNode)
  988 				w.err = visitor.handlePage(ss, n)
  989 				return w.err != nil
  990 			})
  991 		}
  992 
  993 		w.err = visitor.handleSectionPost()
  994 
  995 		return w.err != nil
  996 	})
  997 
  998 	w.m.sections.WalkBelow(prefix, func(s string, v any) bool {
  999 		currentLevel := strings.Count(s, "/")
 1000 		if currentLevel > level+1 {
 1001 			return false
 1002 		}
 1003 
 1004 		n := v.(*contentNode)
 1005 
 1006 		if w.err = visitor.handleSectionPre(s, n); w.err != nil {
 1007 			return true
 1008 		}
 1009 
 1010 		w.m.pages.WalkPrefix(s+cmBranchSeparator, func(s string, v any) bool {
 1011 			w.err = visitor.handlePage(s, v.(*contentNode))
 1012 			return w.err != nil
 1013 		})
 1014 
 1015 		if w.err != nil {
 1016 			return true
 1017 		}
 1018 
 1019 		nested := w.walkLevel(s, createVisitor)
 1020 		if w.err = visitor.handleNested(nested); w.err != nil {
 1021 			return true
 1022 		}
 1023 
 1024 		w.err = visitor.handleSectionPost()
 1025 
 1026 		return w.err != nil
 1027 	})
 1028 
 1029 	return visitor
 1030 }
 1031 
 1032 type viewName struct {
 1033 	singular string // e.g. "category"
 1034 	plural   string // e.g. "categories"
 1035 }
 1036 
 1037 func (v viewName) IsZero() bool {
 1038 	return v.singular == ""
 1039 }