github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/hugolib/content_map.go (about)

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