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,813 @@
// 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 allconfig contains the full configuration for Hugo.
// <docsmeta>{ "name": "Configuration", "description": "This section holds all configiration options in Hugo." }</docsmeta>
package allconfig
import (
"errors"
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/spf13/afero"
xmaps "golang.org/x/exp/maps"
)
// InternalConfig is the internal configuration for Hugo, not read from any user provided config file.
type InternalConfig struct {
// Server mode?
Running bool
Quiet bool
Verbose bool
Clock string
Watch bool
DisableLiveReload bool
LiveReloadPort int
}
type Config struct {
// For internal use only.
Internal InternalConfig `mapstructure:"-" json:"-"`
// For internal use only.
C ConfigCompiled `mapstructure:"-" json:"-"`
RootConfig
// Author information.
Author map[string]any
// Social links.
Social map[string]string
// The build configuration section contains build-related configuration options.
// <docsmeta>{"identifiers": ["build"] }</docsmeta>
Build config.BuildConfig `mapstructure:"-"`
// The caches configuration section contains cache-related configuration options.
// <docsmeta>{"identifiers": ["caches"] }</docsmeta>
Caches filecache.Configs `mapstructure:"-"`
// The markup configuration section contains markup-related configuration options.
// <docsmeta>{"identifiers": ["markup"] }</docsmeta>
Markup markup_config.Config `mapstructure:"-"`
// The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type.
// <docsmeta>{"identifiers": ["mediatypes"], "refs": ["types:media:type"] }</docsmeta>
MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"`
Imaging *config.ConfigNamespace[images.ImagingConfig, images.ImagingConfigInternal] `mapstructure:"-"`
// The outputformats configuration sections maps a format name (a string) to a configuration object for that format.
OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"`
// The outputs configuration section maps a Page Kind (a string) to a slice of output formats.
// This can be overridden in the front matter.
Outputs map[string][]string `mapstructure:"-"`
// The cascade configuration section contains the top level front matter cascade configuration options,
// a slice of page matcher and params to apply to those pages.
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"`
// Menu configuration.
// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
Menus *config.ConfigNamespace[map[string]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"`
// The deployment configuration section contains for hugo deploy.
Deployment deploy.DeployConfig `mapstructure:"-"`
// Module configuration.
Module modules.Config `mapstructure:"-"`
// Front matter configuration.
Frontmatter pagemeta.FrontmatterConfig `mapstructure:"-"`
// Minification configuration.
Minify minifiers.MinifyConfig `mapstructure:"-"`
// Permalink configuration.
Permalinks map[string]string `mapstructure:"-"`
// Taxonomy configuration.
Taxonomies map[string]string `mapstructure:"-"`
// Sitemap configuration.
Sitemap config.SitemapConfig `mapstructure:"-"`
// Related content configuration.
Related related.Config `mapstructure:"-"`
// Server configuration.
Server config.Server `mapstructure:"-"`
// Privacy configuration.
Privacy privacy.Config `mapstructure:"-"`
// Security configuration.
Security security.Config `mapstructure:"-"`
// Services configuration.
Services services.Config `mapstructure:"-"`
// User provided parameters.
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
Params maps.Params `mapstructure:"-"`
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
Languages map[string]langs.LanguageConfig `mapstructure:"-"`
// UglyURLs configuration. Either a boolean or a sections map.
UglyURLs any `mapstructure:"-"`
}
type configCompiler interface {
CompileConfig() error
}
func (c Config) cloneForLang() *Config {
x := c
// Collapse all static dirs to one.
x.StaticDir = x.staticDirs()
// These will go away soon ...
x.StaticDir0 = nil
x.StaticDir1 = nil
x.StaticDir2 = nil
x.StaticDir3 = nil
x.StaticDir4 = nil
x.StaticDir5 = nil
x.StaticDir6 = nil
x.StaticDir7 = nil
x.StaticDir8 = nil
x.StaticDir9 = nil
x.StaticDir10 = nil
return &x
}
func (c *Config) CompileConfig() error {
s := c.Timeout
if _, err := strconv.Atoi(s); err == nil {
// A number, assume seconds.
s = s + "s"
}
timeout, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("failed to parse timeout: %s", err)
}
disabledKinds := make(map[string]bool)
for _, kind := range c.DisableKinds {
disabledKinds[strings.ToLower(kind)] = true
}
kindOutputFormats := make(map[string]output.Formats)
isRssDisabled := disabledKinds["rss"]
outputFormats := c.OutputFormats.Config
for kind, formats := range c.Outputs {
if disabledKinds[kind] {
continue
}
for _, format := range formats {
if isRssDisabled && format == "rss" {
// Legacy config.
continue
}
f, found := outputFormats.GetByName(format)
if !found {
return fmt.Errorf("unknown output format %q for kind %q", format, kind)
}
kindOutputFormats[kind] = append(kindOutputFormats[kind], f)
}
}
disabledLangs := make(map[string]bool)
for _, lang := range c.DisableLanguages {
if lang == c.DefaultContentLanguage {
return fmt.Errorf("cannot disable default content language %q", lang)
}
disabledLangs[lang] = true
}
ignoredErrors := make(map[string]bool)
for _, err := range c.IgnoreErrors {
ignoredErrors[strings.ToLower(err)] = true
}
baseURL, err := urls.NewBaseURLFromString(c.BaseURL)
if err != nil {
return err
}
isUglyURL := func(section string) bool {
switch v := c.UglyURLs.(type) {
case bool:
return v
case map[string]bool:
return v[section]
default:
return false
}
}
ignoreFile := func(s string) bool {
return false
}
if len(c.IgnoreFiles) > 0 {
regexps := make([]*regexp.Regexp, len(c.IgnoreFiles))
for i, pattern := range c.IgnoreFiles {
var err error
regexps[i], err = regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("failed to compile ignoreFiles pattern %q: %s", pattern, err)
}
}
ignoreFile = func(s string) bool {
for _, r := range regexps {
if r.MatchString(s) {
return true
}
}
return false
}
}
var clock time.Time
if c.Internal.Clock != "" {
var err error
clock, err = time.Parse(time.RFC3339, c.Internal.Clock)
if err != nil {
return fmt.Errorf("failed to parse clock: %s", err)
}
}
c.C = ConfigCompiled{
Timeout: timeout,
BaseURL: baseURL,
BaseURLLiveReload: baseURL,
DisabledKinds: disabledKinds,
DisabledLanguages: disabledLangs,
IgnoredErrors: ignoredErrors,
KindOutputFormats: kindOutputFormats,
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
MainSections: c.MainSections,
Clock: clock,
}
for _, s := range allDecoderSetups {
if getCompiler := s.getCompiler; getCompiler != nil {
if err := getCompiler(c).CompileConfig(); err != nil {
return err
}
}
}
return nil
}
func (c Config) IsKindEnabled(kind string) bool {
return !c.C.DisabledKinds[kind]
}
func (c Config) IsLangDisabled(lang string) bool {
return c.C.DisabledLanguages[lang]
}
// ConfigCompiled holds values and functions that are derived from the config.
type ConfigCompiled struct {
Timeout time.Duration
BaseURL urls.BaseURL
BaseURLLiveReload urls.BaseURL
KindOutputFormats map[string]output.Formats
DisabledKinds map[string]bool
DisabledLanguages map[string]bool
IgnoredErrors map[string]bool
CreateTitle func(s string) string
IsUglyURLSection func(section string) bool
IgnoreFile func(filename string) bool
MainSections []string
Clock time.Time
}
// This may be set after the config is compiled.
func (c *ConfigCompiled) SetMainSections(sections []string) {
c.MainSections = sections
}
// This is set after the config is compiled by the server command.
func (c *ConfigCompiled) SetBaseURL(baseURL, baseURLLiveReload urls.BaseURL) {
c.BaseURL = baseURL
c.BaseURLLiveReload = baseURLLiveReload
}
// RootConfig holds all the top-level configuration options in Hugo
type RootConfig struct {
// The base URL of the site.
// Note that the default value is empty, but Hugo requires a valid URL (e.g. "https://example.com/") to work properly.
// <docsmeta>{"identifiers": ["URL"] }</docsmeta>
BaseURL string
// Whether to build content marked as draft.X
// <docsmeta>{"identifiers": ["draft"] }</docsmeta>
BuildDrafts bool
// Whether to build content with expiryDate in the past.
// <docsmeta>{"identifiers": ["expiryDate"] }</docsmeta>
BuildExpired bool
// Whether to build content with publishDate in the future.
// <docsmeta>{"identifiers": ["publishDate"] }</docsmeta>
BuildFuture bool
// Copyright information.
Copyright string
// The language to apply to content without any Clolanguage indicator.
DefaultContentLanguage string
// By defefault, we put the default content language in the root and the others below their language ID, e.g. /no/.
// Set this to true to put all languages below their language ID.
DefaultContentLanguageInSubdir bool
// Disable creation of alias redirect pages.
DisableAliases bool
// Disable lower casing of path segments.
DisablePathToLower bool
// Disable page kinds from build.
DisableKinds []string
// A list of languages to disable.
DisableLanguages []string
// Disable the injection of the Hugo generator tag on the home page.
DisableHugoGeneratorInject bool
// Enable replacement in Pages' Content of Emoji shortcodes with their equivalent Unicode characters.
// <docsmeta>{"identifiers": ["Content", "Unicode"] }</docsmeta>
EnableEmoji bool
// THe main section(s) of the site.
// If not set, Hugo will try to guess this from the content.
MainSections []string
// Enable robots.txt generation.
EnableRobotsTXT bool
// When enabled, Hugo will apply Git version information to each Page if possible, which
// can be used to keep lastUpdated in synch and to print version information.
// <docsmeta>{"identifiers": ["Page"] }</docsmeta>
EnableGitInfo bool
// Enable to track, calculate and print metrics.
TemplateMetrics bool
// Enable to track, print and calculate metric hints.
TemplateMetricsHints bool
// Enable to disable the build lock file.
NoBuildLock bool
// A list of error IDs to ignore.
IgnoreErrors []string
// A list of regexps that match paths to ignore.
// Deprecated: Use the settings on module imports.
IgnoreFiles []string
// Ignore cache.
IgnoreCache bool
// Enable to print greppable placeholders (on the form "[i18n] TRANSLATIONID") for missing translation strings.
EnableMissingTranslationPlaceholders bool
// Enable to print warnings for missing translation strings.
LogI18nWarnings bool
// ENable to print warnings for multiple files published to the same destination.
LogPathWarnings bool
// The configured environment. Default is "development" for server and "production" for build.
Environment string
// The default language code.
LanguageCode string
// Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words.
HasCJKLanguage bool
// The default number of pages per page when paginating.
Paginate int
// The path to use when creating pagination URLs, e.g. "page" in /page/2/.
PaginatePath string
// Whether to pluralize default list titles.
// Note that this currently only works for English, but you can provide your own title in the content file's front matter.
PluralizeListTitles bool
// Make all relative URLs absolute using the baseURL.
// <docsmeta>{"identifiers": ["baseURL"] }</docsmeta>
CanonifyURLs bool
// Enable this to make all relative URLs relative to content root. Note that this does not affect absolute URLs.
RelativeURLs bool
// Removes non-spacing marks from composite characters in content paths.
RemovePathAccents bool
// Whether to track and print unused templates during the build.
PrintUnusedTemplates bool
// URL to be used as a placeholder when a page reference cannot be found in ref or relref. Is used as-is.
RefLinksNotFoundURL string
// When using ref or relref to resolve page links and a link cannot be resolved, it will be logged with this log level.
// Valid values are ERROR (default) or WARNING. Any ERROR will fail the build (exit -1).
RefLinksErrorLevel string
// This will create a menu with all the sections as menu items and all the sections pages as “shadow-members”.
SectionPagesMenu string
// The length of text in words to show in a .Summary.
SummaryLength int
// The site title.
Title string
// The theme(s) to use.
// See Modules for more a more flexible way to load themes.
Theme []string
// Timeout for generating page contents, specified as a duration or in milliseconds.
Timeout string
// The time zone (or location), e.g. Europe/Oslo, used to parse front matter dates without such information and in the time function.
TimeZone string
// Set titleCaseStyle to specify the title style used by the title template function and the automatic section titles in Hugo.
// It defaults to AP Stylebook for title casing, but you can also set it to Chicago or Go (every word starts with a capital letter).
TitleCaseStyle string
// The editor used for opening up new content.
NewContentEditor string
// Don't sync modification time of files for the static mounts.
NoTimes bool
// Don't sync modification time of files for the static mounts.
NoChmod bool
// Clean the destination folder before a new build.
// This currently only handles static files.
CleanDestinationDir bool
// A Glob pattern of module paths to ignore in the _vendor folder.
IgnoreVendorPaths string
config.CommonDirs `mapstructure:",squash"`
// The odd constructs below are kept for backwards compatibility.
// Deprecated: Use module mount config instead.
StaticDir []string
// Deprecated: Use module mount config instead.
StaticDir0 []string
// Deprecated: Use module mount config instead.
StaticDir1 []string
// Deprecated: Use module mount config instead.
StaticDir2 []string
// Deprecated: Use module mount config instead.
StaticDir3 []string
// Deprecated: Use module mount config instead.
StaticDir4 []string
// Deprecated: Use module mount config instead.
StaticDir5 []string
// Deprecated: Use module mount config instead.
StaticDir6 []string
// Deprecated: Use module mount config instead.
StaticDir7 []string
// Deprecated: Use module mount config instead.
StaticDir8 []string
// Deprecated: Use module mount config instead.
StaticDir9 []string
// Deprecated: Use module mount config instead.
StaticDir10 []string
}
func (c RootConfig) staticDirs() []string {
var dirs []string
dirs = append(dirs, c.StaticDir...)
dirs = append(dirs, c.StaticDir0...)
dirs = append(dirs, c.StaticDir1...)
dirs = append(dirs, c.StaticDir2...)
dirs = append(dirs, c.StaticDir3...)
dirs = append(dirs, c.StaticDir4...)
dirs = append(dirs, c.StaticDir5...)
dirs = append(dirs, c.StaticDir6...)
dirs = append(dirs, c.StaticDir7...)
dirs = append(dirs, c.StaticDir8...)
dirs = append(dirs, c.StaticDir9...)
dirs = append(dirs, c.StaticDir10...)
return helpers.UniqueStringsReuse(dirs)
}
type Configs struct {
Base *Config
LoadingInfo config.LoadConfigResult
LanguageConfigMap map[string]*Config
LanguageConfigSlice []*Config
IsMultihost bool
Languages langs.Languages
LanguagesDefaultFirst langs.Languages
Modules modules.Modules
ModulesClient *modules.Client
configLangs []config.AllProvider
}
func (c *Configs) IsZero() bool {
// A config always has at least one language.
return c == nil || len(c.Languages) == 0
}
func (c *Configs) Init() error {
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
c.configLangs[i] = ConfigLanguage{
m: c,
config: c.LanguageConfigMap[l.Lang],
baseConfig: c.LoadingInfo.BaseConfig,
language: l,
}
}
if len(c.Modules) == 0 {
return errors.New("no modules loaded (ned at least the main module)")
}
// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil {
return err
}
return nil
}
func (c Configs) ConfigLangs() []config.AllProvider {
return c.configLangs
}
func (c Configs) GetFirstLanguageConfig() config.AllProvider {
return c.configLangs[0]
}
func (c Configs) GetByLang(lang string) config.AllProvider {
for _, l := range c.configLangs {
if l.Language().Lang == lang {
return l
}
}
return nil
}
// FromLoadConfigResult creates a new Config from res.
func FromLoadConfigResult(fs afero.Fs, res config.LoadConfigResult) (*Configs, error) {
if !res.Cfg.IsSet("languages") {
// We need at least one
lang := res.Cfg.GetString("defaultContentLanguage")
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
}
bcfg := res.BaseConfig
cfg := res.Cfg
all := &Config{}
err := decodeConfigFromParams(fs, bcfg, cfg, all, nil)
if err != nil {
return nil, err
}
langConfigMap := make(map[string]*Config)
var langConfigs []*Config
languagesConfig := cfg.GetStringMap("languages")
var isMultiHost bool
if err := all.CompileConfig(); err != nil {
return nil, err
}
for k, v := range languagesConfig {
mergedConfig := config.New()
var differentRootKeys []string
switch x := v.(type) {
case maps.Params:
for kk, vv := range x {
if kk == "baseurl" {
// baseURL configure don the language level is a multihost setup.
isMultiHost = true
}
mergedConfig.Set(kk, vv)
if cfg.IsSet(kk) {
rootv := cfg.Get(kk)
// This overrides a root key and potentially needs a merge.
if !reflect.DeepEqual(rootv, vv) {
switch vvv := vv.(type) {
case maps.Params:
differentRootKeys = append(differentRootKeys, kk)
// Use the language value as base.
mergedConfigEntry := xmaps.Clone(vvv)
// Merge in the root value.
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
mergedConfig.Set(kk, mergedConfigEntry)
default:
// Apply new values to the root.
differentRootKeys = append(differentRootKeys, "")
}
}
} else {
// Apply new values to the root.
differentRootKeys = append(differentRootKeys, "")
}
}
differentRootKeys = helpers.UniqueStringsSorted(differentRootKeys)
if len(differentRootKeys) == 0 {
langConfigMap[k] = all
continue
}
// Create a copy of the complete config and replace the root keys with the language specific ones.
clone := all.cloneForLang()
if err := decodeConfigFromParams(fs, bcfg, mergedConfig, clone, differentRootKeys); err != nil {
return nil, fmt.Errorf("failed to decode config for language %q: %w", k, err)
}
if err := clone.CompileConfig(); err != nil {
return nil, err
}
langConfigMap[k] = clone
case maps.ParamsMergeStrategy:
default:
panic(fmt.Sprintf("unknown type in languages config: %T", v))
}
}
var languages langs.Languages
defaultContentLanguage := all.DefaultContentLanguage
for k, v := range langConfigMap {
languageConf := v.Languages[k]
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
if err != nil {
return nil, err
}
languages = append(languages, language)
}
// Sort the sites by language weight (if set) or lang.
sort.Slice(languages, func(i, j int) bool {
li := languages[i]
lj := languages[j]
if li.Weight != lj.Weight {
return li.Weight < lj.Weight
}
return li.Lang < lj.Lang
})
for _, l := range languages {
langConfigs = append(langConfigs, langConfigMap[l.Lang])
}
var languagesDefaultFirst langs.Languages
for _, l := range languages {
if l.Lang == defaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
for _, l := range languages {
if l.Lang != defaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
bcfg.PublishDir = all.PublishDir
res.BaseConfig = bcfg
cm := &Configs{
Base: all,
LanguageConfigMap: langConfigMap,
LanguageConfigSlice: langConfigs,
LoadingInfo: res,
IsMultihost: isMultiHost,
Languages: languages,
LanguagesDefaultFirst: languagesDefaultFirst,
}
return cm, nil
}
func decodeConfigFromParams(fs afero.Fs, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error {
var decoderSetups []decodeWeight
if len(keys) == 0 {
for _, v := range allDecoderSetups {
decoderSetups = append(decoderSetups, v)
}
} else {
for _, key := range keys {
if v, found := allDecoderSetups[key]; found {
decoderSetups = append(decoderSetups, v)
} else {
return fmt.Errorf("unknown config key %q", key)
}
}
}
// Sort them to get the dependency order right.
sort.Slice(decoderSetups, func(i, j int) bool {
ki, kj := decoderSetups[i], decoderSetups[j]
if ki.weight == kj.weight {
return ki.key < kj.key
}
return ki.weight < kj.weight
})
for _, v := range decoderSetups {
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg}
if err := v.decode(v, p); err != nil {
return fmt.Errorf("failed to decode %q: %w", v.key, err)
}
}
return nil
}
func createDefaultOutputFormats(allFormats output.Formats) map[string][]string {
if len(allFormats) == 0 {
panic("no output formats")
}
rssOut, rssFound := allFormats.GetByName(output.RSSFormat.Name)
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
defaultListTypes := []string{htmlOut.Name}
if rssFound {
defaultListTypes = append(defaultListTypes, rssOut.Name)
}
m := map[string][]string{
page.KindPage: {htmlOut.Name},
page.KindHome: defaultListTypes,
page.KindSection: defaultListTypes,
page.KindTerm: defaultListTypes,
page.KindTaxonomy: defaultListTypes,
}
// May be disabled
if rssFound {
m["rss"] = []string{rssOut.Name}
}
return m
}

View File

@@ -0,0 +1,325 @@
// 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 allconfig
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/mitchellh/mapstructure"
"github.com/spf13/afero"
"github.com/spf13/cast"
)
type decodeConfig struct {
p config.Provider
c *Config
fs afero.Fs
bcfg config.BaseConfig
}
type decodeWeight struct {
key string
decode func(decodeWeight, decodeConfig) error
getCompiler func(c *Config) configCompiler
weight int
}
var allDecoderSetups = map[string]decodeWeight{
"": {
key: "",
weight: -100, // Always first.
decode: func(d decodeWeight, p decodeConfig) error {
return mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig)
},
},
"imaging": {
key: "imaging",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Imaging, err = images.DecodeConfig(p.p.GetStringMap(d.key))
return err
},
},
"caches": {
key: "caches",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Caches, err = filecache.DecodeConfig(p.fs, p.bcfg, p.p.GetStringMap(d.key))
if p.c.IgnoreCache {
// Set MaxAge in all caches to 0.
for k, cache := range p.c.Caches {
cache.MaxAge = 0
p.c.Caches[k] = cache
}
}
return err
},
},
"build": {
key: "build",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Build = config.DecodeBuildConfig(p.p)
return nil
},
},
"frontmatter": {
key: "frontmatter",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(p.p)
return err
},
},
"markup": {
key: "markup",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Markup, err = markup_config.Decode(p.p)
return err
},
},
"server": {
key: "server",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Server, err = config.DecodeServer(p.p)
return err
},
getCompiler: func(c *Config) configCompiler {
return &c.Server
},
},
"minify": {
key: "minify",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Minify, err = minifiers.DecodeConfig(p.p.Get(d.key))
return err
},
},
"mediaTypes": {
key: "mediaTypes",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.MediaTypes, err = media.DecodeTypes(p.p.GetStringMap(d.key))
return err
},
},
"outputs": {
key: "outputs",
decode: func(d decodeWeight, p decodeConfig) error {
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
m := p.p.GetStringMap("outputs")
p.c.Outputs = make(map[string][]string)
for k, v := range m {
s := types.ToStringSlicePreserveString(v)
for i, v := range s {
s[i] = strings.ToLower(v)
}
p.c.Outputs[k] = s
}
// Apply defaults.
for k, v := range defaults {
if _, found := p.c.Outputs[k]; !found {
p.c.Outputs[k] = v
}
}
return nil
},
},
"outputFormats": {
key: "outputFormats",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.OutputFormats, err = output.DecodeConfig(p.c.MediaTypes.Config, p.p.Get(d.key))
return err
},
},
"params": {
key: "params",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params"))
if p.c.Params == nil {
p.c.Params = make(map[string]any)
}
// Before Hugo 0.112.0 this was configured via site Params.
if mainSections, found := p.c.Params["mainsections"]; found {
p.c.MainSections = types.ToStringSlicePreserveString(mainSections)
}
return nil
},
},
"module": {
key: "module",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Module, err = modules.DecodeConfig(p.p)
return err
},
},
"permalinks": {
key: "permalinks",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Permalinks = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
},
"sitemap": {
key: "sitemap",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key))
return err
},
},
"taxonomies": {
key: "taxonomies",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
},
"related": {
key: "related",
weight: 100, // This needs to be decoded after taxonomies.
decode: func(d decodeWeight, p decodeConfig) error {
if p.p.IsSet(d.key) {
var err error
p.c.Related, err = related.DecodeConfig(p.p.GetParams(d.key))
if err != nil {
return fmt.Errorf("failed to decode related config: %w", err)
}
} else {
p.c.Related = related.DefaultConfig
if _, found := p.c.Taxonomies["tag"]; found {
p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80})
}
}
return nil
},
},
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Languages, err = langs.DecodeConfig(p.p.GetStringMap(d.key))
return err
},
},
"cascade": {
key: "cascade",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key))
return err
},
},
"menus": {
key: "menus",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Menus, err = navigation.DecodeConfig(p.p.Get(d.key))
return err
},
},
"privacy": {
key: "privacy",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Privacy, err = privacy.DecodeConfig(p.p)
return err
},
},
"security": {
key: "security",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Security, err = security.DecodeConfig(p.p)
return err
},
},
"services": {
key: "services",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Services, err = services.DecodeConfig(p.p)
return err
},
},
"deployment": {
key: "deployment",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Deployment, err = deploy.DecodeConfig(p.p)
return err
},
},
"author": {
key: "author",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Author = p.p.GetStringMap(d.key)
return nil
},
},
"social": {
key: "social",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Social = p.p.GetStringMapString(d.key)
return nil
},
},
"uglyurls": {
key: "uglyurls",
decode: func(d decodeWeight, p decodeConfig) error {
v := p.p.Get(d.key)
switch vv := v.(type) {
case bool:
p.c.UglyURLs = vv
case string:
p.c.UglyURLs = vv == "true"
default:
p.c.UglyURLs = cast.ToStringMapBool(v)
}
return nil
},
},
"internal": {
key: "internal",
decode: func(d decodeWeight, p decodeConfig) error {
return mapstructure.WeakDecode(p.p.GetStringMap(d.key), &p.c.Internal)
},
},
}

