Hugo: Add template funcs namespaces

Created on 16 Feb 2017  路  6Comments  路  Source: gohugoio/hugo

See https://github.com/spf13/hugo/issues/3040

I don't mind adding more template funcs, but we should have some kind of name spacing to make them easier to manage/document, i.e. strings.trimPrefix etc. Not sure how the "." works in this case, but then find something else ("_"?)

We should keep the "old" names as aliases, of course, but this will lead to more natural names like:

  • image.config
  • image.resize
  • etc.

@moorereason @digitalcraftsman

Enhancement

Most helpful comment

I made a proof of concept locally that allows strings.TrimPrefix. The trick is to create a func that returns a struct which has the namespace funcs defined on it:

func stringsNS() *stringsNamespace {
    // should probably use a global instead of a new instance
    return &stringsNamespace{}
}

type stringsNamespace struct{}

// TrimPrefix returns s without the provided leading prefix string. If s doesn't
// start with prefix, s is returned unchanged.
func (_ *stringsNamespace) TrimPrefix(prefix, s interface{}) (string, error) {
    p, err := cast.ToStringE(prefix)
    if err != nil {
        return "", err
    }

    ss, err := cast.ToStringE(s)
    if err != nil {
        return "", err
    }

    return strings.TrimPrefix(ss, p), nil
}

// funcMap := template.FuncMap{
//     "strings":  stringsNS,

Usage:

{{ strings.TrimPrefix "$" "$BatMan" }}
{{ "$BatMan" | strings.TrimPrefix "$" }}

So, we can accomplish what we want. We just need to come up with a more formal proposal to redo the funcmap with namespaces.

All 6 comments

I think it's way easier to have fewer functions, but with the namespaces to add more options. Thumbs up for this issue!

I made a proof of concept locally that allows strings.TrimPrefix. The trick is to create a func that returns a struct which has the namespace funcs defined on it:

func stringsNS() *stringsNamespace {
    // should probably use a global instead of a new instance
    return &stringsNamespace{}
}

type stringsNamespace struct{}

// TrimPrefix returns s without the provided leading prefix string. If s doesn't
// start with prefix, s is returned unchanged.
func (_ *stringsNamespace) TrimPrefix(prefix, s interface{}) (string, error) {
    p, err := cast.ToStringE(prefix)
    if err != nil {
        return "", err
    }

    ss, err := cast.ToStringE(s)
    if err != nil {
        return "", err
    }

    return strings.TrimPrefix(ss, p), nil
}

// funcMap := template.FuncMap{
//     "strings":  stringsNS,

Usage:

{{ strings.TrimPrefix "$" "$BatMan" }}
{{ "$BatMan" | strings.TrimPrefix "$" }}

So, we can accomplish what we want. We just need to come up with a more formal proposal to redo the funcmap with namespaces.

Adding namespaces is a good idea if we add more functionality to the templates. The docs already loosely groups template functions by "namespaces" / usecases.

However, do you want to put every function in a namespace, even standard ones like div in math.div?

How do we handle this redundancy of template functions from a long-term perspective?

Will we depecrate them (not immediately, but one day in the future) or do we continue with this double-barreled approach? Deprecating them would be affect nearly every Hugo site.

While introducing namespaces, should we document the functions by namespaces and just mention that the aliases still exist? Or will it be done vice versa by leaving the docs as they are and note that the usage of namespaces if preferred/advised?

How do we handle this redundancy of template functions from a long-term perspective?

I think we leave the once we have as aliases -- some of them may make sense to deprecate at some point, but some of them is obviously good as "short form" (where being one example).

When we redo the docs, we might put emphasis on the "namespaced" versions.

  • For future template funcs, they will be put in its namespace and only there
  • If it is in the "where league", it may get a shortform alias

Here is the suggested game plan:

  • Add an interface in /tpl, say:

```go
interface TplFuncsNamespacer {
NewNamespace(d *deps.Deps) TplFuncsNameSpace // name + interface{} + global TemplateFuncs aliases
}
````

Then

  • Create a package below /tpl for each logical unit (if we wrap funcs from the Go stdlib, we should name them the same), so /tpl/strings /tpl/math etc.
  • Add a top level NamespaceRegister and add to that register from each package's init() func.
  • Do a blank import of those packages etc. from /tplimpl

Related discussion: https://discuss.gohugo.io/t/5800

Was this page helpful?
0 / 5 - 0 ratings