diff options
Diffstat (limited to 'hugolib/segments/segments.go')
-rw-r--r-- | hugolib/segments/segments.go | 257 |
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 +} |