mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
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
This commit is contained in:
parent
9a215d6950
commit
41a080b268
7 changed files with 218 additions and 1 deletions
|
@ -93,6 +93,7 @@ func (c *Client) toCSS(args godartsass.Args, src io.Reader) (godartsass.Result,
|
||||||
var res godartsass.Result
|
var res godartsass.Result
|
||||||
|
|
||||||
in := helpers.ReaderToString(src)
|
in := helpers.ReaderToString(src)
|
||||||
|
|
||||||
args.Source = in
|
args.Source = in
|
||||||
|
|
||||||
res, err := c.transpiler.Execute(args)
|
res, err := c.transpiler.Execute(args)
|
||||||
|
@ -130,6 +131,11 @@ type Options struct {
|
||||||
|
|
||||||
// If enabled, sources will be embedded in the generated source map.
|
// If enabled, sources will be embedded in the generated source map.
|
||||||
SourceMapIncludeSources bool
|
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) {
|
func decodeOptions(m map[string]any) (opts Options, err error) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTransformIncludePaths(t *testing.T) {
|
func TestTransformIncludePaths(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !dartsass.Supports() {
|
if !dartsass.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -55,6 +56,7 @@ T1: {{ $r.Content }}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformImportRegularCSS(t *testing.T) {
|
func TestTransformImportRegularCSS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !dartsass.Supports() {
|
if !dartsass.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -108,6 +110,7 @@ T1: {{ $r.Content | safeHTML }}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformThemeOverrides(t *testing.T) {
|
func TestTransformThemeOverrides(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !dartsass.Supports() {
|
if !dartsass.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -169,6 +172,7 @@ zoo {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformLogging(t *testing.T) {
|
func TestTransformLogging(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !dartsass.Supports() {
|
if !dartsass.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -200,6 +204,7 @@ T1: {{ $r.Content }}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformErrors(t *testing.T) {
|
func TestTransformErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !dartsass.Supports() {
|
if !dartsass.Supports() {
|
||||||
t.Skip()
|
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}`)
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with 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"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/resources/internal"
|
"github.com/gohugoio/hugo/resources/internal"
|
||||||
|
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/internal/sass"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
|
@ -84,6 +85,8 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
|
||||||
ImportResolver: importResolver{
|
ImportResolver: importResolver{
|
||||||
baseDir: baseDir,
|
baseDir: baseDir,
|
||||||
c: t.c,
|
c: t.c,
|
||||||
|
|
||||||
|
varsStylesheet: sass.CreateVarsStyleSheet(opts.Vars),
|
||||||
},
|
},
|
||||||
OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle),
|
OutputStyle: godartsass.ParseOutputStyle(opts.OutputStyle),
|
||||||
EnableSourceMap: opts.EnableSourceMap,
|
EnableSourceMap: opts.EnableSourceMap,
|
||||||
|
@ -128,9 +131,14 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
|
||||||
type importResolver struct {
|
type importResolver struct {
|
||||||
baseDir string
|
baseDir string
|
||||||
c *Client
|
c *Client
|
||||||
|
|
||||||
|
varsStylesheet string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t importResolver) CanonicalizeURL(url string) (string, error) {
|
func (t importResolver) CanonicalizeURL(url string) (string, error) {
|
||||||
|
if url == sass.HugoVarsNamespace {
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
filePath, isURL := paths.UrlToFilename(url)
|
filePath, isURL := paths.UrlToFilename(url)
|
||||||
var prevDir string
|
var prevDir string
|
||||||
var pathDir string
|
var pathDir string
|
||||||
|
@ -177,6 +185,9 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t importResolver) Load(url string) (string, error) {
|
func (t importResolver) Load(url string) (string, error) {
|
||||||
|
if url == sass.HugoVarsNamespace {
|
||||||
|
return t.varsStylesheet, nil
|
||||||
|
}
|
||||||
filename, _ := paths.UrlToFilename(url)
|
filename, _ := paths.UrlToFilename(url)
|
||||||
b, err := afero.ReadFile(hugofs.Os, filename)
|
b, err := afero.ReadFile(hugofs.Os, filename)
|
||||||
return string(b), err
|
return string(b), err
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -60,6 +60,10 @@ type Options struct {
|
||||||
|
|
||||||
// When enabled, Hugo will generate a source map.
|
// When enabled, Hugo will generate a source map.
|
||||||
EnableSourceMap bool
|
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) {
|
func DecodeOptions(m map[string]any) (opts Options, err error) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTransformIncludePaths(t *testing.T) {
|
func TestTransformIncludePaths(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !scss.Supports() {
|
if !scss.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -57,6 +58,7 @@ T1: {{ $r.Content }}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformImportRegularCSS(t *testing.T) {
|
func TestTransformImportRegularCSS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !scss.Supports() {
|
if !scss.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -113,6 +115,7 @@ moo {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformThemeOverrides(t *testing.T) {
|
func TestTransformThemeOverrides(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !scss.Supports() {
|
if !scss.Supports() {
|
||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
@ -175,6 +178,7 @@ zoo {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTransformErrors(t *testing.T) {
|
func TestTransformErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
if !scss.Supports() {
|
if !scss.Supports() {
|
||||||
t.Skip()
|
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}`)
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/gohugoio/hugo/hugofs"
|
"github.com/gohugoio/hugo/hugofs"
|
||||||
"github.com/gohugoio/hugo/media"
|
"github.com/gohugoio/hugo/media"
|
||||||
"github.com/gohugoio/hugo/resources"
|
"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.
|
// 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 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.
|
// 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
|
// 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.
|
// that only work for overrides on the top level.
|
||||||
options.to.ImportResolver = func(url string, prev string) (newUrl string, body string, resolved bool) {
|
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.
|
// We get URL paths from LibSASS, but we need file paths.
|
||||||
url = filepath.FromSlash(url)
|
url = filepath.FromSlash(url)
|
||||||
prev = filepath.FromSlash(prev)
|
prev = filepath.FromSlash(prev)
|
||||||
|
|
Loading…
Reference in a new issue