diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 61ae7e611..3d13d3912 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -79,6 +79,76 @@ T1: {{ $r.Content }} } +func TestSCSSWithThemeOverrides(t *testing.T) { + if !scss.Supports() { + t.Skip("Skip SCSS") + } + assert := require.New(t) + workDir, clean, err := createTempDir("hugo-scss-include") + assert.NoError(err) + defer clean() + + theme := "mytheme" + themesDir := filepath.Join(workDir, "themes") + themeDirs := filepath.Join(themesDir, theme) + v := viper.New() + v.Set("workingDir", workDir) + v.Set("theme", theme) + b := newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger()) + b.WithViper(v) + b.WithWorkingDir(workDir) + // Need to use OS fs for this. + b.Fs = hugofs.NewDefault(v) + + fooDir := filepath.Join(workDir, "node_modules", "foo") + scssDir := filepath.Join(workDir, "assets", "scss") + scssThemeDir := filepath.Join(themeDirs, "assets", "scss") + assert.NoError(os.MkdirAll(fooDir, 0777)) + assert.NoError(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777)) + assert.NoError(os.MkdirAll(filepath.Join(workDir, "data"), 0777)) + assert.NoError(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777)) + assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777)) + assert.NoError(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777)) + assert.NoError(os.MkdirAll(filepath.Join(scssDir, "components"), 0777)) + assert.NoError(os.MkdirAll(filepath.Join(scssThemeDir, "components"), 0777)) + + b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_imports.scss"), ` +@import "moo"; + +`) + + b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_moo.scss"), ` +$moolor: #fff; + +moo { + color: $moolor; +} +`) + + b.WithSourceFile(filepath.Join(scssThemeDir, "main.scss"), ` +@import "components/imports"; + +`) + + b.WithSourceFile(filepath.Join(scssDir, "components", "_moo.scss"), ` +$moolor: #ccc; + +moo { + color: $moolor; +} +`) + + b.WithTemplatesAdded("index.html", ` +{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }} +{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }} +T1: {{ $r.Content }} +`) + b.Build(BuildCfg{}) + + b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#ccc}`) + +} + func TestResourceChain(t *testing.T) { t.Parallel() diff --git a/resource/tocss/scss/tocss.go b/resource/tocss/scss/tocss.go index 715d5fd9f..c50b054b7 100644 --- a/resource/tocss/scss/tocss.go +++ b/resource/tocss/scss/tocss.go @@ -26,6 +26,7 @@ import ( "github.com/bep/go-tocss/scss/libsass" "github.com/bep/go-tocss/tocss" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resource" ) @@ -48,14 +49,64 @@ func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) outName = path.Base(ctx.OutPath) options := t.options - - options.to.IncludePaths = t.c.sfs.RealDirs(path.Dir(ctx.SourcePath)) + baseDir := path.Dir(ctx.SourcePath) + options.to.IncludePaths = t.c.sfs.RealDirs(baseDir) // Append any workDir relative include paths for _, ip := range options.from.IncludePaths { options.to.IncludePaths = append(options.to.IncludePaths, t.c.workFs.RealDirs(filepath.Clean(ip))...) } + // To allow for overrides of SCSS files anywhere in the project/theme hierarchy, we need + // to help libsass revolve the filename by looking in the composite filesystem first. + // We add the entry directories for both project and themes to the include paths list, but + // that only work for overrides on the top level. + options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) { + // We get URL paths from LibSASS, but we need file paths. + url = filepath.FromSlash(url) + prev = filepath.FromSlash(prev) + + var basePath string + urlDir := filepath.Dir(url) + var prevDir string + if prev == "stdin" { + prevDir = baseDir + } else { + prevDir = t.c.sfs.MakePathRelative(filepath.Dir(prev)) + if prevDir == "" { + // Not a member of this filesystem. Let LibSASS handle it. + return "", "", false + } + } + + basePath = filepath.Join(prevDir, urlDir) + name := filepath.Base(url) + + // Libsass throws an error in cases where you have several possible candidates. + // We make this simpler and pick the first match. + var namePatterns []string + if strings.Contains(name, ".") { + namePatterns = []string{"_%s", "%s"} + } else if strings.HasPrefix(name, "_") { + namePatterns = []string{"_%s.scss", "_%s.sass"} + } else { + namePatterns = []string{"_%s.scss", "%s.scss", "_%s.sass", "%s.sass"} + } + + for _, namePattern := range namePatterns { + filenameToCheck := filepath.Join(basePath, fmt.Sprintf(namePattern, name)) + fi, err := t.c.sfs.Fs.Stat(filenameToCheck) + if err == nil { + if fir, ok := fi.(hugofs.RealFilenameInfo); ok { + return fir.RealFilename(), "", true + } + } + } + + // Not found, let LibSASS handle it + return "", "", false + } + if ctx.InMediaType.SubType == media.SASSType.SubType { options.to.SassSyntax = true }