From 098b09e8b2179a87edf51f35795687968483b8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 20 Jan 2023 09:40:02 +0100 Subject: [PATCH] Work --- cache/docs.go | 2 + cache/filecache/filecache_config.go | 9 +- config/allconfig/allconfig.go | 149 +++++++++---------- config/commonConfig.go | 16 +-- config/commonConfig_test.go | 4 +- config/namespace.go | 24 +++- config/namespace_test.go | 2 +- hugolib/config.go | 16 +++ hugolib/config_test.go | 2 + hugolib/page__meta.go | 4 +- hugolib/site.go | 4 +- hugolib/sitemap_test.go | 4 +- langs/config.go | 158 +++++++++++++++++++++ navigation/menu.go | 58 ++++++++ output/config.go | 61 ++++++++ parser/lowercase_camel_json.go | 17 +++ resources/images/config.go | 18 +-- resources/images/image.go | 6 +- resources/page/page.go | 2 +- resources/page/page_marshaljson.autogen.go | 7 +- resources/page/page_matcher.go | 8 +- resources/page/page_matcher_test.go | 29 +++- resources/page/page_nop.go | 4 +- resources/page/testhelpers_test.go | 4 +- resources/resource_spec.go | 4 +- 25 files changed, 478 insertions(+), 134 deletions(-) create mode 100644 cache/docs.go diff --git a/cache/docs.go b/cache/docs.go new file mode 100644 index 00000000000..babecec22bc --- /dev/null +++ b/cache/docs.go @@ -0,0 +1,2 @@ +// Package cache contains the differenct cache implementations. +package cache diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go index a82133ab7f9..569156ecb7c 100644 --- a/cache/filecache/filecache_config.go +++ b/cache/filecache/filecache_config.go @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package filecache provides a file based cache for Hugo. package filecache import ( @@ -39,7 +40,7 @@ const ( cacheDirProject = ":cacheDir/:project" ) -var defaultCacheConfig = Config{ +var defaultCacheConfig = FileCacheConfig{ MaxAge: -1, // Never expire Dir: cacheDirProject, } @@ -53,7 +54,7 @@ const ( cacheKeyGetResource = "getresource" ) -type Configs map[string]Config +type Configs map[string]FileCacheConfig func (c Configs) CacheDirModules() string { return c[cacheKeyModules].Dir @@ -74,13 +75,13 @@ var defaultCacheConfigs = Configs{ MaxAge: -1, Dir: resourcesGenDir, }, - cacheKeyGetResource: Config{ + cacheKeyGetResource: FileCacheConfig{ MaxAge: -1, // Never expire Dir: cacheDirProject, }, } -type Config struct { +type FileCacheConfig struct { // Max age of cache entries in this cache. Any items older than this will // be removed and not returned from the cache. // a negative value means forever, 0 means cache is disabled. diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 77410307b80..b5404b14e8c 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -21,21 +21,26 @@ import ( "github.com/gohugoio/hugo/config/privacy" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/services" + "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/minifiers" "github.com/gohugoio/hugo/modules" + "github.com/gohugoio/hugo/navigation" + "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/resources/images" + "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/mitchellh/mapstructure" "github.com/spf13/afero" ) -type Config2 struct { +type Config struct { + RootConfig // The build configuration section contains global build-related configuration options. - Build config.Build `mapstructure:"-"` + Build config.BuildConfig `mapstructure:"-"` // The caches configuration section contains global cache-related configuration options. // cache:filecache:configs @@ -45,58 +50,45 @@ type Config2 struct { Markup markup_config.Config `mapstructure:"-"` // Media type configuration. + // TODO1 language. MediaTypes *config.ConfigNamespace[map[string]media.DocsMediaTypeConfig, media.Types] `mapstructure:"-"` - Imaging *config.ConfigNamespace[images.Imaging, images.ImagingConfigInternal] + // Output format configuration. + // TODO1 language. + Outputformats *config.ConfigNamespace[map[string]output.Format, output.Formats] `mapstructure:"-"` + + Imaging *config.ConfigNamespace[images.ImagingConfig, images.ImagingConfigInternal] // The cascade configuration section contains global the top level front matter cascade configuration options. - Cascade []CascadeEntryConfig `mapstructure:"-"` + Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"` + + // Menu configuration. + // TODO1 Per lang. + Menus *config.ConfigNamespace[[]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"` + + // Language configuration. + Languages *config.ConfigNamespace[map[string]langs.LanguageConfig, langs.LanguagesConfig] `mapstructure:"-"` // Front matter configuration. Frontmatter pagemeta.FrontmatterConfig `mapstructure:"-"` - // Where to put the generated files. - PublishDir string - // Module configuration. Module modules.Config `mapstructure:"-"` // Permalink configuration. Permalinks map[string]string - // The directory to put the generated resources files. This directory should in most situations be considered temporary - // and not be committed to version control. But there may be cached content in here that you want to keep, - // e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup. - ResourceDir string - // Minification configuration. Minify minifiers.MinifyConfig `mapstructure:"-"` // Sitemap configuration. - Sitemap config.Sitemap + Sitemap config.SitemapConfig // Taxonomy configuration. Taxonomies map[string]string // Related content configuration. Related related.Config `mapstructure:"-"` -} - -// Config holds all configuration options in Hugo. -// TODO(bep) make sure that the keys gets the sensible casing in JSON etc. -type Config struct { - - // Language configuration. - Languages map[string]LanguageConfig `mapstructure:"-"` - - // Menu configuration. - Menus []MenuConfig `mapstructure:"-"` - - // Output format configuration. - Outputformats map[string]OutputFormatConfig `mapstructure:"-"` - - // Params configuration. - Params maps.Params // Privacy configuration. Privacy privacy.Config `mapstructure:"-"` @@ -107,6 +99,17 @@ type Config struct { // Services configuration. Services services.Config `mapstructure:"-"` + // Params configuration. + // TODO1 language + Params maps.Params +} + +// RootConfig holds all the top-level configuration options in Hugo +type RootConfig struct { + + // The base URL of the site. + BaseURL string + // Whether to build content marked as draft. BuildDrafts bool @@ -192,58 +195,22 @@ type Config struct { TitleCaseStyle string } -// TODO(bep) move these. -type CascadeEntryConfig struct { - // A Glob pattern matching the content path below /content. - // Expects Unix-styled slashes. Note that this is the virtual path, so it starts at the mount root. - // The matching supports double-asterisks so you can match for patterns like /blog/*/** to match anything from the third level and - Path string - - // A Glob pattern matching the Page’s Kind(s), e.g. “{home,section}”. - Kind string - - // A Glob pattern matching the Page’s language, e.g. “{en,sv}”. - Lang string - - // A Glob pattern matching the build environment, e.g. “{production,development}” - Environment string -} - -type OutputFormatConfig struct { - BaseName string - IsPlainText bool - MediaType string - Protocol string -} - -type LanguageConfig struct { - LanguageDirection string - Title string - Weight int - Params maps.Params -} - -type MenuConfig struct { - Identifier string - Name string - Pre string - URL string - Weight int -} +// TODO(bep) move or remove these. // FromProvider creates a new Config from the given Provider. -func FromProvider(cfg config.Provider) (all Config2, err error) { +func FromProvider(cfg config.Provider) (all Config, err error) { // TODO1 per language. cfg.Set("cacheDir", "/mycachedir") m := cfg.Get("") fs := afero.NewMemMapFs() - //litter.Dump(m) - if err = mapstructure.WeakDecode(m, &all); err != nil { + // First decode the top level config. + if err = mapstructure.WeakDecode(m, &all.RootConfig); err != nil { return } + // Then all the namespaces. all.Imaging, err = images.DecodeConfig(cfg.GetStringMap("imaging")) if err != nil { return @@ -252,7 +219,8 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { if err != nil { return } - all.Build = config.DecodeBuild(cfg) + all.Build = config.DecodeBuildConfig(cfg) + all.Markup, err = markup_config.Decode(cfg) if err != nil { return @@ -261,6 +229,11 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { if err != nil { return } + all.Outputformats, err = output.DecodeConfig(all.MediaTypes.Config, cfg.Get("outputFormats")) + if err != nil { + return + } + all.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(cfg) if err != nil { return @@ -277,8 +250,14 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { return } - all.Sitemap = config.DecodeSitemap(config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, cfg.GetStringMap("sitemap")) + all.Sitemap = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, cfg.GetStringMap("sitemap")) all.Taxonomies = cfg.GetStringMapString("taxonomies") + all.Params = cfg.GetStringMap("params") + + all.Languages, err = langs.DecodeConfig(cfg) + if err != nil { + return + } // Related config if cfg.IsSet("related") { @@ -294,13 +273,27 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { } // Per language (check others) - if cfg.IsSet("cascade") { - /*cascade, err = page.DecodeCascade(cfg.Get("cascade")) - if err != nil { - return - }*/ + all.Cascade, err = page.DecodeCascadeConfig(cfg.Get("cascade")) + if err != nil { + return + } + all.Privacy, err = privacy.DecodeConfig(cfg) + if err != nil { + return } + all.Security, err = security.DecodeConfig(cfg) + if err != nil { + return + } + + all.Services, err = services.DecodeConfig(cfg) + if err != nil { + return + } + + all.Menus, err = navigation.DecodeConfig(cfg.Get("menus")) + return } diff --git a/config/commonConfig.go b/config/commonConfig.go index 31705841ef2..b5ff57b5fdc 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -28,13 +28,13 @@ import ( jww "github.com/spf13/jwalterweatherman" ) -var DefaultBuild = Build{ +var DefaultBuild = BuildConfig{ UseResourceCacheWhen: "fallback", WriteStats: false, } -// Build holds some build related configuration. -type Build struct { +// BuildConfig holds some build related configuration. +type BuildConfig struct { UseResourceCacheWhen string // never, fallback, always. Default is fallback // When enabled, will collect and write a hugo_stats.json with some build @@ -46,7 +46,7 @@ type Build struct { NoJSConfigInAssets bool } -func (b Build) UseResourceCache(err error) bool { +func (b BuildConfig) UseResourceCache(err error) bool { if b.UseResourceCacheWhen == "never" { return false } @@ -58,7 +58,7 @@ func (b Build) UseResourceCache(err error) bool { return true } -func DecodeBuild(cfg Provider) Build { +func DecodeBuildConfig(cfg Provider) BuildConfig { m := cfg.GetStringMap("build") b := DefaultBuild if m == nil { @@ -79,14 +79,14 @@ func DecodeBuild(cfg Provider) Build { return b } -// Sitemap configures the sitemap to be generated. -type Sitemap struct { +// SitemapConfig configures the sitemap to be generated. +type SitemapConfig struct { ChangeFreq string Priority float64 Filename string } -func DecodeSitemap(prototype Sitemap, input map[string]any) Sitemap { +func DecodeSitemap(prototype SitemapConfig, input map[string]any) SitemapConfig { for key, value := range input { switch key { case "changefreq": diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index 4ff2e8ed5f7..23e86c27e08 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -31,7 +31,7 @@ func TestBuild(t *testing.T) { "useResourceCacheWhen": "always", }) - b := DecodeBuild(v) + b := DecodeBuildConfig(v) c.Assert(b.UseResourceCacheWhen, qt.Equals, "always") @@ -39,7 +39,7 @@ func TestBuild(t *testing.T) { "useResourceCacheWhen": "foo", }) - b = DecodeBuild(v) + b = DecodeBuildConfig(v) c.Assert(b.UseResourceCacheWhen, qt.Equals, "fallback") diff --git a/config/namespace.go b/config/namespace.go index bc27aaa63fe..e0a23dd1866 100644 --- a/config/namespace.go +++ b/config/namespace.go @@ -36,24 +36,36 @@ func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, } ns := &ConfigNamespace[S, C]{ - External: ext, - SourceHash: h, - Config: c, + SourceStructure: ext, + SourceHash: h, + Config: c, } return ns, nil } +// ConfigNamespace holds a Hugo configuration namespace. +// The construct looks a little odd, but it's built to make the configuration elements +// both self-documenting and contained in a common structure. type ConfigNamespace[S, C any] struct { - External any + // SourceStructure represents the source configuration with any defaults applied. + // This is used for documentation and printing of the configuration setup to the user. + SourceStructure any + + // SourceHash is a hash of the source configuration before any defaults gets applied. SourceHash string - Config C + + // Config is the final configuration as used by Hugo. + Config C } +// MarshalJSON marshals the source structure. func (ns *ConfigNamespace[S, C]) MarshalJSON() ([]byte, error) { - return json.Marshal(ns.External) + return json.Marshal(ns.SourceStructure) } +// Signature returns the signature of the source structure. +// Note that this is for documentation purposes only and SourceStructure may not always be cast to S (it's usually just a map). func (ns *ConfigNamespace[S, C]) Signature() S { var s S return s diff --git a/config/namespace_test.go b/config/namespace_test.go index a472b05542d..008237c1378 100644 --- a/config/namespace_test.go +++ b/config/namespace_test.go @@ -42,7 +42,7 @@ func TestNamespace(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(ns, qt.Not(qt.IsNil)) - c.Assert(ns.External, qt.DeepEquals, map[string]interface{}{"foo": "bar"}) + c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"}) c.Assert(ns.SourceHash, qt.Equals, "14368731254619220105") c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"}) c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil)) diff --git a/hugolib/config.go b/hugolib/config.go index 059424e85fa..a85a2e5fb52 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -41,6 +41,7 @@ import ( "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/privacy" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/services" @@ -255,6 +256,7 @@ func (l configLoader) applyConfigAliases() error { func (l configLoader) applyConfigDefaults() error { defaultSettings := maps.Params{ + "baseURL": "https://example.com/", "cleanDestinationDir": false, "watch": false, "resourceDir": "resources", @@ -539,3 +541,17 @@ func (l configLoader) wrapFileError(err error, filename string) error { } return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil) } + +// DefaultConfig returns the default configuration. +func DefaultConfig() allconfig.Config { + fs := afero.NewMemMapFs() + cfg, err := LoadConfigDefault(fs) + if err != nil { + panic(err) + } + all, err := allconfig.FromProvider(cfg) + if err != nil { + panic(err) + } + return all +} diff --git a/hugolib/config_test.go b/hugolib/config_test.go index bc183e1b6e9..9145f449077 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -856,11 +856,13 @@ func TestAllConfigDefaults(t *testing.T) { c.Assert(all.Build.UseResourceCacheWhen, qt.Equals, "fallback") c.Assert(all.Markup.DefaultMarkdownHandler, qt.Equals, "goldmark") c.Assert(all.MediaTypes.Config, qt.HasLen, 36) + c.Assert(all.Outputformats.Config, qt.HasLen, 11) c.Assert(all.Frontmatter.Date, qt.DeepEquals, []string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}) c.Assert(all.Module.Proxy, qt.Equals, "direct") c.Assert(all.Permalinks, qt.DeepEquals, map[string]string{}) c.Assert(all.Sitemap.Filename, qt.Equals, "sitemap.xml") c.Assert(all.Related.Threshold, qt.Equals, 80) + c.Assert(all.Languages.Config.Languages, qt.HasLen, 1) f, err := os.Create("/Users/bep/dump/allconfig/all.json") c.Assert(err, qt.IsNil) diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index bb038a1d9d7..2b04e17a820 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -116,7 +116,7 @@ type pageMeta struct { sections []string // Sitemap overrides from front matter. - sitemap config.Sitemap + sitemap config.SitemapConfig s *Site @@ -298,7 +298,7 @@ func (p *pageMeta) SectionsPath() string { return path.Join(p.SectionsEntries()...) } -func (p *pageMeta) Sitemap() config.Sitemap { +func (p *pageMeta) Sitemap() config.SitemapConfig { return p.sitemap } diff --git a/hugolib/site.go b/hugolib/site.go index 2d01f89b2b7..dd136b35704 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -192,7 +192,7 @@ func (t taxonomiesConfig) Values() []viewName { } type siteConfigHolder struct { - sitemap config.Sitemap + sitemap config.SitemapConfig taxonomiesConfig taxonomiesConfig timeout time.Duration hasCJKLanguage bool @@ -533,7 +533,7 @@ But this also means that your site configuration may not do what you expect. If } siteConfig := siteConfigHolder{ - sitemap: config.DecodeSitemap(config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")), + sitemap: config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")), taxonomiesConfig: taxonomies, timeout: timeout, hasCJKLanguage: cfg.Language.GetBool("hasCJKLanguage"), diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index cb4eea23449..8ae57ad7d5d 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -87,14 +87,14 @@ func doTestSitemapOutput(t *testing.T, internal bool) { func TestParseSitemap(t *testing.T) { t.Parallel() - expected := config.Sitemap{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"} + expected := config.SitemapConfig{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"} input := map[string]any{ "changefreq": "3", "priority": 3.0, "filename": "doo.xml", "unknown": "ignore", } - result := config.DecodeSitemap(config.Sitemap{}, input) + result := config.DecodeSitemap(config.SitemapConfig{}, input) if !reflect.DeepEqual(expected, result) { t.Errorf("Got \n%v expected \n%v", result, expected) diff --git a/langs/config.go b/langs/config.go index 81e6fc2aba2..a5229e52368 100644 --- a/langs/config.go +++ b/langs/config.go @@ -34,6 +34,164 @@ type LanguagesConfig struct { DefaultContentLanguageInSubdir bool } +type LanguageConfig struct { + LanguageDirection string + Title string + Weight int + Params maps.Params +} + +func DecodeConfig(cfg config.Provider) (*config.ConfigNamespace[map[string]LanguageConfig, LanguagesConfig], error) { + var oldLangs Languages = nil // TODO1 + buildConfig := func(in any) (LanguagesConfig, any, error) { + var c LanguagesConfig + if in == nil { + return c, nil, nil + } + defaultLang := strings.ToLower(cfg.GetString("defaultContentLanguage")) + if defaultLang == "" { + defaultLang = "en" + // TODO1 remove this + cfg.Set("defaultContentLanguage", defaultLang) + } + + var languages map[string]any + + languagesFromConfig := cfg.GetParams("languages") + disableLanguages := cfg.GetStringSlice("disableLanguages") + + if len(disableLanguages) == 0 { + languages = languagesFromConfig + } else { + languages = make(maps.Params) + for k, v := range languagesFromConfig { + for _, disabled := range disableLanguages { + if disabled == defaultLang { + return c, nil, fmt.Errorf("cannot disable default language %q", defaultLang) + } + + if strings.EqualFold(k, disabled) { + v.(maps.Params)["disabled"] = true + break + } + } + languages[k] = v + } + } + + var languages2 Languages + + if len(languages) == 0 { + languages2 = append(languages2, NewDefaultLanguage(cfg)) + } else { + var err error + languages2, err = toSortedLanguages(cfg, languages) + if err != nil { + return c, nil, fmt.Errorf("Failed to parse multilingual config: %w", err) + } + } + + if oldLangs != nil { + // When in multihost mode, the languages are mapped to a server, so + // some structural language changes will need a restart of the dev server. + // The validation below isn't complete, but should cover the most + // important cases. + var invalid bool + if languages2.IsMultihost() != oldLangs.IsMultihost() { + invalid = true + } else { + if languages2.IsMultihost() && len(languages2) != len(oldLangs) { + invalid = true + } + } + + if invalid { + return c, nil, errors.New("language change needing a server restart detected") + } + + if languages2.IsMultihost() { + // We need to transfer any server baseURL to the new language + for i, ol := range oldLangs { + nl := languages2[i] + nl.Set("baseURL", ol.GetString("baseURL")) + } + } + } + + // The defaultContentLanguage is something the user has to decide, but it needs + // to match a language in the language definition list. + langExists := false + for _, lang := range languages2 { + if lang.Lang == defaultLang { + langExists = true + break + } + } + + if !langExists { + return c, nil, fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) + } + + c.Languages = languages2 + c.Multihost = languages2.IsMultihost() + c.DefaultContentLanguageInSubdir = c.Multihost + + sortedDefaultFirst := make(Languages, len(c.Languages)) + for i, v := range c.Languages { + sortedDefaultFirst[i] = v + } + sort.Slice(sortedDefaultFirst, func(i, j int) bool { + li, lj := sortedDefaultFirst[i], sortedDefaultFirst[j] + if li.Lang == defaultLang { + return true + } + + if lj.Lang == defaultLang { + return false + } + + return i < j + }) + + cfg.Set("languagesSorted", c.Languages) + cfg.Set("languagesSortedDefaultFirst", sortedDefaultFirst) + cfg.Set("multilingual", len(languages2) > 1) + + multihost := c.Multihost + + if multihost { + cfg.Set("defaultContentLanguageInSubdir", true) + cfg.Set("multihost", true) + } + + if multihost { + // The baseURL may be provided at the language level. If that is true, + // then every language must have a baseURL. In this case we always render + // to a language sub folder, which is then stripped from all the Permalink URLs etc. + for _, l := range languages2 { + burl := l.GetLocal("baseURL") + if burl == nil { + return c, nil, errors.New("baseURL must be set on all or none of the languages") + } + } + } + + for _, language := range c.Languages { + if language.initErr != nil { + return c, nil, language.initErr + } + } + + return c, nil, nil + } + + in := cfg.GetParams("languages") + + return config.DecodeNamespace[map[string]LanguageConfig](in, buildConfig) + +} + +// TODO1 remove this func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesConfig, err error) { defaultLang := strings.ToLower(cfg.GetString("defaultContentLanguage")) if defaultLang == "" { diff --git a/navigation/menu.go b/navigation/menu.go index cb280823cbb..0f1dcee896e 100644 --- a/navigation/menu.go +++ b/navigation/menu.go @@ -23,6 +23,7 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/compare" + "github.com/gohugoio/hugo/config" "github.com/spf13/cast" ) @@ -314,3 +315,60 @@ func (m *MenuEntry) Title() string { return "" } + +type MenuConfig struct { + Identifier string + Name string + Pre string + URL string + Weight int + // User defined params. + Params maps.Params +} + +func DecodeConfig(in any) (*config.ConfigNamespace[[]MenuConfig, Menus], error) { + buildConfig := func(in any) (Menus, any, error) { + ret := Menus{} + + if in == nil { + return ret, nil, nil + } + + menus, err := maps.ToStringMapE(in) + if err != nil { + return ret, nil, err + } + + for name, menu := range menus { + m, err := cast.ToSliceE(menu) + if err != nil { + return ret, nil, err + } else { + + for _, entry := range m { + menuEntry := MenuEntry{Menu: name} + ime, err := maps.ToStringMapE(entry) + if err != nil { + return ret, nil, err + } + err = menuEntry.MarshallMap(ime) + if err != nil { + return ret, nil, err + } + + // TODO1 menuEntry.ConfiguredURL = s.Info.createNodeMenuEntryURL(menuEntry.ConfiguredURL) + + if ret[name] == nil { + ret[name] = Menu{} + } + ret[name] = ret[name].Add(&menuEntry) + } + } + } + + return ret, nil, nil + + } + + return config.DecodeNamespace[[]MenuConfig](in, buildConfig) +} diff --git a/output/config.go b/output/config.go index 646ad0da5eb..d3b434b79aa 100644 --- a/output/config.go +++ b/output/config.go @@ -19,12 +19,73 @@ import ( "sort" "strings" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/media" "github.com/mitchellh/mapstructure" ) +func DecodeConfig(mediaTypes media.Types, in any) (*config.ConfigNamespace[map[string]Format, Formats], error) { + buildConfig := func(in any) (Formats, any, error) { + f := make(Formats, len(DefaultFormats)) + copy(f, DefaultFormats) + if in == nil { + return f, nil, nil + } + m, err := maps.ToStringMapE(in) + if err != nil { + return nil, nil, err + } + + for k, v := range m { + found := false + for i, vv := range f { + if strings.EqualFold(k, vv.Name) { + // Merge it with the existing + if err := decode(mediaTypes, v, &f[i]); err != nil { + return f, nil, err + } + found = true + } + } + if found { + continue + } + + var newOutFormat Format + newOutFormat.Name = k + if err := decode(mediaTypes, v, &newOutFormat); err != nil { + return f, nil, err + } + + // We need values for these + if newOutFormat.BaseName == "" { + newOutFormat.BaseName = "index" + } + if newOutFormat.Rel == "" { + newOutFormat.Rel = "alternate" + } + + f = append(f, newOutFormat) + + } + + // Also format is a map for documentation purposes. + docm := make(map[string]Format, len(f)) + for _, ff := range f { + m[ff.Name] = ff + } + + sort.Sort(f) + return f, docm, nil + } + + return config.DecodeNamespace[map[string]Format](in, buildConfig) +} + // DecodeFormats takes a list of output format configurations and merges those, // in the order given, with the Hugo defaults as the last resort. +// TODO1 remove this. func DecodeFormats(mediaTypes media.Types, maps ...map[string]any) (Formats, error) { f := make(Formats, len(DefaultFormats)) copy(f, DefaultFormats) diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go index e6605c80363..163af9d7539 100644 --- a/parser/lowercase_camel_json.go +++ b/parser/lowercase_camel_json.go @@ -57,3 +57,20 @@ func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) { return converted, err } + +type LowerCaseJSONMarshaller struct { + Value any +} + +func (c LowerCaseJSONMarshaller) MarshalJSON() ([]byte, error) { + marshalled, err := json.Marshal(c.Value) + + converted := keyMatchRegex.ReplaceAllFunc( + marshalled, + func(match []byte) []byte { + return bytes.ToLower(match) + }, + ) + + return converted, err +} diff --git a/resources/images/config.go b/resources/images/config.go index 17a6fa257aa..a3ca0c3597a 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -133,7 +133,7 @@ var ( "quality": defaultJPEGQuality, } - defaultImageConfig *config.ConfigNamespace[Imaging, ImagingConfigInternal] + defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] ) func init() { @@ -144,7 +144,7 @@ func init() { } } -func DecodeConfig(in map[string]any) (*config.ConfigNamespace[Imaging, ImagingConfigInternal], error) { +func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], error) { if in == nil { in = make(map[string]any) } @@ -189,7 +189,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[Imaging, ImagingCo return i, nil, nil } - ns, err := config.DecodeNamespace[Imaging](in, buildConfig) + ns, err := config.DecodeNamespace[ImagingConfig](in, buildConfig) if err != nil { return nil, fmt.Errorf("failed to decode media types: %w", err) } @@ -197,7 +197,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[Imaging, ImagingCo } -func DecodeImageConfig(action, config string, defaults *config.ConfigNamespace[Imaging, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) { +func DecodeImageConfig(action, config string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) { var ( c ImageConfig = GetDefaultImageConfig(action, defaults) err error @@ -412,10 +412,10 @@ type ImagingConfigInternal struct { ResampleFilter gift.Resampling Anchor gift.Anchor - Imaging Imaging + Imaging ImagingConfig } -func (i *ImagingConfigInternal) Compile(externalCfg *Imaging) error { +func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error { var err error i.BgColor, err = hexStringToColor(externalCfg.BgColor) if err != nil { @@ -440,9 +440,9 @@ func (i *ImagingConfigInternal) Compile(externalCfg *Imaging) error { } -// Imaging contains default image processing configuration. This will be fetched +// ImagingConfig contains default image processing configuration. This will be fetched // from site (or language) config. -type Imaging struct { +type ImagingConfig struct { // Default image quality setting (1-100). Only used for JPEG images. Quality int @@ -464,7 +464,7 @@ type Imaging struct { Exif ExifConfig } -func (cfg *Imaging) init() error { +func (cfg *ImagingConfig) init() error { if cfg.Quality < 0 || cfg.Quality > 100 { return errors.New("image quality must be a number between 1 and 100") } diff --git a/resources/images/image.go b/resources/images/image.go index 4a70e336682..530057d800e 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -175,7 +175,7 @@ func (i *Image) initConfig() error { return nil } -func NewImageProcessor(cfg *config.ConfigNamespace[Imaging, ImagingConfigInternal]) (*ImageProcessor, error) { +func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { e := cfg.Config.Imaging.Exif exifDecoder, err := exif.NewDecoder( exif.WithDateDisabled(e.DisableDate), @@ -194,7 +194,7 @@ func NewImageProcessor(cfg *config.ConfigNamespace[Imaging, ImagingConfigInterna } type ImageProcessor struct { - Cfg *config.ConfigNamespace[Imaging, ImagingConfigInternal] + Cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] exifDecoder *exif.Decoder } @@ -305,7 +305,7 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters return dst, nil } -func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[Imaging, ImagingConfigInternal]) ImageConfig { +func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig { if defaults == nil { defaults = defaultImageConfig } diff --git a/resources/page/page.go b/resources/page/page.go index eeb2cdb2809..21e04f0ef68 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -238,7 +238,7 @@ type PageMetaProvider interface { // Sitemap returns the sitemap configuration for this page. // This is for internal use only. - Sitemap() config.Sitemap + Sitemap() config.SitemapConfig // Type is a discriminator used to select layouts etc. It is typically set // in front matter, but will fall back to the root section. diff --git a/resources/page/page_marshaljson.autogen.go b/resources/page/page_marshaljson.autogen.go index 373257878eb..016286a0438 100644 --- a/resources/page/page_marshaljson.autogen.go +++ b/resources/page/page_marshaljson.autogen.go @@ -17,6 +17,9 @@ package page import ( "encoding/json" + "html/template" + "time" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/hugofs/files" @@ -25,8 +28,6 @@ import ( "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/source" - "html/template" - "time" ) func MarshalPageToJSON(p Page) ([]byte, error) { @@ -135,7 +136,7 @@ func MarshalPageToJSON(p Page) ([]byte, error) { Section string SectionsEntries []string SectionsPath string - Sitemap config.Sitemap + Sitemap config.SitemapConfig Type string Weight int Language *langs.Language diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go index 82897bb816a..d38b8d96bf1 100644 --- a/resources/page/page_matcher.go +++ b/resources/page/page_matcher.go @@ -83,6 +83,10 @@ func (m PageMatcher) Matches(p Page) bool { func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, map[PageMatcher]maps.Params], error) { buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) { + cascade := make(map[PageMatcher]maps.Params) + if in == nil { + return cascade, nil, nil + } ms, err := maps.ToSliceStringMap(in) if err != nil { return nil, nil, err @@ -98,8 +102,6 @@ func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsCon cfgs = append(cfgs, c) } - cascade := make(map[PageMatcher]maps.Params) - for _, cfg := range cfgs { m := cfg.Target c, found := cascade[m] @@ -152,7 +154,7 @@ func mapToPageMatcherParamsConfig(m map[string]any) (PageMatcherParamsConfig, er } // DecodeCascade decodes in which could be either a map or a slice of maps. -// TODO1 unexport. +// TODO1 remove. func DecodeCascade(in any) (map[PageMatcher]maps.Params, error) { m, err := maps.ToSliceStringMap(in) if err != nil { diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go index 40196976ec4..990312ed1eb 100644 --- a/resources/page/page_matcher_test.go +++ b/resources/page/page_matcher_test.go @@ -112,15 +112,16 @@ func TestDecodeCascadeConfig(t *testing.T) { in := []map[string]any{ { "params": map[string]any{ - "foo": "bar", + "a": "av", }, "target": map[string]any{ - "kind": "page", + "kind": "page", + "Environment": "production", }, }, { "params": map[string]any{ - "foo": "bar", + "b": "bv", }, "target": map[string]any{ "kind": "page", @@ -132,6 +133,26 @@ func TestDecodeCascadeConfig(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(got, qt.IsNotNil) - c.Assert(got.Config, qt.DeepEquals, "foo") + c.Assert(got.Config, qt.DeepEquals, + map[PageMatcher]maps.Params{ + {Path: "", Kind: "page", Lang: "", Environment: ""}: { + "b": "bv", + }, + {Path: "", Kind: "page", Lang: "", Environment: "production"}: { + "a": "av", + }, + }, + ) + c.Assert(got.SourceStructure, qt.DeepEquals, []PageMatcherParamsConfig{ + { + Params: maps.Params{"a": string("av")}, + Target: PageMatcher{Kind: "page", Environment: "production"}, + }, + {Params: maps.Params{"b": string("bv")}, Target: PageMatcher{Kind: "page"}}, + }) + + got, err = DecodeCascadeConfig(nil) + c.Assert(err, qt.IsNil) + c.Assert(got, qt.IsNotNil) } diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index c4af3f554e0..35893acb920 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -65,8 +65,8 @@ func (p *nopPage) Aliases() []string { return nil } -func (p *nopPage) Sitemap() config.Sitemap { - return config.Sitemap{} +func (p *nopPage) Sitemap() config.SitemapConfig { + return config.SitemapConfig{} } func (p *nopPage) Layout() string { diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index e8275ba4050..df79625e3fa 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -169,8 +169,8 @@ func (p *testPage) Data() any { return p.data } -func (p *testPage) Sitemap() config.Sitemap { - return config.Sitemap{} +func (p *testPage) Sitemap() config.SitemapConfig { + return config.SitemapConfig{} } func (p *testPage) Layout() string { diff --git a/resources/resource_spec.go b/resources/resource_spec.go index 8ef69318362..b9c91f884fb 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -88,7 +88,7 @@ func NewSpec( MediaTypes: mimeTypes, OutputFormats: outputFormats, Permalinks: permalinks, - BuildConfig: config.DecodeBuild(s.Cfg), + BuildConfig: config.DecodeBuildConfig(s.Cfg), FileCaches: fileCaches, PostBuildAssets: &PostBuildAssets{ PostProcessResources: make(map[string]postpub.PostPublishedResource), @@ -118,7 +118,7 @@ type Spec struct { TextTemplates tpl.TemplateParseFinder Permalinks page.PermalinkExpander - BuildConfig config.Build + BuildConfig config.BuildConfig // Holds default filter settings etc. imaging *images.ImageProcessor