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