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