Compare commits

...

64 Commits

Author SHA1 Message Date
hugoreleaser 76a5e1880a releaser: Bump versions for release of 0.165.0
[ci skip]
2026-08-12 14:26:28 +00:00
dependabot[bot] 0bb337b22a build(deps): bump github.com/bep/imagemeta from 0.17.3 to 1.0.0
Bumps [github.com/bep/imagemeta](https://github.com/bep/imagemeta) from 0.17.3 to 1.0.0.
- [Release notes](https://github.com/bep/imagemeta/releases)
- [Commits](https://github.com/bep/imagemeta/compare/v0.17.3...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/bep/imagemeta
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 13:49:11 +02:00
dependabot[bot] 03dc9170b3 build(deps): bump github.com/evanw/esbuild from 0.28.1 to 0.28.2
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.28.1 to 0.28.2.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.1...v0.28.2)

---
updated-dependencies:
- dependency-name: github.com/evanw/esbuild
  dependency-version: 0.28.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 13:48:55 +02:00
dependabot[bot] c829b736cc build(deps): bump github.com/tdewolff/minify/v2 from 2.24.14 to 2.24.16
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.14 to 2.24.16.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.14...v2.24.16)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-version: 2.24.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 11:38:40 +02:00
Bjørn Erik Pedersen 995a2159e5 resources: Resume chained resource transformations
This is a follow up the bug fix in #15189. With the example given in that issue, the `css.Build` would be performed twice, which was unfortunate.

This commit fixes that by resuming the transformation from the last executed transformation.

See #15189
2026-08-11 19:12:24 +02:00
Bjørn Erik Pedersen f88f0a9f59 resources/jsconfig: Drop source root mapping for the current source root
* Intellisense doesn't need it to do its work.
* This also avoids creating a jsconfig.js file in the common cases.

Fixes #15169
2026-08-11 15:57:03 +02:00
Bjørn Erik Pedersen dd3f2731e0 Remove Star History from README
Removed Star History section from README.

Closes #15190
2026-08-10 23:08:30 +02:00
Bjørn Erik Pedersen f772998fa3 Fix resource transformation chaining after content access
When a transformed resource had been initialized (e.g. via .Data, .Content
or .RelPermalink) before chaining another transformation, the new chain
would run on the transformed output instead of the original source,
re-running all transformations on their own output.

Fixes #15189

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 18:10:06 +02:00
Bjørn Erik Pedersen 8a55df7af2 Remove tailwindcss from the default security.exec.allow list (note)
Fixes #15178
Closes #15171
2026-08-10 15:59:57 +02:00
Bjørn Erik Pedersen 52c9bd7908 circleci: Upgrade to Go 1.26.5 2026-08-09 21:38:57 +02:00
Bjørn Erik Pedersen 44da086082 Add Data.Artifacts to css.Build and js.Build
Artifacts are the additional output files published as part of the build:
source maps and files emitted by ESBuild's file loader (e.g. fonts). Each
artifact provides Permalink, RelPermalink and MediaType, so e.g. font
preload links can be constructed in templates.

Also add media type definitions for source maps (application/source-map),
font/woff and font/woff2.

Fixes #15173

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:51:42 +02:00
Bjørn Erik Pedersen 2ffaf1fc1e Fix server static file detection for deleted files/directories in the static syncer
Related to #15174
2026-08-08 18:08:03 +02:00
Bjørn Erik Pedersen a808f6e40d Fix server errors when deleting static files or directories
Fixes #15174
2026-08-08 18:08:03 +02:00
dependabot[bot] 94f3908ec6 build(deps): bump github.com/getkin/kin-openapi from 0.145.0 to 0.146.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.145.0 to 0.146.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.145.0...v0.146.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.146.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-07 11:31:51 +02:00
dependabot[bot] 75fcc7524a build(deps): bump github.com/rogpeppe/go-internal from 1.15.0 to 1.16.0
Bumps [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/rogpeppe/go-internal/releases)
- [Commits](https://github.com/rogpeppe/go-internal/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/rogpeppe/go-internal
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-07 11:31:37 +02:00
Bjørn Erik Pedersen 33d1f2c853 css: Add classDark and classLight options to css.ChromaStyles and gen chromastyles
Closes #15167
2026-08-05 16:04:48 +02:00
Bjørn Erik Pedersen 64da6d7c62 markup/highlight: Re-emit token colors dropped by Chroma's minifier
Chroma drops token rules whose color equals the style's default foreground
(e.g. .nx in github-dark). In a paired light/dark setup the other sheet's
explicit rule then leaks in, since an explicit declaration beats inheritance
from .chroma. Apply the chromaCSSOverrides custom CSS unconditionally so
these rules survive in modeSelector sheets too.

Updates #15161
2026-08-04 16:10:27 +02:00
Bjørn Erik Pedersen 70db201ed4 Add importContext option to css.Build, js.Build, css.Sass and css.TailwindCSS
This allows @import statements to be resolved in a set of user provided resources (e.g. from resources.FromString or css.ChromaStyles) before the assets filesystem.

The option also applies to css.PostCSS via the shared import inlining, and css.Sass requires the dartsass transpiler. The import context is part of the transformation cache key.

Fixes #15103

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:20:06 +02:00
Bjørn Erik Pedersen 8a468df065 Remove some old deprecations 2026-07-29 20:48:47 +02:00
dependabot[bot] b5fa03d3aa build(deps): bump github.com/mattn/go-isatty from 0.0.22 to 0.0.24
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.22 to 0.0.24.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.22...v0.0.24)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:42:01 +02:00
dependabot[bot] 9da472dd3b build(deps): bump github.com/yuin/goldmark from 1.8.4 to 1.8.5
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.8.4 to 1.8.5.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.8.4...v1.8.5)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-version: 1.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 18:41:48 +02:00
Bjørn Erik Pedersen 615e45d6e8 Add css.ChromaStyles
Move the stylesheet generation from the gen chromastyles command into
markup/highlight and share it with the new template function.

