diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-09-10 12:26:34 +0300 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2020-02-18 11:49:42 +0300 |
commit | eada236f87d9669885da1ff647672bb3dc6b4954 (patch) | |
tree | a0303f067b2cbe06c55637013dbd7702a551c64f | |
parent | e5329f13c02b87f0c30f8837759c810cd90ff8da (diff) |
Introduce a tree map for all content
This commit introduces a new data structure to store pages and their resources.
This data structure is backed by radix trees.
This simplies tree operations, makes all pages a bundle, and paves the way for #6310.
It also solves a set of annoying issues (see list below).
Not a motivation behind this, but this commit also makes Hugo in general a little bit faster and more memory effective (see benchmarks). Especially for partial rebuilds on content edits, but also when taxonomies is in use.
```
name old time/op new time/op delta
SiteNew/Bundle_with_image/Edit-16 1.32ms ± 8% 1.00ms ± 9% -24.42% (p=0.029 n=4+4)
SiteNew/Bundle_with_JSON_file/Edit-16 1.28ms ± 0% 0.94ms ± 0% -26.26% (p=0.029 n=4+4)
SiteNew/Tags_and_categories/Edit-16 33.9ms ± 2% 21.8ms ± 1% -35.67% (p=0.029 n=4+4)
SiteNew/Canonify_URLs/Edit-16 40.6ms ± 1% 37.7ms ± 3% -7.20% (p=0.029 n=4+4)
SiteNew/Deep_content_tree/Edit-16 56.7ms ± 0% 51.7ms ± 1% -8.82% (p=0.029 n=4+4)
SiteNew/Many_HTML_templates/Edit-16 19.9ms ± 2% 18.3ms ± 3% -7.64% (p=0.029 n=4+4)
SiteNew/Page_collections/Edit-16 37.9ms ± 4% 34.0ms ± 2% -10.28% (p=0.029 n=4+4)
SiteNew/Bundle_with_image-16 10.7ms ± 0% 10.6ms ± 0% -1.15% (p=0.029 n=4+4)
SiteNew/Bundle_with_JSON_file-16 10.8ms ± 0% 10.7ms ± 0% -1.05% (p=0.029 n=4+4)
SiteNew/Tags_and_categories-16 43.2ms ± 1% 39.6ms ± 1% -8.35% (p=0.029 n=4+4)
SiteNew/Canonify_URLs-16 47.6ms ± 1% 47.3ms ± 0% ~ (p=0.057 n=4+4)
SiteNew/Deep_content_tree-16 73.0ms ± 1% 74.2ms ± 1% ~ (p=0.114 n=4+4)
SiteNew/Many_HTML_templates-16 37.9ms ± 0% 38.1ms ± 1% ~ (p=0.114 n=4+4)
SiteNew/Page_collections-16 53.6ms ± 1% 54.7ms ± 1% +2.09% (p=0.029 n=4+4)
name old alloc/op new alloc/op delta
SiteNew/Bundle_with_image/Edit-16 486kB ± 0% 430kB ± 0% -11.47% (p=0.029 n=4+4)
SiteNew/Bundle_with_JSON_file/Edit-16 265kB ± 0% 209kB ± 0% -21.06% (p=0.029 n=4+4)
SiteNew/Tags_and_categories/Edit-16 13.6MB ± 0% 8.8MB ± 0% -34.93% (p=0.029 n=4+4)
SiteNew/Canonify_URLs/Edit-16 66.5MB ± 0% 63.9MB ± 0% -3.95% (p=0.029 n=4+4)
SiteNew/Deep_content_tree/Edit-16 28.8MB ± 0% 25.8MB ± 0% -10.55% (p=0.029 n=4+4)
SiteNew/Many_HTML_templates/Edit-16 6.16MB ± 0% 5.56MB ± 0% -9.86% (p=0.029 n=4+4)
SiteNew/Page_collections/Edit-16 16.9MB ± 0% 16.0MB ± 0% -5.19% (p=0.029 n=4+4)
SiteNew/Bundle_with_image-16 2.28MB ± 0% 2.29MB ± 0% +0.35% (p=0.029 n=4+4)
SiteNew/Bundle_with_JSON_file-16 2.07MB ± 0% 2.07MB ± 0% ~ (p=0.114 n=4+4)
SiteNew/Tags_and_categories-16 14.3MB ± 0% 13.2MB ± 0% -7.30% (p=0.029 n=4+4)
SiteNew/Canonify_URLs-16 69.1MB ± 0% 69.0MB ± 0% ~ (p=0.343 n=4+4)
SiteNew/Deep_content_tree-16 31.3MB ± 0% 31.8MB ± 0% +1.49% (p=0.029 n=4+4)
SiteNew/Many_HTML_templates-16 10.8MB ± 0% 10.9MB ± 0% +1.11% (p=0.029 n=4+4)
SiteNew/Page_collections-16 21.4MB ± 0% 21.6MB ± 0% +1.15% (p=0.029 n=4+4)
name old allocs/op new allocs/op delta
SiteNew/Bundle_with_image/Edit-16 4.74k ± 0% 3.86k ± 0% -18.57% (p=0.029 n=4+4)
SiteNew/Bundle_with_JSON_file/Edit-16 4.73k ± 0% 3.85k ± 0% -18.58% (p=0.029 n=4+4)
SiteNew/Tags_and_categories/Edit-16 301k ± 0% 198k ± 0% -34.14% (p=0.029 n=4+4)
SiteNew/Canonify_URLs/Edit-16 389k ± 0% 373k ± 0% -4.07% (p=0.029 n=4+4)
SiteNew/Deep_content_tree/Edit-16 338k ± 0% 262k ± 0% -22.63% (p=0.029 n=4+4)
SiteNew/Many_HTML_templates/Edit-16 102k ± 0% 88k ± 0% -13.81% (p=0.029 n=4+4)
SiteNew/Page_collections/Edit-16 176k ± 0% 152k ± 0% -13.32% (p=0.029 n=4+4)
SiteNew/Bundle_with_image-16 26.8k ± 0% 26.8k ± 0% +0.05% (p=0.029 n=4+4)
SiteNew/Bundle_with_JSON_file-16 26.8k ± 0% 26.8k ± 0% +0.05% (p=0.029 n=4+4)
SiteNew/Tags_and_categories-16 273k ± 0% 245k ± 0% -10.36% (p=0.029 n=4+4)
SiteNew/Canonify_URLs-16 396k ± 0% 398k ± 0% +0.39% (p=0.029 n=4+4)
SiteNew/Deep_content_tree-16 317k ± 0% 325k ± 0% +2.53% (p=0.029 n=4+4)
SiteNew/Many_HTML_templates-16 146k ± 0% 147k ± 0% +0.98% (p=0.029 n=4+4)
SiteNew/Page_collections-16 210k ± 0% 215k ± 0% +2.44% (p=0.029 n=4+4)
```
Fixes #6312
Fixes #6087
Fixes #6738
Fixes #6412
Fixes #6743
Fixes #6875
Fixes #6034
Fixes #6902
Fixes #6173
Fixes #6590
71 files changed, 4758 insertions, 2430 deletions
diff --git a/commands/convert.go b/commands/convert.go index b9129e594..fe64405e9 100644 --- a/commands/convert.go +++ b/commands/convert.go @@ -16,10 +16,11 @@ package commands import ( "bytes" "fmt" - "io" "strings" "time" + "github.com/gohugoio/hugo/parser/pageparser" + "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/hugofs" @@ -28,7 +29,6 @@ import ( "github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/parser/pageparser" "github.com/pkg/errors" @@ -157,7 +157,7 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target return nil } - pf, err := parseContentFile(file) + pf, err := pageparser.ParseFrontMatterAndContent(file) if err != nil { site.Log.ERROR.Println(errMsg) file.Close() @@ -167,23 +167,23 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target file.Close() // better handling of dates in formats that don't have support for them - if pf.frontMatterFormat == metadecoders.JSON || pf.frontMatterFormat == metadecoders.YAML || pf.frontMatterFormat == metadecoders.TOML { - for k, v := range pf.frontMatter { + if pf.FrontMatterFormat == metadecoders.JSON || pf.FrontMatterFormat == metadecoders.YAML || pf.FrontMatterFormat == metadecoders.TOML { + for k, v := range pf.FrontMatter { switch vv := v.(type) { case time.Time: - pf.frontMatter[k] = vv.Format(time.RFC3339) + pf.FrontMatter[k] = vv.Format(time.RFC3339) } } } var newContent bytes.Buffer - err = parser.InterfaceToFrontMatter(pf.frontMatter, targetFormat, &newContent) + err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent) if err != nil { site.Log.ERROR.Println(errMsg) return err } - newContent.Write(pf.content) + newContent.Write(pf.Content) newFilename := p.File().Filename() @@ -210,39 +210,3 @@ type parsedFile struct { // Everything after Front Matter content []byte } - -func parseContentFile(r io.Reader) (parsedFile, error) { - var pf parsedFile - - psr, err := pageparser.Parse(r, pageparser.Config{}) - if err != nil { - return pf, err - } - - iter := psr.Iterator() - - walkFn := func(item pageparser.Item) bool { - if pf.frontMatterSource != nil { - // The rest is content. - pf.content = psr.Input()[item.Pos:] - // Done - return false - } else if item.IsFrontMatter() { - pf.frontMatterFormat = metadecoders.FormatFromFrontMatterType(item.Type) - pf.frontMatterSource = item.Val - } - return true - - } - - iter.PeekWalk(walkFn) - - metadata, err := metadecoders.Default.UnmarshalToMap(pf.frontMatterSource, pf.frontMatterFormat) - if err != nil { - return pf, err - } - pf.frontMatter = metadata - - return pf, nil - -} diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go index d2e7b27da..2dd0fc051 100644 --- a/commands/import_jekyll.go +++ b/commands/import_jekyll.go @@ -26,6 +26,8 @@ import ( "time" "unicode" + "github.com/gohugoio/hugo/parser/pageparser" + "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/parser/metadecoders" @@ -397,19 +399,19 @@ func convertJekyllPost(path, relPath, targetDir string, draft bool) error { return err } - pf, err := parseContentFile(bytes.NewReader(contentBytes)) + pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes)) if err != nil { jww.ERROR.Println("Parse file error:", path) return err } - newmetadata, err := convertJekyllMetaData(pf.frontMatter, postName, postDate, draft) + newmetadata, err := convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft) if err != nil { jww.ERROR.Println("Convert metadata error:", path) return err } - content, err := convertJekyllContent(newmetadata, string(pf.content)) + content, err := convertJekyllContent(newmetadata, string(pf.Content)) if err != nil { jww.ERROR.Println("Converting Jekyll error:", path) return err diff --git a/common/herrors/errors.go b/common/herrors/errors.go index 5fae6fcae..fded30b1a 100644 --- a/common/herrors/errors.go +++ b/common/herrors/errors.go @@ -57,6 +57,11 @@ func PrintStackTrace(w io.Writer) { fmt.Fprintf(w, "%s", buf) } +// ErrorSender is a, typically, non-blocking error handler. +type ErrorSender interface { + SendError(err error) +} + // Recover is a helper function that can be used to capture panics. // Put this at the top of a method/function that crashes in a template: // defer herrors.Recover() diff --git a/common/para/para_test.go b/common/para/para_test.go index bda7f5d27..9b268b0c0 100644 --- a/common/para/para_test.go +++ b/common/para/para_test.go @@ -16,6 +16,7 @@ package para import ( "context" "runtime" + "sort" "sync" "sync/atomic" diff --git a/common/types/convert.go b/common/types/convert.go new file mode 100644 index 000000000..b55330757 --- /dev/null +++ b/common/types/convert.go @@ -0,0 +1,28 @@ +// Copyright 2019 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 types + +import "github.com/spf13/cast" + +// ToStringSlicePreserveString converts v to a string slice. +// If v is a string, it will be wrapped in a string slice. +func ToStringSlicePreserveString(v interface{}) []string { + if v == nil { + return nil + } + if sds, ok := v.(string); ok { + return []string{sds} + } + return cast.ToStringSlice(v) +} diff --git a/common/types/convert_test.go b/common/types/convert_test.go new file mode 100644 index 000000000..7f86f4c8a --- /dev/null +++ b/common/types/convert_test.go @@ -0,0 +1,29 @@ +// Copyright 2019 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 types + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestToStringSlicePreserveString(t *testing.T) { + c := qt.New(t) + + c.Assert(ToStringSlicePreserveString("Hugo"), qt.DeepEquals, []string{"Hugo"}) + c.Assert(ToStringSlicePreserveString([]interface{}{"A", "B"}), qt.DeepEquals, []string{"A", "B"}) + c.Assert(ToStringSlicePreserveString(nil), qt.IsNil) + +} diff --git a/config/configProvider.go b/config/configProvider.go index 187fb7b10..928bf948a 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -14,7 +14,7 @@ package config import ( - "github.com/spf13/cast" + "github.com/gohugoio/hugo/common/types" ) // Provider provides the configuration settings for Hugo. @@ -35,14 +35,7 @@ type Provider interface { // we do not attempt to split it into fields. func GetStringSlicePreserveString(cfg Provider, key string) []string { sd := cfg.Get(key) - return toStringSlicePreserveString(sd) -} - -func toStringSlicePreserveString(v interface{}) []string { - if sds, ok := v.(string); ok { - return []string{sds} - } - return cast.ToStringSlice(v) + return types.ToStringSlicePreserveString(sd) } // SetBaseTestDefaults provides some common config defaults used in tests. diff --git a/create/content_template_handler.go b/create/content_template_handler.go index e4cddedf5..3a7007f1a 100644 --- a/create/content_template_handler.go +++ b/create/content_template_handler.go @@ -110,7 +110,7 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archety Date: time.Now().Format(time.RFC3339), Name: name, File: f, - Site: &s.Info, + Site: s.Info, } if archetypeFilename == "" { @@ -73,6 +73,7 @@ github.com/bep/gitmap v1.1.1 h1:Nf8ySnC3I7/xPjuWeCwzukUFv185iTUQ6nOvLy9gCJA= github.com/bep/gitmap v1.1.1/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= github.com/bep/golibsass v0.4.0 h1:B2jsNZuRgpsyzv0I5iubJYApDhib87RzjTcRhVOjg78= github.com/bep/golibsass v0.4.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/golibsass v0.5.0 h1:b+Uxsk826Q35OmbenSmU65P+FJJQoVs2gI2mk1ba28s= github.com/bep/golibsass v0.5.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= diff --git a/helpers/general.go b/helpers/general.go index aa1e00d3a..80e303087 100644 --- a/helpers/general.go +++ b/helpers/general.go @@ -437,36 +437,6 @@ func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { return pflag.NormalizedName(name) } -// DiffStringSlices returns the difference between two string slices. -// Useful in tests. -// See: -// http://stackoverflow.com/questions/19374219/how-to-find-the-difference-between-two-slices-of-strings-in-golang -func DiffStringSlices(slice1 []string, slice2 []string) []string { - diffStr := []string{} - m := map[string]int{} - - for _, s1Val := range slice1 { - m[s1Val] = 1 - } - for _, s2Val := range slice2 { - m[s2Val] = m[s2Val] + 1 - } - - for mKey, mVal := range m { - if mVal == 1 { - diffStr = append(diffStr, mKey) - } - } - - return diffStr -} - -// DiffStrings splits the strings into fields and runs it into DiffStringSlices. -// Useful for tests. -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) { diff --git a/helpers/path.go b/helpers/path.go index d97789e15..29e1e6071 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "regexp" "sort" @@ -243,13 +244,19 @@ func FileAndExtNoDelimiter(in string) (string, string) { return file, strings.TrimPrefix(ext, ".") } -// Filename takes a path, strips out the extension, +// Filename takes a file path, strips out the extension, // and returns the name of the file. func Filename(in string) (name string) { name, _ = fileAndExt(in, fpb) return } +// PathNoExt takes a path, strips out the extension, +// and returns the name of the file. +func PathNoExt(in string) string { + return strings.TrimSuffix(in, path.Ext(in)) +} + // FileAndExt returns the filename and any extension of a file path as // two separate strings. // diff --git a/htesting/hqt/checkers.go b/htesting/hqt/checkers.go index eb8b733ac..6fb65ee47 100644 --- a/htesting/hqt/checkers.go +++ b/htesting/hqt/checkers.go @@ -15,12 +15,24 @@ package hqt import ( "errors" + "fmt" "reflect" + "strings" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/htesting" "github.com/google/go-cmp/cmp" + "github.com/spf13/cast" ) +// IsSameString asserts that two strings are equal. The two strings +// are normalized (whitespace removed) before doing a ==. +// Also note that two strings can be the same even if they're of different +// types. +var IsSameString qt.Checker = &stringChecker{ + argNames: []string{"got", "want"}, +} + // IsSameType asserts that got is the same type as want. var IsSameType qt.Checker = &typeChecker{ argNames: []string{"got", "want"}, @@ -47,6 +59,36 @@ func (c *typeChecker) Check(got interface{}, args []interface{}, note func(key s return nil } +type stringChecker struct { + argNames +} + +// Check implements Checker.Check by checking that got and args[0] represents the same normalized text (whitespace etc. trimmed). +func (c *stringChecker) Check(got interface{}, args []interface{}, note func(key string, value interface{})) (err error) { + s1, s2 := cast.ToString(got), cast.ToString(args[0]) + + if s1 == s2 { + return nil + } + + s1, s2 = normalizeString(s1), normalizeString(s2) + + if s1 == s2 { + return nil + } + + return fmt.Errorf("values are not the same text: %s", htesting.DiffStrings(s1, s2)) +} + +func normalizeString(s string) string { + lines := strings.Split(strings.TrimSpace(s), "\n") + for i, line := range lines { + lines[i] = strings.TrimSpace(line) + } + + return strings.Join(lines, "\n") +} + // DeepAllowUnexported creates an option to allow compare of unexported types // in the given list of types. // see https://github.com/google/go-cmp/issues/40#issuecomment-328615283 diff --git a/htesting/test_helpers.go b/htesting/test_helpers.go index 660c76a44..3804f28fe 100644 --- a/htesting/test_helpers.go +++ b/htesting/test_helpers.go @@ -56,3 +56,33 @@ var rnd = rand.New(rand.NewSource(time.Now().UnixNano())) func RandIntn(n int) int { return rnd.Intn(n) } + +// DiffStringSlices returns the difference between two string slices. +// Useful in tests. +// See: +// http://stackoverflow.com/questions/19374219/how-to-find-the-difference-between-two-slices-of-strings-in-golang +func DiffStringSlices(slice1 []string, slice2 []string) []string { + diffStr := []string{} + m := map[string]int{} + + for _, s1Val := range slice1 { + m[s1Val] = 1 + } + for _, s2Val := range slice2 { + m[s2Val] = m[s2Val] + 1 + } + + for mKey, mVal := range m { + if mVal == 1 { + diffStr = append(diffStr, mKey) + } + } + + return diffStr +} + +// DiffStrings splits the strings into fields and runs it into DiffStringSlices. +// Useful for tests. +func DiffStrings(s1, s2 string) []string { + return DiffStringSlices(strings.Fields(s1), strings.Fields(s2)) +} diff --git a/hugofs/decorators.go b/hugofs/decorators.go index 123655ba0..6247f6183 100644 --- a/hugofs/decorators.go +++ b/hugofs/decorators.go @@ -80,7 +80,8 @@ func DecorateBasePathFs(base *afero.BasePathFs) afero.Fs { // NewBaseFileDecorator decorates the given Fs to provide the real filename // and an Opener func. -func NewBaseFileDecorator(fs afero.Fs) afero.Fs { +func NewBaseFileDecorator(fs afero.Fs, callbacks ...func(fi FileMetaInfo)) afero.Fs { + ffs := &baseFileDecoratorFs{Fs: fs} decorator := func(fi os.FileInfo, filename string) (os.FileInfo, error) { @@ -120,7 +121,14 @@ func NewBaseFileDecorator(fs afero.Fs) afero.Fs { return ffs.open(filename) } - return decorateFileInfo(fi, ffs, opener, filename, "", meta), nil + fim := decorateFileInfo(fi, ffs, opener, filename, "", meta) + + for _, cb := range callbacks { + cb(fim) + } + + return fim, nil + } ffs.decorate = decorator diff --git a/hugofs/fileinfo.go b/hugofs/fileinfo.go index f5e95e952..79d89a88b 100644 --- a/hugofs/fileinfo.go +++ b/hugofs/fileinfo.go @@ -39,6 +39,7 @@ const ( metaKeyBaseDir = "baseDir" // Abs base directory of source file. metaKeyMountRoot = "mountRoot" + metaKeyModule = "module" metaKeyOriginalFilename = "originalFilename" metaKeyName = "name" metaKeyPath = "path" @@ -100,10 +101,10 @@ func (f FileMeta) Name() string { return f.stringV(metaKeyName) } -func (f FileMeta) Classifier() string { - c := f.stringV(metaKeyClassifier) - if c != "" { - return c +func (f FileMeta) Classifier() files.ContentClass { + c, found := f[metaKeyClassifier] + if found { + return c.(files.ContentClass) } return files.ContentClassFile // For sorting @@ -131,6 +132,10 @@ func (f FileMeta) MountRoot() string { return f.stringV(metaKeyMountRoot) } +func (f FileMeta) Module() string { + return f.stringV(metaKeyModule) +} + func (f FileMeta) Weight() int { return f.GetInt(metaKeyWeight) } diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go index 9aa2476b7..e8f8241b7 100644 --- a/hugofs/files/classifier.go +++ b/hugofs/files/classifier.go @@ -49,14 +49,20 @@ func IsContentExt(ext string) bool { return contentFileExtensionsSet[ext] } +type ContentClass string + const ( - ContentClassLeaf = "leaf" - ContentClassBranch = "branch" - ContentClassFile = "zfile" // Sort below - ContentClassContent = "zcontent" + ContentClassLeaf ContentClass = "leaf" + ContentClassBranch ContentClass = "branch" + ContentClassFile ContentClass = "zfile" // Sort below + ContentClassContent ContentClass = "zcontent" ) -func ClassifyContentFile(filename string) string { +func (c ContentClass) IsBundle() bool { + return c == ContentClassLeaf || c == ContentClassBranch +} + +func ClassifyContentFile(filename string) ContentClass { if !IsContentFile(filename) { return ContentClassFile } diff --git a/hugofs/filter_fs.go b/hugofs/filter_fs.go index 952b276cf..a42cd233a 100644 --- a/hugofs/filter_fs.go +++ b/hugofs/filter_fs.go @@ -185,7 +185,7 @@ func (fs *FilterFs) Open(name string) (afero.File, error) { } func (fs *FilterFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { - panic("not implemented") + return fs.fs.Open(name) } func (fs *FilterFs) ReadDir(name string) ([]os.FileInfo, error) { diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go index 229f25fc6..283bb042d 100644 --- a/hugofs/rootmapping_fs.go +++ b/hugofs/rootmapping_fs.go @@ -65,6 +65,7 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { rm.Meta[metaKeyBaseDir] = rm.ToBasedir rm.Meta[metaKeyMountRoot] = rm.path + rm.Meta[metaKeyModule] = rm.Module meta := copyFileMeta(rm.Meta) @@ -121,6 +122,7 @@ type RootMapping struct { From string // The virtual mount. To string // The source directory or file. ToBasedir string // The base of To. May be empty if an absolute path was provided. + Module string // The module path/ID. Meta FileMeta // File metadata (lang etc.) fi FileMetaInfo diff --git a/hugolib/alias.go b/hugolib/alias.go index 9eba8b335..b97094ecc 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -17,7 +17,6 @@ import ( "bytes" "errors" "fmt" - "html/template" "io" "path" "path/filepath" @@ -32,8 +31,6 @@ import ( "github.com/gohugoio/hugo/tpl" ) -var defaultAliasTemplates *template.Template - type aliasHandler struct { t tpl.TemplateHandler log *loggers.Logger diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index 9cd402d6a..dd3aa72a6 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -17,6 +17,7 @@ import ( "bytes" "fmt" "path" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -60,29 +61,33 @@ func TestCascade(t *testing.T) { b.Build(BuildCfg{}) b.AssertFileContent("public/index.html", ` - 12|taxonomy|categories/cool/_index.md|Cascade Category|cat.png|categories|HTML-| - 12|taxonomy|categories/catsect1|catsect1|cat.png|categories|HTML-| - 12|taxonomy|categories/funny|funny|cat.png|categories|HTML-| - 12|taxonomyTerm|categories/_index.md|My Categories|cat.png|categories|HTML-| - 32|taxonomy|categories/sad/_index.md|Cascade Category|sad.png|categories|HTML-| - 42|taxonomy|tags/blue|blue|home.png|tags|HTML-| - 42|section|sect3|Cascade Home|home.png|sect3|HTML-| - 42|taxonomyTerm|tags|Cascade Home|home.png|tags|HTML-| - 42|page|bundle1/index.md|Cascade Home|home.png|page|HTML-| - 42|page|p2.md|Cascade Home|home.png|page|HTML-| - 42|page|sect2/p2.md|Cascade Home|home.png|sect2|HTML-| - 42|page|sect3/p1.md|Cascade Home|home.png|sect3|HTML-| - 42|taxonomy|tags/green|green|home.png|tags|HTML-| - 42|home|_index.md|Home|home.png|page|HTML-| - 42|page|p1.md|p1|home.png|page|HTML-| - 42|section|sect1/_index.md|Sect1|sect1.png|stype|HTML-| - 42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|HTML-| - 42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|HTML-| - 42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|HTML-| - 42|section|sect2/_index.md|Sect2|home.png|sect2|HTML-| - 42|page|sect2/p1.md|Sect2_p1|home.png|sect2|HTML-| - 52|page|sect4/p1.md|Cascade Home|home.png|sect4|RSS-| - 52|section|sect4/_index.md|Sect4|home.png|sect4|RSS-| +12|taxonomy|categories/cool/_index.md|Cascade Category|cat.png|categories|HTML-| +12|taxonomy|categories/catsect1|catsect1|cat.png|categories|HTML-| +12|taxonomy|categories/funny|funny|cat.png|categories|HTML-| +12|taxonomyTerm|categories/_index.md|My Categories|cat.png|categories|HTML-| +32|taxonomy|categories/sad/_index.md|Cascade Category|sad.png|categories|HTML-| +42|taxonomy|tags/blue|blue|home.png|tags|HTML-| +42|taxonomyTerm|tags|Cascade Home|home.png|tags|HTML-| +42|section|sectnocontent|Cascade Home|home.png|sectnocontent|HTML-| +42|section|sect3|Cascade Home|home.png|sect3|HTML-| +42|page|bundle1/index.md|Cascade Home|home.png|page|HTML-| +42|page|p2.md|Cascade Home|home.png|page|HTML-| +42|page|sect2/p2.md|Cascade Home|home.png|sect2|HTML-| +42|page|sect3/nofrontmatter.md|Cascade Home|home.png|sect3|HTML-| +42|page|sect3/p1.md|Cascade Home|home.png|sect3|HTML-| +42|page|sectnocontent/p1.md|Cascade Home|home.png|sectnocontent|HTML-| +42|section|sectnofrontmatter/_index.md|Cascade Home|home.png|sectnofrontmatter|HTML-| +42|taxonomy|tags/green|green|home.png|tags|HTML-| +42|home|_index.md|Home|home.png|page|HTML-| +42|page|p1.md|p1|home.png|page|HTML-| +42|section|sect1/_index.md|Sect1|sect1.png|stype|HTML-| +42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|HTML-| +42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|HTML-| +42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|HTML-| +42|section|sect2/_index.md|Sect2|home.png|sect2|HTML-| +42|page|sect2/p1.md|Sect2_p1|home.png|sect2|HTML-| +52|page|sect4/p1.md|Cascade Home|home.png|sect4|RSS-| +52|section|sect4/_index.md|Sect4|home.png|sect4|RSS-| `) // Check that type set in cascade gets the correct layout. @@ -106,43 +111,131 @@ func TestCascadeEdit(t *testing.T) { title: P1 --- ` - b := newTestSitesBuilder(t).Running() - b.WithTemplatesAdded("_default/single.html", `Banner: {{ .Params.banner }}|Layout: {{ .Layout }}|Type: {{ .Type }}|Content: {{ .Content }}`) - b.WithContent("post/_index.md", ` + + indexContentNoCascade := ` +--- +title: Home +--- +` + + indexContentCascade := ` --- -title: Post +title: Section cascade: banner: post.jpg layout: postlayout type: posttype --- -`) +` - b.WithContent("post/dir/_index.md", ` ---- -title: Dir ---- -`, "post/dir/p1.md", p1Content) - b.Build(BuildCfg{}) + layout := `Banner: {{ .Params.banner }}|Layout: {{ .Layout }}|Type: {{ .Type }}|Content: {{ .Content }}` + + newSite := func(t *testing.T, cascade bool) *sitesBuilder { + b := newTestSitesBuilder(t).Running() + b.WithTemplates("_default/single.html", layout) + b.WithTemplates("_default/list.html", layout) + if cascade { + b.WithContent("post/_index.md", indexContentCascade) + } else { + b.WithContent("post/_index.md", indexContentNoCascade) + } + b.WithContent("post/dir/p1.md", p1Content) - assert := func() { - b.Helper() + return b + } + + t.Run("Edit descendant", func(t *testing.T) { + t.Parallel() + + b := newSite(t, true) + b.Build(BuildCfg{}) + + assert := func() { + b.Helper() + b.AssertFileContent("public/post/dir/p1/index.html", + `Banner: post.jpg|`, + `Layout: postlayout`, + `Type: posttype`, + ) + } + + assert() + + b.EditFiles("content/post/dir/p1.md", p1Content+"\ncontent edit") + b.Build(BuildCfg{}) + + assert() b.AssertFileContent("public/post/dir/p1/index.html", - `Banner: post.jpg|`, - `Layout: postlayout`, - `Type: posttype`, + `content edit +Banner: post.jpg`, ) - } + }) + + t.Run("Edit ancestor", func(t *testing.T) { + t.Parallel() - assert() + b := newSite(t, true) + b.Build(BuildCfg{}) - b.EditFiles("content/post/dir/p1.md", p1Content+"\ncontent edit") - b.Build(BuildCfg{}) + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content:`) - assert() - b.AssertFileContent("public/post/dir/p1/index.html", - `content edit`, - ) + b.EditFiles("content/post/_index.md", strings.Replace(indexContentCascade, "post.jpg", "edit.jpg", 1)) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/post/index.html", `Banner: edit.jpg|Layout: postlayout|Type: posttype|`) + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: edit.jpg|Layout: postlayout|Type: posttype|`) + }) + + t.Run("Edit ancestor, add cascade", func(t *testing.T) { + t.Parallel() + + b := newSite(t, true) + b.Build(BuildCfg{}) + + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg`) + + b.EditFiles("content/post/_index.md", indexContentCascade) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|`) + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`) + }) + + t.Run("Edit ancestor, remove cascade", func(t *testing.T) { + t.Parallel() + + b := newSite(t, false) + b.Build(BuildCfg{}) + + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: |Layout: |`) + + b.EditFiles("content/post/_index.md", indexContentNoCascade) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/post/index.html", `Banner: |Layout: |Type: post|`) + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: |Layout: |`) + }) + + t.Run("Edit ancestor, content only", func(t *testing.T) { + t.Parallel() + + b := newSite(t, true) + b.Build(BuildCfg{}) + + b.EditFiles("content/post/_index.md", indexContentCascade+"\ncontent edit") + + counters := &testCounters{} + b.Build(BuildCfg{testCounters: counters}) + // As we only changed the content, not the cascade front matter, make + // only the home page is re-rendered. + b.Assert(int(counters.contentRenderCounter), qt.Equals, 1) + + b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content: <p>content edit</p>`) + b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`) + }) } func newCascadeTestBuilder(t testing.TB, langs []string) *sitesBuilder { @@ -247,6 +340,12 @@ defaultContentLanguageInSubDir = false }), "sect2/p2.md", p(map[string]interface{}{}), "sect3/p1.md", p(map[string]interface{}{}), + + // No front matter, see #6855 + "sect3/nofrontmatter.md", `**Hello**`, + "sectnocontent/p1.md", `**Hello**`, + "sectnofrontmatter/_index.md", `**Hello**`, + "sect4/_index.md", p(map[string]interface{}{ "title": "Sect4", "cascade": map[string]interface{}{ diff --git a/hugolib/content_map.go b/hugolib/content_map.go new file mode 100644 index 000000000..f0b66d859 --- /dev/null +++ b/hugolib/content_map.go @@ -0,0 +1,971 @@ +// Copyright 2019 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 hugolib + +import ( + "fmt" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/gohugoio/hugo/resources/page" + "github.com/pkg/errors" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/gohugoio/hugo/hugofs" + + radix "github.com/armon/go-radix" +) + +// We store the branch nodes in either the `sections` or `taxonomies` tree +// with their path as a key; Unix style slashes, a leading slash but no +// trailing slash. +// +// E.g. "/blog" or "/categories/funny" +// +// Pages that belongs to a section are stored in the `pages` tree below +// the section name and a branch separator, e.g. "/blog__hb_". A page is +// given a key using the path below the section and the base filename with no extension +// with a leaf separator added. +// +// For bundled pages (/mybundle/index.md), we use the folder name. +// +// An exmple of a full page key would be "/blog__hb_/page1__hl_" +// +// Bundled resources are stored in the `resources` having their path prefixed +// with the bundle they belong to, e.g. +// "/blog__hb_/bundle__hl_data.json". +// +// The weighted taxonomy entries extracted from page front matter are stored in +// the `taxonomyEntries` tree below /plural/term/page-key, e.g. +// "/categories/funny/blog__hb_/bundle__hl_". +const ( + cmBranchSeparator = "__hb_" + cmLeafSeparator = "__hl_" +) + +// Used to mark ambigous keys in reverse index lookups. +var ambigousContentNode = &contentNode{} + +func newContentMap(cfg contentMapConfig) *contentMap { + m := &contentMap{ + cfg: &cfg, + pages: &contentTree{Name: "pages", Tree: radix.New()}, + sections: &contentTree{Name: "sections", Tree: radix.New()}, + taxonomies: &contentTree{Name: "taxonomies", Tree: radix.New()}, + taxonomyEntries: &contentTree{Name: "taxonomyEntries", Tree: radix.New()}, + resources: &contentTree{Name: "resources", Tree: radix.New()}, + } + + m.pageTrees = []*contentTree{ + m.pages, m.sections, m.taxonomies, + } + + m.bundleTrees = []*contentTree{ + m.pages, m.sections, m.taxonomies, m.resources, + } + + m.branchTrees = []*contentTree{ + m.sections, m.taxonomies, + } + + addToReverseMap := func(k string, n *contentNode, m map[interface{}]*contentNode) { + k = strings.ToLower(k) + existing, found := m[k] + if found && existing != ambigousContentNode { + m[k] = ambigousContentNode + } else if !found { + m[k] = n + } + } + + m.pageReverseIndex = &contentTreeReverseIndex{ + t: []*contentTree{m.pages, m.sections, m.taxonomies}, + initFn: func(t *contentTree, m map[interface{}]*contentNode) { + t.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + if n.p != nil && !n.p.File().IsZero() { + meta := n.p.File().FileInfo().Meta() + if meta.Path() != meta.PathFile() { + // Keep track of the original mount source. + mountKey := filepath.ToSlash(filepath.Join(meta.Module(), meta.PathFile())) + addToReverseMap(mountKey, n, m) + } + } + k := strings.TrimSuffix(path.Base(s), cmLeafSeparator) + addToReverseMap(k, n, m) + return false + }) + }, + } + + return m +} + +type cmInsertKeyBuilder struct { + m *contentMap + + err error + + // Builder state + tree *contentTree + baseKey string // Section or page key + key string +} + +func (b cmInsertKeyBuilder) ForPage(s string) *cmInsertKeyBuilder { + // TODO2 fmt.Println("ForPage:", s, "baseKey:", b.baseKey, "key:", b.key) + baseKey := b.baseKey + b.baseKey = s + + if !strings.HasPrefix(s, "/") { + s = "/" + s + } + + if baseKey != "/" { + // Don't repeat the section path in the key. + s = strings.TrimPrefix(s, baseKey) + } + + switch b.tree { + case b.m.sections: + b.tree = b.m.pages + b.key = baseKey + cmBranchSeparator + s + cmLeafSeparator + case b.m.taxonomies: + b.key = path.Join(baseKey, s) + default: + panic("invalid state") + } + + return &b +} + +func (b cmInsertKeyBuilder) ForResource(s string) *cmInsertKeyBuilder { + // TODO2 fmt.Println("ForResource:", s, "baseKey:", b.baseKey, "key:", b.key) + + s = strings.TrimPrefix(s, "/") + s = strings.TrimPrefix(s, strings.TrimPrefix(b.baseKey, "/")+"/") + + switch b.tree { + case b.m.pages: + b.key = b.key + s + case b.m.sections, b.m.taxonomies: + b.key = b.key + cmLeafSeparator + s + default: + panic(fmt.Sprintf("invalid state: %#v", b.tree)) + } + b.tree = b.m.resources + return &b +} + +func (b *cmInsertKeyBuilder) Insert(n *contentNode) *cmInsertKeyBuilder { + if b.err == nil { + b.tree.Insert(cleanTreeKey(b.key), n) + } + return b +} + +func (b *cmInsertKeyBuilder) DeleteAll() *cmInsertKeyBuilder { + if b.err == nil { + b.tree.DeletePrefix(cleanTreeKey(b.key)) + } + return b +} + +func (b *cmInsertKeyBuilder) WithFile(fi hugofs.FileMetaInfo) *cmInsertKeyBuilder { + b.newTopLevel() + m := b.m + meta := fi.Meta() + p := cleanTreeKey(meta.Path()) + bundlePath := m.getBundleDir(meta) + isBundle := meta.Classifier().IsBundle() + if isBundle { + panic("not implemented") + } + + p, k := b.getBundle(p) + if k == "" { + b.err = errors.Errorf("no bundle header found for %q", bundlePath) + return b + } + + id := k + m.reduceKeyPart(p, fi.Meta().Path()) + b.tree = b.m.resources + b.key = id + b.baseKey = p + + return b +} + +func (b *cmInsertKeyBuilder) WithSection(s string) *cmInsertKeyBuilder { + b.newTopLevel() + b.tree = b.m.sections + b.baseKey = s + b.key = s + // TODO2 fmt.Println("WithSection:", s, "baseKey:", b.baseKey, "key:", b.key) + return b +} + +func (b *cmInsertKeyBuilder) WithTaxonomy(s string) *cmInsertKeyBuilder { + b.newTopLevel() + b.tree = b.m.taxonomies + b.baseKey = s + b.key = s + return b +} + +// getBundle gets both the key to the section and the prefix to where to store +// this page bundle and its resources. +func (b *cmInsertKeyBuilder) getBundle(s string) (string, string) { + m := b.m + section, _ := m.getSection(s) + + p := s + if section != "/" { + p = strings.TrimPrefix(s, section) + } + + bundlePathParts := strings.Split(p, "/")[1:] + basePath := section + cmBranchSeparator + + // Put it into an existing bundle if found. + for i := len(bundlePathParts) - 2; i >= 0; i-- { + bundlePath := path.Join(bundlePathParts[:i]...) + searchKey := basePath + "/" + bundlePath + cmLeafSeparator + if _, found := m.pages.Get(searchKey); found { + return section + "/" + bundlePath, searchKey + } + } + + // Put it into the section bundle. + return section, section + cmLeafSeparator +} + +func (b *cmInsertKeyBuilder) newTopLevel() { + b.key = "" +} + +type contentBundleViewInfo struct { + name viewName + termKey string + termOrigin string + weight int + ref *contentNode +} + +func (c *contentBundleViewInfo) kind() string { + if c.termKey != "" { + return page.KindTaxonomy + } + return page.KindTaxonomyTerm +} + +func (c *contentBundleViewInfo) sections() []string { + if c.kind() == page.KindTaxonomyTerm { + return []string{c.name.plural} + } + + return []string{c.name.plural, c.termKey} + +} + +func (c *contentBundleViewInfo) term() string { + if c.termOrigin != "" { + return c.termOrigin + } + + return c.termKey +} + +type contentMap struct { + cfg *contentMapConfig + + // View of regular pages, sections, and taxonomies. + pageTrees contentTrees + + // View of pages, sections, taxonomies, and resources. + bundleTrees contentTrees + + // View of sections and taxonomies. + branchTrees contentTrees + + // Stores page bundles keyed by its path's directory or the base filename, + // e.g. "blog/post.md" => "/blog/post", "blog/post/index.md" => "/blog/post" + // These are the "regular pages" and all of them are bundles. + pages *contentTree + + // A reverse index used as a fallback in GetPage. + // There are currently two cases where this is used: + // 1. Short name lookups in ref/relRef, e.g. using only "mypage.md" without a path. + // 2. Links resolved from a remounted content directory. These are restricted to the same module. + // Both of the above cases can result in ambigous lookup errors. + pageReverseIndex *contentTreeReverseIndex + + // Section nodes. + sections *contentTree + + // Taxonomy nodes. + taxonomies *contentTree + + // Pages in a taxonomy. + taxonomyEntries *contentTree + + // Resources stored per bundle below a common prefix, e.g. "/blog/post__hb_". + resources *contentTree +} + +func (m *contentMap) AddFiles(fis ...hugofs.FileMetaInfo) error { + for _, fi := range fis { + if err := m.addFile(fi); err != nil { + return err + } + } + + return nil +} + +func (m *contentMap) AddFilesBundle(header hugofs.FileMetaInfo, resources ...hugofs.FileMetaInfo) error { + var ( + meta = header.Meta() + classifier = meta.Classifier() + isBranch = classifier == files.ContentClassBranch + bundlePath = m.getBundleDir(meta) + + n = m.newContentNodeFromFi(header) + b = m.newKeyBuilder() + + section string + ) + + if isBranch { + // Either a section or a taxonomy node. + section = bundlePath + if tc := m.cfg.getTaxonomyConfig(section); !tc.IsZero() { + term := strings.TrimPrefix(strings.TrimPrefix(section, "/"+tc.plural), "/") + + n.viewInfo = &contentBundleViewInfo{ + name: tc, + termKey: term, + termOrigin: term, + } + + n.viewInfo.ref = n + b.WithTaxonomy(section).Insert(n) + } else { + b.WithSection(section).Insert(n) + } + } else { + // A regular page. Attach it to its section. + section, _ = m.getOrCreateSection(n, bundlePath) + b = b.WithSection(section).ForPage(bundlePath).Insert(n) + } + + if m.cfg.isRebuild { + // The resource owner will be either deleted or overwritten on rebuilds, + // but make sure we handle deletion of resources (images etc.) as well. + b.ForResource("").DeleteAll() + } + + for _, r := range resources { + rb := b.ForResource(cleanTreeKey(r.Meta().Path())) + rb.Insert(&contentNode{fi: r}) + } + + return nil + +} + +func (m *contentMap) CreateMissingNodes() error { + // Create missing home and root sections + rootSections := make(map[string]interface{}) + trackRootSection := func(s string, b *contentNode) { + parts := strings.Split(s, "/") + if len(parts) > 2 { + root := strings.TrimSuffix(parts[1], cmBranchSeparator) + if root != "" { + if _, found := rootSections[root]; !found { + rootSections[root] = b + } + } + } + } + + m.sections.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + + if s == "/" { + return false + } + + trackRootSection(s, n) + return false + }) + + m.pages.Walk(func(s string, v interface{}) bool { + trackRootSection(s, v.(*contentNode)) + return false + }) + + if _, found := rootSections["/"]; !found { + rootSections["/"] = true + } + + for sect, v := range rootSections { + var sectionPath string + if n, ok := v.(*contentNode); ok && n.path != "" { + sectionPath = n.path + firstSlash := strings.Index(sectionPath, "/") + if firstSlash != -1 { + sectionPath = sectionPath[:firstSlash] + } + } + sect = cleanTreeKey(sect) + _, found := m.sections.Get(sect) + if !found { + m.sections.Insert(sect, &contentNode{path: sectionPath}) + } + } + + for _, view := range m.cfg.taxonomyConfig { + s := cleanTreeKey(view.plural) + _, found := m.taxonomies.Get(s) + if !found { + b := &contentNode{ + viewInfo: &contentBundleViewInfo{ + name: view, + }, + } + b.viewInfo.ref = b + m.taxonomies.Insert(s, b) + } + } + + return nil + +} + +func (m *contentMap) getBundleDir(meta hugofs.FileMeta) string { + dir := cleanTreeKey(filepath.Dir(meta.Path())) + + switch meta.Classifier() { + case files.ContentClassContent: + return path.Join(dir, meta.TranslationBaseName()) + default: + return dir + } +} + +func (m *contentMap) newContentNodeFromFi(fi hugofs.FileMetaInfo) *contentNode { + return &contentNode{ + fi: fi, + path: strings.TrimPrefix(filepath.ToSlash(fi.Meta().Path()), "/"), + } +} + +func (m *contentMap) getFirstSection(s string) (string, *contentNode) { + for { + k, v, found := m.sections.LongestPrefix(s) + if !found { + return "", nil + } + if strings.Count(k, "/") == 1 { + return k, v.(*contentNode) + } + s = path.Dir(s) + } +} + +func (m *contentMap) newKeyBuilder() *cmInsertKeyBuilder { + return &cmInsertKeyBuilder{m: m} +} + +func (m *contentMap) getOrCreateSection(n *contentNode, s string) (string, *contentNode) { + level := strings.Count(s, "/") + k, b := m.getSection(s) + + mustCreate := false + + if k == "" { + mustCreate = true + } else if level > 1 && k == "/" { + // We found the home section, but this page needs to be placed in + // the root, e.g. "/blog", section. + mustCreate = true + } + + if mustCreate { + k = s[:strings.Index(s[1:], "/")+1] + if k == "" { + k = "/" + } + + b = &contentNode{ + path: n.rootSection(), + } + + m.sections.Insert(k, b) + } + + return k, b +} + +func (m *contentMap) getPage(section, name string) *contentNode { + key := section + cmBranchSeparator + "/" + name + cmLeafSeparator + v, found := m.pages.Get(key) + if found { + return v.(*contentNode) + } + return nil +} + +func (m *contentMap) getSection(s string) (string, *contentNode) { + k, v, found := m.sections.LongestPrefix(path.Dir(s)) + if found { + return k, v.(*contentNode) + } + return "", nil +} + +func (m *contentMap) getTaxonomyParent(s string) (string, *contentNode) { + s = path.Dir(s) + if s == "/" { + v, found := m.sections.Get(s) + if found { + return s, v.(*contentNode) + } + return "", nil + } + + for _, tree := range []*contentTree{m.taxonomies, m.sections} { + k, v, found := tree.LongestPrefix(s) + if found { + return k, v.(*contentNode) + } + } + return "", nil +} + +func (m *contentMap) addFile(fi hugofs.FileMetaInfo) error { + b := m.newKeyBuilder() + return b.WithFile(fi).Insert(m.newContentNodeFromFi(fi)).err +} + +func cleanTreeKey(k string) string { + k = "/" + strings.ToLower(strings.Trim(path.Clean(filepath.ToSlash(k)), "./")) + return k +} + +func (m *contentMap) onSameLevel(s1, s2 string) bool { + return strings.Count(s1, "/") == strings.Count(s2, "/") +} + +func (m *contentMap) deleteBundleMatching(matches func(b *contentNode) bool) { + // Check sections first + s := m.sections.getMatch(matches) + if s != "" { + m.deleteSectionByPath(s) + return + } + + s = m.pages.getMatch(matches) + if s != "" { + m.deletePage(s) + return + } + + s = m.resources.getMatch(matches) + if s != "" { + m.resources.Delete(s) + } + +} + +// Deletes any empty root section that's not backed by a content file. +func (m *contentMap) deleteOrphanSections() { + + m.sections.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + + if n.fi != nil { + // Section may be empty, but is backed by a content file. + return false + } + + if s == "/" || strings.Count(s, "/") > 1 { + return false + } + + prefixBundle := s + cmBranchSeparator + + if !(m.sections.hasPrefix(s+"/") || m.pages.hasPrefix(prefixBundle) || m.resources.hasPrefix(prefixBundle)) { + m.sections.Delete(s) + } + + return false + }) +} + +func (m *contentMap) deletePage(s string) { + m.pages.DeletePrefix(s) + m.resources.DeletePrefix(s) +} + +func (m *contentMap) deleteSectionByPath(s string) { + m.sections.Delete(s) + m.sections.DeletePrefix(s + "/") + m.pages.DeletePrefix(s + cmBranchSeparator) + m.pages.DeletePrefix(s + "/") + m.resources.DeletePrefix(s + cmBranchSeparator) + m.resources.DeletePrefix(s + cmLeafSeparator) + m.resources.DeletePrefix(s + "/") +} + +func (m *contentMap) deletePageByPath(s string) { + m.pages.Walk(func(s string, v interface{}) bool { + fmt.Println("S", s) + + return false + }) +} + +func (m *contentMap) deleteTaxonomy(s string) { + m.taxonomies.Delete(s) + m.taxonomies.DeletePrefix(s + "/") +} + +func (m *contentMap) reduceKeyPart(dir, filename string) string { + dir, filename = filepath.ToSlash(dir), filepath.ToSlash(filename) + dir, filename = strings.TrimPrefix(dir, "/"), strings.TrimPrefix(filename, "/") + + return strings.TrimPrefix(strings.TrimPrefix(filename, dir), "/") +} + +func (m *contentMap) splitKey(k string) []string { + if k == "" || k == "/" { + return nil + } + + return strings.Split(k, "/")[1:] + +} + +func (m *contentMap) testDump() string { + var sb strings.Builder + + for i, r := range []*contentTree{m.pages, m.sections, m.resources} { + sb.WriteString(fmt.Sprintf("Tree %d:\n", i)) + r.Walk(func(s string, v interface{}) bool { + sb.WriteString("\t" + s + "\n") + return false + }) + } + + for i, r := range []*contentTree{m.pages, m.sections} { + + r.Walk(func(s string, v interface{}) bool { + c := v.(*contentNode) + cpToString := func(c *contentNode) string { + var sb strings.Builder + if c.p != nil { + sb.WriteString("|p:" + c.p.Title()) + } + if c.fi != nil { + sb.WriteString("|f:" + filepath.ToSlash(c.fi.Meta().Path())) + } + return sb.String() + } + sb.WriteString(path.Join(m.cfg.lang, r.Name) + s + cpToString(c) + "\n") + + resourcesPrefix := s + + if i == 1 { + resourcesPrefix += cmLeafSeparator + + m.pages.WalkPrefix(s+cmBranchSeparator, func(s string, v interface{}) bool { + sb.WriteString("\t - P: " + filepath.ToSlash((v.(*contentNode).fi.(hugofs.FileMetaInfo)).Meta().Filename()) + "\n") + return false + }) + } + + m.resources.WalkPrefix(resourcesPrefix, func(s string, v interface{}) bool { + sb.WriteString("\t - R: " + filepath.ToSlash((v.(*contentNode).fi.(hugofs.FileMetaInfo)).Meta().Filename()) + "\n") + return false + + }) + + return false + }) + } + + return sb.String() + +} + +type contentMapConfig struct { + lang string + taxonomyConfig []viewName + taxonomyDisabled bool + taxonomyTermDisabled bool + pageDisabled bool + isRebuild bool +} + +func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) { + s = strings.TrimPrefix(s, "/") + if s == "" { + return + } + for _, n := range cfg.taxonomyConfig { + if strings.HasPrefix(s, n.plural) { + return n + } + } + + return +} + +type contentNode struct { + p *pageState + + // Set for taxonomy nodes. + viewInfo *contentBundleViewInfo + + // Set if source is a file. + // We will soon get other sources. + fi hugofs.FileMetaInfo + + // The source path. Unix slashes. No leading slash. + path string +} + +func (b *contentNode) rootSection() string { + if b.path == "" { + return "" + } + firstSlash := strings.Index(b.path, "/") + if firstSlash == -1 { + return b.path + } + return b.path[:firstSlash] + +} + +type contentTree struct { + Name string + *radix.Tree +} + +type contentTrees []*contentTree + +func (t contentTrees) DeletePrefix(prefix string) int { + var count int + for _, tree := range t { + tree.Walk(func(s string, v interface{}) bool { + return false + }) + count += tree.DeletePrefix(prefix) + } + return count +} + +type contentTreeNodeCallback func(s string, n *contentNode) bool + +var ( + contentTreeNoListFilter = func(s string, n *contentNode) bool { + if n.p == nil { + return true + } + return n.p.m.noList() + } + + contentTreeNoRenderFilter = func(s string, n *contentNode) bool { + if n.p == nil { + return true + } + return n.p.m.noRender() + } +) + +func (c *contentTree) WalkPrefixListable(prefix string, fn contentTreeNodeCallback) { + c.WalkPrefixFilter(prefix, contentTreeNoListFilter, fn) +} + +func (c *contentTree) WalkPrefixFilter(prefix string, filter, walkFn contentTreeNodeCallback) { + c.WalkPrefix(prefix, func(s string, v interface{}) bool { + n := v.(*contentNode) + if filter(s, n) { + return false + } + return walkFn(s, n) + }) +} + +func (c *contentTree) WalkListable(fn contentTreeNodeCallback) { + c.WalkFilter(contentTreeNoListFilter, fn) +} + +func (c *contentTree) WalkFilter(filter, walkFn contentTreeNodeCallback) { + c.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + if filter(s, n) { + return false + } + return walkFn(s, n) + }) +} + +func (c contentTrees) WalkListable(fn contentTreeNodeCallback) { + for _, tree := range c { + tree.WalkListable(fn) + } +} + +func (c contentTrees) WalkRenderable(fn contentTreeNodeCallback) { + for _, tree := range c { + tree.WalkFilter(contentTreeNoRenderFilter, fn) + } +} + +func (c contentTrees) Walk(fn contentTreeNodeCallback) { + for _, tree := range c { + tree.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + return fn(s, n) + }) + } +} + +func (c contentTrees) WalkPrefix(prefix string, fn contentTreeNodeCallback) { + for _, tree := range c { + tree.WalkPrefix(prefix, func(s string, v interface{}) bool { + n := v.(*contentNode) + return fn(s, n) + }) + } +} + +func (c *contentTree) getMatch(matches func(b *contentNode) bool) string { + var match string + c.Walk(func(s string, v interface{}) bool { + n, ok := v.(*contentNode) + if !ok { + return false + } + + if matches(n) { + match = s + return true + } + + return false + }) + + return match +} + +func (c *contentTree) hasPrefix(s string) bool { + var t bool + c.Tree.WalkPrefix(s, func(s string, v interface{}) bool { + t = true + return true + }) + return t +} + +func (c *contentTree) printKeys() { + c.Walk(func(s string, v interface{}) bool { + fmt.Println(s) + return false + }) +} + +func (c *contentTree) printKeysPrefix(prefix string) { + c.WalkPrefix(prefix, func(s string, v interface{}) bool { + fmt.Println(s) + return false + }) +} + +// contentTreeRef points to a node in the given tree. +type contentTreeRef struct { + m *pageMap + t *contentTree + n *contentNode + key string +} + +func (c *contentTreeRef) getCurrentSection() (string, *contentNode) { + if c.isSection() { + return c.key, c.n + } + return c.getSection() +} + +func (c *contentTreeRef) isSection() bool { + return c.t == c.m.sections +} + +func (c *contentTreeRef) getSection() (string, *contentNode) { + return c.m.getSection(c.key) +} + +func (c *contentTreeRef) collectPages() page.Pages { + var pas page.Pages + c.m.collectPages(c.key+cmBranchSeparator, func(c *contentNode) { + pas = append(pas, c.p) + }) + page.SortByDefault(pas) + + return pas +} + +func (c *contentTreeRef) collectPagesAndSections() page.Pages { + var pas page.Pages + c.m.collectPagesAndSections(c.key, func(c *contentNode) { + pas = append(pas, c.p) + }) + page.SortByDefault(pas) + + return pas +} + +func (c *contentTreeRef) collectSections() page.Pages { + var pas page.Pages + c.m.collectSections(c.key, func(c *contentNode) { + pas = append(pas, c.p) + }) + page.SortByDefault(pas) + + return pas +} + +type contentTreeReverseIndex struct { + t []*contentTree + m map[interface{}]*contentNode + + init sync.Once + initFn func(*contentTree, map[interface{}]*contentNode) +} + +func (c *contentTreeReverseIndex) Get(key interface{}) *contentNode { + c.init.Do(func() { + c.m = make(map[interface{}]*contentNode) + for _, tree := range c.t { + c.initFn(tree, c.m) + } + }) + return c.m[key] +} diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go new file mode 100644 index 000000000..3269abe12 --- /dev/null +++ b/hugolib/content_map_page.go @@ -0,0 +1,998 @@ +// Copyright 2019 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 hugolib + +import ( + "context" + "fmt" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/gohugoio/hugo/common/maps" + + "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/resources" + + "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/parser/pageparser" + "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/resource" + "github.com/spf13/cast" + + "github.com/gohugoio/hugo/common/para" + "github.com/pkg/errors" +) + +func newPageMaps(h *HugoSites) *pageMaps { + mps := make([]*pageMap, len(h.Sites)) + for i, s := range h.Sites { + mps[i] = s.pageMap + } + return &pageMaps{ + workers: para.New(h.numWorkers), + pmaps: mps, + } + +} + +type pageMap struct { + s *Site + *contentMap +} + +func (m *pageMap) Len() int { + l := 0 + for _, t := range m.contentMap.pageTrees { + l += t.Len() + } + return l +} + +func (m *pageMap) createMissingTaxonomyNodes() error { + if m.cfg.taxonomyDisabled { + return nil + } + m.taxonomyEntries.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + vi := n.viewInfo + k := cleanTreeKey(vi.name.plural + "/" + vi.termKey) + + if _, found := m.taxonomies.Get(k); !found { + vic := &contentBundleViewInfo{ + name: vi.name, + termKey: vi.termKey, + termOrigin: vi.termOrigin, + } + m.taxonomies.Insert(k, &contentNode{viewInfo: vic}) + } + return false + }) + + return nil +} + +func (m *pageMap) newPageFromContentNode(n *contentNode, parentBucket *pagesMapBucket, owner *pageState) (*pageState, error) { + if n.fi == nil { + panic("FileInfo must (currently) be set") + } + + f, err := newFileInfo(m.s.SourceSpec, n.fi) + if err != nil { + return nil, err + } + + meta := n.fi.Meta() + content := func() (hugio.ReadSeekCloser, error) { + return meta.Open() + } + + bundled := owner != nil + s := m.s + + sections := s.sectionsFromFile(f) + + kind := s.kindFromFileInfoOrSections(f, sections) + if kind == page.KindTaxonomy { + s.PathSpec.MakePathsSanitized(sections) + } + + metaProvider := &pageMeta{kind: kind, sections: sections, bundled: bundled, s: s, f: f} + + ps, err := newPageBase(metaProvider) + if err != nil { + return nil, err + } + + if n.fi.Meta().GetBool(walkIsRootFileMetaKey) { + // Make sure that the bundle/section we start walking from is always + // rendered. + // This is only relevant in server fast render mode. + ps.forceRender = true + } + + n.p = ps + if ps.IsNode() { + ps.bucket = newPageBucket(ps) + } + + gi, err := s.h.gitInfoForPage(ps) + if err != nil { + return nil, errors.Wrap(err, "failed to load Git data") + } + ps.gitInfo = gi + + r, err := content() + if err != nil { + return nil, err + } + defer r.Close() + + parseResult, err := pageparser.Parse( + r, + pageparser.Config{EnableEmoji: s.siteCfg.enableEmoji}, + ) + if err != nil { + return nil, err + } + + ps.pageContent = pageContent{ + source: rawPageContent{ + parsed: parseResult, + posMainContent: -1, + posSummaryEnd: -1, + posBodyStart: -1, + }, + } + + ps.shortcodeState = newShortcodeHandler(ps, ps.s, nil) + + if err := ps.mapContent(parentBucket, metaProvider); err != nil { + return nil, ps.wrapError(err) + } + + if err := metaProvider.applyDefaultValues(n); err != nil { + return nil, err + } + + ps.init.Add(func() (interface{}, error) { + pp, err := newPagePaths(s, ps, metaProvider) + if err != nil { + return nil, err + } + + outputFormatsForPage := ps.m.outputFormats() + + if !ps.m.noRender() { + // Prepare output formats for all sites. + ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats)) + created := make(map[string]*pageOutput) + + for i, f := range ps.s.h.renderFormats { + if po, found := created[f.Name]; found { + ps.pageOutputs[i] = po + continue + } + + _, render := outputFormatsForPage.GetByName(f.Name) + po := newPageOutput(ps, pp, f, render) + + // Create a content provider for the first, + // we may be able to reuse it. + if i == 0 { + contentProvider, err := newPageContentOutput(ps, po) + if err != nil { + return nil, err + } + po.initContentProvider(contentProvider) + } + + ps.pageOutputs[i] = po + created[f.Name] = po + } + } else if ps.m.buildConfig.PublishResources { + // We need one output format for potential resources to publish. + po := newPageOutput(ps, pp, outputFormatsForPage[0], false) + contentProvider, err := newPageContentOutput(ps, po) + if err != nil { + return nil, err + } + po.initContentProvider(contentProvider) + ps.pageOutputs = []*pageOutput{po} + } + + if err := ps.initCommonProviders(pp); err != nil { + return nil, err + } + + return nil, nil + }) + + ps.parent = owner + + return ps, nil +} + +func (m *pageMap) newResource(fim hugofs.FileMetaInfo, owner *pageState) (resource.Resource, error) { + + if owner == nil { + panic("owner is nil") + } + // TODO(bep) consolidate with multihost logic + clean up + outputFormats := owner.m.outputFormats() + seen := make(map[string]bool) + var targetBasePaths []string + // Make sure bundled resources are published to all of the ouptput formats' + // sub paths. + for _, f := range outputFormats { + p := f.Path + if seen[p] { + continue + } + seen[p] = true + targetBasePaths = append(targetBasePaths, p) + + } + + meta := fim.Meta() + r := func() (hugio.ReadSeekCloser, error) { + return meta.Open() + } + + target := strings.TrimPrefix(meta.Path(), owner.File().Dir()) + + return owner.s.ResourceSpec.New( + resources.ResourceSourceDescriptor{ + TargetPaths: owner.getTargetPaths, + OpenReadSeekCloser: r, + FileInfo: fim, + RelTargetFilename: target, + TargetBasePaths: targetBasePaths, + }) +} + +func (m *pageMap) createSiteTaxonomies() error { + m.s.taxonomies = make(TaxonomyList) + m.taxonomies.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + t := n.viewInfo + + viewName := t.name + + if t.termKey == "" { + m.s.taxonomies[viewName.plural] = make(Taxonomy) + } else { + taxonomy := m.s.taxonomies[viewName.plural] + m.taxonomyEntries.WalkPrefix(s+"/", func(ss string, v interface{}) bool { + b2 := v.(*contentNode) + info := b2.viewInfo + taxonomy.add(info.termKey, page.NewWeightedPage(info.weight, info.ref.p, n.p)) + + return false + }) + } + + return false + }) + + for _, taxonomy := range m.s.taxonomies { + for _, v := range taxonomy { + v.Sort() + } + } + + return nil +} + +func (m *pageMap) createListAllPages() page.Pages { + pages := make(page.Pages, 0) + + m.contentMap.pageTrees.Walk(func(s string, n *contentNode) bool { + if n.p == nil { + panic(fmt.Sprintf("BUG: page not set for %q", s)) + } + if contentTreeNoListFilter(s, n) { + return false + } + pages = append(pages, n.p) + return false + }) + + page.SortByDefault(pages) + return pages +} + +func (m *pageMap) assemblePages() error { + m.taxonomyEntries.DeletePrefix("/") + + if err := m.assembleSections(); err != nil { + return err + } + + var err error + + if err != nil { + return err + } + + m.pages.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + + var shouldBuild bool + + defer func() { + // Make sure we always rebuild the view cache. + if shouldBuild && err == nil && n.p != nil { + m.attachPageToViews(s, n) + } + }() + + if n.p != nil { + // A rebuild + shouldBuild = true + return false + } + + var parent *contentNode + var parentBucket *pagesMapBucket + + _, parent = m.getSection(s) + if parent == nil { + panic(fmt.Sprintf("BUG: parent not set for %q", s)) + } + parentBucket = parent.p.bucket + + n.p, err = m.newPageFromContentNode(n, parentBucket, nil) + if err != nil { + return true + } + + shouldBuild = !(n.p.Kind() == page.KindPage && m.cfg.pageDisabled) && m.s.shouldBuild(n.p) + if !shouldBuild { + m.deletePage(s) + return false + } + + n.p.treeRef = &contentTreeRef{ + m: m, + t: m.pages, + n: n, + key: s, + } + + if err = m.assembleResources(s, n.p, parentBucket); err != nil { + return true + } + + return false + }) + + m.deleteOrphanSections() + + return err +} + +func (m *pageMap) assembleResources(s string, p *pageState, parentBucket *pagesMapBucket) error { + var err error + + m.resources.WalkPrefix(s, func(s string, v interface{}) bool { + n := v.(*contentNode) + meta := n.fi.Meta() + classifier := meta.Classifier() + var r resource.Resource + switch classifier { + case files.ContentClassContent: + var rp *pageState + rp, err = m.newPageFromContentNode(n, parentBucket, p) + if err != nil { + return true + } + rp.m.resourcePath = filepath.ToSlash(strings.TrimPrefix(rp.Path(), p.File().Dir())) + r = rp + + case files.ContentClassFile: + r, err = m.newResource(n.fi, p) + if err != nil { + return true + } + default: + panic(fmt.Sprintf("invalid classifier: %q", classifier)) + } + + p.resources = append(p.resources, r) + return false + }) + + return err +} + +func (m *pageMap) assembleSections() error { + + var sectionsToDelete []string + var err error + + m.sections.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + + var shouldBuild bool + + defer func() { + // Make sure we always rebuild the view cache. + if shouldBuild && err == nil && n.p != nil { + m.attachPageToViews(s, n) + if n.p.IsHome() { + m.s.home = n.p + } + } + }() + + sections := m.splitKey(s) + + if n.p != nil { + if n.p.IsHome() { + m.s.home = n.p + } + shouldBuild = true + return false + } + + var parent *contentNode + var parentBucket *pagesMapBucket + + if s != "/" { + _, parent = m.getSection(s) + if parent == nil || parent.p == nil { + panic(fmt.Sprintf("BUG: parent not set for %q", s)) + } + } + + if parent != nil { + parentBucket = parent.p.bucket + } + + kind := page.KindSection + if s == "/" { + kind = page.KindHome + } + + if n.fi != nil { + n.p, err = m.newPageFromContentNode(n, parentBucket, nil) + if err != nil { + return true + } + } else { + n.p = m.s.newPage(n, parentBucket, kind, "", sections...) + } + + shouldBuild = m.s.shouldBuild(n.p) + if !shouldBuild { + sectionsToDelete = append(sectionsToDelete, s) + return false + } + + n.p.treeRef = &contentTreeRef{ + m: m, + t: m.sections, + n: n, + key: s, + } + + if err = m.assembleResources(s+cmLeafSeparator, n.p, parentBucket); err != nil { + return true + } + + return false + }) + + for _, s := range sectionsToDelete { + m.deleteSectionByPath(s) + } + + return err +} + +func (m *pageMap) assembleTaxonomies() error { + + var taxonomiesToDelete []string + var err error + + m.taxonomies.Walk(func(s string, v interface{}) bool { + n := v.(*contentNode) + + if n.p != nil { + return false + } + + kind := n.viewInfo.kind() + sections := n.viewInfo.sections() + + _, parent := m.getTaxonomyParent(s) + if parent == nil || parent.p == nil { + panic(fmt.Sprintf("BUG: parent not set for %q", s)) + } + parentBucket := parent.p.bucket + + if n.fi != nil { + n.p, err = m.newPageFromContentNode(n, parent.p.bucket, nil) + if err != nil { + return true + } + } else { + title := "" + if kind == page.KindTaxonomy { + title = n.viewInfo.term() + } + n.p = m.s.newPage(n, parent.p.bucket, kind, title, sections...) + } + + if !m.s.shouldBuild(n.p) { + taxonomiesToDelete = append(taxonomiesToDelete, s) + return false + } + + n.p.treeRef = &contentTreeRef{ + m: m, + t: m.taxonomies, + n: n, + key: s, + } + + if err = m.assembleResources(s+cmLeafSeparator, n.p, parentBucket); err != nil { + return true + } + + return false + }) + + for _, s := range taxonomiesToDelete { + m.deleteTaxonomy(s) + } + + return err + +} + +func (m *pageMap) attachPageToViews(s string, b *contentNode) { + if m.cfg.taxonomyDisabled { + return + } + + for _, viewName := range m.cfg.taxonomyConfig { + vals := types.ToStringSlicePreserveString(getParam(b.p, viewName.plural, false)) + if vals == nil { + continue + } + + w := getParamToLower(b.p, viewName.plural+"_weight") + weight, err := cast.ToIntE(w) + if err != nil { + m.s.Log.ERROR.Printf("Unable to convert taxonomy weight %#v to int for %q", w, b.p.Path()) + // weight will equal zero, so let the flow continue + } + + for _, v := range vals { + termKey := m.s.getTaxonomyKey(v) + + bv := &contentNode{ + viewInfo: &contentBundleViewInfo{ + name: viewName, + termKey: termKey, + termOrigin: v, + weight: weight, + ref: b, + }, + } + + if s == "/" { + // To avoid getting an empty key. + s = "home" + } + key := cleanTreeKey(path.Join(viewName.plural, termKey, s)) + m.taxonomyEntries.Insert(key, bv) + } + } +} + +func (m *pageMap) collectPages(prefix string, fn func(c *contentNode)) error { + m.pages.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { + fn(n) + return false + }) + return nil +} + +func (m *pageMap) collectPagesAndSections(prefix string, fn func(c *contentNode)) error { + if err := m.collectSections(prefix, fn); err != nil { + return err + } + + if err := m.collectPages(prefix+cmBranchSeparator, fn); err != nil { + return err + } + + return nil +} + +func (m *pageMap) collectSections(prefix string, fn func(c *contentNode)) error { + var level int + isHome := prefix == "/" + + if !isHome { + level = strings.Count(prefix, "/") + } + + return m.collectSectionsFn(prefix, func(s string, c *contentNode) bool { + if s == prefix { + return false + } + + if (strings.Count(s, "/") - level) != 1 { + return false + } + + fn(c) + + return false + }) +} + +func (m *pageMap) collectSectionsFn(prefix string, fn func(s string, c *contentNode) bool) error { + if !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + + m.sections.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { + return fn(s, n) + }) + + return nil +} + +func (m *pageMap) collectSectionsRecursiveIncludingSelf(prefix string, fn func(c *contentNode)) error { + return m.collectSectionsFn(prefix, func(s string, c *contentNode) bool { + fn(c) + return false + }) +} + +func (m *pageMap) collectTaxonomies(prefix string, fn func(c *contentNode)) error { + m.taxonomies.WalkPrefixListable(prefix, func(s string, n *contentNode) bool { + fn(n) + return false + }) + return nil +} + +// withEveryBundlePage applies fn to every Page, including those bundled inside +// leaf bundles. +func (m *pageMap) withEveryBundlePage(fn func(p *pageState) bool) { + m.bundleTrees.Walk(func(s string, n *contentNode) bool { + if n.p != nil { + return fn(n.p) + } + return false + }) +} + +type pageMaps struct { + workers *para.Workers + pmaps []*pageMap +} + +// deleteSection deletes the entire section from s. +func (m *pageMaps) deleteSection(s string) { + m.withMaps(func(pm *pageMap) error { + pm.deleteSectionByPath(s) + return nil + }) +} + +func (m *pageMaps) AssemblePages() error { + return m.withMaps(func(pm *pageMap) error { + if err := pm.CreateMissingNodes(); err != nil { + return err + } + + if err := pm.assemblePages(); err != nil { + return err + } + + if err := pm.createMissingTaxonomyNodes(); err != nil { + return err + } + + // Handle any new sections created in the step above. + if err := pm.assembleSections(); err != nil { + return err + } + + if err := pm.assembleTaxonomies(); err != nil { + return err + } + + if err := pm.createSiteTaxonomies(); err != nil { + return err + } + + a := (§ionWalker{m: pm.contentMap}).applyAggregates() + _, mainSectionsSet := pm.s.s.Info.Params()["mainsections"] + if !mainSectionsSet && a.mainSection != "" { + mainSections := []string{a.mainSection} + pm.s.s.Info.Params()["mainSections"] = mainSections + pm.s.s.Info.Params()["mainsections"] = mainSections + } + + pm.s.lastmod = a.datesAll.Lastmod() + if resource.IsZeroDates(pm.s.home) { + pm.s.home.m.Dates = a.datesAll + } + + return nil + }) +} + +func (m *pageMaps) walkBundles(fn func(n *contentNode) bool) { + _ = m.withMaps(func(pm *pageMap) error { + pm.bundleTrees.Walk(func(s string, n *contentNode) bool { + return fn(n) + }) + return nil + }) +} + +func (m *pageMaps) walkBranchesPrefix(prefix string, fn func(s string, n *contentNode) bool) { + _ = m.withMaps(func(pm *pageMap) error { + pm.branchTrees.WalkPrefix(prefix, func(s string, n *contentNode) bool { + return fn(s, n) + }) + return nil + }) +} + +func (m *pageMaps) withMaps(fn func(pm *pageMap) error) error { + g, _ := m.workers.Start(context.Background()) + for _, pm := range m.pmaps { + pm := pm + g.Run(func() error { + return fn(pm) + }) + } + return g.Wait() +} + +type pagesMapBucket struct { + // Cascading front matter. + cascade maps.Params + + owner *pageState // The branch node + + pagesInit sync.Once + pages page.Pages + + pagesAndSectionsInit sync.Once + pagesAndSections page.Pages + + sectionsInit sync.Once + sections page.Pages +} + +func (b *pagesMapBucket) getPages() page.Pages { + b.pagesInit.Do(func() { + b.pages = b.owner.treeRef.collectPages() + page.SortByDefault(b.pages) + }) + return b.pages +} + +func (b *pagesMapBucket) getPagesAndSections() page.Pages { + b.pagesAndSectionsInit.Do(func() { + b.pagesAndSections = b.owner.treeRef.collectPagesAndSections() + }) + return b.pagesAndSections +} + +func (b *pagesMapBucket) getSections() page.Pages { + b.sectionsInit.Do(func() { + b.sections = b.owner.treeRef.collectSections() + }) + + return b.sections +} + +func (b *pagesMapBucket) getTaxonomies() page.Pages { + b.sectionsInit.Do(func() { + var pas page.Pages + ref := b.owner.treeRef + ref.m.collectTaxonomies(ref.key+"/", func(c *contentNode) { + pas = append(pas, c.p) + }) + page.SortByDefault(pas) + b.sections = pas + }) + + return b.sections +} + +type sectionAggregate struct { + datesAll resource.Dates + datesSection resource.Dates + pageCount int + mainSection string + mainSectionPageCount int +} + +type sectionAggregateHandler struct { + sectionAggregate + sectionPageCount int + + // Section + b *contentNode + s string +} + +func (h *sectionAggregateHandler) isRootSection() bool { + return h.s != "/" && strings.Count(h.s, "/") == 1 +} + +func (h *sectionAggregateHandler) handleNested(v sectionWalkHandler) error { + nested := v.(*sectionAggregateHandler) + h.sectionPageCount += nested.pageCount + h.pageCount += h.sectionPageCount + h.datesAll.UpdateDateAndLastmodIfAfter(nested.datesAll) + h.datesSection.UpdateDateAndLastmodIfAfter(nested.datesAll) + return nil +} + +func (h *sectionAggregateHandler) handlePage(s string, n *contentNode) error { + h.sectionPageCount++ + + var d resource.Dated + if n.p != nil { + d = n.p + } else if n.viewInfo != nil && n.viewInfo.ref != nil { + d = n.viewInfo.ref.p + } else { + return nil + } + + h.datesAll.UpdateDateAndLastmodIfAfter(d) + h.datesSection.UpdateDateAndLastmodIfAfter(d) + return nil +} + +func (h *sectionAggregateHandler) handleSectionPost() error { + if h.sectionPageCount > h.mainSectionPageCount && h.isRootSection() { + h.mainSectionPageCount = h.sectionPageCount + h.mainSection = strings.TrimPrefix(h.s, "/") + } + + if resource.IsZeroDates(h.b.p) { + h.b.p.m.Dates = h.datesSection + } + + h.datesSection = resource.Dates{} + + return nil +} + +func (h *sectionAggregateHandler) handleSectionPre(s string, b *contentNode) error { + h.s = s + h.b = b + h.sectionPageCount = 0 + h.datesAll.UpdateDateAndLastmodIfAfter(b.p) + return nil +} + +type sectionWalkHandler interface { + handleNested(v sectionWalkHandler) error + handlePage(s string, b *contentNode) error + handleSectionPost() error + handleSectionPre(s string, b *contentNode) error +} + +type sectionWalker struct { + err error + m *contentMap +} + +func (w *sectionWalker) applyAggregates() *sectionAggregateHandler { + return w.walkLevel("/", func() sectionWalkHandler { + return §ionAggregateHandler{} + }).(*sectionAggregateHandler) + +} + +func (w *sectionWalker) walkLevel(prefix string, createVisitor func() sectionWalkHandler) sectionWalkHandler { + + level := strings.Count(prefix, "/") + visitor := createVisitor() + + w.m.taxonomies.WalkPrefix(prefix, func(s string, v interface{}) bool { + currentLevel := strings.Count(s, "/") + if currentLevel > level { + return false + } + + n := v.(*contentNode) + + if w.err = visitor.handleSectionPre(s, n); w.err != nil { + return true + } + + if currentLevel == 1 { + nested := w.walkLevel(s+"/", createVisitor) + if w.err = visitor.handleNested(nested); w.err != nil { + return true + } + } else { + w.m.taxonomyEntries.WalkPrefix(s, func(ss string, v interface{}) bool { + n := v.(*contentNode) + w.err = visitor.handlePage(ss, n) + return w.err != nil + }) + } + + w.err = visitor.handleSectionPost() + + return w.err != nil + }) + + w.m.sections.WalkPrefix(prefix, func(s string, v interface{}) bool { + currentLevel := strings.Count(s, "/") + if currentLevel > level { + return false + } + + n := v.(*contentNode) + + if w.err = visitor.handleSectionPre(s, n); w.err != nil { + return true + } + + w.m.pages.WalkPrefix(s+cmBranchSeparator, func(s string, v interface{}) bool { + w.err = visitor.handlePage(s, v.(*contentNode)) + return w.err != nil + }) + + if w.err != nil { + return true + } + + if s != "/" { + nested := w.walkLevel(s+"/", createVisitor) + if w.err = visitor.handleNested(nested); w.err != nil { + return true + } + } + + w.err = visitor.handleSectionPost() + + return w.err != nil + }) + + return visitor + +} + +type viewName struct { + singular string // e.g. "category" + plural string // e.g. "categories" +} + +func (v viewName) IsZero() bool { + return v.singular == "" +} diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go new file mode 100644 index 000000000..7a2829478 --- /dev/null +++ b/hugolib/content_map_test.go @@ -0,0 +1,455 @@ +// Copyright 2019 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 hugolib + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/helpers" + + "github.com/gohugoio/hugo/htesting/hqt" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/afero" + + qt "github.com/frankban/quicktest" +) + +func BenchmarkContentMap(b *testing.B) { + writeFile := func(c *qt.C, fs afero.Fs, filename, content string) hugofs.FileMetaInfo { + c.Helper() + filename = filepath.FromSlash(filename) + c.Assert(fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil) + c.Assert(afero.WriteFile(fs, filename, []byte(content), 0777), qt.IsNil) + + fi, err := fs.Stat(filename) + c.Assert(err, qt.IsNil) + + mfi := fi.(hugofs.FileMetaInfo) + return mfi + + } + + createFs := func(fs afero.Fs, lang string) afero.Fs { + return hugofs.NewBaseFileDecorator(fs, + func(fi hugofs.FileMetaInfo) { + meta := fi.Meta() + // We have a more elaborate filesystem setup in the + // real flow, so simulate this here. + meta["lang"] = lang + meta["path"] = meta.Filename() + meta["classifier"] = files.ClassifyContentFile(fi.Name()) + + }) + } + + b.Run("CreateMissingNodes", func(b *testing.B) { + c := qt.New(b) + b.StopTimer() + mps := make([]*contentMap, b.N) + for i := 0; i < b.N; i++ { + m := newContentMap(contentMapConfig{lang: "en"}) + mps[i] = m + memfs := afero.NewMemMapFs() + fs := createFs(memfs, "en") + for i := 1; i <= 20; i++ { + c.Assert(m.AddFilesBundle(writeFile(c, fs, fmt.Sprintf("sect%d/a/index.md", i), "page")), qt.IsNil) + c.Assert(m.AddFilesBundle(writeFile(c, fs, fmt.Sprintf("sect2%d/%sindex.md", i, strings.Repeat("b/", i)), "page")), qt.IsNil) + } + + } + + b.StartTimer() + + for i := 0; i < b.N; i++ { + m := mps[i] + c.Assert(m.CreateMissingNodes(), qt.IsNil) + + b.StopTimer() + m.pages.DeletePrefix("/") + m.sections.DeletePrefix("/") + b.StartTimer() + } + }) + +} + +func TestContentMap(t *testing.T) { + c := qt.New(t) + + writeFile := func(c *qt.C, fs afero.Fs, filename, content string) hugofs.FileMetaInfo { + c.Helper() + filename = filepath.FromSlash(filename) + c.Assert(fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil) + c.Assert(afero.WriteFile(fs, filename, []byte(content), 0777), qt.IsNil) + + fi, err := fs.Stat(filename) + c.Assert(err, qt.IsNil) + + mfi := fi.(hugofs.FileMetaInfo) + return mfi + + } + + createFs := func(fs afero.Fs, lang string) afero.Fs { + return hugofs.NewBaseFileDecorator(fs, + func(fi hugofs.FileMetaInfo) { + meta := fi.Meta() + // We have a more elaborate filesystem setup in the + // real flow, so simulate this here. + meta["lang"] = lang + meta["path"] = meta.Filename() + meta["classifier"] = files.ClassifyContentFile(fi.Name()) + meta["translationBaseName"] = helpers.Filename(fi.Name()) + + }) + } + + c.Run("AddFiles", func(c *qt.C) { + + memfs := afero.NewMemMapFs() + + fsl := func(lang string) afero.Fs { + return createFs(memfs, lang) + } + + fs := fsl("en") + + header := writeFile(c, fs, "blog/a/index.md", "page") + + c.Assert(header.Meta().Lang(), qt.Equals, "en") + + resources := []hugofs.FileMetaInfo{ + writeFile(c, fs, "blog/a/b/data.json", "data"), + writeFile(c, fs, "blog/a/logo.png", "image"), + } + + m := newContentMap(contentMapConfig{lang: "en"}) + + c.Assert(m.AddFilesBundle(header, resources...), qt.IsNil) + + c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/b/c/index.md", "page")), qt.IsNil) + + c.Assert(m.AddFilesBundle( + writeFile(c, fs, "blog/_index.md", "section page"), + writeFile(c, fs, "blog/sectiondata.json", "section resource"), + ), qt.IsNil) + + got := m.testDump() + + expect := ` + Tree 0: + /blog__hb_/a__hl_ + /blog__hb_/b/c__hl_ + Tree 1: + /blog + Tree 2: + /blog__hb_/a__hl_b/data.json + /blog__hb_/a__hl_logo.png + /blog__hl_sectiondata.json + en/pages/blog__hb_/a__hl_|f:blog/a/index.md + - R: blog/a/b/data.json + - R: blog/a/logo.png + en/pages/blog__hb_/b/c__hl_|f:blog/b/c/index.md + en/sections/blog|f:blog/_index.md + - P: blog/a/index.md + - P: blog/b/c/index.md + - R: blog/sectiondata.json + +` + + c.Assert(got, hqt.IsSameString, expect, qt.Commentf(got)) + + // Add a data file to the section bundle + c.Assert(m.AddFiles( + writeFile(c, fs, "blog/sectiondata2.json", "section resource"), + ), qt.IsNil) + + // And then one to the leaf bundles + c.Assert(m.AddFiles( + writeFile(c, fs, "blog/a/b/data2.json", "data2"), + ), qt.IsNil) + + c.Assert(m.AddFiles( + writeFile(c, fs, "blog/b/c/d/data3.json", "data3"), + ), qt.IsNil) + + got = m.testDump() + + expect = ` + Tree 0: + /blog__hb_/a__hl_ + /blog__hb_/b/c__hl_ + Tree 1: + /blog + Tree 2: + /blog__hb_/a__hl_b/data.json + /blog__hb_/a__hl_b/data2.json + /blog__hb_/a__hl_logo.png + /blog__hb_/b/c__hl_d/data3.json + /blog__hl_sectiondata.json + /blog__hl_sectiondata2.json + en/pages/blog__hb_/a__hl_|f:blog/a/index.md + - R: blog/a/b/data.json + - R: blog/a/b/data2.json + - R: blog/a/logo.png + en/pages/blog__hb_/b/c__hl_|f:blog/b/c/index.md + - R: blog/b/c/d/data3.json + en/sections/blog|f:blog/_index.md + - P: blog/a/index.md + - P: blog/b/c/index.md + - R: blog/sectiondata.json + - R: blog/sectiondata2.json + +` + + c.Assert(got, hqt.IsSameString, expect, qt.Commentf(got)) + + // Add a regular page (i.e. not a bundle) + c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/b.md", "page")), qt.IsNil) + + c.Assert(m.testDump(), hqt.IsSameString, ` + Tree 0: + /blog__hb_/a__hl_ + /blog__hb_/b/c__hl_ + /blog__hb_/b__hl_ + Tree 1: + /blog + Tree 2: + /blog__hb_/a__hl_b/data.json + /blog__hb_/a__hl_b/data2.json + /blog__hb_/a__hl_logo.png + /blog__hb_/b/c__hl_d/data3.json + /blog__hl_sectiondata.json + /blog__hl_sectiondata2.json + en/pages/blog__hb_/a__hl_|f:blog/a/index.md + - R: blog/a/b/data.json + - R: blog/a/b/data2.json + - R: blog/a/logo.png + en/pages/blog__hb_/b/c__hl_|f:blog/b/c/index.md + - R: blog/b/c/d/data3.json + en/pages/blog__hb_/b__hl_|f:blog/b.md + en/sections/blog|f:blog/_index.md + - P: blog/a/index.md + - P: blog/b/c/index.md + - P: blog/b.md + - R: blog/sectiondata.json + - R: blog/sectiondata2.json + + + `, qt.Commentf(m.testDump())) + + }) + + c.Run("CreateMissingNodes", func(c *qt.C) { + + memfs := afero.NewMemMapFs() + + fsl := func(lang string) afero.Fs { + return createFs(memfs, lang) + } + + fs := fsl("en") + + m := newContentMap(contentMapConfig{lang: "en"}) + + c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/page.md", "page")), qt.IsNil) + c.Assert(m.AddFilesBundle(writeFile(c, fs, "blog/a/index.md", "page")), qt.IsNil) + c.Assert(m.AddFilesBundle(writeFile(c, fs, "bundle/index.md", "page")), qt.IsNil) + + c.Assert(m.CreateMissingNodes(), qt.IsNil) + + got := m.testDump() + + c.Assert(got, hqt.IsSameString, ` + + Tree 0: + /__hb_/bundle__hl_ + /blog__hb_/a__hl_ + /blog__hb_/page__hl_ + Tree 1: + / + /blog + Tree 2: + en/pages/__hb_/bundle__hl_|f:bundle/index.md + en/pages/blog__hb_/a__hl_|f:blog/a/index.md + en/pages/blog__hb_/page__hl_|f:blog/page.md + en/sections/ + - P: bundle/index.md + en/sections/blog + - P: blog/a/index.md + - P: blog/page.md + + `, qt.Commentf(got)) + + }) + + c.Run("cleanKey", func(c *qt.C) { + for _, test := range []struct { + in string + expected string + }{ + {"/a/b/", "/a/b"}, + {filepath.FromSlash("/a/b/"), "/a/b"}, + {"/a//b/", "/a/b"}, + } { + + c.Assert(cleanTreeKey(test.in), qt.Equals, test.expected) + + } + }) +} + +func TestContentMapSite(t *testing.T) { + + b := newTestSitesBuilder(t) + + pageTempl := ` +--- +title: "Page %d" +date: "2019-06-0%d" +lastMod: "2019-06-0%d" +categories: ["funny"] +--- + +Page content. +` + createPage := func(i int) string { + return fmt.Sprintf(pageTempl, i, i, i+1) + } + + draftTemplate := `--- +title: "Draft" +draft: true +--- + +` + + b.WithContent("_index.md", ` +--- +title: "Hugo Home" +cascade: + description: "Common Description" + +--- + +Home Content. +`) + + b.WithContent("blog/page1.md", createPage(1)) + b.WithContent("blog/page2.md", createPage(2)) + b.WithContent("blog/page3.md", createPage(3)) + b.WithContent("blog/bundle/index.md", createPage(12)) + b.WithContent("blog/bundle/data.json", "data") + b.WithContent("blog/bundle/page.md", createPage(99)) + b.WithContent("blog/subsection/_index.md", createPage(3)) + b.WithContent("blog/subsection/subdata.json", "data") + b.WithContent("blog/subsection/page4.md", createPage(8)) + b.WithContent("blog/subsection/page5.md", createPage(10)) + b.WithContent("blog/subsection/draft/index.md", draftTemplate) + b.WithContent("blog/subsection/draft/data.json", "data") + b.WithContent("blog/draftsection/_index.md", draftTemplate) + b.WithContent("blog/draftsection/page/index.md", createPage(12)) + b.WithContent("blog/draftsection/page/folder/data.json", "data") + b.WithContent("blog/draftsection/sub/_index.md", createPage(12)) + b.WithContent("blog/draftsection/sub/page.md", createPage(13)) + b.WithContent("docs/page6.md", createPage(11)) + b.WithContent("tags/_index.md", createPage(32)) + + b.WithTemplatesAdded("layouts/index.html", ` +Num Regular: {{ len .Site.RegularPages }} +Main Sections: {{ .Site.Params.mainSections }} +Pag Num Pages: {{ len .Paginator.Pages }} +{{ $home := .Site.Home }} +{{ $blog := .Site.GetPage "blog" }} +{{ $categories := .Site.GetPage "categories" }} +{{ $funny := .Site.GetPage "categories/funny" }} +{{ $blogSub := .Site.GetPage "blog/subsection" }} +{{ $page := .Site.GetPage "blog/page1" }} +{{ $page2 := .Site.GetPage "blog/page2" }} +{{ $page4 := .Site.GetPage "blog/subsection/page4" }} +{{ $bundle := .Site.GetPage "blog/bundle" }} +Home: {{ template "print-page" $home }} +Blog Section: {{ template "print-page" $blog }} +Blog Sub Section: {{ template "print-page" $blogSub }} +Page: {{ template "print-page" $page }} +Bundle: {{ template "print-page" $bundle }} +IsDescendant: true: {{ $page.IsDescendant $blog }} true: {{ $blogSub.IsDescendant $blog }} true: {{ $blog.IsDescendant $home }} false: {{ $home.IsDescendant $blog }} +IsAncestor: true: {{ $blog.IsAncestor $page }} true: {{ $home.IsAncestor $blog }} true: {{ $blog.IsAncestor $blogSub }} true: {{ $home.IsAncestor $page }} false: {{ $page.IsAncestor $blog }} false: {{ $blog.IsAncestor $home }} false: {{ $blogSub.IsAncestor $blog }} +FirstSection: {{ $blogSub.FirstSection.RelPermalink }} {{ $blog.FirstSection.RelPermalink }} {{ $home.FirstSection.RelPermalink }} {{ $page.FirstSection.RelPermalink }} +InSection: true: {{ $page.InSection $blog }} false: {{ $page.InSection $blogSub }} +Next: {{ $page2.Next.RelPermalink }} +NextInSection: {{ $page2.NextInSection.RelPermalink }} +Pages: {{ range $blog.Pages }}{{ .RelPermalink }}|{{ end }} +Sections: {{ range $home.Sections }}{{ .RelPermalink }}|{{ end }} +Categories: {{ range .Site.Taxonomies.categories }}{{ .Page.RelPermalink }}; {{ .Page.Title }}; {{ .Count }}|{{ end }} +Category Terms: {{ $categories.Kind}}: {{ range $categories.Data.Terms.Alphabetical }}{{ .Page.RelPermalink }}; {{ .Page.Title }}; {{ .Count }}|{{ end }} +Category Funny: {{ $funny.Kind}}; {{ $funny.Data.Term }}: {{ range $funny.Pages }}{{ .RelPermalink }};|{{ end }} +Pag Num Pages: {{ len .Paginator.Pages }} +Pag Blog Num Pages: {{ len $blog.Paginator.Pages }} +Blog Num RegularPages: {{ len $blog.RegularPages }} +Blog Num Pages: {{ len $blog.Pages }} + +Draft1: {{ if (.Site.GetPage "blog/subsection/draft") }}FOUND{{ end }}| +Draft2: {{ if (.Site.GetPage "blog/draftsection") }}FOUND{{ end }}| +Draft3: {{ if (.Site.GetPage "blog/draftsection/page") }}FOUND{{ end }}| +Draft4: {{ if (.Site.GetPage "blog/draftsection/sub") }}FOUND{{ end }}| +Draft5: {{ if (.Site.GetPage "blog/draftsection/sub/page") }}FOUND{{ end }}| + +{{ define "print-page" }}{{ .Title }}|{{ .RelPermalink }}|{{ .Date.Format "2006-01-02" }}|Current Section: {{ .CurrentSection.SectionsPath }}|Resources: {{ range .Resources }}{{ .ResourceType }}: {{ .RelPermalink }}|{{ end }}{{ end }} +`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", + + ` + Num Regular: 7 + Main Sections: [blog] + Pag Num Pages: 7 + + Home: Hugo Home|/|2019-06-08|Current Section: |Resources: + Blog Section: Blogs|/blog/|2019-06-08|Current Section: blog|Resources: + Blog Sub Section: Page 3|/blog/subsection/|2019-06-03|Current Section: blog/subsection|Resources: json: /blog/subsection/subdata.json| + Page: Page 1|/blog/page1/|2019-06-01|Current Section: blog|Resources: + Bundle: Page 12|/blog/bundle/|0001-01-01|Current Section: blog|Resources: json: /blog/bundle/data.json|page: | + IsDescendant: true: true true: true true: true false: false + IsAncestor: true: true true: true true: true true: true false: false false: false false: false + FirstSection: /blog/ /blog/ / /blog/ + InSection: true: true false: false + Next: /blog/page3/ + NextInSection: /blog/page3/ + Pages: /blog/page3/|/blog/subsection/|/blog/page2/|/blog/page1/|/blog/bundle/| + Sections: /blog/|/docs/| + Categories: /categories/funny/; funny; 9| + Category Terms: taxonomyTerm: /categories/funny/; funny; 9| + Category Funny: taxonomy; funny: /blog/subsection/page4/;|/blog/page3/;|/blog/subsection/;|/blog/page2/;|/blog/page1/;|/blog/subsection/page5/;|/docs/page6/;|/blog/bundle/;|;| + Pag Num Pages: 7 + Pag Blog Num Pages: 4 + Blog Num RegularPages: 4 + Blog Num Pages: 5 + + Draft1: | + Draft2: | + Draft3: | + Draft4: | + Draft5: | + +`) +} diff --git a/hugolib/disableKinds_test.go b/hugolib/disableKinds_test.go index b26385cc3..f5b2c65a1 100644 --- a/hugolib/disableKinds_test.go +++ b/hugolib/disableKinds_test.go @@ -13,188 +13,256 @@ package hugolib import ( - "strings" "testing" "fmt" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/hugo/helpers" ) -func TestDisableKindsNoneDisabled(t *testing.T) { - t.Parallel() - doTestDisableKinds(t) -} - -func TestDisableKindsSomeDisabled(t *testing.T) { - t.Parallel() - doTestDisableKinds(t, page.KindSection, kind404) -} +func TestDisable(t *testing.T) { + c := qt.New(t) -func TestDisableKindsOneDisabled(t *testing.T) { - t.Parallel() - for _, kind := range allKinds { - if kind == page.KindPage { - // Turning off regular page generation have some side-effects - // not handled by the assertions below (no sections), so - // skip that for now. - continue - } - doTestDisableKinds(t, kind) - } -} - -func TestDisableKindsAllDisabled(t *testing.T) { - t.Parallel() - doTestDisableKinds(t, allKinds...) -} - -func doTestDisableKinds(t *testing.T, disabled ...string) { - siteConfigTemplate := ` + newSitesBuilder := func(c *qt.C, disableKind string) *sitesBuilder { + config := fmt.Sprintf(` baseURL = "http://example.com/blog" enableRobotsTXT = true -disableKinds = %s - -paginate = 1 -defaultContentLanguage = "en" - -[Taxonomies] -tag = "tags" -category = "categories" -` - - pageTemplate := `--- -title: "%s" -tags: -%s -categories: -- Hugo ---- -# Doc -` +disableKinds = [%q] +`, disableKind) - disabledStr := "[]" - - if len(disabled) > 0 { - disabledStr = strings.Replace(fmt.Sprintf("%#v", disabled), "[]string{", "[", -1) - disabledStr = strings.Replace(disabledStr, "}", "]", -1) - } + b := newTestSitesBuilder(c) + b.WithConfigFile("toml", config).WithContent("sect/page.md", ` +--- +title: Page +categories: ["mycat"] +tags: ["mytag"] +--- - siteConfig := fmt.Sprintf(siteConfigTemplate, disabledStr) +`, "sect/no-list.md", ` +--- +title: No List +_build: + list: false +--- - b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) +`, "sect/no-render.md", ` +--- +title: No List +_build: + render: false +--- +`, "sect/no-publishresources/index.md", ` +--- +title: No Publish Resources +_build: + publishResources: false +--- - b.WithTemplates( - "index.html", "Home|{{ .Title }}|{{ .Content }}", - "_default/single.html", "Single|{{ .Title }}|{{ .Content }}", - "_default/list.html", "List|{{ .Title }}|{{ .Content }}", - "_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}", - "layouts/404.html", "Page Not Found", - ) +`, "sect/headlessbundle/index.md", ` +--- +title: Headless +headless: true +--- - b.WithContent( - "sect/p1.md", fmt.Sprintf(pageTemplate, "P1", "- tag1"), - "categories/_index.md", newTestPage("Category Terms", "2017-01-01", 10), - "tags/tag1/_index.md", newTestPage("Tag1 List", "2017-01-01", 10), - ) +`) + b.WithSourceFile("content/sect/headlessbundle/data.json", "DATA") + b.WithSourceFile("content/sect/no-publishresources/data.json", "DATA") - b.Build(BuildCfg{}) - h := b.H + return b - assertDisabledKinds(b, h.Sites[0], disabled...) + } -} + getPage := func(b *sitesBuilder, ref string) page.Page { + b.Helper() + p, err := b.H.Sites[0].getPageNew(nil, ref) + b.Assert(err, qt.IsNil) + return p + } -func assertDisabledKinds(b *sitesBuilder, s *Site, disabled ...string) { - assertDisabledKind(b, - func(isDisabled bool) bool { - if isDisabled { - return len(s.RegularPages()) == 0 - } - return len(s.RegularPages()) > 0 - }, disabled, page.KindPage, "public/sect/p1/index.html", "Single|P1") - assertDisabledKind(b, - func(isDisabled bool) bool { - p := s.getPage(page.KindHome) - if isDisabled { - return p == nil - } - return p != nil - }, disabled, page.KindHome, "public/index.html", "Home") - assertDisabledKind(b, - func(isDisabled bool) bool { - p := s.getPage(page.KindSection, "sect") - if isDisabled { - return p == nil - } - return p != nil - }, disabled, page.KindSection, "public/sect/index.html", "Sects") - assertDisabledKind(b, - func(isDisabled bool) bool { - p := s.getPage(page.KindTaxonomy, "tags", "tag1") - - if isDisabled { - return p == nil - } - return p != nil - - }, disabled, page.KindTaxonomy, "public/tags/tag1/index.html", "Tag1") - assertDisabledKind(b, - func(isDisabled bool) bool { - p := s.getPage(page.KindTaxonomyTerm, "tags") - if isDisabled { - return p == nil + getPageInSitePages := func(b *sitesBuilder, ref string) page.Page { + b.Helper() + for _, pages := range []page.Pages{b.H.Sites[0].Pages(), b.H.Sites[0].RegularPages()} { + for _, p := range pages { + if ref == p.(*pageState).sourceRef() { + return p + } } - return p != nil - - }, disabled, page.KindTaxonomyTerm, "public/tags/index.html", "Tags") - assertDisabledKind(b, - func(isDisabled bool) bool { - p := s.getPage(page.KindTaxonomyTerm, "categories") + } + return nil + } - if isDisabled { - return p == nil + getPageInPagePages := func(p page.Page, ref string) page.Page { + for _, pages := range []page.Pages{p.Pages(), p.RegularPages(), p.Sections()} { + for _, p := range pages { + if ref == p.(*pageState).sourceRef() { + return p + } } - return p != nil - - }, disabled, page.KindTaxonomyTerm, "public/categories/index.html", "Category Terms") - assertDisabledKind(b, - func(isDisabled bool) bool { - p := s.getPage(page.KindTaxonomy, "categories", "hugo") - if isDisabled { - return p == nil - } - return p != nil - - }, disabled, page.KindTaxonomy, "public/categories/hugo/index.html", "Hugo") - // The below have no page in any collection. - assertDisabledKind(b, func(isDisabled bool) bool { return true }, disabled, kindRSS, "public/index.xml", "<link>") - assertDisabledKind(b, func(isDisabled bool) bool { return true }, disabled, kindSitemap, "public/sitemap.xml", "sitemap") - assertDisabledKind(b, func(isDisabled bool) bool { return true }, disabled, kindRobotsTXT, "public/robots.txt", "User-agent") - assertDisabledKind(b, func(isDisabled bool) bool { return true }, disabled, kind404, "public/404.html", "Page Not Found") -} - -func assertDisabledKind(b *sitesBuilder, kindAssert func(bool) bool, disabled []string, kind, path, matcher string) { - isDisabled := stringSliceContains(kind, disabled...) - b.Assert(kindAssert(isDisabled), qt.Equals, true) - - if kind == kindRSS && !isDisabled { - // If the home page is also disabled, there is not RSS to look for. - if stringSliceContains(page.KindHome, disabled...) { - isDisabled = true } + return nil } - if isDisabled { - // Path should not exist - fileExists, err := helpers.Exists(path, b.Fs.Destination) - b.Assert(err, qt.IsNil) - b.Assert(fileExists, qt.Equals, false) - - } else { - b.AssertFileContent(path, matcher) - } + disableKind := page.KindPage + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + s := b.H.Sites[0] + b.Assert(getPage(b, "/sect/page.md"), qt.IsNil) + b.Assert(b.CheckExists("public/sect/page/index.html"), qt.Equals, false) + b.Assert(getPageInSitePages(b, "/sect/page.md"), qt.IsNil) + b.Assert(getPageInPagePages(getPage(b, "/"), "/sect/page.md"), qt.IsNil) + + // Also check the side effects + b.Assert(b.CheckExists("public/categories/mycat/index.html"), qt.Equals, false) + b.Assert(len(s.Taxonomies()["categories"]), qt.Equals, 0) + }) + + disableKind = page.KindTaxonomy + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + s := b.H.Sites[0] + b.Assert(b.CheckExists("public/categories/index.html"), qt.Equals, true) + b.Assert(b.CheckExists("public/categories/mycat/index.html"), qt.Equals, false) + b.Assert(len(s.Taxonomies()["categories"]), qt.Equals, 0) + b.Assert(getPage(b, "/categories"), qt.Not(qt.IsNil)) + b.Assert(getPage(b, "/categories/mycat"), qt.IsNil) + }) + + disableKind = page.KindTaxonomyTerm + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + s := b.H.Sites[0] + b.Assert(b.CheckExists("public/categories/mycat/index.html"), qt.Equals, true) + b.Assert(b.CheckExists("public/categories/index.html"), qt.Equals, false) + b.Assert(len(s.Taxonomies()["categories"]), qt.Equals, 1) + b.Assert(getPage(b, "/categories/mycat"), qt.Not(qt.IsNil)) + categories := getPage(b, "/categories") + b.Assert(categories, qt.Not(qt.IsNil)) + b.Assert(categories.RelPermalink(), qt.Equals, "") + b.Assert(getPageInSitePages(b, "/categories"), qt.IsNil) + b.Assert(getPageInPagePages(getPage(b, "/"), "/categories"), qt.IsNil) + }) + + disableKind = page.KindHome + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/index.html"), qt.Equals, false) + home := getPage(b, "/") + b.Assert(home, qt.Not(qt.IsNil)) + b.Assert(home.RelPermalink(), qt.Equals, "") + b.Assert(getPageInSitePages(b, "/"), qt.IsNil) + b.Assert(getPageInPagePages(home, "/"), qt.IsNil) + b.Assert(getPage(b, "/sect/page.md"), qt.Not(qt.IsNil)) + }) + + disableKind = page.KindSection + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/sect/index.html"), qt.Equals, false) + sect := getPage(b, "/sect") + b.Assert(sect, qt.Not(qt.IsNil)) + b.Assert(sect.RelPermalink(), qt.Equals, "") + b.Assert(getPageInSitePages(b, "/sect"), qt.IsNil) + home := getPage(b, "/") + b.Assert(getPageInPagePages(home, "/sect"), qt.IsNil) + b.Assert(home.OutputFormats(), qt.HasLen, 2) + page := getPage(b, "/sect/page.md") + b.Assert(page, qt.Not(qt.IsNil)) + b.Assert(page.CurrentSection(), qt.Equals, sect) + b.Assert(getPageInPagePages(sect, "/sect/page.md"), qt.Not(qt.IsNil)) + b.AssertFileContent("public/sitemap.xml", "sitemap") + b.AssertFileContent("public/index.xml", "rss") + + }) + + disableKind = kindRSS + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/index.xml"), qt.Equals, false) + home := getPage(b, "/") + b.Assert(home.OutputFormats(), qt.HasLen, 1) + }) + + disableKind = kindSitemap + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/sitemap.xml"), qt.Equals, false) + }) + + disableKind = kind404 + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/404.html"), qt.Equals, false) + }) + + disableKind = kindRobotsTXT + c.Run("Disable "+disableKind, func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.WithTemplatesAdded("robots.txt", "myrobots") + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/robots.txt"), qt.Equals, false) + }) + + c.Run("Headless bundle", func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/sect/headlessbundle/index.html"), qt.Equals, false) + b.Assert(b.CheckExists("public/sect/headlessbundle/data.json"), qt.Equals, true) + bundle := getPage(b, "/sect/headlessbundle/index.md") + b.Assert(bundle, qt.Not(qt.IsNil)) + b.Assert(bundle.RelPermalink(), qt.Equals, "") + resource := bundle.Resources()[0] + b.Assert(resource.RelPermalink(), qt.Equals, "/blog/sect/headlessbundle/data.json") + b.Assert(bundle.OutputFormats(), qt.HasLen, 0) + b.Assert(bundle.AlternativeOutputFormats(), qt.HasLen, 0) + }) + + c.Run("Build config, no list", func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + ref := "/sect/no-list.md" + b.Assert(b.CheckExists("public/sect/no-list/index.html"), qt.Equals, true) + p := getPage(b, ref) + b.Assert(p, qt.Not(qt.IsNil)) + b.Assert(p.RelPermalink(), qt.Equals, "/blog/sect/no-list/") + b.Assert(getPageInSitePages(b, ref), qt.IsNil) + sect := getPage(b, "/sect") + b.Assert(getPageInPagePages(sect, ref), qt.IsNil) + + }) + + c.Run("Build config, no render", func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + ref := "/sect/no-render.md" + b.Assert(b.CheckExists("public/sect/no-render/index.html"), qt.Equals, false) + p := getPage(b, ref) + b.Assert(p, qt.Not(qt.IsNil)) + b.Assert(p.RelPermalink(), qt.Equals, "") + b.Assert(p.OutputFormats(), qt.HasLen, 0) + b.Assert(getPageInSitePages(b, ref), qt.Not(qt.IsNil)) + sect := getPage(b, "/sect") + b.Assert(getPageInPagePages(sect, ref), qt.Not(qt.IsNil)) + }) + + c.Run("Build config, no publish resources", func(c *qt.C) { + b := newSitesBuilder(c, disableKind) + b.Build(BuildCfg{}) + b.Assert(b.CheckExists("public/sect/no-publishresources/index.html"), qt.Equals, true) + b.Assert(b.CheckExists("public/sect/no-publishresources/data.json"), qt.Equals, false) + bundle := getPage(b, "/sect/no-publishresources/index.md") + b.Assert(bundle, qt.Not(qt.IsNil)) + b.Assert(bundle.RelPermalink(), qt.Equals, "/blog/sect/no-publishresources/") + b.Assert(bundle.Resources(), qt.HasLen, 1) + resource := bundle.Resources()[0] + b.Assert(resource.RelPermalink(), qt.Equals, "/blog/sect/no-publishresources/data.json") + }) } diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index 34770520f..47d6d11f5 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -556,6 +556,7 @@ func (b *sourceFilesystemsBuilder) createModFs( From: mount.Target, To: filename, ToBasedir: base, + Module: md.Module.Path(), Meta: hugofs.FileMeta{ "watch": md.Watch(), "mountWeight": mountWeight, diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index 65d232208..25217363c 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -73,6 +73,11 @@ module github.com/gohugoio/tests/testHugoModules `) + b.WithSourceFile("go.sum", ` +github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877 h1:WLM2bQCKIWo04T6NsIWsX/Vtirhf0TnpY66xyqGlgVY= +github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877/go.mod h1:CBFZS3khIAXKxReMwq0le8sEl/D8hcXmixlOHVv+Gd0= +`) + b.Build(BuildCfg{}) b.AssertFileContent("public/p1/index.html", `<p>Page|https://bep.is|Title: |Text: A link|END</p>`) diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 50694fbba..04a231fdf 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "io" "path/filepath" "sort" @@ -28,8 +29,8 @@ import ( "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/parser/metadecoders" + "github.com/gohugoio/hugo/common/para" "github.com/gohugoio/hugo/hugofs" - "github.com/pkg/errors" "github.com/gohugoio/hugo/source" @@ -77,11 +78,16 @@ type HugoSites struct { // As loaded from the /data dirs data map[string]interface{} + content *pageMaps + // Keeps track of bundle directories and symlinks to enable partial rebuilding. ContentChanges *contentChangeMap init *hugoSitesInit + workers *para.Workers + numWorkers int + *fatalErrorHandler *testCounters } @@ -175,7 +181,7 @@ func (h *HugoSites) gitInfoForPage(p page.Page) (*gitmap.GitInfo, error) { func (h *HugoSites) siteInfos() page.Sites { infos := make(page.Sites, len(h.Sites)) for i, site := range h.Sites { - infos[i] = &site.Info + infos[i] = site.Info } return infos } @@ -245,25 +251,22 @@ func (h *HugoSites) PrintProcessingStats(w io.Writer) { // GetContentPage finds a Page with content given the absolute filename. // Returns nil if none found. func (h *HugoSites) GetContentPage(filename string) page.Page { - for _, s := range h.Sites { - pos := s.rawAllPages.findPagePosByFilename(filename) - if pos == -1 { - continue - } - return s.rawAllPages[pos] - } + var p page.Page - // If not found already, this may be bundled in another content file. - dir := filepath.Dir(filename) + h.content.walkBundles(func(b *contentNode) bool { + if b.p == nil || b.fi == nil { + return false + } - for _, s := range h.Sites { - pos := s.rawAllPages.findPagePosByFilnamePrefix(dir) - if pos == -1 { - continue + if b.fi.Meta().Filename() == filename { + p = b.p + return true } - return s.rawAllPages[pos] - } - return nil + + return false + }) + + return p } // NewHugoSites creates a new collection of sites given the input sites, building @@ -282,11 +285,22 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { var contentChangeTracker *contentChangeMap + numWorkers := config.GetNumWorkerMultiplier() + if numWorkers > len(sites) { + numWorkers = len(sites) + } + var workers *para.Workers + if numWorkers > 1 { + workers = para.New(numWorkers) + } + h := &HugoSites{ running: cfg.Running, multilingual: langConfig, multihost: cfg.Cfg.GetBool("multihost"), Sites: sites, + workers: workers, + numWorkers: numWorkers, init: &hugoSitesInit{ data: lazy.New(), layouts: lazy.New(), @@ -400,13 +414,27 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error { return err } - d.Site = &s.Info + d.Site = s.Info siteConfig, err := loadSiteConfig(s.language) if err != nil { return errors.Wrap(err, "load site config") } s.siteConfigConfig = siteConfig + + pm := &pageMap{ + contentMap: newContentMap(contentMapConfig{ + lang: s.Lang(), + taxonomyConfig: s.siteCfg.taxonomiesConfig.Values(), + taxonomyDisabled: !s.isEnabled(page.KindTaxonomy), + taxonomyTermDisabled: !s.isEnabled(page.KindTaxonomyTerm), + pageDisabled: !s.isEnabled(page.KindPage), + }), + s: s, + } + + s.PageCollections = newPageCollections(pm) + s.siteRefLinker, err = newSiteRefLinker(s.language, s) return err } @@ -525,6 +553,26 @@ func (h *HugoSites) resetLogs() { } } +func (h *HugoSites) withSite(fn func(s *Site) error) error { + if h.workers == nil { + for _, s := range h.Sites { + if err := fn(s); err != nil { + return err + } + } + return nil + } + + g, _ := h.workers.Start(context.Background()) + for _, s := range h.Sites { + s := s + g.Run(func() error { + return fn(s) + }) + } + return g.Wait() +} + func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error { oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages) @@ -567,7 +615,7 @@ func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error { func (h *HugoSites) toSiteInfos() []*SiteInfo { infos := make([]*SiteInfo, len(h.Sites)) for i, s := range h.Sites { - infos[i] = &s.Info + infos[i] = s.Info } return infos } @@ -603,9 +651,6 @@ type BuildCfg struct { // For regular builds, this will allways return true. // TODO(bep) rename/work this. func (cfg *BuildCfg) shouldRender(p *pageState) bool { - if !p.render { - return false - } if p.forceRender { return true } @@ -652,9 +697,21 @@ func (h *HugoSites) renderCrossSitesArtifacts() error { } func (h *HugoSites) removePageByFilename(filename string) { - for _, s := range h.Sites { - s.removePageFilename(filename) - } + h.content.withMaps(func(m *pageMap) error { + m.deleteBundleMatching(func(b *contentNode) bool { + if b.p == nil { + return false + } + + if b.fi == nil { + return false + } + + return b.fi.Meta().Filename() == filename + }) + return nil + }) + } func (h *HugoSites) createPageCollections() error { @@ -683,19 +740,13 @@ func (h *HugoSites) createPageCollections() error { } func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error { - - for _, p := range s.workAllPages { - if err := p.initOutputFormat(isRenderingSite, idx); err != nil { - return err + var err error + s.pageMap.withEveryBundlePage(func(p *pageState) bool { + if err = p.initOutputFormat(isRenderingSite, idx); err != nil { + return true } - } - - for _, p := range s.headlessPages { - if err := p.initOutputFormat(isRenderingSite, idx); err != nil { - return err - } - } - + return false + }) return nil } @@ -837,49 +888,60 @@ func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Page } func (h *HugoSites) resetPageState() { - for _, s := range h.Sites { - for _, p := range s.rawAllPages { - for _, po := range p.pageOutputs { - if po.cp == nil { - continue - } - po.cp.Reset() + h.content.walkBundles(func(n *contentNode) bool { + if n.p == nil { + return false + } + p := n.p + for _, po := range p.pageOutputs { + if po.cp == nil { + continue } + po.cp.Reset() } - } + + return false + }) } func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) { - for _, s := range h.Sites { - PAGES: - for _, p := range s.rawAllPages { - OUTPUTS: - for _, po := range p.pageOutputs { - if po.cp == nil { - continue - } - for id := range idset { - if po.cp.dependencyTracker.Search(id) != nil { - po.cp.Reset() - continue OUTPUTS - } + h.content.walkBundles(func(n *contentNode) bool { + if n.p == nil { + return false + } + p := n.p + OUTPUTS: + for _, po := range p.pageOutputs { + if po.cp == nil { + continue + } + for id := range idset { + if po.cp.dependencyTracker.Search(id) != nil { + po.cp.Reset() + continue OUTPUTS } } + } + + if p.shortcodeState == nil { + return false + } - for _, s := range p.shortcodeState.shortcodes { - for id := range idset { - if idm, ok := s.info.(identity.Manager); ok && idm.Search(id) != nil { - for _, po := range p.pageOutputs { - if po.cp != nil { - po.cp.Reset() - } + for _, s := range p.shortcodeState.shortcodes { + for id := range idset { + if idm, ok := s.info.(identity.Manager); ok && idm.Search(id) != nil { + for _, po := range p.pageOutputs { + if po.cp != nil { + po.cp.Reset() } - continue PAGES } + return false } } } - } + return false + }) + } // Used in partial reloading to determine if the change is in a bundle. diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index cf7b14311..15eca4bb3 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -19,10 +19,7 @@ import ( "fmt" "runtime/trace" - "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/output" - "golang.org/x/sync/errgroup" - "golang.org/x/sync/semaphore" "github.com/pkg/errors" @@ -246,41 +243,7 @@ func (h *HugoSites) assemble(bcfg *BuildCfg) error { return nil } - numWorkers := config.GetNumWorkerMultiplier() - sem := semaphore.NewWeighted(int64(numWorkers)) - g, ctx := errgroup.WithContext(context.Background()) - - for _, s := range h.Sites { - s := s - g.Go(func() error { - err := sem.Acquire(ctx, 1) - if err != nil { - return err - } - defer sem.Release(1) - - if err := s.assemblePagesMap(s); err != nil { - return err - } - - if err := s.pagesMap.assemblePageMeta(); err != nil { - return err - } - - if err := s.pagesMap.assembleTaxonomies(s); err != nil { - return err - } - - if err := s.createWorkAllPages(); err != nil { - return err - } - - return nil - - }) - } - - if err := g.Wait(); err != nil { + if err := h.content.AssemblePages(); err != nil { return err } @@ -301,8 +264,12 @@ func (h *HugoSites) render(config *BuildCfg) error { if !config.PartialReRender { h.renderFormats = output.Formats{} - for _, s := range h.Sites { + h.withSite(func(s *Site) error { s.initRenderFormats() + return nil + }) + + for _, s := range h.Sites { h.renderFormats = append(h.renderFormats, s.renderFormats...) } } diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index d62d6d519..59f228fef 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -9,6 +9,7 @@ import ( "time" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/resources/page" "github.com/fortytw2/leaktest" @@ -276,8 +277,8 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) { c.Assert(len(doc4.Translations()), qt.Equals, 0) // Taxonomies and their URLs - c.Assert(len(enSite.Taxonomies), qt.Equals, 1) - tags := enSite.Taxonomies["tags"] + c.Assert(len(enSite.Taxonomies()), qt.Equals, 1) + tags := enSite.Taxonomies()["tags"] c.Assert(len(tags), qt.Equals, 2) c.Assert(doc1en, qt.Equals, tags["tag1"][0].Page) @@ -357,8 +358,8 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) { b.AssertFileContent("public/fr/sitemap.xml", "http://example.com/blog/fr/sect/doc1/") // Check taxonomies - enTags := enSite.Taxonomies["tags"] - frTags := frSite.Taxonomies["plaques"] + enTags := enSite.Taxonomies()["tags"] + frTags := frSite.Taxonomies()["plaques"] c.Assert(len(enTags), qt.Equals, 2, qt.Commentf("Tags in en: %v", enTags)) c.Assert(len(frTags), qt.Equals, 2, qt.Commentf("Tags in fr: %v", frTags)) c.Assert(enTags["tag1"], qt.Not(qt.IsNil)) @@ -706,7 +707,7 @@ func checkContent(s *sitesBuilder, filename string, matches ...string) { content := readDestination(s.T, s.Fs, filename) for _, match := range matches { if !strings.Contains(content, match) { - s.Fatalf("No match for %q in content for %s\n%q", match, filename, content) + s.Fatalf("No match for\n%q\nin content for %s\n%q\nDiff:\n%s", match, filename, content, htesting.DiffStrings(content, match)) } } } diff --git a/hugolib/page.go b/hugolib/page.go index bb5107747..4ffcb6264 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -25,13 +25,11 @@ import ( "github.com/mitchellh/mapstructure" - "github.com/gohugoio/hugo/tpl" - "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/hugofs/files" @@ -153,7 +151,6 @@ func (p *pageState) getPagesAndSections() page.Pages { return b.getPagesAndSections() } -// TODO(bep) cm add a test func (p *pageState) RegularPages() page.Pages { p.regularPagesInit.Do(func() { var pages page.Pages @@ -189,13 +186,12 @@ func (p *pageState) Pages() page.Pages { case page.KindSection, page.KindHome: pages = p.getPagesAndSections() case page.KindTaxonomy: - termInfo := p.bucket - plural := maps.GetString(termInfo.meta, "plural") - term := maps.GetString(termInfo.meta, "termKey") - taxonomy := p.s.Taxonomies[plural].Get(term) + b := p.treeRef.n + viewInfo := b.viewInfo + taxonomy := p.s.Taxonomies()[viewInfo.name.plural].Get(viewInfo.termKey) pages = taxonomy.Pages() case page.KindTaxonomyTerm: - pages = p.getPagesAndSections() + pages = p.bucket.getTaxonomies() default: pages = p.s.Pages() } @@ -219,38 +215,35 @@ func (p *pageState) RawContent() string { return string(p.source.parsed.Input()[start:]) } -func (p *pageState) Resources() resource.Resources { - p.resourcesInit.Do(func() { - - sort := func() { - sort.SliceStable(p.resources, func(i, j int) bool { - ri, rj := p.resources[i], p.resources[j] - if ri.ResourceType() < rj.ResourceType() { - return true - } - - p1, ok1 := ri.(page.Page) - p2, ok2 := rj.(page.Page) +func (p *pageState) sortResources() { + sort.SliceStable(p.resources, func(i, j int) bool { + ri, rj := p.resources[i], p.resources[j] + if ri.ResourceType() < rj.ResourceType() { + return true + } - if ok1 != ok2 { - return ok2 - } + p1, ok1 := ri.(page.Page) + p2, ok2 := rj.(page.Page) - if ok1 { - return page.DefaultPageSort(p1, p2) - } + if ok1 != ok2 { + return ok2 + } - return ri.RelPermalink() < rj.RelPermalink() - }) + if ok1 { + return page.DefaultPageSort(p1, p2) } - sort() + return ri.RelPermalink() < rj.RelPermalink() + }) +} +func (p *pageState) Resources() resource.Resources { + p.resourcesInit.Do(func() { + p.sortResources() if len(p.m.resourcesMetadata) > 0 { resources.AssignMetadata(p.m.resourcesMetadata, p.resources...) - sort() + p.sortResources() } - }) return p.resources } @@ -264,7 +257,7 @@ func (p *pageState) HasShortcode(name string) bool { } func (p *pageState) Site() page.Site { - return &p.s.Info + return p.s.Info } func (p *pageState) String() string { @@ -324,7 +317,7 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error { ps.OutputFormatsProvider = pp ps.targetPathDescriptor = pp.targetPathDescriptor ps.RefProvider = newPageRef(ps) - ps.SitesProvider = &ps.s.Info + ps.SitesProvider = ps.s.Info return nil } @@ -384,8 +377,8 @@ func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor { section = sections[0] } case page.KindTaxonomyTerm, page.KindTaxonomy: - section = maps.GetString(p.bucket.meta, "singular") - + b := p.getTreeRef().n + section = b.viewInfo.name.singular default: } @@ -641,10 +634,6 @@ func (p *pageState) getContentConverter() converter.Converter { return p.m.contentConverter } -func (p *pageState) addResources(r ...resource.Resource) { - p.resources = append(p.resources, r...) -} - func (p *pageState) mapContent(bucket *pagesMapBucket, meta *pageMeta) error { s := p.shortcodeState @@ -665,6 +654,7 @@ func (p *pageState) mapContent(bucket *pagesMapBucket, meta *pageMeta) error { // … it's safe to keep some "global" state var currShortcode shortcode var ordinal int + var frontMatterSet bool Loop: for { @@ -679,7 +669,7 @@ Loop: p.s.BuildFlags.HasLateTemplate.CAS(false, true) rn.AddBytes(it) case it.IsFrontMatter(): - f := metadecoders.FormatFromFrontMatterType(it.Type) + f := pageparser.FormatFromFrontMatterType(it.Type) m, err := metadecoders.Default.UnmarshalToMap(it.Val, f) if err != nil { if fe, ok := err.(herrors.FileError); ok { @@ -692,6 +682,7 @@ Loop: if err := meta.setMetadata(bucket, p, m); err != nil { return err } + frontMatterSet = true next := iter.Peek() if !next.IsDone() { @@ -779,6 +770,14 @@ Loop: } } + if !frontMatterSet { + // Page content without front matter. Assign default front matter from + // cascades etc. + if err := meta.setMetadata(bucket, p, nil); err != nil { + return err + } + } + p.cmap = rn return nil @@ -856,12 +855,11 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { return err } - if idx >= len(p.pageOutputs) { - panic(fmt.Sprintf("invalid page state for %q: got output format index %d, have %d", p.pathOrTitle(), idx, len(p.pageOutputs))) + if len(p.pageOutputs) == 1 { + idx = 0 } p.pageOutput = p.pageOutputs[idx] - if p.pageOutput == nil { panic(fmt.Sprintf("pageOutput is nil for output idx %d", idx)) } @@ -901,13 +899,6 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { p.pageOutput.cp = cp } - for _, r := range p.Resources().ByType(pageResourceType) { - rp := r.(*pageState) - if err := rp.shiftToOutputFormat(isRenderingSite, idx); err != nil { - return errors.Wrap(err, "failed to shift outputformat in Page resource") - } - } - return nil } @@ -934,75 +925,6 @@ func (p *pageState) sourceRef() string { return "" } -func (p *pageState) sourceRefs() []string { - refs := []string{p.sourceRef()} - - if !p.File().IsZero() { - meta := p.File().FileInfo().Meta() - path := meta.PathFile() - - if path != "" { - ref := "/" + filepath.ToSlash(path) - if ref != refs[0] { - refs = append(refs, ref) - } - - } - } - return refs -} - -type pageStatePages []*pageState - -// Implement sorting. -func (ps pageStatePages) Len() int { return len(ps) } - -func (ps pageStatePages) Less(i, j int) bool { return page.DefaultPageSort(ps[i], ps[j]) } - -func (ps pageStatePages) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } - -// findPagePos Given a page, it will find the position in Pages -// will return -1 if not found -func (ps pageStatePages) findPagePos(page *pageState) int { - for i, x := range ps { - if x.File().Filename() == page.File().Filename() { - return i - } - } - return -1 -} - -func (ps pageStatePages) findPagePosByFilename(filename string) int { - for i, x := range ps { - if x.File().Filename() == filename { - return i - } - } - return -1 -} - -func (ps pageStatePages) findPagePosByFilnamePrefix(prefix string) int { - if prefix == "" { - return -1 - } - - lenDiff := -1 - currPos := -1 - prefixLen := len(prefix) - - // Find the closest match - for i, x := range ps { - if strings.HasPrefix(x.File().Filename(), prefix) { - diff := len(x.File().Filename()) - prefixLen - if lenDiff == -1 || diff < lenDiff { - lenDiff = diff - currPos = i - } - } - } - return currPos -} - func (s *Site) sectionsFromFile(fi source.File) []string { dirname := fi.Dir() diff --git a/hugolib/page__common.go b/hugolib/page__common.go index b13a71a40..8187f6719 100644 --- a/hugolib/page__common.go +++ b/hugolib/page__common.go @@ -26,18 +26,40 @@ import ( "github.com/gohugoio/hugo/resources/resource" ) +type treeRefProvider interface { + getTreeRef() *contentTreeRef +} + +func (p *pageCommon) getTreeRef() *contentTreeRef { + return p.treeRef +} + +type nextPrevProvider interface { + getNextPrev() *nextPrev +} + +func (p *pageCommon) getNextPrev() *nextPrev { + return p.posNextPrev +} + +type nextPrevInSectionProvider interface { + getNextPrevInSection() *nextPrev +} + +func (p *pageCommon) getNextPrevInSection() *nextPrev { + return p.posNextPrevSection +} + type pageCommon struct { s *Site m *pageMeta - bucket *pagesMapBucket + bucket *pagesMapBucket + treeRef *contentTreeRef // Laziliy initialized dependencies. init *lazy.Init - metaInit sync.Once - metaInitFn func(bucket *pagesMapBucket) error - // All of these represents the common parts of a page.Page maps.Scratcher navigation.PageMenusProvider diff --git a/hugolib/page__data.go b/hugolib/page__data.go index 8bc818a00..131bf8d5d 100644 --- a/hugolib/page__data.go +++ b/hugolib/page__data.go @@ -16,8 +16,6 @@ package hugolib import ( "sync" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/resources/page" ) @@ -38,26 +36,23 @@ func (p *pageData) Data() interface{} { switch p.Kind() { case page.KindTaxonomy: - bucket := p.bucket - meta := bucket.meta - plural := maps.GetString(meta, "plural") - singular := maps.GetString(meta, "singular") + b := p.treeRef.n + name := b.viewInfo.name + termKey := b.viewInfo.termKey - taxonomy := p.s.Taxonomies[plural].Get(maps.GetString(meta, "termKey")) + taxonomy := p.s.Taxonomies()[name.plural].Get(termKey) - p.data[singular] = taxonomy - p.data["Singular"] = meta["singular"] - p.data["Plural"] = plural - p.data["Term"] = meta["term"] + p.data[name.singular] = taxonomy + p.data["Singular"] = name.singular + p.data["Plural"] = name.plural + p.data["Term"] = b.viewInfo.term() case page.KindTaxonomyTerm: - bucket := p.bucket - meta := bucket.meta - plural := maps.GetString(meta, "plural") - singular := maps.GetString(meta, "singular") + b := p.treeRef.n + name := b.viewInfo.name - p.data["Singular"] = singular - p.data["Plural"] = plural - p.data["Terms"] = p.s.Taxonomies[plural] + p.data["Singular"] = name.singular + p.data["Plural"] = name.plural + p.data["Terms"] = p.s.Taxonomies()[name.plural] // keep the following just for legacy reasons p.data["OrderedIndex"] = p.data["Terms"] p.data["Index"] = p.data["Terms"] diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 60952c831..8ecffbda3 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -61,7 +61,10 @@ type pageMeta struct { // a fixed pageOutput. standalone bool - bundleType string + draft bool // Only published when running with -D flag + buildConfig pagemeta.BuildConfig + + bundleType files.ContentClass // Params contains configuration defined in the params section of page frontmatter. params map[string]interface{} @@ -85,8 +88,6 @@ type pageMeta struct { aliases []string - draft bool - description string keywords []string @@ -94,13 +95,6 @@ type pageMeta struct { resource.Dates - // This is enabled if it is a leaf bundle (the "index.md" type) and it is marked as headless in front matter. - // Being headless means that - // 1. The page itself is not rendered to disk - // 2. It is not available in .Site.Pages etc. - // 3. But you can get it via .Site.GetPage - headless bool - // Set if this page is bundled inside another. bundled bool @@ -160,7 +154,7 @@ func (p *pageMeta) Authors() page.AuthorList { return al } -func (p *pageMeta) BundleType() string { +func (p *pageMeta) BundleType() files.ContentClass { return p.bundleType } @@ -309,40 +303,53 @@ func (p *pageMeta) Weight() int { return p.weight } -func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatter map[string]interface{}) error { - if frontmatter == nil && bucket.cascade == nil { - return errors.New("missing frontmatter data") +func (pm *pageMeta) mergeBucketCascades(b1, b2 *pagesMapBucket) { + if b1.cascade == nil { + b1.cascade = make(map[string]interface{}) + } + if b2 != nil && b2.cascade != nil { + for k, v := range b2.cascade { + if _, found := b1.cascade[k]; !found { + b1.cascade[k] = v + } + } } +} +func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, frontmatter map[string]interface{}) error { pm.params = make(maps.Params) + if frontmatter == nil && (parentBucket == nil || parentBucket.cascade == nil) { + return nil + } + if frontmatter != nil { // Needed for case insensitive fetching of params values maps.ToLower(frontmatter) - if p.IsNode() { + if p.bucket != nil { // Check for any cascade define on itself. if cv, found := frontmatter["cascade"]; found { - cvm := maps.ToStringMap(cv) - if bucket.cascade == nil { - bucket.cascade = cvm - } else { - for k, v := range cvm { - bucket.cascade[k] = v - } - } - } - } - - if bucket != nil && bucket.cascade != nil { - for k, v := range bucket.cascade { - if _, found := frontmatter[k]; !found { - frontmatter[k] = v - } + p.bucket.cascade = maps.ToStringMap(cv) } } } else { frontmatter = make(map[string]interface{}) - for k, v := range bucket.cascade { + } + + var cascade map[string]interface{} + + if p.bucket != nil { + if parentBucket != nil { + // Merge missing keys from parent into this. + pm.mergeBucketCascades(p.bucket, parentBucket) + } + cascade = p.bucket.cascade + } else if parentBucket != nil { + cascade = parentBucket.cascade + } + + for k, v := range cascade { + if _, found := frontmatter[k]; !found { frontmatter[k] = v } } @@ -379,6 +386,11 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err) } + pm.buildConfig, err = pagemeta.DecodeBuildConfig(frontmatter["_build"]) + if err != nil { + return err + } + var sitemapSet bool var draft, published, isCJKLanguage *bool @@ -439,12 +451,15 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte pm.keywords = cast.ToStringSlice(v) pm.params[loki] = pm.keywords case "headless": - // For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output). - // We may expand on this in the future, but that gets more complex pretty fast. - if p.File().TranslationBaseName() == "index" { - pm.headless = cast.ToBool(v) + // Legacy setting for leaf bundles. + // This is since Hugo 0.63 handled in a more general way for all + // pages. + isHeadless := cast.ToBool(v) + pm.params[loki] = isHeadless + if p.File().TranslationBaseName() == "index" && isHeadless { + pm.buildConfig.List = false + pm.buildConfig.Render = false } - pm.params[loki] = pm.headless case "outputs": o := cast.ToStringSlice(v) if len(o) > 0 { @@ -594,7 +609,23 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte return nil } -func (p *pageMeta) applyDefaultValues(ps *pageState) error { +func (p *pageMeta) noList() bool { + return !p.buildConfig.List +} + +func (p *pageMeta) noRender() bool { + return !p.buildConfig.Render +} + +func (p *pageMeta) applyDefaultValues(n *contentNode) error { + if p.buildConfig.IsZero() { + p.buildConfig, _ = pagemeta.DecodeBuildConfig(nil) + } + + if !p.s.isEnabled(p.Kind()) { + (&p.buildConfig).Disable() + } + if p.markup == "" { if !p.File().IsZero() { // Fall back to file extension @@ -610,13 +641,21 @@ func (p *pageMeta) applyDefaultValues(ps *pageState) error { case page.KindHome: p.title = p.s.Info.title case page.KindSection: - sectionName := helpers.FirstUpper(p.sections[0]) + var sectionName string + if n != nil { + sectionName = n.rootSection() + } else { + sectionName = p.sections[0] + } + + sectionName = helpers.FirstUpper(sectionName) if p.s.Cfg.GetBool("pluralizeListTitles") { p.title = inflect.Pluralize(sectionName) } else { p.title = sectionName } case page.KindTaxonomy: + // TODO(bep) improve key := p.sections[len(p.sections)-1] p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1) case page.KindTaxonomyTerm: @@ -653,7 +692,7 @@ func (p *pageMeta) applyDefaultValues(ps *pageState) error { markup = "markdown" } - cp, err := p.newContentConverter(ps, markup, renderingConfigOverrides) + cp, err := p.newContentConverter(n.p, markup, renderingConfigOverrides) if err != nil { return err } @@ -665,6 +704,9 @@ func (p *pageMeta) applyDefaultValues(ps *pageState) error { } func (p *pageMeta) newContentConverter(ps *pageState, markup string, renderingConfigOverrides map[string]interface{}) (converter.Converter, error) { + if ps == nil { + panic("no Page provided") + } cp := p.s.ContentSpec.Converters.Get(markup) if cp == nil { return nil, errors.Errorf("no content renderer found for markup %q", p.markup) diff --git a/hugolib/page__new.go b/hugolib/page__new.go index d810c8df6..12aeadd35 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -22,15 +22,11 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/source" - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/pkg/errors" - "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" ) func newPageBase(metaProvider *pageMeta) (*pageState, error) { @@ -62,7 +58,8 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) { InternalDependencies: s, init: lazy.New(), m: metaProvider, - s: s}, + s: s, + }, } siteAdapter := pageSiteAdapter{s: s, p: ps} @@ -95,7 +92,16 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) { } -func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*pageState, error) { +func newPageBucket(p *pageState) *pagesMapBucket { + return &pagesMapBucket{owner: p} +} + +func newPageFromMeta( + n *contentNode, + parentBucket *pagesMapBucket, + meta map[string]interface{}, + metaProvider *pageMeta) (*pageState, error) { + if metaProvider.f == nil { metaProvider.f = page.NewZeroFile(metaProvider.s.DistinctWarningLog) } @@ -105,26 +111,20 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page return nil, err } - initMeta := func(bucket *pagesMapBucket) error { - if meta != nil || bucket != nil { - if err := metaProvider.setMetadata(bucket, ps, meta); err != nil { - return ps.wrapError(err) - } - } + bucket := parentBucket - if err := metaProvider.applyDefaultValues(ps); err != nil { - return err - } + if ps.IsNode() { + ps.bucket = newPageBucket(ps) + } - return nil + if meta != nil || parentBucket != nil { + if err := metaProvider.setMetadata(bucket, ps, meta); err != nil { + return nil, ps.wrapError(err) + } } - if metaProvider.standalone { - initMeta(nil) - } else { - // Because of possible cascade keywords, we need to delay this - // until we have the complete page graph. - ps.metaInitFn = initMeta + if err := metaProvider.applyDefaultValues(n); err != nil { + return nil, err } ps.init.Add(func() (interface{}, error) { @@ -138,19 +138,25 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page } if ps.m.standalone { - ps.pageOutput = makeOut(ps.m.outputFormats()[0], true) + ps.pageOutput = makeOut(ps.m.outputFormats()[0], !ps.m.noRender()) } else { - ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats)) - created := make(map[string]*pageOutput) outputFormatsForPage := ps.m.outputFormats() - for i, f := range ps.s.h.renderFormats { - po, found := created[f.Name] - if !found { - _, shouldRender := outputFormatsForPage.GetByName(f.Name) - po = makeOut(f, shouldRender) - created[f.Name] = po + + if !ps.m.noRender() { + ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats)) + created := make(map[string]*pageOutput) + for i, f := range ps.s.h.renderFormats { + po, found := created[f.Name] + if !found { + _, shouldRender := outputFormatsForPage.GetByName(f.Name) + po = makeOut(f, shouldRender) + created[f.Name] = po + } + ps.pageOutputs[i] = po } - ps.pageOutputs[i] = po + } else { + // We need one output format for potential resources to publish. + ps.pageOutputs = []*pageOutput{makeOut(outputFormatsForPage[0], false)} } } @@ -170,7 +176,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page func newPageStandalone(m *pageMeta, f output.Format) (*pageState, error) { m.configuredOutputFormats = output.Formats{f} m.standalone = true - p, err := newPageFromMeta(nil, m) + p, err := newPageFromMeta(nil, nil, nil, m) if err != nil { return nil, err @@ -184,108 +190,6 @@ func newPageStandalone(m *pageMeta, f output.Format) (*pageState, error) { } -func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.OpenReadSeekCloser) (*pageState, error) { - sections := s.sectionsFromFile(f) - kind := s.kindFromFileInfoOrSections(f, sections) - if kind == page.KindTaxonomy { - s.PathSpec.MakePathsSanitized(sections) - } - - metaProvider := &pageMeta{kind: kind, sections: sections, bundled: bundled, s: s, f: f} - - ps, err := newPageBase(metaProvider) - if err != nil { - return nil, err - } - - gi, err := s.h.gitInfoForPage(ps) - if err != nil { - return nil, errors.Wrap(err, "failed to load Git data") - } - ps.gitInfo = gi - - r, err := content() - if err != nil { - return nil, err - } - defer r.Close() - - parseResult, err := pageparser.Parse( - r, - pageparser.Config{EnableEmoji: s.siteCfg.enableEmoji}, - ) - if err != nil { - return nil, err - } - - ps.pageContent = pageContent{ - source: rawPageContent{ - parsed: parseResult, - posMainContent: -1, - posSummaryEnd: -1, - posBodyStart: -1, - }, - } - - ps.shortcodeState = newShortcodeHandler(ps, ps.s, nil) - - ps.metaInitFn = func(bucket *pagesMapBucket) error { - if err := ps.mapContent(bucket, metaProvider); err != nil { - return ps.wrapError(err) - } - - if err := metaProvider.applyDefaultValues(ps); err != nil { - return err - } - - return nil - } - - ps.init.Add(func() (interface{}, error) { - - pp, err := newPagePaths(s, ps, metaProvider) - if err != nil { - return nil, err - } - - // Prepare output formats for all sites. - ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats)) - created := make(map[string]*pageOutput) - outputFormatsForPage := ps.m.outputFormats() - - for i, f := range ps.s.h.renderFormats { - if po, found := created[f.Name]; found { - ps.pageOutputs[i] = po - continue - } - - _, render := outputFormatsForPage.GetByName(f.Name) - po := newPageOutput(ps, pp, f, render) - - // Create a content provider for the first, - // we may be able to reuse it. - if i == 0 { - contentProvider, err := newPageContentOutput(ps, po) - if err != nil { - return nil, err - } - po.initContentProvider(contentProvider) - } - - ps.pageOutputs[i] = po - created[f.Name] = po - } - - if err := ps.initCommonProviders(pp); err != nil { - return nil, err - } - - return nil, nil - }) - - return ps, nil -} - type pageDeprecatedWarning struct { p *pageState } diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 183bf010d..7d5b78aae 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -32,7 +32,7 @@ func newPageOutput( ft, found := pp.targetPaths[f.Name] if !found { // Link to the main output format - ft = pp.targetPaths[pp.OutputFormats()[0].Format.Name] + ft = pp.targetPaths[pp.firstOutputFormat.Format.Name] } targetPathsProvider = ft linksProvider = ft diff --git a/hugolib/page__paths.go b/hugolib/page__paths.go index a9152f44a..5dc42bc2a 100644 --- a/hugolib/page__paths.go +++ b/hugolib/page__paths.go @@ -34,14 +34,10 @@ func newPagePaths( outputFormats := pm.outputFormats() if len(outputFormats) == 0 { - outputFormats = pm.s.outputFormats[pm.Kind()] - } - - if len(outputFormats) == 0 { return pagePaths{}, nil } - if pm.headless { + if pm.noRender() { outputFormats = outputFormats[:1] } @@ -55,9 +51,9 @@ func newPagePaths( var relPermalink, permalink string - // If a page is headless or bundled in another, it will not get published - // on its own and it will have no links. - if !pm.headless && !pm.bundled { + // If a page is headless or marked as "no render", or bundled in another, + // it will not get published on its own and it will have no links. + if !pm.noRender() && !pm.bundled { relPermalink = paths.RelPermalink(s.PathSpec) permalink = paths.PermalinkForOutputFormat(s.PathSpec, f) } @@ -77,8 +73,14 @@ func newPagePaths( } + var out page.OutputFormats + if !pm.noRender() { + out = pageOutputFormats + } + return pagePaths{ - outputFormats: pageOutputFormats, + outputFormats: out, + firstOutputFormat: pageOutputFormats[0], targetPaths: targets, targetPathDescriptor: targetPathDescriptor, }, nil @@ -86,7 +88,8 @@ func newPagePaths( } type pagePaths struct { - outputFormats page.OutputFormats + outputFormats page.OutputFormats + firstOutputFormat page.OutputFormat targetPaths map[string]targetPathsHolder targetPathDescriptor page.TargetPathDescriptor diff --git a/hugolib/page__tree.go b/hugolib/page__tree.go index 7bd2874bf..776c92166 100644 --- a/hugolib/page__tree.go +++ b/hugolib/page__tree.go @@ -14,8 +14,10 @@ package hugolib import ( + "path" + "strings" + "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/resources/page" ) @@ -28,17 +30,18 @@ func (pt pageTree) IsAncestor(other interface{}) (bool, error) { return false, nil } - pp, err := unwrapPage(other) - if err != nil || pp == nil { - return false, err + tp, ok := other.(treeRefProvider) + if !ok { + return false, nil } - if pt.p.Kind() == page.KindPage && len(pt.p.SectionsEntries()) == len(pp.SectionsEntries()) { - // A regular page is never its section's ancestor. + ref1, ref2 := pt.p.getTreeRef(), tp.getTreeRef() + + if !ref1.isSection() { return false, nil } - return helpers.HasStringsPrefix(pp.SectionsEntries(), pt.p.SectionsEntries()), nil + return strings.HasPrefix(ref2.key, ref1.key), nil } func (pt pageTree) CurrentSection() page.Page { @@ -55,35 +58,33 @@ func (pt pageTree) IsDescendant(other interface{}) (bool, error) { if pt.p == nil { return false, nil } - pp, err := unwrapPage(other) - if err != nil || pp == nil { - return false, err + + tp, ok := other.(treeRefProvider) + if !ok { + return false, nil } - if pp.Kind() == page.KindPage && len(pt.p.SectionsEntries()) == len(pp.SectionsEntries()) { - // A regular page is never its section's descendant. + ref1, ref2 := pt.p.getTreeRef(), tp.getTreeRef() + + if !ref2.isSection() { return false, nil } - return helpers.HasStringsPrefix(pt.p.SectionsEntries(), pp.SectionsEntries()), nil -} -func (pt pageTree) FirstSection() page.Page { - p := pt.p + return strings.HasPrefix(ref1.key, ref2.key), nil - parent := p.Parent() +} - if types.IsNil(parent) || parent.IsHome() { - return p +func (pt pageTree) FirstSection() page.Page { + ref := pt.p.getTreeRef() + key := ref.key + if !ref.isSection() { + key = path.Dir(key) } - - for { - current := parent - parent = parent.Parent() - if types.IsNil(parent) || parent.IsHome() { - return current - } + _, b := ref.m.getFirstSection(key) + if b == nil { + return nil } - + return b.p } func (pt pageTree) InSection(other interface{}) (bool, error) { @@ -91,16 +92,17 @@ func (pt pageTree) InSection(other interface{}) (bool, error) { return false, nil } - pp, err := unwrapPage(other) - if err != nil { - return false, err - } - - if pp == nil { + tp, ok := other.(treeRefProvider) + if !ok { return false, nil } - return pp.CurrentSection().Eq(pt.p.CurrentSection()), nil + ref1, ref2 := pt.p.getTreeRef(), tp.getTreeRef() + + s1, _ := ref1.getCurrentSection() + s2, _ := ref2.getCurrentSection() + + return s1 == s2, nil } @@ -109,15 +111,22 @@ func (pt pageTree) Page() page.Page { } func (pt pageTree) Parent() page.Page { - if pt.p.parent != nil { - return pt.p.parent + p := pt.p + + if p.parent != nil { + return p.parent + } + + if pt.p.IsHome() { + return nil } - if pt.p.bucket == nil || pt.p.bucket.parent == nil { + _, b := p.getTreeRef().getSection() + if b == nil { return nil } - return pt.p.bucket.parent.owner + return b.p } func (pt pageTree) Sections() page.Pages { diff --git a/hugolib/page_kinds.go b/hugolib/page_kinds.go index 6b6a00c5f..4f000c3e5 100644 --- a/hugolib/page_kinds.go +++ b/hugolib/page_kinds.go @@ -23,7 +23,6 @@ var ( // This is all the kinds we can expect to find in .Site.Pages. allKindsInPages = []string{page.KindPage, page.KindHome, page.KindSection, page.KindTaxonomy, page.KindTaxonomyTerm} - allKinds = append(allKindsInPages, []string{kindRSS, kindSitemap, kindRobotsTXT, kind404}...) ) const ( diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 9bcfc1fc8..55aca8246 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -481,7 +481,7 @@ categories: ["cool stuff"] s := b.H.Sites[0] checkDate := func(t time.Time, msg string) { - b.Assert(t.Year(), qt.Equals, 2017) + b.Assert(t.Year(), qt.Equals, 2017, qt.Commentf(msg)) } checkDated := func(d resource.Dated, msg string) { @@ -524,7 +524,7 @@ date: 2018-01-15 b.Assert(len(b.H.Sites), qt.Equals, 1) s := b.H.Sites[0] - b.Assert(s.getPage("/").Date().Year(), qt.Equals, 2017) + b.Assert(s.getPage("/").Date().Year(), qt.Equals, 2018) b.Assert(s.getPage("/no-index").Date().Year(), qt.Equals, 2017) b.Assert(s.getPage("/with-index-no-date").Date().IsZero(), qt.Equals, true) b.Assert(s.getPage("/with-index-date").Date().Year(), qt.Equals, 2018) diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go index 62b041d7c..4566c5f97 100644 --- a/hugolib/pagebundler_test.go +++ b/hugolib/pagebundler_test.go @@ -20,6 +20,8 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" @@ -101,7 +103,7 @@ func TestPageBundlerSiteRegular(t *testing.T) { c.Assert(len(s.RegularPages()), qt.Equals, 8) singlePage := s.getPage(page.KindPage, "a/1.md") - c.Assert(singlePage.BundleType(), qt.Equals, "") + c.Assert(singlePage.BundleType(), qt.Equals, files.ContentClass("")) c.Assert(singlePage, qt.Not(qt.IsNil)) c.Assert(s.getPage("page", "a/1"), qt.Equals, singlePage) @@ -148,12 +150,12 @@ func TestPageBundlerSiteRegular(t *testing.T) { leafBundle1 := s.getPage(page.KindPage, "b/my-bundle/index.md") c.Assert(leafBundle1, qt.Not(qt.IsNil)) - c.Assert(leafBundle1.BundleType(), qt.Equals, "leaf") + c.Assert(leafBundle1.BundleType(), qt.Equals, files.ContentClassLeaf) c.Assert(leafBundle1.Section(), qt.Equals, "b") sectionB := s.getPage(page.KindSection, "b") c.Assert(sectionB, qt.Not(qt.IsNil)) home, _ := s.Info.Home() - c.Assert(home.BundleType(), qt.Equals, "branch") + c.Assert(home.BundleType(), qt.Equals, files.ContentClassBranch) // This is a root bundle and should live in the "home section" // See https://github.com/gohugoio/hugo/issues/4332 @@ -387,12 +389,10 @@ func TestMultilingualDisableLanguage(t *testing.T) { c.Assert(len(s.Pages()), qt.Equals, 16) // No nn pages c.Assert(len(s.AllPages()), qt.Equals, 16) - for _, p := range s.rawAllPages { - c.Assert(p.Language().Lang != "nn", qt.Equals, true) - } - for _, p := range s.AllPages() { + s.pageMap.withEveryBundlePage(func(p *pageState) bool { c.Assert(p.Language().Lang != "nn", qt.Equals, true) - } + return false + }) } @@ -549,7 +549,6 @@ HEADLESS {{< myShort >}} s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) c.Assert(len(s.RegularPages()), qt.Equals, 1) - c.Assert(len(s.headlessPages), qt.Equals, 1) regular := s.getPage(page.KindPage, "a/index") c.Assert(regular.RelPermalink(), qt.Equals, "/s1/") @@ -1147,18 +1146,15 @@ baseURL = "https://example.org" defaultContentLanguage = "en" defaultContentLanguageInSubDir = true disableKinds = ["taxonomyTerm", "taxonomy"] - [languages] [languages.nn] languageName = "Nynorsk" weight = 2 title = "Tittel på Nynorsk" - [languages.en] title = "Title in English" languageName = "English" weight = 1 - ` pageContent := func(id string) string { diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go index 023222bdd..74d48fe22 100644 --- a/hugolib/pagecollections.go +++ b/hugolib/pagecollections.go @@ -17,43 +17,25 @@ import ( "fmt" "path" "path/filepath" - "sort" "strings" "sync" - "time" - "github.com/gohugoio/hugo/resources/resource" + "github.com/gohugoio/hugo/common/herrors" - "github.com/pkg/errors" + "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/cache" "github.com/gohugoio/hugo/resources/page" ) -// Used in the page cache to mark more than one hit for a given key. -var ambiguityFlag = &pageState{} - // PageCollections contains the page collections for a site. type PageCollections struct { - pagesMap *pagesMap - - // Includes absolute all pages (of all types), including drafts etc. - rawAllPages pageStatePages - - // rawAllPages plus additional pages created during the build process. - workAllPages pageStatePages - - // Includes headless bundles, i.e. bundles that produce no output for its content page. - headlessPages pageStatePages + pageMap *pageMap // Lazy initialized page collections pages *lazyPagesFactory regularPages *lazyPagesFactory allPages *lazyPagesFactory allRegularPages *lazyPagesFactory - - // The index for .Site.GetPage etc. - pageIndex *cache.Lazy } // Pages returns all pages. @@ -78,25 +60,6 @@ func (c *PageCollections) AllRegularPages() page.Pages { return c.allRegularPages.get() } -// Get initializes the index if not already done so, then -// looks up the given page ref, returns nil if no value found. -func (c *PageCollections) getFromCache(ref string) (page.Page, error) { - v, found, err := c.pageIndex.Get(ref) - if err != nil { - return nil, err - } - if !found { - return nil, nil - } - - p := v.(page.Page) - - if p != ambiguityFlag { - return p, nil - } - return nil, fmt.Errorf("page reference %q is ambiguous", ref) -} - type lazyPagesFactory struct { pages page.Pages @@ -115,83 +78,19 @@ func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory { return &lazyPagesFactory{factory: factory} } -func newPageCollections() *PageCollections { - return newPageCollectionsFromPages(nil) -} - -func newPageCollectionsFromPages(pages pageStatePages) *PageCollections { +func newPageCollections(m *pageMap) *PageCollections { + if m == nil { + panic("must provide a pageMap") + } - c := &PageCollections{rawAllPages: pages} + c := &PageCollections{pageMap: m} c.pages = newLazyPagesFactory(func() page.Pages { - pages := make(page.Pages, len(c.workAllPages)) - for i, p := range c.workAllPages { - pages[i] = p - } - return pages + return m.createListAllPages() }) c.regularPages = newLazyPagesFactory(func() page.Pages { - return c.findPagesByKindInWorkPages(page.KindPage, c.workAllPages) - }) - - c.pageIndex = cache.NewLazy(func() (map[string]interface{}, error) { - index := make(map[string]interface{}) - - add := func(ref string, p page.Page) { - ref = strings.ToLower(ref) - existing := index[ref] - if existing == nil { - index[ref] = p - } else if existing != ambiguityFlag && existing != p { - index[ref] = ambiguityFlag - } - } - - for _, pageCollection := range []pageStatePages{c.workAllPages, c.headlessPages} { - for _, p := range pageCollection { - if p.IsPage() { - sourceRefs := p.sourceRefs() - for _, ref := range sourceRefs { - add(ref, p) - } - sourceRef := sourceRefs[0] - - // Ref/Relref supports this potentially ambiguous lookup. - add(p.File().LogicalName(), p) - - translationBaseName := p.File().TranslationBaseName() - - dir, _ := path.Split(sourceRef) - dir = strings.TrimSuffix(dir, "/") - - if translationBaseName == "index" { - add(dir, p) - add(path.Base(dir), p) - } else { - add(translationBaseName, p) - } - - // We need a way to get to the current language version. - pathWithNoExtensions := path.Join(dir, translationBaseName) - add(pathWithNoExtensions, p) - } else { - sourceRefs := p.sourceRefs() - for _, ref := range sourceRefs { - add(ref, p) - } - - ref := p.SectionsPath() - - // index the canonical, unambiguous virtual ref - // e.g. /section - // (this may already have been indexed above) - add("/"+ref, p) - } - } - } - - return index, nil + return c.findPagesByKindIn(page.KindPage, c.pages.get()) }) return c @@ -249,307 +148,165 @@ func (c *PageCollections) getPage(typ string, sections ...string) page.Page { return p } -// Case insensitive page lookup. -func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) { - var anError error - - ref = strings.ToLower(ref) - - // Absolute (content root relative) reference. - if strings.HasPrefix(ref, "/") { - p, err := c.getFromCache(ref) - if err == nil && p != nil { - return p, nil - } - if err != nil { - anError = err - } - - } else if context != nil { - // Try the page-relative path. - var dir string - if !context.File().IsZero() { - dir = filepath.ToSlash(context.File().Dir()) - } else { - dir = context.SectionsPath() - } - ppath := path.Join("/", strings.ToLower(dir), ref) - - p, err := c.getFromCache(ppath) - if err == nil && p != nil { - return p, nil - } - if err != nil { - anError = err - } - } - - if !strings.HasPrefix(ref, "/") { - // Many people will have "post/foo.md" in their content files. - p, err := c.getFromCache("/" + ref) - if err == nil && p != nil { - return p, nil - } - if err != nil { - anError = err - } - } - - // Last try. - ref = strings.TrimPrefix(ref, "/") - p, err := c.getFromCache(ref) - if err != nil { - anError = err - } - - if p == nil && anError != nil { - return nil, wrapErr(errors.Wrap(anError, "failed to resolve ref"), context) +// getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive +// search path than getPageNew. +func (c *PageCollections) getPageRef(context page.Page, ref string) (page.Page, error) { + n, err := c.getContentNode(context, true, ref) + if err != nil || n == nil || n.p == nil { + return nil, err } - - return p, nil + return n.p, nil } -func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages { - var pages page.Pages - for _, p := range inPages { - if p.Kind() == kind { - pages = append(pages, p) - } +func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) { + n, err := c.getContentNode(context, false, ref) + if err != nil || n == nil || n.p == nil { + return nil, err } - return pages + return n.p, nil } -func (c *PageCollections) findPagesByKind(kind string) page.Pages { - return c.findPagesByKindIn(kind, c.Pages()) -} +func (c *PageCollections) getSectionOrPage(ref string) (*contentNode, string) { + var n *contentNode -func (c *PageCollections) findWorkPagesByKind(kind string) pageStatePages { - var pages pageStatePages - for _, p := range c.workAllPages { - if p.Kind() == kind { - pages = append(pages, p) - } - } - return pages -} + s, v, found := c.pageMap.sections.LongestPrefix(ref) -func (*PageCollections) findPagesByKindInWorkPages(kind string, inPages pageStatePages) page.Pages { - var pages page.Pages - for _, p := range inPages { - if p.Kind() == kind { - pages = append(pages, p) - } + if found { + n = v.(*contentNode) } - return pages -} -func (c *PageCollections) addPage(page *pageState) { - c.rawAllPages = append(c.rawAllPages, page) -} - -func (c *PageCollections) removePageFilename(filename string) { - if i := c.rawAllPages.findPagePosByFilename(filename); i >= 0 { - c.clearResourceCacheForPage(c.rawAllPages[i]) - c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + if found && s == ref { + // A section + return n, "" } -} + m := c.pageMap + filename := strings.TrimPrefix(strings.TrimPrefix(ref, s), "/") + langSuffix := "." + m.s.Lang() -func (c *PageCollections) removePage(page *pageState) { - if i := c.rawAllPages.findPagePos(page); i >= 0 { - c.clearResourceCacheForPage(c.rawAllPages[i]) - c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) - } -} + // Trim both extension and any language code. + name := helpers.PathNoExt(filename) + name = strings.TrimSuffix(name, langSuffix) -func (c *PageCollections) replacePage(page *pageState) { - // will find existing page that matches filepath and remove it - c.removePage(page) - c.addPage(page) -} + // These are reserved bundle names and will always be stored by their owning + // folder name. + name = strings.TrimSuffix(name, "/index") + name = strings.TrimSuffix(name, "/_index") -func (c *PageCollections) clearResourceCacheForPage(page *pageState) { - if len(page.resources) > 0 { - page.s.ResourceSpec.DeleteCacheByPrefix(page.targetPaths().SubResourceBaseTarget) + if !found { + return nil, name } -} -func (c *PageCollections) assemblePagesMap(s *Site) error { + // Check if it's a section with filename provided. + if !n.p.File().IsZero() && n.p.File().LogicalName() == filename { + return n, name + } - c.pagesMap = newPagesMap(s) + return m.getPage(s, name), name - rootSections := make(map[string]bool) +} - // Add all branch nodes first. - for _, p := range c.rawAllPages { - rootSections[p.Section()] = true - if p.IsPage() { - continue - } - c.pagesMap.addPage(p) +func (c *PageCollections) getContentNode(context page.Page, isReflink bool, ref string) (*contentNode, error) { + defer herrors.Recover() + ref = filepath.ToSlash(strings.ToLower(strings.TrimSpace(ref))) + if ref == "" { + ref = "/" } + inRef := ref - // Create missing home page and the first level sections if no - // _index provided. - s.home = c.pagesMap.getOrCreateHome() - for k := range rootSections { - c.pagesMap.createSectionIfNotExists(k) + var doSimpleLookup bool + if isReflink || context == nil { + // For Ref/Reflink and .Site.GetPage do simple name lookups for the potentially ambigous myarticle.md and /myarticle.md, + // but not when we get ./myarticle*, section/myarticle. + doSimpleLookup = ref[0] != '.' || ref[0] == '/' && strings.Count(ref, "/") == 1 } - // Attach the regular pages to their section. - for _, p := range c.rawAllPages { - if p.IsNode() { - continue + if context != nil && !strings.HasPrefix(ref, "/") { + // Try the page-relative path. + var base string + if context.File().IsZero() { + base = context.SectionsPath() + } else { + base = filepath.ToSlash(filepath.Dir(context.File().FileInfo().Meta().Path())) } - c.pagesMap.addPage(p) + ref = path.Join("/", strings.ToLower(base), ref) } - return nil -} - -func (c *PageCollections) createWorkAllPages() error { - c.workAllPages = make(pageStatePages, 0, len(c.rawAllPages)) - c.headlessPages = make(pageStatePages, 0) - - var ( - homeDates *resource.Dates - sectionDates *resource.Dates - siteLastmod time.Time - siteLastDate time.Time - - sectionsParamId = "mainSections" - sectionsParamIdLower = strings.ToLower(sectionsParamId) - ) - - mainSections, mainSectionsFound := c.pagesMap.s.Info.Params()[sectionsParamIdLower] - - var ( - bucketsToRemove []string - rootBuckets []*pagesMapBucket - walkErr error - ) - - c.pagesMap.r.Walk(func(s string, v interface{}) bool { - bucket := v.(*pagesMapBucket) - parentBucket := c.pagesMap.parentBucket(s) - - if parentBucket != nil { - - if !mainSectionsFound && strings.Count(s, "/") == 1 && bucket.owner.IsSection() { - // Root section - rootBuckets = append(rootBuckets, bucket) - } - } - - if bucket.owner.IsHome() { - if resource.IsZeroDates(bucket.owner) { - // Calculate dates from the page tree. - homeDates = &bucket.owner.m.Dates - } - } + if !strings.HasPrefix(ref, "/") { + ref = "/" + ref + } - sectionDates = nil - if resource.IsZeroDates(bucket.owner) { - sectionDates = &bucket.owner.m.Dates - } + m := c.pageMap - if parentBucket != nil { - bucket.parent = parentBucket - if bucket.owner.IsSection() { - parentBucket.bucketSections = append(parentBucket.bucketSections, bucket) - } - } + // It's either a section, a page in a section or a taxonomy node. + // Start with the most likely: + n, name := c.getSectionOrPage(ref) + if n != nil { + return n, nil + } - if bucket.isEmpty() { - if bucket.owner.IsSection() && bucket.owner.File().IsZero() { - // Check for any nested section. - var hasDescendant bool - c.pagesMap.r.WalkPrefix(s, func(ss string, v interface{}) bool { - if s != ss { - hasDescendant = true - return true - } - return false - }) - if !hasDescendant { - // This is an auto-created section with, now, nothing in it. - bucketsToRemove = append(bucketsToRemove, s) - return false - } - } + if !strings.HasPrefix(inRef, "/") { + // Many people will have "post/foo.md" in their content files. + if n, _ := c.getSectionOrPage("/" + inRef); n != nil { + return n, nil } + } - if !bucket.disabled { - c.workAllPages = append(c.workAllPages, bucket.owner) + // Check if it's a taxonomy node + s, v, found := m.taxonomies.LongestPrefix(ref) + if found { + if !m.onSameLevel(ref, s) { + return nil, nil } + return v.(*contentNode), nil + } - if !bucket.view { - for _, p := range bucket.headlessPages { - ps := p.(*pageState) - ps.parent = bucket.owner - c.headlessPages = append(c.headlessPages, ps) - } - for _, p := range bucket.pages { - ps := p.(*pageState) - ps.parent = bucket.owner - c.workAllPages = append(c.workAllPages, ps) - - if homeDates != nil { - homeDates.UpdateDateAndLastmodIfAfter(ps) - } - - if sectionDates != nil { - sectionDates.UpdateDateAndLastmodIfAfter(ps) - } - - if p.Lastmod().After(siteLastmod) { - siteLastmod = p.Lastmod() - } - if p.Date().After(siteLastDate) { - siteLastDate = p.Date() - } + getByName := func(s string) (*contentNode, error) { + n := m.pageReverseIndex.Get(s) + if n != nil { + if n == ambigousContentNode { + return nil, fmt.Errorf("page reference %q is ambiguous", ref) } + return n, nil } - return false - }) - - if walkErr != nil { - return walkErr + return nil, nil } - c.pagesMap.s.lastmod = siteLastmod - - if !mainSectionsFound { + var module string + if context != nil && !context.File().IsZero() { + module = context.File().FileInfo().Meta().Module() + } - // Calculare main section - var ( - maxRootBucketWeight int - maxRootBucket *pagesMapBucket - ) + if module == "" && !c.pageMap.s.home.File().IsZero() { + module = c.pageMap.s.home.File().FileInfo().Meta().Module() + } - for _, b := range rootBuckets { - weight := len(b.pages) + (len(b.bucketSections) * 5) - if weight >= maxRootBucketWeight { - maxRootBucket = b - maxRootBucketWeight = weight - } + if module != "" { + n, err := getByName(module + ref) + if err != nil { + return nil, err } - - if maxRootBucket != nil { - // Try to make this as backwards compatible as possible. - mainSections = []string{maxRootBucket.owner.Section()} + if n != nil { + return n, nil } } - c.pagesMap.s.Info.Params()[sectionsParamId] = mainSections - c.pagesMap.s.Info.Params()[sectionsParamIdLower] = mainSections - - for _, key := range bucketsToRemove { - c.pagesMap.r.Delete(key) + if !doSimpleLookup { + return nil, nil } - sort.Sort(c.workAllPages) + // Ref/relref supports this potentially ambigous lookup. + return getByName(name) + +} - return nil +func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages { + var pages page.Pages + for _, p := range inPages { + if p.Kind() == kind { + pages = append(pages, p) + } + } + return pages } diff --git a/hugolib/pagecollections_test.go b/hugolib/pagecollections_test.go index 0f7520e1b..690a315a8 100644 --- a/hugolib/pagecollections_test.go +++ b/hugolib/pagecollections_test.go @@ -70,43 +70,91 @@ func BenchmarkGetPage(b *testing.B) { } } -func BenchmarkGetPageRegular(b *testing.B) { +func createGetPageRegularBenchmarkSite(t testing.TB) *Site { + var ( - c = qt.New(b) + c = qt.New(t) cfg, fs = newTestCfg() - r = rand.New(rand.NewSource(time.Now().UnixNano())) ) + pc := func(title string) string { + return fmt.Sprintf(pageCollectionsPageTemplate, title) + } + for i := 0; i < 10; i++ { for j := 0; j < 100; j++ { - content := fmt.Sprintf(pageCollectionsPageTemplate, fmt.Sprintf("Title%d_%d", i, j)) - writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + content := pc(fmt.Sprintf("Title%d_%d", i, j)) + writeSource(c, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) } } - s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + return buildSingleSite(c, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - pagePaths := make([]string, b.N) +} - for i := 0; i < b.N; i++ { - pagePaths[i] = path.Join(fmt.Sprintf("sect%d", r.Intn(10)), fmt.Sprintf("page%d.md", r.Intn(100))) - } +func TestBenchmarkGetPageRegular(t *testing.T) { + c := qt.New(t) + s := createGetPageRegularBenchmarkSite(t) - b.ResetTimer() - for i := 0; i < b.N; i++ { - page, _ := s.getPageNew(nil, pagePaths[i]) - c.Assert(page, qt.Not(qt.IsNil)) + for i := 0; i < 10; i++ { + pp := path.Join("/", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", i)) + page, _ := s.getPageNew(nil, pp) + c.Assert(page, qt.Not(qt.IsNil), qt.Commentf(pp)) } } -type testCase struct { +func BenchmarkGetPageRegular(b *testing.B) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + b.Run("From root", func(b *testing.B) { + s := createGetPageRegularBenchmarkSite(b) + c := qt.New(b) + + pagePaths := make([]string, b.N) + + for i := 0; i < b.N; i++ { + pagePaths[i] = path.Join(fmt.Sprintf("/sect%d", r.Intn(10)), fmt.Sprintf("page%d.md", r.Intn(100))) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + page, _ := s.getPageNew(nil, pagePaths[i]) + c.Assert(page, qt.Not(qt.IsNil)) + } + }) + + b.Run("Page relative", func(b *testing.B) { + s := createGetPageRegularBenchmarkSite(b) + c := qt.New(b) + allPages := s.RegularPages() + + pagePaths := make([]string, b.N) + pages := make([]page.Page, b.N) + + for i := 0; i < b.N; i++ { + pagePaths[i] = fmt.Sprintf("page%d.md", r.Intn(100)) + pages[i] = allPages[r.Intn(len(allPages)/3)] + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + page, _ := s.getPageNew(pages[i], pagePaths[i]) + c.Assert(page, qt.Not(qt.IsNil)) + } + }) + +} + +type getPageTest struct { + name string kind string context page.Page - path []string + pathVariants []string expectedTitle string } -func (t *testCase) check(p page.Page, err error, errorMsg string, c *qt.C) { +func (t *getPageTest) check(p page.Page, err error, errorMsg string, c *qt.C) { + c.Helper() errorComment := qt.Commentf(errorMsg) switch t.kind { case "Ambiguous": @@ -130,117 +178,159 @@ func TestGetPage(t *testing.T) { c = qt.New(t) ) + pc := func(title string) string { + return fmt.Sprintf(pageCollectionsPageTemplate, title) + } + for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { - content := fmt.Sprintf(pageCollectionsPageTemplate, fmt.Sprintf("Title%d_%d", i, j)) + content := pc(fmt.Sprintf("Title%d_%d", i, j)) writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) } } - content := fmt.Sprintf(pageCollectionsPageTemplate, "home page") + content := pc("home page") writeSource(t, fs, filepath.Join("content", "_index.md"), content) - content = fmt.Sprintf(pageCollectionsPageTemplate, "about page") + content = pc("about page") writeSource(t, fs, filepath.Join("content", "about.md"), content) - content = fmt.Sprintf(pageCollectionsPageTemplate, "section 3") + content = pc("section 3") writeSource(t, fs, filepath.Join("content", "sect3", "_index.md"), content) - content = fmt.Sprintf(pageCollectionsPageTemplate, "UniqueBase") - writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), content) + writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), pc("UniqueBase")) + writeSource(t, fs, filepath.Join("content", "sect3", "Unique2.md"), pc("UniqueBase2")) - content = fmt.Sprintf(pageCollectionsPageTemplate, "another sect7") + content = pc("another sect7") writeSource(t, fs, filepath.Join("content", "sect3", "sect7", "_index.md"), content) - content = fmt.Sprintf(pageCollectionsPageTemplate, "deep page") + content = pc("deep page") writeSource(t, fs, filepath.Join("content", "sect3", "subsect", "deep.md"), content) + // Bundle variants + writeSource(t, fs, filepath.Join("content", "sect3", "b1", "index.md"), pc("b1 bundle")) + writeSource(t, fs, filepath.Join("content", "sect3", "index", "index.md"), pc("index bundle")) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) sec3, err := s.getPageNew(nil, "/sect3") c.Assert(err, qt.IsNil) c.Assert(sec3, qt.Not(qt.IsNil)) - tests := []testCase{ + tests := []getPageTest{ // legacy content root relative paths - {page.KindHome, nil, []string{}, "home page"}, - {page.KindPage, nil, []string{"about.md"}, "about page"}, - {page.KindSection, nil, []string{"sect3"}, "section 3"}, - {page.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"}, - {page.KindPage, nil, []string{"sect4/page2.md"}, "Title4_2"}, - {page.KindSection, nil, []string{"sect3/sect7"}, "another sect7"}, - {page.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"}, - {page.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, //test OS-specific path - - // shorthand refs (potentially ambiguous) - {page.KindPage, nil, []string{"unique.md"}, "UniqueBase"}, - {"Ambiguous", nil, []string{"page1.md"}, ""}, + {"Root relative, no slash, home", page.KindHome, nil, []string{""}, "home page"}, + {"Root relative, no slash, root page", page.KindPage, nil, []string{"about.md", "ABOUT.md"}, "about page"}, + {"Root relative, no slash, section", page.KindSection, nil, []string{"sect3"}, "section 3"}, + {"Root relative, no slash, section page", page.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"}, + {"Root relative, no slash, sub setion", page.KindSection, nil, []string{"sect3/sect7"}, "another sect7"}, + {"Root relative, no slash, nested page", page.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"}, + {"Root relative, no slash, OS slashes", page.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, + + {"Short ref, unique", page.KindPage, nil, []string{"unique.md", "unique"}, "UniqueBase"}, + {"Short ref, unique, upper case", page.KindPage, nil, []string{"Unique2.md", "unique2.md", "unique2"}, "UniqueBase2"}, + {"Short ref, ambiguous", "Ambiguous", nil, []string{"page1.md"}, ""}, // ISSUE: This is an ambiguous ref, but because we have to support the legacy // content root relative paths without a leading slash, the lookup // returns /sect7. This undermines ambiguity detection, but we have no choice. //{"Ambiguous", nil, []string{"sect7"}, ""}, - {page.KindSection, nil, []string{"sect7"}, "Sect7s"}, - - // absolute paths - {page.KindHome, nil, []string{"/"}, "home page"}, - {page.KindPage, nil, []string{"/about.md"}, "about page"}, - {page.KindSection, nil, []string{"/sect3"}, "section 3"}, - {page.KindPage, nil, []string{"/sect3/page1.md"}, "Title3_1"}, - {page.KindPage, nil, []string{"/sect4/page2.md"}, "Title4_2"}, - {page.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"}, - {page.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"}, - {page.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, //test OS-specific path - {page.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, //next test depends on this page existing + {"Section, ambigous", page.KindSection, nil, []string{"sect7"}, "Sect7s"}, + + {"Absolute, home", page.KindHome, nil, []string{"/", ""}, "home page"}, + {"Absolute, page", page.KindPage, nil, []string{"/about.md", "/about"}, "about page"}, + {"Absolute, sect", page.KindSection, nil, []string{"/sect3"}, "section 3"}, + {"Absolute, page in subsection", page.KindPage, nil, []string{"/sect3/page1.md", "/Sect3/Page1.md"}, "Title3_1"}, + {"Absolute, section, subsection with same name", page.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"}, + {"Absolute, page, deep", page.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"}, + {"Absolute, page, OS slashes", page.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, //test OS-specific path + {"Absolute, unique", page.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, + {"Absolute, unique, case", page.KindPage, nil, []string{"/sect3/Unique2.md", "/sect3/unique2.md", "/sect3/unique2", "/sect3/Unique2"}, "UniqueBase2"}, + //next test depends on this page existing // {"NoPage", nil, []string{"/unique.md"}, ""}, // ISSUE #4969: this is resolving to /sect3/unique.md - {"NoPage", nil, []string{"/missing-page.md"}, ""}, - {"NoPage", nil, []string{"/missing-section"}, ""}, + {"Absolute, missing page", "NoPage", nil, []string{"/missing-page.md"}, ""}, + {"Absolute, missing section", "NoPage", nil, []string{"/missing-section"}, ""}, // relative paths - {page.KindHome, sec3, []string{".."}, "home page"}, - {page.KindHome, sec3, []string{"../"}, "home page"}, - {page.KindPage, sec3, []string{"../about.md"}, "about page"}, - {page.KindSection, sec3, []string{"."}, "section 3"}, - {page.KindSection, sec3, []string{"./"}, "section 3"}, - {page.KindPage, sec3, []string{"page1.md"}, "Title3_1"}, - {page.KindPage, sec3, []string{"./page1.md"}, "Title3_1"}, - {page.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"}, - {page.KindSection, sec3, []string{"sect7"}, "another sect7"}, - {page.KindSection, sec3, []string{"./sect7"}, "another sect7"}, - {page.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"}, - {page.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"}, - {page.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, //test OS-specific path - {page.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"}, - {"NoPage", sec3, []string{"./sect2"}, ""}, + {"Dot relative, home", page.KindHome, sec3, []string{".."}, "home page"}, + {"Dot relative, home, slash", page.KindHome, sec3, []string{"../"}, "home page"}, + {"Dot relative about", page.KindPage, sec3, []string{"../about.md"}, "about page"}, + {"Dot", page.KindSection, sec3, []string{"."}, "section 3"}, + {"Dot slash", page.KindSection, sec3, []string{"./"}, "section 3"}, + {"Page relative, no dot", page.KindPage, sec3, []string{"page1.md"}, "Title3_1"}, + {"Page relative, dot", page.KindPage, sec3, []string{"./page1.md"}, "Title3_1"}, + {"Up and down another section", page.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"}, + {"Rel sect7", page.KindSection, sec3, []string{"sect7"}, "another sect7"}, + {"Rel sect7 dot", page.KindSection, sec3, []string{"./sect7"}, "another sect7"}, + {"Dot deep", page.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"}, + {"Dot dot inner", page.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"}, + {"Dot OS slash", page.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, //test OS-specific path + {"Dot unique", page.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"}, + {"Dot sect", "NoPage", sec3, []string{"./sect2"}, ""}, //{"NoPage", sec3, []string{"sect2"}, ""}, // ISSUE: /sect3 page relative query is resolving to /sect2 - // absolute paths ignore context - {page.KindHome, sec3, []string{"/"}, "home page"}, - {page.KindPage, sec3, []string{"/about.md"}, "about page"}, - {page.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"}, - {page.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, //next test depends on this page existing - {"NoPage", sec3, []string{"/subsect/deep.md"}, ""}, + {"Abs, ignore context, home", page.KindHome, sec3, []string{"/"}, "home page"}, + {"Abs, ignore context, about", page.KindPage, sec3, []string{"/about.md"}, "about page"}, + {"Abs, ignore context, page in section", page.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"}, + {"Abs, ignore context, page subsect deep", page.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, //next test depends on this page existing + {"Abs, ignore context, page deep", "NoPage", sec3, []string{"/subsect/deep.md"}, ""}, + + // Taxonomies + {"Taxonomy term", page.KindTaxonomyTerm, nil, []string{"categories"}, "Categories"}, + {"Taxonomy", page.KindTaxonomy, nil, []string{"categories/hugo", "categories/Hugo"}, "Hugo"}, + + // Bundle variants + {"Bundle regular", page.KindPage, nil, []string{"sect3/b1", "sect3/b1/index.md", "sect3/b1/index.en.md"}, "b1 bundle"}, + {"Bundle index name", page.KindPage, nil, []string{"sect3/index/index.md", "sect3/index"}, "index bundle"}, } for _, test := range tests { - errorMsg := fmt.Sprintf("Test case %s %v -> %s", test.context, test.path, test.expectedTitle) + c.Run(test.name, func(c *qt.C) { + errorMsg := fmt.Sprintf("Test case %v %v -> %s", test.context, test.pathVariants, test.expectedTitle) + + // test legacy public Site.GetPage (which does not support page context relative queries) + if test.context == nil { + for _, ref := range test.pathVariants { + args := append([]string{test.kind}, ref) + page, err := s.Info.GetPage(args...) + test.check(page, err, errorMsg, c) + } + } + + // test new internal Site.getPageNew + for _, ref := range test.pathVariants { + page2, err := s.getPageNew(test.context, ref) + test.check(page2, err, errorMsg, c) + } + + }) + } - // test legacy public Site.GetPage (which does not support page context relative queries) - if test.context == nil { - args := append([]string{test.kind}, test.path...) - page, err := s.Info.GetPage(args...) - test.check(page, err, errorMsg, c) - } +} - // test new internal Site.getPageNew - var ref string - if len(test.path) == 1 { - ref = filepath.ToSlash(test.path[0]) - } else { - ref = path.Join(test.path...) - } - page2, err := s.getPageNew(test.context, ref) - test.check(page2, err, errorMsg, c) +// https://github.com/gohugoio/hugo/issues/6034 +func TestGetPageRelative(t *testing.T) { + b := newTestSitesBuilder(t) + for i, section := range []string{"what", "where", "who"} { + isDraft := i == 2 + b.WithContent( + section+"/_index.md", fmt.Sprintf("---title: %s\n---", section), + section+"/members.md", fmt.Sprintf("---title: members %s\ndraft: %t\n---", section, isDraft), + ) } + b.WithTemplates("_default/list.html", ` +{{ with .GetPage "members.md" }} + Members: {{ .Title }} +{{ else }} +NOT FOUND +{{ end }} +`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/what/index.html", `Members: members what`) + b.AssertFileContent("public/where/index.html", `Members: members where`) + b.AssertFileContent("public/who/index.html", `NOT FOUND`) + } diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index 58d652689..de7ccb084 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -19,20 +19,13 @@ import ( "os" pth "path" "path/filepath" - "strings" + "reflect" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/gohugoio/hugo/resources" - - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" - - "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/resources/resource" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/source" @@ -41,30 +34,32 @@ import ( "github.com/spf13/afero" ) +const ( + walkIsRootFileMetaKey = "walkIsRootFileMetaKey" +) + func newPagesCollector( sp *source.SourceSpec, + contentMap *pageMaps, logger *loggers.Logger, contentTracker *contentChangeMap, proc pagesCollectorProcessorProvider, filenames ...string) *pagesCollector { return &pagesCollector{ - fs: sp.SourceFs, - proc: proc, - sp: sp, - logger: logger, - filenames: filenames, - tracker: contentTracker, + fs: sp.SourceFs, + contentMap: contentMap, + proc: proc, + sp: sp, + logger: logger, + filenames: filenames, + tracker: contentTracker, } } -func newPagesProcessor(h *HugoSites, sp *source.SourceSpec, partialBuild bool) *pagesProcessor { - - return &pagesProcessor{ - h: h, - sp: sp, - partialBuild: partialBuild, - numWorkers: config.GetNumWorkerMultiplier() * 3, - } +type contentDirKey struct { + dirname string + filename string + tp bundleDirType } type fileinfoBundle struct { @@ -90,6 +85,8 @@ type pagesCollector struct { fs afero.Fs logger *loggers.Logger + contentMap *pageMaps + // Ordered list (bundle headers first) used in partial builds. filenames []string @@ -99,21 +96,78 @@ type pagesCollector struct { proc pagesCollectorProcessorProvider } -type contentDirKey struct { - dirname string - filename string - tp bundleDirType +// isCascadingEdit returns whether the dir represents a cascading edit. +// That is, if a front matter cascade section is removed, added or edited. +// If this is the case we must re-evaluate its descendants. +func (c *pagesCollector) isCascadingEdit(dir contentDirKey) (bool, string) { + // This is eiter a section or a taxonomy node. Find it. + prefix := cleanTreeKey(dir.dirname) + + section := "/" + var isCascade bool + + c.contentMap.walkBranchesPrefix(prefix, func(s string, n *contentNode) bool { + if n.fi == nil || dir.filename != n.fi.Meta().Filename() { + return false + } + + f, err := n.fi.Meta().Open() + if err != nil { + // File may have been removed, assume a cascading edit. + // Some false positives is not too bad. + isCascade = true + return true + } + + pf, err := pageparser.ParseFrontMatterAndContent(f) + f.Close() + if err != nil { + isCascade = true + return true + } + + if n.p == nil || n.p.bucket == nil { + return true + } + + section = s + + maps.ToLower(pf.FrontMatter) + cascade1, ok := pf.FrontMatter["cascade"] + hasCascade := n.p.bucket.cascade != nil && len(n.p.bucket.cascade) > 0 + if !ok { + isCascade = hasCascade + return true + } + + if !hasCascade { + isCascade = true + return true + } + + isCascade = !reflect.DeepEqual(cascade1, n.p.bucket.cascade) + + return true + + }) + + return isCascade, section } // Collect. -func (c *pagesCollector) Collect() error { +func (c *pagesCollector) Collect() (collectErr error) { c.proc.Start(context.Background()) + defer func() { + collectErr = c.proc.Wait() + }() - var collectErr error if len(c.filenames) == 0 { // Collect everything. collectErr = c.collectDir("", false, nil) } else { + for _, pm := range c.contentMap.pmaps { + pm.cfg.isRebuild = true + } dirs := make(map[contentDirKey]bool) for _, filename := range c.filenames { dir, btype := c.tracker.resolveAndRemove(filename) @@ -121,9 +175,19 @@ func (c *pagesCollector) Collect() error { } for dir := range dirs { + for _, pm := range c.contentMap.pmaps { + pm.s.ResourceSpec.DeleteBySubstring(dir.dirname) + } + switch dir.tp { - case bundleLeaf, bundleBranch: + case bundleLeaf: collectErr = c.collectDir(dir.dirname, true, nil) + case bundleBranch: + isCascading, section := c.isCascadingEdit(dir) + if isCascading { + c.contentMap.deleteSection(section) + } + collectErr = c.collectDir(dir.dirname, !isCascading, nil) default: // We always start from a directory. collectErr = c.collectDir(dir.dirname, true, func(fim hugofs.FileMetaInfo) bool { @@ -138,13 +202,120 @@ func (c *pagesCollector) Collect() error { } - err := c.proc.Wait() + return - if collectErr != nil { - return collectErr +} + +func (c *pagesCollector) isBundleHeader(fi hugofs.FileMetaInfo) bool { + class := fi.Meta().Classifier() + return class == files.ContentClassLeaf || class == files.ContentClassBranch +} + +func (c *pagesCollector) getLang(fi hugofs.FileMetaInfo) string { + lang := fi.Meta().Lang() + if lang != "" { + return lang } - return err + return c.sp.DefaultContentLanguage +} + +func (c *pagesCollector) addToBundle(info hugofs.FileMetaInfo, btyp bundleDirType, bundles pageBundles) error { + getBundle := func(lang string) *fileinfoBundle { + return bundles[lang] + } + + cloneBundle := func(lang string) *fileinfoBundle { + // Every bundled content file needs a content file header. + // Use the default content language if found, else just + // pick one. + var ( + source *fileinfoBundle + found bool + ) + + source, found = bundles[c.sp.DefaultContentLanguage] + if !found { + for _, b := range bundles { + source = b + break + } + } + + if source == nil { + panic(fmt.Sprintf("no source found, %d", len(bundles))) + } + + clone := c.cloneFileInfo(source.header) + clone.Meta()["lang"] = lang + + return &fileinfoBundle{ + header: clone, + } + } + + lang := c.getLang(info) + bundle := getBundle(lang) + isBundleHeader := c.isBundleHeader(info) + if bundle != nil && isBundleHeader { + // index.md file inside a bundle, see issue 6208. + info.Meta()["classifier"] = files.ContentClassContent + isBundleHeader = false + } + classifier := info.Meta().Classifier() + isContent := classifier == files.ContentClassContent + if bundle == nil { + if isBundleHeader { + bundle = &fileinfoBundle{header: info} + bundles[lang] = bundle + } else { + if btyp == bundleBranch { + // No special logic for branch bundles. + // Every language needs its own _index.md file. + // Also, we only clone bundle headers for lonsesome, bundled, + // content files. + return c.handleFiles(info) + } + + if isContent { + bundle = cloneBundle(lang) + bundles[lang] = bundle + } + } + } + + if !isBundleHeader && bundle != nil { + bundle.resources = append(bundle.resources, info) + } + + if classifier == files.ContentClassFile { + translations := info.Meta().Translations() + + for lang, b := range bundles { + if !stringSliceContains(lang, translations...) && !b.containsResource(info.Name()) { + + // Clone and add it to the bundle. + clone := c.cloneFileInfo(info) + clone.Meta()["lang"] = lang + b.resources = append(b.resources, clone) + } + } + } + + return nil +} + +func (c *pagesCollector) cloneFileInfo(fi hugofs.FileMetaInfo) hugofs.FileMetaInfo { + cm := hugofs.FileMeta{} + meta := fi.Meta() + if meta == nil { + panic(fmt.Sprintf("not meta: %v", fi.Name())) + } + for k, v := range meta { + cm[k] = v + } + + return hugofs.NewFileMetaInfo(fi, cm) } func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func(fim hugofs.FileMetaInfo) bool) error { @@ -218,6 +389,7 @@ func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func( } } } + walkRoot := dir.Meta().GetBool(walkIsRootFileMetaKey) readdir = filtered // We merge language directories, so there can be duplicates, but they @@ -232,6 +404,9 @@ func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func( } meta := fi.Meta() + if walkRoot { + meta[walkIsRootFileMetaKey] = true + } class := meta.Classifier() translationBase := meta.TranslationBaseNameWithExt() key := pth.Join(meta.Lang(), translationBase) @@ -307,11 +482,16 @@ func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func( return nil } + fim := fi.(hugofs.FileMetaInfo) + // Make sure the pages in this directory gets re-rendered, + // even in fast render mode. + fim.Meta()[walkIsRootFileMetaKey] = true + w := hugofs.NewWalkway(hugofs.WalkwayConfig{ Fs: c.fs, Logger: c.logger, Root: dirname, - Info: fi.(hugofs.FileMetaInfo), + Info: fim, HookPre: preHook, HookPost: postHook, WalkFn: wfn}) @@ -320,123 +500,13 @@ func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func( } -func (c *pagesCollector) isBundleHeader(fi hugofs.FileMetaInfo) bool { - class := fi.Meta().Classifier() - return class == files.ContentClassLeaf || class == files.ContentClassBranch -} - -func (c *pagesCollector) getLang(fi hugofs.FileMetaInfo) string { - lang := fi.Meta().Lang() - if lang != "" { - return lang - } - - return c.sp.DefaultContentLanguage -} - -func (c *pagesCollector) addToBundle(info hugofs.FileMetaInfo, btyp bundleDirType, bundles pageBundles) error { - getBundle := func(lang string) *fileinfoBundle { - return bundles[lang] - } - - cloneBundle := func(lang string) *fileinfoBundle { - // Every bundled content file needs a content file header. - // Use the default content language if found, else just - // pick one. - var ( - source *fileinfoBundle - found bool - ) - - source, found = bundles[c.sp.DefaultContentLanguage] - if !found { - for _, b := range bundles { - source = b - break - } - } - - if source == nil { - panic(fmt.Sprintf("no source found, %d", len(bundles))) - } - - clone := c.cloneFileInfo(source.header) - clone.Meta()["lang"] = lang - - return &fileinfoBundle{ - header: clone, - } - } - - lang := c.getLang(info) - bundle := getBundle(lang) - isBundleHeader := c.isBundleHeader(info) - if bundle != nil && isBundleHeader { - // index.md file inside a bundle, see issue 6208. - info.Meta()["classifier"] = files.ContentClassContent - isBundleHeader = false - } - classifier := info.Meta().Classifier() - isContent := classifier == files.ContentClassContent - if bundle == nil { - if isBundleHeader { - bundle = &fileinfoBundle{header: info} - bundles[lang] = bundle - } else { - if btyp == bundleBranch { - // No special logic for branch bundles. - // Every language needs its own _index.md file. - // Also, we only clone bundle headers for lonsesome, bundled, - // content files. - return c.handleFiles(info) - } - - if isContent { - bundle = cloneBundle(lang) - bundles[lang] = bundle - } - } - } - - if !isBundleHeader && bundle != nil { - bundle.resources = append(bundle.resources, info) - } - - if classifier == files.ContentClassFile { - translations := info.Meta().Translations() - - for lang, b := range bundles { - if !stringSliceContains(lang, translations...) && !b.containsResource(info.Name()) { - - // Clone and add it to the bundle. - clone := c.cloneFileInfo(info) - clone.Meta()["lang"] = lang - b.resources = append(b.resources, clone) - } - } - } - - return nil -} - -func (c *pagesCollector) cloneFileInfo(fi hugofs.FileMetaInfo) hugofs.FileMetaInfo { - cm := hugofs.FileMeta{} - meta := fi.Meta() - if meta == nil { - panic(fmt.Sprintf("not meta: %v", fi.Name())) - } - for k, v := range meta { - cm[k] = v - } - - return hugofs.NewFileMetaInfo(fi, cm) -} - func (c *pagesCollector) handleBundleBranch(readdir []hugofs.FileMetaInfo) error { // Maps bundles to its language. bundles := pageBundles{} + var contentFiles []hugofs.FileMetaInfo + for _, fim := range readdir { if fim.IsDir() { @@ -447,9 +517,7 @@ func (c *pagesCollector) handleBundleBranch(readdir []hugofs.FileMetaInfo) error switch meta.Classifier() { case files.ContentClassContent: - if err := c.handleFiles(fim); err != nil { - return err - } + contentFiles = append(contentFiles, fim) default: if err := c.addToBundle(fim, bundleBranch, bundles); err != nil { return err @@ -458,7 +526,12 @@ func (c *pagesCollector) handleBundleBranch(readdir []hugofs.FileMetaInfo) error } - return c.proc.Process(bundles) + // Make sure the section is created before its pages. + if err := c.proc.Process(bundles); err != nil { + return err + } + + return c.handleFiles(contentFiles...) } @@ -508,273 +581,6 @@ func (c *pagesCollector) handleFiles(fis ...hugofs.FileMetaInfo) error { return nil } -type pagesCollectorProcessorProvider interface { - Process(item interface{}) error - Start(ctx context.Context) context.Context - Wait() error -} - -type pagesProcessor struct { - h *HugoSites - sp *source.SourceSpec - - itemChan chan interface{} - itemGroup *errgroup.Group - - // The output Pages - pagesChan chan *pageState - pagesGroup *errgroup.Group - - numWorkers int - - partialBuild bool -} - -func (proc *pagesProcessor) Process(item interface{}) error { - proc.itemChan <- item - return nil -} - -func (proc *pagesProcessor) Start(ctx context.Context) context.Context { - proc.pagesChan = make(chan *pageState, proc.numWorkers) - proc.pagesGroup, ctx = errgroup.WithContext(ctx) - proc.itemChan = make(chan interface{}, proc.numWorkers) - proc.itemGroup, ctx = errgroup.WithContext(ctx) - - proc.pagesGroup.Go(func() error { - for p := range proc.pagesChan { - s := p.s - p.forceRender = proc.partialBuild - - if p.forceRender { - s.replacePage(p) - } else { - s.addPage(p) - } - } - return nil - }) - - for i := 0; i < proc.numWorkers; i++ { - proc.itemGroup.Go(func() error { - for item := range proc.itemChan { - select { - case <-proc.h.Done(): - return nil - default: - if err := proc.process(item); err != nil { - proc.h.SendError(err) - } - } - } - - return nil - }) - } - - return ctx -} - -func (proc *pagesProcessor) Wait() error { - close(proc.itemChan) - - err := proc.itemGroup.Wait() - - close(proc.pagesChan) - - if err != nil { - return err - } - - return proc.pagesGroup.Wait() -} - -func (proc *pagesProcessor) newPageFromBundle(b *fileinfoBundle) (*pageState, error) { - p, err := proc.newPageFromFi(b.header, nil) - if err != nil { - return nil, err - } - - if len(b.resources) > 0 { - - resources := make(resource.Resources, len(b.resources)) - - for i, rfi := range b.resources { - meta := rfi.Meta() - classifier := meta.Classifier() - var r resource.Resource - switch classifier { - case files.ContentClassContent: - rp, err := proc.newPageFromFi(rfi, p) - if err != nil { - return nil, err - } - rp.m.resourcePath = filepath.ToSlash(strings.TrimPrefix(rp.Path(), p.File().Dir())) - - r = rp - - case files.ContentClassFile: - r, err = proc.newResource(rfi, p) - if err != nil { - return nil, err - } - default: - panic(fmt.Sprintf("invalid classifier: %q", classifier)) - } - - resources[i] = r - - } - - p.addResources(resources...) - } - - return p, nil -} - -func (proc *pagesProcessor) newPageFromFi(fim hugofs.FileMetaInfo, owner *pageState) (*pageState, error) { - fi, err := newFileInfo(proc.sp, fim) - if err != nil { - return nil, err - } - - var s *Site - meta := fim.Meta() - - if owner != nil { - s = owner.s - } else { - lang := meta.Lang() - s = proc.getSite(lang) - } - - r := func() (hugio.ReadSeekCloser, error) { - return meta.Open() - } - - p, err := newPageWithContent(fi, s, owner != nil, r) - if err != nil { - return nil, err - } - p.parent = owner - return p, nil -} - -func (proc *pagesProcessor) newResource(fim hugofs.FileMetaInfo, owner *pageState) (resource.Resource, error) { - - // TODO(bep) consolidate with multihost logic + clean up - outputFormats := owner.m.outputFormats() - seen := make(map[string]bool) - var targetBasePaths []string - // Make sure bundled resources are published to all of the ouptput formats' - // sub paths. - for _, f := range outputFormats { - p := f.Path - if seen[p] { - continue - } - seen[p] = true - targetBasePaths = append(targetBasePaths, p) - - } - - meta := fim.Meta() - r := func() (hugio.ReadSeekCloser, error) { - return meta.Open() - } - - target := strings.TrimPrefix(meta.Path(), owner.File().Dir()) - - return owner.s.ResourceSpec.New( - resources.ResourceSourceDescriptor{ - TargetPaths: owner.getTargetPaths, - OpenReadSeekCloser: r, - FileInfo: fim, - RelTargetFilename: target, - TargetBasePaths: targetBasePaths, - }) -} - -func (proc *pagesProcessor) getSite(lang string) *Site { - if lang == "" { - return proc.h.Sites[0] - } - - for _, s := range proc.h.Sites { - if lang == s.Lang() { - return s - } - } - return proc.h.Sites[0] -} - -func (proc *pagesProcessor) copyFile(fim hugofs.FileMetaInfo) error { - meta := fim.Meta() - s := proc.getSite(meta.Lang()) - f, err := meta.Open() - if err != nil { - return errors.Wrap(err, "copyFile: failed to open") - } - - target := filepath.Join(s.PathSpec.GetTargetLanguageBasePath(), meta.Path()) - - defer f.Close() - - return s.publish(&s.PathSpec.ProcessingStats.Files, target, f) - -} - -func (proc *pagesProcessor) process(item interface{}) error { - send := func(p *pageState, err error) { - if err != nil { - proc.sendError(err) - } else { - proc.pagesChan <- p - } - } - - switch v := item.(type) { - // Page bundles mapped to their language. - case pageBundles: - for _, bundle := range v { - if proc.shouldSkip(bundle.header) { - continue - } - send(proc.newPageFromBundle(bundle)) - } - case hugofs.FileMetaInfo: - if proc.shouldSkip(v) { - return nil - } - meta := v.Meta() - - classifier := meta.Classifier() - switch classifier { - case files.ContentClassContent: - send(proc.newPageFromFi(v, nil)) - case files.ContentClassFile: - proc.sendError(proc.copyFile(v)) - default: - panic(fmt.Sprintf("invalid classifier: %q", classifier)) - } - default: - panic(fmt.Sprintf("unrecognized item type in Process: %T", item)) - } - - return nil -} - -func (proc *pagesProcessor) sendError(err error) { - if err == nil { - return - } - proc.h.SendError(err) -} - -func (proc *pagesProcessor) shouldSkip(fim hugofs.FileMetaInfo) bool { - return proc.sp.DisabledLanguages[fim.Meta().Lang()] -} - func stringSliceContains(k string, values ...string) bool { for _, v := range values { if k == v { diff --git a/hugolib/pages_capture_test.go b/hugolib/pages_capture_test.go index b5b2bf31c..4401ca6ed 100644 --- a/hugolib/pages_capture_test.go +++ b/hugolib/pages_capture_test.go @@ -19,8 +19,6 @@ import ( "path/filepath" "testing" - "github.com/pkg/errors" - "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/source" @@ -59,17 +57,11 @@ func TestPagesCapture(t *testing.T) { t.Run("Collect", func(t *testing.T) { c := qt.New(t) proc := &testPagesCollectorProcessor{} - coll := newPagesCollector(sourceSpec, loggers.NewErrorLogger(), nil, proc) + coll := newPagesCollector(sourceSpec, nil, loggers.NewErrorLogger(), nil, proc) c.Assert(coll.Collect(), qt.IsNil) c.Assert(len(proc.items), qt.Equals, 4) }) - t.Run("error in Wait", func(t *testing.T) { - c := qt.New(t) - coll := newPagesCollector(sourceSpec, loggers.NewErrorLogger(), nil, - &testPagesCollectorProcessor{waitErr: errors.New("failed")}) - c.Assert(coll.Collect(), qt.Not(qt.IsNil)) - }) } type testPagesCollectorProcessor struct { diff --git a/hugolib/pages_map.go b/hugolib/pages_map.go deleted file mode 100644 index 57b50cdee..000000000 --- a/hugolib/pages_map.go +++ /dev/null @@ -1,474 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "path" - "path/filepath" - "strings" - "sync" - - "github.com/gohugoio/hugo/common/maps" - - radix "github.com/armon/go-radix" - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/resources/page" -) - -func newPagesMap(s *Site) *pagesMap { - return &pagesMap{ - r: radix.New(), - s: s, - } -} - -type pagesMap struct { - r *radix.Tree - s *Site -} - -func (m *pagesMap) Get(key string) *pagesMapBucket { - key = m.cleanKey(key) - v, found := m.r.Get(key) - if !found { - return nil - } - - return v.(*pagesMapBucket) -} - -func (m *pagesMap) getKey(p *pageState) string { - if !p.File().IsZero() { - return m.cleanKey(p.File().Dir()) - } - return m.cleanKey(p.SectionsPath()) -} - -func (m *pagesMap) getOrCreateHome() *pageState { - var home *pageState - b, found := m.r.Get("/") - if !found { - home = m.s.newPage(page.KindHome) - m.addBucketFor("/", home, nil) - } else { - home = b.(*pagesMapBucket).owner - } - - return home -} - -func (m *pagesMap) initPageMeta(p *pageState, bucket *pagesMapBucket) error { - var err error - p.metaInit.Do(func() { - if p.metaInitFn != nil { - err = p.metaInitFn(bucket) - } - }) - return err -} - -func (m *pagesMap) initPageMetaFor(prefix string, bucket *pagesMapBucket) error { - parentBucket := m.parentBucket(prefix) - - m.mergeCascades(bucket, parentBucket) - - if err := m.initPageMeta(bucket.owner, bucket); err != nil { - return err - } - - if !bucket.view { - for _, p := range bucket.pages { - ps := p.(*pageState) - if err := m.initPageMeta(ps, bucket); err != nil { - return err - } - - for _, p := range ps.resources.ByType(pageResourceType) { - if err := m.initPageMeta(p.(*pageState), bucket); err != nil { - return err - } - } - } - - // Now that the metadata is initialized (with dates, draft set etc.) - // we can remove the pages that we for some reason should not include - // in this build. - tmp := bucket.pages[:0] - for _, x := range bucket.pages { - if m.s.shouldBuild(x) { - if x.(*pageState).m.headless { - bucket.headlessPages = append(bucket.headlessPages, x) - } else { - tmp = append(tmp, x) - } - - } - } - bucket.pages = tmp - } - - return nil -} - -func (m *pagesMap) createSectionIfNotExists(section string) { - key := m.cleanKey(section) - _, found := m.r.Get(key) - if !found { - kind := m.s.kindFromSectionPath(section) - p := m.s.newPage(kind, section) - m.addBucketFor(key, p, nil) - } -} - -func (m *pagesMap) addBucket(p *pageState) { - key := m.getKey(p) - - m.addBucketFor(key, p, nil) -} - -func (m *pagesMap) addBucketFor(key string, p *pageState, meta map[string]interface{}) *pagesMapBucket { - var isView bool - switch p.Kind() { - case page.KindTaxonomy, page.KindTaxonomyTerm: - isView = true - } - - disabled := !m.s.isEnabled(p.Kind()) - - var cascade map[string]interface{} - if p.bucket != nil { - cascade = p.bucket.cascade - } - - bucket := &pagesMapBucket{ - owner: p, - view: isView, - cascade: cascade, - meta: meta, - disabled: disabled, - } - - p.bucket = bucket - - m.r.Insert(key, bucket) - - return bucket -} - -func (m *pagesMap) addPage(p *pageState) { - if !p.IsPage() { - m.addBucket(p) - return - } - - if !m.s.isEnabled(page.KindPage) { - return - } - - key := m.getKey(p) - - var bucket *pagesMapBucket - - _, v, found := m.r.LongestPrefix(key) - if !found { - panic(fmt.Sprintf("[BUG] bucket with key %q not found", key)) - } - - bucket = v.(*pagesMapBucket) - bucket.pages = append(bucket.pages, p) -} - -func (m *pagesMap) assemblePageMeta() error { - var walkErr error - m.r.Walk(func(s string, v interface{}) bool { - bucket := v.(*pagesMapBucket) - - if err := m.initPageMetaFor(s, bucket); err != nil { - walkErr = err - return true - } - return false - }) - - return walkErr -} - -func (m *pagesMap) assembleTaxonomies(s *Site) error { - s.Taxonomies = make(TaxonomyList) - - type bucketKey struct { - plural string - termKey string - } - - // Temporary cache. - taxonomyBuckets := make(map[bucketKey]*pagesMapBucket) - - for singular, plural := range s.siteCfg.taxonomiesConfig { - s.Taxonomies[plural] = make(Taxonomy) - bkey := bucketKey{ - plural: plural, - } - - bucket := m.Get(plural) - - if bucket == nil { - // Create the page and bucket - n := s.newPage(page.KindTaxonomyTerm, plural) - - key := m.cleanKey(plural) - bucket = m.addBucketFor(key, n, nil) - if err := m.initPageMetaFor(key, bucket); err != nil { - return err - } - } - - if bucket.meta == nil { - bucket.meta = map[string]interface{}{ - "singular": singular, - "plural": plural, - } - } - - // Add it to the temporary cache. - taxonomyBuckets[bkey] = bucket - - // Taxonomy entries used in page front matter will be picked up later, - // but there may be some yet to be used. - pluralPrefix := m.cleanKey(plural) + "/" - m.r.WalkPrefix(pluralPrefix, func(k string, v interface{}) bool { - tb := v.(*pagesMapBucket) - termKey := strings.TrimPrefix(k, pluralPrefix) - if tb.meta == nil { - tb.meta = map[string]interface{}{ - "singular": singular, - "plural": plural, - "term": tb.owner.Title(), - "termKey": termKey, - } - } - - bucket.pages = append(bucket.pages, tb.owner) - bkey.termKey = termKey - taxonomyBuckets[bkey] = tb - - return false - }) - - } - - addTaxonomy := func(singular, plural, term string, weight int, p page.Page) error { - bkey := bucketKey{ - plural: plural, - } - - termKey := s.getTaxonomyKey(term) - - b1 := taxonomyBuckets[bkey] - - var b2 *pagesMapBucket - bkey.termKey = termKey - b, found := taxonomyBuckets[bkey] - if found { - b2 = b - } else { - - // Create the page and bucket - n := s.newTaxonomyPage(term, plural, termKey) - meta := map[string]interface{}{ - "singular": singular, - "plural": plural, - "term": term, - "termKey": termKey, - } - - key := m.cleanKey(path.Join(plural, termKey)) - b2 = m.addBucketFor(key, n, meta) - if err := m.initPageMetaFor(key, b2); err != nil { - return err - } - b1.pages = append(b1.pages, b2.owner) - taxonomyBuckets[bkey] = b2 - - } - - w := page.NewWeightedPage(weight, p, b2.owner) - - s.Taxonomies[plural].add(termKey, w) - - b1.owner.m.Dates.UpdateDateAndLastmodIfAfter(p) - b2.owner.m.Dates.UpdateDateAndLastmodIfAfter(p) - - return nil - } - - m.r.Walk(func(k string, v interface{}) bool { - b := v.(*pagesMapBucket) - if b.view { - return false - } - - for singular, plural := range s.siteCfg.taxonomiesConfig { - for _, p := range b.pages { - - vals := getParam(p, plural, false) - - w := getParamToLower(p, plural+"_weight") - weight, err := cast.ToIntE(w) - if err != nil { - m.s.Log.ERROR.Printf("Unable to convert taxonomy weight %#v to int for %q", w, p.Path()) - // weight will equal zero, so let the flow continue - } - - if vals != nil { - if v, ok := vals.([]string); ok { - for _, idx := range v { - if err := addTaxonomy(singular, plural, idx, weight, p); err != nil { - m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err) - } - } - } else if v, ok := vals.(string); ok { - if err := addTaxonomy(singular, plural, v, weight, p); err != nil { - m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err) - } - } else { - m.s.Log.ERROR.Printf("Invalid %s in %q\n", plural, p.Path()) - } - } - - } - } - return false - }) - - for _, plural := range s.siteCfg.taxonomiesConfig { - for k := range s.Taxonomies[plural] { - s.Taxonomies[plural][k].Sort() - } - } - - return nil -} - -func (m *pagesMap) cleanKey(key string) string { - key = filepath.ToSlash(strings.ToLower(key)) - key = strings.Trim(key, "/") - return "/" + key -} - -func (m *pagesMap) mergeCascades(b1, b2 *pagesMapBucket) { - if b1.cascade == nil { - b1.cascade = make(maps.Params) - } - if b2 != nil && b2.cascade != nil { - for k, v := range b2.cascade { - if _, found := b1.cascade[k]; !found { - b1.cascade[k] = v - } - } - } -} - -func (m *pagesMap) parentBucket(prefix string) *pagesMapBucket { - if prefix == "/" { - return nil - } - _, parentv, found := m.r.LongestPrefix(path.Dir(prefix)) - if !found { - panic(fmt.Sprintf("[BUG] parent bucket not found for %q", prefix)) - } - return parentv.(*pagesMapBucket) - -} - -func (m *pagesMap) withEveryPage(f func(p *pageState)) { - m.r.Walk(func(k string, v interface{}) bool { - b := v.(*pagesMapBucket) - f(b.owner) - if !b.view { - for _, p := range b.pages { - f(p.(*pageState)) - } - } - - return false - }) -} - -type pagesMapBucket struct { - // Set if the pages in this bucket is also present in another bucket. - view bool - - // Some additional metatadata attached to this node. - meta map[string]interface{} - - // Cascading front matter. - cascade map[string]interface{} - - owner *pageState // The branch node - - // When disableKinds is enabled for this node. - disabled bool - - // Used to navigate the sections tree - parent *pagesMapBucket - bucketSections []*pagesMapBucket - - pagesInit sync.Once - pages page.Pages - headlessPages page.Pages - - pagesAndSectionsInit sync.Once - pagesAndSections page.Pages - - sectionsInit sync.Once - sections page.Pages -} - -func (b *pagesMapBucket) isEmpty() bool { - return len(b.pages) == 0 && len(b.headlessPages) == 0 && len(b.bucketSections) == 0 -} - -func (b *pagesMapBucket) getPages() page.Pages { - b.pagesInit.Do(func() { - page.SortByDefault(b.pages) - }) - return b.pages -} - -func (b *pagesMapBucket) getPagesAndSections() page.Pages { - b.pagesAndSectionsInit.Do(func() { - var pas page.Pages - pas = append(pas, b.getPages()...) - for _, p := range b.bucketSections { - pas = append(pas, p.owner) - } - b.pagesAndSections = pas - page.SortByDefault(b.pagesAndSections) - }) - return b.pagesAndSections -} - -func (b *pagesMapBucket) getSections() page.Pages { - b.sectionsInit.Do(func() { - for _, p := range b.bucketSections { - b.sections = append(b.sections, p.owner) - } - page.SortByDefault(b.sections) - }) - - return b.sections -} diff --git a/hugolib/pages_process.go b/hugolib/pages_process.go new file mode 100644 index 000000000..af029fee9 --- /dev/null +++ b/hugolib/pages_process.go @@ -0,0 +1,198 @@ +// Copyright 2019 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 hugolib + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/source" + + "github.com/gohugoio/hugo/hugofs/files" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/hugofs" +) + +func newPagesProcessor(h *HugoSites, sp *source.SourceSpec) *pagesProcessor { + procs := make(map[string]pagesCollectorProcessorProvider) + for _, s := range h.Sites { + procs[s.Lang()] = &sitePagesProcessor{ + m: s.pageMap, + errorSender: s.h, + itemChan: make(chan interface{}, config.GetNumWorkerMultiplier()*2), + } + } + return &pagesProcessor{ + procs: procs, + } +} + +type pagesCollectorProcessorProvider interface { + Process(item interface{}) error + Start(ctx context.Context) context.Context + Wait() error +} + +type pagesProcessor struct { + // Per language/Site + procs map[string]pagesCollectorProcessorProvider +} + +func (proc *pagesProcessor) Process(item interface{}) error { + switch v := item.(type) { + // Page bundles mapped to their language. + case pageBundles: + for _, vv := range v { + proc.getProcFromFi(vv.header).Process(vv) + } + case hugofs.FileMetaInfo: + proc.getProcFromFi(v).Process(v) + default: + panic(fmt.Sprintf("unrecognized item type in Process: %T", item)) + + } + + return nil +} + +func (proc *pagesProcessor) Start(ctx context.Context) context.Context { + for _, p := range proc.procs { + ctx = p.Start(ctx) + } + return ctx +} + +func (proc *pagesProcessor) Wait() error { + var err error + for _, p := range proc.procs { + if e := p.Wait(); e != nil { + err = e + } + } + return err +} + +func (proc *pagesProcessor) getProcFromFi(fi hugofs.FileMetaInfo) pagesCollectorProcessorProvider { + if p, found := proc.procs[fi.Meta().Lang()]; found { + return p + } + return defaultPageProcessor +} + +type nopPageProcessor int + +func (nopPageProcessor) Process(item interface{}) error { + return nil +} + +func (nopPageProcessor) Start(ctx context.Context) context.Context { + return context.Background() +} + +func (nopPageProcessor) Wait() error { + return nil +} + +var defaultPageProcessor = new(nopPageProcessor) + +type sitePagesProcessor struct { + m *pageMap + errorSender herrors.ErrorSender + + itemChan chan interface{} + itemGroup *errgroup.Group +} + +func (p *sitePagesProcessor) Process(item interface{}) error { + p.itemChan <- item + return nil +} + +func (p *sitePagesProcessor) Start(ctx context.Context) context.Context { + p.itemGroup, ctx = errgroup.WithContext(ctx) + p.itemGroup.Go(func() error { + for item := range p.itemChan { + if err := p.doProcess(item); err != nil { + return err + } + } + return nil + }) + return ctx +} + +func (p *sitePagesProcessor) Wait() error { + close(p.itemChan) + return p.itemGroup.Wait() +} + +func (p *sitePagesProcessor) copyFile(fim hugofs.FileMetaInfo) error { + meta := fim.Meta() + f, err := meta.Open() + if err != nil { + return errors.Wrap(err, "copyFile: failed to open") + } + + s := p.m.s + + target := filepath.Join(s.PathSpec.GetTargetLanguageBasePath(), meta.Path()) + + defer f.Close() + + return s.publish(&s.PathSpec.ProcessingStats.Files, target, f) + +} + +func (p *sitePagesProcessor) doProcess(item interface{}) error { + m := p.m + switch v := item.(type) { + case *fileinfoBundle: + if err := m.AddFilesBundle(v.header, v.resources...); err != nil { + return err + } + case hugofs.FileMetaInfo: + if p.shouldSkip(v) { + return nil + } + meta := v.Meta() + + classifier := meta.Classifier() + switch classifier { + case files.ContentClassContent: + if err := m.AddFilesBundle(v); err != nil { + return err + } + case files.ContentClassFile: + if err := p.copyFile(v); err != nil { + return err + } + default: + panic(fmt.Sprintf("invalid classifier: %q", classifier)) + } + default: + panic(fmt.Sprintf("unrecognized item type in Process: %T", item)) + } + return nil + +} + +func (p *sitePagesProcessor) shouldSkip(fim hugofs.FileMetaInfo) bool { + // TODO(ep) unify + return p.m.s.SourceSpec.DisabledLanguages[fim.Meta().Lang()] +} diff --git a/hugolib/site.go b/hugolib/site.go index bbcbcd27a..34e5ad156 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -100,10 +100,10 @@ type Site struct { *PageCollections - Taxonomies TaxonomyList + taxonomies TaxonomyList Sections Taxonomy - Info SiteInfo + Info *SiteInfo language *langs.Language @@ -163,9 +163,28 @@ type Site struct { init *siteInit } +func (s *Site) Taxonomies() TaxonomyList { + s.init.taxonomies.Do() + return s.taxonomies +} + +type taxonomiesConfig map[string]string + +func (t taxonomiesConfig) Values() []viewName { + var vals []viewName + for k, v := range t { + vals = append(vals, viewName{singular: k, plural: v}) + } + sort.Slice(vals, func(i, j int) bool { + return vals[i].plural < vals[j].plural + }) + + return vals +} + type siteConfigHolder struct { sitemap config.Sitemap - taxonomiesConfig map[string]string + taxonomiesConfig taxonomiesConfig timeout time.Duration hasCJKLanguage bool enableEmoji bool @@ -176,12 +195,14 @@ type siteInit struct { prevNext *lazy.Init prevNextInSection *lazy.Init menus *lazy.Init + taxonomies *lazy.Init } func (init *siteInit) Reset() { init.prevNext.Reset() init.prevNextInSection.Reset() init.menus.Reset() + init.taxonomies.Reset() } func (s *Site) initInit(init *lazy.Init, pctx pageContext) bool { @@ -198,65 +219,87 @@ func (s *Site) prepareInits() { var init lazy.Init s.init.prevNext = init.Branch(func() (interface{}, error) { - regularPages := s.findWorkPagesByKind(page.KindPage) + regularPages := s.RegularPages() for i, p := range regularPages { - if p.posNextPrev == nil { + np, ok := p.(nextPrevProvider) + if !ok { + continue + } + + pos := np.getNextPrev() + if pos == nil { continue } - p.posNextPrev.nextPage = nil - p.posNextPrev.prevPage = nil + + pos.nextPage = nil + pos.prevPage = nil if i > 0 { - p.posNextPrev.nextPage = regularPages[i-1] + pos.nextPage = regularPages[i-1] } if i < len(regularPages)-1 { - p.posNextPrev.prevPage = regularPages[i+1] + pos.prevPage = regularPages[i+1] } } return nil, nil }) s.init.prevNextInSection = init.Branch(func() (interface{}, error) { - var rootSection []int - // TODO(bep) cm attach this to the bucket. - for i, p1 := range s.workAllPages { - if p1.IsPage() && p1.Section() == "" { - rootSection = append(rootSection, i) - } - if p1.IsSection() { - sectionPages := p1.RegularPages() - for i, p2 := range sectionPages { - p2s := p2.(*pageState) - if p2s.posNextPrevSection == nil { - continue - } - p2s.posNextPrevSection.nextPage = nil - p2s.posNextPrevSection.prevPage = nil + var sections page.Pages + s.home.treeRef.m.collectSectionsRecursiveIncludingSelf(s.home.treeRef.key, func(n *contentNode) { + sections = append(sections, n.p) + }) - if i > 0 { - p2s.posNextPrevSection.nextPage = sectionPages[i-1] - } + setNextPrev := func(pas page.Pages) { + for i, p := range pas { + np, ok := p.(nextPrevInSectionProvider) + if !ok { + continue + } - if i < len(sectionPages)-1 { - p2s.posNextPrevSection.prevPage = sectionPages[i+1] - } + pos := np.getNextPrevInSection() + if pos == nil { + continue + } + + pos.nextPage = nil + pos.prevPage = nil + + if i > 0 { + pos.nextPage = pas[i-1] + } + + if i < len(pas)-1 { + pos.prevPage = pas[i+1] } } } - for i, j := range rootSection { - p := s.workAllPages[j] - if i > 0 { - p.posNextPrevSection.nextPage = s.workAllPages[rootSection[i-1]] - } + for _, sect := range sections { + treeRef := sect.(treeRefProvider).getTreeRef() - if i < len(rootSection)-1 { - p.posNextPrevSection.prevPage = s.workAllPages[rootSection[i+1]] - } + var pas page.Pages + treeRef.m.collectPages(treeRef.key+cmBranchSeparator, func(c *contentNode) { + pas = append(pas, c.p) + }) + page.SortByDefault(pas) + + setNextPrev(pas) } + // The root section only goes one level down. + treeRef := s.home.getTreeRef() + + var pas page.Pages + treeRef.m.collectPages(treeRef.key+cmBranchSeparator, func(c *contentNode) { + pas = append(pas, c.p) + }) + page.SortByDefault(pas) + + setNextPrev(pas) + return nil, nil }) @@ -265,6 +308,11 @@ func (s *Site) prepareInits() { return nil, nil }) + s.init.taxonomies = init.Branch(func() (interface{}, error) { + err := s.pageMap.assembleTaxonomies() + return nil, err + }) + } type siteRenderingContext struct { @@ -279,14 +327,15 @@ func (s *Site) Menus() navigation.Menus { func (s *Site) initRenderFormats() { formatSet := make(map[string]bool) formats := output.Formats{} - for _, p := range s.workAllPages { - for _, f := range p.m.configuredOutputFormats { + s.pageMap.pageTrees.WalkRenderable(func(s string, n *contentNode) bool { + for _, f := range n.p.m.configuredOutputFormats { if !formatSet[f.Name] { formats = append(formats, f) formatSet[f.Name] = true } } - } + return false + }) // Add the per kind configured output formats for _, kind := range allKindsInPages { @@ -345,8 +394,6 @@ func (s *Site) reset() *Site { // newSite creates a new site with the given configuration. func newSite(cfg deps.DepsCfg) (*Site, error) { - c := newPageCollections() - if cfg.Language == nil { cfg.Language = langs.NewDefaultLanguage(cfg.Cfg) } @@ -385,6 +432,17 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { return nil, err } + if disabledKinds[kindRSS] { + // Legacy + tmp := siteOutputFormatsConfig[:0] + for _, x := range siteOutputFormatsConfig { + if !strings.EqualFold(x.Name, "rss") { + tmp = append(tmp, x) + } + } + siteOutputFormatsConfig = tmp + } + outputFormats, err := createSiteOutputFormats(siteOutputFormatsConfig, cfg.Language) if err != nil { return nil, err @@ -435,18 +493,23 @@ func newSite(cfg deps.DepsCfg) (*Site, error) { } s := &Site{ - PageCollections: c, - language: cfg.Language, - disabledKinds: disabledKinds, - titleFunc: titleFunc, - relatedDocsHandler: page.NewRelatedDocsHandler(relatedContentConfig), - outputFormats: outputFormats, - rc: &siteRenderingContext{output.HTMLFormat}, - outputFormatsConfig: siteOutputFormatsConfig, - mediaTypesConfig: siteMediaTypesConfig, - frontmatterHandler: frontMatterHandler, + + language: cfg.Language, + disabledKinds: disabledKinds, + + outputFormats: outputFormats, + outputFormatsConfig: siteOutputFormatsConfig, + mediaTypesConfig: siteMediaTypesConfig, + enableInlineShortcodes: cfg.Language.GetBool("enableInlineShortcodes"), siteCfg: siteConfig, + + titleFunc: titleFunc, + + rc: &siteRenderingContext{output.HTMLFormat}, + + frontmatterHandler: frontMatterHandler, + relatedDocsHandler: page.NewRelatedDocsHandler(relatedContentConfig), } s.prepareInits() @@ -595,7 +658,7 @@ func (s *SiteInfo) Menus() navigation.Menus { // TODO(bep) type func (s *SiteInfo) Taxonomies() interface{} { - return s.s.Taxonomies + return s.s.Taxonomies() } func (s *SiteInfo) Params() maps.Params { @@ -734,7 +797,7 @@ func (s *siteRefLinker) refLink(ref string, source interface{}, relative bool, o if refURL.Path != "" { var err error - target, err = s.s.getPageNew(p, refURL.Path) + target, err = s.s.getPageRef(p, refURL.Path) var pos text.Position if err != nil || target == nil { if p, ok := source.(text.Positioner); ok { @@ -988,7 +1051,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro OutputFormats: site.outputFormatsConfig, } site.Deps, err = first.Deps.ForLanguage(depsCfg, func(d *deps.Deps) error { - d.Site = &site.Info + d.Site = site.Info return nil }) if err != nil { @@ -1189,7 +1252,7 @@ func (s *Site) initializeSiteInfo() error { } } - s.Info = SiteInfo{ + s.Info = &SiteInfo{ title: lang.GetString("title"), Author: lang.GetStringMap("author"), Social: lang.GetStringMapString("social"), @@ -1231,11 +1294,17 @@ func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) { func (s *Site) readAndProcessContent(filenames ...string) error { sourceSpec := source.NewSourceSpec(s.PathSpec, s.BaseFs.Content.Fs) - proc := newPagesProcessor(s.h, sourceSpec, len(filenames) > 0) + proc := newPagesProcessor(s.h, sourceSpec) - c := newPagesCollector(sourceSpec, s.Log, s.h.ContentChanges, proc, filenames...) + c := newPagesCollector(sourceSpec, s.h.content, s.Log, s.h.ContentChanges, proc, filenames...) - return c.Collect() + if err := c.Collect(); err != nil { + return err + } + + s.h.content = newPageMaps(s.h) + + return nil } func (s *Site) getMenusFromConfig() navigation.Menus { @@ -1309,35 +1378,45 @@ func (s *Site) assembleMenus() { sectionPagesMenu := s.Info.sectionPagesMenu if sectionPagesMenu != "" { - for _, p := range s.workAllPages { - if p.Kind() == page.KindSection { - // From Hugo 0.22 we have nested sections, but until we get a - // feel of how that would work in this setting, let us keep - // this menu for the top level only. - id := p.Section() - if _, ok := flat[twoD{sectionPagesMenu, id}]; ok { - continue - } - - me := navigation.MenuEntry{Identifier: id, - Name: p.LinkTitle(), - Weight: p.Weight(), - Page: p} - flat[twoD{sectionPagesMenu, me.KeyName()}] = &me + s.pageMap.sections.Walk(func(s string, v interface{}) bool { + p := v.(*contentNode).p + if p.IsHome() { + return false } - } + // From Hugo 0.22 we have nested sections, but until we get a + // feel of how that would work in this setting, let us keep + // this menu for the top level only. + id := p.Section() + if _, ok := flat[twoD{sectionPagesMenu, id}]; ok { + return false + } + + me := navigation.MenuEntry{Identifier: id, + Name: p.LinkTitle(), + Weight: p.Weight(), + Page: p} + flat[twoD{sectionPagesMenu, me.KeyName()}] = &me + + return false + }) + } // Add menu entries provided by pages - for _, p := range s.workAllPages { + s.pageMap.pageTrees.WalkRenderable(func(ss string, n *contentNode) bool { + p := n.p + for name, me := range p.pageMenus.menus() { if _, ok := flat[twoD{name, me.KeyName()}]; ok { - s.SendError(p.wrapError(errors.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name))) + err := p.wrapError(errors.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name)) + s.Log.WARN.Println(err) continue } flat[twoD{name, me.KeyName()}] = me } - } + + return false + }) // Create Children Menus First for _, e := range flat { @@ -1410,15 +1489,17 @@ func (s *Site) resetBuildState(sourceChanged bool) { s.init.Reset() if sourceChanged { - s.PageCollections = newPageCollectionsFromPages(s.rawAllPages) - for _, p := range s.rawAllPages { + s.PageCollections = newPageCollections(s.pageMap) + s.pageMap.withEveryBundlePage(func(p *pageState) bool { p.pagePages = &pagePages{} p.parent = nil p.Scratcher = maps.NewScratcher() - } + return false + }) } else { - s.pagesMap.withEveryPage(func(p *pageState) { + s.pageMap.withEveryBundlePage(func(p *pageState) bool { p.Scratcher = maps.NewScratcher() + return false }) } } @@ -1613,6 +1694,7 @@ func (s *Site) kindFromFileInfoOrSections(fi *fileInfo, sections []string) strin return s.kindFromSections(sections) } + return page.KindPage } @@ -1640,26 +1722,21 @@ func (s *Site) kindFromSectionPath(sectionPath string) string { return page.KindSection } -func (s *Site) newTaxonomyPage(title string, sections ...string) *pageState { - p, err := newPageFromMeta( - map[string]interface{}{"title": title}, - &pageMeta{ - s: s, - kind: page.KindTaxonomy, - sections: sections, - }) +func (s *Site) newPage( + n *contentNode, + parentbBucket *pagesMapBucket, + kind, title string, + sections ...string) *pageState { - if err != nil { - panic(err) + m := map[string]interface{}{} + if title != "" { + m["title"] = title } - return p - -} - -func (s *Site) newPage(kind string, sections ...string) *pageState { p, err := newPageFromMeta( - map[string]interface{}{}, + n, + parentbBucket, + m, &pageMeta{ s: s, kind: kind, diff --git a/hugolib/site_benchmark_new_test.go b/hugolib/site_benchmark_new_test.go index cd5d94bb0..a97c0808c 100644 --- a/hugolib/site_benchmark_new_test.go +++ b/hugolib/site_benchmark_new_test.go @@ -379,6 +379,29 @@ func TestBenchmarkSiteNew(b *testing.T) { } } +func TestBenchmarkSiteDeepContentEdit(t *testing.T) { + b := getBenchmarkSiteDeepContent(t).Running() + b.Build(BuildCfg{}) + + p := b.H.Sites[0].RegularPages()[12] + + b.EditFiles(p.File().Filename(), fmt.Sprintf(`--- +title: %s +--- + +Edited!!`, p.Title())) + + counters := &testCounters{} + + b.Build(BuildCfg{testCounters: counters}) + + // We currently rebuild all the language versions of the same content file. + // We could probably optimize that case, but it's not trivial. + b.Assert(int(counters.contentRenderCounter), qt.Equals, 4) + b.AssertFileContent("public"+p.RelPermalink()+"index.html", "Edited!!") + +} + func BenchmarkSiteNew(b *testing.B) { rnd := rand.New(rand.NewSource(32)) benchmarks := getBenchmarkSiteNewTestCases() diff --git a/hugolib/site_output.go b/hugolib/site_output.go index b0a5d4f35..b57794400 100644 --- a/hugolib/site_output.go +++ b/hugolib/site_output.go @@ -23,24 +23,35 @@ import ( ) func createDefaultOutputFormats(allFormats output.Formats, cfg config.Provider) map[string]output.Formats { - rssOut, _ := allFormats.GetByName(output.RSSFormat.Name) + rssOut, rssFound := allFormats.GetByName(output.RSSFormat.Name) htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name) robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name) sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name) - return map[string]output.Formats{ + defaultListTypes := output.Formats{htmlOut} + if rssFound { + defaultListTypes = append(defaultListTypes, rssOut) + } + + m := map[string]output.Formats{ page.KindPage: {htmlOut}, - page.KindHome: {htmlOut, rssOut}, - page.KindSection: {htmlOut, rssOut}, - page.KindTaxonomy: {htmlOut, rssOut}, - page.KindTaxonomyTerm: {htmlOut, rssOut}, + page.KindHome: defaultListTypes, + page.KindSection: defaultListTypes, + page.KindTaxonomy: defaultListTypes, + page.KindTaxonomyTerm: defaultListTypes, // Below are for consistency. They are currently not used during rendering. - kindRSS: {rssOut}, kindSitemap: {sitemapOut}, kindRobotsTXT: {robotsOut}, kind404: {htmlOut}, } + // May be disabled + if rssFound { + m[kindRSS] = output.Formats{rssOut} + } + + return m + } func createSiteOutputFormats(allFormats output.Formats, cfg config.Provider) (map[string]output.Formats, error) { diff --git a/hugolib/site_render.go b/hugolib/site_render.go index dd79a7264..5327cbfc9 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -77,22 +77,17 @@ func (s *Site) renderPages(ctx *siteRenderContext) error { cfg := ctx.cfg - if !cfg.PartialReRender && ctx.outIdx == 0 && len(s.headlessPages) > 0 { - wg.Add(1) - go headlessPagesPublisher(s, wg) - } - -L: - for _, page := range s.workAllPages { - if cfg.shouldRender(page) { + s.pageMap.pageTrees.Walk(func(ss string, n *contentNode) bool { + if cfg.shouldRender(n.p) { select { case <-s.h.Done(): - break L + return true default: - pages <- page + pages <- n.p } } - } + return false + }) close(pages) @@ -107,15 +102,6 @@ L: return nil } -func headlessPagesPublisher(s *Site, wg *sync.WaitGroup) { - defer wg.Done() - for _, p := range s.headlessPages { - if err := p.renderResources(); err != nil { - s.SendError(p.errorf(err, "failed to render page resources")) - } - } -} - func pageRenderer( ctx *siteRenderContext, s *Site, @@ -126,15 +112,15 @@ func pageRenderer( defer wg.Done() for p := range pages { - f := p.outputFormat() - - // TODO(bep) get rid of this odd construct. RSS is an output format. - if f.Name == "RSS" && !s.isEnabled(kindRSS) { - continue + if p.m.buildConfig.PublishResources { + if err := p.renderResources(); err != nil { + s.SendError(p.errorf(err, "failed to render page resources")) + continue + } } - if err := p.renderResources(); err != nil { - s.SendError(p.errorf(err, "failed to render page resources")) + if !p.render { + // Nothing more to do for this page. continue } @@ -145,7 +131,7 @@ func pageRenderer( } if !found { - s.logMissingLayout("", p.Kind(), f.Name) + s.logMissingLayout("", p.Kind(), p.f.Name) continue } @@ -235,10 +221,6 @@ func (s *Site) renderPaginator(p *pageState, templ tpl.Template) error { } func (s *Site) render404() error { - if !s.isEnabled(kind404) { - return nil - } - p, err := newPageStandalone(&pageMeta{ s: s, kind: kind404, @@ -253,6 +235,10 @@ func (s *Site) render404() error { return err } + if !p.render { + return nil + } + var d output.LayoutDescriptor d.Kind = kind404 @@ -274,10 +260,6 @@ func (s *Site) render404() error { } func (s *Site) renderSitemap() error { - if !s.isEnabled(kindSitemap) { - return nil - } - p, err := newPageStandalone(&pageMeta{ s: s, kind: kindSitemap, @@ -291,6 +273,10 @@ func (s *Site) renderSitemap() error { return err } + if !p.render { + return nil + } + targetPath := p.targetPaths().TargetFilename if targetPath == "" { @@ -303,10 +289,6 @@ func (s *Site) renderSitemap() error { } func (s *Site) renderRobotsTXT() error { - if !s.isEnabled(kindRobotsTXT) { - return nil - } - if !s.Cfg.GetBool("enableRobotsTXT") { return nil } @@ -324,6 +306,10 @@ func (s *Site) renderRobotsTXT() error { return err } + if !p.render { + return nil + } + templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt") return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", p.targetPaths().TargetFilename, p, templ) @@ -332,15 +318,16 @@ func (s *Site) renderRobotsTXT() error { // renderAliases renders shell pages that simply have a redirect in the header. func (s *Site) renderAliases() error { - for _, p := range s.workAllPages { - + var err error + s.pageMap.pageTrees.WalkRenderable(func(ss string, n *contentNode) bool { + p := n.p if len(p.Aliases()) == 0 { - continue + return false } for _, of := range p.OutputFormats() { if !of.Format.IsHTML { - continue + return false } plink := of.Permalink() @@ -372,14 +359,16 @@ func (s *Site) renderAliases() error { a = path.Join(lang, a) } - if err := s.writeDestAlias(a, plink, f, p); err != nil { - return err + err = s.writeDestAlias(a, plink, f, p) + if err != nil { + return true } } } - } + return false + }) - return nil + return err } // renderMainLanguageRedirect creates a redirect to the main language home, diff --git a/hugolib/site_sections_test.go b/hugolib/site_sections_test.go index eba8a4391..81196be7f 100644 --- a/hugolib/site_sections_test.go +++ b/hugolib/site_sections_test.go @@ -303,7 +303,7 @@ PAG|{{ .Title }}|{{ $sect.InSection . }} c := qt.New(t) sections := strings.Split(test.sections, ",") p := s.getPage(page.KindSection, sections...) - c.Assert(p, qt.Not(qt.IsNil)) + c.Assert(p, qt.Not(qt.IsNil), qt.Commentf(fmt.Sprint(sections))) if p.Pages() != nil { c.Assert(p.Data().(page.Data).Pages(), deepEqualsPages, p.Pages()) diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 096c4d480..cd8c87c43 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -905,16 +905,16 @@ func TestWeightedTaxonomies(t *testing.T) { writeSourcesToSource(t, "content", fs, sources...) s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - if s.Taxonomies["tags"]["a"][0].Page.Title() != "foo" { - t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title()) + if s.Taxonomies()["tags"]["a"][0].Page.Title() != "foo" { + t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies()["tags"]["a"][0].Page.Title()) } - if s.Taxonomies["categories"]["d"][0].Page.Title() != "bar" { - t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies["categories"]["d"][0].Page.Title()) + if s.Taxonomies()["categories"]["d"][0].Page.Title() != "bar" { + t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies()["categories"]["d"][0].Page.Title()) } - if s.Taxonomies["categories"]["e"][0].Page.Title() != "bza" { - t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies["categories"]["e"][0].Page.Title()) + if s.Taxonomies()["categories"]["e"][0].Page.Title() != "bza" { + t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies()["categories"]["e"][0].Page.Title()) } } @@ -1008,10 +1008,13 @@ func TestRefLinking(t *testing.T) { //test empty link, as well as fragment only link {"", "", true, ""}, } { - checkLinkCase(site, test.link, currentPage, test.relative, test.outputFormat, test.expected, t, i) - //make sure fragment links are also handled - checkLinkCase(site, test.link+"#intro", currentPage, test.relative, test.outputFormat, test.expected+"#intro", t, i) + t.Run(fmt.Sprint(i), func(t *testing.T) { + checkLinkCase(site, test.link, currentPage, test.relative, test.outputFormat, test.expected, t, i) + + //make sure fragment links are also handled + checkLinkCase(site, test.link+"#intro", currentPage, test.relative, test.outputFormat, test.expected+"#intro", t, i) + }) } // TODO: and then the failure cases. diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go index e159f8c13..7c0644d51 100644 --- a/hugolib/taxonomy_test.go +++ b/hugolib/taxonomy_test.go @@ -50,7 +50,7 @@ YAML frontmatter with tags and categories taxonomy.` s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) st := make([]string, 0) - for _, t := range s.Taxonomies["tags"].ByCount() { + for _, t := range s.Taxonomies()["tags"].ByCount() { st = append(st, t.Page().Title()+":"+t.Name) } @@ -166,9 +166,10 @@ permalinkeds: } for taxonomy, count := range taxonomyTermPageCounts { + msg := qt.Commentf(taxonomy) term := s.getPage(page.KindTaxonomyTerm, taxonomy) - b.Assert(term, qt.Not(qt.IsNil)) - b.Assert(len(term.Pages()), qt.Equals, count, qt.Commentf(taxonomy)) + b.Assert(term, qt.Not(qt.IsNil), msg) + b.Assert(len(term.Pages()), qt.Equals, count, msg) for _, p := range term.Pages() { b.Assert(p.Kind(), qt.Equals, page.KindTaxonomy) @@ -258,8 +259,18 @@ title: "This is S3s" s := b.H.Sites[0] - ta := s.findPagesByKind(page.KindTaxonomy) - te := s.findPagesByKind(page.KindTaxonomyTerm) + filterbyKind := func(kind string) page.Pages { + var pages page.Pages + for _, p := range s.Pages() { + if p.Kind() == kind { + pages = append(pages, p) + } + } + return pages + } + + ta := filterbyKind(page.KindTaxonomy) + te := filterbyKind(page.KindTaxonomyTerm) b.Assert(len(te), qt.Equals, 4) b.Assert(len(ta), qt.Equals, 7) @@ -353,9 +364,6 @@ categories: ["regular"] } -// See https://github.com/gohugoio/hugo/issues/6222 -// We need to revisit this once we figure out what to do with the -// draft etc _index pages, but for now we need to avoid the crash. func TestTaxonomiesIndexDraft(t *testing.T) { t.Parallel() @@ -366,9 +374,18 @@ title: "The Categories" draft: true --- -This is the invisible content. +Content. -`) +`, + "page.md", `--- +title: "The Page" +categories: ["cool"] +--- + +Content. + +`, + ) b.WithTemplates("index.html", ` {{ range .Site.Pages }} @@ -378,7 +395,145 @@ This is the invisible content. b.Build(BuildCfg{}) - // We publish the index page, but the content will be empty. - b.AssertFileContent("public/index.html", " /categories/|The Categories|0||") + b.AssertFileContentFn("public/index.html", func(s string) bool { + return !strings.Contains(s, "categories") + }) + +} + +// https://github.com/gohugoio/hugo/issues/6173 +func TestTaxonomiesWithBundledResources(t *testing.T) { + b := newTestSitesBuilder(t) + b.WithTemplates("_default/list.html", ` +List {{ .Title }}: +{{ range .Resources }} +Resource: {{ .RelPermalink }}|{{ .MediaType }} +{{ end }} + `) + + b.WithContent("p1.md", `--- +title: Page +categories: ["funny"] +--- + `, + "categories/_index.md", "---\ntitle: Categories Page\n---", + "categories/data.json", "Category data", + "categories/funny/_index.md", "---\ntitle: Funnny Category\n---", + "categories/funny/funnydata.json", "Category funny data", + ) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/categories/index.html", `Resource: /categories/data.json|application/json`) + b.AssertFileContent("public/categories/funny/index.html", `Resource: /categories/funny/funnydata.json|application/json`) + +} + +func TestTaxonomiesRemoveOne(t *testing.T) { + b := newTestSitesBuilder(t).Running() + b.WithTemplates("index.html", ` + {{ $cats := .Site.Taxonomies.categories.cats }} + {{ if $cats }} + Len cats: {{ len $cats }} + {{ range $cats }} + Cats:|{{ .Page.RelPermalink }}| + {{ end }} + {{ end }} + {{ $funny := .Site.Taxonomies.categories.funny }} + {{ if $funny }} + Len funny: {{ len $funny }} + {{ range $funny }} + Funny:|{{ .Page.RelPermalink }}| + {{ end }} + {{ end }} + `) + + b.WithContent("p1.md", `--- +title: Page +categories: ["funny", "cats"] +--- + `, "p2.md", `--- +title: Page2 +categories: ["funny", "cats"] +--- + `, + ) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` +Len cats: 2 +Len funny: 2 +Cats:|/p1/| +Cats:|/p2/| +Funny:|/p1/| +Funny:|/p2/|`) + + // Remove one category from one of the pages. + b.EditFiles("content/p1.md", `--- +title: Page +categories: ["funny"] +--- + `) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` +Len cats: 1 +Len funny: 2 +Cats:|/p2/| +Funny:|/p1/| +Funny:|/p2/|`) + +} + +//https://github.com/gohugoio/hugo/issues/6590 +func TestTaxonomiesListPages(t *testing.T) { + b := newTestSitesBuilder(t) + b.WithTemplates("_default/list.html", ` + +{{ template "print-taxo" "categories.cats" }} +{{ template "print-taxo" "categories.funny" }} + +{{ define "print-taxo" }} +{{ $node := index site.Taxonomies (split $ ".") }} +{{ if $node }} +Len {{ $ }}: {{ len $node }} +{{ range $node }} + {{ $ }}:|{{ .Page.RelPermalink }}| +{{ end }} +{{ else }} +{{ $ }} not found. +{{ end }} +{{ end }} + `) + + b.WithContent("_index.md", `--- +title: Home +categories: ["funny", "cats"] +--- + `, "blog/p1.md", `--- +title: Page1 +categories: ["funny"] +--- + `, "blog/_index.md", `--- +title: Blog Section +categories: ["cats"] +--- + `, + ) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` + +Len categories.cats: 2 +categories.cats:|/blog/| +categories.cats:|/| + +Len categories.funny: 2 +categories.funny:|/| +categories.funny:|/blog/p1/| +`) } diff --git a/hugolib/template_test.go b/hugolib/template_test.go index ccf5d103e..99e57c75f 100644 --- a/hugolib/template_test.go +++ b/hugolib/template_test.go @@ -16,6 +16,7 @@ package hugolib import ( "fmt" "path/filepath" + "strings" "testing" "github.com/gohugoio/hugo/identity" @@ -656,23 +657,6 @@ func collectIdentities(set map[identity.Identity]bool, provider identity.Provide } } -func printRecursiveIdentities(level int, id identity.Provider) { - if level == 0 { - fmt.Println(id.GetIdentity(), "===>") - } - if ids, ok := id.(identity.IdentitiesProvider); ok { - level++ - for _, id := range ids.GetIdentities() { - printRecursiveIdentities(level, id) - } - } else { - ident(level) - fmt.Println("ID", id) - } -} - -func ident(n int) { - for i := 0; i < n; i++ { - fmt.Print(" ") - } +func ident(level int) string { + return strings.Repeat(" ", level) } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index f43ac6907..ac6fe4348 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -11,6 +11,8 @@ import ( "time" "unicode/utf8" + "github.com/gohugoio/hugo/htesting" + "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/parser/metadecoders" @@ -750,7 +752,7 @@ func (s *sitesBuilder) AssertObject(expected string, object interface{}) { if expected != got { fmt.Println(got) - diff := helpers.DiffStrings(expected, got) + diff := htesting.DiffStrings(expected, got) s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got) } } @@ -775,6 +777,12 @@ func (s *sitesBuilder) GetPage(ref string) page.Page { return p } +func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page { + p, err := s.H.Sites[0].getPageNew(p, ref) + s.Assert(err, qt.IsNil) + return p +} + func newTestHelper(cfg config.Provider, fs *hugofs.Fs, t testing.TB) testHelper { return testHelper{ Cfg: cfg, diff --git a/hugolib/translations.go b/hugolib/translations.go index 072ce33e5..76beafba9 100644 --- a/hugolib/translations.go +++ b/hugolib/translations.go @@ -21,7 +21,8 @@ func pagesToTranslationsMap(sites []*Site) map[string]page.Pages { out := make(map[string]page.Pages) for _, s := range sites { - for _, p := range s.workAllPages { + s.pageMap.pageTrees.Walk(func(ss string, n *contentNode) bool { + p := n.p // TranslationKey is implemented for all page types. base := p.TranslationKey() @@ -32,7 +33,9 @@ func pagesToTranslationsMap(sites []*Site) map[string]page.Pages { pageTranslations = append(pageTranslations, p) out[base] = pageTranslations - } + + return false + }) } return out @@ -40,14 +43,15 @@ func pagesToTranslationsMap(sites []*Site) map[string]page.Pages { func assignTranslationsToPages(allTranslations map[string]page.Pages, sites []*Site) { for _, s := range sites { - for _, p := range s.workAllPages { + s.pageMap.pageTrees.Walk(func(ss string, n *contentNode) bool { + p := n.p base := p.TranslationKey() translations, found := allTranslations[base] if !found { - continue + return false } - p.setTranslations(translations) - } + return false + }) } } diff --git a/parser/metadecoders/format.go b/parser/metadecoders/format.go index 4f81528c3..9e9cc2e1f 100644 --- a/parser/metadecoders/format.go +++ b/parser/metadecoders/format.go @@ -18,8 +18,6 @@ import ( "strings" "github.com/gohugoio/hugo/media" - - "github.com/gohugoio/hugo/parser/pageparser" ) type Format string @@ -72,22 +70,6 @@ func FormatFromMediaType(m media.Type) Format { return "" } -// FormatFromFrontMatterType will return empty if not supported. -func FormatFromFrontMatterType(typ pageparser.ItemType) Format { - switch typ { - case pageparser.TypeFrontMatterJSON: - return JSON - case pageparser.TypeFrontMatterORG: - return ORG - case pageparser.TypeFrontMatterTOML: - return TOML - case pageparser.TypeFrontMatterYAML: - return YAML - default: - return "" - } -} - // FormatFromContentString tries to detect the format (JSON, YAML or TOML) // in the given string. // It return an empty string if no format could be detected. diff --git a/parser/metadecoders/format_test.go b/parser/metadecoders/format_test.go index 74d105010..2f625935e 100644 --- a/parser/metadecoders/format_test.go +++ b/parser/metadecoders/format_test.go @@ -18,8 +18,6 @@ import ( "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/parser/pageparser" - qt "github.com/frankban/quicktest" ) @@ -57,22 +55,6 @@ func TestFormatFromMediaType(t *testing.T) { } } -func TestFormatFromFrontMatterType(t *testing.T) { - c := qt.New(t) - for _, test := range []struct { - typ pageparser.ItemType - expect Format - }{ - {pageparser.TypeFrontMatterJSON, JSON}, - {pageparser.TypeFrontMatterTOML, TOML}, - {pageparser.TypeFrontMatterYAML, YAML}, - {pageparser.TypeFrontMatterORG, ORG}, - {pageparser.TypeIgnore, ""}, - } { - c.Assert(FormatFromFrontMatterType(test.typ), qt.Equals, test.expect) - } -} - func TestFormatFromContentString(t *testing.T) { t.Parallel() c := qt.New(t) diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go index acdb09587..f73eee706 100644 --- a/parser/pageparser/pageparser.go +++ b/parser/pageparser/pageparser.go @@ -22,6 +22,7 @@ import ( "io" "io/ioutil" + "github.com/gohugoio/hugo/parser/metadecoders" "github.com/pkg/errors" ) @@ -43,6 +44,61 @@ func Parse(r io.Reader, cfg Config) (Result, error) { return parseSection(r, cfg, lexIntroSection) } +type ContentFrontMatter struct { + Content []byte + FrontMatter map[string]interface{} + FrontMatterFormat metadecoders.Format +} + +// ParseFrontMatterAndContent is a convenience method to extract front matter +// and content from a content page. +func ParseFrontMatterAndContent(r io.Reader) (ContentFrontMatter, error) { + var cf ContentFrontMatter + + psr, err := Parse(r, Config{}) + if err != nil { + return cf, err + } + + var frontMatterSource []byte + + iter := psr.Iterator() + + walkFn := func(item Item) bool { + if frontMatterSource != nil { + // The rest is content. + cf.Content = psr.Input()[item.Pos:] + // Done + return false + } else if item.IsFrontMatter() { + cf.FrontMatterFormat = FormatFromFrontMatterType(item.Type) + frontMatterSource = item.Val + } + return true + + } + + iter.PeekWalk(walkFn) + + cf.FrontMatter, err = metadecoders.Default.UnmarshalToMap(frontMatterSource, cf.FrontMatterFormat) + return cf, err +} + +func FormatFromFrontMatterType(typ ItemType) metadecoders.Format { + switch typ { + case TypeFrontMatterJSON: + return metadecoders.JSON + case TypeFrontMatterORG: + return metadecoders.ORG + case TypeFrontMatterTOML: + return metadecoders.TOML + case TypeFrontMatterYAML: + return metadecoders.YAML + default: + return "" + } +} + // ParseMain parses starting with the main section. Used in tests. func ParseMain(r io.Reader, cfg Config) (Result, error) { return parseSection(r, cfg, lexMainSection) diff --git a/parser/pageparser/pageparser_test.go b/parser/pageparser/pageparser_test.go index f54376c33..f7f719938 100644 --- a/parser/pageparser/pageparser_test.go +++ b/parser/pageparser/pageparser_test.go @@ -16,6 +16,9 @@ package pageparser import ( "strings" "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/parser/metadecoders" ) func BenchmarkParse(b *testing.B) { @@ -69,3 +72,19 @@ This is some summary. This is some summary. This is some summary. This is some s } } } + +func TestFormatFromFrontMatterType(t *testing.T) { + c := qt.New(t) + for _, test := range []struct { + typ ItemType + expect metadecoders.Format + }{ + {TypeFrontMatterJSON, metadecoders.JSON}, + {TypeFrontMatterTOML, metadecoders.TOML}, + {TypeFrontMatterYAML, metadecoders.YAML}, + {TypeFrontMatterORG, metadecoders.ORG}, + {TypeIgnore, ""}, + } { + c.Assert(FormatFromFrontMatterType(test.typ), qt.Equals, test.expect) + } +} diff --git a/resources/image_cache.go b/resources/image_cache.go index f57d73ede..1888b457f 100644 --- a/resources/image_cache.go +++ b/resources/image_cache.go @@ -35,32 +35,25 @@ type imageCache struct { store map[string]*resourceAdapter } -func (c *imageCache) isInCache(key string) bool { - c.mu.RLock() - _, found := c.store[c.normalizeKey(key)] - c.mu.RUnlock() - return found -} - -func (c *imageCache) deleteByPrefix(prefix string) { +func (c *imageCache) deleteIfContains(s string) { c.mu.Lock() defer c.mu.Unlock() - prefix = c.normalizeKey(prefix) + s = c.normalizeKeyBase(s) for k := range c.store { - if strings.HasPrefix(k, prefix) { + if strings.Contains(k, s) { delete(c.store, k) } } } +// The cache key is a lowecase path with Unix style slashes and it always starts with +// a leading slash. func (c *imageCache) normalizeKey(key string) string { - // It is a path with Unix style slashes and it always starts with a leading slash. - key = filepath.ToSlash(key) - if !strings.HasPrefix(key, "/") { - key = "/" + key - } + return "/" + c.normalizeKeyBase(key) +} - return key +func (c *imageCache) normalizeKeyBase(key string) string { + return strings.Trim(strings.ToLower(filepath.ToSlash(key)), "/") } func (c *imageCache) clear() { @@ -74,6 +67,7 @@ func (c *imageCache) getOrCreate( createImage func() (*imageResource, image.Image, error)) (*resourceAdapter, error) { relTarget := parent.relTargetPathFromConfig(conf) memKey := parent.relTargetPathForRel(relTarget.path(), false, false, false) + memKey = c.normalizeKey(memKey) // For the file cache we want to generate and store it once if possible. fileKeyPath := relTarget diff --git a/resources/image_test.go b/resources/image_test.go index c5564a5cb..f98d9f4bb 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -598,6 +598,7 @@ func TestImageOperationsGolden(t *testing.T) { } resized, err := orig.Fill("400x200 center") + c.Assert(err, qt.IsNil) for _, filter := range filters { resized, err := resized.Filter(filter) diff --git a/resources/page/page.go b/resources/page/page.go index 28094a4a9..c096cb726 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -23,8 +23,8 @@ import ( "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/compare" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/related" @@ -133,7 +133,7 @@ type PageMetaProvider interface { // BundleType returns the bundle type: "leaf", "branch" or an empty string if it is none. // See https://gohugo.io/content-management/page-bundles/ - BundleType() string + BundleType() files.ContentClass // A configured description. Description() string diff --git a/resources/page/page_marshaljson.autogen.go b/resources/page/page_marshaljson.autogen.go index 6e08210ac..47f385e8a 100644 --- a/resources/page/page_marshaljson.autogen.go +++ b/resources/page/page_marshaljson.autogen.go @@ -20,6 +20,7 @@ import ( "github.com/bep/gitmap" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/navigation" @@ -112,7 +113,7 @@ func MarshalPageToJSON(p Page) ([]byte, error) { PublishDate time.Time ExpiryDate time.Time Aliases []string - BundleType string + BundleType files.ContentClass Description string Draft bool IsHome bool diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index 19c7068e0..16663ab39 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -19,6 +19,8 @@ import ( "html/template" "time" + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/hugofs" "github.com/bep/gitmap" @@ -83,7 +85,7 @@ func (p *nopPage) BaseFileName() string { return "" } -func (p *nopPage) BundleType() string { +func (p *nopPage) BundleType() files.ContentClass { return "" } diff --git a/resources/page/pagemeta/pagemeta.go b/resources/page/pagemeta/pagemeta.go index 07e5c5673..1f8afdc18 100644 --- a/resources/page/pagemeta/pagemeta.go +++ b/resources/page/pagemeta/pagemeta.go @@ -13,9 +13,59 @@ package pagemeta +import ( + "github.com/mitchellh/mapstructure" +) + type URLPath struct { URL string Permalink string Slug string Section string } + +var defaultBuildConfig = BuildConfig{ + List: true, + Render: true, + PublishResources: true, + set: true, +} + +// BuildConfig holds configuration options about how to handle a Page in Hugo's +// build process. +type BuildConfig struct { + // Whether to add it to any of the page collections. + // Note that the page can still be found with .Site.GetPage. + List bool + + // Whether to render it. + Render bool + + // Whether to publish its resources. These will still be published on demand, + // but enabling this can be useful if the originals (e.g. images) are + // never used. + PublishResources bool + + set bool // BuildCfg is non-zero if this is set to true. +} + +// Disable sets all options to their off value. +func (b *BuildConfig) Disable() { + b.List = false + b.Render = false + b.PublishResources = false + b.set = true +} + +func (b BuildConfig) IsZero() bool { + return !b.set +} + +func DecodeBuildConfig(m interface{}) (BuildConfig, error) { + b := defaultBuildConfig + if m == nil { + return b, nil + } + err := mapstructure.WeakDecode(m, &b) + return b, err +} diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 0d21faa51..dd28fa2cb 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -19,6 +19,8 @@ import ( "path/filepath" "time" + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/modules" "github.com/bep/gitmap" @@ -133,7 +135,7 @@ func (p *testPage) BaseFileName() string { panic("not implemented") } -func (p *testPage) BundleType() string { +func (p *testPage) BundleType() files.ContentClass { panic("not implemented") } diff --git a/resources/resource_spec.go b/resources/resource_spec.go index a992df355..89b28bf83 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -129,15 +129,8 @@ func (r *Spec) ClearCaches() { r.ResourceCache.clear() } -func (r *Spec) DeleteCacheByPrefix(prefix string) { - r.imageCache.deleteByPrefix(prefix) -} - -// TODO(bep) unify -func (r *Spec) IsInImageCache(key string) bool { - // This is used for cache pruning. We currently only have images, but we could - // imagine expanding on this. - return r.imageCache.isInCache(key) +func (r *Spec) DeleteBySubstring(s string) { + r.imageCache.deleteIfContains(s) } func (s *Spec) String() string { diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go index e005aff06..50dafae3c 100644 --- a/tpl/compare/compare.go +++ b/tpl/compare/compare.go @@ -111,6 +111,8 @@ func (n *Namespace) Eq(first interface{}, others ...interface{}) bool { return vv.Float() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return vv.Uint() + case reflect.String: + return vv.String() default: return v } diff --git a/tpl/compare/compare_test.go b/tpl/compare/compare_test.go index 3eb793d30..c21ca11bc 100644 --- a/tpl/compare/compare_test.go +++ b/tpl/compare/compare_test.go @@ -63,6 +63,8 @@ func (t tstEqerType1) String() string { return string(t) } +type stringType string + type tstCompareType int const ( @@ -391,6 +393,15 @@ func TestLessThanExtend(t *testing.T) { func TestCase(t *testing.T) { c := qt.New(t) + n := New(false) + + c.Assert(n.Eq("az", "az"), qt.Equals, true) + c.Assert(n.Eq("az", stringType("az")), qt.Equals, true) + +} + +func TestStringType(t *testing.T) { + c := qt.New(t) n := New(true) c.Assert(n.Lt("az", "Za"), qt.Equals, true) diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go index 12d8aafb5..daf99fdb4 100644 --- a/tpl/transform/remarshal_test.go +++ b/tpl/transform/remarshal_test.go @@ -16,8 +16,9 @@ package transform import ( "testing" + "github.com/gohugoio/hugo/htesting" + qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/helpers" "github.com/spf13/viper" ) @@ -99,7 +100,7 @@ title: Test Metadata converted, err := ns.Remarshal(v1.format, v2.data) c.Assert(err, qt.IsNil, fromTo) - diff := helpers.DiffStrings(v1.data, converted) + diff := htesting.DiffStrings(v1.data, converted) if len(diff) > 0 { t.Errorf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v", fromTo, v1.data, converted, diff) } @@ -147,7 +148,7 @@ Hugo = "Rules" c.Assert(err, qt.IsNil, fromTo) } - diff := helpers.DiffStrings(expected, converted) + diff := htesting.DiffStrings(expected, converted) if len(diff) > 0 { t.Fatalf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v\n", fromTo, expected, converted, diff) } |