mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
committed by
GitHub
parent
e569dd59a0
commit
192e3c453e
@@ -153,6 +153,29 @@ func (p PR[T]) FilterCopy(s []T) []T {
|
||||
return result
|
||||
}
|
||||
|
||||
const (
|
||||
rangeOpNone = iota
|
||||
rangeOpLT
|
||||
rangeOpLTE
|
||||
rangeOpGT
|
||||
rangeOpGTE
|
||||
)
|
||||
|
||||
func cutRangeOp(s string) (op int, rest string) {
|
||||
switch {
|
||||
case strings.HasPrefix(s, ">= "):
|
||||
return rangeOpGTE, s[3:]
|
||||
case strings.HasPrefix(s, "<= "):
|
||||
return rangeOpLTE, s[3:]
|
||||
case strings.HasPrefix(s, "> "):
|
||||
return rangeOpGT, s[2:]
|
||||
case strings.HasPrefix(s, "< "):
|
||||
return rangeOpLT, s[2:]
|
||||
default:
|
||||
return rangeOpNone, s
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
|
||||
@@ -187,6 +210,78 @@ func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string)
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
type IndexMatcher interface {
|
||||
IndexMatch(match P[string]) (iter.Seq[int], error)
|
||||
// NewIndexStringPredicateFromGlobsAndRanges creates an IndexString predicate from the given glob patterns and range patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
// A range pattern is one of "> value", ">= value", "< value" or "<= value".
|
||||
func NewIndexStringPredicateFromGlobsAndRanges(patterns []string, getIndex func(s string) int, getGlob func(pattern string) (glob.Glob, error)) (P[IndexString], error) {
|
||||
var p PR[IndexString]
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
|
||||
if negate {
|
||||
pattern = pattern[2:]
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(!g.Match(s.String))
|
||||
})
|
||||
} else {
|
||||
// This can be either a glob or a value prefixed with one of >, >=, < or <=.
|
||||
o, v := cutRangeOp(pattern)
|
||||
if o != rangeOpNone {
|
||||
i := getIndex(v)
|
||||
if i == -1 {
|
||||
// No match possible.
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(false)
|
||||
})
|
||||
continue
|
||||
}
|
||||
switch o {
|
||||
// The greater values starts at the top with index 0.
|
||||
case rangeOpGT:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index < i)
|
||||
})
|
||||
case rangeOpGTE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index <= i)
|
||||
})
|
||||
case rangeOpLT:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index > i)
|
||||
})
|
||||
case rangeOpLTE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index >= i)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.Or(func(s IndexString) Match {
|
||||
return BoolMatch(g.Match(s.String))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
type IndexString struct {
|
||||
Index int
|
||||
String string
|
||||
}
|
||||
|
||||
type IndexMatcher interface {
|
||||
IndexMatch(match P[IndexString]) (iter.Seq[int], error)
|
||||
}
|
||||
|
||||
@@ -146,6 +146,86 @@ func TestNewStringPredicateFromGlobs(t *testing.T) {
|
||||
c.Assert(m("anything"), qt.IsFalse)
|
||||
}
|
||||
|
||||
func TestNewIndexStringPredicateFromGlobsAndRanges(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Simulate versions: v4.0.0=0, v3.0.0=1, v2.0.0=2, v1.0.0=3
|
||||
// Lower index = greater value.
|
||||
versions := []string{"v4.0.0", "v3.0.0", "v2.0.0", "v1.0.0"}
|
||||
getIndex := func(s string) int {
|
||||
for i, v := range versions {
|
||||
if v == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
getGlob := func(pattern string) (glob.Glob, error) {
|
||||
return glob.Compile(pattern)
|
||||
}
|
||||
|
||||
n := func(patterns ...string) predicate.P[predicate.IndexString] {
|
||||
p, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(patterns, getIndex, getGlob)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
is := func(i int) predicate.IndexString {
|
||||
return predicate.IndexString{Index: i, String: versions[i]}
|
||||
}
|
||||
|
||||
// Test >= v2.0.0 (index 2): should match indices <= 2
|
||||
m := n(">= v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test > v2.0.0 (index 2): should match indices < 2
|
||||
m = n("> v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test < v3.0.0 (index 1): should match indices > 1
|
||||
m = n("< v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
|
||||
|
||||
// Test range: >= v2.0.0 AND <= v3.0.0
|
||||
m = n(">= v2.0.0", "<= v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0 - too high
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - too low
|
||||
|
||||
// Test glob pattern
|
||||
m = n("v2.*.*")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
|
||||
// Test glob with negation
|
||||
m = n("v*.*.*", "! v3.*.*")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
|
||||
// Test range with negation: >= v2.0.0 but not v3.0.0
|
||||
m = n(">= v2.0.0", "! v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
|
||||
|
||||
// Test unknown value in range returns no match
|
||||
m = n(">= v99.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse)
|
||||
c.Assert(m(is(3)), qt.IsFalse)
|
||||
}
|
||||
|
||||
func BenchmarkPredicate(b *testing.B) {
|
||||
b.Run("and or no match", func(b *testing.B) {
|
||||
var p predicate.PR[int] = intP1
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCutRangeOp(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
op, rest := cutRangeOp(">= value")
|
||||
c.Assert(op, qt.Equals, rangeOpGTE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("<= value")
|
||||
c.Assert(op, qt.Equals, rangeOpLTE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("> value")
|
||||
c.Assert(op, qt.Equals, rangeOpGT)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("< value")
|
||||
c.Assert(op, qt.Equals, rangeOpLT)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("value")
|
||||
c.Assert(op, qt.Equals, rangeOpNone)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
}
|
||||
@@ -102,14 +102,14 @@ func (r RolesInternal) ResolveIndex(name string) int {
|
||||
return i
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf("no role found for name %q", name))
|
||||
return -1
|
||||
}
|
||||
|
||||
// IndexMatch returns an iterator for the roles that match the filter.
|
||||
func (r RolesInternal) IndexMatch(match predicate.P[string]) (iter.Seq[int], error) {
|
||||
func (r RolesInternal) IndexMatch(match predicate.P[predicate.IndexString]) (iter.Seq[int], error) {
|
||||
return func(yield func(i int) bool) {
|
||||
for i, role := range r.Sorted {
|
||||
if match(role.Name) {
|
||||
if match(predicate.IndexString{Index: i, String: role.Name}) {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1431,6 +1431,115 @@ All.
|
||||
b.AssertFileContent("public/guest/en/index.html", "url=https://example.org/guest/\"")
|
||||
}
|
||||
|
||||
func TestSitesMatrixRangeMatchers(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `TestSitesMatrixRangeMatchers
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/"
|
||||
defaultContentVersion = "v4.0.0"
|
||||
defaultContentVersionInSubDir = true
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
|
||||
[versions]
|
||||
[versions."v1.0.0"]
|
||||
[versions."v2.0.0"]
|
||||
[versions."v2.1.0"]
|
||||
[versions."v3.0.0"]
|
||||
[versions."v4.0.0"]
|
||||
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
sites:
|
||||
matrix:
|
||||
versions: ["**"]
|
||||
---
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "P1 from v2.0.0"
|
||||
sites:
|
||||
matrix:
|
||||
versions: [">= v2.0.0"]
|
||||
---
|
||||
Content for v2.0.0 and later.
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "P2 between v2 and v3"
|
||||
sites:
|
||||
matrix:
|
||||
versions: [">= v2.0.0", "<= v3.0.0"]
|
||||
---
|
||||
Content between v2.0.0 and v3.0.0 (inclusive).
|
||||
-- content/p3.md --
|
||||
---
|
||||
title: "P3 before v3"
|
||||
sites:
|
||||
matrix:
|
||||
versions: ["< v3.0.0"]
|
||||
---
|
||||
Content before v3.0.0.
|
||||
-- content/p4.md --
|
||||
---
|
||||
title: "P4 after v2"
|
||||
sites:
|
||||
matrix:
|
||||
versions: ["> v2.0.0"]
|
||||
---
|
||||
Content after v2.0.0 (exclusive).
|
||||
-- content/p5.md --
|
||||
---
|
||||
title: "P5 range with negation"
|
||||
sites:
|
||||
matrix:
|
||||
versions: [">= v2.0.0", "! v2.1.0", "<= v4.0.0"]
|
||||
---
|
||||
Content from v2.0.0 to v4.0.0 but not v2.1.0.
|
||||
-- layouts/all.html --
|
||||
Title: {{ .Title }}|
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// P1: >= v2.0.0 should be in v2.0.0, v2.1.0, v3.0.0, v4.0.0
|
||||
b.AssertFileContent("public/v2.0.0/p1/index.html", "Title: P1 from v2.0.0|")
|
||||
b.AssertFileContent("public/v2.1.0/p1/index.html", "Title: P1 from v2.0.0|")
|
||||
b.AssertFileContent("public/v3.0.0/p1/index.html", "Title: P1 from v2.0.0|")
|
||||
b.AssertFileContent("public/v4.0.0/p1/index.html", "Title: P1 from v2.0.0|")
|
||||
b.AssertFileExists("public/v1.0.0/p1/index.html", false)
|
||||
|
||||
// P2: >= v2.0.0 AND <= v3.0.0 should be in v2.0.0, v2.1.0, v3.0.0
|
||||
b.AssertFileContent("public/v2.0.0/p2/index.html", "Title: P2 between v2 and v3|")
|
||||
b.AssertFileContent("public/v2.1.0/p2/index.html", "Title: P2 between v2 and v3|")
|
||||
b.AssertFileContent("public/v3.0.0/p2/index.html", "Title: P2 between v2 and v3|")
|
||||
b.AssertFileExists("public/v1.0.0/p2/index.html", false)
|
||||
b.AssertFileExists("public/v4.0.0/p2/index.html", false)
|
||||
|
||||
// P3: < v3.0.0 should be in v1.0.0, v2.0.0, v2.1.0
|
||||
b.AssertFileContent("public/v1.0.0/p3/index.html", "Title: P3 before v3|")
|
||||
b.AssertFileContent("public/v2.0.0/p3/index.html", "Title: P3 before v3|")
|
||||
b.AssertFileContent("public/v2.1.0/p3/index.html", "Title: P3 before v3|")
|
||||
b.AssertFileExists("public/v3.0.0/p3/index.html", false)
|
||||
b.AssertFileExists("public/v4.0.0/p3/index.html", false)
|
||||
|
||||
// P4: > v2.0.0 should be in v2.1.0, v3.0.0, v4.0.0
|
||||
b.AssertFileContent("public/v2.1.0/p4/index.html", "Title: P4 after v2|")
|
||||
b.AssertFileContent("public/v3.0.0/p4/index.html", "Title: P4 after v2|")
|
||||
b.AssertFileContent("public/v4.0.0/p4/index.html", "Title: P4 after v2|")
|
||||
b.AssertFileExists("public/v1.0.0/p4/index.html", false)
|
||||
b.AssertFileExists("public/v2.0.0/p4/index.html", false)
|
||||
|
||||
// P5: >= v2.0.0 AND ! v2.1.0 AND <= v4.0.0 should be in v2.0.0, v3.0.0, v4.0.0
|
||||
b.AssertFileContent("public/v2.0.0/p5/index.html", "Title: P5 range with negation|")
|
||||
b.AssertFileContent("public/v3.0.0/p5/index.html", "Title: P5 range with negation|")
|
||||
b.AssertFileContent("public/v4.0.0/p5/index.html", "Title: P5 range with negation|")
|
||||
b.AssertFileExists("public/v1.0.0/p5/index.html", false)
|
||||
b.AssertFileExists("public/v2.1.0/p5/index.html", false) // Excluded by negation
|
||||
}
|
||||
|
||||
func TestSitesMatrixFileMountOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -760,22 +760,19 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Dot separated globs.
|
||||
filter, err := predicate.NewStringPredicateFromGlobs(values, hglob.GetGlobDot)
|
||||
filter, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(values, matcher.ResolveIndex, hglob.GetGlobDot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create filter for %s: %w", what, err)
|
||||
}
|
||||
for _, pattern := range values {
|
||||
iter, err := matcher.IndexMatch(filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to match %s %q: %w", what, pattern, err)
|
||||
}
|
||||
for i := range iter {
|
||||
if result == nil {
|
||||
result = hmaps.NewOrderedIntSet()
|
||||
}
|
||||
result.Set(i)
|
||||
iter, err := matcher.IndexMatch(filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to match %s %q: %w", what, values, err)
|
||||
}
|
||||
for i := range iter {
|
||||
if result == nil {
|
||||
result = hmaps.NewOrderedIntSet()
|
||||
}
|
||||
result.Set(i)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -968,10 +965,10 @@ func (m *testDimension) ForEachIndex() iter.Seq[int] {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *testDimension) IndexMatch(match predicate.P[string]) (iter.Seq[int], error) {
|
||||
func (m *testDimension) IndexMatch(match predicate.P[predicate.IndexString]) (iter.Seq[int], error) {
|
||||
return func(yield func(i int) bool) {
|
||||
for i, n := range m.names {
|
||||
if match(n) {
|
||||
if match(predicate.IndexString{Index: i, String: n}) {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,14 +104,14 @@ func (r VersionsInternal) ResolveIndex(name string) int {
|
||||
return i
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf("no version found for name %q", name))
|
||||
return -1
|
||||
}
|
||||
|
||||
// IndexMatch returns an iterator for the versions that match the filter.
|
||||
func (r VersionsInternal) IndexMatch(match predicate.P[string]) (iter.Seq[int], error) {
|
||||
func (r VersionsInternal) IndexMatch(match predicate.P[predicate.IndexString]) (iter.Seq[int], error) {
|
||||
return func(yield func(i int) bool) {
|
||||
for i, version := range r.Sorted {
|
||||
if match(version.Name) {
|
||||
if match(predicate.IndexString{Index: i, String: version.Name}) {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
|
||||
+3
-3
@@ -89,7 +89,7 @@ func (ls LanguagesInternal) ResolveIndex(name string) int {
|
||||
return i
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf("no language found for name %q", name))
|
||||
return -1
|
||||
}
|
||||
|
||||
func (ls LanguagesInternal) Len() int {
|
||||
@@ -97,10 +97,10 @@ func (ls LanguagesInternal) Len() int {
|
||||
}
|
||||
|
||||
// IndexMatch returns an iterator for the roles that match the filter.
|
||||
func (ls LanguagesInternal) IndexMatch(match predicate.P[string]) (iter.Seq[int], error) {
|
||||
func (ls LanguagesInternal) IndexMatch(match predicate.P[predicate.IndexString]) (iter.Seq[int], error) {
|
||||
return func(yield func(i int) bool) {
|
||||
for i, l := range ls.Sorted {
|
||||
if match(l.Name) {
|
||||
if match(predicate.IndexString{Index: i, String: l.Name}) {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user