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 }