mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-27 16:58:53 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76a5e1880a | |||
| 0bb337b22a | |||
| 03dc9170b3 | |||
| c829b736cc | |||
| 995a2159e5 | |||
| f88f0a9f59 | |||
| dd3f2731e0 | |||
| f772998fa3 | |||
| 8a55df7af2 | |||
| 52c9bd7908 | |||
| 44da086082 | |||
| 2ffaf1fc1e | |||
| a808f6e40d | |||
| 94f3908ec6 | |||
| 75fcc7524a | |||
| 33d1f2c853 | |||
| 64da6d7c62 | |||
| 70db201ed4 | |||
| 8a468df065 | |||
| b5fa03d3aa | |||
| 9da472dd3b | |||
| 615e45d6e8 | |||
| 635532a20f | |||
| a243a6154c | |||
| 9c71f600bf | |||
| 420527fc6b | |||
| 7fe786e3b0 | |||
| 03b244fc0b | |||
| 961181334f | |||
| e35b7f049d | |||
| 7d90277a28 | |||
| 0796fa7ace | |||
| 6bf15241a1 | |||
| 861ede6d10 | |||
| f228c87d41 | |||
| 1b701b72ca | |||
| a32d70b712 | |||
| 948cfb98f9 | |||
| 8930802ef2 | |||
| 7df45f615a | |||
| f961093ea9 | |||
| 89b8c32200 | |||
| 984358f047 | |||
| d1f191c58d |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22600.20300
|
||||
- image: bepsays/ci-hugoreleaser:1.22600.20500
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20300
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20500
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: false
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: true
|
||||
summary: false
|
||||
code_review: false
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
@@ -51,6 +51,7 @@ jobs:
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
gem install asciidoctor-html5s -v "0.5.1"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
- name: Install Python
|
||||
|
||||
+2
-1
@@ -5,4 +5,5 @@ dist/
|
||||
public/
|
||||
.DS_Store
|
||||
cache/filecache/_gen/
|
||||
.claude/
|
||||
.claude/
|
||||
dump.txt
|
||||
@@ -37,7 +37,6 @@ A fast and flexible static site generator built with love by [bep][], [spf13][],
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
[Website][] | [Installation][] | [Documentation][] | [Support][] | [Contributing][] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
|
||||
@@ -139,10 +138,6 @@ To build and install the extended/deploy edition, first install a C compiler suc
|
||||
CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#gohugoio/hugo&Timeline)
|
||||
|
||||
## Documentation
|
||||
|
||||
Hugo's [documentation][] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
|
||||
@@ -195,10 +190,48 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
|
||||
<summary>See current dependencies</summary>
|
||||
|
||||
```text
|
||||
cel.dev/expr="v0.25.1"
|
||||
cloud.google.com/go/auth/oauth2adapt="v0.2.8"
|
||||
cloud.google.com/go/auth="v0.20.0"
|
||||
cloud.google.com/go/compute/metadata="v0.9.0"
|
||||
cloud.google.com/go/iam="v1.5.3"
|
||||
cloud.google.com/go/monitoring="v1.24.3"
|
||||
cloud.google.com/go/storage="v1.57.2"
|
||||
cloud.google.com/go="v0.123.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.20.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.13.1"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.11.2"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.6.3"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.1"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.6.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/JohannesKaufmann/dom="v0.2.0"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.21.1"
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp="v1.31.0"
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric="v0.54.0"
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping="v0.54.0"
|
||||
github.com/JohannesKaufmann/dom="v0.3.1"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.2"
|
||||
github.com/alecthomas/chroma/v2="v2.27.0"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.7.8"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.32.2"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.19.2"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.18.14"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.20.12"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.4.22"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.7.22"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.8.4"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.4.22"
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront="v1.61.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.13.7"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.9.13"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.13.21"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.19.21"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.97.3"
|
||||
github.com/aws/aws-sdk-go-v2/service/signin="v1.0.2"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.30.5"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.35.10"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.41.2"
|
||||
github.com/aws/aws-sdk-go-v2="v1.41.6"
|
||||
github.com/aws/smithy-go="v1.25.0"
|
||||
github.com/aymerick/douceur="v0.2.0"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
@@ -206,105 +239,129 @@ github.com/bep/gitmap="v1.9.0"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.5.0"
|
||||
github.com/bep/golibsass="v1.2.0"
|
||||
github.com/bep/goportabletext="v0.1.0"
|
||||
github.com/bep/helpers="v0.6.0"
|
||||
github.com/bep/imagemeta="v0.12.0"
|
||||
github.com/bep/lazycache="v0.8.0"
|
||||
github.com/bep/golocales="v0.2.0"
|
||||
github.com/bep/goportabletext="v0.2.0"
|
||||
github.com/bep/helpers="v0.12.0"
|
||||
github.com/bep/imagemeta="v0.17.2"
|
||||
github.com/bep/lazycache="v0.8.1"
|
||||
github.com/bep/logg="v0.4.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.10.0"
|
||||
github.com/bep/simplecobra="v0.6.1"
|
||||
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/bits-and-blooms/bitset="v1.24.4"
|
||||
github.com/bep/mclib="v1.20401.20400"
|
||||
github.com/bep/overlayfs="v0.11.0"
|
||||
github.com/bep/simplecobra="v0.7.0"
|
||||
github.com/bep/textandbinarywriter="v0.1.0"
|
||||
github.com/bep/tmc="v0.6.0"
|
||||
github.com/bits-and-blooms/bitset="v1.24.5"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/clipperhouse/displaywidth="v0.6.0"
|
||||
github.com/clipperhouse/stringish="v0.1.1"
|
||||
github.com/clipperhouse/uax29/v2="v2.3.0"
|
||||
github.com/clipperhouse/displaywidth="v0.10.0"
|
||||
github.com/clipperhouse/uax29/v2="v2.6.0"
|
||||
github.com/cncf/xds/go="v0.0.0-20251210132809-ee656c7534f5"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.6"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.11.5"
|
||||
github.com/evanw/esbuild="v0.27.2"
|
||||
github.com/dlclark/regexp2/v2="v2.2.1"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/envoyproxy/go-control-plane/envoy="v1.36.0"
|
||||
github.com/envoyproxy/protoc-gen-validate="v1.3.0"
|
||||
github.com/evanw/esbuild="v0.28.1"
|
||||
github.com/fatih/color="v1.18.0"
|
||||
github.com/felixge/httpsnoop="v1.0.4"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.9.0"
|
||||
github.com/getkin/kin-openapi="v0.133.0"
|
||||
github.com/go-openapi/jsonpointer="v0.21.0"
|
||||
github.com/go-openapi/swag="v0.23.0"
|
||||
github.com/getkin/kin-openapi="v0.140.0"
|
||||
github.com/go-jose/go-jose/v4="v4.1.4"
|
||||
github.com/go-logr/logr="v1.4.3"
|
||||
github.com/go-logr/stdr="v1.2.2"
|
||||
github.com/go-openapi/jsonpointer="v0.22.5"
|
||||
github.com/go-openapi/swag/jsonname="v0.25.5"
|
||||
github.com/gobuffalo/flect="v1.0.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/goccy/go-yaml="v1.19.1"
|
||||
github.com/goccy/go-yaml="v1.19.2"
|
||||
github.com/gohugoio/gift="v0.2.0"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20251018145728-cfcc22d823c6"
|
||||
github.com/gohugoio/go-radix="v1.2.0"
|
||||
github.com/gohugoio/hashstructure="v0.6.0"
|
||||
github.com/gohugoio/httpcache="v0.8.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.5.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.1"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.7.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.5.0"
|
||||
github.com/golang-jwt/jwt/v5="v5.3.0"
|
||||
github.com/google/go-cmp="v0.7.0"
|
||||
github.com/google/s2a-go="v0.1.9"
|
||||
github.com/google/uuid="v1.6.0"
|
||||
github.com/google/wire="v0.7.0"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.3.14"
|
||||
github.com/googleapis/gax-go/v2="v2.21.0"
|
||||
github.com/gorilla/css="v1.0.1"
|
||||
github.com/gorilla/websocket="v1.5.3"
|
||||
github.com/hairyhenderson/go-codeowners="v0.7.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.7"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.13"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/makeworld-the-better-one/dither/v2="v2.4.0"
|
||||
github.com/marekm4/color-extractor="v1.2.1"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.20"
|
||||
github.com/mattn/go-colorable="v0.1.14"
|
||||
github.com/mattn/go-isatty="v0.0.22"
|
||||
github.com/mattn/go-runewidth="v0.0.19"
|
||||
github.com/microcosm-cc/bluemonday="v1.0.27"
|
||||
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
|
||||
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.9.1"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
|
||||
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
|
||||
github.com/oasdiff/yaml3="v0.0.13"
|
||||
github.com/oasdiff/yaml="v0.1.0"
|
||||
github.com/olekukonko/cat="v0.0.0-20250911104152-50322a0618f6"
|
||||
github.com/olekukonko/errors="v1.1.0"
|
||||
github.com/olekukonko/ll="v0.1.3"
|
||||
github.com/olekukonko/tablewriter="v1.1.2"
|
||||
github.com/olekukonko/errors="v1.2.0"
|
||||
github.com/olekukonko/ll="v0.1.6"
|
||||
github.com/olekukonko/tablewriter="v1.1.4"
|
||||
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
|
||||
github.com/pelletier/go-toml/v2="v2.2.4"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pelletier/go-toml/v2="v2.4.3"
|
||||
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.14.1"
|
||||
github.com/rogpeppe/go-internal="v1.15.0"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/sass/dart-sass/compiler="1.97.1"
|
||||
github.com/sass/dart-sass/implementation="1.97.1"
|
||||
github.com/sass/dart-sass/protocol="3.2.0"
|
||||
github.com/santhosh-tekuri/jsonschema/v6="v6.0.2"
|
||||
github.com/spf13/afero="v1.15.0"
|
||||
github.com/spf13/cast="v1.10.0"
|
||||
github.com/spf13/cobra="v1.10.2"
|
||||
github.com/spf13/fsync="v0.10.1"
|
||||
github.com/spf13/pflag="v1.0.9"
|
||||
github.com/tdewolff/minify/v2="v2.24.8"
|
||||
github.com/tdewolff/parse/v2="v2.8.5"
|
||||
github.com/tetratelabs/wazero="v1.10.1"
|
||||
github.com/spf13/pflag="v1.0.10"
|
||||
github.com/spiffe/go-spiffe/v2="v2.6.0"
|
||||
github.com/tdewolff/minify/v2="v2.24.13"
|
||||
github.com/tdewolff/parse/v2="v2.8.12"
|
||||
github.com/tetratelabs/wazero="v1.12.0"
|
||||
github.com/webmproject/libwebp="v1.6.0"
|
||||
github.com/woodsbury/decimal128="v1.3.0"
|
||||
github.com/yuin/goldmark-emoji="v1.0.6"
|
||||
github.com/yuin/goldmark="v1.7.13"
|
||||
github.com/yuin/goldmark="v1.8.2"
|
||||
go.opentelemetry.io/auto/sdk="v1.2.1"
|
||||
go.opentelemetry.io/contrib/detectors/gcp="v1.39.0"
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc="v0.67.0"
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp="v0.67.0"
|
||||
go.opentelemetry.io/otel/metric="v1.43.0"
|
||||
go.opentelemetry.io/otel/sdk/metric="v1.43.0"
|
||||
go.opentelemetry.io/otel/sdk="v1.43.0"
|
||||
go.opentelemetry.io/otel/trace="v1.43.0"
|
||||
go.opentelemetry.io/otel="v1.43.0"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
go.yaml.in/yaml/v3="v3.0.4"
|
||||
golang.org/x/crypto="v0.46.0"
|
||||
golang.org/x/image="v0.34.0"
|
||||
golang.org/x/mod="v0.31.0"
|
||||
golang.org/x/net="v0.48.0"
|
||||
golang.org/x/sync="v0.19.0"
|
||||
golang.org/x/sys="v0.39.0"
|
||||
golang.org/x/text="v0.32.0"
|
||||
golang.org/x/tools="v0.40.0"
|
||||
google.golang.org/protobuf="v1.36.10"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
gocloud.dev="v0.45.0"
|
||||
golang.org/x/crypto="v0.53.0"
|
||||
golang.org/x/image="v0.43.0"
|
||||
golang.org/x/mod="v0.37.0"
|
||||
golang.org/x/net="v0.56.0"
|
||||
golang.org/x/oauth2="v0.36.0"
|
||||
golang.org/x/sync="v0.21.0"
|
||||
golang.org/x/sys="v0.46.0"
|
||||
golang.org/x/text="v0.38.0"
|
||||
golang.org/x/time="v0.15.0"
|
||||
golang.org/x/tools="v0.47.0"
|
||||
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
|
||||
google.golang.org/api="v0.276.0"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20260319201613-d00831a3d3e7"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20260401024825-9d38bb4040a9"
|
||||
google.golang.org/genproto="v0.0.0-20260319201613-d00831a3d3e7"
|
||||
google.golang.org/grpc="v1.80.0"
|
||||
google.golang.org/protobuf="v1.36.11"
|
||||
rsc.io/qr="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.7.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
Vendored
+54
-13
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -59,6 +60,29 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
counter := 0
|
||||
|
||||
seen := c.entryLocker.seen
|
||||
seenByLower := make(map[string]string, seen.Len())
|
||||
for id := range seen.All() {
|
||||
seenByLower[strings.ToLower(id)] = id
|
||||
}
|
||||
|
||||
// Names on disk matching a used cache key except for the case, and the used keys
|
||||
// actually walked. See the note about case-insensitive filesystems below.
|
||||
var candidates map[string]string
|
||||
visited := make(map[string]bool, seen.Len())
|
||||
|
||||
remove := func(name string) error {
|
||||
err := c.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
return nil
|
||||
}
|
||||
if !herrors.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil
|
||||
@@ -93,27 +117,44 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
shouldRemove := force || c.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && c.entryLocker.seen.Len() > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
shouldRemove = !c.entryLocker.seen.Has(name)
|
||||
if seen.Has(name) {
|
||||
visited[name] = true
|
||||
} else if !shouldRemove && seen.Len() > 0 {
|
||||
if id, found := seenByLower[strings.ToLower(name)]; found {
|
||||
// On case-insensitive filesystems this is the same file as id; e.g. an
|
||||
// entry created before Hugo started lowercasing the content paths in
|
||||
// v0.123 (content/MyBundle => _gen/images/MyBundle). Decided once the
|
||||
// walk is done: if id is walked too, they are distinct files and this
|
||||
// one is stale. See issue 15101.
|
||||
if candidates == nil {
|
||||
candidates = make(map[string]string)
|
||||
}
|
||||
candidates[name] = id
|
||||
} else {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
shouldRemove = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldRemove {
|
||||
err := c.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
}
|
||||
|
||||
if err != nil && !herrors.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return remove(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return counter, err
|
||||
}
|
||||
|
||||
return counter, err
|
||||
for name, id := range candidates {
|
||||
if visited[id] {
|
||||
if err := remove(name); err != nil {
|
||||
return counter, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDirs(force bool) (int, error) {
|
||||
|
||||
+61
@@ -20,11 +20,72 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
// A cache entry created before Hugo started lowercasing content paths in v0.123
|
||||
// (e.g. _gen/images/MyBundle) is on a case-insensitive filesystem the same file as
|
||||
// the lowercased cache key used today, and must not be pruned.
|
||||
// See issue 15101.
|
||||
func TestPruneCacheEntryWithOtherCase(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
dir := t.TempDir()
|
||||
if isCaseInsensitive, err := htesting.IsCaseInsensitiveFs(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !isCaseInsensitive {
|
||||
t.Skip("skip test on case-sensitive filesystem")
|
||||
}
|
||||
|
||||
fs := afero.NewBasePathFs(afero.NewOsFs(), dir)
|
||||
newCache := func() *filecache.Cache {
|
||||
return filecache.NewCache(fs, filecache.FileCacheConfig{Dir: "cache", MaxAge: -1})
|
||||
}
|
||||
|
||||
c.Assert(newCache().SetBytes("MyBundle/i1", []byte("abc")), qt.IsNil)
|
||||
|
||||
cache := newCache()
|
||||
_, b, err := cache.GetOrCreateBytes("mybundle/i1", func() ([]byte, error) {
|
||||
return []byte("def"), nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
|
||||
count, err := cache.Prune(false)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 0)
|
||||
c.Assert(cache.GetString("MyBundle/i1"), qt.Equals, "abc")
|
||||
}
|
||||
|
||||
// On a case-sensitive filesystem the entries above are distinct files,
|
||||
// and the one not used in this build should be pruned.
|
||||
func TestPruneCacheEntryWithOtherCaseCaseSensitiveFs(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
cache := filecache.NewCache(fs, filecache.FileCacheConfig{Dir: "cache", MaxAge: -1})
|
||||
|
||||
c.Assert(cache.SetBytes("MyBundle/i1", []byte("abc")), qt.IsNil)
|
||||
|
||||
cache = filecache.NewCache(fs, filecache.FileCacheConfig{Dir: "cache", MaxAge: -1})
|
||||
_, b, err := cache.GetOrCreateBytes("mybundle/i1", func() ([]byte, error) {
|
||||
return []byte("def"), nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Equals, "def")
|
||||
|
||||
count, err := cache.Prune(false)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 1)
|
||||
c.Assert(cache.GetString("MyBundle/i1"), qt.Equals, "")
|
||||
c.Assert(cache.GetString("mybundle/i1"), qt.Equals, "def")
|
||||
}
|
||||
|
||||
func TestPrune(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -38,13 +38,20 @@ run_gofmt() {
|
||||
|
||||
# Run staticcheck
|
||||
run_staticcheck() {
|
||||
# Check if staticcheck is installed, install if not
|
||||
if ! command -v staticcheck &> /dev/null; then
|
||||
local staticcheck_bin
|
||||
if command -v staticcheck &> /dev/null; then
|
||||
staticcheck_bin=$(command -v staticcheck)
|
||||
else
|
||||
echo "==> Installing staticcheck..."
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
staticcheck_bin="$(go env GOBIN)"
|
||||
if [ -z "$staticcheck_bin" ]; then
|
||||
staticcheck_bin="$(go env GOPATH)/bin"
|
||||
fi
|
||||
staticcheck_bin="$staticcheck_bin/staticcheck"
|
||||
fi
|
||||
echo "==> Running staticcheck..."
|
||||
staticcheck $PACKAGES
|
||||
"$staticcheck_bin" $PACKAGES
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
|
||||
+21
-109
@@ -21,12 +21,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
@@ -34,6 +30,7 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/markup/highlight"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
@@ -49,6 +46,8 @@ func newGenCommand() *genCommand {
|
||||
style string
|
||||
mode string
|
||||
modeSelector bool
|
||||
classDark string
|
||||
classLight string
|
||||
highlightStyle string
|
||||
lineNumbersInlineStyle string
|
||||
lineNumbersTableStyle string
|
||||
@@ -65,85 +64,26 @@ func newGenCommand() *genCommand {
|
||||
See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview of the available styles.`,
|
||||
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
style = strings.ToLower(style)
|
||||
if !slices.Contains(styles.Names(), style) {
|
||||
return fmt.Errorf("invalid style: %s", style)
|
||||
}
|
||||
var chromaStyle *chroma.Style
|
||||
if mode != "" {
|
||||
var chromaMode chroma.Mode
|
||||
switch mode {
|
||||
case "light":
|
||||
chromaMode = chroma.Light
|
||||
case "dark":
|
||||
chromaMode = chroma.Dark
|
||||
default:
|
||||
return fmt.Errorf("invalid mode: %s", mode)
|
||||
}
|
||||
|
||||
chromaStyle = styles.GetForMode(style, chromaMode)
|
||||
if chromaStyle.Mode() != chromaMode {
|
||||
return fmt.Errorf("style %q does not have a %q mode", style, mode)
|
||||
}
|
||||
} else {
|
||||
chromaStyle = styles.Get(style)
|
||||
}
|
||||
builder := chromaStyle.Builder()
|
||||
if highlightStyle != "" {
|
||||
builder.Add(chroma.LineHighlight, highlightStyle)
|
||||
}
|
||||
if lineNumbersInlineStyle != "" {
|
||||
builder.Add(chroma.LineNumbers, lineNumbersInlineStyle)
|
||||
}
|
||||
if lineNumbersTableStyle != "" {
|
||||
builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle)
|
||||
}
|
||||
style, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if omitEmpty {
|
||||
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
|
||||
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
|
||||
}
|
||||
options := []html.Option{
|
||||
html.WithCSSComments(!omitClassComments),
|
||||
}
|
||||
if !modeSelector {
|
||||
// Only needed for the shared-scope overlay; --modeSelector
|
||||
// gives each mode its own scope and makes this redundant.
|
||||
options = append(options, html.WithCustomCSS(chromaCSSOverrides(style)))
|
||||
}
|
||||
|
||||
formatter := html.New(options...)
|
||||
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
formatter.WriteCSS(&buf, style)
|
||||
css := buf.String()
|
||||
if modeSelector {
|
||||
// Scope every selector under a top level mode class, e.g. ".dark .chroma".
|
||||
// This allows generating both light and dark stylesheets and toggling
|
||||
// them with a parent class on the page.
|
||||
// There's no upstream option for this (I think), so do string replacements for now.
|
||||
// TODO(bep) upstream option for this.
|
||||
var prefix string
|
||||
switch style.Mode() {
|
||||
case chroma.Light:
|
||||
prefix = ".light "
|
||||
case chroma.Dark:
|
||||
prefix = ".dark "
|
||||
default:
|
||||
return fmt.Errorf("style %q does not have a %q mode", style.Name, mode)
|
||||
}
|
||||
replacer := strings.NewReplacer(
|
||||
".bg {", prefix+".bg {",
|
||||
".chroma ", prefix+".chroma ",
|
||||
)
|
||||
css = replacer.Replace(css)
|
||||
css, err := highlight.ChromaStylesCSS(highlight.ChromaStylesOptions{
|
||||
Style: style,
|
||||
Mode: mode,
|
||||
ModeSelector: modeSelector,
|
||||
ClassDark: classDark,
|
||||
ClassLight: classLight,
|
||||
HighlightStyle: highlightStyle,
|
||||
LineNumbersInlineStyle: lineNumbersInlineStyle,
|
||||
LineNumbersTableStyle: lineNumbersTableStyle,
|
||||
OmitClassComments: omitClassComments,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
fmt.Print(css)
|
||||
return nil
|
||||
},
|
||||
@@ -155,6 +95,10 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
|
||||
_ = cmd.RegisterFlagCompletionFunc("mode", cobra.FixedCompletions([]string{"light", "dark"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
cmd.PersistentFlags().BoolVar(&modeSelector, "modeSelector", false, `scope selectors under a top level mode class, e.g. ".dark .chroma"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("modeSelector", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&classDark, "classDark", "dark", `class name used by --modeSelector for dark styles`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("classDark", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&classLight, "classLight", "light", `class name used by --modeSelector for light styles`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("classLight", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
|
||||
@@ -330,38 +274,6 @@ url: %s
|
||||
}
|
||||
}
|
||||
|
||||
// chromaCSSOverrides re-emits leaf token colors that Chroma's minifier drops
|
||||
// because they equal the style's default foreground (the .chroma color). Without
|
||||
// modeSelector scoping, a paired light/dark stylesheet shares the same .chroma
|
||||
// scope, and the light sheet's explicit rule (e.g. .chroma .nx) would otherwise
|
||||
// leak into dark mode, since an explicit declaration beats inheritance.
|
||||
func chromaCSSOverrides(style *chroma.Style) map[chroma.TokenType]string {
|
||||
bg := style.Get(chroma.Background)
|
||||
m := make(map[chroma.TokenType]string)
|
||||
for tt := range chroma.StandardTypes {
|
||||
if tt == chroma.Background || !style.Has(tt) || !chromaLeafToken(tt) {
|
||||
continue
|
||||
}
|
||||
entry := style.Get(tt)
|
||||
if !entry.Sub(bg).IsZero() || !entry.Colour.IsSet() {
|
||||
continue
|
||||
}
|
||||
if css := html.StyleEntryToCSS(chroma.StyleEntry{Colour: entry.Colour}); css != "" {
|
||||
m[tt] = css
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func chromaLeafToken(tt chroma.TokenType) bool {
|
||||
for other := range chroma.StandardTypes {
|
||||
if other != tt && (other.Category() == tt || other.SubCategory() == tt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type genCommand struct {
|
||||
rootCmd *rootCommand
|
||||
|
||||
|
||||
+1
-1
@@ -1108,7 +1108,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
|
||||
fromPath := ev.Name
|
||||
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath, true)
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath, false)
|
||||
|
||||
if !found {
|
||||
// Not member of this virtual host.
|
||||
|
||||
+41
-21
@@ -18,6 +18,7 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
@@ -105,6 +106,16 @@ func MD5FromStringHexEncoded(f string) string {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// MD5FromReaderHexEncoded returns the MD5 hash of the given reader.
|
||||
func MD5FromReaderHexEncoded(r io.Reader) string {
|
||||
h := md5.New()
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// HashString returns a hash from the given elements.
|
||||
// It will panic if the hash cannot be calculated.
|
||||
// Note that this hash should be used primarily for identity, not for change detection as
|
||||
@@ -124,11 +135,38 @@ func HashStringHex(vs ...any) string {
|
||||
var hashOptsPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &hashstructure.HashOptions{
|
||||
Hasher: xxhash.New(),
|
||||
Hasher: xxhash.New(),
|
||||
UnwrapFunc: unwrapForHashing,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// hashstructure only sees exported struct fields, so rewrite known identity types before hashing,
|
||||
// e.g. a Resource or Page nested in an options map hashes by its Key.
|
||||
func unwrapForHashing(v reflect.Value) (reflect.Value, error) {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return v, nil
|
||||
}
|
||||
var in any
|
||||
if v.CanAddr() {
|
||||
// The common case; pointer receiver methods on a struct
|
||||
// reached through a pointer.
|
||||
in = v.Addr().Interface()
|
||||
} else {
|
||||
in = v.Interface()
|
||||
}
|
||||
switch t := in.(type) {
|
||||
case hashstructure.Hashable:
|
||||
// Let hashstructure handle it.
|
||||
return v, nil
|
||||
case keyer:
|
||||
return reflect.ValueOf(t.Key()), nil
|
||||
case identity.IdentityProvider:
|
||||
return reflect.ValueOf(t.GetIdentity()), nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func getHashOpts() *hashstructure.HashOptions {
|
||||
return hashOptsPool.Get().(*hashstructure.HashOptions)
|
||||
}
|
||||
@@ -145,15 +183,10 @@ func putHashOpts(opts *hashstructure.HashOptions) {
|
||||
func HashUint64(vs ...any) uint64 {
|
||||
var o any
|
||||
if len(vs) == 1 {
|
||||
o = toHashable(vs[0])
|
||||
o = vs[0]
|
||||
} else {
|
||||
elements := make([]any, len(vs))
|
||||
for i, e := range vs {
|
||||
elements[i] = toHashable(e)
|
||||
}
|
||||
o = elements
|
||||
o = vs
|
||||
}
|
||||
|
||||
hash, err := Hash(o)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -176,19 +209,6 @@ type keyer interface {
|
||||
Key() string
|
||||
}
|
||||
|
||||
// For structs, hashstructure.Hash only works on the exported fields,
|
||||
// so rewrite the input slice for known identity types.
|
||||
func toHashable(v any) any {
|
||||
switch t := v.(type) {
|
||||
case keyer:
|
||||
return t.Key()
|
||||
case identity.IdentityProvider:
|
||||
return t.GetIdentity()
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
type xxhashReadFrom struct {
|
||||
buff []byte
|
||||
*xxhash.Digest
|
||||
|
||||
+7
-2
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/version"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/internal/warpc"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
@@ -211,7 +212,10 @@ func GetDependencyList() []string {
|
||||
|
||||
// GetDependencyListNonGo returns a list of non-Go dependencies.
|
||||
func GetDependencyListNonGo() []string {
|
||||
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
|
||||
var deps []string
|
||||
for _, dep := range warpc.GetWASMDeps() {
|
||||
deps = append(deps, formatDep(dep[0], dep[1]))
|
||||
}
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
@@ -225,7 +229,8 @@ func GetDependencyListNonGo() []string {
|
||||
if IsDartSassGeV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
deps = append(deps,
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep(dartSassPath+"/protocol", dartSass.ProtocolVersion),
|
||||
formatDep(dartSassPath+"/compiler", dartSass.CompilerVersion),
|
||||
formatDep(dartSassPath+"/implementation", dartSass.ImplementationVersion),
|
||||
|
||||
@@ -35,8 +35,8 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
|
||||
// Added just to find the threshold for where we can remove deprecated items.
|
||||
// Subtract 5 from the minor version of the first ERRORed version => 0.136.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.141.0"), qt.Equals, logg.LevelError)
|
||||
// Subtract 5 from the minor version of the first ERRORed version => 0.145.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.150.0"), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
func TestMarkupScope(t *testing.T) {
|
||||
|
||||
@@ -21,5 +21,5 @@ var CurrentVersion = version.Version{
|
||||
Major: 0,
|
||||
Minor: 165,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -458,36 +458,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy privacy values.
|
||||
if c.Privacy.Twitter.Disable {
|
||||
hugo.DeprecateWithLogger("project config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
|
||||
}
|
||||
if c.Privacy.Twitter.EnableDNT {
|
||||
hugo.DeprecateWithLogger("project config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
|
||||
}
|
||||
if c.Privacy.Twitter.Simple {
|
||||
hugo.DeprecateWithLogger("project config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
|
||||
}
|
||||
|
||||
// Legacy services values.
|
||||
if c.Services.Twitter.DisableInlineCSS {
|
||||
hugo.DeprecateWithLogger("project config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
|
||||
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
|
||||
}
|
||||
|
||||
// Legacy permalink tokens
|
||||
for _, pc := range c.Permalinks {
|
||||
if strings.Contains(pc.Pattern, ":filename") {
|
||||
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
if strings.Contains(pc.Pattern, ":slugorfilename") {
|
||||
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy render hook values.
|
||||
alternativeDetails := fmt.Sprintf(
|
||||
"Set to %q if previous value was false, or set to %q if previous value was true.",
|
||||
|
||||
@@ -30,7 +30,6 @@ type Config struct {
|
||||
Disqus Disqus
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram
|
||||
Twitter Twitter `json:"-"` // deprecated in favor of X in v0.141.0
|
||||
Vimeo Vimeo
|
||||
YouTube YouTube
|
||||
X X
|
||||
@@ -59,19 +58,6 @@ type Instagram struct {
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// Twitter holds the privacy configuration settings related to the Twitter shortcode.
|
||||
// Deprecated in favor of X in v0.141.0.
|
||||
type Twitter struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
// When set to true, the Tweet and its embedded page on your site are not used
|
||||
// for purposes that include personalized suggestions and personalized ads.
|
||||
EnableDNT bool
|
||||
|
||||
// If simple mode is enabled, a static and no-JS version of the Tweet will be built.
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// Vimeo holds the privacy configuration settings related to the Vimeo shortcode.
|
||||
type Vimeo struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
@@ -39,12 +39,11 @@ const securityConfigKey = "security"
|
||||
var DefaultConfig = Config{
|
||||
Exec: Exec{
|
||||
Allow: MustNewWhitelist(
|
||||
"^(dart-)?sass(-embedded)?$", // sass, dart-sass, dart-sass-embedded.
|
||||
"^go$", // for Go Modules
|
||||
"^git$", // For Git info
|
||||
"^node$", // Used as the runtime for Node tools.
|
||||
"^(dart-)?sass$", // sass, dart-sass
|
||||
"^go$", // for Go Modules
|
||||
"^git$", // For Git info
|
||||
"^node$", // Used as the runtime for Node tools.
|
||||
"^postcss$",
|
||||
"^tailwindcss$",
|
||||
),
|
||||
// These have been tested to work with Hugo's external programs
|
||||
// on Windows, Linux and MacOS.
|
||||
|
||||
@@ -135,7 +135,7 @@ func TestToTOML(t *testing.T) {
|
||||
got := DefaultConfig.ToTOML()
|
||||
|
||||
c.Assert(got, qt.Equals,
|
||||
"[security]\n allowContent = ['! ^text/html$']\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^git$', '^node$', '^postcss$', '^tailwindcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['(?i)^https?://[a-z0-9]', '! ^https?://\\d+\\.', '! (?i)localhost', '! (?i)^https?://[^/?#]*@']\n\n [security.node]\n [security.node.permissions]\n allowAddons = ['tailwindcss']\n allowChildProcess = ['tailwindcss']\n allowRead = ['.']\n allowWorker = ['tailwindcss']\n allowWrite = []\n disable = false",
|
||||
"[security]\n allowContent = ['! ^text/html$']\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['(?i)^https?://[a-z0-9]', '! ^https?://\\d+\\.', '! (?i)localhost', '! (?i)^https?://[^/?#]*@']\n\n [security.node]\n [security.node.permissions]\n allowAddons = ['tailwindcss']\n allowChildProcess = ['tailwindcss']\n allowRead = ['.']\n allowWorker = ['tailwindcss']\n allowWrite = []\n disable = false",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ type Config struct {
|
||||
Disqus Disqus
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram `json:"-"` // the embedded instagram shortcode no longer uses this
|
||||
Twitter Twitter `json:"-"` // deprecated in favor of X in v0.141.0
|
||||
X X
|
||||
RSS RSS
|
||||
}
|
||||
@@ -61,15 +60,6 @@ type Instagram struct {
|
||||
AccessToken string // this is no longer used by the embedded instagram shortcode
|
||||
}
|
||||
|
||||
// Twitter holds the functional configuration settings related to the Twitter shortcodes.
|
||||
// Deprecated in favor of X in v0.141.0.
|
||||
type Twitter struct {
|
||||
// The Simple variant of Twitter is decorated with a basic set of inline styles.
|
||||
// This means that if you want to provide your own CSS, you want
|
||||
// to disable the inline CSS provided by Hugo.
|
||||
DisableInlineCSS bool
|
||||
}
|
||||
|
||||
// X holds the functional configuration settings related to the X shortcodes.
|
||||
type X struct {
|
||||
// The Simple variant of X is decorated with a basic set of inline styles.
|
||||
|
||||
@@ -15,7 +15,7 @@ require (
|
||||
github.com/bep/golocales v0.2.0
|
||||
github.com/bep/goportabletext v0.2.0
|
||||
github.com/bep/helpers v0.12.0
|
||||
github.com/bep/imagemeta v0.17.2
|
||||
github.com/bep/imagemeta v1.0.0
|
||||
github.com/bep/lazycache v0.8.1
|
||||
github.com/bep/logg v0.4.0
|
||||
github.com/bep/mclib v1.20401.20400
|
||||
@@ -27,19 +27,19 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/clbanning/mxj/v2 v2.7.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/evanw/esbuild v0.28.1
|
||||
github.com/evanw/esbuild v0.28.2
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/getkin/kin-openapi v0.140.0
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/getkin/kin-openapi v0.146.0
|
||||
github.com/gobuffalo/flect v1.0.3
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/gohugoio/gift v0.2.0
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6
|
||||
github.com/gohugoio/go-radix v1.2.0
|
||||
github.com/gohugoio/hashstructure v0.6.0
|
||||
github.com/gohugoio/hashstructure v1.0.0
|
||||
github.com/gohugoio/httpcache v0.8.0
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0
|
||||
@@ -47,11 +47,11 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0
|
||||
github.com/jdkato/prose v1.2.1
|
||||
github.com/kyokomi/emoji/v2 v2.2.13
|
||||
github.com/kyokomi/emoji/v2 v2.2.14
|
||||
github.com/magefile/mage v1.17.2
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0
|
||||
github.com/marekm4/color-extractor v1.2.1
|
||||
github.com/mattn/go-isatty v0.0.22
|
||||
github.com/mattn/go-isatty v0.0.24
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
|
||||
github.com/muesli/smartcrop v0.3.0
|
||||
@@ -60,25 +60,25 @@ require (
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/rogpeppe/go-internal v1.15.0
|
||||
github.com/rogpeppe/go-internal v1.16.0
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/fsync v0.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tdewolff/minify/v2 v2.24.13
|
||||
github.com/tdewolff/parse/v2 v2.8.12
|
||||
github.com/tdewolff/minify/v2 v2.24.16
|
||||
github.com/tdewolff/parse/v2 v2.8.15
|
||||
github.com/tetratelabs/wazero v1.12.0
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
github.com/yuin/goldmark v1.8.5
|
||||
github.com/yuin/goldmark-emoji v1.0.6
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
gocloud.dev v0.45.0
|
||||
golang.org/x/image v0.43.0
|
||||
golang.org/x/mod v0.37.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.org/x/tools v0.47.0
|
||||
golang.org/x/image v0.44.0
|
||||
golang.org/x/mod v0.38.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/tools v0.48.0
|
||||
google.golang.org/api v0.276.0
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
@@ -99,7 +99,7 @@ require (
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/JohannesKaufmann/dom v0.3.1 // indirect
|
||||
@@ -125,12 +125,12 @@ require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -152,8 +152,8 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/oasdiff/yaml v0.1.0 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.13 // indirect
|
||||
github.com/oasdiff/yaml v0.1.1 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.14 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.6 // indirect
|
||||
@@ -163,7 +163,7 @@ require (
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
@@ -172,15 +172,15 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
|
||||
|
||||
@@ -84,8 +84,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
|
||||
@@ -164,8 +164,8 @@ github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Y
|
||||
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
|
||||
github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
|
||||
github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
|
||||
github.com/bep/imagemeta v0.17.2 h1:fDyXM1eAqCfBeqGLqS6UsN4OfuLM0cdu70KuLCehjOg=
|
||||
github.com/bep/imagemeta v0.17.2/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
|
||||
github.com/bep/imagemeta v1.0.0 h1:8JZmahegn5p1M9CpFmga7LoSxErpu1uFfpSzMUNju7A=
|
||||
github.com/bep/imagemeta v1.0.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
|
||||
github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg=
|
||||
github.com/bep/lazycache v0.8.1/go.mod h1:pbEiFsZoq7cLXvrTll0AHOPEurB1aGGxx4jKjOtlx9w=
|
||||
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
|
||||
@@ -198,8 +198,8 @@ github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -220,15 +220,15 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc=
|
||||
github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/evanw/esbuild v0.28.2 h1:A2uETn4jrQTcXaT/shwTDTYBxDjl7fV7nXmUrJxfA2w=
|
||||
github.com/evanw/esbuild v0.28.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -238,10 +238,10 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g=
|
||||
github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
|
||||
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -272,8 +272,8 @@ github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eR
|
||||
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
|
||||
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
|
||||
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
|
||||
github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg=
|
||||
github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ=
|
||||
github.com/gohugoio/hashstructure v1.0.0 h1:vWYuyzs1n0LdI0F54TJQeYAiB44fHX7H9hCp9X6gHKg=
|
||||
github.com/gohugoio/hashstructure v1.0.0/go.mod h1:FSbTK4QwxucJ2bC4Lvrs9a6x0DbQDXNoyBO+h4nlCgE=
|
||||
github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU=
|
||||
github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus=
|
||||
@@ -392,8 +392,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
|
||||
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
|
||||
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
|
||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
|
||||
@@ -402,8 +402,8 @@ github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mr
|
||||
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -418,10 +418,10 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||
github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg=
|
||||
github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0=
|
||||
github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg=
|
||||
github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
||||
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
||||
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
||||
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
@@ -450,8 +450,8 @@ github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
@@ -489,11 +489,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58=
|
||||
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0=
|
||||
github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg=
|
||||
github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tdewolff/minify/v2 v2.24.16 h1:1d+IHElvmYM2zJtuykMK/esiqc4H/7pqPKAa7jpSUUU=
|
||||
github.com/tdewolff/minify/v2 v2.24.16/go.mod h1:G6yep6HFo3u2eoOcIVBKSNwpoW2wXvPjvnFRj3w1zlc=
|
||||
github.com/tdewolff/parse/v2 v2.8.15 h1:3/Psth16LHpsy2NS/7ShXDsoWt1x0f3tQMSbmQi+jO8=
|
||||
github.com/tdewolff/parse/v2 v2.8.15/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
|
||||
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||
@@ -502,8 +501,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@@ -514,8 +513,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
@@ -545,8 +544,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -559,8 +558,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -582,8 +581,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -615,8 +614,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -638,8 +637,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -676,8 +675,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -685,8 +684,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -739,8 +738,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -815,10 +814,10 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -835,8 +834,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
@@ -16,6 +16,7 @@ package htesting
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -58,6 +59,27 @@ func CreateTempDir(fs afero.Fs, prefix string) (string, func(), error) {
|
||||
return tempDir, func() { fs.RemoveAll(tempDir) }, nil
|
||||
}
|
||||
|
||||
// IsCaseInsensitiveFs reports whether dir lives on a case-insensitive filesystem
|
||||
// (e.g. the default on macOS and Windows).
|
||||
func IsCaseInsensitiveFs(dir string) (bool, error) {
|
||||
f, err := os.CreateTemp(dir, "case-*.tmp")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
_, err = os.Stat(filepath.Join(dir, strings.ToUpper(filepath.Base(f.Name()))))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// BailOut panics with a stack trace after the given duration. Useful for
|
||||
// hanging tests.
|
||||
func BailOut(after time.Duration) {
|
||||
|
||||
@@ -15,10 +15,37 @@ package hugofs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// hasSymlinkParent reports whether any directory between name and base is a symlink.
|
||||
// Lstat only refrains from following the last element of a path, so callers checking
|
||||
// name itself need this to keep a symlinked parent from escaping base.
|
||||
// base itself is not checked; an empty base walks to the root of fs.
|
||||
func hasSymlinkParent(fs afero.Fs, base, name string) (bool, error) {
|
||||
for name != base {
|
||||
parent := filepath.Dir(name)
|
||||
if parent == name || parent == base || parent == "." {
|
||||
return false, nil
|
||||
}
|
||||
fi, err := LstatIfPossible(fs, parent)
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return true, nil
|
||||
}
|
||||
name = parent
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// NewDropSymlinksFs returns an afero.Fs wrapper that treats symlinks as non-existing files.
|
||||
func NewDropSymlinksFs(base afero.Fs) *DropSymlinksFs {
|
||||
return &DropSymlinksFs{base}
|
||||
@@ -48,5 +75,12 @@ func (fs *DropSymlinksFs) Stat(name string) (os.FileInfo, error) {
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
symlinkParent, err := hasSymlinkParent(fs.Fs, "", name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if symlinkParent {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
@@ -729,10 +729,18 @@ func (fs *RootMappingFs) statRoot(root *RootMapping, filename string) (FileMetaI
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Don't allow symlinks to escape the mount.
|
||||
// Don't allow symlinks to escape the mount, neither as the file itself
|
||||
// nor as any directory between it and the mount root.
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
symlinkParent, err := hasSymlinkParent(fs.Fs, root.To, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if symlinkParent {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
var opener func() (afero.File, error)
|
||||
if !fi.IsDir() {
|
||||
|
||||
@@ -362,7 +362,7 @@ func (a *allPagesAssembler) doCreatePages(prefix string, depth int) error {
|
||||
|
||||
switch v := n.(type) {
|
||||
case contentNodeSeq, contentNodes:
|
||||
return handleContentNodeSeq(contentNodeToSeq(v))
|
||||
return handleContentNodeSeq(cnh.contentNodeToSeq(v))
|
||||
case *pageMetaSource:
|
||||
n2, err = handlePageMetaSource(v, nil, false)
|
||||
if err != nil {
|
||||
@@ -781,7 +781,7 @@ func (a *allPagesAssembler) doCreatePages(prefix string, depth int) error {
|
||||
|
||||
if _, found := nodes[p.s.siteVector]; !found {
|
||||
var rs *resourceSource
|
||||
match := cnh.findContentNodeForSiteVector(p.s.siteVector, duplicateResourceFiles, contentNodeToSeq(n))
|
||||
match := cnh.findContentNodeForSiteVector(p.s.siteVector, duplicateResourceFiles, cnh.contentNodeToSeq(n))
|
||||
if match == nil {
|
||||
return true
|
||||
}
|
||||
@@ -1385,8 +1385,11 @@ func (a *allPagesAssembler) createMissingTaxonomies() error {
|
||||
for viewName, languages := range viewLanguages {
|
||||
key := viewName.pluralTreeKey
|
||||
if a.h.isRebuild() {
|
||||
if v := tree.Get(key); v != nil {
|
||||
// Already there.
|
||||
// Note that we cannot use tree.Get here, as the tree at this point
|
||||
// may hold not yet assembled *pageMetaSource nodes.
|
||||
// We're only interested in whether the auto created (not file backed)
|
||||
// taxonomy node is still there.
|
||||
if n, found := tree.GetRaw(key); found && cnh.hasAutoContentNode(n) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ var _ contentNode = (*contentNodeSeq)(nil)
|
||||
|
||||
var (
|
||||
_ contentNode = (*resourceSource)(nil)
|
||||
_ contentNodeContentWeightProvider = (*pageMetaSource)(nil)
|
||||
_ contentNodeContentWeightProvider = (*pageState)(nil)
|
||||
_ contentNodeForSites = (*pageState)(nil)
|
||||
_ contentNodePage = (*contentNodes)(nil)
|
||||
@@ -395,7 +396,15 @@ func contentNodeToContentNodesPage(n contentNode) (contentNodesMap, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func contentNodeToSeq(n contentNodeForEach) contentNodeSeq {
|
||||
// hasAutoContentNode reports whether n holds at least one node that is not backed by a file.
|
||||
func (h helperContentNode) hasAutoContentNode(n contentNodeForEach) bool {
|
||||
return !n.forEeachContentNode(func(_ sitesmatrix.Vector, nn contentNode) bool {
|
||||
wp, ok := nn.(contentNodeContentWeightProvider)
|
||||
return ok && wp.contentWeight() > 0
|
||||
})
|
||||
}
|
||||
|
||||
func (h helperContentNode) contentNodeToSeq(n contentNodeForEach) contentNodeSeq {
|
||||
if nn, ok := n.(contentNodeSeq); ok {
|
||||
return nn
|
||||
}
|
||||
@@ -406,7 +415,7 @@ func contentNodeToSeq(n contentNodeForEach) contentNodeSeq {
|
||||
}
|
||||
}
|
||||
|
||||
func contentNodeToSeq2(n contentNodeForEach) contentNodeSeq2 {
|
||||
func (h helperContentNode) contentNodeToSeq2(n contentNodeForEach) contentNodeSeq2 {
|
||||
if nn, ok := n.(contentNodeSeq2); ok {
|
||||
return nn
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func (s *contentNodeShifter) ForEeachInAllDimensions(n contentNode, f func(conte
|
||||
|
||||
func (s *contentNodeShifter) ForEeachInDimension(n contentNode, vec sitesmatrix.Vector, d int, f func(contentNode) bool) {
|
||||
LOOP1:
|
||||
for vec2, v := range contentNodeToSeq2(n) {
|
||||
for vec2, v := range cnh.contentNodeToSeq2(n) {
|
||||
for i, v := range vec2 {
|
||||
if i != d && v != vec[i] {
|
||||
continue LOOP1
|
||||
@@ -170,7 +170,7 @@ func (s *contentNodeShifter) Shift(n contentNode, siteVector sitesmatrix.Vector,
|
||||
}
|
||||
// The exact match is an auto page (not backed by a file).
|
||||
// Check if there's a file-backed complement that should take precedence.
|
||||
if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, contentNodeToSeq(n)); vvv != nil {
|
||||
if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, cnh.contentNodeToSeq(n)); vvv != nil {
|
||||
return vvv, true
|
||||
}
|
||||
return exact, true
|
||||
@@ -180,7 +180,7 @@ func (s *contentNodeShifter) Shift(n contentNode, siteVector sitesmatrix.Vector,
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, contentNodeToSeq(n)); vvv != nil {
|
||||
if vvv := cnh.findContentNodeForSiteVector(siteVector, fallback, cnh.contentNodeToSeq(n)); vvv != nil {
|
||||
return vvv, true
|
||||
}
|
||||
|
||||
|
||||
@@ -426,16 +426,10 @@ func (d *SourceFilesystem) RealFilename(rel string) string {
|
||||
}
|
||||
|
||||
// Contains returns whether the given filename is a member of the current filesystem.
|
||||
// It's stat-free, so it also works for deleted files.
|
||||
func (d *SourceFilesystem) Contains(filename string) bool {
|
||||
for _, dir := range d.mounts() {
|
||||
if !dir.IsDir() {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(filename, dir.Meta().Filename) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
cps, _ := d.ReverseLookup(filename, false)
|
||||
return len(cps) > 0
|
||||
}
|
||||
|
||||
// RealDirs gets a list of absolute paths to directories starting from the given
|
||||
|
||||
@@ -1080,6 +1080,8 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
|
||||
changes = append(changes, identity.GenghisKhan)
|
||||
case files.ComponentFolderArchetypes:
|
||||
// Ignore for now.
|
||||
case files.ComponentFolderStatic:
|
||||
// Handled by the static file syncer.
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown component: %q", pathInfo.Component()))
|
||||
}
|
||||
|
||||
@@ -231,11 +231,12 @@ type IntegrationTestBuilder struct {
|
||||
|
||||
Cfg IntegrationTestConfig
|
||||
|
||||
changedFiles []string
|
||||
createdFiles []string
|
||||
removedFiles []string
|
||||
renamedFiles []string
|
||||
renamedDirs []string
|
||||
changedFiles []string
|
||||
createdFiles []string
|
||||
removedFiles []string
|
||||
renamedFiles []string
|
||||
atomicSavedFiles []string
|
||||
renamedDirs []string
|
||||
|
||||
buildCount int
|
||||
GCCount int
|
||||
@@ -748,6 +749,14 @@ func (s *IntegrationTestBuilder) EditFileReplaceAll(filename, old, new string) *
|
||||
})
|
||||
}
|
||||
|
||||
// EditFileAtomicReplaceAll edits a file and simulates an editor atomic save by
|
||||
// emitting a fsnotify.Remove event for an existing path (see changeEvents).
|
||||
func (s *IntegrationTestBuilder) EditFileAtomicReplaceAll(filename, old, new string) *IntegrationTestBuilder {
|
||||
return s.EditFileAtomicReplaceFunc(filename, func(s string) string {
|
||||
return strings.ReplaceAll(s, old, new)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *IntegrationTestBuilder) EditFileReplaceFunc(filename string, replacementFunc func(s string) string) *IntegrationTestBuilder {
|
||||
absFilename := s.absFilename(filename)
|
||||
b, err := afero.ReadFile(s.fs.Source, absFilename)
|
||||
@@ -758,6 +767,16 @@ func (s *IntegrationTestBuilder) EditFileReplaceFunc(filename string, replacemen
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *IntegrationTestBuilder) EditFileAtomicReplaceFunc(filename string, replacementFunc func(s string) string) *IntegrationTestBuilder {
|
||||
absFilename := s.absFilename(filename)
|
||||
b, err := afero.ReadFile(s.fs.Source, absFilename)
|
||||
s.Assert(err, qt.IsNil)
|
||||
s.atomicSavedFiles = append(s.atomicSavedFiles, absFilename)
|
||||
oldContent := string(b)
|
||||
s.writeSource(absFilename, replacementFunc(oldContent))
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *IntegrationTestBuilder) EditFileAppend(filename, contnt string) *IntegrationTestBuilder {
|
||||
absFilename := s.absFilename(filename)
|
||||
b, err := afero.ReadFile(s.fs.Source, absFilename)
|
||||
@@ -1043,6 +1062,7 @@ func (s *IntegrationTestBuilder) reset() {
|
||||
s.createdFiles = nil
|
||||
s.removedFiles = nil
|
||||
s.renamedFiles = nil
|
||||
s.atomicSavedFiles = nil
|
||||
}
|
||||
|
||||
func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
|
||||
@@ -1091,6 +1111,15 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
|
||||
})
|
||||
}
|
||||
|
||||
for _, v := range s.atomicSavedFiles {
|
||||
events = append(events, fsnotify.Event{
|
||||
Name: v,
|
||||
// The watcher was watching the inode that got replaced by the rename,
|
||||
// so we get a Remove for a file that's still on disk.
|
||||
Op: fsnotify.Remove,
|
||||
})
|
||||
}
|
||||
|
||||
for _, v := range s.renamedDirs {
|
||||
events = append(events, fsnotify.Event{
|
||||
Name: v,
|
||||
|
||||
+1
-9
@@ -19,7 +19,6 @@ import (
|
||||
"iter"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -174,10 +173,6 @@ func (ps *pageState) Param(key any) (any, error) {
|
||||
return resource.Param(ps, ps.s.Params(), key)
|
||||
}
|
||||
|
||||
func (ps *pageState) Key() string {
|
||||
return "page-" + strconv.FormatUint(ps.pid, 10)
|
||||
}
|
||||
|
||||
// RelatedKeywords implements the related.Document interface needed for fast page searches.
|
||||
func (ps *pageState) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
|
||||
v, found, err := page.NamedPageMetaValue(ps, cfg.Name)
|
||||
@@ -225,10 +220,7 @@ func (ps *pageState) forEeachContentNode(f func(v sitesmatrix.Vector, n contentN
|
||||
}
|
||||
|
||||
func (ps *pageState) contentWeight() int {
|
||||
if ps.m.f == nil {
|
||||
return 0
|
||||
}
|
||||
return ps.m.f.FileInfo().Meta().Weight
|
||||
return ps.m.pageMetaSource.contentWeight()
|
||||
}
|
||||
|
||||
func (ps *pageState) nodeSourceEntryID() any {
|
||||
|
||||
+7
-11
@@ -21,7 +21,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gobuffalo/flect"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
@@ -98,6 +97,13 @@ func (m *pageMetaSource) String() string {
|
||||
return fmt.Sprintf("pageMetaSource(%s)", m.pathInfo)
|
||||
}
|
||||
|
||||
func (m *pageMetaSource) contentWeight() int {
|
||||
if m.f == nil {
|
||||
return 0
|
||||
}
|
||||
return m.f.FileInfo().Meta().Weight
|
||||
}
|
||||
|
||||
func (m *pageMetaSource) nodeCategoryPage() {
|
||||
// Marker method.
|
||||
}
|
||||
@@ -693,16 +699,6 @@ params:
|
||||
continue
|
||||
}
|
||||
|
||||
if loki == "path" || loki == "kind" || loki == "lang" {
|
||||
// See issue 12484.
|
||||
// Only warn when the key was set at the top level of the
|
||||
// original front matter; cascade.params can legitimately carry
|
||||
// these names as user params (issue 14848).
|
||||
if _, ok := pm.pageConfigSource.Frontmatter[loki]; ok {
|
||||
hugo.DeprecateLevelMin(loki+" in front matter", "", "v0.144.0", logg.LevelWarn)
|
||||
}
|
||||
}
|
||||
|
||||
switch loki {
|
||||
case "title":
|
||||
pcfg.Title = cast.ToString(v)
|
||||
|
||||
@@ -149,6 +149,11 @@ func (po *pageOutput) Aliases() []string {
|
||||
return aliases
|
||||
}
|
||||
|
||||
// Key returns a unique key for this page output, used to e.g. hashing.
|
||||
func (po *pageOutput) Key() string {
|
||||
return po.p.Path() + po.f.Name
|
||||
}
|
||||
|
||||
func (po *pageOutput) incrRenderState() {
|
||||
po.renderState++
|
||||
po.renderOnce = true
|
||||
|
||||
@@ -2341,3 +2341,30 @@ layouts/s1/p3/_views/a.html
|
||||
b.AssertFileContent("public/s1/p2/index.html", "p2: layouts/s1/_views/a.html") // fails
|
||||
b.AssertFileContent("public/s1/p3/index.html", "p3: layouts/s1/p3/_views/a.html") // fails
|
||||
}
|
||||
|
||||
func TestPageConttentWeight(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- content/mysection/page1.md --
|
||||
-- content/myothersection/page2.md --
|
||||
-- content/myothersection/_index.md --
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
check := func(p page.Page, ok bool) {
|
||||
cw := p.(contentNodeContentWeightProvider).contentWeight()
|
||||
b.Assert(ok, qt.Equals, cw > 0)
|
||||
}
|
||||
|
||||
s := b.H.Sites[0]
|
||||
for _, p := range s.RegularPages() {
|
||||
check(p, true)
|
||||
}
|
||||
check(s.home, false)
|
||||
mysection, _ := s.GetPage("mysection")
|
||||
check(mysection, false)
|
||||
myothersection, _ := s.GetPage("myothersection") // backed by a content file.
|
||||
check(myothersection, true)
|
||||
}
|
||||
|
||||
@@ -897,6 +897,17 @@ func TestRebuildEditSectionRemoveDate(t *testing.T) {
|
||||
b.AssertFileContent("public/mysection/index.html", "all. My Section|Param: |Lastmod: 2021-02-01|")
|
||||
}
|
||||
|
||||
// Atomic save (write temp file, then rename into place) of a section's branch bundle.
|
||||
// See issue 15130.
|
||||
func TestRebuildEditAtomicSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := TestRunning(t, rebuildBasicFiles)
|
||||
b.AssertFileContent("public/mysection/index.html", "My Section")
|
||||
b.EditFileAtomicReplaceAll("content/mysection/_index.md", "My Section", "My Changed Section").Build()
|
||||
b.AssertRenderCountPage(5)
|
||||
b.AssertFileContent("public/mysection/index.html", "My Changed Section")
|
||||
}
|
||||
|
||||
func TestRebuildVariations(t *testing.T) {
|
||||
// t.Parallel() not supported, see https://github.com/fortytw2/leaktest/issues/4
|
||||
// This leaktest seems to be a little bit shaky on Travis.
|
||||
@@ -2170,3 +2181,33 @@ Foo.
|
||||
b.EditFileReplaceAll("layouts/tags/list.html", "Foo", "Bar").Build()
|
||||
b.AssertFileContent("public/tags/index.html", "Bar.")
|
||||
}
|
||||
|
||||
// Atomic save (write temp file, then rename into place) of a content backed taxonomy root.
|
||||
// See issue 15130.
|
||||
func TestRebuildEditAtomicTaxonomyRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableLiveReload = true
|
||||
[taxonomies]
|
||||
object = "objects"
|
||||
-- content/objects/_index.md --
|
||||
---
|
||||
title: "Objects"
|
||||
---
|
||||
Objects content.
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "P1"
|
||||
objects: ["o1"]
|
||||
---
|
||||
-- layouts/all.html --
|
||||
{{ .Title }}|{{ .Kind }}|{{ .Content }}
|
||||
`
|
||||
b := TestRunning(t, files)
|
||||
b.AssertFileContent("public/objects/index.html", "Objects|taxonomy|<p>Objects content.</p>")
|
||||
b.EditFileAtomicReplaceAll("content/objects/_index.md", "Objects content.", "Objects content edited.").Build()
|
||||
b.AssertFileContent("public/objects/index.html", "Objects|taxonomy|<p>Objects content edited.</p>")
|
||||
b.AssertFileContent("public/objects/o1/index.html", "O1|term|")
|
||||
}
|
||||
|
||||
+6
-12
@@ -1257,20 +1257,14 @@ func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo
|
||||
removed := false
|
||||
added := false
|
||||
|
||||
if ev.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
removed = true
|
||||
}
|
||||
|
||||
fi, statErr := h.Fs.Source.Stat(ev.Name)
|
||||
|
||||
// Some editors (Vim) sometimes issue only a Rename operation when writing an existing file
|
||||
// Sometimes a rename operation means that file has been renamed other times it means
|
||||
// it's been updated.
|
||||
if ev.Op.Has(fsnotify.Rename) {
|
||||
// If the file is still on disk, it's only been updated, if it's not, it's been moved
|
||||
if statErr != nil {
|
||||
removed = true
|
||||
}
|
||||
// Some editors (Vim) sometimes issue only a Rename operation when writing an existing file,
|
||||
// and an atomic save (write temp file, then rename it into place) makes the watcher
|
||||
// report the replaced file as removed (kqueue/macOS).
|
||||
// So, if the file is still on disk, it's only been updated, if it's not, it's gone.
|
||||
if ev.Op.Has(fsnotify.Remove) || ev.Op.Has(fsnotify.Rename) {
|
||||
removed = statErr != nil
|
||||
}
|
||||
if ev.Op.Has(fsnotify.Create) {
|
||||
added = true
|
||||
|
||||
@@ -183,6 +183,9 @@ type ExternalOptions struct {
|
||||
// Maps a component import to another.
|
||||
Shims map[string]string
|
||||
|
||||
// User provided import context. If set, we will look here first.
|
||||
ImportContext any
|
||||
|
||||
// Configuring a loader for a given file type lets you load that file type with an
|
||||
// import statement or a require call. For example, configuring the .png file extension
|
||||
// to use the data URL loader means importing a .png file gives you a data URL
|
||||
|
||||
@@ -260,6 +260,19 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana
|
||||
}
|
||||
}
|
||||
|
||||
if opts.ImportOnResolveFunc != nil {
|
||||
// Relative imports resolved against the importing file's directory,
|
||||
// e.g. "./foo.css". The path as written was tried above.
|
||||
for _, p := range pathsToTry {
|
||||
if p == impPath {
|
||||
continue
|
||||
}
|
||||
if s := opts.ImportOnResolveFunc(filepath.ToSlash(p), args); s != "" {
|
||||
return api.OnResolveResult{Path: s, Namespace: NsHugoImportResolveFunc}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var m *hugofs.FileMeta
|
||||
for _, p := range pathsToTry {
|
||||
m = assetsResolver.resolveComponent(p, isCSSToken)
|
||||
|
||||
@@ -25,6 +25,13 @@ import (
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
)
|
||||
|
||||
// Keep in sync with genavif/Makefile.
|
||||
const (
|
||||
libavifVersion = "v1.4.1"
|
||||
aomVersion = "v3.14.1"
|
||||
dav1dVersion = "1.5.3"
|
||||
)
|
||||
|
||||
var (
|
||||
_ SourceProvider = AvifInput{}
|
||||
_ DestinationProvider = AvifInput{}
|
||||
|
||||
@@ -280,6 +280,15 @@ static avifPixelFormat avifFormatForHint(const char *hint)
|
||||
return AVIF_PIXEL_FORMAT_YUV420; // photo, picture, and the default.
|
||||
}
|
||||
|
||||
// encoder_errf writes "prefix: result[: diag]", where diag is libavif's
|
||||
// detailed diagnostics (e.g. the underlying aom error, such as an allocation
|
||||
// failure hidden behind the generic "Encoding of color planes failed").
|
||||
static void encoder_errf(char *dst, size_t size, const char *prefix, avifResult result, const avifEncoder *encoder)
|
||||
{
|
||||
snprintf(dst, size, "%s: %s%s%s", prefix, avifResultToString(result),
|
||||
encoder->diag.error[0] ? ": " : "", encoder->diag.error);
|
||||
}
|
||||
|
||||
void handle_commands(FILE *stream)
|
||||
{
|
||||
|
||||
@@ -725,7 +734,7 @@ void handle_commands(FILE *stream)
|
||||
|
||||
result = avifEncoderAddImage(encoder, image, 1, AVIF_ADD_IMAGE_FLAG_SINGLE);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
snprintf(output.header.err, sizeof(output.header.err), "encodeNRGBA: Failed to add image to encoder: %s", avifResultToString(result));
|
||||
encoder_errf(output.header.err, sizeof(output.header.err), "encodeNRGBA: Failed to add image to encoder", result, encoder);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(image);
|
||||
write_output_message(&output);
|
||||
@@ -735,7 +744,7 @@ void handle_commands(FILE *stream)
|
||||
avifRWData raw = { NULL, 0 };
|
||||
result = avifEncoderFinish(encoder, &raw);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
snprintf(output.header.err, sizeof(output.header.err), "encodeNRGBA: Failed to finish encoding: %s", avifResultToString(result));
|
||||
encoder_errf(output.header.err, sizeof(output.header.err), "encodeNRGBA: Failed to finish encoding", result, encoder);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(image);
|
||||
write_output_message(&output);
|
||||
@@ -850,7 +859,7 @@ void handle_commands(FILE *stream)
|
||||
|
||||
avifResult result = avifEncoderAddImage(encoder, image, 1, AVIF_ADD_IMAGE_FLAG_SINGLE);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
snprintf(output.header.err, sizeof(output.header.err), "encodeGray: Failed to add image to encoder: %s", avifResultToString(result));
|
||||
encoder_errf(output.header.err, sizeof(output.header.err), "encodeGray: Failed to add image to encoder", result, encoder);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(image);
|
||||
write_output_message(&output);
|
||||
@@ -860,7 +869,7 @@ void handle_commands(FILE *stream)
|
||||
avifRWData raw = { NULL, 0 };
|
||||
result = avifEncoderFinish(encoder, &raw);
|
||||
if (result != AVIF_RESULT_OK) {
|
||||
snprintf(output.header.err, sizeof(output.header.err), "encodeGray: Failed to finish encoding: %s", avifResultToString(result));
|
||||
encoder_errf(output.header.err, sizeof(output.header.err), "encodeGray: Failed to finish encoding", result, encoder);
|
||||
avifEncoderDestroy(encoder);
|
||||
avifImageDestroy(image);
|
||||
write_output_message(&output);
|
||||
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
// Keep in sync with js/package.json.
|
||||
const katexVersion = "0.16.21"
|
||||
|
||||
//go:embed wasm/renderkatex.wasm
|
||||
var katexWasm []byte
|
||||
|
||||
|
||||
+17
-3
@@ -48,6 +48,17 @@ const (
|
||||
|
||||
const currentVersion = 1
|
||||
|
||||
// GetWASMDeps returns version information for the C libraries bundled as WASM.
|
||||
func GetWASMDeps() [][2]string {
|
||||
return [][2]string{
|
||||
{"aomedia.googlesource.com/aom", aomVersion},
|
||||
{"code.videolan.org/videolan/dav1d", dav1dVersion},
|
||||
{"github.com/AOMediaCodec/libavif", libavifVersion},
|
||||
{"github.com/KaTeX/KaTeX", katexVersion},
|
||||
{"github.com/webmproject/libwebp", libwebpVersion},
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed wasm/quickjs.wasm
|
||||
var quickjsWasm []byte
|
||||
|
||||
@@ -348,7 +359,8 @@ func (d *dispatcher[Q, R]) inputBlobs() error {
|
||||
call.done()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
},
|
||||
); err != nil {
|
||||
inputErr = err
|
||||
break
|
||||
}
|
||||
@@ -384,7 +396,8 @@ func (d *dispatcher[Q, R]) inputJSON() error {
|
||||
call.done()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
},
|
||||
); err != nil {
|
||||
inputErr = err
|
||||
break
|
||||
}
|
||||
@@ -679,7 +692,8 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
|
||||
if err != nil {
|
||||
return toErr("quickjs", errBuff, err)
|
||||
}
|
||||
ctx = experimental.WithImportResolver(ctx,
|
||||
ctx = experimental.WithImportResolver(
|
||||
ctx,
|
||||
func(name string) api.Module {
|
||||
if name == opts.Runtime.Name {
|
||||
return runtimeInstance
|
||||
|
||||
Binary file not shown.
@@ -28,6 +28,9 @@ import (
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
)
|
||||
|
||||
// Keep in sync with genwebp/Makefile.
|
||||
const libwebpVersion = "v1.6.0"
|
||||
|
||||
var (
|
||||
_ SourceProvider = WebpInput{}
|
||||
_ DestinationProvider = WebpInput{}
|
||||
|
||||
@@ -15,6 +15,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -78,10 +80,22 @@ func TestMain(m *testing.M) {
|
||||
})
|
||||
}
|
||||
|
||||
var isCaseInsensitiveFs = sync.OnceValues(func() (bool, error) {
|
||||
return htesting.IsCaseInsensitiveFs(os.TempDir())
|
||||
})
|
||||
|
||||
var commonTestScriptsParam = testscript.Params{
|
||||
Setup: func(env *testscript.Env) error {
|
||||
return testSetupFunc()(env)
|
||||
},
|
||||
Condition: func(cond string) (bool, error) {
|
||||
switch cond {
|
||||
case "caseinsensitivefs":
|
||||
return isCaseInsensitiveFs()
|
||||
default:
|
||||
return false, fmt.Errorf("unknown condition %q", cond)
|
||||
}
|
||||
},
|
||||
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
|
||||
// log prints to stderr.
|
||||
"log": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
@@ -252,6 +266,34 @@ var commonTestScriptsParam = testscript.Params{
|
||||
ts.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
},
|
||||
// base64decode decodes a base64-encoded file into a binary file.
|
||||
"base64decode": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
if len(args) != 2 {
|
||||
ts.Fatalf("usage: base64decode src_base64_file dest_binary_file")
|
||||
}
|
||||
|
||||
// Resolve paths relative to the test sandbox's current directory
|
||||
src := ts.MkAbs(args[0])
|
||||
dest := ts.MkAbs(args[1])
|
||||
|
||||
// Read the base64 text
|
||||
base64Data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
ts.Fatalf("failed to read base64 file: %v", err)
|
||||
}
|
||||
|
||||
// Decode base64 back to raw bytes.
|
||||
binaryBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(base64Data)))
|
||||
if err != nil {
|
||||
ts.Fatalf("failed to decode base64 data: %v", err)
|
||||
}
|
||||
|
||||
// Write the binary file.
|
||||
err = os.WriteFile(dest, binaryBytes, 0o644)
|
||||
if err != nil {
|
||||
ts.Fatalf("failed to write binary file: %v", err)
|
||||
}
|
||||
},
|
||||
|
||||
// httpget checks that a HTTP resource's body matches (if it compiles as a regexp) or contains all of the strings given as arguments.
|
||||
"httpget": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
|
||||
@@ -1153,3 +1153,59 @@ func validatePublishedSite_20(b *hugolib.IntegrationTestBuilder) {
|
||||
b.AssertFileExists(path, true)
|
||||
}
|
||||
}
|
||||
|
||||
// See issue 15121
|
||||
func TestAsciiDocHTML5BackendTOC(t *testing.T) {
|
||||
if !htesting.IsRealCI() {
|
||||
t.Skip()
|
||||
}
|
||||
if ok, err := asciidocext.Supports(); !ok {
|
||||
t.Skip(err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
resetDefaultAsciiDocExtConfig()
|
||||
})
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
|
||||
|
||||
[markup.asciidocExt]
|
||||
backend = 'html5s'
|
||||
extensions = ['asciidoctor-html5s']
|
||||
|
||||
[security.exec]
|
||||
allow = ['^asciidoctor$']
|
||||
-- content/p1.adoc --
|
||||
---
|
||||
title: p1
|
||||
---
|
||||
|
||||
:toc:
|
||||
|
||||
## Section 1
|
||||
|
||||
### Section 1.1
|
||||
|
||||
### Section 1.2
|
||||
|
||||
## Section 2
|
||||
-- layouts/page.html --
|
||||
{{ .TableOfContents }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContentExact("public/p1/index.html", `<nav id="TableOfContents">
|
||||
<ul>
|
||||
<li><a href="#_section_1">Section 1</a>
|
||||
<ul>
|
||||
<li><a href="#_section_1_1">Section 1.1</a></li>
|
||||
<li><a href="#_section_1_2">Section 1.2</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#_section_2">Section 2</a></li>
|
||||
</ul>
|
||||
</nav>`)
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ func (a *AsciiDocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fra
|
||||
toVisit []*html.Node
|
||||
)
|
||||
f = func(n *html.Node) bool {
|
||||
if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" {
|
||||
if n.Type == html.ElementNode && (n.Data == "div" || n.Data == "nav") && attr(n, "id") == "toc" {
|
||||
toc = parseTOC(n)
|
||||
if !a.Cfg.MarkupConfig().AsciiDocExt.PreserveTOC {
|
||||
n.Parent.RemoveChild(n)
|
||||
@@ -364,7 +364,7 @@ func parseTOC(doc *html.Node) *tableofcontents.Fragments {
|
||||
f = func(n *html.Node, row, level int) {
|
||||
if n.Type == html.ElementNode {
|
||||
switch n.Data {
|
||||
case "ul":
|
||||
case "ul", "ol":
|
||||
if level == 0 {
|
||||
row++
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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 highlight
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
)
|
||||
|
||||
// ChromaStylesOptions holds options for ChromaStylesCSS.
|
||||
type ChromaStylesOptions struct {
|
||||
// Highlighter style, e.g. "monokai".
|
||||
Style string
|
||||
|
||||
// Style mode, "light" or "dark".
|
||||
Mode string
|
||||
|
||||
// Scope selectors under a top level mode class, e.g. ".dark .chroma".
|
||||
ModeSelector bool
|
||||
|
||||
// Class name used by ModeSelector for dark styles, defaults to "dark".
|
||||
ClassDark string
|
||||
|
||||
// Class name used by ModeSelector for light styles, defaults to "light".
|
||||
ClassLight string
|
||||
|
||||
// Foreground and background colors for highlighted lines, e.g. "#fff000 bg:#000fff".
|
||||
HighlightStyle string
|
||||
|
||||
// Foreground and background colors for inline line numbers.
|
||||
LineNumbersInlineStyle string
|
||||
|
||||
// Foreground and background colors for table line numbers.
|
||||
LineNumbersTableStyle string
|
||||
|
||||
// Omit CSS class comment prefixes in the generated CSS.
|
||||
OmitClassComments bool
|
||||
}
|
||||
|
||||
// ChromaStylesCSS generates a CSS stylesheet for the Chroma code highlighter.
|
||||
// This stylesheet is needed if markup.highlight.noClasses is disabled in config.
|
||||
func ChromaStylesCSS(opts ChromaStylesOptions) (string, error) {
|
||||
name := strings.ToLower(opts.Style)
|
||||
if !slices.Contains(styles.Names(), name) {
|
||||
return "", fmt.Errorf("invalid style: %s", name)
|
||||
}
|
||||
var chromaStyle *chroma.Style
|
||||
if opts.Mode != "" {
|
||||
var mode chroma.Mode
|
||||
switch opts.Mode {
|
||||
case "light":
|
||||
mode = chroma.Light
|
||||
case "dark":
|
||||
mode = chroma.Dark
|
||||
default:
|
||||
return "", fmt.Errorf("invalid mode: %s", opts.Mode)
|
||||
}
|
||||
|
||||
chromaStyle = styles.GetForMode(name, mode)
|
||||
if chromaStyle.Mode() != mode {
|
||||
return "", fmt.Errorf("style %q does not have a %q mode", name, opts.Mode)
|
||||
}
|
||||
} else {
|
||||
chromaStyle = styles.Get(name)
|
||||
}
|
||||
builder := chromaStyle.Builder()
|
||||
if opts.HighlightStyle != "" {
|
||||
builder.Add(chroma.LineHighlight, opts.HighlightStyle)
|
||||
}
|
||||
if opts.LineNumbersInlineStyle != "" {
|
||||
builder.Add(chroma.LineNumbers, opts.LineNumbersInlineStyle)
|
||||
}
|
||||
if opts.LineNumbersTableStyle != "" {
|
||||
builder.Add(chroma.LineNumbersTable, opts.LineNumbersTableStyle)
|
||||
}
|
||||
style, err := builder.Build()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
options := []html.Option{
|
||||
html.WithCSSComments(!opts.OmitClassComments),
|
||||
html.WithCustomCSS(chromaCSSOverrides(style)),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := html.New(options...).WriteCSS(&buf, style); err != nil {
|
||||
return "", err
|
||||
}
|
||||
css := buf.String()
|
||||
if opts.ModeSelector {
|
||||
// Scope every selector under a top level mode class, e.g. ".dark .chroma".
|
||||
// This allows generating both light and dark stylesheets and toggling
|
||||
// them with a parent class on the page.
|
||||
// There's no upstream option for this (I think), so do string replacements for now.
|
||||
// TODO(bep) upstream option for this.
|
||||
classDark, classLight := opts.ClassDark, opts.ClassLight
|
||||
if classDark == "" {
|
||||
classDark = "dark"
|
||||
}
|
||||
if classLight == "" {
|
||||
classLight = "light"
|
||||
}
|
||||
var prefix string
|
||||
switch style.Mode() {
|
||||
case chroma.Light:
|
||||
prefix = "." + classLight + " "
|
||||
case chroma.Dark:
|
||||
prefix = "." + classDark + " "
|
||||
default:
|
||||
return "", fmt.Errorf("style %q does not have a light or dark mode", style.Name)
|
||||
}
|
||||
replacer := strings.NewReplacer(
|
||||
".bg {", prefix+".bg {",
|
||||
".chroma ", prefix+".chroma ",
|
||||
)
|
||||
css = replacer.Replace(css)
|
||||
}
|
||||
|
||||
return css, nil
|
||||
}
|
||||
|
||||
// chromaCSSOverrides re-emits leaf token colors that Chroma's minifier drops
|
||||
// because they equal the style's default foreground (the .chroma color).
|
||||
// In a paired light/dark setup, the other sheet's explicit rule (e.g. .chroma .nx)
|
||||
// would otherwise leak into this mode, since an explicit declaration beats
|
||||
// inheritance from .chroma.
|
||||
// TODO(bep): replace with an upstream option, see issue 15161.
|
||||
func chromaCSSOverrides(style *chroma.Style) map[chroma.TokenType]string {
|
||||
bg := style.Get(chroma.Background)
|
||||
m := make(map[chroma.TokenType]string)
|
||||
for tt := range chroma.StandardTypes {
|
||||
if tt == chroma.Background || !style.Has(tt) || !chromaLeafToken(tt) {
|
||||
continue
|
||||
}
|
||||
entry := style.Get(tt)
|
||||
if !entry.Sub(bg).IsZero() || !entry.Colour.IsSet() {
|
||||
continue
|
||||
}
|
||||
if css := html.StyleEntryToCSS(chroma.StyleEntry{Colour: entry.Colour}); css != "" {
|
||||
m[tt] = css
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func chromaLeafToken(tt chroma.TokenType) bool {
|
||||
for other := range chroma.StandardTypes {
|
||||
if other != tt && (other.Category() == tt || other.SubCategory() == tt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+11
-2
@@ -21,6 +21,7 @@ type BuiltinTypes struct {
|
||||
TextType Type
|
||||
TOMLType Type
|
||||
YAMLType Type
|
||||
SourceMapType Type
|
||||
|
||||
// Common image types
|
||||
PNGType Type
|
||||
@@ -36,6 +37,8 @@ type BuiltinTypes struct {
|
||||
// Common font types
|
||||
TrueTypeFontType Type
|
||||
OpenTypeFontType Type
|
||||
WOFFFontType Type
|
||||
WOFF2FontType Type
|
||||
|
||||
// Common document types
|
||||
PDFType Type
|
||||
@@ -80,6 +83,7 @@ var Builtin = BuiltinTypes{
|
||||
TextType: Type{Type: "text/plain"},
|
||||
TOMLType: Type{Type: "application/toml"},
|
||||
YAMLType: Type{Type: "application/yaml"},
|
||||
SourceMapType: Type{Type: "application/source-map"},
|
||||
|
||||
// Common image types
|
||||
PNGType: Type{Type: "image/png"},
|
||||
@@ -95,6 +99,8 @@ var Builtin = BuiltinTypes{
|
||||
// Common font types
|
||||
TrueTypeFontType: Type{Type: "font/ttf"},
|
||||
OpenTypeFontType: Type{Type: "font/otf"},
|
||||
WOFFFontType: Type{Type: "font/woff"},
|
||||
WOFF2FontType: Type{Type: "font/woff2"},
|
||||
|
||||
// Common document types
|
||||
PDFType: Type{Type: "application/pdf"},
|
||||
@@ -139,6 +145,7 @@ var defaultMediaTypesConfig = map[string]any{
|
||||
"text/plain": map[string]any{"suffixes": []string{"txt"}},
|
||||
"application/toml": map[string]any{"suffixes": []string{"toml"}},
|
||||
"application/yaml": map[string]any{"suffixes": []string{"yaml", "yml"}},
|
||||
"application/source-map": map[string]any{"suffixes": []string{"map"}},
|
||||
|
||||
// Common image types
|
||||
"image/png": map[string]any{"suffixes": []string{"png"}},
|
||||
@@ -152,8 +159,10 @@ var defaultMediaTypesConfig = map[string]any{
|
||||
"image/heic": map[string]any{"suffixes": []string{"heic"}},
|
||||
|
||||
// Common font types
|
||||
"font/ttf": map[string]any{"suffixes": []string{"ttf"}},
|
||||
"font/otf": map[string]any{"suffixes": []string{"otf"}},
|
||||
"font/ttf": map[string]any{"suffixes": []string{"ttf"}},
|
||||
"font/otf": map[string]any{"suffixes": []string{"otf"}},
|
||||
"font/woff": map[string]any{"suffixes": []string{"woff"}},
|
||||
"font/woff2": map[string]any{"suffixes": []string{"woff2"}},
|
||||
|
||||
// Common document types
|
||||
"application/pdf": map[string]any{"suffixes": []string{"pdf"}},
|
||||
|
||||
@@ -142,6 +142,9 @@ func TestDefaultTypes(t *testing.T) {
|
||||
{Builtin.PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"},
|
||||
{Builtin.TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"},
|
||||
{Builtin.OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"},
|
||||
{Builtin.WOFFFontType, "font", "woff", "woff", "font/woff", "font/woff"},
|
||||
{Builtin.WOFF2FontType, "font", "woff2", "woff2", "font/woff2", "font/woff2"},
|
||||
{Builtin.SourceMapType, "application", "source-map", "map", "application/source-map", "application/source-map"},
|
||||
} {
|
||||
c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType)
|
||||
c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType)
|
||||
@@ -151,5 +154,5 @@ func TestDefaultTypes(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
c.Assert(len(DefaultTypes), qt.Equals, 44)
|
||||
c.Assert(len(DefaultTypes), qt.Equals, 47)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 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 resources
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
// Artifact represents an additional output file published as part of a
|
||||
// resource transformation, e.g. a source map or a file emitted by ESBuild's
|
||||
// file loader. It is exposed to templates via Resource.Data.Artifacts.
|
||||
type Artifact interface {
|
||||
resource.MediaTypeProvider
|
||||
resource.ResourceLinksProvider
|
||||
}
|
||||
|
||||
// NewArtifact creates a new Artifact with the given permalinks and media type.
|
||||
func NewArtifact(permalink, relPermalink string, mediaType media.Type) Artifact {
|
||||
return &artifact{permalink: permalink, relPermalink: relPermalink, mediaType: mediaType}
|
||||
}
|
||||
|
||||
type artifact struct {
|
||||
permalink string
|
||||
relPermalink string
|
||||
mediaType media.Type
|
||||
}
|
||||
|
||||
func (a *artifact) MediaType() media.Type {
|
||||
return a.mediaType
|
||||
}
|
||||
|
||||
func (a *artifact) Permalink() string {
|
||||
return a.permalink
|
||||
}
|
||||
|
||||
func (a *artifact) RelPermalink() string {
|
||||
return a.relPermalink
|
||||
}
|
||||
@@ -16,13 +16,12 @@ package images
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"image/color"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type colorGoProvider interface {
|
||||
@@ -65,10 +64,8 @@ func (c Color) String() string {
|
||||
// For hashstructure. This struct is used in template func options
|
||||
// that needs to be able to hash a Color.
|
||||
// For internal use only.
|
||||
func (c Color) Hash() (uint64, error) {
|
||||
h := fnv.New64a()
|
||||
h.Write([]byte(c.hex))
|
||||
return h.Sum64(), nil
|
||||
func (c Color) Key() string {
|
||||
return c.hex
|
||||
}
|
||||
|
||||
func (c *Color) init() error {
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
package internal
|
||||
|
||||
import "github.com/gohugoio/hugo/common/hashing"
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
)
|
||||
|
||||
// ResourceTransformationKey are provided by the different transformation implementations.
|
||||
// It identifies the transformation (name) and its configuration (elements).
|
||||
|
||||
@@ -39,6 +39,11 @@ func (b *Builder) Build(dir string) *Config {
|
||||
}
|
||||
var roots []string
|
||||
for root := range b.sourceRoots.All() {
|
||||
if root == dir {
|
||||
// We need mappings for other source roots (e.g. mounted modules), but not for the current one.
|
||||
// See issue 15169.
|
||||
continue
|
||||
}
|
||||
rel, err := filepath.Rel(dir, filepath.Join(root, "*"))
|
||||
if err == nil {
|
||||
roots = append(roots, rel)
|
||||
|
||||
@@ -37,4 +37,10 @@ func TestJsConfigBuilder(t *testing.T) {
|
||||
c.Assert(string(data), qt.Not(qt.Contains), "baseUrl")
|
||||
|
||||
c.Assert(NewBuilder().Build("/a/b"), qt.IsNil)
|
||||
|
||||
// Issue 15169.
|
||||
b = NewBuilder()
|
||||
b.AddSourceRoot("/c/assets")
|
||||
conf = b.Build("/c/assets")
|
||||
c.Assert(conf, qt.IsNil)
|
||||
}
|
||||
|
||||
@@ -100,12 +100,6 @@ func (i HugoInfo) Store() *hstore.Scratch {
|
||||
return i.store
|
||||
}
|
||||
|
||||
// Deprecated: Use hugo.IsMultihost instead.
|
||||
func (i HugoInfo) IsMultiHost() bool {
|
||||
hugo.Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
|
||||
return i.opts.Conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultihost reports whether each configured language has a unique baseURL.
|
||||
func (i HugoInfo) IsMultihost() bool {
|
||||
return i.opts.Conf.IsMultihost()
|
||||
|
||||
@@ -96,6 +96,9 @@ type ResourceSourceDescriptor struct {
|
||||
// Delay publishing until either Permalink or RelPermalink is called. Maybe never.
|
||||
LazyPublish bool
|
||||
|
||||
// Whether to include the hash of the source content in the resource key.
|
||||
IncludeHashInKey bool
|
||||
|
||||
// Set when its known up front, else it's resolved from the target filename.
|
||||
MediaType media.Type
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hashstructure"
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hreflect"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
@@ -33,44 +35,56 @@ var _ ResourceFinder = (*Resources)(nil)
|
||||
// I.e. both pages and images etc.
|
||||
type Resources []Resource
|
||||
|
||||
type resourceMount struct {
|
||||
R Resources
|
||||
Base string
|
||||
Target string
|
||||
}
|
||||
|
||||
func (r resourceMount) Get(namev any) Resource {
|
||||
name1, err := cast.ToStringE(namev)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
isTargetAbs := strings.HasPrefix(r.Target, "/")
|
||||
|
||||
if r.Target != "" {
|
||||
name1 = strings.TrimPrefix(name1, r.Target)
|
||||
if !isTargetAbs {
|
||||
name1 = paths.TrimLeading(name1)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Base != "" && isTargetAbs {
|
||||
name1 = path.Join(r.Base, name1)
|
||||
}
|
||||
|
||||
for _, res := range r.R {
|
||||
name2 := res.Name()
|
||||
|
||||
if r.Base != "" && !isTargetAbs {
|
||||
name2 = paths.TrimLeading(strings.TrimPrefix(name2, r.Base))
|
||||
}
|
||||
|
||||
if strings.EqualFold(name1, name2) {
|
||||
return res
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mount mounts the given resources from base to the given target path.
|
||||
// Note that leading slashes in target marks an absolute path.
|
||||
// This method is currently only useful in js.Batch.
|
||||
// This method can be used in any of the template funcs that takes an importContext option, e.g. css.Build.
|
||||
func (r Resources) Mount(base, target string) ResourceGetter {
|
||||
return resourceGetterFunc(func(namev any) Resource {
|
||||
name1, err := cast.ToStringE(namev)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
isTargetAbs := strings.HasPrefix(target, "/")
|
||||
|
||||
if target != "" {
|
||||
name1 = strings.TrimPrefix(name1, target)
|
||||
if !isTargetAbs {
|
||||
name1 = paths.TrimLeading(name1)
|
||||
}
|
||||
}
|
||||
|
||||
if base != "" && isTargetAbs {
|
||||
name1 = path.Join(base, name1)
|
||||
}
|
||||
|
||||
for _, res := range r {
|
||||
name2 := res.Name()
|
||||
|
||||
if base != "" && !isTargetAbs {
|
||||
name2 = paths.TrimLeading(strings.TrimPrefix(name2, base))
|
||||
}
|
||||
|
||||
if strings.EqualFold(name1, name2) {
|
||||
return res
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return resourceMount{
|
||||
R: r,
|
||||
Base: base,
|
||||
Target: target,
|
||||
}
|
||||
}
|
||||
|
||||
type ResourcesProvider interface {
|
||||
@@ -276,12 +290,6 @@ type StaleInfoResourceGetter interface {
|
||||
ResourceGetter
|
||||
}
|
||||
|
||||
type resourceGetterFunc func(name any) Resource
|
||||
|
||||
func (f resourceGetterFunc) Get(name any) Resource {
|
||||
return f(name)
|
||||
}
|
||||
|
||||
// ResourceFinder provides methods to find Resources.
|
||||
// Note that GetRemote (as found in resources.GetRemote) is
|
||||
// not covered by this interface, as this is only available as a global template function.
|
||||
@@ -318,6 +326,8 @@ type ResourceFinder interface {
|
||||
ByType(typ any) Resources
|
||||
}
|
||||
|
||||
var _ hashstructure.Hashable = (*cachedResourceGetter)(nil)
|
||||
|
||||
// NewCachedResourceGetter creates a new ResourceGetter from the given objects.
|
||||
// If multiple objects are provided, they are merged into one where
|
||||
// the first match wins.
|
||||
@@ -329,12 +339,19 @@ func NewCachedResourceGetter(os ...any) *cachedResourceGetter {
|
||||
}
|
||||
}
|
||||
|
||||
hash := hashing.HashUint64(getters)
|
||||
|
||||
return &cachedResourceGetter{
|
||||
cache: hmaps.NewCache[string, Resource](),
|
||||
delegate: getters,
|
||||
hash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cachedResourceGetter) Hash() (uint64, error) {
|
||||
return c.hash, nil
|
||||
}
|
||||
|
||||
type multiResourceGetter []ResourceGetter
|
||||
|
||||
func (m multiResourceGetter) Get(name any) Resource {
|
||||
@@ -354,6 +371,7 @@ var (
|
||||
type cachedResourceGetter struct {
|
||||
cache *hmaps.Cache[string, Resource]
|
||||
delegate ResourceGetter
|
||||
hash uint64
|
||||
}
|
||||
|
||||
func (c *cachedResourceGetter) Get(name any) Resource {
|
||||
@@ -390,8 +408,6 @@ func unwrapResourceGetter(v any) (ResourceGetter, bool) {
|
||||
return vv, true
|
||||
case ResourcesProvider:
|
||||
return vv.Resources(), true
|
||||
case func(name any) Resource:
|
||||
return resourceGetterFunc(vv), true
|
||||
default:
|
||||
vvv, ok := hreflect.ToSliceAny(v)
|
||||
if !ok {
|
||||
|
||||
@@ -304,8 +304,9 @@ func (c *Client) FromOpts(opts Options) (resource.Resource, error) {
|
||||
}
|
||||
return c.rs.NewResource(
|
||||
resources.ResourceSourceDescriptor{
|
||||
LazyPublish: true,
|
||||
GroupIdentity: identity.Anonymous, // All usage of this resource are tracked via its string content.
|
||||
LazyPublish: true,
|
||||
IncludeHashInKey: !opts.TargetPathHasHash,
|
||||
GroupIdentity: identity.Anonymous, // All usage of this resource are tracked via its string content.
|
||||
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
|
||||
return newReadSeeker()
|
||||
},
|
||||
|
||||
@@ -201,7 +201,7 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro
|
||||
h: &resourceHash{},
|
||||
publishInit: &hsync.OnceMore{},
|
||||
keyInit: &sync.Once{},
|
||||
includeHashInKey: isImage,
|
||||
includeHashInKey: isImage || rd.IncludeHashInKey,
|
||||
paths: rp,
|
||||
spec: r,
|
||||
sd: rd,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package cssjs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -30,6 +31,8 @@ import (
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@@ -46,10 +49,12 @@ type fileOffset struct {
|
||||
}
|
||||
|
||||
type importResolver struct {
|
||||
ctx context.Context
|
||||
r io.Reader
|
||||
inPath string
|
||||
opts InlineImports
|
||||
|
||||
importContext resource.ResourceGetter
|
||||
contentSeen map[string]bool
|
||||
dependencyManager identity.Manager
|
||||
linemap map[int]fileOffset
|
||||
@@ -57,8 +62,9 @@ type importResolver struct {
|
||||
logger loggers.Logger
|
||||
}
|
||||
|
||||
func newImportResolver(r io.Reader, inPath string, opts InlineImports, fs afero.Fs, logger loggers.Logger, dependencyManager identity.Manager) *importResolver {
|
||||
return &importResolver{
|
||||
func newImportResolver(ctx context.Context, r io.Reader, inPath string, opts InlineImports, fs afero.Fs, logger loggers.Logger, dependencyManager identity.Manager) *importResolver {
|
||||
imp := &importResolver{
|
||||
ctx: ctx,
|
||||
r: r,
|
||||
dependencyManager: dependencyManager,
|
||||
inPath: inPath,
|
||||
@@ -66,6 +72,10 @@ func newImportResolver(r io.Reader, inPath string, opts InlineImports, fs afero.
|
||||
linemap: make(map[int]fileOffset), contentSeen: make(map[string]bool),
|
||||
opts: opts,
|
||||
}
|
||||
if opts.ImportContext != nil {
|
||||
imp.importContext = resource.NewCachedResourceGetter(opts.ImportContext)
|
||||
}
|
||||
return imp
|
||||
}
|
||||
|
||||
func (imp *importResolver) contentHash(filename string) ([]byte, string) {
|
||||
@@ -73,9 +83,13 @@ func (imp *importResolver) contentHash(filename string) ([]byte, string) {
|
||||
if err != nil {
|
||||
return nil, ""
|
||||
}
|
||||
return b, hashBytes(b)
|
||||
}
|
||||
|
||||
func hashBytes(b []byte) string {
|
||||
h := sha256.New()
|
||||
h.Write(b)
|
||||
return b, hex.EncodeToString(h.Sum(nil))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (imp *importResolver) importRecursive(
|
||||
@@ -105,8 +119,35 @@ func (imp *importResolver) importRecursive(
|
||||
} else {
|
||||
path := strings.Trim(strings.TrimPrefix(line, importIdentifier), " \"';")
|
||||
filename := filepath.Join(basePath, path)
|
||||
imp.dependencyManager.AddIdentity(identity.CleanStringIdentity(filename))
|
||||
importContent, hash := imp.contentHash(filename)
|
||||
var (
|
||||
importContent []byte
|
||||
hash string
|
||||
nestedInPath string
|
||||
)
|
||||
if imp.importContext != nil {
|
||||
// Try first the path as written in the import statement,
|
||||
// then resolved relative to the importing file.
|
||||
for _, name := range []string{path, filepath.ToSlash(filename)} {
|
||||
r := imp.importContext.Get(name)
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
imp.dependencyManager.AddIdentity(identity.FirstIdentity(r))
|
||||
s, err := resources.InternalResourceSourceContent(imp.ctx, r)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
importContent, hash = []byte(s), hashBytes([]byte(s))
|
||||
nestedInPath = name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if importContent == nil {
|
||||
imp.dependencyManager.AddIdentity(identity.CleanStringIdentity(filename))
|
||||
importContent, hash = imp.contentHash(filename)
|
||||
nestedInPath = filepath.ToSlash(filename)
|
||||
}
|
||||
|
||||
if importContent == nil {
|
||||
if imp.opts.SkipInlineImportsNotFound {
|
||||
@@ -135,7 +176,7 @@ func (imp *importResolver) importRecursive(
|
||||
imp.contentSeen[hash] = true
|
||||
|
||||
// Handle recursive imports.
|
||||
l, nested, err := imp.importRecursive(i+lineNum, string(importContent), filepath.ToSlash(filename))
|
||||
l, nested, err := imp.importRecursive(i+lineNum, string(importContent), nestedInPath)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package cssjs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -21,10 +22,12 @@ import (
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
@@ -103,6 +106,7 @@ LOCAL_STYLE
|
||||
@import "e.css";`)
|
||||
|
||||
imp := newImportResolver(
|
||||
t.Context(),
|
||||
mainStyles,
|
||||
"styles.css",
|
||||
InlineImports{},
|
||||
@@ -130,6 +134,70 @@ E_STYLE`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestImportResolverImportContext(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
writeFile := func(name, content string) {
|
||||
c.Assert(afero.WriteFile(fs, name, []byte(content), 0o777), qt.IsNil)
|
||||
}
|
||||
|
||||
// Loses to the import context entry with the same name.
|
||||
writeFile("css/a.css", "A_STYLE_FS")
|
||||
writeFile("css/b.css", "B_STYLE_FS")
|
||||
|
||||
importContext := testImportContext{
|
||||
"a.css": "@import \"c.css\";\nA_STYLE",
|
||||
"c.css": "C_STYLE",
|
||||
"css/d.css": "D_STYLE",
|
||||
}
|
||||
|
||||
mainStyles := strings.NewReader(`@import "a.css";
|
||||
@import "b.css";
|
||||
@import "./d.css";
|
||||
LOCAL_STYLE`)
|
||||
|
||||
imp := newImportResolver(
|
||||
t.Context(),
|
||||
mainStyles,
|
||||
"css/styles.css",
|
||||
InlineImports{ImportContext: importContext},
|
||||
fs, loggers.NewDefault(),
|
||||
identity.NopManager,
|
||||
)
|
||||
|
||||
r, err := imp.resolve()
|
||||
c.Assert(err, qt.IsNil)
|
||||
rs := helpers.ReaderToString(r)
|
||||
result := regexp.MustCompile(`\n+`).ReplaceAllString(rs, "\n")
|
||||
|
||||
c.Assert(result, hqt.IsSameString, `C_STYLE
|
||||
A_STYLE
|
||||
B_STYLE_FS
|
||||
D_STYLE
|
||||
LOCAL_STYLE`)
|
||||
}
|
||||
|
||||
type testImportContext map[string]string
|
||||
|
||||
func (g testImportContext) Get(name any) resource.Resource {
|
||||
s := cast.ToString(name)
|
||||
if content, found := g[s]; found {
|
||||
return testImportContextResource{name: s, content: content}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testImportContextResource struct {
|
||||
resource.Resource
|
||||
name string
|
||||
content string
|
||||
}
|
||||
|
||||
func (r testImportContextResource) Name() string { return r.name }
|
||||
|
||||
func (r testImportContextResource) Content(context.Context) (any, error) { return r.content, nil }
|
||||
|
||||
func BenchmarkImportResolver(b *testing.B) {
|
||||
c := qt.New(b)
|
||||
fs := afero.NewMemMapFs()
|
||||
@@ -161,6 +229,7 @@ LOCAL_STYLE
|
||||
for b.Loop() {
|
||||
b.StopTimer()
|
||||
imp := newImportResolver(
|
||||
b.Context(),
|
||||
strings.NewReader(mainStyles),
|
||||
"styles.css",
|
||||
InlineImports{},
|
||||
|
||||
@@ -87,6 +87,10 @@ type InlineImports struct {
|
||||
// Note that the inline importer does not process url location or imports with media queries,
|
||||
// so those will be left as-is even without enabling this option.
|
||||
SkipInlineImportsNotFound bool
|
||||
|
||||
// User provided import context. If set, imports are looked up here first,
|
||||
// by the path as written in the @import statement, then in the assets filesystem.
|
||||
ImportContext any
|
||||
}
|
||||
|
||||
// Some of the options from https://github.com/postcss/postcss-cli
|
||||
@@ -213,6 +217,7 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
|
||||
src := ctx.From
|
||||
|
||||
imp := newImportResolver(
|
||||
ctx.Ctx,
|
||||
ctx.From,
|
||||
ctx.InPath,
|
||||
options.InlineImports,
|
||||
|
||||
@@ -124,6 +124,7 @@ func (t *tailwindcssTransformation) Transform(ctx *resources.ResourceTransformat
|
||||
src := ctx.From
|
||||
|
||||
imp := newImportResolver(
|
||||
ctx.Ctx,
|
||||
ctx.From,
|
||||
ctx.InPath,
|
||||
options.InlineImports,
|
||||
|
||||
@@ -28,6 +28,7 @@ func TestTailwindV4Basic(t *testing.T) {
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
security.exec.allow = ['^go$', '^git$', '^node$', '^tailwindcss$']
|
||||
-- package.json --
|
||||
{
|
||||
"license": "MIT",
|
||||
@@ -64,6 +65,46 @@ CSS: {{ $css.Content | safeCSS }}|
|
||||
b.AssertFileContent("public/index.html", "/*! tailwindcss v4.")
|
||||
}
|
||||
|
||||
// See issue 15103.
|
||||
func TestTailwindCSSImportContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
htesting.SkipSlowTestUnlessCI(t)
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
security.exec.allow = ['^go$', '^git$', '^node$', '^tailwindcss$']
|
||||
-- assets/css/main.css --
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "foo.css";
|
||||
@import "bar.css";
|
||||
-- assets/css/foo.css --
|
||||
.foo {color: orange;}
|
||||
-- layouts/home.html --
|
||||
{{ $foo := resources.FromString "foo.css" ".foo {color: blue;}" }}
|
||||
{{ $bar := resources.FromString "bar.css" ".bar {color: green;}" }}
|
||||
{{ $opts := dict "importContext" (slice $foo $bar) }}
|
||||
{{ $css := resources.Get "css/main.css" | css.TailwindCSS $opts }}
|
||||
CSS: {{ $css.Content | safeCSS }}|
|
||||
-- package.json --
|
||||
{
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.7",
|
||||
"tailwindcss": "^4.1.7"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstall(), hugolib.TestOptInfo())
|
||||
|
||||
// foo.css resolves in the import context before the assets filesystem.
|
||||
b.AssertFileContent("public/index.html",
|
||||
".foo {\n color: blue;\n }",
|
||||
".bar {\n color: green;\n }",
|
||||
)
|
||||
}
|
||||
|
||||
func TestTailwindCSSNoInlineImportsIssue13719(t *testing.T) {
|
||||
t.Parallel()
|
||||
htesting.SkipSlowTestUnlessCI(t)
|
||||
@@ -73,6 +114,8 @@ func TestTailwindCSSNoInlineImportsIssue13719(t *testing.T) {
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
theme = 'my-theme'
|
||||
|
||||
security.exec.allow = ['^go$', '^git$', '^node$', '^tailwindcss$']
|
||||
|
||||
[[module.mounts]]
|
||||
source = 'assets'
|
||||
target = 'assets'
|
||||
|
||||
@@ -18,12 +18,15 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/evanw/esbuild/pkg/api"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
"github.com/gohugoio/hugo/internal/js/esbuild"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
@@ -42,6 +45,8 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec, cssMode bool) *Cl
|
||||
}
|
||||
|
||||
// Process processes a resource with the user provided options.
|
||||
// If importContext is not nil, imports are resolved in it first,
|
||||
// then in the assets filesystem (the same precedence as in js.Batch).
|
||||
func (c *Client) Process(res resources.ResourceTransformer, opts map[string]any) (resource.Resource, error) {
|
||||
return res.Transform(
|
||||
&buildTransformation{c: c, optsm: opts},
|
||||
@@ -85,9 +90,27 @@ func (c *Client) transform(opts esbuild.Options, transformCtx *resources.Resourc
|
||||
hasLinkedSourceMap := opts.ExternalOptions.SourceMap == "linked"
|
||||
hasSourceMap := hasLinkedSourceMap || opts.ExternalOptions.SourceMap == "external"
|
||||
|
||||
spec := c.c.Spec()
|
||||
mediaTypes := spec.MediaTypes()
|
||||
baseURL := spec.Cfg.BaseURL().WithoutPath
|
||||
newArtifact := func(relPermalink, fileName string) resources.Artifact {
|
||||
mt, _, found := mediaTypes.GetFirstBySuffix(strings.TrimPrefix(path.Ext(fileName), "."))
|
||||
if !found {
|
||||
mt = media.Builtin.OctetType
|
||||
}
|
||||
return resources.NewArtifact(baseURL+relPermalink, relPermalink, mt)
|
||||
}
|
||||
fileLoaderRelPermalink := func(base string) string {
|
||||
if opts.PublicPath != "" {
|
||||
return strings.TrimSuffix(opts.PublicPath, "/") + "/" + paths.PathEscape(base)
|
||||
}
|
||||
return pathSpec.GetBasePath(false) + paths.PathEscape(paths.AddLeadingSlash(path.Join(outDir, base)))
|
||||
}
|
||||
|
||||
// Classify output files by path rather than relying on array ordering,
|
||||
// which esbuild does not guarantee.
|
||||
var mainOutput []byte
|
||||
var artifacts []resources.Artifact
|
||||
for _, file := range result.OutputFiles {
|
||||
basePath := path.Base(filepath.ToSlash(file.Path))
|
||||
if strings.HasSuffix(basePath, ".map") {
|
||||
@@ -95,6 +118,7 @@ func (c *Client) transform(opts esbuild.Options, transformCtx *resources.Resourc
|
||||
if err = transformCtx.PublishSourceMap(file.Contents); err != nil {
|
||||
return result, err
|
||||
}
|
||||
artifacts = append(artifacts, newArtifact(pathSpec.GetBasePath(false)+paths.PathEscape(paths.AddLeadingSlash(transformCtx.OutPath+".map")), basePath))
|
||||
}
|
||||
} else if isStdinEntryOutput(basePath) {
|
||||
mainOutput = file.Contents
|
||||
@@ -103,9 +127,13 @@ func (c *Client) transform(opts esbuild.Options, transformCtx *resources.Resourc
|
||||
if err = publishFileLoaderArtifact(pathSpec, opts.PublicPath, outDir, basePath, file.Contents, transformCtx); err != nil {
|
||||
return result, err
|
||||
}
|
||||
artifacts = append(artifacts, newArtifact(fileLoaderRelPermalink(basePath), basePath))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].RelPermalink() < artifacts[j].RelPermalink() })
|
||||
transformCtx.Data["Artifacts"] = artifacts
|
||||
|
||||
if mainOutput == nil {
|
||||
return result, fmt.Errorf("esbuild: entry point output not found")
|
||||
}
|
||||
|
||||
@@ -78,6 +78,42 @@ JS Content:{{ $js.Content }}:End:
|
||||
})
|
||||
}
|
||||
|
||||
// Issue #15173.
|
||||
func TestBuildDataArtifacts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/"
|
||||
disableKinds=["page", "section", "taxonomy", "term", "sitemap", "robotsTXT"]
|
||||
-- assets/js/main.js --
|
||||
import { hello } from './util';
|
||||
hello();
|
||||
-- assets/js/util.js --
|
||||
export function hello() {
|
||||
return 'abcd';
|
||||
}
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "js/main.js" | js.Build (dict "minify" true "sourcemap" "external") }}
|
||||
COUNT: {{ len .Data.Artifacts }}
|
||||
{{ range .Data.Artifacts }}
|
||||
ARTIFACT: {{ .RelPermalink }}|{{ .Permalink }}|{{ .MediaType.Type }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with resources.Get "js/main.js" | js.Build }}
|
||||
COUNT2: {{ len .Data.Artifacts }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/index.html",
|
||||
"COUNT: 1",
|
||||
"ARTIFACT: /js/main.js.map|https://example.org/js/main.js.map|application/source-map",
|
||||
"COUNT2: 0",
|
||||
)
|
||||
b.AssertFileExists("public/js/main.js.map", true)
|
||||
}
|
||||
|
||||
func TestBuildWithModAndNpm(t *testing.T) {
|
||||
if !htesting.IsCI() {
|
||||
t.Skip("skip (relative) long running modules test when running locally")
|
||||
|
||||
@@ -18,10 +18,14 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/evanw/esbuild/pkg/api"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/internal/js/esbuild"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/internal"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
type buildTransformation struct {
|
||||
@@ -69,6 +73,29 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
|
||||
opts.MediaType = ctx.InMediaType
|
||||
opts.Stdin = true
|
||||
opts.IsCSS = t.c.c.CssMode
|
||||
var ic resource.ResourceGetter
|
||||
if opts.ImportContext != nil {
|
||||
ic = resource.NewCachedResourceGetter(opts.ImportContext)
|
||||
}
|
||||
|
||||
if ic != nil {
|
||||
resolved := hmaps.NewCache[string, resource.Resource]()
|
||||
opts.ImportOnResolveFunc = func(imp string, args api.OnResolveArgs) string {
|
||||
if r := esbuild.ResolveResource(imp, ic); r != nil {
|
||||
p := esbuild.PrefixHugoVirtual + resources.InternalResourceTargetPath(r)
|
||||
resolved.Set(p, r)
|
||||
ctx.DependencyManager.AddIdentity(identity.FirstIdentity(r))
|
||||
return p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
opts.ImportOnLoadFunc = func(args api.OnLoadArgs) (string, error) {
|
||||
if r, found := resolved.Get(args.Path); found {
|
||||
return resources.InternalResourceSourceContent(ctx.Ctx, r)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err = t.c.transform(opts, ctx)
|
||||
|
||||
|
||||
@@ -158,6 +158,10 @@ type Options struct {
|
||||
// $color: vars.$color;
|
||||
Vars map[string]any
|
||||
|
||||
// User provided import context. If set, imports are looked up here first,
|
||||
// then in the assets filesystem.
|
||||
ImportContext any
|
||||
|
||||
// Deprecations IDs in this slice will be silenced.
|
||||
// The IDs can be found in the Dart Sass log output, e.g. "import" in
|
||||
// WARN Dart Sass: DEPRECATED [import].
|
||||
|
||||
@@ -51,6 +51,35 @@ T1: {{ $r.Content }}
|
||||
b.AssertFileContent("public/index.html", `T1: moo{color:#fff}`)
|
||||
}
|
||||
|
||||
// See issue 15103.
|
||||
func TestTransformImportContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dartsass.Supports() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/scss/_foo.scss --
|
||||
body { color: orange; }
|
||||
-- assets/scss/main.scss --
|
||||
@import "foo";
|
||||
@import "bar";
|
||||
-- layouts/home.html --
|
||||
{{ $foo := resources.FromString "foo.scss" "body { color: blue; }" }}
|
||||
{{ $bar := resources.FromString "bar.scss" "@import \"baz\";\nbody { color: green; }" }}
|
||||
{{ $baz := resources.FromString "baz.scss" "p { color: red; }" }}
|
||||
{{ $opts := dict "transpiler" "dartsass" "outputStyle" "compressed" "importContext" (slice $foo $bar $baz) }}
|
||||
{{ $r := resources.Get "scss/main.scss" | css.Sass $opts }}
|
||||
T1: {{ $r.Content }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
|
||||
// foo resolves in the import context before the assets filesystem.
|
||||
b.AssertFileContent("public/index.html", `T1: body{color:blue}p{color:red}body{color:green}`)
|
||||
}
|
||||
|
||||
func TestTransformImportRegularCSS(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dartsass.Supports() {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package dartsass
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/gohugoio/hugo/media"
|
||||
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/internal"
|
||||
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/sass"
|
||||
@@ -38,6 +40,10 @@ import (
|
||||
"github.com/bep/godartsass/v2"
|
||||
)
|
||||
|
||||
// Prefix for canonical URLs of stylesheets resolved in the user provided import context.
|
||||
// Note: This prefix must be all lower case.
|
||||
const dartSassImportContextPrefix = "hugoimportcontext:"
|
||||
|
||||
// Supports returns whether sass, dart-sass, or dart-sass-embedded is found in $PATH.
|
||||
func Supports() bool {
|
||||
if htesting.SupportsAll() {
|
||||
@@ -76,6 +82,11 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
|
||||
filename += t.c.sfs.RealFilename(ctx.SourcePath)
|
||||
}
|
||||
|
||||
var ic resource.ResourceGetter
|
||||
if opts.ImportContext != nil {
|
||||
ic = resource.NewCachedResourceGetter(opts.ImportContext)
|
||||
}
|
||||
|
||||
args := godartsass.Args{
|
||||
URL: filename,
|
||||
IncludePaths: t.c.sfs.RealDirs(baseDir),
|
||||
@@ -83,6 +94,8 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error {
|
||||
baseDir: baseDir,
|
||||
c: t.c,
|
||||
dependencyManager: ctx.DependencyManager,
|
||||
importContext: ic,
|
||||
ctx: ctx.Ctx,
|
||||
|
||||
vars: opts.Vars,
|
||||
},
|
||||
@@ -132,6 +145,8 @@ type importResolver struct {
|
||||
baseDir string
|
||||
c *Client
|
||||
dependencyManager identity.Manager
|
||||
importContext resource.ResourceGetter
|
||||
ctx context.Context
|
||||
vars map[string]any
|
||||
}
|
||||
|
||||
@@ -140,6 +155,11 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
|
||||
return strings.ToLower(url), nil
|
||||
}
|
||||
|
||||
if r := t.resolveInImportContext(url); r != nil {
|
||||
t.dependencyManager.AddIdentity(identity.FirstIdentity(r))
|
||||
return dartSassImportContextPrefix + paths.ToSlashTrimLeading(resources.InternalResourceTargetPath(r)), nil
|
||||
}
|
||||
|
||||
filePath, isURL := paths.UrlStringToFilename(url)
|
||||
var prevDir string
|
||||
var pathDir string
|
||||
@@ -160,21 +180,7 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
|
||||
name := filepath.Base(filePath)
|
||||
|
||||
// Pick the first match.
|
||||
var namePatterns []string
|
||||
if strings.Contains(name, ".") {
|
||||
namePatterns = []string{"_%s", "%s"}
|
||||
} else if strings.HasPrefix(name, "_") {
|
||||
namePatterns = []string{"_%s.scss", "_%s.sass", "_%s.css"}
|
||||
} else {
|
||||
namePatterns = []string{
|
||||
"_%s.scss", "%s.scss",
|
||||
"_%s.sass", "%s.sass",
|
||||
"_%s.css", "%s.css",
|
||||
"%s/_index.scss", "%s/_index.sass",
|
||||
"%s/index.scss", "%s/index.sass",
|
||||
}
|
||||
}
|
||||
|
||||
namePatterns := sassNamePatterns(name)
|
||||
name = strings.TrimPrefix(name, "_")
|
||||
|
||||
for _, namePattern := range namePatterns {
|
||||
@@ -192,21 +198,72 @@ func (t importResolver) CanonicalizeURL(url string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func sassNamePatterns(name string) []string {
|
||||
if strings.Contains(name, ".") {
|
||||
return []string{"_%s", "%s"}
|
||||
}
|
||||
if strings.HasPrefix(name, "_") {
|
||||
return []string{"_%s.scss", "_%s.sass", "_%s.css"}
|
||||
}
|
||||
return []string{
|
||||
"_%s.scss", "%s.scss",
|
||||
"_%s.sass", "%s.sass",
|
||||
"_%s.css", "%s.css",
|
||||
"%s/_index.scss", "%s/_index.sass",
|
||||
"%s/index.scss", "%s/index.sass",
|
||||
}
|
||||
}
|
||||
|
||||
// resolveInImportContext resolves url in the user provided import context, if any,
|
||||
// using the same name patterns as for the file system.
|
||||
func (t importResolver) resolveInImportContext(url string) resource.Resource {
|
||||
if t.importContext == nil {
|
||||
return nil
|
||||
}
|
||||
url = strings.TrimPrefix(url, dartSassImportContextPrefix)
|
||||
if r := t.importContext.Get(url); r != nil {
|
||||
return r
|
||||
}
|
||||
dir, name := path.Split(url)
|
||||
namePatterns := sassNamePatterns(name)
|
||||
name = strings.TrimPrefix(name, "_")
|
||||
for _, namePattern := range namePatterns {
|
||||
if r := t.importContext.Get(path.Join(dir, fmt.Sprintf(namePattern, name))); r != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t importResolver) Load(url string) (godartsass.Import, error) {
|
||||
if subPath, ok := sass.HugoVarsSubPath(url); ok {
|
||||
return godartsass.Import{
|
||||
Content: sass.CreateVarsStyleSheet(sass.TranspilerDart, sass.ResolveVars(t.vars, subPath)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(url, dartSassImportContextPrefix) {
|
||||
r := t.resolveInImportContext(url)
|
||||
if r == nil {
|
||||
return godartsass.Import{}, fmt.Errorf("could not find %q in the import context", url)
|
||||
}
|
||||
content, err := resources.InternalResourceSourceContent(t.ctx, r)
|
||||
return godartsass.Import{Content: content, SourceSyntax: sassSourceSyntax(url)}, err
|
||||
}
|
||||
|
||||
filename, _ := paths.UrlStringToFilename(url)
|
||||
b, err := afero.ReadFile(hugofs.Os, filename)
|
||||
|
||||
sourceSyntax := godartsass.SourceSyntaxSCSS
|
||||
if strings.HasSuffix(filename, ".sass") {
|
||||
sourceSyntax = godartsass.SourceSyntaxSASS
|
||||
} else if strings.HasSuffix(filename, ".css") {
|
||||
sourceSyntax = godartsass.SourceSyntaxCSS
|
||||
}
|
||||
|
||||
return godartsass.Import{Content: string(b), SourceSyntax: sourceSyntax}, err
|
||||
return godartsass.Import{Content: string(b), SourceSyntax: sassSourceSyntax(filename)}, err
|
||||
}
|
||||
|
||||
func sassSourceSyntax(name string) godartsass.SourceSyntax {
|
||||
switch {
|
||||
case strings.HasSuffix(name, ".sass"):
|
||||
return godartsass.SourceSyntaxSASS
|
||||
case strings.HasSuffix(name, ".css"):
|
||||
return godartsass.SourceSyntaxCSS
|
||||
default:
|
||||
return godartsass.SourceSyntaxSCSS
|
||||
}
|
||||
}
|
||||
|
||||
+52
-10
@@ -82,6 +82,7 @@ func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResour
|
||||
return &resourceAdapter{
|
||||
resourceTransformations: &resourceTransformations{},
|
||||
metaProvider: target,
|
||||
sourceTarget: target,
|
||||
resourceAdapterInner: &resourceAdapterInner{
|
||||
ctx: context.Background(),
|
||||
spec: spec,
|
||||
@@ -129,11 +130,12 @@ type ResourceTransformationCtx struct {
|
||||
// The media type of the transformed resource.
|
||||
OutMediaType media.Type
|
||||
|
||||
// Data data can be set on the transformed Resource. Not that this need
|
||||
// to be simple types, as it needs to be serialized to JSON and back.
|
||||
// Data can be set on the transformed Resource. For transformations
|
||||
// cached to disk (see transformationsToCacheOnDisk), this needs to be
|
||||
// simple types, as it will be serialized to JSON and back.
|
||||
Data map[string]any
|
||||
|
||||
// This is used to publish additional artifacts, e.g. source hhmaps.
|
||||
// This is used to publish additional artifacts, e.g. source maps.
|
||||
// We may improve this.
|
||||
OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
|
||||
}
|
||||
@@ -185,6 +187,12 @@ type resourceAdapter struct {
|
||||
commonResource
|
||||
*resourceTransformations
|
||||
*resourceAdapterInner
|
||||
|
||||
// The original untransformed target. The inner target is replaced with the
|
||||
// transformed resource once the transformation chain has run, so any new
|
||||
// transformations appended to the chain must start from this.
|
||||
sourceTarget transformableResource
|
||||
|
||||
metaProvider resource.ResourceMetaProvider
|
||||
}
|
||||
|
||||
@@ -229,6 +237,7 @@ func (r *resourceAdapter) GetDependencyManager() identity.Manager {
|
||||
|
||||
func (r resourceAdapter) cloneTo(targetPath string) resource.Resource {
|
||||
newtTarget := r.target.cloneTo(targetPath)
|
||||
r.sourceTarget = newtTarget.(transformableResource)
|
||||
newInner := &resourceAdapterInner{
|
||||
ctx: r.ctx,
|
||||
spec: r.spec,
|
||||
@@ -378,7 +387,7 @@ func (r resourceAdapter) TransformWithContext(ctx context.Context, t ...Resource
|
||||
spec: r.spec,
|
||||
Staler: r.Staler,
|
||||
publishOnce: &publishOnce{},
|
||||
target: r.target,
|
||||
target: r.sourceTarget,
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
@@ -446,11 +455,26 @@ func (r *resourceAdapter) publish() {
|
||||
}
|
||||
|
||||
func (r *resourceAdapter) TransformationKey() string {
|
||||
var key string
|
||||
for _, tr := range r.transformations {
|
||||
key = key + "_" + tr.Key().Value()
|
||||
return r.transformationKey(r.transformations)
|
||||
}
|
||||
|
||||
func (r *resourceAdapter) transformationKey(trs []ResourceTransformation) string {
|
||||
sb := bp.GetBuffer()
|
||||
defer bp.PutBuffer(sb)
|
||||
|
||||
for _, tr := range trs {
|
||||
sb.WriteString("_")
|
||||
sb.WriteString(tr.Key().Value())
|
||||
}
|
||||
return r.spec.ResourceCache.cleanKey(r.target.Key()) + "_" + hashing.MD5FromStringHexEncoded(key)
|
||||
|
||||
h := hashing.MD5FromReaderHexEncoded(sb)
|
||||
|
||||
sb.Reset()
|
||||
|
||||
sb.WriteString(r.spec.ResourceCache.cleanKey(r.target.Key()))
|
||||
sb.WriteString(h)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
|
||||
@@ -506,6 +530,25 @@ func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
|
||||
func (r *resourceAdapter) transform(key string, publish, setContent bool) (*resourceAdapterInner, error) {
|
||||
cache := r.spec.ResourceCache
|
||||
|
||||
trs := r.transformations
|
||||
writeToFileCache := false
|
||||
|
||||
// If a prefix of this chain has already run (e.g. .Content or .Data was
|
||||
// accessed before more transformations were chained), resume from the
|
||||
// longest cached prefix instead of re-running it.
|
||||
for i := len(trs) - 1; i > 0; i-- {
|
||||
if inner, found := cache.cacheResourceTransformation.Get(r.ctx, r.transformationKey(trs[:i])); found {
|
||||
for _, tr := range trs[:i] {
|
||||
if transformationsToCacheOnDisk[tr.Key().Name] {
|
||||
writeToFileCache = true
|
||||
}
|
||||
}
|
||||
r.target = inner.target
|
||||
trs = trs[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
b1 := bp.GetBuffer()
|
||||
b2 := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b1)
|
||||
@@ -540,11 +583,10 @@ func (r *resourceAdapter) transform(key string, publish, setContent bool) (*reso
|
||||
tctx.SourcePath = strings.TrimPrefix(tctx.InPath, "/")
|
||||
|
||||
counter := 0
|
||||
writeToFileCache := false
|
||||
|
||||
var transformedContentr io.Reader
|
||||
|
||||
for i, tr := range r.transformations {
|
||||
for i, tr := range trs {
|
||||
if i != 0 {
|
||||
tctx.InMediaType = tctx.OutMediaType
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ plugs:
|
||||
|
||||
# Runtime environment
|
||||
environment:
|
||||
# Home
|
||||
HOME: $SNAP_REAL_HOME
|
||||
|
||||
# Ruby
|
||||
GEM_HOME: $SNAP_USER_DATA/gems
|
||||
GEM_PATH: $SNAP/usr/share/rubygems-integration/all:$SNAP/usr/lib/ruby/gems/3.2.0:$SNAP/var/lib/gems/3.2.0
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Test the hugo env command.
|
||||
|
||||
hugo env
|
||||
stdout 'GOOS'
|
||||
stdout 'GOARCH'
|
||||
! stderr .
|
||||
stdout 'GOVERSION'
|
||||
stdout 'aomedia\.googlesource\.com/aom'
|
||||
stdout 'code\.videolan\.org/videolan/dav1d'
|
||||
stdout 'github\.com/AOMediaCodec/libavif'
|
||||
stdout 'github\.com/KaTeX/KaTeX'
|
||||
stdout 'github\.com/webmproject/libwebp'
|
||||
! stderr .
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# In Hugo v0.123 we started lowercasing the content paths, but on case-insensitive
|
||||
# filesystems existing caches keep their mixed-case dir names (e.g. MyBundle).
|
||||
# The pruner would then consider those entries unused and remove them on every
|
||||
# hugo --gc. See issue 15101.
|
||||
|
||||
[!caseinsensitivefs] skip
|
||||
|
||||
base64decode pixel.png.txt content/MyBundle/pix.png
|
||||
|
||||
hugo
|
||||
|
||||
stdout 'Processed images │ 1 '
|
||||
tree resources/_gen
|
||||
stdout 'pix_hu_a354833fd576551d.png'
|
||||
|
||||
# Simulate a cache created by an older Hugo version.
|
||||
mv resources/_gen/images/mybundle resources/_gen/images/MyBundle
|
||||
|
||||
hugo --gc
|
||||
stdout 'Cleaned │ 0 '
|
||||
|
||||
tree resources/_gen
|
||||
stdout 'pix_hu_a354833fd576551d.png'
|
||||
|
||||
-- hugo.toml --
|
||||
disableKinds = ["term", "taxonomy", "home"]
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
-- layouts/page.html --
|
||||
{{ $pix := .Resources.Get "pix.png" }}
|
||||
{{ $resized := $pix.Resize "10x10" }}
|
||||
Resized: {{ $resized.RelPermalink }}
|
||||
-- content/MyBundle/index.md --
|
||||
---
|
||||
title: MyBundle
|
||||
---
|
||||
-- pixel.png.txt --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
@@ -18,6 +18,7 @@ stdout 'modstatictok'
|
||||
! stdout 'modstaticsymlink'
|
||||
stdout pageok
|
||||
! stdout pagesymlink
|
||||
! stdout rootdirfile
|
||||
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss"]
|
||||
@@ -30,15 +31,24 @@ Read me.
|
||||
{{ with resources.Get "modassetsymlink.txt"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
|
||||
{{ with resources.GetMatch "modassetsymlink.txt"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
|
||||
{{ with resources.GetMatch "myassets/symlinkdir/**"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
|
||||
{{ with resources.Get "myassets/symlinkdir/rootdirfile1.txt"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
|
||||
Page: {{ .RelPermalink }}|{{ .Content }}|
|
||||
|
||||
{{/* os template package. */}}
|
||||
{{ $symFilePath := "content/pagesymlink.md" }}
|
||||
|
||||
{{ with os.ReadDir "assets/myassets/symlinkdir" }}FAIL {{ len . }}{{ else }}OK{{ end }}
|
||||
{{ with os.Stat $symFilePath }}FAIL{{ else }}OK{{ end }}
|
||||
{{ with os.ReadFile $symFilePath }}FAIL{{ else }}OK{{ end }}
|
||||
{{ with os.FileExists $symFilePath }}FAIL{{ else }}OK{{ end }}
|
||||
{{ template "check-os-path" $symFilePath }}
|
||||
{{ $symFilePath = "assets/myassets/symlinkdir/rootdirfile1.txt" }}
|
||||
{{ template "check-os-path" $symFilePath }}
|
||||
{{ $symFilePath = "assets/myassets/symlinkdir/subdir/rootdirfile3.txt" }}
|
||||
{{ template "check-os-path" $symFilePath }}
|
||||
|
||||
{{ define "check-os-path" }}
|
||||
{{ with os.Stat . }}FAIL{{ else }}OK{{ end }}
|
||||
{{ with os.ReadFile . }}FAIL{{ else }}OK{{ end }}
|
||||
{{ with os.FileExists . }}FAIL{{ else }}OK{{ end }}
|
||||
{{ end }}
|
||||
|
||||
-- content/pageok.md --
|
||||
-- themes/mytheme/assets/modassetok.txt --
|
||||
@@ -53,5 +63,7 @@ My file.
|
||||
Rootdirfile1 content.
|
||||
-- rootdir/rootdirfile2.txt --
|
||||
Rootdirfile2 content.
|
||||
-- rootdir/subdir/rootdirfile3.txt --
|
||||
Rootdirfile3 content.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Test the hugo server command when removing a file mounted into static.
|
||||
# See issue 15174.
|
||||
|
||||
hugo server &
|
||||
|
||||
waitServer
|
||||
|
||||
rm myfiles/robots.txt
|
||||
|
||||
sleep 2
|
||||
|
||||
stopServer
|
||||
! stderr .
|
||||
|
||||
-- hugo.toml --
|
||||
title = "Hugo Server Test"
|
||||
baseURL = "https://example.org/"
|
||||
disableKinds = ["taxonomy", "term", "sitemap"]
|
||||
[[module.mounts]]
|
||||
source = "content"
|
||||
target = "content"
|
||||
[[module.mounts]]
|
||||
source = "layouts"
|
||||
target = "layouts"
|
||||
[[module.mounts]]
|
||||
source = "myfiles/robots.txt"
|
||||
target = "static/robots.txt"
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
-- content/p1.md --
|
||||
-- myfiles/robots.txt --
|
||||
robots
|
||||
@@ -0,0 +1,26 @@
|
||||
# A deleted static file should be removed from the publish dir.
|
||||
|
||||
hugo server &
|
||||
|
||||
waitServer
|
||||
|
||||
exists public/static.txt
|
||||
|
||||
rm static/static.txt
|
||||
|
||||
sleep 2
|
||||
|
||||
! exists public/static.txt
|
||||
|
||||
stopServer
|
||||
! stderr .
|
||||
|
||||
-- hugo.toml --
|
||||
title = "Hugo Server Test"
|
||||
baseURL = "https://example.org/"
|
||||
disableKinds = ["taxonomy", "term", "sitemap"]
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
-- content/p1.md --
|
||||
-- static/static.txt --
|
||||
static
|
||||
@@ -0,0 +1,32 @@
|
||||
# Test the hugo server command when removing a directory mounted into static.
|
||||
# See issue 15174.
|
||||
|
||||
hugo server &
|
||||
|
||||
waitServer
|
||||
|
||||
rm mystatic/foo
|
||||
|
||||
sleep 2
|
||||
|
||||
stopServer
|
||||
! stderr .
|
||||
|
||||
-- hugo.toml --
|
||||
title = "Hugo Server Test"
|
||||
baseURL = "https://example.org/"
|
||||
disableKinds = ["taxonomy", "term", "sitemap"]
|
||||
[[module.mounts]]
|
||||
source = "content"
|
||||
target = "content"
|
||||
[[module.mounts]]
|
||||
source = "layouts"
|
||||
target = "layouts"
|
||||
[[module.mounts]]
|
||||
source = "mystatic/foo"
|
||||
target = "static/foo"
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
-- content/p1.md --
|
||||
-- mystatic/foo/static.txt --
|
||||
static
|
||||
@@ -458,6 +458,8 @@ fakefontdata
|
||||
{{ with . | css.Build (dict "minify" true) }}
|
||||
INLINE: <style>{{ .Content | safeCSS }}</style>
|
||||
LINKED: <link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
{{ range .Data.Artifacts }}ARTIFACT: {{ .RelPermalink }}|{{ .MediaType.Type }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
@@ -471,6 +473,9 @@ LINKED: <link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
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")
|
||||
// The artifact URL is the same on every host. See issue #15173.
|
||||
b.AssertFileContent("public/en/index.html", "ARTIFACT: /docs/css/ComicNeue-Regular-UA4ODE7N.ttf|font/ttf")
|
||||
b.AssertFileContent("public/fr/index.html", "ARTIFACT: /docs/css/ComicNeue-Regular-UA4ODE7N.ttf|font/ttf")
|
||||
}
|
||||
|
||||
t.Run("en first", func(t *testing.T) {
|
||||
@@ -486,6 +491,95 @@ LINKED: <link rel="stylesheet" href="{{ .RelPermalink }}" />
|
||||
})
|
||||
}
|
||||
|
||||
// Issue #15173.
|
||||
func TestCSSBuildDataArtifacts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/mysite/"
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
-- assets/css/main.css --
|
||||
@import "/fonts/fonts.css";
|
||||
body { color: #222; }
|
||||
-- assets/fonts/fonts.css --
|
||||
@font-face {
|
||||
font-family: 'Comic Neue';
|
||||
src: url(ComicNeue-Regular.woff2) format('woff2'), url(ComicNeue-Regular.ttf) format('truetype');
|
||||
}
|
||||
-- assets/fonts/ComicNeue-Regular.ttf --
|
||||
fakefontdata
|
||||
-- assets/fonts/ComicNeue-Regular.woff2 --
|
||||
fakefontdata2
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main.css" }}
|
||||
{{ with . | css.Build (dict "minify" true "sourcemap" "external") }}
|
||||
{{ range .Data.Artifacts }}
|
||||
ARTIFACT: {{ .RelPermalink }}|{{ .Permalink }}|{{ .MediaType.Type }}
|
||||
{{ end }}
|
||||
{{ range .Data.Artifacts }}{{ if eq .MediaType.MainType "font" }}
|
||||
<link rel="preload" href="{{ .RelPermalink }}" as="font" type="{{ .MediaType.Type }}" crossorigin>
|
||||
{{ end }}{{ end }}
|
||||
<link rel="stylesheet" href="{{ .RelPermalink }}">
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/index.html",
|
||||
"ARTIFACT: /mysite/css/main.css.map|https://example.org/mysite/css/main.css.map|application/source-map",
|
||||
".ttf|https://example.org/mysite/css/ComicNeue-Regular-",
|
||||
".woff2|https://example.org/mysite/css/ComicNeue-Regular-",
|
||||
"|font/ttf",
|
||||
"|font/woff2",
|
||||
`as="font" type="font/woff2" crossorigin`,
|
||||
)
|
||||
b.AssertFileExists("public/css/main.css.map", true)
|
||||
}
|
||||
|
||||
func TestCSSBuildDataArtifactsAndThenFingerprint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org/mysite/"
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
-- assets/css/main.css --
|
||||
@import "/fonts/fonts.css";
|
||||
body { color: #222; }
|
||||
-- assets/fonts/fonts.css --
|
||||
@font-face {
|
||||
font-family: 'Comic Neue';
|
||||
src: url(ComicNeue-Regular.woff2) format('woff2'), url(ComicNeue-Regular.ttf) format('truetype');
|
||||
}
|
||||
-- assets/fonts/ComicNeue-Regular.ttf --
|
||||
fakefontdata
|
||||
-- assets/fonts/ComicNeue-Regular.woff2 --
|
||||
fakefontdata2
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main.css" }}
|
||||
ORIG CSS SHA256: {{ .Content | crypto.Hash "sha256" }}|
|
||||
{{ with . | css.Build (dict "minify" true) }}
|
||||
{{ range .Data.Artifacts }}
|
||||
ARTIFACT: {{ .RelPermalink }}|{{ .Permalink }}|{{ .MediaType.Type }}
|
||||
{{ end }}
|
||||
BUILT CSS SHA256: {{ .Content | crypto.Hash "sha256" }}|
|
||||
{{ with . | fingerprint }}
|
||||
FINGERPRINTED: {{ .RelPermalink }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
b.AssertFileContent("public/index.html",
|
||||
"ARTIFACT: /mysite/css/ComicNeue-Regular-UA4ODE7N.ttf|",
|
||||
"ORIG CSS SHA256: a2b878c05df0f346c3854caebd21f00bb0965b46628440b8ccb31ce675af6bfe|",
|
||||
"BUILT CSS SHA256: 5f8757d5579386b8755e0df759c7eecfd069c385294dfe7bc3695862d7f218b1|",
|
||||
"FINGERPRINTED: /mysite/css/main.5f8757d5579386b8755e0df759c7eecfd069c385294dfe7bc3695862d7f218b1.css|",
|
||||
)
|
||||
}
|
||||
|
||||
// Issue #14623
|
||||
func TestCSSBuildLoadersPartial(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -731,3 +825,181 @@ body {
|
||||
"--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}}
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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/hugolib"
|
||||
"github.com/gohugoio/hugo/tpl/css"
|
||||
)
|
||||
|
||||
// See issue 15112.
|
||||
func TestChromaStyles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section", "page"]
|
||||
[markup.highlight]
|
||||
style = "monokai"
|
||||
-- layouts/home.html --
|
||||
{{ $light := css.ChromaStyles (dict "targetPath" "css/chroma-light.css" "style" "github" "mode" "light" "modeSelector" true) }}
|
||||
{{ $dark := css.ChromaStyles (dict "targetPath" "css/chroma-dark.css" "style" "github" "mode" "dark" "modeSelector" true) }}
|
||||
{{ $default := css.ChromaStyles (dict "targetPath" "css/chroma.css") }}
|
||||
Light: {{ $light.RelPermalink }}|Dark: {{ $dark.RelPermalink }}|Default: {{ $default.RelPermalink }}|
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Light: /css/chroma-light.css|Dark: /css/chroma-dark.css|Default: /css/chroma.css|")
|
||||
b.AssertFileContent("public/css/chroma-light.css", ".light .chroma ", ".light .bg {")
|
||||
b.AssertFileContent("public/css/chroma-dark.css", ".dark .chroma ", ".dark .bg {")
|
||||
// Style defaults to markup.highlight.style, monokai background.
|
||||
b.AssertFileContent("public/css/chroma.css", "/* Background */ .bg {", "background-color:#272822")
|
||||
}
|
||||
|
||||
// See issue 15167.
|
||||
func TestChromaStylesCustomModeClasses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section", "page"]
|
||||
-- layouts/home.html --
|
||||
{{ $light := css.ChromaStyles (dict "targetPath" "css/chroma-light.css" "style" "github" "mode" "light" "modeSelector" true "classLight" "theme-light") }}
|
||||
{{ $dark := css.ChromaStyles (dict "targetPath" "css/chroma-dark.css" "style" "github" "mode" "dark" "modeSelector" true "classDark" "theme-dark") }}
|
||||
Light: {{ $light.RelPermalink }}|Dark: {{ $dark.RelPermalink }}|
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/css/chroma-light.css", ".theme-light .chroma ", ".theme-light .bg {")
|
||||
b.AssertFileContent("public/css/chroma-dark.css", ".theme-dark .chroma ", ".theme-dark .bg {")
|
||||
}
|
||||
|
||||
// Chroma's minifier drops token rules whose color equals the style's default
|
||||
// foreground (e.g. .nx in github-dark). In a paired light/dark setup the light
|
||||
// sheet's explicit rule would then leak into dark mode, so these must be re-emitted.
|
||||
// See issue 15161.
|
||||
func TestChromaStylesKeepDefaultForegroundTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section", "page"]
|
||||
-- layouts/home.html --
|
||||
{{ $light := css.ChromaStyles (dict "targetPath" "css/chroma-light.css" "style" "github" "mode" "light") }}
|
||||
{{ $dark := css.ChromaStyles (dict "targetPath" "css/chroma-dark.css" "style" "github" "mode" "dark" "modeSelector" true) }}
|
||||
Light: {{ $light.RelPermalink }}|Dark: {{ $dark.RelPermalink }}|
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/css/chroma-light.css", "/* NameOther */ .chroma .nx { color:#1f2328 }")
|
||||
b.AssertFileContent("public/css/chroma-dark.css", "/* NameOther */ .dark .chroma .nx { color:#e6edf3 }")
|
||||
}
|
||||
|
||||
func TestChromaStylesErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section", "page"]
|
||||
-- layouts/home.html --
|
||||
{{ css.ChromaStyles OPTS }}
|
||||
`
|
||||
|
||||
for _, test := range []struct {
|
||||
opts string
|
||||
match string
|
||||
}{
|
||||
{`(dict "style" "monokai")`, `.*targetPath cannot be empty.*`},
|
||||
{`(dict "targetPath" "css/chroma.css" "style" "doesnotexist")`, `.*invalid style: doesnotexist.*`},
|
||||
{`(dict "targetPath" "css/chroma.css" "mode" "blue")`, `.*invalid mode: blue.*`},
|
||||
{`(dict "targetPath" "css/chroma.css" "style" "friendly" "mode" "dark")`, `.*style "friendly" does not have a "dark" mode.*`},
|
||||
} {
|
||||
files := strings.ReplaceAll(filesTemplate, "OPTS", test.opts)
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.ErrorMatches, test.match)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChromaStyles(b *testing.B) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section", "page"]
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
`
|
||||
|
||||
bb := hugolib.Test(b, files)
|
||||
|
||||
bb.Assert(true, qt.IsTrue)
|
||||
s := bb.H.Sites[0]
|
||||
ns := s.TemplateStore.GetTemplateFuncsNamespace("css").(*css.Namespace)
|
||||
|
||||
opts := map[string]any{"targetPath": "css/chroma.css", "style": "monokai", "mode": "light", "modeSelector": true}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
_, err := ns.ChromaStyles(opts)
|
||||
bb.Assert(err, qt.IsNil)
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,16 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types/css"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/markup/highlight"
|
||||
"github.com/gohugoio/hugo/markup/markup_config"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/resources/resource_transformers/babel"
|
||||
@@ -21,6 +26,7 @@ import (
|
||||
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
|
||||
"github.com/gohugoio/hugo/tpl/internal"
|
||||
"github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
@@ -34,6 +40,8 @@ type Namespace struct {
|
||||
tailwindcssClient *cssjs.TailwindCSSClient
|
||||
babelClient *babel.Client
|
||||
jsTransformClient *jstransform.Client
|
||||
chromaStylesCache *hmaps.Cache[uint64, resource.Resource]
|
||||
highlightConfig highlight.Config
|
||||
|
||||
// The Dart Client requires a os/exec process, so only
|
||||
// create it if we really need it.
|
||||
@@ -42,6 +50,48 @@ type Namespace struct {
|
||||
scssClientDartSass *dartsass.Client
|
||||
}
|
||||
|
||||
// ChromaStyles generates a CSS stylesheet for the Chroma code highlighter
|
||||
// and returns it as a Resource. This stylesheet is needed if
|
||||
// markup.highlight.noClasses is disabled in config.
|
||||
// The style defaults to the markup.highlight.style config setting.
|
||||
func (ns *Namespace) ChromaStyles(opts any) (resource.Resource, error) {
|
||||
key := hashing.HashUint64(opts)
|
||||
return ns.chromaStylesCache.GetOrCreate(key, func() (resource.Resource, error) {
|
||||
m, err := hmaps.ToStringMapE(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var o struct {
|
||||
TargetPath string
|
||||
highlight.ChromaStylesOptions `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
if err := mapstructure.WeakDecode(m, &o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if o.TargetPath == "" {
|
||||
return nil, errors.New("targetPath cannot be empty")
|
||||
}
|
||||
if o.Style == "" {
|
||||
o.Style = ns.highlightConfig.Style
|
||||
}
|
||||
css, err := highlight.ChromaStylesCSS(o.ChromaStylesOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ns.d.ResourceSpec.NewResource(
|
||||
resources.ResourceSourceDescriptor{
|
||||
LazyPublish: true,
|
||||
GroupIdentity: identity.Anonymous,
|
||||
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
|
||||
return hugio.NewReadSeekerNoOpCloserFromString(css), nil
|
||||
},
|
||||
TargetPath: o.TargetPath,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Quoted returns a string that needs to be quoted in CSS.
|
||||
func (ns *Namespace) Quoted(v any) css.QuotedString {
|
||||
s := cast.ToString(v)
|
||||
@@ -189,6 +239,8 @@ func init() {
|
||||
tailwindcssClient: cssjs.NewTailwindCSSClient(d.ResourceSpec),
|
||||
babelClient: babel.New(d.ResourceSpec),
|
||||
jsTransformClient: jstransform.New(d.BaseFs.Assets, d.ResourceSpec, true),
|
||||
chromaStylesCache: hmaps.NewCacheWithOptions[uint64, resource.Resource](hmaps.CacheOptions{Size: 10}),
|
||||
highlightConfig: d.Conf.GetConfigSection("markup").(markup_config.Config).Highlight,
|
||||
}
|
||||
|
||||
ns := &internal.TemplateFuncsNamespace{
|
||||
@@ -196,6 +248,11 @@ func init() {
|
||||
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
|
||||
}
|
||||
|
||||
ns.AddMethodMapping(ctx.ChromaStyles,
|
||||
nil,
|
||||
[][2]string{},
|
||||
)
|
||||
|
||||
ns.AddMethodMapping(ctx.PostCSS,
|
||||
[]string{"postCSS"},
|
||||
[][2]string{},
|
||||
|
||||
@@ -58,7 +58,10 @@ func New(deps *deps.Deps) (*Namespace, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ resource.ResourceFinder = (*Namespace)(nil)
|
||||
var (
|
||||
_ resource.ResourceFinder = (*Namespace)(nil)
|
||||
_ resource.Identifier = (*Namespace)(nil)
|
||||
)
|
||||
|
||||
// Namespace provides template functions for the "resources" namespace.
|
||||
type Namespace struct {
|
||||
@@ -300,3 +303,8 @@ func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedReso
|
||||
hugo.DeprecateWithLogger("resources.PostProcess", "Use templates.Defer instead. See https://gohugo.io/functions/templates/defer/", "v0.164.0", ns.deps.Log.Logger())
|
||||
return ns.deps.ResourceSpec.PostProcess(r)
|
||||
}
|
||||
|
||||
// For internal use only.
|
||||
func (ns *Namespace) Key() string {
|
||||
return "tpl.resources"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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 testinginternal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/tpl/internal"
|
||||
)
|
||||
|
||||
const name = "testinginternal"
|
||||
|
||||
func init() {
|
||||
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
|
||||
ctx := New(d)
|
||||
|
||||
ns := &internal.TemplateFuncsNamespace{
|
||||
Name: name,
|
||||
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
internal.AddTemplateFuncsNamespace(f)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 testinginternal provides template functions for internal use.
|
||||
// // These should not be used in user templates, as they are not guaranteed to be stable or even useful.
|
||||
package testinginternal
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
// New returns a new instance of the testinginternal-namespaced template functions.
|
||||
func New(d *deps.Deps) *Namespace {
|
||||
ns := &Namespace{}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
// Namespace provides template functions for the "testinginternal" namespace.
|
||||
type Namespace struct{}
|
||||
|
||||
// HashString wraps the core hashing func used for e.g. calculating resource transformation keys.
|
||||
func (ns *Namespace) HashString(args ...any) string {
|
||||
return hashing.HashString(args...)
|
||||
}
|
||||
|
||||
func (ns *Namespace) NewCachedResourceGetter(args ...any) resource.ResourceGetter {
|
||||
return resource.NewCachedResourceGetter(args...)
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
_ "github.com/gohugoio/hugo/tpl/site"
|
||||
_ "github.com/gohugoio/hugo/tpl/strings"
|
||||
_ "github.com/gohugoio/hugo/tpl/templates"
|
||||
_ "github.com/gohugoio/hugo/tpl/testinginternal"
|
||||
_ "github.com/gohugoio/hugo/tpl/time"
|
||||
_ "github.com/gohugoio/hugo/tpl/transform"
|
||||
_ "github.com/gohugoio/hugo/tpl/urls"
|
||||
|
||||
Reference in New Issue
Block a user