From 84a44d0fa89b9d77621090070e10c79d4523f7f3 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 --- config/allconfig/allconfig.go | 117 ++++++++++---------- config/namespace.go | 24 +++-- config/namespace_test.go | 2 +- langs/config.go | 158 ++++++++++++++++++++++++++++ navigation/menu.go | 58 ++++++++++ output/config.go | 58 ++++++++++ resources/page/page_matcher.go | 8 +- resources/page/page_matcher_test.go | 29 ++++- 8 files changed, 378 insertions(+), 76 deletions(-) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 77410307b80..2fd74dd17ea 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -21,12 +21,16 @@ 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" @@ -45,12 +49,24 @@ type Config2 struct { Markup markup_config.Config `mapstructure:"-"` // Media type configuration. + // TODO1 language. MediaTypes *config.ConfigNamespace[map[string]media.DocsMediaTypeConfig, media.Types] `mapstructure:"-"` + // Output format configuration. + // TODO1 language. + Outputformats *config.ConfigNamespace[map[string]output.Format, output.Formats] `mapstructure:"-"` + Imaging *config.ConfigNamespace[images.Imaging, 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:"-"` @@ -80,23 +96,6 @@ type Config2 struct { // 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 +106,15 @@ type Config struct { // Services configuration. Services services.Config `mapstructure:"-"` + // Params configuration. + // TODO1 language + Params maps.Params +} + +// Config holds all configuration options in Hugo. +// TODO(bep) make sure that the keys gets the sensible casing in JSON etc. +type Config struct { + // Whether to build content marked as draft. BuildDrafts bool @@ -192,44 +200,7 @@ 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) { @@ -239,7 +210,6 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { m := cfg.Get("") fs := afero.NewMemMapFs() - //litter.Dump(m) if err = mapstructure.WeakDecode(m, &all); err != nil { return } @@ -253,6 +223,7 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { return } all.Build = config.DecodeBuild(cfg) + all.Markup, err = markup_config.Decode(cfg) if err != nil { return @@ -261,6 +232,8 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { if err != nil { return } + all.Outputformats, err = output.DecodeConfig(all.MediaTypes.Config, cfg) + all.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(cfg) if err != nil { return @@ -279,6 +252,12 @@ func FromProvider(cfg config.Provider) (all Config2, err error) { all.Sitemap = config.DecodeSitemap(config.Sitemap{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/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/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..0c0a2a91d1d 100644 --- a/output/config.go +++ b/output/config.go @@ -19,12 +19,70 @@ 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) { + m, err := maps.ToStringMapE(in) + if err != nil { + return nil, nil, err + } + f := make(Formats, len(DefaultFormats)) + copy(f, DefaultFormats) + + 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/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) }