Skip to content
Permalink
Browse files

tpl: Allow the partial template func to return any type

This commit adds support for return values in partials.

This means that you can now do this and similar:

    {{ $v := add . 42 }}
    {{ return $v }}

Partials without a `return` statement will be rendered as before.

This works for both `partial` and `partialCached`.

Fixes #5783
  • Loading branch information...
bep committed Apr 2, 2019
1 parent 9225db6 commit a55640de8e3944d3b9f64b15155148a0e35cb31e
@@ -20,6 +20,12 @@ type Eqer interface {
Eq(other interface{}) bool
}

// ProbablyEq is an equal check that may return false positives, but never
// a false negative.
type ProbablyEqer interface {
ProbablyEq(other interface{}) bool
}

// Comparer can be used to compare two values.
// This will be used when using the le, ge etc. operators in the templates.
// Compare returns -1 if the given version is less than, 0 if equal and 1 if greater than
@@ -264,3 +264,44 @@ Hugo: {{ hugo.Generator }}
)

}

func TestPartialWithReturn(t *testing.T) {

b := newTestSitesBuilder(t).WithSimpleConfigFile()

b.WithTemplatesAdded(
"index.html", `
Test Partials With Return Values:
add42: 50: {{ partial "add42.tpl" 8 }}
dollarContext: 60: {{ partial "dollarContext.tpl" 18 }}
adder: 70: {{ partial "dict.tpl" (dict "adder" 28) }}
complex: 80: {{ partial "complex.tpl" 38 }}
`,
"partials/add42.tpl", `
{{ $v := add . 42 }}
{{ return $v }}
`,
"partials/dollarContext.tpl", `
{{ $v := add $ 42 }}
{{ return $v }}
`,
"partials/dict.tpl", `
{{ $v := add $.adder 42 }}
{{ return $v }}
`,
"partials/complex.tpl", `
{{ return add . 42 }}
`,
)

b.CreateSites().Build(BuildCfg{})

b.AssertFileContent("public/index.html",
"add42: 50: 50",
"dollarContext: 60: 60",
"adder: 70: 70",
"complex: 80: 80",
)

}
@@ -23,6 +23,12 @@ import (
"strings"
"sync"
"time"

"github.com/gohugoio/hugo/compare"

"github.com/gohugoio/hugo/common/hreflect"

"github.com/spf13/cast"
)

// The Provider interface defines an interface for measuring metrics.
@@ -35,20 +41,20 @@ type Provider interface {
WriteMetrics(w io.Writer)

// TrackValue tracks the value for diff calculations etc.
TrackValue(key, value string)
TrackValue(key string, value interface{})

// Reset clears the metric store.
Reset()
}

type diff struct {
baseline string
baseline interface{}
count int
simSum int
}

