From 8b5b558bb515e80da640f5e114169874771b61e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 27 Mar 2017 20:43:49 +0200 Subject: tpl: Rework to handle both text and HTML templates Before this commit, Hugo used `html/template` for all Go templates. While this is a fine choice for HTML and maybe also RSS feeds, it is painful for plain text formats such as CSV, JSON etc. This commit fixes that by using the `IsPlainText` attribute on the output format to decide what to use. A couple of notes: * The above requires a nonambiguous template name to type mapping. I.e. `/layouts/_default/list.json` will only work if there is only one JSON output format, `/layouts/_default/list.mytype.json` will always work. * Ambiguous types will fall back to HTML. * Partials inherits the text vs HTML identificator of the container template. This also means that plain text templates can only include plain text partials. * Shortcode templates are, by definition, currently HTML templates only. Fixes #3221 --- output/layout.go | 28 ++++++++++++++++++++++++---- output/layout_base.go | 19 ++++++++++++++++++- output/layout_base_test.go | 6 ++++++ output/layout_test.go | 4 ++++ output/outputFormat.go | 40 ++++++++++++++++++++++++++++++++++++++++ output/outputFormat_test.go | 27 ++++++++++++++++++++++++++- 6 files changed, 118 insertions(+), 6 deletions(-) (limited to 'output') diff --git a/output/layout.go b/output/layout.go index a2bfd7717..6dba7f3b4 100644 --- a/output/layout.go +++ b/output/layout.go @@ -152,9 +152,11 @@ func (l *LayoutHandler) For(d LayoutDescriptor, layoutOverride string, f Format) } } - return layoutsWithThemeLayouts, nil + layouts = layoutsWithThemeLayouts } + layouts = prependTextPrefixIfNeeded(f, layouts...) + l.mu.Lock() l.cache[key] = layouts l.mu.Unlock() @@ -184,10 +186,26 @@ func resolveListTemplate(d LayoutDescriptor, f Format, } func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string { - return strings.Fields(replaceKeyValues(templ, + layouts := strings.Fields(replaceKeyValues(templ, "SUFFIX", f.MediaType.Suffix, "NAME", strings.ToLower(f.Name), "SECTION", d.Section)) + + return layouts +} + +func prependTextPrefixIfNeeded(f Format, layouts ...string) []string { + if !f.IsPlainText { + return layouts + } + + newLayouts := make([]string, len(layouts)) + + for i, l := range layouts { + newLayouts[i] = "_text/" + l + } + + return newLayouts } func replaceKeyValues(s string, oldNew ...string) string { @@ -195,7 +213,9 @@ func replaceKeyValues(s string, oldNew ...string) string { return replacer.Replace(s) } -func regularPageLayouts(types string, layout string, f Format) (layouts []string) { +func regularPageLayouts(types string, layout string, f Format) []string { + var layouts []string + if layout == "" { layout = "single" } @@ -219,5 +239,5 @@ func regularPageLayouts(types string, layout string, f Format) (layouts []string layouts = append(layouts, fmt.Sprintf("_default/%s.%s.%s", layout, name, suffix)) layouts = append(layouts, fmt.Sprintf("_default/%s.%s", layout, suffix)) - return + return layouts } diff --git a/output/layout_base.go b/output/layout_base.go index 2bb89c20d..a0d2bc4eb 100644 --- a/output/layout_base.go +++ b/output/layout_base.go @@ -29,7 +29,10 @@ var ( ) type TemplateNames struct { - Name string + // The name used as key in the template map. Note that this will be + // prefixed with "_text/" if it should be parsed with text/template. + Name string + OverlayFilename string MasterFilename string } @@ -51,6 +54,10 @@ type TemplateLookupDescriptor struct { // The theme name if active. Theme string + // All the output formats in play. This is used to decide if text/template or + // html/template. + OutputFormats Formats + FileExists func(filename string) (bool, error) ContainsAny func(filename string, subslices [][]byte) (bool, error) } @@ -74,6 +81,12 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) { // index.amp.html // index.json filename := filepath.Base(d.RelPath) + isPlainText := false + outputFormat, found := d.OutputFormats.FromFilename(filename) + + if found && outputFormat.IsPlainText { + isPlainText = true + } var ext, outFormat string @@ -90,6 +103,10 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) { id.OverlayFilename = fullPath id.Name = name + if isPlainText { + id.Name = "_text/" + id.Name + } + // Ace and Go templates may have both a base and inner template. pathDir := filepath.Dir(fullPath) diff --git a/output/layout_base_test.go b/output/layout_base_test.go index f20d99bef..16be615f2 100644 --- a/output/layout_base_test.go +++ b/output/layout_base_test.go @@ -141,6 +141,7 @@ func TestLayoutBase(t *testing.T) { return this.needsBase, nil } + this.d.OutputFormats = Formats{AMPFormat, HTMLFormat, RSSFormat, JSONFormat} this.d.WorkingDir = filepath.FromSlash(this.d.WorkingDir) this.d.LayoutDir = filepath.FromSlash(this.d.LayoutDir) this.d.RelPath = filepath.FromSlash(this.d.RelPath) @@ -150,6 +151,11 @@ func TestLayoutBase(t *testing.T) { this.expect.MasterFilename = filepath.FromSlash(this.expect.MasterFilename) this.expect.OverlayFilename = filepath.FromSlash(this.expect.OverlayFilename) + if strings.Contains(this.d.RelPath, "json") { + // currently the only plain text templates in this test. + this.expect.Name = "_text/" + this.expect.Name + } + id, err := CreateTemplateNames(this.d) require.NoError(t, err) diff --git a/output/layout_test.go b/output/layout_test.go index e59a16fcb..6ea5a7617 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -64,6 +64,10 @@ func TestLayout(t *testing.T) { []string{"taxonomy/tag.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}}, {"RSS Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, false, "", RSSFormat, []string{"taxonomy/tag.terms.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}}, + {"Home plain text", LayoutDescriptor{Kind: "home"}, true, "", JSONFormat, + []string{"_text/index.json.json", "_text/index.json", "_text/_default/list.json.json", "_text/_default/list.json", "_text/theme/index.json.json", "_text/theme/index.json"}}, + {"Page plain text", LayoutDescriptor{Kind: "page"}, true, "", JSONFormat, + []string{"_text/_default/single.json.json", "_text/_default/single.json", "_text/theme/_default/single.json.json"}}, } { t.Run(this.name, func(t *testing.T) { l := NewLayoutHandler(this.hasTheme) diff --git a/output/outputFormat.go b/output/outputFormat.go index 76329a936..9d43b135a 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -33,6 +33,7 @@ var ( IsHTML: true, } + // CalendarFormat is AAA CalendarFormat = Format{ Name: "Calendar", MediaType: media.CalendarType, @@ -104,6 +105,45 @@ func (formats Formats) GetByName(name string) (f Format, found bool) { return } +func (formats Formats) GetBySuffix(name string) (f Format, found bool) { + for _, ff := range formats { + if name == ff.MediaType.Suffix { + if found { + // ambiguous + found = false + return + } + f = ff + found = true + } + } + return +} + +func (formats Formats) FromFilename(filename string) (f Format, found bool) { + // mytemplate.amp.html + // mytemplate.html + // mytemplate + var ext, outFormat string + + parts := strings.Split(filename, ".") + if len(parts) > 2 { + outFormat = parts[1] + ext = parts[2] + } else if len(parts) > 1 { + ext = parts[1] + } + + if outFormat != "" { + return formats.GetByName(outFormat) + } + + if ext != "" { + return formats.GetBySuffix(ext) + } + return +} + // Format represents an output representation, usually to a file on disk. type Format struct { // The Name is used as an identifier. Internal output formats (i.e. HTML and RSS) diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index e742012ba..b73e53f82 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -65,7 +65,7 @@ func TestDefaultTypes(t *testing.T) { } -func TestGetType(t *testing.T) { +func TestGetFormat(t *testing.T) { tp, _ := GetFormat("html") require.Equal(t, HTMLFormat, tp) tp, _ = GetFormat("HTML") @@ -73,3 +73,28 @@ func TestGetType(t *testing.T) { _, found := GetFormat("FOO") require.False(t, found) } + +func TestGeGetFormatByName(t *testing.T) { + formats := Formats{AMPFormat, CalendarFormat} + tp, _ := formats.GetByName("AMP") + require.Equal(t, AMPFormat, tp) + _, found := formats.GetByName("HTML") + require.False(t, found) + _, found = formats.GetByName("FOO") + require.False(t, found) +} + +func TestGeGetFormatByExt(t *testing.T) { + formats1 := Formats{AMPFormat, CalendarFormat} + formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat} + tp, _ := formats1.GetBySuffix("html") + require.Equal(t, AMPFormat, tp) + tp, _ = formats1.GetBySuffix("ics") + require.Equal(t, CalendarFormat, tp) + _, found := formats1.GetBySuffix("not") + require.False(t, found) + + // ambiguous + _, found = formats2.GetByName("html") + require.False(t, found) +} -- cgit v1.2.3