diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2020-12-23 09:26:23 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2020-12-30 17:32:25 +0100 |
commit | cea157402365f34a69882110a4208999728007a6 (patch) | |
tree | bc29f699e7c901c219cffc5f50fba99dca53d5bd /resources | |
parent | f9f779786edcefc4449a14cfc04dd93379f71373 (diff) |
Add Dart Sass support
But note that the Dart Sass Embedded Protocol is still in beta (beta 5), a main release scheduled for Q1 2021.
Fixes #7380
Fixes #8102
Diffstat (limited to 'resources')
-rw-r--r-- | resources/resource_transformers/tocss/dartsass/client.go | 115 | ||||
-rw-r--r-- | resources/resource_transformers/tocss/dartsass/transform.go | 222 | ||||
-rw-r--r-- | resources/transform.go | 6 |
3 files changed, 343 insertions, 0 deletions
diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go new file mode 100644 index 000000000..1d8250dc5 --- /dev/null +++ b/resources/resource_transformers/tocss/dartsass/client.go @@ -0,0 +1,115 @@ +// Copyright 2020 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 godartsass integrates with the Dass Sass Embedded protocol to transpile +// SCSS/SASS. +package dartsass + +import ( + "io" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib/filesystems" + "github.com/gohugoio/hugo/resources" + "github.com/gohugoio/hugo/resources/resource" + "github.com/spf13/afero" + + "github.com/bep/godartsass" + "github.com/mitchellh/mapstructure" +) + +// used as part of the cache key. +const transformationName = "tocss-dart" + +func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) (*Client, error) { + if !Supports() { + return &Client{dartSassNoAvailable: true}, nil + } + transpiler, err := godartsass.Start(godartsass.Options{}) + if err != nil { + return nil, err + } + return &Client{sfs: fs, workFs: rs.BaseFs.Work, rs: rs, transpiler: transpiler}, nil +} + +type Client struct { + dartSassNoAvailable bool + rs *resources.Spec + sfs *filesystems.SourceFilesystem + workFs afero.Fs + transpiler *godartsass.Transpiler +} + +func (c *Client) ToCSS(res resources.ResourceTransformer, args map[string]interface{}) (resource.Resource, error) { + if c.dartSassNoAvailable { + return res.Transform(resources.NewFeatureNotAvailableTransformer(transformationName, args)) + } + return res.Transform(&transform{c: c, optsm: args}) +} + +func (c *Client) Close() error { + if c.transpiler == nil { + return nil + } + return c.transpiler.Close() +} + +func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result, error) { + var res godartsass.Result + + in := helpers.ReaderToString(src) + args.Source = in + + res, err := c.transpiler.Execute(args) + if err != nil { + return res, err + } + + return res, err +} + +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 + + // When enabled, Hugo will generate a source map. + EnableSourceMap bool +} + +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/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go new file mode 100644 index 000000000..4cbb35fb9 --- /dev/null +++ b/resources/resource_transformers/tocss/dartsass/transform.go @@ -0,0 +1,222 @@ +// Copyright 2020 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 dartsass + +import ( + "fmt" + "io" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/cli/safeexec" + + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/htesting" + "github.com/gohugoio/hugo/media" + + "github.com/gohugoio/hugo/resources" + + "github.com/gohugoio/hugo/resources/internal" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/hugofs" + + "github.com/bep/godartsass" +) + +// See https://github.com/sass/dart-sass-embedded/issues/24 +const stdinPlaceholder = "HUGOSTDIN" + +// Supports returns whether dart-sass-embedded is found in $PATH. +func Supports() bool { + if htesting.SupportsAll() { + return true + } + p, err := safeexec.LookPath("dart-sass-embedded") + return err == nil && p != "" +} + +type transform struct { + optsm map[string]interface{} + c *Client +} + +func (t *transform) Key() internal.ResourceTransformationKey { + return internal.NewResourceTransformationKey(transformationName, t.optsm) +} + +func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error { + ctx.OutMediaType = media.CSSType + + opts, err := decodeOptions(t.optsm) + if err != nil { + return err + } + + if opts.TargetPath != "" { + ctx.OutPath = opts.TargetPath + } else { + ctx.ReplaceOutPathExtension(".css") + } + + baseDir := path.Dir(ctx.SourcePath) + + args := godartsass.Args{ + URL: stdinPlaceholder, + IncludePaths: t.c.sfs.RealDirs(baseDir), + ImportResolver: importResolver{ + baseDir: baseDir, + c: t.c, + }, + EnableSourceMap: opts.EnableSourceMap, + } + + // Append any workDir relative include paths + for _, ip := range opts.IncludePaths { + info, err := t.c.workFs.Stat(filepath.Clean(ip)) + if err == nil { + filename := info.(hugofs.FileMetaInfo).Meta().Filename() + args.IncludePaths = append(args.IncludePaths, filename) + } + } + + if ctx.InMediaType.SubType == media.SASSType.SubType { + args.SourceSyntax = godartsass.SourceSyntaxSASS + } + + res, err := t.c.toCSS(args, ctx.From) + if err != nil { + if sassErr, ok := err.(godartsass.SassError); ok { + start := sassErr.Span.Start + context := strings.TrimSpace(sassErr.Span.Context) + filename, _ := urlToFilename(sassErr.Span.Url) + if filename == stdinPlaceholder { + if ctx.SourcePath == "" { + return sassErr + } + filename = t.c.sfs.RealFilename(ctx.SourcePath) + } + + offsetMatcher := func(m herrors.LineMatcher) bool { + return m.Offset+len(m.Line) >= start.Offset && strings.Contains(m.Line, context) + } + + ferr, ok := herrors.WithFileContextForFile( + herrors.NewFileError("scss", -1, -1, start.Column, sassErr), + filename, + filename, + hugofs.Os, + offsetMatcher) + + if !ok { + return sassErr + } + + return ferr + } + return err + } + + out := res.CSS + + _, err = io.WriteString(ctx.To, out) + if err != nil { + return err + } + + if opts.EnableSourceMap && res.SourceMap != "" { + if err := ctx.PublishSourceMap(res.SourceMap); err != nil { + return err + } + _, err = fmt.Fprintf(ctx.To, "\n\n/*# sourceMappingURL=%s */", path.Base(ctx.OutPath)+".map") + } + + return err +} + +type importResolver struct { + baseDir string + c *Client +} + +func (t importResolver) CanonicalizeURL(url string) (string, error) { + filePath, isURL := urlToFilename(url) + var prevDir string + var pathDir string + if isURL { + var found bool + prevDir, found = t.c.sfs.MakePathRelative(filepath.Dir(filePath)) + + if !found { + // Not a member of this filesystem, let Dart Sass handle it. + return "", nil + } + } else { + prevDir = t.baseDir + pathDir = path.Dir(url) + } + + basePath := filepath.Join(prevDir, pathDir) + name := filepath.Base(filePath) + + // 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 fim, ok := fi.(hugofs.FileMetaInfo); ok { + return "file://" + filepath.ToSlash(fim.Meta().Filename()), nil + } + } + } + + // Not found, let Dart Dass handle it + return "", nil +} + +func (t importResolver) Load(url string) (string, error) { + filename, _ := urlToFilename(url) + b, err := afero.ReadFile(hugofs.Os, filename) + return string(b), err +} + +// TODO(bep) add tests +func urlToFilename(urls string) (string, bool) { + u, err := url.ParseRequestURI(urls) + if err != nil { + return filepath.FromSlash(urls), false + } + p := filepath.FromSlash(u.Path) + + if u.Host != "" { + // C:\data\file.txt + p = strings.ToUpper(u.Host) + ":" + p + } + + return p, true +} diff --git a/resources/transform.go b/resources/transform.go index 9007ead18..f276f00c3 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -411,6 +411,9 @@ func (r *resourceAdapter) transform(publish, setContent bool) error { errMsg = ". Check your PostCSS installation; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/" } else if tr.Key().Name == "tocss" { errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS." + } else if tr.Key().Name == "tocss-dart" { + errMsg = ". You need dart-sass-embedded in your system $PATH." + } else if tr.Key().Name == "babel" { errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/" } @@ -442,6 +445,9 @@ func (r *resourceAdapter) transform(publish, setContent bool) error { if tryFileCache { f := r.target.tryTransformedFileCache(key, updates) if f == nil { + if err != nil { + return newErr(err) + } return newErr(errors.Errorf("resource %q not found in file cache", key)) } transformedContentr = f |