Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5d09b5e

Browse files
bepclaude
andcommitted
tpl/css: Support @import "hugo:vars" for CSS custom properties in css.Build
Closes #14699 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 303e443 commit 5d09b5e

3 files changed

Lines changed: 182 additions & 2 deletions

File tree

internal/js/esbuild/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ type ExternalOptions struct {
207207
// See https://esbuild.github.io/api/#jsx-import-source
208208
JSXImportSource string
209209

210+
// User defined CSS variables. Will be available as CSS global scope CSS variables via @import "hugo:vars".
211+
Vars map[string]any
212+
210213
// There is/was a bug in WebKit with severe performance issue with the tracking
211214
// of TDZ checks in JavaScriptCore.
212215
//

internal/js/esbuild/resolve.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import (
1919
"os"
2020
"path/filepath"
2121
"slices"
22+
"sort"
2223
"strings"
2324

2425
"github.com/evanw/esbuild/pkg/api"
2526
"github.com/gohugoio/hugo/common/hmaps"
27+
"github.com/gohugoio/hugo/common/types/css"
2628
"github.com/gohugoio/hugo/hugofs"
2729
"github.com/gohugoio/hugo/identity"
2830
"github.com/gohugoio/hugo/resources"
@@ -34,12 +36,13 @@ const (
3436
NsHugoImport = "ns-hugo-imp"
3537
NsHugoImportResolveFunc = "ns-hugo-imp-func"
3638
nsHugoParams = "ns-hugo-params"
39+
nsHugoVars = "ns-hugo-vars"
3740
pathHugoConfigParams = "@params/config"
3841

3942
stdinImporter = "<stdin>"
4043
)
4144

42-
var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams}
45+
var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams, nsHugoVars}
4346

4447
const (
4548
PrefixHugoVirtual = "__hu_v"
@@ -371,5 +374,54 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana
371374
},
372375
}
373376

374-
return []api.Plugin{importResolver, paramsPlugin}, nil
377+
varsPlugin := api.Plugin{
378+
Name: "hugo-vars-plugin",
379+
Setup: func(build api.PluginBuild) {
380+
build.OnResolve(api.OnResolveOptions{Filter: `^hugo:vars$`},
381+
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
382+
return api.OnResolveResult{
383+
Path: args.Path,
384+
Namespace: nsHugoVars,
385+
}, nil
386+
})
387+
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsHugoVars},
388+
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
389+
return api.OnLoadResult{
390+
Contents: createCSSVarsStyleSheet(opts.Vars),
391+
Loader: api.LoaderCSS,
392+
}, nil
393+
})
394+
},
395+
}
396+
397+
return []api.Plugin{importResolver, paramsPlugin, varsPlugin}, nil
398+
}
399+
400+
// createCSSVarsStyleSheet creates a CSS custom properties stylesheet from the given vars.
401+
// The result is a :root block with CSS custom properties.
402+
func createCSSVarsStyleSheet(vars map[string]any) *string {
403+
if len(vars) == 0 {
404+
// We need to return a non-nil pointer to an empty string to avoid ESBuild treating this as a missing file.
405+
s := ""
406+
return &s
407+
}
408+
409+
var varsSlice []string
410+
for k, v := range vars {
411+
if !strings.HasPrefix(k, "--") {
412+
k = "--" + k
413+
}
414+
415+
switch v.(type) {
416+
case css.QuotedString:
417+
// E.g. Arial, sans-serif.
418+
varsSlice = append(varsSlice, fmt.Sprintf(" %s: %q;", k, v))
419+
default:
420+
varsSlice = append(varsSlice, fmt.Sprintf(" %s: %v;", k, v))
421+
}
422+
}
423+
sort.Strings(varsSlice)
424+
s := ":root {\n" + strings.Join(varsSlice, "\n") + "\n}\n"
425+
426+
return &s
375427
}

