Compare commits

...

60 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen bcde806340 hugolib: Make .Render take an optional context argument
The signature is now PAGE.Render VIEW [CONTEXT], mirroring the partial API:
The first argument is always the view name, and if CONTEXT is given, it
replaces the Page as the template's data context.

Closes #15077

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 17:00:57 +02:00
Bjørn Erik Pedersen df4ac34856 modules/npm: Make the generated workspace package private with a stable name
The autogenerated packages/hugoautogen/package.json got its name from the
project directory basename and had no "private" field, making it publishable
under checkout-dependent names. Use the stable default name "hugoautogen",
set "private": true, and preserve hand-set name/version/private on
regeneration.

Fixes #15245

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 16:02:56 +02:00
Shantanav Mukherjee 166d3ee1ce common/hstore: Keep read lock while sorting map values
Fixes #15237
2026-08-26 19:34:17 +02:00
Shiwang Bisht a25af7facf metrics: Improve template metrics duration formatting
Fixes #15027
2026-08-24 11:36:58 +02:00
Bjørn Erik Pedersen 723579ff54 Remove comments from rebuild test functions
Removed commented-out explanations for test functions related to mounted assets and their behavior during rebuilds.
2026-08-24 11:32:24 +02:00
Bjørn Erik Pedersen 85ad5e48eb hugolib: Add some fast render mode integration tests 2026-08-24 11:32:24 +02:00
Bjørn Erik Pedersen 7b5199fdef all: Run modernize -fix ./... 2026-08-20 21:56:40 +02:00
Bjørn Erik Pedersen e31ff547d2 Upgrade to Go 1.27
Closes #15228
2026-08-20 20:23:18 +02:00
Joe Mooring 87260e4a60 commands: Fix lang flag description in config command
Closes #15223
2026-08-19 20:00:06 +02:00
Bjørn Erik Pedersen 8405b802cf tpl: Improve the return keyword in templates
Replace the partial return template rewriting with a sentinel error
trapped in the template executor:

* return now works in any template, not just partials.
* return can be used anywhere in the template, e.g. inside if/range;
  it stops execution of the current template, so a bare return in a
  block or template include ends just that template.
* {{ return <value> }} sets the return value of the enclosing partial;
  using it outside a partial is now an error (it was silently ignored).

The fork changes are limited to hugo_template.go plus one mechanical
rename (walkTemplate -> walkTemplateOld) mirrored in the fork script.

Closes #15212

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 16:21:11 +02:00
Shiwang0-0 bf05832d14 page: Add IndexOf method to Pages
See #13589
2026-08-19 15:47:58 +02:00
Bjørn Erik Pedersen a05736cb9f tpl/resources: Add resources.Publish
Closes #15208
2026-08-19 13:13:55 +02:00
Bjørn Erik Pedersen 49dceb19f5 hugolib: Fix slice bounds panic when deleting multiple nodes at same path
The contentNodes cases in Delete/DeleteFunc spliced the slice inside a
forward range loop, panicking when a second deletion hit the last index,
and the shrunken slice was never written back to the tree. Let the
Shifter return the updated node and re-insert it on partial deletes.

