hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }