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