func (d *diff) add(v string) *diff {
if d.baseline == "" {
func (d *diff) add(v interface{}) *diff {
if !hreflect.IsTruthful(v) {
d.baseline = v
d.count = 1
d.simSum = 100 // If we get only one it is very cache friendly.
@@ -90,7 +96,7 @@ func (s *Store) Reset() {
}

// TrackValue tracks the value for diff calculations etc.
func (s *Store) TrackValue(key, value string) {
func (s *Store) TrackValue(key string, value interface{}) {
if !s.calculateHints {
return
}
@@ -191,13 +197,43 @@ func (b bySum) Less(i, j int) bool { return b[i].sum > b[j].sum }

// howSimilar is a naive diff implementation that returns
// a number between 0-100 indicating how similar a and b are.
// 100 is when all words in a also exists in b.
func howSimilar(a, b string) int {

func howSimilar(a, b interface{}) int {
if a == b {
return 100
}

as, err1 := cast.ToStringE(a)
bs, err2 := cast.ToStringE(b)

if err1 == nil && err2 == nil {
return howSimilarStrings(as, bs)
}

if err1 != err2 {
return 0
}

e1, ok1 := a.(compare.Eqer)
e2, ok2 := b.(compare.Eqer)
if ok1 && ok2 && e1.Eq(e2) {
return 100
}

// TODO(bep) implement ProbablyEq for Pages etc.
pe1, pok1 := a.(compare.ProbablyEqer)
pe2, pok2 := b.(compare.ProbablyEqer)
if pok1 && pok2 && pe1.ProbablyEq(pe2) {
return 90
}

return 0
}

// howSimilar is a naive diff implementation that returns
// a number between 0-100 indicating how similar a and b are.
// 100 is when all words in a also exists in b.
func howSimilarStrings(a, b string) int {

// Give some weight to the word positions.
const partitionSize = 4

@@ -36,6 +36,13 @@ func init() {
},
)

// TODO(bep) we need the return to be a valid identifier, but
// should consider another way of adding it.
ns.AddMethodMapping(func() string { return "" },
[]string{"return"},
[][2]string{},
)

ns.AddMethodMapping(ctx.IncludeCached,
[]string{"partialCached"},
[][2]string{},
@@ -18,10 +18,14 @@ package partials
import (
"fmt"
"html/template"
"io"
"io/ioutil"
"strings"
"sync"
texttemplate "text/template"

"github.com/gohugoio/hugo/tpl"

bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/deps"
)
@@ -62,8 +66,22 @@ type Namespace struct {
cachedPartials *partialCache
}

// Include executes the named partial and returns either a string,
// when the partial is a text/template, or template.HTML when html/template.
// contextWrapper makes room for a return value in a partial invocation.
type contextWrapper struct {
Arg interface{}
Result interface{}
}

// Set sets the return value and returns an empty string.
func (c *contextWrapper) Set(in interface{}) string {
c.Result = in
return ""
}

// Include executes the named partial.
// If the partial contains a return statement, that value will be returned.
// Else, the rendered output will be returned:
// A string if the partial is a text/template, or template.HTML when html/template.
func (ns *Namespace) Include(name string, contextList ...interface{}) (interface{}, error) {
if strings.HasPrefix(name, "partials/") {
name = name[8:]
@@ -83,31 +101,54 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface
// For legacy reasons.
templ, found = ns.deps.Tmpl.Lookup(n + ".html")
}
if found {

if !found {
return "", fmt.Errorf("partial %q not found", name)
}

var info tpl.Info
if ip, ok := templ.(tpl.TemplateInfoProvider); ok {
info = ip.TemplateInfo()
}

var w io.Writer

if info.HasReturn {
// Wrap the context sent to the template to capture the return value.
// Note that the template is rewritten to make sure that the dot (".")
// and the $ variable points to Arg.
context = &contextWrapper{
Arg: context,
}

// We don't care about any template output.
w = ioutil.Discard
} else {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
w = b
}

if err := templ.Execute(b, context); err != nil {
return "", err
}
if err := templ.Execute(w, context); err != nil {
return "", err
}

if _, ok := templ.(*texttemplate.Template); ok {
s := b.String()
if ns.deps.Metrics != nil {
ns.deps.Metrics.TrackValue(n, s)
}
return s, nil
}
var result interface{}

s := b.String()
if ns.deps.Metrics != nil {
ns.deps.Metrics.TrackValue(n, s)
}
return template.HTML(s), nil
if ctx, ok := context.(*contextWrapper); ok {
result = ctx.Result
} else if _, ok := templ.(*texttemplate.Template); ok {
result = w.(fmt.Stringer).String()
} else {
result = template.HTML(w.(fmt.Stringer).String())
}

if ns.deps.Metrics != nil {
ns.deps.Metrics.TrackValue(n, result)
}

return "", fmt.Errorf("partial %q not found", name)
return result, nil

}

// IncludeCached executes and caches partial templates. An optional variant
@@ -22,10 +22,17 @@ type Info struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool

// Set for partials with a return statement.
HasReturn bool

// Config extracted from template.
Config Config
}

func (info Info) IsZero() bool {
return info.Config.Version == 0
}

type Config struct {
Version int
}
@@ -51,15 +51,17 @@ func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseC
return err
}

isShort := isShortcode(name)
typ := resolveTemplateType(name)

info, err := applyTemplateTransformersToHMLTTemplate(isShort, templ)
info, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
if err != nil {
return err
}

if isShort {
if typ == templateShortcode {
t.addShortcodeVariant(name, info, templ)
} else {
t.templateInfo[name] = info
}

return nil
@@ -139,6 +139,18 @@ func templateNameAndVariants(name string) (string, []string) {
return name, variants
}

func resolveTemplateType(name string) templateType {
if isShortcode(name) {
return templateShortcode
}

if strings.Contains(name, "partials/") {
return templatePartial
}

return templateUndefined
}

func isShortcode(name string) bool {
return strings.Contains(name, "shortcodes/")
}
Oops, something went wrong.

0 comments on commit a55640d

Please sign in to comment.
You can’t perform that action at this time.