summaryrefslogtreecommitdiffstats
path: root/tpl
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-01-02 12:33:26 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-03-23 18:51:22 +0100
commit597e418cb02883418f2cebb41400e8e61413f651 (patch)
tree177ad9c540b2583b6dab138c9f0490d28989c7f7 /tpl
parent44f5c1c14cb1f42cc5f01739c289e9cfc83602af (diff)
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
Diffstat (limited to 'tpl')
-rw-r--r--tpl/collections/apply_test.go6
-rw-r--r--tpl/collections/collections.go8
-rw-r--r--tpl/collections/collections_test.go38
-rw-r--r--tpl/template.go43
-rw-r--r--tpl/template_info.go35
-rw-r--r--tpl/tplimpl/ace.go15
-rw-r--r--tpl/tplimpl/embedded/generate/generate.go4
-rw-r--r--tpl/tplimpl/embedded/templates.autogen.go24
-rw-r--r--tpl/tplimpl/embedded/templates/_default/rss.xml8
-rw-r--r--tpl/tplimpl/embedded/templates/_default/sitemap.xml5
-rw-r--r--tpl/tplimpl/embedded/templates/_default/sitemapindex.xml1
-rw-r--r--tpl/tplimpl/embedded/templates/disqus.html2
-rw-r--r--tpl/tplimpl/shortcodes.go148
-rw-r--r--tpl/tplimpl/shortcodes_test.go94
-rw-r--r--tpl/tplimpl/template.go201
-rw-r--r--tpl/tplimpl/templateFuncster.go46
-rw-r--r--tpl/tplimpl/template_ast_transformers.go132
-rw-r--r--tpl/tplimpl/template_ast_transformers_test.go66
-rw-r--r--tpl/tplimpl/template_funcs_test.go20
-rw-r--r--tpl/tplimpl/template_info_test.go (renamed from tpl/tplimpl/template_test.go)48
20 files changed, 739 insertions, 205 deletions
diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go
index 0878844b2..edec3da18 100644
--- a/tpl/collections/apply_test.go
+++ b/tpl/collections/apply_test.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -29,6 +29,10 @@ func (templateFinder) Lookup(name string) (tpl.Template, bool) {
return nil, false
}
+func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
+ return nil, false, false
+}
+
func (templateFinder) GetFuncs() map[string]interface{} {
return map[string]interface{}{
"print": fmt.Sprint,
diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index bad65369f..92a61e575 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -329,13 +329,17 @@ func (ns *Namespace) Group(key interface{}, items interface{}) (interface{}, err
return nil, errors.New("nil is not a valid key to group by")
}
+ if g, ok := items.(collections.Grouper); ok {
+ return g.Group(key, items)
+ }
+
in := newSliceElement(items)
if g, ok := in.(collections.Grouper); ok {
return g.Group(key, items)
}
- return nil, fmt.Errorf("grouping not supported for type %T", items)
+ return nil, fmt.Errorf("grouping not supported for type %T %T", items, in)
}
// IsSet returns whether a given array, channel, slice, or map has a key
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
index 0edb8299f..103aee59e 100644
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -311,16 +311,16 @@ func TestIn(t *testing.T) {
}
}
-type page struct {
+type testPage struct {
Title string
}
-func (p page) String() string {
+func (p testPage) String() string {
return "p-" + p.Title
}
-type pagesPtr []*page
-type pagesVals []page
+type pagesPtr []*testPage
+type pagesVals []testPage
func TestIntersect(t *testing.T) {
t.Parallel()
@@ -328,15 +328,15 @@ func TestIntersect(t *testing.T) {
ns := New(&deps.Deps{})
var (
- p1 = &page{"A"}
- p2 = &page{"B"}
- p3 = &page{"C"}
- p4 = &page{"D"}
-
- p1v = page{"A"}
- p2v = page{"B"}
- p3v = page{"C"}
- p4v = page{"D"}
+ p1 = &testPage{"A"}
+ p2 = &testPage{"B"}
+ p3 = &testPage{"C"}
+ p4 = &testPage{"D"}
+
+ p1v = testPage{"A"}
+ p2v = testPage{"B"}
+ p3v = testPage{"C"}
+ p4v = testPage{"D"}
)
for i, test := range []struct {
@@ -672,14 +672,14 @@ func TestUnion(t *testing.T) {
ns := New(&deps.Deps{})
var (
- p1 = &page{"A"}
- p2 = &page{"B"}
+ p1 = &testPage{"A"}
+ p2 = &testPage{"B"}
// p3 = &page{"C"}
- p4 = &page{"D"}
+ p4 = &testPage{"D"}
- p1v = page{"A"}
+ p1v = testPage{"A"}
//p2v = page{"B"}
- p3v = page{"C"}
+ p3v = testPage{"C"}
//p4v = page{"D"}
)
diff --git a/tpl/template.go b/tpl/template.go
index 3225814c0..07152166a 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@ import (
"strings"
"time"
+ "github.com/gohugoio/hugo/output"
+
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
@@ -37,7 +39,8 @@ import (
)
var (
- _ TemplateExecutor = (*TemplateAdapter)(nil)
+ _ TemplateExecutor = (*TemplateAdapter)(nil)
+ _ TemplateInfoProvider = (*TemplateAdapter)(nil)
)
// TemplateHandler manages the collection of templates.
@@ -53,17 +56,47 @@ type TemplateHandler interface {
RebuildClone()
}
+// TemplateVariants describes the possible variants of a template.
+// All of these may be empty.
+type TemplateVariants struct {
+ Language string
+ OutputFormat output.Format
+}
+
// TemplateFinder finds templates.
type TemplateFinder interface {
+ TemplateLookup
+ TemplateLookupVariant
+}
+
+type TemplateLookup interface {
Lookup(name string) (Template, bool)
}
+type TemplateLookupVariant interface {
+ // TODO(bep) this currently only works for shortcodes.
+ // We may unify and expand this variant pattern to the
+ // other templates, but we need this now for the shortcodes to
+ // quickly determine if a shortcode has a template for a given
+ // output format.
+ // It returns the template, if it was found or not and if there are
+ // alternative representations (output format, language).
+ // We are currently only interested in output formats, so we should improve
+ // this for speed.
+ LookupVariant(name string, variants TemplateVariants) (Template, bool, bool)
+}
+
// Template is the common interface between text/template and html/template.
type Template interface {
Execute(wr io.Writer, data interface{}) error
Name() string
}
+// TemplateInfoProvider provides some contextual information about a template.
+type TemplateInfoProvider interface {
+ TemplateInfo() Info
+}
+
// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
type TemplateParser interface {
Parse(name, tpl string) (Template, error)
@@ -92,6 +125,8 @@ type TemplateAdapter struct {
Template
Metrics metrics.Provider
+ Info Info
+
// The filesystem where the templates are stored.
Fs afero.Fs
@@ -133,6 +168,10 @@ func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error)
return
}
+func (t *TemplateAdapter) TemplateInfo() Info {
+ return t.Info
+}
+
// The identifiers may be truncated in the log, e.g.
// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
var identifiersRe = regexp.MustCompile("at \\<(.*?)(\\.{3})?\\>:")
diff --git a/tpl/template_info.go b/tpl/template_info.go
new file mode 100644
index 000000000..8568f46f0
--- /dev/null
+++ b/tpl/template_info.go
@@ -0,0 +1,35 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tpl
+
+// Increments on breaking changes.
+const TemplateVersion = 2
+
+// Info holds some info extracted from a parsed template.
+type Info struct {
+
+ // Set for shortcode templates with any {{ .Inner }}
+ IsInner bool
+
+ // Config extracted from template.
+ Config Config
+}
+
+type Config struct {
+ Version int
+}
+
+var DefaultConfig = Config{
+ Version: TemplateVersion,
+}
diff --git a/tpl/tplimpl/ace.go b/tpl/tplimpl/ace.go
index 6fb4ca439..7a1f849f4 100644
--- a/tpl/tplimpl/ace.go
+++ b/tpl/tplimpl/ace.go
@@ -1,4 +1,4 @@
-// Copyright 2017-present The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@
package tplimpl
import (
- "html/template"
"path/filepath"
"strings"
@@ -52,15 +51,15 @@ func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseC
return err
}
- if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil {
+ isShort := isShortcode(name)
+
+ info, err := applyTemplateTransformersToHMLTTemplate(isShort, templ)
+ if err != nil {
return err
}
- if strings.Contains(name, "shortcodes") {
- // We need to keep track of one ot the output format's shortcode template
- // without knowing the rendering context.
- clone := template.Must(templ.Clone())
- t.html.t.AddParseTree(withoutExt, clone.Tree)
+ if isShort {
+ t.addShortcodeVariant(name, info, templ)
}
return nil
diff --git a/tpl/tplimpl/embedded/generate/generate.go b/tpl/tplimpl/embedded/generate/generate.go
index 76a167a99..a48e00756 100644
--- a/tpl/tplimpl/embedded/generate/generate.go
+++ b/tpl/tplimpl/embedded/generate/generate.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ func main() {
log.Fatal(err)
}
- fmt.Fprint(file, `// Copyright 2018 The Hugo Authors. All rights reserved.
+ fmt.Fprint(file, `// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
diff --git a/tpl/tplimpl/embedded/templates.autogen.go b/tpl/tplimpl/embedded/templates.autogen.go
index ed9ba35ac..d55e5b307 100644
--- a/tpl/tplimpl/embedded/templates.autogen.go
+++ b/tpl/tplimpl/embedded/templates.autogen.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Hugo Authors. All rights reserved.
+// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -19,7 +19,13 @@ package embedded
// EmbeddedTemplates represents all embedded templates.
var EmbeddedTemplates = [][2]string{
{`_default/robots.txt`, `User-agent: *`},
- {`_default/rss.xml`, `<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">
diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/tpl/tplimpl/embedded/templates/_default/rss.xml
index abba0b28a..675ecd43c 100644
--- a/tpl/tplimpl/embedded/templates/_default/rss.xml
+++ b/tpl/tplimpl/embedded/templates/_default/rss.xml
@@ -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>
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemap.xml b/tpl/tplimpl/embedded/templates/_default/sitemap.xml
index e0a2b189d..f5b44c410 100644
--- a/tpl/tplimpl/embedded/templates/_default/sitemap.xml
+++ b/tpl/tplimpl/embedded/templates/_default/sitemap.xml
@@ -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>
diff --git a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml b/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
index 4cd289fe9..60724c7b8 100644
--- a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
+++ b/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml
@@ -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>
diff --git a/tpl/tplimpl/embedded/templates/disqus.html b/tpl/tplimpl/embedded/templates/disqus.html
index 178d84caf..ab51bb5c0 100644
--- a/tpl/tplimpl/embedded/templates/disqus.html
+++ b/tpl/tplimpl/embedded/templates/disqus.html
@@ -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">
diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go
new file mode 100644
index 000000000..8577fbeed
--- /dev/null
+++ b/tpl/tplimpl/shortcodes.go
@@ -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/")
+}
diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go
new file mode 100644
index 000000000..6909feda7
--- /dev/null
+++ b/tpl/tplimpl/shortcodes_test.go
@@ -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},