Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2020-12-23 11:26:23 +0300
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2020-12-30 19:32:25 +0300
commitcea157402365f34a69882110a4208999728007a6 (patch)
treebc29f699e7c901c219cffc5f50fba99dca53d5bd /resources
parentf9f779786edcefc4449a14cfc04dd93379f71373 (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.go115
-rw-r--r--resources/resource_transformers/tocss/dartsass/transform.go222
-rw-r--r--resources/transform.go6
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