Fixes #15207

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 20:42:57 +02:00
Joe Mooring 5e7099256e hugolib: Fix ReadingTime and FuzzyWordCount calculations
Closes #15206
2026-08-15 20:41:03 +02:00
hugoreleaser 0805c734a4 releaser: Prepare repository for 0.166.0-DEV
[ci skip]
2026-08-12 14:47:36 +00:00
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
181 changed files with 3723 additions and 1291 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.22700.20000
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.22700.20000
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
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.26.x]
go-version: [1.27.x]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -51,6 +51,7 @@ jobs:
run: |
gem install asciidoctor -v "2.0.26"
gem install asciidoctor-diagram -v "3.1.0"
gem install asciidoctor-html5s -v "0.5.1"
- name: Install GoAT
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
- name: Install Python
+2 -1
View File
@@ -5,4 +5,5 @@ dist/
public/
.DS_Store
cache/filecache/_gen/
.claude/
.claude/
dump.txt
+2 -2
View File
@@ -2,8 +2,8 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.26"
ARG ALPINE_VERSION="3.22"
ARG GO_VERSION="1.27"
ARG ALPINE_VERSION="3.24"
ARG DART_SASS_VERSION="1.79.3"
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
+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"
}
+1 -1
View File
@@ -116,7 +116,7 @@ func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
cmd.Long = `Display project configuration, both default and custom settings.`
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for (default is the default content language)")
cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`)
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
applyLocalFlagsBuildConfig(cmd, c.r)
+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.
+3 -3
View File
@@ -81,9 +81,9 @@ func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) {
items = append(items, s.items[i])
for i, v := range slices.Backward(s.items) {
if predicate(v) {
items = append(items, v)
s.items = slices.Delete(s.items, i, i+1)
}
}
+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
+11
View File
@@ -178,3 +178,14 @@ func improveIfNilPointerMsg(inErr error) string {
s := fmt.Sprintf(" %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s)
}
// Or returns the first non-nil error from the given list of errors.
// If all errors are nil, it returns nil.
func Or(errs ...error) error {
for _, err := range errs {
if err != nil {
return err
}
}
return nil
}
+1 -2
View File
@@ -131,14 +131,13 @@ func (c *Scratch) DeleteInMap(key string, mapKey string) string {
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
func (c *Scratch) GetSortedMapValues(key string) any {
c.mu.RLock()
defer c.mu.RUnlock()
if c.values[key] == nil {
c.mu.RUnlock()
return nil
}
unsortedMap := c.values[key].(map[string]any)
c.mu.RUnlock()
var keys []string
for mapKey := range unsortedMap {
keys = append(keys, mapKey)
+24
View File
@@ -207,6 +207,30 @@ func TestScratchGetSortedMapValues(t *testing.T) {
}
}
func TestScratchGetSortedMapValuesConcurrentWithSetInMap(t *testing.T) {
t.Parallel()
scratch := NewScratch()
scratch.SetInMap("key", "initial", "initial")
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := range 1000 {
scratch.SetInMap("key", "value", i)
}
}()
go func() {
defer wg.Done()
for range 1000 {
scratch.GetSortedMapValues("key")
}
}()
wg.Wait()
}
func BenchmarkScratchGet(b *testing.B) {
scratch := NewScratch()
scratch.Add("A", 1)
+3
View File
@@ -82,6 +82,9 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool
return err
}
} else {
if shouldCopy != nil && !shouldCopy(fromFilename) {
continue
}
if err := CopyFile(fs, fromFilename, toFilename); err != nil {
return 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: 166,
PatchLevel: 0,
Suffix: "",
Suffix: "-DEV",
}
+23 -52
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.",
@@ -1035,29 +1005,30 @@ func (c Configs) GetByLang(lang string) config.AllProvider {
func newDefaultConfig() *Config {
return &Config{
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
RootConfig: RootConfig{
Environment: hugo.EnvironmentProduction,
TitleCaseStyle: "AP",
PluralizeListTitles: true,
CapitalizeListTitles: true,
StaticDir: []string{"static"},
SummaryLength: 70,
Timeout: "60s",
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
Environment: hugo.EnvironmentProduction,
TitleCaseStyle: "AP",
PluralizeListTitles: true,
CapitalizeListTitles: true,
StaticDir: []string{"static"},
SummaryLength: 70,
Timeout: "60s",
CommonDirs: config.CommonDirs{
ArcheTypeDir: "archetypes",
ContentDir: "content",
ResourceDir: "resources",
PublishDir: "public",
ThemesDir: "themes",
AssetDir: "assets",
LayoutDir: "layouts",
I18nDir: "i18n",
DataDir: "data",
},
},
//lint:ignore SA1019 Keep as adapter for now.
ArcheTypeDir: "archetypes",
ContentDir: "content",
ResourceDir: "resources",
PublishDir: "public",
ThemesDir: "themes",
//lint:ignore SA1019 Keep as adapter for now.
AssetDir: "assets",
//lint:ignore SA1019 Keep as adapter for now.
LayoutDir: "layouts",
//lint:ignore SA1019 Keep as adapter for now.
I18nDir: "i18n",
//lint:ignore SA1019 Keep as adapter for now.
DataDir: "data",
}
}
-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.
+1 -1
View File
@@ -317,7 +317,7 @@ func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
b, err := afero.ReadFile(fs, filename)
if err != nil {
// Print some debug info
root := strings.Split(filename, helpers.FilePathSeparator)[0]
root, _, _ := strings.Cut(filename, helpers.FilePathSeparator)
afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error {
if info != nil && !info.IsDir() {
fmt.Println(" ", path)
+2 -2
View File
@@ -459,7 +459,7 @@ func (c TestConfig) IsZero() bool {
// BuildState are state used during a build.
type BuildState struct {
counter uint64
counter atomic.Uint64
// Tracks invocations of the Build method.
BuildCounter atomic.Uint64
@@ -538,5 +538,5 @@ func (b *BuildState) GetFilenamesWithPostPrefix() []string {
}
func (b *BuildState) Incr() int {
return int(atomic.AddUint64(&b.counter, uint64(1)))
return int(b.counter.Add(uint64(1)))
}
+30 -30
View File
@@ -15,7 +15,7 @@ require (
github.com/bep/golocales v0.2.0
github.com/bep/goportabletext v0.2.0
github.com/bep/helpers v0.12.0
github.com/bep/imagemeta v0.17.2
github.com/bep/imagemeta v1.0.0
github.com/bep/lazycache v0.8.1
github.com/bep/logg v0.4.0
github.com/bep/mclib v1.20401.20400
@@ -27,19 +27,19 @@ require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/clbanning/mxj/v2 v2.7.0
github.com/dustin/go-humanize v1.0.1
github.com/evanw/esbuild v0.28.1
github.com/evanw/esbuild v0.28.2
github.com/fatih/color v1.18.0
github.com/fortytw2/leaktest v1.3.0
github.com/frankban/quicktest v1.14.6
github.com/fsnotify/fsnotify v1.9.0
github.com/getkin/kin-openapi v0.140.0
github.com/fsnotify/fsnotify v1.10.1
github.com/getkin/kin-openapi v0.146.0
github.com/gobuffalo/flect v1.0.3
github.com/gobwas/glob v0.2.3
github.com/goccy/go-yaml v1.19.2
github.com/gohugoio/gift v0.2.0
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6
github.com/gohugoio/go-radix v1.2.0
github.com/gohugoio/hashstructure v0.6.0
github.com/gohugoio/hashstructure v1.0.0
github.com/gohugoio/httpcache v0.8.0
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0
@@ -47,11 +47,11 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hairyhenderson/go-codeowners v0.7.0
github.com/jdkato/prose v1.2.1
github.com/kyokomi/emoji/v2 v2.2.13
github.com/kyokomi/emoji/v2 v2.2.14
github.com/magefile/mage v1.17.2
github.com/makeworld-the-better-one/dither/v2 v2.4.0
github.com/marekm4/color-extractor v1.2.1
github.com/mattn/go-isatty v0.0.22
github.com/mattn/go-isatty v0.0.24
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
github.com/muesli/smartcrop v0.3.0
@@ -60,25 +60,25 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pelletier/go-toml/v2 v2.4.3
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/rogpeppe/go-internal v1.15.0
github.com/rogpeppe/go-internal v1.16.0
github.com/spf13/afero v1.15.0
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
github.com/spf13/fsync v0.10.1
github.com/spf13/pflag v1.0.10
github.com/tdewolff/minify/v2 v2.24.13
github.com/tdewolff/parse/v2 v2.8.12
github.com/tdewolff/minify/v2 v2.24.16
github.com/tdewolff/parse/v2 v2.8.15
github.com/tetratelabs/wazero v1.12.0
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark v1.8.5
github.com/yuin/goldmark-emoji v1.0.6
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.45.0
golang.org/x/image v0.43.0
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
golang.org/x/text v0.38.0
golang.org/x/tools v0.47.0
golang.org/x/image v0.44.0
golang.org/x/mod v0.38.0
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
golang.org/x/text v0.40.0
golang.org/x/tools v0.48.0
google.golang.org/api v0.276.0
rsc.io/qr v0.2.0
)
@@ -99,7 +99,7 @@ require (
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/JohannesKaufmann/dom v0.3.1 // indirect
@@ -125,12 +125,12 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -152,8 +152,8 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/oasdiff/yaml v0.1.0 // indirect
github.com/oasdiff/yaml3 v0.0.13 // indirect
github.com/oasdiff/yaml v0.1.1 // indirect
github.com/oasdiff/yaml3 v0.0.14 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect
@@ -163,7 +163,7 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
@@ -172,18 +172,18 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
howett.net/plist v1.0.1 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)
go 1.26.0
go 1.27.0
+58 -59
View File
@@ -84,8 +84,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
@@ -164,8 +164,8 @@ github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Y
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
github.com/bep/imagemeta v0.17.2 h1:fDyXM1eAqCfBeqGLqS6UsN4OfuLM0cdu70KuLCehjOg=
github.com/bep/imagemeta v0.17.2/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
github.com/bep/imagemeta v1.0.0 h1:8JZmahegn5p1M9CpFmga7LoSxErpu1uFfpSzMUNju7A=
github.com/bep/imagemeta v1.0.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg=
github.com/bep/lazycache v0.8.1/go.mod h1:pbEiFsZoq7cLXvrTll0AHOPEurB1aGGxx4jKjOtlx9w=
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
@@ -198,8 +198,8 @@ github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -220,15 +220,15 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc=
github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/evanw/esbuild v0.28.2 h1:A2uETn4jrQTcXaT/shwTDTYBxDjl7fV7nXmUrJxfA2w=
github.com/evanw/esbuild v0.28.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -238,10 +238,10 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g=
github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -272,8 +272,8 @@ github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eR
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg=
github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ=
github.com/gohugoio/hashstructure v1.0.0 h1:vWYuyzs1n0LdI0F54TJQeYAiB44fHX7H9hCp9X6gHKg=
github.com/gohugoio/hashstructure v1.0.0/go.mod h1:FSbTK4QwxucJ2bC4Lvrs9a6x0DbQDXNoyBO+h4nlCgE=
github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU=
github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus=
@@ -392,8 +392,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
@@ -402,8 +402,8 @@ github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mr
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
@@ -418,10 +418,10 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg=
github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0=
github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg=
github.com/oasdiff/yaml3 v0.0.13/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
@@ -450,8 +450,8 @@ github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
@@ -489,11 +489,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58=
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0=
github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg=
github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tdewolff/minify/v2 v2.24.16 h1:1d+IHElvmYM2zJtuykMK/esiqc4H/7pqPKAa7jpSUUU=
github.com/tdewolff/minify/v2 v2.24.16/go.mod h1:G6yep6HFo3u2eoOcIVBKSNwpoW2wXvPjvnFRj3w1zlc=
github.com/tdewolff/parse/v2 v2.8.15 h1:3/Psth16LHpsy2NS/7ShXDsoWt1x0f3tQMSbmQi+jO8=
github.com/tdewolff/parse/v2 v2.8.15/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
@@ -502,8 +501,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -514,8 +513,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
@@ -545,8 +544,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -559,8 +558,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -582,8 +581,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -615,8 +614,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -638,8 +637,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -676,8 +675,8 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -685,8 +684,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -739,8 +738,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -815,10 +814,10 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -835,8 +834,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+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() {
+9 -6
View File
@@ -14,7 +14,6 @@
package hugolib
import (
"cmp"
"context"
"fmt"
"path"
@@ -23,6 +22,7 @@ import (
"github.com/bep/helpers/maphelpers"
"github.com/gohugoio/go-radix"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
@@ -134,7 +134,7 @@ func (a *allPagesAssembler) createAllPages() error {
}()
}
if err := cmp.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
if err := herrors.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
return err
}
if err := a.pwRoot.WalkContext.HandleEventsAndHooks(); err != nil {
@@ -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
}
+28 -24
View File
@@ -25,7 +25,7 @@ type contentNodeShifter struct {
conf config.AllProvider // Used for logging/debugging.
}
func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (contentNode, bool, bool) {
func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (contentNode, contentNode, bool, bool) {
switch v := n.(type) {
case contentNodesMap:
deleted, wasDeleted := v[vec]
@@ -33,49 +33,53 @@ func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (cont
delete(v, vec)
resource.MarkStale(deleted)
}
return deleted, wasDeleted, len(v) == 0
return v, deleted, wasDeleted, len(v) == 0
case contentNodeForSite:
if v.siteVector() != vec {
return nil, false, false
return v, nil, false, false
}
resource.MarkStale(v)
return v, true, true
return nil, v, true, true
case contentNodes:
var deleted contentNodes
for i, nn := range v {
if vv, ok, _ := s.Delete(nn, vec); ok {
var deleted, remaining contentNodes
for _, nn := range v {
updated, vv, ok, isEmpty := s.Delete(nn, vec)
if ok {
deleted = append(deleted, vv)
v = append(v[:i], v[i+1:]...)
}
if !isEmpty {
remaining = append(remaining, updated)
}
}
if len(deleted) == 0 {
return nil, false, false
return v, nil, false, false
}
return deleted, true, len(v) == 0
return remaining, deleted, true, len(remaining) == 0
default:
v = v.(contentNodeSingle) // Ensure single node.
resource.MarkStale(v)
return v, true, true
vv := v.(contentNodeSingle) // Ensure single node.
resource.MarkStale(vv)
return nil, vv, true, true
}
}
func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) bool) bool {
func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) bool) (contentNode, bool) {
switch ss := v.(type) {
case contentNodeSingle:
if f(ss) {
resource.MarkStale(ss)
return true
return nil, true
}
return false
return ss, false
case contentNodes:
for i, n := range ss {
var remaining contentNodes
for _, n := range ss {
if f(n) {
resource.MarkStale(n)
ss = append(ss[:i], ss[i+1:]...)
} else {
remaining = append(remaining, n)
}
}
return len(ss) == 0
return remaining, len(remaining) == 0
case contentNodesMap:
for k, n := range ss {
if f(n) {
@@ -83,7 +87,7 @@ func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) boo
delete(ss, k)
}
}
return len(ss) == 0
return ss, len(ss) == 0
default:
panic(fmt.Sprintf("DeleteFunc: unknown type %T", v))
}
@@ -105,7 +109,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 +174,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 +184,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
}
@@ -0,0 +1,79 @@
// 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 hugolib
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
)
type testContentNodeForSite struct {
vec sitesmatrix.Vector
}
func (n *testContentNodeForSite) Path() string { return "/test" }
func (n *testContentNodeForSite) nodeCategorySingle() {}
func (n *testContentNodeForSite) siteVector() sitesmatrix.Vector { return n.vec }
func (n *testContentNodeForSite) forEeachContentNode(f func(sitesmatrix.Vector, contentNode) bool) bool {
return f(n.vec, n)
}
// See issue 15207.
func TestContentNodeShifterDeleteMultipleFromNodes(t *testing.T) {
c := qt.New(t)
s := &contentNodeShifter{}
vec := sitesmatrix.Vector{2, 0, 0}
other := sitesmatrix.Vector{1, 0, 0}
newNodes := func() contentNodes {
return contentNodes{
&testContentNodeForSite{vec: other},
&testContentNodeForSite{vec: vec},
&testContentNodeForSite{vec: other},
&testContentNodeForSite{vec: other},
&testContentNodeForSite{vec: vec},
}
}
updated, deleted, wasDeleted, isEmpty := s.Delete(newNodes(), vec)
c.Assert(wasDeleted, qt.IsTrue)
c.Assert(isEmpty, qt.IsFalse)
c.Assert(deleted.(contentNodes), qt.HasLen, 2)
remaining := updated.(contentNodes)
c.Assert(remaining, qt.HasLen, 3)
for _, n := range remaining {
c.Assert(n.(contentNodeForSite).siteVector(), qt.Equals, other)
}
// Delete all.
updated, deleted, wasDeleted, isEmpty = s.Delete(contentNodes{
&testContentNodeForSite{vec: vec},
&testContentNodeForSite{vec: vec},
}, vec)
c.Assert(wasDeleted, qt.IsTrue)
c.Assert(isEmpty, qt.IsTrue)
c.Assert(deleted.(contentNodes), qt.HasLen, 2)
c.Assert(updated, qt.IsNil)
// Delete none.
nodes := contentNodes{&testContentNodeForSite{vec: other}}
updated, deleted, wasDeleted, isEmpty = s.Delete(nodes, vec)
c.Assert(wasDeleted, qt.IsFalse)
c.Assert(isEmpty, qt.IsFalse)
c.Assert(deleted, qt.IsNil)
c.Assert(updated.(contentNodes), qt.HasLen, 1)
}
+7 -4
View File
@@ -258,12 +258,15 @@ func (s *testShifter) Insert(old, new *testValue) (*testValue, *testValue, bool)
return new, old, true
}
func (s *testShifter) Delete(n *testValue, dimension sitesmatrix.Vector) (*testValue, bool, bool) {
return nil, true, true
func (s *testShifter) Delete(n *testValue, dimension sitesmatrix.Vector) (*testValue, *testValue, bool, bool) {
return nil, n, true, true
}
func (s *testShifter) DeleteFunc(v *testValue, f func(*testValue) bool) bool {
return f(v)
func (s *testShifter) DeleteFunc(v *testValue, f func(*testValue) bool) (*testValue, bool) {
if f(v) {
return nil, true
}
return v, false
}
func (s *testShifter) Shift(n *testValue, dimension sitesmatrix.Vector, fallback bool) (v *testValue, ok bool) {
+17 -8
View File
@@ -49,12 +49,14 @@ type (
// and a bool indicating if an existing record is updated.
Insert(old, new T) (T, T, bool)
// Delete deletes T from the given dimension and returns the deleted T and whether the dimension was deleted and if it's empty after the delete.
Delete(v T, dimension sitesmatrix.Vector) (T, bool, bool)
// Delete deletes T from the given dimension.
// It returns the updated T, the deleted T, whether anything was deleted
// and whether T is empty after the delete.
Delete(v T, dimension sitesmatrix.Vector) (T, T, bool, bool)
// DeleteFunc deletes nodes in v from the tree where the given function returns true.
// It returns true if it's empty after the delete.
DeleteFunc(v T, f func(n T) bool) bool
// DeleteFunc deletes nodes in v where the given function returns true.
// It returns the updated T and whether it's empty after the delete.
DeleteFunc(v T, f func(n T) bool) (T, bool)
// Shift shifts v into the given dimension,
// if fallback is true, it will fall back a fallback match if found.
@@ -118,7 +120,7 @@ func (r *NodeShiftTree[T]) DeleteFuncRaw(key string, f func(T) bool) (T, int) {
return lastDeleted, count
}
isEmpty := r.shifter.DeleteFunc(v, func(n T) bool {
updated, isEmpty := r.shifter.DeleteFunc(v, func(n T) bool {
if f(n) {
count++
lastDeleted = n
@@ -129,6 +131,8 @@ func (r *NodeShiftTree[T]) DeleteFuncRaw(key string, f func(T) bool) (T, int) {
if isEmpty {
r.tree.Delete(key)
} else if count > 0 {
r.tree.Insert(key, updated)
}
return lastDeleted, count
@@ -161,10 +165,15 @@ func (r *NodeShiftTree[T]) delete(key string) (T, bool) {
var wasDeleted bool
var deleted T
if v, ok := r.tree.Get(key); ok {
var isEmpty bool
deleted, wasDeleted, isEmpty = r.shifter.Delete(v, r.siteVector)
var (
updated T
isEmpty bool
)
updated, deleted, wasDeleted, isEmpty = r.shifter.Delete(v, r.siteVector)
if isEmpty {
r.tree.Delete(key)
} else if wasDeleted {
r.tree.Insert(key, updated)
}
}
return deleted, wasDeleted
+3 -2
View File
@@ -16,6 +16,7 @@ package doctree
import (
"fmt"
"iter"
"slices"
"strings"
"sync"
@@ -251,8 +252,8 @@ func (ctx *WalkContext[T]) HandleEvents() error {
// Loop the event handlers in reverse order so
// that events created by the handlers themselves will
// be picked up further up the tree.
for i := len(ctx.eventHandlers[event.Name]) - 1; i >= 0; i-- {
ctx.eventHandlers[event.Name][i](event)
for _, v := range slices.Backward(ctx.eventHandlers[event.Name]) {
v(event)
if event.stopPropagation {
break
}
+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
+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()))
}
+42 -24
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)
@@ -905,8 +924,9 @@ func (s *IntegrationTestBuilder) initBuilder() error {
if s.Cfg.Running {
flags.Set("internal", hmaps.Params{
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
"fastRenderMode": s.Cfg.FastRenderMode,
})
} else if s.Cfg.Watching {
flags.Set("internal", hmaps.Params{
@@ -1043,6 +1063,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 +1112,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,
@@ -1150,23 +1180,7 @@ func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filen
t.Helper()
filename = filepath.Clean(filename)
b, err := afero.ReadFile(fs, filename)
if err != nil {
// Print some debug info
hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator)
start := 0
if hadSlash {
start = 1
}
end := start + 1
parts := strings.Split(filename, helpers.FilePathSeparator)
if parts[start] == "work" {
end++
}
s.Assert(err, qt.IsNil)
}
s.Assert(err, qt.IsNil)
return string(b)
}
@@ -1203,6 +1217,10 @@ type IntegrationTestConfig struct {
// Whether to simulate server mode.
Running bool
// Whether to simulate the server's fast render mode.
// Only used when Running is set.
FastRenderMode bool
// Watch for changes.
// This is (currently) always set to true when Running is set.
// Note that the CLI for the server does allow for --watch=false, but that is not used in these test.
+13 -29
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 {
@@ -340,10 +332,8 @@ func (ps *pageState) RegularPagesRecursive() page.Pages {
case kinds.KindSection, kinds.KindHome:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
Recursive: true,
},
)
@@ -362,10 +352,8 @@ func (ps *pageState) RegularPages() page.Pages {
case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
)
case kinds.KindTerm:
@@ -387,13 +375,11 @@ func (ps *pageState) Pages() page.Pages {
case kinds.KindSection, kinds.KindHome:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
KeyPart: "page-section",
Include: pagePredicates.ShouldListLocal.And(
pagePredicates.KindPage.Or(pagePredicates.KindSection),
).BoolFunc(),
},
Path: ps.Path(),
KeyPart: "page-section",
Include: pagePredicates.ShouldListLocal.And(
pagePredicates.KindPage.Or(pagePredicates.KindSection),
).BoolFunc(),
},
)
case kinds.KindTerm:
@@ -405,11 +391,9 @@ func (ps *pageState) Pages() page.Pages {
case kinds.KindTaxonomy:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
KeyPart: "term",
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
},
Path: ps.Path(),
KeyPart: "term",
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
Recursive: true,
},
)
+5 -7
View File
@@ -807,13 +807,13 @@ func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlai
// TODO(bep) is set in a test. Fix that.
if result.fuzzyWordCount == 0 {
result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100
result.fuzzyWordCount = (result.wordCount + 99) / 100 * 100
}
if isCJKLanguage {
result.readingTime = (result.wordCount + 500) / 501
result.readingTime = (result.wordCount + 499) / 500
} else {
result.readingTime = (result.wordCount + 212) / 213
result.readingTime = (result.wordCount + 211) / 212
}
rs.Value = result
@@ -941,10 +941,8 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
if pageparser.HasShortcode(contentToRender) {
ct := contentTableOfContents{
sourceInfo: sourceInfo{
filename: pco.po.p.pathOrTitle() + " (rendered from string)",
source: []byte(contentToRender),
},
filename: pco.po.p.pathOrTitle() + " (rendered from string)",
source: []byte(contentToRender),
}
ct.contentToRender = ct.source
// String contains a shortcode.
+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)
+5
View File
@@ -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
+20 -7
View File
@@ -104,21 +104,34 @@ func (pco *pageContentOutput) Reset() {
pco.renderHooks = &renderHooks{}
}
func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
if len(layout) == 0 {
return "", errors.New("no layout given")
func (pco *pageContentOutput) Render(ctx context.Context, args ...any) (template.HTML, error) {
if len(args) == 0 {
return "", errors.New("no view given")
}
templ, found, err := pco.po.p.resolveTemplate(layout...)
if len(args) > 2 {
return "", errors.New("too many arguments, expected VIEW [CONTEXT]")
}
view, err := cast.ToStringE(args[0])
if err != nil {
return "", fmt.Errorf("failed to convert view argument to string: %w", err)
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
var data any = pco.po.p
if len(args) == 2 {
data = args[1]
}
templ, found, err := pco.po.p.resolveTemplate(view)
if err != nil {
return "", pco.po.p.wrapError(err)
}
if !found {
return "", fmt.Errorf("template %q not found", layout[0])
return "", fmt.Errorf("template %q not found", view)
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, data)
if err != nil {
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
}
+183 -16
View File
@@ -412,7 +412,8 @@ baseURL = "http://example.com/"
p := b.H.Sites[0].RegularPages()[0]
b.Assert(p.Summary(context.Background()), qt.Equals, template.HTML(
"<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>"))
"<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>",
))
cnt := content(p)
b.Assert(cnt, qt.Equals, "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\">\n<p>Many people say so.&#160;<a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>")
@@ -730,7 +731,8 @@ This is **content**.
Summary: {{ .Summary }}|Truncated: {{ .Truncated }}|
Content: {{ .Content }}|
`).AssertFileContent("public/simple/index.html",
`).AssertFileContent(
"public/simple/index.html",
"Summary: <p>This is <strong>summary</strong>.</p>|",
"Truncated: true|",
"Content: <p>This is <strong>summary</strong>.</p>\n<p>This is <strong>content</strong>.</p>|",
@@ -1286,13 +1288,15 @@ AllTranslations: {{ range .AllTranslations }}{{ .Language.Lang }}|{{ end }}|
`
b := Test(t, files)
b.AssertFileContent("public/en/sect/p1/index.html",
b.AssertFileContent(
"public/en/sect/p1/index.html",
"TranslationKey: adfasdf|",
"AllTranslations: en|nn||",
"Translations: nn||",
)
b.AssertFileContent("public/nn/sect/p1/index.html",
b.AssertFileContent(
"public/nn/sect/p1/index.html",
"TranslationKey: adfasdf|",
"Translations: en||",
"AllTranslations: en|nn||",
@@ -1378,12 +1382,14 @@ Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ .Content }}|{{ end }}|
`
b := Test(t, files)
b.AssertFileContent("public/en/sect/mybundle_en/index.html",
b.AssertFileContent(
"public/en/sect/mybundle_en/index.html",
"TranslationKey: adfasdf|",
"Resources: /en/sect/mybundle_en/f1.txt|f1.en|/en/sect/mybundle_en/f2.txt|f2.en||",
)
b.AssertFileContent("public/nn/sect/mybundle_nn/index.html",
b.AssertFileContent(
"public/nn/sect/mybundle_nn/index.html",
"TranslationKey: adfasdf|",
"Title: mybundle nn|TranslationKey: adfasdf|\nResources: /en/sect/mybundle_en/f1.txt|f1.en|/nn/sect/mybundle_nn/f2.nn.txt|f2.nn||",
)
@@ -1497,31 +1503,37 @@ CONTENT:{{ .Content }}
`
b := Test(t, files)
b.AssertFileContent("public/page-md-shortcode/index.html",
b.AssertFileContent(
"public/page-md-shortcode/index.html",
"SUMMARY:<p>This is a a shortcode.</p>:END",
"CONTENT:<p>This is a a shortcode.</p>\n\n<p>Content.</p>\n",
)
b.AssertFileContent("public/page-md-shortcode-same-line/index.html",
b.AssertFileContent(
"public/page-md-shortcode-same-line/index.html",
"SUMMARY:<p>This is a a shortcode</p>:END",
"CONTENT:<p>This is a a shortcode</p>\n\n<p>Same line.</p>\n",
)
b.AssertFileContent("public/page-md-shortcode-same-line-after/index.html",
b.AssertFileContent(
"public/page-md-shortcode-same-line-after/index.html",
"SUMMARY:<p>Summary</p>:END",
"CONTENT:<p>Summary</p>\n\na shortcode",
)
b.AssertFileContent("public/page-org-shortcode/index.html",
b.AssertFileContent(
"public/page-org-shortcode/index.html",
"SUMMARY:<p>\nThis is a a shortcode.\n</p>:END",
"CONTENT:<p>\nThis is a a shortcode.\n</p>\n<p>\nContent.\t\n</p>\n",
)
b.AssertFileContent("public/page-org-variant1/index.html",
b.AssertFileContent(
"public/page-org-variant1/index.html",
"SUMMARY:<p>\nSummary.\n</p>:END",
"CONTENT:<p>\nSummary.\n</p>\n<p>\nContent.\t\n</p>\n",
)
b.AssertFileContent("public/page-md-only-shortcode/index.html",
b.AssertFileContent(
"public/page-md-only-shortcode/index.html",
"SUMMARY:a shortcode:END",
"CONTENT:a shortcode\n\na shortcode\n",
)
@@ -1731,7 +1743,8 @@ c: {{ .Scratch.Get "c" }}
b := Test(t, files)
b.AssertFileContent("public/index.html",
b.AssertFileContent(
"public/index.html",
".Scratch eq .Store: true",
"a: b",
"c: d",
@@ -1900,6 +1913,56 @@ func TestRenderWithoutArgument(t *testing.T) {
b.Assert(err, qt.IsNotNil)
}
// See issue 15077.
func TestRenderWithContext(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- content/p1.md --
---
title: "P1"
---
-- layouts/page.html --
{{ .Render "li" }}|{{ .Render "li" (dict "Title" "Custom") }}
-- layouts/li.html --
Title: {{ .Title }}{{- /**/ -}}
`
b := Test(t, files)
b.AssertFileContent("public/p1/index.html", "Title: P1|Title: Custom")
}
// See issue 15077.
func TestRenderWithContextErrors(t *testing.T) {
t.Parallel()
filesTemplate := `
-- hugo.toml --
-- content/p1.md --
---
title: "P1"
---
-- layouts/li.html --
li
-- layouts/page.html --
RENDER
`
for _, test := range []struct {
render string
message string
}{
{`{{ .Render "li" "foo" "bar" }}`, `(?s).*too many arguments, expected VIEW \[CONTEXT\].*`},
{`{{ .Render (slice "li") }}`, `(?s).*failed to convert view argument to string: unable to cast \[\]string{"li"} of type \[\]string to string.*`},
} {
files := strings.ReplaceAll(filesTemplate, "RENDER", test.render)
b, err := TestE(t, files)
b.Assert(err, qt.ErrorMatches, test.message)
}
}
// Issue #13021
func TestAllStores(t *testing.T) {
t.Parallel()
@@ -1928,7 +1991,8 @@ Site: {{ site.Store.Get "Site" }}|
b := TestRunning(t, files)
b.AssertFileContent("public/index.html",
b.AssertFileContent(
"public/index.html",
`
Shortcode: sh-Home|
Page: p-Home|
@@ -1939,7 +2003,8 @@ Hugo: h-Home|
b.EditFileReplaceAll("content/_index.md", "Home", "Homer").Build()
b.AssertFileContent("public/index.html",
b.AssertFileContent(
"public/index.html",
`
Shortcode: sh-Homer|
Page: p-Homer|
@@ -2149,7 +2214,8 @@ EF
b := Test(t, files)
b.AssertFileContent("public/s1/p1/index.html",
b.AssertFileContent(
"public/s1/p1/index.html",
"ab|ab|ab",
"cD|cD|cD",
"EF|EF|EF",
@@ -2341,3 +2407,104 @@ layouts/s1/p3/_views/a.html
b.AssertFileContent("public/s1/p2/index.html", "p2: layouts/s1/_views/a.html") // fails
b.AssertFileContent("public/s1/p3/index.html", "p3: layouts/s1/p3/_views/a.html") // fails
}
func TestPageConttentWeight(t *testing.T) {
files := `
-- hugo.toml --
-- content/mysection/page1.md --
-- content/myothersection/page2.md --
-- content/myothersection/_index.md --
-- layouts/all.html --
All.
`
b := Test(t, files)
check := func(p page.Page, ok bool) {
cw := p.(contentNodeContentWeightProvider).contentWeight()
b.Assert(ok, qt.Equals, cw > 0)
}
s := b.H.Sites[0]
for _, p := range s.RegularPages() {
check(p, true)
}
check(s.home, false)
mysection, _ := s.GetPage("mysection")
check(mysection, false)
myothersection, _ := s.GetPage("myothersection") // backed by a content file.
check(myothersection, true)
}
// See issue 15206.
func TestReadingTimeAndFuzzyWordCountBoundaries(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["home", "section", "taxonomy", "term", "rss", "sitemap"]
-- content/p99.md --
---
title: p99
---
` + strings.Repeat("word ", 99) + `
-- content/p100.md --
---
title: p100
---
` + strings.Repeat("word ", 100) + `
-- content/p101.md --
---
title: p101
---
` + strings.Repeat("word ", 101) + `
-- content/p211.md --
---
title: p211
---
` + strings.Repeat("word ", 211) + `
-- content/p212.md --
---
title: p212
---
` + strings.Repeat("word ", 212) + `
-- content/p213.md --
---
title: p213
---
` + strings.Repeat("word ", 213) + `
-- content/p499.md --
---
title: p499
isCJKLanguage: true
---
` + strings.Repeat("你", 499) + `
-- content/p500.md --
---
title: p500
isCJKLanguage: true
---
` + strings.Repeat("你", 500) + `
-- content/p501.md --
---
title: p501
isCJKLanguage: true
---
` + strings.Repeat("你", 501) + `
-- layouts/page.html --
{{ .WordCount }}|{{ .FuzzyWordCount }}|{{ .ReadingTime }}
`
b := Test(t, files)
b.AssertFileContent("public/p99/index.html", "99|100|1")
b.AssertFileContent("public/p100/index.html", "100|100|1")
b.AssertFileContent("public/p101/index.html", "101|200|1")
b.AssertFileContent("public/p211/index.html", "211|300|1")
b.AssertFileContent("public/p212/index.html", "212|300|1")
b.AssertFileContent("public/p213/index.html", "213|300|2")
b.AssertFileContent("public/p499/index.html", "499|500|1")
b.AssertFileContent("public/p500/index.html", "500|500|1")
b.AssertFileContent("public/p501/index.html", "501|600|2")
}
+168
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,160 @@ 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|")
}
func TestRebuildEditMountedAssetOnlyRelPermalinkUsed(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
[module]
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "out"
target = "assets/out"
-- out/data.json --
{"version": "v1"}
-- content/_index.md --
---
title: "Home"
---
-- content/mytext.txt --
mytext v1
-- layouts/home.html --
{{ $data := resources.Get "out/data.json" | minify }}
Data: {{ $data.RelPermalink }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Data: /out/data.min.json|")
b.AssertFileContent("public/out/data.min.json", `{"version":"v1"}`)
// Simulate the periodic data refresh.
b.EditFileReplaceAll("out/data.json", "v1", "v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
// An unrelated edit to a home bundle resource must not revert the
// published data to an older version.
b.EditFileReplaceAll("content/mytext.txt", "mytext v1", "mytext v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
// One more refresh round.
b.EditFileReplaceAll("out/data.json", "v2", "v3").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v3"}`)
}
func TestRebuildEditMountedAssetContentUsed(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
[module]
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "out"
target = "assets/out"
-- out/data.json --
{"version": "v1"}
-- content/_index.md --
---
title: "Home"
---
-- content/mytext.txt --
mytext v1
-- layouts/home.html --
{{ $data := resources.Get "out/data.json" | minify }}
Data: {{ $data.RelPermalink }}|{{ $data.Content | safeHTML }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Data: /out/data.min.json|")
b.AssertFileContent("public/out/data.min.json", `{"version":"v1"}`)
b.EditFileReplaceAll("out/data.json", "v1", "v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
b.AssertFileContent("public/index.html", `{"version":"v2"}`)
b.EditFileReplaceAll("content/mytext.txt", "mytext v1", "mytext v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
b.EditFileReplaceAll("out/data.json", "v2", "v3").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v3"}`)
b.AssertFileContent("public/index.html", `{"version":"v3"}`)
}
func TestRebuildFastRenderEditMountedAssetNotRecentlyVisited(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
[module]
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "out"
target = "assets/out"
-- out/data.json --
{"version": "v1"}
-- content/_index.md --
---
title: "Home"
---
-- content/p1.md --
---
title: "P1"
---
-- layouts/home.html --
{{ $data := resources.Get "out/data.json" | minify }}
Data: {{ $data.RelPermalink }}|
-- layouts/single.html --
Single: {{ .Title }}|
`
recentlyVisited := types.NewEvictingQueue[string](20).Add("/p1/")
b := TestRunning(t, files, func(cfg *IntegrationTestConfig) {
cfg.FastRenderMode = true
cfg.BuildCfg = BuildCfg{RecentlyTouched: recentlyVisited}
})
b.AssertFileContent("public/index.html", "Data: /out/data.min.json|")
b.AssertFileContent("public/out/data.min.json", `{"version":"v1"}`)
// Simulate the periodic data refresh.
b.EditFileReplaceAll("out/data.json", "v1", "v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
}
+20 -36
View File
@@ -759,11 +759,9 @@ func (s *Site) Pages() page.Pages {
s.CheckReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.BoolFunc(),
},
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.BoolFunc(),
Recursive: true,
IncludeSelf: true,
},
@@ -776,11 +774,9 @@ func (s *Site) RegularPages() page.Pages {
s.CheckReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage).BoolFunc(),
},
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage).BoolFunc(),
Recursive: true,
},
)
@@ -922,11 +918,9 @@ func (s *Site) prepareInits() {
sections := s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: "",
KeyPart: "sectionorhome",
Include: pagePredicates.KindSection.Or(pagePredicates.KindHome).BoolFunc(),
},
Path: "",
KeyPart: "sectionorhome",
Include: pagePredicates.KindSection.Or(pagePredicates.KindHome).BoolFunc(),
IncludeSelf: true,
Recursive: true,
},
@@ -1257,20 +1251,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
@@ -1470,12 +1458,10 @@ func (s *Site) assembleMenus() (navigation.Menus, error) {
return false, nil
}
me := navigation.MenuEntry{
MenuConfig: navigation.MenuConfig{
Identifier: id,
Name: p.LinkTitle(),
Weight: p.Weight(),
},
Page: p,
Identifier: id,
Name: p.LinkTitle(),
Weight: p.Weight(),
Page: p,
}
navigation.SetPageValues(&me, p)
@@ -1514,9 +1500,7 @@ func (s *Site) assembleMenus() (navigation.Menus, error) {
if !ok {
// if parent does not exist, create one without a URL
flat[twoD{p.MenuName, p.EntryName}] = &navigation.MenuEntry{
MenuConfig: navigation.MenuConfig{
Name: p.EntryName,
},
Name: p.EntryName,
}
}
flat[twoD{p.MenuName, p.EntryName}].Children = childmenu
+8 -13
View File
@@ -14,7 +14,6 @@
package sitesmatrix
import (
"cmp"
"fmt"
"iter"
"maps"
@@ -780,7 +779,7 @@ func (b *IntSetsBuilder) Build() *IntSets {
}
func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
applyFilter := func(what string, values []string, matcher ConfiguredDimension) (*hmaps.OrderedIntSet, error) {
applyFilter := func(what string, values []string, matcher ConfiguredDimension) *hmaps.OrderedIntSet {
var result *hmaps.OrderedIntSet
if len(values) == 0 {
@@ -800,16 +799,16 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
}
}
return result, nil
return result
}
filter, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(values, matcher.ResolveIndex, hglob.GetGlobDot)
if err != nil {
return nil, fmt.Errorf("failed to create filter for %s: %w", what, err)
panic(fmt.Errorf("failed to create filter for %s: %w", what, err))
}
iter, err := matcher.IndexMatch(filter)
if err != nil {
return nil, fmt.Errorf("failed to match %s %q: %w", what, values, err)
panic(fmt.Errorf("failed to match %s %q: %w", what, values, err))
}
for i := range iter {
if result == nil {
@@ -818,16 +817,12 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
result.Set(i)
}
return result, nil
return result
}
l, err1 := applyFilter("languages", cfg.Globs.Languages, b.cfg.ConfiguredLanguages)
v, err2 := applyFilter("versions", cfg.Globs.Versions, b.cfg.ConfiguredVersions)
r, err3 := applyFilter("roles", cfg.Globs.Roles, b.cfg.ConfiguredRoles)
if err := cmp.Or(err1, err2, err3); err != nil {
panic(fmt.Errorf("failed to apply filters: %w", err))
}
l := applyFilter("languages", cfg.Globs.Languages, b.cfg.ConfiguredLanguages)
v := applyFilter("versions", cfg.Globs.Versions, b.cfg.ConfiguredVersions)
r := applyFilter("roles", cfg.Globs.Roles, b.cfg.ConfiguredRoles)
b.GlobFilterMisses = Bools{
len(cfg.Globs.Languages) > 0 && l == nil,
+66
View File
@@ -17,6 +17,8 @@ import (
"fmt"
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
// https://github.com/gohugoio/hugo/issues/4895
@@ -314,6 +316,70 @@ X123X
`)
}
// See issue 15212.
func TestTemplateReturnEarly(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "http://example.com/"
-- layouts/home.html --
home-start|{{ if true }}{{ return }}{{ end }}home-end
`
b := Test(t, files)
b.AssertFileContent("public/index.html", "home-start|", "! home-end")
}
// See issue 15212.
func TestTemplateReturnEarlyFromBlock(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "http://example.com/"
-- layouts/baseof.html --
header|{{ block "main" . }}{{ end }}|footer
-- layouts/home.html --
{{ define "main" }}main-start|{{ return }}main-end{{ end }}
`
b := Test(t, files)
b.AssertFileContent("public/index.html", "header|main-start||footer", "! main-end")
}
// See issue 15212.
func TestTemplateReturnEarlyFromTemplateInclude(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "http://example.com/"
-- layouts/home.html --
{{ template "foo" . }}|after
{{ define "foo" }}foo-start|{{ return }}foo-end{{ end }}
`
b := Test(t, files)
b.AssertFileContent("public/index.html", "foo-start||after", "! foo-end")
}
// See issue 15212.
func TestTemplateReturnWithValueOutsidePartialFails(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "http://example.com/"
-- layouts/home.html --
{{ return 32 }}
`
b, err := TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "return with a value is only supported in partials")
}
func TestPartialCached(t *testing.T) {
t.Parallel()
+4 -2
View File
@@ -1,7 +1,9 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.163.3
HUGORELEASER_COMMITISH=4d22555aebf458d5d150500c9ac4bee5b24cf0d3
HUGORELEASER_TAG=v0.165.0
HUGORELEASER_COMMITISH=76a5e1880ab46688155b02e99bab9be2a6134492
+2 -2
View File
@@ -252,11 +252,11 @@ type IsRebuildProvider interface {
// IncrementByOne implements Incrementer adding 1 every time Incr is called.
type IncrementByOne struct {
counter uint64
counter atomic.Uint64
}
func (c *IncrementByOne) Incr() int {
return int(atomic.AddUint64(&c.counter, uint64(1)))
return int(c.counter.Add(uint64(1)))
}
// Incrementer increments and returns the value.
+82 -84
View File
@@ -528,96 +528,94 @@ func (b *batcher) doBuild(ctx context.Context) (*Package, error) {
}
jsOpts := Options{
ExternalOptions: externalOptions,
InternalOptions: InternalOptions{
DependencyManager: b.dependencyManager,
Splitting: true,
ImportOnResolveFunc: func(imp string, args api.OnResolveArgs) string {
var importContextPath string
if args.Kind == api.ResolveEntryPoint {
importContextPath = args.Path
} else {
importContextPath = args.Importer
}
importContext, importContextFound := state.importerImportContext.Get(importContextPath)
ExternalOptions: externalOptions,
DependencyManager: b.dependencyManager,
Splitting: true,
ImportOnResolveFunc: func(imp string, args api.OnResolveArgs) string {
var importContextPath string
if args.Kind == api.ResolveEntryPoint {
importContextPath = args.Path
} else {
importContextPath = args.Importer
}
importContext, importContextFound := state.importerImportContext.Get(importContextPath)
// We want to track the dependencies closest to where they're used.
dm := b.dependencyManager
if importContextFound {
dm = importContext.dm
}
// We want to track the dependencies closest to where they're used.
dm := b.dependencyManager
if importContextFound {
dm = importContext.dm
}
if r, found := state.importResource.Get(imp); found {
dm.AddIdentity(identity.FirstIdentity(r))
if r, found := state.importResource.Get(imp); found {
dm.AddIdentity(identity.FirstIdentity(r))
return imp
}
if importContext.resourceGetter != nil {
resolved := ResolveResource(imp, importContext.resourceGetter)
if resolved != nil {
resolvePath := resources.InternalResourceTargetPath(resolved)
dm.AddIdentity(identity.FirstIdentity(resolved))
imp := PrefixHugoVirtual + resolvePath
state.importResource.Set(imp, resolved)
state.importerImportContext.Set(imp, importContext)
return imp
}
if importContext.resourceGetter != nil {
resolved := ResolveResource(imp, importContext.resourceGetter)
if resolved != nil {
resolvePath := resources.InternalResourceTargetPath(resolved)
dm.AddIdentity(identity.FirstIdentity(resolved))
imp := PrefixHugoVirtual + resolvePath
state.importResource.Set(imp, resolved)
state.importerImportContext.Set(imp, importContext)
return imp
}
}
return ""
},
ImportOnLoadFunc: func(args api.OnLoadArgs) (string, error) {
imp := args.Path
if r, found := state.importResource.Get(imp); found {
content, err := resources.InternalResourceSourceContent(ctx, r)
if err != nil {
return "", fmt.Errorf("failed to read import %q: %w", resources.InternalResourceSourcePathBestEffort(r), err)
}
return content, nil
}
return "", nil
},
ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage {
if importContext, found := state.importerImportContext.Get(args.Path); found {
if !importContext.scriptOptions.IsZero() {
return importContext.scriptOptions.Params
}
}
return nil
},
ErrorMessageResolveFunc: func(args api.Message) *ErrorMessageResolved {
if loc := args.Location; loc != nil {
path := strings.TrimPrefix(loc.File, NsHugoImportResolveFunc+":")
if r, found := state.importResource.Get(path); found {
sourcePath := resources.InternalResourceSourcePathBestEffort(r)
var contentr hugio.ReadSeekCloser
if cp, ok := r.(hugio.ReadSeekCloserProvider); ok {
contentr, _ = cp.ReadSeekCloser()
}
return &ErrorMessageResolved{
Content: contentr,
Path: sourcePath,
Message: args.Text,
}
}
}
return nil
},
ResolveSourceMapSource: func(s string) string {
if r, found := state.importResource.Get(s); found {
if ss := resources.InternalResourceSourcePath(r); ss != "" {
return ss
}
return PrefixHugoMemory + s
}
return ""
},
EntryPoints: entryPoints,
}
return ""
},
ImportOnLoadFunc: func(args api.OnLoadArgs) (string, error) {
imp := args.Path
if r, found := state.importResource.Get(imp); found {
content, err := resources.InternalResourceSourceContent(ctx, r)
if err != nil {
return "", fmt.Errorf("failed to read import %q: %w", resources.InternalResourceSourcePathBestEffort(r), err)
}
return content, nil
}
return "", nil
},
ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage {
if importContext, found := state.importerImportContext.Get(args.Path); found {
if !importContext.scriptOptions.IsZero() {
return importContext.scriptOptions.Params
}
}
return nil
},
ErrorMessageResolveFunc: func(args api.Message) *ErrorMessageResolved {
if loc := args.Location; loc != nil {
path := strings.TrimPrefix(loc.File, NsHugoImportResolveFunc+":")
if r, found := state.importResource.Get(path); found {
sourcePath := resources.InternalResourceSourcePathBestEffort(r)
var contentr hugio.ReadSeekCloser
if cp, ok := r.(hugio.ReadSeekCloserProvider); ok {
contentr, _ = cp.ReadSeekCloser()
}
return &ErrorMessageResolved{
Content: contentr,
Path: sourcePath,
Message: args.Text,
}
}
}
return nil
},
ResolveSourceMapSource: func(s string) string {
if r, found := state.importResource.Get(s); found {
if ss := resources.InternalResourceSourcePath(r); ss != "" {
return ss
}
return PrefixHugoMemory + s
}
return ""
},
EntryPoints: entryPoints,
}
result, err := b.client.buildClient.Build(jsOpts)
+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
+28 -60
View File
@@ -27,10 +27,8 @@ func TestToBuildOptions(t *testing.T) {
c := qt.New(t)
opts := Options{
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
MediaType: media.Builtin.JavascriptType,
Stdin: true,
}
c.Assert(opts.compile(), qt.IsNil)
@@ -46,16 +44,12 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
ExternalOptions: ExternalOptions{
Target: []string{"es2018"},
Format: "cjs",
Minify: true,
AvoidTDZ: true,
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
Target: []string{"es2018"},
Format: "cjs",
Minify: true,
AvoidTDZ: true,
MediaType: media.Builtin.JavascriptType,
Stdin: true,
}
c.Assert(opts.compile(), qt.IsNil)
@@ -74,14 +68,10 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
ExternalOptions: ExternalOptions{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
}
c.Assert(opts.compile(), qt.IsNil)
@@ -101,14 +91,10 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
ExternalOptions: ExternalOptions{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
}
c.Assert(opts.compile(), qt.IsNil)
@@ -128,14 +114,10 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
ExternalOptions: ExternalOptions{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "external",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "external",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
}
c.Assert(opts.compile(), qt.IsNil)
@@ -155,13 +137,9 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
ExternalOptions: ExternalOptions{
JSX: "automatic", JSXImportSource: "preact",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
JSX: "automatic", JSXImportSource: "preact",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
}
c.Assert(opts.compile(), qt.IsNil)
@@ -179,24 +157,18 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
ExternalOptions: ExternalOptions{
Drop: "console",
},
Drop: "console",
}
c.Assert(opts.compile(), qt.IsNil)
c.Assert(opts.compiled.Drop, qt.Equals, api.DropConsole)
opts = Options{
ExternalOptions: ExternalOptions{
Drop: "debugger",
},
Drop: "debugger",
}
c.Assert(opts.compile(), qt.IsNil)
c.Assert(opts.compiled.Drop, qt.Equals, api.DropDebugger)
opts = Options{
ExternalOptions: ExternalOptions{
Drop: "adsfadsf",
},
Drop: "adsfadsf",
}
c.Assert(opts.compile(), qt.ErrorMatches, `unsupported drop type: "adsfadsf"`)
}
@@ -222,12 +194,8 @@ func TestToBuildOptionsTarget(t *testing.T) {
} {
c.Run(test.target, func(c *qt.C) {
opts := Options{
ExternalOptions: ExternalOptions{
Target: []string{test.target},
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
},
Target: []string{test.target},
MediaType: media.Builtin.JavascriptType,
}
c.Assert(opts.compile(), qt.IsNil)
+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
+25 -4
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
}
@@ -569,6 +582,9 @@ func (p *dispatcherPool[Q, R]) Err() error {
}
}
// Workaround for data race, see https://github.com/wazero/wazero/issues/2532
var wazeroCacheMu sync.Mutex
func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
if opts.Ctx == nil {
opts.Ctx = context.Background()
@@ -597,6 +613,8 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
runtimeConfig = runtimeConfig.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesThreads)
if opts.CompilationCacheDir != "" {
wazeroCacheMu.Lock()
defer wazeroCacheMu.Unlock()
compilationCache, err := wazero.NewCompilationCacheWithDir(opts.CompilationCacheDir)
if err != nil {
return nil, err
@@ -679,7 +697,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
@@ -752,7 +771,9 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
}
for _, d := range dp.dispatchers {
if err := d.inGroup.Wait(); err != nil {
// ErrShutdown is expected here; since Go 1.27 (json/v2), Decode
// surfaces the read error from the pipes we just closed.
if err := d.inGroup.Wait(); err != nil && err != ErrShutdown {
return err
}
}
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{}
+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++
}
+1 -3
View File
@@ -326,9 +326,7 @@ func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
renderer.WithNodeRenderers(
util.Prioritized(&hugoContextRenderer{
logger: a.logger,
Config: html.Config{
Writer: html.DefaultWriter,
},
Writer: html.DefaultWriter,
}, 50),
),
)
+8 -12
View File
@@ -36,9 +36,7 @@ var _ renderer.SetOptioner = (*hookedRenderer)(nil)
func newLinkRenderer(cfg goldmark_config.Config) renderer.NodeRenderer {
r := &hookedRenderer{
linkifyProtocol: []byte(cfg.Extensions.LinkifyProtocol),
Config: html.Config{
Writer: html.DefaultWriter,
},
Writer: html.DefaultWriter,
}
return r
}
@@ -169,15 +167,13 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
ctx.RenderContext().Ctx,
w,
imageLinkContext{
linkContext: linkContext{
BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.HTML(text),
plainText: render.TextPlain(n, source),
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
},
isBlock: isBlock,
BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.HTML(text),
plainText: render.TextPlain(n, source),
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
isBlock: isBlock,
},
)
+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
}
+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)
}
+2 -2
View File
@@ -70,7 +70,7 @@ type SuffixInfo struct {
// If http.DetectContentType resolves to application/octet-stream, a zero Type is returned.
// If http.DetectContentType resolves to text/plain or application/xml, we try to get more specific using types and ext.
func FromContent(types Types, extensionHints []string, content []byte) Type {
t := strings.Split(http.DetectContentType(content), ";")[0]
t, _, _ := strings.Cut(http.DetectContentType(content), ";")
if t == "application/octet-stream" {
return zero
}
@@ -143,7 +143,7 @@ func FromString(t string) (Type, error) {
mainType := parts[0]
subParts := strings.Split(parts[1], "+")
subType := strings.Split(subParts[0], ";")[0]
subType, _, _ := strings.Cut(subParts[0], ";")
var suffix string
+16 -2
View File
@@ -178,15 +178,29 @@ func (s *Store) WriteMetrics(w io.Writer) {
}
sort.Sort(bySum(results))
for _, v := range results {
if s.calculateHints {
fmt.Fprintf(w, " %15s %12s %12s %9d %7.f %6d %5d %s\n", v.sum, v.avg, v.max, v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key)
fmt.Fprintf(w, " %15s %12s %12s %9d %7.f %6d %5d %s\n", formatDuration(v.sum), formatDuration(v.avg), formatDuration(v.max), v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key)
} else {
fmt.Fprintf(w, " %15s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key)
fmt.Fprintf(w, " %15s %12s %12s %5d %s\n", formatDuration(v.sum), formatDuration(v.avg), formatDuration(v.max), v.count, v.key)
}
}
}
func formatDuration(d time.Duration) string {
switch {
case d >= time.Second:
return fmt.Sprintf("%.2f s", float64(d)/float64(time.Second)) // additional spacing between value and unit
case d >= time.Millisecond:
return fmt.Sprintf("%.2f ms", float64(d)/float64(time.Millisecond))
case d >= time.Microsecond:
return fmt.Sprintf("%.2f µs", float64(d)/float64(time.Microsecond))
default:
return fmt.Sprintf("%.2f ns", float64(d)/float64(time.Nanosecond))
}
}
// A result represents the calculated results for a given metric.
type result struct {
key string
+19
View File
@@ -17,6 +17,7 @@ import (
"html/template"
"strings"
"testing"
"time"
"github.com/gohugoio/hugo/resources/page"
@@ -66,3 +67,21 @@ func BenchmarkHowSimilar(b *testing.B) {
howSimilar(s1, s2)
}
}
func TestFormatDuration(t *testing.T) {
c := qt.New(t)
tests := []struct {
duration time.Duration
want string
}{
{4*time.Second + 342*time.Millisecond, "4.34 s"}, // additional spacing between value and unit
{170*time.Millisecond + 289*time.Microsecond, "170.29 ms"},
{16*time.Microsecond + 90*time.Nanosecond, "16.09 µs"},
{147 * time.Nanosecond, "147.00 ns"},
}
for _, tt := range tests {
got := formatDuration(tt.duration)
c.Assert(got, qt.Equals, tt.want)
}
}
+1 -1
View File
@@ -615,7 +615,7 @@ func (c *Client) writeHugoDirectSum(mods Modules) error {
continue
}
if m.IsGoMod() && m.VersionQuery() != "" {
sums = append(sums, modSum{pathVersionKey: pathVersionKey{path: m.Path(), version: m.Version()}, sum: m.Sum()})
sums = append(sums, modSum{path: m.Path(), version: m.Version(), sum: m.Sum()})
}
}
+18 -5
View File
@@ -195,15 +195,28 @@ func Pack(sourceFs, assetsWithDuplicatesPreservedFs afero.Fs, mods modules.Modul
}
}
name := "project"
rfi, err := sourceFs.Stat("")
if err == nil {
name = rfi.Name()
// Stable defaults; mark the workspace private so npm never publishes it.
// Preserve any hand-set values from an existing file.
name, version, private := "hugoautogen", "0.1.0", true
if data, err := afero.ReadFile(sourceFs, workspacePackageJSON); err == nil {
var existing map[string]any
if err := json.Unmarshal(data, &existing); err == nil {
if s, ok := existing["name"].(string); ok && s != "" {
name = s
}
if s, ok := existing["version"].(string); ok && s != "" {
version = s
}
if b, ok := existing["private"].(bool); ok {
private = b
}
}
}
autoGenPkg := map[string]any{
"name": name,
"version": "0.1.0",
"version": version,
"private": private,
dependenciesKey: moduleDeps,
devDependenciesKey: moduleDevDeps,
}
@@ -92,6 +92,40 @@ func TestPackageBuilder(t *testing.T) {
b.Assert(string(meta2), qt.Equals, string(meta1))
}
// The generated workspace package.json should get a stable name and
// "private": true, and preserve hand-set name/version/private on regeneration.
// See issue 15245.
func TestPackStableNameAndPrivate(t *testing.T) {
files := getPackageBuilderTestFiles()
b := hugolib.Test(t, files)
sourceFs := b.H.BaseFs.ProjectSourceFs
assetsFs := b.H.BaseFs.AssetsWithDuplicatesPreserved.Fs
mods := b.H.Configs.Modules
b.Assert(npm.Pack(sourceFs, assetsFs, mods), qt.IsNil)
pkg, err := afero.ReadFile(sourceFs, "packages/hugoautogen/package.json")
b.Assert(err, qt.IsNil)
b.Assert(string(pkg), qt.Contains, `"name": "hugoautogen"`)
b.Assert(string(pkg), qt.Contains, `"private": true`)
b.Assert(string(pkg), qt.Contains, `"version": "0.1.0"`)
// Hand-set fields survive a re-pack.
edited := strings.NewReplacer(
`"name": "hugoautogen"`, `"name": "@foo/hugoautogen"`,
`"version": "0.1.0"`, `"version": "1.2.3"`,
`"private": true`, `"private": false`,
).Replace(string(pkg))
b.Assert(afero.WriteFile(sourceFs, "packages/hugoautogen/package.json", []byte(edited), 0o666), qt.IsNil)
b.Assert(npm.Pack(sourceFs, assetsFs, mods), qt.IsNil)
pkg, err = afero.ReadFile(sourceFs, "packages/hugoautogen/package.json")
b.Assert(err, qt.IsNil)
b.Assert(string(pkg), qt.Contains, `"name": "@foo/hugoautogen"`)
b.Assert(string(pkg), qt.Contains, `"version": "1.2.3"`)
b.Assert(string(pkg), qt.Contains, `"private": false`)
}
func BenchmarkPackageFilesSum(b *testing.B) {
files := getPackageBuilderTestFiles()
bb := hugolib.Test(b, files)
+4 -4
View File
@@ -40,8 +40,8 @@ func TestMenuCache(t *testing.T) {
m[0].MenuConfig.Title = "changed"
}
var o1 uint64
var o2 uint64
var o1 atomic.Uint64
var o2 atomic.Uint64
var wg sync.WaitGroup
@@ -59,7 +59,7 @@ func TestMenuCache(t *testing.T) {
for k, menu := range testMenuSets {
l1.Lock()
m, ca := c1.get("k1", nil, menu)
c.Assert(ca, qt.Equals, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)))
c.Assert(ca, qt.Equals, !o1.CompareAndSwap(uint64(k), uint64(k+1)))
l1.Unlock()
m2, c2 := c1.get("k1", nil, m)
c.Assert(c2, qt.Equals, true)
@@ -69,7 +69,7 @@ func TestMenuCache(t *testing.T) {
l2.Lock()
m3, c3 := c1.get("k2", changeFirst, menu)
c.Assert(c3, qt.Equals, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)))
c.Assert(c3, qt.Equals, !o2.CompareAndSwap(uint64(k), uint64(k+1)))
l2.Unlock()
c.Assert(m3, qt.Not(qt.IsNil))
c.Assert("changed", qt.Equals, m3[0].Title)
+8 -10
View File
@@ -72,16 +72,14 @@ type Config struct {
// can be set if position of first shortcode is known
func newPageLexer(input []byte, stateStart stateFunc, cfg Config) *pageLexer {
lexer := &pageLexer{
input: input,
stateStart: stateStart,
summaryDivider: summaryDivider,
cfg: cfg,
lexerShortcodeState: lexerShortcodeState{
currLeftDelimItem: tLeftDelimScNoMarkup,
currRightDelimItem: tRightDelimScNoMarkup,
openShortcodes: make(map[unique.Handle[string]]bool),
},
items: make([]Item, 0, 5),
input: input,
stateStart: stateStart,
summaryDivider: summaryDivider,
cfg: cfg,
currLeftDelimItem: tLeftDelimScNoMarkup,
currRightDelimItem: tRightDelimScNoMarkup,
openShortcodes: make(map[unique.Handle[string]]bool),
items: make([]Item, 0, 5),
}
lexer.sectionHandlers = createSectionHandlers(lexer)
+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 {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+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)
}
+1 -7
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()
@@ -168,7 +162,7 @@ func NewHugoInfo(opts HugoInfoOptions) HugoInfo {
BuildDate: opts.BuildDate,
GoVersion: opts.GoVersion,
hugoInfoProviders: hugoInfoProviders{HugoInfoHugoSitesProvider: opts.HugoInfoHugoSitesProvider},
HugoInfoHugoSitesProvider: opts.HugoInfoHugoSitesProvider,
opts: opts,
store: hstore.NewScratch(),
+3 -2
View File
@@ -327,8 +327,9 @@ type PageMetaInternalProvider interface {
// PageRenderProvider provides a way for a Page to render content.
type PageRenderProvider interface {
// Render renders the given layout with this Page as context.
Render(ctx context.Context, layout ...string) (template.HTML, error)
// Render renders the given view (a layout template) with this Page as
// the data context, or, if given, the second argument CONTEXT.
Render(ctx context.Context, args ...any) (template.HTML, error)
// RenderString renders the first value in args with the content renderer defined
// for this Page.
// It takes an optional map as a second argument:
+2 -2
View File
@@ -117,8 +117,8 @@ func (lcp *LazyContentProvider) Len(ctx context.Context) int {
return lcp.init.Value(ctx).Len(ctx)
}
func (lcp *LazyContentProvider) Render(ctx context.Context, layout ...string) (template.HTML, error) {
return lcp.init.Value(ctx).Render(ctx, layout...)
func (lcp *LazyContentProvider) Render(ctx context.Context, args ...any) (template.HTML, error) {
return lcp.init.Value(ctx).Render(ctx, args...)
}
func (lcp *LazyContentProvider) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
+1 -1
View File
@@ -399,7 +399,7 @@ func (p *nopPage) RelRef(argsm map[string]any) (string, error) {
return "", nil
}
func (p *nopPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
func (p *nopPage) Render(ctx context.Context, args ...any) (template.HTML, error) {
return "", nil
}
+5
View File
@@ -130,6 +130,11 @@ func (pages Pages) ProbablyEq(other any) bool {
return true
}
// IndexOf returns the index of page in p, or -1 if not found.
func (p Pages) IndexOf(page Page) int {
return searchPage(page, p)
}
// PagesFactory somehow creates some Pages.
// We do a lot of lazy Pages initialization in Hugo, so we need a type.
type PagesFactory func() Pages
+4 -4
View File
@@ -31,8 +31,8 @@ func TestPageCache(t *testing.T) {
p[0].(*testPage).description = "changed"
}
var o1 uint64
var o2 uint64
var o1 atomic.Uint64
var o2 atomic.Uint64
var wg sync.WaitGroup
@@ -50,7 +50,7 @@ func TestPageCache(t *testing.T) {
for k, pages := range testPageSets {
l1.Lock()
p, ca := c1.get("k1", nil, pages)
c.Assert(ca, qt.Equals, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)))
c.Assert(ca, qt.Equals, !o1.CompareAndSwap(uint64(k), uint64(k+1)))
l1.Unlock()
p2, c2 := c1.get("k1", nil, p)
c.Assert(c2, qt.Equals, true)
@@ -60,7 +60,7 @@ func TestPageCache(t *testing.T) {
l2.Lock()
p3, c3 := c1.get("k2", changeFirst, pages)
c.Assert(c3, qt.Equals, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)))
c.Assert(c3, qt.Equals, !o2.CompareAndSwap(uint64(k), uint64(k+1)))
l2.Unlock()
c.Assert(p3, qt.Not(qt.IsNil))
c.Assert("changed", qt.Equals, p3[0].(*testPage).description)
+12
View File
@@ -70,3 +70,15 @@ func TestToPages(t *testing.T) {
_, err := ToPages("not a page")
c.Assert(err, qt.Not(qt.IsNil))
}
func TestIndexOf(t *testing.T) {
c := qt.New(t)
p1, p2, p3, p4 := &testPage{title: "p1"}, &testPage{title: "p2"}, &testPage{title: "p3"}, &testPage{title: "p4"}
pages := Pages{p1, p2, p3}
c.Assert(pages.IndexOf(p1), qt.Equals, 0)
c.Assert(pages.IndexOf(p2), qt.Equals, 1)
c.Assert(pages.IndexOf(p3), qt.Equals, 2)
c.Assert(pages.IndexOf(p4), qt.Equals, -1)
}
+1 -1
View File
@@ -483,7 +483,7 @@ func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error)
return "", nil
}
func (p *testPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
func (p *testPage) Render(ctx context.Context, args ...any) (template.HTML, error) {
panic("testpage: not implemented")
}
+1 -5
View File
@@ -26,10 +26,6 @@ type transformationKeyer interface {
func (spec *Spec) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) {
key := r.(transformationKeyer).TransformationKey()
return spec.PostProcessResources.GetOrCreate(key, func() (postpub.PostPublishedResource, error) {
result := postpub.NewPostPublishResource(spec.incr.Incr(), r)
if result == nil {
panic("got nil result")
}
return result, nil
return postpub.NewPostPublishResource(spec.incr.Incr(), r), nil
})
}

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