Compare commits

...

69 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen f61346ede6 related: Speed up index creation
```
                              │ relatedperfcmp.bench │       feat-relatedperf.bench       │
                              │        sec/op        │   sec/op     vs base               │
RelatedNewIndex/singles-10               37.85µ ± 3%   23.83µ ± 0%  -37.05% (p=0.000 n=8)
RelatedNewIndex/all-10                   34.47µ ± 1%   21.86µ ± 0%  -36.59% (p=0.000 n=8)
RelatedMatchesIn-10                      42.38µ ± 1%   41.56µ ± 1%   -1.95% (p=0.000 n=8)
RelatedTokenizeCreateIndex-10            72.00µ ± 0%   46.49µ ± 0%  -35.43% (p=0.000 n=8)
RelatedSite-10                           14.24m ± 5%   14.66m ± 5%        ~ (p=0.161 n=8)
geomean                                  141.5µ        108.1µ       -23.60%

                              │ relatedperfcmp.bench │       feat-relatedperf.bench        │
                              │         B/op         │     B/op      vs base               │
RelatedNewIndex/singles-10              39.78Ki ± 0%   24.33Ki ± 0%  -38.85% (p=0.000 n=8)
RelatedNewIndex/all-10                  41.53Ki ± 0%   26.08Ki ± 0%  -37.21% (p=0.000 n=8)
RelatedMatchesIn-10                     12.11Ki ± 0%   12.11Ki ± 0%        ~ (p=0.324 n=8)
RelatedTokenizeCreateIndex-10           75.70Ki ± 0%   50.01Ki ± 0%  -33.94% (p=0.000 n=8)
RelatedSite-10                          37.95Mi ± 0%   37.85Mi ± 0%   -0.26% (p=0.000 n=8)
geomean                                 142.5Ki        108.3Ki       -24.03%

                              │ relatedperfcmp.bench │        feat-relatedperf.bench        │
                              │      allocs/op       │  allocs/op   vs base                 │
RelatedNewIndex/singles-10                886.0 ± 0%    325.0 ± 0%  -63.32% (p=0.000 n=8)
RelatedNewIndex/all-10                    887.0 ± 0%    326.0 ± 0%  -63.25% (p=0.000 n=8)
RelatedMatchesIn-10                       7.000 ± 0%    7.000 ± 0%        ~ (p=1.000 n=8) ¹
RelatedTokenizeCreateIndex-10            1448.0 ± 0%    342.0 ± 0%  -76.38% (p=0.000 n=8)
RelatedSite-10                           166.2k ± 0%   159.5k ± 0%   -4.04% (p=0.000 n=8)
geomean                                  1.058k         526.5       -50.22%
¹ all samples are equal
¹ all samples are equal
```
2026-09-01 12:41:21 +02:00
Joe Mooring efe5cbc75a github: Fix workflow for stale issues
Closes #15261
2026-09-01 09:45:52 +02:00
Joe Mooring 850f11c060 related: Add tokenize option for index values
Closes #7515
Closes #15199

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-31 11:23:54 +02:00
Rodrigo Ipince 39507d51bf resources/page: Don't let a title's "/" split an auto-derived slug
When a page has no explicit slug, the :title and :slug permalink tokens
fall back to a URL-safe form of the title. That sanitization step
(PathSpec.URLize / MakePathSanitized) intentionally treats "/" as a
valid path character, since it is also used for things like taxonomy
term names that legitimately span multiple path segments.

For a regular content page, though, the auto-derived slug is meant to
be a single path segment (the front matter "slug" field docs say it
"overrides the last segment of the URL path"). A title containing a
literal "/", e.g. "Watch/listen to this", was passed straight through
into the permalink pattern, silently creating an extra nested path
segment (.../watch/listen-to-this/) instead of a single sanitized one
(.../watch-listen-to-this/). No warning or error was produced; the
site just built with an unintended URL structure.

This behavior regressed from a fix already applied once via #4092 and
#5282 (which used a stricter helpers.PathSpec.MakeSegment for titles,
exempting only taxonomy pages). That distinction was lost when slash
handling in taxonomy terms was restored for #5571, which switched
title handling back to the slash-preserving URLize function for all
page kinds, not just taxonomies.

