summaryrefslogtreecommitdiffstats
path: root/hugofs
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-10-13 08:12:06 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2021-10-16 15:22:03 +0200
commit9185e11effa682ea1ef7dc98f2943743671023a6 (patch)
treef89d4138ddffd163a2afcd814ed2c26d3c66c4c9 /hugofs
parent168a3aab4622786ccd0943137fce3912707f2a46 (diff)
Reimplement archetypes
The old implementation had some issues, mostly related to the context (e.g. name, file paths) passed to the template. This new implementation is using the exact same code path for evaluating the pages as in a regular build. This also makes it more robust and easier to reason about in a multilingual setup. Now, if you are explicit about the target path, Hugo will now always pick the correct mount and language: ```bash hugo new content/en/posts/my-first-post.md ``` Fixes #9032 Fixes #7589 Fixes #9043 Fixes #9046 Fixes #9047
Diffstat (limited to 'hugofs')
-rw-r--r--hugofs/glob/glob.go168
-rw-r--r--hugofs/glob/glob_test.go35
2 files changed, 184 insertions, 19 deletions
diff --git a/hugofs/glob/glob.go b/hugofs/glob/glob.go
index 57115ddfa..6dd0df5ed 100644
--- a/hugofs/glob/glob.go
+++ b/hugofs/glob/glob.go
@@ -16,6 +16,7 @@ package glob
import (
"path"
"path/filepath"
+ "runtime"
"strings"
"sync"
@@ -23,46 +24,100 @@ import (
"github.com/gobwas/glob/syntax"
)
+var (
+ isWindows = runtime.GOOS == "windows"
+ defaultGlobCache = &globCache{
+ isCaseSensitive: false,
+ isWindows: isWindows,
+ cache: make(map[string]globErr),
+ }
+
+ filenamesGlobCache = &globCache{
+ isCaseSensitive: true, // TODO(bep) bench
+ isWindows: isWindows,
+ cache: make(map[string]globErr),
+ }
+)
+
type globErr struct {
glob glob.Glob
err error
}
-var (
- globCache = make(map[string]globErr)
- globMu sync.RWMutex
-)
+type globCache struct {
+ // Config
+ isCaseSensitive bool
+ isWindows bool
-type caseInsensitiveGlob struct {
- g glob.Glob
+ // Cache
+ sync.RWMutex
+ cache map[string]globErr
}
-func (g caseInsensitiveGlob) Match(s string) bool {
- return g.g.Match(strings.ToLower(s))
-
-}
-func GetGlob(pattern string) (glob.Glob, error) {
+func (gc *globCache) GetGlob(pattern string) (glob.Glob, error) {
var eg globErr
- globMu.RLock()
+ gc.RLock()
var found bool
- eg, found = globCache[pattern]
- globMu.RUnlock()
+ eg, found = gc.cache[pattern]
+ gc.RUnlock()
if found {
return eg.glob, eg.err
}
+ var g glob.Glob
var err error
- g, err := glob.Compile(strings.ToLower(pattern), '/')
- eg = globErr{caseInsensitiveGlob{g: g}, err}
- globMu.Lock()
- globCache[pattern] = eg
- globMu.Unlock()
+ pattern = filepath.ToSlash(pattern)
+
+ if gc.isCaseSensitive {
+ g, err = glob.Compile(pattern, '/')
+ } else {
+ g, err = glob.Compile(strings.ToLower(pattern), '/')
+
+ }
+
+ eg = globErr{
+ globDecorator{
+ g: g,
+ isCaseSensitive: gc.isCaseSensitive,
+ isWindows: gc.isWindows},
+ err,
+ }
+
+ gc.Lock()
+ gc.cache[pattern] = eg
+ gc.Unlock()
return eg.glob, eg.err
}
+type globDecorator struct {
+ // Whether both pattern and the strings to match will be matched
+ // by their original case.
+ isCaseSensitive bool
+
+ // On Windows we may get filenames with Windows slashes to match,
+ // which wee need to normalize.
+ isWindows bool
+
+ g glob.Glob
+}
+
+func (g globDecorator) Match(s string) bool {
+ if g.isWindows {
+ s = filepath.ToSlash(s)
+ }
+ if !g.isCaseSensitive {
+ s = strings.ToLower(s)
+ }
+ return g.g.Match(s)
+}
+
+func GetGlob(pattern string) (glob.Glob, error) {
+ return defaultGlobCache.GetGlob(pattern)
+}
+
func NormalizePath(p string) string {
return strings.Trim(path.Clean(filepath.ToSlash(strings.ToLower(p))), "/.")
}
@@ -106,3 +161,78 @@ func HasGlobChar(s string) bool {
}
return false
}
+
+type FilenameFilter struct {
+ shouldInclude func(filename string) bool
+ inclusions []glob.Glob
+ exclusions []glob.Glob
+ isWindows bool
+}
+
+// NewFilenameFilter creates a new Glob where the Match method will
+// return true if the file should be exluded.
+// Note that the inclusions will be checked first.
+func NewFilenameFilter(inclusions, exclusions []string) (*FilenameFilter, error) {
+ filter := &FilenameFilter{isWindows: isWindows}
+
+ for _, include := range inclusions {
+ g, err := filenamesGlobCache.GetGlob(filepath.FromSlash(include))
+ if err != nil {
+ return nil, err
+ }
+ filter.inclusions = append(filter.inclusions, g)
+ }
+ for _, exclude := range exclusions {
+ g, err := filenamesGlobCache.GetGlob(filepath.FromSlash(exclude))
+ if err != nil {
+ return nil, err
+ }
+ filter.exclusions = append(filter.exclusions, g)
+ }
+
+ return filter, nil
+}
+
+// NewFilenameFilterForInclusionFunc create a new filter using the provided inclusion func.
+func NewFilenameFilterForInclusionFunc(shouldInclude func(filename string) bool) *FilenameFilter {
+ return &FilenameFilter{shouldInclude: shouldInclude, isWindows: isWindows}
+}
+
+// Match returns whether filename should be included.
+func (f *FilenameFilter) Match(filename string) bool {
+ if f == nil {
+ return true
+ }
+
+ if f.shouldInclude != nil {
+ if f.shouldInclude(filename) {
+ return true
+ }
+ if f.isWindows {
+ // The Glob matchers below handles this by themselves,
+ // for the shouldInclude we need to take some extra steps
+ // to make this robust.
+ winFilename := filepath.FromSlash(filename)
+ if filename != winFilename {
+ if f.shouldInclude(winFilename) {
+ return true
+ }
+ }
+ }
+
+ }
+
+ for _, inclusion := range f.inclusions {
+ if inclusion.Match(filename) {
+ return true
+ }
+ }
+
+ for _, exclusion := range f.exclusions {
+ if exclusion.Match(filename) {
+ return false
+ }
+ }
+
+ return f.inclusions == nil && f.shouldInclude == nil
+}
diff --git a/hugofs/glob/glob_test.go b/hugofs/glob/glob_test.go
index cd64ba112..7ef3fbbed 100644
--- a/hugofs/glob/glob_test.go
+++ b/hugofs/glob/glob_test.go
@@ -15,6 +15,7 @@ package glob
import (
"path/filepath"
+ "strings"
"testing"
qt "github.com/frankban/quicktest"
@@ -72,6 +73,40 @@ func TestGetGlob(t *testing.T) {
c.Assert(g.Match("data/my.json"), qt.Equals, true)
}
+func TestFilenameFilter(t *testing.T) {
+ c := qt.New(t)
+
+ excludeAlmostAllJSON, err := NewFilenameFilter([]string{"a/b/c/foo.json"}, []string{"**.json"})
+ c.Assert(err, qt.IsNil)
+ c.Assert(excludeAlmostAllJSON.Match(filepath.FromSlash("data/my.json")), qt.Equals, false)
+ c.Assert(excludeAlmostAllJSON.Match(filepath.FromSlash("a/b/c/foo.json")), qt.Equals, true)
+ c.Assert(excludeAlmostAllJSON.Match(filepath.FromSlash("a/b/c/foo.bar")), qt.Equals, false)
+
+ nopFilter, err := NewFilenameFilter(nil, nil)
+ c.Assert(err, qt.IsNil)
+ c.Assert(nopFilter.Match("ab.txt"), qt.Equals, true)
+
+ includeOnlyFilter, err := NewFilenameFilter([]string{"**.json", "**.jpg"}, nil)
+ c.Assert(err, qt.IsNil)
+ c.Assert(includeOnlyFilter.Match("ab.json"), qt.Equals, true)
+ c.Assert(includeOnlyFilter.Match("ab.jpg"), qt.Equals, true)
+ c.Assert(includeOnlyFilter.Match("ab.gif"), qt.Equals, false)
+
+ exlcudeOnlyFilter, err := NewFilenameFilter(nil, []string{"**.json", "**.jpg"})
+ c.Assert(err, qt.IsNil)
+ c.Assert(exlcudeOnlyFilter.Match("ab.json"), qt.Equals, false)
+ c.Assert(exlcudeOnlyFilter.Match("ab.jpg"), qt.Equals, false)
+ c.Assert(exlcudeOnlyFilter.Match("ab.gif"), qt.Equals, true)
+
+ var nilFilter *FilenameFilter
+ c.Assert(nilFilter.Match("ab.gif"), qt.Equals, true)
+
+ funcFilter := NewFilenameFilterForInclusionFunc(func(s string) bool { return strings.HasSuffix(s, ".json") })
+ c.Assert(funcFilter.Match("ab.json"), qt.Equals, true)
+ c.Assert(funcFilter.Match("ab.bson"), qt.Equals, false)
+
+}
+
func BenchmarkGetGlob(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := GetGlob("**/foo")