Repository: gohugoio/hugo (language: go)

Issue:
Transpiled hex-string color from config file cannot be used as color in css functions such as `lighten()` and `darken()`
### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.110.0-e32a493b7826d02763c3b79623952e625402b168+extended
</pre>

### Does this issue reproduce with the latest release?

Yes

### Context

Followed documentation from [v0.109.0](https://github.com/gohugoio/hugo/releases/tag/v0.109.0) relase notes as well as https://discourse.gohugo.io/t/initialize-sass-variables-from-hugo-templates/42053/1 for LibSass transpiler

**Cannot use css functions such as `lighten()` and `darken()`, if the value comes transpiled from config file, because it's not a color**

Setup is as follows…

config.yaml
```yaml
params:
  style:
    color_palette_white: '#fff'
```

style.sass
```css
@import "bulma/bulma"
@import "mystyle/core"
```

mystyle/core.scss
```css
@import "hugo:vars";

@import 'partials/something';
// and many more .scss imports
```

mystyle/partials/_something.scss
```css
.something {
  border-bottom: 1px solid lighten($color-palette-white, 5%);
}
```

error log
```
Start building sites … 
hugo v0.110.0-e32a493b7826d02763c3b79623952e625402b168+extended
Error: Error building site: TOCSS: failed to transform "style.sass" (text/x-sass): "/path/themes/my-theme/assets/mystyle/partials/_something.scss:101:34": argument `$color` of `lighten($color, $amount)` must be a color
Built in 266 ms
```



Discussion / hints:
It looks like everything is injected as a string. I was unable to pass numbers (e.g., RGB values per channel, opacity, etc.), and there doesn't appear to be any _simple_ way to cast in Sass.
I have labeled this a bug. Not sure what happens here, but we (I) surely need to look closer into how types/values are converted between Go and Dart.

Fix patch:
diff --git a/common/types/css/csstypes.go b/common/types/css/csstypes.go
new file mode 100644
index 00000000000..a31df00e768
--- /dev/null
+++ b/common/types/css/csstypes.go
@@ -0,0 +1,20 @@
+// Copyright 2023 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 css
+
+// QuotedString is a string that needs to be quoted in CSS.
+type QuotedString string
+
+// UnquotedString is a string that does not need to be quoted in CSS.
+type UnquotedString string
diff --git a/resources/resource_transformers/tocss/dartsass/client.go b/resources/resource_transformers/tocss/dartsass/client.go
index 49cafb52d6b..f45e6537abd 100644
--- a/resources/resource_transformers/tocss/dartsass/client.go
+++ b/resources/resource_transformers/tocss/dartsass/client.go
@@ -135,7 +135,7 @@ type Options struct {
 	// Vars will be available in 'hugo:vars', e.g:
 	//     @use "hugo:vars";
 	//     $color: vars.$color;
-	Vars map[string]string
+	Vars map[string]any
 }
 
 func decodeOptions(m map[string]any) (opts Options, err error) {
diff --git a/resources/resource_transformers/tocss/internal/sass/cssValues.go b/resources/resource_transformers/tocss/internal/sass/cssValues.go
new file mode 100644
index 00000000000..410e89e04a6
--- /dev/null
+++ b/resources/resource_transformers/tocss/internal/sass/cssValues.go
@@ -0,0 +1,81 @@
+// 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
+
+type cssValue struct {
+	prefix []string
+	suffix []string
+}
+
+var (
+	cssValues = cssValue{
+		// https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
+		prefix: []string{
+			"#",
+			"attr(",
+			"calc(",
+			"clamp(",
+			"hsl(",
+			"hwb(",
+			"lab(",
+			"lch(",
+			"max(",
+			"min(",
+			"minmax(",
+			"rgb(",
+		},
+		// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Values_and_Units#dimensions
+		suffix: []string{
+			"%",
+			"cap",
+			"ch",
+			"cm",
+			"cqb",
+			"cqh",
+			"cqi",
+			"cqmax",
+			"cqmin",
+			"cqw",
+			"deg",
+			"dpcm",
+			"dpi",
+			"dppx",
+			"em",
+			"ex",
+			"fr",
+			"grad",
+			"ic",
+			"in",
+			"lh",
+			"mm",
+			"ms",
+			"pc",
+			"pt",
+			"px",
+			"Q",
+			"rad",
+			"rem",
+			"rlh",
+			"s",
+			"turn",
+			"vb",
+			"vh",
+			"vi",
+			"vmax",
+			"vmin",
+			"vw",
+			"x",
+		},
+	}
+)
diff --git a/resources/resource_transformers/tocss/internal/sass/helpers.go b/resources/resource_transformers/tocss/internal/sass/helpers.go
index ae1de7daa3d..f0425015710 100644
--- a/resources/resource_transformers/tocss/internal/sass/helpers.go
+++ b/resources/resource_transformers/tocss/internal/sass/helpers.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Hugo Authors. All rights reserved.
+// Copyright 2023 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.
@@ -15,15 +15,18 @@ package sass
 
 import (
 	"fmt"
+	"regexp"
 	"sort"
 	"strings"
+
+	"github.com/gohugoio/hugo/common/types/css"
 )
 
 const (
 	HugoVarsNamespace = "hugo:vars"
 )
 
-func CreateVarsStyleSheet(vars map[string]string) string {
+func CreateVarsStyleSheet(vars map[string]any) string {
 	if vars == nil {
 		return ""
 	}
@@ -35,14 +38,56 @@ func CreateVarsStyleSheet(vars map[string]string) 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(%q);", prefix, k, v))
+
+		switch v.(type) {
+		case css.QuotedString:
+			// Marked by the user as a string that needs to be quoted.
+			varsSlice = append(varsSlice, fmt.Sprintf("%s%s: %q;", prefix, k, v))
+		default:
+			if isTypedCSSValue(v) {
+				// E.g. 24px, 1.5rem, 10%, hsl(0, 0%, 100%), calc(24px + 36px), #fff, #ffffff.
+				varsSlice = append(varsSlice, fmt.Sprintf("%s%s: %v;", prefix, k, v))
+			} else {
+				// unquote will preserve quotes around URLs etc. if needed.
+				varsSlice = append(varsSlice, fmt.Sprintf("%s%s: unquote(%q);", prefix, k, v))
+			}
+		}
 	}
 	sort.Strings(varsSlice)
 	varsStylesheet = strings.Join(varsSlice, "\n")
 
+	fmt.Println(varsStylesheet)
+
+	fmt.Println(varsStylesheet)
+
 	return varsStylesheet
 
 }
+
+var (
+	isCSSColor = regexp.MustCompile(`^#[0-9a-fA-F]{3,6}$`)
+	isCSSFunc  = regexp.MustCompile(`^([a-zA-Z-]+)\(`)
+	isCSSUnit  = regexp.MustCompile(`^([0-9]+)(\.[0-9]+)?([a-zA-Z-%]+)$`)
+)
+
+// isTypedCSSValue returns true if the given string is a CSS value that
+// we should preserve the type of, as in: Not wrap it in quotes.
+func isTypedCSSValue(v any) bool {
+	switch s := v.(type) {
+	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, css.UnquotedString:
+		return true
+	case string:
+		if isCSSColor.MatchString(s) {
+			return true
+		}
+		if isCSSFunc.MatchString(s) {
+			return true
+		}
+		if isCSSUnit.MatchString(s) {
+			return true
+		}
+
+	}
+
+	return false
+}
diff --git a/resources/resource_transformers/tocss/scss/client.go b/resources/resource_transformers/tocss/scss/client.go
index 0d027d8880b..2028163ff2e 100644
--- a/resources/resource_transformers/tocss/scss/client.go
+++ b/resources/resource_transformers/tocss/scss/client.go
@@ -63,7 +63,7 @@ type Options struct {
 
 	// Vars will be available in 'hugo:vars', e.g:
 	//     @import "hugo:vars";
-	Vars map[string]string
+	Vars map[string]any
 }
 
 func DecodeOptions(m map[string]any) (opts Options, err error) {
diff --git a/tpl/css/css.go b/tpl/css/css.go
new file mode 100644
index 00000000000..e1783334ee8
--- /dev/null
+++ b/tpl/css/css.go
@@ -0,0 +1,41 @@
+package css
+
+import (
+	"github.com/gohugoio/hugo/common/types/css"
+	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/tpl/internal"
+	"github.com/spf13/cast"
+)
+
+const name = "css"
+
+// Namespace provides template functions for the "css" namespace.
+type Namespace struct {
+}
+
+// Quoted returns a string that needs to be quoted in CSS.
+func (ns *Namespace) Quoted(v any) css.QuotedString {
+	s := cast.ToString(v)
+	return css.QuotedString(s)
+}
+
+// Unquoted returns a string that does not need to be quoted in CSS.
+func (ns *Namespace) Unquoted(v any) css.UnquotedString {
+	s := cast.ToString(v)
+	return css.UnquotedString(s)
+}
+
+func init() {
+	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+		ctx := &Namespace{}
+
+		ns := &internal.TemplateFuncsNamespace{
+			Name:    name,
+			Context: func(args ...any) (any, error) { return ctx, nil },
+		}
+
+		return ns
+	}
+
+	internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index e664bd6c5a9..b8102c75d82 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -36,6 +36,7 @@ import (
 	_ "github.com/gohugoio/hugo/tpl/collections"
 	_ "github.com/gohugoio/hugo/tpl/compare"
 	_ "github.com/gohugoio/hugo/tpl/crypto"
+	_ "github.com/gohugoio/hugo/tpl/css"
 	_ "github.com/gohugoio/hugo/tpl/data"
 	_ "github.com/gohugoio/hugo/tpl/debug"
 	_ "github.com/gohugoio/hugo/tpl/diagrams"


Test patch:
diff --git a/resources/resource_transformers/tocss/dartsass/integration_test.go b/resources/resource_transformers/tocss/dartsass/integration_test.go
index 3d7c07ba67d..e7432f12bc5 100644
--- a/resources/resource_transformers/tocss/dartsass/integration_test.go
+++ b/resources/resource_transformers/tocss/dartsass/integration_test.go
@@ -368,3 +368,72 @@ T1: {{ $r.Content }}
 
 	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 TestVarsCasting(t *testing.T) {
+	t.Parallel()
+	if !dartsass.Supports() {
+		t.Skip()
+	}
+
+	files := `
+-- config.toml --
+disableKinds = ["term", "taxonomy", "section", "page"]
+
+[params]
+[params.sassvars]
+color_hex = "#fff"
+color_rgb = "rgb(255, 255, 255)"
+color_hsl = "hsl(0, 0%, 100%)"
+dimension = "24px"
+percentage = "10%"
+flex = "5fr"
+name = "Hugo"
+url = "https://gohugo.io"
+integer = 32
+float = 3.14
+-- assets/scss/main.scss --
+@use "hugo:vars";
+@use "sass:meta";
+
+@debug meta.type-of(vars.$color_hex);
+@debug meta.type-of(vars.$color_rgb);
+@debug meta.type-of(vars.$color_hsl);
+@debug meta.type-of(vars.$dimension);
+@debug meta.type-of(vars.$percentage);
+@debug meta.type-of(vars.$flex);
+@debug meta.type-of(vars.$name);
+@debug meta.type-of(vars.$url);
+@debug meta.type-of(vars.$not_a_number);
+@debug meta.type-of(vars.$integer);
+@debug meta.type-of(vars.$float);
+@debug meta.type-of(vars.$a_number);
+-- layouts/index.html --
+{{ $vars := site.Params.sassvars}}
+{{ $vars = merge $vars (dict "not_a_number" ("32xxx" | css.Quoted) "a_number" ("234" | css.Unquoted) )}}
+{{ $cssOpts := (dict "transpiler" "dartsass" "vars" $vars ) }}
+{{ $r := resources.Get "scss/main.scss" |  toCSS $cssOpts }}
+T1: {{ $r.Content }}
+		`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+			LogLevel:    jww.LevelInfo,
+		}).Build()
+
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:3:0: color`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:4:0: color`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:5:0: color`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:6:0: number`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:7:0: number`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:8:0: number`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:9:0: string`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:10:0: string`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:11:0: string`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:12:0: number`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:13:0: number`)
+	b.AssertLogMatches(`INFO.*Dart Sass: .*assets.*main.scss:14:0: number`)
+
+}
diff --git a/resources/resource_transformers/tocss/internal/sass/helpers_test.go b/resources/resource_transformers/tocss/internal/sass/helpers_test.go
new file mode 100644
index 00000000000..56e73736ee9
--- /dev/null
+++ b/resources/resource_transformers/tocss/internal/sass/helpers_test.go
@@ -0,0 +1,44 @@
+// Copyright 2023 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 (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestIsUnquotedCSSValue(t *testing.T) {
+	c := qt.New(t)
+
+	for _, test := range []struct {
+		in  any
+		out bool
+	}{
+		{"24px", true},
+		{"1.5rem", true},
+		{"10%", true},
+		{"hsl(0, 0%, 100%)", true},
+		{"calc(24px + 36px)", true},
+		{"24xxx", true}, // a false positive.
+		{123, true},
+		{123.12, true},
+		{"#fff", true},
+		{"#ffffff", true},
+		{"#ffffffff", false},
+	} {
+		c.Assert(isTypedCSSValue(test.in), qt.Equals, test.out)
+	}
+
+}