github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/hugolib/content_map_page.go (about)

     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(context.Context) (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(page.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(page.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[info.termKey] = append(taxonomy[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  }