diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-03-01 15:01:25 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-06-10 23:55:20 +0200 |
commit | 80230f26a3020ff33bac2bef01b2c0e314b89f86 (patch) | |
tree | 6da3114350477866065d265d6e1db9cb55639dc1 /helpers | |
parent | 6464981adb4d7d0f41e8e2c987342082982210a1 (diff) |
Add support for theme composition and inheritance
This commit adds support for theme composition and inheritance in Hugo.
With this, it helps thinking about a theme as a set of ordered components:
```toml
theme = ["my-shortcodes", "base-theme", "hyde"]
```
The theme definition example above in `config.toml` creates a theme with the 3 components with presedence from left to right.
So, Hugo will, for any given file, data entry etc., look first in the project, and then in `my-shortcode`, `base-theme` and lastly `hyde`.
Hugo uses two different algorithms to merge the filesystems, depending on the file type:
* For `i18n` and `data` files, Hugo merges deeply using the translation id and data key inside the files.
* For `static`, `layouts` (templates) and `archetypes` files, these are merged on file level. So the left-most file will be chosen.
The name used in the `theme` definition above must match a folder in `/your-site/themes`, e.g. `/your-site/themes/my-shortcodes`. There are plans to improve on this and get a URL scheme so this can be resolved automatically.
Also note that a component that is part of a theme can have its own configuration file, e.g. `config.toml`. There are currently some restrictions to what a theme component can configure:
* `params` (global and per language)
* `menu` (global and per language)
* `outputformats` and `mediatypes`
The same rules apply here: The left-most param/menu etc. with the same ID will win. There are some hidden and experimental namespace support in the above, which we will work to improve in the future, but theme authors are encouraged to create their own namespaces to avoid naming conflicts.
A final note: Themes/components can also have a `theme` definition in their `config.toml` and similar, which is the "inheritance" part of this commit's title. This is currently not supported by the Hugo theme site. We will have to wait for some "auto dependency" feature to be implemented for that to happen, but this can be a powerful feature if you want to create your own theme-variant based on others.
Fixes #4460
Fixes #4450
Diffstat (limited to 'helpers')
-rw-r--r-- | helpers/baseURL.go | 75 | ||||
-rw-r--r-- | helpers/baseURL_test.go | 61 | ||||
-rw-r--r-- | helpers/content.go | 4 | ||||
-rw-r--r-- | helpers/general.go | 54 | ||||
-rw-r--r-- | helpers/general_test.go | 55 | ||||
-rw-r--r-- | helpers/language.go | 216 | ||||
-rw-r--r-- | helpers/language_test.go | 48 | ||||
-rw-r--r-- | helpers/path.go | 105 | ||||
-rw-r--r-- | helpers/path_test.go | 18 | ||||
-rw-r--r-- | helpers/pathspec.go | 333 | ||||
-rw-r--r-- | helpers/pathspec_test.go | 32 | ||||
-rw-r--r-- | helpers/testhelpers_test.go | 21 | ||||
-rw-r--r-- | helpers/url.go | 40 | ||||
-rw-r--r-- | helpers/url_test.go | 21 |
14 files changed, 139 insertions, 944 deletions
diff --git a/helpers/baseURL.go b/helpers/baseURL.go deleted file mode 100644 index eb39ced5b..000000000 --- a/helpers/baseURL.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2017-present 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 helpers - -import ( - "fmt" - "net/url" - "strings" -) - -// A BaseURL in Hugo is normally on the form scheme://path, but the -// form scheme: is also valid (mailto:hugo@rules.com). -type BaseURL struct { - url *url.URL - urlStr string -} - -func (b BaseURL) String() string { - return b.urlStr -} - -// WithProtocol returns the BaseURL prefixed with the given protocol. -// The Protocol is normally of the form "scheme://", i.e. "webcal://". -func (b BaseURL) WithProtocol(protocol string) (string, error) { - u := b.URL() - - scheme := protocol - isFullProtocol := strings.HasSuffix(scheme, "://") - isOpaqueProtocol := strings.HasSuffix(scheme, ":") - - if isFullProtocol { - scheme = strings.TrimSuffix(scheme, "://") - } else if isOpaqueProtocol { - scheme = strings.TrimSuffix(scheme, ":") - } - - u.Scheme = scheme - - if isFullProtocol && u.Opaque != "" { - u.Opaque = "//" + u.Opaque - } else if isOpaqueProtocol && u.Opaque == "" { - return "", fmt.Errorf("Cannot determine BaseURL for protocol %q", protocol) - } - - return u.String(), nil -} - -// URL returns a copy of the internal URL. -// The copy can be safely used and modified. -func (b BaseURL) URL() *url.URL { - c := *b.url - return &c -} - -func newBaseURLFromString(b string) (BaseURL, error) { - var result BaseURL - - base, err := url.Parse(b) - if err != nil { - return result, err - } - - return BaseURL{url: base, urlStr: base.String()}, nil -} diff --git a/helpers/baseURL_test.go b/helpers/baseURL_test.go deleted file mode 100644 index 437152f34..000000000 --- a/helpers/baseURL_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2017-present 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 helpers - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestBaseURL(t *testing.T) { - b, err := newBaseURLFromString("http://example.com") - require.NoError(t, err) - require.Equal(t, "http://example.com", b.String()) - - p, err := b.WithProtocol("webcal://") - require.NoError(t, err) - require.Equal(t, "webcal://example.com", p) - - p, err = b.WithProtocol("webcal") - require.NoError(t, err) - require.Equal(t, "webcal://example.com", p) - - _, err = b.WithProtocol("mailto:") - require.Error(t, err) - - b, err = newBaseURLFromString("mailto:hugo@rules.com") - require.NoError(t, err) - require.Equal(t, "mailto:hugo@rules.com", b.String()) - - // These are pretty constructed - p, err = b.WithProtocol("webcal") - require.NoError(t, err) - require.Equal(t, "webcal:hugo@rules.com", p) - - p, err = b.WithProtocol("webcal://") - require.NoError(t, err) - require.Equal(t, "webcal://hugo@rules.com", p) - - // Test with "non-URLs". Some people will try to use these as a way to get - // relative URLs working etc. - b, err = newBaseURLFromString("/") - require.NoError(t, err) - require.Equal(t, "/", b.String()) - - b, err = newBaseURLFromString("") - require.NoError(t, err) - require.Equal(t, "", b.String()) - -} diff --git a/helpers/content.go b/helpers/content.go index 1c0a7b7e9..55d8ce202 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -25,6 +25,8 @@ import ( "unicode" "unicode/utf8" + "github.com/gohugoio/hugo/common/maps" + "github.com/chaseadamsio/goorgeous" bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/config" @@ -134,7 +136,7 @@ func newBlackfriday(config map[string]interface{}) *BlackFriday { "taskLists": true, } - ToLowerMap(defaultParam) + maps.ToLower(defaultParam) siteConfig := make(map[string]interface{}) diff --git a/helpers/general.go b/helpers/general.go index 5b46520e5..b442b1eb4 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -20,18 +20,20 @@ import ( "fmt" "io" "net" + "os" "path/filepath" "strings" "sync" "unicode" "unicode/utf8" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" "github.com/jdkato/prose/transform" bp "github.com/gohugoio/hugo/bufferpool" - "github.com/spf13/cast" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/pflag" ) @@ -129,30 +131,6 @@ func ReaderToBytes(lines io.Reader) []byte { return bc } -// ToLowerMap makes all the keys in the given map lower cased and will do so -// recursively. -// Notes: -// * This will modify the map given. -// * Any nested map[interface{}]interface{} will be converted to map[string]interface{}. -func ToLowerMap(m map[string]interface{}) { - for k, v := range m { - switch v.(type) { - case map[interface{}]interface{}: - v = cast.ToStringMap(v) - ToLowerMap(v.(map[string]interface{})) - case map[string]interface{}: - ToLowerMap(v.(map[string]interface{})) - } - - lKey := strings.ToLower(k) - if k != lKey { - delete(m, k) - m[lKey] = v - } - - } -} - // ReaderToString is the same as ReaderToBytes, but returns a string. func ReaderToString(lines io.Reader) string { if lines == nil { @@ -255,11 +233,6 @@ func compareStringSlices(a, b []string) bool { return true } -// ThemeSet checks whether a theme is in use or not. -func (p *PathSpec) ThemeSet() bool { - return p.theme != "" -} - // LogPrinter is the common interface of the JWWs loggers. type LogPrinter interface { // Println is the only common method that works in all of JWWs loggers. @@ -477,3 +450,24 @@ func DiffStringSlices(slice1 []string, slice2 []string) []string { func DiffStrings(s1, s2 string) []string { return DiffStringSlices(strings.Fields(s1), strings.Fields(s2)) } + +// PrintFs prints the given filesystem to the given writer starting from the given path. +// This is useful for debugging. +func PrintFs(fs afero.Fs, path string, w io.Writer) { + if fs == nil { + return + } + afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() { + s := path + if lang, ok := info.(hugofs.LanguageAnnouncer); ok { + s = s + "\tLANG: " + lang.Lang() + } + if fp, ok := info.(hugofs.FilePather); ok { + s = s + "\tRF: " + fp.Filename() + "\tBP: " + fp.BaseDir() + } + fmt.Fprintln(w, " ", s) + } + return nil + }) +} diff --git a/helpers/general_test.go b/helpers/general_test.go index 16df69d24..08fe4890e 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 The Hugo Authors. All rights reserved. +// 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. @@ -220,59 +220,6 @@ func TestFindAvailablePort(t *testing.T) { assert.True(t, addr.Port > 0) } -func TestToLowerMap(t *testing.T) { - - tests := []struct { - input map[string]interface{} - expected map[string]interface{} - }{ - { - map[string]interface{}{ - "abC": 32, - }, - map[string]interface{}{ - "abc": 32, - }, - }, - { - map[string]interface{}{ - "abC": 32, - "deF": map[interface{}]interface{}{ - 23: "A value", - 24: map[string]interface{}{ - "AbCDe": "A value", - "eFgHi": "Another value", - }, - }, - "gHi": map[string]interface{}{ - "J": 25, - }, - }, - map[string]interface{}{ - "abc": 32, - "def": map[string]interface{}{ - "23": "A value", - "24": map[string]interface{}{ - "abcde": "A value", - "efghi": "Another value", - }, - }, - "ghi": map[string]interface{}{ - "j": 25, - }, - }, - }, - } - - for i, test := range tests { - // ToLowerMap modifies input. - ToLowerMap(test.input) - if !reflect.DeepEqual(test.expected, test.input) { - t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) - } - } -} - func TestFastMD5FromFile(t *testing.T) { fs := afero.NewMemMapFs() diff --git a/helpers/language.go b/helpers/language.go deleted file mode 100644 index 731e9b088..000000000 --- a/helpers/language.go +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2016-present 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 helpers - -import ( - "sort" - "strings" - - "github.com/gohugoio/hugo/config" - "github.com/spf13/cast" -) - -// These are the settings that should only be looked up in the global Viper -// config and not per language. -// This list may not be complete, but contains only settings that we know -// will be looked up in both. -// This isn't perfect, but it is ultimately the user who shoots him/herself in -// the foot. -// See the pathSpec. -var globalOnlySettings = map[string]bool{ - strings.ToLower("defaultContentLanguageInSubdir"): true, - strings.ToLower("defaultContentLanguage"): true, - strings.ToLower("multilingual"): true, -} - -// Language manages specific-language configuration. -type Language struct { - Lang string - LanguageName string - Title string - Weight int - - Disabled bool - - // If set per language, this tells Hugo that all content files without any - // language indicator (e.g. my-page.en.md) is in this language. - // This is usually a path relative to the working dir, but it can be an - // absolute directory referenece. It is what we get. - ContentDir string - - Cfg config.Provider - - // These are params declared in the [params] section of the language merged with the - // site's params, the most specific (language) wins on duplicate keys. - params map[string]interface{} - - // These are config values, i.e. the settings declared outside of the [params] section of the language. - // This is the map Hugo looks in when looking for configuration values (baseURL etc.). - // Values in this map can also be fetched from the params map above. - settings map[string]interface{} -} - -func (l *Language) String() string { - return l.Lang -} - -// NewLanguage creates a new language. -func NewLanguage(lang string, cfg config.Provider) *Language { - // Note that language specific params will be overridden later. - // We should improve that, but we need to make a copy: - params := make(map[string]interface{}) - for k, v := range cfg.GetStringMap("params") { - params[k] = v - } - ToLowerMap(params) - - defaultContentDir := cfg.GetString("contentDir") - if defaultContentDir == "" { - panic("contentDir not set") - } - - l := &Language{Lang: lang, ContentDir: defaultContentDir, Cfg: cfg, params: params, settings: make(map[string]interface{})} - return l -} - -// NewDefaultLanguage creates the default language for a config.Provider. -// If not otherwise specified the default is "en". -func NewDefaultLanguage(cfg config.Provider) *Language { - defaultLang := cfg.GetString("defaultContentLanguage") - - if defaultLang == "" { - defaultLang = "en" - } - - return NewLanguage(defaultLang, cfg) -} - -// Languages is a sortable list of languages. -type Languages []*Language - -// NewLanguages creates a sorted list of languages. -// NOTE: function is currently unused. -func NewLanguages(l ...*Language) Languages { - languages := make(Languages, len(l)) - for i := 0; i < len(l); i++ { - languages[i] = l[i] - } - sort.Sort(languages) - return languages -} - -func (l Languages) Len() int { return len(l) } -func (l Languages) Less(i, j int) bool { return l[i].Weight < l[j].Weight } -func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } - -// Params retunrs language-specific params merged with the global params. -func (l *Language) Params() map[string]interface{} { - return l.params -} - -// IsMultihost returns whether there are more than one language and at least one of -// the languages has baseURL specificed on the language level. -func (l Languages) IsMultihost() bool { - if len(l) <= 1 { - return false - } - - for _, lang := range l { - if lang.GetLocal("baseURL") != nil { - return true - } - } - return false -} - -// SetParam sets a param with the given key and value. -// SetParam is case-insensitive. -func (l *Language) SetParam(k string, v interface{}) { - l.params[strings.ToLower(k)] = v -} - -// GetBool returns the value associated with the key as a boolean. -func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) } - -// GetString returns the value associated with the key as a string. -func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) } - -// GetInt returns the value associated with the key as an int. -func (l *Language) GetInt(key string) int { return cast.ToInt(l.Get(key)) } - -// GetStringMap returns the value associated with the key as a map of interfaces. -func (l *Language) GetStringMap(key string) map[string]interface{} { - return cast.ToStringMap(l.Get(key)) -} - -// GetStringMapString returns the value associated with the key as a map of strings. -func (l *Language) GetStringMapString(key string) map[string]string { - return cast.ToStringMapString(l.Get(key)) -} - -// returns the value associated with the key as a slice of strings. -func (l *Language) GetStringSlice(key string) []string { - return cast.ToStringSlice(l.Get(key)) -} - -// Get returns a value associated with the key relying on specified language. -// Get is case-insensitive for a key. -// -// Get returns an interface. For a specific value use one of the Get____ methods. -func (l *Language) Get(key string) interface{} { - local := l.GetLocal(key) - if local != nil { - return local - } - return l.Cfg.Get(key) -} - -// GetLocal gets a configuration value set on language level. It will -// not fall back to any global value. -// It will return nil if a value with the given key cannot be found. -func (l *Language) GetLocal(key string) interface{} { - if l == nil { - panic("language not set") - } - key = strings.ToLower(key) - if !globalOnlySettings[key] { - if v, ok := l.settings[key]; ok { - return v - } - } - return nil -} - -// Set sets the value for the key in the language's params. -func (l *Language) Set(key string, value interface{}) { - if l == nil { - panic("language not set") - } - key = strings.ToLower(key) - l.settings[key] = value -} - -// IsSet checks whether the key is set in the language or the related config store. -func (l *Language) IsSet(key string) bool { - key = strings.ToLower(key) - - key = strings.ToLower(key) - if !globalOnlySettings[key] { - if _, ok := l.settings[key]; ok { - return true - } - } - return l.Cfg.IsSet(key) - -} diff --git a/helpers/language_test.go b/helpers/language_test.go deleted file mode 100644 index 4c4670321..000000000 --- a/helpers/language_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2016-present 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 helpers - -import ( - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/require" -) - -func TestGetGlobalOnlySetting(t *testing.T) { - v := viper.New() - v.Set("defaultContentLanguageInSubdir", true) - v.Set("contentDir", "content") - v.Set("paginatePath", "page") - lang := NewDefaultLanguage(v) - lang.Set("defaultContentLanguageInSubdir", false) - lang.Set("paginatePath", "side") - - require.True(t, lang.GetBool("defaultContentLanguageInSubdir")) - require.Equal(t, "side", lang.GetString("paginatePath")) -} - -func TestLanguageParams(t *testing.T) { - assert := require.New(t) - - v := viper.New() - v.Set("p1", "p1cfg") - v.Set("contentDir", "content") - - lang := NewDefaultLanguage(v) - lang.SetParam("p1", "p1p") - - assert.Equal("p1p", lang.Params()["p1"]) - assert.Equal("p1cfg", lang.Get("p1")) -} diff --git a/helpers/path.go b/helpers/path.go index 7ac9208bf..76f13d653 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strings" "unicode" @@ -31,9 +32,6 @@ import ( var ( // ErrThemeUndefined is returned when a theme has not be defined by the user. ErrThemeUndefined = errors.New("no theme set") - - // ErrWalkRootTooShort is returned when the root specified for a file walk is shorter than 4 characters. - ErrPathTooShort = errors.New("file path is too short") ) // filepathPathBridge is a bridge for common functionality in filepath vs path @@ -86,7 +84,7 @@ func (p *PathSpec) MakePath(s string) string { // MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced func (p *PathSpec) MakePathSanitized(s string) string { - if p.disablePathToLower { + if p.DisablePathToLower { return p.MakePath(s) } return strings.ToLower(p.MakePath(s)) @@ -129,7 +127,7 @@ func (p *PathSpec) UnicodeSanitize(s string) string { var result string - if p.removePathAccents { + if p.RemovePathAccents { // remove accents - see https://blog.golang.org/normalization t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) result, _, _ = transform.String(t, string(target)) @@ -151,32 +149,19 @@ func ReplaceExtension(path string, newExt string) string { return f + "." + newExt } -// AbsPathify creates an absolute path if given a relative path. If already -// absolute, the path is just cleaned. -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) { - return filepath.Clean(inPath) +// GetFirstThemeDir gets the root directory of the first theme, if there is one. +// If there is no theme, returns the empty string. +func (p *PathSpec) GetFirstThemeDir() string { + if p.ThemeSet() { + return p.AbsPathify(filepath.Join(p.ThemesDir, p.Themes()[0])) } - return filepath.Join(workingDir, inPath) -} - -// GetLayoutDirPath returns the absolute path to the layout file dir -// for the current Hugo project. -func (p *PathSpec) GetLayoutDirPath() string { - return p.AbsPathify(p.layoutDir) + return "" } -// GetThemeDir gets the root directory of the current theme, if there is one. -// If there is no theme, returns the empty string. -func (p *PathSpec) GetThemeDir() string { +// GetThemesDir gets the absolute root theme dir path. +func (p *PathSpec) GetThemesDir() string { if p.ThemeSet() { - return p.AbsPathify(filepath.Join(p.themesDir, p.theme)) + return p.AbsPathify(p.ThemesDir) } return "" } @@ -185,50 +170,11 @@ func (p *PathSpec) GetThemeDir() string { // If there is no theme, returns the empty string. func (p *PathSpec) GetRelativeThemeDir() string { if p.ThemeSet() { - return strings.TrimPrefix(filepath.Join(p.themesDir, p.theme), FilePathSeparator) + return strings.TrimPrefix(filepath.Join(p.ThemesDir, p.Themes()[0]), FilePathSeparator) } return "" } -// GetThemeStaticDirPath returns the theme's static dir path if theme is set. -// If theme is set and the static dir doesn't exist, an error is returned. -func (p *PathSpec) GetThemeStaticDirPath() (string, error) { - return p.getThemeDirPath("static") -} - -// GetThemeDataDirPath returns the theme's data dir path if theme is set. -// If theme is set and the data dir doesn't exist, an error is returned. -func (p *PathSpec) GetThemeDataDirPath() (string, error) { - return p.getThemeDirPath("data") -} - -// GetThemeI18nDirPath returns the theme's i18n dir path if theme is set. -// If theme is set and the i18n dir doesn't exist, an error is returned. -func (p *PathSpec) GetThemeI18nDirPath() (string, error) { - return p.getThemeDirPath("i18n") -} - -func (p *PathSpec) getThemeDirPath(path string) (string, error) { - if !p.ThemeSet() { - return "", ErrThemeUndefined - } - - themeDir := filepath.Join(p.GetThemeDir(), path) - if _, err := p.Fs.Source.Stat(themeDir); os.IsNotExist(err) { - return "", fmt.Errorf("Unable to find %s directory for theme %s in %s", path, p.theme, themeDir) - } - - return themeDir, nil -} - -// GetThemesDirPath gets the static files directory of the current theme, if there is one. -// Ignores underlying errors. -// TODO(bep) Candidate for deprecation? -func (p *PathSpec) GetThemesDirPath() string { - dir, _ := p.getThemeDirPath("static") - return dir -} - func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { for _, currentPath := range possibleDirectories { @@ -445,8 +391,8 @@ func FindCWD() (string, error) { func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error { // Sanity check - if len(root) < 4 { - return ErrPathTooShort + if root != "" && len(root) < 4 { + return errors.New("Path is too short") } // Handle the root first @@ -464,7 +410,10 @@ func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error { return err } - rootContent, err := afero.ReadDir(fs, root) + // Some of Hugo's filesystems represents an ordered root folder, i.e. project first, then theme folders. + // Make sure that order is preserved. afero.Walk will sort the directories down in the file tree, + // but we don't care about that. + rootContent, err := readDir(fs, root, false) if err != nil { return walker(root, nil, err) @@ -480,6 +429,22 @@ func SymbolicWalk(fs afero.Fs, root string, |