tocss: Add vars option - hugo - [fork] hugo port for 9front
 (HTM) git clone git@git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 41a080b26877a737e74444f83fe54c46a9c9f6bc
 (DIR) parent 9a215d6950e6705f9109497e9f38cc3844172612
 (HTM) Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Mon, 19 Dec 2022 18:49:02 +0100
       
       tocss: Add vars option
       
       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
       
       Diffstat:
         M resources/resource_transformers/to… |       6 ++++++
         M resources/resource_transformers/to… |      95 ++++++++++++++++++++++++++++++
         M resources/resource_transformers/to… |      13 ++++++++++++-
         A resources/resource_transformers/to… |      48 +++++++++++++++++++++++++++++++
         M resources/resource_transformers/to… |       4 ++++
         M resources/resource_transformers/to… |      46 +++++++++++++++++++++++++++++++
         M resources/resource_transformers/to… |       7 +++++++
       
       7 files changed, 218 insertions(+), 1 deletion(-)
       ---
 (DIR) diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go
       @@ -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) {
 (DIR) diff --git a/resources/resource_transformers/tocss/dartsass/integration_test.go b/resources/resource_transformers/tocss/dartsass/integration_test.go
       @@ -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}`)
       +}
 (DIR) diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go
       @@ -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
 (DIR) diff --git a/resources/resource_transformers/tocss/internal/sass/helpers.go b/resources/resource_transformers/tocss/internal/sass/helpers.go
       @@ -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
       +
       +}
 (DIR) diff --git a/resources/resource_transformers/tocss/scss/client.go b/resources/resource_transformers/tocss/scss/client.go
       @@ -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) {
 (DIR) diff --git a/resources/resource_transformers/tocss/scss/integration_test.go b/resources/resource_transformers/tocss/scss/integration_test.go
       @@ -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}`)
       +}
 (DIR) diff --git a/resources/resource_transformers/tocss/scss/tocss.go b/resources/resource_transformers/tocss/scss/tocss.go
       @@ -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)