github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/resources/resource_transformers/tocss/dartsass/transform.go (about) 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 "net/url" 20 "path" 21 "path/filepath" 22 "strings" 23 24 "github.com/gohugoio/hugo/common/herrors" 25 "github.com/gohugoio/hugo/common/hexec" 26 "github.com/gohugoio/hugo/htesting" 27 "github.com/gohugoio/hugo/media" 28 29 "github.com/gohugoio/hugo/resources" 30 31 "github.com/gohugoio/hugo/resources/internal" 32 33 "github.com/spf13/afero" 34 35 "github.com/gohugoio/hugo/hugofs" 36 37 "github.com/bep/godartsass" 38 ) 39 40 const ( 41 // See https://github.com/sass/dart-sass-embedded/issues/24 42 stdinPlaceholder = "HUGOSTDIN" 43 dartSassEmbeddedBinaryName = "dart-sass-embedded" 44 ) 45 46 // Supports returns whether dart-sass-embedded is found in $PATH. 47 func Supports() bool { 48 if htesting.SupportsAll() { 49 return true 50 } 51 return hexec.InPath(dartSassEmbeddedBinaryName) 52 } 53 54 type transform struct { 55 optsm map[string]interface{} 56 c *Client 57 } 58 59 func (t *transform) Key() internal.ResourceTransformationKey { 60 return internal.NewResourceTransformationKey(transformationName, t.optsm) 61 } 62 63 func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error { 64 ctx.OutMediaType = media.CSSType 65 66 opts, err := decodeOptions(t.optsm) 67 if err != nil { 68 return err 69 } 70 71 if opts.TargetPath != "" { 72 ctx.OutPath = opts.TargetPath 73 } else { 74 ctx.ReplaceOutPathExtension(".css") 75 } 76 77 baseDir := path.Dir(ctx.SourcePath) 78 79 args := godartsass.Args{ 80 URL: stdinPlaceholder, 81 IncludePaths: t.c.sfs.RealDirs(baseDir), 82 ImportResolver: importResolver{ 83 baseDir: baseDir, 84 c: t.c, 85 }, 86 OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle), 87 EnableSourceMap: opts.EnableSourceMap, 88 } 89 90 // Append any workDir relative include paths 91 for _, ip := range opts.IncludePaths { 92 info, err := t.c.workFs.Stat(filepath.Clean(ip)) 93 if err == nil { 94 filename := info.(hugofs.FileMetaInfo).Meta().Filename 95 args.IncludePaths = append(args.IncludePaths, filename) 96 } 97 } 98 99 if ctx.InMediaType.SubType == media.SASSType.SubType { 100 args.SourceSyntax = godartsass.SourceSyntaxSASS 101 } 102 103 res, err := t.c.toCSS(args, ctx.From) 104 if err != nil { 105 if sassErr, ok := err.(godartsass.SassError); ok { 106 start := sassErr.Span.Start 107 context := strings.TrimSpace(sassErr.Span.Context) 108 filename, _ := urlToFilename(sassErr.Span.Url) 109 if filename == stdinPlaceholder { 110 if ctx.SourcePath == "" { 111 return sassErr 112 } 113 filename = t.c.sfs.RealFilename(ctx.SourcePath) 114 } 115 116 offsetMatcher := func(m herrors.LineMatcher) bool { 117 return m.Offset+len(m.Line) >= start.Offset && strings.Contains(m.Line, context) 118 } 119 120 ferr, ok := herrors.WithFileContextForFile( 121 herrors.NewFileError("scss", -1, -1, start.Column, sassErr), 122 filename, 123 filename, 124 hugofs.Os, 125 offsetMatcher) 126 127 if !ok { 128 return sassErr 129 } 130 131 return ferr 132 } 133 return err 134 } 135 136 out := res.CSS 137 138 _, err = io.WriteString(ctx.To, out) 139 if err != nil { 140 return err 141 } 142 143 if opts.EnableSourceMap && res.SourceMap != "" { 144 if err := ctx.PublishSourceMap(res.SourceMap); err != nil { 145 return err 146 } 147 _, err = fmt.Fprintf(ctx.To, "\n\n/*# sourceMappingURL=%s */", path.Base(ctx.OutPath)+".map") 148 } 149 150 return err 151 } 152 153 type importResolver struct { 154 baseDir string 155 c *Client 156 } 157 158 func (t importResolver) CanonicalizeURL(url string) (string, error) { 159 filePath, isURL := urlToFilename(url) 160 var prevDir string 161 var pathDir string 162 if isURL { 163 var found bool 164 prevDir, found = t.c.sfs.MakePathRelative(filepath.Dir(filePath)) 165 166 if !found { 167 // Not a member of this filesystem, let Dart Sass handle it. 168 return "", nil 169 } 170 } else { 171 prevDir = t.baseDir 172 pathDir = path.Dir(url) 173 } 174 175 basePath := filepath.Join(prevDir, pathDir) 176 name := filepath.Base(filePath) 177 178 // Pick the first match. 179 var namePatterns []string 180 if strings.Contains(name, ".") { 181 namePatterns = []string{"_%s", "%s"} 182 } else if strings.HasPrefix(name, "_") { 183 namePatterns = []string{"_%s.scss", "_%s.sass"} 184 } else { 185 namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"} 186 } 187 188 name = strings.TrimPrefix(name, "_") 189 190 for _, namePattern := range namePatterns { 191 filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name)) 192 fi, err := t.c.sfs.Fs.Stat(filenameToCheck) 193 if err == nil { 194 if fim, ok := fi.(hugofs.FileMetaInfo); ok { 195 return "file://" + filepath.ToSlash(fim.Meta().Filename), nil 196 } 197 } 198 } 199 200 // Not found, let Dart Dass handle it 201 return "", nil 202 } 203 204 func (t importResolver) Load(url string) (string, error) { 205 filename, _ := urlToFilename(url) 206 b, err := afero.ReadFile(hugofs.Os, filename) 207 return string(b), err 208 } 209 210 // TODO(bep) add tests 211 func urlToFilename(urls string) (string, bool) { 212 u, err := url.ParseRequestURI(urls) 213 if err != nil { 214 return filepath.FromSlash(urls), false 215 } 216 p := filepath.FromSlash(u.Path) 217 218 if u.Host != "" { 219 // C:\data\file.txt 220 p = strings.ToUpper(u.Host) + ":" + p 221 } 222 223 return p, true 224 }