"path"
"path/filepath"
"regexp"
+ "slices"
"sort"
"strings"
+ "github.com/gohugoio/go-radix"
"github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/htesting"
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
}
-func ExtractAndGroupRootPaths(paths []string) []NamedSlice {
- if len(paths) == 0 {
+// ExtractAndGroupRootPaths extracts and groups root paths from the supplied list of paths.
+// Note that the in slice will be sorted in place.
+func ExtractAndGroupRootPaths(in []string) []string {
+ if len(in) == 0 {
return nil
}
-
- pathsCopy := make([]string, len(paths))
- hadSlashPrefix := strings.HasPrefix(paths[0], FilePathSeparator)
-
- for i, p := range paths {
- pathsCopy[i] = strings.Trim(filepath.ToSlash(p), "/")
- }
-
- sort.Strings(pathsCopy)
-
- pathsParts := make([][]string, len(pathsCopy))
-
- for i, p := range pathsCopy {
- pathsParts[i] = strings.Split(p, "/")
- }
-
- var groups [][]string
-
- for i, p1 := range pathsParts {
- c1 := -1
-
- for j, p2 := range pathsParts {
- if i == j {
- continue
+ const maxGroups = 5
+ sort.Strings(in)
+ var groups []string
+ tree := radix.New[[]string]()
+
+LOOP:
+ for _, s := range in {
+ s = filepath.ToSlash(s)
+ if ss, g, found := tree.LongestPrefix(s); found {
+ if len(g) > maxGroups {
+ continue LOOP
}
-
- c2 := -1
-
- for i, v := range p1 {
- if i >= len(p2) {
- break
- }
- if v != p2[i] {
- break
- }
-
- c2 = i
- }
-
- if c1 == -1 || (c2 != -1 && c2 < c1) {
- c1 = c2
+ parts := strings.Split(strings.TrimPrefix(strings.TrimPrefix(s, ss), "/"), "/")
+ if len(parts) > 0 && parts[0] != "" && !slices.Contains(g, parts[0]) {
+ g = append(g, parts[0])
+ tree.Insert(ss, g)
}
- }
- if c1 != -1 {
- groups = append(groups, p1[:c1+1])
} else {
- groups = append(groups, p1)
+ tree.Insert(s, []string{})
}
}
- groupsStr := make([]string, len(groups))
- for i, g := range groups {
- groupsStr[i] = strings.Join(g, "/")
- }
-
- groupsStr = hstrings.UniqueStringsSorted(groupsStr)
-
- var result []NamedSlice
-
- for _, g := range groupsStr {
- name := filepath.FromSlash(g)
- if hadSlashPrefix {
- name = FilePathSeparator + name
+ var collect radix.WalkFn[[]string] = func(s string, g []string) (radix.WalkFlag, []string, error) {
+ if len(g) == 0 {
+ groups = append(groups, s)
+ return radix.WalkContinue, nil, nil
}
- ns := NamedSlice{Name: name}
- for _, p := range pathsCopy {
- if !strings.HasPrefix(p, g) {
- continue
- }
-
- p = strings.TrimPrefix(p, g)
- if p != "" {
- ns.Slice = append(ns.Slice, p)
- }
+ if len(g) == 1 {
+ groups = append(groups, path.Join(s, g[0]))
+ return radix.WalkContinue, nil, nil
}
-
- ns.Slice = hstrings.UniqueStrings(ExtractRootPaths(ns.Slice))
-
- result = append(result, ns)
+ var sb strings.Builder
+ sb.WriteString(s)
+ // This is used to print "Watching for changes in /Users/bep/dev/sites/hugotestsites/60k/content/{section0,section1,section10..."
+ // Having too many groups here is not helpful.
+ if len(g) > maxGroups {
+ // This will modify the slice in the tree, but that is OK since we are done with it.
+ g = g[:maxGroups]
+ g = append(g, "...")
+ }
+ sb.WriteString("/{")
+ sb.WriteString(strings.Join(g, ","))
+ sb.WriteString("}")
+ groups = append(groups, sb.String())
+ return radix.WalkContinue, nil, nil
}
- return result
-}
+ tree.Walk(collect)
-// ExtractRootPaths extracts the root paths from the supplied list of paths.
-// The resulting root path will not contain any file separators, but there
-// may be duplicates.
-// So "/content/section/" becomes "content"
-func ExtractRootPaths(paths []string) []string {
- r := make([]string, len(paths))
- for i, p := range paths {
- root := filepath.ToSlash(p)
- sections := strings.SplitSeq(root, "/")
- for section := range sections {
- if section != "" {
- root = section
- break
- }
- }
- r[i] = root
- }
- return r
+ return groups
}
// FindCWD returns the current working directory from where the Hugo
"fmt"
"os"
"path/filepath"
- "reflect"
"runtime"
"strconv"
"strings"
filepath.FromSlash("/c/d/e"),
}
- inCopy := make([]string, len(in))
- copy(inCopy, in)
-
result := helpers.ExtractAndGroupRootPaths(in)
c := qt.New(t)
- c.Assert(fmt.Sprint(result), qt.Equals, filepath.FromSlash("[/a/b/{c,e} /c/d/e]"))
-
- // Make sure the original is preserved
- c.Assert(in, qt.DeepEquals, inCopy)
+ c.Assert(result, qt.DeepEquals, []string{"/a/b/{c,e}", "/c/d/e"})
}
-func TestExtractRootPaths(t *testing.T) {
- tests := []struct {
- input []string
- expected []string
- }{{
- []string{
- filepath.FromSlash("a/b"), filepath.FromSlash("a/b/c/"), "b",
- filepath.FromSlash("/c/d"), filepath.FromSlash("d/"), filepath.FromSlash("//e//"),
- },
- []string{"a", "a", "b", "c", "d", "e"},
- }}
-
- for _, test := range tests {
- output := helpers.ExtractRootPaths(test.input)
- if !reflect.DeepEqual(output, test.expected) {
- t.Errorf("Expected %#v, got %#v\n", test.expected, output)
+func BenchmarkExtractAndGroupRootPaths(b *testing.B) {
+ in := []string{}
+ for i := 0; i < 10; i++ {
+ for j := 0; j < 1000; j++ {
+ in = append(in, fmt.Sprintf("/a/b/c/s%d/p%d", i, j))
}
}
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ helpers.ExtractAndGroupRootPaths(in)
+ }
}
func TestFindCWD(t *testing.T) {