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