From 241b21b0fd34d91fccb2ce69874110dceae6f926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 4 Jan 2023 18:24:36 +0100 Subject: 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 --- hugolib/alias.go | 2 +- hugolib/breaking_changes_test.go | 118 +---- hugolib/cascade_test.go | 54 +- hugolib/codeowners.go | 5 +- hugolib/config.go | 670 ++++++------------------- hugolib/config_test.go | 577 +++++++++------------- hugolib/configdir_test.go | 153 ++---- hugolib/content_map.go | 2 +- hugolib/content_map_page.go | 10 +- hugolib/datafiles_test.go | 444 ++--------------- hugolib/dates_test.go | 2 +- hugolib/embedded_shortcodes_test.go | 422 ++-------------- hugolib/filesystems/basefs.go | 24 +- hugolib/filesystems/basefs_test.go | 231 ++++----- hugolib/gitinfo.go | 4 +- hugolib/hugo_modules_test.go | 35 +- hugolib/hugo_sites.go | 343 +------------ hugolib/hugo_sites_build.go | 100 ++-- hugolib/hugo_sites_build_errors_test.go | 1 + hugolib/hugo_sites_build_test.go | 19 +- hugolib/hugo_sites_multihost_test.go | 2 + hugolib/hugo_smoke_test.go | 26 +- hugolib/integrationtest_builder.go | 67 ++- hugolib/language_content_dir_test.go | 2 +- hugolib/menu_test.go | 33 ++ hugolib/minify_publisher_test.go | 2 +- hugolib/multilingual.go | 82 ---- hugolib/page.go | 17 +- hugolib/page__common.go | 6 +- hugolib/page__meta.go | 56 +-- hugolib/page__new.go | 5 +- hugolib/page__paginator.go | 7 +- hugolib/page__paths.go | 4 +- hugolib/page__per_output.go | 2 +- hugolib/page_kinds.go | 4 +- hugolib/page_permalink_test.go | 35 +- hugolib/page_test.go | 170 +++---- hugolib/pagebundler_test.go | 51 +- hugolib/pagecollections_test.go | 21 +- hugolib/pages_capture.go | 5 +- hugolib/pages_capture_test.go | 27 +- hugolib/pages_process.go | 3 +- hugolib/paths/baseURL.go | 87 ---- hugolib/paths/baseURL_test.go | 67 --- hugolib/paths/paths.go | 173 ++----- hugolib/paths/paths_test.go | 50 -- hugolib/prune_resources.go | 2 +- hugolib/robotstxt_test.go | 2 +- hugolib/rss_test.go | 12 +- hugolib/shortcode_test.go | 5 +- hugolib/site.go | 847 +++----------------------------- hugolib/site_benchmark_new_test.go | 8 +- hugolib/site_new.go | 458 +++++++++++++++++ hugolib/site_output_test.go | 36 +- hugolib/site_render.go | 27 +- hugolib/site_sections.go | 4 +- hugolib/site_sections_test.go | 7 +- hugolib/site_test.go | 204 ++++++-- hugolib/site_url_test.go | 45 +- hugolib/sitemap_test.go | 31 +- hugolib/taxonomy_test.go | 7 +- hugolib/template_test.go | 15 +- hugolib/testhelpers_test.go | 123 +++-- 63 files changed, 1913 insertions(+), 4140 deletions(-) delete mode 100644 hugolib/multilingual.go delete mode 100644 hugolib/paths/baseURL.go delete mode 100644 hugolib/paths/baseURL_test.go delete mode 100644 hugolib/paths/paths_test.go create mode 100644 hugolib/site_new.go (limited to 'hugolib') diff --git a/hugolib/alias.go b/hugolib/alias.go index 071f73d41..1bc0e5424 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -101,7 +101,7 @@ func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFo OutputFormat: outputFormat, } - if s.Info.relativeURLs || s.Info.canonifyURLs { + if s.conf.RelativeURLs || s.conf.CanonifyURLs { pd.AbsURLPath = s.absURLPath(targetPath) } diff --git a/hugolib/breaking_changes_test.go b/hugolib/breaking_changes_test.go index 495baff3e..533205deb 100644 --- a/hugolib/breaking_changes_test.go +++ b/hugolib/breaking_changes_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. +// 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. @@ -12,119 +12,3 @@ // limitations under the License. package hugolib - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" -) - -func Test073(t *testing.T) { - assertDisabledTaxonomyAndTerm := func(b *sitesBuilder, taxonomy, term bool) { - b.Assert(b.CheckExists("public/tags/index.html"), qt.Equals, taxonomy) - b.Assert(b.CheckExists("public/tags/tag1/index.html"), qt.Equals, term) - } - - assertOutputTaxonomyAndTerm := func(b *sitesBuilder, taxonomy, term bool) { - b.Assert(b.CheckExists("public/tags/index.json"), qt.Equals, taxonomy) - b.Assert(b.CheckExists("public/tags/tag1/index.json"), qt.Equals, term) - } - - for _, this := range []struct { - name string - config string - assert func(err error, out string, b *sitesBuilder) - }{ - { - "Outputs for both taxonomy and taxonomyTerm", - `[outputs] - taxonomy = ["JSON"] - taxonomyTerm = ["JSON"] - -`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.IsNil) - assertOutputTaxonomyAndTerm(b, true, true) - }, - }, - { - "Outputs for taxonomyTerm", - `[outputs] -taxonomyTerm = ["JSON"] - -`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.IsNil) - assertOutputTaxonomyAndTerm(b, true, false) - }, - }, - { - "Outputs for taxonomy only", - `[outputs] -taxonomy = ["JSON"] - -`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(out, qt.Contains, `ignoreErrors = ["error-output-taxonomy"]`) - }, - }, - { - "Outputs for taxonomy only, ignore error", - ` -ignoreErrors = ["error-output-taxonomy"] -[outputs] -taxonomy = ["JSON"] - -`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.IsNil) - assertOutputTaxonomyAndTerm(b, true, false) - }, - }, - { - "Disable both taxonomy and taxonomyTerm", - `disableKinds = ["taxonomy", "taxonomyTerm"]`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.IsNil) - assertDisabledTaxonomyAndTerm(b, false, false) - }, - }, - { - "Disable only taxonomyTerm", - `disableKinds = ["taxonomyTerm"]`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.IsNil) - assertDisabledTaxonomyAndTerm(b, false, true) - }, - }, - { - "Disable only taxonomy", - `disableKinds = ["taxonomy"]`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(out, qt.Contains, `ignoreErrors = ["error-disable-taxonomy"]`) - }, - }, - { - "Disable only taxonomy, ignore error", - `disableKinds = ["taxonomy"] - ignoreErrors = ["error-disable-taxonomy"]`, - func(err error, out string, b *sitesBuilder) { - b.Assert(err, qt.IsNil) - assertDisabledTaxonomyAndTerm(b, false, true) - }, - }, - } { - t.Run(this.name, func(t *testing.T) { - b := newTestSitesBuilder(t).WithConfigFile("toml", this.config) - b.WithTemplatesAdded("_default/list.json", "JSON") - out, err := captureStdout(func() error { - return b.BuildE(BuildCfg{}) - }) - fmt.Println(out) - this.assert(err, out, b) - }) - } -} diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index dff2082b6..0f607ecb5 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -159,33 +159,33 @@ func TestCascade(t *testing.T) { b.Build(BuildCfg{}) b.AssertFileContent("public/index.html", ` -12|term|categories/cool/_index.md|Cascade Category|cat.png|categories|HTML-| -12|term|categories/catsect1|catsect1|cat.png|categories|HTML-| -12|term|categories/funny|funny|cat.png|categories|HTML-| -12|taxonomy|categories/_index.md|My Categories|cat.png|categories|HTML-| -32|term|categories/sad/_index.md|Cascade Category|sad.png|categories|HTML-| -42|term|tags/blue|blue|home.png|tags|HTML-| -42|taxonomy|tags|Cascade Home|home.png|tags|HTML-| -42|section|sectnocontent|Cascade Home|home.png|sectnocontent|HTML-| -42|section|sect3|Cascade Home|home.png|sect3|HTML-| -42|page|bundle1/index.md|Cascade Home|home.png|page|HTML-| -42|page|p2.md|Cascade Home|home.png|page|HTML-| -42|page|sect2/p2.md|Cascade Home|home.png|sect2|HTML-| -42|page|sect3/nofrontmatter.md|Cascade Home|home.png|sect3|HTML-| -42|page|sect3/p1.md|Cascade Home|home.png|sect3|HTML-| -42|page|sectnocontent/p1.md|Cascade Home|home.png|sectnocontent|HTML-| -42|section|sectnofrontmatter/_index.md|Cascade Home|home.png|sectnofrontmatter|HTML-| -42|term|tags/green|green|home.png|tags|HTML-| -42|home|_index.md|Home|home.png|page|HTML-| -42|page|p1.md|p1|home.png|page|HTML-| -42|section|sect1/_index.md|Sect1|sect1.png|stype|HTML-| -42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|HTML-| -42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|HTML-| -42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|HTML-| -42|section|sect2/_index.md|Sect2|home.png|sect2|HTML-| -42|page|sect2/p1.md|Sect2_p1|home.png|sect2|HTML-| -52|page|sect4/p1.md|Cascade Home|home.png|sect4|RSS-| -52|section|sect4/_index.md|Sect4|home.png|sect4|RSS-| +12|term|categories/cool/_index.md|Cascade Category|cat.png|categories|html-| +12|term|categories/catsect1|catsect1|cat.png|categories|html-| +12|term|categories/funny|funny|cat.png|categories|html-| +12|taxonomy|categories/_index.md|My Categories|cat.png|categories|html-| +32|term|categories/sad/_index.md|Cascade Category|sad.png|categories|html-| +42|term|tags/blue|blue|home.png|tags|html-| +42|taxonomy|tags|Cascade Home|home.png|tags|html-| +42|section|sectnocontent|Cascade Home|home.png|sectnocontent|html-| +42|section|sect3|Cascade Home|home.png|sect3|html-| +42|page|bundle1/index.md|Cascade Home|home.png|page|html-| +42|page|p2.md|Cascade Home|home.png|page|html-| +42|page|sect2/p2.md|Cascade Home|home.png|sect2|html-| +42|page|sect3/nofrontmatter.md|Cascade Home|home.png|sect3|html-| +42|page|sect3/p1.md|Cascade Home|home.png|sect3|html-| +42|page|sectnocontent/p1.md|Cascade Home|home.png|sectnocontent|html-| +42|section|sectnofrontmatter/_index.md|Cascade Home|home.png|sectnofrontmatter|html-| +42|term|tags/green|green|home.png|tags|html-| +42|home|_index.md|Home|home.png|page|html-| +42|page|p1.md|p1|home.png|page|html-| +42|section|sect1/_index.md|Sect1|sect1.png|stype|html-| +42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|html-| +42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|html-| +42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|html-| +42|section|sect2/_index.md|Sect2|home.png|sect2|html-| +42|page|sect2/p1.md|Sect2_p1|home.png|sect2|html-| +52|page|sect4/p1.md|Cascade Home|home.png|sect4|rss-| +52|section|sect4/_index.md|Sect4|home.png|sect4|rss-| `) // Check that type set in cascade gets the correct layout. diff --git a/hugolib/codeowners.go b/hugolib/codeowners.go index 162ee16ae..c1a6a2b7b 100644 --- a/hugolib/codeowners.go +++ b/hugolib/codeowners.go @@ -18,7 +18,6 @@ import ( "path" "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/resources/page" "github.com/hairyhenderson/go-codeowners" "github.com/spf13/afero" @@ -52,9 +51,7 @@ func (c *codeownerInfo) forPage(p page.Page) []string { return c.owners.Owners(p.File().Filename()) } -func newCodeOwners(cfg config.Provider) (*codeownerInfo, error) { - workingDir := cfg.GetString("workingDir") - +func newCodeOwners(workingDir string) (*codeownerInfo, error) { r, err := findCodeOwnersFile(workingDir) if err != nil || r == nil { return nil, err 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) } diff --git a/hugolib/config_test.go b/hugolib/config_test.go index 37605b4c2..169674acb 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -21,59 +21,241 @@ import ( "testing" "github.com/gohugoio/hugo/config" - - "github.com/gohugoio/hugo/media" - "github.com/google/go-cmp/cmp" + "github.com/gohugoio/hugo/config/allconfig" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/common/maps" "github.com/spf13/afero" ) +func TestLoadConfigLanguageParamsOverrideIssue10620(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "setion"] +title = "Base Title" +staticDir = "mystatic" +[params] +[params.comments] +color = "blue" +title = "Default Comments Title" +[languages] +[languages.en] +title = "English Title" +[languages.en.params.comments] +title = "English Comments Title" + + + +` + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + enSite := b.H.Sites[0] + b.Assert(enSite.Title(), qt.Equals, "English Title") + b.Assert(enSite.Home().Title(), qt.Equals, "English Title") + b.Assert(enSite.Params(), qt.DeepEquals, maps.Params{ + "comments": maps.Params{ + "color": "blue", + "title": "English Comments Title", + }, + }, + ) + +} + func TestLoadConfig(t *testing.T) { - c := qt.New(t) + t.Run("2 languages", func(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "setion"] +title = "Base Title" +staticDir = "mystatic" +[params] +p1 = "p1base" +p2 = "p2base" +[languages] +[languages.en] +title = "English Title" +[languages.en.params] +myparam = "enParamValue" +p1 = "p1en" +weight = 1 +[languages.sv] +title = "Svensk Title" +staticDir = "mysvstatic" +weight = 2 +[languages.sv.params] +myparam = "svParamValue" - loadConfig := func(c *qt.C, configContent string, fromDir bool) config.Provider { - mm := afero.NewMemMapFs() - filename := "config.toml" - descriptor := ConfigSourceDescriptor{Fs: mm} - if fromDir { - filename = filepath.Join("config", "_default", filename) - descriptor.AbsConfigDir = "config" - } - writeToFs(t, mm, filename, configContent) - cfg, _, err := LoadConfig(descriptor) - c.Assert(err, qt.IsNil) - return cfg - } - c.Run("Basic", func(c *qt.C) { - c.Parallel() - // Add a random config variable for testing. - // side = page in Norwegian. - cfg := loadConfig(c, `PaginatePath = "side"`, false) - c.Assert(cfg.GetString("paginatePath"), qt.Equals, "side") +` + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + enSite := b.H.Sites[0] + svSite := b.H.Sites[1] + b.Assert(enSite.Title(), qt.Equals, "English Title") + b.Assert(enSite.Home().Title(), qt.Equals, "English Title") + b.Assert(enSite.Params()["myparam"], qt.Equals, "enParamValue") + b.Assert(enSite.Params()["p1"], qt.Equals, "p1en") + b.Assert(enSite.Params()["p2"], qt.Equals, "p2base") + b.Assert(svSite.Params()["p1"], qt.Equals, "p1base") + b.Assert(enSite.conf.StaticDir[0], qt.Equals, "mystatic") + + b.Assert(svSite.Title(), qt.Equals, "Svensk Title") + b.Assert(svSite.Home().Title(), qt.Equals, "Svensk Title") + b.Assert(svSite.Params()["myparam"], qt.Equals, "svParamValue") + b.Assert(svSite.conf.StaticDir[0], qt.Equals, "mysvstatic") + }) - // Issue #8763 - for _, fromDir := range []bool{false, true} { - testName := "Taxonomy overrides" - if fromDir { - testName += " from dir" - } - c.Run(testName, func(c *qt.C) { - c.Parallel() - cfg := loadConfig(c, `[taxonomies] -appellation = "appellations" -vigneron = "vignerons"`, fromDir) - - c.Assert(cfg.Get("taxonomies"), qt.DeepEquals, maps.Params{ - "appellation": "appellations", - "vigneron": "vignerons", - }) - }) - } + t.Run("disable default language", func(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "setion"] +title = "Base Title" +defaultContentLanguage = "sv" +disableLanguages = ["sv"] +[languages.en] +weight = 1 +[languages.sv] +weight = 2 +` + b, err := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).BuildE() + + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, "cannot disable default content language") + + }) + + t.Run("no internal config from outside", func(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +[internal] +running = true +` + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + b.Assert(b.H.Conf.Running(), qt.Equals, false) + + }) + + t.Run("env overrides", func(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "setion"] +title = "Base Title" +[params] +p1 = "p1base" +p2 = "p2base" +[params.pm2] +pm21 = "pm21base" +pm22 = "pm22base" +-- layouts/index.html -- +p1: {{ .Site.Params.p1 }} +p2: {{ .Site.Params.p2 }} +pm21: {{ .Site.Params.pm2.pm21 }} +pm22: {{ .Site.Params.pm2.pm22 }} +pm31: {{ .Site.Params.pm3.pm31 }} + + + +` + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + Environ: []string{"HUGO_PARAMS_P2=p2env", "HUGO_PARAMS_PM2_PM21=pm21env", "HUGO_PARAMS_PM3_PM31=pm31env"}, + }, + ).Build() + + b.AssertFileContent("public/index.html", "p1: p1base\np2: p2env\npm21: pm21env\npm22: pm22base\npm31: pm31env") + + }) + +} + +func TestLoadConfigThemeLanguage(t *testing.T) { + t.Parallel() + + files := ` +-- /hugo.toml -- +baseURL = "https://example.com" +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +theme = "mytheme" +[languages] +[languages.en] +title = "English Title" +weight = 1 +[languages.sv] +weight = 2 +-- themes/mytheme/hugo.toml -- +[params] +p1 = "p1base" +[languages] +[languages.en] +title = "English Title Theme" +[languages.en.params] +p2 = "p2en" +[languages.en.params.sub] +sub1 = "sub1en" +[languages.sv] +title = "Svensk Title Theme" +-- layouts/index.html -- +title: {{ .Title }}| +p1: {{ .Site.Params.p1 }}| +p2: {{ .Site.Params.p2 }}| +sub: {{ .Site.Params.sub }}| +` + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() + + b.AssertFileContent("public/en/index.html", ` +title: English Title| +p1: p1base +p2: p2en +sub: map[sub1:sub1en] +`) + } func TestLoadMultiConfig(t *testing.T) { @@ -84,7 +266,7 @@ func TestLoadMultiConfig(t *testing.T) { // Add a random config variable for testing. // side = page in Norwegian. configContentBase := ` - DontChange = "same" + Paginate = 32 PaginatePath = "side" ` configContentSub := ` @@ -96,11 +278,13 @@ func TestLoadMultiConfig(t *testing.T) { writeToFs(t, mm, "override.toml", configContentSub) - cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "base.toml,override.toml"}) + all, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: mm, Filename: "base.toml,override.toml"}) c.Assert(err, qt.IsNil) + cfg := all.Base + + c.Assert(cfg.PaginatePath, qt.Equals, "top") + c.Assert(cfg.Paginate, qt.Equals, 32) - c.Assert(cfg.GetString("paginatePath"), qt.Equals, "top") - c.Assert(cfg.GetString("DontChange"), qt.Equals, "same") } func TestLoadConfigFromThemes(t *testing.T) { @@ -229,12 +413,9 @@ name = "menu-theme" c.Run("Merge default", func(c *qt.C) { b := buildForStrategy(c, "") - got := b.Cfg.Get("").(maps.Params) - - // Issue #8866 - b.Assert(b.Cfg.Get("disableKinds"), qt.IsNil) + got := b.Configs.Base - b.Assert(got["params"], qt.DeepEquals, maps.Params{ + b.Assert(got.Params, qt.DeepEquals, maps.Params{ "b": maps.Params{ "b1": "b1 main", "c": maps.Params{ @@ -248,100 +429,16 @@ name = "menu-theme" "p1": "p1 main", }) - b.Assert(got["mediatypes"], qt.DeepEquals, maps.Params{ - "text/m2": maps.Params{ - "suffixes": []any{ - "m2theme", - }, - }, - "text/m1": maps.Params{ - "suffixes": []any{ - "m1main", - }, - }, - }) - - var eq = qt.CmpEquals( - cmp.Comparer(func(m1, m2 media.Type) bool { - if m1.SubType != m2.SubType { - return false - } - return m1.FirstSuffix == m2.FirstSuffix - }), - ) - - mediaTypes := b.H.Sites[0].mediaTypesConfig - m1, _ := mediaTypes.GetByType("text/m1") - m2, _ := mediaTypes.GetByType("text/m2") - - b.Assert(got["outputformats"], eq, maps.Params{ - "o1": maps.Params{ - "mediatype": m1, - "basename": "o1main", - }, - "o2": maps.Params{ - "basename": "o2theme", - "mediatype": m2, - }, - }) - - b.Assert(got["languages"], qt.DeepEquals, maps.Params{ - "en": maps.Params{ - "languagename": "English", - "params": maps.Params{ - "pl2": "p2-en-theme", - "pl1": "p1-en-main", - }, - "menus": maps.Params{ - "main": []any{ - map[string]any{ - "name": "menu-lang-en-main", - }, - }, - "theme": []any{ - map[string]any{ - "name": "menu-lang-en-theme", - }, - }, - }, - }, - "nb": maps.Params{ - "languagename": "Norsk", - "params": maps.Params{ - "top": "top-nb-theme", - "pl1": "p1-nb-main", - "pl2": "p2-nb-theme", - }, - "menus": maps.Params{ - "main": []any{ - map[string]any{ - "name": "menu-lang-nb-main", - }, - }, - "theme": []any{ - map[string]any{ - "name": "menu-lang-nb-theme", - }, - }, - "top": []any{ - map[string]any{ - "name": "menu-lang-nb-top", - }, - }, - }, - }, - }) - - c.Assert(got["baseurl"], qt.Equals, "https://example.com/") + c.Assert(got.BaseURL, qt.Equals, "https://example.com/") }) c.Run("Merge shallow", func(c *qt.C) { b := buildForStrategy(c, fmt.Sprintf("_merge=%q", "shallow")) - got := b.Cfg.Get("").(maps.Params) + got := b.Configs.Base.Params // Shallow merge, only add new keys to params. - b.Assert(got["params"], qt.DeepEquals, maps.Params{ + b.Assert(got, qt.DeepEquals, maps.Params{ "p1": "p1 main", "b": maps.Params{ "b1": "b1 main", @@ -360,59 +457,13 @@ name = "menu-theme" "[params]\np1 = \"p1 theme\"\n", ) - got := b.Cfg.Get("").(maps.Params) + got := b.Configs.Base.Params - b.Assert(got["params"], qt.DeepEquals, maps.Params{ + b.Assert(got, qt.DeepEquals, maps.Params{ "p1": "p1 theme", }) }) - c.Run("Merge language no menus or params in project", func(c *qt.C) { - b := buildForConfig( - c, - ` -theme = "test-theme" -baseURL = "https://example.com/" - -[languages] -[languages.en] -languageName = "English" - -`, - ` -[languages] -[languages.en] -languageName = "EnglishTheme" - -[languages.en.params] -p1="themep1" - -[[languages.en.menus.main]] -name = "menu-theme" -`, - ) - - got := b.Cfg.Get("").(maps.Params) - - b.Assert(got["languages"], qt.DeepEquals, - maps.Params{ - "en": maps.Params{ - "languagename": "English", - "menus": maps.Params{ - "main": []any{ - map[string]any{ - "name": "menu-theme", - }, - }, - }, - "params": maps.Params{ - "p1": "themep1", - }, - }, - }, - ) - }) - // Issue #8724 for _, mergeStrategy := range []string{"none", "shallow"} { c.Run(fmt.Sprintf("Merge with sitemap config in theme, mergestrategy %s", mergeStrategy), func(c *qt.C) { @@ -428,22 +479,14 @@ name = "menu-theme" "baseURL=\"http://example.com\"\n"+fmt.Sprintf(smapConfigTempl, "monthly"), ) - got := b.Cfg.Get("").(maps.Params) + got := b.Configs.Base if mergeStrategy == "none" { - b.Assert(got["sitemap"], qt.DeepEquals, maps.Params{ - "priority": int(-1), - "filename": "sitemap.xml", - }) + b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "", Priority: -1, Filename: "sitemap.xml"}) b.AssertFileContent("public/sitemap.xml", "schemas/sitemap") } else { - b.Assert(got["sitemap"], qt.DeepEquals, maps.Params{ - "priority": int(-1), - "filename": "sitemap.xml", - "changefreq": "monthly", - }) - + b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Priority: -1, Filename: "sitemap.xml"}) b.AssertFileContent("public/sitemap.xml", "monthly") } @@ -494,7 +537,7 @@ t3 = "tv3p" b.Build(BuildCfg{}) - got := b.Cfg.Get("params").(maps.Params) + got := b.Configs.Base.Params b.Assert(got, qt.DeepEquals, maps.Params{ "t3": "tv3p", @@ -523,7 +566,7 @@ privacyEnhanced = true b.WithConfigFile("toml", tomlConfig) b.Build(BuildCfg{SkipRender: true}) - c.Assert(b.H.Sites[0].Info.Config().Privacy.YouTube.PrivacyEnhanced, qt.Equals, true) + c.Assert(b.H.Sites[0].Config().Privacy.YouTube.PrivacyEnhanced, qt.Equals, true) } func TestLoadConfigModules(t *testing.T) { @@ -607,7 +650,7 @@ path="n4" b.Build(BuildCfg{}) - modulesClient := b.H.Paths.ModulesClient + modulesClient := b.H.Configs.ModulesClient var graphb bytes.Buffer modulesClient.Graph(&graphb) @@ -621,142 +664,6 @@ project n4 c.Assert(graphb.String(), qt.Equals, expected) } -func TestLoadConfigWithOsEnvOverrides(t *testing.T) { - c := qt.New(t) - - baseConfig := ` - -theme = "mytheme" -environment = "production" -enableGitInfo = true -intSlice = [5,7,9] -floatSlice = [3.14, 5.19] -stringSlice = ["a", "b"] - -[outputFormats] -[outputFormats.ofbase] -mediaType = "text/plain" - -[params] -paramWithNoEnvOverride="nooverride" -[params.api_config] -api_key="default_key" -another_key="default another_key" - -[imaging] -anchor = "smart" -quality = 75 -` - - newB := func(t testing.TB) *sitesBuilder { - b := newTestSitesBuilder(t).WithConfigFile("toml", baseConfig) - - b.WithSourceFile("themes/mytheme/config.toml", ` - -[outputFormats] -[outputFormats.oftheme] -mediaType = "text/plain" -[outputFormats.ofbase] -mediaType = "application/xml" - -[params] -[params.mytheme_section] -theme_param="themevalue" -theme_param_nooverride="nooverride" -[params.mytheme_section2] -theme_param="themevalue2" - -`) - - return b - } - - c.Run("Variations", func(c *qt.C) { - - b := newB(c) - - b.WithEnviron( - "HUGO_ENVIRONMENT", "test", - "HUGO_NEW", "new", // key not in config.toml - "HUGO_ENABLEGITINFO", "false", - "HUGO_IMAGING_ANCHOR", "top", - "HUGO_IMAGING_RESAMPLEFILTER", "CatmullRom", - "HUGO_STRINGSLICE", `["c", "d"]`, - "HUGO_INTSLICE", `[5, 8, 9]`, - "HUGO_FLOATSLICE", `[5.32]`, - // Issue #7829 - "HUGOxPARAMSxAPI_CONFIGxAPI_KEY", "new_key", - // Delimiters are case sensitive. - "HUGOxPARAMSxAPI_CONFIGXANOTHER_KEY", "another_key", - // Issue #8346 - "HUGOxPARAMSxMYTHEME_SECTIONxTHEME_PARAM", "themevalue_changed", - "HUGOxPARAMSxMYTHEME_SECTION2xTHEME_PARAM", "themevalue2_changed", - "HUGO_PARAMS_EMPTY", ``, - "HUGO_PARAMS_HTML", ``, - // Issue #8618 - "HUGO_SERVICES_GOOGLEANALYTICS_ID", `gaid`, - "HUGO_PARAMS_A_B_C", "abc", - ) - - b.Build(BuildCfg{}) - - cfg := b.H.Cfg - s := b.H.Sites[0] - scfg := s.siteConfigConfig.Services - - c.Assert(cfg.Get("environment"), qt.Equals, "test") - c.Assert(cfg.GetBool("enablegitinfo"), qt.Equals, false) - c.Assert(cfg.Get("new"), qt.Equals, "new") - c.Assert(cfg.Get("imaging.anchor"), qt.Equals, "top") - c.Assert(cfg.Get("imaging.quality"), qt.Equals, int64(75)) - c.Assert(cfg.Get("imaging.resamplefilter"), qt.Equals, "CatmullRom") - c.Assert(cfg.Get("stringSlice"), qt.DeepEquals, []any{"c", "d"}) - c.Assert(cfg.Get("floatSlice"), qt.DeepEquals, []any{5.32}) - c.Assert(cfg.Get("intSlice"), qt.DeepEquals, []any{5, 8, 9}) - c.Assert(cfg.Get("params.api_config.api_key"), qt.Equals, "new_key") - c.Assert(cfg.Get("params.api_config.another_key"), qt.Equals, "default another_key") - c.Assert(cfg.Get("params.mytheme_section.theme_param"), qt.Equals, "themevalue_changed") - c.Assert(cfg.Get("params.mytheme_section.theme_param_nooverride"), qt.Equals, "nooverride") - c.Assert(cfg.Get("params.mytheme_section2.theme_param"), qt.Equals, "themevalue2_changed") - c.Assert(cfg.Get("params.empty"), qt.Equals, ``) - c.Assert(cfg.Get("params.html"), qt.Equals, ``) - - params := cfg.Get("params").(maps.Params) - c.Assert(params["paramwithnoenvoverride"], qt.Equals, "nooverride") - c.Assert(cfg.Get("params.paramwithnoenvoverride"), qt.Equals, "nooverride") - c.Assert(scfg.GoogleAnalytics.ID, qt.Equals, "gaid") - c.Assert(cfg.Get("params.a.b"), qt.DeepEquals, maps.Params{ - "c": "abc", - }) - - ofBase, _ := s.outputFormatsConfig.GetByName("ofbase") - ofTheme, _ := s.outputFormatsConfig.GetByName("oftheme") - - c.Assert(ofBase.MediaType, qt.Equals, media.TextType) - c.Assert(ofTheme.MediaType, qt.Equals, media.TextType) - - }) - - // Issue #8709 - c.Run("Set in string", func(c *qt.C) { - b := newB(c) - - b.WithEnviron( - "HUGO_ENABLEGITINFO", "false", - // imaging.anchor is a string, and it's not possible - // to set a child attribute. - "HUGO_IMAGING_ANCHOR_FOO", "top", - ) - - b.Build(BuildCfg{}) - - cfg := b.H.Cfg - c.Assert(cfg.Get("imaging.anchor"), qt.Equals, "smart") - - }) - -} - func TestInvalidDefaultMarkdownHandler(t *testing.T) { t.Parallel() diff --git a/hugolib/configdir_test.go b/hugolib/configdir_test.go index 7ac3f969d..3ab84c1bd 100644 --- a/hugolib/configdir_test.go +++ b/hugolib/configdir_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// 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. @@ -13,140 +13,43 @@ package hugolib -import ( - "path/filepath" - "testing" +import "testing" - "github.com/gohugoio/hugo/common/herrors" +func TestConfigDir(t *testing.T) { - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/htesting" - "github.com/spf13/afero" -) - -func TestLoadConfigDir(t *testing.T) { t.Parallel() - c := qt.New(t) - - configContent := ` -baseURL = "https://example.org" -paginagePath = "pag_root" - -[languages.en] -weight = 0 -languageName = "English" - -[languages.no] -weight = 10 -languageName = "FOO" - + files := ` +-- config/_default/params.toml -- +a = "acp1" +d = "dcp1" +-- config/_default/config.toml -- [params] -p1 = "p1_base" +a = "ac1" +b = "bc1" +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "setion"] +ignoreErrors = ["error-missing-instagram-accesstoken"] +[params] +a = "a1" +b = "b1" +c = "c1" +-- layouts/index.html -- +Params: {{ site.Params}} ` + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, + }, + ).Build() - mm := afero.NewMemMapFs() - - writeToFs(t, mm, "hugo.toml", configContent) - - fb := htesting.NewTestdataBuilder(mm, "config/_default", t) + b.AssertFileContent("public/index.html", ` +Params: map[a:acp1 b:bc1 c:c1 d:dcp1] - fb.Add("config.toml", `paginatePath = "pag_default"`) - fb.Add("params.yaml", ` -p2: "p2params_default" -p3: "p3params_default" -p4: "p4params_default" `) - fb.Add("menus.toml", ` -[[docs]] -name = "About Hugo" -weight = 1 -[[docs]] -name = "Home" -weight = 2 - `) - - fb.Add("menus.no.toml", ` - [[docs]] - name = "Om Hugo" - weight = 1 - `) - - fb.Add("params.no.toml", - ` -p3 = "p3params_no_default" -p4 = "p4params_no_default"`, - ) - fb.Add("languages.no.toml", `languageName = "Norsk_no_default"`) - - fb.Build() - - fb = fb.WithWorkingDir("config/production") - - fb.Add("config.toml", `paginatePath = "pag_production"`) - - fb.Add("params.no.toml", ` -p2 = "p2params_no_production" -p3 = "p3params_no_production" -`) - - fb.Build() - - fb = fb.WithWorkingDir("config/development") - - // This is set in all the config.toml variants above, but this will win. - fb.Add("config.TOML", `paginatePath = "pag_development"`) - // Issue #5646 - fb.Add("config.toml.swp", `p3 = "paginatePath = "nono"`) - - fb.Add("params.no.toml", `p3 = "p3params_no_development"`) - fb.Add("params.toml", `p3 = "p3params_development"`) - - fb.Build() - - cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Environment: "development", Filename: "hugo.toml", AbsConfigDir: "config"}) - c.Assert(err, qt.IsNil) - - c.Assert(cfg.GetString("paginatePath"), qt.Equals, "pag_development") // /config/development/config.toml - - c.Assert(cfg.GetInt("languages.no.weight"), qt.Equals, 10) // /config.toml - c.Assert(cfg.GetString("languages.no.languageName"), qt.Equals, "Norsk_no_default") // /config/_default/languages.no.toml - - c.Assert(cfg.GetString("params.p1"), qt.Equals, "p1_base") - c.Assert(cfg.GetString("params.p2"), qt.Equals, "p2params_default") // Is in both _default and production - c.Assert(cfg.GetString("params.p3"), qt.Equals, "p3params_development") - c.Assert(cfg.GetString("languages.no.params.p3"), qt.Equals, "p3params_no_development") - - c.Assert(len(cfg.Get("menus.docs").([]any)), qt.Equals, 2) - noMenus := cfg.Get("languages.no.menus.docs") - c.Assert(noMenus, qt.Not(qt.IsNil)) - c.Assert(len(noMenus.([]any)), qt.Equals, 1) -} - -func TestLoadConfigDirError(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - configContent := ` -baseURL = "https://example.org" - -` - - mm := afero.NewMemMapFs() - - writeToFs(t, mm, "hugo.toml", configContent) - - fb := htesting.NewTestdataBuilder(mm, "config/development", t) - - fb.Add("config.toml", `invalid & syntax`).Build() - - _, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Environment: "development", Filename: "hugo.toml", AbsConfigDir: "config"}) - c.Assert(err, qt.Not(qt.IsNil)) - fe := herrors.UnwrapFileError(err) - c.Assert(fe, qt.Not(qt.IsNil)) - c.Assert(fe.Position().Filename, qt.Equals, filepath.FromSlash("config/development/config.toml")) } diff --git a/hugolib/content_map.go b/hugolib/content_map.go index bf77e7f1b..a7f344004 100644 --- a/hugolib/content_map.go +++ b/hugolib/content_map.go @@ -129,7 +129,7 @@ type cmInsertKeyBuilder struct { } func (b cmInsertKeyBuilder) ForPage(s string) *cmInsertKeyBuilder { - // fmt.Println("ForPage:", s, "baseKey:", b.baseKey, "key:", b.key) + //fmt.Println("ForPage:", s, "baseKey:", b.baseKey, "key:", b.key, "tree:", b.tree.Name) baseKey := b.baseKey b.baseKey = s diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 70c5d6a27..1b6fd40e9 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -148,7 +148,7 @@ func (m *pageMap) newPageFromContentNode(n *contentNode, parentBucket *pagesMapB parseResult, err := pageparser.Parse( r, - pageparser.Config{EnableEmoji: s.siteCfg.enableEmoji}, + pageparser.Config{EnableEmoji: s.conf.EnableEmoji}, ) if err != nil { return nil, err @@ -742,13 +742,11 @@ func (m *pageMaps) AssemblePages() error { sw := §ionWalker{m: pm.contentMap} a := sw.applyAggregates() - _, mainSectionsSet := pm.s.s.Info.Params()["mainsections"] - if !mainSectionsSet && a.mainSection != "" { + + if a.mainSection != "" && len(pm.s.s.conf.C.MainSections) == 0 { mainSections := []string{strings.TrimRight(a.mainSection, "/")} - pm.s.s.Info.Params()["mainSections"] = mainSections - pm.s.s.Info.Params()["mainsections"] = mainSections + pm.s.s.conf.C.SetMainSections(mainSections) } - pm.s.lastmod = a.datesAll.Lastmod() if resource.IsZeroDates(pm.s.home) { pm.s.home.m.Dates = a.datesAll diff --git a/hugolib/datafiles_test.go b/hugolib/datafiles_test.go index a6bcae944..4fb3d5bdb 100644 --- a/hugolib/datafiles_test.go +++ b/hugolib/datafiles_test.go @@ -14,431 +14,43 @@ package hugolib import ( - "fmt" - "path/filepath" - "reflect" - "runtime" "testing" - - "github.com/gohugoio/hugo/common/loggers" - - "github.com/gohugoio/hugo/deps" - - qt "github.com/frankban/quicktest" ) -func TestDataFromTheme(t *testing.T) { - t.Parallel() +func TestData(t *testing.T) { + + t.Run("with theme", func(t *testing.T) { + t.Parallel() - files := ` --- config.toml -- -[module] -[[module.imports]] -path = "mytheme" + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "setion"] +theme = "mytheme" -- data/a.toml -- -d1 = "d1main" -d2 = "d2main" +v1 = "a_v1" +-- data/b.yaml -- +v1: b_v1 +-- data/c/d.yaml -- +v1: c_d_v1 -- themes/mytheme/data/a.toml -- -d1 = "d1theme" -d2 = "d2theme" -d3 = "d3theme" +v1 = "a_v1_theme" +-- themes/mytheme/data/d.toml -- +v1 = "d_v1_theme" -- layouts/index.html -- -d1: {{ site.Data.a.d1 }}|d2: {{ site.Data.a.d2 }}|d3: {{ site.Data.a.d3 }} - +a: {{ site.Data.a.v1 }}| +b: {{ site.Data.b.v1 }}| +cd: {{ site.Data.c.d.v1 }}| +d: {{ site.Data.d.v1 }}| ` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).Build() - - b.AssertFileContent("public/index.html", ` -d1: d1main|d2: d2main|d3: d3theme - `) -} - -func TestDataDir(t *testing.T) { - t.Parallel() - equivDataDirs := make([]dataDir, 3) - equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": "red" , "c2": "blue" } }`) - equivDataDirs[1].addSource("data/test/a.yaml", "b:\n c1: red\n c2: blue") - equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = \"red\"\nc2 = \"blue\"\n") - expected := map[string]any{ - "test": map[string]any{ - "a": map[string]any{ - "b": map[string]any{ - "c1": "red", - "c2": "blue", - }, - }, - }, - } - doTestEquivalentDataDirs(t, equivDataDirs, expected) -} - -// Unable to enforce equivalency for int values as -// the JSON, YAML and TOML parsers return -// float64, int, int64 respectively. They all return -// float64 for float values though: -func TestDataDirNumeric(t *testing.T) { - t.Parallel() - equivDataDirs := make([]dataDir, 3) - equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": 1.7 , "c2": 2.9 } }`) - equivDataDirs[1].addSource("data/test/a.yaml", "b:\n c1: 1.7\n c2: 2.9") - equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = 1.7\nc2 = 2.9\n") - expected := map[string]any{ - "test": map[string]any{ - "a": map[string]any{ - "b": map[string]any{ - "c1": 1.7, - "c2": 2.9, - }, + b := NewIntegrationTestBuilder( + IntegrationTestConfig{ + T: t, + TxtarString: files, }, - }, - } - doTestEquivalentDataDirs(t, equivDataDirs, expected) -} - -func TestDataDirBoolean(t *testing.T) { - t.Parallel() - equivDataDirs := make([]dataDir, 3) - equivDataDirs[0].addSource("data/test/a.json", `{ "b" : { "c1": true , "c2": false } }`) - equivDataDirs[1].addSource("data/test/a.yaml", "b:\n c1: true\n c2: false") - equivDataDirs[2].addSource("data/test/a.toml", "[b]\nc1 = true\nc2 = false\n") - expected := map[string]any{ - "test": map[string]any{ - "a": map[string]any{ - "b": map[string]any{ - "c1": true, - "c2": false, - }, - }, - }, - } - doTestEquivalentDataDirs(t, equivDataDirs, expected) -} - -func TestDataDirTwoFiles(t *testing.T) { - t.Parallel() - equivDataDirs := make([]dataDir, 3) - - equivDataDirs[0].addSource("data/test/foo.json", `{ "bar": "foofoo" }`) - equivDataDirs[0].addSource("data/test.json", `{ "hello": [ "world", "foo" ] }`) - - equivDataDirs[1].addSource("data/test/foo.yaml", "bar: foofoo") - equivDataDirs[1].addSource("data/test.yaml", "hello:\n- world\n- foo") - - equivDataDirs[2].addSource("data/test/foo.toml", "bar = \"foofoo\"") - equivDataDirs[2].addSource("data/test.toml", "hello = [\"world\", \"foo\"]") - - expected := - map[string]any{ - "test": map[string]any{ - "hello": []any{ - "world", - "foo", - }, - "foo": map[string]any{ - "bar": "foofoo", - }, - }, - } - - doTestEquivalentDataDirs(t, equivDataDirs, expected) -} - -func TestDataDirOverriddenValue(t *testing.T) { - t.Parallel() - equivDataDirs := make([]dataDir, 3) - - // filepath.Walk walks the files in lexical order, '/' comes before '.'. Simulate this: - equivDataDirs[0].addSource("data/a.json", `{"a": "1"}`) - equivDataDirs[0].addSource("data/test/v1.json", `{"v1-2": "2"}`) - equivDataDirs[0].addSource("data/test/v2.json", `{"v2": ["2", "3"]}`) - equivDataDirs[0].addSource("data/test.json", `{"v1": "1"}`) - - equivDataDirs[1].addSource("data/a.yaml", "a: \"1\"") - equivDataDirs[1].addSource("data/test/v1.yaml", "v1-2: \"2\"") - equivDataDirs[1].addSource("data/test/v2.yaml", "v2:\n- \"2\"\n- \"3\"") - equivDataDirs[1].addSource("data/test.yaml", "v1: \"1\"") - - equivDataDirs[2].addSource("data/a.toml", "a = \"1\"") - equivDataDirs[2].addSource("data/test/v1.toml", "v1-2 = \"2\"") - equivDataDirs[2].addSource("data/test/v2.toml", "v2 = [\"2\", \"3\"]") - equivDataDirs[2].addSource("data/test.toml", "v1 = \"1\"") - - expected := - map[string]any{ - "a": map[string]any{"a": "1"}, - "test": map[string]any{ - "v1": map[string]any{"v1-2": "2"}, - "v2": map[string]any{"v2": []any{"2", "3"}}, - }, - } - - doTestEquivalentDataDirs(t, equivDataDirs, expected) -} - -// Issue #4361, #3890 -func TestDataDirArrayAtTopLevelOfFile(t *testing.T) { - t.Parallel() - equivDataDirs := make([]dataDir, 2) - - equivDataDirs[0].addSource("data/test.json", `[ { "hello": "world" }, { "what": "time" }, { "is": "lunch?" } ]`) - equivDataDirs[1].addSource("data/test.yaml", ` -- hello: world -- what: time -- is: lunch? -`) - - expected := - map[string]any{ - "test": []any{ - map[string]any{"hello": "world"}, - map[string]any{"what": "time"}, - map[string]any{"is": "lunch?"}, - }, - } - - doTestEquivalentDataDirs(t, equivDataDirs, expected) -} - -// Issue #892 -func TestDataDirMultipleSources(t *testing.T) { - t.Parallel() - - var dd dataDir - dd.addSource("data/test/first.yaml", "bar: 1") - dd.addSource("themes/mytheme/data/test/first.yaml", "bar: 2") - dd.addSource("data/test/second.yaml", "tender: 2") - - expected := - map[string]any{ - "test": map[string]any{ - "first": map[string]any{ - "bar": 1, - }, - "second": map[string]any{ - "tender": 2, - }, - }, - } - - doTestDataDir(t, dd, expected, - "theme", "mytheme") -} - -// test (and show) the way values from four different sources, -// including theme data, commingle and override -func TestDataDirMultipleSourcesCommingled(t *testing.T) { - t.Parallel() - - var dd dataDir - dd.addSource("data/a.json", `{ "b1" : { "c1": "data/a" }, "b2": "data/a", "b3": ["x", "y", "z"] }`) - dd.addSource("themes/mytheme/data/a.json", `{ "b1": "mytheme/data/a", "b2": "mytheme/data/a", "b3": "mytheme/data/a" }`) - dd.addSource("themes/mytheme/data/a/b1.json", `{ "c1": "mytheme/data/a/b1", "c2": "mytheme/data/a/b1" }`) - dd.addSource("data/a/b1.json", `{ "c1": "data/a/b1" }`) - - // Per handleDataFile() comment: - // 1. A theme uses the same key; the main data folder wins - // 2. A sub folder uses the same key: the sub folder wins - expected := - map[string]any{ - "a": map[string]any{ - "b1": map[string]any{ - "c1": "data/a/b1", - "c2": "mytheme/data/a/b1", - }, - "b2": "data/a", - "b3": []any{"x", "y", "z"}, - }, - } - - doTestDataDir(t, dd, expected, "theme", "mytheme") -} - -func TestDataDirCollidingChildArrays(t *testing.T) { - t.Parallel() - - var dd dataDir - dd.addSource("themes/mytheme/data/a/b2.json", `["Q", "R", "S"]`) - dd.addSource("data/a.json", `{ "b1" : "data/a", "b2" : ["x", "y", "z"] }`) - dd.addSource("data/a/b2.json", `["1", "2", "3"]`) - - // Per handleDataFile() comment: - // 1. A theme uses the same key; the main data folder wins - // 2. A sub folder uses the same key: the sub folder wins - expected := - map[string]any{ - "a": map[string]any{ - "b1": "data/a", - "b2": []any{"1", "2", "3"}, - }, - } - - doTestDataDir(t, dd, expected, "theme", "mytheme") -} - -func TestDataDirCollidingTopLevelArrays(t *testing.T) { - t.Parallel() - - var dd dataDir - dd.addSource("themes/mytheme/data/a/b1.json", `["x", "y", "z"]`) - dd.addSource("data/a/b1.json", `["1", "2", "3"]`) - - expected := - map[string]any{ - "a": map[string]any{ - "b1": []any{"1", "2", "3"}, - }, - } - - doTestDataDir(t, dd, expected, "theme", "mytheme") -} - -func TestDataDirCollidingMapsAndArrays(t *testing.T) { - t.Parallel() - - var dd dataDir - // on - dd.addSource("themes/mytheme/data/a.json", `["1", "2", "3"]`) - dd.addSource("themes/mytheme/data/b.json", `{ "film" : "Logan Lucky" }`) - dd.addSource("data/a.json", `{ "music" : "Queen's Rebuke" }`) - dd.addSource("data/b.json", `["x", "y", "z"]`) - - expected := - map[string]any{ - "a": map[string]any{ - "music": "Queen's Rebuke", - }, - "b": []any{"x", "y", "z"}, - } - - doTestDataDir(t, dd, expected, "theme", "mytheme") -} - -// https://discourse.gohugo.io/t/recursive-data-file-parsing/26192 -func TestDataDirNestedDirectories(t *testing.T) { - t.Parallel() - - var dd dataDir - dd.addSource("themes/mytheme/data/a.json", `["1", "2", "3"]`) - dd.addSource("data/test1/20/06/a.json", `{ "artist" : "Michael Brecker" }`) - dd.addSource("data/test1/20/05/b.json", `{ "artist" : "Charlie Parker" }`) - - expected := - map[string]any{ - "a": []any{"1", "2", "3"}, - "test1": map[string]any{"20": map[string]any{"05": map[string]any{"b": map[string]any{"artist": "Charlie Parker"}}, "06": map[string]any{"a": map[string]any{"artist": "Michael Brecker"}}}}, - } - - doTestDataDir(t, dd, expected, "theme", "mytheme") -} - -type dataDir struct { - sources [][2]string -} - -func (d *dataDir) addSource(path, content string) { - d.sources = append(d.sources, [2]string{path, content}) -} - -func doTestEquivalentDataDirs(t *testing.T, equivDataDirs []dataDir, expected