summaryrefslogtreecommitdiffstats
path: root/hugolib
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib')
-rw-r--r--hugolib/alias_test.go4
-rw-r--r--hugolib/case_insensitive_test.go3
-rw-r--r--hugolib/config.go145
-rw-r--r--hugolib/datafiles_test.go4
-rw-r--r--hugolib/filesystems/basefs.go644
-rw-r--r--hugolib/filesystems/basefs_test.go170
-rw-r--r--hugolib/hugo_sites.go8
-rw-r--r--hugolib/hugo_sites_build_test.go28
-rw-r--r--hugolib/hugo_sites_multihost_test.go3
-rw-r--r--hugolib/hugo_themes_test.go268
-rw-r--r--hugolib/multilingual.go43
-rw-r--r--hugolib/page.go10
-rw-r--r--hugolib/page_bundler_capture.go2
-rw-r--r--hugolib/page_bundler_capture_test.go12
-rw-r--r--hugolib/page_bundler_test.go8
-rw-r--r--hugolib/pagination.go2
-rw-r--r--hugolib/paths/baseURL.go79
-rw-r--r--hugolib/paths/baseURL_test.go61
-rw-r--r--hugolib/paths/paths.go231
-rw-r--r--hugolib/paths/paths_test.go40
-rw-r--r--hugolib/paths/themes.go162
-rw-r--r--hugolib/shortcode_test.go4
-rw-r--r--hugolib/site.go188
-rw-r--r--hugolib/testhelpers_test.go47
24 files changed, 1849 insertions, 317 deletions
diff --git a/hugolib/alias_test.go b/hugolib/alias_test.go
index d20409512..04c5b4358 100644
--- a/hugolib/alias_test.go
+++ b/hugolib/alias_test.go
@@ -18,6 +18,8 @@ import (
"runtime"
"testing"
+ "github.com/gohugoio/hugo/common/loggers"
+
"github.com/stretchr/testify/require"
)
@@ -97,7 +99,7 @@ func TestAliasTemplate(t *testing.T) {
}
func TestTargetPathHTMLRedirectAlias(t *testing.T) {
- h := newAliasHandler(nil, newErrorLogger(), false)
+ h := newAliasHandler(nil, loggers.NewErrorLogger(), false)
errIsNilForThisOS := runtime.GOOS != "windows"
diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go
index 52ef198a5..f3ba5f933 100644
--- a/hugolib/case_insensitive_test.go
+++ b/hugolib/case_insensitive_test.go
@@ -19,8 +19,9 @@ import (
"strings"
"testing"
- "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/deps"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
diff --git a/hugolib/config.go b/hugolib/config.go
index 73ba84686..dec5b870d 100644
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -16,11 +16,14 @@ package hugolib
import (
"errors"
"fmt"
- "path/filepath"
+
+ "github.com/gohugoio/hugo/hugolib/paths"
"io"
"strings"
+ "github.com/gohugoio/hugo/langs"
+
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/services"
@@ -81,6 +84,8 @@ func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
return v, err
}
+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")
+
// LoadConfig loads Hugo configuration into a new Viper and then adds
// a set of defaults.
func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) {
@@ -100,41 +105,50 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid
v.SetConfigFile(configFilenames[0])
v.AddConfigPath(d.Path)
+ var configFileErr error
+
err := v.ReadInConfig()
if err != nil {
if _, ok := err.(viper.ConfigParseError); ok {
return nil, configFiles, 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)
+ configFileErr = ErrNoConfigFile
}
- if cf := v.ConfigFileUsed(); cf != "" {
- configFiles = append(configFiles, cf)
- }
+ if configFileErr == nil {
- for _, configFile := range configFilenames[1:] {
- var r io.Reader
- var err error
- if r, err = fs.Open(configFile); err != nil {
- return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err)
+ if cf := v.ConfigFileUsed(); cf != "" {
+ configFiles = append(configFiles, cf)
}
- if err = v.MergeConfig(r); err != nil {
- return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err)
+
+ for _, configFile := range configFilenames[1:] {
+ var r io.Reader
+ var err error
+ if r, err = fs.Open(configFile); err != nil {
+ return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err)
+ }
+ if err = v.MergeConfig(r); err != nil {
+ return nil, configFiles, fmt.Errorf("Unable to parse/merge Config file (%s).\n (%s)\n", configFile, err)
+ }
+ configFiles = append(configFiles, configFile)
}
- configFiles = append(configFiles, configFile)
+
}
if err := loadDefaultSettingsFor(v); err != nil {
return v, configFiles, err
}
- themeConfigFile, err := loadThemeConfig(d, v)
- if err != nil {
- return v, configFiles, err
- }
+ if configFileErr == nil {
- if themeConfigFile != "" {
- configFiles = append(configFiles, themeConfigFile)
+ themeConfigFiles, err := loadThemeConfig(d, v)
+ if err != nil {
+ return v, configFiles, err
+ }
+
+ if len(themeConfigFiles) > 0 {
+ configFiles = append(configFiles, themeConfigFiles...)
+ }
}
// We create languages based on the settings, so we need to make sure that
@@ -149,11 +163,11 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid
return v, configFiles, err
}
- return v, configFiles, nil
+ return v, configFiles, configFileErr
}
-func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error {
+func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
defaultLang := cfg.GetString("defaultContentLanguage")
@@ -182,14 +196,14 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
}
var (
- langs helpers.Languages
- err error
+ languages2 langs.Languages
+ err error
)
if len(languages) == 0 {
- langs = append(langs, helpers.NewDefaultLanguage(cfg))
+ languages2 = append(languages2, langs.NewDefaultLanguage(cfg))
} else {
- langs, err = toSortedLanguages(cfg, languages)
+ languages2, err = toSortedLanguages(cfg, languages)
if err != nil {
return fmt.Errorf("Failed to parse multilingual config: %s", err)
}
@@ -201,10 +215,10 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
// The validation below isn't complete, but should cover the most
// important cases.
var invalid bool
- if langs.IsMultihost() != oldLangs.IsMultihost() {
+ if languages2.IsMultihost() != oldLangs.IsMultihost() {
invalid = true
} else {
- if langs.IsMultihost() && len(langs) != len(oldLangs) {
+ if languages2.IsMultihost() && len(languages2) != len(oldLangs) {
invalid = true
}
}
@@ -213,10 +227,10 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
return errors.New("language change needing a server restart detected")
}
- if langs.IsMultihost() {
+ if languages2.IsMultihost() {
// We need to transfer any server baseURL to the new language
for i, ol := range oldLangs {
- nl := langs[i]
+ nl := languages2[i]
nl.Set("baseURL", ol.GetString("baseURL"))
}
}
@@ -225,7 +239,7 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
// The defaultContentLanguage is something the user has to decide, but it needs
// to match a language in the language definition list.
langExists := false
- for _, lang := range langs {
+ for _, lang := range languages2 {
if lang.Lang == defaultLang {
langExists = true
break
@@ -236,10 +250,10 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
return fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang)
}
- cfg.Set("languagesSorted", langs)
- cfg.Set("multilingual", len(langs) > 1)
+ cfg.Set("languagesSorted", languages2)
+ cfg.Set("multilingual", len(languages2) > 1)
- multihost := langs.IsMultihost()
+ multihost := languages2.IsMultihost()
if multihost {
cfg.Set("defaultContentLanguageInSubdir", true)
@@ -250,7 +264,7 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
// The baseURL may be provided at the language level. If that is true,
// then every language must have a baseURL. In this case we always render
// to a language sub folder, which is then stripped from all the Permalink URLs etc.
- for _, l := range langs {
+ for _, l := range languages2 {
burl := l.GetLocal("baseURL")
if burl == nil {
return errors.New("baseURL must be set on all or none of the languages")
@@ -262,49 +276,32 @@ func loadLanguageSettings(cfg config.Provider, oldLangs helpers.Languages) error
return nil
}
-func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error) {
+func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error) {
+ themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir"))
+ themes := config.GetStringSlicePreserveString(v1, "theme")
- theme := v1.GetString("theme")
- if theme == "" {
- return "", nil
+ // CollectThemes(fs afero.Fs, themesDir string, themes []strin
+ themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes)
+ if err != nil {
+ return nil, err
}
-
- 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
+ v1.Set("allThemes", themeConfigs)
+
+ var configFilenames []string
+ for _, tc := range themeConfigs {
+ if tc.ConfigFilename != "" {
+ configFilenames = append(configFilenames, tc.ConfigFilename)
+ if err := applyThemeConfig(v1, tc); err != nil {
+ return nil, err
+ }
}
}
- if !exists {
- // No theme config set.
- return "", nil
- }
+ return configFilenames, nil
- v2 := viper.New()
- v2.SetFs(d.Fs)
- v2.AutomaticEnv()
- v2.SetEnvPrefix("hugo")
- v2.SetConfigFile(configPath)
+}
- err = v2.ReadInConfig()
- if err != nil {
- return "", err
- }
+func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
const (
paramsKey = "params"
@@ -312,11 +309,13 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error)
menuKey = "menu"
)
+ v2 := theme.Cfg
+
for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
mergeStringMapKeepLeft("", key, v1, v2)
}
- themeLower := strings.ToLower(theme)
+ themeLower := strings.ToLower(theme.Name)
themeParamsNamespace := paramsKey + "." + themeLower
// Set namespaced params
@@ -371,11 +370,11 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) (string, error)
}
}
- return v2.ConfigFileUsed(), nil
+ return nil
}
-func mergeStringMapKeepLeft(rootKey, key string, v1, v2 *viper.Viper) {
+func mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) {
if !v2.IsSet(key) {
return
}
diff --git a/hugolib/datafiles_test.go b/hugolib/datafiles_test.go
index cd1ad8411..8b2dc8c0f 100644
--- a/hugolib/datafiles_test.go
+++ b/hugolib/datafiles_test.go
@@ -19,6 +19,8 @@ import (
"strings"
"testing"
+ "github.com/gohugoio/hugo/common/loggers"
+
"github.com/gohugoio/hugo/deps"
"fmt"
@@ -322,7 +324,7 @@ func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKey
}
var (
- logger = newErrorLogger()
+ logger = loggers.NewErrorLogger()
depsCfg = deps.DepsCfg{Fs: fs, Cfg: cfg, Logger: logger}
)
diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go
new file mode 100644
index 000000000..deecd69a5
--- /dev/null
+++ b/hugolib/filesystems/basefs.go
@@ -0,0 +1,644 @@
+// Copyright 2018 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 filesystems provides the fine grained file systems used by Hugo. These
+// are typically virtual filesystems that are composites of project and theme content.
+package filesystems
+
+import (
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "fmt"
+
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/hugolib/paths"
+ "github.com/gohugoio/hugo/langs"
+ "github.com/spf13/afero"
+)
+
+// When we create a virtual filesystem with data and i18n bundles for the project and the themes,
+// this is the name of the project's virtual root. It got it's funky name to make sure
+// (or very unlikely) that it collides with a theme name.
+const projectVirtualFolder = "__h__project"
+
+var filePathSeparator = string(filepath.Separator)
+
+// BaseFs contains the core base filesystems used by Hugo. The name "base" is used
+// to underline that even if they can be composites, they all have a base path set to a specific
+// resource folder, e.g "/my-project/content". So, no absolute filenames needed.
+type BaseFs struct {
+ // TODO(bep) make this go away
+ AbsContentDirs []types.KeyValueStr
+
+ // The filesystem used to capture content. This can be a composite and
+ // language aware file system.
+ ContentFs afero.Fs
+
+ // SourceFilesystems contains the different source file systems.
+ *SourceFilesystems
+
+ // The filesystem used to store resources (processed images etc.).
+ // This usually maps to /my-project/resources.
+ ResourcesFs afero.Fs
+
+ // The filesystem used to publish the rendered site.
+ // This usually maps to /my-project/public.
+ PublishFs afero.Fs
+
+ themeFs afero.Fs
+
+ // TODO(bep) improve the "theme interaction"
+ AbsThemeDirs []string
+}
+
+// RelContentDir tries to create a path relative to the content root from
+// the given filename. The return value is the path and language code.
+func (b *BaseFs) RelContentDir(filename string) (string, string) {
+ for _, dir := range b.AbsContentDirs {
+ if strings.HasPrefix(filename, dir.Value) {
+ rel := strings.TrimPrefix(filename, dir.Value)
+ return strings.TrimPrefix(rel, filePathSeparator), dir.Key
+ }
+ }
+ // Either not a content dir or already relative.
+ return filename, ""
+}
+
+// IsContent returns whether the given filename is in the content filesystem.
+func (b *BaseFs) IsContent(filename string) bool {
+ for _, dir := range b.AbsContentDirs {
+ if strings.HasPrefix(filename, dir.Value) {
+ return true
+ }
+ }
+ return false
+}
+
+// SourceFilesystems contains the different source file systems. These can be
+// composite file systems (theme and project etc.), and they have all root
+// set to the source type the provides: data, i18n, static, layouts.
+type SourceFilesystems struct {
+ Data *SourceFilesystem
+ I18n *SourceFilesystem
+ Layouts *SourceFilesystem
+ Archetypes *SourceFilesystem
+
+ // When in multihost we have one static filesystem per language. The sync
+ // static files is currently done outside of the Hugo build (where there is
+ // a concept of a site per language).
+ // When in non-multihost mode there will be one entry in this map with a blank key.
+ Static map[string]*SourceFilesystem
+}
+
+// A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
+// i18n, layouts, static) and additional metadata to be able to use that filesystem
+// in server mode.
+type SourceFilesystem struct {
+ Fs afero.Fs
+
+ Dirnames []string
+
+ // When syncing a source folder to the target (e.g. /public), this may
+ // be set to publish into a subfolder. This is used for static syncing
+ // in multihost mode.
+ PublishFolder string
+}
+
+// IsStatic returns true if the given filename is a member of one of the static
+// filesystems.
+func (s SourceFilesystems) IsStatic(filename string) bool {
+ for _, staticFs := range s.Static {
+ if staticFs.Contains(filename) {
+ return true
+ }
+ }
+ return false
+}
+
+// IsLayout returns true if the given filename is a member of the layouts filesystem.
+func (s SourceFilesystems) IsLayout(filename string) bool {
+ return s.Layouts.Contains(filename)
+}
+
+// IsData returns true if the given filename is a member of the data filesystem.
+func (s SourceFilesystems) IsData(filename string) bool {
+ return s.Data.Contains(filename)
+}
+
+// IsI18n returns true if the given filename is a member of the i18n filesystem.
+func (s SourceFilesystems) IsI18n(filename string) bool {
+ return s.I18n.Contains(filename)
+}
+
+// MakeStaticPathRelative makes an absolute static filename into a relative one.
+// It will return an empty string if the filename is not a member of a static filesystem.
+func (s SourceFilesystems) MakeStaticPathRelative(filename string) string {
+ for _, staticFs := range s.Static {
+ rel := staticFs.MakePathRelative(filename)
+ if rel != "" {
+ return rel
+ }
+ }
+ return ""
+}
+
+// MakePathRelative creates a relative path from the given filename.
+// It will return an empty string if the filename is not a member of this filesystem.
+func (d *SourceFilesystem) MakePathRelative(filename string) string {
+ for _, currentPath := range d.Dirnames {
+ if strings.HasPrefix(filename, currentPath) {
+ return strings.TrimPrefix(filename, currentPath)
+ }
+ }
+ return ""
+}
+
+// Contains returns whether the given filename is a member of the current filesystem.
+func (d *SourceFilesystem) Contains(filename string) bool {
+ for _, dir := range d.Dirnames {
+ if strings.HasPrefix(filename, dir) {
+ return true
+ }
+ }
+ return false
+}
+
+// WithBaseFs allows reuse of some potentially expensive to create parts that remain
+// the same across sites/languages.
+func WithBaseFs(b *BaseFs) func(*BaseFs) error {
+ return func(bb *BaseFs) error {
+ bb.themeFs = b.themeFs
+ bb.AbsThemeDirs = b.AbsThemeDirs
+ return nil
+ }
+}
+
+// NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase
+func NewBase(p *paths.Paths, options ...func(*BaseFs) error) (*BaseFs, error) {
+ fs := p.Fs
+
+ resourcesFs := afero.NewBasePathFs(fs.Source, p.AbsResourcesDir)
+ publishFs := afero.NewBasePathFs(fs.Destination, p.AbsPublishDir)
+
+ contentFs, absContentDirs, err := createContentFs(fs.Source, p.WorkingDir, p.DefaultContentLanguage, p.Languages)
+ if err != nil {
+ return nil, err
+ }
+
+ // Make sure we don't have any overlapping content dirs. That will never work.
+ for i, d1 := range absContentDirs {
+ for j, d2 := range absContentDirs {
+ if i == j {
+ continue
+ }
+ if strings.HasPrefix(d1.Value, d2.Value) || strings.HasPrefix(d2.Value, d1.Value) {
+ return nil, fmt.Errorf("found overlapping content dirs (%q and %q)", d1, d2)
+ }
+ }
+ }
+
+ b := &BaseFs{
+ AbsContentDirs: absContentDirs,
+ ContentFs: contentFs,
+ ResourcesFs: resourcesFs,
+ PublishFs: publishFs,
+ }
+
+ for _, opt := range options {
+ if err := opt(b); err != nil {
+ return nil, err
+ }
+ }
+
+ builder := newSourceFilesystemsBuilder(p, b)
+ sourceFilesystems, err := builder.Build()
+ if err != nil {
+ return nil, err
+ }
+
+ b.SourceFilesystems = sourceFilesystems
+ b.themeFs = builder.themeFs
+ b.AbsThemeDirs = builder.absThemeDirs
+
+ return b, nil
+}
+
+type sourceFilesystemsBuilder struct {
+ p *paths.Paths
+ result *SourceFilesystems
+ themeFs afero.Fs
+ hasTheme bool
+ absThemeDirs []string
+}
+
+func newSourceFilesystemsBuilder(p *paths.Paths, b *BaseFs) *sourceFilesystemsBuilder {
+ return &sourceFilesystemsBuilder{p: p, themeFs: b.themeFs, absThemeDirs: b.AbsThemeDirs, result: &SourceFilesystems{}}
+}
+
+func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
+ if b.themeFs == nil && b.p.ThemeSet() {
+ themeFs, absThemeDirs, err := createThemesOverlayFs(b.p)
+ if err != nil {
+ return nil, err
+ }
+ if themeFs == nil {
+ panic("createThemesFs returned nil")
+ }
+ b.themeFs = themeFs
+ b.absThemeDirs = absThemeDirs
+
+ }
+
+ b.hasTheme = len(b.absThemeDirs) > 0
+
+ sfs, err := b.createRootMappingFs("dataDir", "data")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Data = sfs
+
+ sfs, err = b.createRootMappingFs("i18nDir", "i18n")
+ if err != nil {
+ return nil, err
+ }
+ b.result.I18n = sfs
+
+ sfs, err = b.createFs("layoutDir", "layouts")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Layouts = sfs
+
+ sfs, err = b.createFs("archetypeDir", "archetypes")
+ if err != nil {
+ return nil, err
+ }
+ b.result.Archetypes = sfs
+
+ err = b.createStaticFs()
+ if err != nil {
+ return nil, err
+ }
+
+ return b.result, nil
+}
+
+func (b *sourceFilesystemsBuilder) createFs(dirKey, themeFolder string) (*SourceFilesystem, error) {
+ s := &SourceFilesystem{}
+ dir := b.p.Cfg.GetString(dirKey)
+ if dir == "" {
+ return s, fmt.Errorf("config %q not set", dirKey)
+ }
+
+ var fs afero.Fs
+
+ absDir := b.p.AbsPathify(dir)
+ if b.existsInSource(absDir) {
+ fs = afero.NewBasePathFs(b.p.Fs.Source, absDir)
+ s.Dirnames = []string{absDir}
+ }
+
+ if b.hasTheme {
+ themeFolderFs := afero.NewBasePathFs(b.themeFs, themeFolder)
+ if fs == nil {
+ fs = themeFolderFs
+ } else {
+ fs = afero.NewCopyOnWriteFs(themeFolderFs, fs)
+ }
+
+ for _, absThemeDir := range b.absThemeDirs {
+ absThemeFolderDir := filepath.Join(absThemeDir, themeFolder)
+ if b.existsInSource(absThemeFolderDir) {
+ s.Dirnames = append(s.Dirnames, absThemeFolderDir)
+ }
+ }
+ }
+
+ if fs == nil {
+ s.Fs = hugofs.NoOpFs
+ } else {
+ s.Fs = afero.NewReadOnlyFs(fs)
+ }
+
+ return s, nil
+}
+
+// Used for data, i18n -- we cannot use overlay filsesystems for those, but we need
+// to keep a strict order.
+func (b *sourceFilesystemsBuilder) createRootMappingFs(dirKey, themeFolder string) (*SourceFilesystem, error) {
+ s := &SourceFilesystem{}
+
+ projectDir := b.p.Cfg.GetString(dirKey)
+ if projectDir == "" {
+ return nil, fmt.Errorf("config %q not set", dirKey)
+ }
+
+ var fromTo []string
+ to := b.p.AbsPathify(projectDir)
+
+ if b.existsInSource(to) {
+ s.Dirnames = []string{to}
+ fromTo = []string{projectVirtualFolder, to}
+ }
+
+ for _, theme := range b.p.AllThemes {
+ to := b.p.AbsPathify(filepath.Join(b.p.ThemesDir, theme.Name, themeFolder))
+ if b.existsInSource(to) {
+ s.Dirnames = append(s.Dirnames, to)
+ from := theme
+ fromTo = append(fromTo, from.Name, to)
+ }
+ }
+
+ if len(fromTo) == 0 {
+ s.Fs = hugofs.NoOpFs
+ return s, nil
+ }
+
+ fs, err := hugofs.NewRootMappingFs(b.p.Fs.Source, fromTo...)
+ if err != nil {
+ return nil, err
+ }
+
+ s.Fs = afero.NewReadOnlyFs(fs)
+
+ return s, nil
+
+}
+
+func (b *sourceFilesystemsBuilder) existsInSource(abspath string) bool {
+ exists, _ := afero.Exists(b.p.Fs.Source, abspath)
+ return exists
+}
+
+func (b *sourceFilesystemsBuilder) createStaticFs() error {
+ isMultihost := b.p.Cfg.GetBool("multihost")
+ ms := make(map[string]*SourceFilesystem)
+ b.result.Static = ms
+
+ if isMultihost {
+ for _, l := range b.p.Languages {
+ s := &SourceFilesystem{PublishFolder: l.Lang}
+ staticDirs := removeDuplicatesKeepRight(getStaticDirs(l))
+ if len(staticDirs) == 0 {
+ continue
+ }
+
+ for _, dir := range staticDirs {
+ absDir := b.p.AbsPathify(dir)
+ if !b.existsInSource(absDir) {
+ continue
+ }
+
+ s.Dirnames = append(s.Dirnames, absDir)
+ }
+
+ fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames)
+ if err != nil {
+ return err
+ }
+
+ s.Fs = fs
+ ms[l.Lang] = s
+