From 597e418cb02883418f2cebb41400e8e61413f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 2 Jan 2019 12:33:26 +0100 Subject: Make Page an interface The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct. This is all a preparation step for issue #5074, "pages from other data sources". But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes. Most notable changes: * The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday. This means that any markdown will partake in the global ToC and footnote context etc. * The Custom Output formats are now "fully virtualized". This removes many of the current limitations. * The taxonomy list type now has a reference to the `Page` object. This improves the taxonomy template `.Title` situation and make common template constructs much simpler. See #5074 Fixes #5763 Fixes #5758 Fixes #5090 Fixes #5204 Fixes #4695 Fixes #5607 Fixes #5707 Fixes #5719 Fixes #3113 Fixes #5706 Fixes #5767 Fixes #5723 Fixes #5769 Fixes #5770 Fixes #5771 Fixes #5759 Fixes #5776 Fixes #5777 Fixes #5778 --- tpl/collections/apply_test.go | 6 +- tpl/collections/collections.go | 8 +- tpl/collections/collections_test.go | 38 ++-- tpl/template.go | 43 ++++- tpl/template_info.go | 35 ++++ tpl/tplimpl/ace.go | 15 +- tpl/tplimpl/embedded/generate/generate.go | 4 +- tpl/tplimpl/embedded/templates.autogen.go | 24 ++- tpl/tplimpl/embedded/templates/_default/rss.xml | 8 +- .../embedded/templates/_default/sitemap.xml | 5 +- .../embedded/templates/_default/sitemapindex.xml | 1 + tpl/tplimpl/embedded/templates/disqus.html | 2 +- tpl/tplimpl/shortcodes.go | 148 +++++++++++++++ tpl/tplimpl/shortcodes_test.go | 94 ++++++++++ tpl/tplimpl/template.go | 201 +++++++++++++++++---- tpl/tplimpl/templateFuncster.go | 46 +---- tpl/tplimpl/template_ast_transformers.go | 132 ++++++++++++-- tpl/tplimpl/template_ast_transformers_test.go | 66 +++++-- tpl/tplimpl/template_funcs_test.go | 20 +- tpl/tplimpl/template_info_test.go | 56 ++++++ tpl/tplimpl/template_test.go | 66 ------- 21 files changed, 776 insertions(+), 242 deletions(-) create mode 100644 tpl/template_info.go create mode 100644 tpl/tplimpl/shortcodes.go create mode 100644 tpl/tplimpl/shortcodes_test.go create mode 100644 tpl/tplimpl/template_info_test.go delete mode 100644 tpl/tplimpl/template_test.go (limited to 'tpl') diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go index 0878844b2..edec3da18 100644 --- a/tpl/collections/apply_test.go +++ b/tpl/collections/apply_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -29,6 +29,10 @@ func (templateFinder) Lookup(name string) (tpl.Template, bool) { return nil, false } +func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { + return nil, false, false +} + func (templateFinder) GetFuncs() map[string]interface{} { return map[string]interface{}{ "print": fmt.Sprint, diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go index bad65369f..92a61e575 100644 --- a/tpl/collections/collections.go +++ b/tpl/collections/collections.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -329,13 +329,17 @@ func (ns *Namespace) Group(key interface{}, items interface{}) (interface{}, err return nil, errors.New("nil is not a valid key to group by") } + if g, ok := items.(collections.Grouper); ok { + return g.Group(key, items) + } + in := newSliceElement(items) if g, ok := in.(collections.Grouper); ok { return g.Group(key, items) } - return nil, fmt.Errorf("grouping not supported for type %T", items) + return nil, fmt.Errorf("grouping not supported for type %T %T", items, in) } // IsSet returns whether a given array, channel, slice, or map has a key diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index 0edb8299f..103aee59e 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -311,16 +311,16 @@ func TestIn(t *testing.T) { } } -type page struct { +type testPage struct { Title string } -func (p page) String() string { +func (p testPage) String() string { return "p-" + p.Title } -type pagesPtr []*page -type pagesVals []page +type pagesPtr []*testPage +type pagesVals []testPage func TestIntersect(t *testing.T) { t.Parallel() @@ -328,15 +328,15 @@ func TestIntersect(t *testing.T) { ns := New(&deps.Deps{}) var ( - p1 = &page{"A"} - p2 = &page{"B"} - p3 = &page{"C"} - p4 = &page{"D"} - - p1v = page{"A"} - p2v = page{"B"} - p3v = page{"C"} - p4v = page{"D"} + p1 = &testPage{"A"} + p2 = &testPage{"B"} + p3 = &testPage{"C"} + p4 = &testPage{"D"} + + p1v = testPage{"A"} + p2v = testPage{"B"} + p3v = testPage{"C"} + p4v = testPage{"D"} ) for i, test := range []struct { @@ -672,14 +672,14 @@ func TestUnion(t *testing.T) { ns := New(&deps.Deps{}) var ( - p1 = &page{"A"} - p2 = &page{"B"} + p1 = &testPage{"A"} + p2 = &testPage{"B"} // p3 = &page{"C"} - p4 = &page{"D"} + p4 = &testPage{"D"} - p1v = page{"A"} + p1v = testPage{"A"} //p2v = page{"B"} - p3v = page{"C"} + p3v = testPage{"C"} //p4v = page{"D"} ) diff --git a/tpl/template.go b/tpl/template.go index 3225814c0..07152166a 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -21,6 +21,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/hugofs" @@ -37,7 +39,8 @@ import ( ) var ( - _ TemplateExecutor = (*TemplateAdapter)(nil) + _ TemplateExecutor = (*TemplateAdapter)(nil) + _ TemplateInfoProvider = (*TemplateAdapter)(nil) ) // TemplateHandler manages the collection of templates. @@ -53,17 +56,47 @@ type TemplateHandler interface { RebuildClone() } +// TemplateVariants describes the possible variants of a template. +// All of these may be empty. +type TemplateVariants struct { + Language string + OutputFormat output.Format +} + // TemplateFinder finds templates. type TemplateFinder interface { + TemplateLookup + TemplateLookupVariant +} + +type TemplateLookup interface { Lookup(name string) (Template, bool) } +type TemplateLookupVariant interface { + // TODO(bep) this currently only works for shortcodes. + // We may unify and expand this variant pattern to the + // other templates, but we need this now for the shortcodes to + // quickly determine if a shortcode has a template for a given + // output format. + // It returns the template, if it was found or not and if there are + // alternative representations (output format, language). + // We are currently only interested in output formats, so we should improve + // this for speed. + LookupVariant(name string, variants TemplateVariants) (Template, bool, bool) +} + // Template is the common interface between text/template and html/template. type Template interface { Execute(wr io.Writer, data interface{}) error Name() string } +// TemplateInfoProvider provides some contextual information about a template. +type TemplateInfoProvider interface { + TemplateInfo() Info +} + // TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain. type TemplateParser interface { Parse(name, tpl string) (Template, error) @@ -92,6 +125,8 @@ type TemplateAdapter struct { Template Metrics metrics.Provider + Info Info + // The filesystem where the templates are stored. Fs afero.Fs @@ -133,6 +168,10 @@ func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) return } +func (t *TemplateAdapter) TemplateInfo() Info { + return t.Info +} + // The identifiers may be truncated in the log, e.g. // "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image" var identifiersRe = regexp.MustCompile("at \\<(.*?)(\\.{3})?\\>:") diff --git a/tpl/template_info.go b/tpl/template_info.go new file mode 100644 index 000000000..8568f46f0 --- /dev/null +++ b/tpl/template_info.go @@ -0,0 +1,35 @@ +// Copyright 2019 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 tpl + +// Increments on breaking changes. +const TemplateVersion = 2 + +// Info holds some info extracted from a parsed template. +type Info struct { + + // Set for shortcode templates with any {{ .Inner }} + IsInner bool + + // Config extracted from template. + Config Config +} + +type Config struct { + Version int +} + +var DefaultConfig = Config{ + Version: TemplateVersion, +} diff --git a/tpl/tplimpl/ace.go b/tpl/tplimpl/ace.go index 6fb4ca439..7a1f849f4 100644 --- a/tpl/tplimpl/ace.go +++ b/tpl/tplimpl/ace.go @@ -1,4 +1,4 @@ -// Copyright 2017-present The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -14,7 +14,6 @@ package tplimpl import ( - "html/template" "path/filepath" "strings" @@ -52,15 +51,15 @@ func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseC return err } - if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil { + isShort := isShortcode(name) + + info, err := applyTemplateTransformersToHMLTTemplate(isShort, templ) + if err != nil { return err } - if strings.Contains(name, "shortcodes") { - // We need to keep track of one ot the output format's shortcode template - // without knowing the rendering context. - clone := template.Must(templ.Clone()) - t.html.t.AddParseTree(withoutExt, clone.Tree) + if isShort { + t.addShortcodeVariant(name, info, templ) } return nil diff --git a/tpl/tplimpl/embedded/generate/generate.go b/tpl/tplimpl/embedded/generate/generate.go index 76a167a99..a48e00756 100644 --- a/tpl/tplimpl/embedded/generate/generate.go +++ b/tpl/tplimpl/embedded/generate/generate.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -63,7 +63,7 @@ func main() { log.Fatal(err) } - fmt.Fprint(file, `// Copyright 2018 The Hugo Authors. All rights reserved. + fmt.Fprint(file, `// Copyright 2019 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. diff --git a/tpl/tplimpl/embedded/templates.autogen.go b/tpl/tplimpl/embedded/templates.autogen.go index ed9ba35ac..d55e5b307 100644 --- a/tpl/tplimpl/embedded/templates.autogen.go +++ b/tpl/tplimpl/embedded/templates.autogen.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 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. @@ -19,7 +19,13 @@ package embedded // EmbeddedTemplates represents all embedded templates. var EmbeddedTemplates = [][2]string{ {`_default/robots.txt`, `User-agent: *`}, - {`_default/rss.xml`, ` + {`_default/rss.xml`, `{{- $pages := .Data.Pages -}} +{{- $limit := .Site.Config.Services.RSS.Limit -}} +{{- if ge $limit 1 -}} +{{- $pages = $pages | first $limit -}} +{{- end -}} +{{- printf "" | safeHTML }} + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} @@ -33,7 +39,7 @@ var EmbeddedTemplates = [][2]string{ {{ with .OutputFormats.Get "RSS" }} {{ printf "" .Permalink .MediaType | safeHTML }} {{ end }} - {{ range .Data.Pages }} + {{ range $pages }} {{ .Title }} {{ .Permalink }} @@ -45,7 +51,8 @@ var EmbeddedTemplates = [][2]string{ {{ end }} `}, - {`_default/sitemap.xml`, `" | safeHTML }} + {{ range .Data.Pages }} @@ -55,18 +62,19 @@ var EmbeddedTemplates = [][2]string{ {{ .Sitemap.Priority }}{{ end }}{{ if .IsTranslated }}{{ range .Translations }} {{ end }} {{ end }} {{ end }} `}, - {`_default/sitemapindex.xml`, ` + {`_default/sitemapindex.xml`, `{{ printf "" | safeHTML }} + {{ range . }} {{ .SitemapAbsURL }} @@ -77,7 +85,7 @@ var EmbeddedTemplates = [][2]string{ {{ end }} `}, - {`disqus.html`, `{{- $pc := .Page.Site.Config.Privacy.Disqus -}} + {`disqus.html`, `{{- $pc := .Site.Config.Privacy.Disqus -}} {{- if not $pc.Disable -}} {{ if .Site.DisqusShortname }}