]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
tocss: Add vars option
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 19 Dec 2022 17:49:02 +0000 (18:49 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 20 Dec 2022 18:36:30 +0000 (19:36 +0100)
This commit adds a new `vars` option to both the Sass transpilers (Dart Sass and Libsass).

This means that you can pass a map with key/value pairs to the transpiler:

```handlebars
{{ $vars := dict "$color1" "blue" "$color2" "green" "$font_size" "24px" }}
{{ $cssOpts := (dict "transpiler" "dartsass" "outputStyle" "compressed" "vars" $vars ) }}
{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts }}
```

And the the variables will be available in the `hugo:vars` namespace. Example usage for Dart Sass:

```scss
@use "hugo:vars" as v;

p {
    color: v.$color1;
    font-size: v.$font_size;
}
```

Note that Libsass does not support the `use` keyword, so you need to `import` them as global variables:

```scss
@import "hugo:vars";

p {
    color: $color1;
    font-size: $font_size;
}
```

Hugo will:

* Add a missing leading `$` for the variable names if needed.
* Wrap the values in `unquote('VALUE')` (Sass built-in) to get proper handling of identifiers vs other strings.

This means that you can pull variables directly from e.g. the site config:

```toml
[params]
[params.sassvars]
color1 = "blue"
color2 = "green"
font_size = "24px"
image = "images/hero.jpg"
```

```handlebars
{{ $vars := site.Params.sassvars}}
{{ $cssOpts := (dict "transpiler" "dartsass" "outputStyle" "compressed" "vars" $vars ) }}
{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts }}
```

Fixes #10555

resources/resource_transformers/tocss/dartsass/client.go
resources/resource_transformers/tocss/dartsass/integration_test.go
resources/resource_transformers/tocss/dartsass/transform.go
resources/resource_transformers/tocss/internal/sass/helpers.go [new file with mode: 0644]
resources/resource_transformers/tocss/scss/client.go
resources/resource_transformers/tocss/scss/integration_test.go
resources/resource_transformers/tocss/scss/tocss.go

index f5f42a6dce39273880a1e5871de95e3b90f34f03..49cafb52d6b7b18150327dd3247e784514ee4aea 100644 (file)
@@ -93,6 +93,7 @@ func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result,
        var res godartsass.Result
 
        in := helpers.ReaderToString(src)
+
        args.Source = in
 
        res, err := c.transpiler.Execute(args)
@@ -130,6 +131,11 @@ type Options struct {
 
        // If enabled, sources will be embedded in the generated source map.
        SourceMapIncludeSources bool
+
+       // Vars will be available in 'hugo:vars', e.g:
+       //     @use "hugo:vars";
+       //     $color: vars.$color;
+       Vars map[string]string
 }
 
 func decodeOptions(m map[string]any) (opts Options, err error) {
index c127057a5a40e3b42d85a3924cb49b4000ddb040..083cef14f3a72f745dcef33cad9050534e309037 100644 (file)
@@ -24,6 +24,7 @@ import (
 )
 
 func TestTransformIncludePaths(t *testing.T) {
+       t.Parallel()
        if !dartsass.Supports() {
                t.Skip()
        }
@@ -55,6 +56,7 @@ T1: {{ $r.Content }}
 }
 
 func TestTransformImportRegularCSS(t *testing.T) {
+       t.Parallel()
        if !dartsass.Supports() {
                t.Skip()
        }
@@ -108,6 +110,7 @@ T1: {{ $r.Content | safeHTML }}
 }
 
 func TestTransformThemeOverrides(t *testing.T) {
+       t.Parallel()
        if !dartsass.Supports() {
                t.Skip()
        }
@@ -169,6 +172,7 @@ zoo {
 }
 
 func TestTransformLogging(t *testing.T) {
+       t.Parallel()
        if !dartsass.Supports() {
                t.Skip()
        }
@@ -200,6 +204,7 @@ T1: {{ $r.Content }}
 }
 
 func TestTransformErrors(t *testing.T) {
+       t.Parallel()
        if !dartsass.Supports() {
                t.Skip()
        }
@@ -271,3 +276,93 @@ T1: {{ $r.Content }}
        })
 
 }
+
+func TestOptionVars(t *testing.T) {
+       t.Parallel()
+       if !dartsass.Supports() {
+               t.Skip()
+       }
+
+       files := `
+-- assets/scss/main.scss --
+@use "hugo:vars";
+
+body {
+       body {
+               background: url(vars.$image) no-repeat center/cover;
+         }       
+}
+
+p {
+       color: vars.$color1;
+       font-size: vars.$font_size;
+}
+
+b {
+       color: vars.$color2;
+}
+-- layouts/index.html --
+{{ $image := "images/hero.jpg" }}
+{{ $vars := dict "$color1" "blue" "$color2" "green" "font_size" "24px" "image" $image }}
+{{ $cssOpts := (dict "transpiler" "dartsass" "outputStyle" "compressed" "vars" $vars ) }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts }}
+T1: {{ $r.Content }}
+       `
+
+       b := hugolib.NewIntegrationTestBuilder(
+               hugolib.IntegrationTestConfig{
+                       T:           t,
+                       TxtarString: files,
+                       NeedsOsFS:   true,
+               }).Build()
+
+       b.AssertFileContent("public/index.html", `T1: body body{background:url(images/hero.jpg) no-repeat center/cover}p{color:blue;font-size:24px}b{color:green}`)
+}
+
+func TestOptionVarsParams(t *testing.T) {
+       t.Parallel()
+       if !dartsass.Supports() {
+               t.Skip()
+       }
+
+       files := `
+-- config.toml --
+[params]
+[params.sassvars]
+color1 = "blue"
+color2 = "green"
+font_size = "24px"
+image = "images/hero.jpg"
+-- assets/scss/main.scss --
+@use "hugo:vars";
+
+body {
+       body {
+               background: url(vars.$image) no-repeat center/cover;
+         }       
+}
+
+p {
+       color: vars.$color1;
+       font-size: vars.$font_size;
+}
+
+b {
+       color: vars.$color2;
+}
+-- layouts/index.html --
+{{ $vars := site.Params.sassvars}}
+{{ $cssOpts := (dict "transpiler" "dartsass" "outputStyle" "compressed" "vars" $vars ) }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts }}
+T1: {{ $r.Content }}
+       `
+
+       b := hugolib.NewIntegrationTestBuilder(
+               hugolib.IntegrationTestConfig{
+                       T:           t,
+                       TxtarString: files,
+                       NeedsOsFS:   true,
+               }).Build()
+
+       b.AssertFileContent("public/index.html", `T1: body body{background:url(images/hero.jpg) no-repeat center/cover}p{color:blue;font-size:24px}b{color:green}`)
+}
index 3aca916fc2c824ada3ed9162dd95be166355ee94..1a5b81b47c3346c3e7eb49f7bcdf72eb5726b52f 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright 2020 The Hugo Authors. All rights reserved.
+// Copyright 2022 The Hugo Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import (
        "github.com/gohugoio/hugo/resources"
 
        "github.com/gohugoio/hugo/resources/internal"
+       "github.com/gohugoio/hugo/resources/resource_transformers/tocss/internal/sass"
 
        "github.com/spf13/afero"
 
@@ -84,6 +85,8 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
                ImportResolver: importResolver{
                        baseDir: baseDir,
                        c:       t.c,
+
+                       varsStylesheet: sass.CreateVarsStyleSheet(opts.Vars),
                },
                OutputStyle:             godartsass.ParseOutputStyle(opts.OutputStyle),
                EnableSourceMap:         opts.EnableSourceMap,
@@ -128,9 +131,14 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
 type importResolver struct {
        baseDir string
        c       *Client
+
+       varsStylesheet string
 }
 
 func (t importResolver) CanonicalizeURL(url string) (string, error) {
+       if url == sass.HugoVarsNamespace {
+               return url, nil
+       }
        filePath, isURL := paths.UrlToFilename(url)
        var prevDir string
        var pathDir string
@@ -177,6 +185,9 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
 }
 
 func (t importResolver) Load(url string) (string, error) {
+       if url == sass.HugoVarsNamespace {
+               return t.varsStylesheet, nil
+       }
        filename, _ := paths.UrlToFilename(url)
        b, err := afero.ReadFile(hugofs.Os, filename)
        return string(b), err
diff --git a/resources/resource_transformers/tocss/internal/sass/helpers.go b/resources/resource_transformers/tocss/internal/sass/helpers.go
new file mode 100644 (file)
index 0000000..8128fd4
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package sass
+
+import (
+       "fmt"
+       "sort"
+       "strings"
+)
+
+const (
+       HugoVarsNamespace = "hugo:vars"
+)
+
+func CreateVarsStyleSheet(vars map[string]string) string {
+       if vars == nil {
+               return ""
+       }
+       var varsStylesheet string
+
+       var varsSlice []string
+       for k, v := range vars {
+               var prefix string
+               if !strings.HasPrefix(k, "$") {
+                       prefix = "$"
+               }
+               // These variables can be a combination of Sass identifiers (e.g. sans-serif), which
+               // should not be quoted, and URLs et, which should be quoted.
+               // unquote() is knowing what to do with each.
+               varsSlice = append(varsSlice, fmt.Sprintf("%s%s: unquote('%s');", prefix, k, v))
+       }
+       sort.Strings(varsSlice)
+       varsStylesheet = strings.Join(varsSlice, "\n")
+
+       return varsStylesheet
+
+}
index ecaceaa6c2b47dea4549c574532631f9fa4059a0..0d027d8880bc1097007755b82623cae5a769501f 100644 (file)
@@ -60,6 +60,10 @@ type Options struct {
 
        // When enabled, Hugo will generate a source map.
        EnableSourceMap bool
+
+       // Vars will be available in 'hugo:vars', e.g:
+       //     @import "hugo:vars";
+       Vars map[string]string
 }
 
 func DecodeOptions(m map[string]any) (opts Options, err error) {
index 13b664cc723e6ef1d5cf82c7b53a8b7e6ffdf268..799c70ee591d246b4528df4f5148c83b0adcfa44 100644 (file)
@@ -25,6 +25,7 @@ import (
 )
 
 func TestTransformIncludePaths(t *testing.T) {
+       t.Parallel()
        if !scss.Supports() {
                t.Skip()
        }
@@ -57,6 +58,7 @@ T1: {{ $r.Content }}
 }
 
 func TestTransformImportRegularCSS(t *testing.T) {
+       t.Parallel()
        if !scss.Supports() {
                t.Skip()
        }
@@ -113,6 +115,7 @@ moo {
 }
 
 func TestTransformThemeOverrides(t *testing.T) {
+       t.Parallel()
        if !scss.Supports() {
                t.Skip()
        }
@@ -175,6 +178,7 @@ zoo {
 }
 
 func TestTransformErrors(t *testing.T) {
+       t.Parallel()
        if !scss.Supports() {
                t.Skip()
        }
@@ -245,3 +249,45 @@ T1: {{ $r.Content }}
        })
 
 }
+
+func TestOptionVars(t *testing.T) {
+       t.Parallel()
+       if !scss.Supports() {
+               t.Skip()
+       }
+
+       files := `
+-- assets/scss/main.scss --
+@import "hugo:vars";
+
+body {
+       body {
+               background: url($image) no-repeat center/cover;
+         }       
+}
+
+p {
+       color: $color1;
+       font-size: var$font_size;
+}
+
+b {
+       color: $color2;
+}
+-- layouts/index.html --
+{{ $image := "images/hero.jpg" }}
+{{ $vars := dict "$color1" "blue" "$color2" "green" "font_size" "24px" "image" $image }}
+{{ $cssOpts := (dict "transpiler" "libsass" "outputStyle" "compressed" "vars" $vars ) }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts }}
+T1: {{ $r.Content }}
+       `
+
+       b := hugolib.NewIntegrationTestBuilder(
+               hugolib.IntegrationTestConfig{
+                       T:           t,
+                       TxtarString: files,
+                       NeedsOsFS:   true,
+               }).Build()
+
+       b.AssertFileContent("public/index.html", `T1: body body{background:url(images/hero.jpg) no-repeat center/cover}p{color:blue;font-size:var 24px}b{color:green}`)
+}
index 10bd1f6f8d8cd53eb7218103dda3e2b1a852db9b..7e44f327e6f00c45489624d7b408a3c662d7806a 100644 (file)
@@ -31,6 +31,7 @@ import (
        "github.com/gohugoio/hugo/hugofs"
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/resources"
+       "github.com/gohugoio/hugo/resources/resource_transformers/tocss/internal/sass"
 )
 
 // Used in tests. This feature requires Hugo to be built with the extended tag.
@@ -63,11 +64,17 @@ func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx
                }
        }
 
+       varsStylesheet := sass.CreateVarsStyleSheet(options.from.Vars)
+
        // 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) {
+               if url == sass.HugoVarsNamespace {
+                       return url, varsStylesheet, true
+               }
+
                // We get URL paths from LibSASS, but we need file paths.
                url = filepath.FromSlash(url)
                prev = filepath.FromSlash(prev)