Create a struct with all of Hugo's config options

Primary motivation is documentation, but it will also hopefully simplify the code.

Also,

* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.

Closes #10896
Closes #10620
This commit is contained in:
Bjørn Erik Pedersen
2023-01-04 18:24:36 +01:00
parent 6aededf6b4
commit 241b21b0fd
337 changed files with 13377 additions and 14898 deletions

View File

@@ -0,0 +1,57 @@
// Copyright 2023 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 hstrings
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/compare"
)
var _ compare.Eqer = StringEqualFold("")
// StringEqualFold is a string that implements the compare.Eqer interface and considers
// two strings equal if they are equal when folded to lower case.
// The compare.Eqer interface is used in Hugo to compare values in templates (e.g. using the eq template function).
type StringEqualFold string
func (s StringEqualFold) EqualFold(s2 string) bool {
return strings.EqualFold(string(s), s2)
}
func (s StringEqualFold) String() string {
return string(s)
}
func (s StringEqualFold) Eq(s2 any) bool {
switch ss := s2.(type) {
case string:
return s.EqualFold(ss)
case fmt.Stringer:
return s.EqualFold(ss.String())
}
return false
}
// EqualAny returns whether a string is equal to any of the given strings.
func EqualAny(a string, b ...string) bool {
for _, s := range b {
if a == s {
return true
}
}
return false
}

View File

@@ -0,0 +1,36 @@
// Copyright 2023 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 hstrings
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestStringEqualFold(t *testing.T) {
c := qt.New(t)
s1 := "A"
s2 := "a"
c.Assert(StringEqualFold(s1).EqualFold(s2), qt.Equals, true)
c.Assert(StringEqualFold(s1).EqualFold(s1), qt.Equals, true)
c.Assert(StringEqualFold(s2).EqualFold(s1), qt.Equals, true)
c.Assert(StringEqualFold(s2).EqualFold(s2), qt.Equals, true)
c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false)
c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true)
c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false)
}

View File

