2020-12-23 03:26:23 -05:00
|
|
|
// 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,
|
|
|
|
},
|
2020-12-31 04:49:39 -05:00
|
|
|
OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle),
|
2020-12-23 03:26:23 -05:00
|
|
|
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 {
|
2021-07-13 05:41:02 -04:00
|
|
|
filename := info.(hugofs.FileMetaInfo).Meta().Filename
|
2020-12-23 03:26:23 -05:00
|
|
|
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 {
|
2021-07-13 05:41:02 -04:00
|
|
|
return "file://" + filepath.ToSlash(fim.Meta().Filename), nil
|
2020-12-23 03:26:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|