"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"
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"
},
}
- 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
}
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;",
+ )
+}