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

@@ -16,526 +16,170 @@ package hugolib
import (
"os"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/maps"
cpaths "github.com/gohugoio/hugo/common/paths"
"github.com/gobwas/glob"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/parser/metadecoders"
"errors"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/modules"
"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/helpers"
"github.com/gohugoio/hugo/config/allconfig"
"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")
// LoadConfig loads Hugo configuration into a new Viper and then adds
// a set of defaults.
func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (config.Provider, []string, error) {
if d.Environment == "" {
d.Environment = hugo.EnvironmentProduction
}
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
d.Environ = os.Environ()
}
var configFiles []string
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()
names := d.configFilenames()
if names != nil {
for _, name := range names {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
configFiles = append(configFiles, filename)
} else if err != ErrNoConfigFile {
return nil, nil, l.wrapFileError(err, filename)
}
}
} else {
for _, name := range config.DefaultConfigNames {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
configFiles = append(configFiles, filename)
break
} else if err != ErrNoConfigFile {
return nil, nil, l.wrapFileError(err, filename)
}
}
}
if d.AbsConfigDir != "" {
dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, d.AbsConfigDir, l.Environment)
if err == nil {
if len(dirnames) > 0 {
l.cfg.Set("", dcfg.Get(""))
configFiles = append(configFiles, dirnames...)
}
} else if err != ErrNoConfigFile {
if len(dirnames) > 0 {
return nil, nil, l.wrapFileError(err, dirnames[0])
}
return nil, nil, err
}
}
if err := l.applyConfigDefaults(); err != nil {
return l.cfg, configFiles, err
}
l.cfg.SetDefaultMergeStrategy()
// We create languages based on the settings, so we need to make sure that
// all configuration is loaded/set before doing that.
for _, d := range doWithConfig {
if err := d(l.cfg); err != nil {
return l.cfg, configFiles, 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 l.cfg, configFiles, err
}
modulesConfig, err := l.loadModulesConfig()
// DefaultConfig returns the default configuration.
func DefaultConfig() *allconfig.Config {
fs := afero.NewMemMapFs()
all, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs})
if err != nil {
return l.cfg, configFiles, err
panic(err)
}
return all.Base
}
// ExampleConfig returns the some example configuration for documentation.
func ExampleConfig() (*allconfig.Config, error) {
// Apply some example settings for the settings that does not come with a sensible default.
configToml := `
title = 'My Blog'
baseURL = "https://example.com/"
disableKinds = ["term", "taxonomy"]
[outputs]
home = ['html', 'html', 'rss']
page = ['html']
[imaging]
bgcolor = '#ffffff'
hint = 'photo'
quality = 81
resamplefilter = 'CatmullRom'
[imaging.exif]
disableDate = true
disableLatLong = true
excludeFields = 'ColorSpace|Metering'
[params]
color = 'blue'
style = 'dark'
[languages]
[languages.ar]
languagedirection = 'rtl'
title = 'مدونتي'
weight = 2
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.fr.params]
linkedin = 'https://linkedin.com/fr/whoever'
color = 'green'
[[languages.fr.menus.main]]
name = 'Des produits'
pageRef = '/products'
weight = 20
[menus]
[[menus.main]]
name = 'Home'
pageRef = '/'
weight = 10
[[menus.main]]
name = 'Products'
pageRef = '/products'
weight = 20
[[menus.main]]
name = 'Services'
pageRef = '/services'
weight = 30
[deployment]
order = [".jpg$", ".gif$"]
[[deployment.targets]]
name = "mydeployment"
url = "s3://mybucket?region=us-east-1"
cloudFrontDistributionID = "mydistributionid"
[[deployment.matchers]]
pattern = "^.+\\.(js|css|svg|ttf)$"
cacheControl = "max-age=31536000, no-transform, public"
gzip = true
[[deployment.matchers]]
pattern = "^.+\\.(png|jpg)$"
cacheControl = "max-age=31536000, no-transform, public"
gzip = false
[[deployment.matchers]]
pattern = "^sitemap\\.xml$"
contentType = "application/xml"
gzip = true
[[deployment.matchers]]
pattern = "^.+\\.(html|xml|json)$"
gzip = true
[permalinks]
posts = '/posts/:year/:month/:title/'
[taxonomies]
category = 'categories'
series = 'series'
tag = 'tags'
[module]
[module.hugoVersion]
min = '0.80.0'
[[module.imports]]
path = "github.com/bep/hugo-mod-misc/dummy-content"
ignoreconfig = true
ignoreimports = true
[[module.mounts]]
source = "content/blog"
target = "content"
[minify]
[minify.tdewolff]
[minify.tdewolff.json]
precision = 2
[[cascade]]
background = 'yosemite.jpg'
[cascade._target]
kind = 'page'
path = '/blog/**'
[[cascade]]
background = 'goldenbridge.jpg'
[cascade._target]
kind = 'section'
`
goMod := `
module github.com/bep/mymod
`
cfg := config.New()
tempDir := os.TempDir()
cacheDir := filepath.Join(tempDir, "hugocache")
if err := os.MkdirAll(cacheDir, 0777); err != nil {
return nil, err
}
cfg.Set("cacheDir", cacheDir)
cfg.Set("workingDir", tempDir)
defer func() {
os.RemoveAll(tempDir)
}()
fs := afero.NewOsFs()
if err := afero.WriteFile(fs, filepath.Join(tempDir, "hugo.toml"), []byte(configToml), 0644); err != nil {
return nil, err
}
// Need to run these after the modules are loaded, but before
// they are finalized.
collectHook := func(m *modules.ModulesConfig) error {
// We don't need the merge strategy configuration anymore,
// remove it so it doesn't accidentally show up in other settings.
l.deleteMergeStrategies()
if err := l.loadLanguageSettings(nil); err != nil {
return err
}
mods := m.ActiveModules
// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(l.cfg, mods[0]); err != nil {
return err
}
return nil
if err := afero.WriteFile(fs, filepath.Join(tempDir, "go.mod"), []byte(goMod), 0644); err != nil {
return nil, err
}
_, modulesConfigFiles, modulesCollectErr := l.collectModules(modulesConfig, l.cfg, collectHook)
conf, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs, Flags: cfg})
if err != nil {
return l.cfg, configFiles, err
return nil, err
}
return conf.Base, err
configFiles = append(configFiles, modulesConfigFiles...)
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return l.cfg, configFiles, err
}
if err = l.applyConfigAliases(); err != nil {
return l.cfg, configFiles, err
}
if err == nil {
err = modulesCollectErr
}
return l.cfg, configFiles, err
}
// LoadConfigDefault is a convenience method to load the default "hugo.toml" config.
func LoadConfigDefault(fs afero.Fs) (config.Provider, error) {
v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs})
return v, err
}
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
type ConfigSourceDescriptor struct {
Fs afero.Fs
Logger loggers.Logger
// Path to the config file to use, e.g. /my/project/config.toml
Filename string
// The path to the directory to look for configuration. Is used if Filename is not
// set or if it is set to a relative filename.
Path string
// The project's working dir. Is used to look for additional theme config.
WorkingDir string
// The (optional) directory for additional configuration files.
AbsConfigDir string
// production, development
Environment string
// Defaults to os.Environ if not set.
Environ []string
}
func (d ConfigSourceDescriptor) configFileDir() string {
if d.Path != "" {
return d.Path
}
return d.WorkingDir
}
func (d ConfigSourceDescriptor) configFilenames() []string {
if d.Filename == "" {
return nil
}
return strings.Split(d.Filename, ",")
}
// SiteConfig represents the config in .Site.Config.
type SiteConfig struct {
// This contains all privacy related settings that can be used to
// make the YouTube template etc. GDPR compliant.
Privacy privacy.Config
// Services contains config for services such as Google Analytics etc.
Services services.Config
}
type configLoader struct {
cfg config.Provider
ConfigSourceDescriptor
}
// 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) applyConfigDefaults() error {
defaultSettings := maps.Params{
"cleanDestinationDir": false,
"watch": false,
"resourceDir": "resources",
"publishDir": "public",
"publishDirOrig": "public",
"themesDir": "themes",
"buildDrafts": false,
"buildFuture": false,
"buildExpired": false,
"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"},
"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",
"enableInlineShortcodes": false,
}
l.cfg.SetDefaults(defaultSettings)
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) collectModules(modConfig modules.Config, v1 config.Provider, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) {
workingDir := l.WorkingDir
if workingDir == "" {
workingDir = v1.GetString("workingDir")
}
themesDir := cpaths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
var ignoreVendor glob.Glob
if s := v1.GetString("ignoreVendorPaths"); s != "" {
ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
}
filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1)
if err != nil {
return nil, nil, err
}
secConfig, err := security.DecodeConfig(v1)
if err != nil {
return nil, nil, err
}
ex := hexec.New(secConfig)
v1.Set("filecacheConfigs", filecacheConfigs)
var configFilenames []string
hook := func(m *modules.ModulesConfig) error {
for _, tc := range m.ActiveModules {
if len(tc.ConfigFilenames()) > 0 {
if tc.Watch() {
configFilenames = append(configFilenames, tc.ConfigFilenames()...)
}
// Merge from theme config into v1 based on configured
// merge strategy.
v1.Merge("", tc.Cfg().Get(""))
}
}
if hookBeforeFinalize != nil {
return hookBeforeFinalize(m)
}
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: filecacheConfigs.CacheDirModules(),
ModuleConfig: modConfig,
IgnoreVendor: ignoreVendor,
})
v1.Set("modulesClient", modulesClient)
moduleConfig, err := modulesClient.Collect()
// Avoid recreating these later.
v1.Set("allModules", moduleConfig.ActiveModules)
// We want to watch these for changes and trigger rebuild on version
// changes etc.
if moduleConfig.GoModulesFilename != "" {
configFilenames = append(configFilenames, moduleConfig.GoModulesFilename)
}
if moduleConfig.GoWorkspaceFilename != "" {
configFilenames = append(configFilenames, moduleConfig.GoWorkspaceFilename)
}
return moduleConfig.ActiveModules, configFilenames, err
}
func (l configLoader) loadConfig(configName string) (string, error) {
baseDir := l.configFileDir()
var baseFilename string
if filepath.IsAbs(configName) {
baseFilename = configName
} else {
baseFilename = filepath.Join(baseDir, configName)
}
var filename string
if cpaths.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)
return filename, nil
}
func (l configLoader) deleteMergeStrategies() {
l.cfg.WalkParams(func(params ...config.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
}
func (l configLoader) loadLanguageSettings(oldLangs langs.Languages) error {
_, err := langs.LoadLanguageSettings(l.cfg, oldLangs)
return err
}
func (l configLoader) loadModulesConfig() (modules.Config, error) {
modConfig, err := modules.DecodeConfig(l.cfg)
if err != nil {
return modules.Config{}, err
}
return modConfig, nil
}
func (configLoader) loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
privacyConfig, err := privacy.DecodeConfig(cfg)
if err != nil {
return
}
servicesConfig, err := services.DecodeConfig(cfg)
if err != nil {
return
}
scfg.Privacy = privacyConfig
scfg.Services = servicesConfig
return
}
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)
}