github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/resources/page/permalinks.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 page
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"os"
    20  	"path"
    21  	"path/filepath"
    22  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/gohugoio/hugo/common/hstrings"
    28  	"github.com/gohugoio/hugo/common/maps"
    29  	"github.com/gohugoio/hugo/helpers"
    30  	"github.com/gohugoio/hugo/resources/kinds"
    31  )
    32  
    33  // PermalinkExpander holds permalin mappings per section.
    34  type PermalinkExpander struct {
    35  	// knownPermalinkAttributes maps :tags in a permalink specification to a
    36  	// function which, given a page and the tag, returns the resulting string
    37  	// to be used to replace that tag.
    38  	knownPermalinkAttributes map[string]pageToPermaAttribute
    39  
    40  	expanders map[string]map[string]func(Page) (string, error)
    41  
    42  	urlize func(uri string) string
    43  }
    44  
    45  // Time for checking date formats. Every field is different than the
    46  // Go reference time for date formatting. This ensures that formatting this date
    47  // with a Go time format always has a different output than the format itself.
    48  var referenceTime = time.Date(2019, time.November, 9, 23, 1, 42, 1, time.UTC)
    49  
    50  // Return the callback for the given permalink attribute and a boolean indicating if the attribute is valid or not.
    51  func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
    52  	if callback, ok := p.knownPermalinkAttributes[attr]; ok {
    53  		return callback, true
    54  	}
    55  
    56  	if strings.HasPrefix(attr, "sections[") {
    57  		fn := p.toSliceFunc(strings.TrimPrefix(attr, "sections"))
    58  		return func(p Page, s string) (string, error) {
    59  			return path.Join(fn(p.CurrentSection().SectionsEntries())...), nil
    60  		}, true
    61  	}
    62  
    63  	// Make sure this comes after all the other checks.
    64  	if referenceTime.Format(attr) != attr {
    65  		return p.pageToPermalinkDate, true
    66  	}
    67  
    68  	return nil, false
    69  }
    70  
    71  // NewPermalinkExpander creates a new PermalinkExpander configured by the given
    72  // urlize func.
    73  func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
    74  	p := PermalinkExpander{urlize: urlize}
    75  
    76  	p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
    77  		"year":           p.pageToPermalinkDate,
    78  		"month":          p.pageToPermalinkDate,
    79  		"monthname":      p.pageToPermalinkDate,
    80  		"day":            p.pageToPermalinkDate,
    81  		"weekday":        p.pageToPermalinkDate,
    82  		"weekdayname":    p.pageToPermalinkDate,
    83  		"yearday":        p.pageToPermalinkDate,
    84  		"section":        p.pageToPermalinkSection,
    85  		"sections":       p.pageToPermalinkSections,
    86  		"title":          p.pageToPermalinkTitle,
    87  		"slug":           p.pageToPermalinkSlugElseTitle,
    88  		"slugorfilename": p.pageToPermalinkSlugElseFilename,
    89  		"filename":       p.pageToPermalinkFilename,
    90  	}
    91  
    92  	p.expanders = make(map[string]map[string]func(Page) (string, error))
    93  
    94  	for kind, patterns := range patterns {
    95  		e, err := p.parse(patterns)
    96  		if err != nil {
    97  			return p, err
    98  		}
    99  		p.expanders[kind] = e
   100  	}
   101  
   102  	return p, nil
   103  }
   104  
   105  // Expand expands the path in p according to the rules defined for the given key.
   106  // If no rules are found for the given key, an empty string is returned.
   107  func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
   108  	expanders, found := l.expanders[p.Kind()]
   109  
   110  	if !found {
   111  		return "", nil
   112  	}
   113  
   114  	expand, found := expanders[key]
   115  
   116  	if !found {
   117  		return "", nil
   118  	}
   119  
   120  	return expand(p)
   121  }
   122  
   123  func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) {
   124  	expanders := make(map[string]func(Page) (string, error))
   125  
   126  	// Allow " " and / to represent the root section.
   127  	const sectionCutSet = " /" + string(os.PathSeparator)
   128  
   129  	for k, pattern := range patterns {
   130  		k = strings.Trim(k, sectionCutSet)
   131  
   132  		if !l.validate(pattern) {
   133  			return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
   134  		}
   135  
   136  		pattern := pattern
   137  		matches := attributeRegexp.FindAllStringSubmatch(pattern, -1)
   138  
   139  		callbacks := make([]pageToPermaAttribute, len(matches))
   140  		replacements := make([]string, len(matches))
   141  		for i, m := range matches {
   142  			replacement := m[0]
   143  			attr := replacement[1:]
   144  			replacements[i] = replacement
   145  			callback, ok := l.callback(attr)
   146  
   147  			if !ok {
   148  				return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkAttributeUnknown}
   149  			}
   150  
   151  			callbacks[i] = callback
   152  		}
   153  
   154  		expanders[k] = func(p Page) (string, error) {
   155  			if matches == nil {
   156  				return pattern, nil
   157  			}
   158  
   159  			newField := pattern
   160  
   161  			for i, replacement := range replacements {
   162  				attr := replacement[1:]
   163  				callback := callbacks[i]
   164  				newAttr, err := callback(p, attr)
   165  				if err != nil {
   166  					return "", &permalinkExpandError{pattern: pattern, err: err}
   167  				}
   168  
   169  				newField = strings.Replace(newField, replacement, newAttr, 1)
   170  
   171  			}
   172  
   173  			return newField, nil
   174  		}
   175  
   176  	}
   177  
   178  	return expanders, nil
   179  }
   180  
   181  // pageToPermaAttribute is the type of a function which, given a page and a tag
   182  // can return a string to go in that position in the page (or an error)
   183  type pageToPermaAttribute func(Page, string) (string, error)
   184  
   185  var attributeRegexp = regexp.MustCompile(`:\w+(\[.+?\])?`)
   186  
   187  // validate determines if a PathPattern is well-formed
   188  func (l PermalinkExpander) validate(pp string) bool {
   189  	if len(pp) == 0 {
   190  		return false
   191  	}
   192  	fragments := strings.Split(pp[1:], "/")
   193  	bail := false
   194  	for i := range fragments {
   195  		if bail {
   196  			return false
   197  		}
   198  		if len(fragments[i]) == 0 {
   199  			bail = true
   200  			continue
   201  		}
   202  
   203  		matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1)
   204  		if matches == nil {
   205  			continue
   206  		}
   207  
   208  		for _, match := range matches {
   209  			k := match[0][1:]
   210  			if _, ok := l.callback(k); !ok {
   211  				return false
   212  			}
   213  		}
   214  	}
   215  	return true
   216  }
   217  
   218  type permalinkExpandError struct {
   219  	pattern string
   220  	err     error
   221  }
   222  
   223  func (pee *permalinkExpandError) Error() string {
   224  	return fmt.Sprintf("error expanding %q: %s", pee.pattern, pee.err)
   225  }
   226  
   227  var (
   228  	errPermalinkIllFormed        = errors.New("permalink ill-formed")
   229  	errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
   230  )
   231  
   232  func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string, error) {
   233  	// a Page contains a Node which provides a field Date, time.Time
   234  	switch dateField {
   235  	case "year":
   236  		return strconv.Itoa(p.Date().Year()), nil
   237  	case "month":
   238  		return fmt.Sprintf("%02d", int(p.Date().Month())), nil
   239  	case "monthname":
   240  		return p.Date().Month().String(), nil
   241  	case "day":
   242  		return fmt.Sprintf("%02d", p.Date().Day()), nil
   243  	case "weekday":
   244  		return strconv.Itoa(int(p.Date().Weekday())), nil
   245  	case "weekdayname":
   246  		return p.Date().Weekday().String(), nil
   247  	case "yearday":
   248  		return strconv.Itoa(p.Date().YearDay()), nil
   249  	}
   250  
   251  	return p.Date().Format(dateField), nil
   252  }
   253  
   254  // pageToPermalinkTitle returns the URL-safe form of the title
   255  func (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error) {
   256  	return l.urlize(p.Title()), nil
   257  }
   258  
   259  // pageToPermalinkFilename returns the URL-safe form of the filename
   260  func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, error) {
   261  	name := l.translationBaseName(p)
   262  	if name == "index" {
   263  		// Page bundles; the directory name will hopefully have a better name.
   264  		dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator)
   265  		_, name = filepath.Split(dir)
   266  	} else if name == "_index" {
   267  		return "", nil
   268  	}
   269  
   270  	return l.urlize(name), nil
   271  }
   272  
   273  // if the page has a slug, return the slug, else return the title
   274  func (l PermalinkExpander) pageToPermalinkSlugElseTitle(p Page, a string) (string, error) {
   275  	if p.Slug() != "" {
   276  		return l.urlize(p.Slug()), nil
   277  	}
   278  	return l.pageToPermalinkTitle(p, a)
   279  }
   280  
   281  // if the page has a slug, return the slug, else return the filename
   282  func (l PermalinkExpander) pageToPermalinkSlugElseFilename(p Page, a string) (string, error) {
   283  	if p.Slug() != "" {
   284  		return l.urlize(p.Slug()), nil
   285  	}
   286  	return l.pageToPermalinkFilename(p, a)
   287  }
   288  
   289  func (l PermalinkExpander) pageToPermalinkSection(p Page, _ string) (string, error) {
   290  	return p.Section(), nil
   291  }
   292  
   293  func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) {
   294  	return p.CurrentSection().SectionsPath(), nil
   295  }
   296  
   297  func (l PermalinkExpander) translationBaseName(p Page) string {
   298  	if p.File().IsZero() {
   299  		return ""
   300  	}
   301  	return p.File().TranslationBaseName()
   302  }
   303  
   304  var (
   305  	nilSliceFunc = func(s []string) []string {
   306  		return nil
   307  	}
   308  	allSliceFunc = func(s []string) []string {
   309  		return s
   310  	}
   311  )
   312  
   313  // toSliceFunc returns a slice func that slices s according to the cut spec.
   314  // The cut spec must be on form [low:high] (one or both can be omitted),
   315  // also allowing single slice indices (e.g. [2]) and the special [last] keyword
   316  // giving the last element of the slice.
   317  // The returned function will be lenient and not panic in out of bounds situation.
   318  //
   319  // The current use case for this is to use parts of the sections path in permalinks.
   320  func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
   321  	cut = strings.ToLower(strings.TrimSpace(cut))
   322  	if cut == "" {
   323  		return allSliceFunc
   324  	}
   325  
   326  	if len(cut) < 3 || (cut[0] != '[' || cut[len(cut)-1] != ']') {
   327  		return nilSliceFunc
   328  	}
   329  
   330  	toNFunc := func(s string, low bool) func(ss []string) int {
   331  		if s == "" {
   332  			if low {
   333  				return func(ss []string) int {
   334  					return 0
   335  				}
   336  			} else {
   337  				return func(ss []string) int {
   338  					return len(ss)
   339  				}
   340  			}
   341  		}
   342  
   343  		if s == "last" {
   344  			return func(ss []string) int {
   345  				return len(ss) - 1
   346  			}
   347  		}
   348  
   349  		n, _ := strconv.Atoi(s)
   350  		if n < 0 {
   351  			n = 0
   352  		}
   353  		return func(ss []string) int {
   354  			// Prevent out of bound situations. It would not make
   355  			// much sense to panic here.
   356  			if n >= len(ss) {
   357  				if low {
   358  					return -1
   359  				}
   360  				return len(ss)
   361  			}
   362  			return n
   363  		}
   364  	}
   365  
   366  	opsStr := cut[1 : len(cut)-1]
   367  	opts := strings.Split(opsStr, ":")
   368  
   369  	if !strings.Contains(opsStr, ":") {
   370  		toN := toNFunc(opts[0], true)
   371  		return func(s []string) []string {
   372  			if len(s) == 0 {
   373  				return nil
   374  			}
   375  			n := toN(s)
   376  			if n < 0 {
   377  				return []string{}
   378  			}
   379  			v := s[n]
   380  			if v == "" {
   381  				return nil
   382  			}
   383  			return []string{v}
   384  		}
   385  	}
   386  
   387  	toN1, toN2 := toNFunc(opts[0], true), toNFunc(opts[1], false)
   388  
   389  	return func(s []string) []string {
   390  		if len(s) == 0 {
   391  			return nil
   392  		}
   393  		n1, n2 := toN1(s), toN2(s)
   394  		if n1 < 0 || n2 < 0 {
   395  			return []string{}
   396  		}
   397  		return s[n1:n2]
   398  	}
   399  }
   400  
   401  var permalinksKindsSupport = []string{kinds.KindPage, kinds.KindSection, kinds.KindTaxonomy, kinds.KindTerm}
   402  
   403  // DecodePermalinksConfig decodes the permalinks configuration in the given map
   404  func DecodePermalinksConfig(m map[string]any) (map[string]map[string]string, error) {
   405  	permalinksConfig := make(map[string]map[string]string)
   406  
   407  	permalinksConfig[kinds.KindPage] = make(map[string]string)
   408  	permalinksConfig[kinds.KindSection] = make(map[string]string)
   409  	permalinksConfig[kinds.KindTaxonomy] = make(map[string]string)
   410  	permalinksConfig[kinds.KindTerm] = make(map[string]string)
   411  
   412  	config := maps.CleanConfigStringMap(m)
   413  	for k, v := range config {
   414  		switch v := v.(type) {
   415  		case string:
   416  			// [permalinks]
   417  			//   key = '...'
   418  
   419  			// To sucessfully be backward compatible, "default" patterns need to be set for both page and term
   420  			permalinksConfig[kinds.KindPage][k] = v
   421  			permalinksConfig[kinds.KindTerm][k] = v
   422  
   423  		case maps.Params:
   424  			// [permalinks.key]
   425  			//   xyz = ???
   426  
   427  			if hstrings.InSlice(permalinksKindsSupport, k) {
   428  				// TODO: warn if we overwrite an already set value
   429  				for k2, v2 := range v {
   430  					switch v2 := v2.(type) {
   431  					case string:
   432  						permalinksConfig[k][k2] = v2
   433  
   434  					default:
   435  						return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q for kind %q", v2, k2, k)
   436  					}
   437  				}
   438  			} else {
   439  				return nil, fmt.Errorf("permalinks configuration not supported for kind %q, supported kinds are %v", k, permalinksKindsSupport)
   440  			}
   441  
   442  		default:
   443  			return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q", v, k)
   444  		}
   445  	}
   446  	return permalinksConfig, nil
   447  }