From ecf3cd514fe0e4c848513053721ee9573108f666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Tue, 21 Feb 2023 18:32:09 +0100 Subject: [PATCH] tocss: Simplify the hugo:vars type handling Instead of maintaing a list of all CSS units and functions this commit: * Uses 3 regexps to detect typed CSS values (e.g. `24px`) + properly handle numeric Go types. * These regexps may have some false positives -- e.g. strings that needs to be quoted. * For that rare case, you can mark the string with e.g. `"32xxx" | css.Quoted` * For the opposite case: `"32" | css.Unquoted` Updates #10632 --- common/types/css/csstypes.go | 20 ++++++ .../tocss/dartsass/client.go | 2 +- .../tocss/dartsass/integration_test.go | 17 +++++ .../tocss/internal/sass/cssValues.go | 72 ++++++++++--------- .../tocss/internal/sass/helpers.go | 64 +++++++++++------ .../tocss/internal/sass/helpers_test.go | 44 ++++++++++++ .../tocss/scss/client.go | 2 +- tpl/css/css.go | 41 +++++++++++ tpl/tplimpl/template_funcs.go | 1 + 9 files changed, 206 insertions(+), 57 deletions(-) create mode 100644 common/types/css/csstypes.go create mode 100644 resources/resource_transformers/tocss/internal/sass/helpers_test.go create mode 100644 tpl/css/css.go 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/dartsass/integration_test.go b/resources/resource_transformers/tocss/dartsass/integration_test.go index e7ddd6e6f57..e7432f12bc5 100644 --- a/resources/resource_transformers/tocss/dartsass/integration_test.go +++ b/resources/resource_transformers/tocss/dartsass/integration_test.go @@ -387,6 +387,10 @@ 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"; @@ -397,8 +401,15 @@ flex = "5fr" @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 }} @@ -418,5 +429,11 @@ T1: {{ $r.Content }} 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/cssValues.go b/resources/resource_transformers/tocss/internal/sass/cssValues.go index b23c4fc0265..410e89e04a6 100644 --- a/resources/resource_transformers/tocss/internal/sass/cssValues.go +++ b/resources/resource_transformers/tocss/internal/sass/cssValues.go @@ -15,65 +15,67 @@ package sass type cssValue struct { prefix []string - sufix []string + suffix []string } var ( cssValues = cssValue{ + // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value prefix: []string{ "#", - "rgb(", + "attr(", + "calc(", + "clamp(", "hsl(", "hwb(", - "lch(", "lab(", - "calc(", - "min(", + "lch(", "max(", + "min(", "minmax(", - "clamp(", - "attr(", + "rgb(", }, - sufix: []string{ - "em", - "ex", + // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Values_and_Units#dimensions + suffix: []string{ + "%", "cap", "ch", - "ic", - "rem", - "lh", - "rlh", - "vw", - "vh", - "vi", - "vb", - "vmin", - "vmax", - "cqw", + "cm", + "cqb", "cqh", "cqi", - "cqb", - "cqmin", "cqmax", - "cm", - "mm", - "Q", + "cqmin", + "cqw", + "deg", + "dpcm", + "dpi", + "dppx", + "em", + "ex", + "fr", + "grad", + "ic", "in", + "lh", + "mm", + "ms", "pc", "pt", "px", - "deg", - "grad", + "Q", "rad", - "turn", + "rem", + "rlh", "s", - "ms", - "fr", - "dpi", - "dpcm", - "dppx", + "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 e6f246b7c80..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,35 +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. - // Use quoteVar() to check if the variables should be quoted or not. - if quoteVar(v) { - varsSlice = append(varsSlice, fmt.Sprintf("%s%s: unquote(%q);", prefix, k, v)) - } else { - varsSlice = append(varsSlice, fmt.Sprintf("%s%s: %s;", 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 } -func quoteVar(v string) bool { - v = strings.Trim(v, "\"") - for _, p := range cssValues.prefix { - if strings.HasPrefix(v, p) { - return false +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 } - } - for _, s := range cssValues.sufix { - if strings.HasSuffix(v, s) { - return false + if isCSSFunc.MatchString(s) { + return true } + if isCSSUnit.MatchString(s) { + return true + } + } - return true + return false } 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) + } + +} 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"