From: Bjørn Erik Pedersen Date: Wed, 2 Jan 2019 09:36:12 +0000 (+0100) Subject: resource: Move resource processors into sub-packages X-Git-Tag: v0.54.0~26 X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=669ada436787311cc5d02dae5b88e60a09adda58;p=brevno-suite%2Fhugo resource: Move resource processors into sub-packages --- diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 551ac017..0fe9c70c 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -25,7 +25,7 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/resource/tocss/scss" + "github.com/gohugoio/hugo/resource/resource_transformers/tocss/scss" ) func TestSCSSWithIncludePaths(t *testing.T) { diff --git a/resource/bundler/bundler.go b/resource/bundler/bundler.go deleted file mode 100644 index 70b8ee53..00000000 --- a/resource/bundler/bundler.go +++ /dev/null @@ -1,121 +0,0 @@ -// 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 bundler contains functions for concatenation etc. of Resource objects. -package bundler - -import ( - "fmt" - "io" - "path/filepath" - - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/resource" -) - -// Client contains methods perform concatenation and other bundling related -// tasks to Resource objects. -type Client struct { - rs *resource.Spec -} - -// New creates a new Client with the given specification. -func New(rs *resource.Spec) *Client { - return &Client{rs: rs} -} - -type multiReadSeekCloser struct { - mr io.Reader - sources []hugio.ReadSeekCloser -} - -func (r *multiReadSeekCloser) Read(p []byte) (n int, err error) { - return r.mr.Read(p) -} - -func (r *multiReadSeekCloser) Seek(offset int64, whence int) (newOffset int64, err error) { - for _, s := range r.sources { - newOffset, err = s.Seek(offset, whence) - if err != nil { - return - } - } - return -} - -func (r *multiReadSeekCloser) Close() error { - for _, s := range r.sources { - s.Close() - } - return nil -} - -// Concat concatenates the list of Resource objects. -func (c *Client) Concat(targetPath string, resources resource.Resources) (resource.Resource, error) { - // The CACHE_OTHER will make sure this will be re-created and published on rebuilds. - return c.rs.ResourceCache.GetOrCreate(resource.CACHE_OTHER, targetPath, func() (resource.Resource, error) { - var resolvedm media.Type - - // The given set of resources must be of the same Media Type. - // We may improve on that in the future, but then we need to know more. - for i, r := range resources { - if i > 0 && r.MediaType().Type() != resolvedm.Type() { - return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", r.MediaType().Type(), resolvedm.Type()) - } - resolvedm = r.MediaType() - } - - concatr := func() (hugio.ReadSeekCloser, error) { - var rcsources []hugio.ReadSeekCloser - for _, s := range resources { - rcr, ok := s.(resource.ReadSeekCloserResource) - if !ok { - return nil, fmt.Errorf("resource %T does not implement resource.ReadSeekerCloserResource", s) - } - rc, err := rcr.ReadSeekCloser() - if err != nil { - // Close the already opened. - for _, rcs := range rcsources { - rcs.Close() - } - return nil, err - } - rcsources = append(rcsources, rc) - } - - readers := make([]io.Reader, len(rcsources)) - for i := 0; i < len(rcsources); i++ { - readers[i] = rcsources[i] - } - - mr := io.MultiReader(readers...) - - return &multiReadSeekCloser{mr: mr, sources: rcsources}, nil - } - - composite, err := c.rs.NewForFs( - c.rs.FileCaches.AssetsCache().Fs, - resource.ResourceSourceDescriptor{ - LazyPublish: true, - OpenReadSeekCloser: concatr, - RelTargetFilename: filepath.Clean(targetPath)}) - - if err != nil { - return nil, err - } - - return composite, nil - }) - -} diff --git a/resource/create/create.go b/resource/create/create.go deleted file mode 100644 index db23930e..00000000 --- a/resource/create/create.go +++ /dev/null @@ -1,64 +0,0 @@ -// 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 create contains functions for to create Resource objects. This will -// typically non-files. -package create - -import ( - "path/filepath" - - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/resource" -) - -// Client contains methods to create Resource objects. -// tasks to Resource objects. -type Client struct { - rs *resource.Spec -} - -// New creates a new Client with the given specification. -func New(rs *resource.Spec) *Client { - return &Client{rs: rs} -} - -// Get creates a new Resource by opening the given filename in the given filesystem. -func (c *Client) Get(fs afero.Fs, filename string) (resource.Resource, error) { - filename = filepath.Clean(filename) - return c.rs.ResourceCache.GetOrCreate(resource.ResourceKeyPartition(filename), filename, func() (resource.Resource, error) { - return c.rs.NewForFs(fs, - resource.ResourceSourceDescriptor{ - LazyPublish: true, - SourceFilename: filename}) - }) - -} - -// FromString creates a new Resource from a string with the given relative target path. -func (c *Client) FromString(targetPath, content string) (resource.Resource, error) { - return c.rs.ResourceCache.GetOrCreate(resource.CACHE_OTHER, targetPath, func() (resource.Resource, error) { - return c.rs.NewForFs( - c.rs.FileCaches.AssetsCache().Fs, - resource.ResourceSourceDescriptor{ - LazyPublish: true, - OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) { - return hugio.NewReadSeekerNoOpCloserFromString(content), nil - }, - RelTargetFilename: filepath.Clean(targetPath)}) - - }) - -} diff --git a/resource/integrity/integrity.go b/resource/integrity/integrity.go deleted file mode 100644 index 535c06a3..00000000 --- a/resource/integrity/integrity.go +++ /dev/null @@ -1,105 +0,0 @@ -// 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 integrity - -import ( - "crypto/md5" - "crypto/sha256" - "crypto/sha512" - "encoding/base64" - "encoding/hex" - "fmt" - "hash" - "html/template" - "io" - - "github.com/gohugoio/hugo/resource" -) - -const defaultHashAlgo = "sha256" - -// Client contains methods to fingerprint (cachebusting) and other integrity-related -// methods. -type Client struct { - rs *resource.Spec -} - -// New creates a new Client with the given specification. -func New(rs *resource.Spec) *Client { - return &Client{rs: rs} -} - -type fingerprintTransformation struct { - algo string -} - -func (t *fingerprintTransformation) Key() resource.ResourceTransformationKey { - return resource.NewResourceTransformationKey("fingerprint", t.algo) -} - -// Transform creates a MD5 hash of the Resource content and inserts that hash before -// the extension in the filename. -func (t *fingerprintTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { - algo := t.algo - - var h hash.Hash - - switch algo { - case "md5": - h = md5.New() - case "sha256": - h = sha256.New() - case "sha512": - h = sha512.New() - default: - return fmt.Errorf("unsupported crypto algo: %q, use either md5, sha256 or sha512", algo) - } - - io.Copy(io.MultiWriter(h, ctx.To), ctx.From) - d, err := digest(h) - if err != nil { - return err - } - - ctx.Data["Integrity"] = integrity(algo, d) - ctx.AddOutPathIdentifier("." + hex.EncodeToString(d[:])) - return nil -} - -// Fingerprint applies fingerprinting of the given resource and hash algorithm. -// It defaults to sha256 if none given, and the options are md5, sha256 or sha512. -// The same algo is used for both the fingerprinting part (aka cache busting) and -// the base64-encoded Subresource Integrity hash, so you will have to stay away from -// md5 if you plan to use both. -// See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity -func (c *Client) Fingerprint(res resource.Resource, algo string) (resource.Resource, error) { - if algo == "" { - algo = defaultHashAlgo - } - - return c.rs.Transform( - res, - &fingerprintTransformation{algo: algo}, - ) -} - -func integrity(algo string, sum []byte) template.HTMLAttr { - encoded := base64.StdEncoding.EncodeToString(sum) - return template.HTMLAttr(algo + "-" + encoded) -} - -func digest(h hash.Hash) ([]byte, error) { - sum := h.Sum(nil) - return sum, nil -} diff --git a/resource/minifier/minify.go b/resource/minifier/minify.go deleted file mode 100644 index cef22efc..00000000 --- a/resource/minifier/minify.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 minifier - -import ( - "github.com/gohugoio/hugo/minifiers" - "github.com/gohugoio/hugo/resource" -) - -// Client for minification of Resource objects. Supported minfiers are: -// css, html, js, json, svg and xml. -type Client struct { - rs *resource.Spec - m minifiers.Client -} - -// New creates a new Client given a specification. Note that it is the media types -// configured for the site that is used to match files to the correct minifier. -func New(rs *resource.Spec) *Client { - return &Client{rs: rs, m: minifiers.New(rs.MediaTypes, rs.OutputFormats)} -} - -type minifyTransformation struct { - rs *resource.Spec - m minifiers.Client -} - -func (t *minifyTransformation) Key() resource.ResourceTransformationKey { - return resource.NewResourceTransformationKey("minify") -} - -func (t *minifyTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { - if err := t.m.Minify(ctx.InMediaType, ctx.To, ctx.From); err != nil { - return err - } - ctx.AddOutPathIdentifier(".min") - return nil -} - -func (c *Client) Minify(res resource.Resource) (resource.Resource, error) { - return c.rs.Transform( - res, - &minifyTransformation{ - rs: c.rs, - m: c.m}, - ) -} diff --git a/resource/postcss/postcss.go b/resource/postcss/postcss.go deleted file mode 100644 index ec73543d..00000000 --- a/resource/postcss/postcss.go +++ /dev/null @@ -1,184 +0,0 @@ -// 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 postcss - -import ( - "io" - "path/filepath" - - "github.com/gohugoio/hugo/hugofs" - "github.com/pkg/errors" - - "os" - "os/exec" - - "github.com/mitchellh/mapstructure" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/resource" -) - -// Some of the options from https://github.com/postcss/postcss-cli -type Options struct { - - // Set a custom path to look for a config file. - Config string - - NoMap bool `mapstructure:"no-map"` // Disable the default inline sourcemaps - - // Options for when not using a config file - Use string // List of postcss plugins to use - Parser string // Custom postcss parser - Stringifier string // Custom postcss stringifier - Syntax string // Custom postcss syntax -} - -func DecodeOptions(m map[string]interface{}) (opts Options, err error) { - if m == nil { - return - } - err = mapstructure.WeakDecode(m, &opts) - return -} - -func (opts Options) toArgs() []string { - var args []string - if opts.NoMap { - args = append(args, "--no-map") - } - if opts.Use != "" { - args = append(args, "--use", opts.Use) - } - if opts.Parser != "" { - args = append(args, "--parser", opts.Parser) - } - if opts.Stringifier != "" { - args = append(args, "--stringifier", opts.Stringifier) - } - if opts.Syntax != "" { - args = append(args, "--syntax", opts.Syntax) - } - return args -} - -// Client is the client used to do PostCSS transformations. -type Client struct { - rs *resource.Spec -} - -// New creates a new Client with the given specification. -func New(rs *resource.Spec) *Client { - return &Client{rs: rs} -} - -type postcssTransformation struct { - options Options - rs *resource.Spec -} - -func (t *postcssTransformation) Key() resource.ResourceTransformationKey { - return resource.NewResourceTransformationKey("postcss", t.options) -} - -// Transform shells out to postcss-cli to do the heavy lifting. -// For this to work, you need some additional tools. To install them globally: -// npm install -g postcss-cli -// npm install -g autoprefixer -func (t *postcssTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { - - const localPostCSSPath = "node_modules/postcss-cli/bin/" - const binaryName = "postcss" - - // Try first in the project's node_modules. - csiBinPath := filepath.Join(t.rs.WorkingDir, localPostCSSPath, binaryName) - - binary := csiBinPath - - if _, err := exec.LookPath(binary); err != nil { - // Try PATH - binary = binaryName - if _, err := exec.LookPath(binary); err != nil { - // This may be on a CI server etc. Will fall back to pre-built assets. - return herrors.ErrFeatureNotAvailable - } - } - - var configFile string - logger := t.rs.Logger - - if t.options.Config != "" { - configFile = t.options.Config - } else { - configFile = "postcss.config.js" - } - - configFile = filepath.Clean(configFile) - - // We need an abolute filename to the config file. - if !filepath.IsAbs(configFile) { - // We resolve this against the virtual Work filesystem, to allow - // this config file to live in one of the themes if needed. - fi, err := t.rs.BaseFs.Work.Fs.Stat(configFile) - if err != nil { - if t.options.Config != "" { - // Only fail if the user specificed config file is not found. - return errors.Wrapf(err, "postcss config %q not found:", configFile) - } - configFile = "" - } else { - configFile = fi.(hugofs.RealFilenameInfo).RealFilename() - } - } - - var cmdArgs []string - - if configFile != "" { - logger.INFO.Println("postcss: use config file", configFile) - cmdArgs = []string{"--config", configFile} - } - - if optArgs := t.options.toArgs(); len(optArgs) > 0 { - cmdArgs = append(cmdArgs, optArgs...) - } - - cmd := exec.Command(binary, cmdArgs...) - - cmd.Stdout = ctx.To - cmd.Stderr = os.Stderr - - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - - go func() { - defer stdin.Close() - io.Copy(stdin, ctx.From) - }() - - err = cmd.Run() - if err != nil { - return err - } - - return nil -} - -// Process transforms the given Resource with the PostCSS processor. -func (c *Client) Process(res resource.Resource, options Options) (resource.Resource, error) { - return c.rs.Transform( - res, - &postcssTransformation{rs: c.rs, options: options}, - ) -} diff --git a/resource/resource_factories/bundler/bundler.go b/resource/resource_factories/bundler/bundler.go new file mode 100644 index 00000000..70b8ee53 --- /dev/null +++ b/resource/resource_factories/bundler/bundler.go @@ -0,0 +1,121 @@ +// 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 bundler contains functions for concatenation etc. of Resource objects. +package bundler + +import ( + "fmt" + "io" + "path/filepath" + + "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/resource" +) + +// Client contains methods perform concatenation and other bundling related +// tasks to Resource objects. +type Client struct { + rs *resource.Spec +} + +// New creates a new Client with the given specification. +func New(rs *resource.Spec) *Client { + return &Client{rs: rs} +} + +type multiReadSeekCloser struct { + mr io.Reader + sources []hugio.ReadSeekCloser +} + +func (r *multiReadSeekCloser) Read(p []byte) (n int, err error) { + return r.mr.Read(p) +} + +func (r *multiReadSeekCloser) Seek(offset int64, whence int) (newOffset int64, err error) { + for _, s := range r.sources { + newOffset, err = s.Seek(offset, whence) + if err != nil { + return + } + } + return +} + +func (r *multiReadSeekCloser) Close() error { + for _, s := range r.sources { + s.Close() + } + return nil +} + +// Concat concatenates the list of Resource objects. +func (c *Client) Concat(targetPath string, resources resource.Resources) (resource.Resource, error) { + // The CACHE_OTHER will make sure this will be re-created and published on rebuilds. + return c.rs.ResourceCache.GetOrCreate(resource.CACHE_OTHER, targetPath, func() (resource.Resource, error) { + var resolvedm media.Type + + // The given set of resources must be of the same Media Type. + // We may improve on that in the future, but then we need to know more. + for i, r := range resources { + if i > 0 && r.MediaType().Type() != resolvedm.Type() { + return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", r.MediaType().Type(), resolvedm.Type()) + } + resolvedm = r.MediaType() + } + + concatr := func() (hugio.ReadSeekCloser, error) { + var rcsources []hugio.ReadSeekCloser + for _, s := range resources { + rcr, ok := s.(resource.ReadSeekCloserResource) + if !ok { + return nil, fmt.Errorf("resource %T does not implement resource.ReadSeekerCloserResource", s) + } + rc, err := rcr.ReadSeekCloser() + if err != nil { + // Close the already opened. + for _, rcs := range rcsources { + rcs.Close() + } + return nil, err + } + rcsources = append(rcsources, rc) + } + + readers := make([]io.Reader, len(rcsources)) + for i := 0; i < len(rcsources); i++ { + readers[i] = rcsources[i] + } + + mr := io.MultiReader(readers...) + + return &multiReadSeekCloser{mr: mr, sources: rcsources}, nil + } + + composite, err := c.rs.NewForFs( + c.rs.FileCaches.AssetsCache().Fs, + resource.ResourceSourceDescriptor{ + LazyPublish: true, + OpenReadSeekCloser: concatr, + RelTargetFilename: filepath.Clean(targetPath)}) + + if err != nil { + return nil, err + } + + return composite, nil + }) + +} diff --git a/resource/resource_factories/create/create.go b/resource/resource_factories/create/create.go new file mode 100644 index 00000000..db23930e --- /dev/null +++ b/resource/resource_factories/create/create.go @@ -0,0 +1,64 @@ +// 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 create contains functions for to create Resource objects. This will +// typically non-files. +package create + +import ( + "path/filepath" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/resource" +) + +// Client contains methods to create Resource objects. +// tasks to Resource objects. +type Client struct { + rs *resource.Spec +} + +// New creates a new Client with the given specification. +func New(rs *resource.Spec) *Client { + return &Client{rs: rs} +} + +// Get creates a new Resource by opening the given filename in the given filesystem. +func (c *Client) Get(fs afero.Fs, filename string) (resource.Resource, error) { + filename = filepath.Clean(filename) + return c.rs.ResourceCache.GetOrCreate(resource.ResourceKeyPartition(filename), filename, func() (resource.Resource, error) { + return c.rs.NewForFs(fs, + resource.ResourceSourceDescriptor{ + LazyPublish: true, + SourceFilename: filename}) + }) + +} + +// FromString creates a new Resource from a string with the given relative target path. +func (c *Client) FromString(targetPath, content string) (resource.Resource, error) { + return c.rs.ResourceCache.GetOrCreate(resource.CACHE_OTHER, targetPath, func() (resource.Resource, error) { + return c.rs.NewForFs( + c.rs.FileCaches.AssetsCache().Fs, + resource.ResourceSourceDescriptor{ + LazyPublish: true, + OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) { + return hugio.NewReadSeekerNoOpCloserFromString(content), nil + }, + RelTargetFilename: filepath.Clean(targetPath)}) + + }) + +} diff --git a/resource/resource_transformers/integrity/integrity.go b/resource/resource_transformers/integrity/integrity.go new file mode 100644 index 00000000..535c06a3 --- /dev/null +++ b/resource/resource_transformers/integrity/integrity.go @@ -0,0 +1,105 @@ +// 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 integrity + +import ( + "crypto/md5" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "fmt" + "hash" + "html/template" + "io" + + "github.com/gohugoio/hugo/resource" +) + +const defaultHashAlgo = "sha256" + +// Client contains methods to fingerprint (cachebusting) and other integrity-related +// methods. +type Client struct { + rs *resource.Spec +} + +// New creates a new Client with the given specification. +func New(rs *resource.Spec) *Client { + return &Client{rs: rs} +} + +type fingerprintTransformation struct { + algo string +} + +func (t *fingerprintTransformation) Key() resource.ResourceTransformationKey { + return resource.NewResourceTransformationKey("fingerprint", t.algo) +} + +// Transform creates a MD5 hash of the Resource content and inserts that hash before +// the extension in the filename. +func (t *fingerprintTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { + algo := t.algo + + var h hash.Hash + + switch algo { + case "md5": + h = md5.New() + case "sha256": + h = sha256.New() + case "sha512": + h = sha512.New() + default: + return fmt.Errorf("unsupported crypto algo: %q, use either md5, sha256 or sha512", algo) + } + + io.Copy(io.MultiWriter(h, ctx.To), ctx.From) + d, err := digest(h) + if err != nil { + return err + } + + ctx.Data["Integrity"] = integrity(algo, d) + ctx.AddOutPathIdentifier("." + hex.EncodeToString(d[:])) + return nil +} + +// Fingerprint applies fingerprinting of the given resource and hash algorithm. +// It defaults to sha256 if none given, and the options are md5, sha256 or sha512. +// The same algo is used for both the fingerprinting part (aka cache busting) and +// the base64-encoded Subresource Integrity hash, so you will have to stay away from +// md5 if you plan to use both. +// See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity +func (c *Client) Fingerprint(res resource.Resource, algo string) (resource.Resource, error) { + if algo == "" { + algo = defaultHashAlgo + } + + return c.rs.Transform( + res, + &fingerprintTransformation{algo: algo}, + ) +} + +func integrity(algo string, sum []byte) template.HTMLAttr { + encoded := base64.StdEncoding.EncodeToString(sum) + return template.HTMLAttr(algo + "-" + encoded) +} + +func digest(h hash.Hash) ([]byte, error) { + sum := h.Sum(nil) + return sum, nil +} diff --git a/resource/resource_transformers/minifier/minify.go b/resource/resource_transformers/minifier/minify.go new file mode 100644 index 00000000..cef22efc --- /dev/null +++ b/resource/resource_transformers/minifier/minify.go @@ -0,0 +1,58 @@ +// 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 minifier + +import ( + "github.com/gohugoio/hugo/minifiers" + "github.com/gohugoio/hugo/resource" +) + +// Client for minification of Resource objects. Supported minfiers are: +// css, html, js, json, svg and xml. +type Client struct { + rs *resource.Spec + m minifiers.Client +} + +// New creates a new Client given a specification. Note that it is the media types +// configured for the site that is used to match files to the correct minifier. +func New(rs *resource.Spec) *Client { + return &Client{rs: rs, m: minifiers.New(rs.MediaTypes, rs.OutputFormats)} +} + +type minifyTransformation struct { + rs *resource.Spec + m minifiers.Client +} + +func (t *minifyTransformation) Key() resource.ResourceTransformationKey { + return resource.NewResourceTransformationKey("minify") +} + +func (t *minifyTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { + if err := t.m.Minify(ctx.InMediaType, ctx.To, ctx.From); err != nil { + return err + } + ctx.AddOutPathIdentifier(".min") + return nil +} + +func (c *Client) Minify(res resource.Resource) (resource.Resource, error) { + return c.rs.Transform( + res, + &minifyTransformation{ + rs: c.rs, + m: c.m}, + ) +} diff --git a/resource/resource_transformers/postcss/postcss.go b/resource/resource_transformers/postcss/postcss.go new file mode 100644 index 00000000..ec73543d --- /dev/null +++ b/resource/resource_transformers/postcss/postcss.go @@ -0,0 +1,184 @@ +// 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 postcss + +import ( + "io" + "path/filepath" + + "github.com/gohugoio/hugo/hugofs" + "github.com/pkg/errors" + + "os" + "os/exec" + + "github.com/mitchellh/mapstructure" + + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/resource" +) + +// Some of the options from https://github.com/postcss/postcss-cli +type Options struct { + + // Set a custom path to look for a config file. + Config string + + NoMap bool `mapstructure:"no-map"` // Disable the default inline sourcemaps + + // Options for when not using a config file + Use string // List of postcss plugins to use + Parser string // Custom postcss parser + Stringifier string // Custom postcss stringifier + Syntax string // Custom postcss syntax +} + +func DecodeOptions(m map[string]interface{}) (opts Options, err error) { + if m == nil { + return + } + err = mapstructure.WeakDecode(m, &opts) + return +} + +func (opts Options) toArgs() []string { + var args []string + if opts.NoMap { + args = append(args, "--no-map") + } + if opts.Use != "" { + args = append(args, "--use", opts.Use) + } + if opts.Parser != "" { + args = append(args, "--parser", opts.Parser) + } + if opts.Stringifier != "" { + args = append(args, "--stringifier", opts.Stringifier) + } + if opts.Syntax != "" { + args = append(args, "--syntax", opts.Syntax) + } + return args +} + +// Client is the client used to do PostCSS transformations. +type Client struct { + rs *resource.Spec +} + +// New creates a new Client with the given specification. +func New(rs *resource.Spec) *Client { + return &Client{rs: rs} +} + +type postcssTransformation struct { + options Options + rs *resource.Spec +} + +func (t *postcssTransformation) Key() resource.ResourceTransformationKey { + return resource.NewResourceTransformationKey("postcss", t.options) +} + +// Transform shells out to postcss-cli to do the heavy lifting. +// For this to work, you need some additional tools. To install them globally: +// npm install -g postcss-cli +// npm install -g autoprefixer +func (t *postcssTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { + + const localPostCSSPath = "node_modules/postcss-cli/bin/" + const binaryName = "postcss" + + // Try first in the project's node_modules. + csiBinPath := filepath.Join(t.rs.WorkingDir, localPostCSSPath, binaryName) + + binary := csiBinPath + + if _, err := exec.LookPath(binary); err != nil { + // Try PATH + binary = binaryName + if _, err := exec.LookPath(binary); err != nil { + // This may be on a CI server etc. Will fall back to pre-built assets. + return herrors.ErrFeatureNotAvailable + } + } + + var configFile string + logger := t.rs.Logger + + if t.options.Config != "" { + configFile = t.options.Config + } else { + configFile = "postcss.config.js" + } + + configFile = filepath.Clean(configFile) + + // We need an abolute filename to the config file. + if !filepath.IsAbs(configFile) { + // We resolve this against the virtual Work filesystem, to allow + // this config file to live in one of the themes if needed. + fi, err := t.rs.BaseFs.Work.Fs.Stat(configFile) + if err != nil { + if t.options.Config != "" { + // Only fail if the user specificed config file is not found. + return errors.Wrapf(err, "postcss config %q not found:", configFile) + } + configFile = "" + } else { + configFile = fi.(hugofs.RealFilenameInfo).RealFilename() + } + } + + var cmdArgs []string + + if configFile != "" { + logger.INFO.Println("postcss: use config file", configFile) + cmdArgs = []string{"--config", configFile} + } + + if optArgs := t.options.toArgs(); len(optArgs) > 0 { + cmdArgs = append(cmdArgs, optArgs...) + } + + cmd := exec.Command(binary, cmdArgs...) + + cmd.Stdout = ctx.To + cmd.Stderr = os.Stderr + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + + go func() { + defer stdin.Close() + io.Copy(stdin, ctx.From) + }() + + err = cmd.Run() + if err != nil { + return err + } + + return nil +} + +// Process transforms the given Resource with the PostCSS processor. +func (c *Client) Process(res resource.Resource, options Options) (resource.Resource, error) { + return c.rs.Transform( + res, + &postcssTransformation{rs: c.rs, options: options}, + ) +} diff --git a/resource/resource_transformers/templates/execute_as_template.go b/resource/resource_transformers/templates/execute_as_template.go new file mode 100644 index 00000000..a126b26c --- /dev/null +++ b/resource/resource_transformers/templates/execute_as_template.go @@ -0,0 +1,75 @@ +// 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 templates contains functions for template processing of Resource objects. +package templates + +import ( + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/resource" + "github.com/gohugoio/hugo/tpl" + "github.com/pkg/errors" +) + +// Client contains methods to perform template processing of Resource objects. +type Client struct { + rs *resource.Spec + + textTemplate tpl.TemplateParseFinder +} + +// New creates a new Client with the given specification. +func New(rs *resource.Spec, textTemplate tpl.TemplateParseFinder) *Client { + if rs == nil { + panic("must provice a resource Spec") + } + if textTemplate == nil { + panic("must provide a textTemplate") + } + return &Client{rs: rs, textTemplate: textTemplate} +} + +type executeAsTemplateTransform struct { + rs *resource.Spec + textTemplate tpl.TemplateParseFinder + targetPath string + data interface{} +} + +func (t *executeAsTemplateTransform) Key() resource.ResourceTransformationKey { + return resource.NewResourceTransformationKey("execute-as-template", t.targetPath) +} + +func (t *executeAsTemplateTransform) Transform(ctx *resource.ResourceTransformationCtx) error { + tplStr := helpers.ReaderToString(ctx.From) + templ, err := t.textTemplate.Parse(ctx.InPath, tplStr) + if err != nil { + return errors.Wrapf(err, "failed to parse Resource %q as Template:", ctx.InPath) + } + + ctx.OutPath = t.targetPath + + return templ.Execute(ctx.To, t.data) +} + +func (c *Client) ExecuteAsTemplate(res resource.Resource, targetPath string, data interface{}) (resource.Resource, error) { + return c.rs.Transform( + res, + &executeAsTemplateTransform{ + rs: c.rs, + targetPath: helpers.ToSlashTrimLeading(targetPath), + textTemplate: c.textTemplate, + data: data, + }, + ) +} diff --git a/resource/resource_transformers/tocss/scss/client.go b/resource/resource_transformers/tocss/scss/client.go new file mode 100644 index 00000000..126727e0 --- /dev/null +++ b/resource/resource_transformers/tocss/scss/client.go @@ -0,0 +1,109 @@ +// 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 scss + +import ( + "github.com/bep/go-tocss/scss" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib/filesystems" + "github.com/gohugoio/hugo/resource" + "github.com/mitchellh/mapstructure" +) + +type Client struct { + rs *resource.Spec + sfs *filesystems.SourceFilesystem + workFs *filesystems.SourceFilesystem +} + +func New(fs *filesystems.SourceFilesystem, rs *resource.Spec) (*Client, error) { + return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs}, nil +} + +type Options struct { + + // Hugo, will by default, just replace the extension of the source + // to .css, e.g. "scss/main.scss" becomes "scss/main.css". You can + // control this by setting this, e.g. "styles/main.css" will create + // a Resource with that as a base for RelPermalink etc. + TargetPath string + + // Hugo automatically adds the entry directories (where the main.scss lives) + // for project and themes to the list of include paths sent to LibSASS. + // Any paths set in this setting will be appended. Note that these will be + // treated as relative to the working dir, i.e. no include paths outside the + // project/themes. + IncludePaths []string + + // Default is nested. + // One of nested, expanded, compact, compressed. + OutputStyle string + + // Precision of floating point math. + Precision int + + // When enabled, Hugo will generate a source map. + EnableSourceMap bool +} + +type options struct { + // The options we receive from the end user. + from Options + + // The options we send to the SCSS library. + to scss.Options +} + +func (c *Client) ToCSS(res resource.Resource, opts Options) (resource.Resource, error) { + internalOptions := options{ + from: opts, + } + + // Transfer values from client. + internalOptions.to.Precision = opts.Precision + internalOptions.to.OutputStyle = scss.OutputStyleFromString(opts.OutputStyle) + + if internalOptions.to.Precision == 0 { + // bootstrap-sass requires 8 digits precision. The libsass default is 5. + // https://github.com/twbs/bootstrap-sass/blob/master/README.md#sass-number-precision + internalOptions.to.Precision = 8 + } + + return c.rs.Transform( + res, + &toCSSTransformation{c: c, options: internalOptions}, + ) +} + +type toCSSTransformation struct { + c *Client + options options +} + +func (t *toCSSTransformation) Key() resource.ResourceTransformationKey { + return resource.NewResourceTransformationKey("tocss", t.options.from) +} + +func DecodeOptions(m map[string]interface{}) (opts Options, err error) { + if m == nil { + return + } + err = mapstructure.WeakDecode(m, &opts) + + if opts.TargetPath != "" { + opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath) + } + + return +} diff --git a/resource/resource_transformers/tocss/scss/tocss.go b/resource/resource_transformers/tocss/scss/tocss.go new file mode 100644 index 00000000..984e14fc --- /dev/null +++ b/resource/resource_transformers/tocss/scss/tocss.go @@ -0,0 +1,173 @@ +// 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. + +// +build extended + +package scss + +import ( + "fmt" + "io" + "path" + "path/filepath" + "strings" + + "github.com/bep/go-tocss/scss" + "github.com/bep/go-tocss/scss/libsass" + "github.com/bep/go-tocss/tocss" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/resource" + "github.com/pkg/errors" +) + +// Used in tests. This feature requires Hugo to be built with the extended tag. +func Supports() bool { + return true +} + +func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { + ctx.OutMediaType = media.CSSType + + var outName string + if t.options.from.TargetPath != "" { + ctx.OutPath = t.options.from.TargetPath + } else { + ctx.ReplaceOutPathExtension(".css") + } + + outName = path.Base(ctx.OutPath) + + options := t.options + baseDir := path.Dir(ctx.SourcePath) + options.to.IncludePaths = t.c.sfs.RealDirs(baseDir) + + // Append any workDir relative include paths + for _, ip := range options.from.IncludePaths { + options.to.IncludePaths = append(options.to.IncludePaths, t.c.workFs.RealDirs(filepath.Clean(ip))...) + } + + // To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need + // to help libsass revolve the filename by looking in the composite filesystem first. + // We add the entry directories for both project and themes to the include paths list, but + // that only work for overrides on the top level. + options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) { + // We get URL paths from LibSASS, but we need file paths. + url = filepath.FromSlash(url) + prev = filepath.FromSlash(prev) + + var basePath string + urlDir := filepath.Dir(url) + var prevDir string + if prev == "stdin" { + prevDir = baseDir + } else { + prevDir = t.c.sfs.MakePathRelative(filepath.Dir(prev)) + if prevDir == "" { + // Not a member of this filesystem. Let LibSASS handle it. + return "", "", false + } + } + + basePath = filepath.Join(prevDir, urlDir) + name := filepath.Base(url) + + // Libsass throws an error in cases where you have several possible candidates. + // We make this simpler and pick the first match. + var namePatterns []string + if strings.Contains(name, ".") { + namePatterns = []string{"_%s", "%s"} + } else if strings.HasPrefix(name, "_") { + namePatterns = []string{"_%s.scss", "_%s.sass"} + } else { + namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"} + } + + name = strings.TrimPrefix(name, "_") + + for _, namePattern := range namePatterns { + filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name)) + fi, err := t.c.sfs.Fs.Stat(filenameToCheck) + if err == nil { + if fir, ok := fi.(hugofs.RealFilenameInfo); ok { + return fir.RealFilename(), "", true + } + } + } + + // Not found, let LibSASS handle it + return "", "", false + } + + if ctx.InMediaType.SubType == media.SASSType.SubType { + options.to.SassSyntax = true + } + + if options.from.EnableSourceMap { + + options.to.SourceMapFilename = outName + ".map" + options.to.SourceMapRoot = t.c.rs.WorkingDir + + // Setting this to the relative input filename will get the source map + // more correct for the main entry path (main.scss typically), but + // it will mess up the import mappings. As a workaround, we do a replacement + // in the source map itself (see below). + //options.InputPath = inputPath + options.to.OutputPath = outName + options.to.SourceMapContents = true + options.to.OmitSourceMapURL = false + options.to.EnableEmbeddedSourceMap = false + } + + res, err := t.c.toCSS(options.to, ctx.To, ctx.From) + if err != nil { + return err + } + + if options.from.EnableSourceMap && res.SourceMapContent != "" { + sourcePath := t.c.sfs.RealFilename(ctx.SourcePath) + + if strings.HasPrefix(sourcePath, t.c.rs.WorkingDir) { + sourcePath = strings.TrimPrefix(sourcePath, t.c.rs.WorkingDir+helpers.FilePathSeparator) + } + + // This needs to be Unix-style slashes, even on Windows. + // See https://github.com/gohugoio/hugo/issues/4968 + sourcePath = filepath.ToSlash(sourcePath) + + // This is a workaround for what looks like a bug in Libsass. But + // getting this resolution correct in tools like Chrome Workspaces + // is important enough to go this extra mile. + mapContent := strings.Replace(res.SourceMapContent, `stdin",`, fmt.Sprintf("%s\",", sourcePath), 1) + + return ctx.PublishSourceMap(mapContent) + } + return nil +} + +func (c *Client) toCSS(options scss.Options, dst io.Writer, src io.Reader) (tocss.Result, error) { + var res tocss.Result + + transpiler, err := libsass.New(options) + if err != nil { + return res, err + } + + res, err = transpiler.Execute(dst, src) + if err != nil { + return res, errors.Wrap(err, "SCSS processing failed") + } + + return res, nil +} diff --git a/resource/resource_transformers/tocss/scss/tocss_notavailable.go b/resource/resource_transformers/tocss/scss/tocss_notavailable.go new file mode 100644 index 00000000..df918b36 --- /dev/null +++ b/resource/resource_transformers/tocss/scss/tocss_notavailable.go @@ -0,0 +1,30 @@ +// 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. + +// +build !extended + +package scss + +import ( + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/resource" +) + +// Used in tests. +func Supports() bool { + return false +} + +func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { + return herrors.ErrFeatureNotAvailable +} diff --git a/resource/templates/execute_as_template.go b/resource/templates/execute_as_template.go deleted file mode 100644 index a126b26c..00000000 --- a/resource/templates/execute_as_template.go +++ /dev/null @@ -1,75 +0,0 @@ -// 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 templates contains functions for template processing of Resource objects. -package templates - -import ( - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/resource" - "github.com/gohugoio/hugo/tpl" - "github.com/pkg/errors" -) - -// Client contains methods to perform template processing of Resource objects. -type Client struct { - rs *resource.Spec - - textTemplate tpl.TemplateParseFinder -} - -// New creates a new Client with the given specification. -func New(rs *resource.Spec, textTemplate tpl.TemplateParseFinder) *Client { - if rs == nil { - panic("must provice a resource Spec") - } - if textTemplate == nil { - panic("must provide a textTemplate") - } - return &Client{rs: rs, textTemplate: textTemplate} -} - -type executeAsTemplateTransform struct { - rs *resource.Spec - textTemplate tpl.TemplateParseFinder - targetPath string - data interface{} -} - -func (t *executeAsTemplateTransform) Key() resource.ResourceTransformationKey { - return resource.NewResourceTransformationKey("execute-as-template", t.targetPath) -} - -func (t *executeAsTemplateTransform) Transform(ctx *resource.ResourceTransformationCtx) error { - tplStr := helpers.ReaderToString(ctx.From) - templ, err := t.textTemplate.Parse(ctx.InPath, tplStr) - if err != nil { - return errors.Wrapf(err, "failed to parse Resource %q as Template:", ctx.InPath) - } - - ctx.OutPath = t.targetPath - - return templ.Execute(ctx.To, t.data) -} - -func (c *Client) ExecuteAsTemplate(res resource.Resource, targetPath string, data interface{}) (resource.Resource, error) { - return c.rs.Transform( - res, - &executeAsTemplateTransform{ - rs: c.rs, - targetPath: helpers.ToSlashTrimLeading(targetPath), - textTemplate: c.textTemplate, - data: data, - }, - ) -} diff --git a/resource/tocss/scss/client.go b/resource/tocss/scss/client.go deleted file mode 100644 index 126727e0..00000000 --- a/resource/tocss/scss/client.go +++ /dev/null @@ -1,109 +0,0 @@ -// 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 scss - -import ( - "github.com/bep/go-tocss/scss" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugolib/filesystems" - "github.com/gohugoio/hugo/resource" - "github.com/mitchellh/mapstructure" -) - -type Client struct { - rs *resource.Spec - sfs *filesystems.SourceFilesystem - workFs *filesystems.SourceFilesystem -} - -func New(fs *filesystems.SourceFilesystem, rs *resource.Spec) (*Client, error) { - return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs}, nil -} - -type Options struct { - - // Hugo, will by default, just replace the extension of the source - // to .css, e.g. "scss/main.scss" becomes "scss/main.css". You can - // control this by setting this, e.g. "styles/main.css" will create - // a Resource with that as a base for RelPermalink etc. - TargetPath string - - // Hugo automatically adds the entry directories (where the main.scss lives) - // for project and themes to the list of include paths sent to LibSASS. - // Any paths set in this setting will be appended. Note that these will be - // treated as relative to the working dir, i.e. no include paths outside the - // project/themes. - IncludePaths []string - - // Default is nested. - // One of nested, expanded, compact, compressed. - OutputStyle string - - // Precision of floating point math. - Precision int - - // When enabled, Hugo will generate a source map. - EnableSourceMap bool -} - -type options struct { - // The options we receive from the end user. - from Options - - // The options we send to the SCSS library. - to scss.Options -} - -func (c *Client) ToCSS(res resource.Resource, opts Options) (resource.Resource, error) { - internalOptions := options{ - from: opts, - } - - // Transfer values from client. - internalOptions.to.Precision = opts.Precision - internalOptions.to.OutputStyle = scss.OutputStyleFromString(opts.OutputStyle) - - if internalOptions.to.Precision == 0 { - // bootstrap-sass requires 8 digits precision. The libsass default is 5. - // https://github.com/twbs/bootstrap-sass/blob/master/README.md#sass-number-precision - internalOptions.to.Precision = 8 - } - - return c.rs.Transform( - res, - &toCSSTransformation{c: c, options: internalOptions}, - ) -} - -type toCSSTransformation struct { - c *Client - options options -} - -func (t *toCSSTransformation) Key() resource.ResourceTransformationKey { - return resource.NewResourceTransformationKey("tocss", t.options.from) -} - -func DecodeOptions(m map[string]interface{}) (opts Options, err error) { - if m == nil { - return - } - err = mapstructure.WeakDecode(m, &opts) - - if opts.TargetPath != "" { - opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath) - } - - return -} diff --git a/resource/tocss/scss/tocss.go b/resource/tocss/scss/tocss.go deleted file mode 100644 index 984e14fc..00000000 --- a/resource/tocss/scss/tocss.go +++ /dev/null @@ -1,173 +0,0 @@ -// 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. - -// +build extended - -package scss - -import ( - "fmt" - "io" - "path" - "path/filepath" - "strings" - - "github.com/bep/go-tocss/scss" - "github.com/bep/go-tocss/scss/libsass" - "github.com/bep/go-tocss/tocss" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/resource" - "github.com/pkg/errors" -) - -// Used in tests. This feature requires Hugo to be built with the extended tag. -func Supports() bool { - return true -} - -func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { - ctx.OutMediaType = media.CSSType - - var outName string - if t.options.from.TargetPath != "" { - ctx.OutPath = t.options.from.TargetPath - } else { - ctx.ReplaceOutPathExtension(".css") - } - - outName = path.Base(ctx.OutPath) - - options := t.options - baseDir := path.Dir(ctx.SourcePath) - options.to.IncludePaths = t.c.sfs.RealDirs(baseDir) - - // Append any workDir relative include paths - for _, ip := range options.from.IncludePaths { - options.to.IncludePaths = append(options.to.IncludePaths, t.c.workFs.RealDirs(filepath.Clean(ip))...) - } - - // To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need - // to help libsass revolve the filename by looking in the composite filesystem first. - // We add the entry directories for both project and themes to the include paths list, but - // that only work for overrides on the top level. - options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) { - // We get URL paths from LibSASS, but we need file paths. - url = filepath.FromSlash(url) - prev = filepath.FromSlash(prev) - - var basePath string - urlDir := filepath.Dir(url) - var prevDir string - if prev == "stdin" { - prevDir = baseDir - } else { - prevDir = t.c.sfs.MakePathRelative(filepath.Dir(prev)) - if prevDir == "" { - // Not a member of this filesystem. Let LibSASS handle it. - return "", "", false - } - } - - basePath = filepath.Join(prevDir, urlDir) - name := filepath.Base(url) - - // Libsass throws an error in cases where you have several possible candidates. - // We make this simpler and pick the first match. - var namePatterns []string - if strings.Contains(name, ".") { - namePatterns = []string{"_%s", "%s"} - } else if strings.HasPrefix(name, "_") { - namePatterns = []string{"_%s.scss", "_%s.sass"} - } else { - namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"} - } - - name = strings.TrimPrefix(name, "_") - - for _, namePattern := range namePatterns { - filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name)) - fi, err := t.c.sfs.Fs.Stat(filenameToCheck) - if err == nil { - if fir, ok := fi.(hugofs.RealFilenameInfo); ok { - return fir.RealFilename(), "", true - } - } - } - - // Not found, let LibSASS handle it - return "", "", false - } - - if ctx.InMediaType.SubType == media.SASSType.SubType { - options.to.SassSyntax = true - } - - if options.from.EnableSourceMap { - - options.to.SourceMapFilename = outName + ".map" - options.to.SourceMapRoot = t.c.rs.WorkingDir - - // Setting this to the relative input filename will get the source map - // more correct for the main entry path (main.scss typically), but - // it will mess up the import mappings. As a workaround, we do a replacement - // in the source map itself (see below). - //options.InputPath = inputPath - options.to.OutputPath = outName - options.to.SourceMapContents = true - options.to.OmitSourceMapURL = false - options.to.EnableEmbeddedSourceMap = false - } - - res, err := t.c.toCSS(options.to, ctx.To, ctx.From) - if err != nil { - return err - } - - if options.from.EnableSourceMap && res.SourceMapContent != "" { - sourcePath := t.c.sfs.RealFilename(ctx.SourcePath) - - if strings.HasPrefix(sourcePath, t.c.rs.WorkingDir) { - sourcePath = strings.TrimPrefix(sourcePath, t.c.rs.WorkingDir+helpers.FilePathSeparator) - } - - // This needs to be Unix-style slashes, even on Windows. - // See https://github.com/gohugoio/hugo/issues/4968 - sourcePath = filepath.ToSlash(sourcePath) - - // This is a workaround for what looks like a bug in Libsass. But - // getting this resolution correct in tools like Chrome Workspaces - // is important enough to go this extra mile. - mapContent := strings.Replace(res.SourceMapContent, `stdin",`, fmt.Sprintf("%s\",", sourcePath), 1) - - return ctx.PublishSourceMap(mapContent) - } - return nil -} - -func (c *Client) toCSS(options scss.Options, dst io.Writer, src io.Reader) (tocss.Result, error) { - var res tocss.Result - - transpiler, err := libsass.New(options) - if err != nil { - return res, err - } - - res, err = transpiler.Execute(dst, src) - if err != nil { - return res, errors.Wrap(err, "SCSS processing failed") - } - - return res, nil -} diff --git a/resource/tocss/scss/tocss_notavailable.go b/resource/tocss/scss/tocss_notavailable.go deleted file mode 100644 index df918b36..00000000 --- a/resource/tocss/scss/tocss_notavailable.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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. - -// +build !extended - -package scss - -import ( - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/resource" -) - -// Used in tests. -func Supports() bool { - return false -} - -func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { - return herrors.ErrFeatureNotAvailable -} diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index a20b2e50..497611b7 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -23,13 +23,13 @@ import ( "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/minifier" - "github.com/gohugoio/hugo/resource/postcss" - "github.com/gohugoio/hugo/resource/templates" - "github.com/gohugoio/hugo/resource/tocss/scss" + "github.com/gohugoio/hugo/resource/resource_factories/bundler" + "github.com/gohugoio/hugo/resource/resource_factories/create" + "github.com/gohugoio/hugo/resource/resource_transformers/integrity" + "github.com/gohugoio/hugo/resource/resource_transformers/minifier" + "github.com/gohugoio/hugo/resource/resource_transformers/postcss" + "github.com/gohugoio/hugo/resource/resource_transformers/templates" + "github.com/gohugoio/hugo/resource/resource_transformers/tocss/scss" "github.com/spf13/cast" )