github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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  
    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 example 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 ambiguous 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  }