diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-01-02 12:33:26 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-03-23 18:51:22 +0100 |
commit | 597e418cb02883418f2cebb41400e8e61413f651 (patch) | |
tree | 177ad9c540b2583b6dab138c9f0490d28989c7f7 /resources/page/permalinks.go | |
parent | 44f5c1c14cb1f42cc5f01739c289e9cfc83602af (diff) |
Make Page an interface
The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct.
This is all a preparation step for issue #5074, "pages from other data sources".
But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes.
Most notable changes:
* The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday.
This means that any markdown will partake in the global ToC and footnote context etc.
* The Custom Output formats are now "fully virtualized". This removes many of the current limitations.
* The taxonomy list type now has a reference to the `Page` object.
This improves the taxonomy template `.Title` situation and make common template constructs much simpler.
See #5074
Fixes #5763
Fixes #5758
Fixes #5090
Fixes #5204
Fixes #4695
Fixes #5607
Fixes #5707
Fixes #5719
Fixes #3113
Fixes #5706
Fixes #5767
Fixes #5723
Fixes #5769
Fixes #5770
Fixes #5771
Fixes #5759
Fixes #5776
Fixes #5777
Fixes #5778
Diffstat (limited to 'resources/page/permalinks.go')
-rw-r--r-- | resources/page/permalinks.go | 248 |
1 files changed, 248 insertions, 0 deletions
diff --git a/resources/page/permalinks.go b/resources/page/permalinks.go new file mode 100644 index 000000000..98489231b --- /dev/null +++ b/resources/page/permalinks.go @@ -0,0 +1,248 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package page + +import ( + "fmt" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/gohugoio/hugo/helpers" +) + +// PermalinkExpander holds permalin mappings per section. +type PermalinkExpander struct { + // knownPermalinkAttributes maps :tags in a permalink specification to a + // function which, given a page and the tag, returns the resulting string + // to be used to replace that tag. + knownPermalinkAttributes map[string]pageToPermaAttribute + + expanders map[string]func(Page) (string, error) + + ps *helpers.PathSpec +} + +// NewPermalinkExpander creates a new PermalinkExpander configured by the given +// PathSpec. +func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) { + + p := PermalinkExpander{ps: ps} + + p.knownPermalinkAttributes = map[string]pageToPermaAttribute{ + "year": p.pageToPermalinkDate, + "month": p.pageToPermalinkDate, + "monthname": p.pageToPermalinkDate, + "day": p.pageToPermalinkDate, + "weekday": p.pageToPermalinkDate, + "weekdayname": p.pageToPermalinkDate, + "yearday": p.pageToPermalinkDate, + "section": p.pageToPermalinkSection, + "sections": p.pageToPermalinkSections, + "title": p.pageToPermalinkTitle, + "slug": p.pageToPermalinkSlugElseTitle, + "filename": p.pageToPermalinkFilename, + } + + patterns := ps.Cfg.GetStringMapString("permalinks") + if patterns == nil { + return p, nil + } + + e, err := p.parse(patterns) + if err != nil { + return p, err + } + + p.expanders = e + + return p, nil +} + +// Expand expands the path in p according to the rules defined for the given key. +// If no rules are found for the given key, an empty string is returned. +func (l PermalinkExpander) Expand(key string, p Page) (string, error) { + expand, found := l.expanders[key] + + if !found { + return "", nil + } + + return expand(p) + +} + +func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) { + + expanders := make(map[string]func(Page) (string, error)) + + for k, pattern := range patterns { + if !l.validate(pattern) { + return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed} + } + + pattern := pattern + matches := attributeRegexp.FindAllStringSubmatch(pattern, -1) + + callbacks := make([]pageToPermaAttribute, len(matches)) + replacements := make([]string, len(matches)) + for i, m := range matches { + replacement := m[0] + attr := replacement[1:] + replacements[i] = replacement + callback, ok := l.knownPermalinkAttributes[attr] + + if !ok { + return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkAttributeUnknown} + } + + callbacks[i] = callback + } + + expanders[k] = func(p Page) (string, error) { + + if matches == nil { + return pattern, nil + } + + newField := pattern + + for i, replacement := range replacements { + attr := replacement[1:] + callback := callbacks[i] + newAttr, err := callback(p, attr) + + if err != nil { + return "", &permalinkExpandError{pattern: pattern, err: err} + } + + newField = strings.Replace(newField, replacement, newAttr, 1) + + } + + return newField, nil + + } + + } + + return expanders, nil +} + +// pageToPermaAttribute is the type of a function which, given a page and a tag +// can return a string to go in that position in the page (or an error) +type pageToPermaAttribute func(Page, string) (string, error) + +var attributeRegexp = regexp.MustCompile(`:\w+`) + +// validate determines if a PathPattern is well-formed +func (l PermalinkExpander) validate(pp string) bool { + fragments := strings.Split(pp[1:], "/") + var bail = false + for i := range fragments { + if bail { + return false + } + if len(fragments[i]) == 0 { + bail = true + continue + } + + matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1) + if matches == nil { + continue + } + + for _, match := range matches { + k := strings.ToLower(match[0][1:]) + if _, ok := l.knownPermalinkAttributes[k]; !ok { + return false + } + } + } + return true +} + +type permalinkExpandError struct { + pattern string + err error +} + +func (pee *permalinkExpandError) Error() string { + return fmt.Sprintf("error expanding %q: %s", string(pee.pattern), pee.err) +} + +var ( + errPermalinkIllFormed = errors.New("permalink ill-formed") + errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised") +) + +func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string, error) { + // a Page contains a Node which provides a field Date, time.Time + switch dateField { + case "year": + return strconv.Itoa(p.Date().Year()), nil + case "month": + return fmt.Sprintf("%02d", int(p.Date().Month())), nil + case "monthname": + return p.Date().Month().String(), nil + case "day": + return fmt.Sprintf("%02d", p.Date().Day()), nil + case "weekday": + return strconv.Itoa(int(p.Date().Weekday())), nil + case "weekdayname": + return p.Date().Weekday().String(), nil + case "yearday": + return strconv.Itoa(p.Date().YearDay()), nil + } + //TODO: support classic strftime escapes too + // (and pass those through despite not being in the map) + panic("coding error: should not be here") +} + +// pageToPermalinkTitle returns the URL-safe form of the title +func (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error) { + return l.ps.URLize(p.Title()), nil +} + +// pageToPermalinkFilename returns the URL-safe form of the filename +func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, error) { + name := p.File().TranslationBaseName() + if name == "index" { + // Page bundles; the directory name will hopefully have a better name. + dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator) + _, name = filepath.Split(dir) + } + + return l.ps.URLize(name), nil +} + +// if the page has a slug, return the slug, else return the title +func (l PermalinkExpander) pageToPermalinkSlugElseTitle(p Page, a string) (string, error) { + if p.Slug() != "" { + return l.ps.URLize(p.Slug()), nil + } + return l.pageToPermalinkTitle(p, a) +} + +func (l PermalinkExpander) pageToPermalinkSection(p Page, _ string) (string, error) { + return p.Section(), nil +} + +func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) { + return p.CurrentSection().SectionsPath(), nil +} |