content_map.go (24099B)
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 "fmt" 18 "path" 19 "path/filepath" 20 "strings" 21 "sync" 22 23 "github.com/gohugoio/hugo/helpers" 24 25 "github.com/gohugoio/hugo/resources/page" 26 27 "github.com/gohugoio/hugo/hugofs/files" 28 29 "github.com/gohugoio/hugo/hugofs" 30 31 radix "github.com/armon/go-radix" 32 ) 33 34 // We store the branch nodes in either the `sections` or `taxonomies` tree 35 // with their path as a key; Unix style slashes, a leading and trailing slash. 36 // 37 // E.g. "/blog/" or "/categories/funny/" 38 // 39 // Pages that belongs to a section are stored in the `pages` tree below 40 // the section name and a branch separator, e.g. "/blog/__hb_". A page is 41 // given a key using the path below the section and the base filename with no extension 42 // with a leaf separator added. 43 // 44 // For bundled pages (/mybundle/index.md), we use the folder name. 45 // 46 // An exmple of a full page key would be "/blog/__hb_page1__hl_" 47 // 48 // Bundled resources are stored in the `resources` having their path prefixed 49 // with the bundle they belong to, e.g. 50 // "/blog/__hb_bundle__hl_data.json". 51 // 52 // The weighted taxonomy entries extracted from page front matter are stored in 53 // the `taxonomyEntries` tree below /plural/term/page-key, e.g. 54 // "/categories/funny/blog/__hb_bundle__hl_". 55 const ( 56 cmBranchSeparator = "__hb_" 57 cmLeafSeparator = "__hl_" 58 ) 59 60 // Used to mark ambiguous keys in reverse index lookups. 61 var ambiguousContentNode = &contentNode{} 62 63 func newContentMap(cfg contentMapConfig) *contentMap { 64 m := &contentMap{ 65 cfg: &cfg, 66 pages: &contentTree{Name: "pages", Tree: radix.New()}, 67 sections: &contentTree{Name: "sections", Tree: radix.New()}, 68 taxonomies: &contentTree{Name: "taxonomies", Tree: radix.New()}, 69 taxonomyEntries: &contentTree{Name: "taxonomyEntries", Tree: radix.New()}, 70 resources: &contentTree{Name: "resources", Tree: radix.New()}, 71 } 72 73 m.pageTrees = []*contentTree{ 74 m.pages, m.sections, m.taxonomies, 75 } 76 77 m.bundleTrees = []*contentTree{ 78 m.pages, m.sections, m.taxonomies, m.resources, 79 } 80 81 m.branchTrees = []*contentTree{ 82 m.sections, m.taxonomies, 83 } 84 85 addToReverseMap := func(k string, n *contentNode, m map[any]*contentNode) { 86 k = strings.ToLower(k) 87 existing, found := m[k] 88 if found && existing != ambiguousContentNode { 89 m[k] = ambiguousContentNode 90 } else if !found { 91 m[k] = n 92 } 93 } 94 95 m.pageReverseIndex = &contentTreeReverseIndex{ 96 t: []*contentTree{m.pages, m.sections, m.taxonomies}, 97 contentTreeReverseIndexMap: &contentTreeReverseIndexMap{ 98 initFn: func(t *contentTree, m map[any]*contentNode) { 99 t.Walk(func(s string, v any) bool { 100 n := v.(*contentNode) 101 if n.p != nil && !n.p.File().IsZero() { 102 meta := n.p.File().FileInfo().Meta() 103 if meta.Path != meta.PathFile() { 104 // Keep track of the original mount source. 105 mountKey := filepath.ToSlash(filepath.Join(meta.Module, meta.PathFile())) 106 addToReverseMap(mountKey, n, m) 107 } 108 } 109 k := strings.TrimPrefix(strings.TrimSuffix(path.Base(s), cmLeafSeparator), cmBranchSeparator) 110 addToReverseMap(k, n, m) 111 return false 112 }) 113 }, 114 }, 115 } 116 117 return m 118 } 119 120 type cmInsertKeyBuilder struct { 121 m *contentMap 122 123 err error 124 125 // Builder state 126 tree *contentTree 127 baseKey string // Section or page key 128 key string 129 } 130 131 func (b cmInsertKeyBuilder) ForPage(s string) *cmInsertKeyBuilder { 132 // fmt.Println("ForPage:", s, "baseKey:", b.baseKey, "key:", b.key) 133 baseKey := b.baseKey 134 b.baseKey = s 135 136 if baseKey != "/" { 137 // Don't repeat the section path in the key. 138 s = strings.TrimPrefix(s, baseKey) 139 } 140 s = strings.TrimPrefix(s, "/") 141 142 switch b.tree { 143 case b.m.sections: 144 b.tree = b.m.pages 145 b.key = baseKey + cmBranchSeparator + s + cmLeafSeparator 146 case b.m.taxonomies: 147 b.key = path.Join(baseKey, s) 148 default: 149 panic("invalid state") 150 } 151 152 return &b 153 } 154 155 func (b cmInsertKeyBuilder) ForResource(s string) *cmInsertKeyBuilder { 156 // fmt.Println("ForResource:", s, "baseKey:", b.baseKey, "key:", b.key) 157 158 baseKey := helpers.AddTrailingSlash(b.baseKey) 159 s = strings.TrimPrefix(s, baseKey) 160 161 switch b.tree { 162 case b.m.pages: 163 b.key = b.key + s 164 case b.m.sections, b.m.taxonomies: 165 b.key = b.key + cmLeafSeparator + s 166 default: 167 panic(fmt.Sprintf("invalid state: %#v", b.tree)) 168 } 169 b.tree = b.m.resources 170 return &b 171 } 172 173 func (b *cmInsertKeyBuilder) Insert(n *contentNode) *cmInsertKeyBuilder { 174 if b.err == nil { 175 b.tree.Insert(b.Key(), n) 176 } 177 return b 178 } 179 180 func (b *cmInsertKeyBuilder) Key() string { 181 switch b.tree { 182 case b.m.sections, b.m.taxonomies: 183 return cleanSectionTreeKey(b.key) 184 default: 185 return cleanTreeKey(b.key) 186 } 187 } 188 189 func (b *cmInsertKeyBuilder) DeleteAll() *cmInsertKeyBuilder { 190 if b.err == nil { 191 b.tree.DeletePrefix(b.Key()) 192 } 193 return b 194 } 195 196 func (b *cmInsertKeyBuilder) WithFile(fi hugofs.FileMetaInfo) *cmInsertKeyBuilder { 197 b.newTopLevel() 198 m := b.m 199 meta := fi.Meta() 200 p := cleanTreeKey(meta.Path) 201 bundlePath := m.getBundleDir(meta) 202 isBundle := meta.Classifier.IsBundle() 203 if isBundle { 204 panic("not implemented") 205 } 206 207 p, k := b.getBundle(p) 208 if k == "" { 209 b.err = fmt.Errorf("no bundle header found for %q", bundlePath) 210 return b 211 } 212 213 id := k + m.reduceKeyPart(p, fi.Meta().Path) 214 b.tree = b.m.resources 215 b.key = id 216 b.baseKey = p 217 218 return b 219 } 220 221 func (b *cmInsertKeyBuilder) WithSection(s string) *cmInsertKeyBuilder { 222 s = cleanSectionTreeKey(s) 223 b.newTopLevel() 224 b.tree = b.m.sections 225 b.baseKey = s 226 b.key = s 227 return b 228 } 229 230 func (b *cmInsertKeyBuilder) WithTaxonomy(s string) *cmInsertKeyBuilder { 231 s = cleanSectionTreeKey(s) 232 b.newTopLevel() 233 b.tree = b.m.taxonomies 234 b.baseKey = s 235 b.key = s 236 return b 237 } 238 239 // getBundle gets both the key to the section and the prefix to where to store 240 // this page bundle and its resources. 241 func (b *cmInsertKeyBuilder) getBundle(s string) (string, string) { 242 m := b.m 243 section, _ := m.getSection(s) 244 245 p := strings.TrimPrefix(s, section) 246 247 bundlePathParts := strings.Split(p, "/") 248 basePath := section + cmBranchSeparator 249 250 // Put it into an existing bundle if found. 251 for i := len(bundlePathParts) - 2; i >= 0; i-- { 252 bundlePath := path.Join(bundlePathParts[:i]...) 253 searchKey := basePath + bundlePath + cmLeafSeparator 254 if _, found := m.pages.Get(searchKey); found { 255 return section + bundlePath, searchKey 256 } 257 } 258 259 // Put it into the section bundle. 260 return section, section + cmLeafSeparator 261 } 262 263 func (b *cmInsertKeyBuilder) newTopLevel() { 264 b.key = "" 265 } 266 267 type contentBundleViewInfo struct { 268 ordinal int 269 name viewName 270 termKey string 271 termOrigin string 272 weight int 273 ref *contentNode 274 } 275 276 func (c *contentBundleViewInfo) kind() string { 277 if c.termKey != "" { 278 return page.KindTerm 279 } 280 return page.KindTaxonomy 281 } 282 283 func (c *contentBundleViewInfo) sections() []string { 284 if c.kind() == page.KindTaxonomy { 285 return []string{c.name.plural} 286 } 287 288 return []string{c.name.plural, c.termKey} 289 } 290 291 func (c *contentBundleViewInfo) term() string { 292 if c.termOrigin != "" { 293 return c.termOrigin 294 } 295 296 return c.termKey 297 } 298 299 type contentMap struct { 300 cfg *contentMapConfig 301 302 // View of regular pages, sections, and taxonomies. 303 pageTrees contentTrees 304 305 // View of pages, sections, taxonomies, and resources. 306 bundleTrees contentTrees 307 308 // View of sections and taxonomies. 309 branchTrees contentTrees 310 311 // Stores page bundles keyed by its path's directory or the base filename, 312 // e.g. "blog/post.md" => "/blog/post", "blog/post/index.md" => "/blog/post" 313 // These are the "regular pages" and all of them are bundles. 314 pages *contentTree 315 316 // A reverse index used as a fallback in GetPage. 317 // There are currently two cases where this is used: 318 // 1. Short name lookups in ref/relRef, e.g. using only "mypage.md" without a path. 319 // 2. Links resolved from a remounted content directory. These are restricted to the same module. 320 // Both of the above cases can result in ambigous lookup errors. 321 pageReverseIndex *contentTreeReverseIndex 322 323 // Section nodes. 324 sections *contentTree 325 326 // Taxonomy nodes. 327 taxonomies *contentTree 328 329 // Pages in a taxonomy. 330 taxonomyEntries *contentTree 331 332 // Resources stored per bundle below a common prefix, e.g. "/blog/post__hb_". 333 resources *contentTree 334 } 335 336 func (m *contentMap) AddFiles(fis ...hugofs.FileMetaInfo) error { 337 for _, fi := range fis { 338 if err := m.addFile(fi); err != nil { 339 return err 340 } 341 } 342 343 return nil 344 } 345 346 func (m *contentMap) AddFilesBundle(header hugofs.FileMetaInfo, resources ...hugofs.FileMetaInfo) error { 347 var ( 348 meta = header.Meta() 349 classifier = meta.Classifier 350 isBranch = classifier == files.ContentClassBranch 351 bundlePath = m.getBundleDir(meta) 352 353 n = m.newContentNodeFromFi(header) 354 b = m.newKeyBuilder() 355 356 section string 357 ) 358 359 if isBranch { 360 // Either a section or a taxonomy node. 361 section = bundlePath 362 if tc := m.cfg.getTaxonomyConfig(section); !tc.IsZero() { 363 term := strings.TrimPrefix(strings.TrimPrefix(section, "/"+tc.plural), "/") 364 365 n.viewInfo = &contentBundleViewInfo{ 366 name: tc, 367 termKey: term, 368 termOrigin: term, 369 } 370 371 n.viewInfo.ref = n 372 b.WithTaxonomy(section).Insert(n) 373 } else { 374 b.WithSection(section).Insert(n) 375 } 376 } else { 377 // A regular page. Attach it to its section. 378 section, _ = m.getOrCreateSection(n, bundlePath) 379 b = b.WithSection(section).ForPage(bundlePath).Insert(n) 380 } 381 382 if m.cfg.isRebuild { 383 // The resource owner will be either deleted or overwritten on rebuilds, 384 // but make sure we handle deletion of resources (images etc.) as well. 385 b.ForResource("").DeleteAll() 386 } 387 388 for _, r := range resources { 389 rb := b.ForResource(cleanTreeKey(r.Meta().Path)) 390 rb.Insert(&contentNode{fi: r}) 391 } 392 393 return nil 394 } 395 396 func (m *contentMap) CreateMissingNodes() error { 397 // Create missing home and root sections 398 rootSections := make(map[string]any) 399 trackRootSection := func(s string, b *contentNode) { 400 parts := strings.Split(s, "/") 401 if len(parts) > 2 { 402 root := strings.TrimSuffix(parts[1], cmBranchSeparator) 403 if root != "" { 404 if _, found := rootSections[root]; !found { 405 rootSections[root] = b 406 } 407 } 408 } 409 } 410 411 m.sections.Walk(func(s string, v any) bool { 412 n := v.(*contentNode) 413 414 if s == "/" { 415 return false 416 } 417 418 trackRootSection(s, n) 419 return false 420 }) 421 422 m.pages.Walk(func(s string, v any) bool { 423 trackRootSection(s, v.(*contentNode)) 424 return false 425 }) 426 427 if _, found := rootSections["/"]; !found { 428 rootSections["/"] = true 429 } 430 431 for sect, v := range rootSections { 432 var sectionPath string 433 if n, ok := v.(*contentNode); ok && n.path != "" { 434 sectionPath = n.path 435 firstSlash := strings.Index(sectionPath, "/") 436 if firstSlash != -1 { 437 sectionPath = sectionPath[:firstSlash] 438 } 439 } 440 sect = cleanSectionTreeKey(sect) 441 _, found := m.sections.Get(sect) 442 if !found { 443 m.sections.Insert(sect, &contentNode{path: sectionPath}) 444 } 445 } 446 447 for _, view := range m.cfg.taxonomyConfig { 448 s := cleanSectionTreeKey(view.plural) 449 _, found := m.taxonomies.Get(s) 450 if !found { 451 b := &contentNode{ 452 viewInfo: &contentBundleViewInfo{ 453 name: view, 454 }, 455 } 456 b.viewInfo.ref = b 457 m.taxonomies.Insert(s, b) 458 } 459 } 460 461 return nil 462 } 463 464 func (m *contentMap) getBundleDir(meta *hugofs.FileMeta) string { 465 dir := cleanTreeKey(filepath.Dir(meta.Path)) 466 467 switch meta.Classifier { 468 case files.ContentClassContent: 469 return path.Join(dir, meta.TranslationBaseName) 470 default: 471 return dir 472 } 473 } 474 475 func (m *contentMap) newContentNodeFromFi(fi hugofs.FileMetaInfo) *contentNode { 476 return &contentNode{ 477 fi: fi, 478 path: strings.TrimPrefix(filepath.ToSlash(fi.Meta().Path), "/"), 479 } 480 } 481 482 func (m *contentMap) getFirstSection(s string) (string, *contentNode) { 483 s = helpers.AddTrailingSlash(s) 484 for { 485 k, v, found := m.sections.LongestPrefix(s) 486 487 if !found { 488 return "", nil 489 } 490 491 if strings.Count(k, "/") <= 2 { 492 return k, v.(*contentNode) 493 } 494 495 s = helpers.AddTrailingSlash(path.Dir(strings.TrimSuffix(s, "/"))) 496 497 } 498 } 499 500 func (m *contentMap) newKeyBuilder() *cmInsertKeyBuilder { 501 return &cmInsertKeyBuilder{m: m} 502 } 503 504 func (m *contentMap) getOrCreateSection(n *contentNode, s string) (string, *contentNode) { 505 level := strings.Count(s, "/") 506 k, b := m.getSection(s) 507 508 mustCreate := false 509 510 if k == "" { 511 mustCreate = true 512 } else if level > 1 && k == "/" { 513 // We found the home section, but this page needs to be placed in 514 // the root, e.g. "/blog", section. 515 mustCreate = true 516 } 517 518 if mustCreate { 519 k = cleanSectionTreeKey(s[:strings.Index(s[1:], "/")+1]) 520 521 b = &contentNode{ 522 path: n.rootSection(), 523 } 524 525 m.sections.Insert(k, b) 526 } 527 528 return k, b 529 } 530 531 func (m *contentMap) getPage(section, name string) *contentNode { 532 section = helpers.AddTrailingSlash(section) 533 key := section + cmBranchSeparator + name + cmLeafSeparator 534 535 v, found := m.pages.Get(key) 536 if found { 537 return v.(*contentNode) 538 } 539 return nil 540 } 541 542 func (m *contentMap) getSection(s string) (string, *contentNode) { 543 s = helpers.AddTrailingSlash(path.Dir(strings.TrimSuffix(s, "/"))) 544 545 k, v, found := m.sections.LongestPrefix(s) 546 547 if found { 548 return k, v.(*contentNode) 549 } 550 return "", nil 551 } 552 553 func (m *contentMap) getTaxonomyParent(s string) (string, *contentNode) { 554 s = helpers.AddTrailingSlash(path.Dir(strings.TrimSuffix(s, "/"))) 555 k, v, found := m.taxonomies.LongestPrefix(s) 556 557 if found { 558 return k, v.(*contentNode) 559 } 560 561 v, found = m.sections.Get("/") 562 if found { 563 return s, v.(*contentNode) 564 } 565 566 return "", nil 567 } 568 569 func (m *contentMap) addFile(fi hugofs.FileMetaInfo) error { 570 b := m.newKeyBuilder() 571 return b.WithFile(fi).Insert(m.newContentNodeFromFi(fi)).err 572 } 573 574 func cleanTreeKey(k string) string { 575 k = "/" + strings.ToLower(strings.Trim(path.Clean(filepath.ToSlash(k)), "./")) 576 return k 577 } 578 579 func cleanSectionTreeKey(k string) string { 580 k = cleanTreeKey(k) 581 if k != "/" { 582 k += "/" 583 } 584 585 return k 586 } 587 588 func (m *contentMap) onSameLevel(s1, s2 string) bool { 589 return strings.Count(s1, "/") == strings.Count(s2, "/") 590 } 591 592 func (m *contentMap) deleteBundleMatching(matches func(b *contentNode) bool) { 593 // Check sections first 594 s := m.sections.getMatch(matches) 595 if s != "" { 596 m.deleteSectionByPath(s) 597 return 598 } 599 600 s = m.pages.getMatch(matches) 601 if s != "" { 602 m.deletePage(s) 603 return 604 } 605 606 s = m.resources.getMatch(matches) 607 if s != "" { 608 m.resources.Delete(s) 609 } 610 } 611 612 // Deletes any empty root section that's not backed by a content file. 613 func (m *contentMap) deleteOrphanSections() { 614 var sectionsToDelete []string 615 616 m.sections.Walk(func(s string, v any) bool { 617 n := v.(*contentNode) 618 619 if n.fi != nil { 620 // Section may be empty, but is backed by a content file. 621 return false 622 } 623 624 if s == "/" || strings.Count(s, "/") > 2 { 625 return false 626 } 627 628 prefixBundle := s + cmBranchSeparator 629 630 if !(m.sections.hasBelow(s) || m.pages.hasBelow(prefixBundle) || m.resources.hasBelow(prefixBundle)) { 631 sectionsToDelete = append(sectionsToDelete, s) 632 } 633 634 return false 635 }) 636 637 for _, s := range sectionsToDelete { 638 m.sections.Delete(s) 639 } 640 } 641 642 func (m *contentMap) deletePage(s string) { 643 m.pages.DeletePrefix(s) 644 m.resources.DeletePrefix(s) 645 } 646 647 func (m *contentMap) deleteSectionByPath(s string) { 648 if !strings.HasSuffix(s, "/") { 649 panic("section must end with a slash") 650 } 651 if !strings.HasPrefix(s, "/") { 652 panic("section must start with a slash") 653 } 654 m.sections.DeletePrefix(s) 655 m.pages.DeletePrefix(s) 656 m.resources.DeletePrefix(s) 657 } 658 659 func (m *contentMap) deletePageByPath(s string) { 660 m.pages.Walk(func(s string, v any) bool { 661 fmt.Println("S", s) 662 663 return false 664 }) 665 } 666 667 func (m *contentMap) deleteTaxonomy(s string) { 668 m.taxonomies.DeletePrefix(s) 669 } 670 671 func (m *contentMap) reduceKeyPart(dir, filename string) string { 672 dir, filename = filepath.ToSlash(dir), filepath.ToSlash(filename) 673 dir, filename = strings.TrimPrefix(dir, "/"), strings.TrimPrefix(filename, "/") 674 675 return strings.TrimPrefix(strings.TrimPrefix(filename, dir), "/") 676 } 677 678 func (m *contentMap) splitKey(k string) []string { 679 if k == "" || k == "/" { 680 return nil 681 } 682 683 return strings.Split(k, "/")[1:] 684 } 685 686 func (m *contentMap) testDump() string { 687 var sb strings.Builder 688 689 for i, r := range []*contentTree{m.pages, m.sections, m.resources} { 690 sb.WriteString(fmt.Sprintf("Tree %d:\n", i)) 691 r.Walk(func(s string, v any) bool { 692 sb.WriteString("\t" + s + "\n") 693 return false 694 }) 695 } 696 697 for i, r := range []*contentTree{m.pages, m.sections} { 698 r.Walk(func(s string, v any) bool { 699 c := v.(*contentNode) 700 cpToString := func(c *contentNode) string { 701 var sb strings.Builder 702 if c.p != nil { 703 sb.WriteString("|p:" + c.p.Title()) 704 } 705 if c.fi != nil { 706 sb.WriteString("|f:" + filepath.ToSlash(c.fi.Meta().Path)) 707 } 708 return sb.String() 709 } 710 sb.WriteString(path.Join(m.cfg.lang, r.Name) + s + cpToString(c) + "\n") 711 712 resourcesPrefix := s 713 714 if i == 1 { 715 resourcesPrefix += cmLeafSeparator 716 717 m.pages.WalkPrefix(s+cmBranchSeparator, func(s string, v any) bool { 718 sb.WriteString("\t - P: " + filepath.ToSlash((v.(*contentNode).fi.(hugofs.FileMetaInfo)).Meta().Filename) + "\n") 719 return false 720 }) 721 } 722 723 m.resources.WalkPrefix(resourcesPrefix, func(s string, v any) bool { 724 sb.WriteString("\t - R: " + filepath.ToSlash((v.(*contentNode).fi.(hugofs.FileMetaInfo)).Meta().Filename) + "\n") 725 return false 726 }) 727 728 return false 729 }) 730 } 731 732 return sb.String() 733 } 734 735 type contentMapConfig struct { 736 lang string 737 taxonomyConfig []viewName 738 taxonomyDisabled bool 739 taxonomyTermDisabled bool 740 pageDisabled bool 741 isRebuild bool 742 } 743 744 func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) { 745 s = strings.TrimPrefix(s, "/") 746 if s == "" { 747 return 748 } 749 for _, n := range cfg.taxonomyConfig { 750 if strings.HasPrefix(s, n.plural) { 751 return n 752 } 753 } 754 755 return 756 } 757 758 type contentNode struct { 759 p *pageState 760 761 // Set for taxonomy nodes. 762 viewInfo *contentBundleViewInfo 763 764 // Set if source is a file. 765 // We will soon get other sources. 766 fi hugofs.FileMetaInfo 767 768 // The source path. Unix slashes. No leading slash. 769 path string 770 } 771 772 func (b *contentNode) rootSection() string { 773 if b.path == "" { 774 return "" 775 } 776 firstSlash := strings.Index(b.path, "/") 777 if firstSlash == -1 { 778 return b.path 779 } 780 return b.path[:firstSlash] 781 } 782 783 type contentTree struct { 784 Name string 785 *radix.Tree 786 } 787 788 type contentTrees []*contentTree 789 790 func (t contentTrees) DeletePrefix(prefix string) int { 791 var count int 792 for _, tree := range t { 793 tree.Walk(func(s string, v any) bool { 794 return false 795 }) 796 count += tree.DeletePrefix(prefix) 797 } 798 return count 799 } 800 801 type contentTreeNodeCallback func(s string, n *contentNode) bool 802 803 func newContentTreeFilter(fn func(n *contentNode) bool) contentTreeNodeCallback { 804 return func(s string, n *contentNode) bool { 805 return fn(n) 806 } 807 } 808 809 var ( 810 contentTreeNoListAlwaysFilter = func(s string, n *contentNode) bool { 811 if n.p == nil { 812 return true 813 } 814 return n.p.m.noListAlways() 815 } 816 817 contentTreeNoRenderFilter = func(s string, n *contentNode) bool { 818 if n.p == nil { 819 return true 820 } 821 return n.p.m.noRender() 822 } 823 824 contentTreeNoLinkFilter = func(s string, n *contentNode) bool { 825 if n.p == nil { 826 return true 827 } 828 return n.p.m.noLink() 829 } 830 ) 831 832 func (c *contentTree) WalkQuery(query pageMapQuery, walkFn contentTreeNodeCallback) { 833 filter := query.Filter 834 if filter == nil { 835 filter = contentTreeNoListAlwaysFilter 836 } 837 if query.Prefix != "" { 838 c.WalkBelow(query.Prefix, func(s string, v any) bool { 839 n := v.(*contentNode) 840 if filter != nil && filter(s, n) { 841 return false 842 } 843 return walkFn(s, n) 844 }) 845 846 return 847 } 848 849 c.Walk(func(s string, v any) bool { 850 n := v.(*contentNode) 851 if filter != nil && filter(s, n) { 852 return false 853 } 854 return walkFn(s, n) 855 }) 856 } 857 858 func (c contentTrees) WalkRenderable(fn contentTreeNodeCallback) { 859 query := pageMapQuery{Filter: contentTreeNoRenderFilter} 860 for _, tree := range c { 861 tree.WalkQuery(query, fn) 862 } 863 } 864 865 func (c contentTrees) WalkLinkable(fn contentTreeNodeCallback) { 866 query := pageMapQuery{Filter: contentTreeNoLinkFilter} 867 for _, tree := range c { 868 tree.WalkQuery(query, fn) 869 } 870 } 871 872 func (c contentTrees) Walk(fn contentTreeNodeCallback) { 873 for _, tree := range c { 874 tree.Walk(func(s string, v any) bool { 875 n := v.(*contentNode) 876 return fn(s, n) 877 }) 878 } 879 } 880 881 func (c contentTrees) WalkPrefix(prefix string, fn contentTreeNodeCallback) { 882 for _, tree := range c { 883 tree.WalkPrefix(prefix, func(s string, v any) bool { 884 n := v.(*contentNode) 885 return fn(s, n) 886 }) 887 } 888 } 889 890 // WalkBelow walks the tree below the given prefix, i.e. it skips the 891 // node with the given prefix as key. 892 func (c *contentTree) WalkBelow(prefix string, fn radix.WalkFn) { 893 c.Tree.WalkPrefix(prefix, func(s string, v any) bool { 894 if s == prefix { 895 return false 896 } 897 return fn(s, v) 898 }) 899 } 900 901 func (c *contentTree) getMatch(matches func(b *contentNode) bool) string { 902 var match string 903 c.Walk(func(s string, v any) bool { 904 n, ok := v.(*contentNode) 905 if !ok { 906 return false 907 } 908 909 if matches(n) { 910 match = s 911 return true 912 } 913 914 return false 915 }) 916 917 return match 918 } 919 920 func (c *contentTree) hasBelow(s1 string) bool { 921 var t bool 922 c.WalkBelow(s1, func(s2 string, v any) bool { 923 t = true 924 return true 925 }) 926 return t 927 } 928 929 func (c *contentTree) printKeys() { 930 c.Walk(func(s string, v any) bool { 931 fmt.Println(s) 932 return false 933 }) 934 } 935 936 func (c *contentTree) printKeysPrefix(prefix string) { 937 c.WalkPrefix(prefix, func(s string, v any) bool { 938 fmt.Println(s) 939 return false 940 }) 941 } 942 943 // contentTreeRef points to a node in the given tree. 944 type contentTreeRef struct { 945 m *pageMap 946 t *contentTree 947 n *contentNode 948 key string 949 } 950 951 func (c *contentTreeRef) getCurrentSection() (string, *contentNode) { 952 if c.isSection() { 953 return c.key, c.n 954 } 955 return c.getSection() 956 } 957 958 func (c *contentTreeRef) isSection() bool { 959 return c.t == c.m.sections 960 } 961 962 func (c *contentTreeRef) getSection() (string, *contentNode) { 963 if c.t == c.m.taxonomies { 964 return c.m.getTaxonomyParent(c.key) 965 } 966 return c.m.getSection(c.key) 967 } 968 969 func (c *contentTreeRef) getPages() page.Pages { 970 var pas page.Pages 971 c.m.collectPages( 972 pageMapQuery{ 973 Prefix: c.key + cmBranchSeparator, 974 Filter: c.n.p.m.getListFilter(true), 975 }, 976 func(c *contentNode) { 977 pas = append(pas, c.p) 978 }, 979 ) 980 page.SortByDefault(pas) 981 982 return pas 983 } 984 985 func (c *contentTreeRef) getPagesRecursive() page.Pages { 986 var pas page.Pages 987 988 query := pageMapQuery{ 989 Filter: c.n.p.m.getListFilter(true), 990 } 991 992 query.Prefix = c.key 993 c.m.collectPages(query, func(c *contentNode) { 994 pas = append(pas, c.p) 995 }) 996 997 page.SortByDefault(pas) 998 999 return pas 1000 } 1001 1002 func (c *contentTreeRef) getPagesAndSections() page.Pages { 1003 var pas page.Pages 1004 1005 query := pageMapQuery{ 1006 Filter: c.n.p.m.getListFilter(true), 1007 Prefix: c.key, 1008 } 1009 1010 c.m.collectPagesAndSections(query, func(c *contentNode) { 1011 pas = append(pas, c.p) 1012 }) 1013 1014 page.SortByDefault(pas) 1015 1016 return pas 1017 } 1018 1019 func (c *contentTreeRef) getSections() page.Pages { 1020 var pas page.Pages 1021 1022 query := pageMapQuery{ 1023 Filter: c.n.p.m.getListFilter(true), 1024 Prefix: c.key, 1025 } 1026 1027 c.m.collectSections(query, func(c *contentNode) { 1028 pas = append(pas, c.p) 1029 }) 1030 1031 page.SortByDefault(pas) 1032 1033 return pas 1034 } 1035 1036 type contentTreeReverseIndex struct { 1037 t []*contentTree 1038 *contentTreeReverseIndexMap 1039 } 1040 1041 type contentTreeReverseIndexMap struct { 1042 m map[any]*contentNode 1043 init sync.Once 1044 initFn func(*contentTree, map[any]*contentNode) 1045 } 1046 1047 func (c *contentTreeReverseIndex) Reset() { 1048 c.contentTreeReverseIndexMap = &contentTreeReverseIndexMap{ 1049 initFn: c.initFn, 1050 } 1051 } 1052 1053 func (c *contentTreeReverseIndex) Get(key any) *contentNode { 1054 c.init.Do(func() { 1055 c.m = make(map[any]*contentNode) 1056 for _, tree := range c.t { 1057 c.initFn(tree, c.m) 1058 } 1059 }) 1060 return c.m[key] 1061 }