@@ -14,6 +14,7 @@
package htime
import (
"log"
"strings"
"time"
@@ -163,3 +164,11 @@ func Since(t time.Time) time.Duration {
type AsTimeProvider interface {
AsTime(zone *time.Location) time.Time
}
// StopWatch is a simple helper to measure time during development.
func StopWatch(name string) func() {
start := time.Now()
return func() {
log.Printf("StopWatch %q took %s", name, time.Since(start))
}
}

View File

@@ -46,8 +46,8 @@ var (
vendorInfo string
)
// Info contains information about the current Hugo environment
type Info struct {
// HugoInfo contains information about the current Hugo environment
type HugoInfo struct {
CommitHash string
BuildDate string
@@ -64,30 +64,30 @@ type Info struct {
}
// Version returns the current version as a comparable version string.
func (i Info) Version() VersionString {
func (i HugoInfo) Version() VersionString {
return CurrentVersion.Version()
}
// Generator a Hugo meta generator HTML tag.
func (i Info) Generator() template.HTML {
func (i HugoInfo) Generator() template.HTML {
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
}
func (i Info) IsProduction() bool {
func (i HugoInfo) IsProduction() bool {
return i.Environment == EnvironmentProduction
}
func (i Info) IsExtended() bool {
func (i HugoInfo) IsExtended() bool {
return IsExtended
}
// Deps gets a list of dependencies for this Hugo build.
func (i Info) Deps() []*Dependency {
func (i HugoInfo) Deps() []*Dependency {
return i.deps
}
// NewInfo creates a new Hugo Info object.
func NewInfo(environment string, deps []*Dependency) Info {
func NewInfo(environment string, deps []*Dependency) HugoInfo {
if environment == "" {
environment = EnvironmentProduction
}
@@ -104,7 +104,7 @@ func NewInfo(environment string, deps []*Dependency) Info {
goVersion = bi.GoVersion
}
return Info{
return HugoInfo{
CommitHash: commitHash,
BuildDate: buildDate,
Environment: environment,
@@ -115,7 +115,7 @@ func NewInfo(environment string, deps []*Dependency) Info {
// GetExecEnviron creates and gets the common os/exec environment used in the
// external programs we interact with via os/exec, e.g. postcss.
func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
var env []string
nodepath := filepath.Join(workDir, "node_modules")
if np := os.Getenv("NODE_PATH"); np != "" {
@@ -123,10 +123,9 @@ func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
}
config.SetEnvVars(&env, "NODE_PATH", nodepath)
config.SetEnvVars(&env, "PWD", workDir)
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
config.SetEnvVars(&env, "HUGO_ENV", cfg.GetString("environment"))
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.GetString("publishDirOrig")))
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.Environment())
config.SetEnvVars(&env, "HUGO_ENV", cfg.Environment())
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir))
if fs != nil {
fis, err := afero.ReadDir(fs, files.FolderJSConfig)

View File

@@ -15,7 +15,6 @@ package loggers
import (
"fmt"
"strings"
)
// IgnorableLogger is a logger that ignores certain log statements.
@@ -31,14 +30,13 @@ type ignorableLogger struct {
}
// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger {
statementsSet := make(map[string]bool)
for _, s := range statements {
statementsSet[strings.ToLower(s)] = true
func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger {
if statements == nil {
statements = make(map[string]bool)
}
return ignorableLogger{
Logger: logger,
statements: statementsSet,
statements: statements,
}
}

View File

@@ -43,25 +43,25 @@ func ToStringMapE(in any) (map[string]any, error) {
// ToParamsAndPrepare converts in to Params and prepares it for use.
// If in is nil, an empty map is returned.
// See PrepareParams.
func ToParamsAndPrepare(in any) (Params, bool) {
func ToParamsAndPrepare(in any) (Params, error) {
if types.IsNil(in) {
return Params{}, true
return Params{}, nil
}
m, err := ToStringMapE(in)
if err != nil {
return nil, false
return nil, err
}
PrepareParams(m)
return m, true
return m, nil
}
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
func MustToParamsAndPrepare(in any) Params {
if p, ok := ToParamsAndPrepare(in); ok {
return p
} else {
panic(fmt.Sprintf("cannot convert %T to maps.Params", in))
p, err := ToParamsAndPrepare(in)
if err != nil {
panic(fmt.Sprintf("cannot convert %T to maps.Params: %s", in, err))
}
return p
}
// ToStringMap converts in to map[string]interface{}.
@@ -96,6 +96,8 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
switch v := in.(type) {
case []map[string]any:
return v, nil
case Params:
return []map[string]any{v}, nil
case []any:
var s []map[string]any
for _, entry := range v {
@@ -123,6 +125,23 @@ func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) {
return s, false
}
// MergeShallow merges src into dst, but only if the key does not already exist in dst.
// The keys are compared case insensitively.
func MergeShallow(dst, src map[string]any) {
for k, v := range src {
found := false
for dk := range dst {
if strings.EqualFold(dk, k) {
found = true
break
}
}
if !found {
dst[k] = v
}
}
}
type keyRename struct {
pattern glob.Glob
newKey string

View File

@@ -116,11 +116,11 @@ func TestToSliceStringMap(t *testing.T) {
func TestToParamsAndPrepare(t *testing.T) {
c := qt.New(t)
_, ok := ToParamsAndPrepare(map[string]any{"A": "av"})
c.Assert(ok, qt.IsTrue)
_, err := ToParamsAndPrepare(map[string]any{"A": "av"})
c.Assert(err, qt.IsNil)
params, ok := ToParamsAndPrepare(nil)
c.Assert(ok, qt.IsTrue)
params, err := ToParamsAndPrepare(nil)
c.Assert(err, qt.IsNil)
c.Assert(params, qt.DeepEquals, Params{})
}

View File

@@ -23,30 +23,37 @@ import (
// Params is a map where all keys are lower case.
type Params map[string]any
// Get does a lower case and nested search in this map.
// KeyParams is an utility struct for the WalkParams method.
type KeyParams struct {
Key string
Params Params
}
// GetNested does a lower case and nested search in this map.
// It will return nil if none found.
func (p Params) Get(indices ...string) any {
// Make all of these methods internal somehow.
func (p Params) GetNested(indices ...string) any {
v, _, _ := getNested(p, indices)
return v
}
// Set overwrites values in p with values in pp for common or new keys.
// Set overwrites values in dst with values in src for common or new keys.
// This is done recursively.
func (p Params) Set(pp Params) {
for k, v := range pp {
vv, found := p[k]
func SetParams(dst, src Params) {
for k, v := range src {
vv, found := dst[k]
if !found {
p[k] = v
dst[k] = v
} else {
switch vvv := vv.(type) {
case Params:
if pv, ok := v.(Params); ok {
vvv.Set(pv)
SetParams(vvv, pv)
} else {
p[k] = v
dst[k] = v
}
default:
p[k] = v
dst[k] = v
}
}
}
@@ -70,18 +77,17 @@ func (p Params) IsZero() bool {
}
// Merge transfers values from pp to p for new keys.
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given.
// This is done recursively.
func (p Params) Merge(pp Params) {
p.merge("", pp)
func MergeParamsWithStrategy(strategy string, dst, src Params) {
dst.merge(ParamsMergeStrategy(strategy), src)
}
// MergeRoot transfers values from pp to p for new keys where p is the
// root of the tree.
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge encoded in dst.
// This is done recursively.
func (p Params) MergeRoot(pp Params) {
ms, _ := p.GetMergeStrategy()
p.merge(ms, pp)
func MergeParams(dst, src Params) {
ms, _ := dst.GetMergeStrategy()
dst.merge(ms, src)
}
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
@@ -116,6 +122,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
}
}
// For internal use.
func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
if v, found := p[mergeStrategyKey]; found {
if s, ok := v.(ParamsMergeStrategy); ok {
@@ -125,6 +132,7 @@ func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
return ParamsMergeStrategyShallow, false
}
// For internal use.
func (p Params) DeleteMergeStrategy() bool {
if _, found := p[mergeStrategyKey]; found {
delete(p, mergeStrategyKey)
@@ -133,7 +141,8 @@ func (p Params) DeleteMergeStrategy() bool {
return false
}
func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
// For internal use.
func (p Params) SetMergeStrategy(s ParamsMergeStrategy) {
switch s {
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
default:
@@ -187,7 +196,7 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error)
keySegments := strings.Split(keyStr, separator)
for _, m := range candidates {
if v := m.Get(keySegments...); v != nil {
if v := m.GetNested(keySegments...); v != nil {
return v, nil
}
}
@@ -236,6 +245,55 @@ const (
mergeStrategyKey = "_merge"
)
// CleanConfigStringMapString removes any processing instructions from m,
// m will never be modified.
func CleanConfigStringMapString(m map[string]string) map[string]string {
if m == nil || len(m) == 0 {
return m
}
if _, found := m[mergeStrategyKey]; !found {
return m
}
// Create a new map and copy all the keys except the merge strategy key.
m2 := make(map[string]string, len(m)-1)
for k, v := range m {
if k != mergeStrategyKey {
m2[k] = v
}
}
return m2
}
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any.
func CleanConfigStringMap(m map[string]any) map[string]any {
if m == nil || len(m) == 0 {
return m
}
if _, found := m[mergeStrategyKey]; !found {
return m
}
// Create a new map and copy all the keys except the merge strategy key.
m2 := make(map[string]any, len(m)-1)
for k, v := range m {
if k != mergeStrategyKey {
m2[k] = v
}
switch v2 := v.(type) {
case map[string]any:
m2[k] = CleanConfigStringMap(v2)
case Params:
var p Params = CleanConfigStringMap(v2)
m2[k] = p
case map[string]string:
m2[k] = CleanConfigStringMapString(v2)
}
}
return m2
}
func toMergeStrategy(v any) ParamsMergeStrategy {
s := ParamsMergeStrategy(cast.ToString(v))
switch s {

View File

@@ -81,7 +81,7 @@ func TestParamsSetAndMerge(t *testing.T) {
p1, p2 := createParamsPair()
p1.Set(p2)
SetParams(p1, p2)
c.Assert(p1, qt.DeepEquals, Params{
"a": "abv",
@@ -97,7 +97,7 @@ func TestParamsSetAndMerge(t *testing.T) {
p1, p2 = createParamsPair()
p1.Merge(p2)
MergeParamsWithStrategy("", p1, p2)
// Default is to do a shallow merge.
c.Assert(p1, qt.DeepEquals, Params{
@@ -111,8 +111,8 @@ func TestParamsSetAndMerge(t *testing.T) {
})
p1, p2 = createParamsPair()
p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone)
p1.Merge(p2)
p1.SetMergeStrategy(ParamsMergeStrategyNone)
MergeParamsWithStrategy("", p1, p2)
p1.DeleteMergeStrategy()
c.Assert(p1, qt.DeepEquals, Params{
@@ -125,8 +125,8 @@ func TestParamsSetAndMerge(t *testing.T) {
})
p1, p2 = createParamsPair()
p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow)
p1.Merge(p2)
p1.SetMergeStrategy(ParamsMergeStrategyShallow)
MergeParamsWithStrategy("", p1, p2)
p1.DeleteMergeStrategy()
c.Assert(p1, qt.DeepEquals, Params{
@@ -140,8 +140,8 @@ func TestParamsSetAndMerge(t *testing.T) {
})
p1, p2 = createParamsPair()
p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep)
p1.Merge(p2)
p1.SetMergeStrategy(ParamsMergeStrategyDeep)
MergeParamsWithStrategy("", p1, p2)
p1.DeleteMergeStrategy()
c.Assert(p1, qt.DeepEquals, Params{

110
common/urls/baseURL.go Normal file
View File

@@ -0,0 +1,110 @@
// Copyright 2023 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 urls
import (
"fmt"
"net/url"
"strconv"
"strings"
)
// A BaseURL in Hugo is normally on the form scheme://path, but the
// form scheme: is also valid (mailto:hugo@rules.com).
type BaseURL struct {
url *url.URL
WithPath string
WithoutPath string
BasePath string
}
func (b BaseURL) String() string {
return b.WithPath
}
func (b BaseURL) Path() string {
return b.url.Path
}
func (b BaseURL) Port() int {
p, _ := strconv.Atoi(b.url.Port())
return p
}
// HostURL returns the URL to the host root without any path elements.
func (b BaseURL) HostURL() string {
return strings.TrimSuffix(b.String(), b.Path())
}
// WithProtocol returns the BaseURL prefixed with the given protocol.
// The Protocol is normally of the form "scheme://", i.e. "webcal://".
func (b BaseURL) WithProtocol(protocol string) (BaseURL, error) {
u := b.URL()
scheme := protocol
isFullProtocol := strings.HasSuffix(scheme, "://")
isOpaqueProtocol := strings.HasSuffix(scheme, ":")
if isFullProtocol {
scheme = strings.TrimSuffix(scheme, "://")
} else if isOpaqueProtocol {
scheme = strings.TrimSuffix(scheme, ":")
}
u.Scheme = scheme
if isFullProtocol && u.Opaque != "" {
u.Opaque = "//" + u.Opaque
} else if isOpaqueProtocol && u.Opaque == "" {
return BaseURL{}, fmt.Errorf("cannot determine BaseURL for protocol %q", protocol)
}
return newBaseURLFromURL(u)
}
func (b BaseURL) WithPort(port int) (BaseURL, error) {
u := b.URL()
u.Host = u.Hostname() + ":" + strconv.Itoa(port)
return newBaseURLFromURL(u)
}
// URL returns a copy of the internal URL.
// The copy can be safely used and modified.
func (b BaseURL) URL() *url.URL {
c := *b.url
return &c
}
func NewBaseURLFromString(b string) (BaseURL, error) {
u, err := url.Parse(b)
if err != nil {
return BaseURL{}, err
}
return newBaseURLFromURL(u)
}
func newBaseURLFromURL(u *url.URL) (BaseURL, error) {
baseURL := BaseURL{url: u, WithPath: u.String()}
var baseURLNoPath = baseURL.URL()
baseURLNoPath.Path = ""
baseURL.WithoutPath = baseURLNoPath.String()
basePath := u.Path
if basePath != "" && basePath != "/" {
baseURL.BasePath = basePath
}
return baseURL, nil
}

View File

@@ -0,0 +1,67 @@
// Copyright 2023 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 urls
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestBaseURL(t *testing.T) {
c := qt.New(t)
b, err := NewBaseURLFromString("http://example.com")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "http://example.com")
p, err := b.WithProtocol("webcal://")
c.Assert(err, qt.IsNil)
c.Assert(p.String(), qt.Equals, "webcal://example.com")
p, err = b.WithProtocol("webcal")
c.Assert(err, qt.IsNil)
c.Assert(p.String(), qt.Equals, "webcal://example.com")
_, err = b.WithProtocol("mailto:")
c.Assert(err, qt.Not(qt.IsNil))
b, err = NewBaseURLFromString("mailto:hugo@rules.com")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "mailto:hugo@rules.com")
// These are pretty constructed
p, err = b.WithProtocol("webcal")
c.Assert(err, qt.IsNil)
c.Assert(p.String(), qt.Equals, "webcal:hugo@rules.com")
p, err = b.WithProtocol("webcal://")
c.Assert(err, qt.IsNil)
c.Assert(p.String(), qt.Equals, "webcal://hugo@rules.com")
// Test with "non-URLs". Some people will try to use these as a way to get
// relative URLs working etc.
b, err = NewBaseURLFromString("/")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "/")
b, err = NewBaseURLFromString("")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "")
// BaseURL with sub path
b, err = NewBaseURLFromString("http://example.com/sub")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "http://example.com/sub")
c.Assert(b.HostURL(), qt.Equals, "http://example.com")
}