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) + } + + for _, lang := range languages { + sites = append(sites, hugolib.NewSite(lang)) + } - if err != nil { - return nil, fmt.Errorf("Failed to parse multilingual config: %s", err) } - for _, lang := range langConfigsList { - s := hugolib.NewSite(lang) - s.SetMultilingualConfig(lang, langConfigsList) - h = append(h, s) - } + return hugolib.NewHugoSites(sites...) - return h, nil } 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_abs", "") - - // From site_test.go - createAndRenderPages(t, s) + "head_abs", ""); err != nil { + t.Fatalf("Failed to render site: %s", err) + } tests := []struct { doc string expected string }{ - {filepath.FromSlash("sect/doc1.html"), "\n\nsome content
\n"}, - {filepath.FromSlash("sect/doc2.html"), "more content"}, - {filepath.FromSlash("sect/doc3.html"), "\n\nsome content
\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"), "doc7 content"}, - {filepath.FromSlash("sect/doc8.html"), "\n\nsome content
\n"}, + {filepath.FromSlash("public/sect/doc1.html"), "\n\nsome content
\n"}, + {filepath.FromSlash("public/sect/doc2.html"), "more content"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\nsome content
\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"), "doc7 content"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\nsome content
\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] - site.RunMode.Watching = watching + for _, s := range h.Sites { + // TODO(bep) ml + s.Multilingual = h.Multilingual + s.RunMode.Watching = config.Watching + } - if err := site.Build(); err != nil { - return err - } - if printStats { - site.Stats(t1) + if err := firstSite.PreProcess(config); err != nil { + return err + } + + h.setupTranslations(firstSite) + + if len(h.Sites) > 1 { + // Initialize the rest + for _, site := range h.Sites[1:] { + site.Tmpl = firstSite.Tmpl + site.initializeSiteInfo() } } - if printStats { + for _, s := range h.Sites { + + if err := s.PostProcess(); err != nil { + return err + } + + 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 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 diff --git a/hugolib/public/index.html b/hugolib/public/index.html new file mode 100644 index 000000000..e69de29bb 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 @@ + +text
\n\nmore text
\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() + if err := buildAndRenderSite(s, "_default/single.html", "{{.Content}}"); err != nil { + t.Fatalf("Failed to build site: %s", err) + } - s.prepTemplates("_default/single.html", "{{.Content}}") - - 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", "some content
\n"}, - {filepath.FromSlash("404.html"), "Page Not Found."}, - {filepath.FromSlash("index.xml"), "\ndoc2 content
\n"}, + {filepath.FromSlash("public/ugly.html"), "\n\ndoc2 content
\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_abs", "") - - createAndRenderPages(t, s) + "head_abs", ""); 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("link")}, - {Name: filepath.FromSlash("content/blue/doc2.html"), Content: []byte("---\nf: t\n---\nmore content")}, + {Name: filepath.FromSlash("blue/doc2.html"), Content: []byte("---\nf: t\n---\nmore content")}, } 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", "Going"}, + {"blue/doc2.html", "Going"}, {"sect/doc1.html", "link"}, } @@ -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 = `