github.com/lyeb/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 }