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/resource_transformers/tocss/dartsass/transform.go | |
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/resource_transformers/tocss/dartsass/transform.go')
-rw-r--r-- | resources/resource_transformers/tocss/dartsass/transform.go | 222 |
1 files changed, 222 insertions, 0 deletions
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 +} |