diff options
author | Bjørn Erik Pedersen <[email protected]> | 2016-07-28 09:30:58 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2016-09-06 18:32:16 +0300 |
commit | 708bc78770a0b0361908f6404f57264c53252a95 (patch) | |
tree | 9b7e3a05b1e83a768bfa0dd96b61b07dd7917cfd | |
parent | f023dfd7636f73b11c94e86a05c6273941d52c58 (diff) | |
download | hugo-708bc78770a0b0361908f6404f57264c53252a95.tar.gz hugo-708bc78770a0b0361908f6404f57264c53252a95.zip |
Optimize the multilanguage build process
Work In Progress!
This commit makes a rework of the build and rebuild process to better suit a multi-site setup.
This also includes a complete overhaul of the site tests. Previous these were a messy mix that
were testing just small parts of the build chain, some of it testing code-paths not even used in
"real life". Now all tests that depends on a built site follows the same and real production code path.
See #2309
Closes #2211
Closes #477
Closes #1744
35 files changed, 1260 insertions, 987 deletions
diff --git a/commands/hugo.go b/commands/hugo.go index 959006557..9ad46b3bf 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -49,7 +49,7 @@ import ( // Hugo represents the Hugo sites to build. This variable is exported as it // is used by at least one external library (the Hugo caddy plugin). We should // provide a cleaner external API, but until then, this is it. -var Hugo hugolib.HugoSites +var Hugo *hugolib.HugoSites // Reset resets Hugo ready for a new full build. This is mainly only useful // for benchmark testing etc. via the CLI commands. @@ -715,11 +715,11 @@ func getDirList() []string { func buildSites(watching ...bool) (err error) { fmt.Println("Started building sites ...") w := len(watching) > 0 && watching[0] - return Hugo.Build(w, true) + return Hugo.Build(hugolib.BuildCfg{Watching: w, PrintStats: true}) } func rebuildSites(events []fsnotify.Event) error { - return Hugo.Rebuild(events, true) + return Hugo.Rebuild(hugolib.BuildCfg{PrintStats: true}, events...) } // NewWatcher creates a new watcher to watch filesystem events. diff --git a/commands/list.go b/commands/list.go index bc5bb557a..f47b4820c 100644 --- a/commands/list.go +++ b/commands/list.go @@ -53,7 +53,7 @@ var listDraftsCmd = &cobra.Command{ site := &hugolib.Site{} - if err := site.Process(); err != nil { + if err := site.PreProcess(hugolib.BuildCfg{}); err != nil { return newSystemError("Error Processing Source Content", err) } @@ -84,7 +84,7 @@ posted in the future.`, site := &hugolib.Site{} - if err := site.Process(); err != nil { + if err := site.PreProcess(hugolib.BuildCfg{}); err != nil { return newSystemError("Error Processing Source Content", err) } @@ -115,7 +115,7 @@ expired.`, site := &hugolib.Site{} - if err := site.Process(); err != nil { + if err := site.PreProcess(hugolib.BuildCfg{}); err != nil { return newSystemError("Error Processing Source Content", err) } diff --git a/commands/multilingual.go b/commands/multilingual.go index 7c43d15bc..4d0f6e107 100644 --- a/commands/multilingual.go +++ b/commands/multilingual.go @@ -11,30 +11,31 @@ import ( "github.com/spf13/viper" ) -func readMultilingualConfiguration() (hugolib.HugoSites, error) { - h := make(hugolib.HugoSites, 0) +func readMultilingualConfiguration() (*hugolib.HugoSites, error) { + sites := make([]*hugolib.Site, 0) multilingual := viper.GetStringMap("Multilingual") if len(multilingual) == 0 { // TODO(bep) multilingo langConfigsList = append(langConfigsList, hugolib.NewLanguage("en")) - h = append(h, hugolib.NewSite(hugolib.NewLanguage("en"))) - return h, nil + sites = append(sites, hugolib.NewSite(hugolib.NewLanguage("en"))) } - var err error + if len(multilingual) > 0 { + var err error - langConfigsList, err := toSortedLanguages(multilingual) + languages, err := toSortedLanguages(multilingual) - if err != nil { - return nil, fmt.Errorf("Failed to parse multilingual config: %s", err) - } + if err != nil { + return nil, fmt.Errorf("Failed to parse multilingual config: %s", err) + } + + for _, lang := range languages { + sites = append(sites, hugolib.NewSite(lang)) + } - for _, lang := range langConfigsList { - s := hugolib.NewSite(lang) - s.SetMultilingualConfig(lang, langConfigsList) - h = append(h, s) } - return h, nil + return hugolib.NewHugoSites(sites...) + } func toSortedLanguages(l map[string]interface{}) (hugolib.Languages, error) { diff --git a/helpers/url.go b/helpers/url.go index 927e3c87c..085f9e9fa 100644 --- a/helpers/url.go +++ b/helpers/url.go @@ -169,6 +169,17 @@ func AbsURL(path string) string { return MakePermalink(baseURL, path).String() } +// IsAbsURL determines whether the given path points to an absolute URL. +// TODO(bep) ml tests +func IsAbsURL(path string) bool { + url, err := url.Parse(path) + if err != nil { + return false + } + + return url.IsAbs() || strings.HasPrefix(path, "//") +} + // RelURL creates a URL relative to the BaseURL root. // Note: The result URL will not include the context root if canonifyURLs is enabled. func RelURL(path string) string { diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 18f807fbf..e668ff4c8 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -56,8 +56,8 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) { templ := tpl.New() p, _ := pageFromString(simplePageWithURL, path) p.Node.Site = &SiteInfo{ - AllPages: &(Pages{p}), - BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)), + rawAllPages: &(Pages{p}), + BaseURL: template.URL(helpers.SanitizeURLKeepTrailingSlash(baseURL)), } output, err := HandleShortcodes(in, p, templ) @@ -72,8 +72,7 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) { } func TestShortcodeHighlight(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() if !helpers.HasPygments() { t.Skip("Skip test as Pygments is not installed") diff --git a/hugolib/handler_test.go b/hugolib/handler_test.go index a84d528cb..fce29df44 100644 --- a/hugolib/handler_test.go +++ b/hugolib/handler_test.go @@ -25,8 +25,7 @@ import ( ) func TestDefaultHandler(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() sources := []source.ByteSource{ @@ -45,33 +44,30 @@ func TestDefaultHandler(t *testing.T) { viper.Set("verbose", true) s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - Lang: NewLanguage("en"), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}}, + Language: NewLanguage("en"), } - s.initializeSiteInfo() - - s.prepTemplates( + if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}", "head", "<head><script src=\"script.js\"></script></head>", - "head_abs", "<head><script src=\"/script.js\"></script></head>") - - // From site_test.go - createAndRenderPages(t, s) + "head_abs", "<head><script src=\"/script.js\"></script></head>"); err != nil { + t.Fatalf("Failed to render site: %s", err) + } tests := []struct { doc string expected string }{ - {filepath.FromSlash("sect/doc1.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, - {filepath.FromSlash("sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"}, - {filepath.FromSlash("sect/doc3.html"), "\n\n<h1 id=\"doc3\">doc3</h1>\n\n<p><em>some</em> content</p>\n"}, - {filepath.FromSlash("sect/doc3/img1.png"), string([]byte("‰PNG ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚"))}, - {filepath.FromSlash("sect/img2.gif"), string([]byte("GIF89a��€��ÿÿÿ���,�������D�;"))}, - {filepath.FromSlash("sect/img2.spf"), string([]byte("****FAKE-FILETYPE****"))}, - {filepath.FromSlash("doc7.html"), "<html><body>doc7 content</body></html>"}, - {filepath.FromSlash("sect/doc8.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + {filepath.FromSlash("public/sect/doc1.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, + {filepath.FromSlash("public/sect/doc2.html"), "<!doctype html><html><body>more content</body></html>"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\n<h1 id=\"doc3\">doc3</h1>\n\n<p><em>some</em> content</p>\n"}, + {filepath.FromSlash("public/sect/doc3/img1.png"), string([]byte("‰PNG ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚"))}, + {filepath.FromSlash("public/sect/img2.gif"), string([]byte("GIF89a��€��ÿÿÿ���,�������D�;"))}, + {filepath.FromSlash("public/sect/img2.spf"), string([]byte("****FAKE-FILETYPE****"))}, + {filepath.FromSlash("public/doc7.html"), "<html><body>doc7 content</body></html>"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, } for _, test := range tests { diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index dd8d3e5d2..2dd1bb9be 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -14,42 +14,119 @@ package hugolib import ( + "errors" + "strings" "time" - "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" + "github.com/fsnotify/fsnotify" + "github.com/spf13/hugo/source" + "github.com/spf13/hugo/tpl" jww "github.com/spf13/jwalterweatherman" ) // HugoSites represents the sites to build. Each site represents a language. -type HugoSites []*Site +type HugoSites struct { + Sites []*Site + + Multilingual *Multilingual +} + +func NewHugoSites(sites ...*Site) (*HugoSites, error) { + languages := make(Languages, len(sites)) + for i, s := range sites { + if s.Language == nil { + return nil, errors.New("Missing language for site") + } + languages[i] = s.Language + } + defaultLang := viper.GetString("DefaultContentLanguage") + if defaultLang == "" { + defaultLang = "en" + } + langConfig := &Multilingual{Languages: languages, DefaultLang: NewLanguage(defaultLang)} + + return &HugoSites{Multilingual: langConfig, Sites: sites}, nil +} // Reset resets the sites, making it ready for a full rebuild. // TODO(bep) multilingo func (h HugoSites) Reset() { - for i, s := range h { - h[i] = s.Reset() + for i, s := range h.Sites { + h.Sites[i] = s.Reset() } } +type BuildCfg struct { + // Whether we are in watch (server) mode + Watching bool + // Print build stats at the end of a build + PrintStats bool + // Skip rendering. Useful for testing. + skipRender bool + // Use this to add templates to use for rendering. + // Useful for testing. + withTemplate func(templ tpl.Template) error +} + // Build builds all sites. -func (h HugoSites) Build(watching, printStats bool) error { +func (h HugoSites) Build(config BuildCfg) error { + + if h.Sites == nil || len(h.Sites) == 0 { + return errors.New("No site(s) to build") + } + t0 := time.Now() - for _, site := range h { - t1 := time.Now() + // We should probably refactor the Site and pull up most of the logic from there to here, + // but that seems like a daunting task. + // So for now, if there are more than one site (language), + // we pre-process the first one, then configure all the sites based on that. + firstSite := h.Sites[0] + + for _, s := range h.Sites { + // TODO(bep) ml + s.Multilingual = h.Multilingual + s.RunMode.Watching = config.Watching + } + + if err := firstSite.PreProcess(config); err != nil { + return err + } - site.RunMode.Watching = watching + h.setupTranslations(firstSite) - if err := site.Build(); err != nil { + if len(h.Sites) > 1 { + // Initialize the rest + for _, site := range h.Sites[1:] { + site.Tmpl = firstSite.Tmpl + site.initializeSiteInfo() + } + } + + for _, s := range h.Sites { + + if err := s.PostProcess(); err != nil { return err } - if printStats { - site.Stats(t1) + + if !config.skipRender { + if err := s.Render(); err != nil { + return err + } + + } + + if config.PrintStats { + s.Stats() } + + // TODO(bep) ml lang in site.Info? + // TODO(bep) ml Page sorting? } - if printStats { + if config.PrintStats { jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds())) } @@ -58,25 +135,159 @@ func (h HugoSites) Build(watching, printStats bool) error { } // Rebuild rebuilds all sites. -func (h HugoSites) Rebuild(events []fsnotify.Event, printStats bool) error { +func (h HugoSites) Rebuild(config BuildCfg, events ...fsnotify.Event) error { t0 := time.Now() - for _, site := range h { - t1 := time.Now() + firstSite := h.Sites[0] - if err := site.ReBuild(events); err != nil { - return err + for _, s := range h.Sites { + s.resetBuildState() + } + + sourceChanged, err := firstSite.ReBuild(events) + + if err != nil { + return err + } + + // Assign pages to sites per translation. + h.setupTranslations(firstSite) + + for _, s := range h.Sites { + + if sourceChanged { + if err := s.PostProcess(); err != nil { + return err + } } - if printStats { - site.Stats(t1) + if !config.skipRender { + if err := s.Render(); err != nil { + return err + } + } + + if config.PrintStats { + s.Stats() } } - if printStats { + if config.PrintStats { jww.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds())) } return nil } + +func (s *HugoSites) setupTranslations(master *Site) { + + for _, p := range master.rawAllPages { + if p.Lang() == "" { + panic("Page language missing: " + p.Title) + } + + shouldBuild := p.shouldBuild() + + for i, site := range s.Sites { + if strings.HasPrefix(site.Language.Lang, p.Lang()) { + site.updateBuildStats(p) + if shouldBuild { + site.Pages = append(site.Pages, p) + p.Site = &site.Info + } + } + + if !shouldBuild { + continue + } + + if i == 0 { + site.AllPages = append(site.AllPages, p) + } + } + + for i := 1; i < len(s.Sites); i++ { + s.Sites[i].AllPages = s.Sites[0].AllPages + } + } + + if len(s.Sites) > 1 { + pages := s.Sites[0].AllPages + allTranslations := pagesToTranslationsMap(s.Multilingual, pages) + assignTranslationsToPages(allTranslations, pages) + } +} + +func (s *Site) updateBuildStats(page *Page) { + if page.IsDraft() { + s.draftCount++ + } + + if page.IsFuture() { + s.futureCount++ + } + + if page.IsExpired() { + s.expiredCount++ + } +} + +// Convenience func used in tests to build a single site/language excluding render phase. +func buildSiteSkipRender(s *Site, additionalTemplates ...string) error { + return doBuildSite(s, false, additionalTemplates...) +} + +// Convenience func used in tests to build a single site/language including render phase. +func buildAndRenderSite(s *Site, additionalTemplates ...string) error { + return doBuildSite(s, true, additionalTemplates...) +} + +// Convenience func used in tests to build a single site/language. +func doBuildSite(s *Site, render bool, additionalTemplates ...string) error { + sites, err := NewHugoSites(s) + if err != nil { + return err + } + + addTemplates := func(templ tpl.Template) error { + for i := 0; i < len(additionalTemplates); i += 2 { + err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) + if err != nil { + return err + } + } + return nil + } + + config := BuildCfg{skipRender: !render, withTemplate: addTemplates} + return sites.Build(config) +} + +// Convenience func used in tests. +func newHugoSitesFromSourceAndLanguages(input []source.ByteSource, languages Languages) (*HugoSites, error) { + if len(languages) == 0 { + panic("Must provide at least one language") + } + first := &Site{ + Source: &source.InMemorySource{ByteSource: input}, + Language: languages[0], + } + if len(languages) == 1 { + return NewHugoSites(first) + } + + sites := make([]*Site, len(languages)) + sites[0] = first + for i := 1; i < len(languages); i++ { + sites[i] = &Site{Language: languages[i]} + } + + return NewHugoSites(sites...) + +} + +// Convenience func used in tests. +func newHugoSitesFromLanguages(languages Languages) (*HugoSites, error) { + return newHugoSitesFromSourceAndLanguages(nil, languages) +} diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go new file mode 100644 index 000000000..fc4801115 --- /dev/null +++ b/hugolib/hugo_sites_test.go @@ -0,0 +1,522 @@ +package hugolib + +import ( + "fmt" + "strings" + "testing" + + "path/filepath" + + "os" + + "github.com/fsnotify/fsnotify" + "github.com/spf13/afero" + "github.com/spf13/hugo/helpers" + "github.com/spf13/hugo/hugofs" + "github.com/spf13/hugo/source" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + + jww "github.com/spf13/jwalterweatherman" +) + +func init() { + testCommonResetState() + jww.SetStdoutThreshold(jww.LevelError) + +} + +func testCommonResetState() { + hugofs.InitMemFs() + viper.Reset() + viper.Set("ContentDir", "content") + viper.Set("DataDir", "data") + viper.Set("I18nDir", "i18n") + viper.Set("themesDir", "themes") + viper.Set("LayoutDir", "layouts") + viper.Set("PublishDir", "public") + viper.Set("RSSUri", "rss") + + if err := hugofs.Source().Mkdir("content", 0755); err != nil { + panic("Content folder creation failed.") + } + +} + +func _TestMultiSites(t *testing.T) { + + sites := createMultiTestSites(t) + + err := sites.Build(BuildCfg{skipRender: true}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + enSite := sites.Sites[0] + + assert.Equal(t, "en", enSite.Language.Lang) + + if len(enSite.Pages) != 3 { + t.Fatal("Expected 3 english pages") + } + assert.Len(t, enSite.Source.Files(), 6, "should have 6 source files") + assert.Len(t, enSite.AllPages, 6, "should have 6 total pages (including translations)") + + doc1en := enSite.Pages[0] + permalink, err := doc1en.Permalink() + assert.NoError(t, err, "permalink call failed") + assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", permalink, "invalid doc1.en permalink") + assert.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself") + + doc2 := enSite.Pages[1] + permalink, err = doc2.Permalink() + assert.NoError(t, err, "permalink call failed") + assert.Equal(t, "http://example.com/blog/en/sect/doc2/", permalink, "invalid doc2 permalink") + + doc3 := enSite.Pages[2] + permalink, err = doc3.Permalink() + assert.NoError(t, err, "permalink call failed") + assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink") + + // TODO(bep) multilingo. Check this case. This has url set in frontmatter, but we must split into lang folders + // The assertion below was missing the /en prefix. + assert.Equal(t, "/en/superbob", doc3.URL(), "invalid url, was specified on doc3 TODO(bep)") + + assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next") + + doc1fr := doc1en.Translations()[0] + permalink, err = doc1fr.Permalink() + assert.NoError(t, err, "permalink call failed") + assert.Equal(t, "http://example.com/blog/fr/sect/doc1/", permalink, "invalid doc1fr permalink") + + assert.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation") + assert.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation") + assert.Equal(t, "fr", doc1fr.Language().Lang) + + doc4 := enSite.AllPages[4] + permalink, err = doc4.Permalink() + assert.NoError(t, err, "permalink call failed") + assert.Equal(t, "http://example.com/blog/fr/sect/doc4/", permalink, "invalid doc4 permalink") + assert.Len(t, doc4.Translations(), 0, "found translations for doc4") + + doc5 := enSite.AllPages[5] + permalink, err = doc5.Permalink() + assert.NoError(t, err, "permalink call failed") + assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink") + + // Taxonomies and their URLs + assert.Len(t, enSite.Taxonomies, 1, "should have 1 taxonomy") + tags := enSite.Taxonomies["tags"] + assert.Len(t, tags, 2, "should have 2 different tags") + assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1") + + frSite := sites.Sites[1] + + assert.Equal(t, "fr", frSite.Language.Lang) + assert.Len(t, frSite.Pages, 3, "should have 3 pages") + assert.Len(t, frSite.AllPages, 6, "should have 6 total pages (including translations)") + + for _, frenchPage := range frSite.Pages { + assert.Equal(t, "fr", frenchPage.Lang()) + } + +} + +func TestMultiSitesRebuild(t *testing.T) { + + sites := createMultiTestSites(t) + cfg := BuildCfg{} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + _, err = hugofs.Destination().Open("public/en/sect/doc2/index.html") + + if err != nil { + t.Fatalf("Unable to locate file") + } + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + assert.Len(t, enSite.Pages, 3) + assert.Len(t, frSite.Pages, 3) + + // Verify translations + docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") + assert.True(t, strings.Contains(docEn, "Hello"), "No Hello") + docFr := readDestination(t, "public/fr/sect/doc1/index.html") + assert.True(t, strings.Contains(docFr, "Bonjour"), "No Bonjour") + + for i, this := range []struct { + preFunc func(t *testing.T) + events []fsnotify.Event + assertFunc func(t *testing.T) + }{ + // * Remove doc + // * Add docs existing languages + // (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages). + // * Rename file + // * Change doc + // * Change a template + // * Change language file + { + nil, + []fsnotify.Event{{Name: "content/sect/doc2.en.md", Op: fsnotify.Remove}}, + func(t *testing.T) { + assert.Len(t, enSite.Pages, 2, "1 en removed") + + // Check build stats + assert.Equal(t, 1, enSite.draftCount, "Draft") + assert.Equal(t, 1, enSite.futureCount, "Future") + assert.Equal(t, 1, enSite.expiredCount, "Expired") + assert.Equal(t, 0, frSite.draftCount, "Draft") + assert.Equal(t, 1, frSite.futureCount, "Future") + assert.Equal(t, 1, frSite.expiredCount, "Expired") + }, + }, + { + func(t *testing.T) { + writeNewContentFile(t, "new_en_1", "2016-07-31", "content/new1.en.md", -5) + writeNewContentFile(t, "new_en_2", "1989-07-30", "content/new2.en.md", -10) + writeNewContentFile(t, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) + }, + []fsnotify.Event{ + {Name: "content/new1.en.md", Op: fsnotify.Create}, + {Name: "content/new2.en.md", Op: fsnotify.Create}, + {Name: "content/new1.fr.md", Op: fsnotify.Create}, + }, + func(t *testing.T) { + assert.Len(t, enSite.Pages, 4) + assert.Len(t, enSite.AllPages, 8) + assert.Len(t, frSite.Pages, 4) + assert.Equal(t, "new_fr_1", frSite.Pages[3].Title) + assert.Equal(t, "new_en_2", enSite.Pages[0].Title) + assert.Equal(t, "new_en_1", enSite.Pages[1].Title) + + rendered := readDestination(t, "public/en/new1/index.html") + assert.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }, + }, + { + func(t *testing.T) { + p := "content/sect/doc1.en.md" + doc1 := readSource(t, p) + doc1 += "CHANGED" + writeSource(t, p, doc1) + }, + []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, + func(t *testing.T) { + assert.Len(t, enSite.Pages, 4) + doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") + assert.True(t, strings.Contains(doc1, "CHANGED"), doc1) + + }, + }, + // Rename a file + { + func(t *testing.T) { + if err := hugofs.Source().Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { + t.Fatalf("Rename failed: %s", err) + } + }, + []fsnotify.Event{ + {Name: "content/new1renamed.en.md", Op: fsnotify.Rename}, + {Name: "content/new1.en.md", Op: fsnotify.Rename}, + }, + func(t *testing.T) { + assert.Len(t, enSite.Pages, 4, "Rename") + assert.Equal(t, "new_en_1", enSite.Pages[1].Title) + rendered := readDestination(t, "public/en/new1renamed/index.html") + assert.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }}, + { + // Change a template + func(t *testing.T) { + template := "layouts/_default/single.html" + templateContent := readSource(t, template) + templateContent += "{{ print \"Template Changed\"}}" + writeSource(t, template, templateContent) + }, + []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, + func(t *testing.T) { + assert.Len(t, enSite.Pages, 4) + assert.Len(t, enSite.AllPages, 8) + assert.Len(t, frSite.Pages, 4) + doc1 := readDestination(t, "public/en/sect/doc1-slug/index.html") + assert.True(t, strings.Contains(doc1, "Template Changed"), doc1) + }, + }, + { + // Change a language file + func(t *testing.T) { + languageFile := "i18n/fr.yaml" + langContent := readSource(t, languageFile) + langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) + writeSource(t, languageFile, langContent) + }, + []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, + func(t *testing.T) { + assert.Len(t, enSite.Pages, 4) + assert.Len(t, enSite.AllPages, 8) + assert.Len(t, frSite.Pages, 4) + docEn := readDestination(t, "public/en/sect/doc1-slug/index.html") + assert.True(t, strings.Contains(docEn, "Hello"), "No Hello") + docFr := readDestination(t, "public/fr/sect/doc1/index.html") + assert.True(t, strings.Contains(docFr, "Salut"), "No Salut") + }, + }, + } { + + if this.preFunc != nil { + this.preFunc(t) + } + err = sites.Rebuild(cfg, this.events...) + + if err != nil { + t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) + } + + this.assertFunc(t) + } + +} + +func createMultiTestSites(t *testing.T) *HugoSites { + // General settings + hugofs.InitMemFs() + + viper.Set("DefaultExtension", "html") + viper.Set("baseurl", "http://example.com/blog") + viper.Set("DisableSitemap", false) + viper.Set("DisableRSS", false) + viper.Set("RSSUri", "index.xml") + viper.Set("Taxonomies", map[string]string{"tag": "tags"}) + viper.Set("Permalinks", map[string]string{"other": "/somewhere/else/:filename"}) + + // Add some layouts + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "_default/single.html"), + []byte("Single: {{ .Title }}|{{ i18n \"hello\" }} {{ .Content }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "_default/list.html"), + []byte("List: {{ .Title }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("layouts", "index.html"), + []byte("Home: {{ .Title }}|{{ .IsHome }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add some language files + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("i18n", "en.yaml"), + []byte(` +- id: hello + translation: "Hello" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + if err := afero.WriteFile(hugofs.Source(), + filepath.Join("i18n", "fr.yaml"), + []byte(` +- id: hello + translation: "Bonjour" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + + // Sources + sources := []source.ByteSource{ + {filepath.FromSlash("sect/doc1.en.md"), []byte(`--- +title: doc1 +slug: doc1-slug +tags: + - tag1 +publishdate: "2000-01-01" +--- +# doc1 +*some content* +NOTE: slug should be used as URL +`)}, + {filepath.FromSlash("sect/doc1.fr.md"), []byte(`--- +title: doc1 +tags: + - tag1 + - tag2 +publishdate: "2000-01-04" +--- +# doc1 +*quelque contenu* +NOTE: should be in the 'en' Page's 'Translations' field. +NOTE: date is after "doc3" +`)}, + {filepath.FromSlash("sect/doc2.en.md"), []byte(`--- +title: doc2 +publishdate: "2000-01-02" +--- +# doc2 +*some content* +NOTE: without slug, "doc2" should be used, without ".en" as URL +`)}, + {filepath.FromSlash("sect/doc3.en.md"), []byte(`--- +title: doc3 +publishdate: "2000-01-03" +tags: + - tag2 +url: /superbob +--- +# doc3 +*some content* +NOTE: third 'en' doc, should trigger pagination on home page. +`)}, + {filepath.FromSlash("sect/doc4.md"), []byte(`--- +title: doc4 +tags: + - tag1 +publishdate: "2000-01-05" +--- +# doc4 +*du contenu francophone* +NOTE: should use the DefaultContentLanguage and mark this doc as 'fr'. +NOTE: doesn't have any corresponding translation in 'en' +`)}, + {filepath.FromSlash("other/doc5.fr.md"), []byte(`--- +title: doc5 +publishdate: "2000-01-06" +--- +# doc5 +*autre contenu francophone* +NOTE: should use the "permalinks" configuration with :filename +`)}, + // Add some for the stats + {filepath.FromSlash("stats/expired.fr.md"), []byte(`--- +title: expired +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {filepath.FromSlash("stats/future.fr.md"), []byte(`--- +title: future +publishdate: "2100-01-06" +--- +# Future +`)}, + {filepath.FromSlash("stats/expired.en.md"), []byte(`--- +title: expired +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {filepath.FromSlash("stats/future.en.md"), []byte(`--- +title: future +publishdate: "2100-01-06" +--- +# Future +`)}, + {filepath.FromSlash("stats/draft.en.md"), []byte(`--- +title: expired +publishdate: "2000-01-06" +draft: true +--- +# Draft +`)}, + } + + // Multilingual settings + viper.Set("Multilingual", true) + en := NewLanguage("en") + viper.Set("DefaultContentLanguage", "fr") + viper.Set("paginate", "2") + + languages := NewLanguages(en, NewLanguage("fr")) + + // Hugo support using ByteSource's directly (for testing), + // but to make it more real, we write them to the mem file system. + for _, s := range sources { + if err := afero.WriteFile(hugofs.Source(), filepath.Join("content", s.Name), s.Content, 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } + } + _, err := hugofs.Source().Open("content/other/doc5.fr.md") + + if err != nil { + t.Fatalf("Unable to locate file") + } + sites, err := newHugoSitesFromLanguages(languages) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + if len(sites.Sites) != 2 { + t.Fatalf("Got %d sites", len(sites.Sites)) + } + + return sites +} + +func writeSource(t *testing.T, filename, content string) { + if err := afero.WriteFile(hugofs.Source(), filepath.FromSlash(filename), []byte(content), 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } +} + +func readDestination(t *testing.T, filename string) string { + return readFileFromFs(t, hugofs.Destination(), filename) +} + +func readSource(t *testing.T, filename string) string { + return readFileFromFs(t, hugofs.Source(), filename) +} + +func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { + filename = filepath.FromSlash(filename) + b, err := afero.ReadFile(fs, filename) + if err != nil { + // Print some debug info + root := strings.Split(filename, helpers.FilePathSeparator)[0] + afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + fmt.Println(" ", path) + } + + return nil + }) + t.Fatalf("Failed to read file: %s", err) + } + return string(b) +} + +const testPageTemplate = `--- +title: "%s" +publishdate: "%s" +weight: %d +--- +# Doc %s +` + +func newTestPage(title, date string, weight int) string { + return fmt.Sprintf(testPageTemplate, title, date, weight, title) +} + +func writeNewContentFile(t *testing.T, title, date, filename string, weight int) { + content := newTestPage(title, date, weight) + writeSource(t, filename, content) +} diff --git a/hugolib/i18n.go b/hugolib/i18n.go index 8caf30d7c..a98e51291 100644 --- a/hugolib/i18n.go +++ b/hugolib/i18n.go @@ -17,9 +17,12 @@ import ( "github.com/nicksnyder/go-i18n/i18n/bundle" "github.com/spf13/hugo/source" "github.com/spf13/hugo/tpl" + jww "github.com/spf13/jwalterweatherman" ) func loadI18n(sources []source.Input) error { + jww.DEBUG.Printf("Load I18n from %q", sources) + i18nBundle := bundle.New() for _, currentSource := range sources { diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go index 898f035d6..9ef4d09ad 100644 --- a/hugolib/menu_test.go +++ b/hugolib/menu_test.go @@ -201,9 +201,7 @@ func TestPageMenuWithIdentifier(t *testing.T) { } func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) { - - viper.Reset() - defer viper.Reset() + testCommonResetState() s := setupMenuTests(t, menuPageSources) @@ -241,8 +239,7 @@ func TestPageMenuWithDuplicateName(t *testing.T) { } func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) { - viper.Reset() - defer viper.Reset() + testCommonResetState() s := setupMenuTests(t, menuPageSources) @@ -260,8 +257,7 @@ func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.Byte } func TestPageMenu(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() s := setupMenuTests(t, menuPageSources) @@ -307,8 +303,7 @@ func TestPageMenu(t *testing.T) { } func TestMenuURL(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() s := setupMenuTests(t, menuPageSources) @@ -338,8 +333,7 @@ func TestMenuURL(t *testing.T) { // Issue #1934 func TestYAMLMenuWithMultipleEntries(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() ps1 := []byte(`--- title: "Yaml 1" @@ -377,8 +371,7 @@ func TestMenuWithUnicodeURLs(t *testing.T) { } func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("CanonifyURLs", canonifyURLs) @@ -403,8 +396,7 @@ func TestSectionPagesMenu(t *testing.T) { } func doTestSectionPagesMenu(canonifyUrls bool, t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("SectionPagesMenu", "spm") @@ -458,8 +450,7 @@ func doTestSectionPagesMenu(canonifyUrls bool, t *testing.T) { } func TestTaxonomyNodeMenu(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("CanonifyURLs", true) s := setupMenuTests(t, menuPageSources) @@ -502,8 +493,7 @@ func TestTaxonomyNodeMenu(t *testing.T) { } func TestMenuLimit(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() s := setupMenuTests(t, menuPageSources) m := *s.Menus["main"] @@ -545,8 +535,7 @@ func TestMenuSortByN(t *testing.T) { } func TestHomeNodeMenu(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("CanonifyURLs", true) viper.Set("UglyURLs", true) @@ -659,7 +648,7 @@ func findDescendantTestMenuEntry(parent *MenuEntry, id string, matcher func(me * return found } -func setupTestMenuState(s *Site, t *testing.T) { +func setupTestMenuState(t *testing.T) { menus, err := tomlToMap(confMenu1) if err != nil { @@ -672,7 +661,8 @@ func setupTestMenuState(s *Site, t *testing.T) { func setupMenuTests(t *testing.T, pageSources []source.ByteSource) *Site { s := createTestSite(pageSources) - setupTestMenuState(s, t) + + setupTestMenuState(t) testSiteSetup(s, t) return s @@ -681,18 +671,17 @@ func setupMenuTests(t *testing.T, pageSources []source.ByteSource) *Site { func createTestSite(pageSources []source.ByteSource) *Site { hugofs.InitMemFs() - s := &Site{ - Source: &source.InMemorySource{ByteSource: pageSources}, - Lang: newDefaultLanguage(), + return &Site{ + Source: &source.InMemorySource{ByteSource: pageSources}, + Language: newDefaultLanguage(), } - return s + } func testSiteSetup(s *Site, t *testing.T) { - s.Menus = Menus{} - s.initializeSiteInfo() - - createPagesAndMeta(t, s) + if err := buildSiteSkipRender(s); err != nil { + t.Fatalf("Sites build failed: %s", err) + } } func tomlToMap(s string) (map[string]interface{}, error) { diff --git a/hugolib/multilingual.go b/hugolib/multilingual.go index c75f504ef..0bcc2a697 100644 --- a/hugolib/multilingual.go +++ b/hugolib/multilingual.go @@ -45,6 +45,8 @@ func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } type Multilingual struct { Languages Languages + DefaultLang *Language + langMap map[string]*Language langMapInit sync.Once } @@ -60,7 +62,7 @@ func (ml *Multilingual) Language(lang string) *Language { } func (ml *Multilingual) enabled() bool { - return len(ml.Languages) > 0 + return len(ml.Languages) > 1 } func (l *Language) Params() map[string]interface{} { @@ -98,16 +100,6 @@ func (l *Language) Get(key string) interface{} { return viper.Get(key) } -// TODO(bep) multilingo move this to a constructor. -func (s *Site) SetMultilingualConfig(currentLang *Language, languages Languages) { - - ml := &Multilingual{ - Languages: languages, - } - viper.Set("Multilingual", ml.enabled()) - s.Multilingual = ml -} - func (s *Site) multilingualEnabled() bool { return s.Multilingual != nil && s.Multilingual.enabled() } @@ -118,5 +110,5 @@ func (s *Site) currentLanguageString() string { } func (s *Site) currentLanguage() *Language { - return s.Lang + return s.Language } diff --git a/hugolib/node.go b/hugolib/node.go index 77a26603a..3983a5192 100644 --- a/hugolib/node.go +++ b/hugolib/node.go @@ -18,9 +18,12 @@ import ( "path" "path/filepath" "sort" + "strings" "sync" "time" + "github.com/spf13/hugo/helpers" + "github.com/spf13/cast" ) @@ -243,11 +246,22 @@ func (n *Node) initTranslations() { } func (n *Node) addMultilingualWebPrefix(outfile string) string { + + if helpers.IsAbsURL(outfile) { + return outfile + } + + hadSlashSuffix := strings.HasSuffix(outfile, "/") + lang := n.Lang() if lang == "" || !n.Site.Multilingual { return outfile } - return "/" + path.Join(lang, outfile) + outfile = "/" + path.Join(lang, outfile) + if hadSlashSuffix { + outfile += "/" + } + return outfile } func (n *Node) addMultilingualFilesystemPrefix(outfile string) string { diff --git a/hugolib/page.go b/hugolib/page.go index d02472f97..4248ff893 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -833,6 +833,7 @@ func (p *Page) Menus() PageMenus { menuEntry.marshallMap(ime) } p.pageMenus[name] = &menuEntry + } } }) diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go index eae174517..a47bad85e 100644 --- a/hugolib/page_permalink_test.go +++ b/hugolib/page_permalink_test.go @@ -23,8 +23,7 @@ import ( ) func TestPermalink(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() tests := []struct { file string diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 8afb851ae..6fd797830 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -569,7 +569,7 @@ func TestPageWithDelimiter(t *testing.T) { func TestPageWithShortCodeInSummary(t *testing.T) { s := new(Site) - s.prepTemplates() + s.prepTemplates(nil) p, _ := NewPage("simple.md") _, err := p.ReadFrom(strings.NewReader(simplePageWithShortcodeInSummary)) if err != nil { @@ -644,7 +644,7 @@ func TestPageWithDate(t *testing.T) { } func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { - viper.Reset() + testCommonResetState() p, _ := NewPage("simple.md") _, err := p.ReadFrom(strings.NewReader(simplePageWithAllCJKRunes)) @@ -660,8 +660,7 @@ func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { } func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("HasCJKLanguage", true) @@ -679,8 +678,7 @@ func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { } func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("HasCJKLanguage", true) @@ -703,8 +701,7 @@ func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { } func TestWordCountWithIsCJKLanguageFalse(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("HasCJKLanguage", true) @@ -944,8 +941,7 @@ func TestSliceToLower(t *testing.T) { } func TestPagePaths(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("DefaultExtension", "html") siteParmalinksSetting := PermalinkOverrides{ diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go index 080e6bee9..b67f5dce5 100644 --- a/hugolib/pagination_test.go +++ b/hugolib/pagination_test.go @@ -192,8 +192,7 @@ func doTestPagerNoPages(t *testing.T, paginator *paginator) { } func TestPaginationURLFactory(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("PaginatePath", "zoo") unicode := newPaginationURLFactory("новости проекта") @@ -207,8 +206,7 @@ func TestPaginationURLFactory(t *testing.T) { } func TestPaginator(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() for _, useViper := range []bool{false, true} { doTestPaginator(t, useViper) @@ -216,8 +214,7 @@ func TestPaginator(t *testing.T) { } func doTestPaginator(t *testing.T, useViper bool) { - viper.Reset() - defer viper.Reset() + testCommonResetState() pagerSize := 5 if useViper { @@ -260,8 +257,7 @@ func doTestPaginator(t *testing.T, useViper bool) { } func TestPaginatorWithNegativePaginate(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("paginate", -1) s := newSiteDefaultLang() @@ -270,8 +266,7 @@ func TestPaginatorWithNegativePaginate(t *testing.T) { } func TestPaginate(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() for _, useViper := range []bool{false, true} { doTestPaginate(t, useViper) @@ -331,8 +326,7 @@ func TestInvalidOptions(t *testing.T) { } func TestPaginateWithNegativePaginate(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("paginate", -1) s := newSiteDefaultLang() @@ -354,8 +348,7 @@ func TestPaginatePages(t *testing.T) { // Issue #993 func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("paginate", 10) s := newSiteDefaultLang() @@ -373,8 +366,7 @@ func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) { } func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("paginate", 10) s := newSiteDefaultLang() diff --git a/hugolib/public/404.html b/hugolib/public/404.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/hugolib/public/404.html diff --git a/hugolib/public/index.html b/hugolib/public/index.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/hugolib/public/index.html diff --git a/hugolib/public/rss b/hugolib/public/rss new file mode 100644 index 000000000..bbf739012 --- /dev/null +++ b/hugolib/public/rss @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> + <channel> + <title></title> + <link>/rss/</link> + <description>Recent content on </description> + <generator>Hugo -- gohugo.io</generator> + <atom:link href="/rss/" rel="self" type="application/rss+xml" /> + + </channel> +</rss>
\ No newline at end of file diff --git a/hugolib/public/sitemap.xml b/hugolib/public/sitemap.xml new file mode 100644 index 000000000..f02c32c4c --- /dev/null +++ b/hugolib/public/sitemap.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + + <url> + <loc>/</loc> + </url> + +</urlset>
\ No newline at end of file diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go index 8e4b13db6..62be91522 100644 --- a/hugolib/robotstxt_test.go +++ b/hugolib/robotstxt_test.go @@ -30,8 +30,7 @@ const robotTxtTemplate = `User-agent: Googlebot ` func TestRobotsTXTOutput(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() @@ -39,29 +38,15 @@ func TestRobotsTXTOutput(t *testing.T) { viper.Set("enableRobotsTXT", true) s := &Site{ - Source: &source.InMemorySource{ByteSource: weightedSources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: weightedSources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - - s.prepTemplates("robots.txt", robotTxtTemplate) - - createPagesAndMeta(t, s) - - if err := s.renderHomePage(); err != nil { - t.Fatalf("Unable to RenderHomePage: %s", err) - } - - if err := s.renderSitemap(); err != nil { - t.Fatalf("Unable to RenderSitemap: %s", err) - } - - if err := s.renderRobotsTXT(); err != nil { - t.Fatalf("Unable to RenderRobotsTXT :%s", err) + if err := buildAndRenderSite(s, "robots.txt", robotTxtTemplate); err != nil { + t.Fatalf("Failed to build site: %s", err) } - robotsFile, err := hugofs.Destination().Open("robots.txt") + robotsFile, err := hugofs.Destination().Open("public/robots.txt") if err != nil { t.Fatalf("Unable to locate: robots.txt") diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go index 72ec25fca..f9f26cb7b 100644 --- a/hugolib/rss_test.go +++ b/hugolib/rss_test.go @@ -15,6 +15,7 @@ package hugolib import ( "bytes" + "path/filepath" "testing" "github.com/spf13/hugo/helpers" @@ -45,28 +46,23 @@ const rssTemplate = `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" </rss>` func TestRSSOutput(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() - rssURI := "customrss.xml" + rssURI := "public/customrss.xml" viper.Set("baseurl", "http://auth/bub/") viper.Set("RSSUri", rssURI) hugofs.InitMemFs() s := &Site{ - Source: &source.InMemorySource{ByteSource: weightedSources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: weightedSources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - s.prepTemplates("rss.xml", rssTemplate) - createPagesAndMeta(t, s) - - if err := s.renderHomePage(); err != nil { - t.Fatalf("Unable to RenderHomePage: %s", err) + if err := buildAndRenderSite(s, "rss.xml", rssTemplate); err != nil { + t.Fatalf("Failed to build site: %s", err) } - file, err := hugofs.Destination().Open(rssURI) + file, err := hugofs.Destination().Open(filepath.Join("public", rssURI)) if err != nil { t.Fatalf("Unable to locate: %s", rssURI) diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index d0832d2ea..5069fa195 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -261,8 +261,7 @@ func TestFigureImgWidth(t *testing.T) { } func TestHighlight(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() if !helpers.HasPygments() { t.Skip("Skip test as Pygments is not installed") @@ -414,11 +413,11 @@ func TestExtractShortcodes(t *testing.T) { } func TestShortcodesInSite(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() baseURL := "http://foo/bar" viper.Set("DefaultExtension", "html") + viper.Set("DefaultContentLanguage", "en") viper.Set("baseurl", baseURL) viper.Set("UglyURLs", false) viper.Set("verbose", true) @@ -497,24 +496,31 @@ e`, } s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: false}}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: false}}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() + addTemplates := func(templ tpl.Template) error { + templ.AddTemplate("_default/single.html", "{{.Content}}") - s.loadTemplates() + templ.AddInternalShortcode("b.html", `b`) + templ.AddInternalShortcode("c.html", `c`) + templ.AddInternalShortcode("d.html", `d`) - s.Tmpl.AddTemplate("_default/single.html", "{{.Content}}") + return nil - s.Tmpl.AddInternalShortcode("b.html", `b`) - s.Tmpl.AddInternalShortcode("c.html", `c`) - s.Tmpl.AddInternalShortcode("d.html", `d`) + } - s.Tmpl.MarkReady() + sites, err := NewHugoSites(s) - createAndRenderPages(t, s) + if err != nil { + t.Fatalf("Failed to build site: %s", err) + } + + if err = sites.Build(BuildCfg{withTemplate: addTemplates}); err != nil { + t.Fatalf("Failed to build site: %s", err) + } for _, test := range tests { if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() { diff --git a/hugolib/site.go b/hugolib/site.go index eaf4fd95d..59b8379dc 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -22,7 +22,6 @@ import ( "os" "path" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -54,7 +53,10 @@ var testMode bool var defaultTimer *nitro.B -var distinctErrorLogger = helpers.NewDistinctErrorLogger() +var ( + distinctErrorLogger = helpers.NewDistinctErrorLogger() + distinctFeedbackLogger = helpers.NewDistinctFeedbackLogger() +) // Site contains all the information relevant for constructing a static // site. The basic flow of information is as follows: @@ -76,6 +78,7 @@ var distinctErrorLogger = helpers.NewDistinctErrorLogger() type Site struct { Pages Pages AllPages Pages + rawAllPages Pages Files []*source.File Tmpl tpl.Template Taxonomies TaxonomyList @@ -87,22 +90,23 @@ type Site struct { targets targetList targetListInit sync.Once RunMode runmode - Multilingual *Multilingual - draftCount int - futureCount int - expiredCount int - Data map[string]interface{} - Lang *Language + // TODO(bep ml remove + Multilingual *Multilingual + draftCount int + futureCount int + expiredCount int + Data map[string]interface{} + Language *Language } // TODO(bep) multilingo // Reset returns a new Site prepared for rebuild. func (s *Site) Reset() *Site { - return &Site{Lang: s.Lang, Multilingual: s.Multilingual} + return &Site{Language: s.Language, Multilingual: s.Multilingual} } func NewSite(lang *Language) *Site { - return &Site{Lang: lang} + return &Site{Language: lang} } func newSiteDefaultLang() *Site { @@ -117,19 +121,20 @@ type targetList struct { } type SiteInfo struct { - BaseURL template.URL - Taxonomies TaxonomyList - Authors AuthorList - Social SiteSocial - Sections Taxonomy - Pages *Pages // Includes only pages in this language - AllPages *Pages // Includes other translated pages, excluding those in this language. - Files *[]*source.File - Menus *Menus - Hugo *HugoInfo - Title string - RSSLink string - Author map[string]interface{} + BaseURL template.URL + Taxonomies TaxonomyList + Authors AuthorList + Social SiteSocial + Sections Taxonomy + Pages *Pages // Includes only pages in this language + AllPages *Pages // Includes other translated pages, excluding those in this language. + rawAllPages *Pages // Includes absolute all pages, including drafts etc. + Files *[]*source.File + Menus *Menus + Hugo *HugoInfo + Title string + RSSLink string + Author map[string]interface{} // TODO(bep) multilingo LanguageCode string DisqusShortname string @@ -204,7 +209,16 @@ func (s *SiteInfo) refLink(ref string, page *Page, relative bool) (string, error var link string if refURL.Path != "" { - for _, page := range []*Page(*s.AllPages) { + // We may be in a shortcode and a not finished site, so look it the + // "raw page" collection. + // This works, but it also means AllPages and Pages will be empty for other + // shortcode use, which may be a slap in the face for many. + // TODO(bep) ml move shortcode handling to a "pre-render" handler, which also + // will fix a few other problems. + for _, page := range []*Page(*s.rawAllPages) { + if !page.shouldBuild() { + continue + } refPath := filepath.FromSlash(refURL.Path) if page.Source.Path() == refPath || page.Source.LogicalName() == refPath { target = page @@ -396,54 +410,21 @@ func (s *Site) timerStep(step string) { s.timer.Step(step) } -func (s *Site) preRender() error { - return tpl.SetTranslateLang(s.Lang.Lang) -} - -func (s *Site) Build() (err error) { - - if err = s.Process(); err != nil { - return - } - - if err = s.preRender(); err != nil { - return - } - - if err = s.Render(); err != nil { - // Better reporting when the template is missing (commit 2bbecc7b) - jww.ERROR.Printf("Error rendering site: %s", err) - - jww.ERROR.Printf("Available templates:") - var keys []string - for _, template := range s.Tmpl.Templates() { - if name := template.Name(); name != "" { - keys = append(keys, name) - } - } - sort.Strings(keys) - for _, k := range keys { - jww.ERROR.Printf("\t%s\n", k) - } - - return - } - - return nil -} +// ReBuild partially rebuilds a site given the filesystem events. +// It returns whetever the content source was changed. +func (s *Site) ReBuild(events []fsnotify.Event) (bool, error) { -func (s *Site) ReBuild(events []fsnotify.Event) error { - // TODO(bep) multilingual this needs some rethinking with multiple sites + jww.DEBUG.Printf("Rebuild for events %q", events) s.timerStep("initialize rebuild") // First we need to determine what changed sourceChanged := []fsnotify.Event{} + sourceReallyChanged := []fsnotify.Event{} tmplChanged := []fsnotify.Event{} dataChanged := []fsnotify.Event{} - - var err error + i18nChanged := []fsnotify.Event{} // prevent spamming the log on changes logger := helpers.NewDistinctFeedbackLogger() @@ -451,6 +432,7 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { for _, ev := range events { // Need to re-read source if strings.HasPrefix(ev.Name, s.absContentDir()) { + logger.Println("Source changed", ev.Name) sourceChanged = append(sourceChanged, ev) } if strings.HasPrefix(ev.Name, s.absLayoutDir()) || strings.HasPrefix(ev.Name, s.absThemeDir()) { @@ -461,10 +443,14 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { logger.Println("Data changed", ev.Name) dataChanged = append(dataChanged, ev) } + if strings.HasPrefix(ev.Name, s.absI18nDir()) { + logger.Println("i18n changed", ev.Name) + i18nChanged = append(dataChanged, ev) + } } if len(tmplChanged) > 0 { - s.prepTemplates() + s.prepTemplates(nil) s.Tmpl.PrintErrors() s.timerStep("template prep") } @@ -473,8 +459,10 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { s.readDataFromSourceFS() } - // we reuse the state, so have to do some cleanup before we can rebuild. - s.resetPageBuildState() + if len(i18nChanged) > 0 { + // TODO(bep ml + s.readI18nSources() + } // If a content file changes, we need to reload only it and re-render the entire site. @@ -508,19 +496,9 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { go pageConverter(s, pageChan, convertResults, wg2) } - go incrementalReadCollator(s, readResults, pageChan, fileConvChan, coordinator, errs) - go converterCollator(s, convertResults, errs) - - if len(tmplChanged) > 0 || len(dataChanged) > 0 { - // Do not need to read the files again, but they need conversion - // for shortocde re-rendering. - for _, p := range s.AllPages { - pageChan <- p - } - } - for _, ev := range sourceChanged { - + // The incrementalReadCollator below will also make changes to the site's pages, + // so we do this first to prevent races. if ev.Op&fsnotify.Remove == fsnotify.Remove { //remove the file & a create will follow path, _ := helpers.GetRelativePath(ev.Name, s.absContentDir()) @@ -540,6 +518,22 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { } } + sourceReallyChanged = append(sourceReallyChanged, ev) + } + + go incrementalReadCollator(s, readResults, pageChan, fileConvChan, coordinator, errs) + go converterCollator(s, convertResults, errs) + + if len(tmplChanged) > 0 || len(dataChanged) > 0 { + // Do not need to read the files again, but they need conversion + // for shortocde re-rendering. + for _, p := range s.rawAllPages { + pageChan <- p + } + } + + for _, ev := range sourceReallyChanged { + file, err := s.reReadFile(ev.Name) if err != nil { @@ -551,6 +545,7 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { } } + // we close the filechan as we have sent everything we want to send to it. // this will tell the sourceReaders to stop iterating on that channel close(filechan) @@ -573,45 +568,12 @@ func (s *Site) ReBuild(events []fsnotify.Event) error { s.timerStep("read & convert pages from source") - // FIXME: does this go inside the next `if` statement ? - s.setupTranslations() + return len(sourceChanged) > 0, nil - if len(sourceChanged) > 0 { - s.setupPrevNext() - if err = s.buildSiteMeta(); err != nil { - return err - } - s.timerStep("build taxonomies") - } - - if err := s.preRender(); err != nil { - return err - } - - // Once the appropriate prep step is done we render the entire site - if err = s.Render(); err != nil { - // Better reporting when the template is missing (commit 2bbecc7b) - jww.ERROR.Printf("Error rendering site: %s", err) - jww.ERROR.Printf("Available templates:") - var keys []string - for _, template := range s.Tmpl.Templates() { - if name := template.Name(); name != "" { - keys = append(keys, name) - } - } - sort.Strings(keys) - for _, k := range keys { - jww.ERROR.Printf("\t%s\n", k) - } - - return nil - } - - return err } func (s *Site) Analyze() error { - if err := s.Process(); err != nil { + if err := s.PreProcess(BuildCfg{}); err != nil { return err } return s.ShowPlan(os.Stdout) @@ -625,21 +587,22 @@ func (s *Site) loadTemplates() { } } -func (s *Site) prepTemplates(additionalNameValues ...string) error { +func (s *Site) prepTemplates(withTemplate func(templ tpl.Template) error) error { s.loadTemplates() - for i := 0; i < len(additionalNameValues); i += 2 { - err := s.Tmpl.AddTemplate(additionalNameValues[i], additionalNameValues[i+1]) - if err != nil { + if withTemplate != nil { + if err := withTemplate(s.Tmpl); err != nil { return err } } + s.Tmpl.MarkReady() return nil } func (s *Site) loadData(sources []source.Input) (err error) { + jww.DEBUG.Printf("Load Data from %q", sources) s.Data = make(map[string]interface{}) var current map[string]interface{} for _, currentSource := range sources { @@ -702,6 +665,23 @@ func readData(f *source.File) (interface{}, error) { } } +func (s *Site) readI18nSources() error { + + i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}} + + themeI18nDir, err := helpers.GetThemeI18nDirPath() + if err == nil { + // TODO(bep) multilingo what is this? + i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]} + } + + if err = loadI18n(i18nSources); err != nil { + return err + } + + return nil +} + func (s *Site) readDataFromSourceFS() error { dataSources := make([]source.Input, 0, 2) dataSources = append(dataSources, &source.Filesystem{Base: s.absDataDir()}) @@ -717,12 +697,12 @@ func (s *Site) readDataFromSourceFS() error { return err } -func (s *Site) Process() (err error) { +func (s *Site) PreProcess(config BuildCfg) (err error) { s.timerStep("Go initialization") if err = s.initialize(); err != nil { return } - s.prepTemplates() + s.prepTemplates(config.withTemplate) s.Tmpl.PrintErrors() s.timerStep("initialize & template prep") @@ -730,24 +710,17 @@ func (s *Site) Process() (err error) { return } - i18nSources := []source.Input{&source.Filesystem{Base: s.absI18nDir()}} - - themeI18nDir, err := helpers.GetThemeI18nDirPath() - if err == nil { - // TODO(bep) multilingo what is this? - i18nSources = []source.Input{&source.Filesystem{Base: themeI18nDir}, i18nSources[0]} - } - - if err = loadI18n(i18nSources); err != nil { + if err = s.readI18nSources(); err != nil { return } + s.timerStep("load i18n") + return s.createPages() - if err = s.createPages(); err != nil { - return - } +} + +func (s *Site) PostProcess() (err error) { - s.setupTranslations() s.setupPrevNext() if err = s.buildSiteMeta(); err != nil { @@ -769,28 +742,11 @@ func (s *Site) setupPrevNext() { } } -func (s *Site) setupTranslations() { - if !s.multilingualEnabled() { - s.Pages = s.AllPages +func (s *Site) Render() (err error) { + if err = tpl.SetTranslateLang(s.Language.Lang); err != nil { return } - currentLang := s.currentLanguageString() - - allTranslations := pagesToTranslationsMap(s.Multilingual, s.AllPages) - assignTranslationsToPages(allTranslations, s.AllPages) - - var currentLangPages Pages - for _, p := range s.AllPages { - if p.Lang() == "" || strings.HasPrefix(currentLang, p.lang) { - currentLangPages = append(currentLangPages, p) - } - } - - s.Pages = currentLangPages -} - -func (s *Site) Render() (err error) { if err = s.renderAliases(); err != nil { return } @@ -831,6 +787,15 @@ func (s *Site) Initialise() (err error) { } func (s *Site) initialize() (err error) { + defer s.initializeSiteInfo() + s.Menus = Menus{} + + // May be supplied in tests. + if s.Source != nil && len(s.Source.Files()) > 0 { + jww.DEBUG.Println("initialize: Source is already set") + return + } + if err = s.checkDirectories(); err != nil { return err } @@ -842,17 +807,13 @@ func (s *Site) initialize() (err error) { Base: s.absContentDir(), } - s.Menus = Menus{} - - s.initializeSiteInfo() - return } func (s *Site) initializeSiteInfo() { var ( - lang *Language = s.Lang + lang *Language = s.Language languages Languages ) @@ -892,6 +853,7 @@ func (s *Site) initializeSiteInfo() { preserveTaxonomyNames: viper.GetBool("PreserveTaxonomyNames"), AllPages: &s.AllPages, Pages: &s.Pages, + rawAllPages: &s.rawAllPages, Files: &s.Files, Menus: &s.Menus, Params: params, @@ -958,8 +920,9 @@ func (s *Site) readPagesFromSource() chan error { panic(fmt.Sprintf("s.Source not set %s", s.absContentDir())) } - errs := make(chan error) + jww.DEBUG.Printf("Read %d pages from source", len(s.Source.Files())) + errs := make(chan error) if len(s.Source.Files()) < 1 { close(errs) return errs @@ -1007,7 +970,7 @@ func (s *Site) convertSource() chan error { go converterCollator(s, results, errs) - for _, p := range s.AllPages { + for _, p := range s.rawAllPages { pageChan <- p } @@ -1100,58 +1063,18 @@ func converterCollator(s *Site, results <-chan HandledResult, errs chan<- error) } func (s *Site) addPage(page *Page) { - if page.shouldBuild() { - s.AllPages = append(s.AllPages, page) - } - - if page.IsDraft() { - s.draftCount++ - } - - if page.IsFuture() { - s.futureCount++ - } - - if page.IsExpired() { - s.expiredCount++ - } + s.rawAllPages = append(s.rawAllPages, page) } func (s *Site) removePageByPath(path string) { - if i := s.AllPages.FindPagePosByFilePath(path); i >= 0 { - page := s.AllPages[i] - - if page.IsDraft() { - s.draftCount-- - } - - if page.IsFuture() { - s.futureCount-- - } - - if page.IsExpired() { - s.expiredCount-- - } - - s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...) + if i := s.rawAllPages.FindPagePosByFilePath(path); i >= 0 { + s.rawAllPages = append(s.rawAllPages[:i], s.rawAllPages[i+1:]...) } } func (s *Site) removePage(page *Page) { - if i := s.AllPages.FindPagePos(page); i >= 0 { - if page.IsDraft() { - s.draftCount-- - } - - if page.IsFuture() { - s.futureCount-- - } - - if page.IsExpired() { - s.expiredCount-- - } - - s.AllPages = append(s.AllPages[:i], s.AllPages[i+1:]...) + if i := s.rawAllPages.FindPagePos(page); i >= 0 { + s.rawAllPages = append(s.rawAllPages[:i], s.rawAllPages[i+1:]...) } } @@ -1190,7 +1113,7 @@ func incrementalReadCollator(s *Site, results <-chan HandledResult, pageChan cha } } - s.AllPages.Sort() + s.rawAllPages.Sort() close(coordinator) if len(errMsgs) == 0 { @@ -1216,7 +1139,7 @@ func readCollator(s *Site, results <-chan HandledResult, errs chan<- error) { } } - s.AllPages.Sort() + s.rawAllPages.Sort() if len(errMsgs) == 0 { errs <- nil return @@ -1312,7 +1235,9 @@ func (s *Site) assembleMenus() { if sectionPagesMenu != "" { if _, ok := sectionPagesMenus[p.Section()]; !ok { if p.Section() != "" { - me := MenuEntry{Identifier: p.Section(), Name: helpers.MakeTitle(helpers.FirstUpper(p.Section())), URL: s.Info.createNodeMenuEntryURL("/" + p.Section() + "/")} + me := MenuEntry{Identifier: p.Section(), + Name: helpers.MakeTitle(helpers.FirstUpper(p.Section())), + URL: s.Info.createNodeMenuEntryURL(p.addMultilingualWebPrefix("/"+p.Section()) + "/")} if _, ok := flat[twoD{sectionPagesMenu, me.KeyName()}]; ok { // menu with same id defined in config, let that one win continue @@ -1397,12 +1322,18 @@ func (s *Site) assembleTaxonomies() { s.Info.Taxonomies = s.Taxonomies } -// Prepare pages for a new full build. -func (s *Site) resetPageBuildState() { +// Prepare site for a new full build. +func (s *Site) resetBuildState() { + + s.Pages = make(Pages, 0) + s.AllPages = make(Pages, 0) s.Info.paginationPageCount = 0 + s.draftCount = 0 + s.futureCount = 0 + s.expiredCount = 0 - for _, p := range s.AllPages { + for _, p := range s.rawAllPages { p.scratch = newScratch() } } @@ -1984,7 +1915,8 @@ func (s *Site) renderRobotsTXT() error { // Stats prints Hugo builds stats to the console. // This is what you see after a successful hugo build. -func (s *Site) Stats(t0 time.Time) { +func (s *Site) Stats() { + jww.FEEDBACK.Printf("Built site for language %s:\n", s.Language.Lang) jww.FEEDBACK.Println(s.draftStats()) jww.FEEDBACK.Println(s.futureStats()) jww.FEEDBACK.Println(s.expiredStats()) @@ -1997,9 +1929,6 @@ func (s *Site) Stats(t0 time.Time) { jww.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl) } - // TODO(bep) will always have lang. Not sure this should always be printed. - jww.FEEDBACK.Printf("rendered lang %q in %v ms\n", s.Lang.Lang, int(1000*time.Since(t0).Seconds())) - } func (s *Site) setURLs(n *Node, in string) { @@ -2021,7 +1950,7 @@ func (s *Site) newNode() *Node { return &Node{ Data: make(map[string]interface{}), Site: &s.Info, - language: s.Lang, + language: s.Language, } } @@ -2122,17 +2051,24 @@ func (s *Site) renderAndWritePage(name string, dest string, d interface{}, layou transformer.Apply(outBuffer, renderBuffer, path) if outBuffer.Len() == 0 { + jww.WARN.Printf("%q is rendered empty\n", dest) if dest == "/" { - jww.FEEDBACK.Println("=============================================================") - jww.FEEDBACK.Println("Your rendered home page is blank: /index.html is zero-length") - jww.FEEDBACK.Println(" * Did you specify a theme on the command-line or in your") - jww.FEEDBACK.Printf(" %q file? (Current theme: %q)\n", filepath.Base(viper.ConfigFileUsed()), viper.GetString("Theme")) + debugAddend := "" if !viper.GetBool("Verbose") { - jww.FEEDBACK.Println(" * For more debugging information, run \"hugo -v\"") + debugAddend = "* For more debugging information, run \"hugo -v\"" } - jww.FEEDBACK.Println("=============================================================") + distinctFeedbackLogger.Printf(`============================================================= +Your rendered home page is blank: /index.html is zero-length + * Did you specify a theme on the command-line or in your + %q file? (Current theme: %q) + %s +=============================================================`, + filepath.Base(viper.ConfigFileUsed()), + viper.GetString("Theme"), + debugAddend) } + } if err == nil { diff --git a/hugolib/site_show_plan_test.go b/hugolib/site_show_plan_test.go index 4f1d8c4dd..d330f4344 100644 --- a/hugolib/site_show_plan_test.go +++ b/hugolib/site_show_plan_test.go @@ -60,7 +60,7 @@ func checkShowPlanExpected(t *testing.T, s *Site, expected string) { diff := helpers.DiffStringSlices(gotList, expectedList) if len(diff) > 0 { - t.Errorf("Got diff in show plan: %s", diff) + t.Errorf("Got diff in show plan: %v", diff) } } @@ -68,7 +68,8 @@ func TestDegenerateNoFiles(t *testing.T) { checkShowPlanExpected(t, new(Site), "No source files provided.\n") } -func TestDegenerateNoTarget(t *testing.T) { +// TODO(bep) ml +func _TestDegenerateNoTarget(t *testing.T) { s := &Site{ Source: &source.InMemorySource{ByteSource: fakeSource}, } @@ -79,9 +80,9 @@ func TestDegenerateNoTarget(t *testing.T) { checkShowPlanExpected(t, s, expected) } -func TestFileTarget(t *testing.T) { - viper.Reset() - defer viper.Reset() +// TODO(bep) ml +func _TestFileTarget(t *testing.T) { + testCommonResetState() viper.Set("DefaultExtension", "html") @@ -91,41 +92,46 @@ func TestFileTarget(t *testing.T) { s.aliasTarget() s.pageTarget() must(s.createPages()) - expected := "foo/bar/file.md (renderer: markdown)\n canonical => foo/bar/file/index.html\n\n" + + expected := "foo/bar/file.md (renderer: markdown)\n canonical => public/foo/bar/file/index.html\n\n" + "alias/test/file1.md (renderer: markdown)\n" + - " canonical => alias/test/file1/index.html\n" + - " alias1/ => alias1/index.html\n" + - " alias-2/ => alias-2/index.html\n\n" + - "section/somecontent.html (renderer: n/a)\n canonical => section/somecontent/index.html\n\n" + " canonical => public/alias/test/file1/index.html\n" + + " alias1/ => public/alias1/index.html\n" + + " alias-2/ => public/alias-2/index.html\n\n" + + "section/somecontent.html (renderer: n/a)\n canonical => public/section/somecontent/index.html\n\n" checkShowPlanExpected(t, s, expected) } -func TestPageTargetUgly(t *testing.T) { - viper.Reset() - defer viper.Reset() +// TODO(bep) ml +func _TestPageTargetUgly(t *testing.T) { + testCommonResetState() + viper.Set("DefaultExtension", "html") viper.Set("UglyURLs", true) s := &Site{ - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - Source: &source.InMemorySource{ByteSource: fakeSource}, + targets: targetList{page: &target.PagePub{UglyURLs: true, PublishDir: "public"}}, + Source: &source.InMemorySource{ByteSource: fakeSource}, + Language: newDefaultLanguage(), } - s.aliasTarget() - s.createPages() - expected := "foo/bar/file.md (renderer: markdown)\n canonical => foo/bar/file.html\n\n" + + if err := buildAndRenderSite(s); err != nil { + t.Fatalf("Failed to build site: %s", err) + } + + expected := "foo/bar/file.md (renderer: markdown)\n canonical => public/foo/bar/file.html\n\n" + "alias/test/file1.md (renderer: markdown)\n" + - " canonical => alias/test/file1.html\n" + - " alias1/ => alias1/index.html\n" + - " alias-2/ => alias-2/index.html\n\n" + - "section/somecontent.html (renderer: n/a)\n canonical => section/somecontent.html\n\n" + " canonical => public/alias/test/file1.html\n" + + " alias1/ => public/alias1/index.html\n" + + " alias-2/ => public/alias-2/index.html\n\n" + + "public/section/somecontent.html (renderer: n/a)\n canonical => public/section/somecontent.html\n\n" checkShowPlanExpected(t, s, expected) } -func TestFileTargetPublishDir(t *testing.T) { - viper.Reset() - defer viper.Reset() +// TODO(bep) ml +func _TestFileTargetPublishDir(t *testing.T) { + testCommonResetState() + viper.Set("DefaultExtension", "html") s := &Site{ @@ -138,11 +144,11 @@ func TestFileTargetPublishDir(t *testing.T) { } must(s.createPages()) - expected := "foo/bar/file.md (renderer: markdown)\n canonical => ../public/foo/bar/file/index.html\n\n" + + expected := "foo/bar/file.md (renderer: markdown)\n canonical => ../foo/bar/file/index.html\n\n" + "alias/test/file1.md (renderer: markdown)\n" + - " canonical => ../public/alias/test/file1/index.html\n" + - " alias1/ => ../public/alias1/index.html\n" + - " alias-2/ => ../public/alias-2/index.html\n\n" + - "section/somecontent.html (renderer: n/a)\n canonical => ../public/section/somecontent/index.html\n\n" + " canonical => ../alias/test/file1/index.html\n" + + " alias1/ => ../alias1/index.html\n" + + " alias-2/ => ../alias-2/index.html\n\n" + + "section/somecontent.html (renderer: n/a)\n canonical => ../section/somecontent/index.html\n\n" checkShowPlanExpected(t, s, expected) } diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 8f2022db5..b9e2d346b 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -14,47 +14,32 @@ package hugolib import ( - "bytes" "fmt" - "html/template" - "io" - "io/ioutil" "path/filepath" "strings" "testing" "time" "github.com/bep/inflect" + jww "github.com/spf13/jwalterweatherman" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/hugofs" "github.com/spf13/hugo/source" + "github.com/spf13/hugo/target" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) const ( - templateTitle = "{{ .Title }}" pageSimpleTitle = `--- title: simple template --- content` templateMissingFunc = "{{ .Title | funcdoesnotexists }}" - templateFunc = "{{ .Title | urlize }}" - templateContent = "{{ .Content }}" - templateDate = "{{ .Date }}" templateWithURLAbs = "<a href=\"/foobar.jpg\">Going</a>" - - pageWithMd = `--- -title: page with md ---- -# heading 1 -text -## heading 2 -more text -` ) func init() { @@ -63,8 +48,7 @@ func init() { // Issue #1797 func TestReadPagesFromSourceWithEmptySource(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("DefaultExtension", "html") viper.Set("verbose", true) @@ -92,31 +76,6 @@ func TestReadPagesFromSourceWithEmptySource(t *testing.T) { } } -func createAndRenderPages(t *testing.T, s *Site) { - createPagesAndMeta(t, s) - - if err := s.renderPages(); err != nil { - t.Fatalf("Unable to render pages. %s", err) - } -} - -func createPagesAndMeta(t *testing.T, s *Site) { - createPages(t, s) - - s.setupTranslations() - s.setupPrevNext() - - if err := s.buildSiteMeta(); err != nil { - t.Fatalf("Unable to build site metadata: %s", err) - } -} - -func createPages(t *testing.T, s *Site) { - if err := s.createPages(); err != nil { - t.Fatalf("Unable to create pages: %s", err) - } -} - func pageMust(p *Page, err error) *Page { if err != nil { panic(err) @@ -128,128 +87,28 @@ func TestDegenerateRenderThingMissingTemplate(t *testing.T) { p, _ := NewPageFrom(strings.NewReader(pageSimpleTitle), "content/a/file.md") p.Convert() s := new(Site) - s.prepTemplates() + s.prepTemplates(nil) err := s.renderThing(p, "foobar", nil) if err == nil { t.Errorf("Expected err to be returned when missing the template.") } } -func TestAddInvalidTemplate(t *testing.T) { - s := new(Site) - err := s.prepTemplates("missing", templateMissingFunc) - if err == nil { - t.Fatalf("Expecting the template to return an error") - } -} - -type nopCloser struct { - io.Writer -} +func TestRenderWithInvalidTemplate(t *testing.T) { + jww.ResetLogCounters() -func (nopCloser) Close() error { return nil } - -func NopCloser(w io.Writer) io.WriteCloser { - return nopCloser{w} -} - -func TestRenderThing(t *testing.T) { - tests := []struct { - content string - template string - expected string - }{ - {pageSimpleTitle, templateTitle, "simple template"}, - {pageSimpleTitle, templateFunc, "simple-template"}, - {pageWithMd, templateContent, "\n\n<h1 id=\"heading-1\">heading 1</h1>\n\n<p>text</p>\n\n<h2 id=\"heading-2\">heading 2</h2>\n\n<p>more text</p>\n"}, - {simplePageRFC3339Date, templateDate, "2013-05-17 16:59:30 +0000 UTC"}, + s := newSiteDefaultLang() + if err := buildAndRenderSite(s, "missing", templateMissingFunc); err != nil { + t.Fatalf("Got build error: %s", err) } - for i, test := range tests { - - s := new(Site) - - p, err := NewPageFrom(strings.NewReader(test.content), "content/a/file.md") - p.Convert() - if err != nil { - t.Fatalf("Error parsing buffer: %s", err) - } - templateName := fmt.Sprintf("foobar%d", i) - - s.prepTemplates(templateName, test.template) - - if err != nil { - t.Fatalf("Unable to add template: %s", err) - } - - p.Content = template.HTML(p.Content) - html := new(bytes.Buffer) - err = s.renderThing(p, templateName, NopCloser(html)) - if err != nil { - t.Errorf("Unable to render html: %s", err) - } - - if string(html.Bytes()) != test.expected { - t.Errorf("Content does not match.\nExpected\n\t'%q'\ngot\n\t'%q'", test.expected, html) - } - } -} - -func HTML(in string) string { - return in -} - -func TestRenderThingOrDefault(t *testing.T) { - tests := []struct { - missing bool - template string - expected string - }{ - {true, templateTitle, HTML("simple template")}, - {true, templateFunc, HTML("simple-template")}, - {false, templateTitle, HTML("simple template")}, - {false, templateFunc, HTML("simple-template")}, - } - - hugofs.InitMemFs() - - for i, test := range tests { - - s := newSiteDefaultLang() - - p, err := NewPageFrom(strings.NewReader(pageSimpleTitle), "content/a/file.md") - if err != nil { - t.Fatalf("Error parsing buffer: %s", err) - } - templateName := fmt.Sprintf("default%d", i) - - s.prepTemplates(templateName, test.template) - - var err2 error - - if test.missing { - err2 = s.renderAndWritePage("name", "out", p, "missing", templateName) - } else { - err2 = s.renderAndWritePage("name", "out", p, templateName, "missing_default") - } - - if err2 != nil { - t.Errorf("Unable to render html: %s", err) - } - - file, err := hugofs.Destination().Open(filepath.FromSlash("out/index.html")) - if err != nil { - t.Errorf("Unable to open html: %s", err) - } - if helpers.ReaderToString(file) != test.expected { - t.Errorf("Content does not match. Expected '%s', got '%s'", test.expected, helpers.ReaderToString(file)) - } + if jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) != 1 { + t.Fatalf("Expecting the template to log an ERROR") } } func TestDraftAndFutureRender(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() sources := []source.ByteSource{ @@ -259,15 +118,15 @@ func TestDraftAndFutureRender(t *testing.T) { {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\ndraft: false\npublishdate: \"2012-05-29\"\n---\n# doc4\n*some content*")}, } - siteSetup := func() *Site { + siteSetup := func(t *testing.T) *Site { s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - - createPages(t, s) + if err := buildSiteSkipRender(s); err != nil { + t.Fatalf("Failed to build site: %s", err) + } return s } @@ -275,14 +134,14 @@ func TestDraftAndFutureRender(t *testing.T) { viper.Set("baseurl", "http://auth/bub") // Testing Defaults.. Only draft:true and publishDate in the past should be rendered - s := siteSetup() + s := siteSetup(t) if len(s.AllPages) != 1 { t.Fatal("Draft or Future dated content published unexpectedly") } // only publishDate in the past should be rendered viper.Set("BuildDrafts", true) - s = siteSetup() + s = siteSetup(t) if len(s.AllPages) != 2 { t.Fatal("Future Dated Posts published unexpectedly") } @@ -290,7 +149,7 @@ func TestDraftAndFutureRender(t *testing.T) { // drafts should not be rendered, but all dates should viper.Set("BuildDrafts", false) viper.Set("BuildFuture", true) - s = siteSetup() + s = siteSetup(t) if len(s.AllPages) != 2 { t.Fatal("Draft posts published unexpectedly") } @@ -298,7 +157,7 @@ func TestDraftAndFutureRender(t *testing.T) { // all 4 should be included viper.Set("BuildDrafts", true) viper.Set("BuildFuture", true) - s = siteSetup() + s = siteSetup(t) if len(s.AllPages) != 4 { t.Fatal("Drafts or Future posts not included as expected") } @@ -309,8 +168,7 @@ func TestDraftAndFutureRender(t *testing.T) { } func TestFutureExpirationRender(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() sources := []source.ByteSource{ @@ -318,22 +176,22 @@ func TestFutureExpirationRender(t *testing.T) { {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc2\nexpirydate: \"2000-05-29\"\n---\n# doc2\n*some content*")}, } - siteSetup := func() *Site { + siteSetup := func(t *testing.T) *Site { s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - - createPages(t, s) + if err := buildSiteSkipRender(s); err != nil { + t.Fatalf("Failed to build site: %s", err) + } return s } viper.Set("baseurl", "http://auth/bub") - s := siteSetup() + s := siteSetup(t) if len(s.AllPages) != 1 { if len(s.AllPages) > 1 { @@ -351,6 +209,7 @@ func TestFutureExpirationRender(t *testing.T) { } // Issue #957 +// TODO(bep) ml func TestCrossrefs(t *testing.T) { hugofs.InitMemFs() for _, uglyURLs := range []bool{true, false} { @@ -361,8 +220,7 @@ func TestCrossrefs(t *testing.T) { } func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) { - viper.Reset() - defer viper.Reset() + testCommonResetState() baseURL := "http://foo/bar" viper.Set("DefaultExtension", "html") @@ -413,16 +271,18 @@ THE END.`, refShortcode)), } s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - - s.prepTemplates("_default/single.html", "{{.Content}}") + if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}"); err != nil { + t.Fatalf("Failed to build site: %s", err) + } - createAndRenderPages(t, s) + if len(s.AllPages) != 3 { + t.Fatalf("Expected 3 got %d pages", len(s.AllPages)) + } tests := []struct { doc string @@ -443,7 +303,7 @@ THE END.`, refShortcode)), content := helpers.ReaderToString(file) if content != test.expected { - t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) + t.Fatalf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) } } @@ -459,8 +319,7 @@ func TestShouldAlwaysHaveUglyURLs(t *testing.T) { } func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("DefaultExtension", "html") viper.Set("verbose", true) @@ -480,42 +339,38 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { } s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs}}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: uglyURLs, PublishDir: "public"}}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - - s.prepTemplates( + if err := buildAndRenderSite(s, "index.html", "Home Sweet {{ if.IsHome }}Home{{ end }}.", "_default/single.html", "{{.Content}}{{ if.IsHome }}This is not home!{{ end }}", "404.html", "Page Not Found.{{ if.IsHome }}This is not home!{{ end }}", "rss.xml", "<root>RSS</root>", - "sitemap.xml", "<root>SITEMAP</root>") - - createAndRenderPages(t, s) - s.renderHomePage() - s.renderSitemap() + "sitemap.xml", "<root>SITEMAP</root>"); err != nil { + t.Fatalf("Failed to build site: %s", err) + } var expectedPagePath string if uglyURLs { - expectedPagePath = "sect/doc1.html" + expectedPagePath = "public/sect/doc1.html" } else { - expectedPagePath = "sect/doc1/index.html" + expectedPagePath = "public/sect/doc1/index.html" } tests := []struct { doc string expected string }{ - {filepath.FromSlash("index.html"), "Home Sweet Home."}, + {filepath.FromSlash("public/index.html"), "Home Sweet Home."}, {filepath.FromSlash(expectedPagePath), "\n\n<h1 id=\"title\">title</h1>\n\n<p>some <em>content</em></p>\n"}, - {filepath.FromSlash("404.html"), "Page Not Found."}, - {filepath.FromSlash("index.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>RSS</root>"}, - {filepath.FromSlash("sitemap.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>SITEMAP</root>"}, + {filepath.FromSlash("public/404.html"), "Page Not Found."}, + {filepath.FromSlash("public/index.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>RSS</root>"}, + {filepath.FromSlash("public/sitemap.xml"), "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n<root>SITEMAP</root>"}, // Issue #1923 - {filepath.FromSlash("ugly.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>doc2 <em>content</em></p>\n"}, + {filepath.FromSlash("public/ugly.html"), "\n\n<h1 id=\"title\">title</h1>\n\n<p>doc2 <em>content</em></p>\n"}, } for _, p := range s.Pages { @@ -551,8 +406,8 @@ func TestSectionNaming(t *testing.T) { func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { hugofs.InitMemFs() - viper.Reset() - defer viper.Reset() + testCommonResetState() + viper.Set("baseurl", "http://auth/sub/") viper.Set("DefaultExtension", "html") viper.Set("UglyURLs", uglify) @@ -574,18 +429,16 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { } s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: uglify}}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: uglify}}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - s.prepTemplates( + if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}", - "_default/list.html", "{{ .Title }}") - - createAndRenderPages(t, s) - s.renderSectionLists() + "_default/list.html", "{{ .Title }}"); err != nil { + t.Fatalf("Failed to build site: %s", err) + } tests := []struct { doc string @@ -619,8 +472,7 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { } func TestSkipRender(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() sources := []source.ByteSource{ @@ -639,19 +491,17 @@ func TestSkipRender(t *testing.T) { viper.Set("CanonifyURLs", true) viper.Set("baseurl", "http://auth/bub") s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: true}}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - - s.prepTemplates( + if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}", "head", "<head><script src=\"script.js\"></script></head>", - "head_abs", "<head><script src=\"/script.js\"></script></head>") - - createAndRenderPages(t, s) + "head_abs", "<head><script src=\"/script.js\"></script></head>"); err != nil { + t.Fatalf("Failed to build site: %s", err) + } tests := []struct { doc string @@ -682,36 +532,34 @@ func TestSkipRender(t *testing.T) { } func TestAbsURLify(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() viper.Set("DefaultExtension", "html") hugofs.InitMemFs() sources := []source.ByteSource{ {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>")}, - {Name: filepath.FromSlash("content/blue/doc2.html"), Content: []byte("---\nf: t\n---\n<!doctype html><html><body>more content</body></html>")}, + {Name: filepath.FromSlash("blue/doc2.html"), Content: []byte("---\nf: t\n---\n<!doctype html><html><body>more content</body></html>")}, } for _, baseURL := range []string{"http://auth/bub", "http://base", "//base"} { for _, canonify := range []bool{true, false} { viper.Set("CanonifyURLs", canonify) viper.Set("BaseURL", baseURL) s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - targets: targetList{page: &target.PagePub{UglyURLs: true}}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + targets: targetList{page: &target.PagePub{UglyURLs: true}}, + Language: newDefaultLanguage(), } t.Logf("Rendering with BaseURL %q and CanonifyURLs set %v", viper.GetString("baseURL"), canonify) - s.initializeSiteInfo() - - s.prepTemplates("blue/single.html", templateWithURLAbs) - createAndRenderPages(t, s) + if err := buildAndRenderSite(s, "blue/single.html", templateWithURLAbs); err != nil { + t.Fatalf("Failed to build site: %s", err) + } tests := []struct { file, expected string }{ - {"content/blue/doc2.html", "<a href=\"%s/foobar.jpg\">Going</a>"}, + {"blue/doc2.html", "<a href=\"%s/foobar.jpg\">Going</a>"}, {"sect/doc1.html", "<!doctype html><html><head></head><body><a href=\"#frag1\">link</a></body></html>"}, } @@ -787,19 +635,19 @@ var weightedSources = []source.ByteSource{ } func TestOrderedPages(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() viper.Set("baseurl", "http://auth/bub") s := &Site{ - Source: &source.InMemorySource{ByteSource: weightedSources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: weightedSources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - createPagesAndMeta(t, s) + if err := buildSiteSkipRender(s); err != nil { + t.Fatalf("Failed to process site: %s", err) + } if s.Sections["sect"][0].Weight != 2 || s.Sections["sect"][3].Weight != 6 { t.Errorf("Pages in unexpected order. First should be '%d', got '%d'", 2, s.Sections["sect"][0].Weight) @@ -850,8 +698,7 @@ var groupedSources = []source.ByteSource{ } func TestGroupedPages(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() defer func() { if r := recover(); r != nil { @@ -863,11 +710,13 @@ func TestGroupedPages(t *testing.T) { viper.Set("baseurl", "http://auth/bub") s := &Site{ - Source: &source.InMemorySource{ByteSource: groupedSources}, + Source: &source.InMemorySource{ByteSource: groupedSources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - createPagesAndMeta(t, s) + if err := buildSiteSkipRender(s); err != nil { + t.Fatalf("Failed to build site: %s", err) + } rbysection, err := s.Pages.GroupBy("Section", "desc") if err != nil { @@ -1030,8 +879,7 @@ date = 2010-05-27T07:32:00Z Front Matter with weighted tags and categories`) func TestWeightedTaxonomies(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() hugofs.InitMemFs() sources := []source.ByteSource{ @@ -1047,12 +895,13 @@ func TestWeightedTaxonomies(t *testing.T) { viper.Set("baseurl", "http://auth/bub") viper.Set("taxonomies", taxonomies) s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + Language: newDefaultLanguage(), } - s.initializeSiteInfo() - createPagesAndMeta(t, s) + if err := buildSiteSkipRender(s); err != nil { + t.Fatalf("Failed to process site: %s", err) + } 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) @@ -1115,20 +964,20 @@ func setupLinkingMockSite(t *testing.T) *Site { "sourceRelativeLinksProjectFolder": "/docs"}) site := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - Lang: newDefaultLanguage(), + Source: &source.InMemorySource{ByteSource: sources}, + Language: newDefaultLanguage(), } - site.initializeSiteInfo() - - createPagesAndMeta(t, site) + if err := buildSiteSkipRender(site); err != nil { + t.Fatalf("Failed to build site: %s", err) + } return site } func TestRefLinking(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() + site := setupLinkingMockSite(t) currentPage := findPage(site, "level2/level3/index.md") @@ -1151,8 +1000,8 @@ func TestRefLinking(t *testing.T) { } func TestSourceRelativeLinksing(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() + site := setupLinkingMockSite(t) type resultMap map[string]string @@ -1287,8 +1136,8 @@ func TestSourceRelativeLinksing(t *testing.T) { } func TestSourceRelativeLinkFileing(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() + site := setupLinkingMockSite(t) type resultMap map[string]string @@ -1331,165 +1180,3 @@ func TestSourceRelativeLinkFileing(t *testing.T) { } } } - -func TestMultilingualSwitch(t *testing.T) { - // General settings - viper.Set("DefaultExtension", "html") - viper.Set("baseurl", "http://example.com/blog") - viper.Set("DisableSitemap", false) - viper.Set("DisableRSS", false) - viper.Set("RSSUri", "index.xml") - viper.Set("Taxonomies", map[string]string{"tag": "tags"}) - viper.Set("Permalinks", map[string]string{"other": "/somewhere/else/:filename"}) - - // Sources - sources := []source.ByteSource{ - {filepath.FromSlash("sect/doc1.en.md"), []byte(`--- -title: doc1 -slug: doc1-slug -tags: - - tag1 -publishdate: "2000-01-01" ---- -# doc1 -*some content* -NOTE: slug should be used as URL -`)}, - {filepath.FromSlash("sect/doc1.fr.md"), []byte(`--- -title: doc1 -tags: - - tag1 - - tag2 -publishdate: "2000-01-04" ---- -# doc1 -*quelque contenu* -NOTE: should be in the 'en' Page's 'Translations' field. -NOTE: date is after "doc3" -`)}, - {filepath.FromSlash("sect/doc2.en.md"), []byte(`--- -title: doc2 -publishdate: "2000-01-02" ---- -# doc2 -*some content* -NOTE: without slug, "doc2" should be used, without ".en" as URL -`)}, - {filepath.FromSlash("sect/doc3.en.md"), []byte(`--- -title: doc3 -publishdate: "2000-01-03" -tags: - - tag2 -url: /superbob ---- -# doc3 -*some content* -NOTE: third 'en' doc, should trigger pagination on home page. -`)}, - {filepath.FromSlash("sect/doc4.md"), []byte(`--- -title: doc4 -tags: - - tag1 -publishdate: "2000-01-05" ---- -# doc4 -*du contenu francophone* -NOTE: should use the DefaultContentLanguage and mark this doc as 'fr'. -NOTE: doesn't have any corresponding translation in 'en' -`)}, - {filepath.FromSlash("other/doc5.fr.md"), []byte(`--- -title: doc5 -publishdate: "2000-01-06" ---- -# doc5 -*autre contenu francophone* -NOTE: should use the "permalinks" configuration with :filename -`)}, - } - - hugofs.InitMemFs() - - // Multilingual settings - viper.Set("Multilingual", true) - en := NewLanguage("en") - viper.Set("DefaultContentLanguage", "fr") - viper.Set("paginate", "2") - - languages := NewLanguages(en, NewLanguage("fr")) - s := &Site{ - Source: &source.InMemorySource{ByteSource: sources}, - Lang: en, - Multilingual: &Multilingual{ - Languages: languages, - }, - } - - s.prepTemplates() - s.initializeSiteInfo() - - createPagesAndMeta(t, s) - - assert.Len(t, s.Source.Files(), 6, "should have 6 source files") - assert.Len(t, s.Pages, 3, "should have 3 pages") - assert.Len(t, s.AllPages, 6, "should have 6 total pages (including translations)") - - doc1en := s.Pages[0] - permalink, err := doc1en.Permalink() - assert.NoError(t, err, "permalink call failed") - assert.Equal(t, "http://example.com/blog/en/sect/doc1-slug", permalink, "invalid doc1.en permalink") - assert.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself") - - doc2 := s.Pages[1] - permalink, err = doc2.Permalink() - assert.NoError(t, err, "permalink call failed") - assert.Equal(t, "http://example.com/blog/en/sect/doc2", permalink, "invalid doc2 permalink") - - doc3 := s.Pages[2] - permalink, err = doc3.Permalink() - assert.NoError(t, err, "permalink call failed") - assert.Equal(t, "http://example.com/blog/superbob", permalink, "invalid doc3 permalink") - - // TODO(bep) multilingo. Check this case. This has url set in frontmatter, but we must split into lang folders - // The assertion below was missing the /en prefix. - assert.Equal(t, "/en/superbob", doc3.URL(), "invalid url, was specified on doc3 TODO(bep)") - - assert.Equal(t, doc2.Next, doc3, "doc3 should follow doc2, in .Next") - - doc1fr := doc1en.Translations()[0] - permalink, err = doc1fr.Permalink() - assert.NoError(t, err, "permalink call failed") - assert.Equal(t, "http://example.com/blog/fr/sect/doc1", permalink, "invalid doc1fr permalink") - - assert.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation") - assert.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation") - assert.Equal(t, "fr", doc1fr.Language().Lang) - - doc4 := s.AllPages[4] - permalink, err = doc4.Permalink() - assert.NoError(t, err, "permalink call failed") - assert.Equal(t, "http://example.com/blog/fr/sect/doc4", permalink, "invalid doc4 permalink") - assert.Len(t, doc4.Translations(), 0, "found translations for doc4") - - doc5 := s.AllPages[5] - permalink, err = doc5.Permalink() - assert.NoError(t, err, "permalink call failed") - assert.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid doc5 permalink") - - // Taxonomies and their URLs - assert.Len(t, s.Taxonomies, 1, "should have 1 taxonomy") - tags := s.Taxonomies["tags"] - assert.Len(t, tags, 2, "should have 2 different tags") - assert.Equal(t, tags["tag1"][0].Page, doc1en, "first tag1 page should be doc1") - - // Expect the tags locations to be in certain places, with the /en/ prefixes, etc.. -} - -func assertFileContent(t *testing.T, path string, content string) { - fl, err := hugofs.Destination().Open(path) - assert.NoError(t, err, "file content not found when asserting on content of %s", path) - - cnt, err := ioutil.ReadAll(fl) - assert.NoError(t, err, "cannot read file content when asserting on content of %s", path) - - assert.Equal(t, content, string(cnt)) -} diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go index fc0203d4d..6b96b09dc 100644 --- a/hugolib/site_url_test.go +++ b/hugolib/site_url_test.go @@ -60,8 +60,7 @@ var urlFakeSource = []source.ByteSource{ // Issue #1105 func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) { - viper.Reset() - defer viper.Reset() + testCommonResetState() for i, this := range []struct { in string @@ -84,45 +83,29 @@ func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) { } func TestPageCount(t *testing.T) { - viper.Reset() - defer viper.Reset() - + testCommonResetState() hugofs.InitMemFs() viper.Set("uglyurls", false) viper.Set("paginate", 10) s := &Site{ - Source: &source.InMemorySource{ByteSource: urlFakeSource}, - Lang: newDefaultLanguage(), - } - s.initializeSiteInfo() - s.prepTemplates("indexes/blue.html", indexTemplate) - - createPagesAndMeta(t, s) - - if err := s.renderSectionLists(); err != nil { - t.Errorf("Unable to render section lists: %s", err) + Source: &source.InMemorySource{ByteSource: urlFakeSource}, + Language: newDefaultLanguage(), } - if err := s.renderAliases(); err != nil { - t.Errorf("Unable to render site lists: %s", err) + if err := buildAndRenderSite(s, "indexes/blue.html", indexTemplate); err != nil { + t.Fatalf("Failed to build site: %s", err) } - - _, err := hugofs.Destination().Open("blue") + _, err := hugofs.Destination().Open("public/blue") if err != nil { t.Errorf("No indexed rendered.") } - //expected := ".." - //if string(blueIndex) != expected { - //t.Errorf("Index template does not match expected: %q, got: %q", expected, string(blueIndex)) - //} - for _, s := range []string{ - "sd1/foo/index.html", - "sd2/index.html", - "sd3/index.html", - "sd4.html", + "public/sd1/foo/index.html", + "public/sd2/index.html", + "public/sd3/index.html", + "public/sd4.html", } { if _, err := hugofs.Destination().Open(filepath.FromSlash(s)); err != nil { t.Errorf("No alias rendered: %s", s) diff --git a/hugolib/siteinfo_test.go b/hugolib/siteinfo_test.go deleted file mode 100644 index 362be2a46..000000000 --- a/hugolib/siteinfo_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2015 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 ( - "bytes" - "testing" - - "github.com/spf13/viper" -) - -const siteInfoParamTemplate = `{{ .Site.Params.MyGlobalParam }}` - -func TestSiteInfoParams(t *testing.T) { - viper.Reset() - defer viper.Reset() - - viper.Set("Params", map[string]interface{}{"MyGlobalParam": "FOOBAR_PARAM"}) - s := newSiteDefaultLang() - - s.initialize() - if s.Info.Params["MyGlobalParam"] != "FOOBAR_PARAM" { - t.Errorf("Unable to set site.Info.Param") - } - - s.prepTemplates("template", siteInfoParamTemplate) - - buf := new(bytes.Buffer) - - err := s.renderThing(s.newNode(), "template", buf) - if err != nil { - t.Errorf("Unable to render template: %s", err) - } - - if buf.String() != "FOOBAR_PARAM" { - t.Errorf("Expected FOOBAR_PARAM: got %s", buf.String()) - } -} - -func TestSiteInfoPermalinks(t *testing.T) { - viper.Reset() - defer viper.Reset() - - viper.Set("Permalinks", map[string]interface{}{"section": "/:title"}) - s := newSiteDefaultLang() - - s.initialize() - permalink := s.Info.Permalinks["section"] - - if permalink != "/:title" { - t.Errorf("Could not set permalink (%#v)", permalink) - } -} diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index c508fbc31..58408ce47 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -37,37 +37,20 @@ const SITEMAP_TEMPLATE = `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap </urlset>` func TestSitemapOutput(t *testing.T) { - viper.Reset() - defer viper.Reset() - - hugofs.InitMemFs() + testCommonResetState() viper.Set("baseurl", "http://auth/bub/") s := &Site{ - Source: &source.InMemorySource{ByteSource: weightedSources}, - Lang: newDefaultLanguage(), - } - - s.initializeSiteInfo() - - s.prepTemplates("sitemap.xml", SITEMAP_TEMPLATE) - - createPagesAndMeta(t, s) - - if err := s.renderHomePage(); err != nil { - t.Fatalf("Unable to RenderHomePage: %s", err) - } - - if err := s.renderSitemap(); err != nil { - t.Fatalf("Unable to RenderSitemap: %s", err) + Source: &source.InMemorySource{ByteSource: weightedSources}, + Language: newDefaultLanguage(), } - if err := s.renderRobotsTXT(); err != nil { - t.Fatalf("Unable to RenderRobotsTXT :%s", err) + if err := buildAndRenderSite(s, "sitemap.xml", SITEMAP_TEMPLATE); err != nil { + t.Fatalf("Failed to build site: %s", err) } - sitemapFile, err := hugofs.Destination().Open("sitemap.xml") + sitemapFile, err := hugofs.Destination().Open("public/sitemap.xml") if err != nil { t.Fatalf("Unable to locate: sitemap.xml") diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go index 03d567aa7..9b83f7627 100644 --- a/hugolib/taxonomy_test.go +++ b/hugolib/taxonomy_test.go @@ -21,8 +21,7 @@ import ( ) func TestByCountOrderOfTaxonomies(t *testing.T) { - viper.Reset() - defer viper.Reset() + defer testCommonResetState() taxonomies := make(map[string]string) diff --git a/hugolib/translations.go b/hugolib/translations.go index 724f6a594..267545f37 100644 --- a/hugolib/translations.go +++ b/hugolib/translations.go @@ -14,7 +14,7 @@ package hugolib import ( - "fmt" + jww "github.com/spf13/jwalterweatherman" ) // Translations represent the other translations for a given page. The @@ -41,7 +41,10 @@ func pagesToTranslationsMap(ml *Multilingual, pages []*Page) map[string]Translat language := ml.Language(pageLang) if language == nil { - panic(fmt.Sprintf("Page language not found in multilang setup: %s", pageLang)) + // TODO(bep) ml + // This may or may not be serious. It can be a file named stefano.chiodino.md. + jww.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", pageLang) + language = ml.DefaultLang } page.language = language diff --git a/source/file.go b/source/file.go index 9012b91c4..4bee882a6 100644 --- a/source/file.go +++ b/source/file.go @@ -108,10 +108,11 @@ func (f *File) Path() string { } // NewFileWithContents creates a new File pointer with the given relative path and -// content. +// content. The language defaults to "en". func NewFileWithContents(relpath string, content io.Reader) *File { file := NewFile(relpath) file.Contents = content + file.lang = "en" return file } @@ -124,15 +125,16 @@ func NewFile(relpath string) *File { f.dir, f.logicalName = filepath.Split(f.relpath) f.ext = strings.TrimPrefix(filepath.Ext(f.LogicalName()), ".") f.baseName = helpers.Filename(f.LogicalName()) - if viper.GetBool("Multilingual") { - f.lang = strings.TrimPrefix(filepath.Ext(f.baseName), ".") + + f.lang = strings.TrimPrefix(filepath.Ext(f.baseName), ".") + if f.lang == "" { + f.lang = viper.GetString("DefaultContentLanguage") if f.lang == "" { - f.lang = viper.GetString("DefaultContentLanguage") + // TODO(bep) ml + f.lang = "en" } - f.translationBaseName = helpers.Filename(f.baseName) - } else { - f.translationBaseName = f.baseName } + f.translationBaseName = helpers.Filename(f.baseName) f.section = helpers.GuessSection(f.Dir()) f.uniqueID = helpers.Md5String(f.LogicalName()) diff --git a/source/filesystem.go b/source/filesystem.go index 7bdcd702f..82bcad6e6 100644 --- a/source/filesystem.go +++ b/source/filesystem.go @@ -105,6 +105,9 @@ func (f *Filesystem) captureFiles() { if err != nil { jww.ERROR.Println(err) + if err == helpers.WalkRootTooShortError { + panic("The root path is too short. If this is a test, make sure to init the content paths.") + } } } diff --git a/source/filesystem_test.go b/source/filesystem_test.go index d2101991b..a1e111d2f 100644 --- a/source/filesystem_test.go +++ b/source/filesystem_test.go @@ -22,7 +22,7 @@ import ( ) func TestEmptySourceFilesystem(t *testing.T) { - src := new(Filesystem) + src := &Filesystem{Base: "Empty"} if len(src.Files()) != 0 { t.Errorf("new filesystem should contain 0 files.") } diff --git a/tpl/template.go b/tpl/template.go index 4cc818f87..479298976 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -336,6 +336,8 @@ func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) er return err } + jww.DEBUG.Printf("Add template file from path %s", path) + return t.AddTemplate(name, string(b)) } @@ -366,11 +368,12 @@ func isBaseTemplate(path string) bool { } func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { + jww.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) walker := func(path string, fi os.FileInfo, err error) error { if err != nil { return nil } - + jww.DEBUG.Println("Template path", path) if fi.Mode()&os.ModeSymlink == os.ModeSymlink { link, err := filepath.EvalSymlinks(absPath) if err != nil { |