Fixes #15112

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:41:31 +02:00
dependabot[bot] 635532a20f build(deps): bump golang.org/x/tools from 0.47.0 to 0.48.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.47.0 to 0.48.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.47.0...v0.48.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 11:52:08 +02:00
Shantanav Mukherjee a243a6154c check.sh: Handle staticcheck not installed/in PATH 2026-07-28 12:31:02 +02:00
dependabot[bot] 9c71f600bf build(deps): bump github.com/kyokomi/emoji/v2 from 2.2.13 to 2.2.14
Bumps [github.com/kyokomi/emoji/v2](https://github.com/kyokomi/emoji) from 2.2.13 to 2.2.14.
- [Release notes](https://github.com/kyokomi/emoji/releases)
- [Commits](https://github.com/kyokomi/emoji/compare/v2.2.13...v2.2.14)

---
updated-dependencies:
- dependency-name: github.com/kyokomi/emoji/v2
  dependency-version: 2.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 12:29:19 +02:00
dependabot[bot] 420527fc6b build(deps): bump github.com/getkin/kin-openapi from 0.144.0 to 0.145.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.144.0 to 0.145.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.144.0...v0.145.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.145.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 12:28:40 +02:00
dependabot[bot] 7fe786e3b0 build(deps): bump golang.org/x/image from 0.43.0 to 0.44.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.43.0 to 0.44.0.
- [Commits](https://github.com/golang/image/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 17:44:56 +02:00
dependabot[bot] 03b244fc0b build(deps): bump github.com/bep/imagemeta from 0.17.2 to 0.17.3
Bumps [github.com/bep/imagemeta](https://github.com/bep/imagemeta) from 0.17.2 to 0.17.3.
- [Release notes](https://github.com/bep/imagemeta/releases)
- [Commits](https://github.com/bep/imagemeta/compare/v0.17.2...v0.17.3)

---
updated-dependencies:
- dependency-name: github.com/bep/imagemeta
  dependency-version: 0.17.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 17:44:44 +02:00
dependabot[bot] 961181334f build(deps): bump github.com/tdewolff/minify/v2 from 2.24.13 to 2.24.14
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.13 to 2.24.14.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.13...v2.24.14)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-version: 2.24.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 17:44:32 +02:00
dependabot[bot] e35b7f049d build(deps): bump github.com/yuin/goldmark from 1.8.2 to 1.8.4
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.8.2 to 1.8.4.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.8.2...v1.8.4)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-version: 1.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 17:44:14 +02:00
Bjørn Erik Pedersen 7d90277a28 warpc: Improve AVIF error message on memory allocation failure
See https://discourse.gohugo.io/t/avif-conversion-fails-for-jpg-files-with-embedded-colour-profiles-when-not-resizing/57392/4
2026-07-27 17:44:01 +02:00
dependabot[bot] 0796fa7ace build(deps): bump golang.org/x/net from 0.56.0 to 0.57.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-26 17:03:18 +02:00
Bjørn Erik Pedersen 6bf15241a1 Fix panic on server atomic save edits on MacOS
An atomic save (write temp file, rename into place) unlinks the inode the
watcher holds, so kqueue reports Remove for a file that's still on disk.
That took the delete branch and wiped the entire taxonomy subtree; the
following assemble then panicked in createMissingTaxonomies, where the
shifting tree.Get hit a not yet assembled *pageMetaSource.

Treat Remove of a path that still exists as an update, and use the
non-shifting GetRaw when checking for the auto created taxonomy node.

Fixes #15130

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 14:00:32 +02:00
Bjørn Erik Pedersen 861ede6d10 cache/filecache: Don't prune used cache entries with mixed-case dir names
On case-insensitive filesystems, entries created before we started lowercasing
the content paths in v0.123 are the same file as today's lowercased cache key,
and were removed on every hugo --gc.

Fixes #15101

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-26 11:40:27 +02:00
Bjørn Erik Pedersen f228c87d41 Drop symlinks in parent directories
Lstat only refrains from following the last element of a path, so a symlink
in an intermediate directory was still resolved, and reads could escape the
mount via e.g. resources.Get "symlinkdir/secret.txt".

Walk the directories up to the mount root and reject any that is a symlink.

Follow-up to cf9c8f93c and f8b5fa09a.
2026-07-25 18:17:06 +02:00
dependabot[bot] 1b701b72ca build(deps): bump golang.org/x/text from 0.38.0 to 0.40.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.38.0 to 0.40.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.38.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 18:14:11 +02:00
dependabot[bot] a32d70b712 build(deps): bump github.com/getkin/kin-openapi from 0.140.0 to 0.144.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.140.0 to 0.144.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.140.0...v0.144.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.144.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 18:13:45 +02:00
dependabot[bot] 948cfb98f9 build(deps): bump google.golang.org/grpc from 1.80.0 to 1.82.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.80.0 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 18:12:39 +02:00
dependabot[bot] 8930802ef2 build(deps): bump golang.org/x/mod from 0.37.0 to 0.38.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/mod/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 16:45:18 +02:00
Bjørn Erik Pedersen 7df45f615a Delete .gemini 2026-07-25 11:30:55 +02:00
Joe Mooring f961093ea9 markup/asciidocext: Fix TOC parsing for asciidoctor-html5s
Closes #15121

Co-authored-by: mike.szewil <szewil_michael@bah.com>
2026-07-22 19:32:19 +02:00
Joe Mooring 89b8c32200 common/hugo: Include non-go dependencies in go env output
Closes #15116

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 10:58:38 +02:00
Joe Mooring 984358f047 snap: Fix snap home environment
Closes #15114
2026-07-13 10:57:03 +02:00
Joe Mooring d1f191c58d Update README.md
- Remove Go Report Card badge (services has been sunset)
- Updated dependencies section
2026-07-13 10:56:09 +02:00
hugoreleaser a198116669 releaser: Prepare repository for 0.165.0-DEV
[ci skip]
2026-07-06 17:47:51 +00:00
hugoreleaser ce2470e701 releaser: Bump versions for release of 0.164.0
[ci skip]
2026-07-06 16:39:30 +00:00
dependabot[bot] 921db7b52c build(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2
Bumps [github.com/JohannesKaufmann/html-to-markdown/v2](https://github.com/JohannesKaufmann/html-to-markdown) from 2.5.1 to 2.5.2.
- [Release notes](https://github.com/JohannesKaufmann/html-to-markdown/releases)
- [Commits](https://github.com/JohannesKaufmann/html-to-markdown/compare/v2.5.1...v2.5.2)

---
updated-dependencies:
- dependency-name: github.com/JohannesKaufmann/html-to-markdown/v2
  dependency-version: 2.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 15:21:13 +02:00
dependabot[bot] 786ce71e59 build(deps): bump golang.org/x/tools from 0.45.0 to 0.47.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.45.0 to 0.47.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.45.0...v0.47.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 13:34:38 +02:00
dependabot[bot] 5ad2846161 build(deps): bump golang.org/x/image from 0.42.0 to 0.43.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.42.0 to 0.43.0.
- [Commits](https://github.com/golang/image/compare/v0.42.0...v0.43.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 13:33:50 +02:00
dependabot[bot] 36ad9f583a build(deps): bump golang.org/x/net from 0.55.0 to 0.56.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0.
- [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 11:50:17 +02:00
dependabot[bot] 7c0a0bc979 build(deps): bump github.com/pelletier/go-toml/v2 from 2.4.2 to 2.4.3
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.4.2 to 2.4.3.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.4.2...v2.4.3)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-06 11:49:16 +02:00
Bjørn Erik Pedersen d83ce27ae0 tpl/tplimpl: Support sub paths in layouts passed to .Render
E.g. {{ .Render "foo/mylayout" }} matches mylayout templates in
<dir>/foo for every dir from the page's layout path up to the layouts root.

Closes #15056

Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:14:32 +02:00
Bjørn Erik Pedersen 5a5f4a5495 all: Rewrite deprecated constructs in tests 2026-07-04 23:10:26 +02:00
Bjørn Erik Pedersen c6acc246ab Add markup.rst.syntaxHighlight option
Fixes #5349
2026-07-04 22:27:29 +02:00
Bjørn Erik Pedersen 29ed932513 tpl/resources: Deprecate resources.PostProcess in favour of templates.Defer
Fixes #15086
2026-07-04 21:28:31 +02:00
Bjørn Erik Pedersen 7b4ddd1863 contributing: Add a note about co-authoring attributions when borrowing test cases 2026-07-04 18:10:03 +02:00
bejaratommy 671897ae91 tpl/collections: Include key in IsSet unsupported-type warning
The warning logged when calling IsSet with an unsupported type did not
identify which key triggered it, making it hard to locate the offending
template. Include the key in the message.

Fixes #11794
2026-07-04 15:18:42 +02:00
dependabot[bot] a879ebfaaa build(deps): bump github.com/getkin/kin-openapi from 0.139.0 to 0.140.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.139.0 to 0.140.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.139.0...v0.140.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.140.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-04 14:43:32 +02:00
sjh9714 499794d19e create: Keep new content placeholders buildable
Fixes #15078
2026-07-04 14:31:54 +02:00
hexbinoct feb3d494b9 hugolib: Use the output format's suffix for alias paths
Alias generation hardcoded "index.html" and only recognized a ".html"
suffix, so an alias with an explicit ".htm" extension (or any other
suffix configured for the html media type) was written to the wrong
path. For example "d.htm" produced the directory "d.htm/index.html"
instead of the file "d.htm".

Build the alias file name from the output format's BaseName and the
media type's first suffix, and detect an explicit file by matching the
alias extension against the configured suffixes. The same check now
drives the uglyURLs case in Aliases.

Fixes #15066
2026-07-04 14:31:34 +02:00
Bjørn Erik Pedersen 65c82178b7 hugio: Speedup hasBytesWriter
The old implementation copied the incoming bytes into a fixed-size buffer
one byte at a time, and for every byte ran bytes.Contains for every
pattern over the whole buffer. That's O(n·patterns) scans of the buffer,
which showed up badly on large output (a full rendered page).

Scan each not-yet-matched pattern once per Write with bytes.Contains over
the chunk itself, in place, rather than per byte. The only carried state
is a small boundary window (the last maxPatternLen-1 bytes) joined with
the head of the next chunk, so a pattern straddling a Write boundary is
still detected. The chunk is never copied, so the extra allocation is
bounded by the longest pattern and independent of the output size. Once
all patterns have matched we mark done and drop the buffer.

patternLen summed the pattern lengths (to size the old buffer); the
boundary window only needs the longest pattern, so it's renamed
maxPatternLen and returns the max.

```bash
                  │ benchcmp.bench  │       fix-hasbytewriter.bench       │
                  │     sec/op      │    sec/op     vs base               │
HasBytesWriter-10   2473.095µ ± ∞ ¹   6.114µ ± ∞ ¹  -99.75% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                  │ benchcmp.bench │       fix-hasbytewriter.bench        │
                  │      B/op      │     B/op      vs base                │
HasBytesWriter-10      48.00 ± ∞ ¹   128.00 ± ∞ ¹  +166.67% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                  │ benchcmp.bench │       fix-hasbytewriter.bench       │
                  │   allocs/op    │  allocs/op   vs base                │
```
2026-07-04 13:25:35 +02:00
dependabot[bot] 332d5ec823 build(deps): bump golang.org/x/mod from 0.36.0 to 0.37.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/mod/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-04 12:54:41 +02:00
dependabot[bot] 212cc11ada build(deps): bump github.com/pelletier/go-toml/v2 from 2.3.1 to 2.4.2
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.3.1 to 2.4.2.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.3.1...v2.4.2)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-04 12:54:04 +02:00
Bjørn Erik Pedersen dfb35dcd7a tpl/crypto: Add crypto.Hash
Add a generic crypto.Hash template function returning the hex-encoded
checksum of a string using one of md5, sha1, sha256 (default), sha384 or
sha512. The supported algorithms match those used for the SRI hash in
.Data.Integrity on fingerprinted resources, so an SRI hash can be built
by composing with encoding.HexDecode and encoding.Base64Encode.

Fixes #15072

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:03:05 +02:00
127 changed files with 3248 additions and 763 deletions
+2 -2
View File
@@ -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
-13
View File
@@ -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: []
+2 -1
View File
@@ -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
@@ -63,7 +64,7 @@ jobs:
run: go install github.com/gohugoio/gotmplfmt@latest
- name: Install docutils
run: |
pip install docutils
pip install docutils Pygments
rst2html --version
- if: matrix.os == 'ubuntu-latest'
name: Install pandoc on Linux
+2 -1
View File
@@ -5,4 +5,5 @@ dist/
public/
.DS_Store
cache/filecache/_gen/
.claude/
.claude/
dump.txt
+1
View File
@@ -12,6 +12,7 @@
* In tests, use `qt` matchers (e.g. `b.Assert(err, qt.ErrorMatches, ...)`) instead of raw `if`/`t.Fatal` checks.
* In tests, always use the latest Hugo specification, e.g. for layouts, it's `layouts/page.html` and not `layouts/_default/single.html`, `layouts/list.html` and not `layouts/_default/list.html`
* Never name tests `TestIssue1234`; always give the test function a descriptive name, e.g. `TestDisablePathToLower`, and add any issue reference as a Go doc function comment, e.g. `// See issue 1234.`.
* If you borrow a test case (e.g. from the issue), that test's author must be added as co-author in the commit.
* If you're a security researcher, read @SECURITY.md carefully.
* Brevity is good. This applies to code, comments and commit messages. Don't write a novel.
* Use `./check.sh ./somepackage/...` when iterating.
+1
View File
@@ -72,6 +72,7 @@ To make the contribution process as seamless as possible, we ask for the followi
* When youre ready to create a pull request, be sure to:
* Sign the [CLA](https://cla-assistant.io/gohugoio/hugo).
* Have test cases for the new code. If you have questions about how to do this, please ask in your pull request.
* If you borrow a test case (e.g. from the issue), that test's author must be added as [co-author](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors) in the commit.
* Run `go fmt`.
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
* Squash your commits into a single commit. `git rebase -i`. Its okay to force update your pull request with `git push -f`.
+122 -65
View File
@@ -37,7 +37,6 @@ A fast and flexible static site generator built with love by [bep][], [spf13][],
[![GoDoc](https://godoc.org/github.com/gohugoio/hugo?status.svg)](https://godoc.org/github.com/gohugoio/hugo)
[![Tests on Linux, MacOS and Windows](https://github.com/gohugoio/hugo/workflows/Test/badge.svg)](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](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
[![Star History Chart](https://api.star-history.com/svg?repos=gohugoio/hugo&type=Timeline)](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>
+54 -13
View File
@@ -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
View File
@@ -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()
+10 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+51 -28
View File
@@ -21,8 +21,9 @@ import (
type HasBytesWriter struct {
Patterns []*HasBytesPattern
i int
done bool
// The tail of the bytes written so far, retained so we can detect a
// pattern that straddles the boundary between two Write calls.
buff []byte
}
@@ -31,10 +32,13 @@ type HasBytesPattern struct {
Pattern []byte
}
func (h *HasBytesWriter) patternLen() int {
// maxPatternLen returns the length of the longest pattern.
func (h *HasBytesWriter) maxPatternLen() int {
l := 0
for _, p := range h.Patterns {
l += len(p.Pattern)
if len(p.Pattern) > l {
l = len(p.Pattern)
}
}
return l
}
@@ -44,36 +48,55 @@ func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
if len(h.buff) == 0 {
h.buff = make([]byte, h.patternLen()*2)
keep := h.maxPatternLen() - 1
// Join the tail retained from previous Writes with the head of this chunk
// so a pattern straddling the boundary is still detected. Only the
// boundary window is copied; the chunk itself is scanned in place below.
var boundary []byte
if keep > 0 && len(h.buff) > 0 {
head := p
if len(head) > keep {
head = head[:keep]
}
boundary = make([]byte, 0, len(h.buff)+len(head))
boundary = append(boundary, h.buff...)
boundary = append(boundary, head...)
}
for i := range p {
h.buff[h.i] = p[i]
h.i++
if h.i == len(h.buff) {
// Shift left.
copy(h.buff, h.buff[len(h.buff)/2:])
h.i = len(h.buff) / 2
// Scan each not-yet-matched pattern once per Write instead of once per byte.
done := true
for _, pp := range h.Patterns {
if pp.Match {
continue
}
for _, pp := range h.Patterns {
if bytes.Contains(h.buff, pp.Pattern) {
pp.Match = true
done := true
for _, ppp := range h.Patterns {
if !ppp.Match {
done = false
break
}
}
if done {
h.done = true
}
return len(p), nil
}
if bytes.Contains(p, pp.Pattern) || bytes.Contains(boundary, pp.Pattern) {
pp.Match = true
continue
}
done = false
}
if done {
// All patterns found; no need to look at any more data.
h.done = true
h.buff = nil
return len(p), nil
}
// Retain the last keep bytes of (previous tail + this chunk) to detect a
// pattern straddling into the next Write.
switch {
case keep <= 0:
h.buff = h.buff[:0]
case len(p) >= keep:
h.buff = append(h.buff[:0], p[len(p)-keep:]...)
default:
// Chunk shorter than keep: slide the window over the retained tail.
if total := len(h.buff) + len(p); total > keep {
h.buff = h.buff[total-keep:]
}
h.buff = append(h.buff, p...)
}
return len(p), nil
+58
View File
@@ -65,3 +65,61 @@ func TestHasBytesWriter(t *testing.T) {
fmt.Fprintf(w, "__foo")
c.Assert(h.Patterns[0].Match, qt.Equals, true)
}
func TestHasBytesWriterMultiplePatterns(t *testing.T) {
c := qt.New(t)
neww := func() (*HasBytesWriter, io.Writer) {
var b bytes.Buffer
h := &HasBytesWriter{
Patterns: []*HasBytesPattern{
{Pattern: []byte("__hdeferred/")},
{Pattern: []byte("__h_pp_l1")},
},
}
return h, io.MultiWriter(&b, h)
}
// Neither pattern present.
h, w := neww()
fmt.Fprint(w, "the quick brown fox jumps over the lazy dog")
c.Assert(h.Patterns[0].Match, qt.Equals, false)
c.Assert(h.Patterns[1].Match, qt.Equals, false)
c.Assert(h.done, qt.Equals, false)
// Only the second pattern present; the writer must not report a match
// for the first, and must not prematurely mark itself done.
h, w = neww()
fmt.Fprint(w, "prefix __h_pp_l1 suffix")
c.Assert(h.Patterns[0].Match, qt.Equals, false)
c.Assert(h.Patterns[1].Match, qt.Equals, true)
c.Assert(h.done, qt.Equals, false)
// Both patterns present across multiple writes; done once all match.
h, w = neww()
fmt.Fprint(w, "aaa __hdef")
fmt.Fprint(w, "erred/xyz bbb __h_p")
fmt.Fprint(w, "p_l1 ccc")
c.Assert(h.Patterns[0].Match, qt.Equals, true)
c.Assert(h.Patterns[1].Match, qt.Equals, true)
c.Assert(h.done, qt.Equals, true)
}
func BenchmarkHasBytesWriter(b *testing.B) {
// A large chunk of output containing neither pattern is the common case
// (a normal rendered page): the writer must scan all of it.
content := []byte(strings.Repeat("<div class=\"nav\"><a href=\"/foo/bar\">baz</a></div>\n", 4000))
b.ResetTimer()
for range b.N {
h := &HasBytesWriter{
Patterns: []*HasBytesPattern{
{Pattern: []byte("__hdeferred/")},
{Pattern: []byte("__h_pp_l1")},
},
}
if _, err := h.Write(content); err != nil {
b.Fatal(err)
}
}
}
+7 -2
View File
@@ -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),
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -19,7 +19,7 @@ import "github.com/gohugoio/hugo/common/version"
// This should be the only one.
var CurrentVersion = version.Version{
Major: 0,
Minor: 164,
Minor: 165,
PatchLevel: 0,
Suffix: "-DEV",
Suffix: "",
}
-30
View File
@@ -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.",
@@ -25,11 +25,13 @@ weight = 2
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'en'
[module.mounts.sites.matrix]
languages = 'en'
[[module.mounts]]
source = 'content/sv'
target = 'content'
lang = 'sv'
[module.mounts.sites.matrix]
languages = 'sv'
-- content/en/p1.md --
---
title: "p1"
+8 -4
View File
@@ -19,19 +19,23 @@ defaultContentLanguage = 'en'
[[module.mounts]]
source = 'content/en'
target = 'content/en'
lang = 'en'
[module.mounts.sites.matrix]
languages = 'en'
[[module.mounts]]
source = 'content/nn'
target = 'content/nn'
lang = 'nn'
[module.mounts.sites.matrix]
languages = 'nn'
[[module.mounts]]
source = 'content/no'
target = 'content/no'
lang = 'no'
[module.mounts.sites.matrix]
languages = 'no'
[[module.mounts]]
source = 'content/sv'
target = 'content/sv'
lang = 'sv'
[module.mounts.sites.matrix]
languages = 'sv'
[[module.mounts]]
source = 'layouts'
target = 'layouts'
-14
View File
@@ -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"`
+4 -5
View File
@@ -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.
+1 -1
View File
@@ -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",
)
}
-10
View File
@@ -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.
+37 -6
View File
@@ -74,7 +74,6 @@ func TestNewContentFromFile(t *testing.T) {
c := qt.New(t)
for i, cas := range cases {
c.Run(cas.name, func(c *qt.C) {
c.Parallel()
@@ -109,7 +108,6 @@ func TestNewContentFromFile(t *testing.T) {
}
}
})
}
}
@@ -159,6 +157,37 @@ site RegularPages: {{ len site.RegularPages }}
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "mypage.md")), `draft: true`)
}
// See issue 15078.
func TestNewContentWithBuildCascade(t *testing.T) {
t.Parallel()
mm := afero.NewMemMapFs()
c := qt.New(t)
c.Assert(initFs(mm), qt.IsNil)
c.Assert(mm.MkdirAll(filepath.Join("content", "posts", "drafts"), 0o755), qt.IsNil)
c.Assert(afero.WriteFile(mm, filepath.Join("content", "posts", "drafts", "_index.md"), []byte(`---
title: Drafts
build:
render: never
cascade:
draft: true
build:
render: link
draft: true
---
`), 0o755), qt.IsNil)
cfg, fs := newTestCfg(c, mm)
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
const target = "posts/drafts/trip-to-puebla/index.md"
c.Assert(create.NewContent(h, "", target, false), qt.IsNil)
c.Assert(readFileFromFs(c, fs.Source, filepath.Join("content", target)), qt.Contains, `title: "Trip to Puebla"`)
}
func initFs(fs afero.Fs) error {
perm := os.FileMode(0o755)
var err error
@@ -308,10 +337,10 @@ theme = "mytheme"
[languages]
[languages.en]
weight = 1
languageName = "English"
label = "English"
[languages.nn]
weight = 2
languageName = "Nynorsk"
label = "Nynorsk"
[module]
[[module.mounts]]
@@ -320,11 +349,13 @@ languageName = "Nynorsk"
[[module.mounts]]
source = 'content'
target = 'content'
lang = 'en'
[module.mounts.sites.matrix]
languages = 'en'
[[module.mounts]]
source = 'content_nn'
target = 'content'
lang = 'nn'
[module.mounts.sites.matrix]
languages = 'nn'
`
if mm == nil {
mm = afero.NewMemMapFs()
+36
View File
@@ -0,0 +1,36 @@
---
title: crypto.Hash
description: Hashes the given input with the given algorithm and returns its checksum encoded to a hexadecimal string.
categories: []
keywords: []
params:
functions_and_methods:
aliases: []
returnType: string
signatures: ['crypto.Hash [ALGORITHM] INPUT']
---
The `ALGORITHM` is one of `md5`, `sha1`, `sha256` (the default), `sha384`, or `sha512`:
```go-html-template
{{ crypto.Hash "sha256" "Hello world" }} → 64ec88ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c
{{ "Hello world" | crypto.Hash "sha512" }} → b7f783baed8297f0db917462184ff4f08e69c2d5e5f79a942600f9725f58ce1f29c18139bf80b06c0fff2bdd34738452ecf40c488c22a7e3d80cdf6f9c1c0d47
```
If you omit the algorithm, it defaults to `sha256`:
```go-html-template
{{ "Hello world" | crypto.Hash }} → 64ec88ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c
```
The supported algorithms match those used for the [Subresource Integrity] hash in [`.Data.Integrity`] on a fingerprinted resource. Combine `crypto.Hash` with [`encoding.HexDecode`] and [`encoding.Base64Encode`] to construct an SRI hash from a string:
```go-html-template
{{ $algo := "sha256" }}
{{ $integrity := printf "%s-%s" $algo ("Hello world" | crypto.Hash $algo | encoding.HexDecode | encoding.Base64Encode) }}
```
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[`.Data.Integrity`]: /methods/resource/data/
[`encoding.HexDecode`]: /functions/encoding/hexdecode/
[`encoding.Base64Encode`]: /functions/encoding/base64encode/
@@ -0,0 +1,15 @@
---
title: encoding.HexDecode
description: Returns the hexadecimal decoding of the given content.
categories: []
keywords: []
params:
functions_and_methods:
aliases: []
returnType: string
signatures: [encoding.HexDecode INPUT]
---
```go-html-template
{{ "48656c6c6f20776f726c64" | encoding.HexDecode }} → Hello world
```
@@ -0,0 +1,15 @@
---
title: encoding.HexEncode
description: Returns the hexadecimal encoding of the given content.
categories: []
keywords: []
params:
functions_and_methods:
aliases: []
returnType: string
signatures: [encoding.HexEncode INPUT]
---
```go-html-template
{{ "Hello world" | encoding.HexEncode }} → 48656c6c6f20776f726c64
```
+34 -40
View File
@@ -2,7 +2,7 @@ module github.com/gohugoio/hugo
require (
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.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 v1.41.6
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.61.1
@@ -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.139.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,38 +47,38 @@ 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
github.com/niklasfasching/go-org v1.9.1
github.com/olekukonko/tablewriter v1.1.4
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pelletier/go-toml/v2 v2.3.1
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.42.0
golang.org/x/mod v0.36.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.21.0
golang.org/x/text v0.38.0
golang.org/x/tools v0.45.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,10 +99,10 @@ 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.2.0 // indirect
github.com/JohannesKaufmann/dom v0.3.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.2 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 // indirect
@@ -125,18 +125,18 @@ 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
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -146,29 +146,24 @@ require (
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // 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
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/woodsbury/decimal128 v1.3.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
@@ -177,17 +172,16 @@ 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.51.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.45.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
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)
+70 -93
View File
@@ -84,24 +84,20 @@ 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=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ=
github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1 h1:IpUgup6ucCE4wB59wAP0Y2qSApYjFhSfGVjShUBoVSw=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1/go.mod h1:KUwy/WLgv9kv2yeBZkPCgDokHzg0M6EdRc17thnbVFw=
github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0=
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
@@ -168,16 +164,14 @@ 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=
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
github.com/bep/mclib v1.20401.20400 h1:silTOMNlNI7yHBb+HxEE0THIVFVWo/0I4SCH69FxtmU=
github.com/bep/mclib v1.20401.20400/go.mod h1:v5Hh3EIinPn7epigP28uf9JCkZlYzBS2vEOPe2wrHzM=
github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs=
github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc=
github.com/bep/simplecobra v0.7.0 h1:kG8ZPwEc1o96hlIVGXcrrvwC8RornBqvMD3+pS0Z7y0=
@@ -204,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=
@@ -215,8 +209,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -228,17 +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.0 h1:V96ghtc5p5JnNUQIUsc5H3kr+AcFcMqOJll2ZmJW6Lo=
github.com/evanw/esbuild v0.28.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
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=
@@ -248,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.139.0 h1:pBFXcZJFwz9J1X64jzxlOoNgFm+TF7kNrs9+HJVN6Ic=
github.com/getkin/kin-openapi v0.139.0/go.mod h1:NGxPfE4PwS/TRXEbyx2RrxDFPZvxcWw31Tw8XXjPZLs=
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=
@@ -262,14 +252,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -282,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=
@@ -387,8 +377,6 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
@@ -404,28 +392,24 @@ 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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
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=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
@@ -434,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=
@@ -448,10 +432,8 @@ github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVW
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -468,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=
@@ -507,25 +489,20 @@ 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=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
@@ -536,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=
@@ -567,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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
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=
@@ -581,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.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.42.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=
@@ -604,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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
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=
@@ -637,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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
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=
@@ -660,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=
@@ -698,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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.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=
@@ -707,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=
@@ -761,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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
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=
@@ -837,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=
@@ -857,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=
+22
View File
@@ -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) {
+34
View File
@@ -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
}
+9 -1
View File
@@ -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() {
+16 -6
View File
@@ -96,7 +96,7 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
handler := newAliasHandler(s.GetTemplateStore(), s.Log, allowRoot)
targetPath, err := handler.targetPathAlias(path)
targetPath, err := handler.targetPathAlias(path, outputFormat)
if err != nil {
return err
}
@@ -120,7 +120,7 @@ func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFo
return s.publisher.Publish(pd)
}
func (a aliasHandler) targetPathAlias(src string) (string, error) {
func (a aliasHandler) targetPathAlias(src string, of output.Format) (string, error) {
originalAlias := src
if len(src) <= 0 {
return "", fmt.Errorf("alias \"\" is an empty string")
@@ -176,13 +176,23 @@ func (a aliasHandler) targetPathAlias(src string) (string, error) {
}
}
// Add the final touch
// Add the final touch. When the alias does not already end in one of the
// output format's configured suffixes, treat it as a directory and append
// the format's base name and suffix (e.g. index.html).
alias = strings.TrimPrefix(alias, "/")
baseFile := of.BaseName + of.MediaType.FirstSuffix.FullSuffix
if strings.HasSuffix(alias, "/") {
alias = alias + "index.html"
} else if !strings.HasSuffix(alias, ".html") {
alias = alias + "/" + "index.html"
alias = alias + baseFile
} else if !pathHasOutputFormatSuffix(alias, of) {
alias = alias + "/" + baseFile
}
return filepath.FromSlash(alias), nil
}
// pathHasOutputFormatSuffix reports whether the last element of p ends in one
// of the suffixes configured for the output format's media type.
func pathHasOutputFormatSuffix(p string, of output.Format) bool {
ext := path.Ext(p)
return ext != "" && of.MediaType.HasSuffix(ext[1:])
}
+77 -1
View File
@@ -21,6 +21,7 @@ import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/output"
)
func TestAlias(t *testing.T) {
@@ -158,7 +159,7 @@ func TestTargetPathHTMLRedirectAlias(t *testing.T) {
}
for _, test := range tests {
path, err := h.targetPathAlias(test.value)
path, err := h.targetPathAlias(test.value, output.HTMLFormat)
if (err == nil) != test.errIsNil {
t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)
continue
@@ -789,3 +790,78 @@ build:
b.AssertFileExists("public/p2-alias/index.html", false)
b.AssertFileExists("public/p3-alias/index.html", false)
}
// See issue 15066.
func TestAliasExplicitExtension(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
uglyURLs = UGLYURLS
[mediaTypes]
[mediaTypes.'text/html']
suffixes = SUFFIXES
[outputFormats]
[outputFormats.html]
mediaType = 'text/html'
-- content/s1/p1.md --
---
title: p1
aliases: [a, b.foo, c.html, d.htm, e.foo.html, f.foo.htm]
---
-- layouts/page.html --
{{ .Title }}
`
r := strings.NewReplacer("UGLYURLS", "false", "SUFFIXES", "['html', 'htm']")
f := r.Replace(files)
b := Test(t, f)
b.AssertFileContent("public/s1/a/index.html", "url=/s1/p1/")
b.AssertFileContent("public/s1/b.foo/index.html", "url=/s1/p1/")
b.AssertFileContent("public/s1/c.html", "url=/s1/p1/")
b.AssertFileContent("public/s1/d.htm", "url=/s1/p1/")
b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1/")
b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1/")
r = strings.NewReplacer("UGLYURLS", "false", "SUFFIXES", "['htm', 'html']")
f = r.Replace(files)
b = Test(t, f)
b.AssertFileContent("public/s1/a/index.htm", "url=/s1/p1/")
b.AssertFileContent("public/s1/b.foo/index.htm", "url=/s1/p1/")
b.AssertFileContent("public/s1/c.html", "url=/s1/p1/")
b.AssertFileContent("public/s1/d.htm", "url=/s1/p1/")
b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1/")
b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1/")
r = strings.NewReplacer("UGLYURLS", "true", "SUFFIXES", "['html', 'htm']")
f = r.Replace(files)
b = Test(t, f)
b.AssertFileContent("public/s1/a.html", "url=/s1/p1.html")
b.AssertFileContent("public/s1/b.foo.html", "url=/s1/p1.html")
b.AssertFileContent("public/s1/c.html", "url=/s1/p1.html")
b.AssertFileContent("public/s1/d.htm", "url=/s1/p1.html")
b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1.html")
b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1.html")
r = strings.NewReplacer("UGLYURLS", "true", "SUFFIXES", "['htm', 'html']")
f = r.Replace(files)
b = Test(t, f)
b.AssertFileContent("public/s1/a.htm", "url=/s1/p1.htm")
b.AssertFileContent("public/s1/b.foo.htm", "url=/s1/p1.htm")
b.AssertFileContent("public/s1/c.html", "url=/s1/p1.htm")
b.AssertFileContent("public/s1/d.htm", "url=/s1/p1.htm")
b.AssertFileContent("public/s1/e.foo.html", "url=/s1/p1.htm")
b.AssertFileContent("public/s1/f.foo.htm", "url=/s1/p1.htm")
}
+8 -25
View File
@@ -315,11 +315,11 @@ baseName = "o1main"
[languages]
[languages.en]
languageName = "English"
label = "English"
[languages.en.params]
pl1 = "p1-en-main"
[languages.nb]
languageName = "Norsk"
label = "Norsk"
[languages.nb.params]
pl1 = "p1-nb-main"
@@ -368,7 +368,7 @@ baseName = "o2theme"
[languages]
[languages.en]
languageName = "English2"
label = "English2"
[languages.en.params]
pl1 = "p1-en-theme"
pl2 = "p2-en-theme"
@@ -377,7 +377,7 @@ name = "menu-lang-en-main"
[[languages.en.menu.theme]]
name = "menu-lang-en-theme"
[languages.nb]
languageName = "Norsk2"
label = "Norsk2"
[languages.nb.params]
pl1 = "p1-nb-theme"
pl2 = "p2-nb-theme"
@@ -905,16 +905,16 @@ func TestConfigMergeLanguageDeepEmptyLeftSide(t *testing.T) {
[params]
p1 = "p1base"
[languages.en]
languageCode = 'en-US'
languageName = 'English'
locale = 'en-US'
label = 'English'
weight = 1
[languages.en.markup.goldmark.extensions.typographer]
leftDoubleQuote = '&ldquo;' # default &ldquo;
rightDoubleQuote = '&rdquo;' # default &rdquo;
[languages.de]
languageCode = 'de-DE'
languageName = 'Deutsch'
locale = 'de-DE'
label = 'Deutsch'
weight = 2
[languages.de.params]
p1 = "p1de"
@@ -1000,23 +1000,6 @@ HTACCESS.
b.AssertFileContent("public/.htaccess", "HTACCESS")
}
func TestConfigLanguageCodeTopLevel(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
languageCode = "en-US"
-- layouts/home.html --
LanguageCode: {{ .Site.LanguageCode }}|{{ site.Language.LanguageCode }}|
`
b := Test(t, files, TestOptInfo())
b.AssertLogContains("deprecated")
b.AssertFileContent("public/index.html", "LanguageCode: en-US|en-US|")
}
// See #11159
func TestConfigOutputFormatsPerLanguage(t *testing.T) {
t.Parallel()
+1
View File
@@ -114,6 +114,7 @@ func (f ContentFactory) CreateContentPlaceHolder(filename string, force bool) (s
// the paths correct.
placeholder := `---
title: "Content Placeholder"
draft: false
build:
render: never
list: never
+7 -4
View File
@@ -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
}
}
+11 -2
View File
@@ -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
}
+3 -9
View File
@@ -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
+5 -5
View File
@@ -69,18 +69,18 @@ path="d"
[languages.en]
title = "Title in English"
languageName = "English"
label = "English"
weight = 1
[languages.nn]
languageName = "Nynorsk"
label = "Nynorsk"
weight = 2
title = "Tittel på nynorsk"
[languages.nb]
languageName = "Bokmål"
label = "Bokmål"
weight = 3
title = "Tittel på bokmål"
[languages.fr]
languageName = "French"
label = "French"
weight = 4
title = "French Title"
@@ -312,7 +312,7 @@ func TestMountSameSource(t *testing.T) {
files := `
-- hugo.toml --
baseURL = 'https://example.org/'
languageCode = 'en-us'
locale = 'en-us'
title = 'Hugo GitHub Issue #9426'
disableKinds = ['RSS','sitemap','taxonomy','term']
+2
View File
@@ -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()))
}
+2 -2
View File
@@ -51,7 +51,7 @@ defaultContentLanguage = "nn"
[languages]
[languages.nn]
languageName = "Nynorsk"
label = "Nynorsk"
weight = 1
title = "Tittel på Nynorsk"
[languages.nn.params]
@@ -59,7 +59,7 @@ p1 = "p1nn"
[languages.en]
title = "Title in English"
languageName = "English"
label = "English"
weight = 2
[languages.en.params]
p1 = "p1en"
+2 -2
View File
@@ -31,13 +31,13 @@ staticDir2 = ["staticen"]
baseURL = "https://example.com/docs"
weight = 10
title = "In English"
languageName = "English"
label = "English"
[languages.fr]
staticDir2 = ["staticfr"]
baseURL = "https://example.fr"
weight = 20
title = "Le Français"
languageName = "Français"
label = "Français"
-- assets/css/main.css --
body { color: red; }
-- content/mysect/mybundle/index.md --
+16 -8
View File
@@ -72,11 +72,13 @@ weight = 100
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'en'
[module.mounts.sites.matrix]
languages = 'en'
[[module.mounts]]
source = 'content/nn'
target = 'content'
lang = 'nn'
[module.mounts.sites.matrix]
languages = 'nn'
[[module.mounts]]
source = 'content/all'
target = 'content'
@@ -611,11 +613,13 @@ target = "data"
[[module.mounts]]
source = "content/en"
target = "content"
lang = "en"
[module.mounts.sites.matrix]
languages = "en"
[[module.mounts]]
source = "content/nn"
target = "content"
lang = "nn"
[module.mounts.sites.matrix]
languages = "nn"
[[module.imports]]
path = "mytheme"
-- layouts/home.html --
@@ -775,19 +779,23 @@ defaultContentLanguage = 'en'
[[module.mounts]]
source = 'content/en'
target = 'content/en'
lang = 'en'
[module.mounts.sites.matrix]
languages = 'en'
[[module.mounts]]
source = 'content/nn'
target = 'content/nn'
lang = 'nn'
[module.mounts.sites.matrix]
languages = 'nn'
[[module.mounts]]
source = 'content/no'
target = 'content/no'
lang = 'no'
[module.mounts.sites.matrix]
languages = 'no'
[[module.mounts]]
source = 'content/sv'
target = 'content/sv'
lang = 'sv'
[module.mounts.sites.matrix]
languages = 'sv'
[[module.mounts]]
source = 'layouts'
target = 'layouts'
+34 -5
View File
@@ -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,
+22 -13
View File
@@ -63,7 +63,7 @@ func TestContentMountMerge(t *testing.T) {
files := `
-- hugo.toml --
baseURL = 'https://example.org/'
languageCode = 'en-us'
locale = 'en-us'
title = 'Hugo Forum Topic #37225'
theme = 'mytheme'
@@ -72,44 +72,49 @@ defaultContentLanguage = 'en'
defaultContentLanguageInSubdir = true
[languages.en]
languageName = 'English'
label = 'English'
weight = 1
[languages.de]
languageName = 'Deutsch'
label = 'Deutsch'
weight = 2
[languages.nl]
languageName = 'Nederlands'
label = 'Nederlands'
weight = 3
# EN content
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'en'
[module.mounts.sites.matrix]
languages = 'en'
# DE content
[[module.mounts]]
source = 'content/de'
target = 'content'
lang = 'de'
[module.mounts.sites.matrix]
languages = 'de'
# This fills in the gaps in DE content with EN content
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'de'
[module.mounts.sites.matrix]
languages = 'de'
# NL content
[[module.mounts]]
source = 'content/nl'
target = 'content'
lang = 'nl'
[module.mounts.sites.matrix]
languages = 'nl'
# This should fill in the gaps in NL content with EN content
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'nl'
[module.mounts.sites.matrix]
languages = 'nl'
-- content/de/_index.md --
---
@@ -153,7 +158,8 @@ title: "p3 (nl)"
[[module.mounts]]
source = 'content/nlt'
target = 'content'
lang = 'nl'
[module.mounts.sites.matrix]
languages = 'nl'
-- themes/mytheme/content/nlt/p3.md --
---
title: "p3 theme (nl)"
@@ -255,19 +261,22 @@ weight = 2
[[module.mounts]]
source = "content/en"
target = "content"
lang = "en"
[module.mounts.sites.matrix]
languages = "en"
[[module.mounts]]
source = "content/es"
target = "content"
lang = "es"
[module.mounts.sites.matrix]
languages = "es"
# Populate the missing es content with en content
[[module.mounts]]
source = "content/en"
target = "content"
lang = "es"
[module.mounts.sites.matrix]
languages = "es"
-- layouts/all.html --
{{ .Title }}
-- content/en/p1.md --
+14 -12
View File
@@ -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 {
@@ -628,15 +620,25 @@ func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimp
}
func (ps *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) {
dir, d := ps.GetInternalTemplateBasePathAndDescriptor()
pth, d := ps.GetInternalTemplateBasePathAndDescriptor()
var subPath string
if len(layouts) > 0 {
d.LayoutFromUser = layouts[0]
layout := layouts[0]
if i := strings.LastIndexByte(layout, '/'); i != -1 {
// A layout in a sub path, e.g. "foo/mylayout".
subPath, layout = layout[:i], layout[i+1:]
if layout == "" {
return nil, false, nil
}
}
d.LayoutFromUser = layout
d.LayoutFromUserMustMatch = true
}
q := tplimpl.TemplateQuery{
Path: dir,
Path: pth,
SubPath: subPath,
Category: tplimpl.CategoryLayout,
Sites: ps.s.siteVector,
Desc: d,
+7 -11
View File
@@ -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)
+7 -2
View File
@@ -140,8 +140,8 @@ func (po *pageOutput) Aliases() []string {
a = path.Join(baseDir, a)
if conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") {
a += ".html"
if conf.C.IsUglyURLSection(p.Section()) && !pathHasOutputFormatSuffix(a, f) {
a += f.MediaType.FirstSuffix.FullSuffix
}
aliases[i] = a
@@ -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
+203 -2
View File
@@ -881,12 +881,12 @@ defaultContentLanguage = "en"
[languages]
[languages.en]
title = "Repro"
languageName = "English"
label = "English"
contentDir = "content/en"
[languages.zh_CN]
title = "Repro"
languageName = "简体中文"
label = "简体中文"
contentDir = "content/zh_CN"
[outputFormats]
@@ -2167,3 +2167,204 @@ EF
b.AssertFileContent("public/s4/p9/index.html", "EF")
b.AssertFileContent("public/s4/p10/index.html", "EF")
}
// See issue 15056.
func TestRenderViewSubdir(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/s1/p1.md --
---
title: p1
---
-- layouts/s1/p1/page.html --
{{ .Render "a" }}|{{ .Render "b" }}|{{ .Render "c" }}|{{ .Render "foo/d" }}|{{ .Render "foo/e" }}|{{ .Render "foo/f" }}|{{ .Render "sub/foo/g" }}
-- layouts/s1/p1/a.html --
a{{- /**/ -}}
-- layouts/s1/b.html --
b{{- /**/ -}}
-- layouts/c.html --
c{{- /**/ -}}
-- layouts/s1/p1/foo/d.html --
d{{- /**/ -}}
-- layouts/s1/foo/e.html --
e{{- /**/ -}}
-- layouts/foo/f.html --
f{{- /**/ -}}
-- layouts/sub/foo/g.html --
g{{- /**/ -}}
`
b := Test(t, files)
b.AssertFileContent("public/s1/p1/index.html", "a|b|c|d|e|f|g")
}
// See issue 15056.
func TestRenderViewSubdirRootPage(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/p1.md --
---
title: p1
---
-- layouts/page.html --
{{ .Render "foo/a" }}
-- layouts/foo/a.html --
a{{- /**/ -}}
`
b := Test(t, files)
b.AssertFileContent("public/p1/index.html", "a")
}
// See issue 15056.
func TestRenderViewSubdirNotFound(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/s1/p1.md --
---
title: p1
---
-- layouts/s1/p1/page.html --
{{ .Render "foo/missing" }}
-- layouts/s1/p1/missing.html --
should-not-match{{- /**/ -}}
`
b, err := TestE(t, files)
b.Assert(err, qt.ErrorMatches, `.*template "foo/missing" not found.*`)
}
// See issue 15056.
func TestRenderViewSubdirTrailingSlash(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/s1/p1.md --
---
title: p1
---
-- layouts/s1/p1/page.html --
{{ .Render "foo/" }}
-- layouts/s1/p1/foo.html --
should-not-match{{- /**/ -}}
`
b, err := TestE(t, files)
b.Assert(err, qt.ErrorMatches, `.*template "foo/" not found.*`)
}
// See issue 15056.
func TestRenderViewSubdirCaseInsensitiveArgument(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/s1/p1.md --
---
title: p1
---
-- layouts/s1/p1/page.html --
{{ .Render "Foo/Bar" }}
-- layouts/s1/p1/foo/bar.html --
bar{{- /**/ -}}
`
b := Test(t, files)
b.AssertFileContent("public/s1/p1/index.html", "bar")
}
// See issue 15056.
func TestRenderViewSubdirCaseInsensitivePath(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/s1/p1.md --
---
title: p1
---
-- layouts/s1/p1/page.html --
{{ .Render "foo/bar" }}
-- layouts/s1/p1/Foo/Bar.html --
bar{{- /**/ -}}
`
b := Test(t, files)
b.AssertFileContent("public/s1/p1/index.html", "bar")
}
// See issue 15056.
func TestRenderViewSubdirWithUnderscore(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/p1.md --
---
title: p1
---
-- content/s1/p2.md --
---
title: p2
---
-- content/s1/p3.md --
---
title: p3
---
-- layouts/page.html --
{{ .Title }}: {{ .Render "_views/a" }}
-- layouts/_views/a.html --
layouts/_views/a.html
-- layouts/s1/_views/a.html --
layouts/s1/_views/a.html
-- layouts/s1/p3/_views/a.html --
layouts/s1/p3/_views/a.html
`
b := Test(t, files)
b.AssertFileContent("public/p1/index.html", "p1: layouts/_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)
}
+1 -1
View File
@@ -608,7 +608,7 @@ func TestGetPageMultilingual(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "http://example.org/"
languageCode = "en-us"
locale = "en-us"
defaultContentLanguage = "ru"
title = "My New Hugo Project"
uglyurls = true
+41
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -41,12 +41,12 @@ pagerSize = 1
[languages]
[languages.nn]
languageName = "Nynorsk"
label = "Nynorsk"
weight = 1
title = "Hugo på norsk"
[languages.en]
languageName = "English"
label = "English"
weight = 2
title = "Hugo in English"
-- layouts/single.html --
+2 -1
View File
@@ -451,7 +451,8 @@ func TestClassCollector(t *testing.T) {
files := fmt.Sprintf(`
-- hugo.toml --
minify = %t
[minify]
minifyOutput = %t
[build]
writeStats = true
+3 -3
View File
@@ -59,10 +59,10 @@ defaultContentLanguage = "en"
[languages]
[languages.en]
weight = 1
languageName = "English"
label = "English"
[languages.nn]
weight = 2
languageName = "Nynorsk"
label = "Nynorsk"
-- content/sect/doc1.md --
---
title: doc1
@@ -139,7 +139,7 @@ disableKinds = ["term", "taxonomy"]
[languages]
[languages.en]
weight = 1
languageName = "English"
label = "English"
[languages.nn]
weight = 2
-- layouts/list.xml --
@@ -1570,11 +1570,11 @@ defaultContentRoleInSubDir: true
defaultContentLanguage: en
languages:
en:
languageName: English
label: English
bn:
languageName: Bengali
label: Bengali
fr:
languageName: French
label: French
-- layouts/all.html --
All.
+3 -2
View File
@@ -1,7 +1,8 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.163.3
HUGORELEASER_COMMITISH=4d22555aebf458d5d150500c9ac4bee5b24cf0d3
HUGORELEASER_TAG=v0.164.0
HUGORELEASER_COMMITISH=ce2470e7012b5ab5fc4e10ebe4027e9f8d9e00dc
+3
View File
@@ -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
+13
View File
@@ -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)
+7
View File
@@ -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{}
+13 -4
View File
@@ -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);
+3
View File
@@ -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
View File
@@ -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.
+3
View File
@@ -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{}
+5 -5
View File
@@ -167,14 +167,14 @@ b = 'b translated'
b.Assert(err.Error(), qt.Contains, "failed to load translations: reserved keys [description] mixed with unreserved keys [a b]: see the lang.Translate documentation for a list of reserved keys")
}
func TestI18nUseLanguageCodeWhenBothTranslationFilesArePresent(t *testing.T) {
func TestI18nUseLocaleWhenBothTranslationFilesArePresent(t *testing.T) {
t.Parallel()
filesTemplate := `
-- hugo.yaml --
languages:
en:
languageCode: en-us
locale: en-us
-- i18n/en.yml --
hello: Greetings from en!
-- i18n/en-us.yml --
@@ -189,11 +189,11 @@ hello: Greetings from en-us!
}
runTest(filesTemplate)
runTest(strings.ReplaceAll(filesTemplate, "languageCode: en-us", "languageCode: En-US"))
runTest(strings.ReplaceAll(filesTemplate, "locale: en-us", "locale: En-US"))
runTest(strings.ReplaceAll(filesTemplate, "-- i18n/en-us.yml --", "-- i18n/en-US.yml --"))
}
func TestI18nUseLangWhenLanguageCodeFileIsMissing(t *testing.T) {
func TestI18nUseLangWhenLocaleFileIsMissing(t *testing.T) {
t.Parallel()
filesTemplate := `
@@ -202,7 +202,7 @@ languages:
en:
title: English
pt:
languageCode: pt-br
locale: pt-br
-- i18n/en.yml --
hello: Greetings from en!
-- i18n/pt.yml --
+42
View File
@@ -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>`)
}
+2 -2
View File
@@ -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++
}
+171
View File
@@ -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
}
+14 -1
View File
@@ -14,11 +14,14 @@
package markup_config
import (
"fmt"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/rst/rst_config"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/mitchellh/mapstructure"
)
@@ -39,10 +42,19 @@ type Config struct {
// Configuration for the AsciiDoc external markdown engine.
AsciiDocExt asciidocext_config.Config
// Configuration for the reStructuredText external markdown engine.
RST rst_config.Config
}
func (c *Config) Init() error {
return c.Goldmark.Init()
if err := c.Goldmark.Init(); err != nil {
return fmt.Errorf("goldmark: %s", err)
}
if err := c.RST.Init(); err != nil {
return fmt.Errorf("rst: %s", err)
}
return nil
}
func Decode(cfg config.Provider) (conf Config, err error) {
@@ -119,4 +131,5 @@ var Default = Config{
Goldmark: goldmark_config.Default,
AsciiDocExt: asciidocext_config.Default,
RST: rst_config.Default,
}
+20 -2
View File
@@ -17,6 +17,7 @@ package rst
import (
"bytes"
"fmt"
"os/exec"
"runtime"
"github.com/gohugoio/hugo/common/hexec"
@@ -71,6 +72,11 @@ func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext)
logger.Infoln("Rendering", ctx.DocumentName, "with", binaryName, "...")
args := []string{"--leave-comments", "--initial-header-level=2"}
if c.cfg.Conf != nil && c.cfg.MarkupConfig().RST.SyntaxHighlight != "long" {
args = append(args, "--syntax-highlight="+c.cfg.MarkupConfig().RST.SyntaxHighlight)
}
var result []byte
var err error
@@ -80,10 +86,9 @@ func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext)
// handle Windows manually because it doesn't do shebangs
if runtime.GOOS == "windows" {
pythonBinary, _ := internal.GetPythonBinaryAndExecPath()
args := []string{binaryPath, "--leave-comments", "--initial-header-level=2"}
args = append([]string{binaryPath}, args...)
result, err = internal.ExternallyRenderContent(c.cfg, ctx, src, pythonBinary, args)
} else {
args := []string{"--leave-comments", "--initial-header-level=2"}
result, err = internal.ExternallyRenderContent(c.cfg, ctx, src, binaryName, args)
}
@@ -128,3 +133,16 @@ func Supports() bool {
}
return hasBin
}
// SupportsPygments returns whether Pygments is (or should be) installed on this computer.
func SupportsPygments() bool {
for _, python := range []string{"python3", "python"} {
if err := exec.Command(python, "-c", "import pygments").Run(); err == nil {
return true
}
}
if htesting.SupportsAll() {
panic("Pygments not installed")
}
return false
}
+35
View File
@@ -0,0 +1,35 @@
// 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 rst_config holds reStructuredText related configuration.
package rst_config
import "fmt"
// Default holds Hugo's default reStructuredText configuration.
var Default = Config{
SyntaxHighlight: "long",
}
// Config configures reStructuredText.
type Config struct {
// Configures Pygments syntax highlighting. Valid values are "short", "long" (default) and "none".
SyntaxHighlight string
}
func (c *Config) Init() error {
if c.SyntaxHighlight != "short" && c.SyntaxHighlight != "long" && c.SyntaxHighlight != "none" {
return fmt.Errorf("invalid value for syntaxHighlight: %q", c.SyntaxHighlight)
}
return nil
}
+105
View File
@@ -0,0 +1,105 @@
// 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 rst_test
import (
"runtime"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/markup/rst"
)
// --syntax-highlight, default = long.
func TestRSTSyntaxHighlight(t *testing.T) {
if !rst.Supports() {
t.Skip("rst not installed")
}
if !rst.SupportsPygments() {
t.Skip("Pygments not installed")
}
execAllow := `'^rst2html(\.py)?$'`
if runtime.GOOS == "windows" {
execAllow = `'^python(\.exe)?$'`
}
filesTemplate := strings.ReplaceAll(`
-- hugo.toml --
baseURL = "https://example.org"
disableKinds = ["home", "section", "taxonomy", "term", "rss", "sitemap"]
markup.rst.syntaxHighlight = 'SYNTAX_HIGHLIGHT'
[security.exec]
allow = [RST_EXEC_ALLOW]
-- layouts/page.html --
{{ .Content }}
-- content/p.rst --
---
title: p
---
.. code:: go
if true {}
`, "RST_EXEC_ALLOW", execAllow)
files := strings.ReplaceAll(filesTemplate, "SYNTAX_HIGHLIGHT", "none")
b := hugolib.Test(t, files)
b.AssertFileContent("public/p/index.html", `
! Pygments
`)
b.AssertFileContent("public/p/index.html", `
if true {}
! if</span>
`)
files = strings.ReplaceAll(filesTemplate, "SYNTAX_HIGHLIGHT", "short")
b = hugolib.Test(t, files)
b.AssertFileContent("public/p/index.html", `
<span class="k">if</span>
! <span class="keyword">if</span>
`)
files = strings.ReplaceAll(filesTemplate, "SYNTAX_HIGHLIGHT", "long")
b = hugolib.Test(t, files)
b.AssertFileContent("public/p/index.html", `
! <span class="k">if</span>
<span class="keyword">if</span>
`)
// Default long.
files = strings.ReplaceAll(filesTemplate, "markup.rst.syntaxHighlight = 'SYNTAX_HIGHLIGHT'", "")
b = hugolib.Test(t, files)
b.AssertFileContent("public/p/index.html", `
! <span class="k">if</span>
<span class="keyword">if</span>
`)
// Invalid.
files = strings.ReplaceAll(filesTemplate, "SYNTAX_HIGHLIGHT", "foo")
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "rst: invalid value for syntaxHighlight: \"foo\"")
}
+11 -2
View File
@@ -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"}},
+4 -1
View File
@@ -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)
}
+50
View File
@@ -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
}
+3 -6
View File
@@ -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 {
+3 -1
View File
@@ -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).
+5
View File
@@ -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)
+6
View File
@@ -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)
}
-6
View File
@@ -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()
+3 -3
View File
@@ -29,13 +29,13 @@ disableKinds = ['page','rss','section','sitemap','taxonomy','term']
defaultContentLanguage = 'de'
defaultContentLanguageInSubdir = true
[languages.en]
languageName = 'English'
label = 'English'
weight = 1
[languages.de]
languageName = 'Deutsch'
label = 'Deutsch'
weight = 2
-- layouts/home.html --
{{ hugo.Sites.Default.Language.LanguageName }}
{{ hugo.Sites.Default.Language.Label }}
`
b := hugolib.Test(t, files)
+1 -1
View File
@@ -60,7 +60,7 @@ func newTestPageWithFile(filename string) *testPage {
"en",
"UTC",
langs.LanguageConfig{
LanguageName: "English",
Label: "English",
},
loggers.NewDefault(),
)
+3
View File
@@ -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
+59 -43
View File
@@ -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()
},
+1 -1
View File
@@ -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)

Some files were not shown because too many files have changed in this diff Show More