mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-30 22:39:58 +02:00
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:
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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"}
|
||||
)
|
||||
|
||||
|
@@ -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
35
tpl/template_info.go
Normal 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,
|
||||
}
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
24
tpl/tplimpl/embedded/templates.autogen.go
generated
24
tpl/tplimpl/embedded/templates.autogen.go
generated
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
148
tpl/tplimpl/shortcodes.go
Normal 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/")
|
||||
}
|
94
tpl/tplimpl/shortcodes_test.go
Normal file
94
tpl/tplimpl/shortcodes_test.go
Normal 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)
|
||||
|
||||
})
|
||||
}
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
}
|
Reference in New Issue
Block a user