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/resources/init.go | 68 ++++++++++++ tpl/resources/resources.go | 255 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 tpl/resources/init.go create mode 100644 tpl/resources/resources.go (limited to 'tpl/resources') 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 +} -- cgit v1.2.3