mirror of
https://github.com/gohugoio/hugo.git
synced 2025-04-29 07:00:31 +03:00
Allow themes to define output formats, media types and params
This allows a `config.toml` (or `yaml`, ´yml`, or `json`) in the theme to set: 1) `params` (but cannot override params in project. Will also get its own "namespace", i.e. `{{ .Site.Params.mytheme.my_param }}` will be the same as `{{ .Site.Params.my_param }}` providing that the main project does not define a param with that key. 2) `menu` -- but cannot redefine/add menus in the project. Must create its own menus with its own identifiers. 3) `languages` -- only `params` and `menu`. Same rules as above. 4) **new** `outputFormats` 5) **new** `mediaTypes` This should help with the "theme portability" issue and people having to copy and paste lots of setting into their projects. Fixes #4490
This commit is contained in:
parent
3d1a6e109c
commit
e9c7b6205f
12 changed files with 796 additions and 218 deletions
13
Gopkg.lock
generated
13
Gopkg.lock
generated
|
@ -163,6 +163,7 @@
|
||||||
".",
|
".",
|
||||||
"hcl/ast",
|
"hcl/ast",
|
||||||
"hcl/parser",
|
"hcl/parser",
|
||||||
|
"hcl/printer",
|
||||||
"hcl/scanner",
|
"hcl/scanner",
|
||||||
"hcl/strconv",
|
"hcl/strconv",
|
||||||
"hcl/token",
|
"hcl/token",
|
||||||
|
@ -274,6 +275,12 @@
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5"
|
revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/sanity-io/litter"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "ae543b7ba8fd6af63e4976198f146e1348ae53c1"
|
||||||
|
version = "v1.1.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/shurcooL/sanitized_anchor_name"
|
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||||
|
@ -331,8 +338,8 @@
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/spf13/viper"
|
name = "github.com/spf13/viper"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
|
revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736"
|
||||||
version = "v1.0.0"
|
version = "v1.0.2"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
|
@ -417,6 +424,6 @@
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "4657586103d844434bda6db23d03f30e2ae0db16dc48746b9559ce742902535a"
|
inputs-digest = "13ab39f8bfafadc12c05726e565ee3f3d94bf7d6c0e8adf04056de0691bf2dd6"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
|
@ -141,3 +141,7 @@
|
||||||
name = "github.com/muesli/smartcrop"
|
name = "github.com/muesli/smartcrop"
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/sanity-io/litter"
|
||||||
|
version = "1.1.0"
|
||||||
|
|
|
@ -14,6 +14,18 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/utils"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/hugolib"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/types"
|
"github.com/gohugoio/hugo/common/types"
|
||||||
"github.com/gohugoio/hugo/deps"
|
"github.com/gohugoio/hugo/deps"
|
||||||
"github.com/gohugoio/hugo/helpers"
|
"github.com/gohugoio/hugo/helpers"
|
||||||
|
@ -23,11 +35,22 @@ import (
|
||||||
|
|
||||||
type commandeer struct {
|
type commandeer struct {
|
||||||
*deps.DepsCfg
|
*deps.DepsCfg
|
||||||
|
|
||||||
|
subCmdVs []*cobra.Command
|
||||||
|
|
||||||
pathSpec *helpers.PathSpec
|
pathSpec *helpers.PathSpec
|
||||||
visitedURLs *types.EvictingStringQueue
|
visitedURLs *types.EvictingStringQueue
|
||||||
|
|
||||||
staticDirsConfig []*src.Dirs
|
staticDirsConfig []*src.Dirs
|
||||||
|
|
||||||
|
// We watch these for changes.
|
||||||
|
configFiles []string
|
||||||
|
|
||||||
|
doWithCommandeer func(c *commandeer) error
|
||||||
|
|
||||||
|
// We can do this only once.
|
||||||
|
fsCreate sync.Once
|
||||||
|
|
||||||
serverPorts []int
|
serverPorts []int
|
||||||
languages helpers.Languages
|
languages helpers.Languages
|
||||||
|
|
||||||
|
@ -65,16 +88,158 @@ func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommandeer(cfg *deps.DepsCfg, running bool) (*commandeer, error) {
|
func newCommandeer(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||||
cfg.Running = running
|
|
||||||
|
|
||||||
var languages helpers.Languages
|
c := &commandeer{
|
||||||
|
doWithCommandeer: doWithCommandeer,
|
||||||
|
subCmdVs: append([]*cobra.Command{hugoCmdV}, subCmdVs...),
|
||||||
|
visitedURLs: types.NewEvictingStringQueue(10)}
|
||||||
|
|
||||||
if l, ok := cfg.Cfg.Get("languagesSorted").(helpers.Languages); ok {
|
return c, c.loadConfig(running)
|
||||||
languages = l
|
}
|
||||||
|
|
||||||
|
func (c *commandeer) loadConfig(running bool) error {
|
||||||
|
|
||||||
|
if c.DepsCfg == nil {
|
||||||
|
c.DepsCfg = &deps.DepsCfg{}
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &commandeer{DepsCfg: cfg, languages: languages, visitedURLs: types.NewEvictingStringQueue(10)}
|
cfg := c.DepsCfg
|
||||||
|
c.configured = false
|
||||||
|
cfg.Running = running
|
||||||
|
|
||||||
|
var dir string
|
||||||
|
if source != "" {
|
||||||
|
dir, _ = filepath.Abs(source)
|
||||||
|
} else {
|
||||||
|
dir, _ = os.Getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceFs afero.Fs = hugofs.Os
|
||||||
|
if c.DepsCfg.Fs != nil {
|
||||||
|
sourceFs = c.DepsCfg.Fs.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
config, configFiles, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: source, WorkingDir: dir, Filename: cfgFile})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cfg = config
|
||||||
|
c.configFiles = configFiles
|
||||||
|
|
||||||
|
for _, cmdV := range c.subCmdVs {
|
||||||
|
c.initializeFlags(cmdV)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l, ok := c.Cfg.Get("languagesSorted").(helpers.Languages); ok {
|
||||||
|
c.languages = l
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseURL != "" {
|
||||||
|
config.Set("baseURL", baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.doWithCommandeer != nil {
|
||||||
|
err = c.doWithCommandeer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(disableKinds) > 0 {
|
||||||
|
c.Set("disableKinds", disableKinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := createLogger(cfg.Cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger = logger
|
||||||
|
|
||||||
|
config.Set("logI18nWarnings", logI18nWarnings)
|
||||||
|
|
||||||
|
if theme != "" {
|
||||||
|
config.Set("theme", theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if themesDir != "" {
|
||||||
|
config.Set("themesDir", themesDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if destination != "" {
|
||||||
|
config.Set("publishDir", destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Set("workingDir", dir)
|
||||||
|
|
||||||
|
if contentDir != "" {
|
||||||
|
config.Set("contentDir", contentDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if layoutDir != "" {
|
||||||
|
config.Set("layoutDir", layoutDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheDir != "" {
|
||||||
|
config.Set("cacheDir", cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
createMemFs := config.GetBool("renderToMemory")
|
||||||
|
|
||||||
|
if createMemFs {
|
||||||
|
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
||||||
|
config.Set("publishDir", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.fsCreate.Do(func() {
|
||||||
|
fs := hugofs.NewFrom(sourceFs, config)
|
||||||
|
|
||||||
|
// Hugo writes the output to memory instead of the disk.
|
||||||
|
if createMemFs {
|
||||||
|
fs.Destination = new(afero.MemMapFs)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.initFs(fs)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir = config.GetString("cacheDir")
|
||||||
|
if cacheDir != "" {
|
||||||
|
if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
|
||||||
|
cacheDir = cacheDir + helpers.FilePathSeparator
|
||||||
|
}
|
||||||
|
isDir, err := helpers.DirExists(cacheDir, sourceFs)
|
||||||
|
utils.CheckErr(cfg.Logger, err)
|
||||||
|
if !isDir {
|
||||||
|
mkdir(cacheDir)
|
||||||
|
}
|
||||||
|
config.Set("cacheDir", cacheDir)
|
||||||
|
} else {
|
||||||
|
config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
||||||
|
|
||||||
|
themeDir := c.PathSpec().GetThemeDir()
|
||||||
|
if themeDir != "" {
|
||||||
|
if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
|
||||||
|
return newSystemError("Unable to find theme Directory:", themeDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
|
||||||
|
|
||||||
|
if themeVersionMismatch {
|
||||||
|
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||||
|
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
}
|
||||||
|
|
173
commands/hugo.go
173
commands/hugo.go
|
@ -25,8 +25,6 @@ import (
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/hugofs"
|
|
||||||
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -44,7 +42,6 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/gohugoio/hugo/deps"
|
|
||||||
"github.com/gohugoio/hugo/helpers"
|
"github.com/gohugoio/hugo/helpers"
|
||||||
"github.com/gohugoio/hugo/hugolib"
|
"github.com/gohugoio/hugo/hugolib"
|
||||||
"github.com/gohugoio/hugo/livereload"
|
"github.com/gohugoio/hugo/livereload"
|
||||||
|
@ -55,7 +52,6 @@ import (
|
||||||
"github.com/spf13/fsync"
|
"github.com/spf13/fsync"
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
"github.com/spf13/nitro"
|
"github.com/spf13/nitro"
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hugo represents the Hugo sites to build. This variable is exported as it
|
// Hugo represents the Hugo sites to build. This variable is exported as it
|
||||||
|
@ -142,10 +138,6 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if buildWatch {
|
|
||||||
c.watchConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.build()
|
return c.build()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -301,129 +293,11 @@ func init() {
|
||||||
// InitializeConfig initializes a config file with sensible default configuration flags.
|
// InitializeConfig initializes a config file with sensible default configuration flags.
|
||||||
func InitializeConfig(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
func InitializeConfig(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||||
|
|
||||||
var cfg *deps.DepsCfg = &deps.DepsCfg{}
|
c, err := newCommandeer(running, doWithCommandeer, subCmdVs...)
|
||||||
|
|
||||||
// Init file systems. This may be changed at a later point.
|
|
||||||
osFs := hugofs.Os
|
|
||||||
|
|
||||||
config, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: osFs, Src: source, Name: cfgFile})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init file systems. This may be changed at a later point.
|
|
||||||
cfg.Cfg = config
|
|
||||||
|
|
||||||
c, err := newCommandeer(cfg, running)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, cmdV := range append([]*cobra.Command{hugoCmdV}, subCmdVs...) {
|
|
||||||
c.initializeFlags(cmdV)
|
|
||||||
}
|
|
||||||
|
|
||||||
if baseURL != "" {
|
|
||||||
config.Set("baseURL", baseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if doWithCommandeer != nil {
|
|
||||||
if err := doWithCommandeer(c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(disableKinds) > 0 {
|
|
||||||
c.Set("disableKinds", disableKinds)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := createLogger(cfg.Cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Logger = logger
|
|
||||||
|
|
||||||
config.Set("logI18nWarnings", logI18nWarnings)
|
|
||||||
|
|
||||||
if theme != "" {
|
|
||||||
config.Set("theme", theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
if themesDir != "" {
|
|
||||||
config.Set("themesDir", themesDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if destination != "" {
|
|
||||||
config.Set("publishDir", destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dir string
|
|
||||||
if source != "" {
|
|
||||||
dir, _ = filepath.Abs(source)
|
|
||||||
} else {
|
|
||||||
dir, _ = os.Getwd()
|
|
||||||
}
|
|
||||||
config.Set("workingDir", dir)
|
|
||||||
|
|
||||||
if contentDir != "" {
|
|
||||||
config.Set("contentDir", contentDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if layoutDir != "" {
|
|
||||||
config.Set("layoutDir", layoutDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cacheDir != "" {
|
|
||||||
config.Set("cacheDir", cacheDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := hugofs.NewFrom(osFs, config)
|
|
||||||
|
|
||||||
// Hugo writes the output to memory instead of the disk.
|
|
||||||
// This is only used for benchmark testing. Cause the content is only visible
|
|
||||||
// in memory.
|
|
||||||
if config.GetBool("renderToMemory") {
|
|
||||||
fs.Destination = new(afero.MemMapFs)
|
|
||||||
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
|
||||||
config.Set("publishDir", "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir = config.GetString("cacheDir")
|
|
||||||
if cacheDir != "" {
|
|
||||||
if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
|
|
||||||
cacheDir = cacheDir + helpers.FilePathSeparator
|
|
||||||
}
|
|
||||||
isDir, err := helpers.DirExists(cacheDir, fs.Source)
|
|
||||||
utils.CheckErr(cfg.Logger, err)
|
|
||||||
if !isDir {
|
|
||||||
mkdir(cacheDir)
|
|
||||||
}
|
|
||||||
config.Set("cacheDir", cacheDir)
|
|
||||||
} else {
|
|
||||||
config.Set("cacheDir", helpers.GetTempDir("hugo_cache", fs.Source))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.initFs(fs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
|
||||||
|
|
||||||
themeDir := c.PathSpec().GetThemeDir()
|
|
||||||
if themeDir != "" {
|
|
||||||
if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) {
|
|
||||||
return nil, newSystemError("Unable to find theme Directory:", themeDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch()
|
|
||||||
|
|
||||||
if themeVersionMismatch {
|
|
||||||
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
|
||||||
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -524,20 +398,6 @@ If you need to set this configuration value from the command line, set it via an
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *commandeer) watchConfig() {
|
|
||||||
v := c.Cfg.(*viper.Viper)
|
|
||||||
v.WatchConfig()
|
|
||||||
v.OnConfigChange(func(e fsnotify.Event) {
|
|
||||||
c.Logger.FEEDBACK.Println("Config file changed:", e.Name)
|
|
||||||
// Force a full rebuild
|
|
||||||
utils.CheckErr(c.Logger, c.recreateAndBuildSites(true))
|
|
||||||
if !c.Cfg.GetBool("disableLiveReload") {
|
|
||||||
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
|
|
||||||
livereload.ForceRefresh()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *commandeer) fullBuild() error {
|
func (c *commandeer) fullBuild() error {
|
||||||
var (
|
var (
|
||||||
g errgroup.Group
|
g errgroup.Group
|
||||||
|
@ -942,6 +802,7 @@ func (c *commandeer) resetAndBuildSites() (err error) {
|
||||||
|
|
||||||
func (c *commandeer) initSites() error {
|
func (c *commandeer) initSites() error {
|
||||||
if Hugo != nil {
|
if Hugo != nil {
|
||||||
|
Hugo.Cfg = c.Cfg
|
||||||
Hugo.Log.ResetLogCounters()
|
Hugo.Log.ResetLogCounters()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1009,6 +870,15 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identifies changes to config (config.toml) files.
|
||||||
|
configSet := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, configFile := range c.configFiles {
|
||||||
|
c.Logger.FEEDBACK.Println("Watching for config changes in", configFile)
|
||||||
|
watcher.Add(configFile)
|
||||||
|
configSet[configFile] = true
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -1021,6 +891,21 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||||
// Special handling for symbolic links inside /content.
|
// Special handling for symbolic links inside /content.
|
||||||
filtered := []fsnotify.Event{}
|
filtered := []fsnotify.Event{}
|
||||||
for _, ev := range evs {
|
for _, ev := range evs {
|
||||||
|
if configSet[ev.Name] {
|
||||||
|
if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Config file changed. Need full rebuild.
|
||||||
|
if err := c.loadConfig(true); err != nil {
|
||||||
|
jww.ERROR.Println("Failed to reload config:", err)
|
||||||
|
} else if err := c.recreateAndBuildSites(true); err != nil {
|
||||||
|
jww.ERROR.Println(err)
|
||||||
|
} else if !buildWatch && !c.Cfg.GetBool("disableLiveReload") {
|
||||||
|
livereload.ForceRefresh()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// Check the most specific first, i.e. files.
|
// Check the most specific first, i.e. files.
|
||||||
contentMapped := Hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
|
contentMapped := Hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
|
||||||
if len(contentMapped) > 0 {
|
if len(contentMapped) > 0 {
|
||||||
|
@ -1212,7 +1097,7 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
|
||||||
|
|
||||||
// isThemeVsHugoVersionMismatch returns whether the current Hugo version is
|
// isThemeVsHugoVersionMismatch returns whether the current Hugo version is
|
||||||
// less than the theme's min_version.
|
// less than the theme's min_version.
|
||||||
func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) {
|
func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) {
|
||||||
if !c.PathSpec().ThemeSet() {
|
if !c.PathSpec().ThemeSet() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1221,13 +1106,13 @@ func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinV
|
||||||
|
|
||||||
path := filepath.Join(themeDir, "theme.toml")
|
path := filepath.Join(themeDir, "theme.toml")
|
||||||
|
|
||||||
exists, err := helpers.Exists(path, c.Fs.Source)
|
exists, err := helpers.Exists(path, fs)
|
||||||
|
|
||||||
if err != nil || !exists {
|
if err != nil || !exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := afero.ReadFile(c.Fs.Source, path)
|
b, err := afero.ReadFile(fs, path)
|
||||||
|
|
||||||
tomlMeta, err := parser.HandleTOMLMetaData(b)
|
tomlMeta, err := parser.HandleTOMLMetaData(b)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -111,12 +112,16 @@ func init() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serverPorts []int
|
||||||
|
|
||||||
func server(cmd *cobra.Command, args []string) error {
|
func server(cmd *cobra.Command, args []string) error {
|
||||||
// If a Destination is provided via flag write to disk
|
// If a Destination is provided via flag write to disk
|
||||||
if destination != "" {
|
if destination != "" {
|
||||||
renderToDisk = true
|
renderToDisk = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serverCfgInit sync.Once
|
||||||
|
|
||||||
cfgInit := func(c *commandeer) error {
|
cfgInit := func(c *commandeer) error {
|
||||||
c.Set("renderToMemory", !renderToDisk)
|
c.Set("renderToMemory", !renderToDisk)
|
||||||
if cmd.Flags().Changed("navigateToChanged") {
|
if cmd.Flags().Changed("navigateToChanged") {
|
||||||
|
@ -132,37 +137,42 @@ func server(cmd *cobra.Command, args []string) error {
|
||||||
c.Set("watch", true)
|
c.Set("watch", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverPorts := make([]int, 1)
|
var err error
|
||||||
|
|
||||||
if c.languages.IsMultihost() {
|
// We can only do this once.
|
||||||
if !serverAppend {
|
serverCfgInit.Do(func() {
|
||||||
return newSystemError("--appendPort=false not supported when in multihost mode")
|
serverPorts = make([]int, 1)
|
||||||
}
|
|
||||||
serverPorts = make([]int, len(c.languages))
|
|
||||||
}
|
|
||||||
|
|
||||||
currentServerPort := serverPort
|
if c.languages.IsMultihost() {
|
||||||
|
if !serverAppend {
|
||||||
for i := 0; i < len(serverPorts); i++ {
|
err = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||||
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort)))
|
|
||||||
if err == nil {
|
|
||||||
l.Close()
|
|
||||||
serverPorts[i] = currentServerPort
|
|
||||||
} else {
|
|
||||||
if i == 0 && serverCmd.Flags().Changed("port") {
|
|
||||||
// port set explicitly by user -- he/she probably meant it!
|
|
||||||
return newSystemErrorF("Server startup failed: %s", err)
|
|
||||||
}
|
}
|
||||||
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
|
serverPorts = make([]int, len(c.languages))
|
||||||
sp, err := helpers.FindAvailablePort()
|
|
||||||
if err != nil {
|
|
||||||
return newSystemError("Unable to find alternative port to use:", err)
|
|
||||||
}
|
|
||||||
serverPorts[i] = sp.Port
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentServerPort = serverPorts[i] + 1
|
currentServerPort := serverPort
|
||||||
}
|
|
||||||
|
for i := 0; i < len(serverPorts); i++ {
|
||||||
|
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort)))
|
||||||
|
if err == nil {
|
||||||
|
l.Close()
|
||||||
|
serverPorts[i] = currentServerPort
|
||||||
|
} else {
|
||||||
|
if i == 0 && serverCmd.Flags().Changed("port") {
|
||||||
|
// port set explicitly by user -- he/she probably meant it!
|
||||||
|
err = newSystemErrorF("Server startup failed: %s", err)
|
||||||
|
}
|
||||||
|
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
|
||||||
|
sp, err := helpers.FindAvailablePort()
|
||||||
|
if err != nil {
|
||||||
|
err = newSystemError("Unable to find alternative port to use:", err)
|
||||||
|
}
|
||||||
|
serverPorts[i] = sp.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
currentServerPort = serverPorts[i] + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
c.serverPorts = serverPorts
|
c.serverPorts = serverPorts
|
||||||
|
|
||||||
|
@ -184,7 +194,7 @@ func server(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
baseURL, err := fixURL(language, baseURL, serverPort)
|
baseURL, err := fixURL(language, baseURL, serverPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
if isMultiHost {
|
if isMultiHost {
|
||||||
language.Set("baseURL", baseURL)
|
language.Set("baseURL", baseURL)
|
||||||
|
@ -194,7 +204,7 @@ func server(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,10 +225,6 @@ func server(cmd *cobra.Command, args []string) error {
|
||||||
s.RegisterMediaTypes()
|
s.RegisterMediaTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverWatch {
|
|
||||||
c.watchConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch runs its own server as part of the routine
|
// Watch runs its own server as part of the routine
|
||||||
if serverWatch {
|
if serverWatch {
|
||||||
|
|
||||||
|
|
|
@ -154,11 +154,16 @@ func ReplaceExtension(path string, newExt string) string {
|
||||||
// AbsPathify creates an absolute path if given a relative path. If already
|
// AbsPathify creates an absolute path if given a relative path. If already
|
||||||
// absolute, the path is just cleaned.
|
// absolute, the path is just cleaned.
|
||||||
func (p *PathSpec) AbsPathify(inPath string) string {
|
func (p *PathSpec) AbsPathify(inPath string) string {
|
||||||
|
return AbsPathify(p.workingDir, inPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbsPathify creates an absolute path if given a working dir and arelative path.
|
||||||
|
// If already absolute, the path is just cleaned.
|
||||||
|
func AbsPathify(workingDir, inPath string) string {
|
||||||
if filepath.IsAbs(inPath) {
|
if filepath.IsAbs(inPath) {
|
||||||
return filepath.Clean(inPath)
|
return filepath.Clean(inPath)
|
||||||
}
|
}
|
||||||
|
return filepath.Join(workingDir, inPath)
|
||||||
return filepath.Join(p.workingDir, inPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLayoutDirPath returns the absolute path to the layout file dir
|
// GetLayoutDirPath returns the absolute path to the layout file dir
|
||||||
|
|
|
@ -149,7 +149,7 @@ func TestCaseInsensitiveConfigurationVariations(t *testing.T) {
|
||||||
|
|
||||||
caseMixingTestsWriteCommonSources(t, mm)
|
caseMixingTestsWriteCommonSources(t, mm)
|
||||||
|
|
||||||
cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm})
|
cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fs := hugofs.NewFrom(mm, cfg)
|
fs := hugofs.NewFrom(mm, cfg)
|
||||||
|
|
|
@ -16,6 +16,7 @@ package hugolib
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -28,64 +29,91 @@ import (
|
||||||
|
|
||||||
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
|
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
|
||||||
type ConfigSourceDescriptor struct {
|
type ConfigSourceDescriptor struct {
|
||||||
Fs afero.Fs
|
Fs afero.Fs
|
||||||
Src string
|
|
||||||
Name string
|
// Full path to the config file to use, i.e. /my/project/config.toml
|
||||||
|
Filename string
|
||||||
|
|
||||||
|
// The path to the directory to look for configuration. Is used if Filename is not
|
||||||
|
// set.
|
||||||
|
Path string
|
||||||
|
|
||||||
|
// The project's working dir. Is used to look for additional theme config.
|
||||||
|
WorkingDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d ConfigSourceDescriptor) configFilenames() []string {
|
func (d ConfigSourceDescriptor) configFilenames() []string {
|
||||||
return strings.Split(d.Name, ",")
|
return strings.Split(d.Filename, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigDefault is a convenience method to load the default "config.toml" config.
|
// LoadConfigDefault is a convenience method to load the default "config.toml" config.
|
||||||
func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
|
func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
|
||||||
return LoadConfig(ConfigSourceDescriptor{Fs: fs, Name: "config.toml"})
|
v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
|
||||||
|
return v, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads Hugo configuration into a new Viper and then adds
|
// LoadConfig loads Hugo configuration into a new Viper and then adds
|
||||||
// a set of defaults.
|
// a set of defaults.
|
||||||
func LoadConfig(d ConfigSourceDescriptor) (*viper.Viper, error) {
|
func LoadConfig(d ConfigSourceDescriptor) (*viper.Viper, []string, error) {
|
||||||
|
var configFiles []string
|
||||||
|
|
||||||
fs := d.Fs
|
fs := d.Fs
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
v.SetFs(fs)
|
v.SetFs(fs)
|
||||||
|
|
||||||
if d.Name == "" {
|
if d.Path == "" {
|
||||||
d.Name = "config.toml"
|
d.Path = "."
|
||||||
}
|
|
||||||
|
|
||||||
if d.Src == "" {
|
|
||||||
d.Src = "."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configFilenames := d.configFilenames()
|
configFilenames := d.configFilenames()
|
||||||
v.AutomaticEnv()
|
v.AutomaticEnv()
|
||||||
v.SetEnvPrefix("hugo")
|
v.SetEnvPrefix("hugo")
|
||||||
v.SetConfigFile(configFilenames[0])
|
v.SetConfigFile(configFilenames[0])
|
||||||
v.AddConfigPath(d.Src)
|
v.AddConfigPath(d.Path)
|
||||||
|
|
||||||
err := v.ReadInConfig()
|
err := v.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(viper.ConfigParseError); ok {
|
if _, ok := err.(viper.ConfigParseError); ok {
|
||||||
return nil, err
|
return nil, configFiles, err
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details. (%s)\n", err)
|
return nil, configFiles, fmt.Errorf("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details. (%s)\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cf := v.ConfigFileUsed(); cf != "" {
|
||||||
|
configFiles = append(configFiles, cf)
|
||||||
|
}
|
||||||
|
|
||||||
for _, configFile := range configFilenames[1:] {
|
for _, configFile := range configFilenames[1:] {
|
||||||
var r io.Reader
|
var r io.Reader
|
||||||
var err error
|
var err error
|
||||||
if r, err = fs.Open(configFile); err != nil {
|
if r, err = fs.Open(configFile); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to open Config file.\n (%s)\n", err)
|
return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err)
|
||||||
}
|
}
|
||||||
if err = v.MergeConfig(r); err != nil {
|
if err = v.MergeConfig(r); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err)
|
return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err)
|
||||||
}
|
}
|
||||||
|
configFiles = append(configFiles, configFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := loadDefaultSettingsFor(v); err != nil {
|
if err := loadDefaultSettingsFor(v); err != nil {
|
||||||
return v, err
|
return v, configFiles, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
themeConfigFile, err := loadThemeConfig(d, v)
|
||||||
|
if err != nil {
|
||||||
|
return v, configFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeConfigFile != "" {
|
||||||
|
configFiles = append(configFiles, themeConfigFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := loadLanguageSettings(v, nil); err != nil {
|
||||||
|
return v, configFiles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, configFiles, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error {
|
func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error {
|
||||||
|
@ -201,6 +229,142 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error) {
|
||||||
|
|
||||||
|
theme := v1.GetString("theme")
|
||||||
|
if theme == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
themesDir := helpers.AbsPathify(d.WorkingDir, v1.GetString("themesDir"))
|
||||||
|
configDir := filepath.Join(themesDir, theme)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configPath string
|
||||||
|
exists bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Viper supports more, but this is the sub-set supported by Hugo.
|
||||||
|
for _, configFormats := range []string{"toml", "yaml", "yml", "json"} {
|
||||||
|
configPath = filepath.Join(configDir, "config."+configFormats)
|
||||||
|
exists, err = helpers.Exists(configPath, d.Fs)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
// No theme config set.
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v2 := viper.New()
|
||||||
|
v2.SetFs(d.Fs)
|
||||||
|
v2.AutomaticEnv()
|
||||||
|
v2.SetEnvPrefix("hugo")
|
||||||
|
v2.SetConfigFile(configPath)
|
||||||
|
|
||||||
|
err = v2.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
paramsKey = "params"
|
||||||
|
languagesKey = "languages"
|
||||||
|
menuKey = "menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
|
||||||
|
mergeStringMapKeepLeft("", key, v1, v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
themeLower := strings.ToLower(theme)
|
||||||
|
themeParamsNamespace := paramsKey + "." + themeLower
|
||||||
|
|
||||||
|
// Set namespaced params
|
||||||
|
if v2.IsSet(paramsKey) && !v1.IsSet(themeParamsNamespace) {
|
||||||
|
// Set it in the default store to make sure it gets in the same or
|
||||||
|
// behind the others.
|
||||||
|
v1.SetDefault(themeParamsNamespace, v2.Get(paramsKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add params and new menu entries, we do not add language definitions.
|
||||||
|
if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) {
|
||||||
|
v1Langs := v1.GetStringMap(languagesKey)
|
||||||
|
for k, _ := range v1Langs {
|
||||||
|
langParamsKey := languagesKey + "." + k + "." + paramsKey
|
||||||
|
mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
|
||||||
|
}
|
||||||
|
v2Langs := v2.GetStringMap(languagesKey)
|
||||||
|
for k, _ := range v2Langs {
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
langParamsKey := languagesKey + "." + k + "." + paramsKey
|
||||||
|
langParamsThemeNamespace := langParamsKey + "." + themeLower
|
||||||
|
// Set namespaced params
|
||||||
|
if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) {
|
||||||
|
v1.SetDefault(langParamsThemeNamespace, v2.Get(langParamsKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
langMenuKey := languagesKey + "." + k + "." + menuKey
|
||||||
|
if v2.IsSet(langMenuKey) {
|
||||||
|
// Only add if not in the main config.
|
||||||
|
v2menus := v2.GetStringMap(langMenuKey)
|
||||||
|
for k, v := range v2menus {
|
||||||
|
menuEntry := menuKey + "." + k
|
||||||
|
menuLangEntry := langMenuKey + "." + k
|
||||||
|
if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) {
|
||||||
|
v1.Set(menuLangEntry, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add menu definitions from theme not found in project
|
||||||
|
if v2.IsSet("menu") {
|
||||||
|
v2menus := v2.GetStringMap(menuKey)
|
||||||
|
for k, v := range v2menus {
|
||||||
|
menuEntry := menuKey + "." + k
|
||||||
|
if !v1.IsSet(menuEntry) {
|
||||||
|
v1.SetDefault(menuEntry, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v2.ConfigFileUsed(), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeStringMapKeepLeft(rootKey, key string, v1, v2 *viper.Viper) {
|
||||||
|
if !v2.IsSet(key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) {
|
||||||
|
v1.Set(key, v2.Get(key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m1 := v1.GetStringMap(key)
|
||||||
|
m2 := v2.GetStringMap(key)
|
||||||
|
|
||||||
|
for k, v := range m2 {
|
||||||
|
if _, found := m1[k]; !found {
|
||||||
|
if rootKey != "" && v1.IsSet(rootKey+"."+k) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m1[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func loadDefaultSettingsFor(v *viper.Viper) error {
|
func loadDefaultSettingsFor(v *viper.Viper) error {
|
||||||
|
|
||||||
c, err := helpers.NewContentSpec(v)
|
c, err := helpers.NewContentSpec(v)
|
||||||
|
@ -281,5 +445,5 @@ lastmod = ["lastmod" ,":fileModTime", ":default"]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadLanguageSettings(v, nil)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,15 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadConfig(t *testing.T) {
|
func TestLoadConfig(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
// Add a random config variable for testing.
|
// Add a random config variable for testing.
|
||||||
// side = page in Norwegian.
|
// side = page in Norwegian.
|
||||||
configContent := `
|
configContent := `
|
||||||
|
@ -34,16 +36,19 @@ func TestLoadConfig(t *testing.T) {
|
||||||
|
|
||||||
writeToFs(t, mm, "hugo.toml", configContent)
|
writeToFs(t, mm, "hugo.toml", configContent)
|
||||||
|
|
||||||
cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Name: "hugo.toml"})
|
cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "hugo.toml"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "side", cfg.GetString("paginatePath"))
|
assert.Equal("side", cfg.GetString("paginatePath"))
|
||||||
// default
|
// default
|
||||||
assert.Equal(t, "layouts", cfg.GetString("layoutDir"))
|
assert.Equal("layouts", cfg.GetString("layoutDir"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadMultiConfig(t *testing.T) {
|
func TestLoadMultiConfig(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
// Add a random config variable for testing.
|
// Add a random config variable for testing.
|
||||||
// side = page in Norwegian.
|
// side = page in Norwegian.
|
||||||
configContentBase := `
|
configContentBase := `
|
||||||
|
@ -59,9 +64,304 @@ func TestLoadMultiConfig(t *testing.T) {
|
||||||
|
|
||||||
writeToFs(t, mm, "override.toml", configContentSub)
|
writeToFs(t, mm, "override.toml", configContentSub)
|
||||||
|
|
||||||
cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Name: "base.toml,override.toml"})
|
cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "base.toml,override.toml"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "top", cfg.GetString("paginatePath"))
|
assert.Equal("top", cfg.GetString("paginatePath"))
|
||||||
assert.Equal(t, "same", cfg.GetString("DontChange"))
|
assert.Equal("same", cfg.GetString("DontChange"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigFromTheme(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := require.New(t)
|
||||||
|
|
||||||
|
mainConfigBasic := `
|
||||||
|
theme = "test-theme"
|
||||||
|
baseURL = "https://example.com/"
|
||||||
|
|
||||||
|
`
|
||||||
|
mainConfig := `
|
||||||
|
theme = "test-theme"
|
||||||
|
baseURL = "https://example.com/"
|
||||||
|
|
||||||
|
[frontmatter]
|
||||||
|
date = ["date","publishDate"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
p1 = "p1 main"
|
||||||
|
p2 = "p2 main"
|
||||||
|
top = "top"
|
||||||
|
|
||||||
|
[mediaTypes]
|
||||||
|
[mediaTypes."text/m1"]
|
||||||
|
suffix = "m1main"
|
||||||
|
|
||||||
|
[outputFormats.o1]
|
||||||
|
mediaType = "text/m1"
|
||||||
|
baseName = "o1main"
|
||||||
|
|
||||||
|
[languages]
|
||||||
|
[languages.en]
|
||||||
|
languageName = "English"
|
||||||
|
[languages.en.params]
|
||||||
|
pl1 = "p1-en-main"
|
||||||
|
[languages.nb]
|
||||||
|
languageName = "Norsk"
|
||||||
|
[languages.nb.params]
|
||||||
|
pl1 = "p1-nb-main"
|
||||||
|
|
||||||
|
[[menu.main]]
|
||||||
|
name = "menu-main-main"
|
||||||
|
|
||||||
|
[[menu.top]]
|
||||||
|
name = "menu-top-main"
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
themeConfig := `
|
||||||
|
baseURL = "http://bep.is/"
|
||||||
|
|
||||||
|
# Can not be set in theme.
|
||||||
|
[frontmatter]
|
||||||
|
expiryDate = ["date"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
p1 = "p1 theme"
|
||||||
|
p2 = "p2 theme"
|
||||||
|
p3 = "p3 theme"
|
||||||
|
|
||||||
|
[mediaTypes]
|
||||||
|
[mediaTypes."text/m1"]
|
||||||
|
suffix = "m1theme"
|
||||||
|
[mediaTypes."text/m2"]
|
||||||
|
suffix = "m2theme"
|
||||||
|
|
||||||
|
[outputFormats.o1]
|
||||||
|
mediaType = "text/m1"
|
||||||
|
baseName = "o1theme"
|
||||||
|
[outputFormats.o2]
|
||||||
|
mediaType = "text/m2"
|
||||||
|
baseName = "o2theme"
|
||||||
|
|
||||||
|
[languages]
|
||||||
|
[languages.en]
|
||||||
|
languageName = "English2"
|
||||||
|
[languages.en.params]
|
||||||
|
pl1 = "p1-en-theme"
|
||||||
|
pl2 = "p2-en-theme"
|
||||||
|
[[languages.en.menu.main]]
|
||||||
|
name = "menu-lang-en-main"
|
||||||
|
[[languages.en.menu.theme]]
|
||||||
|
name = "menu-lang-en-theme"
|
||||||
|
[languages.nb]
|
||||||
|
languageName = "Norsk2"
|
||||||
|
[languages.nb.params]
|
||||||
|
pl1 = "p1-nb-theme"
|
||||||
|
pl2 = "p2-nb-theme"
|
||||||
|
top = "top-nb-theme"
|
||||||
|
[[languages.nb.menu.main]]
|
||||||
|
name = "menu-lang-nb-main"
|
||||||
|
[[languages.nb.menu.theme]]
|
||||||
|
name = "menu-lang-nb-theme"
|
||||||
|
[[languages.nb.menu.top]]
|
||||||
|
name = "menu-lang-nb-top"
|
||||||
|
|
||||||
|
[[menu.main]]
|
||||||
|
name = "menu-main-theme"
|
||||||
|
|
||||||
|
[[menu.thememenu]]
|
||||||
|
name = "menu-theme"
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
b := newTestSitesBuilder(t)
|
||||||
|
b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
|
||||||
|
b.CreateSites().Build(BuildCfg{})
|
||||||
|
|
||||||
|
got := b.Cfg.(*viper.Viper).AllSettings()
|
||||||
|
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"p1": "p1 main",
|
||||||
|
"p2": "p2 main",
|
||||||
|
"p3": "p3 theme",
|
||||||
|
"test-theme": map[string]interface {}{
|
||||||
|
"p1": "p1 theme",
|
||||||
|
"p2": "p2 theme",
|
||||||
|
"p3": "p3 theme",
|
||||||
|
},
|
||||||
|
"top": "top",
|
||||||
|
}`, got["params"])
|
||||||
|
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"date": []interface {}{
|
||||||
|
"date",
|
||||||
|
"publishDate",
|
||||||
|
},
|
||||||
|
}`, got["frontmatter"])
|
||||||
|
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"text/m1": map[string]interface {}{
|
||||||
|
"suffix": "m1main",
|
||||||
|
},
|
||||||
|
"text/m2": map[string]interface {}{
|
||||||
|
"suffix": "m2theme",
|
||||||
|
},
|
||||||
|
}`, got["mediatypes"])
|
||||||
|
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"o1": map[string]interface {}{
|
||||||
|
"basename": "o1main",
|
||||||
|
"mediatype": Type{
|
||||||
|
MainType: "text",
|
||||||
|
SubType: "m1",
|
||||||
|
Suffix: "m1main",
|
||||||
|
Delimiter: ".",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"o2": map[string]interface {}{
|
||||||
|
"basename": "o2theme",
|
||||||
|
"mediatype": Type{
|
||||||
|
MainType: "text",
|
||||||
|
SubType: "m2",
|
||||||
|
Suffix: "m2theme",
|
||||||
|
Delimiter: ".",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`, got["outputformats"])
|
||||||
|
|
||||||
|
b.AssertObject(`map[string]interface {}{
|
||||||
|
"en": map[string]interface {}{
|
||||||
|
"languagename": "English",
|
||||||
|
"menu": map[string]interface {}{
|
||||||
|
"theme": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-lang-en-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"params": map[string]interface {}{
|
||||||
|
"pl1": "p1-en-main",
|
||||||
|
"pl2": "p2-en-theme",
|
||||||
|
"test-theme": map[string]interface {}{
|
||||||
|
"pl1": "p1-en-theme",
|
||||||
|
"pl2": "p2-en-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nb": map[string]interface {}{
|
||||||
|
"languagename": "Norsk",
|
||||||
|
"menu": map[string]interface {}{
|
||||||
|
"theme": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-lang-nb-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"params": map[string]interface {}{
|
||||||
|
"pl1": "p1-nb-main",
|
||||||
|
"pl2": "p2-nb-theme",
|
||||||
|
"test-theme": map[string]interface {}{
|
||||||
|
"pl1": "p1-nb-theme",
|
||||||
|
"pl2": "p2-nb-theme",
|
||||||
|
"top": "top-nb-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`, got["languages"])
|
||||||
|
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"main": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-main-main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"thememenu": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"top": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-top-main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`, got["menu"])
|
||||||
|
|
||||||
|
assert.Equal("https://example.com/", got["baseurl"])
|
||||||
|
|
||||||
|
if true {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Test variants with only values from theme
|
||||||
|
b = newTestSitesBuilder(t)
|
||||||
|
b.WithConfigFile("toml", mainConfigBasic).WithThemeConfigFile("toml", themeConfig)
|
||||||
|
b.CreateSites().Build(BuildCfg{})
|
||||||
|
|
||||||
|
got = b.Cfg.(*viper.Viper).AllSettings()
|
||||||
|
|
||||||
|
b.AssertObject(`map[string]interface {}{
|
||||||
|
"p1": "p1 theme",
|
||||||
|
"p2": "p2 theme",
|
||||||
|
"p3": "p3 theme",
|
||||||
|
"test-theme": map[string]interface {}{
|
||||||
|
"p1": "p1 theme",
|
||||||
|
"p2": "p2 theme",
|
||||||
|
"p3": "p3 theme",
|
||||||
|
},
|
||||||
|
}`, got["params"])
|
||||||
|
|
||||||
|
assert.Nil(got["languages"])
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"text/m1": map[string]interface {}{
|
||||||
|
"suffix": "m1theme",
|
||||||
|
},
|
||||||
|
"text/m2": map[string]interface {}{
|
||||||
|
"suffix": "m2theme",
|
||||||
|
},
|
||||||
|
}`, got["mediatypes"])
|
||||||
|
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"o1": map[string]interface {}{
|
||||||
|
"basename": "o1theme",
|
||||||
|
"mediatype": Type{
|
||||||
|
MainType: "text",
|
||||||
|
SubType: "m1",
|
||||||
|
Suffix: "m1theme",
|
||||||
|
Delimiter: ".",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"o2": map[string]interface {}{
|
||||||
|
"basename": "o2theme",
|
||||||
|
"mediatype": Type{
|
||||||
|
MainType: "text",
|
||||||
|
SubType: "m2",
|
||||||
|
Suffix: "m2theme",
|
||||||
|
Delimiter: ".",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`, got["outputformats"])
|
||||||
|
b.AssertObject(`
|
||||||
|
map[string]interface {}{
|
||||||
|
"main": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-main-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"thememenu": []interface {}{
|
||||||
|
map[string]interface {}{
|
||||||
|
"name": "menu-theme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`, got["menu"])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,6 +200,7 @@ func TestPageBundlerSiteMultilingual(t *testing.T) {
|
||||||
cfg.Set("uglyURLs", ugly)
|
cfg.Set("uglyURLs", ugly)
|
||||||
|
|
||||||
assert.NoError(loadDefaultSettingsFor(cfg))
|
assert.NoError(loadDefaultSettingsFor(cfg))
|
||||||
|
assert.NoError(loadLanguageSettings(cfg, nil))
|
||||||
sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
|
sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.Equal(2, len(sites.Sites))
|
assert.Equal(2, len(sites.Sites))
|
||||||
|
@ -264,6 +265,8 @@ func TestMultilingualDisableDefaultLanguage(t *testing.T) {
|
||||||
cfg.Set("disableLanguages", []string{"en"})
|
cfg.Set("disableLanguages", []string{"en"})
|
||||||
|
|
||||||
err := loadDefaultSettingsFor(cfg)
|
err := loadDefaultSettingsFor(cfg)
|
||||||
|
assert.NoError(err)
|
||||||
|
err = loadLanguageSettings(cfg, nil)
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Contains(err.Error(), "cannot disable default language")
|
assert.Contains(err.Error(), "cannot disable default language")
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,6 +296,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) {
|
||||||
// NewSiteDefaultLang creates a new site in the default language.
|
// NewSiteDefaultLang creates a new site in the default language.
|
||||||
// The site will have a template system loaded and ready to use.
|
// The site will have a template system loaded and ready to use.
|
||||||
// Note: This is mainly used in single site tests.
|
// Note: This is mainly used in single site tests.
|
||||||
|
// TODO(bep) test refactor -- remove
|
||||||
func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
if err := loadDefaultSettingsFor(v); err != nil {
|
if err := loadDefaultSettingsFor(v); err != nil {
|
||||||
|
@ -307,6 +308,7 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (
|
||||||
// NewEnglishSite creates a new site in English language.
|
// NewEnglishSite creates a new site in English language.
|
||||||
// The site will have a template system loaded and ready to use.
|
// The site will have a template system loaded and ready to use.
|
||||||
// Note: This is mainly used in single site tests.
|
// Note: This is mainly used in single site tests.
|
||||||
|
// TODO(bep) test refactor -- remove
|
||||||
func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
if err := loadDefaultSettingsFor(v); err != nil {
|
if err := loadDefaultSettingsFor(v); err != nil {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/sanity-io/litter"
|
||||||
|
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/config"
|
"github.com/gohugoio/hugo/config"
|
||||||
|
@ -37,11 +39,15 @@ type sitesBuilder struct {
|
||||||
Fs *hugofs.Fs
|
Fs *hugofs.Fs
|
||||||
T testing.TB
|
T testing.TB
|
||||||
|
|
||||||
|
dumper litter.Options
|
||||||
|
|
||||||
// Aka the Hugo server mode.
|
// Aka the Hugo server mode.
|
||||||
running bool
|
running bool
|
||||||
|
|
||||||
H *HugoSites
|
H *HugoSites
|
||||||
|
|
||||||
|
theme string
|
||||||
|
|
||||||
// Default toml
|
// Default toml
|
||||||
configFormat string
|
configFormat string
|
||||||
|
|
||||||
|
@ -63,7 +69,13 @@ func newTestSitesBuilder(t testing.TB) *sitesBuilder {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
fs := hugofs.NewMem(v)
|
fs := hugofs.NewMem(v)
|
||||||
|
|
||||||
return &sitesBuilder{T: t, Fs: fs, configFormat: "toml"}
|
litterOptions := litter.Options{
|
||||||
|
HidePrivateFields: true,
|
||||||
|
StripPackageNames: true,
|
||||||
|
Separator: " ",
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sitesBuilder{T: t, Fs: fs, configFormat: "toml", dumper: litterOptions}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sitesBuilder) Running() *sitesBuilder {
|
func (s *sitesBuilder) Running() *sitesBuilder {
|
||||||
|
@ -97,6 +109,15 @@ func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder {
|
||||||
|
if s.theme == "" {
|
||||||
|
s.theme = "test-theme"
|
||||||
|
}
|
||||||
|
filename := filepath.Join("themes", s.theme, "config."+format)
|
||||||
|
writeSource(s.T, s.Fs, filename, conf)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder {
|
func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder {
|
||||||
var config = `
|
var config = `
|
||||||
baseURL = "http://example.com/"
|
baseURL = "http://example.com/"
|
||||||
|
@ -229,10 +250,15 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder {
|
||||||
s.writeFilePairs("i18n", s.i18nFilePairsAdded)
|
s.writeFilePairs("i18n", s.i18nFilePairsAdded)
|
||||||
|
|
||||||
if s.Cfg == nil {
|
if s.Cfg == nil {
|
||||||
cfg, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Name: "config." + s.configFormat})
|
cfg, configFiles, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Fatalf("Failed to load config: %s", err)
|
s.Fatalf("Failed to load config: %s", err)
|
||||||
}
|
}
|
||||||
|
expectedConfigs := 1
|
||||||
|
if s.theme != "" {
|
||||||
|
expectedConfigs = 2
|
||||||
|
}
|
||||||
|
require.Equal(s.T, expectedConfigs, len(configFiles), fmt.Sprintf("Configs: %v", configFiles))
|
||||||
s.Cfg = cfg
|
s.Cfg = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,6 +371,17 @@ func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *sitesBuilder) AssertObject(expected string, object interface{}) {
|
||||||
|
got := s.dumper.Sdump(object)
|
||||||
|
expected = strings.TrimSpace(expected)
|
||||||
|
|
||||||
|
if expected != got {
|
||||||
|
fmt.Println(got)
|
||||||
|
diff := helpers.DiffStrings(expected, got)
|
||||||
|
s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
|
func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
|
||||||
content := readDestination(s.T, s.Fs, filename)
|
content := readDestination(s.T, s.Fs, filename)
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue