tocss.go (6255B)
1 // Copyright 2018 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 //go:build extended 15 // +build extended 16 17 package scss 18 19 import ( 20 "fmt" 21 "io" 22 "path" 23 24 "path/filepath" 25 "strings" 26 27 "github.com/bep/golibsass/libsass" 28 "github.com/bep/golibsass/libsass/libsasserrors" 29 "github.com/gohugoio/hugo/common/herrors" 30 "github.com/gohugoio/hugo/helpers" 31 "github.com/gohugoio/hugo/hugofs" 32 "github.com/gohugoio/hugo/media" 33 "github.com/gohugoio/hugo/resources" 34 ) 35 36 // Used in tests. This feature requires Hugo to be built with the extended tag. 37 func Supports() bool { 38 return true 39 } 40 41 func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx) error { 42 ctx.OutMediaType = media.CSSType 43 44 var outName string 45 if t.options.from.TargetPath != "" { 46 ctx.OutPath = t.options.from.TargetPath 47 } else { 48 ctx.ReplaceOutPathExtension(".css") 49 } 50 51 outName = path.Base(ctx.OutPath) 52 53 options := t.options 54 baseDir := path.Dir(ctx.SourcePath) 55 options.to.IncludePaths = t.c.sfs.RealDirs(baseDir) 56 57 // Append any workDir relative include paths 58 for _, ip := range options.from.IncludePaths { 59 info, err := t.c.workFs.Stat(filepath.Clean(ip)) 60 if err == nil { 61 filename := info.(hugofs.FileMetaInfo).Meta().Filename 62 options.to.IncludePaths = append(options.to.IncludePaths, filename) 63 } 64 } 65 66 // To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need 67 // to help libsass revolve the filename by looking in the composite filesystem first. 68 // We add the entry directories for both project and themes to the include paths list, but 69 // that only work for overrides on the top level. 70 options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) { 71 // We get URL paths from LibSASS, but we need file paths. 72 url = filepath.FromSlash(url) 73 prev = filepath.FromSlash(prev) 74 75 var basePath string 76 urlDir := filepath.Dir(url) 77 var prevDir string 78 79 if prev == "stdin" { 80 prevDir = baseDir 81 } else { 82 prevDir, _ = t.c.sfs.MakePathRelative(filepath.Dir(prev)) 83 84 if prevDir == "" { 85 // Not a member of this filesystem. Let LibSASS handle it. 86 return "", "", false 87 } 88 } 89 90 basePath = filepath.Join(prevDir, urlDir) 91 name := filepath.Base(url) 92 93 // Libsass throws an error in cases where you have several possible candidates. 94 // We make this simpler and pick the first match. 95 var namePatterns []string 96 if strings.Contains(name, ".") { 97 namePatterns = []string{"_%s", "%s"} 98 } else if strings.HasPrefix(name, "_") { 99 namePatterns = []string{"_%s.scss", "_%s.sass"} 100 } else { 101 namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"} 102 } 103 104 name = strings.TrimPrefix(name, "_") 105 106 for _, namePattern := range namePatterns { 107 filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name)) 108 fi, err := t.c.sfs.Fs.Stat(filenameToCheck) 109 if err == nil { 110 if fim, ok := fi.(hugofs.FileMetaInfo); ok { 111 return fim.Meta().Filename, "", true 112 } 113 } 114 } 115 116 // Not found, let LibSASS handle it 117 return "", "", false 118 } 119 120 if ctx.InMediaType.SubType == media.SASSType.SubType { 121 options.to.SassSyntax = true 122 } 123 124 if options.from.EnableSourceMap { 125 126 options.to.SourceMapOptions.Filename = outName + ".map" 127 options.to.SourceMapOptions.Root = t.c.rs.WorkingDir 128 129 // Setting this to the relative input filename will get the source map 130 // more correct for the main entry path (main.scss typically), but 131 // it will mess up the import mappings. As a workaround, we do a replacement 132 // in the source map itself (see below). 133 // options.InputPath = inputPath 134 options.to.SourceMapOptions.OutputPath = outName 135 options.to.SourceMapOptions.Contents = true 136 options.to.SourceMapOptions.OmitURL = false 137 options.to.SourceMapOptions.EnableEmbedded = false 138 } 139 140 res, err := t.c.toCSS(options.to, ctx.To, ctx.From) 141 if err != nil { 142 if sasserr, ok := err.(libsasserrors.Error); ok { 143 if sasserr.File == "stdin" && ctx.SourcePath != "" { 144 sasserr.File = t.c.sfs.RealFilename(ctx.SourcePath) 145 err = sasserr 146 } 147 } 148 return herrors.NewFileErrorFromFileInErr(err, hugofs.Os, nil) 149 150 } 151 152 if options.from.EnableSourceMap && res.SourceMapContent != "" { 153 sourcePath := t.c.sfs.RealFilename(ctx.SourcePath) 154 155 if strings.HasPrefix(sourcePath, t.c.rs.WorkingDir) { 156 sourcePath = strings.TrimPrefix(sourcePath, t.c.rs.WorkingDir+helpers.FilePathSeparator) 157 } 158 159 // This needs to be Unix-style slashes, even on Windows. 160 // See https://github.com/gohugoio/hugo/issues/4968 161 sourcePath = filepath.ToSlash(sourcePath) 162 163 // This is a workaround for what looks like a bug in Libsass. But 164 // getting this resolution correct in tools like Chrome Workspaces 165 // is important enough to go this extra mile. 166 mapContent := strings.Replace(res.SourceMapContent, `stdin",`, fmt.Sprintf("%s\",", sourcePath), 1) 167 168 return ctx.PublishSourceMap(mapContent) 169 } 170 return nil 171 } 172 173 func (c *Client) toCSS(options libsass.Options, dst io.Writer, src io.Reader) (libsass.Result, error) { 174 var res libsass.Result 175 176 transpiler, err := libsass.New(options) 177 if err != nil { 178 return res, err 179 } 180 181 in := helpers.ReaderToString(src) 182 183 // See https://github.com/gohugoio/hugo/issues/7059 184 // We need to preserve the regular CSS imports. This is by far 185 // a perfect solution, and only works for the main entry file, but 186 // that should cover many use cases, e.g. using SCSS as a preprocessor 187 // for Tailwind. 188 var importsReplaced bool 189 in, importsReplaced = replaceRegularImportsIn(in) 190 191 res, err = transpiler.Execute(in) 192 if err != nil { 193 return res, err 194 } 195 196 out := res.CSS 197 if importsReplaced { 198 out = replaceRegularImportsOut(out) 199 } 200 201 _, err = io.WriteString(dst, out) 202 203 return res, err 204 }