/
githubmirror
/
hugo
Обзор
Документация
Войти
/
githubmirror
/
hugo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
config/configLoader.go
254 строки
7 KB
Bjørn Erik Pedersen
config: Allow repeating the root key in /config files
13 май 2026, 19:22
13 май 2026, 19:22
ba5d812
Код
Авторство
О чём код?
// Copyright 2025 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 config import ( "fmt" "os" "path/filepath" "strings" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hmaps" "github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/spf13/afero" ) var ( // See issue #8979 for context. // Hugo has always used config.toml etc. as the default config file name. // But hugo.toml is a more descriptive name, but we need to check for both. DefaultConfigNames = []string{"hugo", "config"} DefaultConfigNamesSet = make(map[string]bool) ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"} validConfigFileExtensionsMap map[string]bool = make(map[string]bool) ) func init() { for _, name := range DefaultConfigNames { DefaultConfigNamesSet[name] = true } for _, ext := range ValidConfigFileExtensions { validConfigFileExtensionsMap[ext] = true } } // IsValidConfigFilename returns whether filename is one of the supported // config formats in Hugo. func IsValidConfigFilename(filename string) bool { ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), ".")) return validConfigFileExtensionsMap[ext] } // FromTOMLConfigString creates a config from the given TOML config. This is useful in tests. func FromTOMLConfigString(config string) Provider { cfg, err := FromConfigString(config, "toml") if err != nil { panic(err) } return cfg } // FromMapToTOMLString converts the given map to a TOML string. This is useful in tests. func FromMapToTOMLString(v map[string]any) string { var sb strings.Builder err := parser.InterfaceToConfig(v, metadecoders.TOML, &sb) if err != nil { panic(err) } return sb.String() } // FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests. func FromConfigString(config, configType string) (Provider, error) { m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config)) if err != nil { return nil, err } return NewFrom(m), nil } // FromFile loads the configuration from the given filename. func FromFile(fs afero.Fs, filename string) (Provider, error) { m, err := loadConfigFromFile(fs, filename) if err != nil { fe := herrors.UnwrapFileError(err) if fe != nil { pos := fe.Position() pos.Filename = filename fe.UpdatePosition(pos) return nil, err } return nil, herrors.NewFileErrorFromFile(err, filename, fs, nil) } return NewFrom(m), nil } // FromFileToMap is the same as FromFile, but it returns the config values // as a simple map. func FromFileToMap(fs afero.Fs, filename string) (map[string]any, error) { return loadConfigFromFile(fs, filename) } func readConfig(format metadecoders.Format, data []byte) (map[string]any, error) { m, err := metadecoders.Default.UnmarshalToMap(data, format) if err != nil { return nil, err } RenameKeys(m) return m, nil } func loadConfigFromFile(fs afero.Fs, filename string) (map[string]any, error) { m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename) if err != nil { return nil, err } RenameKeys(m) return m, nil } func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) { defaultConfigDir := filepath.Join(configDir, "_default") environmentConfigDir := filepath.Join(configDir, environment) cfg := New() var configDirs []string // Merge from least to most specific. for _, dir := range []string{defaultConfigDir, environmentConfigDir} { if _, err := sourceFs.Stat(dir); err == nil { configDirs = append(configDirs, dir) } } if len(configDirs) == 0 { return nil, nil, nil } // Keep track of these so we can watch them for changes. var dirnames []string for _, configDir := range configDirs { err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error { if fi == nil || err != nil { return nil } if fi.IsDir() { dirnames = append(dirnames, path) return nil } if !IsValidConfigFilename(path) { return nil } name := paths.Filename(filepath.Base(path)) item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path) if err != nil { // This will be used in error reporting, use the most specific value. dirnames = []string{path} return fmt.Errorf("failed to unmarshal config for path %q: %w", path, err) } var keyPath []string var unwrapKey string if !DefaultConfigNamesSet[name] { // Can be params.jp, menus.en etc. name, lang := paths.FileAndExtNoDelimiter(name) unwrapKey = name keyPath = []string{name} if lang != "" { keyPath = []string{"languages", lang} switch name { case "menu", "menus": keyPath = append(keyPath, "menus") case "params": keyPath = append(keyPath, "params") } } } // TOML/YAML can't represent a headless top-level array, so allow a // file to wrap its content under a single top-level key matching // the basename (e.g. cascade.yaml with `cascade: [...]`). var itemValue any = item if unwrapKey != "" && len(item) == 1 { if inner, ok := item[unwrapKey]; ok { itemValue = inner } } root := item if len(keyPath) > 0 { root = make(map[string]any) m := root for i, key := range keyPath { if i >= len(keyPath)-1 { m[key] = itemValue } else { nm := make(map[string]any) m[key] = nm m = nm } } } // Migrate menu => menus etc. RenameKeys(root) // Set will overwrite keys with the same name, recursively. cfg.Set("", root) return nil }) if err != nil { return nil, dirnames, err } } return cfg, dirnames, nil } var keyAliases hmaps.KeyRenamer func init() { var err error keyAliases, err = hmaps.NewKeyRenamer( // Before 0.53 we used singular for "menu". "{menu,languages/*/menu}", "menus", ) if err != nil { panic(err) } } // RenameKeys renames config keys in m recursively according to a global Hugo // alias definition. func RenameKeys(m map[string]any) { keyAliases.Rename(m) }