mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
tpl: Rework to handle both text and HTML templates
Before this commit, Hugo used `html/template` for all Go templates. While this is a fine choice for HTML and maybe also RSS feeds, it is painful for plain text formats such as CSV, JSON etc. This commit fixes that by using the `IsPlainText` attribute on the output format to decide what to use. A couple of notes: * The above requires a nonambiguous template name to type mapping. I.e. `/layouts/_default/list.json` will only work if there is only one JSON output format, `/layouts/_default/list.mytype.json` will always work. * Ambiguous types will fall back to HTML. * Partials inherits the text vs HTML identificator of the container template. This also means that plain text templates can only include plain text partials. * Shortcode templates are, by definition, currently HTML templates only. Fixes #3221
This commit is contained in:
parent
73c1c7b69d
commit
5c5efa03d2
31 changed files with 1208 additions and 840 deletions
6
deps/deps.go
vendored
6
deps/deps.go
vendored
|
@ -20,7 +20,7 @@ type Deps struct {
|
|||
Log *jww.Notepad `json:"-"`
|
||||
|
||||
// The templates to use.
|
||||
Tmpl tpl.Template `json:"-"`
|
||||
Tmpl tpl.TemplateHandler `json:"-"`
|
||||
|
||||
// The file systems to use.
|
||||
Fs *hugofs.Fs `json:"-"`
|
||||
|
@ -40,7 +40,7 @@ type Deps struct {
|
|||
Language *helpers.Language
|
||||
|
||||
templateProvider ResourceProvider
|
||||
WithTemplate func(templ tpl.Template) error `json:"-"`
|
||||
WithTemplate func(templ tpl.TemplateHandler) error `json:"-"`
|
||||
|
||||
translationProvider ResourceProvider
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ type DepsCfg struct {
|
|||
|
||||
// Template handling.
|
||||
TemplateProvider ResourceProvider
|
||||
WithTemplate func(templ tpl.Template) error
|
||||
WithTemplate func(templ tpl.TemplateHandler) error
|
||||
|
||||
// i18n handling.
|
||||
TranslationProvider ResourceProvider
|
||||
|
|
|
@ -22,6 +22,8 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/hugo/tpl"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"github.com/spf13/hugo/helpers"
|
||||
|
@ -35,18 +37,19 @@ const (
|
|||
var defaultAliasTemplates *template.Template
|
||||
|
||||
func init() {
|
||||
//TODO(bep) consolidate
|
||||
defaultAliasTemplates = template.New("")
|
||||
template.Must(defaultAliasTemplates.New("alias").Parse(alias))
|
||||
template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml))
|
||||
}
|
||||
|
||||
type aliasHandler struct {
|
||||
Templates *template.Template
|
||||
t tpl.TemplateHandler
|
||||
log *jww.Notepad
|
||||
allowRoot bool
|
||||
}
|
||||
|
||||
func newAliasHandler(t *template.Template, l *jww.Notepad, allowRoot bool) aliasHandler {
|
||||
func newAliasHandler(t tpl.TemplateHandler, l *jww.Notepad, allowRoot bool) aliasHandler {
|
||||
return aliasHandler{t, l, allowRoot}
|
||||
}
|
||||
|
||||
|
@ -56,12 +59,19 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i
|
|||
t = "alias-xhtml"
|
||||
}
|
||||
|
||||
template := defaultAliasTemplates
|
||||
if a.Templates != nil {
|
||||
template = a.Templates
|
||||
t = "alias.html"
|
||||
var templ *tpl.TemplateAdapter
|
||||
|
||||
if a.t != nil {
|
||||
templ = a.t.Lookup("alias.html")
|
||||
}
|
||||
|
||||
if templ == nil {
|
||||
def := defaultAliasTemplates.Lookup(t)
|
||||
if def != nil {
|
||||
templ = &tpl.TemplateAdapter{def}
|
||||
}
|
||||
|
||||
}
|
||||
data := struct {
|
||||
Permalink string
|
||||
Page *Page
|
||||
|
@ -71,7 +81,7 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i
|
|||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
err := template.ExecuteTemplate(buffer, t, data)
|
||||
err := templ.Execute(buffer, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -83,8 +93,7 @@ func (s *Site) writeDestAlias(path, permalink string, p *Page) (err error) {
|
|||
}
|
||||
|
||||
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, p *Page) (err error) {
|
||||
|
||||
handler := newAliasHandler(s.Tmpl.Lookup("alias.html"), s.Log, allowRoot)
|
||||
handler := newAliasHandler(s.Tmpl, s.Log, allowRoot)
|
||||
|
||||
isXHTML := strings.HasSuffix(path, ".xhtml")
|
||||
|
||||
|
|
|
@ -335,8 +335,8 @@ func TestShortcodeTweet(t *testing.T) {
|
|||
th = testHelper{cfg, fs, t}
|
||||
)
|
||||
|
||||
withTemplate := func(templ tpl.Template) error {
|
||||
templ.Funcs(tweetFuncMap)
|
||||
withTemplate := func(templ tpl.TemplateHandler) error {
|
||||
templ.(tpl.TemplateTestMocker).SetFuncs(tweetFuncMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -390,8 +390,8 @@ func TestShortcodeInstagram(t *testing.T) {
|
|||
th = testHelper{cfg, fs, t}
|
||||
)
|
||||
|
||||
withTemplate := func(templ tpl.Template) error {
|
||||
templ.Funcs(instagramFuncMap)
|
||||
withTemplate := func(templ tpl.TemplateHandler) error {
|
||||
templ.(tpl.TemplateTestMocker).SetFuncs(instagramFuncMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -127,11 +127,11 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
|
|||
return newHugoSites(cfg, sites...)
|
||||
}
|
||||
|
||||
func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.Template) error) func(templ tpl.Template) error {
|
||||
return func(templ tpl.Template) error {
|
||||
templ.LoadTemplates(s.PathSpec.GetLayoutDirPath())
|
||||
func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler) error) func(templ tpl.TemplateHandler) error {
|
||||
return func(templ tpl.TemplateHandler) error {
|
||||
templ.LoadTemplates(s.PathSpec.GetLayoutDirPath(), "")
|
||||
if s.PathSpec.ThemeSet() {
|
||||
templ.LoadTemplatesWithPrefix(s.PathSpec.GetThemeDir()+"/layouts", "theme")
|
||||
templ.LoadTemplates(s.PathSpec.GetThemeDir()+"/layouts", "theme")
|
||||
}
|
||||
|
||||
for _, wt := range withTemplates {
|
||||
|
|
|
@ -1384,13 +1384,14 @@ func (p *Page) prepareLayouts() error {
|
|||
if p.Kind == KindPage {
|
||||
if !p.IsRenderable() {
|
||||
self := "__" + p.UniqueID()
|
||||
_, err := p.s.Tmpl.GetClone().New(self).Parse(string(p.Content))
|
||||
err := p.s.Tmpl.AddLateTemplate(self, string(p.Content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.selfLayout = self
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -110,9 +110,29 @@ func (p *PageOutput) Render(layout ...string) template.HTML {
|
|||
l, err := p.layouts(layout...)
|
||||
if err != nil {
|
||||
helpers.DistinctErrorLog.Printf("in .Render: Failed to resolve layout %q for page %q", layout, p.pathOrTitle())
|
||||
return template.HTML("")
|
||||
return ""
|
||||
}
|
||||
return p.s.Tmpl.ExecuteTemplateToHTML(p, l...)
|
||||
|
||||
for _, layout := range l {
|
||||
templ := p.s.Tmpl.Lookup(layout)
|
||||
if templ == nil {
|
||||
// This is legacy from when we had only one output format and
|
||||
// HTML templates only. Some have references to layouts without suffix.
|
||||
// We default to good old HTML.
|
||||
templ = p.s.Tmpl.Lookup(layout + ".html")
|
||||
}
|
||||
if templ != nil {
|
||||
res, err := templ.ExecuteToString(p)
|
||||
if err != nil {
|
||||
helpers.DistinctErrorLog.Printf("in .Render: Failed to execute template %q for page %q", layout, p.pathOrTitle())
|
||||
return template.HTML("")
|
||||
}
|
||||
return template.HTML(res)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
|
||||
}
|
||||
|
||||
func (p *Page) Render(layout ...string) template.HTML {
|
||||
|
|
|
@ -177,7 +177,7 @@ var isInnerShortcodeCache = struct {
|
|||
// to avoid potential costly look-aheads for closing tags we look inside the template itself
|
||||
// we could change the syntax to self-closing tags, but that would make users cry
|
||||
// the value found is cached
|
||||
func isInnerShortcode(t *template.Template) (bool, error) {
|
||||
func isInnerShortcode(t tpl.TemplateExecutor) (bool, error) {
|
||||
isInnerShortcodeCache.RLock()
|
||||
m, ok := isInnerShortcodeCache.m[t.Name()]
|
||||
isInnerShortcodeCache.RUnlock()
|
||||
|
@ -188,10 +188,7 @@ func isInnerShortcode(t *template.Template) (bool, error) {
|
|||
|
||||
isInnerShortcodeCache.Lock()
|
||||
defer isInnerShortcodeCache.Unlock()
|
||||
if t.Tree == nil {
|
||||
return false, errors.New("Template failed to compile")
|
||||
}
|
||||
match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree.Root.String())
|
||||
match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree())
|
||||
isInnerShortcodeCache.m[t.Name()] = match
|
||||
|
||||
return match, nil
|
||||
|
@ -398,8 +395,6 @@ Loop:
|
|||
case tScName:
|
||||
sc.name = currItem.val
|
||||
tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
|
||||
{
|
||||
}
|
||||
if tmpl == nil {
|
||||
return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
|
||||
}
|
||||
|
@ -570,7 +565,10 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin
|
|||
return source, nil
|
||||
}
|
||||
|
||||
func getShortcodeTemplate(name string, t tpl.Template) *template.Template {
|
||||
func getShortcodeTemplate(name string, t tpl.TemplateHandler) *tpl.TemplateAdapter {
|
||||
isInnerShortcodeCache.RLock()
|
||||
defer isInnerShortcodeCache.RUnlock()
|
||||
|
||||
if x := t.Lookup("shortcodes/" + name + ".html"); x != nil {
|
||||
return x
|
||||
}
|
||||
|
@ -580,7 +578,7 @@ func getShortcodeTemplate(name string, t tpl.Template) *template.Template {
|
|||
return t.Lookup("_internal/shortcodes/" + name + ".html")
|
||||
}
|
||||
|
||||
func renderShortcodeWithPage(tmpl *template.Template, data *ShortcodeWithPage) string {
|
||||
func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string {
|
||||
buffer := bp.GetBuffer()
|
||||
defer bp.PutBuffer(buffer)
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import (
|
|||
)
|
||||
|
||||
// TODO(bep) remove
|
||||
func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template) error) (*Page, error) {
|
||||
func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) {
|
||||
s := newTestSite(nil)
|
||||
if len(withTemplate) > 0 {
|
||||
// Have to create a new site
|
||||
|
@ -47,11 +47,11 @@ func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template
|
|||
return s.NewPageFrom(strings.NewReader(in), filename)
|
||||
}
|
||||
|
||||
func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error) {
|
||||
func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) {
|
||||
CheckShortCodeMatchAndError(t, input, expected, withTemplate, false)
|
||||
}
|
||||
|
||||
func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error, expectError bool) {
|
||||
func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {
|
||||
|
||||
cfg, fs := newTestCfg()
|
||||
|
||||
|
@ -100,8 +100,9 @@ func TestNonSC(t *testing.T) {
|
|||
// Issue #929
|
||||
func TestHyphenatedSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("hyphenated-video.html", `Playing Video {{ .Get 0 }}`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
|
||||
tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -111,8 +112,8 @@ func TestHyphenatedSC(t *testing.T) {
|
|||
// Issue #1753
|
||||
func TestNoTrailingNewline(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("a.html", `{{ .Get 0 }}`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -121,8 +122,8 @@ func TestNoTrailingNewline(t *testing.T) {
|
|||
|
||||
func TestPositionalParamSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 0 }}`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -135,8 +136,8 @@ func TestPositionalParamSC(t *testing.T) {
|
|||
|
||||
func TestPositionalParamIndexOutOfBounds(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 1 }}`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 1 }}`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video error: index out of range for positional param at position 1", wt)
|
||||
|
@ -146,8 +147,8 @@ func TestPositionalParamIndexOutOfBounds(t *testing.T) {
|
|||
|
||||
func TestNamedParamSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt)
|
||||
|
@ -161,10 +162,10 @@ func TestNamedParamSC(t *testing.T) {
|
|||
// Issue #2294
|
||||
func TestNestedNamedMissingParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("acc.html", `<div class="acc">{{ .Inner }}</div>`)
|
||||
tem.AddInternalShortcode("div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
|
||||
tem.AddInternalShortcode("div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`)
|
||||
tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
|
||||
tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t,
|
||||
|
@ -174,10 +175,10 @@ func TestNestedNamedMissingParam(t *testing.T) {
|
|||
|
||||
func TestIsNamedParamsSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("byposition.html", `<div id="{{ .Get 0 }}">`)
|
||||
tem.AddInternalShortcode("byname.html", `<div id="{{ .Get "id" }}">`)
|
||||
tem.AddInternalShortcode("ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/byposition.html", `<div id="{{ .Get 0 }}">`)
|
||||
tem.AddTemplate("_internal/shortcodes/byname.html", `<div id="{{ .Get "id" }}">`)
|
||||
tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt)
|
||||
|
@ -190,8 +191,8 @@ func TestIsNamedParamsSC(t *testing.T) {
|
|||
|
||||
func TestInnerSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt)
|
||||
|
@ -201,8 +202,8 @@ func TestInnerSC(t *testing.T) {
|
|||
|
||||
func TestInnerSCWithMarkdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{% inside %}}
|
||||
|
@ -215,8 +216,8 @@ func TestInnerSCWithMarkdown(t *testing.T) {
|
|||
|
||||
func TestInnerSCWithAndWithoutMarkdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{% inside %}}
|
||||
|
@ -246,9 +247,9 @@ func TestEmbeddedSC(t *testing.T) {
|
|||
|
||||
func TestNestedSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`)
|
||||
tem.AddInternalShortcode("scn2.html", `<div>SC2</div>`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`)
|
||||
tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div>\n</div>", wt)
|
||||
|
@ -258,10 +259,10 @@ func TestNestedSC(t *testing.T) {
|
|||
|
||||
func TestNestedComplexSC(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("row.html", `-row-{{ .Inner}}-rowStop-`)
|
||||
tem.AddInternalShortcode("column.html", `-col-{{.Inner }}-colStop-`)
|
||||
tem.AddInternalShortcode("aside.html", `-aside-{{ .Inner }}-asideStop-`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`)
|
||||
tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner }}-colStop-`)
|
||||
tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{ .Inner }}-asideStop-`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`,
|
||||
|
@ -274,10 +275,10 @@ func TestNestedComplexSC(t *testing.T) {
|
|||
|
||||
func TestParentShortcode(t *testing.T) {
|
||||
t.Parallel()
|
||||
wt := func(tem tpl.Template) error {
|
||||
tem.AddInternalShortcode("r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`)
|
||||
tem.AddInternalShortcode("r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`)
|
||||
tem.AddInternalShortcode("r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`)
|
||||
wt := func(tem tpl.TemplateHandler) error {
|
||||
tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`)
|
||||
tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`)
|
||||
tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`)
|
||||
return nil
|
||||
}
|
||||
CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`,
|
||||
|
@ -342,13 +343,13 @@ func TestExtractShortcodes(t *testing.T) {
|
|||
fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""},
|
||||
} {
|
||||
|
||||
p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error {
|
||||
templ.AddInternalShortcode("tag.html", `tag`)
|
||||
templ.AddInternalShortcode("sc1.html", `sc1`)
|
||||
templ.AddInternalShortcode("sc2.html", `sc2`)
|
||||
templ.AddInternalShortcode("inner.html", `{{with .Inner }}{{ . }}{{ end }}`)
|
||||
templ.AddInternalShortcode("inner2.html", `{{.Inner}}`)
|
||||
templ.AddInternalShortcode("inner3.html", `{{.Inner}}`)
|
||||
p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.TemplateHandler) error {
|
||||
templ.AddTemplate("_internal/shortcodes/tag.html", `tag`)
|
||||
templ.AddTemplate("_internal/shortcodes/sc1.html", `sc1`)
|
||||
templ.AddTemplate("_internal/shortcodes/sc2.html", `sc2`)
|
||||
templ.AddTemplate("_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`)
|
||||
templ.AddTemplate("_internal/shortcodes/inner2.html", `{{.Inner}}`)
|
||||
templ.AddTemplate("_internal/shortcodes/inner3.html", `{{.Inner}}`)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -517,14 +518,14 @@ tags:
|
|||
sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)}
|
||||
}
|
||||
|
||||
addTemplates := func(templ tpl.Template) error {
|
||||
addTemplates := func(templ tpl.TemplateHandler) error {
|
||||
templ.AddTemplate("_default/single.html", "{{.Content}}")
|
||||
|
||||
templ.AddInternalShortcode("b.html", `b`)
|
||||
templ.AddInternalShortcode("c.html", `c`)
|
||||
templ.AddInternalShortcode("d.html", `d`)
|
||||
templ.AddInternalShortcode("menu.html", `{{ len (index .Page.Menus "main").Children }}`)
|
||||
templ.AddInternalShortcode("tags.html", `{{ len .Page.Site.Taxonomies.tags }}`)
|
||||
templ.AddTemplate("_internal/shortcodes/b.html", `b`)
|
||||
templ.AddTemplate("_internal/shortcodes/c.html", `c`)
|
||||
templ.AddTemplate("_internal/shortcodes/d.html", `d`)
|
||||
templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`)
|
||||
templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`)
|
||||
|
||||
return nil
|
||||
|
||||
|
|
|
@ -193,7 +193,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) {
|
|||
// NewSiteDefaultLang creates a new site in the default language.
|
||||
// The site will have a template system loaded and ready to use.
|
||||
// Note: This is mainly used in single site tests.
|
||||
func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) (*Site, error) {
|
||||
func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
||||
v := viper.New()
|
||||
loadDefaultSettingsFor(v)
|
||||
return newSiteForLang(helpers.NewDefaultLanguage(v), withTemplate...)
|
||||
|
@ -202,15 +202,15 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) (*Site,
|
|||
// NewEnglishSite creates a new site in English language.
|
||||
// The site will have a template system loaded and ready to use.
|
||||
// Note: This is mainly used in single site tests.
|
||||
func NewEnglishSite(withTemplate ...func(templ tpl.Template) error) (*Site, error) {
|
||||
func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
||||
v := viper.New()
|
||||
loadDefaultSettingsFor(v)
|
||||
return newSiteForLang(helpers.NewLanguage("en", v), withTemplate...)
|
||||
}
|
||||
|
||||
// newSiteForLang creates a new site in the given language.
|
||||
func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.Template) error) (*Site, error) {
|
||||
withTemplates := func(templ tpl.Template) error {
|
||||
func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {
|
||||
withTemplates := func(templ tpl.TemplateHandler) error {
|
||||
for _, wt := range withTemplate {
|
||||
if err := wt(templ); err != nil {
|
||||
return err
|
||||
|
@ -1911,14 +1911,14 @@ Your rendered home page is blank: /index.html is zero-length
|
|||
}
|
||||
|
||||
func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) error {
|
||||
layout, found := s.findFirstLayout(layouts...)
|
||||
if !found {
|
||||
templ := s.findFirstTemplate(layouts...)
|
||||
if templ == nil {
|
||||
s.Log.WARN.Printf("[%s] Unable to locate layout for %s: %s\n", s.Language.Lang, name, layouts)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.renderThing(d, layout, w); err != nil {
|
||||
if err := templ.Execute(w, d); err != nil {
|
||||
|
||||
// Behavior here should be dependent on if running in server or watch mode.
|
||||
distinctErrorLogger.Printf("Error while rendering %q: %s", name, err)
|
||||
|
@ -1933,23 +1933,13 @@ func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) findFirstLayout(layouts ...string) (string, bool) {
|
||||
func (s *Site) findFirstTemplate(layouts ...string) tpl.Template {
|
||||
for _, layout := range layouts {
|
||||
if s.Tmpl.Lookup(layout) != nil {
|
||||
return layout, true
|
||||
if templ := s.Tmpl.Lookup(layout); templ != nil {
|
||||
return templ
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (s *Site) renderThing(d interface{}, layout string, w io.Writer) error {
|
||||
|
||||
// If the template doesn't exist, then return, but leave the Writer open
|
||||
if templ := s.Tmpl.Lookup(layout); templ != nil {
|
||||
return templ.Execute(w, d)
|
||||
}
|
||||
return fmt.Errorf("Layout not found: %s", layout)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) publish(path string, r io.Reader) (err error) {
|
||||
|
|
|
@ -90,9 +90,15 @@ outputs: %s
|
|||
Alt Output: {{ .Name -}}|
|
||||
{{- end -}}|
|
||||
{{- range .OutputFormats -}}
|
||||
Output/Rel: {{ .Name -}}/{{ .Rel }}|
|
||||
Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }}
|
||||
{{- end -}}
|
||||
{{ with .OutputFormats.Get "JSON" }}
|
||||
<atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" />
|
||||
{{ end }}
|
||||
`,
|
||||
"layouts/_default/list.html", `List HTML|{{ with .OutputFormats.Get "HTML" -}}
|
||||
<atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" />
|
||||
{{- end -}}`,
|
||||
)
|
||||
require.Len(t, h.Sites, 1)
|
||||
|
||||
|
@ -113,7 +119,6 @@ Output/Rel: {{ .Name -}}/{{ .Rel }}|
|
|||
|
||||
require.Len(t, home.outputFormats, lenOut)
|
||||
|
||||
// TODO(bep) output assert template/text
|
||||
// There is currently always a JSON output to make it simpler ...
|
||||
altFormats := lenOut - 1
|
||||
hasHTML := helpers.InStringArray(outputs, "html")
|
||||
|
@ -128,9 +133,16 @@ Output/Rel: {{ .Name -}}/{{ .Rel }}|
|
|||
"Output/Rel: JSON/alternate|",
|
||||
"Output/Rel: HTML/canonical|",
|
||||
)
|
||||
th.assertFileContent("public/index.html",
|
||||
// The HTML entity is a deliberate part of this test: The HTML templates are
|
||||
// parsed with html/template.
|
||||
`List HTML|<atom:link href=http://example.com/blog/ rel="self" type="text/html+html" />`,
|
||||
)
|
||||
} else {
|
||||
th.assertFileContent("public/index.json",
|
||||
"Output/Rel: JSON/canonical|",
|
||||
// JSON is plain text, so no need to safeHTML this and that
|
||||
`<atom:link href=http://example.com/blog/index.json rel="self" type="application/json+json" />`,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -52,24 +52,6 @@ func pageMust(p *Page, err error) *Page {
|
|||
return p
|
||||
}
|
||||
|
||||
func TestDegenerateRenderThingMissingTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg, fs := newTestCfg()
|
||||
|
||||
writeSource(t, fs, filepath.Join("content", "a", "file.md"), pageSimpleTitle)
|
||||
|
||||
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
|
||||
|
||||
require.Len(t, s.RegularPages, 1)
|
||||
|
||||
p := s.RegularPages[0]
|
||||
|
||||
err := s.renderThing(p, "foobar", nil)
|
||||
if err == nil {
|
||||
t.Errorf("Expected err to be returned when missing the template.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderWithInvalidTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg, fs := newTestCfg()
|
||||
|
|
|
@ -48,7 +48,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) {
|
|||
depsCfg := deps.DepsCfg{Fs: fs, Cfg: cfg}
|
||||
|
||||
if !internal {
|
||||
depsCfg.WithTemplate = func(templ tpl.Template) error {
|
||||
depsCfg.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
templ.AddTemplate("sitemap.xml", sitemapTemplate)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -164,9 +164,9 @@ func newDebugLogger() *jww.Notepad {
|
|||
func newErrorLogger() *jww.Notepad {
|
||||
return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
|
||||
}
|
||||
func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.Template) error {
|
||||
func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error {
|
||||
|
||||
return func(templ tpl.Template) error {
|
||||
return func(templ tpl.TemplateHandler) error {
|
||||
for i := 0; i < len(additionalTemplates); i += 2 {
|
||||
err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1])
|
||||
if err != nil {
|
||||
|
|
|
@ -152,9 +152,11 @@ func (l *LayoutHandler) For(d LayoutDescriptor, layoutOverride string, f Format)
|
|||
}
|
||||
}
|
||||
|
||||
return layoutsWithThemeLayouts, nil
|
||||
layouts = layoutsWithThemeLayouts
|
||||
}
|
||||
|
||||
layouts = prependTextPrefixIfNeeded(f, layouts...)
|
||||
|
||||
l.mu.Lock()
|
||||
l.cache[key] = layouts
|
||||
l.mu.Unlock()
|
||||
|
@ -184,10 +186,26 @@ func resolveListTemplate(d LayoutDescriptor, f Format,
|
|||
}
|
||||
|
||||
func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string {
|
||||
return strings.Fields(replaceKeyValues(templ,
|
||||
layouts := strings.Fields(replaceKeyValues(templ,
|
||||
"SUFFIX", f.MediaType.Suffix,
|
||||
"NAME", strings.ToLower(f.Name),
|
||||
"SECTION", d.Section))
|
||||
|
||||
return layouts
|
||||
}
|
||||
|
||||
func prependTextPrefixIfNeeded(f Format, layouts ...string) []string {
|
||||
if !f.IsPlainText {
|
||||
return layouts
|
||||
}
|
||||
|
||||
newLayouts := make([]string, len(layouts))
|
||||
|
||||
for i, l := range layouts {
|
||||
newLayouts[i] = "_text/" + l
|
||||
}
|
||||
|
||||
return newLayouts
|
||||
}
|
||||
|
||||
func replaceKeyValues(s string, oldNew ...string) string {
|
||||
|
@ -195,7 +213,9 @@ func replaceKeyValues(s string, oldNew ...string) string {
|
|||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
func regularPageLayouts(types string, layout string, f Format) (layouts []string) {
|
||||
func regularPageLayouts(types string, layout string, f Format) []string {
|
||||
var layouts []string
|
||||
|
||||
if layout == "" {
|
||||
layout = "single"
|
||||
}
|
||||
|
@ -219,5 +239,5 @@ func regularPageLayouts(types string, layout string, f Format) (layouts []string
|
|||
layouts = append(layouts, fmt.Sprintf("_default/%s.%s.%s", layout, name, suffix))
|
||||
layouts = append(layouts, fmt.Sprintf("_default/%s.%s", layout, suffix))
|
||||
|
||||
return
|
||||
return layouts
|
||||
}
|
||||
|
|
|
@ -29,7 +29,10 @@ var (
|
|||
)
|
||||
|
||||
type TemplateNames struct {
|
||||
Name string
|
||||
// The name used as key in the template map. Note that this will be
|
||||
// prefixed with "_text/" if it should be parsed with text/template.
|
||||
Name string
|
||||
|
||||
OverlayFilename string
|
||||
MasterFilename string
|
||||
}
|
||||
|
@ -51,6 +54,10 @@ type TemplateLookupDescriptor struct {
|
|||
// The theme name if active.
|
||||
Theme string
|
||||
|
||||
// All the output formats in play. This is used to decide if text/template or
|
||||
// html/template.
|
||||
OutputFormats Formats
|
||||
|
||||
FileExists func(filename string) (bool, error)
|
||||
ContainsAny func(filename string, subslices [][]byte) (bool, error)
|
||||
}
|
||||
|
@ -74,6 +81,12 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) {
|
|||
// index.amp.html
|
||||
// index.json
|
||||
filename := filepath.Base(d.RelPath)
|
||||
isPlainText := false
|
||||
outputFormat, found := d.OutputFormats.FromFilename(filename)
|
||||
|
||||
if found && outputFormat.IsPlainText {
|
||||
isPlainText = true
|
||||
}
|
||||
|
||||
var ext, outFormat string
|
||||
|
||||
|
@ -90,6 +103,10 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) {
|
|||
id.OverlayFilename = fullPath
|
||||
id.Name = name
|
||||
|
||||
if isPlainText {
|
||||
id.Name = "_text/" + id.Name
|
||||
}
|
||||
|
||||
// Ace and Go templates may have both a base and inner template.
|
||||
pathDir := filepath.Dir(fullPath)
|
||||
|
||||
|
|
|
@ -141,6 +141,7 @@ func TestLayoutBase(t *testing.T) {
|
|||
return this.needsBase, nil
|
||||
}
|
||||
|
||||
this.d.OutputFormats = Formats{AMPFormat, HTMLFormat, RSSFormat, JSONFormat}
|
||||
this.d.WorkingDir = filepath.FromSlash(this.d.WorkingDir)
|
||||
this.d.LayoutDir = filepath.FromSlash(this.d.LayoutDir)
|
||||
this.d.RelPath = filepath.FromSlash(this.d.RelPath)
|
||||
|
@ -150,6 +151,11 @@ func TestLayoutBase(t *testing.T) {
|
|||
this.expect.MasterFilename = filepath.FromSlash(this.expect.MasterFilename)
|
||||
this.expect.OverlayFilename = filepath.FromSlash(this.expect.OverlayFilename)
|
||||
|
||||
if strings.Contains(this.d.RelPath, "json") {
|
||||
// currently the only plain text templates in this test.
|
||||
this.expect.Name = "_text/" + this.expect.Name
|
||||
}
|
||||
|
||||
id, err := CreateTemplateNames(this.d)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -64,6 +64,10 @@ func TestLayout(t *testing.T) {
|
|||
[]string{"taxonomy/tag.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}},
|
||||
{"RSS Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, false, "", RSSFormat,
|
||||
[]string{"taxonomy/tag.terms.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}},
|
||||
{"Home plain text", LayoutDescriptor{Kind: "home"}, true, "", JSONFormat,
|
||||
[]string{"_text/index.json.json", "_text/index.json", "_text/_default/list.json.json", "_text/_default/list.json", "_text/theme/index.json.json", "_text/theme/index.json"}},
|
||||
{"Page plain text", LayoutDescriptor{Kind: "page"}, true, "", JSONFormat,
|
||||
[]string{"_text/_default/single.json.json", "_text/_default/single.json", "_text/theme/_default/single.json.json"}},
|
||||
} {
|
||||
t.Run(this.name, func(t *testing.T) {
|
||||
l := NewLayoutHandler(this.hasTheme)
|
||||
|
|
|
@ -33,6 +33,7 @@ var (
|
|||
IsHTML: true,
|
||||
}
|
||||
|
||||
// CalendarFormat is AAA
|
||||
CalendarFormat = Format{
|
||||
Name: "Calendar",
|
||||
MediaType: media.CalendarType,
|
||||
|
@ -104,6 +105,45 @@ func (formats Formats) GetByName(name string) (f Format, found bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func (formats Formats) GetBySuffix(name string) (f Format, found bool) {
|
||||
for _, ff := range formats {
|
||||
if name == ff.MediaType.Suffix {
|
||||
if found {
|
||||
// ambiguous
|
||||
found = false
|
||||
return
|
||||
}
|
||||
f = ff
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (formats Formats) FromFilename(filename string) (f Format, found bool) {
|
||||
// mytemplate.amp.html
|
||||
// mytemplate.html
|
||||
// mytemplate
|
||||
var ext, outFormat string
|
||||
|
||||
parts := strings.Split(filename, ".")
|
||||
if len(parts) > 2 {
|
||||
outFormat = parts[1]
|
||||
ext = parts[2]
|
||||
} else if len(parts) > 1 {
|
||||
ext = parts[1]
|
||||
}
|
||||
|
||||
if outFormat != "" {
|
||||
return formats.GetByName(outFormat)
|
||||
}
|
||||
|
||||
if ext != "" {
|
||||
return formats.GetBySuffix(ext)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Format represents an output representation, usually to a file on disk.
|
||||
type Format struct {
|
||||
// The Name is used as an identifier. Internal output formats (i.e. HTML and RSS)
|
||||
|
|
|
@ -65,7 +65,7 @@ func TestDefaultTypes(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestGetType(t *testing.T) {
|
||||
func TestGetFormat(t *testing.T) {
|
||||
tp, _ := GetFormat("html")
|
||||
require.Equal(t, HTMLFormat, tp)
|
||||
tp, _ = GetFormat("HTML")
|
||||
|
@ -73,3 +73,28 @@ func TestGetType(t *testing.T) {
|
|||
_, found := GetFormat("FOO")
|
||||
require.False(t, found)
|
||||
}
|
||||
|
||||
func TestGeGetFormatByName(t *testing.T) {
|
||||
formats := Formats{AMPFormat, CalendarFormat}
|
||||
tp, _ := formats.GetByName("AMP")
|
||||
require.Equal(t, AMPFormat, tp)
|
||||
_, found := formats.GetByName("HTML")
|
||||
require.False(t, found)
|
||||
_, found = formats.GetByName("FOO")
|
||||
require.False(t, found)
|
||||
}
|
||||
|
||||
func TestGeGetFormatByExt(t *testing.T) {
|
||||
formats1 := Formats{AMPFormat, CalendarFormat}
|
||||
formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat}
|
||||
tp, _ := formats1.GetBySuffix("html")
|
||||
require.Equal(t, AMPFormat, tp)
|
||||
tp, _ = formats1.GetBySuffix("ics")
|
||||
require.Equal(t, CalendarFormat, tp)
|
||||
_, found := formats1.GetBySuffix("not")
|
||||
require.False(t, found)
|
||||
|
||||
// ambiguous
|
||||
_, found = formats2.GetByName("html")
|
||||
require.False(t, found)
|
||||
}
|
||||
|
|
111
tpl/template.go
111
tpl/template.go
|
@ -1,28 +1,103 @@
|
|||
// Copyright 2017-present 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
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"text/template/parse"
|
||||
|
||||
"html/template"
|
||||
texttemplate "text/template"
|
||||
|
||||
bp "github.com/spf13/hugo/bufferpool"
|
||||
)
|
||||
|
||||
// TODO(bep) make smaller
|
||||
type Template interface {
|
||||
ExecuteTemplate(wr io.Writer, name string, data interface{}) error
|
||||
ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML
|
||||
Lookup(name string) *template.Template
|
||||
Templates() []*template.Template
|
||||
New(name string) *template.Template
|
||||
GetClone() *template.Template
|
||||
RebuildClone() *template.Template
|
||||
LoadTemplates(absPath string)
|
||||
LoadTemplatesWithPrefix(absPath, prefix string)
|
||||
var (
|
||||
_ TemplateExecutor = (*TemplateAdapter)(nil)
|
||||
)
|
||||
|
||||
// TemplateHandler manages the collection of templates.
|
||||
type TemplateHandler interface {
|
||||
TemplateFinder
|
||||
AddTemplate(name, tpl string) error
|
||||
AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error
|
||||
AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error
|
||||
AddInternalTemplate(prefix, name, tpl string) error
|
||||
AddInternalShortcode(name, tpl string) error
|
||||
Partial(name string, contextList ...interface{}) template.HTML
|
||||
AddLateTemplate(name, tpl string) error
|
||||
LoadTemplates(absPath, prefix string)
|
||||
PrintErrors()
|
||||
Funcs(funcMap template.FuncMap)
|
||||
|
||||
MarkReady()
|
||||
RebuildClone()
|
||||
}
|
||||
|
||||
// TemplateFinder finds templates.
|
||||
type TemplateFinder interface {
|
||||
Lookup(name string) *TemplateAdapter
|
||||
}
|
||||
|
||||
// Template is the common interface between text/template and html/template.
|
||||
type Template interface {
|
||||
Execute(wr io.Writer, data interface{}) error
|
||||
Name() string
|
||||
}
|
||||
|
||||
// TemplateExecutor adds some extras to Template.
|
||||
type TemplateExecutor interface {
|
||||
Template
|
||||
ExecuteToString(data interface{}) (string, error)
|
||||
Tree() string
|
||||
}
|
||||
|
||||
// TemplateAdapter implements the TemplateExecutor interface.
|
||||
type TemplateAdapter struct {
|
||||
Template
|
||||
}
|
||||
|
||||
// ExecuteToString executes the current template and returns the result as a
|
||||
// string.
|
||||
func (t *TemplateAdapter) ExecuteToString(data interface{}) (string, error) {
|
||||
b := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b)
|
||||
if err := t.Execute(b, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// Tree returns the template Parse tree as a string.
|
||||
// Note: this isn't safe for parallel execution on the same template
|
||||
// vs Lookup and Execute.
|
||||
func (t *TemplateAdapter) Tree() string {
|
||||
var tree *parse.Tree
|
||||
switch tt := t.Template.(type) {
|
||||
case *template.Template:
|
||||
tree = tt.Tree
|
||||
case *texttemplate.Template:
|
||||
tree = tt.Tree
|
||||
default:
|
||||
panic("Unknown template")
|
||||
}
|
||||
|
||||
if tree.Root == nil {
|
||||
return ""
|
||||
}
|
||||
s := tree.Root.String()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// TemplateTestMocker adds a way to override some template funcs during tests.
|
||||
// The interface is named so it's not used in regular application code.
|
||||
type TemplateTestMocker interface {
|
||||
SetFuncs(funcMap map[string]interface{})
|
||||
}
|
||||
|
|
51
tpl/tplimpl/ace.go
Normal file
51
tpl/tplimpl/ace.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2017-present 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 (
|
||||
"path/filepath"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/yosssi/ace"
|
||||
)
|
||||
|
||||
func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error {
|
||||
t.checkState()
|
||||
var base, inner *ace.File
|
||||
name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html"
|
||||
|
||||
// Fixes issue #1178
|
||||
basePath = strings.Replace(basePath, "\\", "/", -1)
|
||||
innerPath = strings.Replace(innerPath, "\\", "/", -1)
|
||||
|
||||
if basePath != "" {
|
||||
base = ace.NewFile(basePath, baseContent)
|
||||
inner = ace.NewFile(innerPath, innerContent)
|
||||
} else {
|
||||
base = ace.NewFile(innerPath, innerContent)
|
||||
inner = ace.NewFile("", []byte{})
|
||||
}
|
||||
parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
templ, err := ace.CompileResultWithTemplate(t.html.t.New(name), parsed, nil)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
return applyTemplateTransformersToHMLTTemplate(templ)
|
||||
}
|
|
@ -19,7 +19,7 @@ import (
|
|||
"github.com/eknkc/amber"
|
||||
)
|
||||
|
||||
func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) {
|
||||
func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) {
|
||||
c := amber.New()
|
||||
|
||||
if err := c.ParseData(b, path); err != nil {
|
||||
|
@ -32,7 +32,7 @@ func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *tem
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tpl, err := t.Funcs(gt.amberFuncMap).Parse(data)
|
||||
tpl, err := templ.Funcs(t.amberFuncMap).Parse(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2016 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2017-present 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.
|
||||
|
@ -15,23 +15,39 @@ package tplimpl
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sync"
|
||||
texttemplate "text/template"
|
||||
|
||||
"github.com/eknkc/amber"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/spf13/hugo/output"
|
||||
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
bp "github.com/spf13/hugo/bufferpool"
|
||||
"github.com/spf13/hugo/deps"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
"github.com/spf13/hugo/output"
|
||||
"github.com/yosssi/ace"
|
||||
"github.com/spf13/hugo/tpl"
|
||||
)
|
||||
|
||||
// TODO(bep) globals get rid of the rest of the jww.ERR etc.
|
||||
const (
|
||||
textTmplNamePrefix = "_text/"
|
||||
)
|
||||
|
||||
var (
|
||||
_ tpl.TemplateHandler = (*templateHandler)(nil)
|
||||
_ tpl.TemplateTestMocker = (*templateHandler)(nil)
|
||||
_ tpl.TemplateFinder = (*htmlTemplates)(nil)
|
||||
_ tpl.TemplateFinder = (*textTemplates)(nil)
|
||||
_ templateLoader = (*htmlTemplates)(nil)
|
||||
_ templateLoader = (*textTemplates)(nil)
|
||||
_ templateLoader = (*templateHandler)(nil)
|
||||
_ templateFuncsterTemplater = (*htmlTemplates)(nil)
|
||||
_ templateFuncsterTemplater = (*textTemplates)(nil)
|
||||
)
|
||||
|
||||
// Protecting global map access (Amber)
|
||||
var amberMu sync.Mutex
|
||||
|
@ -41,8 +57,120 @@ type templateErr struct {
|
|||
err error
|
||||
}
|
||||
|
||||
type GoHTMLTemplate struct {
|
||||
*template.Template
|
||||
type templateLoader interface {
|
||||
handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error
|
||||
addTemplate(name, tpl string) error
|
||||
addLateTemplate(name, tpl string) error
|
||||
}
|
||||
|
||||
type templateFuncsterTemplater interface {
|
||||
tpl.TemplateFinder
|
||||
setFuncs(funcMap map[string]interface{})
|
||||
setTemplateFuncster(f *templateFuncster)
|
||||
}
|
||||
|
||||
// templateHandler holds the templates in play.
|
||||
// It implements the templateLoader and tpl.TemplateHandler interfaces.
|
||||
type templateHandler struct {
|
||||
// text holds all the pure text templates.
|
||||
text *textTemplates
|
||||
html *htmlTemplates
|
||||
|
||||
amberFuncMap template.FuncMap
|
||||
|
||||
errors []*templateErr
|
||||
|
||||
*deps.Deps
|
||||
}
|
||||
|
||||
func (t *templateHandler) addError(name string, err error) {
|
||||
t.errors = append(t.errors, &templateErr{name, err})
|
||||
}
|
||||
|
||||
// PrintErrors prints the accumulated errors as ERROR to the log.
|
||||
func (t *templateHandler) PrintErrors() {
|
||||
for _, e := range t.errors {
|
||||
t.Log.ERROR.Println(e.name, ":", e.err)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup tries to find a template with the given name in both template
|
||||
// collections: First HTML, then the plain text template collection.
|
||||
func (t *templateHandler) Lookup(name string) *tpl.TemplateAdapter {
|
||||
var te *tpl.TemplateAdapter
|
||||
|
||||
isTextTemplate := strings.HasPrefix(name, textTmplNamePrefix)
|
||||
|
||||
if isTextTemplate {
|
||||
// The templates are stored without the prefix identificator.
|
||||
name = strings.TrimPrefix(name, textTmplNamePrefix)
|
||||
te = t.text.Lookup(name)
|
||||
} else {
|
||||
te = t.html.Lookup(name)
|
||||
}
|
||||
|
||||
if te == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return te
|
||||
}
|
||||
|
||||
func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
|
||||
c := &templateHandler{
|
||||
Deps: d,
|
||||
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)},
|
||||
text: &textTemplates{t: texttemplate.Must(t.text.t.Clone()), overlays: make(map[string]*texttemplate.Template)},
|
||||
errors: make([]*templateErr, 0),
|
||||
}
|
||||
|
||||
d.Tmpl = c
|
||||
|
||||
c.initFuncs()
|
||||
|
||||
for k, v := range t.html.overlays {
|
||||
vc := template.Must(v.Clone())
|
||||
// The extra lookup is a workaround, see
|
||||
// * https://github.com/golang/go/issues/16101
|
||||
// * https://github.com/spf13/hugo/issues/2549
|
||||
vc = vc.Lookup(vc.Name())
|
||||
vc.Funcs(t.html.funcster.funcMap)
|
||||
c.html.overlays[k] = vc
|
||||
}
|
||||
|
||||
for k, v := range t.text.overlays {
|
||||
vc := texttemplate.Must(v.Clone())
|
||||
vc = vc.Lookup(vc.Name())
|
||||
vc.Funcs(texttemplate.FuncMap(t.text.funcster.funcMap))
|
||||
c.text.overlays[k] = vc
|
||||
}
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
func newTemplateAdapter(deps *deps.Deps) *templateHandler {
|
||||
htmlT := &htmlTemplates{
|
||||
t: template.New(""),
|
||||
overlays: make(map[string]*template.Template),
|
||||
}
|
||||
textT := &textTemplates{
|
||||
t: texttemplate.New(""),
|
||||
overlays: make(map[string]*texttemplate.Template),
|
||||
}
|
||||
return &templateHandler{
|
||||
Deps: deps,
|
||||
html: htmlT,
|
||||
text: textT,
|
||||
errors: make([]*templateErr, 0),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type htmlTemplates struct {
|
||||
funcster *templateFuncster
|
||||
|
||||
t *template.Template
|
||||
|
||||
// This looks, and is, strange.
|
||||
// The clone is used by non-renderable content pages, and these need to be
|
||||
|
@ -54,397 +182,201 @@ type GoHTMLTemplate struct {
|
|||
// a separate storage for the overlays created from cloned master templates.
|
||||
// note: No mutex protection, so we add these in one Go routine, then just read.
|
||||
overlays map[string]*template.Template
|
||||
|
||||
errors []*templateErr
|
||||
|
||||
funcster *templateFuncster
|
||||
|
||||
amberFuncMap template.FuncMap
|
||||
|
||||
*deps.Deps
|
||||
}
|
||||
|
||||
type TemplateProvider struct{}
|
||||
|
||||
var DefaultTemplateProvider *TemplateProvider
|
||||
|
||||
// Update updates the Hugo Template System in the provided Deps.
|
||||
// with all the additional features, templates & functions
|
||||
func (*TemplateProvider) Update(deps *deps.Deps) error {
|
||||
tmpl := &GoHTMLTemplate{
|
||||
Template: template.New(""),
|
||||
overlays: make(map[string]*template.Template),
|
||||
errors: make([]*templateErr, 0),
|
||||
Deps: deps,
|
||||
}
|
||||
|
||||
deps.Tmpl = tmpl
|
||||
|
||||
tmpl.initFuncs(deps)
|
||||
|
||||
tmpl.LoadEmbedded()
|
||||
|
||||
if deps.WithTemplate != nil {
|
||||
err := deps.WithTemplate(tmpl)
|
||||
if err != nil {
|
||||
tmpl.errors = append(tmpl.errors, &templateErr{"init", err})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tmpl.MarkReady()
|
||||
|
||||
return nil
|
||||
|
||||
func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) {
|
||||
t.funcster = f
|
||||
}
|
||||
|
||||
// Clone clones
|
||||
func (*TemplateProvider) Clone(d *deps.Deps) error {
|
||||
|
||||
t := d.Tmpl.(*GoHTMLTemplate)
|
||||
|
||||
// 1. Clone the clone with new template funcs
|
||||
// 2. Clone any overlays with new template funcs
|
||||
|
||||
tmpl := &GoHTMLTemplate{
|
||||
Template: template.Must(t.Template.Clone()),
|
||||
overlays: make(map[string]*template.Template),
|
||||
errors: make([]*templateErr, 0),
|
||||
Deps: d,
|
||||
func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter {
|
||||
templ := t.lookup(name)
|
||||
if templ == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.Tmpl = tmpl
|
||||
tmpl.initFuncs(d)
|
||||
|
||||
for k, v := range t.overlays {
|
||||
vc := template.Must(v.Clone())
|
||||
// The extra lookup is a workaround, see
|
||||
// * https://github.com/golang/go/issues/16101
|
||||
// * https://github.com/spf13/hugo/issues/2549
|
||||
vc = vc.Lookup(vc.Name())
|
||||
vc.Funcs(tmpl.funcster.funcMap)
|
||||
tmpl.overlays[k] = vc
|
||||
}
|
||||
|
||||
tmpl.MarkReady()
|
||||
|
||||
return nil
|
||||
return &tpl.TemplateAdapter{templ}
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) {
|
||||
|
||||
t.funcster = newTemplateFuncster(d)
|
||||
|
||||
// The URL funcs in the funcMap is somewhat language dependent,
|
||||
// so we need to wait until the language and site config is loaded.
|
||||
t.funcster.initFuncMap()
|
||||
|
||||
t.amberFuncMap = template.FuncMap{}
|
||||
|
||||
amberMu.Lock()
|
||||
for k, v := range amber.FuncMap {
|
||||
t.amberFuncMap[k] = v
|
||||
}
|
||||
|
||||
for k, v := range t.funcster.funcMap {
|
||||
t.amberFuncMap[k] = v
|
||||
// Hacky, but we need to make sure that the func names are in the global map.
|
||||
amber.FuncMap[k] = func() string {
|
||||
panic("should never be invoked")
|
||||
}
|
||||
}
|
||||
amberMu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) {
|
||||
t.Template.Funcs(funcMap)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML {
|
||||
if strings.HasPrefix("partials/", name) {
|
||||
name = name[8:]
|
||||
}
|
||||
var context interface{}
|
||||
|
||||
if len(contextList) == 0 {
|
||||
context = nil
|
||||
} else {
|
||||
context = contextList[0]
|
||||
}
|
||||
return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) executeTemplate(context interface{}, w io.Writer, layouts ...string) {
|
||||
var worked bool
|
||||
for _, layout := range layouts {
|
||||
templ := t.Lookup(layout)
|
||||
if templ == nil {
|
||||
// TODO(bep) output
|
||||
layout += ".html"
|
||||
templ = t.Lookup(layout)
|
||||
}
|
||||
|
||||
if templ != nil {
|
||||
if err := templ.Execute(w, context); err != nil {
|
||||
helpers.DistinctErrorLog.Println(layout, err)
|
||||
}
|
||||
worked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !worked {
|
||||
t.Log.ERROR.Println("Unable to render", layouts)
|
||||
t.Log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML {
|
||||
b := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b)
|
||||
t.executeTemplate(context, b, layouts...)
|
||||
return template.HTML(b.String())
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) Lookup(name string) *template.Template {
|
||||
|
||||
if templ := t.Template.Lookup(name); templ != nil {
|
||||
func (t *htmlTemplates) lookup(name string) *template.Template {
|
||||
if templ := t.t.Lookup(name); templ != nil {
|
||||
return templ
|
||||
}
|
||||
|
||||
if t.overlays != nil {
|
||||
if templ, ok := t.overlays[name]; ok {
|
||||
return templ
|
||||
}
|
||||
}
|
||||
|
||||
// The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed
|
||||
// as Go templates late in the build process.
|
||||
if t.clone != nil {
|
||||
if templ := t.clone.Lookup(name); templ != nil {
|
||||
return t.clone.Lookup(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type textTemplates struct {
|
||||
funcster *templateFuncster
|
||||
|
||||
t *texttemplate.Template
|
||||
|
||||
clone *texttemplate.Template
|
||||
cloneClone *texttemplate.Template
|
||||
|
||||
overlays map[string]*texttemplate.Template
|
||||
}
|
||||
|
||||
func (t *textTemplates) setTemplateFuncster(f *templateFuncster) {
|
||||
t.funcster = f
|
||||
}
|
||||
|
||||
func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter {
|
||||
templ := t.lookup(name)
|
||||
if templ == nil {
|
||||
return nil
|
||||
}
|
||||
return &tpl.TemplateAdapter{templ}
|
||||
}
|
||||
|
||||
func (t *textTemplates) lookup(name string) *texttemplate.Template {
|
||||
if templ := t.t.Lookup(name); templ != nil {
|
||||
return templ
|
||||
}
|
||||
if t.overlays != nil {
|
||||
if templ, ok := t.overlays[name]; ok {
|
||||
return templ
|
||||
}
|
||||
}
|
||||
|
||||
if t.clone != nil {
|
||||
return t.clone.Lookup(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *templateHandler) setFuncs(funcMap map[string]interface{}) {
|
||||
t.html.setFuncs(funcMap)
|
||||
t.text.setFuncs(funcMap)
|
||||
}
|
||||
|
||||
// SetFuncs replaces the funcs in the func maps with new definitions.
|
||||
// This is only used in tests.
|
||||
func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) {
|
||||
t.setFuncs(funcMap)
|
||||
}
|
||||
|
||||
func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) {
|
||||
t.t.Funcs(funcMap)
|
||||
}
|
||||
|
||||
func (t *textTemplates) setFuncs(funcMap map[string]interface{}) {
|
||||
t.t.Funcs(funcMap)
|
||||
}
|
||||
|
||||
// LoadTemplates loads the templates, starting from the given absolute path.
|
||||
// A prefix can be given to indicate a template namespace to load the templates
|
||||
// into, i.e. "_internal" etc.
|
||||
func (t *templateHandler) LoadTemplates(absPath, prefix string) {
|
||||
// TODO(bep) output formats. Will have to get to complete list when that is ready.
|
||||
t.loadTemplates(absPath, prefix, output.Formats{output.HTMLFormat, output.RSSFormat, output.CalendarFormat, output.AMPFormat, output.JSONFormat})
|
||||
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) GetClone() *template.Template {
|
||||
return t.clone
|
||||
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error {
|
||||
templ, err := tt.New(name).Parse(tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) RebuildClone() *template.Template {
|
||||
t.clone = template.Must(t.cloneClone.Clone())
|
||||
return t.clone
|
||||
func (t *htmlTemplates) addTemplate(name, tpl string) error {
|
||||
return t.addTemplateIn(t.t, name, tpl)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) LoadEmbedded() {
|
||||
t.EmbedShortcodes()
|
||||
t.EmbedTemplates()
|
||||
func (t *htmlTemplates) addLateTemplate(name, tpl string) error {
|
||||
return t.addTemplateIn(t.clone, name, tpl)
|
||||
}
|
||||
|
||||
// MarkReady marks the template as "ready for execution". No changes allowed
|
||||
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error {
|
||||
name = strings.TrimPrefix(name, textTmplNamePrefix)
|
||||
templ, err := tt.New(name).Parse(tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := applyTemplateTransformersToTextTemplate(templ); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *textTemplates) addTemplate(name, tpl string) error {
|
||||
return t.addTemplateIn(t.t, name, tpl)
|
||||
}
|
||||
|
||||
func (t *textTemplates) addLateTemplate(name, tpl string) error {
|
||||
return t.addTemplateIn(t.clone, name, tpl)
|
||||
}
|
||||
|
||||
func (t *templateHandler) addTemplate(name, tpl string) error {
|
||||
return t.AddTemplate(name, tpl)
|
||||
}
|
||||
|
||||
func (t *templateHandler) addLateTemplate(name, tpl string) error {
|
||||
return t.AddLateTemplate(name, tpl)
|
||||
}
|
||||
|
||||
// AddLateTemplate is used to add a template late, i.e. after the
|
||||
// regular templates have started its execution.
|
||||
func (t *templateHandler) AddLateTemplate(name, tpl string) error {
|
||||
h := t.getTemplateHandler(name)
|
||||
if err := h.addLateTemplate(name, tpl); err != nil {
|
||||
t.addError(name, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTemplate parses and adds a template to the collection.
|
||||
// Templates with name prefixed with "_text" will be handled as plain
|
||||
// text templates.
|
||||
func (t *templateHandler) AddTemplate(name, tpl string) error {
|
||||
h := t.getTemplateHandler(name)
|
||||
if err := h.addTemplate(name, tpl); err != nil {
|
||||
t.addError(name, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkReady marks the templates as "ready for execution". No changes allowed
|
||||
// after this is set.
|
||||
// TODO(bep) if this proves to be resource heavy, we could detect
|
||||
// earlier if we really need this, or make it lazy.
|
||||
func (t *GoHTMLTemplate) MarkReady() {
|
||||
if t.clone == nil {
|
||||
t.clone = template.Must(t.Template.Clone())
|
||||
t.cloneClone = template.Must(t.clone.Clone())
|
||||
func (t *templateHandler) MarkReady() {
|
||||
if t.html.clone == nil {
|
||||
t.html.clone = template.Must(t.html.t.Clone())
|
||||
t.html.cloneClone = template.Must(t.html.clone.Clone())
|
||||
}
|
||||
if t.text.clone == nil {
|
||||
t.text.clone = texttemplate.Must(t.text.t.Clone())
|
||||
t.text.cloneClone = texttemplate.Must(t.text.clone.Clone())
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) checkState() {
|
||||
if t.clone != nil {
|
||||
panic("template is cloned and cannot be modfified")
|
||||
}
|
||||
// RebuildClone rebuilds the cloned templates. Used for live-reloads.
|
||||
func (t *templateHandler) RebuildClone() {
|
||||
t.html.clone = template.Must(t.html.cloneClone.Clone())
|
||||
t.text.clone = texttemplate.Must(t.text.cloneClone.Clone())
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) AddInternalTemplate(prefix, name, tpl string) error {
|
||||
if prefix != "" {
|
||||
return t.AddTemplate("_internal/"+prefix+"/"+name, tpl)
|
||||
}
|
||||
return t.AddTemplate("_internal/"+name, tpl)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error {
|
||||
return t.AddInternalTemplate("shortcodes", name, content)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error {
|
||||
t.checkState()
|
||||
templ, err := t.New(name).Parse(tpl)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
if err := applyTemplateTransformers(templ); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error {
|
||||
|
||||
// There is currently no known way to associate a cloned template with an existing one.
|
||||
// This funky master/overlay design will hopefully improve in a future version of Go.
|
||||
//
|
||||
// Simplicity is hard.
|
||||
//
|
||||
// Until then we'll have to live with this hackery.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/14285
|
||||
//
|
||||
// So, to do minimum amount of changes to get this to work:
|
||||
//
|
||||
// 1. Lookup or Parse the master
|
||||
// 2. Parse and store the overlay in a separate map
|
||||
|
||||
masterTpl := t.Lookup(masterFilename)
|
||||
|
||||
if masterTpl == nil {
|
||||
b, err := afero.ReadFile(t.Fs.Source, masterFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
masterTpl, err = t.New(masterFilename).Parse(string(b))
|
||||
|
||||
if err != nil {
|
||||
// TODO(bep) Add a method that does this
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := afero.ReadFile(t.Fs.Source, overlayFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b))
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
} else {
|
||||
// The extra lookup is a workaround, see
|
||||
// * https://github.com/golang/go/issues/16101
|
||||
// * https://github.com/spf13/hugo/issues/2549
|
||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||
if err := applyTemplateTransformers(overlayTpl); err != nil {
|
||||
return err
|
||||
}
|
||||
t.overlays[name] = overlayTpl
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error {
|
||||
t.checkState()
|
||||
var base, inner *ace.File
|
||||
name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html"
|
||||
|
||||
// Fixes issue #1178
|
||||
basePath = strings.Replace(basePath, "\\", "/", -1)
|
||||
innerPath = strings.Replace(innerPath, "\\", "/", -1)
|
||||
|
||||
if basePath != "" {
|
||||
base = ace.NewFile(basePath, baseContent)
|
||||
inner = ace.NewFile(innerPath, innerContent)
|
||||
} else {
|
||||
base = ace.NewFile(innerPath, innerContent)
|
||||
inner = ace.NewFile("", []byte{})
|
||||
}
|
||||
parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil)
|
||||
if err != nil {
|
||||
t.errors = append(t.errors, &templateErr{name: name, err: err})
|
||||
return err
|
||||
}
|
||||
return applyTemplateTransformers(templ)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) error {
|
||||
t.checkState()
|
||||
// get the suffix and switch on that
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".amber":
|
||||
templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html"
|
||||
b, err := afero.ReadFile(t.Fs.Source, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amberMu.Lock()
|
||||
templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName))
|
||||
amberMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return applyTemplateTransformers(templ)
|
||||
case ".ace":
|
||||
var innerContent, baseContent []byte
|
||||
innerContent, err := afero.ReadFile(t.Fs.Source, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if baseTemplatePath != "" {
|
||||
baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
|
||||
default:
|
||||
|
||||
if baseTemplatePath != "" {
|
||||
return t.AddTemplateFileWithMaster(name, path, baseTemplatePath)
|
||||
}
|
||||
|
||||
b, err := afero.ReadFile(t.Fs.Source, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Log.DEBUG.Printf("Add template file from path %s", path)
|
||||
|
||||
return t.AddTemplate(name, string(b))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string {
|
||||
name, _ := filepath.Rel(base, path)
|
||||
return filepath.ToSlash(name)
|
||||
}
|
||||
|
||||
func isDotFile(path string) bool {
|
||||
return filepath.Base(path)[0] == '.'
|
||||
}
|
||||
|
||||
func isBackupFile(path string) bool {
|
||||
return path[len(path)-1] == '~'
|
||||
}
|
||||
|
||||
const baseFileBase = "baseof"
|
||||
|
||||
func isBaseTemplate(path string) bool {
|
||||
return strings.Contains(path, baseFileBase)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
|
||||
func (t *templateHandler) loadTemplates(absPath string, prefix string, formats output.Formats) {
|
||||
t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix)
|
||||
walker := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
@ -491,11 +423,12 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
|
|||
relPath := path[li:]
|
||||
|
||||
descriptor := output.TemplateLookupDescriptor{
|
||||
WorkingDir: workingDir,
|
||||
LayoutDir: layoutDir,
|
||||
RelPath: relPath,
|
||||
Prefix: prefix,
|
||||
Theme: t.PathSpec.Theme(),
|
||||
WorkingDir: workingDir,
|
||||
LayoutDir: layoutDir,
|
||||
RelPath: relPath,
|
||||
Prefix: prefix,
|
||||
Theme: t.PathSpec.Theme(),
|
||||
OutputFormats: formats,
|
||||
FileExists: func(filename string) (bool, error) {
|
||||
return helpers.Exists(filename, t.Fs.Source)
|
||||
},
|
||||
|
@ -511,7 +444,7 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := t.AddTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil {
|
||||
if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil {
|
||||
t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err)
|
||||
}
|
||||
|
||||
|
@ -523,16 +456,223 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) {
|
||||
t.loadTemplates(absPath, prefix)
|
||||
func (t *templateHandler) initFuncs() {
|
||||
|
||||
// The template funcs need separation between text and html templates.
|
||||
for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} {
|
||||
funcster := newTemplateFuncster(t.Deps, funcsterHolder)
|
||||
|
||||
// The URL funcs in the funcMap is somewhat language dependent,
|
||||
// so we need to wait until the language and site config is loaded.
|
||||
funcster.initFuncMap()
|
||||
|
||||
funcsterHolder.setTemplateFuncster(funcster)
|
||||
|
||||
}
|
||||
|
||||
// Amber is HTML only.
|
||||
t.amberFuncMap = template.FuncMap{}
|
||||
|
||||
amberMu.Lock()
|
||||
for k, v := range amber.FuncMap {
|
||||
t.amberFuncMap[k] = v
|
||||
}
|
||||
|
||||
for k, v := range t.html.funcster.funcMap {
|
||||
t.amberFuncMap[k] = v
|
||||
// Hacky, but we need to make sure that the func names are in the global map.
|
||||
amber.FuncMap[k] = func() string {
|
||||
panic("should never be invoked")
|
||||
}
|
||||
}
|
||||
amberMu.Unlock()
|
||||
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) LoadTemplates(absPath string) {
|
||||
t.loadTemplates(absPath, "")
|
||||
func (t *templateHandler) getTemplateHandler(name string) templateLoader {
|
||||
if strings.HasPrefix(name, textTmplNamePrefix) {
|
||||
return t.text
|
||||
}
|
||||
return t.html
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) PrintErrors() {
|
||||
for i, e := range t.errors {
|
||||
t.Log.ERROR.Println(i, ":", e.err)
|
||||
func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
|
||||
h := t.getTemplateHandler(name)
|
||||
return h.handleMaster(name, overlayFilename, masterFilename, onMissing)
|
||||
}
|
||||
|
||||
func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
|
||||
masterTpl := t.lookup(masterFilename)
|
||||
|
||||
if masterTpl == nil {
|
||||
templ, err := onMissing(masterFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
masterTpl, err = t.t.New(overlayFilename).Parse(templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
templ, err := onMissing(overlayFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The extra lookup is a workaround, see
|
||||
// * https://github.com/golang/go/issues/16101
|
||||
// * https://github.com/spf13/hugo/issues/2549
|
||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||
if err := applyTemplateTransformersToHMLTTemplate(overlayTpl); err != nil {
|
||||
return err
|
||||
}
|
||||
t.overlays[name] = overlayTpl
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
|
||||
masterTpl := t.lookup(masterFilename)
|
||||
|
||||
if masterTpl == nil {
|
||||
templ, err := onMissing(masterFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
masterTpl, err = t.t.New(overlayFilename).Parse(templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
templ, err := onMissing(overlayFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||
if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil {
|
||||
return err
|
||||
}
|
||||
t.overlays[name] = overlayTpl
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error {
|
||||
t.checkState()
|
||||
|
||||
getTemplate := func(filename string) (string, error) {
|
||||
b, err := afero.ReadFile(t.Fs.Source, filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// get the suffix and switch on that
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".amber":
|
||||
// Only HTML support for Amber
|
||||
templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html"
|
||||
b, err := afero.ReadFile(t.Fs.Source, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amberMu.Lock()
|
||||
templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName))
|
||||
amberMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return applyTemplateTransformersToHMLTTemplate(templ)
|
||||
case ".ace":
|
||||
// Only HTML support for Ace
|
||||
var innerContent, baseContent []byte
|
||||
innerContent, err := afero.ReadFile(t.Fs.Source, path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if baseTemplatePath != "" {
|
||||
baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
|
||||
default:
|
||||
|
||||
if baseTemplatePath != "" {
|
||||
return t.handleMaster(name, path, baseTemplatePath, getTemplate)
|
||||
}
|
||||
|
||||
templ, err := getTemplate(path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Log.DEBUG.Printf("Add template file from path %s", path)
|
||||
|
||||
return t.AddTemplate(name, templ)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *templateHandler) loadEmbedded() {
|
||||
t.embedShortcodes()
|
||||
t.embedTemplates()
|
||||
}
|
||||
|
||||
func (t *templateHandler) addInternalTemplate(prefix, name, tpl string) error {
|
||||
if prefix != "" {
|
||||
return t.AddTemplate("_internal/"+prefix+"/"+name, tpl)
|
||||
}
|
||||
return t.AddTemplate("_internal/"+name, tpl)
|
||||
}
|
||||
|
||||
func (t *templateHandler) addInternalShortcode(name, content string) error {
|
||||
return t.addInternalTemplate("shortcodes", name, content)
|
||||
}
|
||||
|
||||
func (t *templateHandler) checkState() {
|
||||
if t.html.clone != nil || t.text.clone != nil {
|
||||
panic("template is cloned and cannot be modfified")
|
||||
}
|
||||
}
|
||||
|
||||
func isDotFile(path string) bool {
|
||||
return filepath.Base(path)[0] == '.'
|
||||
}
|
||||
|
||||
func isBackupFile(path string) bool {
|
||||
return path[len(path)-1] == '~'
|
||||
}
|
||||
|
||||
const baseFileBase = "baseof"
|
||||
|
||||
func isBaseTemplate(path string) bool {
|
||||
return strings.Contains(path, baseFileBase)
|
||||
}
|
||||
|
|
86
tpl/tplimpl/templateFuncster.go
Normal file
86
tpl/tplimpl/templateFuncster.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
// Copyright 2017-present 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"
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
bp "github.com/spf13/hugo/bufferpool"
|
||||
|
||||
"image"
|
||||
|
||||
"github.com/spf13/hugo/deps"
|
||||
)
|
||||
|
||||
// Some of the template funcs are'nt entirely stateless.
|
||||
type templateFuncster struct {
|
||||
funcMap template.FuncMap
|
||||
cachedPartials partialCache
|
||||
image *imageHandler
|
||||
|
||||
// Make sure each funcster gets its own TemplateFinder to get
|
||||
// proper text and HTML template separation.
|
||||
Tmpl templateFuncsterTemplater
|
||||
|
||||
*deps.Deps
|
||||
}
|
||||
|
||||
func newTemplateFuncster(deps *deps.Deps, t templateFuncsterTemplater) *templateFuncster {
|
||||
return &templateFuncster{
|
||||
Deps: deps,
|
||||
Tmpl: t,
|
||||
cachedPartials: partialCache{p: make(map[string]interface{})},
|
||||
image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}},
|
||||
}
|
||||
}
|
||||
|
||||
// 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("partials/", name) {
|
||||
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 := t.Tmpl.Lookup(n)
|
||||
if templ != nil {
|
||||
b := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b)
|
||||
|
||||
if err := templ.Execute(b, context); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch t.Tmpl.(type) {
|
||||
case *htmlTemplates:
|
||||
return template.HTML(b.String()), nil
|
||||
case *textTemplates:
|
||||
return b.String(), nil
|
||||
default:
|
||||
panic("Unknown type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Partial %q not found", name)
|
||||
}
|
59
tpl/tplimpl/templateProvider.go
Normal file
59
tpl/tplimpl/templateProvider.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2017-present 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 (
|
||||
"github.com/spf13/hugo/deps"
|
||||
)
|
||||
|
||||
type TemplateProvider struct{}
|
||||
|
||||
var DefaultTemplateProvider *TemplateProvider
|
||||
|
||||
// Update updates the Hugo Template System in the provided Deps.
|
||||
// with all the additional features, templates & functions
|
||||
func (*TemplateProvider) Update(deps *deps.Deps) error {
|
||||
|
||||
newTmpl := newTemplateAdapter(deps)
|
||||
deps.Tmpl = newTmpl
|
||||
|
||||
newTmpl.initFuncs()
|
||||
newTmpl.loadEmbedded()
|
||||
|
||||
if deps.WithTemplate != nil {
|
||||
err := deps.WithTemplate(newTmpl)
|
||||
if err != nil {
|
||||
newTmpl.addError("init", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
newTmpl.MarkReady()
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Clone clones.
|
||||
func (*TemplateProvider) Clone(d *deps.Deps) error {
|
||||
|
||||
t := d.Tmpl.(*templateHandler)
|
||||
clone := t.clone(d)
|
||||
|
||||
d.Tmpl = clone
|
||||
|
||||
clone.MarkReady()
|
||||
|
||||
return nil
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"errors"
|
||||
"html/template"
|
||||
"strings"
|
||||
texttemplate "text/template"
|
||||
"text/template/parse"
|
||||
)
|
||||
|
||||
|
@ -35,32 +36,57 @@ var paramsPaths = [][]string{
|
|||
}
|
||||
|
||||
type templateContext struct {
|
||||
decl decl
|
||||
templ *template.Template
|
||||
visited map[string]bool
|
||||
decl decl
|
||||
visited map[string]bool
|
||||
lookupFn func(name string) *parse.Tree
|
||||
}
|
||||
|
||||
func (c templateContext) getIfNotVisited(name string) *template.Template {
|
||||
func (c templateContext) getIfNotVisited(name string) *parse.Tree {
|
||||
if c.visited[name] {
|
||||
return nil
|
||||
}
|
||||
c.visited[name] = true
|
||||
return c.templ.Lookup(name)
|
||||
return c.lookupFn(name)
|
||||
}
|
||||
|
||||
func newTemplateContext(templ *template.Template) *templateContext {
|
||||
return &templateContext{templ: templ, decl: make(map[string]string), visited: make(map[string]bool)}
|
||||
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
|
||||
return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)}
|
||||
|
||||
}
|
||||
|
||||
func applyTemplateTransformers(templ *template.Template) error {
|
||||
if templ == nil || templ.Tree == nil {
|
||||
func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
|
||||
return func(nn string) *parse.Tree {
|
||||
tt := templ.Lookup(nn)
|
||||
if tt != nil {
|
||||
return tt.Tree
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func applyTemplateTransformersToHMLTTemplate(templ *template.Template) error {
|
||||
return applyTemplateTransformers(templ.Tree, createParseTreeLookup(templ))
|
||||
}
|
||||
|
||||
func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error {
|
||||
return applyTemplateTransformers(templ.Tree,
|
||||
func(nn string) *parse.Tree {
|
||||
tt := templ.Lookup(nn)
|
||||
if tt != nil {
|
||||
return tt.Tree
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *parse.Tree) error {
|
||||
if templ == nil {
|
||||
return errors.New("expected template, but none provided")
|
||||
}
|
||||
|
||||
c := newTemplateContext(templ)
|
||||
c := newTemplateContext(lookupFn)
|
||||
|
||||
c.paramsKeysToLower(templ.Tree.Root)
|
||||
c.paramsKeysToLower(templ.Root)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -84,7 +110,7 @@ func (c *templateContext) paramsKeysToLower(n parse.Node) {
|
|||
case *parse.TemplateNode:
|
||||
subTempl := c.getIfNotVisited(x.Name)
|
||||
if subTempl != nil {
|
||||
c.paramsKeysToLowerForNodes(subTempl.Tree.Root)
|
||||
c.paramsKeysToLowerForNodes(subTempl.Root)
|
||||
}
|
||||
case *parse.PipeNode:
|
||||
for i, elem := range x.Decl {
|
||||
|
|
|
@ -115,13 +115,13 @@ F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }}
|
|||
func TestParamsKeysToLower(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Error(t, applyTemplateTransformers(nil))
|
||||
require.Error(t, applyTemplateTransformers(nil, nil))
|
||||
|
||||
templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
c := newTemplateContext(templ)
|
||||
c := newTemplateContext(createParseTreeLookup(templ))
|
||||
|
||||
require.Equal(t, -1, c.decl.indexOfReplacementStart([]string{}))
|
||||
|
||||
|
@ -185,7 +185,7 @@ func BenchmarkTemplateParamsKeysToLower(b *testing.B) {
|
|||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
c := newTemplateContext(templates[i])
|
||||
c := newTemplateContext(createParseTreeLookup(templates[i]))
|
||||
c.paramsKeysToLower(templ.Tree.Root)
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ Blue: {{ $__amber_1.Blue}}
|
|||
|
||||
require.NoError(t, err)
|
||||
|
||||
c := newTemplateContext(templ)
|
||||
c := newTemplateContext(createParseTreeLookup(templ))
|
||||
|
||||
c.paramsKeysToLower(templ.Tree.Root)
|
||||
|
||||
|
@ -254,7 +254,7 @@ P2: {{ .Params.LOWER }}
|
|||
require.NoError(t, err)
|
||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||
|
||||
c := newTemplateContext(overlayTpl)
|
||||
c := newTemplateContext(createParseTreeLookup(overlayTpl))
|
||||
|
||||
c.paramsKeysToLower(overlayTpl.Tree.Root)
|
||||
|
||||
|
@ -284,7 +284,7 @@ func TestTransformRecursiveTemplate(t *testing.T) {
|
|||
templ, err := template.New("foo").Parse(recursive)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := newTemplateContext(templ)
|
||||
c := newTemplateContext(createParseTreeLookup(templ))
|
||||
c.paramsKeysToLower(templ.Tree.Root)
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2017-present 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.
|
||||
|
@ -13,17 +13,12 @@
|
|||
|
||||
package tplimpl
|
||||
|
||||
type Tmpl struct {
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) EmbedShortcodes() {
|
||||
t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`)
|
||||
t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`)
|
||||
t.AddInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`)
|
||||
t.AddInternalShortcode("test.html", `This is a simple Test`)
|
||||
t.AddInternalShortcode("figure.html", `<!-- image -->
|
||||
func (t *templateHandler) embedShortcodes() {
|
||||
t.addInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`)
|
||||
t.addInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`)
|
||||
t.addInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`)
|
||||
t.addInternalShortcode("test.html", `This is a simple Test`)
|
||||
t.addInternalShortcode("figure.html", `<!-- image -->
|
||||
<figure {{ with .Get "class" }}class="{{.}}"{{ end }}>
|
||||
{{ with .Get "link"}}<a href="{{.}}">{{ end }}
|
||||
<img src="{{ .Get "src" }}" {{ if or (.Get "alt") (.Get "caption") }}alt="{{ with .Get "alt"}}{{.}}{{else}}{{ .Get "caption" }}{{ end }}" {{ end }}{{ with .Get "width" }}width="{{.}}" {{ end }}/>
|
||||
|
@ -41,8 +36,8 @@ func (t *GoHTMLTemplate) EmbedShortcodes() {
|
|||
{{ end }}
|
||||
</figure>
|
||||
<!-- image -->`)
|
||||
t.AddInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>")
|
||||
t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
|
||||
t.addInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>")
|
||||
t.addInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
|
||||
<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
|
||||
<iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}"
|
||||
{{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe>
|
||||
|
@ -51,21 +46,21 @@ func (t *GoHTMLTemplate) EmbedShortcodes() {
|
|||
<iframe src="//www.youtube.com/embed/{{ .Get 0 }}" {{ if len .Params | eq 1 }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe>
|
||||
</div>
|
||||
{{ end }}`)
|
||||
t.AddInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
|
||||
t.addInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
|
||||
<iframe src="//player.vimeo.com/video/{{ .Get "id" }}" {{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
|
||||
</div>{{ else }}
|
||||
<div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
|
||||
<iframe src="//player.vimeo.com/video/{{ .Get 0 }}" {{ if len .Params | eq 1 }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
|
||||
</div>
|
||||
{{ end }}`)
|
||||
t.AddInternalShortcode("gist.html", `<script src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`)
|
||||
t.AddInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`)
|
||||
t.AddInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1" }}{{ .html | safeHTML }}{{ end }}{{ end }}{{ else }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0" }}{{ .html | safeHTML }}{{ end }}{{ end }}`)
|
||||
t.addInternalShortcode("gist.html", `<script src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`)
|
||||
t.addInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`)
|
||||
t.addInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1" }}{{ .html | safeHTML }}{{ end }}{{ end }}{{ else }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0" }}{{ .html | safeHTML }}{{ end }}{{ end }}`)
|
||||
}
|
||||
|
||||
func (t *GoHTMLTemplate) EmbedTemplates() {
|
||||
func (t *templateHandler) embedTemplates() {
|
||||
|
||||
t.AddInternalTemplate("_default", "rss.xml", `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
t.addInternalTemplate("_default", "rss.xml", `<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>
|
||||
|
@ -92,7 +87,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
</channel>
|
||||
</rss>`)
|
||||
|
||||
t.AddInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
t.addInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
{{ range .Data.Pages }}
|
||||
<url>
|
||||
<loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
|
||||
|
@ -104,7 +99,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
</urlset>`)
|
||||
|
||||
// For multilanguage sites
|
||||
t.AddInternalTemplate("_default", "sitemapindex.xml", `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
t.addInternalTemplate("_default", "sitemapindex.xml", `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
{{ range . }}
|
||||
<sitemap>
|
||||
<loc>{{ .SitemapAbsURL }}</loc>
|
||||
|
@ -116,7 +111,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
</sitemapindex>
|
||||
`)
|
||||
|
||||
t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }}
|
||||
t.addInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }}
|
||||
{{ if gt $pag.TotalPages 1 }}
|
||||
<ul class="pagination">
|
||||
{{ with $pag.First }}
|
||||
|
@ -144,7 +139,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
</ul>
|
||||
{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
|
||||
t.addInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
|
||||
<script type="text/javascript">
|
||||
var disqus_shortname = '{{ .Site.DisqusShortname }}';
|
||||
var disqus_identifier = '{{with .GetParam "disqus_identifier" }}{{ . }}{{ else }}{{ .Permalink }}{{end}}';
|
||||
|
@ -161,7 +156,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}`)
|
||||
|
||||
// Add SEO & Social metadata
|
||||
t.AddInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" />
|
||||
t.addInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" />
|
||||
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
|
||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
||||
<meta property="og:url" content="{{ .Permalink }}" />
|
||||
|
@ -205,7 +200,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
<!-- Facebook Page Admin ID for Domain Insights -->
|
||||
{{ with .Site.Social.facebook_admin }}<meta property="fb:admins" content="{{ . }}" />{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }}
|
||||
t.addInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }}
|
||||
{{ with .Params.images }}
|
||||
<!-- Twitter summary card with large image must be at least 280x150px -->
|
||||
<meta name="twitter:card" content="summary_large_image"/>
|
||||
|
@ -223,11 +218,11 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
{{ with .twitter }}<meta name="twitter:creator" content="@{{ . }}"/>{{ end }}
|
||||
{{ end }}{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }}
|
||||
t.addInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }}
|
||||
<meta name="news_keywords" content="{{ range $i, $kw := first 10 . }}{{ if $i }},{{ end }}{{ $kw }}{{ end }}" />
|
||||
{{ end }}{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }}
|
||||
t.addInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }}
|
||||
<meta itemprop="name" content="{{ .Title }}">
|
||||
<meta itemprop="description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}">
|
||||
|
||||
|
@ -243,7 +238,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
|
|||
<meta itemprop="keywords" content="{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}" />
|
||||
{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }}
|
||||
t.addInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }}
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
|
@ -255,7 +250,7 @@ ga('send', 'pageview');
|
|||
</script>
|
||||
{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }}
|
||||
t.addInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }}
|
||||
<script>
|
||||
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
|
||||
ga('create', '{{ . }}', 'auto');
|
||||
|
@ -264,5 +259,5 @@ ga('send', 'pageview');
|
|||
<script async src='//www.google-analytics.com/analytics.js'></script>
|
||||
{{ end }}`)
|
||||
|
||||
t.AddInternalTemplate("_default", "robots.txt", "User-agent: *")
|
||||
t.addInternalTemplate("_default", "robots.txt", "User-agent: *")
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ import (
|
|||
"github.com/bep/inflect"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/hugo/deps"
|
||||
"github.com/spf13/hugo/helpers"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
|
@ -55,22 +54,6 @@ import (
|
|||
_ "image/png"
|
||||
)
|
||||
|
||||
// Some of the template funcs are'nt entirely stateless.
|
||||
type templateFuncster struct {
|
||||
funcMap template.FuncMap
|
||||
cachedPartials partialCache
|
||||
image *imageHandler
|
||||
*deps.Deps
|
||||
}
|
||||
|
||||
func newTemplateFuncster(deps *deps.Deps) *templateFuncster {
|
||||
return &templateFuncster{
|
||||
Deps: deps,
|
||||
cachedPartials: partialCache{p: make(map[string]template.HTML)},
|
||||
image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}},
|
||||
}
|
||||
}
|
||||
|
||||
// eq returns the boolean truth of arg1 == arg2.
|
||||
func eq(x, y interface{}) bool {
|
||||
normalize := func(v interface{}) interface{} {
|
||||
|
@ -1558,13 +1541,13 @@ func replace(a, b, c interface{}) (string, error) {
|
|||
// partialCache represents a cache of partials protected by a mutex.
|
||||
type partialCache struct {
|
||||
sync.RWMutex
|
||||
p map[string]template.HTML
|
||||
p map[string]interface{}
|
||||
}
|
||||
|
||||
// Get retrieves partial output from the cache based upon the partial name.
|
||||
// If the partial is not found in the cache, the partial is rendered and added
|
||||
// to the cache.
|
||||
func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) {
|
||||
func (t *templateFuncster) Get(key, name string, context interface{}) (p interface{}, err error) {
|
||||
var ok bool
|
||||
|
||||
t.cachedPartials.RLock()
|
||||
|
@ -1572,13 +1555,13 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p templat
|
|||
t.cachedPartials.RUnlock()
|
||||
|
||||
if ok {
|
||||
return p
|
||||
return
|
||||
}
|
||||
|
||||
t.cachedPartials.Lock()
|
||||
if p, ok = t.cachedPartials.p[key]; !ok {
|
||||
t.cachedPartials.Unlock()
|
||||
p = t.Tmpl.Partial(name, context)
|
||||
p, err = t.partial(name, context)
|
||||
|
||||
t.cachedPartials.Lock()
|
||||
t.cachedPartials.p[key] = p
|
||||
|
@ -1586,14 +1569,14 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p templat
|
|||
}
|
||||
t.cachedPartials.Unlock()
|
||||
|
||||
return p
|
||||
return
|
||||
}
|
||||
|
||||
// partialCached executes and caches partial templates. An optional variant
|
||||
// string parameter (a string slice actually, but be only use a variadic
|
||||
// argument to make it optional) can be passed so that a given partial can have
|
||||
// multiple uses. The cache is created with name+variant as the key.
|
||||
func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML {
|
||||
func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) (interface{}, error) {
|
||||
key := name
|
||||
if len(variant) > 0 {
|
||||
for i := 0; i < len(variant); i++ {
|
||||
|
@ -2195,7 +2178,7 @@ func (t *templateFuncster) initFuncMap() {
|
|||
"mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
|
||||
"ne": ne,
|
||||
"now": func() time.Time { return time.Now() },
|
||||
"partial": t.Tmpl.Partial,
|
||||
"partial": t.partial,
|
||||
"partialCached": t.partialCached,
|
||||
"plainify": plainify,
|
||||
"pluralize": pluralize,
|
||||
|
@ -2249,5 +2232,5 @@ func (t *templateFuncster) initFuncMap() {
|
|||
}
|
||||
|
||||
t.funcMap = funcMap
|
||||
t.Tmpl.Funcs(funcMap)
|
||||
t.Tmpl.setFuncs(funcMap)
|
||||
}
|
||||
|
|
|
@ -281,8 +281,8 @@ urlize: bat-man
|
|||
v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v))
|
||||
|
||||
config := newDepsConfig(v)
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
if _, err := templ.New("test").Parse(in); err != nil {
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
if err := templ.AddTemplate("test", in); err != nil {
|
||||
t.Fatal("Got error on parse", err)
|
||||
}
|
||||
return nil
|
||||
|
@ -2858,6 +2858,56 @@ func TestReadFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPartialHTMLAndText(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := newDepsConfig(viper.New())
|
||||
|
||||
data := struct {
|
||||
Name string
|
||||
}{
|
||||
Name: "a+b+c", // This should get encoded in HTML.
|
||||
}
|
||||
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
if err := templ.AddTemplate("htmlTemplate.html", `HTML Test Partial: {{ partial "test.foo" . -}}`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := templ.AddTemplate("_text/textTemplate.txt", `Text Test Partial: {{ partial "test.foo" . -}}`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use "foo" here to say that the extension doesn't really matter in this scenario.
|
||||
// It will look for templates in "partials/test.foo" and "partials/test.foo.html".
|
||||
if err := templ.AddTemplate("partials/test.foo", "HTML Name: {{ .Name }}"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := templ.AddTemplate("_text/partials/test.foo", "Text Name: {{ .Name }}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
de, err := deps.New(config)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, de.LoadResources())
|
||||
|
||||
templ := de.Tmpl.Lookup("htmlTemplate.html")
|
||||
require.NotNil(t, templ)
|
||||
resultHTML, err := templ.ExecuteToString(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
templ = de.Tmpl.Lookup("_text/textTemplate.txt")
|
||||
require.NotNil(t, templ)
|
||||
resultText, err := templ.ExecuteToString(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, resultHTML, "HTML Test Partial: HTML Name: a+b+c")
|
||||
require.Contains(t, resultText, "Text Test Partial: Text Name: a+b+c")
|
||||
|
||||
}
|
||||
|
||||
func TestPartialCached(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
|
@ -2893,7 +2943,7 @@ func TestPartialCached(t *testing.T) {
|
|||
|
||||
config := newDepsConfig(viper.New())
|
||||
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
err := templ.AddTemplate("testroot", tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2933,7 +2983,7 @@ func TestPartialCached(t *testing.T) {
|
|||
|
||||
func BenchmarkPartial(b *testing.B) {
|
||||
config := newDepsConfig(viper.New())
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -2965,7 +3015,7 @@ func BenchmarkPartial(b *testing.B) {
|
|||
|
||||
func BenchmarkPartialCached(b *testing.B) {
|
||||
config := newDepsConfig(viper.New())
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -3010,12 +3060,12 @@ func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
return d.Tmpl.(*GoHTMLTemplate).funcster
|
||||
return d.Tmpl.(*templateHandler).html.funcster
|
||||
}
|
||||
|
||||
func newTestTemplate(t *testing.T, name, template string) *template.Template {
|
||||
func newTestTemplate(t *testing.T, name, template string) tpl.Template {
|
||||
config := newDepsConfig(viper.New())
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
err := templ.AddTemplate(name, template)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -14,17 +14,10 @@
|
|||
package tplimpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/hugo/deps"
|
||||
|
||||
"github.com/spf13/hugo/tpl"
|
||||
|
@ -32,223 +25,6 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Some tests for Issue #1178 -- Ace
|
||||
func TestAceTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for i, this := range []struct {
|
||||
basePath string
|
||||
innerPath string
|
||||
baseContent string
|
||||
innerContent string
|
||||
expect string
|
||||
expectErr int
|
||||
}{
|
||||
{"", filepath.FromSlash("_default/single.ace"), "", "{{ . }}", "DATA", 0},
|
||||
{filepath.FromSlash("_default/baseof.ace"), filepath.FromSlash("_default/single.ace"),
|
||||
`= content main
|
||||
h2 This is a content named "main" of an inner template. {{ . }}`,
|
||||
`= doctype html
|
||||
html lang=en
|
||||
head
|
||||
meta charset=utf-8
|
||||
title Base and Inner Template
|
||||
body
|
||||
h1 This is a base template {{ . }}
|
||||
= yield main`, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Base and Inner Template</title></head><body><h1>This is a base template DATA</h1></body></html>`, 0},
|
||||
} {
|
||||
|
||||
for _, root := range []string{"", os.TempDir()} {
|
||||
|
||||
basePath := this.basePath
|
||||
innerPath := this.innerPath
|
||||
|
||||
if basePath != "" && root != "" {
|
||||
basePath = filepath.Join(root, basePath)
|
||||
}
|
||||
|
||||
if innerPath != "" && root != "" {
|
||||
innerPath = filepath.Join(root, innerPath)
|
||||
}
|
||||
|
||||
d := "DATA"
|
||||
|
||||
config := newDepsConfig(viper.New())
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath,
|
||||
[]byte(this.baseContent), []byte(this.innerContent))
|
||||
}
|
||||
|
||||
a, err := deps.New(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
if err := a.LoadResources(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
templ := a.Tmpl.(*GoHTMLTemplate)
|
||||
|
||||
if len(templ.errors) > 0 && this.expectErr == 0 {
|
||||
t.Errorf("Test %d with root '%s' errored: %v", i, root, templ.errors)
|
||||
} else if len(templ.errors) == 0 && this.expectErr == 1 {
|
||||
t.Errorf("#1 Test %d with root '%s' should have errored", i, root)
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
err = a.Tmpl.ExecuteTemplate(&buff, "mytemplate.html", d)
|
||||
|
||||
if err != nil && this.expectErr == 0 {
|
||||
t.Errorf("Test %d with root '%s' errored: %s", i, root, err)
|
||||
} else if err == nil && this.expectErr == 2 {
|
||||
t.Errorf("#2 Test with root '%s' %d should have errored", root, i)
|
||||
} else {
|
||||
result := buff.String()
|
||||
if result != this.expect {
|
||||
t.Errorf("Test %d with root '%s' got\n%s\nexpected\n%s", i, root, result, this.expect)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func isAtLeastGo16() bool {
|
||||
version := runtime.Version()
|
||||
return strings.Contains(version, "1.6") || strings.Contains(version, "1.7")
|
||||
}
|
||||
|
||||
func TestAddTemplateFileWithMaster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !isAtLeastGo16() {
|
||||
t.Skip("This test only runs on Go >= 1.6")
|
||||
}
|
||||
|
||||
for i, this := range []struct {
|
||||
masterTplContent string
|
||||
overlayTplContent string
|
||||
writeSkipper int
|
||||
expect interface{}
|
||||
}{
|
||||
{`A{{block "main" .}}C{{end}}C`, `{{define "main"}}B{{end}}`, 0, "ABC"},
|
||||
{`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}`, 0, "ABCDE"},
|
||||
{`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}{{define "sub"}}Z{{end}}`, 0, "ABCZE"},
|
||||
{`tpl`, `tpl`, 1, false},
|
||||
{`tpl`, `tpl`, 2, false},
|
||||
{`{{.0.E}}`, `tpl`, 0, false},
|
||||
{`tpl`, `{{.0.E}}`, 0, false},
|
||||
} {
|
||||
|
||||
overlayTplName := "ot"
|
||||
masterTplName := "mt"
|
||||
finalTplName := "tp"
|
||||
|
||||
config := newDepsConfig(viper.New())
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
|
||||
err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName)
|
||||
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i)
|
||||
}
|
||||
} else {
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
resultTpl := templ.Lookup(finalTplName)
|
||||
|
||||
if resultTpl == nil {
|
||||
t.Errorf("[%d] AddTemplateFileWithMaster: Result template not found", i)
|
||||
return nil
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
err := resultTpl.Execute(&b, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err)
|
||||
return nil
|
||||
}
|
||||
resultContent := b.String()
|
||||
|
||||
if resultContent != this.expect {
|
||||
t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if this.writeSkipper != 1 {
|
||||
afero.WriteFile(config.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644)
|
||||
}
|
||||
if this.writeSkipper != 2 {
|
||||
afero.WriteFile(config.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644)
|
||||
}
|
||||
|
||||
deps.New(config)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// A Go stdlib test for linux/arm. Will remove later.
|
||||
// See #1771
|
||||
func TestBigIntegerFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
var func1 = func(v int64) error {
|
||||
return nil
|
||||
}
|
||||
var funcs = map[string]interface{}{
|
||||
"A": func1,
|
||||
}
|
||||
|
||||
tpl, err := template.New("foo").Funcs(funcs).Parse("{{ A 3e80 }}")
|
||||
if err != nil {
|
||||
t.Fatal("Parse failed:", err)
|
||||
}
|
||||
err = tpl.Execute(ioutil.Discard, "foo")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Execute should have failed")
|
||||
}
|
||||
|
||||
t.Log("Got expected error:", err)
|
||||
|
||||
}
|
||||
|
||||
// A Go stdlib test for linux/arm. Will remove later.
|
||||
// See #1771
|
||||
type BI struct {
|
||||
}
|
||||
|
||||
func (b BI) A(v int64) error {
|
||||
return nil
|
||||
}
|
||||
func TestBigIntegerMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := &BI{}
|
||||
|
||||
tpl, err := template.New("foo2").Parse("{{ .A 3e80 }}")
|
||||
if err != nil {
|
||||
t.Fatal("Parse failed:", err)
|
||||
}
|
||||
err = tpl.ExecuteTemplate(ioutil.Discard, "foo2", data)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Execute should have failed")
|
||||
}
|
||||
|
||||
t.Log("Got expected error:", err)
|
||||
|
||||
}
|
||||
|
||||
// Test for bugs discovered by https://github.com/dvyukov/go-fuzz
|
||||
func TestTplGoFuzzReports(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -285,7 +61,7 @@ func TestTplGoFuzzReports(t *testing.T) {
|
|||
|
||||
config := newDepsConfig(viper.New())
|
||||
|
||||
config.WithTemplate = func(templ tpl.Template) error {
|
||||
config.WithTemplate = func(templ tpl.TemplateHandler) error {
|
||||
return templ.AddTemplate("fuzz", this.data)
|
||||
}
|
||||
|
||||
|
@ -293,7 +69,7 @@ func TestTplGoFuzzReports(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.NoError(t, de.LoadResources())
|
||||
|
||||
templ := de.Tmpl.(*GoHTMLTemplate)
|
||||
templ := de.Tmpl.(*templateHandler)
|
||||
|
||||
if len(templ.errors) > 0 && this.expectErr == 0 {
|
||||
t.Errorf("Test %d errored: %v", i, templ.errors)
|
||||
|
@ -301,7 +77,9 @@ func TestTplGoFuzzReports(t *testing.T) {
|
|||
t.Errorf("#1 Test %d should have errored", i)
|
||||
}
|
||||
|
||||
err = de.Tmpl.ExecuteTemplate(ioutil.Discard, "fuzz", d)
|
||||
tt := de.Tmpl.Lookup("fuzz")
|
||||
require.NotNil(t, tt)
|
||||
err = tt.Execute(ioutil.Discard, d)
|
||||
|
||||
if err != nil && this.expectErr == 0 {
|
||||
t.Fatalf("Test %d errored: %s", i, err)
|
||||
|
|
Loading…
Reference in a new issue