mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
parent
4d22ad580e
commit
2dc222cec4
4 changed files with 167 additions and 17 deletions
|
@ -83,7 +83,7 @@ The following is a list of values that can be used in a `permalink` definition i
|
||||||
: the content's section
|
: the content's section
|
||||||
|
|
||||||
`:sections`
|
`:sections`
|
||||||
: the content's sections hierarchy
|
: the content's sections hierarchy. {{< new-in "0.83.0" >}} Since Hugo 0.83 you can use a selection of the sections using _slice syntax_: `:sections[1:]` includes all but the first, `:sections[:last]` includes all but the last, `:sections[last]` includes only the last, `:sections[1:2]` includes section 2 and 3. Note that this slice access will not throw any out-of-bounds errors, so you don't have to be exact.
|
||||||
|
|
||||||
`:title`
|
`:title`
|
||||||
: the content's title
|
: the content's title
|
||||||
|
|
|
@ -16,6 +16,7 @@ package page
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -54,6 +55,13 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
|
||||||
return p.pageToPermalinkDate, true
|
return p.pageToPermalinkDate, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(attr, "sections[") {
|
||||||
|
fn := p.toSliceFunc(strings.TrimPrefix(attr, "sections"))
|
||||||
|
return func(p Page, s string) (string, error) {
|
||||||
|
return path.Join(fn(p.CurrentSection().SectionsEntries())...), nil
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +120,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
|
||||||
|
|
||||||
for k, pattern := range patterns {
|
for k, pattern := range patterns {
|
||||||
k = strings.Trim(k, sectionCutSet)
|
k = strings.Trim(k, sectionCutSet)
|
||||||
|
|
||||||
if !l.validate(pattern) {
|
if !l.validate(pattern) {
|
||||||
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
|
return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed}
|
||||||
}
|
}
|
||||||
|
@ -165,7 +174,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
|
||||||
// can return a string to go in that position in the page (or an error)
|
// can return a string to go in that position in the page (or an error)
|
||||||
type pageToPermaAttribute func(Page, string) (string, error)
|
type pageToPermaAttribute func(Page, string) (string, error)
|
||||||
|
|
||||||
var attributeRegexp = regexp.MustCompile(`:\w+`)
|
var attributeRegexp = regexp.MustCompile(`:\w+(\[.+\])?`)
|
||||||
|
|
||||||
// validate determines if a PathPattern is well-formed
|
// validate determines if a PathPattern is well-formed
|
||||||
func (l PermalinkExpander) validate(pp string) bool {
|
func (l PermalinkExpander) validate(pp string) bool {
|
||||||
|
@ -263,3 +272,90 @@ func (l PermalinkExpander) pageToPermalinkSection(p Page, _ string) (string, err
|
||||||
func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) {
|
func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) {
|
||||||
return p.CurrentSection().SectionsPath(), nil
|
return p.CurrentSection().SectionsPath(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
nilSliceFunc = func(s []string) []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
allSliceFunc = func(s []string) []string {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// toSliceFunc returns a slice func that slices s according to the cut spec.
|
||||||
|
// The cut spec must be on form [low:high] (one or both can be omitted),
|
||||||
|
// also allowing single slice indices (e.g. [2]) and the special [last] keyword
|
||||||
|
// giving the last element of the slice.
|
||||||
|
// The returned function will be lenient and not panic in out of bounds situation.
|
||||||
|
//
|
||||||
|
// The current use case for this is to use parts of the sections path in permalinks.
|
||||||
|
func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
|
||||||
|
cut = strings.ToLower(strings.TrimSpace(cut))
|
||||||
|
if cut == "" {
|
||||||
|
return allSliceFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cut) < 3 || (cut[0] != '[' || cut[len(cut)-1] != ']') {
|
||||||
|
return nilSliceFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
toNFunc := func(s string, low bool) func(ss []string) int {
|
||||||
|
if s == "" {
|
||||||
|
if low {
|
||||||
|
return func(ss []string) int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return func(ss []string) int {
|
||||||
|
return len(ss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == "last" {
|
||||||
|
return func(ss []string) int {
|
||||||
|
return len(ss) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _ := strconv.Atoi(s)
|
||||||
|
if n < 0 {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
return func(ss []string) int {
|
||||||
|
// Prevent out of bound situations. It would not make
|
||||||
|
// much sense to panic here.
|
||||||
|
if n > len(ss) {
|
||||||
|
return len(ss)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opsStr := cut[1 : len(cut)-1]
|
||||||
|
opts := strings.Split(opsStr, ":")
|
||||||
|
|
||||||
|
if !strings.Contains(opsStr, ":") {
|
||||||
|
toN := toNFunc(opts[0], true)
|
||||||
|
return func(s []string) []string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := s[toN(s)]
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toN1, toN2 := toNFunc(opts[0], true), toNFunc(opts[1], false)
|
||||||
|
|
||||||
|
return func(s []string) []string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s[toN1(s):toN2(s)]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -38,8 +39,8 @@ var testdataPermalinks = []struct {
|
||||||
{"/:filename/", true, "/test-page/"}, // Filename
|
{"/:filename/", true, "/test-page/"}, // Filename
|
||||||
{"/:06-:1-:2-:Monday", true, "/12-4-6-Friday"}, // Dates with Go formatting
|
{"/:06-:1-:2-:Monday", true, "/12-4-6-Friday"}, // Dates with Go formatting
|
||||||
{"/:2006_01_02_15_04_05.000", true, "/2012_04_06_03_01_59.000"}, // Complicated custom date format
|
{"/:2006_01_02_15_04_05.000", true, "/2012_04_06_03_01_59.000"}, // Complicated custom date format
|
||||||
// TODO(moorereason): need test scaffolding for this.
|
{"/:sections/", true, "/a/b/c/"}, // Sections
|
||||||
//{"/:sections/", false, "/blue/"}, // Sections
|
{"/:sections[last]/", true, "/c/"}, // Sections
|
||||||
|
|
||||||
// Failures
|
// Failures
|
||||||
{"/blog/:fred", false, ""},
|
{"/blog/:fred", false, ""},
|
||||||
|
@ -66,19 +67,25 @@ func TestPermalinkExpansion(t *testing.T) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
permalinksConfig := map[string]string{
|
specNameCleaner := regexp.MustCompile(`[\:\/\[\]]`)
|
||||||
"posts": item.spec,
|
name := specNameCleaner.ReplaceAllString(item.spec, "")
|
||||||
}
|
|
||||||
|
|
||||||
ps := newTestPathSpec()
|
c.Run(name, func(c *qt.C) {
|
||||||
ps.Cfg.Set("permalinks", permalinksConfig)
|
|
||||||
|
|
||||||
expander, err := NewPermalinkExpander(ps)
|
permalinksConfig := map[string]string{
|
||||||
c.Assert(err, qt.IsNil)
|
"posts": item.spec,
|
||||||
|
}
|
||||||
|
|
||||||
expanded, err := expander.Expand("posts", page)
|
ps := newTestPathSpec()
|
||||||
c.Assert(err, qt.IsNil)
|
ps.Cfg.Set("permalinks", permalinksConfig)
|
||||||
c.Assert(expanded, qt.Equals, item.expandsTo)
|
|
||||||
|
expander, err := NewPermalinkExpander(ps)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
|
expanded, err := expander.Expand("posts", page)
|
||||||
|
c.Assert(err, qt.IsNil)
|
||||||
|
c.Assert(expanded, qt.Equals, item.expandsTo)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +156,46 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPermalinkExpansionSliceSyntax(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
c := qt.New(t)
|
||||||
|
exp, _ := NewPermalinkExpander(newTestPathSpec())
|
||||||
|
slice := []string{"a", "b", "c", "d"}
|
||||||
|
fn := func(s string) []string {
|
||||||
|
return exp.toSliceFunc(s)(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Run("Basic", func(c *qt.C) {
|
||||||
|
c.Assert(fn("[1:3]"), qt.DeepEquals, []string{"b", "c"})
|
||||||
|
c.Assert(fn("[1:]"), qt.DeepEquals, []string{"b", "c", "d"})
|
||||||
|
c.Assert(fn("[:2]"), qt.DeepEquals, []string{"a", "b"})
|
||||||
|
c.Assert(fn("[0:2]"), qt.DeepEquals, []string{"a", "b"})
|
||||||
|
c.Assert(fn("[:]"), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||||
|
c.Assert(fn(""), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||||
|
c.Assert(fn("[last]"), qt.DeepEquals, []string{"d"})
|
||||||
|
c.Assert(fn("[:last]"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Run("Out of bounds", func(c *qt.C) {
|
||||||
|
c.Assert(fn("[1:5]"), qt.DeepEquals, []string{"b", "c", "d"})
|
||||||
|
c.Assert(fn("[-1:5]"), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||||
|
c.Assert(fn("[5:]"), qt.DeepEquals, []string{})
|
||||||
|
c.Assert(fn("[5:]"), qt.DeepEquals, []string{})
|
||||||
|
c.Assert(fn("[5:32]"), qt.DeepEquals, []string{})
|
||||||
|
c.Assert(exp.toSliceFunc("[:1]")(nil), qt.DeepEquals, []string(nil))
|
||||||
|
c.Assert(exp.toSliceFunc("[:1]")([]string{}), qt.DeepEquals, []string(nil))
|
||||||
|
|
||||||
|
// These all return nil
|
||||||
|
c.Assert(fn("[]"), qt.IsNil)
|
||||||
|
c.Assert(fn("[1:}"), qt.IsNil)
|
||||||
|
c.Assert(fn("foo"), qt.IsNil)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkPermalinkExpand(b *testing.B) {
|
func BenchmarkPermalinkExpand(b *testing.B) {
|
||||||
page := newTestPage()
|
page := newTestPage()
|
||||||
page.title = "Hugo Rocks"
|
page.title = "Hugo Rocks"
|
||||||
|
|
|
@ -16,6 +16,7 @@ package page
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -61,6 +62,9 @@ func newTestPageWithFile(filename string) *testPage {
|
||||||
params: make(map[string]interface{}),
|
params: make(map[string]interface{}),
|
||||||
data: make(map[string]interface{}),
|
data: make(map[string]interface{}),
|
||||||
file: file,
|
file: file,
|
||||||
|
currentSection: &testPage{
|
||||||
|
sectionEntries: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +116,9 @@ type testPage struct {
|
||||||
data map[string]interface{}
|
data map[string]interface{}
|
||||||
|
|
||||||
file source.File
|
file source.File
|
||||||
|
|
||||||
|
currentSection *testPage
|
||||||
|
sectionEntries []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) Aliases() []string {
|
func (p *testPage) Aliases() []string {
|
||||||
|
@ -151,7 +158,7 @@ func (p *testPage) ContentBaseName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) CurrentSection() Page {
|
func (p *testPage) CurrentSection() Page {
|
||||||
panic("not implemented")
|
return p.currentSection
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) Data() interface{} {
|
func (p *testPage) Data() interface{} {
|
||||||
|
@ -502,11 +509,11 @@ func (p *testPage) Sections() Pages {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) SectionsEntries() []string {
|
func (p *testPage) SectionsEntries() []string {
|
||||||
panic("not implemented")
|
return p.sectionEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) SectionsPath() string {
|
func (p *testPage) SectionsPath() string {
|
||||||
panic("not implemented")
|
return path.Join(p.sectionEntries...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) Site() Site {
|
func (p *testPage) Site() Site {
|
||||||
|
|
Loading…
Reference in a new issue