From dea71670c059ab4d5a42bd22503f18c087dd22d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 20 Feb 2018 10:02:14 +0100 Subject: Add Hugo Piper with SCSS support and much more Before this commit, you would have to use page bundles to do image processing etc. in Hugo. This commit adds * A new `/assets` top-level project or theme dir (configurable via `assetDir`) * A new template func, `resources.Get` which can be used to "get a resource" that can be further processed. This means that you can now do this in your templates (or shortcodes): ```bash {{ $sunset := (resources.Get "images/sunset.jpg").Fill "300x200" }} ``` This also adds a new `extended` build tag that enables powerful SCSS/SASS support with source maps. To compile this from source, you will also need a C compiler installed: ``` HUGO_BUILD_TAGS=extended mage install ``` Note that you can use output of the SCSS processing later in a non-SCSSS-enabled Hugo. The `SCSS` processor is a _Resource transformation step_ and it can be chained with the many others in a pipeline: ```bash {{ $css := resources.Get "styles.scss" | resources.ToCSS | resources.PostCSS | resources.Minify | resources.Fingerprint }} ``` The transformation funcs above have aliases, so it can be shortened to: ```bash {{ $css := resources.Get "styles.scss" | toCSS | postCSS | minify | fingerprint }} ``` A quick tip would be to avoid the fingerprinting part, and possibly also the not-superfast `postCSS` when you're doing development, as it allows Hugo to be smarter about the rebuilding. Documentation will follow, but have a look at the demo repo in https://github.com/bep/hugo-sass-test New functions to create `Resource` objects: * `resources.Get` (see above) * `resources.FromString`: Create a Resource from a string. New `Resource` transformation funcs: * `resources.ToCSS`: Compile `SCSS` or `SASS` into `CSS`. * `resources.PostCSS`: Process your CSS with PostCSS. Config file support (project or theme or passed as an option). * `resources.Minify`: Currently supports `css`, `js`, `json`, `html`, `svg`, `xml`. * `resources.Fingerprint`: Creates a fingerprinted version of the given Resource with Subresource Integrity.. * `resources.Concat`: Concatenates a list of Resource objects. Think of this as a poor man's bundler. * `resources.ExecuteAsTemplate`: Parses and executes the given Resource and data context (e.g. .Site) as a Go template. Fixes #4381 Fixes #4903 Fixes #4858 --- tpl/collections/apply_test.go | 4 +- tpl/os/init.go | 4 +- tpl/os/os.go | 2 +- tpl/partials/partials.go | 11 +- tpl/resources/init.go | 68 ++++++++++ tpl/resources/resources.go | 255 +++++++++++++++++++++++++++++++++++++ tpl/template.go | 15 ++- tpl/tplimpl/template.go | 97 ++++++++++---- tpl/tplimpl/templateFuncster.go | 10 +- tpl/tplimpl/templateProvider.go | 2 + tpl/tplimpl/template_funcs.go | 12 +- tpl/tplimpl/template_funcs_test.go | 9 +- tpl/tplimpl/template_test.go | 11 +- 13 files changed, 452 insertions(+), 48 deletions(-) create mode 100644 tpl/resources/init.go create mode 100644 tpl/resources/resources.go (limited to 'tpl') diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go index de24b06c8..0878844b2 100644 --- a/tpl/collections/apply_test.go +++ b/tpl/collections/apply_test.go @@ -25,8 +25,8 @@ import ( type templateFinder int -func (templateFinder) Lookup(name string) *tpl.TemplateAdapter { - return nil +func (templateFinder) Lookup(name string) (tpl.Template, bool) { + return nil, false } func (templateFinder) GetFuncs() map[string]interface{} { diff --git a/tpl/os/init.go b/tpl/os/init.go index 012f43b1f..3ef8702d6 100644 --- a/tpl/os/init.go +++ b/tpl/os/init.go @@ -37,14 +37,14 @@ func init() { ns.AddMethodMapping(ctx.ReadDir, []string{"readDir"}, [][2]string{ - {`{{ range (readDir ".") }}{{ .Name }}{{ end }}`, "README.txt"}, + {`{{ range (readDir "files") }}{{ .Name }}{{ end }}`, "README.txt"}, }, ) ns.AddMethodMapping(ctx.ReadFile, []string{"readFile"}, [][2]string{ - {`{{ readFile "README.txt" }}`, `Hugo Rocks!`}, + {`{{ readFile "files/README.txt" }}`, `Hugo Rocks!`}, }, ) diff --git a/tpl/os/os.go b/tpl/os/os.go index f7f9537ff..79d035d7e 100644 --- a/tpl/os/os.go +++ b/tpl/os/os.go @@ -34,7 +34,7 @@ func New(deps *deps.Deps) *Namespace { if deps.Fs != nil { rfs = deps.Fs.WorkingDir if deps.PathSpec != nil && deps.PathSpec.BaseFs != nil { - rfs = afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(deps.PathSpec.BaseFs.ContentFs, deps.Fs.WorkingDir)) + rfs = afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(deps.PathSpec.BaseFs.Content.Fs, deps.Fs.WorkingDir)) } } diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index beb09f426..18b8d7ed6 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -63,12 +63,13 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface } for _, n := range []string{"partials/" + name, "theme/partials/" + name} { - templ := ns.deps.Tmpl.Lookup(n) - if templ == nil { + templ, found := ns.deps.Tmpl.Lookup(n) + + if !found { // For legacy reasons. - templ = ns.deps.Tmpl.Lookup(n + ".html") + templ, found = ns.deps.Tmpl.Lookup(n + ".html") } - if templ != nil { + if found { b := bp.GetBuffer() defer bp.PutBuffer(b) @@ -76,7 +77,7 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface return "", err } - if _, ok := templ.Template.(*texttemplate.Template); ok { + if _, ok := templ.(*texttemplate.Template); ok { s := b.String() if ns.deps.Metrics != nil { ns.deps.Metrics.TrackValue(n, s) diff --git a/tpl/resources/init.go b/tpl/resources/init.go new file mode 100644 index 000000000..3e750f325 --- /dev/null +++ b/tpl/resources/init.go @@ -0,0 +1,68 @@ +// Copyright 2018 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 resources + +import ( + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" +) + +const name = "resources" + +func init() { + f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { + ctx, err := New(d) + if err != nil { + // TODO(bep) no panic. + panic(err) + } + + ns := &internal.TemplateFuncsNamespace{ + Name: name, + Context: func(args ...interface{}) interface{} { return ctx }, + } + + ns.AddMethodMapping(ctx.Get, + nil, + [][2]string{}, + ) + + // Add aliases for the most common transformations. + + ns.AddMethodMapping(ctx.Fingerprint, + []string{"fingerprint"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.Minify, + []string{"minify"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.ToCSS, + []string{"toCSS"}, + [][2]string{}, + ) + + ns.AddMethodMapping(ctx.PostCSS, + []string{"postCSS"}, + [][2]string{}, + ) + + return ns + + } + + internal.AddTemplateFuncsNamespace(f) +} diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go new file mode 100644 index 000000000..5d4f6e315 --- /dev/null +++ b/tpl/resources/resources.go @@ -0,0 +1,255 @@ +// Copyright 2018 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 resources + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/resource" + "github.com/gohugoio/hugo/resource/bundler" + "github.com/gohugoio/hugo/resource/create" + "github.com/gohugoio/hugo/resource/integrity" + "github.com/gohugoio/hugo/resource/minifiers" + "github.com/gohugoio/hugo/resource/postcss" + "github.com/gohugoio/hugo/resource/templates" + "github.com/gohugoio/hugo/resource/tocss/scss" + "github.com/spf13/cast" +) + +// New returns a new instance of the resources-namespaced template functions. +func New(deps *deps.Deps) (*Namespace, error) { + scssClient, err := scss.New(deps.BaseFs.Assets, deps.ResourceSpec) + if err != nil { + return nil, err + } + return &Namespace{ + deps: deps, + scssClient: scssClient, + createClient: create.New(deps.ResourceSpec), + bundlerClient: bundler.New(deps.ResourceSpec), + integrityClient: integrity.New(deps.ResourceSpec), + minifyClient: minifiers.New(deps.ResourceSpec), + postcssClient: postcss.New(deps.ResourceSpec), + templatesClient: templates.New(deps.ResourceSpec, deps.TextTmpl), + }, nil +} + +// Namespace provides template functions for the "resources" namespace. +type Namespace struct { + deps *deps.Deps + + createClient *create.Client + bundlerClient *bundler.Client + scssClient *scss.Client + integrityClient *integrity.Client + minifyClient *minifiers.Client + postcssClient *postcss.Client + templatesClient *templates.Client +} + +// Get locates the filename given in Hugo's filesystems: static, assets and content (in that order) +// and creates a Resource object that can be used for further transformations. +func (ns *Namespace) Get(filename interface{}) (resource.Resource, error) { + filenamestr, err := cast.ToStringE(filename) + if err != nil { + return nil, err + } + + filenamestr = filepath.Clean(filenamestr) + + // Resource Get'ing is currently limited to /assets to make it simpler + // to control the behaviour of publishing and partial rebuilding. + return ns.createClient.Get(ns.deps.BaseFs.Assets.Fs, filenamestr) + +} + +// Concat concatenates a slice of Resource objects. These resources must +// (currently) be of the same Media Type. +func (ns *Namespace) Concat(targetPathIn interface{}, r []interface{}) (resource.Resource, error) { + targetPath, err := cast.ToStringE(targetPathIn) + if err != nil { + return nil, err + } + rr := make([]resource.Resource, len(r)) + for i := 0; i < len(r); i++ { + rv, ok := r[i].(resource.Resource) + if !ok { + return nil, fmt.Errorf("cannot concat type %T", rv) + } + rr[i] = rv + } + return ns.bundlerClient.Concat(targetPath, rr) +} + +// FromString creates a Resource from a string published to the relative target path. +func (ns *Namespace) FromString(targetPathIn, contentIn interface{}) (resource.Resource, error) { + targetPath, err := cast.ToStringE(targetPathIn) + if err != nil { + return nil, err + } + content, err := cast.ToStringE(contentIn) + if err != nil { + return nil, err + } + + return ns.createClient.FromString(targetPath, content) +} + +// ExecuteAsTemplate creates a Resource from a Go template, parsed and executed with +// the given data, and published to the relative target path. +func (ns *Namespace) ExecuteAsTemplate(args ...interface{}) (resource.Resource, error) { + if len(args) != 3 { + return nil, fmt.Errorf("must provide targetPath, the template data context and a Resource object") + } + targetPath, err := cast.ToStringE(args[0]) + if err != nil { + return nil, err + } + data := args[1] + + r, ok := args[2].(resource.Resource) + if !ok { + return nil, fmt.Errorf("type %T not supported in Resource transformations", args[2]) + } + + return ns.templatesClient.ExecuteAsTemplate(r, targetPath, data) +} + +// Fingerprint transforms the given Resource with a MD5 hash of the content in +// the RelPermalink and Permalink. +func (ns *Namespace) Fingerprint(args ...interface{}) (resource.Resource, error) { + if len(args) < 1 || len(args) > 2 { + return nil, errors.New("must provide a Resource and (optional) crypto algo") + } + + var algo string + resIdx := 0 + + if len(args) == 2 { + resIdx = 1 + var err error + algo, err = cast.ToStringE(args[0]) + if err != nil { + return nil, err + } + } + + r, ok := args[resIdx].(resource.Resource) + if !ok { + return nil, fmt.Errorf("%T is not a Resource", args[resIdx]) + } + + return ns.integrityClient.Fingerprint(r, algo) +} + +// Minify minifies the given Resource using the MediaType to pick the correct +// minifier. +func (ns *Namespace) Minify(r resource.Resource) (resource.Resource, error) { + return ns.minifyClient.Minify(r) +} + +// ToCSS converts the given Resource to CSS. You can optional provide an Options +// object or a target path (string) as first argument. +func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) { + var ( + r resource.Resource + m map[string]interface{} + targetPath string + err error + ok bool + ) + + r, targetPath, ok = ns.resolveIfFirstArgIsString(args) + + if !ok { + r, m, err = ns.resolveArgs(args) + if err != nil { + return nil, err + } + } + + var options scss.Options + if targetPath != "" { + options.TargetPath = targetPath + } else if m != nil { + options, err = scss.DecodeOptions(m) + if err != nil { + return nil, err + } + } + + return ns.scssClient.ToCSS(r, options) +} + +// PostCSS processes the given Resource with PostCSS +func (ns *Namespace) PostCSS(args ...interface{}) (resource.Resource, error) { + r, m, err := ns.resolveArgs(args) + if err != nil { + return nil, err + } + var options postcss.Options + if m != nil { + options, err = postcss.DecodeOptions(m) + if err != nil { + return nil, err + } + } + + return ns.postcssClient.Process(r, options) +} + +// We allow string or a map as the first argument in some cases. +func (ns *Namespace) resolveIfFirstArgIsString(args []interface{}) (resource.Resource, string, bool) { + if len(args) != 2 { + return nil, "", false + } + + v1, ok1 := args[0].(string) + if !ok1 { + return nil, "", false + } + v2, ok2 := args[1].(resource.Resource) + + return v2, v1, ok2 +} + +// This roundabout way of doing it is needed to get both pipeline behaviour and options as arguments. +func (ns *Namespace) resolveArgs(args []interface{}) (resource.Resource, map[string]interface{}, error) { + if len(args) == 0 { + return nil, nil, errors.New("no Resource provided in transformation") + } + + if len(args) == 1 { + r, ok := args[0].(resource.Resource) + if !ok { + return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0]) + } + return r, nil, nil + } + + r, ok := args[1].(resource.Resource) + if !ok { + return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0]) + } + + m, err := cast.ToStringMapE(args[0]) + if err != nil { + return nil, nil, fmt.Errorf("invalid options type: %s", err) + } + + return r, m, nil +} diff --git a/tpl/template.go b/tpl/template.go index e04d2cc6c..2cef92bb2 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -38,13 +38,15 @@ type TemplateHandler interface { LoadTemplates(prefix string) PrintErrors() + NewTextTemplate() TemplateParseFinder + MarkReady() RebuildClone() } // TemplateFinder finds templates. type TemplateFinder interface { - Lookup(name string) *TemplateAdapter + Lookup(name string) (Template, bool) } // Template is the common interface between text/template and html/template. @@ -53,6 +55,17 @@ type Template interface { Name() string } +// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain. +type TemplateParser interface { + Parse(name, tpl string) (Template, error) +} + +// TemplateParseFinder provides both parsing and finding. +type TemplateParseFinder interface { + TemplateParser + TemplateFinder +} + // TemplateExecutor adds some extras to Template. type TemplateExecutor interface { Template diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index e838ebc57..f19c312ec 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -55,7 +55,7 @@ var ( _ templateFuncsterTemplater = (*textTemplates)(nil) ) -// Protecting global map access (Amber) +// Protecting global map access (Amber) var amberMu sync.Mutex type templateErr struct { @@ -70,18 +70,26 @@ type templateLoader interface { } type templateFuncsterTemplater interface { + templateFuncsterSetter tpl.TemplateFinder setFuncs(funcMap map[string]interface{}) +} + +type templateFuncsterSetter interface { setTemplateFuncster(f *templateFuncster) } // templateHandler holds the templates in play. // It implements the templateLoader and tpl.TemplateHandler interfaces. type templateHandler struct { + mu sync.Mutex + // text holds all the pure text templates. text *textTemplates html *htmlTemplates + extTextTemplates []*textTemplate + amberFuncMap template.FuncMap errors []*templateErr @@ -93,6 +101,19 @@ type templateHandler struct { *deps.Deps } +// NewTextTemplate provides a text template parser that has all the Hugo +// template funcs etc. built-in. +func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder { + t.mu.Lock() + t.mu.Unlock() + + tt := &textTemplate{t: texttemplate.New("")} + t.extTextTemplates = append(t.extTextTemplates, tt) + + return tt + +} + func (t *templateHandler) addError(name string, err error) { t.errors = append(t.errors, &templateErr{name, err}) } @@ -111,7 +132,7 @@ func (t *templateHandler) PrintErrors() { // 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 { +func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { if strings.HasPrefix(name, textTmplNamePrefix) { // The caller has explicitly asked for a text template, so only look @@ -123,8 +144,8 @@ func (t *templateHandler) Lookup(name string) *tpl.TemplateAdapter { } // Look in both - if te := t.html.Lookup(name); te != nil { - return te + if te, found := t.html.Lookup(name); found { + return te, true } return t.text.Lookup(name) @@ -136,7 +157,7 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler { Deps: d, layoutsFs: d.BaseFs.Layouts.Fs, 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)}, + text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template)}, errors: make([]*templateErr, 0), } @@ -171,8 +192,8 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { overlays: make(map[string]*template.Template), } textT := &textTemplates{ - t: texttemplate.New(""), - overlays: make(map[string]*texttemplate.Template), + textTemplate: &textTemplate{t: texttemplate.New("")}, + overlays: make(map[string]*texttemplate.Template), } return &templateHandler{ Deps: deps, @@ -205,12 +226,12 @@ func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) { t.funcster = f } -func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter { +func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) { templ := t.lookup(name) if templ == nil { - return nil + return nil, false } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics} + return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true } func (t *htmlTemplates) lookup(name string) *template.Template { @@ -233,27 +254,25 @@ func (t *htmlTemplates) lookup(name string) *template.Template { return nil } -type textTemplates struct { - funcster *templateFuncster - - t *texttemplate.Template +func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { + t.funcster = f +} +type textTemplates struct { + *textTemplate + funcster *templateFuncster 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 { +func (t *textTemplates) Lookup(name string) (tpl.Template, bool) { templ := t.lookup(name) if templ == nil { - return nil + return nil, false } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics} + return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true } func (t *textTemplates) lookup(name string) *texttemplate.Template { @@ -336,9 +355,34 @@ func (t *htmlTemplates) addLateTemplate(name, tpl string) error { return t.addTemplateIn(t.clone, name, tpl) } +type textTemplate struct { + t *texttemplate.Template +} + +func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) { + return t.parSeIn(t.t, name, tpl) +} + +func (t *textTemplate) Lookup(name string) (tpl.Template, bool) { + tpl := t.t.Lookup(name) + return tpl, tpl != nil +} + +func (t *textTemplate) parSeIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) { + templ, err := tt.New(name).Parse(tpl) + if err != nil { + return nil, err + } + + if err := applyTemplateTransformersToTextTemplate(templ); err != nil { + return nil, err + } + return templ, nil +} + func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error { name = strings.TrimPrefix(name, textTmplNamePrefix) - templ, err := tt.New(name).Parse(tpl) + templ, err := t.parSeIn(tt, name, tpl) if err != nil { return err } @@ -467,17 +511,22 @@ func (t *templateHandler) initFuncs() { // Both template types will get their own funcster instance, which // in the current case contains the same set of funcs. - for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} { + funcMap := createFuncMap(t.Deps) + for _, funcsterHolder := range []templateFuncsterSetter{t.html, t.text} { funcster := newTemplateFuncster(t.Deps) // 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() + funcster.initFuncMap(funcMap) funcsterHolder.setTemplateFuncster(funcster) } + for _, extText := range t.extTextTemplates { + extText.t.Funcs(funcMap) + } + // Amber is HTML only. t.amberFuncMap = template.FuncMap{} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go index e6bbde8ec..9490123ab 100644 --- a/tpl/tplimpl/templateFuncster.go +++ b/tpl/tplimpl/templateFuncster.go @@ -51,12 +51,12 @@ func (t *templateFuncster) partial(name string, contextList ...interface{}) (int } for _, n := range []string{"partials/" + name, "theme/partials/" + name} { - templ := t.Tmpl.Lookup(n) - if templ == nil { + templ, found := t.Tmpl.Lookup(n) + if !found { // For legacy reasons. - templ = t.Tmpl.Lookup(n + ".html") + templ, found = t.Tmpl.Lookup(n + ".html") } - if templ != nil { + if found { b := bp.GetBuffer() defer bp.PutBuffer(b) @@ -64,7 +64,7 @@ func (t *templateFuncster) partial(name string, contextList ...interface{}) (int return "", err } - if _, ok := templ.Template.(*texttemplate.Template); ok { + if _, ok := templ.(*texttemplate.Template); ok { return b.String(), nil } diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go index af89fed11..df44e81a6 100644 --- a/tpl/tplimpl/templateProvider.go +++ b/tpl/tplimpl/templateProvider.go @@ -30,6 +30,8 @@ func (*TemplateProvider) Update(deps *deps.Deps) error { newTmpl := newTemplateAdapter(deps) deps.Tmpl = newTmpl + deps.TextTmpl = newTmpl.NewTextTemplate() + newTmpl.initFuncs() newTmpl.loadEmbedded() diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index 6ce387aca..f1ed7f36f 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -18,6 +18,8 @@ package tplimpl import ( "html/template" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl/internal" // Init the namespaces @@ -35,6 +37,7 @@ import ( _ "github.com/gohugoio/hugo/tpl/os" _ "github.com/gohugoio/hugo/tpl/partials" _ "github.com/gohugoio/hugo/tpl/path" + _ "github.com/gohugoio/hugo/tpl/resources" _ "github.com/gohugoio/hugo/tpl/safe" _ "github.com/gohugoio/hugo/tpl/strings" _ "github.com/gohugoio/hugo/tpl/time" @@ -42,12 +45,12 @@ import ( _ "github.com/gohugoio/hugo/tpl/urls" ) -func (t *templateFuncster) initFuncMap() { +func createFuncMap(d *deps.Deps) map[string]interface{} { funcMap := template.FuncMap{} // Merge the namespace funcs for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns := nsf(t.Deps) + ns := nsf(d) if _, exists := funcMap[ns.Name]; exists { panic(ns.Name + " is a duplicate template func") } @@ -61,8 +64,13 @@ func (t *templateFuncster) initFuncMap() { } } + } + return funcMap + +} +func (t *templateFuncster) initFuncMap(funcMap template.FuncMap) { t.funcMap = funcMap t.Tmpl.(*templateHandler).setFuncs(funcMap) } diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index a1745282d..341be805a 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -51,6 +51,9 @@ func newTestConfig() config.Provider { v.Set("i18nDir", "i18n") v.Set("layoutDir", "layouts") v.Set("archetypeDir", "archetypes") + v.Set("assetDir", "assets") + v.Set("resourceDir", "resources") + v.Set("publishDir", "public") return v } @@ -76,12 +79,13 @@ func TestTemplateFuncsExamples(t *testing.T) { v.Set("workingDir", workingDir) v.Set("multilingual", true) v.Set("contentDir", "content") + v.Set("assetDir", "assets") v.Set("baseURL", "http://mysite.com/hugo/") v.Set("CurrentContentLanguage", langs.NewLanguage("en", v)) fs := hugofs.NewMem(v) - afero.WriteFile(fs.Source, filepath.Join(workingDir, "README.txt"), []byte("Hugo Rocks!"), 0755) + afero.WriteFile(fs.Source, filepath.Join(workingDir, "files", "README.txt"), []byte("Hugo Rocks!"), 0755) depsCfg := newDepsConfig(v) depsCfg.Fs = fs @@ -113,7 +117,8 @@ func TestTemplateFuncsExamples(t *testing.T) { require.NoError(t, d.LoadResources()) var b bytes.Buffer - require.NoError(t, d.Tmpl.Lookup("test").Execute(&b, &data)) + templ, _ := d.Tmpl.Lookup("test") + require.NoError(t, templ.Execute(&b, &data)) if b.String() != expected { t.Fatalf("%s[%d]: got %q expected %q", ns.Name, i, b.String(), expected) } diff --git a/tpl/tplimpl/template_test.go b/tpl/tplimpl/template_test.go index 3ce2a88a2..683850fa5 100644 --- a/tpl/tplimpl/template_test.go +++ b/tpl/tplimpl/template_test.go @@ -18,6 +18,7 @@ import ( "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/tpl" "github.com/stretchr/testify/require" ) @@ -43,20 +44,22 @@ func TestHTMLEscape(t *testing.T) { d, err := deps.New(depsCfg) assert.NoError(err) - tpl := `{{ "

Hi!

" | safeHTML }}` + templ := `{{ "

Hi!

" | safeHTML }}` provider := DefaultTemplateProvider provider.Update(d) h := d.Tmpl.(handler) - assert.NoError(h.addTemplate("shortcodes/myShort.html", tpl)) + assert.NoError(h.addTemplate("shortcodes/myShort.html", templ)) - s, err := d.Tmpl.Lookup("shortcodes/myShort.html").ExecuteToString(data) + tt, _ := d.Tmpl.Lookup("shortcodes/myShort.html") + s, err := tt.(tpl.TemplateExecutor).ExecuteToString(data) assert.NoError(err) assert.Contains(s, "

Hi!

") - s, err = d.Tmpl.Lookup("shortcodes/myShort").ExecuteToString(data) + tt, _ = d.Tmpl.Lookup("shortcodes/myShort") + s, err = tt.(tpl.TemplateExecutor).ExecuteToString(data) assert.NoError(err) assert.Contains(s, "

Hi!

") -- cgit v1.2.3