mirror of
https://github.com/gohugoio/hugo.git
synced 2025-09-01 22:42:45 +02:00
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 }} <link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Digest }}" media="screen"> ``` The transformation funcs above have aliases, so it can be shortened to: ```bash {{ $css := resources.Get "styles.scss" | toCSS | postCSS | minify | fingerprint }} <link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Digest }}" media="screen"> ``` 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
This commit is contained in:
@@ -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{} {
|
||||
|
@@ -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!`},
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
68
tpl/resources/init.go
Normal file
68
tpl/resources/init.go
Normal file
@@ -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)
|
||||
}
|
255
tpl/resources/resources.go
Normal file
255
tpl/resources/resources.go
Normal file
@@ -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
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
func (t *textTemplates) setTemplateFuncster(f *templateFuncster) {
|
||||
t.funcster = f
|
||||
}
|
||||
|
||||
type textTemplates struct {
|
||||
funcster *templateFuncster
|
||||
|
||||
t *texttemplate.Template
|
||||
|
||||
*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{}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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 := `{{ "<h1>Hi!</h1>" | safeHTML }}`
|
||||
templ := `{{ "<h1>Hi!</h1>" | 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, "<h1>Hi!</h1>")
|
||||
|
||||
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, "<h1>Hi!</h1>")
|
||||
|
||||
|
Reference in New Issue
Block a user