mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 07:18:54 +00:00
tpl/css: Support @import "hugo:vars" for CSS custom properties in css.Build
Closes #14699 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -207,6 +207,9 @@ type ExternalOptions struct {
|
||||
// See https://esbuild.github.io/api/#jsx-import-source
|
||||
JSXImportSource string
|
||||
|
||||
// User defined CSS variables. Will be available as CSS global scope CSS variables via @import "hugo:vars".
|
||||
Vars map[string]any
|
||||
|
||||
// There is/was a bug in WebKit with severe performance issue with the tracking
|
||||
// of TDZ checks in JavaScriptCore.
|
||||
//
|
||||
|
||||
@@ -19,10 +19,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/evanw/esbuild/pkg/api"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/types/css"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
@@ -34,12 +36,13 @@ const (
|
||||
NsHugoImport = "ns-hugo-imp"
|
||||
NsHugoImportResolveFunc = "ns-hugo-imp-func"
|
||||
nsHugoParams = "ns-hugo-params"
|
||||
nsHugoVars = "ns-hugo-vars"
|
||||
pathHugoConfigParams = "@params/config"
|
||||
|
||||
stdinImporter = "<stdin>"
|
||||
)
|
||||
|
||||
var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams}
|
||||
var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams, nsHugoVars}
|
||||
|
||||
const (
|
||||
PrefixHugoVirtual = "__hu_v"
|
||||
@@ -371,5 +374,54 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana
|
||||
},
|
||||
}
|
||||
|
||||
return []api.Plugin{importResolver, paramsPlugin}, nil
|
||||
varsPlugin := api.Plugin{
|
||||
Name: "hugo-vars-plugin",
|
||||
Setup: func(build api.PluginBuild) {
|
||||
build.OnResolve(api.OnResolveOptions{Filter: `^hugo:vars$`},
|
||||
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
|
||||
return api.OnResolveResult{
|
||||
Path: args.Path,
|
||||
Namespace: nsHugoVars,
|
||||
}, nil
|
||||
})
|
||||
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsHugoVars},
|
||||
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
|
||||
return api.OnLoadResult{
|
||||
Contents: createCSSVarsStyleSheet(opts.Vars),
|
||||
Loader: api.LoaderCSS,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return []api.Plugin{importResolver, paramsPlugin, varsPlugin}, nil
|
||||
}
|
||||
|
||||
// createCSSVarsStyleSheet creates a CSS custom properties stylesheet from the given vars.
|
||||
// The result is a :root block with CSS custom properties.
|
||||
func createCSSVarsStyleSheet(vars map[string]any) *string {
|
||||
if len(vars) == 0 {
|
||||
// We need to return a non-nil pointer to an empty string to avoid ESBuild treating this as a missing file.
|
||||
s := ""
|
||||
return &s
|
||||
}
|
||||
|
||||
var varsSlice []string
|
||||
for k, v := range vars {
|
||||
if !strings.HasPrefix(k, "--") {
|
||||
k = "--" + k
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case css.QuotedString:
|
||||
// E.g. Arial, sans-serif.
|
||||
varsSlice = append(varsSlice, fmt.Sprintf(" %s: %q;", k, v))
|
||||
default:
|
||||
varsSlice = append(varsSlice, fmt.Sprintf(" %s: %v;", k, v))
|
||||
}
|
||||
}
|
||||
sort.Strings(varsSlice)
|
||||
s := ":root {\n" + strings.Join(varsSlice, "\n") + "\n}\n"
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
@@ -430,3 +430,128 @@ disableKinds = ["taxonomy", "term"]
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstall())
|
||||
b.AssertFileContent("public/css/main.css", `--bs-indigo: #6610f2;`)
|
||||
}
|
||||
|
||||
func TestCSSBuildVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/css/main.css --
|
||||
@import "hugo:vars";
|
||||
|
||||
body {
|
||||
background-color: var(--primary-color);
|
||||
font-size: var(--font-size);
|
||||
color: var(--text-color);
|
||||
}
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main.css" }}
|
||||
{{ $opts := dict
|
||||
"vars" (dict "primary-color" "blue" "font-size" "24px" "text-color" "#333" "--already-prefixed" "red")
|
||||
}}
|
||||
{{ with . | css.Build $opts }}
|
||||
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/css/main.css",
|
||||
"--primary-color: blue;",
|
||||
"--font-size: 24px;",
|
||||
"--text-color: #333;",
|
||||
"--already-prefixed: red;",
|
||||
"background-color: var(--primary-color)",
|
||||
)
|
||||
}
|
||||
|
||||
func TestCSSBuildVarsEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/css/main.css --
|
||||
@import "hugo:vars";
|
||||
|
||||
body {
|
||||
background-color: red;
|
||||
}
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main.css" }}
|
||||
{{ with . | css.Build }}
|
||||
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/css/main.css", "background-color: red;")
|
||||
}
|
||||
|
||||
func TestCSSBuildVarsQuoted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/css/main.css --
|
||||
@import "hugo:vars";
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
color: var(--brand-color);
|
||||
}
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main.css" }}
|
||||
{{ $opts := dict
|
||||
"vars" (dict "font-family" (css.Quoted "Arial, sans-serif") "brand-color" "hsl(0, 0%, 20%)")
|
||||
}}
|
||||
{{ with . | css.Build $opts }}
|
||||
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/css/main.css",
|
||||
`--font-family: "Arial, sans-serif";`,
|
||||
"--brand-color: hsl(0, 0%, 20%);",
|
||||
)
|
||||
}
|
||||
|
||||
func TestCSSBuildVarsFromParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[params.styles]
|
||||
primary-color = "blue"
|
||||
font-size = "24px"
|
||||
text-color = "#333"
|
||||
-- assets/css/main.css --
|
||||
@import "hugo:vars";
|
||||
-- assets/css/main.css --
|
||||
@import "hugo:vars";
|
||||
|
||||
body {
|
||||
background-color: var(--primary-color);
|
||||
font-size: var(--font-size);
|
||||
color: var(--text-color);
|
||||
}
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main.css" }}
|
||||
{{ $opts := dict
|
||||
"vars" site.Params.styles
|
||||
}}
|
||||
{{ with . | css.Build $opts }}
|
||||
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/css/main.css",
|
||||
"--primary-color: blue;",
|
||||
"--font-size: 24px;",
|
||||
"--text-color: #333;",
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user