From 41a080b26877a737e74444f83fe54c46a9c9f6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 19 Dec 2022 18:49:02 +0100 Subject: 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 --- .../resource_transformers/tocss/dartsass/client.go | 6 ++ .../tocss/dartsass/integration_test.go | 95 ++++++++++++++++++++++ .../tocss/dartsass/transform.go | 13 ++- .../tocss/internal/sass/helpers.go | 48 +++++++++++ .../resource_transformers/tocss/scss/client.go | 4 + .../tocss/scss/integration_test.go | 46 +++++++++++ .../resource_transformers/tocss/scss/tocss.go | 7 ++ 7 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 resources/resource_transformers/tocss/internal/sass/helpers.go (limited to 'resources/resource_transformers/tocss') diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go index f5f42a6dc..49cafb52d 100644 --- 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) { diff --git a/resources/resource_transformers/tocss/dartsass/integration_test.go b/resources/resource_transformers/tocss/dartsass/integration_test.go index c127057a5..083cef14f 100644 --- 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}`) +} diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go index 3aca916fc..1a5b81b47 100644 --- 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 diff --git a/resources/resource_transformers/tocss/internal/sass/helpers.go b/resources/resource_transformers/tocss/internal/sass/helpers.go new file mode 100644 index 000000000..8128fd4d7 --- /dev/null +++ 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 + +} diff --git a/resources/resource_transformers/tocss/scss/client.go b/resources/resource_transformers/tocss/scss/client.go index ecaceaa6c..0d027d888 100644 --- 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) { diff --git a/resources/resource_transformers/tocss/scss/integration_test.go b/resources/resource_transformers/tocss/scss/integration_test.go index 13b664cc7..799c70ee5 100644 --- 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}`) +} diff --git a/resources/resource_transformers/tocss/scss/tocss.go b/resources/resource_transformers/tocss/scss/tocss.go index 10bd1f6f8..7e44f327e 100644 --- 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) -- cgit v1.2.3