github.com/jbramsden/hugo@v0.47.1/hugolib/page_collections.go (about)

     1  // Copyright 2016 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  
    22  	"github.com/gohugoio/hugo/cache"
    23  	"github.com/gohugoio/hugo/helpers"
    24  )
    25  
    26  // PageCollections contains the page collections for a site.
    27  type PageCollections struct {
    28  	// Includes only pages of all types, and only pages in the current language.
    29  	Pages Pages
    30  
    31  	// Includes all pages in all languages, including the current one.
    32  	// Includes pages of all types.
    33  	AllPages Pages
    34  
    35  	// A convenience cache for the traditional index types, taxonomies, home page etc.
    36  	// This is for the current language only.
    37  	indexPages Pages
    38  
    39  	// A convenience cache for the regular pages.
    40  	// This is for the current language only.
    41  	RegularPages Pages
    42  
    43  	// A convenience cache for the all the regular pages.
    44  	AllRegularPages Pages
    45  
    46  	// Includes absolute all pages (of all types), including drafts etc.
    47  	rawAllPages Pages
    48  
    49  	// Includes headless bundles, i.e. bundles that produce no output for its content page.
    50  	headlessPages Pages
    51  
    52  	pageIndex *cache.Lazy
    53  }
    54  
    55  // Get initializes the index if not already done so, then
    56  // looks up the given page ref, returns nil if no value found.
    57  func (c *PageCollections) getFromCache(ref string) (*Page, error) {
    58  	v, found, err := c.pageIndex.Get(ref)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	if !found {
    63  		return nil, nil
    64  	}
    65  
    66  	p := v.(*Page)
    67  
    68  	if p != ambiguityFlag {
    69  		return p, nil
    70  	}
    71  	return nil, fmt.Errorf("page reference %q is ambiguous", ref)
    72  }
    73  
    74  var ambiguityFlag = &Page{Kind: kindUnknown, title: "ambiguity flag"}
    75  
    76  func (c *PageCollections) refreshPageCaches() {
    77  	c.indexPages = c.findPagesByKindNotIn(KindPage, c.Pages)
    78  	c.RegularPages = c.findPagesByKindIn(KindPage, c.Pages)
    79  	c.AllRegularPages = c.findPagesByKindIn(KindPage, c.AllPages)
    80  
    81  	indexLoader := func() (map[string]interface{}, error) {
    82  		index := make(map[string]interface{})
    83  
    84  		add := func(ref string, p *Page) {
    85  			existing := index[ref]
    86  			if existing == nil {
    87  				index[ref] = p
    88  			} else if existing != ambiguityFlag && existing != p {
    89  				index[ref] = ambiguityFlag
    90  			}
    91  		}
    92  
    93  		for _, pageCollection := range []Pages{c.RegularPages, c.headlessPages} {
    94  			for _, p := range pageCollection {
    95  				sourceRef := p.absoluteSourceRef()
    96  
    97  				if sourceRef != "" {
    98  					// index the canonical ref
    99  					// e.g. /section/article.md
   100  					add(sourceRef, p)
   101  				}
   102  
   103  				// Ref/Relref supports this potentially ambiguous lookup.
   104  				add(p.Source.LogicalName(), p)
   105  
   106  				translationBaseName := p.Source.TranslationBaseName()
   107  
   108  				dir, _ := path.Split(sourceRef)
   109  				dir = strings.TrimSuffix(dir, "/")
   110  
   111  				if translationBaseName == "index" {
   112  					add(dir, p)
   113  					add(path.Base(dir), p)
   114  				} else {
   115  					add(translationBaseName, p)
   116  				}
   117  
   118  				// We need a way to get to the current language version.
   119  				pathWithNoExtensions := path.Join(dir, translationBaseName)
   120  				add(pathWithNoExtensions, p)
   121  			}
   122  		}
   123  
   124  		for _, p := range c.indexPages {
   125  			// index the canonical, unambiguous ref for any backing file
   126  			// e.g. /section/_index.md
   127  			sourceRef := p.absoluteSourceRef()
   128  			if sourceRef != "" {
   129  				add(sourceRef, p)
   130  			}
   131  
   132  			ref := path.Join(p.sections...)
   133  
   134  			// index the canonical, unambiguous virtual ref
   135  			// e.g. /section
   136  			// (this may already have been indexed above)
   137  			add("/"+ref, p)
   138  		}
   139  
   140  		return index, nil
   141  	}
   142  
   143  	c.pageIndex = cache.NewLazy(indexLoader)
   144  }
   145  
   146  func newPageCollections() *PageCollections {
   147  	return &PageCollections{}
   148  }
   149  
   150  func newPageCollectionsFromPages(pages Pages) *PageCollections {
   151  	return &PageCollections{rawAllPages: pages}
   152  }
   153  
   154  // This is an adapter func for the old API with Kind as first argument.
   155  // This is invoked when you do .Site.GetPage. We drop the Kind and fails
   156  // if there are more than 2 arguments, which would be ambigous.
   157  func (c *PageCollections) getPageOldVersion(ref ...string) (*Page, error) {
   158  	var refs []string
   159  	for _, r := range ref {
   160  		// A common construct in the wild is
   161  		// .Site.GetPage "home" "" or
   162  		// .Site.GetPage "home" "/"
   163  		if r != "" && r != "/" {
   164  			refs = append(refs, r)
   165  		}
   166  	}
   167  
   168  	var key string
   169  
   170  	if len(refs) > 2 {
   171  		// This was allowed in Hugo <= 0.44, but we cannot support this with the
   172  		// new API. This should be the most unusual case.
   173  		return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
   174  	}
   175  
   176  	if len(refs) == 0 || refs[0] == KindHome {
   177  		key = "/"
   178  	} else if len(refs) == 1 {
   179  		if len(ref) == 2 && refs[0] == KindSection {
   180  			// This is an old style reference to the "Home Page section".
   181  			// Typically fetched via {{ .Site.GetPage "section" .Section }}
   182  			// See https://github.com/gohugoio/hugo/issues/4989
   183  			key = "/"
   184  		} else {
   185  			key = refs[0]
   186  		}
   187  	} else {
   188  		key = refs[1]
   189  	}
   190  
   191  	key = filepath.ToSlash(key)
   192  	if !strings.HasPrefix(key, "/") {
   193  		key = "/" + key
   194  	}
   195  
   196  	return c.getPageNew(nil, key)
   197  }
   198  
   199  // 	Only used in tests.
   200  func (c *PageCollections) getPage(typ string, sections ...string) *Page {
   201  	refs := append([]string{typ}, path.Join(sections...))
   202  	p, _ := c.getPageOldVersion(refs...)
   203  	return p
   204  }
   205  
   206  // Ref is either unix-style paths (i.e. callers responsible for
   207  // calling filepath.ToSlash as necessary) or shorthand refs.
   208  func (c *PageCollections) getPageNew(context *Page, ref string) (*Page, error) {
   209  
   210  	// Absolute (content root relative) reference.
   211  	if strings.HasPrefix(ref, "/") {
   212  		if p, err := c.getFromCache(ref); err == nil && p != nil {
   213  			return p, nil
   214  		}
   215  	} else if context != nil {
   216  		// Try the page-relative path.
   217  		ppath := path.Join("/", strings.Join(context.sections, "/"), ref)
   218  		if p, err := c.getFromCache(ppath); err == nil && p != nil {
   219  			return p, nil
   220  		}
   221  	}
   222  
   223  	if !strings.HasPrefix(ref, "/") {
   224  		// Many people will have "post/foo.md" in their content files.
   225  		if p, err := c.getFromCache("/" + ref); err == nil && p != nil {
   226  			if context != nil {
   227  				// TODO(bep) remove this case and the message below when the storm has passed
   228  				helpers.DistinctFeedbackLog.Printf(`WARNING: make non-relative ref/relref page reference(s) in page %q absolute, e.g. {{< ref "/blog/my-post.md" >}}`, context.absoluteSourceRef())
   229  			}
   230  			return p, nil
   231  		}
   232  	}
   233  
   234  	// Last try.
   235  	ref = strings.TrimPrefix(ref, "/")
   236  	p, err := c.getFromCache(ref)
   237  
   238  	if err != nil {
   239  		if context != nil {
   240  			return nil, fmt.Errorf("failed to resolve path from page %q: %s", context.absoluteSourceRef(), err)
   241  		}
   242  		return nil, fmt.Errorf("failed to resolve page: %s", err)
   243  	}
   244  
   245  	return p, nil
   246  }
   247  
   248  func (*PageCollections) findPagesByKindIn(kind string, inPages Pages) Pages {
   249  	var pages Pages
   250  	for _, p := range inPages {
   251  		if p.Kind == kind {
   252  			pages = append(pages, p)
   253  		}
   254  	}
   255  	return pages
   256  }
   257  
   258  func (*PageCollections) findFirstPageByKindIn(kind string, inPages Pages) *Page {
   259  	for _, p := range inPages {
   260  		if p.Kind == kind {
   261  			return p
   262  		}
   263  	}
   264  	return nil
   265  }
   266  
   267  func (*PageCollections) findPagesByKindNotIn(kind string, inPages Pages) Pages {
   268  	var pages Pages
   269  	for _, p := range inPages {
   270  		if p.Kind != kind {
   271  			pages = append(pages, p)
   272  		}
   273  	}
   274  	return pages
   275  }
   276  
   277  func (c *PageCollections) findPagesByKind(kind string) Pages {
   278  	return c.findPagesByKindIn(kind, c.Pages)
   279  }
   280  
   281  func (c *PageCollections) addPage(page *Page) {
   282  	c.rawAllPages = append(c.rawAllPages, page)
   283  }
   284  
   285  func (c *PageCollections) removePageFilename(filename string) {
   286  	if i := c.rawAllPages.findPagePosByFilename(filename); i >= 0 {
   287  		c.clearResourceCacheForPage(c.rawAllPages[i])
   288  		c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...)
   289  	}
   290  
   291  }
   292  
   293  func (c *PageCollections) removePage(page *Page) {
   294  	if i := c.rawAllPages.findPagePos(page); i >= 0 {
   295  		c.clearResourceCacheForPage(c.rawAllPages[i])
   296  		c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...)
   297  	}
   298  
   299  }
   300  
   301  func (c *PageCollections) findPagesByShortcode(shortcode string) Pages {
   302  	var pages Pages
   303  
   304  	for _, p := range c.rawAllPages {
   305  		if p.shortcodeState != nil {
   306  			if _, ok := p.shortcodeState.nameSet[shortcode]; ok {
   307  				pages = append(pages, p)
   308  			}
   309  		}
   310  	}
   311  	return pages
   312  }
   313  
   314  func (c *PageCollections) replacePage(page *Page) {
   315  	// will find existing page that matches filepath and remove it
   316  	c.removePage(page)
   317  	c.addPage(page)
   318  }
   319  
   320  func (c *PageCollections) clearResourceCacheForPage(page *Page) {
   321  	if len(page.Resources) > 0 {
   322  		page.s.ResourceSpec.DeleteCacheByPrefix(page.relTargetPathBase)
   323  	}
   324  }