tpl/css/build_integration_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,3 +430,128 @@ disableKinds = ["taxonomy", "term"]
430430
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstall())
431431
b.AssertFileContent("public/css/main.css", `--bs-indigo: #6610f2;`)
432432
}
433+
434+
func TestCSSBuildVars(t *testing.T) {
435+
t.Parallel()
436+
437+
files := `
438+
-- hugo.toml --
439+
-- assets/css/main.css --
440+
@import "hugo:vars";
441+
442+
body {
443+
background-color: var(--primary-color);
444+
font-size: var(--font-size);
445+
color: var(--text-color);
446+
}
447+
-- layouts/home.html --
448+
{{ with resources.Get "css/main.css" }}
449+
{{ $opts := dict
450+
"vars" (dict "primary-color" "blue" "font-size" "24px" "text-color" "#333" "--already-prefixed" "red")
451+
}}
452+
{{ with . | css.Build $opts }}
453+
<link rel="stylesheet" href="{{ .RelPermalink }}" />
454+
{{ end }}
455+
{{ end }}
456+
`
457+
458+
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
459+
b.AssertFileContent("public/css/main.css",
460+
"--primary-color: blue;",
461+
"--font-size: 24px;",
462+
"--text-color: #333;",
463+
"--already-prefixed: red;",
464+
"background-color: var(--primary-color)",
465+
)
466+
}
467+
468+
func TestCSSBuildVarsEmpty(t *testing.T) {
469+
t.Parallel()
470+
471+
files := `
472+
-- hugo.toml --
473+
-- assets/css/main.css --
474+
@import "hugo:vars";
475+
476+
body {
477+
background-color: red;
478+
}
479+
-- layouts/home.html --
480+
{{ with resources.Get "css/main.css" }}
481+
{{ with . | css.Build }}
482+
<link rel="stylesheet" href="{{ .RelPermalink }}" />
483+
{{ end }}
484+
{{ end }}
485+
`
486+
487+
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
488+
b.AssertFileContent("public/css/main.css", "background-color: red;")
489+
}
490+
491+
func TestCSSBuildVarsQuoted(t *testing.T) {
492+
t.Parallel()
493+
494+
files := `
495+
-- hugo.toml --
496+
-- assets/css/main.css --
497+
@import "hugo:vars";
498+
499+
body {
500+
font-family: var(--font-family);
501+
color: var(--brand-color);
502+
}
503+
-- layouts/home.html --
504+
{{ with resources.Get "css/main.css" }}
505+
{{ $opts := dict
506+
"vars" (dict "font-family" (css.Quoted "Arial, sans-serif") "brand-color" "hsl(0, 0%, 20%)")
507+
}}
508+
{{ with . | css.Build $opts }}
509+
<link rel="stylesheet" href="{{ .RelPermalink }}" />
510+
{{ end }}
511+
{{ end }}
512+
`
513+
514+
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
515+
b.AssertFileContent("public/css/main.css",
516+
`--font-family: "Arial, sans-serif";`,
517+
"--brand-color: hsl(0, 0%, 20%);",
518+
)
519+
}
520+
521+
func TestCSSBuildVarsFromParams(t *testing.T) {
522+
t.Parallel()
523+
524+
files := `
525+
-- hugo.toml --
526+
[params.styles]
527+
primary-color = "blue"
528+
font-size = "24px"
529+
text-color = "#333"
530+
-- assets/css/main.css --
531+
@import "hugo:vars";
532+
-- assets/css/main.css --
533+
@import "hugo:vars";
534+
535+
body {
536+
background-color: var(--primary-color);
537+
font-size: var(--font-size);
538+
color: var(--text-color);
539+
}
540+
-- layouts/home.html --
541+
{{ with resources.Get "css/main.css" }}
542+
{{ $opts := dict
543+
"vars" site.Params.styles
544+
}}
545+
{{ with . | css.Build $opts }}
546+
<link rel="stylesheet" href="{{ .RelPermalink }}" />
547+
{{ end }}
548+
{{ end }}
549+
`
550+
551+
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
552+
b.AssertFileContent("public/css/main.css",
553+
"--primary-color: blue;",
554+
"--font-size: 24px;",
555+
"--text-color: #333;",
556+
)
557+
}

0 commit comments

Comments
 (0)