hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

permalinks.go (10034B)

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