transform.go (4451B)
1 // Copyright 2020 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package dartsass
15
16 import (
17 "fmt"
18 "io"
19 "path"
20 "path/filepath"
21 "strings"
22
23 "github.com/gohugoio/hugo/common/hexec"
24 "github.com/gohugoio/hugo/common/paths"
25 "github.com/gohugoio/hugo/htesting"
26 "github.com/gohugoio/hugo/media"
27
28 "github.com/gohugoio/hugo/resources"
29
30 "github.com/gohugoio/hugo/resources/internal"
31
32 "github.com/spf13/afero"
33
34 "github.com/gohugoio/hugo/hugofs"
35
36 "github.com/bep/godartsass"
37 )
38
39 const (
40 dartSassEmbeddedBinaryName = "dart-sass-embedded"
41 )
42
43 // Supports returns whether dart-sass-embedded is found in $PATH.
44 func Supports() bool {
45 if htesting.SupportsAll() {
46 return true
47 }
48 return hexec.InPath(dartSassEmbeddedBinaryName)
49 }
50
51 type transform struct {
52 optsm map[string]any
53 c *Client
54 }
55
56 func (t *transform) Key() internal.ResourceTransformationKey {
57 return internal.NewResourceTransformationKey(transformationName, t.optsm)
58 }
59
60 func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
61 ctx.OutMediaType = media.CSSType
62
63 opts, err := decodeOptions(t.optsm)
64 if err != nil {
65 return err
66 }
67
68 if opts.TargetPath != "" {
69 ctx.OutPath = opts.TargetPath
70 } else {
71 ctx.ReplaceOutPathExtension(".css")
72 }
73
74 baseDir := path.Dir(ctx.SourcePath)
75 filename := dartSassStdinPrefix
76
77 if ctx.SourcePath != "" {
78 filename += t.c.sfs.RealFilename(ctx.SourcePath)
79 }
80
81 args := godartsass.Args{
82 URL: filename,
83 IncludePaths: t.c.sfs.RealDirs(baseDir),
84 ImportResolver: importResolver{
85 baseDir: baseDir,
86 c: t.c,
87 },
88 OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle),
89 EnableSourceMap: opts.EnableSourceMap,
90 }
91
92 // Append any workDir relative include paths
93 for _, ip := range opts.IncludePaths {
94 info, err := t.c.workFs.Stat(filepath.Clean(ip))
95 if err == nil {
96 filename := info.(hugofs.FileMetaInfo).Meta().Filename
97 args.IncludePaths = append(args.IncludePaths, filename)
98 }
99 }
100
101 if ctx.InMediaType.SubType == media.SASSType.SubType {
102 args.SourceSyntax = godartsass.SourceSyntaxSASS
103 }
104
105 res, err := t.c.toCSS(args, ctx.From)
106 if err != nil {
107 return err
108 }
109
110 out := res.CSS
111
112 _, err = io.WriteString(ctx.To, out)
113 if err != nil {
114 return err
115 }
116
117 if opts.EnableSourceMap && res.SourceMap != "" {
118 if err := ctx.PublishSourceMap(res.SourceMap); err != nil {
119 return err
120 }
121 _, err = fmt.Fprintf(ctx.To, "\n\n/*# sourceMappingURL=%s */", path.Base(ctx.OutPath)+".map")
122 }
123
124 return err
125 }
126
127 type importResolver struct {
128 baseDir string
129 c *Client
130 }
131
132 func (t importResolver) CanonicalizeURL(url string) (string, error) {
133 filePath, isURL := paths.UrlToFilename(url)
134 var prevDir string
135 var pathDir string
136 if isURL {
137 var found bool
138 prevDir, found = t.c.sfs.MakePathRelative(filepath.Dir(filePath))
139
140 if !found {
141 // Not a member of this filesystem, let Dart Sass handle it.
142 return "", nil
143 }
144 } else {
145 prevDir = t.baseDir
146 pathDir = path.Dir(url)
147 }
148
149 basePath := filepath.Join(prevDir, pathDir)
150 name := filepath.Base(filePath)
151
152 // Pick the first match.
153 var namePatterns []string
154 if strings.Contains(name, ".") {
155 namePatterns = []string{"_%s", "%s"}
156 } else if strings.HasPrefix(name, "_") {
157 namePatterns = []string{"_%s.scss", "_%s.sass"}
158 } else {
159 namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"}
160 }
161
162 name = strings.TrimPrefix(name, "_")
163
164 for _, namePattern := range namePatterns {
165 filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name))
166 fi, err := t.c.sfs.Fs.Stat(filenameToCheck)
167 if err == nil {
168 if fim, ok := fi.(hugofs.FileMetaInfo); ok {
169 return "file://" + filepath.ToSlash(fim.Meta().Filename), nil
170 }
171 }
172 }
173
174 // Not found, let Dart Dass handle it
175 return "", nil
176 }
177
178 func (t importResolver) Load(url string) (string, error) {
179 filename, _ := paths.UrlToFilename(url)
180 b, err := afero.ReadFile(hugofs.Os, filename)
181 return string(b), err
182 }