mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
1f1c62e6c7
Named segments can be defined in `hugo.toml`. * Eeach segment consists of zero or more `exclude` filters and zero or more `include` filters. * Eeach filter consists of one or more field Glob matchers. * Eeach filter in a section (`exclude` or `include`) is ORed together, each matcher in a filter is ANDed together. The current list of fields that can be filtered are: * path as defined in https://gohugo.io/methods/page/path/ * kind * lang * output (output format, e.g. html). It is recommended to put coarse grained filters (e.g. for language and output format) in the excludes section, e.g.: ```toml [segments.segment1] [[segments.segment1.excludes]] lang = "n*" [[segments.segment1.excludes]] no = "en" output = "rss" [[segments.segment1.includes]] term = "{home,term,taxonomy}" [[segments.segment1.includes]] path = "{/docs,/docs/**}" ``` By default, Hugo will render all segments, but you can enable filters by setting the `renderSegments` option or `--renderSegments` flag, e.g: ``` hugo --renderSegments segment1,segment2 ``` For segment `segment1` in the configuration above, this will: * Skip rendering of all languages matching `n*`, e.g. `no`. * Skip rendering of the output format `rss` for the `en` language. * It will render all pages of kind `home`, `term` or `taxonomy` * It will render the `/docs` section and all pages below. Fixes #10106
257 lines
6.5 KiB
Go
257 lines
6.5 KiB
Go
// 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
|
|
}
|