View File

@@ -0,0 +1,216 @@
// 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 allconfig
import (
"time"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/langs"
)
type ConfigLanguage struct {
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
}
func (c ConfigLanguage) Language() *langs.Language {
return c.language
}
func (c ConfigLanguage) Languages() langs.Languages {
return c.m.Languages
}
func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
return c.m.LanguagesDefaultFirst
}
func (c ConfigLanguage) BaseURL() urls.BaseURL {
return c.config.C.BaseURL
}
func (c ConfigLanguage) BaseURLLiveReload() urls.BaseURL {
return c.config.C.BaseURLLiveReload
}
func (c ConfigLanguage) Environment() string {
return c.config.Environment
}
func (c ConfigLanguage) IsMultihost() bool {
return c.m.IsMultihost
}
func (c ConfigLanguage) IsMultiLingual() bool {
return len(c.m.Languages) > 1
}
func (c ConfigLanguage) TemplateMetrics() bool {
return c.config.TemplateMetrics
}
func (c ConfigLanguage) TemplateMetricsHints() bool {
return c.config.TemplateMetricsHints
}
func (c ConfigLanguage) IsLangDisabled(lang string) bool {
return c.config.C.DisabledLanguages[lang]
}
func (c ConfigLanguage) IgnoredErrors() map[string]bool {
return c.config.C.IgnoredErrors
}
func (c ConfigLanguage) NoBuildLock() bool {
return c.config.NoBuildLock
}
func (c ConfigLanguage) NewContentEditor() string {
return c.config.NewContentEditor
}
func (c ConfigLanguage) Timeout() time.Duration {
return c.config.C.Timeout
}
func (c ConfigLanguage) BaseConfig() config.BaseConfig {
return c.baseConfig
}
func (c ConfigLanguage) Dirs() config.CommonDirs {
return c.config.CommonDirs
}
func (c ConfigLanguage) DirsBase() config.CommonDirs {
return c.m.Base.CommonDirs
}
func (c ConfigLanguage) Quiet() bool {
return c.m.Base.Internal.Quiet
}
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
func (c ConfigLanguage) GetConfigSection(s string) any {
switch s {
case "security":
return c.config.Security
case "build":
return c.config.Build
case "frontmatter":
return c.config.Frontmatter
case "caches":
return c.config.Caches
case "markup":
return c.config.Markup
case "mediaTypes":
return c.config.MediaTypes.Config
case "outputFormats":
return c.config.OutputFormats.Config
case "permalinks":
return c.config.Permalinks
case "minify":
return c.config.Minify
case "activeModules":
return c.m.Modules
case "deployment":
return c.config.Deployment
default:
panic("not implemented: " + s)
}
}
func (c ConfigLanguage) GetConfig() any {
return c.config
}
func (c ConfigLanguage) CanonifyURLs() bool {
return c.config.CanonifyURLs
}
func (c ConfigLanguage) IsUglyURLs(section string) bool {
return c.config.C.IsUglyURLSection(section)
}
func (c ConfigLanguage) IgnoreFile(s string) bool {
return c.config.C.IgnoreFile(s)
}
func (c ConfigLanguage) DisablePathToLower() bool {
return c.config.DisablePathToLower
}
func (c ConfigLanguage) RemovePathAccents() bool {
return c.config.RemovePathAccents
}
func (c ConfigLanguage) DefaultContentLanguage() string {
return c.config.DefaultContentLanguage
}
func (c ConfigLanguage) DefaultContentLanguageInSubdir() bool {
return c.config.DefaultContentLanguageInSubdir
}
func (c ConfigLanguage) SummaryLength() int {
return c.config.SummaryLength
}
func (c ConfigLanguage) BuildExpired() bool {
return c.config.BuildExpired
}
func (c ConfigLanguage) BuildFuture() bool {
return c.config.BuildFuture
}
func (c ConfigLanguage) BuildDrafts() bool {
return c.config.BuildDrafts
}
func (c ConfigLanguage) Running() bool {
return c.config.Internal.Running
}
func (c ConfigLanguage) PrintUnusedTemplates() bool {
return c.config.PrintUnusedTemplates
}
func (c ConfigLanguage) EnableMissingTranslationPlaceholders() bool {
return c.config.EnableMissingTranslationPlaceholders
}
func (c ConfigLanguage) LogI18nWarnings() bool {
return c.config.LogI18nWarnings
}
func (c ConfigLanguage) CreateTitle(s string) string {
return c.config.C.CreateTitle(s)
}
func (c ConfigLanguage) Paginate() int {
return c.config.Paginate
}
func (c ConfigLanguage) PaginatePath() string {
return c.config.PaginatePath
}
func (c ConfigLanguage) StaticDirs() []string {
return c.config.staticDirs()
}

