github.com/neohugo/neohugo@v0.123.8/hugolib/pagecollections.go (about)

     1  // Copyright 2024 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/neohugo/neohugo/hugofs"
    23  	"github.com/neohugo/neohugo/hugofs/files"
    24  
    25  	"github.com/neohugo/neohugo/common/paths"
    26  
    27  	"github.com/neohugo/neohugo/resources/kinds"
    28  	"github.com/neohugo/neohugo/resources/page"
    29  )
    30  
    31  // pageFinder provides ways to find a Page in a Site.
    32  type pageFinder struct {
    33  	pageMap *pageMap
    34  }
    35  
    36  func newPageFinder(m *pageMap) *pageFinder {
    37  	if m == nil {
    38  		panic("must provide a pageMap")
    39  	}
    40  	c := &pageFinder{pageMap: m}
    41  	return c
    42  }
    43  
    44  // getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive
    45  // search path than getPage.
    46  func (c *pageFinder) getPageRef(context page.Page, ref string) (page.Page, error) {
    47  	n, err := c.getContentNode(context, true, ref)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	if p, ok := n.(page.Page); ok {
    53  		return p, nil
    54  	}
    55  	return nil, nil
    56  }
    57  
    58  func (c *pageFinder) getPage(context page.Page, ref string) (page.Page, error) {
    59  	n, err := c.getContentNode(context, false, ref)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	if p, ok := n.(page.Page); ok {
    64  		return p, nil
    65  	}
    66  	return nil, nil
    67  }
    68  
    69  // Only used in tests.
    70  func (c *pageFinder) getPageOldVersion(kind string, sections ...string) page.Page {
    71  	refs := append([]string{kind}, path.Join(sections...))
    72  	p, _ := c.getPageForRefs(refs...)
    73  	return p
    74  }
    75  
    76  // This is an adapter func for the old API with Kind as first argument.
    77  // This is invoked when you do .Site.GetPage. We drop the Kind and fails
    78  // if there are more than 2 arguments, which would be ambiguous.
    79  func (c *pageFinder) getPageForRefs(ref ...string) (page.Page, error) {
    80  	var refs []string
    81  	for _, r := range ref {
    82  		// A common construct in the wild is
    83  		// .Site.GetPage "home" "" or
    84  		// .Site.GetPage "home" "/"
    85  		if r != "" && r != "/" {
    86  			refs = append(refs, r)
    87  		}
    88  	}
    89  
    90  	var key string
    91  
    92  	if len(refs) > 2 {
    93  		// This was allowed in Hugo <= 0.44, but we cannot support this with the
    94  		// new API. This should be the most unusual case.
    95  		return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
    96  	}
    97  
    98  	if len(refs) == 0 || refs[0] == kinds.KindHome {
    99  		key = "/"
   100  	} else if len(refs) == 1 {
   101  		if len(ref) == 2 && refs[0] == kinds.KindSection {
   102  			// This is an old style reference to the "Home Page section".
   103  			// Typically fetched via {{ .Site.GetPage "section" .Section }}
   104  			// See https://github.com/gohugoio/hugo/issues/4989
   105  			key = "/"
   106  		} else {
   107  			key = refs[0]
   108  		}
   109  	} else {
   110  		key = refs[1]
   111  	}
   112  
   113  	key = filepath.ToSlash(key)
   114  	if !strings.HasPrefix(key, "/") {
   115  		key = "/" + key
   116  	}
   117  
   118  	return c.getPage(nil, key)
   119  }
   120  
   121  const defaultContentExt = ".md"
   122  
   123  func (c *pageFinder) getContentNode(context page.Page, isReflink bool, ref string) (contentNodeI, error) {
   124  	ref = paths.ToSlashTrimTrailing(ref)
   125  	inRef := ref
   126  	if ref == "" {
   127  		ref = "/"
   128  	}
   129  
   130  	if paths.HasExt(ref) {
   131  		return c.getContentNodeForRef(context, isReflink, true, inRef, ref)
   132  	}
   133  
   134  	// We are always looking for a content file and having an extension greatly simplifies the code that follows,
   135  	// even in the case where the extension does not match this one.
   136  	if ref == "/" {
   137  		if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, "/_index"+defaultContentExt); n != nil || err != nil {
   138  			return n, err
   139  		}
   140  	} else if strings.HasSuffix(ref, "/index") {
   141  		if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+"/index"+defaultContentExt); n != nil || err != nil {
   142  			return n, err
   143  		}
   144  		if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+defaultContentExt); n != nil || err != nil {
   145  			return n, err
   146  		}
   147  	} else {
   148  		if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+defaultContentExt); n != nil || err != nil {
   149  			return n, err
   150  		}
   151  	}
   152  
   153  	return nil, nil
   154  }
   155  
   156  func (c *pageFinder) getContentNodeForRef(context page.Page, isReflink, hadExtension bool, inRef, ref string) (contentNodeI, error) {
   157  	s := c.pageMap.s
   158  	contentPathParser := s.Conf.PathParser()
   159  
   160  	if context != nil && !strings.HasPrefix(ref, "/") {
   161  		// Try the page-relative path first.
   162  		// Branch pages: /mysection, "./mypage" => /mysection/mypage
   163  		// Regular pages: /mysection/mypage.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage
   164  		// Regular leaf bundles: /mysection/mypage/index.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage
   165  		// Given the above, for regular pages we use the containing folder.
   166  		var baseDir string
   167  		if pi := context.PathInfo(); pi != nil {
   168  			if pi.IsBranchBundle() || (hadExtension && strings.HasPrefix(ref, "../")) {
   169  				baseDir = pi.Dir()
   170  			} else {
   171  				baseDir = pi.ContainerDir()
   172  			}
   173  		}
   174  
   175  		rel := path.Join(baseDir, ref)
   176  
   177  		relPath, _ := contentPathParser.ParseBaseAndBaseNameNoIdentifier(files.ComponentFolderContent, rel)
   178  
   179  		n, err := c.getContentNodeFromPath(relPath, ref)
   180  		if n != nil || err != nil {
   181  			return n, err
   182  		}
   183  
   184  		if hadExtension && context.File() != nil {
   185  			if n, err := c.getContentNodeFromRefReverseLookup(inRef, context.File().FileInfo()); n != nil || err != nil {
   186  				return n, err
   187  			}
   188  		}
   189  
   190  	}
   191  
   192  	if strings.HasPrefix(ref, ".") {
   193  		// Page relative, no need to look further.
   194  		return nil, nil
   195  	}
   196  
   197  	relPath, nameNoIdentifier := contentPathParser.ParseBaseAndBaseNameNoIdentifier(files.ComponentFolderContent, ref)
   198  
   199  	n, err := c.getContentNodeFromPath(relPath, ref)
   200  
   201  	if n != nil || err != nil {
   202  		return n, err
   203  	}
   204  
   205  	if hadExtension && s.home != nil && s.home.File() != nil {
   206  		if n, err := c.getContentNodeFromRefReverseLookup(inRef, s.home.File().FileInfo()); n != nil || err != nil {
   207  			return n, err
   208  		}
   209  	}
   210  
   211  	var doSimpleLookup bool
   212  	if isReflink || context == nil {
   213  		slashCount := strings.Count(inRef, "/")
   214  		if slashCount <= 1 {
   215  			doSimpleLookup = slashCount == 0 || ref[0] == '/'
   216  		}
   217  	}
   218  
   219  	if !doSimpleLookup {
   220  		return nil, nil
   221  	}
   222  
   223  	n = c.pageMap.pageReverseIndex.Get(nameNoIdentifier)
   224  	if n == ambiguousContentNode {
   225  		return nil, fmt.Errorf("page reference %q is ambiguous", inRef)
   226  	}
   227  
   228  	return n, nil
   229  }
   230  
   231  func (c *pageFinder) getContentNodeFromRefReverseLookup(ref string, fi hugofs.FileMetaInfo) (contentNodeI, error) {
   232  	s := c.pageMap.s
   233  	meta := fi.Meta()
   234  	dir := meta.Filename
   235  	if !fi.IsDir() {
   236  		dir = filepath.Dir(meta.Filename)
   237  	}
   238  
   239  	realFilename := filepath.Join(dir, ref)
   240  
   241  	pcs, err := s.BaseFs.Content.ReverseLookup(realFilename, true)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	// There may be multiple matches, but we will only use the first one.
   247  	for _, pc := range pcs {
   248  		pi := s.Conf.PathParser().Parse(pc.Component, pc.Path)
   249  		if n := c.pageMap.treePages.Get(pi.Base()); n != nil {
   250  			return n, nil
   251  		}
   252  	}
   253  	return nil, nil
   254  }
   255  
   256  func (c *pageFinder) getContentNodeFromPath(s string, ref string) (contentNodeI, error) {
   257  	n := c.pageMap.treePages.Get(s)
   258  	if n != nil {
   259  		return n, nil
   260  	}
   261  
   262  	return nil, nil
   263  }