github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  
    22  	"github.com/gohugoio/hugo/common/hugo"
    23  	"github.com/gohugoio/hugo/hugofs/files"
    24  
    25  	"github.com/gohugoio/hugo/config"
    26  	"github.com/mitchellh/mapstructure"
    27  )
    28  
    29  const WorkspaceDisabled = "off"
    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  	// Default is no workspace resolution.
    47  	Workspace: WorkspaceDisabled,
    48  
    49  	// A list of replacement directives mapping a module path to a directory
    50  	// or a theme component in the themes folder.
    51  	// Note that this will turn the component into a traditional theme component
    52  	// that does not partake in vendoring etc.
    53  	// The syntax is the similar to the replacement directives used in go.mod, e.g:
    54  	//    github.com/mod1 -> ../mod1,github.com/mod2 -> ../mod2
    55  	Replacements: nil,
    56  }
    57  
    58  // ApplyProjectConfigDefaults applies default/missing module configuration for
    59  // the main project.
    60  func ApplyProjectConfigDefaults(mod Module, cfgs ...config.AllProvider) error {
    61  
    62  	moda := mod.(*moduleAdapter)
    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  	var mounts []Mount
    75  
    76  	for _, component := range []string{
    77  		files.ComponentFolderContent,
    78  		files.ComponentFolderData,
    79  		files.ComponentFolderLayouts,
    80  		files.ComponentFolderI18n,
    81  		files.ComponentFolderArchetypes,
    82  		files.ComponentFolderAssets,
    83  		files.ComponentFolderStatic,
    84  	} {
    85  		if componentsConfigured[component] {
    86  			continue
    87  		}
    88  
    89  		first := cfgs[0]
    90  		dirsBase := first.DirsBase()
    91  		isMultiHost := first.IsMultihost()
    92  
    93  		for i, cfg := range cfgs {
    94  			dirs := cfg.Dirs()
    95  			var dir string
    96  			var dropLang bool
    97  			switch component {
    98  			case files.ComponentFolderContent:
    99  				dir = dirs.ContentDir
   100  				dropLang = dir == dirsBase.ContentDir
   101  			case files.ComponentFolderData:
   102  				dir = dirs.DataDir
   103  			case files.ComponentFolderLayouts:
   104  				dir = dirs.LayoutDir
   105  			case files.ComponentFolderI18n:
   106  				dir = dirs.I18nDir
   107  			case files.ComponentFolderArchetypes:
   108  				dir = dirs.ArcheTypeDir
   109  			case files.ComponentFolderAssets:
   110  				dir = dirs.AssetDir
   111  			case files.ComponentFolderStatic:
   112  				// For static dirs, we only care about the language in multihost setups.
   113  				dropLang = !isMultiHost
   114  			}
   115  
   116  			var perLang bool
   117  			switch component {
   118  			case files.ComponentFolderContent, files.ComponentFolderStatic:
   119  				perLang = true
   120  			default:
   121  			}
   122  			if i > 0 && !perLang {
   123  				continue
   124  			}
   125  
   126  			var lang string
   127  			if perLang && !dropLang {
   128  				lang = cfg.Language().Lang
   129  			}
   130  
   131  			// Static mounts are a little special.
   132  			if component == files.ComponentFolderStatic {
   133  				staticDirs := cfg.StaticDirs()
   134  				for _, dir := range staticDirs {
   135  					mounts = append(mounts, Mount{Lang: lang, Source: dir, Target: component})
   136  				}
   137  				continue
   138  			}
   139  
   140  			if dir != "" {
   141  				mounts = append(mounts, Mount{Lang: lang, Source: dir, Target: component})
   142  			}
   143  		}
   144  	}
   145  
   146  	moda.mounts = append(moda.mounts, mounts...)
   147  
   148  	// Temporary: Remove duplicates.
   149  	seen := make(map[string]bool)
   150  	var newMounts []Mount
   151  	for _, m := range moda.mounts {
   152  		key := m.Source + m.Target + m.Lang
   153  		if seen[key] {
   154  			continue
   155  		}
   156  		seen[key] = true
   157  		newMounts = append(newMounts, m)
   158  	}
   159  	moda.mounts = newMounts
   160  
   161  	return nil
   162  }
   163  
   164  // DecodeConfig creates a modules Config from a given Hugo configuration.
   165  func DecodeConfig(cfg config.Provider) (Config, error) {
   166  	return decodeConfig(cfg, nil)
   167  }
   168  
   169  func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Config, error) {
   170  	c := DefaultModuleConfig
   171  	c.replacementsMap = pathReplacements
   172  
   173  	if cfg == nil {
   174  		return c, nil
   175  	}
   176  
   177  	themeSet := cfg.IsSet("theme")
   178  	moduleSet := cfg.IsSet("module")
   179  
   180  	if moduleSet {
   181  		m := cfg.GetStringMap("module")
   182  		if err := mapstructure.WeakDecode(m, &c); err != nil {
   183  			return c, err
   184  		}
   185  
   186  		if c.replacementsMap == nil {
   187  
   188  			if len(c.Replacements) == 1 {
   189  				c.Replacements = strings.Split(c.Replacements[0], ",")
   190  			}
   191  
   192  			for i, repl := range c.Replacements {
   193  				c.Replacements[i] = strings.TrimSpace(repl)
   194  			}
   195  
   196  			c.replacementsMap = make(map[string]string)
   197  			for _, repl := range c.Replacements {
   198  				parts := strings.Split(repl, "->")
   199  				if len(parts) != 2 {
   200  					return c, fmt.Errorf(`invalid module.replacements: %q; configure replacement pairs on the form "oldpath->newpath" `, repl)
   201  				}
   202  
   203  				c.replacementsMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
   204  			}
   205  		}
   206  
   207  		if c.replacementsMap != nil && c.Imports != nil {
   208  			for i, imp := range c.Imports {
   209  				if newImp, found := c.replacementsMap[imp.Path]; found {
   210  					imp.Path = newImp
   211  					imp.pathProjectReplaced = true
   212  					c.Imports[i] = imp
   213  				}
   214  			}
   215  		}
   216  
   217  		for i, mnt := range c.Mounts {
   218  			mnt.Source = filepath.Clean(mnt.Source)
   219  			mnt.Target = filepath.Clean(mnt.Target)
   220  			c.Mounts[i] = mnt
   221  		}
   222  
   223  		if c.Workspace == "" {
   224  			c.Workspace = WorkspaceDisabled
   225  		}
   226  		if c.Workspace != WorkspaceDisabled {
   227  			c.Workspace = filepath.Clean(c.Workspace)
   228  			if !filepath.IsAbs(c.Workspace) {
   229  				workingDir := cfg.GetString("workingDir")
   230  				c.Workspace = filepath.Join(workingDir, c.Workspace)
   231  			}
   232  			if _, err := os.Stat(c.Workspace); err != nil {
   233  				return c, fmt.Errorf("module workspace %q does not exist. Check your module.workspace setting (or HUGO_MODULE_WORKSPACE env var).", c.Workspace)
   234  			}
   235  		}
   236  	}
   237  
   238  	if themeSet {
   239  		imports := config.GetStringSlicePreserveString(cfg, "theme")
   240  		for _, imp := range imports {
   241  			c.Imports = append(c.Imports, Import{
   242  				Path: imp,
   243  			})
   244  		}
   245  	}
   246  
   247  	return c, nil
   248  }
   249  
   250  // Config holds a module config.
   251  type Config struct {
   252  	// File system mounts.
   253  	Mounts []Mount
   254  
   255  	// Module imports.
   256  	Imports []Import
   257  
   258  	// Meta info about this module (license information etc.).
   259  	Params map[string]any
   260  
   261  	// Will be validated against the running Hugo version.
   262  	HugoVersion HugoVersion
   263  
   264  	// Optional Glob pattern matching module paths to skip when vendoring, e.g. “github.com/**”
   265  	NoVendor string
   266  
   267  	// When enabled, we will pick the vendored module closest to the module
   268  	// using it.
   269  	// The default behaviour is to pick the first.
   270  	// Note that there can still be only one dependency of a given module path,
   271  	// so once it is in use it cannot be redefined.
   272  	VendorClosest bool
   273  
   274  	// A comma separated (or a slice) list of module path to directory replacement mapping,
   275  	// e.g. github.com/bep/my-theme -> ../..,github.com/bep/shortcodes -> /some/path.
   276  	// This is mostly useful for temporary locally development of a module, and then it makes sense to set it as an
   277  	// OS environment variable, e.g: env HUGO_MODULE_REPLACEMENTS="github.com/bep/my-theme -> ../..".
   278  	// Any relative path is relate to themesDir, and absolute paths are allowed.
   279  	Replacements    []string
   280  	replacementsMap map[string]string
   281  
   282  	// Defines the proxy server to use to download remote modules. Default is direct, which means “git clone” and similar.
   283  	// Configures GOPROXY when running the Go command for module operations.
   284  	Proxy string
   285  
   286  	// Comma separated glob list matching paths that should not use the proxy configured above.
   287  	// Configures GONOPROXY when running the Go command for module operations.
   288  	NoProxy string
   289  
   290  	// Comma separated glob list matching paths that should be treated as private.
   291  	// Configures GOPRIVATE when running the Go command for module operations.
   292  	Private string
   293  
   294  	// Defaults to "off".
   295  	// Set to a work file, e.g. hugo.work, to enable Go "Workspace" mode.
   296  	// Can be relative to the working directory or absolute.
   297  	// Requires Go 1.18+.
   298  	// Note that this can also be set via OS env, e.g. export HUGO_MODULE_WORKSPACE=/my/hugo.work.
   299  	Workspace string
   300  }
   301  
   302  // hasModuleImport reports whether the project config have one or more
   303  // modules imports, e.g. github.com/bep/myshortcodes.
   304  func (c Config) hasModuleImport() bool {
   305  	for _, imp := range c.Imports {
   306  		if isProbablyModule(imp.Path) {
   307  			return true
   308  		}
   309  	}
   310  	return false
   311  }
   312  
   313  // HugoVersion holds Hugo binary version requirements for a module.
   314  type HugoVersion struct {
   315  	// The minimum Hugo version that this module works with.
   316  	Min hugo.VersionString
   317  
   318  	// The maximum Hugo version that this module works with.
   319  	Max hugo.VersionString
   320  
   321  	// Set if the extended version is needed.
   322  	Extended bool
   323  }
   324  
   325  func (v HugoVersion) String() string {
   326  	extended := ""
   327  	if v.Extended {
   328  		extended = " extended"
   329  	}
   330  
   331  	if v.Min != "" && v.Max != "" {
   332  		return fmt.Sprintf("%s/%s%s", v.Min, v.Max, extended)
   333  	}
   334  
   335  	if v.Min != "" {
   336  		return fmt.Sprintf("Min %s%s", v.Min, extended)
   337  	}
   338  
   339  	if v.Max != "" {
   340  		return fmt.Sprintf("Max %s%s", v.Max, extended)
   341  	}
   342  
   343  	return extended
   344  }
   345  
   346  // IsValid reports whether this version is valid compared to the running
   347  // Hugo binary.
   348  func (v HugoVersion) IsValid() bool {
   349  	current := hugo.CurrentVersion.Version()
   350  	if v.Extended && !hugo.IsExtended {
   351  		return false
   352  	}
   353  
   354  	isValid := true
   355  
   356  	if v.Min != "" && current.Compare(v.Min) > 0 {
   357  		isValid = false
   358  	}
   359  
   360  	if v.Max != "" && current.Compare(v.Max) < 0 {
   361  		isValid = false
   362  	}
   363  
   364  	return isValid
   365  }
   366  
   367  type Import struct {
   368  	// Module path
   369  	Path string
   370  	// Set when Path is replaced in project config.
   371  	pathProjectReplaced bool
   372  	// Ignore any config in config.toml (will still follow imports).
   373  	IgnoreConfig bool
   374  	// Do not follow any configured imports.
   375  	IgnoreImports bool
   376  	// Do not mount any folder in this import.
   377  	NoMounts bool
   378  	// Never vendor this import (only allowed in main project).
   379  	NoVendor bool
   380  	// Turn off this module.
   381  	Disable bool
   382  	// File mounts.
   383  	Mounts []Mount
   384  }
   385  
   386  type Mount struct {
   387  	// Relative path in source repo, e.g. "scss".
   388  	Source string
   389  
   390  	// Relative target path, e.g. "assets/bootstrap/scss".
   391  	Target string
   392  
   393  	// Any file in this mount will be associated with this language.
   394  	Lang string
   395  
   396  	// Include only files matching the given Glob patterns (string or slice).
   397  	IncludeFiles any
   398  
   399  	// Exclude all files matching the given Glob patterns (string or slice).
   400  	ExcludeFiles any
   401  }
   402  
   403  // Used as key to remove duplicates.
   404  func (m Mount) key() string {
   405  	return strings.Join([]string{m.Lang, m.Source, m.Target}, "/")
   406  }
   407  
   408  func (m Mount) Component() string {
   409  	return strings.Split(m.Target, fileSeparator)[0]
   410  }
   411  
   412  func (m Mount) ComponentAndName() (string, string) {
   413  	c, n, _ := strings.Cut(m.Target, fileSeparator)
   414  	return c, n
   415  }