mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
Add slice-based permalinks config with PageMatcher target
Closes #14744 Clses #4641 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-1
@@ -553,7 +553,8 @@ type SiteVectorProvider interface {
|
||||
SiteVector() sitesmatrix.Vector
|
||||
}
|
||||
|
||||
// GetSiteVector returns the site vector for a Page.
|
||||
// GetSiteVector returns the site vector for a Page,
|
||||
// or the zero value if the Page does not implement SiteVectorProvider.
|
||||
func GetSiteVector(p Page) sitesmatrix.Vector {
|
||||
if sp, ok := p.(SiteVectorProvider); ok {
|
||||
return sp.SiteVector()
|
||||
@@ -561,6 +562,14 @@ func GetSiteVector(p Page) sitesmatrix.Vector {
|
||||
return sitesmatrix.Vector{}
|
||||
}
|
||||
|
||||
// LookupSiteVector returns the site vector for a Page and whether it was found.
|
||||
func LookupSiteVector(p Page) (sitesmatrix.Vector, bool) {
|
||||
if sp, ok := p.(SiteVectorProvider); ok {
|
||||
return sp.SiteVector(), true
|
||||
}
|
||||
return sitesmatrix.Vector{}, false
|
||||
}
|
||||
|
||||
// PageWithContext is a Page with a context.Context.
|
||||
type PageWithContext struct {
|
||||
Page
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
@@ -58,6 +59,18 @@ type PageMatcher struct {
|
||||
// Compiled values.
|
||||
// The site vectors to apply this to.
|
||||
SitesMatrixCompiled sitesmatrix.VectorProvider `mapstructure:"-"`
|
||||
kindGlob glob.Glob
|
||||
pathGlob glob.Glob
|
||||
environmentGlob glob.Glob
|
||||
}
|
||||
|
||||
// Equal compares the configured fields; compiled state is ignored.
|
||||
func (m PageMatcher) Equal(other PageMatcher) bool {
|
||||
return m.Path == other.Path &&
|
||||
m.Kind == other.Kind &&
|
||||
m.Lang == other.Lang &&
|
||||
m.Environment == other.Environment &&
|
||||
m.Sites.Equal(other.Sites)
|
||||
}
|
||||
|
||||
func (m PageMatcher) Matches(p Page) bool {
|
||||
@@ -70,30 +83,23 @@ func (m PageMatcher) Match(kind, path, environment string, sitesMatrix sitesmatr
|
||||
return false
|
||||
}
|
||||
}
|
||||
if m.Kind != "" {
|
||||
g, err := hglob.GetGlob(m.Kind)
|
||||
if err == nil && !g.Match(kind) {
|
||||
return false
|
||||
}
|
||||
if m.kindGlob != nil && !m.kindGlob.Match(kind) {
|
||||
return false
|
||||
}
|
||||
|
||||
if m.Path != "" {
|
||||
g, err := hglob.GetGlob(m.Path)
|
||||
if m.pathGlob != nil {
|
||||
// TODO(bep) Path() vs filepath vs leading slash.
|
||||
p := strings.ToLower(filepath.ToSlash(path))
|
||||
if !(strings.HasPrefix(p, "/")) {
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
p = "/" + p
|
||||
}
|
||||
if err == nil && !g.Match(p) {
|
||||
if !m.pathGlob.Match(p) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if m.Environment != "" {
|
||||
g, err := hglob.GetGlob(m.Environment)
|
||||
if err == nil && !g.Match(environment) {
|
||||
return false
|
||||
}
|
||||
if m.environmentGlob != nil && !m.environmentGlob.Match(environment) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -117,7 +123,7 @@ func isGlobWithExtension(s string) bool {
|
||||
|
||||
func checkCascadePattern(logger loggers.Logger, m PageMatcher) {
|
||||
if m.Lang != "" {
|
||||
hugo.Deprecate("cascade.target.language", "cascade.target.sites.matrix instead, see https://gohugo.io/content-management/front-matter/#target", "v0.150.0")
|
||||
hugo.DeprecateWithLogger("cascade.target.language", "cascade.target.sites.matrix instead, see https://gohugo.io/content-management/front-matter/#target", "v0.150.0", logger.Logger())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,8 +255,35 @@ func (d cascadeConfigDecoder) decodePageMatcher(m any, v *PageMatcher) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *PageMatcher) compileGlobs() error {
|
||||
var err error
|
||||
if v.Kind != "" {
|
||||
v.kindGlob, err = hglob.GetGlob(v.Kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if v.Path != "" {
|
||||
v.pathGlob, err = hglob.GetGlob(v.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if v.Environment != "" {
|
||||
v.environmentGlob, err = hglob.GetGlob(v.Environment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeCascadeConfigOptions
|
||||
func (v *PageMatcher) compileSitesMatrix(defaults sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
|
||||
if err := v.compileGlobs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v.Sites.Matrix.IsZero() && defaults == nil {
|
||||
// Nothing to do.
|
||||
v.SitesMatrixCompiled = nil
|
||||
|
||||
@@ -44,35 +44,33 @@ func TestPageMatcher(t *testing.T) {
|
||||
&testPage{path: "p3", kind: "page", lang: "en", site: developmentTestSite}
|
||||
|
||||
c.Run("Matches", func(c *qt.C) {
|
||||
m := PageMatcher{Kind: "section"}
|
||||
matches := func(m PageMatcher, p Page) bool {
|
||||
c.Assert(m.compileGlobs(), qt.IsNil)
|
||||
return m.Matches(p)
|
||||
}
|
||||
|
||||
c.Assert(m.Matches(p1), qt.Equals, true)
|
||||
c.Assert(m.Matches(p2), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Kind: "section"}, p1), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Kind: "section"}, p2), qt.Equals, false)
|
||||
|
||||
m = PageMatcher{Kind: "page"}
|
||||
c.Assert(m.Matches(p1), qt.Equals, false)
|
||||
c.Assert(m.Matches(p2), qt.Equals, true)
|
||||
c.Assert(m.Matches(p3), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Kind: "page"}, p1), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Kind: "page"}, p2), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Kind: "page"}, p3), qt.Equals, true)
|
||||
|
||||
m = PageMatcher{Kind: "page", Path: "/p2"}
|
||||
c.Assert(m.Matches(p1), qt.Equals, false)
|
||||
c.Assert(m.Matches(p2), qt.Equals, true)
|
||||
c.Assert(m.Matches(p3), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Kind: "page", Path: "/p2"}, p1), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Kind: "page", Path: "/p2"}, p2), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Kind: "page", Path: "/p2"}, p3), qt.Equals, false)
|
||||
|
||||
m = PageMatcher{Path: "/p*"}
|
||||
c.Assert(m.Matches(p1), qt.Equals, true)
|
||||
c.Assert(m.Matches(p2), qt.Equals, true)
|
||||
c.Assert(m.Matches(p3), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Path: "/p*"}, p1), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Path: "/p*"}, p2), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Path: "/p*"}, p3), qt.Equals, true)
|
||||
|
||||
m = PageMatcher{Environment: "development"}
|
||||
c.Assert(m.Matches(p1), qt.Equals, true)
|
||||
c.Assert(m.Matches(p2), qt.Equals, false)
|
||||
c.Assert(m.Matches(p3), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Environment: "development"}, p1), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Environment: "development"}, p2), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Environment: "development"}, p3), qt.Equals, true)
|
||||
|
||||
m = PageMatcher{Environment: "production"}
|
||||
c.Assert(m.Matches(p1), qt.Equals, false)
|
||||
c.Assert(m.Matches(p2), qt.Equals, true)
|
||||
c.Assert(m.Matches(p3), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Environment: "production"}, p1), qt.Equals, false)
|
||||
c.Assert(matches(PageMatcher{Environment: "production"}, p2), qt.Equals, true)
|
||||
c.Assert(matches(PageMatcher{Environment: "production"}, p3), qt.Equals, false)
|
||||
})
|
||||
|
||||
c.Run("Decode", func(c *qt.C) {
|
||||
|
||||
+134
-65
@@ -26,18 +26,41 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// PermalinkExpander holds permalink mappings per section.
|
||||
// PermalinkConfig holds a single permalink rule with a target matcher and a pattern.
|
||||
type PermalinkConfig struct {
|
||||
Target PageMatcher
|
||||
Pattern string
|
||||
}
|
||||
|
||||
// PermalinksConfig is an ordered slice of permalink rules.
|
||||
// For any given Page, the first matching rule wins.
|
||||
type PermalinksConfig []PermalinkConfig
|
||||
|
||||
// InitConfig compiles the sites matrix for each permalink target.
|
||||
func (c PermalinksConfig) InitConfig(logger loggers.Logger, defaultSitesMatrix sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
|
||||
for i := range c {
|
||||
if err := c[i].Target.compileSitesMatrix(nil, configuredDimensions); err != nil {
|
||||
return fmt.Errorf("failed to compile permalink target %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PermalinkExpander holds permalink mappings.
|
||||
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]map[string]func(Page) (string, error)
|
||||
configs PermalinksConfig
|
||||
|
||||
urlize func(uri string) string
|
||||
|
||||
@@ -80,9 +103,10 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
|
||||
|
||||
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
|
||||
// urlize func.
|
||||
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
|
||||
func NewPermalinkExpander(urlize func(uri string) string, configs PermalinksConfig) (PermalinkExpander, error) {
|
||||
p := PermalinkExpander{
|
||||
urlize: urlize,
|
||||
configs: configs,
|
||||
patternCache: hmaps.NewCache[string, func(Page) (string, error)](),
|
||||
}
|
||||
|
||||
@@ -106,14 +130,15 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]ma
|
||||
"slugorcontentbasename": p.pageToPermalinkSlugOrContentBaseName,
|
||||
}
|
||||
|
||||
p.expanders = make(map[string]map[string]func(Page) (string, error))
|
||||
|
||||
for kind, patterns := range patterns {
|
||||
e, err := p.parse(patterns)
|
||||
if err != nil {
|
||||
// Validate all patterns and compile matcher globs at init time.
|
||||
for i := range configs {
|
||||
if _, err := p.getOrParsePattern(configs[i].Pattern); err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.expanders[kind] = e
|
||||
if err := configs[i].Target.compileGlobs(); err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return p, nil
|
||||
@@ -141,20 +166,29 @@ func (l PermalinkExpander) ExpandPattern(pattern string, p Page) (string, error)
|
||||
return expand(p)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
expanders, found := l.expanders[p.Kind()]
|
||||
if !found {
|
||||
// Expand expands the path in p according to the first matching permalink rule.
|
||||
// If no rules match, an empty string is returned.
|
||||
func (l PermalinkExpander) Expand(p Page) (string, error) {
|
||||
kind := p.Kind()
|
||||
if !hstrings.InSlice(permalinksKindsSupport, kind) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
expand, found := expanders[key]
|
||||
if !found {
|
||||
return "", nil
|
||||
var siteVector sitesmatrix.VectorProvider
|
||||
if sv, ok := LookupSiteVector(p); ok {
|
||||
siteVector = sv
|
||||
}
|
||||
|
||||
return expand(p)
|
||||
var environment string
|
||||
var environmentResolved bool
|
||||
for _, cfg := range l.configs {
|
||||
if cfg.Target.Environment != "" && !environmentResolved {
|
||||
environment = p.Site().Hugo().Environment()
|
||||
environmentResolved = true
|
||||
}
|
||||
if cfg.Target.Match(kind, p.Path(), environment, siteVector) {
|
||||
return l.ExpandPattern(cfg.Pattern, p)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Allow " " and / to represent the root section.
|
||||
@@ -220,23 +254,6 @@ func (l PermalinkExpander) getOrParsePattern(pattern string) (func(Page) (string
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
k = strings.Trim(k, sectionCutSet)
|
||||
|
||||
expander, err := l.getOrParsePattern(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expanders[k] = expander
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -483,50 +500,102 @@ func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
var permalinksKindsSupport = []string{kinds.KindPage, kinds.KindSection, kinds.KindTaxonomy, kinds.KindTerm}
|
||||
var permalinksKindsSupport = []string{kinds.KindPage, kinds.KindHome, kinds.KindSection, kinds.KindTaxonomy, kinds.KindTerm}
|
||||
|
||||
// DecodePermalinksConfig decodes the permalinks configuration in the given map
|
||||
func DecodePermalinksConfig(m map[string]any) (map[string]map[string]string, error) {
|
||||
permalinksConfig := make(map[string]map[string]string)
|
||||
func sectionToPathGlob(section string) string {
|
||||
section = strings.Trim(section, sectionCutSet)
|
||||
if section == "" {
|
||||
return "/*"
|
||||
}
|
||||
return "/{" + section + "," + section + "/**}"
|
||||
}
|
||||
|
||||
permalinksConfig[kinds.KindPage] = make(map[string]string)
|
||||
permalinksConfig[kinds.KindSection] = make(map[string]string)
|
||||
permalinksConfig[kinds.KindTaxonomy] = make(map[string]string)
|
||||
permalinksConfig[kinds.KindTerm] = make(map[string]string)
|
||||
// DecodePermalinksConfig decodes the permalinks configuration.
|
||||
// It supports both the new slice-based format and the legacy map-based formats.
|
||||
func DecodePermalinksConfig(in any) (PermalinksConfig, error) {
|
||||
if in == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch v := in.(type) {
|
||||
// Check legacy formats first.
|
||||
case map[string]any:
|
||||
return decodePermalinksMap(v)
|
||||
case hmaps.Params:
|
||||
return decodePermalinksMap(v)
|
||||
default:
|
||||
if ms, err := hmaps.ToSliceStringMap(in); err == nil {
|
||||
// New slice format.
|
||||
return decodePermalinksSlice(ms)
|
||||
}
|
||||
return nil, fmt.Errorf("permalinks: unsupported config type %T", in)
|
||||
}
|
||||
}
|
||||
|
||||
func decodePermalinksSlice(ms []map[string]any) (PermalinksConfig, error) {
|
||||
var configs PermalinksConfig
|
||||
for _, m := range ms {
|
||||
m = hmaps.CleanConfigStringMap(m)
|
||||
var cfg PermalinkConfig
|
||||
|
||||
if targetVal, ok := m["target"]; ok {
|
||||
if err := mapstructure.WeakDecode(targetVal, &cfg.Target); err != nil {
|
||||
return nil, fmt.Errorf("permalinks: failed to decode target: %w", err)
|
||||
}
|
||||
cfg.Target.Kind = strings.ToLower(cfg.Target.Kind)
|
||||
cfg.Target.Path = filepath.ToSlash(strings.ToLower(cfg.Target.Path))
|
||||
}
|
||||
|
||||
if patternVal, ok := m["pattern"]; ok {
|
||||
cfg.Pattern, ok = patternVal.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("permalinks: pattern must be a string, got %T", patternVal)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("permalinks: missing pattern")
|
||||
}
|
||||
|
||||
configs = append(configs, cfg)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func decodePermalinksMap(m map[string]any) (PermalinksConfig, error) {
|
||||
var configs PermalinksConfig
|
||||
config := hmaps.CleanConfigStringMap(m)
|
||||
|
||||
for k, v := range config {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
// [permalinks]
|
||||
// key = '...'
|
||||
|
||||
// To successfully be backward compatible, "default" patterns need to be set for both page and term
|
||||
permalinksConfig[kinds.KindPage][k] = v
|
||||
permalinksConfig[kinds.KindTerm][k] = v
|
||||
// Backward compat: set for both page and term.
|
||||
configs = append(configs,
|
||||
PermalinkConfig{Target: PageMatcher{Kind: kinds.KindPage, Path: sectionToPathGlob(k)}, Pattern: v},
|
||||
PermalinkConfig{Target: PageMatcher{Kind: kinds.KindTerm, Path: sectionToPathGlob(k)}, Pattern: v},
|
||||
)
|
||||
|
||||
case hmaps.Params:
|
||||
// [permalinks.key]
|
||||
// xyz = ???
|
||||
|
||||
if hstrings.InSlice(permalinksKindsSupport, k) {
|
||||
// TODO: warn if we overwrite an already set value
|
||||
for k2, v2 := range v {
|
||||
switch v2 := v2.(type) {
|
||||
case string:
|
||||
permalinksConfig[k][k2] = v2
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q for kind %q", v2, k2, k)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// [permalinks.kind]
|
||||
// section = '...'
|
||||
if !hstrings.InSlice(permalinksKindsSupport, k) {
|
||||
return nil, fmt.Errorf("permalinks configuration not supported for kind %q, supported kinds are %v", k, permalinksKindsSupport)
|
||||
}
|
||||
for k2, v2 := range v {
|
||||
switch v2 := v2.(type) {
|
||||
case string:
|
||||
configs = append(configs,
|
||||
PermalinkConfig{Target: PageMatcher{Kind: k, Path: sectionToPathGlob(k2)}, Pattern: v2},
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q for kind %q", v2, k2, k)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q", v, k)
|
||||
}
|
||||
}
|
||||
return permalinksConfig, nil
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
@@ -135,12 +135,8 @@ slug: "mytagslug"
|
||||
b.AssertFileContent("public/tagsslug/tagsslug/index.html", "List|taxonomy|/tagsslug/tagsslug/|")
|
||||
|
||||
permalinksConf := b.H.Configs.Base.Permalinks
|
||||
b.Assert(permalinksConf, qt.DeepEquals, map[string]map[string]string{
|
||||
"page": {"withallbutlastsection": "/:sections[:last]/:slug/", "withallbutlastsectionslug": "/:sectionslugs[:last]/:slug/", "withpageslug": "/pageslug/:slug/", "withsectionslug": "/sectionslug/:sectionslug/:slug/", "withsectionslugs": "/sectionslugs/:sectionslugs/:slug/"},
|
||||
"section": {"nofilefilename": "/sectionnofilefilename/:contentbasename/", "nofileslug": "/sectionnofileslug/:slug/", "nofiletitle1": "/sectionnofiletitle1/:title/", "nofiletitle2": "/sectionnofiletitle2/:sections[:last]/", "withfilefilename": "/sectionwithfilefilename/:contentbasename/", "withfilefiletitle": "/sectionwithfilefiletitle/:title/", "withfileslug": "/sectionwithfileslug/:slug/"},
|
||||
"taxonomy": {"tags": "/tagsslug/:slug/"},
|
||||
"term": {"tags": "/tagsslug/tag/:slug/"},
|
||||
})
|
||||
// 5 page + 7 section + 1 taxonomy + 1 term = 14 rules.
|
||||
b.Assert(len(permalinksConf), qt.Equals, 14)
|
||||
}
|
||||
|
||||
func TestPermalinksOldSetup(t *testing.T) {
|
||||
@@ -172,12 +168,8 @@ slug: "p1slugvalue"
|
||||
b.AssertFileContent("public/pageslug/p1slugvalue/index.html", "Single|page|/pageslug/p1slugvalue/|")
|
||||
|
||||
permalinksConf := b.H.Configs.Base.Permalinks
|
||||
b.Assert(permalinksConf, qt.DeepEquals, map[string]map[string]string{
|
||||
"page": {"withpageslug": "/pageslug/:slug/"},
|
||||
"section": {},
|
||||
"taxonomy": {},
|
||||
"term": {"withpageslug": "/pageslug/:slug/"},
|
||||
})
|
||||
// Old flat format: 1 entry creates 2 rules (page + term).
|
||||
b.Assert(len(permalinksConf), qt.Equals, 2)
|
||||
}
|
||||
|
||||
func TestPermalinksNestedSections(t *testing.T) {
|
||||
@@ -515,3 +507,266 @@ title: Tag A (set in front matter)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermalinksNewSliceFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/list.html --
|
||||
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- layouts/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.yaml --
|
||||
permalinks:
|
||||
- target:
|
||||
kind: page
|
||||
path: "/books/**"
|
||||
pattern: /books/:year/:slug/
|
||||
- target:
|
||||
kind: section
|
||||
path: "/{books,books/**}"
|
||||
pattern: /libros/:sections[1:]
|
||||
- target:
|
||||
kind: page
|
||||
pattern: /other/:slug/
|
||||
-- content/books/_index.md --
|
||||
---
|
||||
title: Books
|
||||
---
|
||||
-- content/books/fiction/_index.md --
|
||||
---
|
||||
title: Fiction
|
||||
---
|
||||
-- content/books/fiction/book1.md --
|
||||
---
|
||||
title: Book One
|
||||
date: 2023-06-15
|
||||
slug: book-one
|
||||
---
|
||||
-- content/other/p1.md --
|
||||
---
|
||||
title: Other Page
|
||||
slug: other-page
|
||||
---
|
||||
-- content/unmatched/p2.md --
|
||||
---
|
||||
title: Unmatched Page
|
||||
slug: unmatched-page
|
||||
---
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// Page in /books section gets the books-specific pattern.
|
||||
b.AssertFileContent("public/books/2023/book-one/index.html", "Single|page|/books/2023/book-one/|")
|
||||
// Section page for books/fiction gets the section pattern.
|
||||
b.AssertFileContent("public/libros/fiction/index.html", "List|section|/libros/fiction/|")
|
||||
// Page outside /books matches the default page pattern.
|
||||
b.AssertFileContent("public/other/other-page/index.html", "Single|page|/other/other-page/|")
|
||||
// Page in unmatched section also matches the default page pattern.
|
||||
b.AssertFileContent("public/other/unmatched-page/index.html", "Single|page|/other/unmatched-page/|")
|
||||
}
|
||||
|
||||
func TestPermalinksNewSliceFormatEnvironment(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/list.html --
|
||||
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- layouts/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.toml --
|
||||
disableKinds = ['home','rss','sitemap','taxonomy','term']
|
||||
[[permalinks]]
|
||||
pattern = "/testing/:slug/"
|
||||
[permalinks.target]
|
||||
path = "/books/**"
|
||||
environment = "test"
|
||||
[[permalinks]]
|
||||
pattern = "/prod/:slug/"
|
||||
[permalinks.target]
|
||||
path = "/books/**"
|
||||
environment = "production"
|
||||
-- content/books/fiction/book1.md --
|
||||
---
|
||||
title: Book One
|
||||
date: 2023-06-15
|
||||
slug: book-one
|
||||
---
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertPublishDir("prod", "! testing")
|
||||
}
|
||||
|
||||
func TestPermalinksNewSliceFormatSitesMatrix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|{{ .Language.Lang }}|
|
||||
-- hugo.yaml --
|
||||
defaultContentLanguage: en
|
||||
defaultContentLanguageInSubdir: true
|
||||
disableKinds: ['home','rss','section','sitemap','taxonomy','term']
|
||||
languages:
|
||||
en:
|
||||
weight: 1
|
||||
de:
|
||||
weight: 2
|
||||
permalinks:
|
||||
- target:
|
||||
kind: page
|
||||
sites:
|
||||
matrix:
|
||||
languages: ["en"]
|
||||
pattern: /en-posts/:slug/
|
||||
- target:
|
||||
kind: page
|
||||
sites:
|
||||
matrix:
|
||||
languages: ["de"]
|
||||
pattern: /de-posts/:slug/
|
||||
- target:
|
||||
kind: page
|
||||
pattern: /other/:slug/
|
||||
-- content/p1.en.md --
|
||||
---
|
||||
title: Hello
|
||||
slug: hello
|
||||
---
|
||||
-- content/p1.de.md --
|
||||
---
|
||||
title: Hallo
|
||||
slug: hallo
|
||||
---
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// English page matches the en-specific rule.
|
||||
b.AssertFileContent("public/en/en-posts/hello/index.html", "Single|page|/en/en-posts/hello/|en|")
|
||||
// German page matches the de-specific rule.
|
||||
b.AssertFileContent("public/de/de-posts/hallo/index.html", "Single|page|/de/de-posts/hallo/|de|")
|
||||
}
|
||||
|
||||
// We only apply permalink patterns to kinds of type hone, page, section, taxonomy and term.
|
||||
func TestPermalinksNewSliceFormatAsteriskKind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
enableRobotsTXT = true
|
||||
[[permalinks]]
|
||||
pattern = "/mylink/:slug/"
|
||||
[permalinks.target]
|
||||
kind = "*"
|
||||
-- content/mysection/p1.md --
|
||||
---
|
||||
title: My Page
|
||||
slug: my-page
|
||||
---
|
||||
-- layouts/all.html --
|
||||
{{ .Title }}|{{ .RelPermalink }}|{{ .Kind }}|
|
||||
-- layouts/404.html --
|
||||
404.
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
b.AssertPublishDir(`
|
||||
404.html
|
||||
index.html
|
||||
index.xml
|
||||
mylink/categories/index.html
|
||||
mylink/my-page/index.html
|
||||
mylink/mysections/index.html
|
||||
mylink/tags/index.html
|
||||
sitemap.xml
|
||||
`)
|
||||
}
|
||||
|
||||
func TestPermalinksHomeKind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/home.html --
|
||||
Home|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- layouts/list.html --
|
||||
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.yaml --
|
||||
disableKinds: ['rss','sitemap','taxonomy','term']
|
||||
permalinks:
|
||||
- target:
|
||||
kind: home
|
||||
pattern: /welcome/
|
||||
- target:
|
||||
kind: section
|
||||
pattern: /s/:slug/
|
||||
-- content/mysection/_index.md --
|
||||
---
|
||||
title: My Section
|
||||
slug: my-section
|
||||
---
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptWarn())
|
||||
t.Log(b.LogString())
|
||||
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
|
||||
b.AssertFileContent("public/welcome/index.html", "Home|home|/welcome/|")
|
||||
b.AssertFileContent("public/s/my-section/index.html", "List|section|/s/my-section/|")
|
||||
}
|
||||
|
||||
func TestPermalinksHomeKindLegacyMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/home.html --
|
||||
Home|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.toml --
|
||||
disableKinds = ['rss','sitemap','taxonomy','term']
|
||||
[permalinks.home]
|
||||
"/" = '/welcome/'
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptWarn())
|
||||
t.Log(b.LogString())
|
||||
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
|
||||
b.AssertFileContent("public/welcome/index.html", "Home|home|/welcome/|")
|
||||
}
|
||||
|
||||
func TestPermalinksNewSliceFormatRootSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.yaml --
|
||||
permalinks:
|
||||
- target:
|
||||
kind: page
|
||||
path: "/*"
|
||||
pattern: /root/:slug/
|
||||
- target:
|
||||
kind: page
|
||||
pattern: /deep/:slug/
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: Root Page
|
||||
slug: root-page
|
||||
---
|
||||
-- content/sub/p2.md --
|
||||
---
|
||||
title: Sub Page
|
||||
slug: sub-page
|
||||
---
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// Root section page matches /* (non-recursive).
|
||||
b.AssertFileContent("public/root/root-page/index.html", "Single|page|/root/root-page/|")
|
||||
// Nested page matches the default rule.
|
||||
b.AssertFileContent("public/deep/sub-page/index.html", "Single|page|/deep/sub-page/|")
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestPermalinkExpansion(t *testing.T) {
|
||||
page.section = "blue"
|
||||
page.slug = "The Slug"
|
||||
page.kind = "page"
|
||||
// page.pathInfo
|
||||
page.path = "/posts/test-page"
|
||||
return page
|
||||
}
|
||||
|
||||
@@ -147,14 +147,12 @@ func TestPermalinkExpansion(t *testing.T) {
|
||||
name := fmt.Sprintf("[%d] %s", i, specNameCleaner.ReplaceAllString(item.spec, "_"))
|
||||
|
||||
c.Run(name, func(c *qt.C) {
|
||||
patterns := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": item.spec,
|
||||
},
|
||||
configs := PermalinksConfig{
|
||||
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: item.spec},
|
||||
}
|
||||
expander, err := NewPermalinkExpander(urlize, patterns)
|
||||
expander, err := NewPermalinkExpander(urlize, configs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
expanded, err := expander.Expand(page)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, item.expandsTo)
|
||||
|
||||
@@ -178,39 +176,42 @@ func TestPermalinkExpansionMultiSection(t *testing.T) {
|
||||
page.section = "blue"
|
||||
page.slug = "The Slug"
|
||||
page.kind = "page"
|
||||
page.path = "/posts/my-page"
|
||||
|
||||
page_slug_fallback := newTestPageWithFile("/page-filename/index.md")
|
||||
page_slug_fallback.title = "Page Title"
|
||||
page_slug_fallback.kind = "page"
|
||||
pageSlugFallback := newTestPageWithFile("/page-filename/index.md")
|
||||
pageSlugFallback.title = "Page Title"
|
||||
pageSlugFallback.kind = "page"
|
||||
|
||||
permalinksConfig := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": "/:slug",
|
||||
"blog": "/:section/:year",
|
||||
"recipes": "/:slugorfilename",
|
||||
"special": "/special\\::slug",
|
||||
},
|
||||
configs := PermalinksConfig{
|
||||
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: "/:slug"},
|
||||
{Target: PageMatcher{Kind: "page", Path: "/blog/**"}, Pattern: "/:section/:year"},
|
||||
{Target: PageMatcher{Kind: "page", Path: "/recipes/**"}, Pattern: "/:slugorfilename"},
|
||||
{Target: PageMatcher{Kind: "page", Path: "/special/**"}, Pattern: "/special\\::slug"},
|
||||
}
|
||||
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
|
||||
expander, err := NewPermalinkExpander(urlize, configs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
expanded, err := expander.Expand(page)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, "/the-slug")
|
||||
|
||||
expanded, err = expander.Expand("blog", page)
|
||||
page.path = "/blog/my-page"
|
||||
expanded, err = expander.Expand(page)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, "/blue/2012")
|
||||
|
||||
expanded, err = expander.Expand("posts", page_slug_fallback)
|
||||
pageSlugFallback.path = "/posts/my-page"
|
||||
expanded, err = expander.Expand(pageSlugFallback)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, "/page-title")
|
||||
|
||||
expanded, err = expander.Expand("recipes", page_slug_fallback)
|
||||
pageSlugFallback.path = "/recipes/my-page"
|
||||
expanded, err = expander.Expand(pageSlugFallback)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, "/page-filename")
|
||||
|
||||
expanded, err = expander.Expand("special", page)
|
||||
page.path = "/special/my-page"
|
||||
expanded, err = expander.Expand(page)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, "/special:the-slug")
|
||||
}
|
||||
@@ -220,13 +221,11 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
permalinksConfig := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": "/:slug/",
|
||||
},
|
||||
configs := PermalinksConfig{
|
||||
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: "/:slug/"},
|
||||
}
|
||||
|
||||
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
|
||||
expander, err := NewPermalinkExpander(urlize, configs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -237,9 +236,10 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
|
||||
defer wg.Done()
|
||||
page := newTestPage()
|
||||
page.kind = "page"
|
||||
page.path = "/posts/my-page"
|
||||
for j := 1; j < 20; j++ {
|
||||
page.slug = fmt.Sprintf("slug%d", i+j)
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
expanded, err := expander.Expand(page)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(expanded, qt.Equals, fmt.Sprintf("/%s/", page.slug))
|
||||
}
|
||||
@@ -253,7 +253,7 @@ func TestPermalinkExpansionSliceSyntax(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
exp, err := NewPermalinkExpander(urlize, nil)
|
||||
exp, err := NewPermalinkExpander(urlize, PermalinksConfig{})
|
||||
c.Assert(err, qt.IsNil)
|
||||
slice4 := []string{"a", "b", "c", "d"}
|
||||
fn4 := func(s string) []string {
|
||||
@@ -302,19 +302,18 @@ func BenchmarkPermalinkExpand(b *testing.B) {
|
||||
d, _ := time.Parse("2006-01-02", "2019-02-28")
|
||||
page.date = d
|
||||
page.kind = "page"
|
||||
page.path = "/posts/my-page"
|
||||
|
||||
permalinksConfig := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": "/:year-:month-:title",
|
||||
},
|
||||
configs := PermalinksConfig{
|
||||
{Target: PageMatcher{Kind: "page", Path: "/posts/**"}, Pattern: "/:year-:month-:title"},
|
||||
}
|
||||
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
|
||||
expander, err := NewPermalinkExpander(urlize, configs)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
s, err := expander.Expand("posts", page)
|
||||
s, err := expander.Expand(page)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user