mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
70db201ed4
This allows @import statements to be resolved in a set of user provided resources (e.g. from resources.FromString or css.ChromaStyles) before the assets filesystem. The option also applies to css.PostCSS via the shared import inlining, and css.Sass requires the dartsass transpiler. The import context is part of the transformation cache key. Fixes #15103 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
912 lines
27 KiB
Go
912 lines
27 KiB
Go
// Copyright 2026 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_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/gohugoio/hugo/htesting"
|
|
"github.com/gohugoio/hugo/hugolib"
|
|
"github.com/gohugoio/hugo/internal/js/esbuild"
|
|
)
|
|
|
|
func TestCSSBuild(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
@import "tailwindcss";
|
|
@import url('https://example.org/foo.css');
|
|
@import "./foo.css";
|
|
@import "./bar.css" layer(mylayer);
|
|
@import "./bar.css" layer;
|
|
@import './baz.css' screen and (min-width: 800px);
|
|
@import 'relnodot.css' supports(display: grid);
|
|
@import 'relnodotboth.css';
|
|
@import '/relnodotboth2.css';
|
|
body {
|
|
background-image: url("a/pixel.png");
|
|
}
|
|
.mask1 {
|
|
mask-image: url(a/pixel.png);
|
|
mask-repeat: no-repeat;
|
|
}
|
|
-- assets/css/foo.css --
|
|
p { background: red; }
|
|
-- assets/css/bar.css --
|
|
div { background: blue; }
|
|
-- assets/css/baz.css --
|
|
span { background: green; }
|
|
-- assets/css/relnodot.css --
|
|
article { background: yellow; }
|
|
-- assets/css/relnodotboth.css --
|
|
.relnodotbothrelative { background: green; }
|
|
-- assets/relnodotboth.css --
|
|
.relnodotbothroot { background: blue; }
|
|
-- assets/css/relnodotboth2.css --
|
|
.relnodotboth2relative { background: green; }
|
|
-- assets/relnodotboth2.css --
|
|
.relnodotboth2root { background: blue; }
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := (dict "minify" true "target" (slice "chrome108" "firefox116" "safari16.4" "edge115" "ios16.4" "opera101") "loaders" (dict ".png" "dataurl") "externals" (slice "tailwindcss"))}}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css",
|
|
`-webkit-mask-repeat`, // browser prefix.
|
|
"data:image/png;base64", // dataurl loader
|
|
`@import"tailwindcss";`, // external
|
|
`@import"https://example.org/foo.css";`, // external
|
|
"mylayer{div", // imported layer
|
|
"@media screen and (min-width:800px){span", // imported media query
|
|
"@supports (display: grid){article", // imported supports
|
|
"relnodotbothrelative", //@import 'relnodotboth.css'; file exists in both assets and project root.
|
|
"relnodotboth2root", //@import '/relnodotboth2.css'; file exists in both assets and project root.
|
|
)
|
|
}
|
|
|
|
func TestCSSBuildEdit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term", "rss"]
|
|
-- assets/images/bar.svg --
|
|
barsvg
|
|
-- assets/images/foo.svg --
|
|
foosvg
|
|
-- assets/css/main.css --
|
|
@import "./foo.css";
|
|
@import "./bar.css" layer(mylayer);
|
|
.main { background: red; }
|
|
-- assets/css/foo.css --
|
|
.foo { background: green; }
|
|
-- assets/css/bar.css --
|
|
.bar { background-image: url("images/bar.svg"); }
|
|
-- layouts/all.html --
|
|
All. No CSS here.
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := (dict "minify" true) }}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs())
|
|
|
|
b.AssertFileContent("public/css/main.css", `.foo{background:green}@layer mylayer{.bar{background-image:url("/css/bar-Y35ORVQM.svg")}}`)
|
|
|
|
// Edit svg
|
|
b.EditFileReplaceAll("assets/images/bar.svg", "barsvg", "newbarsvg").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `/css/bar-LVHHRPN5.svg`) // new hash.
|
|
b.AssertFileContent("public/css/bar-LVHHRPN5.svg", "newbarsvg")
|
|
|
|
// Edit foo.css
|
|
b.EditFileReplaceAll("assets/css/foo.css", "green", "red").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `.foo{background:red}`) // updated content.
|
|
|
|
// Edit bar.css
|
|
b.EditFileReplaceAll("assets/css/bar.css", "bar.svg", "foo.svg").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `/css/foo-52JTT5GU.svg`)
|
|
b.AssertFileContent("public/css/foo-52JTT5GU.svg", "foosvg")
|
|
|
|
// Edit main.css
|
|
b.EditFileReplaceAll("assets/css/main.css", "red", "blue").Build()
|
|
b.AssertRenderCountPage(1)
|
|
b.AssertFileContent("public/css/main.css", `.main{background:#00f}`) // updated content, blue gets minified to #00f.
|
|
}
|
|
|
|
func TestCSSBuildEditOptions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term", "rss"]
|
|
disableLiveReload = true
|
|
-- assets/css/main.css --
|
|
body {
|
|
background: red;
|
|
}
|
|
-- layouts/home.html --
|
|
Home.
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := dict "minify" false }}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", ` background: red;`)
|
|
|
|
b.EditFileReplaceAll("layouts/home.html", `"minify" false`, `"minify" true`).Build()
|
|
b.AssertFileContent("public/css/main.css", `{background:red}`)
|
|
}
|
|
|
|
func TestCSSBuildEditOptionsMultiple(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term", "rss"]
|
|
disableLiveReload = true
|
|
-- assets/css/main.css --
|
|
body {
|
|
background: red;
|
|
}
|
|
-- layouts/_partials/css.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := dict "minify" false }}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
-- layouts/all.html --
|
|
All. {{ partial "css.html" . }}
|
|
-- content/p1.md --
|
|
-- content/p2.md --
|
|
-- content/p3.md --
|
|
|
|
|
|
`
|
|
|
|
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs())
|
|
|
|
for range 3 {
|
|
b.AssertFileContent("public/css/main.css", ` background: red;`)
|
|
b.EditFileReplaceAll("layouts/_partials/css.html", `"minify" false`, `"minify" true`).Build()
|
|
b.AssertFileContent("public/css/main.css", `{background:red}`)
|
|
b.EditFileReplaceAll("layouts/_partials/css.html", `"minify" true`, `"minify" false`).Build()
|
|
}
|
|
}
|
|
|
|
func TestCSSBuildSourceMaps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesTemplate := `
|
|
-- hugo.toml --
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
@import "./foo.css";
|
|
@import "./bar.css" layer(mylayer);
|
|
@import "./bar.css" layer;
|
|
@import './baz.css' screen and (min-width: 800px);
|
|
|
|
.main {
|
|
background-color: red;
|
|
}
|
|
-- assets/css/foo.css --
|
|
p { background: red; }
|
|
-- assets/css/bar.css --
|
|
div { background: blue; }
|
|
-- assets/css/baz.css --
|
|
span { background: green; }
|
|
-- assets/css/qux.css --
|
|
article { background: yellow; }
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := (dict "minify" MINIFY "sourceMap" "SOURCE_MAP" "sourcesContent" SOURCES_CONTENT "loaders" (dict ".png" "dataurl"))}}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
var (
|
|
r *strings.Replacer
|
|
files string
|
|
b *hugolib.IntegrationTestBuilder
|
|
)
|
|
|
|
for _, minify := range []string{"true", "false"} {
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "linked",
|
|
"SOURCES_CONTENT", "true",
|
|
"MINIFY", minify,
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", "/*# sourceMappingURL=main.css.map */")
|
|
b.AssertFileContent("public/css/main.css.map",
|
|
`"sourcesContent":["`,
|
|
`"mappings":"`,
|
|
)
|
|
|
|
sources := esbuild.SourcesFromSourceMap(b.FileContent("public/css/main.css.map"))
|
|
// main.css + foo.css + bar.css + baz.css = 4 sources.
|
|
b.Assert(len(sources), qt.Equals, 4)
|
|
}
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "external",
|
|
"SOURCES_CONTENT", "true",
|
|
"MINIFY", "false",
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", "! sourceMappingURL")
|
|
b.AssertFileContent("public/css/main.css.map",
|
|
`"sourcesContent":["p { background: red; }"`,
|
|
"AAAA;AAAI,cAAY;AAAK",
|
|
)
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "external",
|
|
"SOURCES_CONTENT", "false",
|
|
"MINIFY", "false",
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css.map",
|
|
`"sourcesContent":null,"`,
|
|
"AAAA;AAAI,cAAY;AAAK",
|
|
)
|
|
|
|
r = strings.NewReplacer(
|
|
"SOURCE_MAP", "inline",
|
|
"SOURCES_CONTENT", "false",
|
|
"MINIFY", "false",
|
|
)
|
|
|
|
files = r.Replace(filesTemplate)
|
|
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileExists("public/css/main.css.map", false)
|
|
b.AssertFileContent("public/css/main.css", "sourceMappingURL=data:application/json;base64,")
|
|
}
|
|
|
|
func TestCSSBuildMultihost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term"]
|
|
defaultContentLanguage = "en"
|
|
defaultContentLanguageInSubDir = true
|
|
[languages]
|
|
[languages.en]
|
|
baseURL = "https://example.en"
|
|
weight = 1
|
|
[languages.fr]
|
|
baseURL = "https://example.fr"
|
|
weight = 2
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
body {
|
|
background-image: url("a/pixel.png");
|
|
color: red;
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
CSS: {{ .RelPermalink }}|{{ .Content }}
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
|
|
for _, lang := range []string{"en", "fr"} {
|
|
b.AssertFileContent("public/"+lang+"/css/main.css", `/css/pixel-NJRUOINY.png`)
|
|
b.AssertFileExists("public/"+lang+"/css/pixel-NJRUOINY.png", true)
|
|
}
|
|
}
|
|
|
|
func TestCSSBuildLoadersDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/a/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- static/b/issue14619.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/css/main.css --
|
|
body {
|
|
background-image: url("a/pixel.png");
|
|
}
|
|
div {
|
|
background-image: url("static/b/issue14619.png");
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", `/css/pixel-NJRUOINY.png`)
|
|
b.AssertFileExists("public/css/pixel-NJRUOINY.png", true)
|
|
b.AssertFileContent("public/css/main.css", `url("/css/issue14619-NJRUOINY.png")`)
|
|
b.AssertFileExists("public/css/issue14619-NJRUOINY.png", true)
|
|
}
|
|
|
|
// Issue #14849
|
|
func TestCSSBuildFileLoaderURLRelativeToWebContextRoot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesTemplate := `
|
|
-- hugo.toml --
|
|
BASEURL
|
|
disableKinds = ['rss','section','sitemap','taxonomy','term']
|
|
-- assets/css/main.css --
|
|
@import "components/header.css";
|
|
@import "/fonts/comic_neue/fonts.css";
|
|
body { color: #222; }
|
|
-- assets/css/components/header.css --
|
|
header { border-bottom: 1px solid #222; }
|
|
-- assets/fonts/comic_neue/fonts.css --
|
|
@font-face {
|
|
font-family: 'Comic Neue';
|
|
src: url(ComicNeue-Regular.ttf) format('truetype');
|
|
}
|
|
-- assets/fonts/comic_neue/ComicNeue-Regular.ttf --
|
|
fakefontdata
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
INLINE: <style>{{ .Content | safeCSS }}</style>
|
|
LINKED: <link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
// Default baseURL: URLs in inlined CSS must be resolvable from any page,
|
|
// not just relative to the (non-existent) /css/main.css location.
|
|
files := strings.ReplaceAll(filesTemplate, "BASEURL", "")
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/index.html", `url("/css/ComicNeue-Regular-`)
|
|
b.AssertFileContent("public/css/main.css", `url("/css/ComicNeue-Regular-`)
|
|
|
|
// baseURL with a subpath: URLs must include that subpath.
|
|
files = strings.ReplaceAll(filesTemplate, "BASEURL", `baseURL = "https://example.org/mysite/"`)
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/index.html", `url("/mysite/css/ComicNeue-Regular-`)
|
|
b.AssertFileContent("public/css/main.css", `url("/mysite/css/ComicNeue-Regular-`)
|
|
}
|
|
|
|
// Issue #14849
|
|
func TestCSSBuildFileLoaderURLRelativeToWebContextRootMultihost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// The CSS is built once for all hosts. The deepest base path among the
|
|
// hosts is baked into the file-loader URLs so the URL is reachable on every
|
|
// host, and the artifact is published at a per-host compensated path so the
|
|
// URL resolves correctly on each host. EN_WEIGHT/FR_WEIGHT swap which host
|
|
// renders first, to verify the result is independent of build order.
|
|
filesTemplate := `
|
|
-- hugo.toml --
|
|
defaultContentLanguage = "en"
|
|
defaultContentLanguageInSubdir = false
|
|
disableKinds = ['rss','section','sitemap','taxonomy','term']
|
|
[languages]
|
|
[languages.en]
|
|
baseURL = "https://example.com/docs/"
|
|
weight = EN_WEIGHT
|
|
[languages.fr]
|
|
baseURL = "https://example.fr/"
|
|
weight = FR_WEIGHT
|
|
-- assets/css/main.css --
|
|
@import "/fonts/comic_neue/fonts.css";
|
|
body { color: #222; }
|
|
-- assets/fonts/comic_neue/fonts.css --
|
|
@font-face {
|
|
font-family: 'Comic Neue';
|
|
src: url(ComicNeue-Regular.ttf) format('truetype');
|
|
}
|
|
-- assets/fonts/comic_neue/ComicNeue-Regular.ttf --
|
|
fakefontdata
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
INLINE: <style>{{ .Content | safeCSS }}</style>
|
|
LINKED: <link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
assertResult := func(t *testing.T, b *hugolib.IntegrationTestBuilder) {
|
|
t.Helper()
|
|
b.Assert(b.H.Conf.IsMultihost(), qt.Equals, true)
|
|
b.AssertFileContent("public/en/index.html", `url("/docs/css/ComicNeue-Regular-`)
|
|
b.AssertFileContent("public/en/css/main.css", `url("/docs/css/ComicNeue-Regular-`)
|
|
b.AssertFileContent("public/fr/index.html", `url("/docs/css/ComicNeue-Regular-`)
|
|
b.AssertFileContent("public/fr/css/main.css", `url("/docs/css/ComicNeue-Regular-`)
|
|
b.AssertPublishDir("en/css/ComicNeue-Regular-UA4ODE7N.ttf")
|
|
b.AssertPublishDir("fr/docs/css/ComicNeue-Regular-UA4ODE7N.ttf")
|
|
}
|
|
|
|
t.Run("en first", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := strings.NewReplacer("EN_WEIGHT", "10", "FR_WEIGHT", "20").Replace(filesTemplate)
|
|
assertResult(t, hugolib.Test(t, files, hugolib.TestOptOsFs()))
|
|
})
|
|
|
|
t.Run("fr first", func(t *testing.T) {
|
|
t.Parallel()
|
|
files := strings.NewReplacer("EN_WEIGHT", "20", "FR_WEIGHT", "10").Replace(filesTemplate)
|
|
assertResult(t, hugolib.Test(t, files, hugolib.TestOptOsFs()))
|
|
})
|
|
}
|
|
|
|
// Issue #14623
|
|
func TestCSSBuildLoadersPartial(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
|
-- layouts/home.html --
|
|
{{ $opts := dict "loaders" (dict ".svg" "dataurl") }}
|
|
{{ with resources.Get "css/main.css" | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}">
|
|
{{ end }}
|
|
-- assets/css/main.css --
|
|
body { background-image: url('images/pixel.png'); }
|
|
div { background-image: url('images/foo.svg'); }
|
|
-- assets/images/pixel.png --
|
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
|
-- assets/images/foo.svg --
|
|
SVG file.
|
|
`
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileExists("public/css/pixel-NJRUOINY.png", true)
|
|
b.AssertFileContent("public/css/main.css", `url(data:image/svg`) // svg should be inlined as dataurl.
|
|
b.AssertFileContent("public/css/main.css", `pixel-NJRUOINY.png`) // png should be referenced as a hashed file, not inlined.
|
|
}
|
|
|
|
func TestCSSBuildBootstrapFromNPM(t *testing.T) {
|
|
htesting.SkipSlowTestUnlessCI(t)
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term"]
|
|
-- assets/css/main.css --
|
|
@import "bootstrap";
|
|
-- package.json --
|
|
{
|
|
"devDependencies": {
|
|
"bootstrap": "5.3.8"
|
|
}
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ with . | css.Build (dict "minify" true) }}
|
|
CSS size: {{ .Content | len }}|{{ .RelPermalink }}
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
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 TestCSSBuildVarsNestedIssue14705(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/css/main.css --
|
|
@import "hugo:vars";
|
|
@import "hugo:vars/mobile" (max-width: 650px);
|
|
|
|
body {
|
|
background-color: var(--primary-color);
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := dict
|
|
"vars" (dict "primary-color" "blue" "mobile" (dict "primary-color" "red" "font-size" "12px"))
|
|
}}
|
|
{{ 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;",
|
|
"@media (max-width: 650px)",
|
|
"--primary-color: red;",
|
|
"! --mobile:",
|
|
"--font-size: 12px;",
|
|
"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 TestCSSBuildVarsNestedUpperCase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesTemplate := `
|
|
-- hugo.toml --
|
|
-- assets/css/main.css --
|
|
@import "hugo:vars";
|
|
@import "hugo:vars/MOBILE1" (max-width: 650px);
|
|
|
|
body {
|
|
background-color: var(--primary-color);
|
|
}
|
|
-- layouts/home.html --
|
|
{{ with resources.Get "css/main.css" }}
|
|
{{ $opts := dict
|
|
"vars" (dict "primary-color" "blue" "MOBILE2" (dict "primary-color" "red" "font-size" "12px" "MixedCaseKey" "value"))
|
|
}}
|
|
{{ with . | css.Build $opts }}
|
|
<link rel="stylesheet" href="{{ .RelPermalink }}" />
|
|
{{ end }}
|
|
{{ end }}
|
|
`
|
|
|
|
files := strings.ReplaceAll(filesTemplate, "MOBILE1", "Mobile")
|
|
files = strings.ReplaceAll(files, "MOBILE2", "mobile")
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", "primary-color: red;")
|
|
|
|
files = strings.ReplaceAll(filesTemplate, "MOBILE1", "mobile")
|
|
files = strings.ReplaceAll(files, "MOBILE2", "MobilE")
|
|
b = hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
b.AssertFileContent("public/css/main.css", "primary-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;",
|
|
)
|
|
}
|
|
|
|
func TestCSSBuildImportContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/foo.css --
|
|
body { color: orange; }
|
|
-- assets/css/main.css --
|
|
@import "foo.css" screen;
|
|
@import "bar.css" print;
|
|
p {
|
|
font-color: red;
|
|
}
|
|
-- layouts/home.html --
|
|
{{ $foo := resources.FromString "foo.css" "body { color: blue; }" }}
|
|
{{ $bar := resources.FromString "bar.css" "body { color: green; }" }}
|
|
{{/* resource.Resources implements the resources.ResourceGetter interface (the type of importContext). */}}
|
|
{{ $resources := slice $foo $bar}}
|
|
{{ $opts := dict "minify" true "importContext" $resources }}
|
|
{{ $css := resources.Get "css/main.css" | css.Build $opts }}
|
|
CSS: {{ $css.RelPermalink }}|
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
|
|
b.AssertFileContent("public/css/main.css", `@media screen{body{color:#00f}}@media print{body{color:green}}p{font-color:red}`)
|
|
}
|
|
|
|
func TestCSSBuildImportContextChromaStyles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section", "page"]
|
|
[markup.highlight]
|
|
style = "monokai"
|
|
-- assets/css/components/all.css --
|
|
@import "./chroma-light.css";
|
|
@import "./chroma-dark.css" (prefers-color-scheme: dark);
|
|
-- assets/css/main.css --
|
|
@import "./components/all.css";
|
|
|
|
:root {
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
-- layouts/home.html --
|
|
{{ $light := css.ChromaStyles (dict "targetPath" "css/components/chroma-light.css" "style" "github" "mode" "light") }}
|
|
{{ $dark := css.ChromaStyles (dict "targetPath" "css/components/chroma-dark.css" "style" "github" "mode" "dark") }}
|
|
{{ $opts := dict "minify" true "importContext" (slice $light $dark) }}
|
|
{{ $css := resources.Get "css/main.css" | css.Build $opts }}
|
|
CSS: {{ $css.RelPermalink }}|
|
|
`
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
|
|
b.AssertFileContent("public/index.html", "CSS: /css/main.css|")
|
|
b.AssertFileContent("public/css/main.css", ".bg{background-color:#f7f7f7}.chroma", "@media(prefers-color-scheme:dark){.bg{color:#e6edf3")
|
|
}
|
|
|
|
func TestCSSBuildImportContextHashes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
-- assets/foo.css --
|
|
body { color: orange; }
|
|
-- assets/css/main.css --
|
|
@import "foo.css" screen;
|
|
@import "bar.css" print;
|
|
p {
|
|
font-color: red;
|
|
}
|
|
-- layouts/home.html --
|
|
{{ $foo := resources.FromString "foo.css" "body { color: blue; }" }}|
|
|
{{ $foo2 := resources.FromString "foo2.css" "body { color: blue; }" }}|
|
|
{{ $foo3 := resources.FromString "foo.css" "body { color: indigo; }" }}|
|
|
{{ $bar := resources.FromString "bar.css" "body { color: green; }" }}|
|
|
{{ $resources := slice $foo $bar}}
|
|
{{ $resourcesMount1 := $resources.Mount "a" "b"}}
|
|
{{ $resourcesMount2 := $resources.Mount "a" "c"}}
|
|
{{ $opts := dict "minify" true "importContext" $resources }}
|
|
foo: {{ $foo | testinginternal.HashString }}|
|
|
foo2: {{ $foo2 | testinginternal.HashString }}|
|
|
foo3: {{ $foo3 | testinginternal.HashString }}|
|
|
bar: {{ $bar | testinginternal.HashString }}|
|
|
resources: {{ $resources | testinginternal.HashString }}|
|
|
resourcesMount1: {{ $resourcesMount1 | testinginternal.HashString }}|
|
|
resourcesMount2: {{ $resourcesMount2 | testinginternal.HashString }}|
|
|
resources namespace: {{ resources | testinginternal.HashString }}|
|
|
opts: {{ $opts | testinginternal.HashString }}|
|
|
home: {{ . | testinginternal.HashString }}|{{ .Key }}|
|
|
slice with Page: {{ slice $foo $bar . | testinginternal.HashString }}|
|
|
map with Page: {{ dict "foo" $foo "bar" $bar "page" . | testinginternal.HashString }}|
|
|
{{ $css := resources.Get "css/main.css" | css.Build $opts -}}
|
|
css: {{ $css | testinginternal.HashString }}|
|
|
cached1: {{ testinginternal.NewCachedResourceGetter $resources resources | testinginternal.HashString}}
|
|
cached2: {{ testinginternal.NewCachedResourceGetter $resources | testinginternal.HashString}}
|
|
cached3: {{ testinginternal.NewCachedResourceGetter $resources resources $resourcesMount1 $resourcesMount2 | testinginternal.HashString}}
|
|
|
|
|
|
`
|
|
|
|
for range 2 {
|
|
|
|
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
|
|
|
b.AssertFileContent("public/index.html", `
|
|
foo: 17610550322594361312|
|
|
foo2: 12741631597870435822|
|
|
foo3: 4488319114298108946|
|
|
bar: 11981658863051989001|
|
|
resources: 10963320145636139629|
|
|
resourcesMount1: 7843365040860281860|
|
|
resourcesMount2: 10632811173795339581|
|
|
resources namespace: 12714111578321948564|
|
|
opts: 7538269917289793785|
|
|
css: 8075855322897706813|
|
|
home: 166103792234269065|/html|
|
|
slice with Page: 7769684332502419906|
|
|
map with Page: 10393165700333352395|
|
|
cached1: 7064840587060576200
|
|
cached2: 6730658186132701788
|
|
cached3: 13761938017162285970
|
|
`)
|
|
|
|
}
|
|
}
|
|
|
|
func TestCSSBuildImportContextEdit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
files := `
|
|
-- hugo.toml --
|
|
disableLiveReload = true
|
|
-- assets/css/common/baz.css --
|
|
body { color: purple; }
|
|
-- assets/css/main.css --
|
|
@import "foo.css" screen;
|
|
@import "bar.css" print;
|
|
@import "baz.css" screen and (min-width: 768px);
|
|
p {
|
|
font-color: red;
|
|
}
|
|
-- layouts/home.html --
|
|
{{ $foo := resources.FromString "foo.css" "body { color: blue; }" }}
|
|
{{ $bar := resources.FromString "bar.css" "body { color: green; }" }}
|
|
{{ $common := resources.Match "/css/common/*.css" }}
|
|
{{ $commonMount := ($common.Mount "/css/common" ".")}}
|
|
{{ $resources := slice $foo $bar }}
|
|
{{ $opts := dict "minify" true "importContext" (slice $resources $commonMount) }}
|
|
resources: {{ $resources | testinginternal.HashString }}|
|
|
{{ $css := resources.Get "css/main.css" | css.Build $opts }}
|
|
CSS: {{ $css.RelPermalink }}|
|
|
`
|
|
|
|
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs())
|
|
|
|
b.AssertFileContent("public/css/main.css", `
|
|
@media screen{body{color:#00f}}@media print{body{color:green}}
|
|
@media screen and (min-width:768px){body{color:purple}}
|
|
`)
|
|
|
|
// Edit resource built with resources.FromString.
|
|
b.EditFileReplaceAll("layouts/home.html", "color: green", "color: yellow").Build()
|
|
b.AssertFileContent("public/css/main.css", `
|
|
@media screen{body{color:#00f}}@media print{body{color:#ff0}}
|
|
@media screen and (min-width:768px){body{color:purple}}
|
|
`)
|
|
|
|
// Edit resource in the mounted slice.
|
|
b.EditFileReplaceAll("assets/css/common/baz.css", "purple", "orange").Build()
|
|
b.AssertFileContent("public/css/main.css", `
|
|
@media screen{body{color:#00f}}@media print{body{color:#ff0}}
|
|
@media screen and (min-width:768px){body{color:orange}}
|
|
`)
|
|
}
|