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