From aa6b1b9be7c9d7322333893b642aaf8c7a5f2c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 2 Jul 2017 10:46:28 +0200 Subject: [PATCH] output: Support templates per site/language This applies to both regular templates and shortcodes. So, if the site language is French and the output format is AMP, this is the (start) of the lookup order for the home page: 1. index.fr.amp.html 2. index.amp.html 3. index.fr.html 4. index.html 5. ... Fixes #3360 --- hugolib/hugo_sites_build_test.go | 36 ++++++++++++-- hugolib/page.go | 1 + hugolib/shortcode.go | 21 ++++++-- hugolib/shortcode_test.go | 4 +- output/docshelper.go | 1 + output/layout.go | 84 +++++++++++++++++++++----------- output/layout_test.go | 2 + 7 files changed, 109 insertions(+), 40 deletions(-) diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index c343f6087a8..96e2c66b2ed 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -305,12 +305,12 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) { require.True(t, strings.Contains(languageRedirect, "0; url=http://example.com/blog/fr"), languageRedirect) // check home page content (including data files rendering) - th.assertFileContent("public/en/index.html", "Home Page 1", "Hello", "Hugo Rocks!") - th.assertFileContent("public/fr/index.html", "Home Page 1", "Bonjour", "Hugo Rocks!") + th.assertFileContent("public/en/index.html", "Default Home Page 1", "Hello", "Hugo Rocks!") + th.assertFileContent("public/fr/index.html", "French Home Page 1", "Bonjour", "Hugo Rocks!") // check single page content - th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour") - th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello") + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour", "LingoFrench") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault") // Check node translations homeEn := enSite.getPage(KindHome) @@ -1042,7 +1042,14 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf if err := afero.WriteFile(mf, filepath.Join("layouts", "index.html"), - []byte("{{ $p := .Paginator }}Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), + []byte("{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "index.fr.html"), + []byte("{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), 0755); err != nil { t.Fatalf("Failed to write layout file: %s", err) } @@ -1055,6 +1062,21 @@ func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, conf t.Fatalf("Failed to write layout file: %s", err) } + // A shortcode in multiple languages + if err := afero.WriteFile(mf, + filepath.Join("layouts", "shortcodes", "lingo.html"), + []byte("LingoDefault"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "shortcodes", "lingo.fr.html"), + []byte("LingoFrench"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + // Add some language files if err := afero.WriteFile(mf, filepath.Join("i18n", "en.yaml"), @@ -1098,6 +1120,8 @@ publishdate: "2000-01-01" {{< shortcode >}} +{{< lingo >}} + NOTE: slug should be used as URL `)}, {Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`--- @@ -1113,6 +1137,8 @@ publishdate: "2000-01-04" {{< shortcode >}} +{{< lingo >}} + NOTE: should be in the 'en' Page's 'Translations' field. NOTE: date is after "doc3" `)}, diff --git a/hugolib/page.go b/hugolib/page.go index cf0a2144c5a..0f44b8b99cf 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -250,6 +250,7 @@ func (p *Page) createLayoutDescriptor() output.LayoutDescriptor { return output.LayoutDescriptor{ Kind: p.Kind, Type: p.Type(), + Lang: p.Lang(), Layout: p.Layout, Section: section, } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 150d82c444f..3cf472f82ce 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -157,6 +157,7 @@ func (sc shortcode) String() string { // Note that in the below, OutputFormat may be empty. // We will try to look for the most specific shortcode template available. type scKey struct { + Lang string OutputFormat string Suffix string ShortcodePlaceholder string @@ -166,8 +167,8 @@ func newScKey(m media.Type, shortcodeplaceholder string) scKey { return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder} } -func newScKeyFromOutputFormat(o output.Format, shortcodeplaceholder string) scKey { - return scKey{Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder} +func newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey { + return scKey{Lang: lang, Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder} } func newDefaultScKey(shortcodeplaceholder string) scKey { @@ -251,10 +252,11 @@ const innerCleanupExpand = "$1" func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) { m := make(map[scKey]func() (string, error)) + lang := p.Lang() for _, f := range p.outputFormats { // The most specific template will win. - key := newScKeyFromOutputFormat(f, placeholder) + key := newScKeyFromLangAndOutputFormat(lang, f, placeholder) m[key] = func() (string, error) { return renderShortcode(key, sc, nil, p), nil } @@ -371,9 +373,11 @@ func (s *shortcodeHandler) updateDelta() bool { func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) { contentShortcodesForOuputFormat := make(map[scKey]func() (string, error)) + lang := s.p.Lang() + for shortcodePlaceholder := range s.shortcodes { - key := newScKeyFromOutputFormat(f, shortcodePlaceholder) + key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder) renderFn, found := s.contentShortcodes[key] if !found { @@ -390,7 +394,7 @@ func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map if !found { panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder)) } - contentShortcodesForOuputFormat[newScKeyFromOutputFormat(f, shortcodePlaceholder)] = renderFn + contentShortcodesForOuputFormat[newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)] = renderFn } return contentShortcodesForOuputFormat @@ -676,12 +680,19 @@ func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.T suffix := strings.ToLower(key.Suffix) outFormat := strings.ToLower(key.OutputFormat) + lang := strings.ToLower(key.Lang) if outFormat != "" && suffix != "" { + if lang != "" { + names = append(names, fmt.Sprintf("%s.%s.%s.%s", shortcodeName, lang, outFormat, suffix)) + } names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix)) } if suffix != "" { + if lang != "" { + names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, lang, suffix)) + } names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix)) } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 42da9e46549..3d355f947ee 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -837,8 +837,8 @@ func TestReplaceShortcodeTokens(t *testing.T) { func TestScKey(t *testing.T) { require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"}, newScKey(media.XMLType, "ABCD")) - require.Equal(t, scKey{Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"}, - newScKeyFromOutputFormat(output.AMPFormat, "EFGH")) + require.Equal(t, scKey{Lang: "en", Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"}, + newScKeyFromLangAndOutputFormat("en", output.AMPFormat, "EFGH")) require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"}, newDefaultScKey("IJKL")) diff --git a/output/docshelper.go b/output/docshelper.go index 1df45726c64..c45e956e645 100644 --- a/output/docshelper.go +++ b/output/docshelper.go @@ -44,6 +44,7 @@ func createLayoutExamples() interface{} { f Format }{ {`AMP home, with theme "demoTheme".`, LayoutDescriptor{Kind: "home"}, true, "", AMPFormat}, + {`AMP home, French language".`, LayoutDescriptor{Kind: "home", Lang: "fr"}, false, "", AMPFormat}, {"JSON home, no theme.", LayoutDescriptor{Kind: "home"}, false, "", JSONFormat}, {fmt.Sprintf(`CSV regular, "layout: %s" in front matter.`, demoLayout), LayoutDescriptor{Kind: "page", Layout: demoLayout}, false, "", CSVFormat}, {fmt.Sprintf(`JSON regular, "type: %s" in front matter.`, demoType), LayoutDescriptor{Kind: "page", Type: demoType}, false, "", JSONFormat}, diff --git a/output/layout.go b/output/layout.go index cacb92b80d4..6c054b6c44a 100644 --- a/output/layout.go +++ b/output/layout.go @@ -26,6 +26,7 @@ type LayoutDescriptor struct { Type string Section string Kind string + Lang string Layout string } @@ -55,31 +56,33 @@ func NewLayoutHandler(hasTheme bool) *LayoutHandler { const ( + // TODO(bep) variations reduce to 1 "." + // The RSS templates doesn't map easily into the regular pages. - layoutsRSSHome = `NAME.SUFFIX _default/NAME.SUFFIX _internal/_default/rss.xml` - layoutsRSSSection = `section/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml` - layoutsRSSTaxonomy = `taxonomy/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml` - layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml` + layoutsRSSHome = `VARIATIONS _default/VARIATIONS _internal/_default/rss.xml` + layoutsRSSSection = `section/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml` + layoutsRSSTaxonomy = `taxonomy/SECTION.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml` + layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.VARIATIONS _default/VARIATIONS VARIATIONS _internal/_default/rss.xml` - layoutsHome = "index.NAME.SUFFIX index.SUFFIX _default/list.NAME.SUFFIX _default/list.SUFFIX" + layoutsHome = "index.VARIATIONS _default/list.VARIATIONS" layoutsSection = ` -section/SECTION.NAME.SUFFIX section/SECTION.SUFFIX -SECTION/list.NAME.SUFFIX SECTION/list.SUFFIX -_default/section.NAME.SUFFIX _default/section.SUFFIX -_default/list.NAME.SUFFIX _default/list.SUFFIX -indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX -_default/indexes.NAME.SUFFIX _default/indexes.SUFFIX +section/SECTION.VARIATIONS +SECTION/list.VARIATIONS +_default/section.VARIATIONS +_default/list.VARIATIONS +indexes/SECTION.VARIATIONS +_default/indexes.VARIATIONS ` layoutsTaxonomy = ` -taxonomy/SECTION.NAME.SUFFIX taxonomy/SECTION.SUFFIX -indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX -_default/taxonomy.NAME.SUFFIX _default/taxonomy.SUFFIX -_default/list.NAME.SUFFIX _default/list.SUFFIX +taxonomy/SECTION.VARIATIONS +indexes/SECTION.VARIATIONS +_default/taxonomy.VARIATIONS +_default/list.VARIATIONS ` layoutsTaxonomyTerm = ` -taxonomy/SECTION.terms.NAME.SUFFIX taxonomy/SECTION.terms.SUFFIX -_default/terms.NAME.SUFFIX _default/terms.SUFFIX -indexes/indexes.NAME.SUFFIX indexes/indexes.SUFFIX +taxonomy/SECTION.terms.VARIATIONS +_default/terms.VARIATIONS +indexes/indexes.VARIATIONS ` ) @@ -185,14 +188,41 @@ func resolveListTemplate(d LayoutDescriptor, f Format, } func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string { - delim := "." - if f.MediaType.Delimiter == "" { - delim = "" + + // VARIATIONS will be replaced with + // .lang.name.suffix + // .name.suffix + // .lang.suffix + // .suffix + var replacementValues []string + + name := strings.ToLower(f.Name) + + if d.Lang != "" { + replacementValues = append(replacementValues, fmt.Sprintf("%s.%s.%s", d.Lang, name, f.MediaType.Suffix)) + } + + replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", name, f.MediaType.Suffix)) + + if d.Lang != "" { + replacementValues = append(replacementValues, fmt.Sprintf("%s.%s", d.Lang, f.MediaType.Suffix)) + } + + isRSS := f.Name == RSSFormat.Name + + if !isRSS { + replacementValues = append(replacementValues, f.MediaType.Suffix) + } + + var layouts []string + + templFields := strings.Fields(templ) + + for _, field := range templFields { + for _, replacements := range replacementValues { + layouts = append(layouts, replaceKeyValues(field, "VARIATIONS", replacements, "SECTION", d.Section)) + } } - layouts := strings.Fields(replaceKeyValues(templ, - ".SUFFIX", delim+f.MediaType.Suffix, - "NAME", strings.ToLower(f.Name), - "SECTION", d.Section)) return filterDotLess(layouts) } @@ -201,9 +231,7 @@ func filterDotLess(layouts []string) []string { var filteredLayouts []string for _, l := range layouts { - // This may be constructed, but media types can be suffix-less, but can contain - // a delimiter. - l = strings.TrimSuffix(l, ".") + l = strings.Trim(l, ".") // If media type has no suffix, we have "index" type of layouts in this list, which // doesn't make much sense. if strings.Contains(l, ".") { diff --git a/output/layout_test.go b/output/layout_test.go index 9d4d2f6d5f0..6fb958c9dfe 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -59,6 +59,8 @@ func TestLayout(t *testing.T) { }{ {"Home", LayoutDescriptor{Kind: "home"}, true, "", ampType, []string{"index.amp.html", "index.html", "_default/list.amp.html", "_default/list.html", "theme/index.amp.html", "theme/index.html"}}, + {"Home, french language", LayoutDescriptor{Kind: "home", Lang: "fr"}, true, "", ampType, + []string{"index.fr.amp.html", "index.amp.html", "index.fr.html", "index.html", "_default/list.fr.amp.html", "_default/list.amp.html", "_default/list.fr.html", "_default/list.html", "theme/index.fr.amp.html", "theme/index.amp.html", "theme/index.fr.html"}}, {"Home, no ext or delim", LayoutDescriptor{Kind: "home"}, true, "", noExtDelimFormat, []string{"index.nem", "_default/list.nem"}}, {"Home, no ext", LayoutDescriptor{Kind: "home"}, true, "", noExt,