diff --git a/Gopkg.lock b/Gopkg.lock index 1b766e9ff..bf3c7dc6c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -163,6 +163,7 @@ ".", "hcl/ast", "hcl/parser", + "hcl/printer", "hcl/scanner", "hcl/strconv", "hcl/token", @@ -274,6 +275,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" @@ -331,8 +338,8 @@ [[projects]] name = "github.com/spf13/viper" packages = ["."] - revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" - version = "v1.0.0" + revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" + version = "v1.0.2" [[projects]] name = "github.com/stretchr/testify" @@ -417,6 +424,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4657586103d844434bda6db23d03f30e2ae0db16dc48746b9559ce742902535a" + inputs-digest = "13ab39f8bfafadc12c05726e565ee3f3d94bf7d6c0e8adf04056de0691bf2dd6" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 4e0cd5c6b..fc1af824b 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/commands/commandeer.go b/commands/commandeer.go index a69ce2084..e96c97814 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -14,6 +14,18 @@ package commands import ( + "os" + "path/filepath" + "sync" + + "github.com/spf13/cobra" + + "github.com/gohugoio/hugo/utils" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" @@ -23,11 +35,22 @@ import ( type commandeer struct { *deps.DepsCfg + + subCmdVs []*cobra.Command + pathSpec *helpers.PathSpec visitedURLs *types.EvictingStringQueue staticDirsConfig []*src.Dirs + // We watch these for changes. + configFiles []string + + doWithCommandeer func(c *commandeer) error + + // We can do this only once. + fsCreate sync.Once + serverPorts []int languages helpers.Languages @@ -65,16 +88,158 @@ func (c *commandeer) initFs(fs *hugofs.Fs) error { return nil } -func newCommandeer(cfg *deps.DepsCfg, running bool) (*commandeer, error) { - cfg.Running = running +func newCommandeer(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) { - var languages helpers.Languages + c := &commandeer{ + doWithCommandeer: doWithCommandeer, + subCmdVs: append([]*cobra.Command{hugoCmdV}, subCmdVs...), + visitedURLs: types.NewEvictingStringQueue(10)} - if l, ok := cfg.Cfg.Get("languagesSorted").(helpers.Languages); ok { - languages = l + return c, c.loadConfig(running) +} + +func (c *commandeer) loadConfig(running bool) error { + + if c.DepsCfg == nil { + c.DepsCfg = &deps.DepsCfg{} } - c := &commandeer{DepsCfg: cfg, languages: languages, visitedURLs: types.NewEvictingStringQueue(10)} + cfg := c.DepsCfg + c.configured = false + cfg.Running = running + + var dir string + if source != "" { + dir, _ = filepath.Abs(source) + } else { + dir, _ = os.Getwd() + } + + var sourceFs afero.Fs = hugofs.Os + if c.DepsCfg.Fs != nil { + sourceFs = c.DepsCfg.Fs.Source + } + + config, configFiles, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: source, WorkingDir: dir, Filename: cfgFile}) + if err != nil { + return err + } + + c.Cfg = config + c.configFiles = configFiles + + for _, cmdV := range c.subCmdVs { + c.initializeFlags(cmdV) + } + + if l, ok := c.Cfg.Get("languagesSorted").(helpers.Languages); ok { + c.languages = l + } + + if baseURL != "" { + config.Set("baseURL", baseURL) + } + + if c.doWithCommandeer != nil { + err = c.doWithCommandeer(c) + } + + if err != nil { + return err + } + + if len(disableKinds) > 0 { + c.Set("disableKinds", disableKinds) + } + + logger, err := createLogger(cfg.Cfg) + if err != nil { + return err + } + + cfg.Logger = logger + + config.Set("logI18nWarnings", logI18nWarnings) + + if theme != "" { + config.Set("theme", theme) + } + + if themesDir != "" { + config.Set("themesDir", themesDir) + } + + if destination != "" { + config.Set("publishDir", destination) + } + + config.Set("workingDir", dir) + + if contentDir != "" { + config.Set("contentDir", contentDir) + } + + if layoutDir != "" { + config.Set("layoutDir", layoutDir) + } + + if cacheDir != "" { + config.Set("cacheDir", cacheDir) + } + + createMemFs := config.GetBool("renderToMemory") + + if createMemFs { + // Rendering to memoryFS, publish to Root regardless of publishDir. + config.Set("publishDir", "/") + } + + c.fsCreate.Do(func() { + fs := hugofs.NewFrom(sourceFs, config) + + // Hugo writes the output to memory instead of the disk. + if createMemFs { + fs.Destination = new(afero.MemMapFs) + } + + err = c.initFs(fs) + }) + + if err != nil { + return err + } + + cacheDir = config.GetString("cacheDir") + if cacheDir != "" { + if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] { + cacheDir = cacheDir + helpers.FilePathSeparator + } + isDir, err := helpers.DirExists(cacheDir, sourceFs) + utils.CheckErr(cfg.Logger, err) + if !isDir { + mkdir(cacheDir) + } + config.Set("cacheDir", cacheDir) + } else { + config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs)) + } + + cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed()) + + themeDir := c.PathSpec().GetThemeDir() + if themeDir != "" { + if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) { + return newSystemError("Unable to find theme Directory:", themeDir) + } + } + + themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs) + + if themeVersionMismatch { + cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n", + helpers.CurrentHugoVersion.ReleaseVersion(), minVersion) + } + + return nil - return c, nil } diff --git a/commands/hugo.go b/commands/hugo.go index b041fad38..a5b2c8895 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -25,8 +25,6 @@ import ( "golang.org/x/sync/errgroup" - "github.com/gohugoio/hugo/hugofs" - "log" "os" "path/filepath" @@ -44,7 +42,6 @@ import ( "regexp" "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/livereload" @@ -55,7 +52,6 @@ import ( "github.com/spf13/fsync" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/nitro" - "github.com/spf13/viper" ) // Hugo represents the Hugo sites to build. This variable is exported as it @@ -142,10 +138,6 @@ Complete documentation is available at http://gohugo.io/.`, return err } - if buildWatch { - c.watchConfig() - } - return c.build() }, } @@ -301,129 +293,11 @@ func init() { // InitializeConfig initializes a config file with sensible default configuration flags. func InitializeConfig(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) { - var cfg *deps.DepsCfg = &deps.DepsCfg{} - - // Init file systems. This may be changed at a later point. - osFs := hugofs.Os - - config, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: osFs, Src: source, Name: cfgFile}) + c, err := newCommandeer(running, doWithCommandeer, subCmdVs...) if err != nil { return nil, err } - // Init file systems. This may be changed at a later point. - cfg.Cfg = config - - c, err := newCommandeer(cfg, running) - if err != nil { - return nil, err - } - - for _, cmdV := range append([]*cobra.Command{hugoCmdV}, subCmdVs...) { - c.initializeFlags(cmdV) - } - - if baseURL != "" { - config.Set("baseURL", baseURL) - } - - if doWithCommandeer != nil { - if err := doWithCommandeer(c); err != nil { - return nil, err - } - } - - if len(disableKinds) > 0 { - c.Set("disableKinds", disableKinds) - } - - logger, err := createLogger(cfg.Cfg) - if err != nil { - return nil, err - } - - cfg.Logger = logger - - config.Set("logI18nWarnings", logI18nWarnings) - - if theme != "" { - config.Set("theme", theme) - } - - if themesDir != "" { - config.Set("themesDir", themesDir) - } - - if destination != "" { - config.Set("publishDir", destination) - } - - var dir string - if source != "" { - dir, _ = filepath.Abs(source) - } else { - dir, _ = os.Getwd() - } - config.Set("workingDir", dir) - - if contentDir != "" { - config.Set("contentDir", contentDir) - } - - if layoutDir != "" { - config.Set("layoutDir", layoutDir) - } - - if cacheDir != "" { - config.Set("cacheDir", cacheDir) - } - - fs := hugofs.NewFrom(osFs, config) - - // Hugo writes the output to memory instead of the disk. - // This is only used for benchmark testing. Cause the content is only visible - // in memory. - if config.GetBool("renderToMemory") { - fs.Destination = new(afero.MemMapFs) - // Rendering to memoryFS, publish to Root regardless of publishDir. - config.Set("publishDir", "/") - } - - cacheDir = config.GetString("cacheDir") - if cacheDir != "" { - if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] { - cacheDir = cacheDir + helpers.FilePathSeparator - } - isDir, err := helpers.DirExists(cacheDir, fs.Source) - utils.CheckErr(cfg.Logger, err) - if !isDir { - mkdir(cacheDir) - } - config.Set("cacheDir", cacheDir) - } else { - config.Set("cacheDir", helpers.GetTempDir("hugo_cache", fs.Source)) - } - - if err := c.initFs(fs); err != nil { - return nil, err - } - - cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed()) - - themeDir := c.PathSpec().GetThemeDir() - if themeDir != "" { - if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) { - return nil, newSystemError("Unable to find theme Directory:", themeDir) - } - } - - themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch() - - if themeVersionMismatch { - cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n", - helpers.CurrentHugoVersion.ReleaseVersion(), minVersion) - } - return c, nil } @@ -524,20 +398,6 @@ If you need to set this configuration value from the command line, set it via an } } -func (c *commandeer) watchConfig() { - v := c.Cfg.(*viper.Viper) - v.WatchConfig() - v.OnConfigChange(func(e fsnotify.Event) { - c.Logger.FEEDBACK.Println("Config file changed:", e.Name) - // Force a full rebuild - utils.CheckErr(c.Logger, c.recreateAndBuildSites(true)) - if !c.Cfg.GetBool("disableLiveReload") { - // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized - livereload.ForceRefresh() - } - }) -} - func (c *commandeer) fullBuild() error { var ( g errgroup.Group @@ -942,6 +802,7 @@ func (c *commandeer) resetAndBuildSites() (err error) { func (c *commandeer) initSites() error { if Hugo != nil { + Hugo.Cfg = c.Cfg Hugo.Log.ResetLogCounters() return nil } @@ -1009,6 +870,15 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) { } } + // Identifies changes to config (config.toml) files. + configSet := make(map[string]bool) + + for _, configFile := range c.configFiles { + c.Logger.FEEDBACK.Println("Watching for config changes in", configFile) + watcher.Add(configFile) + configSet[configFile] = true + } + go func() { for { select { @@ -1021,6 +891,21 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) { // Special handling for symbolic links inside /content. filtered := []fsnotify.Event{} for _, ev := range evs { + if configSet[ev.Name] { + if ev.Op&fsnotify.Chmod == fsnotify.Chmod { + continue + } + // Config file changed. Need full rebuild. + if err := c.loadConfig(true); err != nil { + jww.ERROR.Println("Failed to reload config:", err) + } else if err := c.recreateAndBuildSites(true); err != nil { + jww.ERROR.Println(err) + } else if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { + livereload.ForceRefresh() + } + break + } + // Check the most specific first, i.e. files. contentMapped := Hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name) if len(contentMapped) > 0 { @@ -1212,7 +1097,7 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string { // isThemeVsHugoVersionMismatch returns whether the current Hugo version is // less than the theme's min_version. -func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) { +func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) { if !c.PathSpec().ThemeSet() { return } @@ -1221,13 +1106,13 @@ func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinV path := filepath.Join(themeDir, "theme.toml") - exists, err := helpers.Exists(path, c.Fs.Source) + exists, err := helpers.Exists(path, fs) if err != nil || !exists { return } - b, err := afero.ReadFile(c.Fs.Source, path) + b, err := afero.ReadFile(fs, path) tomlMeta, err := parser.HandleTOMLMetaData(b) diff --git a/commands/server.go b/commands/server.go index 130ac18be..278ba7f37 100644 --- a/commands/server.go +++ b/commands/server.go @@ -24,6 +24,7 @@ import ( "runtime" "strconv" "strings" + "sync" "syscall" "time" @@ -111,12 +112,16 @@ func init() { } +var serverPorts []int + func server(cmd *cobra.Command, args []string) error { // If a Destination is provided via flag write to disk if destination != "" { renderToDisk = true } + var serverCfgInit sync.Once + cfgInit := func(c *commandeer) error { c.Set("renderToMemory", !renderToDisk) if cmd.Flags().Changed("navigateToChanged") { @@ -132,37 +137,42 @@ func server(cmd *cobra.Command, args []string) error { c.Set("watch", true) } - serverPorts := make([]int, 1) + var err error - if c.languages.IsMultihost() { - if !serverAppend { - return newSystemError("--appendPort=false not supported when in multihost mode") - } - serverPorts = make([]int, len(c.languages)) - } + // We can only do this once. + serverCfgInit.Do(func() { + serverPorts = make([]int, 1) - currentServerPort := serverPort - - for i := 0; i < len(serverPorts); i++ { - l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort))) - if err == nil { - l.Close() - serverPorts[i] = currentServerPort - } else { - if i == 0 && serverCmd.Flags().Changed("port") { - // port set explicitly by user -- he/she probably meant it! - return newSystemErrorF("Server startup failed: %s", err) + if c.languages.IsMultihost() { + if !serverAppend { + err = newSystemError("--appendPort=false not supported when in multihost mode") } - jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port") - sp, err := helpers.FindAvailablePort() - if err != nil { - return newSystemError("Unable to find alternative port to use:", err) - } - serverPorts[i] = sp.Port + serverPorts = make([]int, len(c.languages)) } - currentServerPort = serverPorts[i] + 1 - } + currentServerPort := serverPort + + for i := 0; i < len(serverPorts); i++ { + l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort))) + if err == nil { + l.Close() + serverPorts[i] = currentServerPort + } else { + if i == 0 && serverCmd.Flags().Changed("port") { + // port set explicitly by user -- he/she probably meant it! + err = newSystemErrorF("Server startup failed: %s", err) + } + jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port") + sp, err := helpers.FindAvailablePort() + if err != nil { + err = newSystemError("Unable to find alternative port to use:", err) + } + serverPorts[i] = sp.Port + } + + currentServerPort = serverPorts[i] + 1 + } + }) c.serverPorts = serverPorts @@ -184,7 +194,7 @@ func server(cmd *cobra.Command, args []string) error { baseURL, err := fixURL(language, baseURL, serverPort) if err != nil { - return err + return nil } if isMultiHost { language.Set("baseURL", baseURL) @@ -194,7 +204,7 @@ func server(cmd *cobra.Command, args []string) error { } } - return nil + return err } @@ -215,10 +225,6 @@ func server(cmd *cobra.Command, args []string) error { s.RegisterMediaTypes() } - if serverWatch { - c.watchConfig() - } - // Watch runs its own server as part of the routine if serverWatch { diff --git a/helpers/path.go b/helpers/path.go index 44d53d018..0a8544357 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -154,11 +154,16 @@ func ReplaceExtension(path string, newExt string) string { // AbsPathify creates an absolute path if given a relative path. If already // absolute, the path is just cleaned. func (p *PathSpec) AbsPathify(inPath string) string { + return AbsPathify(p.workingDir, inPath) +} + +// AbsPathify creates an absolute path if given a working dir and arelative path. +// If already absolute, the path is just cleaned. +func AbsPathify(workingDir, inPath string) string { if filepath.IsAbs(inPath) { return filepath.Clean(inPath) } - - return filepath.Join(p.workingDir, inPath) + return filepath.Join(workingDir, inPath) } // GetLayoutDirPath returns the absolute path to the layout file dir diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go index 680a701aa..52ef198a5 100644 --- a/hugolib/case_insensitive_test.go +++ b/hugolib/case_insensitive_test.go @@ -149,7 +149,7 @@ func TestCaseInsensitiveConfigurationVariations(t *testing.T) { caseMixingTestsWriteCommonSources(t, mm) - cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm}) + cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"}) require.NoError(t, err) fs := hugofs.NewFrom(mm, cfg) diff --git a/hugolib/config.go b/hugolib/config.go index e47e65435..6eca1a969 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -16,6 +16,7 @@ package hugolib import ( "errors" "fmt" + "path/filepath" "io" "strings" @@ -28,64 +29,91 @@ import ( // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). type ConfigSourceDescriptor struct { - Fs afero.Fs - Src string - Name string + Fs afero.Fs + + // Full path to the config file to use, i.e. /my/project/config.toml + Filename string + + // The path to the directory to look for configuration. Is used if Filename is not + // set. + Path string + + // The project's working dir. Is used to look for additional theme config. + WorkingDir string } func (d ConfigSourceDescriptor) configFilenames() []string { - return strings.Split(d.Name, ",") + return strings.Split(d.Filename, ",") } // LoadConfigDefault is a convenience method to load the default "config.toml" config. func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) { - return LoadConfig(ConfigSourceDescriptor{Fs: fs, Name: "config.toml"}) + v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"}) + return v, err } // LoadConfig loads Hugo configuration into a new Viper and then adds // a set of defaults. -func LoadConfig(d ConfigSourceDescriptor) (*viper.Viper, error) { +func LoadConfig(d ConfigSourceDescriptor) (*viper.Viper, []string, error) { + var configFiles []string + fs := d.Fs v := viper.New() v.SetFs(fs) - if d.Name == "" { - d.Name = "config.toml" - } - - if d.Src == "" { - d.Src = "." + if d.Path == "" { + d.Path = "." } configFilenames := d.configFilenames() v.AutomaticEnv() v.SetEnvPrefix("hugo") v.SetConfigFile(configFilenames[0]) - v.AddConfigPath(d.Src) + v.AddConfigPath(d.Path) err := v.ReadInConfig() if err != nil { if _, ok := err.(viper.ConfigParseError); ok { - return nil, err + return nil, configFiles, err } - return nil, fmt.Errorf("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details. (%s)\n", err) + return nil, configFiles, fmt.Errorf("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details. (%s)\n", err) } + + if cf := v.ConfigFileUsed(); cf != "" { + configFiles = append(configFiles, cf) + } + for _, configFile := range configFilenames[1:] { var r io.Reader var err error if r, err = fs.Open(configFile); err != nil { - return nil, fmt.Errorf("Unable to open Config file.\n (%s)\n", err) + return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err) } if err = v.MergeConfig(r); err != nil { - return nil, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err) + return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err) } + configFiles = append(configFiles, configFile) } if err := loadDefaultSettingsFor(v); err != nil { - return v, err + return v, configFiles, err } - return v, nil + themeConfigFile, err := loadThemeConfig(d, v) + if err != nil { + return v, configFiles, err + } + + if themeConfigFile != "" { + configFiles = append(configFiles, themeConfigFile) + } + + if err := loadLanguageSettings(v, nil); err != nil { + return v, configFiles, err + } + + return v, configFiles, nil + } func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error { @@ -201,6 +229,142 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error return nil } +func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error) { + + theme := v1.GetString("theme") + if theme == "" { + return "", nil + } + + themesDir := helpers.AbsPathify(d.WorkingDir, v1.GetString("themesDir")) + configDir := filepath.Join(themesDir, theme) + + var ( + configPath string + exists bool + err error + ) + + // Viper supports more, but this is the sub-set supported by Hugo. + for _, configFormats := range []string{"toml", "yaml", "yml", "json"} { + configPath = filepath.Join(configDir, "config."+configFormats) + exists, err = helpers.Exists(configPath, d.Fs) + if err != nil { + return "", err + } + if exists { + break + } + } + + if !exists { + // No theme config set. + return "", nil + } + + v2 := viper.New() + v2.SetFs(d.Fs) + v2.AutomaticEnv() + v2.SetEnvPrefix("hugo") + v2.SetConfigFile(configPath) + + err = v2.ReadInConfig() + if err != nil { + return "", err + } + + const ( + paramsKey = "params" + languagesKey = "languages" + menuKey = "menu" + ) + + for _, key := range []string{paramsKey, "outputformats", "mediatypes"} { + mergeStringMapKeepLeft("", key, v1, v2) + } + + themeLower := strings.ToLower(theme) + themeParamsNamespace := paramsKey + "." + themeLower + + // Set namespaced params + if v2.IsSet(paramsKey) && !v1.IsSet(themeParamsNamespace) { + // Set it in the default store to make sure it gets in the same or + // behind the others. + v1.SetDefault(themeParamsNamespace, v2.Get(paramsKey)) + } + + // Only add params and new menu entries, we do not add language definitions. + if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) { + v1Langs := v1.GetStringMap(languagesKey) + for k, _ := range v1Langs { + langParamsKey := languagesKey + "." + k + "." + paramsKey + mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2) + } + v2Langs := v2.GetStringMap(languagesKey) + for k, _ := range v2Langs { + if k == "" { + continue + } + langParamsKey := languagesKey + "." + k + "." + paramsKey + langParamsThemeNamespace := langParamsKey + "." + themeLower + // Set namespaced params + if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) { + v1.SetDefault(langParamsThemeNamespace, v2.Get(langParamsKey)) + } + + langMenuKey := languagesKey + "." + k + "." + menuKey + if v2.IsSet(langMenuKey) { + // Only add if not in the main config. + v2menus := v2.GetStringMap(langMenuKey) + for k, v := range v2menus { + menuEntry := menuKey + "." + k + menuLangEntry := langMenuKey + "." + k + if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) { + v1.Set(menuLangEntry, v) + } + } + } + } + } + + // Add menu definitions from theme not found in project + if v2.IsSet("menu") { + v2menus := v2.GetStringMap(menuKey) + for k, v := range v2menus { + menuEntry := menuKey + "." + k + if !v1.IsSet(menuEntry) { + v1.SetDefault(menuEntry, v) + } + } + } + + return v2.ConfigFileUsed(), nil + +} + +func mergeStringMapKeepLeft(rootKey, key string, v1, v2 *viper.Viper) { + if !v2.IsSet(key) { + return + } + + if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) { + v1.Set(key, v2.Get(key)) + return + } + + m1 := v1.GetStringMap(key) + m2 := v2.GetStringMap(key) + + for k, v := range m2 { + if _, found := m1[k]; !found { + if rootKey != "" && v1.IsSet(rootKey+"."+k) { + continue + } + m1[k] = v + } + } +} + func loadDefaultSettingsFor(v *viper.Viper) error { c, err := helpers.NewContentSpec(v) @@ -281,5 +445,5 @@ lastmod = ["lastmod" ,":fileModTime", ":default"] } - return loadLanguageSettings(v, nil) + return nil } diff --git a/hugolib/config_test.go b/hugolib/config_test.go index ec543d93d..441bcf541 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 := ` @@ -34,16 +36,19 @@ func TestLoadConfig(t *testing.T) { writeToFs(t, mm, "hugo.toml", configContent) - cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Name: "hugo.toml"}) + cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "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 := ` @@ -59,9 +64,304 @@ func TestLoadMultiConfig(t *testing.T) { writeToFs(t, mm, "override.toml", configContentSub) - cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Name: "base.toml,override.toml"}) + cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "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() + + assert := require.New(t) + + mainConfigBasic := ` +theme = "test-theme" +baseURL = "https://example.com/" + +` + mainConfig := ` +theme = "test-theme" +baseURL = "https://example.com/" + +[frontmatter] +date = ["date","publishDate"] + +[params] +p1 = "p1 main" +p2 = "p2 main" +top = "top" + +[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" + +[[menu.top]] +name = "menu-top-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.en.menu.main]] +name = "menu-lang-en-main" +[[languages.en.menu.theme]] +name = "menu-lang-en-theme" +[languages.nb] +languageName = "Norsk2" +[languages.nb.params] +pl1 = "p1-nb-theme" +pl2 = "p2-nb-theme" +top = "top-nb-theme" +[[languages.nb.menu.main]] +name = "menu-lang-nb-main" +[[languages.nb.menu.theme]] +name = "menu-lang-nb-theme" +[[languages.nb.menu.top]] +name = "menu-lang-nb-top" + +[[menu.main]] +name = "menu-main-theme" + +[[menu.thememenu]] +name = "menu-theme" + +` + + 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", + }, + "top": "top", +}`, 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", + "menu": map[string]interface {}{ + "theme": []interface {}{ + map[string]interface {}{ + "name": "menu-lang-en-theme", + }, + }, + }, + "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", + "menu": map[string]interface {}{ + "theme": []interface {}{ + map[string]interface {}{ + "name": "menu-lang-nb-theme", + }, + }, + }, + "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", + "top": "top-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", + }, + }, + "top": []interface {}{ + map[string]interface {}{ + "name": "menu-top-main", + }, + }, +} +`, got["menu"]) + + assert.Equal("https://example.com/", got["baseurl"]) + + if true { + return + } + // Test variants with only values from theme + b = newTestSitesBuilder(t) + b.WithConfigFile("toml", mainConfigBasic).WithThemeConfigFile("toml", themeConfig) + b.CreateSites().Build(BuildCfg{}) + + got = b.Cfg.(*viper.Viper).AllSettings() + + b.AssertObject(`map[string]interface {}{ + "p1": "p1 theme", + "p2": "p2 theme", + "p3": "p3 theme", + "test-theme": map[string]interface {}{ + "p1": "p1 theme", + "p2": "p2 theme", + "p3": "p3 theme", + }, +}`, got["params"]) + + assert.Nil(got["languages"]) + b.AssertObject(` +map[string]interface {}{ + "text/m1": map[string]interface {}{ + "suffix": "m1theme", + }, + "text/m2": map[string]interface {}{ + "suffix": "m2theme", + }, +}`, got["mediatypes"]) + + b.AssertObject(` +map[string]interface {}{ + "o1": map[string]interface {}{ + "basename": "o1theme", + "mediatype": Type{ + MainType: "text", + SubType: "m1", + Suffix: "m1theme", + Delimiter: ".", + }, + }, + "o2": map[string]interface {}{ + "basename": "o2theme", + "mediatype": Type{ + MainType: "text", + SubType: "m2", + Suffix: "m2theme", + Delimiter: ".", + }, + }, +}`, got["outputformats"]) + b.AssertObject(` +map[string]interface {}{ + "main": []interface {}{ + map[string]interface {}{ + "name": "menu-main-theme", + }, + }, + "thememenu": []interface {}{ + map[string]interface {}{ + "name": "menu-theme", + }, + }, +}`, got["menu"]) + } diff --git a/hugolib/page_bundler_test.go b/hugolib/page_bundler_test.go index bf79d2f86..572d84bcd 100644 --- a/hugolib/page_bundler_test.go +++ b/hugolib/page_bundler_test.go @@ -200,6 +200,7 @@ func TestPageBundlerSiteMultilingual(t *testing.T) { cfg.Set("uglyURLs", ugly) assert.NoError(loadDefaultSettingsFor(cfg)) + assert.NoError(loadLanguageSettings(cfg, nil)) sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) assert.NoError(err) assert.Equal(2, len(sites.Sites)) @@ -264,6 +265,8 @@ func TestMultilingualDisableDefaultLanguage(t *testing.T) { cfg.Set("disableLanguages", []string{"en"}) err := loadDefaultSettingsFor(cfg) + assert.NoError(err) + err = loadLanguageSettings(cfg, nil) assert.Error(err) assert.Contains(err.Error(), "cannot disable default language") } diff --git a/hugolib/site.go b/hugolib/site.go index 2e8898bd6..0ffe153e9 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 ab23b343c..1f22e428d 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/" @@ -229,10 +250,15 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder { s.writeFilePairs("i18n", s.i18nFilePairsAdded) if s.Cfg == nil { - cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Name: "config." + s.configFormat}) + cfg, configFiles, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat}) if err != nil { s.Fatalf("Failed to load config: %s", err) } + expectedConfigs := 1 + if s.theme != "" { + expectedConfigs = 2 + } + require.Equal(s.T, expectedConfigs, len(configFiles), fmt.Sprintf("Configs: %v", configFiles)) s.Cfg = cfg } @@ -345,6 +371,17 @@ 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) + diff := helpers.DiffStrings(expected, got) + s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got) + } +} + func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) { content := readDestination(s.T, s.Fs, filename) for _, match := range matches {