Restore the taxonomy/term exemption: pageToPermalinkTitle now replaces
"/" (and "\") with "-" before urlizing the title, but only for page
kinds other than taxonomy and term, so multi-level taxonomy
hierarchies (the behavior #5571 asked for) keep working exactly as
before. Explicit "slug" front matter values are untouched by this
change, since users should stay in full control of an explicitly set
slug.

Fixes #3577
See #5571
See #4090
2026-08-31 11:06:30 +02:00
Bjørn Erik Pedersen ec52e638c9 warpc/js: Upgrade katex to 0.18.4 (note)
Closes #15254
2026-08-31 11:03:52 +02:00
Bjørn Erik Pedersen e6abb9c3d0 config/security: Deny text/org content by default
Org export blocks and @@html:...@@ snippets pass raw HTML through
unescaped, making text/org the same XSS sink as text/html, which is
already denied. Sites with Org content can opt back in via
security.allowContent.

Thanks to @philipdissert for finding and reporting this issue.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 22:49:08 +02:00
Joe Mooring 90fe506831 config/allconfig: Improve resilience of hugo mod init
Closes #15253

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-30 18:06:10 +02:00
Shantanav Mukherjee 49835f8fe9 tpl: Separate deferred templates by escaping mode
Keep identical deferred bodies shared within the same output escaping mode while preventing HTML and plain-text templates from colliding.

Fixes #15234
2026-08-29 20:34:17 +02:00
Bjørn Erik Pedersen d6e6f9e500 resources: Validate the resolved address on remote fetches
security.http.urls only inspects the URL text, so a hostname that
resolves to a loopback, private or link-local address (e.g. the cloud
metadata endpoint) satisfied the policy and let resources.GetRemote
reach an internal endpoint, embedding the response in the built site.

Install a dial-time hook on the remote-fetch transport that rejects
connections to non-global-unicast or private addresses. It is enforced
only under the default hardened allowlist; a user who customizes
security.http.urls has opted into their own hosts, so the check stands
down and local/internal fetches keep working.

See CVE-2026-10582.
Closes #15247.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 13:03:09 +02:00
Bjørn Erik Pedersen e4dc48cf7f markup: Escape code-fence attribute values in the default renderer
The default fenced-code-block renderer wrote attribute values from the
info string into the wrapper element without escaping. New converts each
value from a []byte to a string, so RenderAttributes' []byte escaping
branch was never reached and values were written verbatim; the class
value in writeDivStart was likewise unescaped. A quote in a value could
close the attribute and inject a further one, e.g. an event handler.

Escape string attribute values too, and escape the class value.

See CVE-2026-10618.
See #15247.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-28 13:03:09 +02:00
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
201 changed files with 4691 additions and 1480 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: []
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@f5f995c727ac99a91dec92781a8e34e7c839a65e # v6.0.0
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
with:
issue-inactive-days: 21
add-issue-labels: 'Outdated'
+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) {
+1 -1
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: 165,
Minor: 166,
PatchLevel: 0,
Suffix: "-DEV",
}
+26 -54
View File
@@ -326,9 +326,10 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
if c.DefaultOutputFormat != "" {
f, found := outputFormats.GetByName(c.DefaultOutputFormat)
if !found {
return fmt.Errorf("unknown default output format %q", c.DefaultOutputFormat)
transientErr = fmt.Errorf("unknown default output format %q", c.DefaultOutputFormat)
} else {
defaultOutputFormat = f
}
defaultOutputFormat = f
} else {
c.DefaultOutputFormat = defaultOutputFormat.Name
}
@@ -458,36 +459,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 +1006,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",
}
}
@@ -197,6 +197,23 @@ x
b.Assert(err.Error(), qt.Contains, `failed to create config: unknown output format "foo" for kind "home"`)
}
// See issue 15253.
func TestInvalidDefaultOutputFormat(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
defaultOutputFormat = 'foo'
-- layouts/home.html --
x
`
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `failed to create config: unknown default output format "foo"`)
}
func TestContentTypesDefault(t *testing.T) {
files := `
-- hugo.toml --
+1 -1
View File
@@ -89,7 +89,7 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
return nil, fmt.Errorf("failed to create config from modules config: %w", err)
}
configs.LoadingInfo.ConfigFiles = append(configs.LoadingInfo.ConfigFiles, l.ModulesConfigFiles...)
} else if err := configs.transientErr(); err != nil {
} else if err := configs.transientErr(); err != nil && !d.IgnoreModuleDoesNotExist {
return nil, fmt.Errorf("failed to create config: %w", err)
}
-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"`
+49 -6
View File
@@ -18,6 +18,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"net/netip"
"net/url"
"reflect"
@@ -39,12 +40,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.
@@ -79,9 +79,11 @@ var DefaultConfig = Config{
},
// Content under /content is treated as untrusted. text/html bodies are
// emitted verbatim and are an XSS sink, so they are denied by default.
// The same goes for text/org, whose export blocks and inline snippets
// pass raw HTML through unescaped.
// Everything else is allowed because Whitelist treats a deny-only list as
// "allow anything not denied".
AllowContent: MustNewWhitelist("! ^text/html$"),
AllowContent: MustNewWhitelist("! ^text/html$", "! ^text/org$"),
}
// Config is the top level security config.
@@ -213,6 +215,47 @@ func (c Config) CheckAllowedHTTPURL(u string) error {
return nil
}
// CheckAllowedHTTPAddress reports whether a dial-time destination address may
// be connected to. address is the resolved "host:port" passed to a net.Dialer
// control hook, i.e. the actual address the HTTP client is about to connect to.
//
// The security.http.urls allowlist only inspects the URL text and never sees
// the resolved address, so a hostname that resolves to a loopback, private or
// link-local (including the cloud metadata endpoint) address would otherwise
// satisfy the policy and let resources.GetRemote reach an internal endpoint.
// We deny any nonglobal-unicast or private address here to close that gap.
func (c Config) CheckAllowedHTTPAddress(network, address string) error {
// Only enforced under the default hardened allowlist. If the user has
// customized security.http.urls they have opted into whatever hosts they
// listed, including internal ones (e.g. a local dev server), so we do not
// second-guess the resolved address.
if !slices.Equal(c.HTTP.URLs.patternsStrings, DefaultConfig.HTTP.URLs.patternsStrings) {
return nil
}
deny := func(name string) error {
return &AccessDeniedError{
name: name,
path: "security.http.urls",
policies: c.ToTOML(),
}
}
host, _, err := net.SplitHostPort(address)
if err != nil {
host = address
}
ip, err := netip.ParseAddr(host)
if err != nil {
// The dial hook always hands us a resolved IP literal; anything else
// is unexpected, so fail closed.
return deny(address)
}
ip = ip.Unmap()
if !ip.IsGlobalUnicast() || ip.IsPrivate() {
return deny(host)
}
return nil
}
// canonicalIPv4URL rewrites an integer/hex/octal IPv4 host in rawURL to its
// canonical dotted-decimal form (inet_aton semantics), returning ok=false when
// the host is a normal name or already dotted-decimal.
+43 -2
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$', '! ^text/org$']\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",
)
}
@@ -247,6 +247,39 @@ urls = ['.*', '! ^https?://evil\.example\.com']
})
}
// A resolved destination address must be validated so a hostname that resolves
// to an internal address cannot reach an internal endpoint via GetRemote.
// See CVE-2026-10582.
func TestCheckAllowedHTTPAddress(t *testing.T) {
t.Parallel()
c := qt.New(t)
pc := DefaultConfig
for _, addr := range []string{
"93.184.216.34:80",
"[2001:db8::1]:443",
} {
c.Assert(pc.CheckAllowedHTTPAddress("tcp", addr), qt.IsNil, qt.Commentf(addr))
}
for _, addr := range []string{
"127.0.0.1:80",
"[::1]:80",
"10.0.0.1:8080",
"172.16.0.1:80",
"192.168.1.1:80",
"169.254.169.254:80", // Cloud metadata.
"[fe80::1]:80",
"[fc00::1]:80",
"0.0.0.0:80",
"[::ffff:127.0.0.1]:80", // IPv4-mapped loopback.
} {
err := pc.CheckAllowedHTTPAddress("tcp", addr)
c.Assert(err, qt.IsNotNil, qt.Commentf(addr))
c.Assert(err, qt.ErrorMatches, `(?s).*is not whitelisted in policy "security\.http\.urls".*`, qt.Commentf(addr))
}
}
func TestCheckAllowedHTTPURLAtInPathIssue14825(t *testing.T) {
t.Parallel()
c := qt.New(t)
@@ -346,6 +379,15 @@ func TestCheckAllowedContent(t *testing.T) {
c.Assert(err, qt.ErrorMatches, `(?s).*"text/html" is not whitelisted in policy "security\.allowContent".*`)
})
c.Run("text/org denied by default", func(c *qt.C) {
c.Parallel()
pc, err := DecodeConfig(config.New())
c.Assert(err, qt.IsNil)
err = pc.CheckAllowedContent("text/org")
c.Assert(err, qt.IsNotNil)
c.Assert(err, qt.ErrorMatches, `(?s).*"text/org" is not whitelisted in policy "security\.allowContent".*`)
})
c.Run("Other content types allowed by default", func(c *qt.C) {
c.Parallel()
pc, err := DecodeConfig(config.New())
@@ -353,7 +395,6 @@ func TestCheckAllowedContent(t *testing.T) {
for _, mt := range []string{
"text/markdown",
"text/asciidoc",
"text/x-org",
"text/rst",
"text/pandoc",
} {
-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.
+3 -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)
@@ -334,6 +334,8 @@ func newTestCfg(c *qt.C, mm afero.Fs) (config.Provider, *hugofs.Fs) {
cfg := `
theme = "mytheme"
[security]
allowContent = ['.*']
[languages]
[languages.en]
weight = 1
+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.
+15 -35
View File
@@ -19,7 +19,6 @@ import (
"iter"
"path/filepath"
"slices"
"strconv"
"strings"
"sync/atomic"
@@ -174,21 +173,13 @@ 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) {
func (ps *pageState) RelatedKeywords(cfg related.IndexConfig) ([]string, error) {
v, found, err := page.NamedPageMetaValue(ps, cfg.Name)
if err != nil {
if err != nil || !found {
return nil, err
}
if !found {
return nil, nil
}
return cfg.ToKeywords(v)
}
@@ -225,10 +216,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 +328,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 +348,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 +371,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 +387,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))
}
+187 -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||",
)
@@ -1450,6 +1456,8 @@ func TestPageManualSummary(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "http://example.com/"
[security]
allowContent = ['.*']
-- content/page-md-shortcode.md --
---
title: "Hugo"
@@ -1497,31 +1505,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 +1745,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 +1915,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 +1993,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 +2005,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|
@@ -1957,6 +2024,8 @@ func TestHomePageIsLeafBundle(t *testing.T) {
-- hugo.toml --
defaultContentLanguage = 'de'
defaultContentLanguageInSubdir = true
[security]
allowContent = ['.*']
[languages.de]
weight = 1
[languages.en]
@@ -2149,7 +2218,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 +2411,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")
}
@@ -162,6 +162,7 @@ func TestPagesFromGoTmplAsciiDocAndSimilar(t *testing.T) {
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
[security]
allowContent = ['.*']
[security.exec]
allow = [%s]
-- layouts/single.html --
@@ -368,11 +369,11 @@ func TestPagesFromGoRelatedKeywords(t *testing.T) {
}
k, err := p1.RelatedKeywords(icfg)
b.Assert(err, qt.IsNil)
b.Assert(k, qt.DeepEquals, icfg.StringsToKeywords("foo", "Bar"))
b.Assert(k, qt.DeepEquals, []string{"foo", "Bar"})
icfg.Name = "title"
k, err = p1.RelatedKeywords(icfg)
b.Assert(err, qt.IsNil)
b.Assert(k, qt.DeepEquals, icfg.StringsToKeywords("p1:p1"))
b.Assert(k, qt.DeepEquals, []string{"p1:p1"})
}
func TestPagesFromGoTmplLanguagePerFile(t *testing.T) {
+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"}`)
}
+52
View File
@@ -98,6 +98,58 @@ allowContent = ['.*']
b.AssertFileContent("public/p1/index.html", "<p>hello</p>")
})
c.Run("Org content, denied by default", func(c *qt.C) {
c.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org"
-- content/page.org --
---
title: "Untrusted"
---
@@html:<script>alert(1)</script>@@
-- layouts/single.html --
{{ .Content }}
`
_, err := TestE(c, files)
c.Assert(err, qt.IsNotNil)
c.Assert(err, qt.ErrorMatches, `(?s).*"text/org" is not whitelisted in policy "security\.allowContent".*`)
})
c.Run("Org content, allowed via override", func(c *qt.C) {
c.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org"
[security]
allowContent = ['.*']
-- content/page.org --
---
title: "Trusted"
---
hello
-- layouts/single.html --
{{ .Content }}
`
b := Test(c, files)
b.AssertFileContent("public/page/index.html", "hello")
})
c.Run("Org content from content adapter, denied by default", func(c *qt.C) {
c.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.org"
-- content/_content.gotmpl --
{{ .AddPage (dict "path" "p1" "title" "Untrusted" "content" (dict "value" "@@html:<script>alert(1)</script>@@" "mediaType" "text/org")) }}
-- layouts/single.html --
{{ .Content }}
`
_, err := TestE(c, files)
c.Assert(err, qt.IsNotNil)
c.Assert(err, qt.ErrorMatches, `(?s).*"text/org" is not whitelisted in policy "security\.allowContent".*`)
})
c.Run("os.GetEnv, denied", func(c *qt.C) {
c.Parallel()
files := `
+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()
+3 -2
View File
@@ -1,7 +1,8 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.164.0
HUGORELEASER_COMMITISH=ce2470e7012b5ab5fc4e10ebe4027e9f8d9e00dc
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);
+1 -1
View File
@@ -9,6 +9,6 @@
"license": "ISC",
"description": "",
"devDependencies": {
"katex": "^0.16.21"
"katex": "0.18.4"
}
}
File diff suppressed because one or more lines are too long
+3
View File
@@ -17,6 +17,9 @@ import (
_ "embed"
)
// Keep in sync with js/package.json.
const katexVersion = "0.18.4"
//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.
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++
}
@@ -437,3 +437,38 @@ Some code.
b.AssertFileContent("public/p1/index.html", "! <script>")
}
// A quote inside a code-fence attribute value must not break out of the
// attribute and inject a further attribute (e.g. an event handler).
// See CVE-2026-10618.
func TestCodeblockAttributeValueEscape(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/single.html --
{{ .Content }}
-- content/p1.md --
---
title: "p1"
---
§§§bash { title="x\" onmouseover=\"alert(1)" }
echo "hello";
§§§
§§§bash { class="x\" onmouseover=\"alert(2)" }
echo "hello";
§§§
`
b := hugolib.Test(t, files)
// The quote must be escaped so it cannot close the attribute and start a
// new one; the literal onmouseover="alert breakout must not appear.
b.AssertFileContent("public/p1/index.html",
`! onmouseover="alert`,
"&#34; onmouseover=&#34;alert(2)",
"&quot; onmouseover=&quot;alert(1)",
)
}
+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
}
+1 -1
View File
@@ -316,7 +316,7 @@ func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute, wrapperCla
if attrs != nil {
for _, attr := range attrs {
if attr.Name == "class" {
w.WriteString(" " + attr.ValueString())
w.WriteString(" " + gohtml.EscapeString(attr.ValueString()))
break
}
}
+16 -12
View File
@@ -187,12 +187,7 @@ func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) {
_, _ = w.Write(attr.Name)
_, _ = w.WriteString(`="`)
switch v := attr.Value.(type) {
case []byte:
_, _ = w.Write(util.EscapeHTML(v))
default:
w.WriteString(cast.ToString(v))
}
writeAttributeValue(w, attr.Value)
_ = w.WriteByte('"')
}
@@ -211,13 +206,22 @@ func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribu
_, _ = w.WriteString(attr.Name)
_, _ = w.WriteString(`="`)
switch v := attr.Value.(type) {
case []byte:
_, _ = w.Write(util.EscapeHTML(v))
default:
w.WriteString(cast.ToString(v))
}
writeAttributeValue(w, attr.Value)
_ = w.WriteByte('"')
}
}
// writeAttributeValue writes v HTML-escaped as a double-quoted HTML attribute
// value. Note that New converts string values from a []byte to a string, so
// the string case is the common one and must be escaped too.
func writeAttributeValue(w hugio.FlexiWriter, v any) {
switch vv := v.(type) {
case []byte:
_, _ = w.Write(util.EscapeHTML(vv))
case string:
_, _ = w.Write(util.EscapeHTML([]byte(vv)))
default:
_, _ = w.Write(util.EscapeHTML([]byte(cast.ToString(vv))))
}
}
+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)
+147 -75
View File
@@ -18,12 +18,16 @@ import (
"context"
"errors"
"fmt"
"html"
"iter"
"maps"
"math"
"sort"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/hmaps"
@@ -46,8 +50,7 @@ var validTypes = map[string]bool{
}
var (
_ Keyword = (*StringKeyword)(nil)
zeroDate = time.Time{}
zeroDate = time.Time{}
// DefaultConfig is the default related config.
DefaultConfig = Config{
@@ -122,15 +125,22 @@ type IndexConfig struct {
// used in more than 50% of the documents in the index.
CardinalityThreshold int
// Will lower case all string values in and queries tothis index.
// Will lower case all string values in and queries to this index.
// May get better accurate results, but at a slight performance cost.
ToLower bool
// Whether to split multi-word values into individual keywords.
Tokenize bool
// Minimum token length in Unicode code points when tokenizing.
MinTokenLength int
}
// Document is the interface an indexable document in Hugo must fulfill.
type Document interface {
// RelatedKeywords returns a list of keywords for the given index config.
RelatedKeywords(cfg IndexConfig) ([]Keyword, error)
// RelatedKeywords returns the keywords for the given index config.
// The returned slice may share backing with the document's data and must not be mutated.
RelatedKeywords(cfg IndexConfig) ([]string, error)
// When this document was or will be published.
PublishDate() time.Time
@@ -152,7 +162,7 @@ type FragmentProvider interface {
// lists, for every possible search term, the documents that contain that term.
type InvertedIndex struct {
cfg Config
index map[string]map[Keyword][]Document
index map[string]map[string][]Document
// Counts the number of documents added to each index.
indexDocCount map[string]int
@@ -176,9 +186,9 @@ func (idx *InvertedIndex) getIndexCfg(name string) (IndexConfig, bool) {
// NewInvertedIndex creates a new InvertedIndex.
// Documents to index must be added in Add.
func NewInvertedIndex(cfg Config) *InvertedIndex {
idx := &InvertedIndex{index: make(map[string]map[Keyword][]Document), indexDocCount: make(map[string]int), cfg: cfg}
idx := &InvertedIndex{index: make(map[string]map[string][]Document), indexDocCount: make(map[string]int), cfg: cfg}
for _, conf := range cfg.Indices {
idx.index[conf.Name] = make(map[Keyword][]Document)
idx.index[conf.Name] = make(map[string][]Document)
if conf.Weight < idx.minWeight {
// By default, the weight scale starts at 0, but we allow
// negative weights.
@@ -207,7 +217,7 @@ func (idx *InvertedIndex) Add(ctx context.Context, docs ...Document) error {
for _, doc := range docs {
var added bool
var words []Keyword
var words []string
words, err = doc.RelatedKeywords(config)
if err != nil {
continue
@@ -220,9 +230,25 @@ func (idx *InvertedIndex) Add(ctx context.Context, docs ...Document) error {
if config.Type == TypeFragments {
if fp, ok := doc.(FragmentProvider); ok {
for _, fragment := range fp.Fragments(ctx).Identifiers {
frags := fp.Fragments(ctx)
seenKW := make(map[string]bool)
for _, fragment := range frags.Identifiers {
added = true
setm[FragmentKeyword(fragment)] = append(setm[FragmentKeyword(fragment)], doc)
if config.Tokenize {
if h, found := frags.HeadingsMap[fragment]; found {
for word := range splitWordsSeq(h.Title) {
if !config.shouldSkipToken(word) {
kw := config.normalizeKeyword(word)
if !seenKW[kw] {
seenKW[kw] = true
setm[kw] = append(setm[kw], doc)
}
}
}
continue
}
}
setm[fragment] = append(setm[fragment], doc)
}
}
}
@@ -270,10 +296,10 @@ func (idx *InvertedIndex) Finalize(ctx context.Context) error {
// search for related content.
type queryElement struct {
Index string
Keywords []Keyword
Keywords []string
}
func newQueryElement(index string, keywords ...Keyword) queryElement {
func newQueryElement(index string, keywords ...string) queryElement {
return queryElement{Index: index, Keywords: keywords}
}
@@ -312,7 +338,7 @@ func (r ranks) Len() int { return len(r) }
func (r ranks) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r ranks) Less(i, j int) bool {
if r[i].Weight == r[j].Weight {
if r[i].Doc.PublishDate() == r[j].Doc.PublishDate() {
if r[i].Doc.PublishDate().Equal(r[j].Doc.PublishDate()) {
return r[i].Doc.Name() < r[j].Doc.Name()
}
return r[i].Doc.PublishDate().After(r[j].Doc.PublishDate())
@@ -363,7 +389,7 @@ func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Docume
}
for _, cfg := range configs {
var keywords []Keyword
var keywords []string
if opts.Document != nil {
k, err := opts.Document.RelatedKeywords(cfg)
if err != nil {
@@ -372,13 +398,27 @@ func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Docume
keywords = append(keywords, k...)
}
if cfg.Type == TypeFragments {
for _, fragment := range opts.Fragments {
keywords = append(keywords, FragmentKeyword(fragment))
}
keywords = append(keywords, opts.Fragments...)
if opts.Document != nil {
if fp, ok := opts.Document.(FragmentProvider); ok {
for _, fragment := range fp.Fragments(ctx).Identifiers {
keywords = append(keywords, FragmentKeyword(fragment))
frags := fp.Fragments(ctx)
seenKW := make(map[string]bool)
for _, fragment := range frags.Identifiers {
if cfg.Tokenize {
if h, found := frags.HeadingsMap[fragment]; found {
for word := range splitWordsSeq(h.Title) {
if !cfg.shouldSkipToken(word) {
kw := cfg.normalizeKeyword(word)
if !seenKW[kw] {
seenKW[kw] = true
keywords = append(keywords, kw)
}
}
}
continue
}
}
keywords = append(keywords, fragment)
}
}
}
@@ -387,7 +427,7 @@ func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Docume
queryElements = append(queryElements, newQueryElement(cfg.Name, keywords...))
}
for _, slice := range opts.NamedSlices {
var keywords []Keyword
var keywords []string
key := slice.KeyString()
if key == "" {
return nil, fmt.Errorf("index %q not valid", slice.Key)
@@ -413,29 +453,52 @@ func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Docume
return idx.search(ctx, queryElements...)
}
func (cfg IndexConfig) stringToKeyword(s string) Keyword {
func (cfg IndexConfig) normalizeKeyword(s string) string {
if cfg.ToLower {
s = strings.ToLower(s)
return strings.ToLower(s)
}
if cfg.Type == TypeFragments {
return FragmentKeyword(s)
}
return StringKeyword(s)
return s
}
// ToKeywords returns a Keyword slice of the given input.
func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
var keywords []Keyword
func (cfg IndexConfig) shouldSkipToken(word string) bool {
return cfg.MinTokenLength > 0 && utf8.RuneCountInString(word) < cfg.MinTokenLength
}
// appendTokens appends the normalized tokens in s to keywords.
func (cfg IndexConfig) appendTokens(keywords []string, s string) []string {
for word := range splitWordsSeq(s) {
if !cfg.shouldSkipToken(word) {
keywords = append(keywords, cfg.normalizeKeyword(word))
}
}
return keywords
}
// ToKeywords returns the keywords for the given input.
// The returned slice may share backing with the input and must not be mutated.
func (cfg IndexConfig) ToKeywords(v any) ([]string, error) {
switch vv := v.(type) {
case string:
keywords = append(keywords, cfg.stringToKeyword(vv))
case []string:
vvv := make([]Keyword, len(vv))
for i := range vvv {
vvv[i] = cfg.stringToKeyword(vv[i])
if cfg.Tokenize {
return cfg.appendTokens(make([]string, 0, 4), vv), nil
}
keywords = append(keywords, vvv...)
return []string{cfg.normalizeKeyword(vv)}, nil
case []string:
if cfg.Tokenize {
keywords := make([]string, 0, len(vv)*2)
for _, s := range vv {
keywords = cfg.appendTokens(keywords, s)
}
return keywords, nil
}
if !cfg.ToLower {
return vv, nil
}
keywords := make([]string, len(vv))
for i, s := range vv {
keywords[i] = strings.ToLower(s)
}
return keywords, nil
case []any:
return cfg.ToKeywords(cast.ToStringSlice(vv))
case time.Time:
@@ -443,14 +506,12 @@ func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
if cfg.Pattern != "" {
layout = cfg.Pattern
}
keywords = append(keywords, StringKeyword(vv.Format(layout)))
return []string{vv.Format(layout)}, nil
case nil:
return keywords, nil
return nil, nil
default:
return keywords, fmt.Errorf("indexing currently not supported for index %q and type %T", cfg.Name, vv)
return nil, fmt.Errorf("indexing currently not supported for index %q and type %T", cfg.Name, vv)
}
return keywords, nil
}
func (idx *InvertedIndex) search(ctx context.Context, query ...queryElement) ([]Document, error) {
@@ -466,7 +527,12 @@ func (idx *InvertedIndex) searchDate(ctx context.Context, self Document, upperDa
}()
applyDateFilter := !idx.cfg.IncludeNewer && !upperDate.IsZero()
var fragmentsFilter collections.SortedStringSlice
// Bundled into one struct so the closure below causes only one heap escape.
var ff struct {
ids collections.SortedStringSlice
words collections.SortedStringSlice
toLower bool
}
for _, el := range query {
setm, found := idx.index[el.Index]
@@ -479,6 +545,10 @@ func (idx *InvertedIndex) searchDate(ctx context.Context, self Document, upperDa
return []Document{}, fmt.Errorf("index config for %q not found", el.Index)
}
if config.Type == TypeFragments && config.ApplyFilter && config.Tokenize {
ff.toLower = config.ToLower
}
for _, kw := range el.Keywords {
if docs, found := setm[kw]; found {
for _, doc := range docs {
@@ -494,11 +564,12 @@ func (idx *InvertedIndex) searchDate(ctx context.Context, self Document, upperDa
}
if config.Type == TypeFragments && config.ApplyFilter {
if fkw, ok := kw.(FragmentKeyword); ok {
fragmentsFilter = append(fragmentsFilter, string(fkw))
if config.Tokenize {
ff.words = append(ff.words, kw)
} else {
ff.ids = append(ff.ids, kw)
}
}
r, found := matchm[doc]
if !found {
r = getRank(doc, config.Weight)
@@ -528,17 +599,31 @@ func (idx *InvertedIndex) searchDate(ctx context.Context, self Document, upperDa
}
sort.Stable(matches)
sort.Strings(fragmentsFilter)
sort.Strings(ff.ids)
sort.Strings(ff.words)
result := make([]Document, len(matches))
for i, m := range matches {
result[i] = m.Doc
if len(fragmentsFilter) > 0 {
if len(ff.ids) > 0 || len(ff.words) > 0 {
if dp, ok := result[i].(FragmentProvider); ok {
result[i] = dp.ApplyFilterToHeadings(ctx, func(h *tableofcontents.Heading) bool {
return fragmentsFilter.Contains(h.ID)
if ff.ids.Contains(h.ID) {
return true
}
if len(ff.words) > 0 {
for word := range splitWordsSeq(h.Title) {
if ff.toLower {
word = strings.ToLower(word)
}
if ff.words.Contains(word) {
return true
}
}
}
return false
})
}
}
@@ -599,32 +684,19 @@ func DecodeConfig(m hmaps.Params) (Config, error) {
return c, nil
}
// StringKeyword is a string search keyword.
type StringKeyword string
func (s StringKeyword) String() string {
return string(s)
}
// FragmentKeyword represents a document fragment.
type FragmentKeyword string
func (f FragmentKeyword) String() string {
return string(f)
}
// Keyword is the interface a keyword in the search index must implement.
type Keyword interface {
String() string
}
// StringsToKeywords converts the given slice of strings to a slice of Keyword.
func (cfg IndexConfig) StringsToKeywords(s ...string) []Keyword {
kw := make([]Keyword, len(s))
for i := range s {
kw[i] = cfg.stringToKeyword(s[i])
// splitWordsSeq iterates the normalized words in title, stripping HTML
// entities and leading/trailing punctuation from each word.
func splitWordsSeq(title string) iter.Seq[string] {
return func(yield func(string) bool) {
for w := range strings.FieldsSeq(html.UnescapeString(title)) {
w = strings.TrimFunc(w, isNotWordRune)
if w != "" && !yield(w) {
return
}
}
}
return kw
}
func isNotWordRune(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}
+95 -32
View File
@@ -17,6 +17,7 @@ import (
"context"
"fmt"
"math/rand"
"slices"
"strings"
"testing"
"time"
@@ -26,7 +27,7 @@ import (
)
type testDoc struct {
keywords map[string][]Keyword
keywords map[string][]string
date time.Time
name string
}
@@ -37,7 +38,7 @@ func (d *testDoc) String() string {
for k, v := range d.keywords {
s.WriteString(k + ":\t\t")
for _, vv := range v {
s.WriteString(" " + vv.String())
s.WriteString(" " + vv)
}
s.WriteString("\n")
}
@@ -54,7 +55,7 @@ func newTestDoc(name string, keywords ...string) *testDoc {
}
func newTestDocWithDate(name string, date time.Time, keywords ...string) *testDoc {
km := make(map[string][]Keyword)
km := make(map[string][]string)
kw := &testDoc{keywords: km, date: date}
@@ -66,11 +67,7 @@ func (d *testDoc) addKeywords(name string, keywords ...string) *testDoc {
keywordm := createTestKeywords(name, keywords...)
for k, v := range keywordm {
keywords := make([]Keyword, len(v))
for i := range v {
keywords[i] = StringKeyword(v[i])
}
d.keywords[k] = keywords
d.keywords[k] = v
}
return d
}
@@ -81,8 +78,8 @@ func createTestKeywords(name string, keywords ...string) map[string][]string {
}
}
func (d *testDoc) RelatedKeywords(cfg IndexConfig) ([]Keyword, error) {
return d.keywords[cfg.Name], nil
func (d *testDoc) RelatedKeywords(cfg IndexConfig) ([]string, error) {
return cfg.ToKeywords(d.keywords[cfg.Name])
}
func (d *testDoc) PublishDate() time.Time {
@@ -102,7 +99,7 @@ func TestCardinalityThreshold(t *testing.T) {
idx := NewInvertedIndex(config)
hasKeyword := func(index, keyword string) bool {
_, found := idx.index[index][StringKeyword(keyword)]
_, found := idx.index[index][keyword]
return found
}
@@ -160,8 +157,7 @@ func TestSearch(t *testing.T) {
t.Run("search-tags", func(t *testing.T) {
c := qt.New(t)
var cfg IndexConfig
m, err := idx.search(context.Background(), newQueryElement("tags", cfg.StringsToKeywords("a", "b", "d", "z")...))
m, err := idx.search(context.Background(), newQueryElement("tags", "a", "b", "d", "z"))
c.Assert(err, qt.IsNil)
c.Assert(len(m), qt.Equals, 2)
c.Assert(m[0], qt.Equals, docs[0])
@@ -170,10 +166,9 @@ func TestSearch(t *testing.T) {
t.Run("search-tags-and-keywords", func(t *testing.T) {
c := qt.New(t)
var cfg IndexConfig
m, err := idx.search(context.Background(),
newQueryElement("tags", cfg.StringsToKeywords("a", "b", "z")...),
newQueryElement("keywords", cfg.StringsToKeywords("a", "b")...))
newQueryElement("tags", "a", "b", "z"),
newQueryElement("keywords", "a", "b"))
c.Assert(err, qt.IsNil)
c.Assert(len(m), qt.Equals, 3)
c.Assert(m[0], qt.Equals, docs[3])
@@ -245,11 +240,7 @@ func TestToKeywordsToLower(t *testing.T) {
keywords, err := config.ToKeywords(slice)
c.Assert(err, qt.IsNil)
c.Assert(slice, qt.DeepEquals, []string{"A", "B", "C"})
c.Assert(keywords, qt.DeepEquals, []Keyword{
StringKeyword("a"),
StringKeyword("b"),
StringKeyword("c"),
})
c.Assert(keywords, qt.DeepEquals, []string{"a", "b", "c"})
}
func TestDecodeConfig(t *testing.T) {
@@ -302,14 +293,43 @@ func TestToKeywordsAnySlice(t *testing.T) {
slice := []any{"A", 32, "C"}
keywords, err := config.ToKeywords(slice)
c.Assert(err, qt.IsNil)
c.Assert(keywords, qt.DeepEquals, []Keyword{
StringKeyword("A"),
StringKeyword("32"),
StringKeyword("C"),
})
c.Assert(keywords, qt.DeepEquals, []string{"A", "32", "C"})
}
func TestSplitWords(t *testing.T) {
c := qt.New(t)
tests := []struct {
in string
want []string
}{
{"", nil},
{" ", nil},
{"hello world", []string{"hello", "world"}},
// leading/trailing punctuation stripped
{"...hello...", []string{"hello"}},
{",hello, world!", []string{"hello", "world"}},
// digits preserved
{"go1.24", []string{"go1.24"}},
// HTML entities decoded
{"foo &amp;bar", []string{"foo", "bar"}},
// fast path: single token, no whitespace
{"&amp;foo&amp;", []string{"foo"}},
// &nbsp; and similar entities that decode to whitespace must not use the fast path
{"Apples&nbsp;and&nbsp;oranges!", []string{"Apples", "and", "oranges"}},
// punctuation-only tokens dropped
{"hello --- world", []string{"hello", "world"}},
// mixed spaces
{" foo bar ", []string{"foo", "bar"}},
}
for _, tt := range tests {
c.Assert(slices.Collect(splitWordsSeq(tt.in)), qt.DeepEquals, tt.want, qt.Commentf("input: %q", tt.in))
}
}
func BenchmarkRelatedNewIndex(b *testing.B) {
rnd := rand.New(rand.NewSource(32))
pages := make([]*testDoc, 100)
numkeywords := 30
allKeywords := make([]string, numkeywords)
@@ -318,7 +338,7 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
}
for i := range pages {
start := rand.Intn(len(allKeywords))
start := rnd.Intn(len(allKeywords))
end := start + 3
if end >= len(allKeywords) {
end = start + 1
@@ -326,7 +346,7 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
kw := newTestDoc("tags", allKeywords[start:end]...)
if i%5 == 0 {
start := rand.Intn(len(allKeywords))
start := rnd.Intn(len(allKeywords))
end := start + 3
if end >= len(allKeywords) {
end = start + 1
@@ -367,10 +387,10 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
}
func BenchmarkRelatedMatchesIn(b *testing.B) {
var icfg IndexConfig
q1 := newQueryElement("tags", icfg.StringsToKeywords("keyword2", "keyword5", "keyword32", "asdf")...)
q2 := newQueryElement("keywords", icfg.StringsToKeywords("keyword3", "keyword4")...)
q1 := newQueryElement("tags", "keyword2", "keyword5", "keyword32", "asdf")
q2 := newQueryElement("keywords", "keyword3", "keyword4")
rnd := rand.New(rand.NewSource(32))
docs := make([]*testDoc, 1000)
numkeywords := 20
allKeywords := make([]string, numkeywords)
@@ -389,7 +409,7 @@ func BenchmarkRelatedMatchesIn(b *testing.B) {
idx := NewInvertedIndex(cfg)
for i := range docs {
start := rand.Intn(len(allKeywords))
start := rnd.Intn(len(allKeywords))
end := start + 3
if end >= len(allKeywords) {
end = start + 1
@@ -412,3 +432,46 @@ func BenchmarkRelatedMatchesIn(b *testing.B) {
}
}
}
func BenchmarkRelatedTokenizeCreateIndex(b *testing.B) {
rnd := rand.New(rand.NewSource(32))
keywordsCfg := IndexConfig{Name: "keywords", Weight: 100, Tokenize: true}
docs := make([]*testDoc, 100)
numGroups := 5
numKeywords := 3
allKeywords := make([]string, numGroups)
for i := range numGroups {
for j := range numKeywords {
allKeywords[i] += fmt.Sprintf("keyword%d ", i*numKeywords+j+1)
}
}
for i := range docs {
start := rnd.Intn(len(allKeywords))
end := start + 3
if end >= len(allKeywords) {
end = start + 1
}
kws := slices.Clone(allKeywords[start:end])
kws = append(kws, "keyword3")
docs[i] = newTestDoc("keywords", kws...)
}
for b.Loop() {
cfg := Config{
Threshold: 10,
Indices: IndicesConfig{keywordsCfg},
}
idx := NewInvertedIndex(cfg)
for j := range docs {
idx.Add(context.Background(), docs[j])
}
if err := idx.Finalize(context.Background()); err != nil {
b.Fatal(err)
}
}
}
+282 -6
View File
@@ -40,7 +40,7 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT"]
weight = 90
[[related.indices]]
name = 'keywords'
weight = 80
weight = 80
-- content/p1.md --
---
title: p1
@@ -112,13 +112,13 @@ Related 2: {{ template "list-related" $related2 }}
-- layouts/single.html --
Content: {{ .Content }}
`
b := hugolib.Test(t, files)
expect := `
P1 Fragments: [p1-title]
P1 Fragments: [p1-title]
P2 Fragments: [p2-title-1 p2-title-2 ref1]
Len Related 1: 1
Related 2: 2
@@ -128,12 +128,288 @@ Related 2: 2
b.AssertFileContent("public/"+p+"/index.html", expect)
}
b.AssertFileContent("public/p1/index.html",
b.AssertFileContent(
"public/p1/index.html",
"Related 1: 0: p2: h0: First title|ref1|::END",
"Related 2: 0: p5: h0: Common p3, p4, p5|common-p3-p4-p5|::END 1: p4: h0: Common p3, p4, p5|common-p3-p4-p5|::END",
)
}
// See issue 15199.
func TestRelatedFragmentsTokenize(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home', 'section', 'taxonomy', 'term', 'rss', 'sitemap']
[related]
includeNewer = true
threshold = 80
toLower = true
[[related.indices]]
type = 'fragments'
weight = 100
tokenize = true
-- content/p1.md --
---
title: p1
---
## Hugo Templates Overview
-- content/p2.md --
---
title: p2
---
## Templates in Hugo
-- content/p3.md --
---
title: p3
---
## JavaScript and CSS
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }} {{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Related: p2")
b.AssertFileContent("public/p2/index.html", "Related: p1")
b.AssertFileContent("public/p3/index.html", "Related: ")
}
// See issue 15199.
func TestRelatedFragmentsTokenizeHTMLEntities(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "http://example.com/"
disableKinds = ["home", "section", "taxonomy", "term", "rss", "sitemap"]
[related]
includeNewer = true
threshold = 80
toLower = true
[[related.indices]]
type = 'fragments'
weight = 100
tokenize = true
-- content/p1.md --
---
title: p1
---
## Types of apples
-- content/p2.md --
---
title: p2
---
## Something about "apples"
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }} {{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Related: p2")
b.AssertFileContent("public/p2/index.html", "Related: p1")
}
// See issue 15199.
func TestRelatedBasicTokenize(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home', 'section', 'taxonomy', 'term', 'rss', 'sitemap']
[related]
includeNewer = true
threshold = 80
toLower = true
[[related.indices]]
name = 'title'
weight = 100
tokenize = true
-- content/p1.md --
---
title: Hugo Templates Overview
---
-- content/p2.md --
---
title: Templates in Hugo
---
-- content/p3.md --
---
title: JavaScript and CSS
---
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }} {{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Related: Templates in Hugo")
b.AssertFileContent("public/p2/index.html", "Related: Hugo Templates Overview")
b.AssertFileContent("public/p3/index.html", "Related: ")
}
// See issue 15199.
func TestRelatedFragmentsTokenizeApplyFilter(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home', 'section', 'taxonomy', 'term', 'rss', 'sitemap']
[related]
includeNewer = true
threshold = 80
toLower = true
[[related.indices]]
type = 'fragments'
weight = 100
applyFilter = true
tokenize = true
-- content/p1.md --
---
title: p1
---
## Hugo Templates Overview
-- content/p2.md --
---
title: p2
---
## Templates in Hugo
## Unrelated Topic
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }}:{{ with .HeadingsFiltered }}{{ range . }}{{ .Title }}|{{ end }}{{ end }}::END {{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Related: p2:Templates in Hugo|::END")
b.AssertFileContent("public/p2/index.html", "Related: p1:Hugo Templates Overview|::END")
}
// See issue 15199.
func TestRelatedTokenizeCaseSensitive(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home', 'section', 'taxonomy', 'term', 'rss', 'sitemap']
[related]
includeNewer = true
threshold = 80
toLower = false
[[related.indices]]
type = 'fragments'
weight = 100
tokenize = true
-- content/p1.md --
---
title: p1
---
## Hugo Templates
-- content/p2.md --
---
title: p2
---
## hugo templates
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }} {{ end }}
`
b := hugolib.Test(t, files)
// toLower=false: "Hugo" != "hugo", so no match.
b.AssertFileContent("public/p1/index.html", "Related: ")
b.AssertFileContent("public/p2/index.html", "Related: ")
}
// See issue 15238.
func TestRelatedTokenizeMinTokenLength(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home', 'section', 'taxonomy', 'term', 'rss', 'sitemap']
[related]
includeNewer = true
threshold = 80
toLower = true
[[related.indices]]
name = 'title'
weight = 100
tokenize = true
minTokenLength = 4
-- content/p1.md --
---
title: The long journey home
---
-- content/p2.md --
---
title: Planning your next big journey
---
-- content/p3.md --
---
title: Go to the top
---
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }} {{ end }}
`
b := hugolib.Test(t, files)
// p1 and p2 share "journey" (>= minTokenLength=4); short words are filtered.
b.AssertFileContent("public/p1/index.html", "Related: Planning your next big journey")
b.AssertFileContent("public/p2/index.html", "Related: The long journey home")
// All tokens in p3's title ("go", "to", "the", "top") are < minTokenLength=4.
b.AssertFileContent("public/p3/index.html", "Related: ")
}
// Repetitive headings must not inflate a document's rank above a document
// that matches on an additional index.
func TestRelatedFragmentsTokenizeDeduplication(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home', 'section', 'taxonomy', 'term', 'rss', 'sitemap']
[related]
includeNewer = true
threshold = 80
toLower = true
[[related.indices]]
type = 'fragments'
weight = 100
tokenize = true
[[related.indices]]
name = 'keywords'
weight = 100
-- content/p1.md --
---
title: p1
keywords: ['fruit']
---
## I love apples, apples and more apples
-- content/p2.md --
---
title: p2
keywords: ['fruit']
---
## I love apples, apples and more apples
## I love apples, apples and more apples
## I love apples, apples and more apples
-- content/p3.md --
---
title: p3
---
## I love apples, apples and more apples
## I love apples, apples and more apples
## I love apples, apples and more apples
-- layouts/page.html --
Related: {{ range site.RegularPages.Related . }}{{ .Title }} {{ end }}
`
b := hugolib.Test(t, files)
// p1 and p3 share the same unique fragment tokens; p1 also matches on
// keywords ("fruit"). Without deduplication, p3's repeated headings
// inflate its weight above p1's combined fragment+keywords weight.
b.AssertFileContent("public/p2/index.html", "Related: p1 p3")
}
func BenchmarkRelatedSite(b *testing.B) {
var files strings.Builder
files.WriteString(`
@@ -146,11 +422,11 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT"]
toLower = false
[[related.indices]]
name = 'keywords'
weight = 70
weight = 70
[[related.indices]]
name = 'pagerefs'
type = 'fragments'
weight = 30
weight = 30
-- layouts/single.html --
Len related: {{ site.RegularPages.Related . | len }}
`)
+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

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