github.com/kristoff-it/hugo@v0.47.1/hugolib/config.go (about)

     1  // Copyright 2016-present The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package hugolib
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  
    20  	"github.com/gohugoio/hugo/hugolib/paths"
    21  
    22  	"io"
    23  	"strings"
    24  
    25  	"github.com/gohugoio/hugo/langs"
    26  
    27  	"github.com/gohugoio/hugo/config"
    28  	"github.com/gohugoio/hugo/config/privacy"
    29  	"github.com/gohugoio/hugo/config/services"
    30  	"github.com/gohugoio/hugo/helpers"
    31  	"github.com/spf13/afero"
    32  	"github.com/spf13/viper"
    33  )
    34  
    35  // SiteConfig represents the config in .Site.Config.
    36  type SiteConfig struct {
    37  	// This contains all privacy related settings that can be used to
    38  	// make the YouTube template etc. GDPR compliant.
    39  	Privacy privacy.Config
    40  
    41  	// Services contains config for services such as Google Analytics etc.
    42  	Services services.Config
    43  }
    44  
    45  func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
    46  	privacyConfig, err := privacy.DecodeConfig(cfg)
    47  	if err != nil {
    48  		return
    49  	}
    50  
    51  	servicesConfig, err := services.DecodeConfig(cfg)
    52  	if err != nil {
    53  		return
    54  	}
    55  
    56  	scfg.Privacy = privacyConfig
    57  	scfg.Services = servicesConfig
    58  
    59  	return
    60  }
    61  
    62  // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
    63  type ConfigSourceDescriptor struct {
    64  	Fs afero.Fs
    65  
    66  	// Full path to the config file to use, i.e. /my/project/config.toml
    67  	Filename string
    68  
    69  	// The path to the directory to look for configuration. Is used if Filename is not
    70  	// set.
    71  	Path string
    72  
    73  	// The project's working dir. Is used to look for additional theme config.
    74  	WorkingDir string
    75  }
    76  
    77  func (d ConfigSourceDescriptor) configFilenames() []string {
    78  	return strings.Split(d.Filename, ",")
    79  }
    80  
    81  // LoadConfigDefault is a convenience method to load the default "config.toml" config.
    82  func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
    83  	v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
    84  	return v, err
    85  }
    86  
    87  var ErrNoConfigFile = errors.New("Unable to locate Config file. Perhaps you need to create a new site.\n       Run `hugo help new` for details.\n")
    88  
    89  // LoadConfig loads Hugo configuration into a new Viper and then adds
    90  // a set of defaults.
    91  func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) {
    92  	var configFiles []string
    93  
    94  	fs := d.Fs
    95  	v := viper.New()
    96  	v.SetFs(fs)
    97  
    98  	if d.Path == "" {
    99  		d.Path = "."
   100  	}
   101  
   102  	configFilenames := d.configFilenames()
   103  	v.AutomaticEnv()
   104  	v.SetEnvPrefix("hugo")
   105  	v.SetConfigFile(configFilenames[0])
   106  	v.AddConfigPath(d.Path)
   107  
   108  	var configFileErr error
   109  
   110  	err := v.ReadInConfig()
   111  	if err != nil {
   112  		if _, ok := err.(viper.ConfigParseError); ok {
   113  			return nil, configFiles, err
   114  		}
   115  		configFileErr = ErrNoConfigFile
   116  	}
   117  
   118  	if configFileErr == nil {
   119  
   120  		if cf := v.ConfigFileUsed(); cf != "" {
   121  			configFiles = append(configFiles, cf)
   122  		}
   123  
   124  		for _, configFile := range configFilenames[1:] {
   125  			var r io.Reader
   126  			var err error
   127  			if r, err = fs.Open(configFile); err != nil {
   128  				return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err)
   129  			}
   130  			if err = v.MergeConfig(r); err != nil {
   131  				return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err)
   132  			}
   133  			configFiles = append(configFiles, configFile)
   134  		}
   135  
   136  	}
   137  
   138  	if err := loadDefaultSettingsFor(v); err != nil {
   139  		return v, configFiles, err
   140  	}
   141  
   142  	if configFileErr == nil {
   143  
   144  		themeConfigFiles, err := loadThemeConfig(d, v)
   145  		if err != nil {
   146  			return v, configFiles, err
   147  		}
   148  
   149  		if len(themeConfigFiles) > 0 {
   150  			configFiles = append(configFiles, themeConfigFiles...)
   151  		}
   152  	}
   153  
   154  	// We create languages based on the settings, so we need to make sure that
   155  	// all configuration is loaded/set before doing that.
   156  	for _, d := range doWithConfig {
   157  		if err := d(v); err != nil {
   158  			return v, configFiles, err
   159  		}
   160  	}
   161  
   162  	if err := loadLanguageSettings(v, nil); err != nil {
   163  		return v, configFiles, err
   164  	}
   165  
   166  	return v, configFiles, configFileErr
   167  
   168  }
   169  
   170  func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
   171  
   172  	defaultLang := cfg.GetString("defaultContentLanguage")
   173  
   174  	var languages map[string]interface{}
   175  
   176  	languagesFromConfig := cfg.GetStringMap("languages")
   177  	disableLanguages := cfg.GetStringSlice("disableLanguages")
   178  
   179  	if len(disableLanguages) == 0 {
   180  		languages = languagesFromConfig
   181  	} else {
   182  		languages = make(map[string]interface{})
   183  		for k, v := range languagesFromConfig {
   184  			for _, disabled := range disableLanguages {
   185  				if disabled == defaultLang {
   186  					return fmt.Errorf("cannot disable default language %q", defaultLang)
   187  				}
   188  
   189  				if strings.EqualFold(k, disabled) {
   190  					v.(map[string]interface{})["disabled"] = true
   191  					break
   192  				}
   193  			}
   194  			languages[k] = v
   195  		}
   196  	}
   197  
   198  	var (
   199  		languages2 langs.Languages
   200  		err        error
   201  	)
   202  
   203  	if len(languages) == 0 {
   204  		languages2 = append(languages2, langs.NewDefaultLanguage(cfg))
   205  	} else {
   206  		languages2, err = toSortedLanguages(cfg, languages)
   207  		if err != nil {
   208  			return fmt.Errorf("Failed to parse multilingual config: %s", err)
   209  		}
   210  	}
   211  
   212  	if oldLangs != nil {
   213  		// When in multihost mode, the languages are mapped to a server, so
   214  		// some structural language changes will need a restart of the dev server.
   215  		// The validation below isn't complete, but should cover the most
   216  		// important cases.
   217  		var invalid bool
   218  		if languages2.IsMultihost() != oldLangs.IsMultihost() {
   219  			invalid = true
   220  		} else {
   221  			if languages2.IsMultihost() && len(languages2) != len(oldLangs) {
   222  				invalid = true
   223  			}
   224  		}
   225  
   226  		if invalid {
   227  			return errors.New("language change needing a server restart detected")
   228  		}
   229  
   230  		if languages2.IsMultihost() {
   231  			// We need to transfer any server baseURL to the new language
   232  			for i, ol := range oldLangs {
   233  				nl := languages2[i]
   234  				nl.Set("baseURL", ol.GetString("baseURL"))
   235  			}
   236  		}
   237  	}
   238  
   239  	// The defaultContentLanguage is something the user has to decide, but it needs
   240  	// to match a language in the language definition list.
   241  	langExists := false
   242  	for _, lang := range languages2 {
   243  		if lang.Lang == defaultLang {
   244  			langExists = true
   245  			break
   246  		}
   247  	}
   248  
   249  	if !langExists {
   250  		return fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang)
   251  	}
   252  
   253  	cfg.Set("languagesSorted", languages2)
   254  	cfg.Set("multilingual", len(languages2) > 1)
   255  
   256  	multihost := languages2.IsMultihost()
   257  
   258  	if multihost {
   259  		cfg.Set("defaultContentLanguageInSubdir", true)
   260  		cfg.Set("multihost", true)
   261  	}
   262  
   263  	if multihost {
   264  		// The baseURL may be provided at the language level. If that is true,
   265  		// then every language must have a baseURL. In this case we always render
   266  		// to a language sub folder, which is then stripped from all the Permalink URLs etc.
   267  		for _, l := range languages2 {
   268  			burl := l.GetLocal("baseURL")
   269  			if burl == nil {
   270  				return errors.New("baseURL must be set on all or none of the languages")
   271  			}
   272  		}
   273  
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error) {
   280  	themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir"))
   281  	themes := config.GetStringSlicePreserveString(v1, "theme")
   282  
   283  	//  CollectThemes(fs afero.Fs, themesDir string, themes []strin
   284  	themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	if len(themeConfigs) == 0 {
   290  		return nil, nil
   291  	}
   292  
   293  	v1.Set("allThemes", themeConfigs)
   294  
   295  	var configFilenames []string
   296  	for _, tc := range themeConfigs {
   297  		if tc.ConfigFilename != "" {
   298  			configFilenames = append(configFilenames, tc.ConfigFilename)
   299  			if err := applyThemeConfig(v1, tc); err != nil {
   300  				return nil, err
   301  			}
   302  		}
   303  	}
   304  
   305  	return configFilenames, nil
   306  
   307  }
   308  
   309  func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
   310  
   311  	const (
   312  		paramsKey    = "params"
   313  		languagesKey = "languages"
   314  		menuKey      = "menu"
   315  	)
   316  
   317  	v2 := theme.Cfg
   318  
   319  	for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
   320  		mergeStringMapKeepLeft("", key, v1, v2)
   321  	}
   322  
   323  	themeLower := strings.ToLower(theme.Name)
   324  	themeParamsNamespace := paramsKey + "." + themeLower
   325  
   326  	// Set namespaced params
   327  	if v2.IsSet(paramsKey) && !v1.IsSet(themeParamsNamespace) {
   328  		// Set it in the default store to make sure it gets in the same or
   329  		// behind the others.
   330  		v1.SetDefault(themeParamsNamespace, v2.Get(paramsKey))
   331  	}
   332  
   333  	// Only add params and new menu entries, we do not add language definitions.
   334  	if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) {
   335  		v1Langs := v1.GetStringMap(languagesKey)
   336  		for k, _ := range v1Langs {
   337  			langParamsKey := languagesKey + "." + k + "." + paramsKey
   338  			mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
   339  		}
   340  		v2Langs := v2.GetStringMap(languagesKey)
   341  		for k, _ := range v2Langs {
   342  			if k == "" {
   343  				continue
   344  			}
   345  			langParamsKey := languagesKey + "." + k + "." + paramsKey
   346  			langParamsThemeNamespace := langParamsKey + "." + themeLower
   347  			// Set namespaced params
   348  			if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) {
   349  				v1.SetDefault(langParamsThemeNamespace, v2.Get(langParamsKey))
   350  			}
   351  
   352  			langMenuKey := languagesKey + "." + k + "." + menuKey
   353  			if v2.IsSet(langMenuKey) {
   354  				// Only add if not in the main config.
   355  				v2menus := v2.GetStringMap(langMenuKey)
   356  				for k, v := range v2menus {
   357  					menuEntry := menuKey + "." + k
   358  					menuLangEntry := langMenuKey + "." + k
   359  					if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) {
   360  						v1.Set(menuLangEntry, v)
   361  					}
   362  				}
   363  			}
   364  		}
   365  	}
   366  
   367  	// Add menu definitions from theme not found in project
   368  	if v2.IsSet("menu") {
   369  		v2menus := v2.GetStringMap(menuKey)
   370  		for k, v := range v2menus {
   371  			menuEntry := menuKey + "." + k
   372  			if !v1.IsSet(menuEntry) {
   373  				v1.SetDefault(menuEntry, v)
   374  			}
   375  		}
   376  	}
   377  
   378  	return nil
   379  
   380  }
   381  
   382  func mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) {
   383  	if !v2.IsSet(key) {
   384  		return
   385  	}
   386  
   387  	if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) {
   388  		v1.Set(key, v2.Get(key))
   389  		return
   390  	}
   391  
   392  	m1 := v1.GetStringMap(key)
   393  	m2 := v2.GetStringMap(key)
   394  
   395  	for k, v := range m2 {
   396  		if _, found := m1[k]; !found {
   397  			if rootKey != "" && v1.IsSet(rootKey+"."+k) {
   398  				continue
   399  			}
   400  			m1[k] = v
   401  		}
   402  	}
   403  }
   404  
   405  func loadDefaultSettingsFor(v *viper.Viper) error {
   406  
   407  	c, err := helpers.NewContentSpec(v)
   408  	if err != nil {
   409  		return err
   410  	}
   411  
   412  	v.RegisterAlias("indexes", "taxonomies")
   413  
   414  	v.SetDefault("cleanDestinationDir", false)
   415  	v.SetDefault("watch", false)
   416  	v.SetDefault("metaDataFormat", "toml")
   417  	v.SetDefault("contentDir", "content")
   418  	v.SetDefault("layoutDir", "layouts")
   419  	v.SetDefault("assetDir", "assets")
   420  	v.SetDefault("staticDir", "static")
   421  	v.SetDefault("resourceDir", "resources")
   422  	v.SetDefault("archetypeDir", "archetypes")
   423  	v.SetDefault("publishDir", "public")
   424  	v.SetDefault("dataDir", "data")
   425  	v.SetDefault("i18nDir", "i18n")
   426  	v.SetDefault("themesDir", "themes")
   427  	v.SetDefault("buildDrafts", false)
   428  	v.SetDefault("buildFuture", false)
   429  	v.SetDefault("buildExpired", false)
   430  	v.SetDefault("uglyURLs", false)
   431  	v.SetDefault("verbose", false)
   432  	v.SetDefault("ignoreCache", false)
   433  	v.SetDefault("canonifyURLs", false)
   434  	v.SetDefault("relativeURLs", false)
   435  	v.SetDefault("removePathAccents", false)
   436  	v.SetDefault("titleCaseStyle", "AP")
   437  	v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"})
   438  	v.SetDefault("permalinks", make(PermalinkOverrides, 0))
   439  	v.SetDefault("sitemap", Sitemap{Priority: -1, Filename: "sitemap.xml"})
   440  	v.SetDefault("pygmentsStyle", "monokai")
   441  	v.SetDefault("pygmentsUseClasses", false)
   442  	v.SetDefault("pygmentsCodeFences", false)
   443  	v.SetDefault("pygmentsUseClassic", false)
   444  	v.SetDefault("pygmentsOptions", "")
   445  	v.SetDefault("disableLiveReload", false)
   446  	v.SetDefault("pluralizeListTitles", true)
   447  	v.SetDefault("preserveTaxonomyNames", false)
   448  	v.SetDefault("forceSyncStatic", false)
   449  	v.SetDefault("footnoteAnchorPrefix", "")
   450  	v.SetDefault("footnoteReturnLinkContents", "")
   451  	v.SetDefault("newContentEditor", "")
   452  	v.SetDefault("paginate", 10)
   453  	v.SetDefault("paginatePath", "page")
   454  	v.SetDefault("summaryLength", 70)
   455  	v.SetDefault("blackfriday", c.BlackFriday)
   456  	v.SetDefault("rSSUri", "index.xml")
   457  	v.SetDefault("rssLimit", -1)
   458  	v.SetDefault("sectionPagesMenu", "")
   459  	v.SetDefault("disablePathToLower", false)
   460  	v.SetDefault("hasCJKLanguage", false)
   461  	v.SetDefault("enableEmoji", false)
   462  	v.SetDefault("pygmentsCodeFencesGuessSyntax", false)
   463  	v.SetDefault("useModTimeAsFallback", false)
   464  	v.SetDefault("defaultContentLanguage", "en")
   465  	v.SetDefault("defaultContentLanguageInSubdir", false)
   466  	v.SetDefault("enableMissingTranslationPlaceholders", false)
   467  	v.SetDefault("enableGitInfo", false)
   468  	v.SetDefault("ignoreFiles", make([]string, 0))
   469  	v.SetDefault("disableAliases", false)
   470  	v.SetDefault("debug", false)
   471  	v.SetDefault("disableFastRender", false)
   472  	v.SetDefault("timeout", 10000) // 10 seconds
   473  
   474  	// Remove in Hugo 0.39
   475  
   476  	if v.GetBool("useModTimeAsFallback") {
   477  
   478  		helpers.Deprecated("Site config", "useModTimeAsFallback", `Replace with this in your config.toml:
   479      
   480  [frontmatter]
   481  date = [ "date",":fileModTime", ":default"]
   482  lastmod = ["lastmod" ,":fileModTime", ":default"]
   483  `, false)
   484  
   485  	}
   486  
   487  	return nil
   488  }