View File

@@ -0,0 +1,71 @@
package allconfig_test
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib"
)
func TestDirsMount(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term"]
[languages]
[languages.en]
weight = 1
[languages.sv]
weight = 2
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'en'
[[module.mounts]]
source = 'content/sv'
target = 'content'
lang = 'sv'
-- content/en/p1.md --
---
title: "p1"
---
-- content/sv/p1.md --
---
title: "p1"
---
-- layouts/_default/single.html --
Title: {{ .Title }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
).Build()
//b.AssertFileContent("public/p1/index.html", "Title: p1")
sites := b.H.Sites
b.Assert(len(sites), qt.Equals, 2)
configs := b.H.Configs
mods := configs.Modules
b.Assert(len(mods), qt.Equals, 1)
mod := mods[0]
b.Assert(mod.Mounts(), qt.HasLen, 8)
enConcp := sites[0].Conf
enConf := enConcp.GetConfig().(*allconfig.Config)
b.Assert(enConcp.BaseURL().String(), qt.Equals, "https://example.com")
modConf := enConf.Module
b.Assert(modConf.Mounts, qt.HasLen, 2)
b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en"))
b.Assert(modConf.Mounts[0].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en")
b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv"))
b.Assert(modConf.Mounts[1].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
}

559
config/allconfig/load.go Normal file
View File

@@ -0,0 +1,559 @@
// 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 allconfig contains the full configuration for Hugo.
package allconfig
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
)
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
d.Environ = os.Environ()
}
l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
// Make sure we always do this, even in error situations,
// as we have commands (e.g. "hugo mod init") that will
// use a partial configuration to do its job.
defer l.deleteMergeStrategies()
res, _, err := l.loadConfigMain(d)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
configs, err := FromLoadConfigResult(d.Fs, res)
if err != nil {
return nil, fmt.Errorf("failed to create config from result: %w", err)
}
moduleConfig, modulesClient, err := l.loadModules(configs)
if err != nil {
return nil, fmt.Errorf("failed to load modules: %w", err)
}
if len(l.ModulesConfigFiles) > 0 {
// Config merged in from modules.
// Re-read the config.
configs, err = FromLoadConfigResult(d.Fs, res)
if err != nil {
return nil, fmt.Errorf("failed to create config: %w", err)
}
}
configs.Modules = moduleConfig.ActiveModules
configs.ModulesClient = modulesClient
if err := configs.Init(); err != nil {
return nil, fmt.Errorf("failed to init config: %w", err)
}
return configs, nil
}
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
type ConfigSourceDescriptor struct {
Fs afero.Fs
Logger loggers.Logger
// Config received from the command line.
// These will override any config file settings.
Flags config.Provider
// Path to the config file to use, e.g. /my/project/config.toml
Filename string
// The (optional) directory for additional configuration files.
ConfigDir string
// production, development
Environment string
// Defaults to os.Environ if not set.
Environ []string
}
func (d ConfigSourceDescriptor) configFilenames() []string {
if d.Filename == "" {
return nil
}
return strings.Split(d.Filename, ",")
}
type configLoader struct {
cfg config.Provider
BaseConfig config.BaseConfig
ConfigSourceDescriptor
// collected
ModulesConfig modules.ModulesConfig
ModulesConfigFiles []string
}
// Handle some legacy values.
func (l configLoader) applyConfigAliases() error {
aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}}
for _, alias := range aliases {
if l.cfg.IsSet(alias.Key) {
vv := l.cfg.Get(alias.Key)
l.cfg.Set(alias.Value, vv)
}
}
return nil
}
func (l configLoader) applyDefaultConfig() error {
defaultSettings := maps.Params{
"baseURL": "",
"cleanDestinationDir": false,
"watch": false,
"contentDir": "content",
"resourceDir": "resources",
"publishDir": "public",
"publishDirOrig": "public",
"themesDir": "themes",
"assetDir": "assets",
"layoutDir": "layouts",
"i18nDir": "i18n",
"dataDir": "data",
"archetypeDir": "archetypes",
"configDir": "config",
"staticDir": "static",
"buildDrafts": false,
"buildFuture": false,
"buildExpired": false,
"params": maps.Params{},
"environment": hugo.EnvironmentProduction,
"uglyURLs": false,
"verbose": false,
"ignoreCache": false,
"canonifyURLs": false,
"relativeURLs": false,
"removePathAccents": false,
"titleCaseStyle": "AP",
"taxonomies": maps.Params{"tag": "tags", "category": "categories"},
"permalinks": maps.Params{},
"sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"},
"menus": maps.Params{},
"disableLiveReload": false,
"pluralizeListTitles": true,
"forceSyncStatic": false,
"footnoteAnchorPrefix": "",
"footnoteReturnLinkContents": "",
"newContentEditor": "",
"paginate": 10,
"paginatePath": "page",
"summaryLength": 70,
"rssLimit": -1,
"sectionPagesMenu": "",
"disablePathToLower": false,
"hasCJKLanguage": false,
"enableEmoji": false,
"defaultContentLanguage": "en",
"defaultContentLanguageInSubdir": false,
"enableMissingTranslationPlaceholders": false,
"enableGitInfo": false,
"ignoreFiles": make([]string, 0),
"disableAliases": false,
"debug": false,
"disableFastRender": false,
"timeout": "30s",
"timeZone": "",
"enableInlineShortcodes": false,
}
l.cfg.SetDefaults(defaultSettings)
return nil
}
func (l configLoader) normalizeCfg(cfg config.Provider) error {
minify := cfg.Get("minify")
if b, ok := minify.(bool); ok && b {
cfg.Set("minify", maps.Params{"minifyOutput": true})
}
// Simplify later merge.
languages := cfg.GetStringMap("languages")
for _, v := range languages {
switch m := v.(type) {
case maps.Params:
// params have merge strategy deep by default.
// The languages config key has strategy none by default.
// This means that if these two sections does not exist on the left side,
// they will not get merged in, so just create some empty maps.
if _, ok := m["params"]; !ok {
m["params"] = maps.Params{}
}
}
}
return nil
}
func (l configLoader) cleanExternalConfig(cfg config.Provider) error {
if cfg.IsSet("internal") {
cfg.Set("internal", nil)
}
return nil
}
func (l configLoader) applyFlagsOverrides(cfg config.Provider) error {
for _, k := range cfg.Keys() {
l.cfg.Set(k, cfg.Get(k))
}
return nil
}
func (l configLoader) applyOsEnvOverrides(environ []string) error {
if len(environ) == 0 {
return nil
}
const delim = "__env__delim"
// Extract all that start with the HUGO prefix.
// The delimiter is the following rune, usually "_".
const hugoEnvPrefix = "HUGO"
var hugoEnv []types.KeyValueStr
for _, v := range environ {
key, val := config.SplitEnvVar(v)
if strings.HasPrefix(key, hugoEnvPrefix) {
delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
if len(delimiterAndKey) < 2 {
continue
}
// Allow delimiters to be case sensitive.
// It turns out there isn't that many allowed special
// chars in environment variables when used in Bash and similar,
// so variables on the form HUGOxPARAMSxFOO=bar is one option.
key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
key = strings.ToLower(key)
hugoEnv = append(hugoEnv, types.KeyValueStr{
Key: key,
Value: val,
})
}
}
for _, env := range hugoEnv {
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
if err != nil {
return err
}
if existing != nil {
val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
if err != nil {
continue
}
if owner != nil {
owner[nestedKey] = val
} else {
l.cfg.Set(env.Key, val)
}
} else if nestedKey != "" {
owner[nestedKey] = env.Value
} else {
// The container does not exist yet.
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
}
}
return nil
}
func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConfigResult, modules.ModulesConfig, error) {
var res config.LoadConfigResult
if d.Flags != nil {
if err := l.normalizeCfg(d.Flags); err != nil {
return res, l.ModulesConfig, err
}
}
if d.Fs == nil {
return res, l.ModulesConfig, errors.New("no filesystem provided")
}
if d.Flags != nil {
if err := l.applyFlagsOverrides(d.Flags); err != nil {
return res, l.ModulesConfig, err
}
workingDir := filepath.Clean(l.cfg.GetString("workingDir"))
l.BaseConfig = config.BaseConfig{
WorkingDir: workingDir,
ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")),
}
}
names := d.configFilenames()
if names != nil {
for _, name := range names {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
res.ConfigFiles = append(res.ConfigFiles, filename)
} else if err != ErrNoConfigFile {
return res, l.ModulesConfig, l.wrapFileError(err, filename)
}
}
} else {
for _, name := range config.DefaultConfigNames {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
res.ConfigFiles = append(res.ConfigFiles, filename)
break
} else if err != ErrNoConfigFile {
return res, l.ModulesConfig, l.wrapFileError(err, filename)
}
}
}
if d.ConfigDir != "" {
absConfigDir := paths.AbsPathify(l.BaseConfig.WorkingDir, d.ConfigDir)
dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, absConfigDir, l.Environment)
if err == nil {
if len(dirnames) > 0 {
if err := l.normalizeCfg(dcfg); err != nil {
return res, l.ModulesConfig, err
}
if err := l.cleanExternalConfig(dcfg); err != nil {
return res, l.ModulesConfig, err
}
l.cfg.Set("", dcfg.Get(""))
res.ConfigFiles = append(res.ConfigFiles, dirnames...)
}
} else if err != ErrNoConfigFile {
if len(dirnames) > 0 {
return res, l.ModulesConfig, l.wrapFileError(err, dirnames[0])
}
return res, l.ModulesConfig, err
}
}
res.Cfg = l.cfg
if err := l.applyDefaultConfig(); err != nil {
return res, l.ModulesConfig, err
}
// Some settings are used before we're done collecting all settings,
// so apply OS environment both before and after.
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return res, l.ModulesConfig, err
}
workingDir := filepath.Clean(l.cfg.GetString("workingDir"))
l.BaseConfig = config.BaseConfig{
WorkingDir: workingDir,
CacheDir: l.cfg.GetString("cacheDir"),
ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")),
}
var err error
l.BaseConfig.CacheDir, err = helpers.GetCacheDir(l.Fs, l.BaseConfig.CacheDir)
if err != nil {
return res, l.ModulesConfig, err
}
res.BaseConfig = l.BaseConfig
l.cfg.SetDefaultMergeStrategy()
res.ConfigFiles = append(res.ConfigFiles, l.ModulesConfigFiles...)
if d.Flags != nil {
if err := l.applyFlagsOverrides(d.Flags); err != nil {
return res, l.ModulesConfig, err
}
}
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return res, l.ModulesConfig, err
}
if err = l.applyConfigAliases(); err != nil {
return res, l.ModulesConfig, err
}
return res, l.ModulesConfig, err
}
func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) {
bcfg := configs.LoadingInfo.BaseConfig
conf := configs.Base
workingDir := bcfg.WorkingDir
themesDir := bcfg.ThemesDir
cfg := configs.LoadingInfo.Cfg
var ignoreVendor glob.Glob
if s := conf.IgnoreVendorPaths; s != "" {
ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
}
ex := hexec.New(conf.Security)
hook := func(m *modules.ModulesConfig) error {
for _, tc := range m.ActiveModules {
if len(tc.ConfigFilenames()) > 0 {
if tc.Watch() {
l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...)
}
// Merge in the theme config using the configured
// merge strategy.
cfg.Merge("", tc.Cfg().Get(""))
}
}
return nil
}
modulesClient := modules.NewClient(modules.ClientConfig{
Fs: l.Fs,
Logger: l.Logger,
Exec: ex,
HookBeforeFinalize: hook,
WorkingDir: workingDir,
ThemesDir: themesDir,
Environment: l.Environment,
CacheDir: conf.Caches.CacheDirModules(),
ModuleConfig: conf.Module,
IgnoreVendor: ignoreVendor,
})
moduleConfig, err := modulesClient.Collect()
// We want to watch these for changes and trigger rebuild on version
// changes etc.
if moduleConfig.GoModulesFilename != "" {
l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoModulesFilename)
}
if moduleConfig.GoWorkspaceFilename != "" {
l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoWorkspaceFilename)
}
return moduleConfig, modulesClient, err
}
func (l configLoader) loadConfig(configName string) (string, error) {
baseDir := l.BaseConfig.WorkingDir
var baseFilename string
if filepath.IsAbs(configName) {
baseFilename = configName
} else {
baseFilename = filepath.Join(baseDir, configName)
}
var filename string
if paths.ExtNoDelimiter(configName) != "" {
exists, _ := helpers.Exists(baseFilename, l.Fs)
if exists {
filename = baseFilename
}
} else {
for _, ext := range config.ValidConfigFileExtensions {
filenameToCheck := baseFilename + "." + ext
exists, _ := helpers.Exists(filenameToCheck, l.Fs)
if exists {
filename = filenameToCheck
break
}
}
}
if filename == "" {
return "", ErrNoConfigFile
}
m, err := config.FromFileToMap(l.Fs, filename)
if err != nil {
return filename, err
}
// Set overwrites keys of the same name, recursively.
l.cfg.Set("", m)
if err := l.normalizeCfg(l.cfg); err != nil {
return filename, err
}
if err := l.cleanExternalConfig(l.cfg); err != nil {
return filename, err
}
return filename, nil
}
func (l configLoader) deleteMergeStrategies() {
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
}
func (l configLoader) loadModulesConfig() (modules.Config, error) {
modConfig, err := modules.DecodeConfig(l.cfg)
if err != nil {
return modules.Config{}, err
}
return modConfig, nil
}
func (l configLoader) wrapFileError(err error, filename string) error {
fe := herrors.UnwrapFileError(err)
if fe != nil {
pos := fe.Position()
pos.Filename = filename
fe.UpdatePosition(pos)
return err
}
return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil)
}

