github.com/fighterlyt/hugo@v0.47.1/hugolib/permalinks.go (about)

     1  // Copyright 2015 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  	"errors"
    18  	"fmt"
    19  	"path"
    20  	"path/filepath"
    21  	"regexp"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/gohugoio/hugo/helpers"
    26  )
    27  
    28  // pathPattern represents a string which builds up a URL from attributes
    29  type pathPattern string
    30  
    31  // pageToPermaAttribute is the type of a function which, given a page and a tag
    32  // can return a string to go in that position in the page (or an error)
    33  type pageToPermaAttribute func(*Page, string) (string, error)
    34  
    35  // PermalinkOverrides maps a section name to a PathPattern
    36  type PermalinkOverrides map[string]pathPattern
    37  
    38  // knownPermalinkAttributes maps :tags in a permalink specification to a
    39  // function which, given a page and the tag, returns the resulting string
    40  // to be used to replace that tag.
    41  var knownPermalinkAttributes map[string]pageToPermaAttribute
    42  
    43  var attributeRegexp *regexp.Regexp
    44  
    45  // validate determines if a PathPattern is well-formed
    46  func (pp pathPattern) validate() bool {
    47  	fragments := strings.Split(string(pp[1:]), "/")
    48  	var bail = false
    49  	for i := range fragments {
    50  		if bail {
    51  			return false
    52  		}
    53  		if len(fragments[i]) == 0 {
    54  			bail = true
    55  			continue
    56  		}
    57  
    58  		matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1)
    59  		if matches == nil {
    60  			continue
    61  		}
    62  
    63  		for _, match := range matches {
    64  			k := strings.ToLower(match[0][1:])
    65  			if _, ok := knownPermalinkAttributes[k]; !ok {
    66  				return false
    67  			}
    68  		}
    69  	}
    70  	return true
    71  }
    72  
    73  type permalinkExpandError struct {
    74  	pattern pathPattern
    75  	section string
    76  	err     error
    77  }
    78  
    79  func (pee *permalinkExpandError) Error() string {
    80  	return fmt.Sprintf("error expanding %q section %q: %s", string(pee.pattern), pee.section, pee.err)
    81  }
    82  
    83  var (
    84  	errPermalinkIllFormed        = errors.New("permalink ill-formed")
    85  	errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
    86  )
    87  
    88  // Expand on a PathPattern takes a Page and returns the fully expanded Permalink
    89  // or an error explaining the failure.
    90  func (pp pathPattern) Expand(p *Page) (string, error) {
    91  	if !pp.validate() {
    92  		return "", &permalinkExpandError{pattern: pp, section: "<all>", err: errPermalinkIllFormed}
    93  	}
    94  	sections := strings.Split(string(pp), "/")
    95  	for i, field := range sections {
    96  		if len(field) == 0 {
    97  			continue
    98  		}
    99  
   100  		matches := attributeRegexp.FindAllStringSubmatch(field, -1)
   101  
   102  		if matches == nil {
   103  			continue
   104  		}
   105  
   106  		newField := field
   107  
   108  		for _, match := range matches {
   109  			attr := match[0][1:]
   110  			callback, ok := knownPermalinkAttributes[attr]
   111  
   112  			if !ok {
   113  				return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: errPermalinkAttributeUnknown}
   114  			}
   115  
   116  			newAttr, err := callback(p, attr)
   117  
   118  			if err != nil {
   119  				return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: err}
   120  			}
   121  
   122  			newField = strings.Replace(newField, match[0], newAttr, 1)
   123  		}
   124  
   125  		sections[i] = newField
   126  	}
   127  	return strings.Join(sections, "/"), nil
   128  }
   129  
   130  func pageToPermalinkDate(p *Page, dateField string) (string, error) {
   131  	// a Page contains a Node which provides a field Date, time.Time
   132  	switch dateField {
   133  	case "year":
   134  		return strconv.Itoa(p.Date.Year()), nil
   135  	case "month":
   136  		return fmt.Sprintf("%02d", int(p.Date.Month())), nil
   137  	case "monthname":
   138  		return p.Date.Month().String(), nil
   139  	case "day":
   140  		return fmt.Sprintf("%02d", p.Date.Day()), nil
   141  	case "weekday":
   142  		return strconv.Itoa(int(p.Date.Weekday())), nil
   143  	case "weekdayname":
   144  		return p.Date.Weekday().String(), nil
   145  	case "yearday":
   146  		return strconv.Itoa(p.Date.YearDay()), nil
   147  	}
   148  	//TODO: support classic strftime escapes too
   149  	// (and pass those through despite not being in the map)
   150  	panic("coding error: should not be here")
   151  }
   152  
   153  // pageToPermalinkTitle returns the URL-safe form of the title
   154  func pageToPermalinkTitle(p *Page, _ string) (string, error) {
   155  	// Page contains Node which has Title
   156  	// (also contains URLPath which has Slug, sometimes)
   157  	return p.s.PathSpec.URLize(p.title), nil
   158  }
   159  
   160  // pageToPermalinkFilename returns the URL-safe form of the filename
   161  func pageToPermalinkFilename(p *Page, _ string) (string, error) {
   162  	name := p.File.TranslationBaseName()
   163  	if name == "index" {
   164  		// Page bundles; the directory name will hopefully have a better name.
   165  		dir := strings.TrimSuffix(p.File.Dir(), helpers.FilePathSeparator)
   166  		_, name = filepath.Split(dir)
   167  	}
   168  
   169  	return p.s.PathSpec.URLize(name), nil
   170  }
   171  
   172  // if the page has a slug, return the slug, else return the title
   173  func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) {
   174  	if p.Slug != "" {
   175  		// Don't start or end with a -
   176  		// TODO(bep) this doesn't look good... Set the Slug once.
   177  		if strings.HasPrefix(p.Slug, "-") {
   178  			p.Slug = p.Slug[1:len(p.Slug)]
   179  		}
   180  
   181  		if strings.HasSuffix(p.Slug, "-") {
   182  			p.Slug = p.Slug[0 : len(p.Slug)-1]
   183  		}
   184  		return p.s.PathSpec.URLize(p.Slug), nil
   185  	}
   186  	return pageToPermalinkTitle(p, a)
   187  }
   188  
   189  func pageToPermalinkSection(p *Page, _ string) (string, error) {
   190  	// Page contains Node contains URLPath which has Section
   191  	return p.Section(), nil
   192  }
   193  
   194  func pageToPermalinkSections(p *Page, _ string) (string, error) {
   195  	// TODO(bep) we have some superflous URLize in this file, but let's
   196  	// deal with that later.
   197  	return path.Join(p.CurrentSection().sections...), nil
   198  }
   199  
   200  func init() {
   201  	knownPermalinkAttributes = map[string]pageToPermaAttribute{
   202  		"year":        pageToPermalinkDate,
   203  		"month":       pageToPermalinkDate,
   204  		"monthname":   pageToPermalinkDate,
   205  		"day":         pageToPermalinkDate,
   206  		"weekday":     pageToPermalinkDate,
   207  		"weekdayname": pageToPermalinkDate,
   208  		"yearday":     pageToPermalinkDate,
   209  		"section":     pageToPermalinkSection,
   210  		"sections":    pageToPermalinkSections,
   211  		"title":       pageToPermalinkTitle,
   212  		"slug":        pageToPermalinkSlugElseTitle,
   213  		"filename":    pageToPermalinkFilename,
   214  	}
   215  
   216  	attributeRegexp = regexp.MustCompile(`:\w+`)
   217  }