From 8310bbc66bf29cbb1a1c72fe06949b3c295cba45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 18 Mar 2018 11:07:24 +0100 Subject: [PATCH] Allow themes to define output formats, media types and params Fixes #4490 --- Gopkg.lock | 8 +- Gopkg.toml | 4 + hugolib/config.go | 110 ++++++++++++++++++- hugolib/config_test.go | 207 +++++++++++++++++++++++++++++++++++- hugolib/site.go | 2 + hugolib/testhelpers_test.go | 33 +++++- 6 files changed, 356 insertions(+), 8 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3bdb1bf72e1..6951cc9c44d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -273,6 +273,12 @@ packages = ["."] revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5" +[[projects]] + name = "github.com/sanity-io/litter" + packages = ["."] + revision = "ae543b7ba8fd6af63e4976198f146e1348ae53c1" + version = "v1.1.0" + [[projects]] branch = "master" name = "github.com/shurcooL/sanitized_anchor_name" @@ -416,6 +422,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "960c7cc0b7d1739f5c1f116cc7fa31056b76cf16e63d865f31be875b8e674da7" + inputs-digest = "2057483645f34020ec672766f25c277b5187c9dd0a1116bb4a960c3636944e76" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5b65c46fc82..ea1e02b0dd8 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -141,3 +141,7 @@ name = "github.com/muesli/smartcrop" branch = "master" + +[[constraint]] + name = "github.com/sanity-io/litter" + version = "1.1.0" diff --git a/hugolib/config.go b/hugolib/config.go index e47e65435b1..822e96d235a 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -16,6 +16,7 @@ package hugolib import ( "errors" "fmt" + "path/filepath" "io" "strings" @@ -85,7 +86,8 @@ func LoadConfig(d ConfigSourceDescriptor) (*viper.Viper, error) { return v, err } - return v, nil + return v, loadThemeConfig(fs, v) + } func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error { @@ -201,6 +203,111 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error return nil } +func loadThemeConfig(fs afero.Fs, v1 *viper.Viper) error { + + theme := v1.GetString("theme") + if theme == "" { + return nil + } + + themesDir := v1.GetString("themesDir") + configDir := filepath.Join(themesDir, theme) + + var configPath string + var exists bool + var err error + + for _, exensionsToCheck := range []string{"toml", "yaml", "yml", "json"} { + configPath = filepath.Join(configDir, "config."+exensionsToCheck) + exists, err = helpers.Exists(configPath, fs) + if err != nil { + return err + } + if exists { + break + } + } + + if !exists { + return nil + } + + v2 := viper.New() + v2.SetFs(fs) + v2.AutomaticEnv() + v2.SetEnvPrefix("hugo") + v2.SetConfigFile(configPath) + + err = v2.ReadInConfig() + if err != nil { + return err + } + + for _, key := range []string{"params", "outputformats", "mediatypes"} { + mergeStringMapKeepLeft(key, v1, v2) + } + + themeLower := strings.ToLower(theme) + themeParamsNamespace := "params." + themeLower + // Set namespaced params + if v2.IsSet("params") && !v1.IsSet(themeParamsNamespace) { + v1.Set(themeParamsNamespace, v2.Get("params")) + } + + // Only add params, we do not add language definitions. + if v1.IsSet("languages") && v2.IsSet("languages") { + v1Langs := v1.GetStringMap("languages") + for k, _ := range v1Langs { + mergeStringMapKeepLeft("languages."+k+".params", v1, v2) + } + v2Langs := v2.GetStringMap("languages") + for k, _ := range v2Langs { + langParamsKey := "languages." + k + ".params" + langParamsThemeNamespace := langParamsKey + "." + themeLower + // Set namespaced params + if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) { + v1.Set(langParamsThemeNamespace, v2.Get(langParamsKey)) + } + } + } + + // Add menu definitions from theme not found in project + if v2.IsSet("menu") { + v2menus := v2.GetStringMap("menu") + for k, v := range v2menus { + if !v1.IsSet("menu." + k) { + v1.Set("menu."+k, v) + } + } + } + + return nil + +} + +func mergeStringMapKeepLeft(key string, v1, v2 *viper.Viper) { + if !v1.IsSet(key) { + if v2.IsSet(key) { + v1.Set(key, v2.Get(key)) + } + return + } + + if !v2.IsSet(key) { + return + } + + m1 := v1.GetStringMap(key) + m2 := v2.GetStringMap(key) + + for k, v := range m2 { + if _, found := m1[k]; !found { + m1[k] = v + } + } + +} + func loadDefaultSettingsFor(v *viper.Viper) error { c, err := helpers.NewContentSpec(v) @@ -282,4 +389,5 @@ lastmod = ["lastmod" ,":fileModTime", ":default"] } return loadLanguageSettings(v, nil) + } diff --git a/hugolib/config_test.go b/hugolib/config_test.go index ec543d93dc6..8364e7b39e6 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -17,13 +17,15 @@ import ( "testing" "github.com/spf13/afero" - "github.com/stretchr/testify/assert" + "github.com/spf13/viper" "github.com/stretchr/testify/require" ) func TestLoadConfig(t *testing.T) { t.Parallel() + assert := require.New(t) + // Add a random config variable for testing. // side = page in Norwegian. configContent := ` @@ -37,13 +39,16 @@ func TestLoadConfig(t *testing.T) { cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Name: "hugo.toml"}) require.NoError(t, err) - assert.Equal(t, "side", cfg.GetString("paginatePath")) + assert.Equal("side", cfg.GetString("paginatePath")) // default - assert.Equal(t, "layouts", cfg.GetString("layoutDir")) + assert.Equal("layouts", cfg.GetString("layoutDir")) } + func TestLoadMultiConfig(t *testing.T) { t.Parallel() + assert := require.New(t) + // Add a random config variable for testing. // side = page in Norwegian. configContentBase := ` @@ -62,6 +67,198 @@ func TestLoadMultiConfig(t *testing.T) { cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Name: "base.toml,override.toml"}) require.NoError(t, err) - assert.Equal(t, "top", cfg.GetString("paginatePath")) - assert.Equal(t, "same", cfg.GetString("DontChange")) + assert.Equal("top", cfg.GetString("paginatePath")) + assert.Equal("same", cfg.GetString("DontChange")) +} + +func TestLoadConfigFromTheme(t *testing.T) { + t.Parallel() + + // TODO(bep) menu + menu language (no merge) + + assert := require.New(t) + + mainConfig := ` +theme = "test-theme" +baseURL = "https://example.com/" + +[frontmatter] +date = ["date","publishDate"] + +[params] +p1 = "p1 main" +p2 = "p2 main" + +[mediaTypes] +[mediaTypes."text/m1"] +suffix = "m1main" + +[outputFormats.o1] +mediaType = "text/m1" +baseName = "o1main" + +[languages] +[languages.en] +languageName = "English" +[languages.en.params] +pl1 = "p1-en-main" +[languages.nb] +languageName = "Norsk" +[languages.nb.params] +pl1 = "p1-nb-main" + + +[[menu.main]] +name = "menu-main-main" + +` + + themeConfig := ` +baseURL = "http://bep.is/" + +# Can not be set in theme. +[frontmatter] +expiryDate = ["date"] + +[params] +p1 = "p1 theme" +p2 = "p2 theme" +p3 = "p3 theme" + +[mediaTypes] +[mediaTypes."text/m1"] +suffix = "m1theme" +[mediaTypes."text/m2"] +suffix = "m2theme" + +[outputFormats.o1] +mediaType = "text/m1" +baseName = "o1theme" +[outputFormats.o2] +mediaType = "text/m2" +baseName = "o2theme" + +[languages] +[languages.en] +languageName = "English2" +[languages.en.params] +pl1 = "p1-en-theme" +pl2 = "p2-en-theme" +[languages.nb] +languageName = "Norsk2" +[languages.nb.params] +pl1 = "p1-nb-theme" +pl2 = "p2-nb-theme" + +[[menu.main]] +name = "menu-main-theme" + +[[menu.thememenu]] +name = "menu-theme" + +` + + // TODO(bep) config watch + b := newTestSitesBuilder(t) + b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig) + b.CreateSites().Build(BuildCfg{}) + + got := b.Cfg.(*viper.Viper).AllSettings() + + b.AssertObject(` +map[string]interface {}{ + "p1": "p1 main", + "p2": "p2 main", + "p3": "p3 theme", + "test-theme": map[string]interface {}{ + "p1": "p1 theme", + "p2": "p2 theme", + "p3": "p3 theme", + }, +}`, got["params"]) + + b.AssertObject(` +map[string]interface {}{ + "date": []interface {}{ + "date", + "publishDate", + }, +}`, got["frontmatter"]) + + b.AssertObject(` +map[string]interface {}{ + "text/m1": map[string]interface {}{ + "suffix": "m1main", + }, + "text/m2": map[string]interface {}{ + "suffix": "m2theme", + }, +}`, got["mediatypes"]) + + b.AssertObject(` +map[string]interface {}{ + "o1": map[string]interface {}{ + "basename": "o1main", + "mediatype": Type{ + MainType: "text", + SubType: "m1", + Suffix: "m1main", + Delimiter: ".", + }, + }, + "o2": map[string]interface {}{ + "basename": "o2theme", + "mediatype": Type{ + MainType: "text", + SubType: "m2", + Suffix: "m2theme", + Delimiter: ".", + }, + }, +}`, got["outputformats"]) + + b.AssertObject(` +map[string]interface {}{ + "en": map[string]interface {}{ + "languagename": "English", + "params": map[string]interface {}{ + "pl1": "p1-en-main", + "pl2": "p2-en-theme", + "test-theme": map[string]interface {}{ + "pl1": "p1-en-theme", + "pl2": "p2-en-theme", + }, + }, + }, + "nb": map[string]interface {}{ + "languagename": "Norsk", + "params": map[string]interface {}{ + "pl1": "p1-nb-main", + "pl2": "p2-nb-theme", + "test-theme": map[string]interface {}{ + "pl1": "p1-nb-theme", + "pl2": "p2-nb-theme", + }, + }, + }, +} +`, got["languages"]) + + b.AssertObject(` +map[string]interface {}{ + "main": []interface {}{ + map[string]interface {}{ + "name": "menu-main-main", + }, + }, + "thememenu": []interface {}{ + map[string]interface {}{ + "name": "menu-theme", + }, + }, +} +`, got["menu"]) + + assert.Equal("https://example.com/", got["baseurl"]) + } diff --git a/hugolib/site.go b/hugolib/site.go index 2e8898bd6bf..0ffe153e919 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -296,6 +296,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) { // NewSiteDefaultLang creates a new site in the default language. // The site will have a template system loaded and ready to use. // Note: This is mainly used in single site tests. +// TODO(bep) test refactor -- remove func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { v := viper.New() if err := loadDefaultSettingsFor(v); err != nil { @@ -307,6 +308,7 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) ( // NewEnglishSite creates a new site in English language. // The site will have a template system loaded and ready to use. // Note: This is mainly used in single site tests. +// TODO(bep) test refactor -- remove func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { v := viper.New() if err := loadDefaultSettingsFor(v); err != nil { diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index ab23b343cf7..ceadf506b5a 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -10,6 +10,8 @@ import ( "strings" "text/template" + "github.com/sanity-io/litter" + jww "github.com/spf13/jwalterweatherman" "github.com/gohugoio/hugo/config" @@ -37,11 +39,15 @@ type sitesBuilder struct { Fs *hugofs.Fs T testing.TB + dumper litter.Options + // Aka the Hugo server mode. running bool H *HugoSites + theme string + // Default toml configFormat string @@ -63,7 +69,13 @@ func newTestSitesBuilder(t testing.TB) *sitesBuilder { v := viper.New() fs := hugofs.NewMem(v) - return &sitesBuilder{T: t, Fs: fs, configFormat: "toml"} + litterOptions := litter.Options{ + HidePrivateFields: true, + StripPackageNames: true, + Separator: " ", + } + + return &sitesBuilder{T: t, Fs: fs, configFormat: "toml", dumper: litterOptions} } func (s *sitesBuilder) Running() *sitesBuilder { @@ -97,6 +109,15 @@ func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder { return s } +func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder { + if s.theme == "" { + s.theme = "test-theme" + } + filename := filepath.Join("themes", s.theme, "config."+format) + writeSource(s.T, s.Fs, filename, conf) + return s +} + func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder { var config = ` baseURL = "http://example.com/" @@ -345,6 +366,16 @@ func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) { } } +func (s *sitesBuilder) AssertObject(expected string, object interface{}) { + got := s.dumper.Sdump(object) + expected = strings.TrimSpace(expected) + + if expected != got { + fmt.Println(got) + s.Fatalf("expected\n%s\ngot\n%s", expected, got) + } +} + func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) { content := readDestination(s.T, s.Fs, filename) for _, match := range matches {