View File

@@ -0,0 +1,67 @@
package allconfig
import (
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
)
func BenchmarkLoad(b *testing.B) {
tempDir := b.TempDir()
configFilename := filepath.Join(tempDir, "hugo.toml")
config := `
baseURL = "https://example.com"
defaultContentLanguage = 'en'
[module]
[[module.mounts]]
source = 'content/en'
target = 'content/en'
lang = 'en'
[[module.mounts]]
source = 'content/nn'
target = 'content/nn'
lang = 'nn'
[[module.mounts]]
source = 'content/no'
target = 'content/no'
lang = 'no'
[[module.mounts]]
source = 'content/sv'
target = 'content/sv'
lang = 'sv'
[[module.mounts]]
source = 'layouts'
target = 'layouts'
[languages]
[languages.en]
title = "English"
weight = 1
[languages.nn]
title = "Nynorsk"
weight = 2
[languages.no]
title = "Norsk"
weight = 3
[languages.sv]
title = "Svenska"
weight = 4
`
if err := os.WriteFile(configFilename, []byte(config), 0666); err != nil {
b.Fatal(err)
}
d := ConfigSourceDescriptor{
Fs: afero.NewOsFs(),
Filename: configFilename,
}
for i := 0; i < b.N; i++ {
_, err := LoadConfig(d)
if err != nil {
b.Fatal(err)
}
}
}

