github.com/gohugoio/hugo@v0.88.1/modules/config.go (about)

     1  // Copyright 2019 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 modules
    15  
    16  import (
    17  	"fmt"
    18  	"path/filepath"
    19  	"strings"
    20  
    21  	"github.com/pkg/errors"
    22  
    23  	"github.com/gohugoio/hugo/common/hugo"
    24  
    25  	"github.com/gohugoio/hugo/config"
    26  	"github.com/gohugoio/hugo/hugofs/files"
    27  	"github.com/gohugoio/hugo/langs"
    28  	"github.com/mitchellh/mapstructure"
    29  )
    30  
    31  var DefaultModuleConfig = Config{
    32  
    33  	// Default to direct, which means "git clone" and similar. We
    34  	// will investigate proxy settings in more depth later.
    35  	// See https://github.com/golang/go/issues/26334
    36  	Proxy: "direct",
    37  
    38  	// Comma separated glob list matching paths that should not use the
    39  	// proxy configured above.
    40  	NoProxy: "none",
    41  
    42  	// Comma separated glob list matching paths that should be
    43  	// treated as private.
    44  	Private: "*.*",
    45  
    46  	// A list of replacement directives mapping a module path to a directory
    47  	// or a theme component in the themes folder.
    48  	// Note that this will turn the component into a traditional theme component
    49  	// that does not partake in vendoring etc.
    50  	// The syntax is the similar to the replacement directives used in go.mod, e.g:
    51  	//    github.com/mod1 -> ../mod1,github.com/mod2 -> ../mod2
    52  	Replacements: nil,
    53  }
    54  
    55  // ApplyProjectConfigDefaults applies default/missing module configuration for
    56  // the main project.
    57  func ApplyProjectConfigDefaults(cfg config.Provider, mod Module) error {
    58  	moda := mod.(*moduleAdapter)
    59  
    60  	// Map legacy directory config into the new module.
    61  	languages := cfg.Get("languagesSortedDefaultFirst").(langs.Languages)
    62  	isMultiHost := languages.IsMultihost()
    63  
    64  	// To bridge between old and new configuration format we need
    65  	// a way to make sure all of the core components are configured on
    66  	// the basic level.
    67  	componentsConfigured := make(map[string]bool)
    68  	for _, mnt := range moda.mounts {
    69  		if !strings.HasPrefix(mnt.Target, files.JsConfigFolderMountPrefix) {
    70  			componentsConfigured[mnt.Component()] = true
    71  		}
    72  	}
    73  
    74  	type dirKeyComponent struct {
    75  		key          string
    76  		component    string
    77  		multilingual bool
    78  	}
    79  
    80  	dirKeys := []dirKeyComponent{
    81  		{"contentDir", files.ComponentFolderContent, true},
    82  		{"dataDir", files.ComponentFolderData, false},
    83  		{"layoutDir", files.ComponentFolderLayouts, false},
    84  		{"i18nDir", files.ComponentFolderI18n, false},
    85  		{"archetypeDir", files.ComponentFolderArchetypes, false},
    86  		{"assetDir", files.ComponentFolderAssets, false},
    87  		{"", files.ComponentFolderStatic, isMultiHost},
    88  	}
    89  
    90  	createMountsFor := func(d dirKeyComponent, cfg config.Provider) []Mount {
    91  		var lang string
    92  		if language, ok := cfg.(*langs.Language); ok {
    93  			lang = language.Lang
    94  		}
    95  
    96  		// Static mounts are a little special.
    97  		if d.component == files.ComponentFolderStatic {
    98  			var mounts []Mount
    99  			staticDirs := getStaticDirs(cfg)
   100  			if len(staticDirs) > 0 {
   101  				componentsConfigured[d.component] = true
   102  			}
   103  
   104  			for _, dir := range staticDirs {
   105  				mounts = append(mounts, Mount{Lang: lang, Source: dir, Target: d.component})
   106  			}
   107  
   108  			return mounts
   109  
   110  		}
   111  
   112  		if cfg.IsSet(d.key) {
   113  			source := cfg.GetString(d.key)
   114  			componentsConfigured[d.component] = true
   115  
   116  			return []Mount{{
   117  				// No lang set for layouts etc.
   118  				Source: source,
   119  				Target: d.component,
   120  			}}
   121  		}
   122  
   123  		return nil
   124  	}
   125  
   126  	createMounts := func(d dirKeyComponent) []Mount {
   127  		var mounts []Mount
   128  		if d.multilingual {
   129  			if d.component == files.ComponentFolderContent {
   130  				seen := make(map[string]bool)
   131  				hasContentDir := false
   132  				for _, language := range languages {
   133  					if language.ContentDir != "" {
   134  						hasContentDir = true
   135  						break
   136  					}
   137  				}
   138  
   139  				if hasContentDir {
   140  					for _, language := range languages {
   141  						contentDir := language.ContentDir
   142  						if contentDir == "" {
   143  							contentDir = files.ComponentFolderContent
   144  						}
   145  						if contentDir == "" || seen[contentDir] {
   146  							continue
   147  						}
   148  						seen[contentDir] = true
   149  						mounts = append(mounts, Mount{Lang: language.Lang, Source: contentDir, Target: d.component})
   150  					}
   151  				}
   152  
   153  				componentsConfigured[d.component] = len(seen) > 0
   154  
   155  			} else {
   156  				for _, language := range languages {
   157  					mounts = append(mounts, createMountsFor(d, language)...)
   158  				}
   159  			}
   160  		} else {
   161  			mounts = append(mounts, createMountsFor(d, cfg)...)
   162  		}
   163  
   164  		return mounts
   165  	}
   166  
   167  	var mounts []Mount
   168  	for _, dirKey := range dirKeys {
   169  		if componentsConfigured[dirKey.component] {
   170  			continue
   171  		}
   172  
   173  		mounts = append(mounts, createMounts(dirKey)...)
   174  
   175  	}
   176  
   177  	// Add default configuration
   178  	for _, dirKey := range dirKeys {
   179  		if componentsConfigured[dirKey.component] {
   180  			continue
   181  		}
   182  		mounts = append(mounts, Mount{Source: dirKey.component, Target: dirKey.component})
   183  	}
   184  
   185  	// Prepend the mounts from configuration.
   186  	mounts = append(moda.mounts, mounts...)
   187  
   188  	moda.mounts = mounts
   189  
   190  	return nil
   191  }
   192  
   193  // DecodeConfig creates a modules Config from a given Hugo configuration.
   194  func DecodeConfig(cfg config.Provider) (Config, error) {
   195  	return decodeConfig(cfg, nil)
   196  }
   197  
   198  func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Config, error) {
   199  	c := DefaultModuleConfig
   200  	c.replacementsMap = pathReplacements
   201  
   202  	if cfg == nil {
   203  		return c, nil
   204  	}
   205  
   206  	themeSet := cfg.IsSet("theme")
   207  	moduleSet := cfg.IsSet("module")
   208  
   209  	if moduleSet {
   210  		m := cfg.GetStringMap("module")
   211  		if err := mapstructure.WeakDecode(m, &c); err != nil {
   212  			return c, err
   213  		}
   214  
   215  		if c.replacementsMap == nil {
   216  
   217  			if len(c.Replacements) == 1 {
   218  				c.Replacements = strings.Split(c.Replacements[0], ",")
   219  			}
   220  
   221  			for i, repl := range c.Replacements {
   222  				c.Replacements[i] = strings.TrimSpace(repl)
   223  			}
   224  
   225  			c.replacementsMap = make(map[string]string)
   226  			for _, repl := range c.Replacements {
   227  				parts := strings.Split(repl, "->")
   228  				if len(parts) != 2 {
   229  					return c, errors.Errorf(`invalid module.replacements: %q; configure replacement pairs on the form "oldpath->newpath" `, repl)
   230  				}
   231  
   232  				c.replacementsMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
   233  			}
   234  		}
   235  
   236  		if c.replacementsMap != nil && c.Imports != nil {
   237  			for i, imp := range c.Imports {
   238  				if newImp, found := c.replacementsMap[imp.Path]; found {
   239  					imp.Path = newImp
   240  					imp.pathProjectReplaced = true
   241  					c.Imports[i] = imp
   242  				}
   243  			}
   244  		}
   245  
   246  		for i, mnt := range c.Mounts {
   247  			mnt.Source = filepath.Clean(mnt.Source)
   248  			mnt.Target = filepath.Clean(mnt.Target)
   249  			c.Mounts[i] = mnt
   250  		}
   251  
   252  	}
   253  
   254  	if themeSet {
   255  		imports := config.GetStringSlicePreserveString(cfg, "theme")
   256  		for _, imp := range imports {
   257  			c.Imports = append(c.Imports, Import{
   258  				Path: imp,
   259  			})
   260  		}
   261  
   262  	}
   263  
   264  	return c, nil
   265  }
   266  
   267  // Config holds a module config.
   268  type Config struct {
   269  	Mounts  []Mount
   270  	Imports []Import
   271  
   272  	// Meta info about this module (license information etc.).
   273  	Params map[string]interface{}
   274  
   275  	// Will be validated against the running Hugo version.
   276  	HugoVersion HugoVersion
   277  
   278  	// A optional Glob pattern matching module paths to skip when vendoring, e.g.
   279  	// "github.com/**".
   280  	NoVendor string
   281  
   282  	// When enabled, we will pick the vendored module closest to the module
   283  	// using it.
   284  	// The default behaviour is to pick the first.
   285  	// Note that there can still be only one dependency of a given module path,
   286  	// so once it is in use it cannot be redefined.
   287  	VendorClosest bool
   288  
   289  	Replacements    []string
   290  	replacementsMap map[string]string
   291  
   292  	// Configures GOPROXY.
   293  	Proxy string
   294  	// Configures GONOPROXY.
   295  	NoProxy string
   296  	// Configures GOPRIVATE.
   297  	Private string
   298  }
   299  
   300  // hasModuleImport reports whether the project config have one or more
   301  // modules imports, e.g. github.com/bep/myshortcodes.
   302  func (c Config) hasModuleImport() bool {
   303  	for _, imp := range c.Imports {
   304  		if isProbablyModule(imp.Path) {
   305  			return true
   306  		}
   307  	}
   308  	return false
   309  }
   310  
   311  // HugoVersion holds Hugo binary version requirements for a module.
   312  type HugoVersion struct {
   313  	// The minimum Hugo version that this module works with.
   314  	Min hugo.VersionString
   315  
   316  	// The maxium Hugo version that this module works with.
   317  	Max hugo.VersionString
   318  
   319  	// Set if the extended version is needed.
   320  	Extended bool
   321  }
   322  
   323  func (v HugoVersion) String() string {
   324  	extended := ""
   325  	if v.Extended {
   326  		extended = " extended"
   327  	}
   328  
   329  	if v.Min != "" && v.Max != "" {
   330  		return fmt.Sprintf("%s/%s%s", v.Min, v.Max, extended)
   331  	}
   332  
   333  	if v.Min != "" {
   334  		return fmt.Sprintf("Min %s%s", v.Min, extended)
   335  	}
   336  
   337  	if v.Max != "" {
   338  		return fmt.Sprintf("Max %s%s", v.Max, extended)
   339  	}
   340  
   341  	return extended
   342  }
   343  
   344  // IsValid reports whether this version is valid compared to the running
   345  // Hugo binary.
   346  func (v HugoVersion) IsValid() bool {
   347  	current := hugo.CurrentVersion.Version()
   348  	if v.Extended && !hugo.IsExtended {
   349  		return false
   350  	}
   351  
   352  	isValid := true
   353  
   354  	if v.Min != "" && current.Compare(v.Min) > 0 {
   355  		isValid = false
   356  	}
   357  
   358  	if v.Max != "" && current.Compare(v.Max) < 0 {
   359  		isValid = false
   360  	}
   361  
   362  	return isValid
   363  }
   364  
   365  type Import struct {
   366  	Path                string // Module path
   367  	pathProjectReplaced bool   // Set when Path is replaced in project config.
   368  	IgnoreConfig        bool   // Ignore any config in config.toml (will still folow imports).
   369  	IgnoreImports       bool   // Do not follow any configured imports.
   370  	NoMounts            bool   // Do not mount any folder in this import.
   371  	NoVendor            bool   // Never vendor this import (only allowed in main project).
   372  	Disable             bool   // Turn off this module.
   373  	Mounts              []Mount
   374  }
   375  
   376  type Mount struct {
   377  	Source string // relative path in source repo, e.g. "scss"
   378  	Target string // relative target path, e.g. "assets/bootstrap/scss"
   379  
   380  	Lang string // any language code associated with this mount.
   381  
   382  }
   383  
   384  func (m Mount) Component() string {
   385  	return strings.Split(m.Target, fileSeparator)[0]
   386  }
   387  
   388  func (m Mount) ComponentAndName() (string, string) {
   389  	k := strings.Index(m.Target, fileSeparator)
   390  	if k == -1 {
   391  		return m.Target, ""
   392  	}
   393  	return m.Target[:k], m.Target[k+1:]
   394  }
   395  
   396  func getStaticDirs(cfg config.Provider) []string {
   397  	var staticDirs []string
   398  	for i := -1; i <= 10; i++ {
   399  		staticDirs = append(staticDirs, getStringOrStringSlice(cfg, "staticDir", i)...)
   400  	}
   401  	return staticDirs
   402  }
   403  
   404  func getStringOrStringSlice(cfg config.Provider, key string, id int) []string {
   405  	if id >= 0 {
   406  		key = fmt.Sprintf("%s%d", key, id)
   407  	}
   408  
   409  	return config.GetStringSlicePreserveString(cfg, key)
   410  }