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
14package hugolib
15
16import (
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_".
56const (
57	cmBranchSeparator = "__hb_"
58	cmLeafSeparator   = "__hl_"
59)
60
61// Used to mark ambiguous keys in reverse index lookups.
62var ambiguousContentNode = &contentNode{}
63
64func 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
121type 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
132func (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
156func (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
174func (b *cmInsertKeyBuilder) Insert(n *contentNode) *cmInsertKeyBuilder {
175	if b.err == nil {
176		b.tree.Insert(b.Key(), n)
177	}
178	return b
179}
180
181func (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
190func (b *cmInsertKeyBuilder) DeleteAll() *cmInsertKeyBuilder {
191	if b.err == nil {
192		b.tree.DeletePrefix(b.Key())
193	}
194	return b
195}
196
197func (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
222func (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
231func (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.
242func (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
264func (b *cmInsertKeyBuilder) newTopLevel() {
265	b.key = ""
266}
267
268type contentBundleViewInfo struct {
269	ordinal    int
270	name       viewName
271	termKey    string
272	termOrigin string
273	weight     int
274	ref        *contentNode
275}
276
277func (c *contentBundleViewInfo) kind() string {
278	if c.termKey != "" {
279		return page.KindTerm
280	}
281	return page.KindTaxonomy
282}
283
284func (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
292func (c *contentBundleViewInfo) term() string {
293	if c.termOrigin != "" {
294		return c.termOrigin
295	}
296
297	return c.termKey
298}
299
300type 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
337func (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
347func (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
397func (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
465func (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
476func (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
483func (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
501func (m *contentMap) newKeyBuilder() *cmInsertKeyBuilder {
502	return &cmInsertKeyBuilder{m: m}
503}
504
505func (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
532func (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
543func (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
554func (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
570func (m *contentMap) addFile(fi hugofs.FileMetaInfo) error {
571	b := m.newKeyBuilder()
572	return b.WithFile(fi).Insert(m.newContentNodeFromFi(fi)).err
573}
574
575func cleanTreeKey(k string) string {
576	k = "/" + strings.ToLower(strings.Trim(path.Clean(filepath.ToSlash(k)), "./"))
577	return k
578}
579
580func cleanSectionTreeKey(k string) string {
581	k = cleanTreeKey(k)
582	if k != "/" {
583		k += "/"
584	}
585
586	return k
587}
588
589func (m *contentMap) onSameLevel(s1, s2 string) bool {
590	return strings.Count(s1, "/") == strings.Count(s2, "/")
591}
592
593func (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.
614func (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
643func (m *contentMap) deletePage(s string) {
644	m.pages.DeletePrefix(s)
645	m.resources.DeletePrefix(s)
646}
647
648func (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
660func (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
668func (m *contentMap) deleteTaxonomy(s string) {
669	m.taxonomies.DeletePrefix(s)
670}
671
672func (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
679func (m *contentMap) splitKey(k string) []string {
680	if k == "" || k == "/" {
681		return nil
682	}
683
684	return strings.Split(k, "/")[1:]
685}
686
687func (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
736type contentMapConfig struct {
737	lang                 string
738	taxonomyConfig       []viewName
739	taxonomyDisabled     bool
740	taxonomyTermDisabled bool
741	pageDisabled         bool
742	isRebuild            bool
743}
744
745func (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
759type 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
773func (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
784type contentTree struct {
785	Name string
786	*radix.Tree
787}
788
789type contentTrees []*contentTree
790
791func (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
802type contentTreeNodeCallback func(s string, n *contentNode) bool
803
804func newContentTreeFilter(fn func(n *contentNode) bool) contentTreeNodeCallback {
805	return func(s string, n *contentNode) bool {
806		return fn(n)
807	}
808}
809
810var (
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
833func (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
859func (c contentTrees) WalkRenderable(fn contentTreeNodeCallback) {
860	query := pageMapQuery{Filter: contentTreeNoRenderFilter}
861	for _, tree := range c {
862		tree.WalkQuery(query, fn)
863	}
864}
865
866func (c contentTrees) WalkLinkable(fn contentTreeNodeCallback) {
867	query := pageMapQuery{Filter: contentTreeNoLinkFilter}
868	for _, tree := range c {
869		tree.WalkQuery(query, fn)
870	}
871}
872
873func (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
882func (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.
893func (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
902func (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
921func (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
930func (c *contentTree) printKeys() {
931	c.Walk(func(s string, v interface{}) bool {
932		fmt.Println(s)
933		return false
934	})
935}
936
937func (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.
945type contentTreeRef struct {
946	m   *pageMap
947	t   *contentTree
948	n   *contentNode
949	key string
950}
951
952func (c *contentTreeRef) getCurrentSection() (string, *contentNode) {
953	if c.isSection() {
954		return c.key, c.n
955	}
956	return c.getSection()
957}
958
959func (c *contentTreeRef) isSection() bool {
960	return c.t == c.m.sections
961}
962
963func (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
970func (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
986func (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
1003func (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
1020func (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
1037type contentTreeReverseIndex struct {
1038	t []*contentTree
1039	*contentTreeReverseIndexMap
1040}
1041
1042type contentTreeReverseIndexMap struct {
1043	m      map[interface{}]*contentNode
1044	init   sync.Once
1045	initFn func(*contentTree, map[interface{}]*contentNode)
1046}
1047
1048func (c *contentTreeReverseIndex) Reset() {
1049	c.contentTreeReverseIndexMap = &contentTreeReverseIndexMap{
1050		initFn: c.initFn,
1051	}
1052}
1053
1054func (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}
1063