summaryrefslogtreecommitdiffstats
path: root/hugolib/segments/segments.go
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib/segments/segments.go')
-rw-r--r--hugolib/segments/segments.go257
1 files changed, 257 insertions, 0 deletions
diff --git a/hugolib/segments/segments.go b/hugolib/segments/segments.go
new file mode 100644
index 000000000..8f7c18121
--- /dev/null
+++ b/hugolib/segments/segments.go
@@ -0,0 +1,257 @@
+// Copyright 2024 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 segments
+
+import (
+ "fmt"
+
+ "github.com/gobwas/glob"
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/predicate"
+ "github.com/gohugoio/hugo/config"
+ hglob "github.com/gohugoio/hugo/hugofs/glob"
+ "github.com/mitchellh/mapstructure"
+)
+
+// Segments is a collection of named segments.
+type Segments struct {
+ s map[string]excludeInclude
+}
+
+type excludeInclude struct {
+ exclude predicate.P[SegmentMatcherFields]
+ include predicate.P[SegmentMatcherFields]
+}
+
+// ShouldExcludeCoarse returns whether the given fields should be excluded.
+// This is used for the coarser grained checks, e.g. language and output format.
+// Note that ShouldExcludeCoarse(fields) == ShouldExcludeFine(fields) may
+// not always be true, but ShouldExcludeCoarse(fields) == true == ShouldExcludeFine(fields)
+// will always be truthful.
+func (e excludeInclude) ShouldExcludeCoarse(fields SegmentMatcherFields) bool {
+ return e.exclude != nil && e.exclude(fields)
+}
+
+// ShouldExcludeFine returns whether the given fields should be excluded.
+// This is used for the finer grained checks, e.g. on invididual pages.
+func (e excludeInclude) ShouldExcludeFine(fields SegmentMatcherFields) bool {
+ if e.exclude != nil && e.exclude(fields) {
+ return true
+ }
+ return e.include != nil && !e.include(fields)
+}
+
+type SegmentFilter interface {
+ // ShouldExcludeCoarse returns whether the given fields should be excluded on a coarse level.
+ ShouldExcludeCoarse(SegmentMatcherFields) bool
+
+ // ShouldExcludeFine returns whether the given fields should be excluded on a fine level.
+ ShouldExcludeFine(SegmentMatcherFields) bool
+}
+
+type segmentFilter struct {
+ coarse predicate.P[SegmentMatcherFields]
+ fine predicate.P[SegmentMatcherFields]
+}
+
+func (f segmentFilter) ShouldExcludeCoarse(field SegmentMatcherFields) bool {
+ return f.coarse(field)
+}
+
+func (f segmentFilter) ShouldExcludeFine(fields SegmentMatcherFields) bool {
+ return f.fine(fields)
+}
+
+var (
+ matchAll = func(SegmentMatcherFields) bool { return true }
+ matchNothing = func(SegmentMatcherFields) bool { return false }
+)
+
+// Get returns a SegmentFilter for the given segments.
+func (sms Segments) Get(onNotFound func(s string), ss ...string) SegmentFilter {
+ if ss == nil {
+ return segmentFilter{coarse: matchNothing, fine: matchNothing}
+ }
+ var sf segmentFilter
+ for _, s := range ss {
+ if seg, ok := sms.s[s]; ok {
+ if sf.coarse == nil {
+ sf.coarse = seg.ShouldExcludeCoarse
+ } else {
+ sf.coarse = sf.coarse.Or(seg.ShouldExcludeCoarse)
+ }
+ if sf.fine == nil {
+ sf.fine = seg.ShouldExcludeFine
+ } else {
+ sf.fine = sf.fine.Or(seg.ShouldExcludeFine)
+ }
+ } else if onNotFound != nil {
+ onNotFound(s)
+ }
+ }
+
+ if sf.coarse == nil {
+ sf.coarse = matchAll
+ }
+ if sf.fine == nil {
+ sf.fine = matchAll
+ }
+
+ return sf
+}
+
+type SegmentConfig struct {
+ Excludes []SegmentMatcherFields
+ Includes []SegmentMatcherFields
+}
+
+// SegmentMatcherFields is a matcher for a segment include or exclude.
+// All of these are Glob patterns.
+type SegmentMatcherFields struct {
+ Kind string
+ Path string
+ Lang string
+ Output string
+}
+
+func getGlob(s string) (glob.Glob, error) {
+ if s == "" {
+ return nil, nil
+ }
+ g, err := hglob.GetGlob(s)
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile Glob %q: %w", s, err)
+ }
+ return g, nil
+}
+
+func compileSegments(f []SegmentMatcherFields) (predicate.P[SegmentMatcherFields], error) {
+ if f == nil {
+ return func(SegmentMatcherFields) bool { return false }, nil
+ }
+ var (
+ result predicate.P[SegmentMatcherFields]
+ section predicate.P[SegmentMatcherFields]
+ )
+
+ addToSection := func(matcherFields SegmentMatcherFields, f func(fields SegmentMatcherFields) string) error {
+ s1 := f(matcherFields)
+ g, err := getGlob(s1)
+ if err != nil {
+ return err
+ }
+ matcher := func(fields SegmentMatcherFields) bool {
+ s2 := f(fields)
+ if s2 == "" {
+ return false
+ }
+ return g.Match(s2)
+ }
+ if section == nil {
+ section = matcher
+ } else {
+ section = section.And(matcher)
+ }
+ return nil
+ }
+
+ for _, fields := range f {
+ if fields.Kind != "" {
+ if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Kind }); err != nil {
+ return result, err
+ }
+ }
+ if fields.Path != "" {
+ if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Path }); err != nil {
+ return result, err
+ }
+ }
+ if fields.Lang != "" {
+ if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Lang }); err != nil {
+ return result, err
+ }
+ }
+ if fields.Output != "" {
+ if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Output }); err != nil {
+ return result, err
+ }
+ }
+
+ if result == nil {
+ result = section
+ } else {
+ result = result.Or(section)
+ }
+ section = nil
+
+ }
+
+ return result, nil
+}
+
+func DecodeSegments(in map[string]any) (*config.ConfigNamespace[map[string]SegmentConfig, Segments], error) {
+ buildConfig := func(in any) (Segments, any, error) {
+ sms := Segments{
+ s: map[string]excludeInclude{},
+ }
+ m, err := maps.ToStringMapE(in)
+ if err != nil {
+ return sms, nil, err
+ }
+ if m == nil {
+ m = map[string]any{}
+ }
+ m = maps.CleanConfigStringMap(m)
+
+ var scfgm map[string]SegmentConfig
+ if err := mapstructure.Decode(m, &scfgm); err != nil {
+ return sms, nil, err
+ }
+
+ for k, v := range scfgm {
+ var (
+ include predicate.P[SegmentMatcherFields]
+ exclude predicate.P[SegmentMatcherFields]
+ err error
+ )
+ if v.Excludes != nil {
+ exclude, err = compileSegments(v.Excludes)
+ if err != nil {
+ return sms, nil, err
+ }
+ }
+ if v.Includes != nil {
+ include, err = compileSegments(v.Includes)
+ if err != nil {
+ return sms, nil, err
+ }
+ }
+
+ ei := excludeInclude{
+ exclude: exclude,
+ include: include,
+ }
+ sms.s[k] = ei
+
+ }
+
+ return sms, nil, nil
+ }
+
+ ns, err := config.DecodeNamespace[map[string]SegmentConfig](in, buildConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode segments: %w", err)
+ }
+ return ns, nil
+}