summaryrefslogtreecommitdiffstats
path: root/resources
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-03-30 07:55:24 +0200
committerGitHub <noreply@github.com>2021-03-30 07:55:24 +0200
commit2dc222cec4460595af8569165d1c498bb45aac84 (patch)
tree675e5fc3e25592664adc9ab0f77b4ea6a03cce30 /resources
parent4d22ad580ec8c8e5e27cf4f5cce69b6828aa8501 (diff)
Add slice syntax to sections permalinks config
Fixes #8363
Diffstat (limited to 'resources')
-rw-r--r--resources/page/permalinks.go98
-rw-r--r--resources/page/permalinks_test.go71
-rw-r--r--resources/page/testhelpers_test.go13
3 files changed, 166 insertions, 16 deletions
diff --git a/resources/page/permalinks.go b/resources/page/permalinks.go
index 19ac52172..aaffcdc21 100644
--- a/resources/page/permalinks.go
+++ b/resources/page/permalinks.go
@@ -16,6 +16,7 @@ package page
import (
"fmt"
"os"
+ "path"
"path/filepath"
"regexp"
"strconv"
@@ -54,6 +55,13 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
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
}
@@ -112,6 +120,7 @@ func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Pa
for k, pattern := range patterns {
k = strings.Trim(k, sectionCutSet)
+
if !l.validate(pattern) {
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)
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
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) {
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)]
+ }
+
+}
diff --git a/resources/page/permalinks_test.go b/resources/page/permalinks_test.go
index e40e7e0ef..ca106c236 100644
--- a/resources/page/permalinks_test.go
+++ b/resources/page/permalinks_test.go
@@ -15,6 +15,7 @@ package page
import (
"fmt"
+ "regexp"
"sync"
"testing"
"time"
@@ -38,8 +39,8 @@ var testdataPermalinks = []struct {
{"/:filename/", true, "/test-page/"}, // Filename
{"/: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
- // TODO(moorereason): need test scaffolding for this.
- //{"/:sections/", false, "/blue/"}, // Sections
+ {"/:sections/", true, "/a/b/c/"}, // Sections
+ {"/:sections[last]/", true, "/c/"}, // Sections
// Failures
{"/blog/:fred", false, ""},
@@ -66,19 +67,25 @@ func TestPermalinkExpansion(t *testing.T) {
continue
}
- permalinksConfig := map[string]string{
- "posts": item.spec,
- }
+ specNameCleaner := regexp.MustCompile(`[\:\/\[\]]`)
+ name := specNameCleaner.ReplaceAllString(item.spec, "")
+
+ c.Run(name, func(c *qt.C) {
+
+ permalinksConfig := map[string]string{
+ "posts": item.spec,
+ }
- ps := newTestPathSpec()
- ps.Cfg.Set("permalinks", permalinksConfig)
+ ps := newTestPathSpec()
+ ps.Cfg.Set("permalinks", permalinksConfig)
- expander, err := NewPermalinkExpander(ps)
- c.Assert(err, qt.IsNil)
+ 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)
+ 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()
}
+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) {
page := newTestPage()
page.title = "Hugo Rocks"
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index 963848439..187930461 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -16,6 +16,7 @@ package page
import (
"fmt"
"html/template"
+ "path"
"path/filepath"
"time"
@@ -61,6 +62,9 @@ func newTestPageWithFile(filename string) *testPage {
params: make(map[string]interface{}),
data: make(map[string]interface{}),
file: file,
+ currentSection: &testPage{
+ sectionEntries: []string{"a", "b", "c"},
+ },
}
}
@@ -112,6 +116,9 @@ type testPage struct {
data map[string]interface{}
file source.File
+
+ currentSection *testPage
+ sectionEntries []string
}
func (p *testPage) Aliases() []string {
@@ -151,7 +158,7 @@ func (p *testPage) ContentBaseName() string {
}
func (p *testPage) CurrentSection() Page {
- panic("not implemented")
+ return p.currentSection
}
func (p *testPage) Data() interface{} {
@@ -502,11 +509,11 @@ func (p *testPage) Sections() Pages {
}
func (p *testPage) SectionsEntries() []string {
- panic("not implemented")
+ return p.sectionEntries
}
func (p *testPage) SectionsPath() string {
- panic("not implemented")
+ return path.Join(p.sectionEntries...)
}
func (p *testPage) Site() Site {