View File

@@ -17,7 +17,6 @@ import (
"fmt"
"sort"
"strings"
"sync"
"github.com/gohugoio/hugo/common/types"
@@ -25,16 +24,66 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
)
var DefaultBuild = Build{
type BaseConfig struct {
WorkingDir string
CacheDir string
ThemesDir string
PublishDir string
}
type CommonDirs struct {
// The directory where Hugo will look for themes.
ThemesDir string
// Where to put the generated files.
PublishDir string
// The directory to put the generated resources files. This directory should in most situations be considered temporary
// and not be committed to version control. But there may be cached content in here that you want to keep,
// e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup.
ResourceDir string
// The project root directory.
WorkingDir string
// The root directory for all cache files.
CacheDir string
// The content source directory.
// Deprecated: Use module mounts.
ContentDir string
// Deprecated: Use module mounts.
// The data source directory.
DataDir string
// Deprecated: Use module mounts.
// The layout source directory.
LayoutDir string
// Deprecated: Use module mounts.
// The i18n source directory.
I18nDir string
// Deprecated: Use module mounts.
// The archetypes source directory.
ArcheTypeDir string
// Deprecated: Use module mounts.
// The assets source directory.
AssetDir string
}
type LoadConfigResult struct {
Cfg Provider
ConfigFiles []string
BaseConfig BaseConfig
}
var DefaultBuild = BuildConfig{
UseResourceCacheWhen: "fallback",
WriteStats: false,
}
// Build holds some build related configuration.
type Build struct {
// BuildConfig holds some build related configuration.
type BuildConfig struct {
UseResourceCacheWhen string // never, fallback, always. Default is fallback
// When enabled, will collect and write a hugo_stats.json with some build
@@ -46,7 +95,7 @@ type Build struct {
NoJSConfigInAssets bool
}
func (b Build) UseResourceCache(err error) bool {
func (b BuildConfig) UseResourceCache(err error) bool {
if b.UseResourceCacheWhen == "never" {
return false
}
@@ -58,7 +107,7 @@ func (b Build) UseResourceCache(err error) bool {
return true
}
func DecodeBuild(cfg Provider) Build {
func DecodeBuildConfig(cfg Provider) BuildConfig {
m := cfg.GetStringMap("build")
b := DefaultBuild
if m == nil {
@@ -79,28 +128,19 @@ func DecodeBuild(cfg Provider) Build {
return b
}
// Sitemap configures the sitemap to be generated.
type Sitemap struct {
// SitemapConfig configures the sitemap to be generated.
type SitemapConfig struct {
// The page change frequency.
ChangeFreq string
Priority float64
Filename string
// The priority of the page.
Priority float64
// The sitemap filename.
Filename string
}
func DecodeSitemap(prototype Sitemap, input map[string]any) Sitemap {
for key, value := range input {
switch key {
case "changefreq":
prototype.ChangeFreq = cast.ToString(value)
case "priority":
prototype.Priority = cast.ToFloat64(value)
case "filename":
prototype.Filename = cast.ToString(value)
default:
jww.WARN.Printf("Unknown Sitemap field: %s\n", key)
}
}
return prototype
func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) {
err := mapstructure.WeakDecode(input, &prototype)
return prototype, err
}
// Config for the dev server.
@@ -108,25 +148,24 @@ type Server struct {
Headers []Headers
Redirects []Redirect
compiledInit sync.Once
compiledHeaders []glob.Glob
compiledRedirects []glob.Glob
}
func (s *Server) init() {
s.compiledInit.Do(func() {
for _, h := range s.Headers {
s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
}
for _, r := range s.Redirects {
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
}
})
func (s *Server) CompileConfig() error {
if s.compiledHeaders != nil {
return nil
}
for _, h := range s.Headers {
s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
}
for _, r := range s.Redirects {
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
}
return nil
}
func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
s.init()
if s.compiledHeaders == nil {
return nil
}
@@ -150,8 +189,6 @@ func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
}
func (s *Server) MatchRedirect(pattern string) Redirect {
s.init()
if s.compiledRedirects == nil {
return Redirect{}
}
@@ -195,14 +232,10 @@ func (r Redirect) IsZero() bool {
return r.From == ""
}
func DecodeServer(cfg Provider) (*Server, error) {
m := cfg.GetStringMap("server")
func DecodeServer(cfg Provider) (Server, error) {
s := &Server{}
if m == nil {
return s, nil
}
_ = mapstructure.WeakDecode(m, s)
_ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s)
for i, redir := range s.Redirects {
// Get it in line with the Hugo server for OK responses.
@@ -213,7 +246,7 @@ func DecodeServer(cfg Provider) (*Server, error) {
// There are some tricky infinite loop situations when dealing
// when the target does not have a trailing slash.
// This can certainly be handled better, but not time for that now.
return nil, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
return Server{}, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
}
}
s.Redirects[i] = redir
@@ -231,5 +264,5 @@ func DecodeServer(cfg Provider) (*Server, error) {
}
return s, nil
return *s, nil
}

View File

@@ -31,7 +31,7 @@ func TestBuild(t *testing.T) {
"useResourceCacheWhen": "always",
})
b := DecodeBuild(v)
b := DecodeBuildConfig(v)
c.Assert(b.UseResourceCacheWhen, qt.Equals, "always")
@@ -39,7 +39,7 @@ func TestBuild(t *testing.T) {
"useResourceCacheWhen": "foo",
})
b = DecodeBuild(v)
b = DecodeBuildConfig(v)
c.Assert(b.UseResourceCacheWhen, qt.Equals, "fallback")
@@ -91,6 +91,7 @@ status = 301
s, err := DecodeServer(cfg)
c.Assert(err, qt.IsNil)
c.Assert(s.CompileConfig(), qt.IsNil)
c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{
{Key: "X-Content-Type-Options", Value: "nosniff"},

View File

@@ -1,117 +0,0 @@
// Copyright 2021 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 config
import (
"github.com/gohugoio/hugo/common/maps"
)
// NewCompositeConfig creates a new composite Provider with a read-only base
// and a writeable layer.
func NewCompositeConfig(base, layer Provider) Provider {
return &compositeConfig{
base: base,
layer: layer,
}
}
// compositeConfig contains a read only config base with
// a possibly writeable config layer on top.
type compositeConfig struct {
base Provider
layer Provider
}
func (c *compositeConfig) GetBool(key string) bool {
if c.layer.IsSet(key) {
return c.layer.GetBool(key)
}
return c.base.GetBool(key)
}
func (c *compositeConfig) GetInt(key string) int {
if c.layer.IsSet(key) {
return c.layer.GetInt(key)
}
return c.base.GetInt(key)
}
func (c *compositeConfig) Merge(key string, value any) {
c.layer.Merge(key, value)
}
func (c *compositeConfig) GetParams(key string) maps.Params {
if c.layer.IsSet(key) {
return c.layer.GetParams(key)
}
return c.base.GetParams(key)
}
func (c *compositeConfig) GetStringMap(key string) map[string]any {
if c.layer.IsSet(key) {
return c.layer.GetStringMap(key)
}
return c.base.GetStringMap(key)
}
func (c *compositeConfig) GetStringMapString(key string) map[string]string {
if c.layer.IsSet(key) {
return c.layer.GetStringMapString(key)
}
return c.base.GetStringMapString(key)
}
func (c *compositeConfig) GetStringSlice(key string) []string {
if c.layer.IsSet(key) {
return c.layer.GetStringSlice(key)
}
return c.base.GetStringSlice(key)
}
func (c *compositeConfig) Get(key string) any {
if c.layer.IsSet(key) {
return c.layer.Get(key)
}
return c.base.Get(key)
}
func (c *compositeConfig) IsSet(key string) bool {
if c.layer.IsSet(key) {
return true
}
return c.base.IsSet(key)
}
func (c *compositeConfig) GetString(key string) string {
if c.layer.IsSet(key) {
return c.layer.GetString(key)
}
return c.base.GetString(key)
}
func (c *compositeConfig) Set(key string, value any) {
c.layer.Set(key, value)
}
func (c *compositeConfig) SetDefaults(params maps.Params) {
c.layer.SetDefaults(params)
}
func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
panic("not supported")
}
func (c *compositeConfig) SetDefaultMergeStrategy() {
panic("not supported")
}

View File

@@ -1,40 +0,0 @@
// Copyright 2021 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 config
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCompositeConfig(t *testing.T) {
c := qt.New(t)
c.Run("Set and get", func(c *qt.C) {
base, layer := New(), New()
cfg := NewCompositeConfig(base, layer)
layer.Set("a1", "av")
base.Set("b1", "bv")
cfg.Set("c1", "cv")
c.Assert(cfg.Get("a1"), qt.Equals, "av")
c.Assert(cfg.Get("b1"), qt.Equals, "bv")
c.Assert(cfg.Get("c1"), qt.Equals, "cv")
c.Assert(cfg.IsSet("c1"), qt.IsTrue)
c.Assert(layer.IsSet("c1"), qt.IsTrue)
c.Assert(base.IsSet("c1"), qt.IsFalse)
})
}

View File

@@ -57,6 +57,14 @@ func IsValidConfigFilename(filename string) bool {
return validConfigFileExtensionsMap[ext]
}
func FromTOMLConfigString(config string) Provider {
cfg, err := FromConfigString(config, "toml")
if err != nil {
panic(err)
}
return cfg
}
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
func FromConfigString(config, configType string) (Provider, error) {
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))

View File

@@ -14,10 +14,58 @@
package config
import (
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/langs"
)
// AllProvider is a sub set of all config settings.
type AllProvider interface {
Language() *langs.Language
Languages() langs.Languages
LanguagesDefaultFirst() langs.Languages
BaseURL() urls.BaseURL
BaseURLLiveReload() urls.BaseURL
Environment() string
IsMultihost() bool
IsMultiLingual() bool
NoBuildLock() bool
BaseConfig() BaseConfig
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
GetConfigSection(string) any
GetConfig() any
CanonifyURLs() bool
DisablePathToLower() bool
RemovePathAccents() bool
IsUglyURLs(section string) bool
DefaultContentLanguage() string
DefaultContentLanguageInSubdir() bool
IsLangDisabled(string) bool
SummaryLength() int
Paginate() int
PaginatePath() string
BuildExpired() bool
BuildFuture() bool
BuildDrafts() bool
Running() bool
PrintUnusedTemplates() bool
EnableMissingTranslationPlaceholders() bool
TemplateMetrics() bool
TemplateMetricsHints() bool
LogI18nWarnings() bool
CreateTitle(s string) string
IgnoreFile(s string) bool
NewContentEditor() string
Timeout() time.Duration
StaticDirs() []string
IgnoredErrors() map[string]bool
}
// Provider provides the configuration settings for Hugo.
type Provider interface {
GetString(key string) string
@@ -29,10 +77,11 @@ type Provider interface {
GetStringSlice(key string) []string
Get(key string) any
Set(key string, value any)
Keys() []string
Merge(key string, value any)
SetDefaults(params maps.Params)
SetDefaultMergeStrategy()
WalkParams(walkFn func(params ...KeyParams) bool)
WalkParams(walkFn func(params ...maps.KeyParams) bool)
IsSet(key string) bool
}
@@ -44,22 +93,6 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
return types.ToStringSlicePreserveString(sd)
}
// SetBaseTestDefaults provides some common config defaults used in tests.
func SetBaseTestDefaults(cfg Provider) Provider {
setIfNotSet(cfg, "baseURL", "https://example.org")
setIfNotSet(cfg, "resourceDir", "resources")
setIfNotSet(cfg, "contentDir", "content")
setIfNotSet(cfg, "dataDir", "data")
setIfNotSet(cfg, "i18nDir", "i18n")
setIfNotSet(cfg, "layoutDir", "layouts")
setIfNotSet(cfg, "assetDir", "assets")
setIfNotSet(cfg, "archetypeDir", "archetypes")
setIfNotSet(cfg, "publishDir", "public")
setIfNotSet(cfg, "workingDir", "")
setIfNotSet(cfg, "defaultContentLanguage", "en")
return cfg
}
func setIfNotSet(cfg Provider, key string, value any) {
if !cfg.IsSet(key) {
cfg.Set(key, value)

View File

@@ -19,6 +19,8 @@ import (
"strings"
"sync"
xmaps "golang.org/x/exp/maps"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
@@ -75,11 +77,6 @@ func NewFrom(params maps.Params) Provider {
}
}
// NewWithTestDefaults is used in tests only.
func NewWithTestDefaults() Provider {
return SetBaseTestDefaults(New())
}
// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
// All methods are thread safe.
type defaultConfigProvider struct {
@@ -160,9 +157,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
k = strings.ToLower(k)
if k == "" {
if p, ok := maps.ToParamsAndPrepare(v); ok {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
// Set the values directly in root.
c.root.Set(p)
maps.SetParams(c.root, p)
} else {
c.root[k] = v
}
@@ -184,7 +181,7 @@ func (c *defaultConfigProvider) Set(k string, v any) {
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
p1.Set(p2)
maps.SetParams(p1, p2)
return
}
}
@@ -208,12 +205,6 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
defer c.mu.Unlock()
k = strings.ToLower(k)
const (
languagesKey = "languages"
paramsKey = "params"
menusKey = "menus"
)
if k == "" {
rs, f := c.root.GetMergeStrategy()
if f && rs == maps.ParamsMergeStrategyNone {
@@ -222,7 +213,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
return
}
if p, ok := maps.ToParamsAndPrepare(v); ok {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
// As there may be keys in p not in root, we need to handle
// those as a special case.
var keysToDelete []string
@@ -230,49 +221,14 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
if pp, ok := vv.(maps.Params); ok {
if pppi, ok := c.root[kk]; ok {
ppp := pppi.(maps.Params)
if kk == languagesKey {
// Languages is currently a special case.
// We may have languages with menus or params in the
// right map that is not present in the left map.
// With the default merge strategy those items will not
// be passed over.
var hasParams, hasMenus bool
for _, rv := range pp {
if lkp, ok := rv.(maps.Params); ok {
_, hasMenus = lkp[menusKey]
_, hasParams = lkp[paramsKey]
}
}
if hasMenus || hasParams {
for _, lv := range ppp {
if lkp, ok := lv.(maps.Params); ok {
if hasMenus {
if _, ok := lkp[menusKey]; !ok {
p := maps.Params{}
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
lkp[menusKey] = p
}
}
if hasParams {
if _, ok := lkp[paramsKey]; !ok {
p := maps.Params{}
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
lkp[paramsKey] = p
}
}
}
}
}
}
ppp.Merge(pp)
maps.MergeParamsWithStrategy("", ppp, pp)
} else {
// We need to use the default merge strategy for
// this key.
np := make(maps.Params)
strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
np.SetDefaultMergeStrategy(strategy)
np.Merge(pp)
strategy := c.determineMergeStrategy(maps.KeyParams{Key: "", Params: c.root}, maps.KeyParams{Key: kk, Params: np})
np.SetMergeStrategy(strategy)
maps.MergeParamsWithStrategy("", np, pp)
c.root[kk] = np
if np.IsZero() {
// Just keep it until merge is done.
@@ -282,7 +238,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
}
// Merge the rest.
c.root.MergeRoot(p)
maps.MergeParams(c.root, p)
for _, k := range keysToDelete {
delete(c.root, k)
}
@@ -307,7 +263,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
p1.Merge(p2)
maps.MergeParamsWithStrategy("", p1, p2)
}
}
} else {
@@ -315,9 +271,15 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
var walk func(params ...KeyParams)
walk = func(params ...KeyParams) {
func (c *defaultConfigProvider) Keys() []string {
c.mu.RLock()
defer c.mu.RUnlock()
return xmaps.Keys(c.root)
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) {
var walk func(params ...maps.KeyParams)
walk = func(params ...maps.KeyParams) {
if walkFn(params...) {
return
}
@@ -325,17 +287,17 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool
i := len(params)
for k, v := range p1.Params {
if p2, ok := v.(maps.Params); ok {
paramsplus1 := make([]KeyParams, i+1)
paramsplus1 := make([]maps.KeyParams, i+1)
copy(paramsplus1, params)
paramsplus1[i] = KeyParams{Key: k, Params: p2}
paramsplus1[i] = maps.KeyParams{Key: k, Params: p2}
walk(paramsplus1...)
}
}
}
walk(KeyParams{Key: "", Params: c.root})
walk(maps.KeyParams{Key: "", Params: c.root})
}
func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy {
if len(params) == 0 {
return maps.ParamsMergeStrategyNone
}
@@ -391,13 +353,8 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps
return strategy
}
type KeyParams struct {
Key string
Params maps.Params
}
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
c.WalkParams(func(params ...KeyParams) bool {
c.WalkParams(func(params ...maps.KeyParams) bool {
if len(params) == 0 {
return false
}
@@ -409,7 +366,7 @@ func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
}
strategy := c.determineMergeStrategy(params...)
if strategy != "" {
p.SetDefaultMergeStrategy(strategy)
p.SetMergeStrategy(strategy)
}
return false
})

76
config/namespace.go Normal file
View File

@@ -0,0 +1,76 @@
// 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 config
import (
"encoding/json"
"github.com/gohugoio/hugo/identity"
)
func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) {
// Calculate the hash of the input (not including any defaults applied later).
// This allows us to introduce new config options without breaking the hash.
h := identity.HashString(configSource)
// Build the config
c, ext, err := buildConfig(configSource)
if err != nil {
return nil, err
}
if ext == nil {
ext = configSource
}
if ext == nil {
panic("ext is nil")
}
ns := &ConfigNamespace[S, C]{
SourceStructure: ext,
SourceHash: h,
Config: c,
}
return ns, nil
}
// ConfigNamespace holds a Hugo configuration namespace.
// The construct looks a little odd, but it's built to make the configuration elements
// both self-documenting and contained in a common structure.
type ConfigNamespace[S, C any] struct {
// SourceStructure represents the source configuration with any defaults applied.
// This is used for documentation and printing of the configuration setup to the user.
SourceStructure any
// SourceHash is a hash of the source configuration before any defaults gets applied.
SourceHash string
// Config is the final configuration as used by Hugo.
Config C
}
// MarshalJSON marshals the source structure.
func (ns *ConfigNamespace[S, C]) MarshalJSON() ([]byte, error) {
return json.Marshal(ns.SourceStructure)
}
// Signature returns the signature of the source structure.
// Note that this is for documentation purposes only and SourceStructure may not always be cast to S (it's usually just a map).
func (ns *ConfigNamespace[S, C]) Signature() S {
var s S
return s
}

68
config/namespace_test.go Normal file
View File

@@ -0,0 +1,68 @@
// 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 config
import (
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/maps"
"github.com/mitchellh/mapstructure"
)
func TestNamespace(t *testing.T) {
c := qt.New(t)
c.Assert(true, qt.Equals, true)
//ns, err := config.DecodeNamespace[map[string]DocsMediaTypeConfig](in, defaultMediaTypesConfig, buildConfig)
ns, err := DecodeNamespace[[]*tstNsExt](
map[string]interface{}{"foo": "bar"},
func(v any) (*tstNsExt, any, error) {
t := &tstNsExt{}
m, err := maps.ToStringMapE(v)
if err != nil {
return nil, nil, err
}
return t, nil, mapstructure.WeakDecode(m, t)
},
)
c.Assert(err, qt.IsNil)
c.Assert(ns, qt.Not(qt.IsNil))
c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"})
c.Assert(ns.SourceHash, qt.Equals, "14368731254619220105")
c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"})
c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
}
type (
tstNsExt struct {
Foo string
}
tstNsInt struct {
Foo string
}
)
func (t *tstNsExt) Init() error {
t.Foo = strings.ToUpper(t.Foo)
return nil
}
func (t *tstNsInt) Compile(ext *tstNsExt) error {
t.Foo = ext.Foo + " qux"
return nil
}

View File

@@ -54,14 +54,16 @@ var DefaultConfig = Config{
}
// Config is the top level security config.
// <docsmeta>{"name": "security", "description": "This section holds the top level security config.", "newIn": "0.91.0" }</docsmeta>
type Config struct {
// Restricts access to os.Exec.
// Restricts access to os.Exec....
// <docsmeta>{ "newIn": "0.91.0" }</docsmeta>
Exec Exec `json:"exec"`
// Restricts access to certain template funcs.
Funcs Funcs `json:"funcs"`
// Restricts access to resources.Get, getJSON, getCSV.
// Restricts access to resources.GetRemote, getJSON, getCSV.
HTTP HTTP `json:"http"`
// Allow inline shortcodes

View File

@@ -54,7 +54,7 @@ disableInlineCSS = true
func TestUseSettingsFromRootIfSet(t *testing.T) {
c := qt.New(t)
cfg := config.NewWithTestDefaults()
cfg := config.New()
cfg.Set("disqusShortname", "root_short")
cfg.Set("googleAnalytics", "ga_root")

View File

@@ -0,0 +1,84 @@
// 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.
// This package should only be used for testing.
package testconfig
import (
_ "unsafe"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
toml "github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
)
func GetTestConfigs(fs afero.Fs, cfg config.Provider) *allconfig.Configs {
if fs == nil {
fs = afero.NewMemMapFs()
}
if cfg == nil {
cfg = config.New()
}
// Make sure that the workingDir exists.
workingDir := cfg.GetString("workingDir")
if workingDir != "" {
if err := fs.MkdirAll(workingDir, 0777); err != nil {
panic(err)
}
}
configs, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs, Flags: cfg})
if err != nil {
panic(err)
}
return configs
}
func GetTestConfig(fs afero.Fs, cfg config.Provider) config.AllProvider {
return GetTestConfigs(fs, cfg).GetFirstLanguageConfig()
}
func GetTestDeps(fs afero.Fs, cfg config.Provider, beforeInit ...func(*deps.Deps)) *deps.Deps {
if fs == nil {
fs = afero.NewMemMapFs()
}
conf := GetTestConfig(fs, cfg)
d := &deps.Deps{
Conf: conf,
Fs: hugofs.NewFrom(fs, conf.BaseConfig()),
}
for _, f := range beforeInit {
f(d)
}
if err := d.Init(); err != nil {
panic(err)
}
return d
}
func GetTestConfigSectionFromStruct(section string, v any) config.AllProvider {
data, err := toml.Marshal(v)
if err != nil {
panic(err)
}
p := maps.Params{
section: config.FromTOMLConfigString(string(data)).Get(""),
}
cfg := config.NewFrom(p)
return GetTestConfig(nil, cfg)
}