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
This commit is contained in:
Bjørn Erik Pedersen
2019-01-02 12:33:26 +01:00
parent 44f5c1c14c
commit 597e418cb0
206 changed files with 14442 additions and 9679 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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"}
p1 = &testPage{"A"}
p2 = &testPage{"B"}
p3 = &testPage{"C"}
p4 = &testPage{"D"}
p1v = page{"A"}
p2v = page{"B"}
p3v = page{"C"}
p4v = page{"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"}
)

View File

@@ -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})?\\>:")

35
tpl/template_info.go Normal file
View File

@@ -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,
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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`, `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
{`_default/rss.xml`, `{{- $pages := .Data.Pages -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ .Permalink }}</link>
@@ -33,7 +39,7 @@ var EmbeddedTemplates = [][2]string{
{{ with .OutputFormats.Get "RSS" }}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{ end }}
{{ range .Data.Pages }}
{{ range $pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
@@ -45,7 +51,8 @@ var EmbeddedTemplates = [][2]string{
{{ end }}
</channel>
</rss>`},
{`_default/sitemap.xml`, `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
{`_default/sitemap.xml`, `{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
{{ range .Data.Pages }}
<url>
@@ -55,18 +62,19 @@ var EmbeddedTemplates = [][2]string{
<priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
<xhtml:link
rel="alternate"
hreflang="{{ .Lang }}"
hreflang="{{ .Language.Lang }}"
href="{{ .Permalink }}"
/>{{ end }}
<xhtml:link
rel="alternate"
hreflang="{{ .Lang }}"
hreflang="{{ .Language.Lang }}"
href="{{ .Permalink }}"
/>{{ end }}
</url>
{{ end }}
</urlset>`},
{`_default/sitemapindex.xml`, `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{`_default/sitemapindex.xml`, `{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{ range . }}
<sitemap>
<loc>{{ .SitemapAbsURL }}</loc>
@@ -77,7 +85,7 @@ var EmbeddedTemplates = [][2]string{
{{ end }}
</sitemapindex>
`},
{`disqus.html`, `{{- $pc := .Page.Site.Config.Privacy.Disqus -}}
{`disqus.html`, `{{- $pc := .Site.Config.Privacy.Disqus -}}
{{- if not $pc.Disable -}}
{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
<script type="application/javascript">

View File

@@ -1,3 +1,9 @@
{{- $pages := .Data.Pages -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
@@ -12,7 +18,7 @@
{{ with .OutputFormats.Get "RSS" }}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{ end }}
{{ range .Data.Pages }}
{{ range $pages }}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>

View File

@@ -1,3 +1,4 @@
{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
{{ range .Data.Pages }}
@@ -8,12 +9,12 @@
<priority>{{ .Sitemap.Priority }}</priority>{{ end }}{{ if .IsTranslated }}{{ range .Translations }}
<xhtml:link
rel="alternate"
hreflang="{{ .Lang }}"
hreflang="{{ .Language.Lang }}"
href="{{ .Permalink }}"
/>{{ end }}
<xhtml:link
rel="alternate"
hreflang="{{ .Lang }}"
hreflang="{{ .Language.Lang }}"
href="{{ .Permalink }}"
/>{{ end }}
</url>

View File

@@ -1,3 +1,4 @@
{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{ range . }}
<sitemap>

View File

@@ -1,4 +1,4 @@
{{- $pc := .Page.Site.Config.Privacy.Disqus -}}
{{- $pc := .Site.Config.Privacy.Disqus -}}
{{- if not $pc.Disable -}}
{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
<script type="application/javascript">

148
tpl/tplimpl/shortcodes.go Normal file
View File

@@ -0,0 +1,148 @@
// 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 tplimpl
import (
"strings"
"github.com/gohugoio/hugo/tpl"
)
// Currently lang, outFormat, suffix
const numTemplateVariants = 3
type shortcodeVariant struct {
// The possible variants: lang, outFormat, suffix
// gtag
// gtag.html
// gtag.no.html
// gtag.no.amp.html
// A slice of length numTemplateVariants.
variants []string
info tpl.Info
templ tpl.Template
}
type shortcodeTemplates struct {
variants []shortcodeVariant
}
func (s *shortcodeTemplates) indexOf(variants []string) int {
L:
for i, v1 := range s.variants {
for i, v2 := range v1.variants {
if v2 != variants[i] {
continue L
}
}
return i
}
return -1
}
func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) {
return s.fromVariantsSlice([]string{
variants.Language,
strings.ToLower(variants.OutputFormat.Name),
variants.OutputFormat.MediaType.Suffix(),
})
}
// Get the most specific template given a full name, e.g gtag.no.amp.html.
func (s *shortcodeTemplates) fromName(name string) (shortcodeVariant, bool) {
return s.fromVariantsSlice(templateVariants(name))
}
func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) {
var (
bestMatch shortcodeVariant
bestMatchWeight int
)
for _, variant := range s.variants {
w := s.compareVariants(variants, variant.variants)
if bestMatchWeight == 0 || w > bestMatchWeight {
bestMatch = variant
bestMatchWeight = w
}
}
return bestMatch, true
}
// calculate a weight for two string slices of same lenght.
// higher value means "better match".
func (s *shortcodeTemplates) compareVariants(a, b []string) int {
weight := 0
for i, av := range a {
bv := b[i]
if av == bv {
weight++
} else {
weight--
}
}
return weight
}
func templateVariants(name string) []string {
_, variants := templateNameAndVariants(name)
return variants
}
func templateNameAndVariants(name string) (string, []string) {
variants := make([]string, numTemplateVariants)
parts := strings.Split(name, ".")
if len(parts) <= 1 {
// No variants.
return name, variants
}
name = parts[0]
parts = parts[1:]
lp := len(parts)
start := len(variants) - lp
for i, j := start, 0; i < len(variants); i, j = i+1, j+1 {
variants[i] = parts[j]
}
if lp > 1 && lp < len(variants) {
for i := lp - 1; i > 0; i-- {
variants[i-1] = variants[i]
}
}
if lp == 1 {
// Suffix only. Duplicate it into the output format field to
// make HTML win over AMP.
variants[len(variants)-2] = variants[len(variants)-1]
}
return name, variants
}
func isShortcode(name string) bool {
return strings.Contains(name, "shortcodes/")
}
func isInternal(name string) bool {
return strings.HasPrefix(name, "_internal/")
}

View File

@@ -0,0 +1,94 @@
// 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 tplimpl
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestShortcodesTemplate(t *testing.T) {
t.Run("isShortcode", func(t *testing.T) {
assert := require.New(t)
assert.True(isShortcode("shortcodes/figures.html"))
assert.True(isShortcode("_internal/shortcodes/figures.html"))
assert.False(isShortcode("shortcodes\\figures.html"))
assert.False(isShortcode("myshortcodes"))
})
t.Run("variantsFromName", func(t *testing.T) {
assert := require.New(t)
assert.Equal([]string{"", "html", "html"}, templateVariants("figure.html"))
assert.Equal([]string{"no", "no", "html"}, templateVariants("figure.no.html"))
assert.Equal([]string{"no", "amp", "html"}, templateVariants("figure.no.amp.html"))
assert.Equal([]string{"amp", "amp", "html"}, templateVariants("figure.amp.html"))
name, variants := templateNameAndVariants("figure.html")
assert.Equal("figure", name)
assert.Equal([]string{"", "html", "html"}, variants)
})
t.Run("compareVariants", func(t *testing.T) {
assert := require.New(t)
var s *shortcodeTemplates
tests := []struct {
name string
name1 string
name2 string
expected int
}{
{"Same suffix", "figure.html", "figure.html", 3},
{"Same suffix and output format", "figure.html.html", "figure.html.html", 3},
{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3},
{"No suffix", "figure", "figure", 3},
{"Different output format", "figure.amp.html", "figure.html.html", -1},
{"One with output format, one without", "figure.amp.html", "figure.html", -1},
}
for i, test := range tests {
w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2))
assert.Equal(test.expected, w, fmt.Sprintf("[%d] %s", i, test.name))
}
})
t.Run("indexOf", func(t *testing.T) {
assert := require.New(t)
s := &shortcodeTemplates{
variants: []shortcodeVariant{
shortcodeVariant{variants: []string{"a", "b", "c"}},
shortcodeVariant{variants: []string{"a", "b", "d"}},
},
}
assert.Equal(0, s.indexOf([]string{"a", "b", "c"}))
assert.Equal(1, s.indexOf([]string{"a", "b", "d"}))
assert.Equal(-1, s.indexOf([]string{"a", "b", "x"}))
})
t.Run("Template", func(t *testing.T) {
assert := require.New(t)
assert.True(true)
})
}

View File

@@ -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.
@@ -86,6 +86,10 @@ type templateFuncsterSetter interface {
type templateHandler struct {
mu sync.Mutex
// shortcodes maps shortcode name to template variants
// (language, output format etc.) of that shortcode.
shortcodes map[string]*shortcodeTemplates
// text holds all the pure text templates.
text *textTemplates
html *htmlTemplates
@@ -103,6 +107,29 @@ type templateHandler struct {
*deps.Deps
}
func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
shortcodename, variants := templateNameAndVariants(path.Base(name))
templs, found := t.shortcodes[shortcodename]
if !found {
templs = &shortcodeTemplates{}
t.shortcodes[shortcodename] = templs
}
sv := shortcodeVariant{variants: variants, info: info, templ: templ}
i := templs.indexOf(variants)
if i != -1 {
// Only replace if it's an override of an internal template.
if !isInternal(name) {
templs.variants[i] = sv
}
} else {
templs.variants = append(templs.variants, sv)
}
}
// NewTextTemplate provides a text template parser that has all the Hugo
// template funcs etc. built-in.
func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder {
@@ -112,10 +139,24 @@ func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder {
tt := &textTemplate{t: texttemplate.New("")}
t.extTextTemplates = append(t.extTextTemplates, tt)
return tt
return struct {
tpl.TemplateParser
tpl.TemplateLookup
tpl.TemplateLookupVariant
}{
tt,
tt,
new(nopLookupVariant),
}
}
type nopLookupVariant int
func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
return nil, false, false
}
func (t *templateHandler) Debug() {
fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates())
fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates())
@@ -143,13 +184,85 @@ func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
}
// This currently only applies to shortcodes and what we get here is the
// shortcode name.
func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
name = path.Base(name)
s, found := t.shortcodes[name]
if !found {
return nil, false, false
}
sv, found := s.fromVariants(variants)
if !found {
return nil, false, false
}
more := len(s.variants) > 1
return &tpl.TemplateAdapter{
Template: sv.templ,
Info: sv.info,
Metrics: t.Deps.Metrics,
Fs: t.layoutsFs,
NameBaseTemplateName: t.html.nameBaseTemplateName}, true, more
}
func (t *textTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
return t.handler.LookupVariant(name, variants)
}
func (t *htmlTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
return t.handler.LookupVariant(name, variants)
}
func (t *templateHandler) cloneTemplate(in interface{}) tpl.Template {
switch templ := in.(type) {
case *texttemplate.Template:
return texttemplate.Must(templ.Clone())
case *template.Template:
return template.Must(templ.Clone())
}
panic(fmt.Sprintf("%T is not a template", in))
}
func (t *templateHandler) setFuncMapInTemplate(in interface{}, funcs map[string]interface{}) {
switch templ := in.(type) {
case *texttemplate.Template:
templ.Funcs(funcs)
return
case *template.Template:
templ.Funcs(funcs)
return
}
panic(fmt.Sprintf("%T is not a template", in))
}
func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
c := &templateHandler{
Deps: d,
layoutsFs: d.BaseFs.Layouts.Fs,
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon},
text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon},
errors: make([]*templateErr, 0),
Deps: d,
layoutsFs: d.BaseFs.Layouts.Fs,
shortcodes: make(map[string]*shortcodeTemplates),
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon},
text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon},
errors: make([]*templateErr, 0),
}
for k, v := range t.shortcodes {
other := *v
variantsc := make([]shortcodeVariant, len(v.variants))
for i, variant := range v.variants {
variantsc[i] = shortcodeVariant{
info: variant.info,
variants: variant.variants,
templ: t.cloneTemplate(variant.templ),
}
}
other.variants = variantsc
c.shortcodes[k] = &other
}
d.Tmpl = c
@@ -193,11 +306,12 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
templatesCommon: common,
}
h := &templateHandler{
Deps: deps,
layoutsFs: deps.BaseFs.Layouts.Fs,
html: htmlT,
text: textT,
errors: make([]*templateErr, 0),
Deps: deps,
layoutsFs: deps.BaseFs.Layouts.Fs,
shortcodes: make(map[string]*shortcodeTemplates),
html: htmlT,
text: textT,
errors: make([]*templateErr, 0),
}
common.handler = h
@@ -215,6 +329,8 @@ type templatesCommon struct {
nameBaseTemplateName map[string]string
}
type htmlTemplates struct {
mu sync.RWMutex
*templatesCommon
t *template.Template
@@ -245,6 +361,8 @@ func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) {
}
func (t *htmlTemplates) lookup(name string) *template.Template {
t.mu.RLock()
defer t.mu.RUnlock()
// Need to check in the overlay registry first as it will also be found below.
if t.overlays != nil {
@@ -337,21 +455,23 @@ func (t *templateHandler) LoadTemplates(prefix string) error {
}
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error {
t.mu.Lock()
defer t.mu.Unlock()
templ, err := tt.New(name).Parse(tpl)
if err != nil {
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.
withoutExt := strings.TrimSuffix(name, path.Ext(name))
clone := template.Must(templ.Clone())
tt.AddParseTree(withoutExt, clone.Tree)
if isShort {
t.handler.addShortcodeVariant(name, info, templ)
}
return nil
@@ -371,7 +491,7 @@ type textTemplate struct {
}
func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) {
return t.parSeIn(t.t, name, tpl)
return t.parseIn(t.t, name, tpl)
}
func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
@@ -382,7 +502,7 @@ func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
return tpl, tpl != nil
}
func (t *textTemplate) parSeIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) {
func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) {
t.mu.Lock()
defer t.mu.Unlock()
@@ -391,7 +511,7 @@ func (t *textTemplate) parSeIn(tt *texttemplate.Template, name, tpl string) (*te
return nil, err
}
if err := applyTemplateTransformersToTextTemplate(templ); err != nil {
if _, err := applyTemplateTransformersToTextTemplate(false, templ); err != nil {
return nil, err
}
return templ, nil
@@ -399,21 +519,20 @@ func (t *textTemplate) parSeIn(tt *texttemplate.Template, name, tpl string) (*te
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error {
name = strings.TrimPrefix(name, textTmplNamePrefix)
templ, err := t.parSeIn(tt, name, tpl)
templ, err := t.parseIn(tt, name, tpl)
if err != nil {
return err
}
if err := applyTemplateTransformersToTextTemplate(templ); err != nil {
isShort := isShortcode(name)
info, err := applyTemplateTransformersToTextTemplate(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.
withoutExt := strings.TrimSuffix(name, path.Ext(name))
clone := texttemplate.Must(templ.Clone())
tt.AddParseTree(withoutExt, clone.Tree)
if isShort {
t.handler.addShortcodeVariant(name, info, templ)
}
return nil
@@ -547,6 +666,12 @@ func (t *templateHandler) initFuncs() {
}
for _, v := range t.shortcodes {
for _, variant := range v.variants {
t.setFuncMapInTemplate(variant.templ, funcMap)
}
}
for _, extText := range t.extTextTemplates {
extText.t.Funcs(funcMap)
}
@@ -612,7 +737,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
// * https://github.com/golang/go/issues/16101
// * https://github.com/gohugoio/hugo/issues/2549
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if err := applyTemplateTransformersToHMLTTemplate(overlayTpl); err != nil {
if _, err := applyTemplateTransformersToHMLTTemplate(false, overlayTpl); err != nil {
return err
}
@@ -652,7 +777,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
}
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil {
if _, err := applyTemplateTransformersToTextTemplate(false, overlayTpl); err != nil {
return err
}
t.overlays[name] = overlayTpl
@@ -722,15 +847,15 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
return err
}
if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil {
isShort := isShortcode(name)
info, err := applyTemplateTransformersToHMLTTemplate(isShort, templ)
if err != nil {
return err
}
if strings.Contains(templateName, "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(templateName, info, templ)
}
return nil

View File

@@ -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,12 +14,8 @@
package tplimpl
import (
"fmt"
"html/template"
"strings"
texttemplate "text/template"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
)
@@ -35,43 +31,3 @@ func newTemplateFuncster(deps *deps.Deps) *templateFuncster {
Deps: deps,
}
}
// Partial executes the named partial and returns either a string,
// when called from text/template, for or a template.HTML.
func (t *templateFuncster) partial(name string, contextList ...interface{}) (interface{}, error) {
if strings.HasPrefix(name, "partials/") {
name = name[8:]
}
var context interface{}
if len(contextList) == 0 {
context = nil
} else {
context = contextList[0]
}
for _, n := range []string{"partials/" + name, "theme/partials/" + name} {
templ, found := t.Tmpl.Lookup(n)
if !found {
// For legacy reasons.
templ, found = t.Tmpl.Lookup(n + ".html")
}
if found {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := templ.Execute(b, context); err != nil {
return "", err
}
if _, ok := templ.(*texttemplate.Template); ok {
return b.String(), nil
}
return template.HTML(b.String()), nil
}
}
return "", fmt.Errorf("Partial %q not found", name)
}

View File

@@ -14,11 +14,16 @@
package tplimpl
import (
"errors"
"html/template"
"strings"
texttemplate "text/template"
"text/template/parse"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
// decl keeps track of the variable mappings, i.e. $mysite => .Site etc.
@@ -38,6 +43,18 @@ type templateContext struct {
decl decl
visited map[string]bool
lookupFn func(name string) *parse.Tree
// The last error encountered.
err error
// Only needed for shortcodes
isShortcode bool
// Set when we're done checking for config header.
configChecked bool
// Contains some info about the template
tpl.Info
}
func (c templateContext) getIfNotVisited(name string) *parse.Tree {
@@ -49,7 +66,11 @@ func (c templateContext) getIfNotVisited(name string) *parse.Tree {
}
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)}
return &templateContext{
Info: tpl.Info{Config: tpl.DefaultConfig},
lookupFn: lookupFn,
decl: make(map[string]string),
visited: make(map[string]bool)}
}
@@ -63,12 +84,12 @@ func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree
}
}
func applyTemplateTransformersToHMLTTemplate(templ *template.Template) error {
return applyTemplateTransformers(templ.Tree, createParseTreeLookup(templ))
func applyTemplateTransformersToHMLTTemplate(isShortcode bool, templ *template.Template) (tpl.Info, error) {
return applyTemplateTransformers(isShortcode, templ.Tree, createParseTreeLookup(templ))
}
func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error {
return applyTemplateTransformers(templ.Tree,
func applyTemplateTransformersToTextTemplate(isShortcode bool, templ *texttemplate.Template) (tpl.Info, error) {
return applyTemplateTransformers(isShortcode, templ.Tree,
func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
@@ -78,16 +99,17 @@ func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error
})
}
func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *parse.Tree) error {
func applyTemplateTransformers(isShortcode bool, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (tpl.Info, error) {
if templ == nil {
return errors.New("expected template, but none provided")
return tpl.Info{}, errors.New("expected template, but none provided")
}
c := newTemplateContext(lookupFn)
c.isShortcode = isShortcode
c.applyTransformations(templ.Root)
err := c.applyTransformations(templ.Root)
return nil
return c.Info, err
}
// The truth logic in Go's template package is broken for certain values
@@ -115,10 +137,11 @@ func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
}
// applyTransformations do two things:
// applyTransformations do 3 things:
// 1) Make all .Params.CamelCase and similar into lowercase.
// 2) Wraps every with and if pipe in getif
func (c *templateContext) applyTransformations(n parse.Node) {
// 3) Collects some information about the template content.
func (c *templateContext) applyTransformations(n parse.Node) error {
switch x := n.(type) {
case *parse.ListNode:
if x != nil {
@@ -140,6 +163,7 @@ func (c *templateContext) applyTransformations(n parse.Node) {
c.applyTransformationsToNodes(subTempl.Root)
}
case *parse.PipeNode:
c.collectConfig(x)
if len(x.Decl) == 1 && len(x.Cmds) == 1 {
// maps $site => .Site etc.
c.decl[x.Decl[0].Ident[0]] = x.Cmds[0].String()
@@ -150,6 +174,8 @@ func (c *templateContext) applyTransformations(n parse.Node) {
}
case *parse.CommandNode:
c.collectInner(x)
for _, elem := range x.Args {
switch an := elem.(type) {
case *parse.FieldNode:
@@ -166,6 +192,8 @@ func (c *templateContext) applyTransformations(n parse.Node) {
}
}
}
return c.err
}
func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) {
@@ -187,6 +215,86 @@ func (c *templateContext) updateIdentsIfNeeded(idents []string) {
}
func (c *templateContext) hasIdent(idents []string, ident string) bool {
for _, id := range idents {
if id == ident {
return true
}
}
return false
}
// collectConfig collects and parses any leading template config variable declaration.
// This will be the first PipeNode in the template, and will be a variable declaration
// on the form:
// {{ $_hugo_config:= `{ "version": 1 }` }}
func (c *templateContext) collectConfig(n *parse.PipeNode) {
if !c.isShortcode {
return
}
if c.configChecked {
return
}
c.configChecked = true
if len(n.Decl) != 1 || len(n.Cmds) != 1 {
// This cannot be a config declaration
return
}
v := n.Decl[0]
if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" {
return
}
cmd := n.Cmds[0]
if len(cmd.Args) == 0 {
return
}
if s, ok := cmd.Args[0].(*parse.StringNode); ok {
errMsg := "failed to decode $_hugo_config in template"
m, err := cast.ToStringMapE(s.Text)
if err != nil {
c.err = errors.Wrap(err, errMsg)
return
}
if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil {
c.err = errors.Wrap(err, errMsg)
}
}
}
// collectInner determines if the given CommandNode represents a
// shortcode call to its .Inner.
func (c *templateContext) collectInner(n *parse.CommandNode) {
if !c.isShortcode {
return
}
if c.Info.IsInner || len(n.Args) == 0 {
return
}
for _, arg := range n.Args {
var idents []string
switch nt := arg.(type) {
case *parse.FieldNode:
idents = nt.Ident
case *parse.VariableNode:
idents = nt.Ident
}
if c.hasIdent(idents, "Inner") {
c.Info.IsInner = true
break
}
}
}
// indexOfReplacementStart will return the index of where to start doing replacement,
// -1 if none needed.
func (d decl) indexOfReplacementStart(idents []string) int {

View File

@@ -1,4 +1,4 @@
// Copyright 2016 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,14 +21,15 @@ import (
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cast"
"github.com/stretchr/testify/require"
)
type handler interface {
addTemplate(name, tpl string) error
}
var (
testFuncs = map[string]interface{}{
"getif": func(v interface{}) interface{} { return v },
@@ -179,7 +180,8 @@ PARAMS SITE GLOBAL3: {{ $site.Params.LOWER }}
func TestParamsKeysToLower(t *testing.T) {
t.Parallel()
require.Error(t, applyTemplateTransformers(nil, nil))
_, err := applyTemplateTransformers(false, nil, nil)
require.Error(t, err)
templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl)
@@ -429,17 +431,7 @@ func TestInsertIsZeroFunc(t *testing.T) {
`
)
v := newTestConfig()
fs := hugofs.NewMem(v)
depsCfg := newDepsConfig(v)
depsCfg.Fs = fs
d, err := deps.New(depsCfg)
assert.NoError(err)
provider := DefaultTemplateProvider
provider.Update(d)
d := newD(assert)
h := d.Tmpl.(handler)
assert.NoError(h.addTemplate("mytemplate.html", templ))
@@ -458,3 +450,45 @@ func TestInsertIsZeroFunc(t *testing.T) {
assert.Contains(result, ".NonEmptyInterfaceTypedNil: FALSE")
}
func TestCollectInfo(t *testing.T) {
configStr := `{ "version": 42 }`
tests := []struct {
name string
tplString string
expected tpl.Info
}{
{"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}},
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{
Config: tpl.Config{
Version: 42,
},
}},
}
echo := func(in interface{}) interface{} {
return in
}
funcs := template.FuncMap{
"highlight": echo,
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert := require.New(t)
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
require.NoError(t, err)
c := newTemplateContext(createParseTreeLookup(templ))
c.isShortcode = true
c.applyTransformations(templ.Tree.Root)
assert.Equal(test.expected, c.Info)
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2016 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.
@@ -220,21 +220,3 @@ func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) {
}
})
}
func newTestFuncster() *templateFuncster {
return newTestFuncsterWithViper(viper.New())
}
func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster {
config := newDepsConfig(v)
d, err := deps.New(config)
if err != nil {
panic(err)
}
if err := d.LoadResources(); err != nil {
panic(err)
}
return d.Tmpl.(*templateHandler).html.funcster
}

View File

@@ -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.
@@ -10,7 +10,6 @@
// 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 tplimpl
import (
@@ -22,45 +21,36 @@ import (
"github.com/stretchr/testify/require"
)
type handler interface {
addTemplate(name, tpl string) error
func TestTemplateInfoShortcode(t *testing.T) {
assert := require.New(t)
d := newD(assert)
h := d.Tmpl.(handler)
assert.NoError(h.addTemplate("shortcodes/mytemplate.html", `
{{ .Inner }}
`))
tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
assert.True(found)
tti, ok := tt.(tpl.TemplateInfoProvider)
assert.True(ok)
assert.True(tti.TemplateInfo().IsInner)
}
// #3876
func TestHTMLEscape(t *testing.T) {
assert := require.New(t)
data := map[string]string{
"html": "<h1>Hi!</h1>",
"other": "<h1>Hi!</h1>",
}
// TODO(bep) move and use in other places
func newD(assert *require.Assertions) *deps.Deps {
v := newTestConfig()
fs := hugofs.NewMem(v)
//afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755)
depsCfg := newDepsConfig(v)
depsCfg.Fs = fs
d, err := deps.New(depsCfg)
assert.NoError(err)
templ := `{{ "<h1>Hi!</h1>" | safeHTML }}`
provider := DefaultTemplateProvider
provider.Update(d)
h := d.Tmpl.(handler)
assert.NoError(h.addTemplate("shortcodes/myShort.html", templ))
tt, _ := d.Tmpl.Lookup("shortcodes/myShort.html")
s, err := tt.(tpl.TemplateExecutor).ExecuteToString(data)
assert.NoError(err)
assert.Contains(s, "<h1>Hi!</h1>")
tt, _ = d.Tmpl.Lookup("shortcodes/myShort")
s, err = tt.(tpl.TemplateExecutor).ExecuteToString(data)
assert.NoError(err)
assert.Contains(s, "<h1>Hi!</h1>")
return d
}