diff --git a/helpers/path.go b/helpers/path.go index 92b58de8470..b463cac941c 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -274,6 +274,13 @@ func FileAndExt(in string) (string, string) { return fileAndExt(in, fpb) } +// FileAndExtNoDelimiter takes a path and returns the file and extension separated, +// the extension excluding the delmiter, e.g "md". +func FileAndExtNoDelimiter(in string) (string, string) { + file, ext := fileAndExt(in, fpb) + return file, strings.TrimPrefix(ext, ".") +} + // Filename takes a path, strips out the extension, // and returns the name of the file. func Filename(in string) (name string) { diff --git a/htesting/testdata_builder.go b/htesting/testdata_builder.go new file mode 100644 index 00000000000..a42d3ea16ae --- /dev/null +++ b/htesting/testdata_builder.go @@ -0,0 +1,53 @@ +// Copyright 2018 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package htesting + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" +) + +type testFile struct { + name string + content string +} + +type testdataBuilder struct { + t testing.TB + fs afero.Fs + workingDir string + + files []testFile +} + +func NewTestdataBuilder(fs afero.Fs, workingDir string, t testing.TB) *testdataBuilder { + workingDir = filepath.Clean(workingDir) + return &testdataBuilder{fs: fs, workingDir: workingDir, t: t} +} + +func (b *testdataBuilder) Add(filename, content string) *testdataBuilder { + b.files = append(b.files, testFile{name: filename, content: content}) + return b +} + +func (b *testdataBuilder) Build() *testdataBuilder { + for _, f := range b.files { + if err := afero.WriteFile(b.fs, filepath.Join(b.workingDir, f.name), []byte(f.content), 0666); err != nil { + b.t.Fatalf("failed to add %q: %s", f.name, err) + } + } + return b +} diff --git a/hugolib/config.go b/hugolib/config.go index 77ebb42ae6f..64475fe5580 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -14,14 +14,17 @@ package hugolib import ( - "errors" "fmt" "io" + "io/ioutil" + "os" "strings" - "github.com/gohugoio/hugo/common/herrors" + "github.com/pkg/errors" + "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/hugolib/paths" + "github.com/gohugoio/hugo/parser/metadecoders" _errors "github.com/pkg/errors" "github.com/gohugoio/hugo/langs" @@ -74,6 +77,9 @@ type ConfigSourceDescriptor struct { // The project's working dir. Is used to look for additional theme config. WorkingDir string + + // The (optional) directory for additional configuration files. + AbsConfigDir string } func (d ConfigSourceDescriptor) configFilenames() []string { @@ -95,6 +101,11 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid fs := d.Fs v := viper.New() + + // Set up aliases + // TODO(bep) config update docs + v.RegisterAlias("menu", "menus") + v.SetFs(fs) if d.Path == "" { @@ -135,7 +146,7 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid } for _, configFile := range configFilenames[1:] { - var r io.Reader + var r io.ReadCloser var err error if r, err = fs.Open(configFile); err != nil { return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err) @@ -143,11 +154,16 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid if err = v.MergeConfig(r); err != nil { return nil, configFiles, applyFileContext(configFile, err) } + r.Close() configFiles = append(configFiles, configFile) } } + if configFileErr == nil && d.AbsConfigDir != "" { + configFileErr = loadConfigFromConfigDir(d.Fs, d.AbsConfigDir, v) + } + if err := loadDefaultSettingsFor(v); err != nil { return v, configFiles, err } @@ -180,6 +196,71 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid } +func loadConfigFromConfigDir(fs afero.Fs, configDir string, v *viper.Viper) error { + return afero.Walk(fs, configDir, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() { + // TODO(bep) config hugofs.LanguageAnnouncer language merge earlier on fs level? + rel := strings.TrimPrefix(path, configDir) + rel = strings.TrimPrefix(rel, helpers.FilePathSeparator) + name, ext := helpers.FileAndExtNoDelimiter(rel) + name, ext = strings.ToLower(name), strings.ToLower(ext) + + var r io.ReadCloser + var err error + if r, err = fs.Open(path); err != nil { + return errors.Wrapf(err, "unable to open Config file %q: %s", path, err) + } + + switch name { + case "config": + if err = v.MergeConfig(r); err != nil { + // TODO(bep) config File error context this and the others + return errors.Wrapf(err, "unable to merge Config file %q: %s", path, err) + } + default: + + format := metadecoders.FormatFromString(ext) + if format == "" { + return errors.Errorf("config format %q in %q not supported", ext, rel) + } + + // Can be params.jp, menus.en etc. + name, lang := helpers.FileAndExtNoDelimiter(name) + + if lang != "" { + name = "languages." + lang + switch name { + case "menu", "menus": + name = name + ".menus" + case "params": + name = name + ".params" + } + + fmt.Println(">>>", name, lang) + } + + b, err := ioutil.ReadAll(r) + if err != nil { + return err + } + m, err := metadecoders.Unmarshal(b, format) + if err != nil { + return err + } + + if v.IsSet(name) { + fmt.Println(">>>", name, "is") + } + + v.Set(name, m) + } + + r.Close() + } + return nil + }) +} + func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error { defaultLang := cfg.GetString("defaultContentLanguage") @@ -293,7 +374,6 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir")) themes := config.GetStringSlicePreserveString(v1, "theme") - // CollectThemes(fs afero.Fs, themesDir string, themes []strin themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes) if err != nil { return nil, err @@ -324,7 +404,8 @@ func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error { const ( paramsKey = "params" languagesKey = "languages" - menuKey = "menu" + // TODO(bep) config vs alias + menuKey = "menu" ) v2 := theme.Cfg @@ -378,7 +459,7 @@ func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error { } // Add menu definitions from theme not found in project - if v2.IsSet("menu") { + if v2.IsSet(menuKey) { v2menus := v2.GetStringMap(menuKey) for k, v := range v2menus { menuEntry := menuKey + "." + k diff --git a/hugolib/configdir_test.go b/hugolib/configdir_test.go new file mode 100644 index 00000000000..c9c6f2474cc --- /dev/null +++ b/hugolib/configdir_test.go @@ -0,0 +1,98 @@ +// Copyright 2018 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. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "fmt" + "testing" + + "github.com/gohugoio/hugo/htesting" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func TestLoadConfigDir(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + // Add a random config variable for testing. + // side = page in Norwegian. + configContent := ` +baseURL = "https://example.org" + +[languages.en] +weight = 0 +languageName = "English" + +[languages.no] +weight = 10 +languageName = "FOO" + +[params] +p1 = "p1base" + +` + + mm := afero.NewMemMapFs() + + writeToFs(t, mm, "hugo.toml", configContent) + + fb := htesting.NewTestdataBuilder(mm, "config", t) + + // Will replace any settings in the base + fb.Add("config.toml", `paginatePath = "side"`) + + fb.Add("params.yaml", `p2: "p2params"`) + 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"`) + fb.Add("languages.no.toml", `languageName = "Norsk"`) + + fb.Build() + + cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "hugo.toml", AbsConfigDir: "config"}) + require.NoError(t, err) + + // Set in /config/config.toml + assert.Equal("side", cfg.GetString("paginatePath")) + + fmt.Println(">>LA", cfg.Get("languages")) + + assert.Equal("Norsk", cfg.GetString("languages.no.languageName")) + assert.Equal(10, cfg.Get("languages.no.weight")) + + assert.Equal("p1base", cfg.GetString("params.p1")) + assert.Equal("p2params", cfg.GetString("params.p2")) + assert.Equal("p3params", cfg.GetString("languages.no.params.p3")) + + assert.Equal(2, len(cfg.Get("menus.docs").(([]map[string]interface{})))) + noMenus := cfg.Get("languages.no.menus.docs") + assert.NotNil(noMenus) + assert.Equal(1, len(noMenus.(([]map[string]interface{})))) + +}