some content") enSite := sites[0] frSite := sites[1] assert.Len(enSite.RegularPages, 5) assert.Len(frSite.RegularPages, 4) // Verify translations b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Hello") b.AssertFileContent("public/fr/sect/doc1/index.html", "Bonjour") // check single page content b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour") b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello") contentFs := b.H.BaseFs.ContentFs 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 { func(t *testing.T) { fs.Source.Remove("content/sect/doc2.en.md") }, []fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc2.en.md"), Op: fsnotify.Remove}}, func(t *testing.T) { assert.Len(enSite.RegularPages, 4, "1 en removed") // Check build stats require.Equal(t, 1, enSite.draftCount, "Draft") require.Equal(t, 1, enSite.futureCount, "Future") require.Equal(t, 1, enSite.expiredCount, "Expired") require.Equal(t, 0, frSite.draftCount, "Draft") require.Equal(t, 1, frSite.futureCount, "Future") require.Equal(t, 1, frSite.expiredCount, "Expired") }, }, { func(t *testing.T) { writeNewContentFile(t, contentFs, "new_en_1", "2016-07-31", "new1.en.md", -5) writeNewContentFile(t, contentFs, "new_en_2", "1989-07-30", "new2.en.md", -10) writeNewContentFile(t, contentFs, "new_fr_1", "2016-07-30", "new1.fr.md", 10) }, []fsnotify.Event{ {Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Create}, {Name: filepath.FromSlash("content/new2.en.md"), Op: fsnotify.Create}, {Name: filepath.FromSlash("content/new1.fr.md"), Op: fsnotify.Create}, }, func(t *testing.T) { assert.Len(enSite.RegularPages, 6) assert.Len(enSite.AllPages, 34) assert.Len(frSite.RegularPages, 5) require.Equal(t, "new_fr_1", frSite.RegularPages[3].title) require.Equal(t, "new_en_2", enSite.RegularPages[0].title) require.Equal(t, "new_en_1", enSite.RegularPages[1].title) rendered := readDestination(t, fs, "public/en/new1/index.html") require.True(t, strings.Contains(rendered, "new_en_1"), rendered) }, }, { func(t *testing.T) { p := "sect/doc1.en.md" doc1 := readFileFromFs(t, contentFs, p) doc1 += "CHANGED" writeToFs(t, contentFs, p, doc1) }, []fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc1.en.md"), Op: fsnotify.Write}}, func(t *testing.T) { assert.Len(enSite.RegularPages, 6) doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") require.True(t, strings.Contains(doc1, "CHANGED"), doc1) }, }, // Rename a file { func(t *testing.T) { if err := contentFs.Rename("new1.en.md", "new1renamed.en.md"); err != nil { t.Fatalf("Rename failed: %s", err) } }, []fsnotify.Event{ {Name: filepath.FromSlash("content/new1renamed.en.md"), Op: fsnotify.Rename}, {Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Rename}, }, func(t *testing.T) { assert.Len(enSite.RegularPages, 6, "Rename") require.Equal(t, "new_en_1", enSite.RegularPages[1].title) rendered := readDestination(t, fs, "public/en/new1renamed/index.html") require.True(t, strings.Contains(rendered, "new_en_1"), rendered) }}, { // Change a template func(t *testing.T) { template := "layouts/_default/single.html" templateContent := readSource(t, fs, template) templateContent += "{{ print \"Template Changed\"}}" writeSource(t, fs, template, templateContent) }, []fsnotify.Event{{Name: filepath.FromSlash("layouts/_default/single.html"), Op: fsnotify.Write}}, func(t *testing.T) { assert.Len(enSite.RegularPages, 6) assert.Len(enSite.AllPages, 34) assert.Len(frSite.RegularPages, 5) doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") require.True(t, strings.Contains(doc1, "Template Changed"), doc1) }, }, { // Change a language file func(t *testing.T) { languageFile := "i18n/fr.yaml" langContent := readSource(t, fs, languageFile) langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) writeSource(t, fs, languageFile, langContent) }, []fsnotify.Event{{Name: filepath.FromSlash("i18n/fr.yaml"), Op: fsnotify.Write}}, func(t *testing.T) { assert.Len(enSite.RegularPages, 6) assert.Len(enSite.AllPages, 34) assert.Len(frSite.RegularPages, 5) docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") require.True(t, strings.Contains(docEn, "Hello"), "No Hello") docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html") require.True(t, strings.Contains(docFr, "Salut"), "No Salut") homeEn := enSite.getPage(KindHome) require.NotNil(t, homeEn) assert.Len(homeEn.Translations(), 3) require.Equal(t, "fr", homeEn.Translations()[0].Lang()) }, }, // Change a shortcode { func(t *testing.T) { writeSource(t, fs, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") }, []fsnotify.Event{ {Name: filepath.FromSlash("layouts/shortcodes/shortcode.html"), Op: fsnotify.Write}, }, func(t *testing.T) { assert.Len(enSite.RegularPages, 6) assert.Len(enSite.AllPages, 34) assert.Len(frSite.RegularPages, 5) b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Modified Shortcode: Salut") b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Modified Shortcode: Hello") }, }, } { if this.preFunc != nil { this.preFunc(t) } err := b.H.Build(BuildCfg{}, this.events...) if err != nil { t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) } this.assertFunc(t) } // Check that the drafts etc. are not built/processed/rendered. assertShouldNotBuild(t, b.H) } func assertShouldNotBuild(t *testing.T, sites *HugoSites) { s := sites.Sites[0] for _, p := range s.rawAllPages { // No HTML when not processed require.Equal(t, p.shouldBuild(), bytes.Contains(p.workContent, []byte("")), p.BaseFileName()+": "+string(p.workContent)) require.Equal(t, p.shouldBuild(), p.content != "", p.BaseFileName()) require.Equal(t, p.shouldBuild(), p.content != "", p.BaseFileName()) } } func TestAddNewLanguage(t *testing.T) { t.Parallel() assert := require.New(t) b := newMultiSiteTestDefaultBuilder(t) b.CreateSites().Build(BuildCfg{}) fs := b.Fs newConfig := multiSiteTOMLConfigTemplate + ` [Languages.sv] weight = 15 title = "Svenska" ` writeNewContentFile(t, fs.Source, "Swedish Contentfile", "2016-01-01", "content/sect/doc1.sv.md", 10) // replace the config b.WithNewConfig(newConfig) sites := b.H // Watching does not work with in-memory fs, so we trigger a reload manually assert.NoError(sites.Cfg.(*helpers.Language).Cfg.(*viper.Viper).ReadInConfig()) err := b.H.Build(BuildCfg{CreateSitesFromConfig: true}) if err != nil { t.Fatalf("Failed to rebuild sites: %s", err) } require.Len(t, sites.Sites, 5, fmt.Sprintf("Len %d", len(sites.Sites))) // The Swedish site should be put in the middle (language weight=15) enSite := sites.Sites[0] svSite := sites.Sites[1] frSite := sites.Sites[2] require.True(t, enSite.Language.Lang == "en", enSite.Language.Lang) require.True(t, svSite.Language.Lang == "sv", svSite.Language.Lang) require.True(t, frSite.Language.Lang == "fr", frSite.Language.Lang) homeEn := enSite.getPage(KindHome) require.NotNil(t, homeEn) require.Len(t, homeEn.Translations(), 4) require.Equal(t, "sv", homeEn.Translations()[0].Lang()) require.Len(t, enSite.RegularPages, 5) require.Len(t, frSite.RegularPages, 4) // Veriy Swedish site require.Len(t, svSite.RegularPages, 1) svPage := svSite.RegularPages[0] require.Equal(t, "Swedish Contentfile", svPage.title) require.Equal(t, "sv", svPage.Lang()) require.Len(t, svPage.Translations(), 2) require.Len(t, svPage.AllTranslations(), 3) require.Equal(t, "en", svPage.Translations()[0].Lang()) // Regular pages have no children require.Len(t, svPage.Pages, 0) require.Len(t, svPage.Data["Pages"], 0) } func TestChangeDefaultLanguage(t *testing.T) { t.Parallel() assert := require.New(t) b := newMultiSiteTestBuilder(t, "", "", map[string]interface{}{ "DefaultContentLanguage": "fr", "DefaultContentLanguageInSubdir": false, }) b.CreateSites().Build(BuildCfg{}) b.AssertFileContent("public/sect/doc1/index.html", "Single", "Bonjour") b.AssertFileContent("public/en/sect/doc2/index.html", "Single", "Hello") // Switch language b.WithNewConfigData(map[string]interface{}{ "DefaultContentLanguage": "en", "DefaultContentLanguageInSubdir": false, }) // Watching does not work with in-memory fs, so we trigger a reload manually // This does not look pretty, so we should think of something else. assert.NoError(b.H.Cfg.(*helpers.Language).Cfg.(*viper.Viper).ReadInConfig()) err := b.H.Build(BuildCfg{CreateSitesFromConfig: true}) if err != nil { t.Fatalf("Failed to rebuild sites: %s", err) } // Default language is now en, so that should now be the "root" language b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Bonjour") b.AssertFileContent("public/sect/doc2/index.html", "Single", "Hello") } func TestTableOfContentsInShortcodes(t *testing.T) { t.Parallel() b := newMultiSiteTestDefaultBuilder(t) b.WithTemplatesAdded("layouts/shortcodes/toc.html", tocShortcode) b.WithContent("post/simple.en.md", tocPageSimple) b.WithContent("post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings) b.CreateSites().Build(BuildCfg{}) b.AssertFileContent("public/en/post/simple/index.html", tocPageSimpleExpected) b.AssertFileContent("public/en/post/withSCInHeading/index.html", tocPageWithShortcodesInHeadingsExpected) } var tocShortcode = ` {{ .Page.TableOfContents }} ` var tocPageSimple = `--- title: tocTest publishdate: "2000-01-01" --- {{< toc >}} # Heading 1 {#1} Some text. ## Subheading 1.1 {#1-1} Some more text. # Heading 2 {#2} Even more text. ## Subheading 2.1 {#2-1} Lorem ipsum... ` var tocPageSimpleExpected = `` var tocPageWithShortcodesInHeadings = `--- title: tocTest publishdate: "2000-01-01" --- {{< toc >}} # Heading 1 {#1} Some text. ## Subheading 1.1 {{< shortcode >}} {#1-1} Some more text. # Heading 2 {{% shortcode %}} {#2} Even more text. ## Subheading 2.1 {#2-1} Lorem ipsum... ` var tocPageWithShortcodesInHeadingsExpected = `` var multiSiteTOMLConfigTemplate = ` baseURL = "http://example.com/blog" rssURI = "index.xml" paginate = 1 disablePathToLower = true defaultContentLanguage = "{{ .DefaultContentLanguage }}" defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }} [permalinks] other = "/somewhere/else/:filename" [blackfriday] angledQuotes = true [Taxonomies] tag = "tags" [Languages] [Languages.en] weight = 10 title = "In English" languageName = "English" [Languages.en.blackfriday] angledQuotes = false [[Languages.en.menu.main]] url = "/" name = "Home" weight = 0 [Languages.fr] weight = 20 title = "Le Français" languageName = "Français" [Languages.fr.Taxonomies] plaque = "plaques" [Languages.nn] weight = 30 title = "På nynorsk" languageName = "Nynorsk" paginatePath = "side" [Languages.nn.Taxonomies] lag = "lag" [[Languages.nn.menu.main]] url = "/" name = "Heim" weight = 1 [Languages.nb] weight = 40 title = "På bokmål" languageName = "Bokmål" paginatePath = "side" [Languages.nb.Taxonomies] lag = "lag" ` var multiSiteYAMLConfigTemplate = ` baseURL: "http://example.com/blog" rssURI: "index.xml" disablePathToLower: true paginate: 1 defaultContentLanguage: "{{ .DefaultContentLanguage }}" defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }} permalinks: other: "/somewhere/else/:filename" blackfriday: angledQuotes: true Taxonomies: tag: "tags" Languages: en: weight: 10 title: "In English" languageName: "English" blackfriday: angledQuotes: false menu: main: - url: "/" name: "Home" weight: 0 fr: weight: 20 title: "Le Français" languageName: "Français" Taxonomies: plaque: "plaques" nn: weight: 30 title: "På nynorsk" languageName: "Nynorsk" paginatePath: "side" Taxonomies: lag: "lag" menu: main: - url: "/" name: "Heim" weight: 1 nb: weight: 40 title: "På bokmål" languageName: "Bokmål" paginatePath: "side" Taxonomies: lag: "lag" ` // TODO(bep) clean move var multiSiteJSONConfigTemplate = ` { "baseURL": "http://example.com/blog", "rssURI": "index.xml", "paginate": 1, "disablePathToLower": true, "defaultContentLanguage": "{{ .DefaultContentLanguage }}", "defaultContentLanguageInSubdir": true, "permalinks": { "other": "/somewhere/else/:filename" }, "blackfriday": { "angledQuotes": true }, "Taxonomies": { "tag": "tags" }, "Languages": { "en": { "weight": 10, "title": "In English", "languageName": "English", "blackfriday": { "angledQuotes": false }, "menu": { "main": [ { "url": "/", "name": "Home", "weight": 0 } ] } }, "fr": { "weight": 20, "title": "Le Français", "languageName": "Français", "Taxonomies": { "plaque": "plaques" } }, "nn": { "weight": 30, "title": "På nynorsk", "paginatePath": "side", "languageName": "Nynorsk", "Taxonomies": { "lag": "lag" }, "menu": { "main": [ { "url": "/", "name": "Heim", "weight": 1 } ] } }, "nb": { "weight": 40, "title": "På bokmål", "paginatePath": "side", "languageName": "Bokmål", "Taxonomies": { "lag": "lag" } } } } ` func writeSource(t testing.TB, fs *hugofs.Fs, filename, content string) { writeToFs(t, fs.Source, filename, content) } func writeToFs(t testing.TB, fs afero.Fs, filename, content string) { if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil { t.Fatalf("Failed to write file: %s", err) } } func readDestination(t testing.TB, fs *hugofs.Fs, filename string) string { return readFileFromFs(t, fs.Destination, filename) } func destinationExists(fs *hugofs.Fs, filename string) bool { b, err := helpers.Exists(filename, fs.Destination) if err != nil { panic(err) } return b } func readSource(t *testing.T, fs *hugofs.Fs, filename string) string { return readFileFromFs(t, fs.Source, filename) } func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { filename = filepath.Clean(filename) b, err := afero.ReadFile(fs, filename) if err != nil { // Print some debug info root := strings.Split(filename, helpers.FilePathSeparator)[0] printFs(fs, root, os.Stdout) Fatalf(t, "Failed to read file: %s", err) } return string(b) } func printFs(fs afero.Fs, path string, w io.Writer) { if fs == nil { return } afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { if info != nil && !info.IsDir() { s := path if lang, ok := info.(hugofs.LanguageAnnouncer); ok { s = s + "\tLANG: " + lang.Lang() } if fp, ok := info.(hugofs.FilePather); ok { s = s + "\tRF: " + fp.Filename() + "\tBP: " + fp.BaseDir() } fmt.Fprintln(w, " ", s) } return nil }) } 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, fs afero.Fs, title, date, filename string, weight int) { content := newTestPage(title, date, weight) writeToFs(t, fs, filename, content) } type multiSiteTestBuilder struct { configData interface{} config string configFormat string *sitesBuilder } func newMultiSiteTestDefaultBuilder(t testing.TB) *multiSiteTestBuilder { return newMultiSiteTestBuilder(t, "", "", nil) } func (b *multiSiteTestBuilder) WithNewConfig(config string) *multiSiteTestBuilder { b.WithConfigTemplate(b.configData, b.configFormat, config) return b } func (b *multiSiteTestBuilder) WithNewConfigData(data interface{}) *multiSiteTestBuilder { b.WithConfigTemplate(data, b.configFormat, b.config) return b } func newMultiSiteTestBuilder(t testing.TB, configFormat, config string, configData interface{}) *multiSiteTestBuilder { if configData == nil { configData = map[string]interface{}{ "DefaultContentLanguage": "fr", "DefaultContentLanguageInSubdir": true, } } if config == "" { config = multiSiteTOMLConfigTemplate } if configFormat == "" { configFormat = "toml" } b := newTestSitesBuilder(t).WithConfigTemplate(configData, configFormat, config) b.WithContent("root.en.md", `--- title: root weight: 10000 slug: root publishdate: "2000-01-01" --- # root `, "sect/doc1.en.md", `--- title: doc1 weight: 1 slug: doc1-slug tags: - tag1 publishdate: "2000-01-01" --- # doc1 *some "content"* {{< shortcode >}} {{< lingo >}} NOTE: slug should be used as URL `, "sect/doc1.fr.md", `--- title: doc1 weight: 1 plaques: - frtag1 - frtag2 publishdate: "2000-01-04" --- # doc1 *quelque "contenu"* {{< shortcode >}} {{< lingo >}} NOTE: should be in the 'en' Page's 'Translations' field. NOTE: date is after "doc3" `, "sect/doc2.en.md", `--- title: doc2 weight: 2 publishdate: "2000-01-02" --- # doc2 *some content* NOTE: without slug, "doc2" should be used, without ".en" as URL `, "sect/doc3.en.md", `--- title: doc3 weight: 3 publishdate: "2000-01-03" aliases: [/en/al/alias1,/al/alias2/] tags: - tag2 - tag1 url: /superbob --- # doc3 *some content* NOTE: third 'en' doc, should trigger pagination on home page. `, "sect/doc4.md", `--- title: doc4 weight: 4 plaques: - frtag1 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' `, "other/doc5.fr.md", `--- title: doc5 weight: 5 publishdate: "2000-01-06" --- # doc5 *autre contenu francophone* NOTE: should use the "permalinks" configuration with :filename `, // Add some for the stats "stats/expired.fr.md", `--- title: expired publishdate: "2000-01-06" expiryDate: "2001-01-06" --- # Expired `, "stats/future.fr.md", `--- title: future weight: 6 publishdate: "2100-01-06" --- # Future `, "stats/expired.en.md", `--- title: expired weight: 7 publishdate: "2000-01-06" expiryDate: "2001-01-06" --- # Expired `, "stats/future.en.md", `--- title: future weight: 6 publishdate: "2100-01-06" --- # Future `, "stats/draft.en.md", `--- title: expired publishdate: "2000-01-06" draft: true --- # Draft `, "stats/tax.nn.md", `--- title: Tax NN weight: 8 publishdate: "2000-01-06" weight: 1001 lag: - Sogndal --- # Tax NN `, "stats/tax.nb.md", `--- title: Tax NB weight: 8 publishdate: "2000-01-06" weight: 1002 lag: - Sogndal --- # Tax NB `, // Bundle "bundles/b1/index.en.md", `--- title: Bundle EN publishdate: "2000-01-06" weight: 2001 --- # Bundle Content EN `, "bundles/b1/index.md", `--- title: Bundle Default publishdate: "2000-01-06" weight: 2002 --- # Bundle Content Default `, "bundles/b1/logo.png", ` PNG Data `) return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData} }