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 }