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 := §ionWalker{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 §ionAggregateHandler{}
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 }