diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-01-04 18:24:36 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-05-16 18:01:29 +0200 |
commit | 241b21b0fd34d91fccb2ce69874110dceae6f926 (patch) | |
tree | d4e0118eac7e9c42f065815447a70805f8d6ad3e /hugolib/config.go | |
parent | 6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff) |
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
Diffstat (limited to 'hugolib/config.go')
-rw-r--r-- | hugolib/config.go | 670 |
1 files changed, 157 insertions, 513 deletions
diff --git a/hugolib/config.go b/hugolib/config.go index 059424e85..af3f0647f 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -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 - } - - // 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 - } - - _, modulesConfigFiles, modulesCollectErr := l.collectModules(modulesConfig, l.cfg, collectHook) + 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 + } + + if err := afero.WriteFile(fs, filepath.Join(tempDir, "go.mod"), []byte(goMod), 0644); err != nil { + return nil, err + } + + conf, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs, Flags: cfg}) if err != nil { - return l.cfg, configFiles, 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 nil, err } - 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 -} + return conf.Base, err -// 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) } |