github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/hugolib/pagecollections.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/common/paths"
    24  
    25  	"github.com/gohugoio/hugo/hugofs/files"
    26  
    27  	"github.com/gohugoio/hugo/helpers"
    28  
    29  	"github.com/gohugoio/hugo/resources/page"
    30  )
    31  
    32  // PageCollections contains the page collections for a site.
    33  type PageCollections struct {
    34  	pageMap *pageMap
    35  
    36  	// Lazy initialized page collections
    37  	pages           *lazyPagesFactory
    38  	regularPages    *lazyPagesFactory
    39  	allPages        *lazyPagesFactory
    40  	allRegularPages *lazyPagesFactory
    41  }
    42  
    43  // Pages returns all pages.
    44  // This is for the current language only.
    45  func (c *PageCollections) Pages() page.Pages {
    46  	return c.pages.get()
    47  }
    48  
    49  // RegularPages returns all the regular pages.
    50  // This is for the current language only.
    51  func (c *PageCollections) RegularPages() page.Pages {
    52  	return c.regularPages.get()
    53  }
    54  
    55  // AllPages returns all pages for all languages.
    56  func (c *PageCollections) AllPages() page.Pages {
    57  	return c.allPages.get()
    58  }
    59  
    60  // AllPages returns all regular pages for all languages.
    61  func (c *PageCollections) AllRegularPages() page.Pages {
    62  	return c.allRegularPages.get()
    63  }
    64  
    65  type lazyPagesFactory struct {
    66  	pages page.Pages
    67  
    68  	init    sync.Once
    69  	factory page.PagesFactory
    70  }
    71  
    72  func (l *lazyPagesFactory) get() page.Pages {
    73  	l.init.Do(func() {
    74  		l.pages = l.factory()
    75  	})
    76  	return l.pages
    77  }
    78  
    79  func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory {
    80  	return &lazyPagesFactory{factory: factory}
    81  }
    82  
    83  func newPageCollections(m *pageMap) *PageCollections {
    84  	if m == nil {
    85  		panic("must provide a pageMap")
    86  	}
    87  
    88  	c := &PageCollections{pageMap: m}
    89  
    90  	c.pages = newLazyPagesFactory(func() page.Pages {
    91  		return m.createListAllPages()
    92  	})
    93  
    94  	c.regularPages = newLazyPagesFactory(func() page.Pages {
    95  		return c.findPagesByKindIn(page.KindPage, c.pages.get())
    96  	})
    97  
    98  	return c
    99  }
   100  
   101  // This is an adapter func for the old API with Kind as first argument.
   102  // This is invoked when you do .Site.GetPage. We drop the Kind and fails
   103  // if there are more than 2 arguments, which would be ambiguous.
   104  func (c *PageCollections) getPageOldVersion(ref ...string) (page.Page, error) {
   105  	var refs []string
   106  	for _, r := range ref {
   107  		// A common construct in the wild is
   108  		// .Site.GetPage "home" "" or
   109  		// .Site.GetPage "home" "/"
   110  		if r != "" && r != "/" {
   111  			refs = append(refs, r)
   112  		}
   113  	}
   114  
   115  	var key string
   116  
   117  	if len(refs) > 2 {
   118  		// This was allowed in Hugo <= 0.44, but we cannot support this with the
   119  		// new API. This should be the most unusual case.
   120  		return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
   121  	}
   122  
   123  	if len(refs) == 0 || refs[0] == page.KindHome {
   124  		key = "/"
   125  	} else if len(refs) == 1 {
   126  		if len(ref) == 2 && refs[0] == page.KindSection {
   127  			// This is an old style reference to the "Home Page section".
   128  			// Typically fetched via {{ .Site.GetPage "section" .Section }}
   129  			// See https://github.com/gohugoio/hugo/issues/4989
   130  			key = "/"
   131  		} else {
   132  			key = refs[0]
   133  		}
   134  	} else {
   135  		key = refs[1]
   136  	}
   137  
   138  	key = filepath.ToSlash(key)
   139  	if !strings.HasPrefix(key, "/") {
   140  		key = "/" + key
   141  	}
   142  
   143  	return c.getPageNew(nil, key)
   144  }
   145  
   146  // 	Only used in tests.
   147  func (c *PageCollections) getPage(typ string, sections ...string) page.Page {
   148  	refs := append([]string{typ}, path.Join(sections...))
   149  	p, _ := c.getPageOldVersion(refs...)
   150  	return p
   151  }
   152  
   153  // getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive
   154  // search path than getPageNew.
   155  func (c *PageCollections) getPageRef(context page.Page, ref string) (page.Page, error) {
   156  	n, err := c.getContentNode(context, true, ref)
   157  	if err != nil || n == nil || n.p == nil {
   158  		return nil, err
   159  	}
   160  	return n.p, nil
   161  }
   162  
   163  func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) {
   164  	n, err := c.getContentNode(context, false, ref)
   165  	if err != nil || n == nil || n.p == nil {
   166  		return nil, err
   167  	}
   168  	return n.p, nil
   169  }
   170  
   171  func (c *PageCollections) getSectionOrPage(ref string) (*contentNode, string) {
   172  	var n *contentNode
   173  
   174  	pref := helpers.AddTrailingSlash(ref)
   175  	s, v, found := c.pageMap.sections.LongestPrefix(pref)
   176  
   177  	if found {
   178  		n = v.(*contentNode)
   179  	}
   180  
   181  	if found && s == pref {
   182  		// A section
   183  		return n, ""
   184  	}
   185  
   186  	m := c.pageMap
   187  
   188  	filename := strings.TrimPrefix(strings.TrimPrefix(ref, s), "/")
   189  	langSuffix := "." + m.s.Lang()
   190  
   191  	// Trim both extension and any language code.
   192  	name := paths.PathNoExt(filename)
   193  	name = strings.TrimSuffix(name, langSuffix)
   194  
   195  	// These are reserved bundle names and will always be stored by their owning
   196  	// folder name.
   197  	name = strings.TrimSuffix(name, "/index")
   198  	name = strings.TrimSuffix(name, "/_index")
   199  
   200  	if !found {
   201  		return nil, name
   202  	}
   203  
   204  	// Check if it's a section with filename provided.
   205  	if !n.p.File().IsZero() && n.p.File().LogicalName() == filename {
   206  		return n, name
   207  	}
   208  
   209  	return m.getPage(s, name), name
   210  }
   211  
   212  // For Ref/Reflink and .Site.GetPage do simple name lookups for the potentially ambiguous myarticle.md and /myarticle.md,
   213  // but not when we get ./myarticle*, section/myarticle.
   214  func shouldDoSimpleLookup(ref string) bool {
   215  	if ref[0] == '.' {
   216  		return false
   217  	}
   218  
   219  	slashCount := strings.Count(ref, "/")
   220  
   221  	if slashCount > 1 {
   222  		return false
   223  	}
   224  
   225  	return slashCount == 0 || ref[0] == '/'
   226  }
   227  
   228  func (c *PageCollections) getContentNode(context page.Page, isReflink bool, ref string) (*contentNode, error) {
   229  	ref = filepath.ToSlash(strings.ToLower(strings.TrimSpace(ref)))
   230  
   231  	if ref == "" {
   232  		ref = "/"
   233  	}
   234  
   235  	inRef := ref
   236  	navUp := strings.HasPrefix(ref, "..")
   237  	var doSimpleLookup bool
   238  	if isReflink || context == nil {
   239  		doSimpleLookup = shouldDoSimpleLookup(ref)
   240  	}
   241  
   242  	if context != nil && !strings.HasPrefix(ref, "/") {
   243  		// Try the page-relative path.
   244  		var base string
   245  		if context.File().IsZero() {
   246  			base = context.SectionsPath()
   247  		} else {
   248  			meta := context.File().FileInfo().Meta()
   249  			base = filepath.ToSlash(filepath.Dir(meta.Path))
   250  			if meta.Classifier == files.ContentClassLeaf {
   251  				// Bundles are stored in subfolders e.g. blog/mybundle/index.md,
   252  				// so if the user has not explicitly asked to go up,
   253  				// look on the "blog" level.
   254  				if !navUp {
   255  					base = path.Dir(base)
   256  				}
   257  			}
   258  		}
   259  		ref = path.Join("/", strings.ToLower(base), ref)
   260  	}
   261  
   262  	if !strings.HasPrefix(ref, "/") {
   263  		ref = "/" + ref
   264  	}
   265  
   266  	m := c.pageMap
   267  
   268  	// It's either a section, a page in a section or a taxonomy node.
   269  	// Start with the most likely:
   270  	n, name := c.getSectionOrPage(ref)
   271  	if n != nil {
   272  		return n, nil
   273  	}
   274  
   275  	if !strings.HasPrefix(inRef, "/") {
   276  		// Many people will have "post/foo.md" in their content files.
   277  		if n, _ := c.getSectionOrPage("/" + inRef); n != nil {
   278  			return n, nil
   279  		}
   280  	}
   281  
   282  	// Check if it's a taxonomy node
   283  	pref := helpers.AddTrailingSlash(ref)
   284  	s, v, found := m.taxonomies.LongestPrefix(pref)
   285  
   286  	if found {
   287  		if !m.onSameLevel(pref, s) {
   288  			return nil, nil
   289  		}
   290  		return v.(*contentNode), nil
   291  	}
   292  
   293  	getByName := func(s string) (*contentNode, error) {
   294  		n := m.pageReverseIndex.Get(s)
   295  		if n != nil {
   296  			if n == ambiguousContentNode {
   297  				return nil, fmt.Errorf("page reference %q is ambiguous", ref)
   298  			}
   299  			return n, nil
   300  		}
   301  
   302  		return nil, nil
   303  	}
   304  
   305  	var module string
   306  	if context != nil && !context.File().IsZero() {
   307  		module = context.File().FileInfo().Meta().Module
   308  	}
   309  
   310  	if module == "" && !c.pageMap.s.home.File().IsZero() {
   311  		module = c.pageMap.s.home.File().FileInfo().Meta().Module
   312  	}
   313  
   314  	if module != "" {
   315  		n, err := getByName(module + ref)
   316  		if err != nil {
   317  			return nil, err
   318  		}
   319  		if n != nil {
   320  			return n, nil
   321  		}
   322  	}
   323  
   324  	if !doSimpleLookup {
   325  		return nil, nil
   326  	}
   327  
   328  	// Ref/relref supports this potentially ambiguous lookup.
   329  	return getByName(path.Base(name))
   330  }
   331  
   332  func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
   333  	var pages page.Pages
   334  	for _, p := range inPages {
   335  		if p.Kind() == kind {
   336  			pages = append(pages, p)
   337  		}
   338  	}
   339  	return pages
   340  }