mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
ac2c3fb20f
Fixes #14414
231 lines
5.7 KiB
Go
231 lines
5.7 KiB
Go
// Copyright 2025 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 versions
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"iter"
|
|
"sort"
|
|
|
|
"github.com/gohugoio/hugo/common/paths"
|
|
"github.com/gohugoio/hugo/common/predicate"
|
|
"github.com/gohugoio/hugo/common/version"
|
|
"github.com/gohugoio/hugo/config"
|
|
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
var _ sitesmatrix.DimensionInfo = (*versionWrapper)(nil)
|
|
|
|
type VersionConfig struct {
|
|
// The weight of the version.
|
|
// Used to determine the order of the versions.
|
|
// If zero, we use the version name to sort.
|
|
Weight int
|
|
}
|
|
|
|
type Version interface {
|
|
Name() string
|
|
}
|
|
|
|
type Versions []Version
|
|
|
|
type versionWrapper struct {
|
|
v VersionInternal
|
|
}
|
|
|
|
func (v versionWrapper) Name() string {
|
|
return v.v.Name
|
|
}
|
|
|
|
func (v versionWrapper) IsDefault() bool {
|
|
return v.v.Default
|
|
}
|
|
|
|
func NewVersion(v VersionInternal) Version {
|
|
return versionWrapper{v: v}
|
|
}
|
|
|
|
var _ Version = (*versionWrapper)(nil)
|
|
|
|
type VersionInternal struct {
|
|
// Name of the version.
|
|
// This is the key from the config.
|
|
Name string
|
|
|
|
// Parsed version of Name.
|
|
parsedVersion version.Version
|
|
|
|
// Whether this version is the default version.
|
|
// This will be by default rendered in the root.
|
|
// There can only be one default version.
|
|
Default bool
|
|
|
|
VersionConfig
|
|
}
|
|
|
|
type VersionsInternal struct {
|
|
versionConfigs map[string]VersionConfig
|
|
|
|
Sorted []VersionInternal
|
|
}
|
|
|
|
func (r VersionsInternal) Len() int {
|
|
return len(r.Sorted)
|
|
}
|
|
|
|
func (r VersionsInternal) IndexDefault() int {
|
|
for i, version := range r.Sorted {
|
|
if version.Default {
|
|
return i
|
|
}
|
|
}
|
|
panic("no default version found")
|
|
}
|
|
|
|
func (r VersionsInternal) ResolveName(i int) string {
|
|
if i < 0 || i >= len(r.Sorted) {
|
|
panic(fmt.Sprintf("index %d out of range for versions", i))
|
|
}
|
|
return r.Sorted[i].Name
|
|
}
|
|
|
|
func (r VersionsInternal) ResolveIndex(name string) int {
|
|
for i, version := range r.Sorted {
|
|
if version.Name == name {
|
|
return i
|
|
}
|
|
}
|
|
// Compare against parsed versions as well.
|
|
v, err := version.ParseVersion(name)
|
|
if err == nil {
|
|
for i, version := range r.Sorted {
|
|
if version.parsedVersion.Compare(v) == 0 {
|
|
return i
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// IndexMatch returns an iterator for the versions that match the filter.
|
|
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(predicate.IndexString{Index: i, String: version.Name}) {
|
|
if !yield(i) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
// ForEachIndex returns an iterator for the indices of the versions.
|
|
func (r VersionsInternal) ForEachIndex() iter.Seq[int] {
|
|
return func(yield func(i int) bool) {
|
|
for i := range r.Sorted {
|
|
if !yield(i) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const defaultContentVersionFallback = "v1.0.0"
|
|
|
|
func (r *VersionsInternal) init(defaultContentVersion string) error {
|
|
if r.versionConfigs == nil {
|
|
r.versionConfigs = make(map[string]VersionConfig)
|
|
}
|
|
defaultContentVersionProvided := defaultContentVersion != ""
|
|
if len(r.versionConfigs) == 0 {
|
|
if defaultContentVersion == "" {
|
|
defaultContentVersion = defaultContentVersionFallback
|
|
}
|
|
// Add a default version.
|
|
r.versionConfigs[defaultContentVersion] = VersionConfig{}
|
|
}
|
|
|
|
var defaultSeen bool
|
|
for k, v := range r.versionConfigs {
|
|
if k == "" {
|
|
return errors.New("version name cannot be empty")
|
|
}
|
|
if err := paths.ValidateIdentifier(k); err != nil {
|
|
return fmt.Errorf("version name %q is invalid: %s", k, err)
|
|
}
|
|
|
|
var isDefault bool
|
|
if k == defaultContentVersion {
|
|
isDefault = true
|
|
defaultSeen = true
|
|
}
|
|
|
|
// Ignore error, this may not be a semver version.
|
|
parsedVersion, _ := version.ParseVersion(k)
|
|
|
|
r.Sorted = append(r.Sorted, VersionInternal{Name: k, parsedVersion: parsedVersion, Default: isDefault, VersionConfig: v})
|
|
}
|
|
|
|
// Sort by weight if set, then semver descending.
|
|
sort.SliceStable(r.Sorted, func(i, j int) bool {
|
|
ri, rj := r.Sorted[i], r.Sorted[j]
|
|
if ri.Weight == rj.Weight {
|
|
return ri.parsedVersion.Compare(rj.parsedVersion) < 0
|
|
}
|
|
if rj.Weight == 0 {
|
|
return true
|
|
}
|
|
if ri.Weight == 0 {
|
|
return false
|
|
}
|
|
return ri.Weight < rj.Weight
|
|
})
|
|
|
|
if !defaultSeen {
|
|
if defaultContentVersionProvided {
|
|
return fmt.Errorf("the configured defaultContentVersion %q does not exist", defaultContentVersion)
|
|
}
|
|
// If no default version is set, we set the first one.
|
|
first := r.Sorted[0]
|
|
first.Default = true
|
|
r.versionConfigs[first.Name] = first.VersionConfig
|
|
r.Sorted[0] = first
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r VersionsInternal) Has(version string) bool {
|
|
_, found := r.versionConfigs[version]
|
|
return found
|
|
}
|
|
|
|
func DecodeConfig(defaultContentVersion string, m map[string]any) (*config.ConfigNamespace[map[string]VersionConfig, VersionsInternal], error) {
|
|
return config.DecodeNamespace[map[string]VersionConfig](m, func(in any) (VersionsInternal, any, error) {
|
|
var versions VersionsInternal
|
|
var conf map[string]VersionConfig
|
|
if err := mapstructure.Decode(m, &conf); err != nil {
|
|
return versions, nil, err
|
|
}
|
|
versions.versionConfigs = conf
|
|
if err := versions.init(defaultContentVersion); err != nil {
|
|
return versions, nil, err
|
|
}
|
|
return versions, versions.versionConfigs, nil
|
|
})
|
|
}
|