Compare commits

...

369 Commits

Author SHA1 Message Date
hugoreleaser d8c0dfccf7 releaser: Bump versions for release of 0.155.2
[ci skip]
2026-02-02 10:04:51 +00:00
Bjørn Erik Pedersen 6bd2bde9d3 resources/image: Add some image decode/encode debug logging
Fixes #14337
Closes #14460
2026-02-01 22:46:01 +01:00
Bjørn Erik Pedersen 0f1c7d1200 Fix template change detection for multi-version sites
When templates were edited in a multi-version/role setup, only pages
from the current site slice (h.Sites) were checked for dependencies,
not pages from all sites. Fix by changing withPage to iterate over
all sites using allSites.

Fixes #14461

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 22:24:52 +01:00
hugoreleaser 10352335e0 releaser: Prepare repository for 0.156.0-DEV
[ci skip]
2026-01-30 10:32:56 +00:00
hugoreleaser f23576f2fb releaser: Bump versions for release of 0.155.1
[ci skip]
2026-01-30 10:10:48 +00:00
Bjørn Erik Pedersen b5d43cdc17 Fix image DecodeConfig regression of WebP images from file cache
Fixes #14453
2026-01-29 22:53:13 +01:00
Bjørn Erik Pedersen 6ef8017f60 Remove go vet from check.sh
Too slow.
2026-01-29 15:45:21 +01:00
Bjørn Erik Pedersen d67925f5a1 Add ./check.sh script
Running `mage check` takes too long time for user (and agent) iterations. That will eventualy run on CI.

This is a faster version that:

* Checks gofmt.
* Runs go vet.
* Runs staticcheck.
* Runs unit tests without the race flag.

It assumes that Go is installed, but will check if staticcheck is available and install it if not with go install.

It takes an optional argument to specify packages to check, otherwise it checks all packages.

This commit also enable local runs of the image golden tests.
2026-01-29 14:24:54 +01:00
Bjørn Erik Pedersen 48dd4f469a Update AGENTS.md with debug printing note
Added note about debug printing and CI build failure.
2026-01-29 11:30:50 +01:00
Joe Mooring b1e1eede50 resources/images: Fix WebP useSharpYuv being ignored
Closes #14449
2026-01-29 10:50:37 +01:00
Joe Mooring f522a7282b tpl/tplimpl: Remove failing Twitter tests
In v0.141.0 we deprecated:
- privacy.twitter.simple in favor of privacy.x.simple
- privacy.twitter.disable in favor of privacy.x.disable

Use of those settings is now causing the integration test to fail.
2026-01-28 23:21:35 -08:00
hugoreleaser 6ac72dd3bf releaser: Prepare repository for 0.156.0-DEV
[ci skip]
2026-01-28 16:50:40 +00:00
hugoreleaser 74c0bbfc6a releaser: Bump versions for release of 0.155.0
[ci skip]
2026-01-28 16:28:23 +00:00
Bjørn Erik Pedersen e66a33d369 Revert "release: Support alpha, beta, and RC releases"
This reverts commit 663075920a.

Closes #14448
2026-01-28 16:54:57 +01:00
Jayson Wang 8a979d54d1 Fix data race when clearing cache in cachebusters 2026-01-28 16:50:57 +01:00
Bjørn Erik Pedersen fd49df8f7a resources/images: Fix comment for Quality field in ImageConfig 2026-01-28 08:41:23 +01:00
Bjørn Erik Pedersen c7b35c8704 Fix panic reported in discourse
Fixes #14441
2026-01-27 22:48:46 +01:00
Bjørn Erik Pedersen 5916b61be9 Remove disableDate and disableLatLong from MetaConfig
Since the meta config is new (and exif config is deprecated), remove
these options from MetaConfig. The fields filter controls which metadata
is extracted, including Date and GPS fields.

The existing exif config options are preserved for backward compatibility.

Closes #14437

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:47:32 +01:00
Bjørn Erik Pedersen 7eafef2269 internal/warpc: Make webp C defaults match the Go defaults
To avoid confusion.
2026-01-27 18:31:45 +01:00
Bjørn Erik Pedersen 00c4228f61 testscripts: Move server tests to own folder
See #14439
2026-01-27 18:27:47 +01:00
Bjørn Erik Pedersen 5f5b2f378f testing: Skip some slow tests when not running in CI
Fixes #14438
2026-01-27 15:38:49 +01:00
Joe Mooring b3ea2a5fab misc: Update image processing description in README.md 2026-01-27 05:01:15 -08:00
Joe Mooring 67b543549b docs: Update docs.yaml 2026-01-27 10:05:34 +01:00
Bjørn Erik Pedersen ef7a22a2ff magefile: Skip commands test when running mage check locally
They're flaky for some reason.
2026-01-27 09:05:01 +01:00
Bjørn Erik Pedersen 8dfcece805 Fix recently introduced partial rendering bug
Fixes #14433
2026-01-26 22:45:16 +01:00
Bjørn Erik Pedersen 9a6bfe2667 Remove -p 2 parallelism limit for local test runs
Only limit parallelism in CI where memory is constrained.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 21:40:52 +01:00
dependabot[bot] 7d45614c6d build(deps): bump golang.org/x/tools from 0.40.0 to 0.41.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.40.0 to 0.41.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.40.0...v0.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 11:01:34 +01:00
dependabot[bot] 7f12c0900d build(deps): bump github.com/olekukonko/tablewriter from 1.1.2 to 1.1.3
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 11:00:53 +01:00
dependabot[bot] dab2c332dc build(deps): bump github.com/alecthomas/chroma/v2 from 2.23.0 to 2.23.1
Bumps [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) from 2.23.0 to 2.23.1.
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.23.0...v2.23.1)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.23.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 10:59:39 +01:00
Bjørn Erik Pedersen 5a64551a07 Make docs helper maxAge JSON output user friendly 2026-01-25 13:58:28 +01:00
Bjørn Erik Pedersen 0fc63fbf57 Add AGENTS.md and CLAUDE.md 2026-01-25 13:35:28 +01:00
dependabot[bot] 576d55d009 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.58.3 to 1.59.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.58.3...service/s3/v1.59.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-24 20:06:28 +01:00
dependabot[bot] b9400b8a95 build(deps): bump golang.org/x/image from 0.34.0 to 0.35.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.34.0 to 0.35.0.
- [Commits](https://github.com/golang/image/compare/v0.34.0...v0.35.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-24 18:11:13 +01:00
dependabot[bot] ce0c7f42c1 build(deps): bump golang.org/x/mod from 0.31.0 to 0.32.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/mod/compare/v0.31.0...v0.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-24 17:00:01 +01:00
Simon Heimlicher f472dd48a7 tpl: Fix partial decorator panic when partial returns falsy
When {{ with partial "foo" }} returns a falsy value (false, nil, ""),
the with block is skipped but _popPartialDecorator was only called
inside the with block. This left an orphan entry on the decorator
stack, causing "partial decorator ID mismatch" panic on subsequent
partial calls.

Add _popPartialDecorator call to the else branch to ensure the
decorator stack is always balanced regardless of the partial's
return value.

Fixes #14419
2026-01-24 16:59:18 +01:00
Bjørn Erik Pedersen 50973e7272 Add == and != operators to range predicates
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 16:51:57 +01:00
Simon Heimlicher 48566b6f37 resources: Fix race condition in test helper
The fetchResourceForSpec function was reusing a single ReadSeekCloser
across multiple goroutines, causing a race condition when
TestImageTransformConcurrent runs with imageProcWorkers > 1.

This was exposed by commit e569dd59a ("Misc webp performance work")
which increased imageProcWorkers from 1 to 2.

Fix by creating a new reader on each call to the open function.

Fixes race condition causing segfault on Apple Silicon.

Co-authored-by: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
2026-01-24 15:41:29 +01:00
Bjørn Erik Pedersen b441472b69 Add modulequeries file cache for module version queries
This adds a new file cache named 'modulequeries' with a 24h maxAge
that caches JSON responses from 'go mod download -json' when querying
module versions with constraints (e.g., version = "<v3.0.0").

On subsequent builds, if the cached JSON exists and the module
directory it references is non-empty, the cached result is used
directly, skipping the costly VCS interaction entirely.

Fixes #14417
2026-01-24 14:25:34 +01:00
Bjørn Erik Pedersen ac2c3fb20f Allow v1,v2 etc. style version names while still supporting full semver in queries
Fixes #14414
2026-01-23 18:53:32 +01:00
Bjørn Erik Pedersen ee91c707ee Make Page.Aliases more useful in multidimensional setups (note)
There are 2 fundamental changes related to alias handling in this PR:

1. the `Page.Aliases`` method returns a ready-to-use list of aliases, not the raw input data. This means that it's prefixed with correct dimension values (e.g. language code) and output format paths, and ready to use in e.g. Netlify's _redirects file.
2. We only render aliases for output formats that is both defined as `IsHTML` and `Permalinkable`.

See #14402
2026-01-23 18:52:35 +01:00
Bjørn Erik Pedersen 11f7f39913 Fix cascade draft panic
Fixes #14409
Fixes #14412

Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
2026-01-23 16:08:29 +01:00
Bjørn Erik Pedersen 192e3c453e Add range matchers for site matrix vector store filtering
Fixes #14359
2026-01-22 12:03:02 +01:00
Bjørn Erik Pedersen e569dd59a0 Misc webp performance work
*- Dynamic pool sizing for WebP and Katex based on CPU count
- WebP decode: use RGB (3 bytes) for opaque images instead of RGBA (4 bytes)
- Add new imaging.config with new encoder method option; this is default 4 (what we had before), if have tested with value 2 for a 30% speedup on my MacBook, but that does sacrifice quality in some cases (quality/speed trade-off 0-6)
- Track hasAlpha in decode response to enable RGB optimization
- Scale image processing workers (1->2) on high-core machines
- Add WebP benchmark test

On my MacBook Pro M1 Pro, I now get these numbers compared to Hugo v0.152.0:

```
goos: darwin
goarch: arm64
pkg: github.com/gohugoio/hugo/internal/warpc
cpu: Apple M1 Pro
        │ cmp152.bench │         fix-webpbench.bench         │
        │    sec/op    │    sec/op     vs base               │
Webp-10   162.6m ± 32%   145.3m ± 21%  -10.60% (p=0.026 n=6)

        │ cmp152.bench │          fix-webpbench.bench          │
        │     B/op     │     B/op       vs base                │
Webp-10   41.69Mi ± 0%   123.79Mi ± 0%  +196.92% (p=0.002 n=6)

        │ cmp152.bench │         fix-webpbench.bench         │
        │  allocs/op   │  allocs/op    vs base               │
Webp-10    9.963k ± 3%   18.797k ± 2%  +88.67% (p=0.002 n=6)
```

For general image resize benchmarks compared to Hugo v0.152.0 (note that that's not the result of this branch):

```bash
goos: darwin
goarch: arm64
pkg: github.com/gohugoio/hugo/resources/images
cpu: Apple M1 Pro
               │ cmp152.bench │         fix-webpbench.bench         │
               │    sec/op    │    sec/op     vs base               │
ImageResize-10   84.12m ± ∞ ¹   40.70m ± ∞ ¹  -51.61% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

               │ cmp152.bench  │         fix-webpbench.bench         │
               │     B/op      │     B/op       vs base              │
ImageResize-10   32.37Mi ± ∞ ¹   29.25Mi ± ∞ ¹  -9.64% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

               │ cmp152.bench │        fix-webpbench.bench         │
               │  allocs/op   │  allocs/op    vs base              │
ImageResize-10   11.34k ± ∞ ¹   12.03k ± ∞ ¹  +6.07% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95
```

Closes #14370
2026-01-21 10:44:22 +01:00
Joe Mooring 5ba03bf657 hugolib: Fix multilingual alias generation
Closes #14388
2026-01-21 10:34:32 +01:00
Bjørn Erik Pedersen c1b2e58bfb Fix file mount specifity issue within the same module
Fixes #14405
2026-01-20 13:15:33 +01:00
Bjørn Erik Pedersen 77214117ae deps: Upgrade github.com/gohugoio/gift v0.1.0 => v0.2.0 2026-01-20 09:33:30 +01:00
Bjørn Erik Pedersen 1a94731669 Move from github.com/disintegration/gift to github.com/gohugoio/gift
With some performance improvements upstream, there's improvements in memory usage. Note that the benchmark below is full site builds with image processing:

```
goos: darwin
goarch: arm64
pkg: github.com/gohugoio/hugo/resources/images
cpu: Apple M1 Pro
               │ benchcmp.bench │  feat-forkgift-14397.bench   │
               │     sec/op     │   sec/op     vs base         │
ImageResize-10      74.60m ± 4%   73.75m ± 4%  ~ (p=0.093 n=6)

               │ benchcmp.bench │     feat-forkgift-14397.bench      │
               │      B/op      │     B/op      vs base              │
ImageResize-10     32.38Mi ± 1%   29.31Mi ± 1%  -9.49% (p=0.002 n=6)

               │ benchcmp.bench │     feat-forkgift-14397.bench     │
               │   allocs/op    │  allocs/op   vs base              │
ImageResize-10      12.53k ± 1%   12.05k ± 1%  -3.82% (p=0.002 n=6)
```

Closes #14397
2026-01-19 22:18:45 +01:00
dependabot[bot] 18784711b5 build(deps): bump github.com/alecthomas/chroma/v2 from 2.22.0 to 2.23.0
Bumps [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) from 2.22.0 to 2.23.0.
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.22.0...v2.23.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 11:12:44 +01:00
Shravani Totre 2c611091fd warpc: Fix typed nil return in Start
Fixes a panic in Close() when initialization fails by ensuring Start returns a nil interface instead of a typed nil.

See #14372
2026-01-18 14:54:31 +01:00
Joe Mooring 8e9e04e395 resources/images: Stabilize order of valid sources in error message
Closes #14390
2026-01-18 10:27:40 +01:00
Joe Mooring 32334d098e hugolib: Fix relative alias generation
Closes #14381
2026-01-17 10:33:44 +01:00
Bjørn Erik Pedersen 8e2e60dd45 Add XMP and IPTC image metadata support
Rename resources/images/exif to resources/images/meta and extend
metadata support to include XMP and IPTC fields. Deprecate .Exif
in favor of .Meta. Update imagemeta dependency and add configuration
options for metadata extraction.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Closes #13146
2026-01-16 20:06:44 +01:00
Bjørn Erik Pedersen 2bc54df622 output: Add TestCanonical integration test 2026-01-16 19:08:54 +01:00
Bjørn Erik Pedersen 608ed09a0a Rename common/maps to common/hmaps (#14384)
Go's stdlid now has a maps package, which is very useful. We have been using imports on the form `xmaps "maps"`, but auto import tools doesn't handle this, which is annoying.

I have been thinking about this for some time, but have been holding back because of all the import changes. However, now is a good time to do this, with very little unmerged code.
2026-01-16 11:47:46 +01:00
dependabot[bot] 94f1ede3aa build(deps): bump golang.org/x/net from 0.48.0 to 0.49.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.48.0 to 0.49.0.
- [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-15 11:54:51 +01:00
dependabot[bot] ab374e3d22 build(deps): bump github.com/bep/lazycache from 0.8.0 to 0.8.1
Bumps [github.com/bep/lazycache](https://github.com/bep/lazycache) from 0.8.0 to 0.8.1.
- [Release notes](https://github.com/bep/lazycache/releases)
- [Commits](https://github.com/bep/lazycache/compare/v0.8.0...v0.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 17:23:07 +01:00
Joe Mooring 1a9133075c deps: Upgrade github.com/alecthomas/chroma v2.21.1 => v2.22.0
Closes #14368
2026-01-13 23:03:16 +01:00
Bjørn Erik Pedersen d36a8f5601 Decode webp.ImageConfig natively
This has 2 main benefits:

1. When you only need width/height, we don't need to spin up a WASM runtime.
2. When all processed images are cached in the file cache, we don't need to spin up a WASM runtime.

The above is only really visible for smaller sites, but it's annoying to potentially have a site building in 15ms take e.g. 200ms because of WASM startup time.

Fixes #14371
2026-01-13 17:58:34 +01:00
hugoreleaser 487b8e3af3 releaser: Prepare repository for 0.155.0-DEV
[ci skip]
2026-01-11 21:14:41 +00:00
hugoreleaser a6f99cca22 releaser: Bump versions for release of 0.154.5
[ci skip]
2026-01-11 20:53:23 +00:00
Bjørn Erik Pedersen a775488304 Fix some default site redirect woes
* The fix for #14357 yesterday sadly had the assumption that default language/version/role always was the first site in the sites matrix. This is common, but not always true.
* Also, that fix forgot to add a redirect the other way when `defaultContentLanguageInSubdir` was disabled, e.g. from `/en/` to `/`.
* This commit also renames config option `DisableDefaultDimensionRedirect` to `DisableDefaultSiteRedirect`. This is stricly a breaking change, but it's only been out for a day, and the old name didn't make much sense.

Fixes #14361
2026-01-11 21:50:10 +01:00
Rohan Hasabe 66ba63cd5f hugolib: Fix newly created shortcodes not found during server rebuild
When a new shortcode file was added during `hugo server`, the shortcode
was detected but not properly registered in the template cache. This caused
subsequent content edits using the shortcode to fail with "template for
shortcode not found" until the server was restarted.

The issue was that when a shortcode was added, only a glob identity for
dependent content was added to the changes list, but not the shortcode
file's own pathInfo. This meant RefreshFiles filtered it out and never
inserted it into the shortcodesByName cache.

The fix adds the shortcode file's pathInfo to changes (in addition to the
glob for dependent content), and corrects the glob pattern from
`shortcodes/...` to `/_shortcodes/...` to match the actual path format.

Fixes #14207
2026-01-11 19:33:00 +01:00
Joe Mooring eb06a3cd0c tpl/tplimpl: Remove trailing slash from void elements 2026-01-11 15:17:19 +01:00
hugoreleaser a798e3e724 releaser: Prepare repository for 0.155.0-DEV
[ci skip]
2026-01-10 15:48:05 +00:00
hugoreleaser fbcf0d52a1 releaser: Bump versions for release of 0.154.4
[ci skip]
2026-01-10 15:24:22 +00:00
dependabot[bot] a2b2a5a978 build(deps): bump github.com/goccy/go-yaml from 1.19.1 to 1.19.2
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.1 to 1.19.2.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.1...v1.19.2)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-10 15:45:39 +01:00
Barkın Balcı ec178ea463 tpl: Fix language resolution for markdown shortcodes
Fixes #14098
2026-01-10 15:45:03 +01:00
Bjørn Erik Pedersen 2d80b8a741 For multiple dimensions setups, fix alias handling and multihost publish path
* The alias handling was left as is when we added new dimensions in v0.152.0, which meant that if you had `defaultContentVersionInSubDir=true` set, the alias page in the root would not be correctly created.
* Also, if you had multihost setup with multiple dimensions, the publish would be incorrect: The language key was added last instead of first.
* The home page alias handling is reworked and made more robust, which also fixes some subtle issues:

* It now supports redircects for multiple HTML output formats for the home page.
* We skip creating aliases for disabled home pages.
* With potentially multiple output formats with one of them canonical, we added a new  `Canonical` method to the `Page.OutputFormats`, which allows you to do this in a template:

```handlebars
{{ with .OutputFormats.Canonical }}<link rel="{{ .Rel }}" href="{{ .Permalink }}">{{ end }}
```

Fixes #14354
Fixes #14356
2026-01-10 15:34:51 +01:00
hugoreleaser 5ea3e13db6 releaser: Prepare repository for 0.155.0-DEV
[ci skip]
2026-01-06 16:51:07 +00:00
hugoreleaser b1c1bd019f releaser: Bump versions for release of 0.154.3
[ci skip]
2026-01-06 16:30:17 +00:00
dependabot[bot] 866b8e5aa6 build(deps): bump github.com/yuin/goldmark from 1.7.13 to 1.7.16
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.13 to 1.7.16.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.13...v1.7.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 10:53:03 +01:00
hugoreleaser 409669fa48 releaser: Prepare repository for 0.155.0-DEV
[ci skip]
2026-01-02 16:30:54 +00:00
hugoreleaser f66d094446 releaser: Bump versions for release of 0.154.2
[ci skip]
2026-01-02 16:08:44 +00:00
Bjørn Erik Pedersen e9b9b36f46 Fix alpha/fuzzy border issue with new webp decoder for images with with transparent background
Fixes #14339
2026-01-02 17:06:10 +01:00
Joe Mooring b8a2c10d06 snap: Limit build platforms to amd64, arm64, ppc64el, and s390x
The Node 24 Snap has dropped support for armhf and riscv64.
2026-01-01 19:58:01 -08:00
Joe Mooring 601df38834 snap: Update to core 24 2026-01-01 15:51:32 -08:00
hugoreleaser 3bed212a2f releaser: Prepare repository for 0.155.0-DEV
[ci skip]
2026-01-01 17:52:38 +00:00
hugoreleaser e2fd6764be releaser: Bump versions for release of 0.154.1
[ci skip]
2026-01-01 17:32:20 +00:00
Bjørn Erik Pedersen 8f3527f61c Add WASM licensing information to README
Added a section on licensing for the Hugo project, detailing the licenses of bundled libraries.
2026-01-01 18:27:57 +01:00
Bjørn Erik Pedersen 09048aad76 Fix partial decorator detection in partial with blocks with outer range break or continue
E.g.:

```handlebars
{{- $items := slice "a" "b" "c" }}
  {{- range $items }}
    {{- with partial "b" . -}}
	 {{break}}
	{{- else }}
	  else: {{ . -}}
	{{- end }}
  {{- end }}
 ```

Fixes #14333
2026-01-01 18:05:10 +01:00
hugoreleaser f42c422a12 releaser: Prepare repository for 0.155.0-DEV
[ci skip]
2025-12-31 13:09:12 +00:00
hugoreleaser 0b71db299a releaser: Bump versions for release of 0.154.0
[ci skip]
2025-12-31 12:45:55 +00:00
Bjørn Erik Pedersen 5c7fad23a3 tpl/collections: Fix apply to work with built-in funcs like len
Fixes #13418
2025-12-31 13:09:04 +01:00
Bjørn Erik Pedersen 700bb78198 Remove Linode sponsor from README
Removed Linode sponsor link from the README.
2025-12-30 19:54:11 +01:00
majiayu000 d3b5d47a43 helpers: Limit verbose watch output for better readability
Limits the number of root groups shown in 'Watching for changes' output
to 10 maximum, with a summary message for remaining paths. This prevents
the message from becoming excessively long when watching sites with many
mount points.

Fixes #14277

Signed-off-by: majiayu000 <1835304752@qq.com>
2025-12-30 19:44:35 +01:00
Bjørn Erik Pedersen 86cd1838ff tpl/reflect: Make the IsImageResource implementation less technical
And closer to the documentation. The old one was correct, but a bit too tied to the internal implementation.
2025-12-30 19:43:55 +01:00
Joe Mooring 871da3370a internal/warpc: Increase WebP memory limit to 384 MiB
Closes #14309
2025-12-30 16:51:58 +01:00
dependabot[bot] 2637aa1540 build(deps): bump github.com/tetratelabs/wazero from 1.10.1 to 1.11.0
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-30 16:01:32 +01:00
Bjørn Erik Pedersen bf1d20d735 Revert "resources/page: Fix slugorcontentbasename for section pages"
This reverts commit 25c7c18f9f.

See #14104
See #14325
2025-12-30 16:00:37 +01:00
Bjørn Erik Pedersen 1deec99bba Update tpl/reflect/reflect.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-30 16:00:19 +01:00
Bjørn Erik Pedersen b7bb557c6a Add reflect.Is{Page,Site,Resource,ImageResource}
Fixes #14307
2025-12-30 16:00:19 +01:00
Bjørn Erik Pedersen 7c19c196c3 Allow partials to work as decorators
Fixes #13193
2025-12-30 15:58:55 +01:00
hugoreleaser 555443b64a releaser: Prepare repository for 0.154.0-DEV
[ci skip]
2025-12-30 10:48:50 +00:00
hugoreleaser 1f2de189ed releaser: Bump versions for release of 0.153.5
[ci skip]
2025-12-30 10:27:17 +00:00
Bjørn Erik Pedersen c6ae33c61d images: Add compression option to image config and clean up some of the options handling
The original idea of setting quality to 0 for lossless WebP was didn't allow a setting that made sense for both JPEG and WebP.

Note that the above didn't really work at all, so this should not break anything.
2025-12-29 21:38:19 +01:00
Bjørn Erik Pedersen edeebf0d33 config: Fix cascade per language in hugo.toml regression
An onfurtunate side effect of the fix in v0.153.4.

Fixes #14321
2025-12-29 20:48:12 +01:00
Simon Heimlicher ea9675f6e7 images: Fix WebP quality and hint parameters being ignored
Fixes #14316
2025-12-29 16:47:43 +01:00
hugoreleaser b1f7e35a98 releaser: Prepare repository for 0.154.0-DEV
[ci skip]
2025-12-28 21:55:32 +00:00
hugoreleaser a0d4e1fb26 releaser: Bump versions for release of 0.153.4
[ci skip]
2025-12-28 21:34:16 +00:00
Bjørn Erik Pedersen 96777d9b89 Set cascade target to the content matrix if not set in the cascade itself
Fixes #14310

Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
2025-12-28 22:30:55 +01:00
hugoreleaser 25126e5f87 releaser: Prepare repository for 0.154.0-DEV
[ci skip]
2025-12-26 16:33:20 +00:00
hugoreleaser 179034abbb releaser: Bump versions for release of 0.153.3
[ci skip]
2025-12-26 15:33:04 +00:00
dependabot[bot] 96ac146eef build(deps): bump github.com/bep/imagemeta from 0.12.0 to 0.12.1
Bumps [github.com/bep/imagemeta](https://github.com/bep/imagemeta) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/bep/imagemeta/releases)
- [Commits](https://github.com/bep/imagemeta/compare/v0.12.0...v0.12.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-26 11:36:41 +01:00
Bjørn Erik Pedersen d4c0e4452c Fix error with _content.gotmpl file with index.md siblings
Regression introduced in Hugo 0.153.0.

Note that this is a very uncommon setup, but it worked before.

Fixes #14299
2025-12-25 14:22:14 +01:00
hugoreleaser 418156ec3e releaser: Prepare repository for 0.154.0-DEV
[ci skip]
2025-12-22 17:15:37 +00:00
hugoreleaser 798533a201 releaser: Bump versions for release of 0.153.2
[ci skip]
2025-12-22 16:53:01 +00:00
Bjørn Erik Pedersen a8c5d0d00d Fix "image: unknown format" error
Fixes #14295
2025-12-22 17:49:44 +01:00
Bjørn Erik Pedersen a94a941fe3 modules: Remove extended edition check
With WebP support now in the standard Hugo binary, this warning has too many false positives.

Fixes #14284
2025-12-22 11:12:10 +01:00
Joe Mooring 385d1a1342 misc: Update edition comparison and guidance in README.md 2025-12-20 21:37:30 +01:00
hugoreleaser d30bd50467 releaser: Prepare repository for 0.154.0-DEV
[ci skip]
2025-12-20 15:38:51 +00:00
hugoreleaser 8e6cac8462 releaser: Bump versions for release of 0.153.1
[ci skip]
2025-12-20 15:15:53 +00:00
Bjørn Erik Pedersen 4085ee9340 Handle PNG named *.webp
Fixes #14288
2025-12-20 16:12:15 +01:00
Bjørn Erik Pedersen 168bf17e70 Revert deprecation logging for contentDir per language
It was accidentally introduced because we compile it to file mounts, and `lang` on file mounts is deprecated in favor of `sites.matrix`.

But contentDir per language is something we have had for a long time, and is to useful too to deprecate.

Fixes #14287
2025-12-20 14:59:46 +01:00
Bjørn Erik Pedersen 80ea90c880 images: Add some more PNG tests
To try to figure out #14288.
2025-12-20 13:42:21 +01:00
Bjørn Erik Pedersen f740d7cfe0 Fix panic when 404 is backed by a content file
Fixes #14283
2025-12-20 13:42:21 +01:00
Joe Mooring 5f46da6e2a internal/warpc: Increase WebP memory limit to 256 MiB
Closes #14282


---------

Co-authored-by: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
2025-12-20 12:16:36 +01:00
hugoreleaser fe64a68225 releaser: Prepare repository for 0.154.0-DEV
[ci skip]
2025-12-19 10:35:25 +00:00
hugoreleaser b4128babb8 releaser: Bump versions for release of 0.153.0
[ci skip]
2025-12-19 10:14:01 +00:00
Bjørn Erik Pedersen 790a5b7014 hugoreleaser: Updage macospkgremote to increase notarization timeout
Apple's servers seems to be busy today.
2025-12-19 11:11:24 +01:00
Bjørn Erik Pedersen 70ce2446b5 Merge commit '08e1ea5c709d3d49bdc3ce3c21e8fa05a33150d0' 2025-12-19 10:07:30 +01:00
Bjørn Erik Pedersen 08e1ea5c70 Squashed 'docs/' changes from 71f739460..1ad3c75ad
1ad3c75ad theme: Run ncu -u to update dependencies (#3311)
2942d3753 content: Update version refs
e298fdfaf content: Remove ref to old version
c1fc876ea content: Improve taxonomic weight example
d706a013d content: Document additional functions
5751c37cc content: Standardize string function signatures
d65e6fae7 misc: Add data/docs.yaml to ignorePaths in cspell config
ba1f98a32 content: Improve collections.First/Last string examples
d7b5a2efd content: Improve collections.First/Last examples
e94e49db2 content: Add Smart Hugo to editor plugins
af204597a content: Clarify config description
9d1cb33cd content: Fix typo
dded23962 content: Clarify note on rare use case for PageRef method
8af9e3be4 content: Update the Homebrew installation description
be56390b3 content: Clarify command description
fa228e9f0 content: Clarify AsciiDoc extensions setting description
3a23a4af2 docs: Add best practices for manual summary marker placement
050d7c75e misc: Fix cspell configuration
74642b18d content: Miscellaneous edits
75a2c8425 content: Add missing reference definitions
6303f27e1 content: Improve description of the SITE.Home method
4dfbd647f Revert the minimal analytics changes (#3265)
c369e534d theme: Move tracking to turbo:load
88b451022 theme: Move tracking to turbo:render
7040dc5ee Move to minimalanalytics for GA tracking
4e8a523fb content: Update KaTeX link attributes to v0.16.25 (phase 2)
21046b942 content: Update KaTeX link attributes to v0.16.25 (phase 1)
b8df9d941 theme: Use Path for the glossary link destination
acd6c2954 theme: Use page Path when generating Markdown links
543f78340 content: Fix formatting
aef89d192 content: Correct page collection quick-reference data
08645236a content: Use RSS output format in shortcode lookup example
58654c106 content: Fix archetype lookup order
9f79bacc4 content: Describe valid uses of the nil keyword
41f139d14 theme: Remove the root view transition
6ab407bb9 content: Improve links on lang.Translate page
91e724065 content: Add missing fragments to links in the module mounts description
2f6b4a58c content: Clarify ByCount sort order
8594590b2 Update netlify.toml
811574060 content: Update version references
a12bcf7dd content: Update version references
23ca199ed Update netlify.toml
5d3da2351 content: Update version references
1617ee75b misc: Update Netlify config to build with v0.152.0
6467bda93 content: Remove outdated new-in badges
4b3ef4bd5 content: Improve description of default mount behavior
5a861f500 content: Revise partials.IncludeCached description and signature
2ad9d705a content: Correct the partials.IncludeCached signature
9f611b879 Update netlify.toml
ca21f5fbd content: Add warning about automatic summaries
2065fadde content: Update commit message guidelines
a0784f261 content: Remove outdated new-in badges
ce5a249f2 content: Remove Codeberg Pages documentation
3424b95f2 content: Update CLI docs
27fd350a6 content: Change KaTeX version references to 0.16.23
ba8c5fd3b content: Change MathJax version reference from v3 to v4
c83465222 content: Fix broken link
c6cde1074 content: Update version references
467277939 content: Fix typo
e424b6b19 content: Document new features in v0.151.0
a46898f79 Update netlify.toml
f8407c3b5 content: Enhance content view template details
974175690 content: Optionally disable whitespace trimming in fenced code blocks
25bffc4c4 Update netlify.toml
98e9ef5f0 theme: Move initColorScheme() to head
a1566495b theme: Preload fonts (#3209)
c0271d266 content: Add missing word in deploy-with-rsync.md
47099c4fa content: Remove outdated new-in badges
439b5c18b content: Fix collections.D seed example
d2486ba67 content: Update description of IsNode method
4de8815e9 content: Update description of the cascade front matter field
0801e9b3f content: Fix broken link
9bbbfb59d content: Update Netlify hosting guide and version refs
4f444fb53 misc: Update docs.yaml
a7a9a7563 Update module.md
a7909fe37 Update netlify.toml
47a2c2038 content: Update collections.D page to remove casting note
ecca5ef48 content: Update summaryLength description with default value
310e11195 content: Change alias example
bd6b53b5d content: Update minify config documentation
21bb0bb66 Update netlify.toml
68912f1a3 content: Update version reference
28a47a50f content: Fix missing link to latest release under Prebuilt Binaries
8eac2a120 theme: Revise inline code span styling
2e71b6d0a Adjust sponsor
d93340a78 content: Update documentation guide
9f6213ebe content: Fix glob pattern-alternatives syntax in cascade.md
1f61fd6be content: Add seed examples to collections.D
65385a5d0 content: Fix Cloudflare deploy not_found_handling value
4f70df235 content: Fix typo
fc216f2d9 content: Fix broken link
e98b340f2 Update cascade.md
e316dbf93 content: Update version references
f8e863e97 misc: Remove textlint config file
3b9c3e2a1 content: Miscellaneous updates related to v0.149.0
3ccc439fc Update netlify.toml
07f5140e8 Merge branch 'tempv0.149.0'
17f8d33d8 content: Add format option to transform.Unmarshal
a6a971596 content: Document the collections.D function
66eee712b content: Update front matter configuration example
863945e13 content: Update fmt.Println example
1d9a92bc7 theme: Don't exclude current section from related content
1386d8ed2 Update introduction.md
0ac89b015 content: Replace inline URLs with autolinks
54fad1e01 content: Fix typo
e09f6b0c5 content: Wrap calls to eturl shortcode in angle brackets
23efd9766 Merge commit 'bfa74537929f409fca841540b971125b7678963a'
e98754e72 resources/page: Add :sectionslug and :sectionslugs permalink tokens

git-subtree-dir: docs
git-subtree-split: 1ad3c75ad0e997d66598c1453db9989fde7d9680
2025-12-19 10:04:19 +01:00
Bjørn Erik Pedersen 65d43e1d0d resources/images: Don't trust the file extension when decoding JPEG and PNG images
Which is it behaved before we added the extended WebP support.

Testing on some sites shows that it's not uncommon to store JPEGs with PNG extensions and vice versa.
This an error situation that needs to be reported, but let us push that to a future Hugo version to reduce the noise in this one.
2025-12-18 19:36:48 +01:00
Bjørn Erik Pedersen 65a76664e3 Add full filename to image processing error messages if possible
Fixes #14278
2025-12-18 17:46:26 +01:00
Bjørn Erik Pedersen da5b1fcf8f tailwindcss: Add referece to skipInlineImportsNotFound when import not found in assets
See #14273
2025-12-18 10:57:01 +01:00
dependabot[bot] 39649dce49 build(deps): bump github.com/goccy/go-yaml from 1.19.0 to 1.19.1
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-18 10:12:10 +01:00
dependabot[bot] 614fb16ec6 build(deps): bump github.com/alecthomas/chroma/v2 from 2.21.0 to 2.21.1
Bumps [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) from 2.21.0 to 2.21.1.
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.21.0...v2.21.1)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-18 10:11:52 +01:00
Bjørn Erik Pedersen 0637adb239 Improve error messages for template failures 2025-12-17 21:12:15 +01:00
Bjørn Erik Pedersen 0bf61353f6 Improve error handling/messages in Hugo Pipes
Fixes #14257
Closes #14270
2025-12-17 21:12:15 +01:00
Bjørn Erik Pedersen b82e496c13 Fix some outdated front matter
Fixes #14271
2025-12-17 18:51:02 +01:00
dependabot[bot] 33542d3966 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.58.2 to 1.58.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.58.2...service/s3/v1.58.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.58.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 13:02:00 +01:00
Bjørn Erik Pedersen 588d20b315 deps: Upgrade github.com/alecthomas/chroma/v2 v2.20.0 => v2.21.0
Closes #14266
2025-12-17 11:18:56 +01:00
dependabot[bot] b33730293d build(deps): bump github.com/aws/aws-sdk-go-v2 from 1.40.1 to 1.41.0
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.40.1 to 1.41.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.40.1...v1.41.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 10:23:06 +01:00
dependabot[bot] e6a3f1d62b build(deps): bump github.com/evanw/esbuild from 0.27.1 to 0.27.2
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.27.1 to 0.27.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.27.1...v0.27.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 10:07:25 +01:00
Bjørn Erik Pedersen 573ecb530f images: Add a webp test with bg color 2025-12-17 09:41:25 +01:00
Bjørn Erik Pedersen 9937a5dcaa tpl/css: Deprecate libsass in favor of dartsass (note)
The upstream library is now EOL and the repo is archived. Also, it's time for us to move away from the extended CGO build and for Hugo users not already done that, move to https://gohugo.io/functions/css/sass/#dart-sass

https://github.com/sass/libsass

Closes #14261
2025-12-16 22:31:13 +01:00
Bjørn Erik Pedersen 1b4514e020 Encode and Decode using the libwebp library via WASM with animation support
Fixes #10030
Fixes #8500
Fixes #12843
Fixes #8879
Fixes #12842
2025-12-16 19:29:16 +01:00
Joe Mooring 429e57201d config/allconfig: Correct error message
Closes #14259
2025-12-15 18:09:57 +01:00
Joe Mooring fa7d37f0c8 tpl: Add missing functions to init files
Closes #14249
2025-12-13 18:32:26 +01:00
Bjørn Erik Pedersen fae49a22f1 github: Add some known humans to the AI whitelist 2025-12-13 18:29:28 +01:00
dependabot[bot] 364d237b51 build(deps): bump golang.org/x/image from 0.33.0 to 0.34.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/image/compare/v0.33.0...v0.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-10 12:12:56 +01:00
dependabot[bot] 079f3eb8d1 build(deps): bump golang.org/x/tools from 0.39.0 to 0.40.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.39.0 to 0.40.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 10:33:42 +01:00
dependabot[bot] 4e6f2b6791 build(deps): bump github.com/tdewolff/minify/v2 from 2.24.7 to 2.24.8
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.7 to 2.24.8.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.7...v2.24.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 10:32:38 +01:00
dependabot[bot] dff5e168a9 build(deps): bump gocloud.dev from 0.43.0 to 0.44.0
Bumps [gocloud.dev](https://github.com/google/go-cloud) from 0.43.0 to 0.44.0.
- [Release notes](https://github.com/google/go-cloud/releases)
- [Commits](https://github.com/google/go-cloud/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: gocloud.dev
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 18:03:07 +01:00
dependabot[bot] c82a035bf1 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.58.1 to 1.58.2.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.58.1...service/s3/v1.58.2)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.58.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 15:58:01 +01:00
dependabot[bot] 86b01eda10 build(deps): bump github.com/evanw/esbuild from 0.27.0 to 0.27.1
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.27.0 to 0.27.1.
- [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.27.0...v0.27.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-05 15:57:45 +01:00
Bjørn Erik Pedersen e2e64aeec5 Fix server rebuilds on editing content with Chinese terms
Fixes #14240
2025-12-05 15:53:48 +01:00
dependabot[bot] 9e7182e9e6 build(deps): bump github.com/aws/aws-sdk-go-v2 from 1.40.0 to 1.40.1
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.40.0 to 1.40.1.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.40.0...v1.40.1)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.40.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 09:59:53 +01:00
dependabot[bot] cf3ad62c3b build(deps): bump github.com/spf13/cobra from 1.9.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.9.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 09:59:04 +01:00
Bjørn Erik Pedersen c1cc65fcb9 release: Skip pushing stable and docs update for pre-releases 2025-12-03 11:51:37 +01:00
Bjørn Erik Pedersen 663075920a release: Support alpha, beta, and RC releases 2025-12-02 18:44:42 +01:00
Joe Mooring 596517a259 snap: Address snapcraft deprecations 2025-12-01 23:44:24 -08:00
dependabot[bot] bf42138661 build(deps): bump github.com/goccy/go-yaml from 1.18.0 to 1.19.0
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 12:05:55 +01:00
dependabot[bot] 57fecb6b71 build(deps): bump github.com/olekukonko/tablewriter from 1.1.1 to 1.1.2
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.1 to 1.1.2.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.1...v1.1.2)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 12:05:40 +01:00
dependabot[bot] 6d0ab6a34e build(deps): bump github.com/JohannesKaufmann/html-to-markdown/v2
Bumps [github.com/JohannesKaufmann/html-to-markdown/v2](https://github.com/JohannesKaufmann/html-to-markdown) from 2.4.0 to 2.5.0.
- [Release notes](https://github.com/JohannesKaufmann/html-to-markdown/releases)
- [Commits](https://github.com/JohannesKaufmann/html-to-markdown/compare/v2.4.0...v2.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 12:04:55 +01:00
Bjørn Erik Pedersen 6be463b586 langs/i18n: Prefer languageCode when picking translation file
Fixes #14204
Closes #14217

Co-authored-by: Patrice Chalin <pchalin@gmail.com>
2025-11-28 21:24:28 +01:00
Bjørn Erik Pedersen 9e24b5645d Add entitlements for WebAssembly for macOS Tahoe
Fixes #14220
2025-11-27 20:43:03 +01:00
Bjørn Erik Pedersen 45b67f6c18 testscripts: Move layouts file to new structure 2025-11-27 20:41:56 +01:00
Joe Mooring 5e649eb4d0 resources: Skip integration test if Dart Sass is not installed 2025-11-27 19:32:23 +01:00
dependabot[bot] 0de8f8607b build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.57.0 to 1.58.1.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.57.0...service/s3/v1.58.1)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.58.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 13:22:29 +01:00
Bjørn Erik Pedersen 3073fd5ccf testing: Replace legacy config.toml with hugo.toml in most tests 2025-11-27 12:08:08 +01:00
Bjørn Erik Pedersen b9b304a126 testing: Port integration tests to new templates structure
As introduced in  v0.146.0. Keep some legecy test to preserve backwards compatibility.
2025-11-27 12:00:15 +01:00
Bjørn Erik Pedersen 8be30ef2cb github: Correct dependabot => dependabot[bot] 2025-11-27 09:48:54 +01:00
Joe Mooring 565a10cbf0 config/privacy: Change GoogleAnalytics.RespectDoNotTrack default to true
Closes #13307
2025-11-27 09:46:53 +01:00
Bjørn Erik Pedersen 438f113f83 Add signed and notarized MacOS pkg builds
Closes #14135
2025-11-26 16:48:51 +01:00
Bjørn Erik Pedersen dab54058ea Upgrade to Go 1.25.4 2025-11-26 16:38:47 +01:00
Joe Mooring 1a1b062a01 tpl/urls: Add PathEscape and PathUnescape functions
Closes #14209
2025-11-25 23:53:04 +01:00
Bjørn Erik Pedersen 7a43b928a6 Fix slow server startup of very big content trees
As in content trees with 10 thousand of directories and more.

A benchmark with the bottle neck code in `helpers.ExtractAndGroupRootPaths`:

```
                            │ cmp20251125.bench │ fix-extractandgrouproot-14211.bench │
                            │      sec/op       │    sec/op     vs base               │
ExtractAndGroupRootPaths-10     1282818.8µ ± 8%   493.8µ ± 38%  -99.96% (p=0.002 n=6)

                            │ cmp20251125.bench │ fix-extractandgrouproot-14211.bench │
                            │       B/op        │     B/op      vs base               │
ExtractAndGroupRootPaths-10       3343.8Ki ± 0%   146.3Ki ± 0%  -95.63% (p=0.002 n=6)

                            │ cmp20251125.bench │ fix-extractandgrouproot-14211.bench │
                            │     allocs/op     │  allocs/op    vs base               │
ExtractAndGroupRootPaths-10        20.043k ± 0%    2.979k ± 0%  -85.14% (p=0.002 n=6)
```

For test project that started this (a 60k directory conent tree), the server startup with no rendering, wen from 1.5 minutes to less than 4 seconds:

```
hugop server --renderSegments none                        main ✚ ✖ ✱ ◼
Watching for changes in /Users/bep/dev/sites/hugotestsites/60k/content/{section0,section1,section10,section100,section101,section102,section103,section104,section105,section106,...}
Watching for changes in /Users/bep/dev/sites/hugotestsites/60k/layouts/_default
Watching for config changes in /Users/bep/dev/sites/hugotestsites/60k/config.toml
Start building sites …
hugo v0.153.0-DEV-7e27c303904ed8b221d6a5a4fc9a764bb7b2935b darwin/arm64 BuildDate=2025-11-25T15:02:58Z

                  │ EN
──────────────────┼────
 Pages            │  0
 Paginator pages  │  0
 Non-page files   │  0
 Static files     │  0
 Processed images │  0
 Aliases          │  0
 Cleaned          │  0

Built in 3884 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at //localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
```

Note that the output may be a little different and a little more verbose than before., but the information is correct and this implementation is significantly faster and simpler.

Fixes #14211
2025-11-25 22:55:25 +01:00
Bjørn Erik Pedersen 555dfa207a Speedup and simplify page assembly for deeper content trees
This commit moves to a forked version go-radix (fork source has not had any updates in 3 years.), whith 2 notable changes:

* It's generic (using Go generics) and thus removes a lot of type conversions/assertions.
* It allows nodes to be replaced during walk, which allows to partition the tree for parallel processing without worrying about locking.

For this repo, this means:

* The assembly step now processes nested sections in parallel, which gives a speedup for deep content trees with a slight allocation penalty (see benchmarks below).
* Nodes that needs to be reinserted are inserted directly.
* Also, there are some drive-by fixes of some allocation issues, e.g. avoid wrapping mutexes in returned anonomous functions, a common source of hidden allocations.

```
                                                                                   │ master.bench │           perf-p3.bench            │
                                                                                   │    sec/op    │   sec/op     vs base               │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10     6.958m ± 3%   7.015m ± 3%        ~ (p=0.589 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    14.25m ± 1%   14.56m ± 8%        ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10    48.07m ± 3%   49.23m ± 3%        ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10    66.66m ± 4%   66.47m ± 6%        ~ (p=0.485 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10    59.57m ± 4%   50.73m ± 5%  -14.85% (p=0.002 n=6)
geomean                                                                               28.54m        27.92m        -2.18%

                                                                                   │ master.bench │           perf-p3.bench            │
                                                                                   │     B/op     │     B/op      vs base              │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10    4.513Mi ± 0%   4.527Mi ± 0%  +0.33% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10   15.35Mi ± 0%   15.49Mi ± 0%  +0.94% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10   62.50Mi ± 0%   63.19Mi ± 0%  +1.10% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10   86.78Mi ± 0%   87.73Mi ± 0%  +1.09% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10   62.96Mi ± 0%   63.66Mi ± 0%  +1.12% (p=0.002 n=6)
geomean                                                                              29.84Mi        30.11Mi       +0.92%

                                                                                   │ master.bench │           perf-p3.bench           │
                                                                                   │  allocs/op   │  allocs/op   vs base              │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10     60.44k ± 0%   60.97k ± 0%  +0.87% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    205.8k ± 0%   211.4k ± 0%  +2.70% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10    831.1k ± 0%   858.3k ± 0%  +3.27% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10    1.157M ± 0%   1.197M ± 0%  +3.41% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10    839.9k ± 0%   867.8k ± 0%  +3.31% (p=0.002 n=6)
geomean                                                                               398.5k        409.3k       +2.71%
```
2025-11-25 11:37:05 +01:00
Joe Mooring 3d21b0687b markup/asciidocext: Improve Asciidoctor integration
Fixes an issue where improper attribute derivation from the page's
relative permalink caused failures with `outdir`, `imagesoutdir`, and
`imagesdir` when `markup.asciidocext.workingFolderCurrent` is enabled.
The updated logic now correctly handles:

- Multi-byte characters
- Multilingual multi-host sites
- Site builds from a subdirectory
- Pages using ugly URLs

Supports diagram caching as implemented in v3.1.0 of the asciidoctor-diagram
extension:

- Enables caching by default
- Sets default cache location to the compiled value of caches.misc.dir

Reduces duration of integration tests by:

- Generating GoAT diagrams instead of Ditaa diagrams
- Taking advantage of asciidoctor-diagram caching

Closes #9202
Closes #10183
Closes #10473
Closes #14160
2025-11-24 23:03:53 +01:00
Bjørn Erik Pedersen 34b0c15a54 gemini: Remove styleguide.md (for now) 2025-11-24 21:00:24 +01:00
Bjørn Erik Pedersen 9dc38e917c github: Reenable Gemini, but no auto code review 2025-11-24 20:37:19 +01:00
Bjørn Erik Pedersen b592d34efe github: Partition tests by their root 2025-11-24 19:42:28 +01:00
Bjørn Erik Pedersen 18a2afa519 github: Make the clean commands work 2025-11-24 19:04:33 +01:00
Bjørn Erik Pedersen 0b0c827d5b github: Also test the root package (left out in previous commit) 2025-11-24 18:38:53 +01:00
Bjørn Erik Pedersen 30dc75e620 github: More disk space saving optimizations 2025-11-24 18:38:21 +01:00
Joe Mooring 6bc8e6f491 github: Add PR Template 2025-11-21 20:22:50 +01:00
Bjørn Erik Pedersen cda4d75187 Adjust benchmark 2025-11-21 16:19:24 +01:00
Bjørn Erik Pedersen 84950ed2b2 tpl/openapi: Add support for OpenAPI external file references
Fixes #8067
2025-11-21 16:03:05 +01:00
dependabot[bot] 2b337cd212 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.56.3 to 1.57.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/ecs/v1.56.3...service/s3/v1.57.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 11:49:04 +01:00
dependabot[bot] cc61d00df9 build(deps): bump golang.org/x/image from 0.32.0 to 0.33.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/image/compare/v0.32.0...v0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 11:48:42 +01:00
dependabot[bot] 8ef2c61985 build(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.43.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 11:47:10 +01:00
Bjørn Erik Pedersen a8ed613742 github: Skip dependabot for AI Watchdog workflow 2025-11-21 11:46:26 +01:00
dependabot[bot] b91688034f build(deps): bump github.com/tdewolff/minify/v2 from 2.24.5 to 2.24.7
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.5 to 2.24.7.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.5...v2.24.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 11:27:13 +01:00
Bjørn Erik Pedersen b971b7f911 github: Remove the 386 test step in GitHub test workflow
Fixes #14201
2025-11-21 11:00:50 +01:00
Bjørn Erik Pedersen 24210dcad9 github: Remove test binaries after CI test runs 2025-11-21 10:59:03 +01:00
Bjørn Erik Pedersen b037b9300c github: Fix "no space left on device" issue in CI 2025-11-20 14:26:52 +01:00
dependabot[bot] 268046732e build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.56.0 to 1.56.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.56.0...service/ecs/v1.56.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.56.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 10:39:15 +01:00
dependabot[bot] 8ce80f755d build(deps): bump golang.org/x/tools from 0.38.0 to 0.39.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.38.0 to 0.39.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.38.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 10:38:53 +01:00
dependabot[bot] 4baa95ba65 build(deps): bump github.com/tetratelabs/wazero from 1.10.0 to 1.10.1
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 10:36:59 +01:00
Bjørn Erik Pedersen 0f94274a28 github: Adjust watchdog run logic (now with correct spelling) 2025-11-19 16:01:38 +01:00
Bjørn Erik Pedersen 5f9c3a3e4c github: Adjust watchdog run logic (again) 2025-11-19 15:59:33 +01:00
Bjørn Erik Pedersen 51d77a6ca8 github: Adjust watchdog run logic 2025-11-19 15:49:52 +01:00
Bjørn Erik Pedersen d25b619ce2 github: Only run AI Watchdog when the PR is ready for review 2025-11-19 15:32:28 +01:00
dependabot[bot] b81861fd21 build(deps): bump github.com/bits-and-blooms/bitset
Bumps [github.com/bits-and-blooms/bitset](https://github.com/bits-and-blooms/bitset) from 1.22.0 to 1.24.4.
- [Release notes](https://github.com/bits-and-blooms/bitset/releases)
- [Commits](https://github.com/bits-and-blooms/bitset/compare/v1.22.0...v1.24.4)

---
updated-dependencies:
- dependency-name: github.com/bits-and-blooms/bitset
  dependency-version: 1.24.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:55:59 +01:00
dependabot[bot] 15cce89e0e build(deps): bump github.com/olekukonko/tablewriter from 1.1.0 to 1.1.1
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.0 to 1.1.1.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:28:46 +01:00
dependabot[bot] 187e683bb2 build(deps): bump golang.org/x/text from 0.30.0 to 0.31.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.30.0...v0.31.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:28:05 +01:00
dependabot[bot] 432078a6af build(deps): bump github.com/evanw/esbuild from 0.25.12 to 0.27.0
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.25.12 to 0.27.0.
- [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.25.12...v0.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 13:24:04 +01:00
Bjørn Erik Pedersen 4a76d8c611 Update aiwatchdog.yml 2025-11-19 12:37:14 +01:00
Bjørn Erik Pedersen d837eff08f Update aiwatchdog.yml 2025-11-19 12:29:22 +01:00
Bjørn Erik Pedersen 323e306bd7 Update aiwatchdog.yml 2025-11-19 12:27:47 +01:00
Bjørn Erik Pedersen 2047638a2c github: Add label to AI suspects and do not fail when confident
The latter is mostly to get the  dependabot PRs through. We may add a filter to that later.
2025-11-19 12:13:11 +01:00
Bjørn Erik Pedersen 38efb702aa github: Adjust AI Watchdog workflow to make it run PRs from forks 2025-11-18 20:31:04 +01:00
Bjørn Erik Pedersen bdf8b1a915 github: Adjust workflow permissions 2025-11-18 19:37:32 +01:00
Bjørn Erik Pedersen f33c1a3fdb performance: Misc allocation improvements
```
                               │ stash.bench  │      perf-allloc20251118.bench      │
                               │    sec/op    │    sec/op     vs base               │
CreateShortcodePlaceholders-10   54.16n ± ∞ ¹   39.06n ± ∞ ¹  -27.87% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                               │ stash.bench │     perf-allloc20251118.bench     │
                               │    B/op     │    B/op      vs base              │
CreateShortcodePlaceholders-10   52.00 ± ∞ ¹   48.00 ± ∞ ¹  -7.69% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                               │ stash.bench │     perf-allloc20251118.bench      │
                               │  allocs/op  │  allocs/op   vs base               │
CreateShortcodePlaceholders-10   2.000 ± ∞ ¹   1.000 ± ∞ ¹  -50.00% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95
````
2025-11-18 19:17:15 +01:00
Bjørn Erik Pedersen fee095768c hugolib/doctree: Simplify lock setup in SimpleThreadSafeTree to reduce read allocation
```
                                      │    sec/op    │   sec/op     vs base               │
SimpleThreadSafeTree/Get-10             45.80n ± 11%   27.71n ± 7%  -39.51% (p=0.002 n=6)
SimpleThreadSafeTree/Insert-10          40.96n ±  1%   40.36n ± 0%   -1.47% (p=0.002 n=6)
SimpleThreadSafeTree/LongestPrefix-10   47.12n ±  0%   32.60n ± 1%  -30.81% (p=0.002 n=6)
geomean                                 44.54n         33.16n       -25.57%

                                      │ cmp152.bench │       perf-allloc20251118.bench        │
                                      │     B/op     │    B/op     vs base                    │
SimpleThreadSafeTree/Get-10               16.00 ± 0%    0.00 ± 0%  -100.00% (p=0.002 n=6)
SimpleThreadSafeTree/Insert-10            8.000 ± 0%   8.000 ± 0%         ~ (p=1.000 n=6) ¹
SimpleThreadSafeTree/LongestPrefix-10     16.00 ± 0%    0.00 ± 0%  -100.00% (p=0.002 n=6)
````
2025-11-18 19:17:15 +01:00
Bjørn Erik Pedersen 56d7925a97 github: Add ai-watchdog workflow and update other workflows' versions
See #14147
2025-11-18 19:15:14 +01:00
dependabot[bot] 38608c7c14 build(deps): bump google.golang.org/api from 0.251.0 to 0.255.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.251.0 to 0.255.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.251.0...v0.255.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.255.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 12:14:13 +01:00
dependabot[bot] d75f0dc6a5 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.54.4 to 1.56.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.54.4...service/s3/v1.56.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.56.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 12:14:02 +01:00
dependabot[bot] 579362efd4 build(deps): bump github.com/tetratelabs/wazero from 1.9.0 to 1.10.0
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 12:12:04 +01:00
Joe Mooring c6b6910a79 tpl/collections: Improve collections.D
Changes:

- If n > hi, return the full, sorted range [0, hi) of size hi.
- Throw errors when seed, n, or hi are < 0
- Improve error messages

Closes #14143
2025-11-18 11:42:33 +01:00
Bjørn Erik Pedersen ca4025405c Optimize memory allocations for sites matrix vector stores
By

* Caching common sites matrix setups (e.g. the single site in single site setups).
* Adding a fast path to IntSets.HasAnyVector for the common case of single vector input.

```
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10   31.62m ± 46%   30.68m ± 42%  ~ (p=0.310 n=6)

                                                                                   │ master.bench │          perfcommon.bench          │
                                                                                   │     B/op     │     B/op      vs base              │
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10   31.98Mi ± 0%   31.24Mi ± 0%  -2.30% (p=0.002 n=6)

                                                                                   │ master.bench │         perfcommon.bench          │
                                                                                   │  allocs/op   │  allocs/op   vs base              │
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10    460.9k ± 0%   419.9k ± 0%  -8.90% (p=0.002 n=6)
````
2025-11-17 16:01:37 +01:00
Bjørn Erik Pedersen 44b5f13f86 gemini: Disable auto PR codereviews for now
I'm assuming it's still possible to trigger commands like `/gemini review` manually.
2025-11-17 14:50:41 +01:00
Jordan Elver 5af311284a docs: Fix link to CGO wiki page
The Go wiki is no longer hosted on GitHub, and has moved to go.dev. See https://github.com/golang/go/wiki/
2025-11-17 14:14:47 +01:00
Joe Mooring 9d80c3978f github: Update asciidoctor-diagrams extension and add GoAT 2025-11-16 12:36:05 -08:00
Bjørn Erik Pedersen 62c47407b2 Fix grammatical error in styleguide.md 2025-11-16 20:25:33 +01:00
Bjørn Erik Pedersen 2c6574ebdb Update styleguide.md 2025-11-16 20:05:32 +01:00
Bjørn Erik Pedersen 60c4245e67 Add gemini setup files 2025-11-16 19:30:46 +01:00
Ahamed_Meyan 5e6b269c41 static: Preserve .gitignore and .gitattributes in --cleanDestinationDir
Fixes #14097
2025-11-16 11:10:20 +01:00
Bjørn Erik Pedersen 94a6233aab hugolib: Fix recently introduced data race
This introduces a new thread safe maps.Map type to avoid abusing the exsting maps.Cache for regular map uses.

Fixes #14140
2025-11-15 21:17:42 +01:00
Bjørn Erik Pedersen 26f31ff6ce hugolib: Improve performance of content trees with many sections
Hugo's build process is roughly divided into three steps:

1. Process content (walk file system and insert source nodes into content tree)
2. Assemble content (assemble pages and resources according to sites matrix)
3. Render content

In #13679 we consolidated the page creation logic into one place (the assemble step). This made it much simpler to reason about, but it lost us some performance esp. in big content trees.

This commit re-introduces parallelization in the first step in the assemble step by handling each top level section in its own goroutine. This gives significant performance improvements for content trees with many sections.

Compared to master:

```
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10    19.26m ± ∞ ¹   14.54m ± ∞ ¹  -24.52% (p=0.029 n=4)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=2/pagesPerSection=100-10    19.74m ± ∞ ¹   16.45m ± ∞ ¹  -16.71% (p=0.029 n=4)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10   106.18m ± ∞ ¹   71.23m ± ∞ ¹  -32.91% (p=0.029 n=4)
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10    38.85m ± ∞ ¹   30.47m ± ∞ ¹  -21.59% (p=0.029 n=4)
```
2025-11-14 11:24:31 +01:00
Joe Mooring bca171b691 markup/asciidocext: Support boolean document attributes
Closes #14138
2025-11-13 19:04:21 +01:00
Joe Mooring 9289aa43aa github: Allow AsciiDoc content in tests to render Ditaa diagrams
In 2c80dee358 we enabled rendering of
PlantUML diagrams within integration tests. This commit removes PlantUML
support from the tests in favor of rendering Ditaa diagrams, which take
less time to generate.
2025-11-12 22:48:43 +01:00
Bjørn Erik Pedersen 606415ec18 Add a site assembly benchmark test for a deeper site structure with more sections and pages 2025-11-12 18:26:52 +01:00
Bjørn Erik Pedersen dc2f6ae27a sitesmatrix: Clary default dimension values
And make the default content version `v.1.0.0`. It has not much practical difference, but this encourages semver usage.
2025-11-12 13:00:55 +01:00
Bjørn Erik Pedersen a0944ac7a4 Run go mod tidy to clean up go.mod and go.sum 2025-11-12 12:58:30 +01:00
Bjørn Erik Pedersen 22d0c17e82 docshelper: Fix some YAML serialization issues with sites matrix configuration
Fixes #14132
2025-11-11 13:49:31 +01:00
David Karlsson 25c7c18f9f resources/page: Fix slugorcontentbasename for section pages
The slugorcontentbasename permalink token was creating an extra
subdirectory for section pages (_index.md files). This was because
pageToPermalinkContentBaseName did not have the same special handling
for _index.md files as pageToPermalinkFilename.

This commit adds the special handling to return an empty string for
section pages, ensuring backward compatibility with slugorfilename.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Fixes #14104
2025-11-10 17:03:26 +01:00
dependabot[bot] 0e8f88fa72 build(deps): bump github.com/evanw/esbuild from 0.25.11 to 0.25.12
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.25.11 to 0.25.12.
- [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.25.11...v0.25.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 12:36:36 +01:00
Bjørn Erik Pedersen a2469d504e testing: Rewrite all the old style integration tests to txtar style tests
And remove some not worth keeping (or too much work to convert).
2025-11-10 12:00:58 +01:00
Alexandru Grigore 0efcb24c37 commands: newDocsHelper encode integers as ints in generated YAML
Previously the JSON -> map roundtrip produced float64 values which caused the YAML encoder to emit integers like 404.0 instead of 404. Use the YAML encoder option yaml.AutoInt() so numbers that are integer-valued are encoded as integers.

This change affects the
ewDocsHelper command (commands/gen.go) where the docs data is written to docs/data/docs.yaml. After this change generated YAML will contain integer values (e.g. status: 404) instead of floats (e.g. status: 404.0).

Fixes #14122
2025-11-09 23:01:16 +01:00
Joe Mooring e1236e3d0d config: Add struct tags to deprecated and unused fields
This will remove these fields from the output of newDocsHelper.
2025-11-09 12:59:46 +01:00
Bjørn Erik Pedersen e24b604c30 hugolib: Delete some old integration tests
We have had several had several integration test setups over the years. What we have now, backed by txtar files, has been working very well for a long time.

The tests deleted here may lose us some coverage, but they are painful to maintain and it's unlikely that we will port them to the new setup.
2025-11-07 19:44:58 +01:00
Bjørn Erik Pedersen 4c7a78f5ca testing: Revise usage of b.N and b.Loop() in benchmarks 2025-11-07 13:33:39 +01:00
Bjørn Erik Pedersen 91eac9e573 all: Fix some benchmarks broken by modernize
It's not possible to use `b.Loop()` when `b.N` is used to prepare test data.

See 264022a75a
See #14107
2025-11-06 10:43:12 +01:00
Bjørn Erik Pedersen 04650ce778 all: Run modernize -fix ./...
Closes #14107
2025-11-05 21:05:42 +01:00
Bjørn Erik Pedersen 264022a75a Add roles and versions as new dimensions (in addition to language)
See the main issue #13776 for details.

Fixes #519
Fixes #13680
Fixes #13663
Fixes #13776
Fixes #13855
Fixes #13648
Fixes #13996
Fixes #14001
Fixes #14031
Fixes #13818
Fixes #13196
2025-11-05 20:39:25 +01:00
Bjørn Erik Pedersen ff0f67ea7f Update CONTRIBUTING.md 2025-11-04 10:57:26 +01:00
Joe Mooring 2c80dee358 github: Allow AsciiDoc content in tests to render diagrams 2025-10-31 12:47:40 -07:00
Bjørn Erik Pedersen e9bda21ce9 hreflect: Cache reflect method lookups used in collections.Where and others
We already cached the method index on the struct, but caching the resolved `reflect.Method` itself saves us from having to do another lookup on each call, which is escpeciall import in the hot path used by collections.Where and otherrs:

```bash
                                        │ master.bench │    fix-reflectmethodcache.bench     │
                                        │    sec/op    │    sec/op     vs base               │
WhereSliceOfStructPointersWithMethod-10   592.2µ ± ∞ ¹   390.1µ ± ∞ ¹  -34.14% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                                        │  master.bench  │     fix-reflectmethodcache.bench     │
                                        │      B/op      │     B/op       vs base               │
WhereSliceOfStructPointersWithMethod-10   205.14Ki ± ∞ ¹   64.52Ki ± ∞ ¹  -68.55% (p=0.029 n=4)
¹ need >= 6 samples for confidence interval at level 0.95

                                        │ master.bench │    fix-reflectmethodcache.bench     │
                                        │  allocs/op   │  allocs/op    vs base               │
WhereSliceOfStructPointersWithMethod-10   9.003k ± ∞ ¹   4.503k ± ∞ ¹  -49.98% (p=0.029 n=4)
````
2025-10-27 15:45:50 +01:00
Bjørn Erik Pedersen 3893e70510 all: Simplify the reflect usage
* Use helper funcs in hreflect package when possible.
* Use hreflect.ConvertIfPossible to handle conversions when possible.
* Move scratch.go from common/maps to common/hstore to clear cyclic import in the next step.
* Move Indirect to hreflect and reimplementing it and adusting the behavior to preserve struct pointers.
* Adjust evaluateSubElem used by where and others making the struct pointer method case slightly faster.
2025-10-26 17:36:58 +01:00
hugoreleaser b95f7d2552 releaser: Prepare repository for 0.153.0-DEV
[ci skip]
2025-10-24 15:44:59 +00:00
hugoreleaser 6abdacad3f releaser: Bump versions for release of 0.152.2
[ci skip]
2025-10-24 15:31:49 +00:00
Joe Mooring 1c8c21e45e deps: Update github.com/tdewolff/minify v2.24.4 => v2.24.5
Closes #14086
2025-10-24 17:28:42 +02:00
Bjørn Erik Pedersen 809ebe01fa hugofs: Make node_modules a "special case" mount
For this and similar mounts in a theme:

```toml
[[module.mounts]]
source = 'node_modules/bootstrap'
target = 'assets/vendor/bootstrap'
```

We first check the theme itself, then the project root.

For backwards compatibility, we also make any `../../node_modules/...` `source` paths into `node_modules/...` paths when defined in themes/modules.

Fixes #14089
2025-10-24 17:12:46 +02:00
Jordan Elver 08a0679a89 github: Fix typo in stale PR message 2025-10-24 14:30:48 +02:00
hugoreleaser 524b98665b releaser: Prepare repository for 0.153.0-DEV
[ci skip]
2025-10-22 19:24:59 +00:00
hugoreleaser 5869cbddd8 releaser: Bump versions for release of 0.152.1
[ci skip]
2025-10-22 19:10:44 +00:00
Bjørn Erik Pedersen e08278d165 Expand the numeric conversions to template funcs/methods
So the example mentioned in #14079 also works for TOML and JSON front matter.

See #14079
2025-10-22 21:04:39 +02:00
Bjørn Erik Pedersen df4f80d54f Fix where with uint64
Fixes #14081
2025-10-22 21:04:39 +02:00
Bjørn Erik Pedersen d4c78885ae Fix it so YAML integer types can be used where Go int types are expected
E.g. in date.AddDate.

In Hugo v0.152.0 we moved to a new YAML library (github.com/goccy/go-yaml) which produces uint64 for unsigned integers.

This unfortunately breaks common constructs like:

  .Date.AddDate 0 0 7

when .Date is a time.Time and the integers are unmarshaled from YAML front matter.

This commit adds code to handle conversion from uint64 (and other int types) to the required int types where possible.

Fixes #14079
2025-10-22 21:04:39 +02:00
Bjørn Erik Pedersen 29e2c2fa92 tpl/compare: Fix compare/sort of uint64s
`uint64` is used by github.com/goccy/go-yaml  for unsigned integers, which is the reason why this long-living bug has surfaced now.

Fixes #14078
2025-10-22 13:40:03 +02:00
Bjørn Erik Pedersen 0579afc3c5 Fix "assignment to entry in nil map" on empty YAML config files
Fixes #14074
2025-10-22 13:40:03 +02:00
hugoreleaser 0ccbc63ab8 releaser: Prepare repository for 0.153.0-DEV
[ci skip]
2025-10-21 16:43:50 +00:00
hugoreleaser 06bc2c16c0 releaser: Bump versions for release of 0.152.0
[ci skip]
2025-10-21 16:29:49 +00:00
Bjørn Erik Pedersen d51adca26a Merge branch 'release-0.151.2' 2025-10-21 16:08:23 +02:00
Bjørn Erik Pedersen a1307700dd config: Clone language map entries before modifying them
Now, with YAML anchor and alias support, these can point to shared data,
which must not be modified in place.

Fixes #14072
2025-10-21 13:34:15 +02:00
Bjørn Erik Pedersen 9425b939c6 Skip flaky test for now
See #14072
2025-10-21 10:32:52 +02:00
Bjørn Erik Pedersen bd50c9c7e7 Misc YAML adjustments
Closes #14067
2025-10-20 16:05:05 +02:00
Bjørn Erik Pedersen a8e0ca9254 hugofs: Make sure that non-project module mounts are local paths
Fixes #14069
2025-10-19 20:27:10 +02:00
Joe Mooring 559a029de7 langs/i18n: Improve reserved key error message
Closes #14061
2025-10-19 13:47:04 +02:00
Bjørn Erik Pedersen 184b10ed45 deps: Upgrade github.com/gohugoio/go-i18n/v2
Gets the fork line with https://github.com/nicksnyder/go-i18n/commit/28dedbee2baafdef1b89b46fc5b729d09232e245 + patch.
2025-10-18 18:21:14 +02:00
Bjørn Erik Pedersen 5bad0d523e langs: Add test case using a "reserved" i18n code
See #14061
2025-10-18 15:28:33 +02:00
Bjørn Erik Pedersen a3d9548469 Replace to gopkg.in/yaml with github.com/goccy/go-yaml (note)
This commit also adds validation to prevent the "Billion Laughs" attack (see https://github.com/goccy/go-yaml/issues/461). The limit of non-scalar aliases to the same node is set to 10,000. See benchmarks below.

```                                                            │        sec/op         │
UnmarshalBillionLaughs/Billion_Laughs_no_validation-10                 125.2µ ± ∞ ¹
UnmarshalBillionLaughs/Billion_Laughs_with_validation-10               655.8µ ± ∞ ¹
UnmarshalBillionLaughs/YAML_Front_Matter_no_validation-10              9.223µ ± ∞ ¹
UnmarshalBillionLaughs/YAML_Front_Matter_with_validation-10            9.443µ ± ∞ ¹
geomean                                                                51.71µ
¹ need >= 6 samples for confidence interval at level 0.95

                                                            │ fix-goyaml-8822.bench │
                                                            │         B/op          │
UnmarshalBillionLaughs/Billion_Laughs_no_validation-10                177.0Ki ± ∞ ¹
UnmarshalBillionLaughs/Billion_Laughs_with_validation-10              177.0Ki ± ∞ ¹
UnmarshalBillionLaughs/YAML_Front_Matter_no_validation-10             11.67Ki ± ∞ ¹
UnmarshalBillionLaughs/YAML_Front_Matter_with_validation-10           11.67Ki ± ∞ ¹
geomean                                                               45.45Ki
¹ need >= 6 samples for confidence interval at level 0.95

                                                            │ fix-goyaml-8822.bench │
                                                            │       allocs/op       │
UnmarshalBillionLaughs/Billion_Laughs_no_validation-10                 3.302k ± ∞ ¹
UnmarshalBillionLaughs/Billion_Laughs_with_validation-10               3.305k ± ∞ ¹
UnmarshalBillionLaughs/YAML_Front_Matter_no_validation-10               253.0 ± ∞ ¹
UnmarshalBillionLaughs/YAML_Front_Matter_with_validation-10             253.0 ± ∞ ¹
````

Fixes #8822
Fixes #13043
Fixes #14053
Fixes ##8427
2025-10-18 13:52:22 +02:00
dependabot[bot] 9e344bbe49 build(deps): bump github.com/tdewolff/minify/v2 from 2.24.3 to 2.24.4
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.3 to 2.24.4.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.3...v2.24.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-17 18:44:56 +02:00
hugoreleaser 1ec0e2ebec releaser: Prepare repository for 0.152.0-DEV
[ci skip]
2025-10-16 17:06:05 +00:00
hugoreleaser a3574f6f70 releaser: Bump versions for release of 0.151.2
[ci skip]
2025-10-16 16:52:34 +00:00
Bjørn Erik Pedersen 989454a52d parser/pageparser: Add a testcase for nested shortcodes of the same name
See #14054
2025-10-16 18:50:02 +02:00
Bjørn Erik Pedersen 1e91e46520 parser/pageparser: Fix shortcode nesting regression
This reverts commit a133393eda.

While that commit fixed a bug, it introduced a worse one.

Closes #14054
2025-10-16 18:50:02 +02:00
Bjørn Erik Pedersen 8a57d0f15f testscripts: Add and improve commands tests for static mounts 2025-10-16 12:39:11 +02:00
hugoreleaser b76c50ac18 releaser: Prepare repository for 0.152.0-DEV
[ci skip]
2025-10-15 15:06:42 +00:00
hugoreleaser 1cdd17882c releaser: Bump versions for release of 0.151.1
[ci skip]
2025-10-15 14:51:34 +00:00
Bjørn Erik Pedersen e2fb0b0e80 Upgrade Go to 1.25.3
See https://go.dev/doc/devel/release#go1.25.3
2025-10-15 13:00:33 +02:00
Kazuo Oishi 88aea56683 tpl: Fix strings/truncate CJK handling
Fix truncate position for mix of CJK and non-CJK text.

Fixes #14039
2025-10-15 13:00:10 +02:00
dependabot[bot] f4c11571bb build(deps): bump github.com/gohugoio/hashstructure from 0.5.0 to 0.6.0
Bumps [github.com/gohugoio/hashstructure](https://github.com/gohugoio/hashstructure) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/gohugoio/hashstructure/releases)
- [Commits](https://github.com/gohugoio/hashstructure/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hashstructure
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-15 11:00:35 +02:00
dependabot[bot] 54075acc29 build(deps): bump golang.org/x/image from 0.30.0 to 0.32.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.30.0 to 0.32.0.
- [Commits](https://github.com/golang/image/compare/v0.30.0...v0.32.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-15 10:59:28 +02:00
dependabot[bot] 8b52303e31 build(deps): bump github.com/evanw/esbuild from 0.25.10 to 0.25.11
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.25.10 to 0.25.11.
- [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.25.10...v0.25.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-15 10:57:33 +02:00
dependabot[bot] 3d45d30a41 build(deps): bump golang.org/x/tools from 0.37.0 to 0.38.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.37.0 to 0.38.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.37.0...v0.38.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-14 22:10:08 +02:00
dependabot[bot] 095157cd65 build(deps): bump golang.org/x/mod from 0.28.0 to 0.29.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/mod/compare/v0.28.0...v0.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-14 16:34:43 +02:00
Sardorbek Imomaliev 29cf87444f create/skeletons: Wrap section and home lists with section tags
Avoiding issues with tag leakage when using .Summary
See issue #14044 for details.
2025-10-14 11:54:45 +02:00
Joe Mooring 1b4dd436dd markup/goldmark: Align blockquote default output with Goldmark
Closes #14046
2025-10-14 09:38:29 +02:00
Bjørn Erik Pedersen 4414ef73f3 parser/pageparser: Store shortcode names as unique.Handle[string] to save memory allocations
```
                        │ stash.bench  │        perf-pagelexer.bench        │
                        │    sec/op    │    sec/op     vs base              │
ShortcodeLexer-10         68.87µ ± ∞ ¹   65.91µ ± ∞ ¹       ~ (p=0.200 n=4)
Parse-10                  7.421µ ± ∞ ¹   7.030µ ± ∞ ¹  -5.28% (p=0.029 n=4)
HasShortcode/Match-10     70.42n ± ∞ ¹   77.37n ± ∞ ¹  +9.86% (p=0.029 n=4)
HasShortcode/NoMatch-10   14.32n ± ∞ ¹   14.38n ± ∞ ¹       ~ (p=0.086 n=4)
geomean                   847.2n         847.3n        +0.01%
¹ need >= 6 samples for confidence interval at level 0.95

                        │  stash.bench  │         perf-pagelexer.bench          │
                        │     B/op      │     B/op       vs base                │
ShortcodeLexer-10         181.3Ki ± ∞ ¹   177.4Ki ± ∞ ¹  -2.19% (p=0.029 n=4)
Parse-10                  24.56Ki ± ∞ ¹   24.19Ki ± ∞ ¹  -1.53% (p=0.029 n=4)
HasShortcode/Match-10       0.000 ± ∞ ¹     0.000 ± ∞ ¹       ~ (p=1.000 n=4) ²
HasShortcode/NoMatch-10     0.000 ± ∞ ¹     0.000 ± ∞ ¹       ~ (p=1.000 n=4) ²
geomean                               ³                  -0.93%               ³
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal
³ summaries must be >0 to compute geomean

                        │ stash.bench  │         perf-pagelexer.bench         │
                        │  allocs/op   │  allocs/op   vs base                 │
ShortcodeLexer-10         1004.0 ± ∞ ¹   921.0 ± ∞ ¹   -8.27% (p=0.029 n=4)
Parse-10                   34.00 ± ∞ ¹   14.00 ± ∞ ¹  -58.82% (p=0.029 n=4)
HasShortcode/Match-10      0.000 ± ∞ ¹   0.000 ± ∞ ¹        ~ (p=1.000 n=4) ²
HasShortcode/NoMatch-10    0.000 ± ∞ ¹   0.000 ± ∞ ¹        ~ (p=1.000 n=4) ²
```
2025-10-05 19:06:46 +02:00
Bjørn Erik Pedersen a133393eda parser/pagerparser: Fix closing shortcode error handling when repeated 2025-10-05 19:06:46 +02:00
Bjørn Erik Pedersen 9197debbfe testscripts: Make test assertion less specific 2025-10-05 18:08:52 +02:00
hugoreleaser c29897fac0 releaser: Prepare repository for 0.152.0-DEV
[ci skip]
2025-10-02 13:45:36 +00:00
hugoreleaser c70ab27ceb releaser: Bump versions for release of 0.151.0
[ci skip]
2025-10-02 13:30:36 +00:00
Bjørn Erik Pedersen 510d98b778 Adjust the terminal progress reporter a little
Mostly to reduce the amount of OSC 9;4 sequences written to stdout.
2025-10-02 14:57:42 +02:00
Andrii Chubatiuk 7fd6762c16 transform/livereloadinject: Skip livereload.js injection if no tags found (note)
This change is mainly motivated to support sites built by HTML fragments with e.g. a JS framework.

Now we don't inject the script if we don't find any of `doctype` (the only one required by the HTML 5 spec), `html` or `head`.

Co-authored-by: bep <bjorn.erik.pedersen@gmail.com>
2025-10-02 13:39:35 +02:00
dependabot[bot] 584f052f39 build(deps): bump google.golang.org/api from 0.248.0 to 0.251.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.248.0 to 0.251.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.248.0...v0.251.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.251.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 23:05:48 +02:00
dependabot[bot] b76d71761e build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.53.0 to 1.54.4.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.53.0...service/s3/v1.54.4)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.54.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 23:05:36 +02:00
Bjørn Erik Pedersen c5dca3bdee Add transform.HTMLToMarkdown
Fixes #13946
2025-10-01 19:17:26 +02:00
Bjørn Erik Pedersen ec463c0977 Report OSC 9;4 progress when building
As supported by the Ghostty terminal and others.
2025-10-01 14:27:23 +02:00
dependabot[bot] 4d2743e4c8 build(deps): bump github.com/aws/aws-sdk-go-v2 from 1.38.1 to 1.39.2
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.38.1 to 1.39.2.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.38.1...v1.39.2)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.39.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 21:13:50 +02:00
dependabot[bot] c20f70d9a3 build(deps): bump golang.org/x/tools from 0.36.0 to 0.37.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.36.0 to 0.37.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.36.0...v0.37.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 21:11:37 +02:00
dependabot[bot] 1b556216a8 build(deps): bump github.com/spf13/afero from 1.14.0 to 1.15.0
Bumps [github.com/spf13/afero](https://github.com/spf13/afero) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/spf13/afero/releases)
- [Commits](https://github.com/spf13/afero/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/afero
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 21:11:14 +02:00
dependabot[bot] 106c8e6de6 build(deps): bump github.com/tdewolff/minify/v2 from 2.24.2 to 2.24.3
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.2 to 2.24.3.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.2...v2.24.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 21:10:54 +02:00
Bjørn Erik Pedersen 03b33ecb5e Fix file caching for 404 responses in resources.GetRemote
Fixes #14019
2025-09-30 20:43:05 +02:00
dependabot[bot] 9928122eb8 build(deps): bump github.com/evanw/esbuild from 0.25.9 to 0.25.10
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.25.9 to 0.25.10.
- [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.25.9...v0.25.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 17:43:39 +02:00
Dr. Tobias Quathamer 105d3bc32f tpl: Workaround s390x precision of Atan and Tan
On s390x, math.Atan(1) and math.Tan(1) give values that slightly differ
from other architectures, causing TestTemplateFuncsExamples to fail.

The golang math package states in the overview:
"This package does not guarantee bit-identical results across architectures."
2025-09-29 17:09:29 +02:00
Bjørn Erik Pedersen 3e46ba5ce2 cache/httpcache: Add respectCacheControlNoStoreInResponse and respectCacheControlNoStoreInRequest options
This cache is used by `resources.GetRemote`.

Default values are:

* respectCacheControlNoStoreInResponse: false
* respectCacheControlNoStoreInRequest: true

This is a slightly breaking change, but the current behaviour is confusing, as:

* Many servers set the `no-store` header without much consideration, see https://developer.chrome.com/docs/web-platform/bfcache-ccns for more context
* We almost always want to cache the `resources.GetRemote` to disk.

Fixes #13990
2025-09-29 17:06:50 +02:00
Bjørn Erik Pedersen 4d1303512b common/hreflect: Speed up IsTrutfulValue
By caching the calculation of whether a type implements `IsZero`:

```
IsTruthFulVAlue-10    467.95n ± ∞ ¹   79.13n ± ∞ ¹  -83.09% (p=0.029 n=4)
```
2025-09-29 17:06:50 +02:00
dependabot[bot] 9943c1bef0 build(deps): bump golang.org/x/text from 0.28.0 to 0.29.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.28.0...v0.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-29 15:09:06 +02:00
dependabot[bot] 766757310a build(deps): bump github.com/olekukonko/tablewriter from 1.0.9 to 1.1.0
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.9 to 1.1.0.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.9...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-29 15:08:34 +02:00
dependabot[bot] d71c07cf3c build(deps): bump github.com/spf13/cast from 1.9.2 to 1.10.0
Bumps [github.com/spf13/cast](https://github.com/spf13/cast) from 1.9.2 to 1.10.0.
- [Release notes](https://github.com/spf13/cast/releases)
- [Commits](https://github.com/spf13/cast/compare/v1.9.2...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/cast
  dependency-version: 1.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-29 15:07:36 +02:00
Joe Mooring b462980ac2 markup/goldmark: Enhance footnote extension with backlinkHTML option
This commit introduces a new option, backlinkHTML, to the Goldmark
footnote extension. This allows users to the set custom text for
footnote backlink anchors.

Closes #11434
2025-09-29 12:21:17 +02:00
Joe Mooring 47678d8cbd markup/goldmark: Enhance footnote extension with auto-prefixing option
This commit introduces a new option, enableAutoIDPrefix, to the Goldmark
footnote extension. When enabled, it prepends a unique prefix to
footnote IDs, preventing clashes when multiple documents are rendered
together. This prefix is unique to each logical path, which means that
the prefix is not unique across content dimensions such as language.
This change also refactors the extension's configuration from a boolean
to a struct.

Closes #8045
2025-09-27 18:16:42 +02:00
hugoreleaser 18b9b6488b releaser: Prepare repository for 0.151.0-DEV
[ci skip]
2025-09-25 10:45:05 +00:00
hugoreleaser ce44a8e835 releaser: Bump versions for release of 0.150.1
[ci skip]
2025-09-25 10:26:04 +00:00
Joe Mooring 64f40731f1 hugolib: Change duplicate content path warning to an info log
Improves #13993
2025-09-20 20:13:08 +02:00
Joe Mooring 1140314be1 hugolib: Restore integration test
Closes #13991
2025-09-18 18:02:13 +02:00
Joe Mooring 404fd9e512 commands: Map --minify CLI flag to the correct configuration key
Closes #13988
2025-09-18 16:50:47 +02:00
Max Kapur b1b0cdee3a snap: Add desktop plug 2025-09-18 06:22:39 -07:00
Max Kapur 3eea082903 test(deps): Update setup-ruby action to v1.257.0 2025-09-17 09:30:21 -07:00
hugoreleaser 0f18cbe990 releaser: Prepare repository for 0.151.0-DEV
[ci skip]
2025-09-08 13:16:24 +00:00
hugoreleaser 3f5473b7d4 releaser: Bump versions for release of 0.150.0
[ci skip]
2025-09-08 13:01:12 +00:00
dependabot[bot] d1f6a1dc59 build(deps): bump golang.org/x/mod from 0.27.0 to 0.28.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/mod/compare/v0.27.0...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 11:23:39 +02:00
Bjørn Erik Pedersen 747cf4ad65 modules: Add support for direct version module imports in hugo.toml
Fixes #13964
2025-09-07 22:47:25 +02:00
Joe Mooring d8774d7fc3 resources/page: Fix truncated summary logic
Fixes #13967
Fixes #13968
2025-09-07 11:07:41 +02:00
Joe Mooring 3b8947d821 config/security: Add PROGRAMDATA to the osenv allowlist 2025-09-06 18:11:38 +02:00
hugoreleaser 321a66ef19 releaser: Prepare repository for 0.150.0-DEV
[ci skip]
2025-09-04 14:18:57 +00:00
hugoreleaser 57a784e027 releaser: Bump versions for release of 0.149.1
[ci skip]
2025-09-04 14:04:46 +00:00
Lukáš Zapletal 25c0f2408a Remove noindex meta tag from alias.html
Google does not behave as expected when an alias is used. It does not index the client-side alias page, but on top of that it also does not index the original page.

Google does not require noindex to be present and since alias pages do not have any content, it makes little sense to provide it. Therefore, this patch removes the line completely.

https://developers.google.com/search/docs/crawling-indexing/301-redirects
2025-09-04 15:58:23 +02:00
Bjørn Erik Pedersen 4f2d2b2cc4 Fix nilpointer on ToC heading
Fixes #11843

Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
2025-09-04 14:01:51 +02:00
Bjørn Erik Pedersen b8eb45c9df tpl/collections: Require collections.D args to be ints
Fixes #13952

Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
2025-09-04 14:01:51 +02:00
Bjørn Erik Pedersen 1d90afff1b Upgrade to Go 1.25.1
Fixes #13960
2025-09-04 11:11:29 +02:00
PikachuTW e751afa9bd Fix config env handling for some slice options
Fixes #13950
2025-09-04 10:49:14 +02:00
Joe Mooring a09b8a60eb minifiers: Update deprecation handling
1. With minify.tdewolff.css and minify.tdewolff.svg, check
   for "decimals" instead of "decimal"

2. Add deprecation messages for:

  - minify.tdewolff.css.decimals
  - minify.tdewolff.svg.decimals
  - minify.tdewolff.html.keepconditionalcomments
  - config root minify (bool)
  - config root minifyoutput (bool)

3. Deprecate minify.tdewolff.css.keepcss2 in favor
   of minify.tdewolff.css.version

4. Refactor minify configuration tests

Closes #11893
Closes #13947
Closes #13948
2025-09-03 10:56:32 +02:00
Bjørn Erik Pedersen 0071b47b8b Update README.md 2025-09-01 17:11:08 +02:00
hugoreleaser 70d62993ee releaser: Prepare repository for 0.150.0-DEV
[ci skip]
2025-08-27 15:51:44 +00:00
hugoreleaser 66240338f1 releaser: Bump versions for release of 0.149.0
[ci skip]
2025-08-27 15:37:16 +00:00
Bjørn Erik Pedersen 84b5123912 tpl/collections: Add an integration test for collections.D
Also
* improve the handling if invalid input
* add a sanity check for number of elements asked for in D (1000000).
2025-08-27 17:25:05 +02:00
Joe Mooring 2912415955 misc: Update Go version to 1.24.0 in README
Update Go version requirements for building Hugo.
2025-08-27 16:46:38 +02:00
Justus Perlwitz bb4e66cd7c create: Fix new content command with future dates
Fixes #12599
2025-08-27 09:33:17 +02:00
dependabot[bot] 45ec2f88bb build(deps): bump github.com/getkin/kin-openapi from 0.132.0 to 0.133.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.132.0 to 0.133.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.132.0...v0.133.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-27 09:31:02 +02:00
Bjørn Erik Pedersen 1ba80874e4 tpl/collections: Add collections.D using Vitter's Method D for sequential random sampling 2025-08-26 20:37:08 +02:00
dependabot[bot] 84dd495f2b build(deps): bump google.golang.org/api from 0.247.0 to 0.248.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.247.0 to 0.248.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.247.0...v0.248.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.248.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 09:54:26 +02:00
dependabot[bot] 327bbc613f build(deps): bump github.com/evanw/esbuild from 0.25.6 to 0.25.9
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.25.6 to 0.25.9.
- [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.25.6...v0.25.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 09:54:07 +02:00
dependabot[bot] 2447138f1d build(deps): bump gocloud.dev from 0.40.0 to 0.43.0
Bumps [gocloud.dev](https://github.com/google/go-cloud) from 0.40.0 to 0.43.0.
- [Release notes](https://github.com/google/go-cloud/releases)
- [Commits](https://github.com/google/go-cloud/compare/v0.40.0...v0.43.0)

---
updated-dependencies:
- dependency-name: gocloud.dev
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 20:47:05 +02:00
Bjørn Erik Pedersen 61ec7a20a5 commands: Deprecate --omitEmpty on chromastyles command
It's no longer needed -- empty classes are now always omitted.

See  See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
2025-08-25 18:24:07 +02:00
Bjørn Erik Pedersen c289fcaaaa commands: Add --omitClassComments to the chromastyles command 2025-08-25 18:24:07 +02:00
Bjørn Erik Pedersen cfc38ecfed deps: Upgrade github.com/alecthomas/chroma/v2 v2.19.0 => v2.20.0
CLoses #13917
2025-08-25 18:24:07 +02:00
dependabot[bot] 22e579e050 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.44.10 to 1.53.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/macie2/v1.44.10...service/s3/v1.53.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/cloudfront
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 15:32:19 +02:00
dependabot[bot] b886615d1b build(deps): bump github.com/tdewolff/minify/v2 from 2.23.11 to 2.24.0
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.23.11 to 2.24.0.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.23.11...v2.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 15:29:50 +02:00
minxinyi ecdef2be70 all: Use slices.Equal 2025-08-24 20:35:18 +02:00
Bjørn Erik Pedersen ff3ae62933 Merge commit 'bfa74537929f409fca841540b971125b7678963a' 2025-08-23 12:39:28 +02:00
Bjørn Erik Pedersen bfa7453792 Squashed 'docs/' changes from 60dd993a6..71f739460
71f739460 content: Reformat procedures to use description lists instead of H3 elements
420a78802 content: Remove 21YunBox hosting guide
abeea342f content: Add hosting guide for Vercel
3aaca6c0a content: Add hosting guide for Render
d77b06aa6 content: Fix formatting
82885415f content: Change build.sh file perms in Cloudflare hosting docs
0569c68ca content: Fix typo
8f14be5da content: Remove duplicate list items
e76f17815 misc: Fix typo in configuration for the Markdown linter
32e450b7c misc: Fix typo in configuration for the Markdown linter
81bd0056a mics: Update configuration for the Markdown linter
480ae9014 content: Improve file names in the PageInner description
b17999021 content: Adjust Markdown to pass linter tests
1d9d56897 misc: Implement Markdown linter
91fc34c58 content: Miscellaneous edits
b7c60ae0b content: Add admonition for outdated learning resources
51b6df588 misc: Activate stale action
84a897f5f misc: Implement stale action
167ea9713 content: Update GitHub Pages workflow example
3672d9116 misc: Use actions/checkout@v5 in workflows
2f2b4c968 misc: Update spellcheck configs
55a8ff7ae content: Fix typo
36e3d7b7b Adjust sponsors
51014e6b9 content: Fix link
37cfc093f content: Add Cloudflare Worker hosting instructions
09862ffad content: Update hosting considerations related to GitInfo
477f3e7c0 misc: Update cspell config
7a7bb4874 content: Miscellaneous edits
48486d5ac content: Remove Cloudflare Pages documentation
382f8052d content: Remove KeyCDN documentation
8ecbc985c content: Update hosting workflow files
57fd362bf content: Clarify applicability of the slug front matter field
0dcfff9c7 content: Update directory-structure.md
58e3609a0 content: Fix typo
22fb43695 content: Fix search/replace errors
fba8342a8 content: Fix missing link refs
97e035006 Update netlify.toml
6ab5ae6df content: Improve GitLab Pages compression performance
172bfbc4b content: Differentiate content view templates from other templates
978770fc8 content: Clarify handling of goldmark extensions conflict
cb74aa4c7 content: Improve Codeberg Pages hosting instructions
794477792 content: Add custom domain instructions to Codeberg Pages setup
235fac27d content: Improve description of code block render hook options map
35a507d31 content: Improve WP2Hugo description
8191f4dcf content: Improve WP2Hugo description
25cca30fd content: Fix type
e955652ae content: Improve base template description
922aa91bd content: Improve description of partial lookup logic
072536ef6 content: Update template type documentation
5bf153531 Update netlify.toml
cdd20a433 content: Indicate language sort order for relevant page methods
fc9a868c9 content: Add Sitepins to list of commercial CMS platforms
b6077449c content: Update version references
b71388cac content: Fix typo
c27dfdef3 content: Move link reference
d008ea6ac content: Fix typo
040f14a3c content: Fix typo
1ac119d26 content: Fix typo
a5a4fa907 content: More updates for v0.148.0
b7247a8ea content: Remove command chaining from GitHub Actions workflow
6ecb96bcd content: Updates for v0.148.0
aea9e4c29 netlify: Update Hugo version
4e71546d1 Add Ancestors (plural) method to GitInfo, rename Ancestor field to Parent
68620a6c2 source: Expose Ancestor in GitInfo

git-subtree-dir: docs
git-subtree-split: 71f739460f
2025-08-23 12:36:13 +02:00
Dustin Fischer 12ace3ad5c resources/page: Add :sectionslug and :sectionslugs permalink tokens
Add slugified section permalink tokens with fallback behavior and slice syntax support.

Fixes #13788
2025-08-23 12:35:21 +02:00
Bjørn Erik Pedersen c14fdddada Upgrade to Go 1.25
Fixes #13924
Fixes #13931
2025-08-22 18:17:10 +02:00
Bjørn Erik Pedersen 186934feb4 common/hcontext: Replace with external package 2025-08-22 09:13:46 +02:00
Bjørn Erik Pedersen 13b43e6117 Fix server rebuild when adding a new leaf bundle with resources in one go
E.g. `cp -r`.

Note that this was not an issue if you first created the bundle, waited, and then created the resource(s).

Fixes #13925
2025-08-21 21:52:31 +02:00
cui bff4dddb12 resources/page: Use reflect.TypeFor 2025-08-20 13:20:14 +02:00
dependabot[bot] 885cd299b7 build(deps): bump github.com/tdewolff/minify/v2 from 2.23.8 to 2.23.11
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.23.8 to 2.23.11.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.23.8...v2.23.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-20 11:42:54 +02:00
Bjørn Erik Pedersen 80e973ea5c Remove test with deprecated path usage
This now creates a warning and flaky CI tests.
2025-08-20 11:36:10 +02:00
dependabot[bot] debf3c559a build(deps): bump github.com/olekukonko/tablewriter from 1.0.8 to 1.0.9
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.8 to 1.0.9.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.8...v1.0.9)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-20 11:29:31 +02:00
Bjørn Erik Pedersen 01b0eda96c Update README.md 2025-08-16 10:51:26 +02:00
Bjørn Erik Pedersen 87e100e61f Fix rebuild when deleting a content adapter file 2025-08-15 17:32:42 +02:00
dependabot[bot] 1649f3126f build(deps): bump google.golang.org/api from 0.237.0 to 0.247.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.237.0 to 0.247.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.237.0...v0.247.0)

---
updated-dependencies:
- dependency-name: google.golang.org/api
  dependency-version: 0.247.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 15:26:53 +02:00
dependabot[bot] ccd6a4b71e build(deps): bump github.com/aws/aws-sdk-go-v2 from 1.36.4 to 1.38.0
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.36.4 to 1.38.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.36.4...v1.38.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2
  dependency-version: 1.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 14:51:29 +02:00
dependabot[bot] 6dc1a1752b build(deps): bump golang.org/x/tools from 0.35.0 to 0.36.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.35.0 to 0.36.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.35.0...v0.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 14:02:06 +02:00
dependabot[bot] 806d4848c3 build(deps): bump github.com/bep/simplecobra from 0.6.0 to 0.6.1
Bumps [github.com/bep/simplecobra](https://github.com/bep/simplecobra) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/bep/simplecobra/releases)
- [Commits](https://github.com/bep/simplecobra/compare/v0.6.0...v0.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 14:01:33 +02:00
Nigel van Keulen 348aae91e8 tpl/strings: Remove unnecessary error check 2025-08-15 13:43:16 +02:00
Joe Mooring 61482cfab6 markup/goldmark: Apply Hugo Goldmark Extras when rendering TOC
Closes #12605
2025-08-14 22:39:14 +02:00
dependabot[bot] 04ee1b9784 build(deps): bump golang.org/x/mod from 0.25.0 to 0.27.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.25.0 to 0.27.0.
- [Commits](https://github.com/golang/mod/compare/v0.25.0...v0.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 11:29:34 +02:00
dependabot[bot] 7a86fe9905 build(deps): bump github.com/yuin/goldmark from 1.7.12 to 1.7.13
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.12 to 1.7.13.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.12...v1.7.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 11:28:53 +02:00
Joe Mooring 5fdcc09062 markup/goldmark: Sanitize TOC heading titles
Fixes #13401
2025-08-13 11:23:30 +02:00
dependabot[bot] f5245a7d5f build(deps): bump golang.org/x/image from 0.28.0 to 0.30.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.28.0 to 0.30.0.
- [Commits](https://github.com/golang/image/compare/v0.28.0...v0.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 14:46:29 +02:00
Joe Mooring 5029676aca deps: Upgrade github.com/niklasfasching/go-org v1.8.0 => v1.9.1
Fixes #13846
2025-08-11 10:54:00 +02:00
n1xx1 2216028620 Add a key to the partialCached deadlock prevention
Fixes #13889
2025-08-07 11:17:14 +02:00
n1xx1 ecc3dd1f53 transform: Add support for "format" option in transform.Unmarshal
Fixes #13887
2025-08-05 22:19:51 +02:00
Bjørn Erik Pedersen de4a7f1e04 Skip flakey test on CI 2025-08-05 19:45:46 +02:00
958 changed files with 42037 additions and 24338 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22400.20000
- image: bepsays/ci-hugoreleaser:1.22500.20400
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20400
steps:
- *restore-cache
- &attach-workspace
+13
View File
@@ -0,0 +1,13 @@
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: []
+10
View File
@@ -0,0 +1,10 @@
🛑 Before submitting this PR
All PRs must be linked to an Issue in this repository, and that Issue must be approved by a project maintainer:
- For a bug, approval means that the "needs triage" label is removed.
- For a proposal, approval means that the "proposal" label is replaced by an "enhancement" label.
PRs without a linked, approved Issue will be closed immediately.
Please review the contribution guide (CONTRIBUTING.md) to understand commit message guidelines and AI assistance disclosures.
+22
View File
@@ -0,0 +1,22 @@
name: AI Detection
on:
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
pull_request_target:
types: [opened, ready_for_review]
permissions:
contents: read
pull-requests: write
jobs:
detect-ai:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: AI Watchdog
uses: bep/ai-watchdog@995475dd81767f8035dc2c943e9230918d06caca # temporary fork (probably)
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
pr-label: AI Suspect
fail-when-confident: false
# CSV list.
pr-user-logins-skip: dependabot[bot],bep,jmooring
+5 -5
View File
@@ -16,20 +16,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Docker meta
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to GHCR
# Login is only needed when the image is pushed
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -37,7 +37,7 @@ jobs:
- name: Build and push
id: build
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
provenance: mode=max
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
operations-per-run: 999
days-before-issue-stale: 365
@@ -43,7 +43,7 @@ jobs:
stale-pr-message: This PR has been automatically marked as stale because it has not had
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell us in a comment.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'Stale'
+19 -17
View File
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
go-version: [1.24.x, 1.25.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
runs-on: ${{ matrix.os }}
steps:
@@ -24,9 +24,7 @@ jobs:
name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
tool-cache: true
android: true
dotnet: true
haskell: true
@@ -34,9 +32,9 @@ jobs:
docker-images: true
swap-storage: true
- name: Checkout code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Install Go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ matrix.go-version }}
check-latest: true
@@ -45,18 +43,21 @@ jobs:
**/go.sum
**/go.mod
- name: Install Ruby
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
with:
ruby-version: "2.7"
bundler-cache: true #
ruby-version: "3.4.5"
- name: Install Ruby gems
run: |
gem install asciidoctor -v "2.0.26"
gem install asciidoctor-diagram -v "3.1.0"
- name: Install GoAT
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
- name: Install Python
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.x"
- name: Install Mage
run: go install github.com/magefile/mage@v1.15.0
- name: Install asciidoctor
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
- name: Install docutils
run: |
pip install docutils
@@ -105,7 +106,10 @@ jobs:
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- if: matrix.os == 'ubuntu-latest'
name: Run staticcheck
run: staticcheck ./...
run: |
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
staticcheck ./...
rm -rf ${{ runner.temp }}/staticcheck
- if: matrix.os != 'windows-latest'
name: Check
run: |
@@ -117,16 +121,14 @@ jobs:
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
name: Test
run: |
mage -v test;
mage -v test
env:
HUGO_BUILD_TAGS: extended,withdeploy
- name: Build tags
run: |
go install -tags extended
- if: matrix.os == 'ubuntu-latest'
name: Build for dragonfly
run: |
go install
go clean -i -cache
env:
GOARCH: amd64
GOOS: dragonfly
+2 -1
View File
@@ -3,4 +3,5 @@
imports.*
dist/
public/
.DS_Store
.DS_Store
cache/filecache/_gen/
+1
View File
@@ -0,0 +1 @@
tpl/tplimpl/embedded/templates/**
+14
View File
@@ -0,0 +1,14 @@
* Brevity is good.
* Assume that the maintainers and readers of the code you write are Go experts:
* Don't use comments to explain the obvious.
* Use self-explanatory variable and function names.
* Use short variable names when the context is clear.
* If you need to add temporary debug printing, use `hdebug.Printf`.[^1]
* Never export symbols that's not needed outside of the package.
* Avoid global state at (almost) all cost.
* This is a project with a long history; assume that a similiar problem has been solved before, look hard for helper functions before creating new ones.
* Use `./check.sh ./somepackage/...` when iterating.
* Use `./check.sh` when you're done.
[^1]: CI build fail if you forget to remove the debug printing.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+13 -2
View File
@@ -19,6 +19,7 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
* [Reporting Issues](#reporting-issues)
* [Submitting Patches](#submitting-patches)
* [Code Contribution Guidelines](#code-contribution-guidelines)
* [AI Assistance Notice](#ai-assistance-notice)
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
@@ -54,7 +55,7 @@ If it is of some complexity, the contributor is expected to maintain and support
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
Note that we do not accept new features that require [CGO](https://go.dev/wiki/cgo).
We have one exception to this rule which is LibSASS.
**Bug fixes are, of course, always welcome.**
@@ -76,9 +77,19 @@ To make the contribution process as seamless as possible, we ask for the followi
* Run `go fmt`.
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
* Squash your commits into a single commit. `git rebase -i`. Its okay to force update your pull request with `git push -f`.
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
* Ensure that `./check.sh` succeeds. Note that some tests are skipped when running locally, some because they are slow. To run these locally, do `CI_LOCAL=true ./check.sh ./somepackage/...`.
* Follow the **Git Commit Message Guidelines** below.
## AI Assistance Notice
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
An example disclosure:
> This PR was written primarily by Claude Code.
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it.
### Git Commit Message Guidelines
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
+1 -1
View File
@@ -2,7 +2,7 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.24"
ARG GO_VERSION="1.25"
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
+82 -75
View File
@@ -52,7 +52,7 @@ Use Hugo's embedded web server during development to instantly see changes to co
Hugo's fast asset pipelines include:
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract metadata
- JavaScript bundling &ndash; Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
- Sass processing &ndash; Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
- Tailwind CSS processing &ndash; Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
@@ -65,30 +65,24 @@ See the [features] section of the documentation for a comprehensive summary of H
<p>&nbsp;</p>
<p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
&nbsp;&nbsp;&nbsp;
<a href="https://pinme.eth.limo/?s=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/logo-pinme.svg" width="200" alt="PinMe."></a>
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/cloudcannon-cms-logo.svg" width="200" alt="CloudCannon"></a>
</p>
## Editions
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
Hugo offers a standard edition with core features, plus extended and extended/deploy editions with more. Use the standard edition unless you need the features below.
Feature|extended edition|extended/deploy edition
:--|:-:|:-:
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
[Transpile Sass to CSS] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass] transpiler instead, which is compatible with any edition.|:heavy_check_mark:|:heavy_check_mark:
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details].|:x:|:heavy_check_mark:
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[processing images]: https://gohugo.io/content-management/image-processing/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
## Installation
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
@@ -102,9 +96,9 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
Prerequisites to build Hugo from source:
- Standard edition: Go 1.23.0 or later
- Extended edition: Go 1.23.0 or later, and GCC
- Extended/deploy edition: Go 1.23.0 or later, and GCC
- Standard edition: Go 1.24.0 or later
- Extended edition: Go 1.24.0 or later, and GCC
- Extended/deploy edition: Go 1.24.0 or later, and GCC
Build the standard edition:
@@ -162,6 +156,16 @@ If there is sufficient interest, [create a proposal]. Do not submit a pull reque
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
## License
For the Hugo source code, see [LICENSE](/LICENSE).
We also bundle some libraries in binary/WASM form:
* [libwebp](https://github.com/webmproject/libwebp), [BSD-3-Clause license](https://github.com/webmproject/libwebp?tab=BSD-3-Clause-1-ov-file#readme)
* [Katex](https://github.com/KaTeX/KaTeX), [MIT license](https://github.com/KaTeX/KaTeX?tab=MIT-1-ov-file#readme)
* [QuickJS](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme), [License](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme)
## Dependencies
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
@@ -171,51 +175,55 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
```text
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
github.com/PuerkitoBio/goquery="v1.10.1"
github.com/alecthomas/chroma/v2="v2.15.0"
github.com/andybalholm/cascadia="v1.3.3"
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
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/aymerick/douceur="v0.2.0"
github.com/bep/clocks="v0.5.0"
github.com/bep/debounce="v1.2.0"
github.com/bep/gitmap="v1.6.0"
github.com/bep/gitmap="v1.9.0"
github.com/bep/goat="v0.5.0"
github.com/bep/godartsass/v2="v2.3.2"
github.com/bep/godartsass/v2="v2.5.0"
github.com/bep/golibsass="v1.2.0"
github.com/bep/gowebp="v0.3.0"
github.com/bep/imagemeta="v0.8.4"
github.com/bep/lazycache="v0.7.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/logg="v0.4.0"
github.com/bep/mclib="v1.20400.20402"
github.com/bep/overlayfs="v0.9.2"
github.com/bep/simplecobra="v0.5.0"
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/cespare/xxhash/v2="v2.3.0"
github.com/clbanning/mxj/v2="v2.7.0"
github.com/cpuguy83/go-md2man/v2="v2.0.4"
github.com/clipperhouse/displaywidth="v0.6.0"
github.com/clipperhouse/stringish="v0.1.1"
github.com/clipperhouse/uax29/v2="v2.3.0"
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/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
github.com/evanw/esbuild="v0.24.2"
github.com/evanw/esbuild="v0.27.2"
github.com/fatih/color="v1.18.0"
github.com/frankban/quicktest="v1.14.6"
github.com/fsnotify/fsnotify="v1.8.0"
github.com/getkin/kin-openapi="v0.129.0"
github.com/ghodss/yaml="v1.0.0"
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/go-sourcemap/sourcemap="v2.1.4+incompatible"
github.com/gobuffalo/flect="v1.0.3"
github.com/gobwas/glob="v0.2.3"
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
github.com/gohugoio/hashstructure="v0.5.0"
github.com/gohugoio/httpcache="v0.7.0"
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
github.com/goccy/go-yaml="v1.19.1"
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/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
github.com/google/go-cmp="v0.6.0"
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941"
github.com/google/go-cmp="v0.7.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"
@@ -224,58 +232,57 @@ github.com/josharian/intern="v1.0.0"
github.com/kr/pretty="v0.3.1"
github.com/kr/text="v0.2.0"
github.com/kyokomi/emoji/v2="v2.2.13"
github.com/lucasb-eyer/go-colorful="v1.2.0"
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-runewidth="v0.0.9"
github.com/mazznoer/csscolorparser="v0.1.5"
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.7.0"
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
github.com/olekukonko/tablewriter="v0.0.5"
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/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/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
github.com/pelletier/go-toml/v2="v2.2.3"
github.com/pelletier/go-toml/v2="v2.2.4"
github.com/perimeterx/marshmallow="v1.1.5"
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
github.com/pkg/errors="v0.9.1"
github.com/rivo/uniseg="v0.4.7"
github.com/rogpeppe/go-internal="v1.13.1"
github.com/rogpeppe/go-internal="v1.14.1"
github.com/russross/blackfriday/v2="v2.1.0"
github.com/sass/libsass="3.6.6"
github.com/spf13/afero="v1.11.0"
github.com/spf13/cast="v1.7.1"
github.com/spf13/cobra="v1.8.1"
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/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.6"
github.com/tdewolff/minify/v2="v2.20.37"
github.com/tdewolff/parse/v2="v2.7.15"
github.com/tetratelabs/wazero="v1.8.2"
github.com/webmproject/libwebp="v1.3.2"
github.com/yuin/goldmark-emoji="v1.0.4"
github.com/yuin/goldmark="v1.7.8"
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/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"
go.uber.org/automaxprocs="v1.5.3"
golang.org/x/crypto="v0.33.0"
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac"
golang.org/x/image="v0.24.0"
golang.org/x/mod="v0.23.0"
golang.org/x/net="v0.35.0"
golang.org/x/sync="v0.11.0"
golang.org/x/sys="v0.30.0"
golang.org/x/text="v0.22.0"
golang.org/x/tools="v0.30.0"
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
gonum.org/v1/plot="v0.15.0"
google.golang.org/protobuf="v1.36.5"
gopkg.in/yaml.v2="v2.4.0"
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"
oss.terrastruct.com/d2="v0.6.9"
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
rsc.io/qr="v0.2.0"
software.sslmate.com/src/go-pkcs12="v0.2.0"
```
+3 -3
View File
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
infol := opts.Log.InfoCommand("dynacache")
evictedIdentities := collections.NewStack[KeyIdentity]()
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
onEvict := func(k, v any) {
if !opts.Watching {
@@ -129,7 +129,7 @@ type Cache struct {
partitions map[string]PartitionManager
onEvict func(k, v any)
evictedIdentities *collections.Stack[KeyIdentity]
evictedIdentities *collections.StackThreadSafe[KeyIdentity]
opts Options
infol logg.LevelLogger
@@ -340,7 +340,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
panic("invalid Weight, must be between 1 and 100")
}
if partitionNameRe.FindString(name) != name {
if !partitionNameRe.MatchString(name) {
panic(fmt.Sprintf("invalid partition name %q", name))
}
+55 -11
View File
@@ -16,6 +16,7 @@ package filecache
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@@ -54,7 +55,11 @@ type Cache struct {
nlocker *lockTracker
isResourceDir bool
baseDir string
initOnce sync.Once
isInited bool
initErr error
}
@@ -109,9 +114,14 @@ func (l *lockedFile) Close() error {
}
func (c *Cache) init() error {
if c == nil {
panic("cache is nil")
}
c.initOnce.Do(func() {
c.isInited = true
// Create the base dir if it does not exist.
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
err = fmt.Errorf("failled to create base cache directory: %s", err)
c.initErr = err
}
})
@@ -286,8 +296,32 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
return info, b, nil
}
// SetBytes sets the file content with the given id in the cache.
func (c *Cache) SetBytes(id string, data []byte) error {
if err := c.init(); err != nil {
return err
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
if c.maxAge == 0 {
// No caching.
return nil
}
return c.writeReader(id, bytes.NewReader(data))
}
// GetBytes gets the file content with the given id from the cache, nil if none found.
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
func (c *Cache) GetBytes(id string) ([]byte, error) {
_, b, err := c.GetItemBytes(id)
return b, err
}
// GetItemBytes gets the ItemInfo and file content with the given id from the cache, nil if none found.
func (c *Cache) GetItemBytes(id string) (ItemInfo, []byte, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
@@ -417,6 +451,17 @@ func (c *Cache) GetString(id string) string {
// Caches is a named set of caches.
type Caches map[string]*Cache
func (f Caches) SetResourceFs(fs afero.Fs) {
for _, c := range f {
if c.isResourceDir {
if c.isInited {
panic("cannot set resource fs after init")
}
c.Fs = hugofs.NewBasePathFs(fs, c.baseDir)
}
}
}
// Get gets a named cache, nil if none found.
func (f Caches) Get(name string) *Cache {
return f[strings.ToLower(name)]
@@ -424,24 +469,19 @@ func (f Caches) Get(name string) *Cache {
// NewCaches creates a new set of file caches from the given
// configuration.
func NewCaches(p *helpers.PathSpec) (Caches, error) {
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
fs := p.Fs.Source
func NewCaches(dcfg Configs, sourceFs afero.Fs) (Caches, error) {
// dcfg := p.Cfg.GetConfigSection("caches").(Configs)
fs := sourceFs
m := make(Caches)
for k, v := range dcfg {
var cfs afero.Fs
if v.IsResourceDir {
cfs = p.BaseFs.ResourcesCache
cfs = nil // Set later. TODO(bep) this needs to be cleanded up.
} else {
cfs = fs
}
if cfs == nil {
panic("nil fs")
}
baseDir := v.DirCompiled
bfs := hugofs.NewBasePathFs(cfs, baseDir)
@@ -451,7 +491,11 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
pruneAllRootDir = "pkg"
}
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
c := NewCache(bfs, v.MaxAge, pruneAllRootDir)
c.isResourceDir = v.IsResourceDir
c.baseDir = baseDir
m[k] = c
}
return m, nil
+49 -9
View File
@@ -15,6 +15,7 @@
package filecache
import (
"encoding/json"
"errors"
"fmt"
"path"
@@ -22,7 +23,7 @@ import (
"strings"
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
@@ -40,27 +41,39 @@ var defaultCacheConfig = FileCacheConfig{
}
const (
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyModuleQueries = "modulequeries"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
)
type Configs map[string]FileCacheConfig
// CacheDirModules returns the compiled path to the modules cache.
// For internal use.
func (c Configs) CacheDirModules() string {
return c[CacheKeyModules].DirCompiled
}
// CacheDirMisc returns the compiled path to the misc cache.
// For internal use.
func (c Configs) CacheDirMisc() string {
return c[CacheKeyMisc].DirCompiled
}
var defaultCacheConfigs = Configs{
CacheKeyModules: {
MaxAge: -1,
Dir: ":cacheDir/modules",
},
CacheKeyModuleQueries: {
MaxAge: 24 * time.Hour,
Dir: ":cacheDir/modules",
},
CacheKeyGetJSON: defaultCacheConfig,
CacheKeyGetCSV: defaultCacheConfig,
CacheKeyImages: {
@@ -100,6 +113,23 @@ type FileCacheConfig struct {
IsResourceDir bool `json:"-"`
}
// MarshalJSON marshals FileCacheConfig to JSON with MaxAge as a human-readable string.
func (c FileCacheConfig) MarshalJSON() ([]byte, error) {
var maxAge any
if c.MaxAge == -1 {
maxAge = -1
} else {
maxAge = strings.TrimSuffix(c.MaxAge.String(), "0m0s")
}
return json.Marshal(&struct {
MaxAge any `json:"maxAge"`
Dir string `json:"dir"`
}{
MaxAge: maxAge,
Dir: c.Dir,
})
}
// GetJSONCache gets the file cache for getJSON.
func (f Caches) GetJSONCache() *Cache {
return f[CacheKeyGetJSON]
@@ -120,6 +150,16 @@ func (f Caches) ModulesCache() *Cache {
return f[CacheKeyModules]
}
// ModuleQueriesCache gets the file cache for Hugo Module version queries.
// Returns nil if not found.
func (f Caches) ModuleQueriesCache() *Cache {
c, ok := f[CacheKeyModuleQueries]
if !ok {
panic("module queries cache not set")
}
return c
}
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
func (f Caches) AssetsCache() *Cache {
return f[CacheKeyAssets]
@@ -147,7 +187,7 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
_, isOsFs := fs.(*afero.OsFs)
for k, v := range m {
if _, ok := v.(maps.Params); !ok {
if _, ok := v.(hmaps.Params); !ok {
continue
}
cc := defaultCacheConfig
+30 -3
View File
@@ -14,6 +14,7 @@
package filecache_test
import (
"encoding/json"
"path/filepath"
"runtime"
"testing"
@@ -59,7 +60,7 @@ dir = "/path/to/c4"
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c.Assert(len(decoded), qt.Equals, 8)
c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
@@ -106,7 +107,7 @@ dir = "/path/to/c4"
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c.Assert(len(decoded), qt.Equals, 8)
for _, v := range decoded {
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
@@ -129,7 +130,7 @@ func TestDecodeConfigDefault(t *testing.T) {
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c.Assert(len(decoded), qt.Equals, 8)
imgConfig := decoded[filecache.CacheKeyImages]
jsonConfig := decoded[filecache.CacheKeyGetJSON]
@@ -144,3 +145,29 @@ func TestDecodeConfigDefault(t *testing.T) {
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
}
func TestFileCacheConfigMarshalJSON(t *testing.T) {
c := qt.New(t)
cfg := config.New()
cfg.Set("cacheDir", "/cache")
cfg.Set("workingDir", "/my/project")
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
moduleQueriesConfig := decoded[filecache.CacheKeyModuleQueries]
c.Assert(moduleQueriesConfig.MaxAge, qt.Equals, 24*time.Hour)
b, err := json.Marshal(moduleQueriesConfig)
c.Assert(err, qt.IsNil)
c.Assert(string(b), qt.Contains, `"maxAge":"24h"`)
c.Assert(string(b), qt.Not(qt.Contains), "86400000000000")
c.Assert(string(b), qt.Not(qt.Contains), "8.64e")
moduleQueriesConfig.MaxAge = -1
b, err = json.Marshal(moduleQueriesConfig)
c.Assert(err, qt.IsNil)
c.Assert(string(b), qt.Contains, `"maxAge":-1`)
}
+1 -1
View File
@@ -67,7 +67,7 @@ title: "Home"
---
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/index.html --
-- layouts/home.html --
{{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }}
+7 -3
View File
@@ -55,9 +55,12 @@ dir = ":resourceDir/_gen"
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
msg := qt.Commentf("cache: %s", name)
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := filecache.NewCaches(p)
fs := afero.NewMemMapFs()
p := newPathsSpec(t, fs, configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, fs)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(fs)
cache := caches[name]
for i := range 10 {
id := fmt.Sprintf("i%d", i)
@@ -84,8 +87,9 @@ dir = ":resourceDir/_gen"
}
}
caches, err = filecache.NewCaches(p)
caches, err = filecache.NewCaches(fileCachConfig, fs)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(fs)
cache = caches[name]
// Touch one and then prune.
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
+11 -5
View File
@@ -18,6 +18,8 @@ import (
"fmt"
"io"
"strings"
"github.com/gohugoio/hugo/htesting"
"sync"
"testing"
"time"
@@ -78,9 +80,11 @@ dir = ":cacheDir/c"
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
p := newPathsSpec(t, osfs, configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(p)
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(p.SourceFs)
cache := caches.Get("GetJSON")
c.Assert(cache, qt.Not(qt.IsNil))
@@ -149,7 +153,7 @@ dir = ":cacheDir/c"
r.Close()
c.Assert(string(b), qt.Equals, "Hugo is great!")
info, b, err = caches.ImageCache().GetBytes("mykey")
info, b, err = caches.ImageCache().GetItemBytes("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
c.Assert(string(b), qt.Equals, "Hugo is great!")
@@ -158,6 +162,7 @@ dir = ":cacheDir/c"
}
func TestFileCacheConcurrent(t *testing.T) {
htesting.SkipSlowTestUnlessCI(t)
t.Parallel()
c := qt.New(t)
@@ -179,9 +184,10 @@ dir = "/cache/c"
`
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := filecache.NewCaches(p)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(p.Fs.Source)
const cacheName = "getjson"
@@ -270,7 +276,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
acfg := testconfig.GetTestConfig(fs, cfg)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil, nil)
c.Assert(err, qt.IsNil)
return p
}
+21 -10
View File
@@ -25,6 +25,8 @@ import (
// DefaultConfig holds the default configuration for the HTTP cache.
var DefaultConfig = Config{
RespectCacheControlNoStoreInRequest: true,
RespectCacheControlNoStoreInResponse: false,
Cache: Cache{
For: GlobMatcher{
Excludes: []string{"**"},
@@ -42,7 +44,13 @@ var DefaultConfig = Config{
// Config holds the configuration for the HTTP cache.
type Config struct {
// Configures the HTTP cache behavior (RFC 9111).
// When enabled and there's a Cache-Control: no-store directive in the request, response will never be stored in disk cache.
RespectCacheControlNoStoreInRequest bool
// When enabled and there's a Cache-Control: no-store directive in the response, response will never be stored in disk cache.
RespectCacheControlNoStoreInResponse bool
// Enables HTTP cache behavior (RFC 9111) for these resources.
// When this is not enabled for a resource, Hugo will go straight to the file cache.
Cache Cache
@@ -57,7 +65,9 @@ type Cache struct {
}
func (c *Config) Compile() (ConfigCompiled, error) {
var cc ConfigCompiled
cc := ConfigCompiled{
Base: *c,
}
p, err := c.Cache.For.CompilePredicate()
if err != nil {
@@ -127,6 +137,7 @@ func (gm GlobMatcher) IsZero() bool {
}
type ConfigCompiled struct {
Base Config
For predicate.P[string]
PollConfigs []PollConfigCompiled
}
@@ -162,16 +173,16 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if gm.IsZero() {
panic("no includes or excludes")
}
var p predicate.P[string]
var b predicate.PR[string]
for _, include := range gm.Includes {
g, err := glob.Compile(include, '/')
if err != nil {
return nil, err
}
fn := func(s string) bool {
return g.Match(s)
fn := func(s string) predicate.Match {
return predicate.BoolMatch(g.Match(s))
}
p = p.Or(fn)
b = b.Or(fn)
}
for _, exclude := range gm.Excludes {
@@ -179,13 +190,13 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if err != nil {
return nil, err
}
fn := func(s string) bool {
return !g.Match(s)
fn := func(s string) predicate.Match {
return predicate.BoolMatch(!g.Match(s))
}
p = p.And(fn)
b = b.And(fn)
}
return p, nil
return b.BoolFunc(), nil
}
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
+1 -1
View File
@@ -52,7 +52,7 @@ func TestDefaultConfig(t *testing.T) {
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{})
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]any{})
c.Assert(err, qt.IsNil)
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
Executable
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
set -e
# Default to all packages if none specified
PACKAGES="${1:-./...}"
echo "==> Checking packages: $PACKAGES"
# Timing arrays
declare -a STEP_NAMES
declare -a STEP_TIMES
time_step() {
local name="$1"
shift
local start=$(date +%s.%N)
"$@"
local end=$(date +%s.%N)
local elapsed=$(echo "$end - $start" | bc)
STEP_NAMES+=("$name")
STEP_TIMES+=("$elapsed")
}
# Check gofmt
run_gofmt() {
echo "==> Running gofmt..."
# Convert package pattern to path (e.g., ./hugolib/... -> ./hugolib)
local path="${PACKAGES%/...}"
GOFMT_OUTPUT=$(gofmt -l "$path" 2>&1) || true
if [ -n "$GOFMT_OUTPUT" ]; then
echo "gofmt found issues in:"
echo "$GOFMT_OUTPUT"
exit 1
fi
echo " OK"
}
# Run staticcheck
run_staticcheck() {
# Check if staticcheck is installed, install if not
if ! command -v staticcheck &> /dev/null; then
echo "==> Installing staticcheck..."
go install honnef.co/go/tools/cmd/staticcheck@latest
fi
echo "==> Running staticcheck..."
staticcheck $PACKAGES
echo " OK"
}
# Run tests
run_tests() {
echo "==> Running tests..."
local output
if ! output=$(go test $PACKAGES 2>&1); then
echo "$output"
exit 1
fi
echo " OK"
}
# Run all steps with timing
TOTAL_START=$(date +%s.%N)
time_step "gofmt" run_gofmt
time_step "staticcheck" run_staticcheck
time_step "tests" run_tests
TOTAL_END=$(date +%s.%N)
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
# Print timing summary
echo ""
echo "==> All checks passed!"
echo ""
echo "Timing summary:"
echo "---------------"
for i in "${!STEP_NAMES[@]}"; do
printf " %-15s %6.2fs\n" "${STEP_NAMES[$i]}" "${STEP_TIMES[$i]}"
done
echo "---------------"
printf " %-15s %6.2fs\n" "Total" "$TOTAL_ELAPSED"
+1 -3
View File
@@ -401,9 +401,7 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
for _, group := range watchGroups {
r.Printf("Watching for changes in %s\n", group)
}
r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
watcher, err := b.newWatcher(r.poll, watchDirs...)
if err != nil {
return err
+8 -7
View File
@@ -23,8 +23,9 @@ import (
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
@@ -71,7 +72,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
return fmt.Errorf("language %q not found", c.lang)
}
} else {
config = conf.configs.LanguageConfigSlice[0]
config = conf.configs.LanguageConfigMap[conf.configs.Base.DefaultContentLanguage]
}
var buf bytes.Buffer
@@ -94,7 +95,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
maps.ConvertFloat64WithNoDecimalsToInt(m)
hmaps.ConvertFloat64WithNoDecimalsToInt(m)
switch format {
case "yaml":
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
@@ -128,9 +129,9 @@ func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
type configModMount struct {
Source string `json:"source"`
Target string `json:"target"`
Lang string `json:"lang,omitempty"`
Source string `json:"source"`
Target string `json:"target"`
Sites sitesmatrix.Sites `json:"sites,omitzero"`
}
type configModMounts struct {
@@ -146,7 +147,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
mounts = append(mounts, configModMount{
Source: mount.Source,
Target: mount.Target,
Lang: mount.Lang,
Sites: mount.Sites,
})
}
+12 -7
View File
@@ -28,6 +28,7 @@ import (
"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"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/helpers"
@@ -36,7 +37,6 @@ import (
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"gopkg.in/yaml.v2"
)
func newGenCommand() *genCommand {
@@ -51,6 +51,7 @@ func newGenCommand() *genCommand {
lineNumbersInlineStyle string
lineNumbersTableStyle string
omitEmpty bool
omitClassComments bool
)
newChromaStyles := func() simplecobra.Commander {
@@ -81,12 +82,14 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
return err
}
var formatter *html.Formatter
if omitEmpty {
formatter = html.New(html.WithClasses(true))
} else {
formatter = html.New(html.WithAllClasses(true))
// 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),
}
formatter := html.New(options...)
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
@@ -103,8 +106,10 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules (deprecated, no longer needed)`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
},
}
}
@@ -244,7 +249,7 @@ url: %s
return err
}
defer f.Close()
yamlEnc := yaml.NewEncoder(f)
yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt())
if err := yamlEnc.Encode(m); err != nil {
return err
}
+1 -1
View File
@@ -76,7 +76,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
// Flags with a different name in the config.
keyMap := map[string]string{
"minify": "minifyOutput",
"minify": "minify.minifyOutput",
"destination": "publishDir",
"editor": "newContentEditor",
}
+34 -5
View File
@@ -27,13 +27,15 @@ import (
"sync/atomic"
"time"
"github.com/bep/debounce"
"github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/terminal"
"github.com/gohugoio/hugo/common/types"
@@ -142,7 +144,7 @@ func (c *hugoBuilder) getDirList() ([]string, error) {
return nil, err
}
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
return hstrings.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
}
func (c *hugoBuilder) initCPUProfile() (func(), error) {
@@ -462,7 +464,15 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
infol.Logf("removing all files from destination that don't exist in static dirs")
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
name := f.Name()
// Keep .gitignore and .gitattributes anywhere
if name == ".gitignore" || name == ".gitattributes" {
return true
}
// Keep Hugo's original dot-directory behavior
return f.IsDir() && strings.HasPrefix(name, ".")
}
}
start := time.Now()
@@ -515,6 +525,14 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
return langCount, nil
}
func (c *hugoBuilder) progressIntermediate() {
terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
}
func (c *hugoBuilder) progressHidden() {
terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
}
func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
var (
g errgroup.Group
@@ -818,7 +836,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
continue
}
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
walkAdder := func(ctx context.Context, path string, f hugofs.FileMetaInfo) error {
if f.IsDir() {
c.r.logger.Println("adding created directory to watchlist", path)
if err := watcher.Add(path); err != nil {
@@ -1027,6 +1045,17 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
}
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
if terminal.PrintANSIColors(os.Stdout) {
defer c.progressHidden()
// If the configuration takes a while to load, we want to show some progress.
// This is typically loading of external modules.
d := debounce.New(500 * time.Millisecond)
d(func() {
c.progressIntermediate()
})
defer d(func() {})
}
cfg := config.New()
cfg.Set("renderToMemory", c.r.renderToMemory)
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
@@ -1048,7 +1077,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
}
cfg.Set("environment", c.r.environment)
cfg.Set("internal", maps.Params{
cfg.Set("internal", hmaps.Params{
"running": running,
"watch": watch,
"verbose": c.r.isVerbose(),
+4 -4
View File
@@ -29,9 +29,9 @@ import (
"unicode"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
@@ -190,7 +190,7 @@ func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyl
}
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
metadata, _ := maps.ToStringMapE(m)
metadata, _ := hmaps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
@@ -246,7 +246,7 @@ func (c *importCommand) convertJekyllContent(m any, content string) (string, err
}
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := maps.ToStringMapE(m)
metadata, err := hmaps.ToStringMapE(m)
if err != nil {
return nil, err
}
@@ -427,7 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
c.r.Println("Importing...")
fileCount := 0
callback := func(path string, fi hugofs.FileMetaInfo) error {
callback := func(ctx context.Context, path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
}
+3 -1
View File
@@ -53,7 +53,9 @@ Ensure you run this within the root directory of your site.`,
if len(args) < 1 {
return newUserError("path needs to be provided")
}
h, err := r.Hugo(flagsToCfg(cd, nil))
cfg := flagsToCfg(cd, nil)
cfg.Set("BuildFuture", true)
h, err := r.Hugo(cfg)
if err != nil {
return err
}
+11 -7
View File
@@ -49,6 +49,8 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
@@ -369,6 +371,12 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
if isNavigation(requestURI, r) {
// See issue 14240.
// Hugo escapes the URL paths when generating them,
// that may not be the case when we receive it back from the browser.
// PathEscape will escape if it is not already escaped.
requestURI = paths.PathEscape(requestURI)
if !f.c.visitedURLs.Contains(requestURI) {
// If not already on stack, re-render that single page.
if err := f.c.partialReRender(requestURI); err != nil {
@@ -491,9 +499,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
for _, group := range watchGroups {
c.r.Printf("Watching for changes in %s\n", group)
}
c.r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
if err != nil {
return err
@@ -627,7 +633,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
panic("no server ports set")
}
return c.withConfE(func(conf *commonConfig) error {
for i, language := range conf.configs.LanguagesDefaultFirst {
for i, language := range conf.configs.Languages {
isMultihost := conf.configs.IsMultihost
var serverPort int
if isMultihost {
@@ -879,7 +885,7 @@ func (c *serverCommand) serve() error {
if isMultihost {
for _, l := range conf.configs.ConfigLangs() {
baseURLs = append(baseURLs, l.BaseURL())
roots = append(roots, l.Language().Lang)
roots = append(roots, l.Language().(*langs.Language).Lang)
}
} else {
l := conf.configs.GetFirstLanguageConfig()
@@ -1034,7 +1040,6 @@ func (c *serverCommand) serve() error {
defer cancel()
wg2, ctx := errgroup.WithContext(ctx)
for _, srv := range servers {
srv := srv
wg2.Go(func() error {
return srv.Shutdown(ctx)
})
@@ -1166,7 +1171,6 @@ func chmodFilter(dst, src os.FileInfo) bool {
}
func cleanErrorLog(content string) string {
content = strings.ReplaceAll(content, "\n", " ")
content = logReplacer.Replace(content)
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
+4 -16
View File
@@ -16,6 +16,8 @@ package collections
import (
"fmt"
"reflect"
"github.com/gohugoio/hugo/common/hreflect"
)
// Append appends from to a slice to and returns the resulting slice.
@@ -25,7 +27,7 @@ func Append(to any, from ...any) (any, error) {
if len(from) == 0 {
return to, nil
}
tov, toIsNil := indirect(reflect.ValueOf(to))
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to))
toIsNil = toIsNil || to == nil
var tot reflect.Type
@@ -100,7 +102,7 @@ func Append(to any, from ...any) (any, error) {
fv := reflect.ValueOf(f)
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
// Fall back to a []interface{} slice.
tov, _ := indirect(reflect.ValueOf(to))
tov, _ := hreflect.Indirect(reflect.ValueOf(to))
return appendToInterfaceSlice(tov, from...)
}
tov = reflect.Append(tov, fv)
@@ -136,17 +138,3 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
return tos, nil
}
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
// TODO(bep) consolidate
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}
-64
View File
@@ -15,7 +15,6 @@ package collections
import (
"html/template"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
@@ -148,66 +147,3 @@ func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
}
func TestIndirect(t *testing.T) {
t.Parallel()
c := qt.New(t)
type testStruct struct {
Field string
}
var (
nilPtr *testStruct
nilIface interface{} = nil
nonNilIface interface{} = &testStruct{Field: "hello"}
)
tests := []struct {
name string
input any
wantKind reflect.Kind
wantNil bool
}{
{
name: "nil pointer",
input: nilPtr,
wantKind: reflect.Ptr,
wantNil: true,
},
{
name: "nil interface",
input: nilIface,
wantKind: reflect.Invalid,
wantNil: false,
},
{
name: "non-nil pointer to struct",
input: &testStruct{Field: "abc"},
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "non-nil interface holding pointer",
input: nonNilIface,
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "plain value",
input: testStruct{Field: "xyz"},
wantKind: reflect.Struct,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := reflect.ValueOf(tt.input)
got, isNil := indirect(v)
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
c.Assert(isNil, qt.Equals, tt.wantNil)
})
}
}
+65 -12
View File
@@ -13,28 +13,32 @@
package collections
import "slices"
import (
"iter"
"slices"
"sync"
import "sync"
"github.com/gohugoio/hugo/common/hiter"
)
// Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct {
// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
type StackThreadSafe[T any] struct {
items []T
zero T
mu sync.RWMutex
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
return &StackThreadSafe[T]{}
}
func (s *Stack[T]) Push(item T) {
func (s *StackThreadSafe[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
func (s *StackThreadSafe[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
@@ -45,7 +49,7 @@ func (s *Stack[T]) Pop() (T, bool) {
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
func (s *StackThreadSafe[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
@@ -54,13 +58,18 @@ func (s *Stack[T]) Peek() (T, bool) {
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
func (s *StackThreadSafe[T]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
func (s *Stack[T]) Drain() []T {
// All returns all items in the stack, from bottom to top.
func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
func (s *StackThreadSafe[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
@@ -68,7 +77,7 @@ func (s *Stack[T]) Drain() []T {
return items
}
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
@@ -80,3 +89,47 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
}
return items
}
// Stack is a simple LIFO stack that is not safe for concurrent use.
type Stack[T any] struct {
items []T
zero T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
return s.zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
return s.zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func (s *Stack[T]) All() iter.Seq2[int, T] {
return slices.All(s.items)
}
func (s *Stack[T]) Drain() []T {
items := s.items
s.items = nil
return items
}
+4 -4
View File
@@ -10,7 +10,7 @@ func TestNewStack(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
s := NewStackThreadSafe[int]()
c.Assert(s, qt.IsNotNil)
}
@@ -19,7 +19,7 @@ func TestStackBasic(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
s := NewStackThreadSafe[int]()
c.Assert(s.Len(), qt.Equals, 0)
@@ -50,7 +50,7 @@ func TestStackDrain(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[string]()
s := NewStackThreadSafe[string]()
s.Push("a")
s.Push("b")
@@ -64,7 +64,7 @@ func TestStackDrainMatching(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
s := NewStackThreadSafe[int]()
s.Push(1)
s.Push(2)
s.Push(3)
+34 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity"
)
@@ -38,6 +39,34 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
return h.Sum64(), size, nil
}
type Hasher interface {
io.StringWriter
io.Writer
io.ReaderFrom
Sum64() uint64
}
type HashCloser interface {
Hasher
io.Closer
}
// XxHasher returns a Hasher that uses xxHash.
// Remember to call Close when done.
func XxHasher() HashCloser {
h := getXxHashReadFrom()
return struct {
Hasher
io.Closer
}{
Hasher: h,
Closer: hugio.CloserFunc(func() error {
putXxHashReadFrom(h)
return nil
}),
}
}
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
// and returns the hash as a hex encoded string.
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
@@ -58,11 +87,13 @@ func XXHashFromString(s string) (uint64, error) {
return h.Sum64(), nil
}
// XxHashFromStringHexEncoded calculates the xxHash for the given string
// XxHashFromStringHexEncoded calculates the xxHash for the given strings
// and returns the hash as a hex encoded string.
func XxHashFromStringHexEncoded(f string) string {
func XxHashFromStringHexEncoded(s ...string) string {
h := xxhash.New()
h.WriteString(f)
for _, f := range s {
h.WriteString(f)
}
hash := h.Sum(nil)
return hex.EncodeToString(hash)
}
+8 -11
View File
@@ -38,7 +38,6 @@ func TestXxHashFromReaderPara(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
i := i
wg.Add(1)
go func() {
defer wg.Done()
@@ -75,8 +74,8 @@ func TestXxHashFromStringHexEncoded(t *testing.T) {
func BenchmarkXXHashFromReader(b *testing.B) {
r := strings.NewReader("Hello World")
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
XXHashFromReader(r)
r.Seek(0, 0)
}
@@ -84,16 +83,16 @@ func BenchmarkXXHashFromReader(b *testing.B) {
func BenchmarkXXHashFromString(b *testing.B) {
s := "Hello World"
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
XXHashFromString(s)
}
}
func BenchmarkXXHashFromStringHexEncoded(b *testing.B) {
s := "The quick brown fox jumps over the lazy dog"
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
XxHashFromStringHexEncoded(s)
}
}
@@ -136,7 +135,7 @@ func BenchmarkHashString(b *testing.B) {
for _, test := range tests {
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
HashString(test)
}
})
@@ -149,9 +148,7 @@ func BenchmarkHashMap(b *testing.B) {
m[fmt.Sprintf("key%d", i)] = i
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
HashString(m)
}
}
-46
View File
@@ -1,46 +0,0 @@
// Copyright 2024 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 hcontext
import "context"
// ContextDispatcher is a generic interface for setting and getting values from a context.
type ContextDispatcher[T any] interface {
Set(ctx context.Context, value T) context.Context
Get(ctx context.Context) T
}
// NewContextDispatcher creates a new ContextDispatcher with the given key.
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
return keyInContext[T, R]{
id: key,
}
}
type keyInContext[T any, R comparable] struct {
zero T
id R
}
func (f keyInContext[T, R]) Get(ctx context.Context) T {
v := ctx.Value(f.id)
if v == nil {
return f.zero
}
return v.(T)
}
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
return context.WithValue(ctx, f.id, value)
}
+61
View File
@@ -0,0 +1,61 @@
// 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 hdebug
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/htesting"
)
// Printf is a debug print function that should be removed before committing code to the repository.
func Printf(format string, args ...any) {
panicIfRealCI()
if len(args) == 1 && !strings.Contains(format, "%") {
format = format + ": %v"
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
fmt.Printf(format, args...)
}
func AssertNotNil(a ...any) {
panicIfRealCI()
for _, v := range a {
if types.IsNil(v) {
panic("hdebug.AssertNotNil: value is nil")
}
}
}
func Panicf(format string, args ...any) {
panicIfRealCI()
// fmt.Println(stack())
if len(args) == 1 && !strings.Contains(format, "%") {
format = format + ": %v"
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
panic(fmt.Sprintf(format, args...))
}
func panicIfRealCI() {
if htesting.IsRealCI() {
panic("This debug statement should be removed before committing code!")
}
}
+3 -10
View File
@@ -17,22 +17,13 @@ package herrors
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"
)
// PrintStackTrace prints the current stacktrace to w.
func PrintStackTrace(w io.Writer) {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Fprintf(w, "%s", buf)
}
// ErrorSender is a, typically, non-blocking error handler.
type ErrorSender interface {
SendError(err error)
@@ -45,7 +36,9 @@ type ErrorSender interface {
func Recover(args ...any) {
if r := recover(); r != nil {
fmt.Println("ERR:", r)
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
buf := make([]byte, 64<<10)
buf = buf[:runtime.Stack(buf, false)]
args = append(args, "stacktrace from panic: \n"+string(buf), "\n")
fmt.Println(args...)
}
}
+53 -34
View File
@@ -110,11 +110,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
fe.errorContext = ectx
if ectx.Position.LineNumber > 0 {
if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
fe.position.LineNumber = ectx.Position.LineNumber
}
if ectx.Position.ColumnNumber > 0 {
if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
fe.position.ColumnNumber = ectx.Position.ColumnNumber
}
@@ -177,6 +177,7 @@ func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
}
@@ -234,7 +235,9 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
fe := NewFileErrorFromName(err, realFilename)
fe = fe.UpdateContent(f, linematcher)
return fe
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -285,6 +288,50 @@ func Unwrap(err error) error {
return err
}
// UnwrapFileErrors returns all FileError contained in err.
func UnwrapFileErrors(err error) []FileError {
if err == nil {
return nil
}
errs := Errors(err)
var fileErrors []FileError
for _, e := range errs {
if v, ok := e.(FileError); ok {
fileErrors = append(fileErrors, v)
}
fileErrors = append(fileErrors, UnwrapFileErrors(errors.Unwrap(e))...)
}
return fileErrors
}
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
errs := UnwrapFileErrors(err)
var n int
for _, e := range errs {
if e.ErrorContext() != nil {
errs[n] = e
n++
}
}
return errs[:n]
}
// Errors returns the list of errors contained in err.
func Errors(err error) []error {
if err == nil {
return nil
}
type unwrapper interface {
Unwrap() []error
}
if u, ok := err.(unwrapper); ok {
return u.Unwrap()
}
return []error{err}
}
func extractFileTypePos(err error) (string, text.Position) {
err = Unwrap(err)
@@ -321,13 +368,9 @@ func extractFileTypePos(err error) (string, text.Position) {
}
// Look in the error message for the line number.
for _, handle := range lineNumberExtractors {
lno, col := handle(err)
if lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
break
}
if lno, col := commonLineNumberExtractor(err); lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
}
if fileType == "" && pos.Filename != "" {
@@ -351,30 +394,6 @@ func UnwrapFileError(err error) FileError {
return nil
}
// UnwrapFileErrors tries to unwrap all FileError.
func UnwrapFileErrors(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
func extractOffsetAndType(e error) (int, string) {
switch v := e.(type) {
case *json.UnmarshalTypeError:
+13 -3
View File
@@ -19,17 +19,27 @@ import (
)
var lineNumberExtractors = []lineNumberExtractor{
// YAML parse errors.
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
// YAML parse errors
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
// i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
}
func commonLineNumberExtractor(e error) (int, int) {
for _, handler := range lineNumberExtractors {
lno, col := handler(e)
if lno > 0 {
return lno, col
}
}
return 0, 0
}
type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
@@ -1,7 +1,5 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Portions Copyright The Go Authors.
// 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
@@ -13,26 +11,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package tplimpl_test
package herrors
import (
"errors"
"testing"
"github.com/gohugoio/hugo/hugolib"
qt "github.com/frankban/quicktest"
)
func TestLegacyPartialIssue13599(t *testing.T) {
func TestCommonLineNumberExtractor(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/partials/mypartial.html --
Mypartial.
-- layouts/_default/index.html --
mypartial: {{ template "partials/mypartial.html" . }}
c := qt.New(t)
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Mypartial.")
lno, col := commonLineNumberExtractor(errors.New("[4:9] value is not allowed in this context"))
c.Assert(lno, qt.Equals, 4)
c.Assert(col, qt.Equals, 9)
}
+3 -3
View File
@@ -27,8 +27,8 @@ import (
"sync"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/security"
)
@@ -102,7 +102,7 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
workingDir: workingDir,
infol: log.InfoCommand("exec"),
baseEnviron: baseEnviron,
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
newNPXRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
}
}
@@ -121,7 +121,7 @@ type Exec struct {
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
baseEnviron []string
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
newNPXRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
npxInit sync.Once
npxAvailable bool
}
+67
View File
@@ -0,0 +1,67 @@
// 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 himage provides some high level image types and interfaces.
package himage
import "image"
// AnimatedImage represents an animated image.
// This is currently supported for GIF and WebP images.
type AnimatedImage interface {
image.Image // The first frame.
GetRaw() any // *gif.GIF or *WEBP.
GetLoopCount() int // Number of times to loop the animation. 0 means infinite.
ImageFrames
}
// ImageFrames provides access to the frames of an animated image.
type ImageFrames interface {
GetFrames() []image.Image
// Frame durations in milliseconds.
// Note that Gif frame durations are in 100ths of a second,
// so they need to be multiplied by 10 to get milliseconds and vice versa.
GetFrameDurations() []int
SetFrames(frames []image.Image)
SetWidthHeight(width, height int)
}
// ImageConfigProvider provides access to the image.Config of an image.
type ImageConfigProvider interface {
GetImageConfig() image.Config
}
// FrameDurationsToGifDelays converts frame durations in milliseconds to
// GIF delays in 100ths of a second.
func FrameDurationsToGifDelays(frameDurations []int) []int {
delays := make([]int, len(frameDurations))
for i, fd := range frameDurations {
delays[i] = fd / 10
if delays[i] == 0 && fd > 0 {
delays[i] = 1
}
}
return delays
}
// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to
// frame durations in milliseconds.
func GifDelaysToFrameDurations(delays []int) []int {
frameDurations := make([]int, len(delays))
for i, d := range delays {
frameDurations[i] = d * 10
}
return frameDurations
}
+66
View File
@@ -0,0 +1,66 @@
package hiter
// Common iterator functions.
// Some of these are are based on this discsussion: https://github.com/golang/go/issues/61898
import "iter"
// Concat returns an iterator over the concatenation of the sequences.
// Any nil sequences are ignored.
func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] {
return func(yield func(V) bool) {
for _, seq := range seqs {
if seq == nil {
continue
}
for e := range seq {
if !yield(e) {
return
}
}
}
}
}
// Concat2 returns an iterator over the concatenation of the sequences.
// Any nil sequences are ignored.
func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for _, seq := range seqs {
if seq == nil {
continue
}
for k, v := range seq {
if !yield(k, v) {
return
}
}
}
}
}
// Lock returns an iterator that locks before iterating and unlocks after.
func Lock[V any](seq iter.Seq[V], lock, unlock func()) iter.Seq[V] {
return func(yield func(V) bool) {
lock()
defer unlock()
for e := range seq {
if !yield(e) {
return
}
}
}
}
// Lock2 returns an iterator that locks before iterating and unlocks after.
func Lock2[K, V any](seq iter.Seq2[K, V], lock, unlock func()) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
lock()
defer unlock()
for k, v := range seq {
if !yield(k, v) {
return
}
}
}
}
+27 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"sync"
@@ -20,13 +20,27 @@ import (
// Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct {
m map[K]T
opts CacheOptions
hasBeenInitialized bool
sync.RWMutex
}
// NewCache creates a new Cache.
// CacheOptions are the options for the Cache.
type CacheOptions struct {
// If set, the cache will not grow beyond this size.
Size uint64
}
var defaultCacheOptions = CacheOptions{}
// NewCache creates a new Cache with default options.
func NewCache[K comparable, T any]() *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T)}
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions}
}
// NewCacheWithOptions creates a new Cache with the given options.
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: opts}
}
// Delete deletes the given key from the cache.
@@ -65,6 +79,7 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
if err != nil {
return v, err
}
c.clearIfNeeded()
c.m[key] = v
return v, nil
}
@@ -127,7 +142,15 @@ func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
}
}
func (c *Cache[K, T]) clearIfNeeded() {
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
// clear the map
clear(c.m)
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.clearIfNeeded()
c.m[key] = value
}
+57
View File
@@ -0,0 +1,57 @@
// 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 hmaps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCacheSize(t *testing.T) {
c := qt.New(t)
cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})
for i := range 30 {
cache.Set(string(rune('a'+i)), "value")
}
c.Assert(len(cache.m), qt.Equals, 10)
for i := 20; i < 50; i++ {
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
return "value", nil
})
}
c.Assert(len(cache.m), qt.Equals, 10)
for i := 100; i < 200; i++ {
cache.SetIfAbsent(string(rune('a'+i)), "value")
}
c.Assert(len(cache.m), qt.Equals, 10)
cache.InitAndGet("foo", func(
get func(key string) (string, bool), set func(key string, value string),
) error {
for i := 50; i < 100; i++ {
set(string(rune('a'+i)), "value")
}
return nil
})
c.Assert(len(cache.m), qt.Equals, 10)
}
+129
View File
@@ -0,0 +1,129 @@
// 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 hmaps
import (
"iter"
"sync"
)
func NewMap[K comparable, T any]() *Map[K, T] {
return &Map[K, T]{
m: make(map[K]T),
}
}
// Map is a thread safe map backed by a Go map.
type Map[K comparable, T any] struct {
m map[K]T
mu sync.RWMutex
}
// Get gets the value for the given key.
// It returns the zero value of T if the key is not found.
func (m *Map[K, T]) Get(key K) T {
v, _ := m.Lookup(key)
return v
}
// Lookup looks up the given key in the map.
// It returns the value and a boolean indicating whether the key was found.
func (m *Map[K, T]) Lookup(key K) (T, bool) {
m.mu.RLock()
v, found := m.m[key]
m.mu.RUnlock()
return v, found
}
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
v, found := m.Lookup(key)
if found {
return v, nil
}
m.mu.Lock()
defer m.mu.Unlock()
v, found = m.m[key]
if found {
return v, nil
}
v, err := create()
if err != nil {
return v, err
}
m.m[key] = v
return v, nil
}
// Set sets the given key to the given value.
func (m *Map[K, T]) Set(key K, value T) {
m.mu.Lock()
m.m[key] = value
m.mu.Unlock()
}
// Delete deletes the given key from the map.
// It returns true if the key was found and deleted, false otherwise.
func (m *Map[K, T]) Delete(key K) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; found {
delete(m.m, key)
return true
}
return false
}
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
m.mu.Lock()
defer m.mu.Unlock()
return f(m.m)
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
// It returns true if the value was set, false otherwise.
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
m.mu.RLock()
if _, found := m.m[key]; !found {
m.mu.RUnlock()
return m.doSetIfAbsent(key, value)
}
m.mu.RUnlock()
return false
}
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; !found {
m.m[key] = value
return true
}
return false
}
// All returns an iterator over all key/value pairs in the map.
// A read lock is held during the iteration.
func (m *Map[K, T]) All() iter.Seq2[K, T] {
return func(yield func(K, T) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for k, v := range m.m {
if !yield(k, v) {
return
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
// 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 hmaps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestMap(t *testing.T) {
c := qt.New(t)
m := NewMap[string, int]()
m.Set("b", 42)
v, found := m.Lookup("b")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 42)
v = m.Get("b")
c.Assert(v, qt.Equals, 42)
v, found = m.Lookup("c")
c.Assert(found, qt.Equals, false)
c.Assert(v, qt.Equals, 0)
v = m.Get("c")
c.Assert(v, qt.Equals, 0)
v, err := m.GetOrCreate("d", func() (int, error) {
return 100, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
v, found = m.Lookup("d")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 100)
v, err = m.GetOrCreate("d", func() (int, error) {
return 200, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
wasSet := m.SetIfAbsent("e", 300)
c.Assert(wasSet, qt.Equals, true)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
wasSet = m.SetIfAbsent("e", 400)
c.Assert(wasSet, qt.Equals, false)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
m.WithWriteLock(func(m map[string]int) error {
m["f"] = 500
return nil
})
v, found = m.Lookup("f")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 500)
}
+4 -2
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"fmt"
@@ -98,6 +98,8 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
return v, nil
case Params:
return []map[string]any{v}, nil
case map[string]any:
return []map[string]any{v}, nil
case []any:
var s []map[string]any
for _, entry := range v {
@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"fmt"
@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"slices"
@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"testing"
+154
View File
@@ -0,0 +1,154 @@
// 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 hmaps
import (
"fmt"
"slices"
"github.com/bits-and-blooms/bitset"
)
type OrderedIntSet struct {
keys []int
values *bitset.BitSet
}
// NewOrderedIntSet creates a new OrderedIntSet.
// Note that this is backed by https://github.com/bits-and-blooms/bitset
func NewOrderedIntSet(vals ...int) *OrderedIntSet {
m := &OrderedIntSet{
keys: make([]int, 0, len(vals)),
values: bitset.New(uint(len(vals))),
}
for _, v := range vals {
m.Set(v)
}
return m
}
// Set sets the value for the given key.
// Note that insertion order is not affected if a key is re-inserted into the set.
func (m *OrderedIntSet) Set(key int) {
if m == nil {
panic("nil OrderedIntSet")
}
keyu := uint(key)
if m.values.Test(keyu) {
return
}
m.values.Set(keyu)
m.keys = append(m.keys, key)
}
// SetFrom sets the values from another OrderedIntSet.
func (m *OrderedIntSet) SetFrom(other *OrderedIntSet) {
if m == nil || other == nil {
return
}
for _, key := range other.keys {
m.Set(key)
}
}
func (m *OrderedIntSet) Clone() *OrderedIntSet {
if m == nil {
return nil
}
newSet := &OrderedIntSet{
keys: slices.Clone(m.keys),
values: m.values.Clone(),
}
return newSet
}
// Next returns the next key in the set possibly including the given key.
// It returns -1 if the key is not found or if there are no keys greater than the given key.
func (m *OrderedIntSet) Next(i int) int {
n, ok := m.values.NextSet(uint(i))
if !ok {
return -1
}
return int(n)
}
// The reason we don't use iter.Seq is https://github.com/golang/go/issues/69015
// This is 70% faster than using iter.Seq2[int, int] for the keys.
// It returns false if the iteration was stopped early.
func (m *OrderedIntSet) ForEachKey(yield func(int) bool) bool {
if m == nil {
return true
}
for _, key := range m.keys {
if !yield(key) {
return false
}
}
return true
}
func (m *OrderedIntSet) Has(key int) bool {
if m == nil {
return false
}
return m.values.Test(uint(key))
}
func (m *OrderedIntSet) Len() int {
if m == nil {
return 0
}
return len(m.keys)
}
// KeysSorted returns the keys in sorted order.
func (m *OrderedIntSet) KeysSorted() []int {
if m == nil {
return nil
}
keys := slices.Clone(m.keys)
slices.Sort(keys)
return m.keys
}
func (m *OrderedIntSet) String() string {
if m == nil {
return "[]"
}
return fmt.Sprintf("%v", m.keys)
}
func (m *OrderedIntSet) Values() *bitset.BitSet {
if m == nil {
return nil
}
return m.values
}
func (m *OrderedIntSet) IsSuperSet(other *OrderedIntSet) bool {
if m == nil || other == nil {
return false
}
return m.values.IsSuperSet(other.values)
}
// Words returns the bitset as array of 64-bit words, giving direct access to the internal representation.
// It is not a copy, so changes to the returned slice will affect the bitset.
// It is meant for advanced users.
func (m *OrderedIntSet) Words() []uint64 {
if m == nil {
return nil
}
return m.values.Words()
}
+117
View File
@@ -0,0 +1,117 @@
// 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 hmaps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestOrderedIntSet(t *testing.T) {
c := qt.New(t)
m := NewOrderedIntSet(2, 1, 3, 7)
c.Assert(m.Len(), qt.Equals, 4)
c.Assert(m.Has(1), qt.Equals, true)
c.Assert(m.Has(4), qt.Equals, false)
c.Assert(m.String(), qt.Equals, "[2 1 3 7]")
m.Set(4)
c.Assert(m.Len(), qt.Equals, 5)
c.Assert(m.Has(4), qt.Equals, true)
c.Assert(m.Next(0), qt.Equals, 1)
c.Assert(m.Next(1), qt.Equals, 1)
c.Assert(m.Next(2), qt.Equals, 2)
c.Assert(m.Next(3), qt.Equals, 3)
c.Assert(m.Next(4), qt.Equals, 4)
c.Assert(m.Next(7), qt.Equals, 7)
c.Assert(m.Next(8), qt.Equals, -1)
c.Assert(m.String(), qt.Equals, "[2 1 3 7 4]")
var nilset *OrderedIntSet
c.Assert(nilset.Len(), qt.Equals, 0)
c.Assert(nilset.Has(1), qt.Equals, false)
c.Assert(nilset.String(), qt.Equals, "[]")
var collected []int
m.ForEachKey(func(key int) bool {
collected = append(collected, key)
return true
})
c.Assert(collected, qt.DeepEquals, []int{2, 1, 3, 7, 4})
}
func BenchmarkOrderedIntSet(b *testing.B) {
smallSet := NewOrderedIntSet()
for i := range 8 {
smallSet.Set(i)
}
mediumSet := NewOrderedIntSet()
for i := range 64 {
mediumSet.Set(i)
}
largeSet := NewOrderedIntSet()
for i := range 1024 {
largeSet.Set(i)
}
b.Run("New", func(b *testing.B) {
for b.Loop() {
NewOrderedIntSet(1, 2, 3, 4, 5, 6, 7, 8)
}
})
b.Run("Has small", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
smallSet.Has(i % 32)
}
})
b.Run("Has medium", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
mediumSet.Has(i % 32)
}
})
b.Run("Next", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
mediumSet.Next(i % 32)
}
})
b.Run("ForEachKey small", func(b *testing.B) {
for b.Loop() {
smallSet.ForEachKey(func(key int) bool {
return true
})
}
})
b.Run("ForEachKey medium", func(b *testing.B) {
for b.Loop() {
mediumSet.ForEachKey(func(key int) bool {
return true
})
}
})
b.Run("ForEachKey large", func(b *testing.B) {
for b.Loop() {
largeSet.ForEachKey(func(key int) bool {
return true
})
}
})
}
@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// 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.
@@ -11,9 +11,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"errors"
"fmt"
"strings"
@@ -40,6 +41,14 @@ func (p Params) GetNested(indices ...string) any {
// SetParams overwrites values in dst with values in src for common or new keys.
// This is done recursively.
func SetParams(dst, src Params) {
setParams(dst, src, 0)
}
func setParams(dst, src Params, depth int) {
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
for k, v := range src {
vv, found := dst[k]
if !found {
@@ -48,7 +57,7 @@ func SetParams(dst, src Params) {
switch vvv := vv.(type) {
case Params:
if pv, ok := v.(Params); ok {
SetParams(vvv, pv)
setParams(vvv, pv, depth+1)
} else {
dst[k] = v
}
@@ -101,7 +110,6 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
for k, v := range pp {
if k == MergeStrategyKey {
continue
}
@@ -179,6 +187,56 @@ func getNested(m map[string]any, indices []string) (any, string, map[string]any)
}
}
// CreateNestedParamsFromSegements creates empty nested maps for the given keySegments in the target map.
func CreateNestedParamsFromSegements(target Params, keySegments ...string) Params {
if len(keySegments) == 0 {
return target
}
m := target
for i, key := range keySegments {
v, found := m[key]
if !found {
nm := Params{}
m[key] = nm
m = nm
if i == len(keySegments)-1 {
return nm
}
continue
}
m = v.(Params)
}
return m
}
// CreateNestedParamsSepString creates empty nested maps for the given keyStr in the target map
// It returns the last map created.
func CreateNestedParamsSepString(keyStr, separator string, target Params) Params {
keySegments := strings.Split(keyStr, separator)
return CreateNestedParamsFromSegements(target, keySegments...)
}
// SetNestedParamIfNotSet sets the value for the given keyStr in the target map if it does not exist.
// It assumes that all but the last key in keyStr is a Params map or should be one.
func SetNestedParamIfNotSet(keyStr, separator string, value any, target Params) Params {
keySegments := strings.Split(keyStr, separator)
if len(keySegments) == 0 {
return target
}
base := keySegments[:len(keySegments)-1]
last := keySegments[len(keySegments)-1]
m := CreateNestedParamsFromSegements(target, base...)
if _, ok := m[last]; !ok {
m[last] = value
}
return target
}
// GetNestedParam gets the first match of the keyStr in the candidates given.
// It will first try the exact match and then try to find it as a nested map value,
// using the given separator, e.g. "mymap.name".
@@ -266,9 +324,17 @@ func CleanConfigStringMapString(m map[string]string) map[string]string {
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any.
func CleanConfigStringMap(m map[string]any) map[string]any {
return doCleanConfigStringMap(m, 0)
}
func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
if len(m) == 0 {
return m
}
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
if _, found := m[MergeStrategyKey]; !found {
return m
}
@@ -280,9 +346,9 @@ func CleanConfigStringMap(m map[string]any) map[string]any {
}
switch v2 := v.(type) {
case map[string]any:
m2[k] = CleanConfigStringMap(v2)
m2[k] = doCleanConfigStringMap(v2, depth+1)
case Params:
var p Params = CleanConfigStringMap(v2)
var p Params = doCleanConfigStringMap(v2, depth+1)
m2[k] = p
case map[string]string:
m2[k] = CleanConfigStringMapString(v2)
@@ -344,6 +410,29 @@ func PrepareParams(m Params) {
}
}
// CloneParamsDeep does a deep clone of the given Params,
// meaning that any nested Params will be cloned as well.
func CloneParamsDeep(m Params) Params {
return cloneParamsDeep(m, 0)
}
func cloneParamsDeep(m Params, depth int) Params {
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
m2 := make(Params)
for k, v := range m {
switch vv := v.(type) {
case Params:
m2[k] = cloneParamsDeep(vv, depth+1)
default:
m2[k] = v
}
}
return m2
}
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
func PrepareParamsClone(m Params) Params {
m2 := make(Params)
@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hmaps
import (
"testing"
@@ -167,3 +167,35 @@ func TestParamsIsZero(t *testing.T) {
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue)
}
func TestSetNestedParamIfNotSet(t *testing.T) {
c := qt.New(t)
m := Params{}
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
c.Assert(m, qt.DeepEquals, Params{
"a": Params{
"b": Params{
"c": "value",
},
},
})
m = Params{
"a": Params{
"b": Params{
"c": "existingValue",
},
},
}
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
c.Assert(m, qt.DeepEquals, Params{
"a": Params{
"b": Params{
"c": "existingValue",
},
},
})
}
+222
View File
@@ -0,0 +1,222 @@
// 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 hreflect
import (
"fmt"
"math"
"reflect"
)
var (
typeInt64 = reflect.TypeFor[int64]()
typeFloat64 = reflect.TypeFor[float64]()
typeString = reflect.TypeFor[string]()
)
// ToInt64 converts v to int64 if possible, returning an error if not.
func ToInt64E(v reflect.Value) (int64, error) {
if v, ok := ConvertIfPossible(v, typeInt64); ok {
return v.Int(), nil
}
return 0, errConvert(v, "int64")
}
// ToInt64 converts v to int64 if possible. It panics if the conversion is not possible.
func ToInt64(v reflect.Value) int64 {
vv, err := ToInt64E(v)
if err != nil {
panic(err)
}
return vv
}
// ToFloat64E converts v to float64 if possible, returning an error if not.
func ToFloat64E(v reflect.Value) (float64, error) {
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
return v.Float(), nil
}
return 0, errConvert(v, "float64")
}
// ToFloat64 converts v to float64 if possible, panicking if not.
func ToFloat64(v reflect.Value) float64 {
vv, err := ToFloat64E(v)
if err != nil {
panic(err)
}
return vv
}
// ToStringE converts v to string if possible, returning an error if not.
func ToStringE(v reflect.Value) (string, error) {
vv, err := ToStringValueE(v)
if err != nil {
return "", err
}
return vv.String(), nil
}
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
if v, ok := ConvertIfPossible(v, typeString); ok {
return v, nil
}
return reflect.Value{}, errConvert(v, "string")
}
// ToString converts v to string if possible, panicking if not.
func ToString(v reflect.Value) string {
vv, err := ToStringE(v)
if err != nil {
panic(err)
}
return vv
}
func errConvert(v reflect.Value, s string) error {
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
}
// ConvertIfPossible tries to convert val to typ if possible.
// This is currently only implemented for int kinds,
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
// We can expand on this later if needed.
// This conversion is lossless.
// See Issue 14079.
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
switch val.Kind() {
case reflect.Ptr, reflect.Interface:
if val.IsNil() {
// Return typ's zero value.
return reflect.Zero(typ), true
}
val = val.Elem()
}
if val.Type().AssignableTo(typ) {
// No conversion needed.
return val, true
}
if IsInt(typ.Kind()) {
return convertToIntIfPossible(val, typ)
}
if IsFloat(typ.Kind()) {
return convertToFloatIfPossible(val, typ)
}
if IsUint(typ.Kind()) {
return convertToUintIfPossible(val, typ)
}
if IsString(typ.Kind()) && IsString(val.Kind()) {
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
if i < 0 {
return reflect.Value{}, false
}
u := uint64(i)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
if IsUint(val.Kind()) {
if typ.OverflowUint(val.Uint()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < 0 || f > float64(math.MaxUint64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
u := uint64(f)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
return reflect.Value{}, false
}
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
f := float64(i)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsUint(val.Kind()) {
u := val.Uint()
f := float64(u)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsFloat(val.Kind()) {
if typ.OverflowFloat(val.Float()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
if typ.OverflowInt(val.Int()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsUint(val.Kind()) {
if val.Uint() > uint64(math.MaxInt64) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(val.Uint())) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(f)) {
return reflect.Value{}, false
}
return reflect.ValueOf(int64(f)).Convert(typ), true
}
return reflect.Value{}, false
}
+284
View File
@@ -0,0 +1,284 @@
// 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 hreflect
import (
"math"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
func TestToFuncs(t *testing.T) {
c := qt.New(t)
c.Assert(ToInt64(reflect.ValueOf(int(42))), qt.Equals, int64(42))
c.Assert(ToFloat64(reflect.ValueOf(float32(3.14))), hqt.IsSameFloat64, float64(3.14))
c.Assert(ToString(reflect.ValueOf("hello")), qt.Equals, "hello")
}
func TestConvertIfPossible(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {
name string
value any
typ any
expected any
ok bool
}{
// From uint to int.
{
name: "uint64(math.MaxUint64) to int16",
value: uint64(math.MaxUint64),
typ: int16(0),
ok: false, // overflow
},
{
name: "uint64(math.MaxUint64) to int64",
value: uint64(math.MaxUint64),
typ: int64(0),
ok: false, // overflow
},
{
name: "uint64(math.MaxInt16) to int16",
value: uint64(math.MaxInt16),
typ: int64(0),
ok: true,
expected: int64(math.MaxInt16),
},
// From int to int.
{
name: "int64(math.MaxInt64) to int16",
value: int64(math.MaxInt64),
typ: int16(0),
ok: false, // overflow
},
{
name: "int64(math.MaxInt16) to int",
value: int64(math.MaxInt16),
typ: int(0),
ok: true,
expected: int(math.MaxInt16),
},
{
name: "int64(math.MaxInt16) to int",
value: int64(math.MaxInt16),
typ: int(0),
ok: true,
expected: int(math.MaxInt16),
},
// From float64 to int.
{
name: "float64(1.5) to int",
value: float64(1.5),
typ: int(0),
ok: false, // loss of precision
},
{
name: "float64(1.0) to int",
value: float64(1.0),
typ: int(0),
ok: true,
expected: int(1),
},
{
name: "float64(math.MaxInt16+1) to int16",
value: float64(math.MaxInt16 + 1),
typ: int16(0),
ok: false, // overflow
},
{
name: "float64(math.MaxFloat64) to int64",
value: float64(math.MaxFloat64),
typ: int64(0),
ok: false, // overflow
},
{
name: "float64(32767) to int16",
value: float64(32767),
typ: int16(0),
ok: true,
expected: int16(32767),
},
// From float32 to int.
{
name: "float32(1.5) to int",
value: float32(1.5),
typ: int(0),
ok: false, // loss of precision
},
{
name: "float32(1.0) to int",
value: float32(1.0),
typ: int(0),
ok: true,
expected: int(1),
},
{
name: "float32(math.MaxFloat32) to int16",
value: float32(math.MaxFloat32),
typ: int16(0),
ok: false, // overflow
},
{
name: "float32(math.MaxFloat32) to int64",
value: float32(math.MaxFloat32),
typ: int64(0),
ok: false, // overflow
},
{
name: "float32(math.MaxInt16) to int16",
value: float32(math.MaxInt16),
typ: int16(0),
ok: true,
expected: int16(32767),
},
{
name: "float32(math.MaxInt16+1) to int16",
value: float32(math.MaxInt16 + 1),
typ: int16(0),
ok: false, // overflow
},
// Int to float.
{
name: "int16(32767) to float32",
value: int16(32767),
typ: float32(0),
ok: true,
expected: float32(32767),
},
{
name: "int64(32767) to float32",
value: int64(32767),
typ: float32(0),
ok: true,
expected: float32(32767),
},
{
name: "int64(math.MaxInt64) to float32",
value: int64(math.MaxInt64),
typ: float32(0),
ok: true,
expected: float32(math.MaxInt64),
},
{
name: "int64(math.MaxInt64) to float64",
value: int64(math.MaxInt64),
typ: float64(0),
ok: true,
expected: float64(math.MaxInt64),
},
// Int to uint.
{
name: "int16(32767) to uint16",
value: int16(32767),
typ: uint16(0),
ok: true,
expected: uint16(32767),
},
{
name: "int16(32767) to uint8",
value: int16(32767),
typ: uint8(0),
ok: false,
},
{
name: "float64(3.14) to uint64",
value: float64(3.14),
typ: uint64(0),
ok: false,
},
{
name: "float64(3.0) to uint64",
value: float64(3.0),
typ: uint64(0),
ok: true,
expected: uint64(3),
},
// From uint to float.
{
name: "uint64(math.MaxInt16) to float64",
value: uint64(math.MaxInt16),
typ: float64(0),
ok: true,
expected: float64(math.MaxInt16),
},
// Float to float.
{
name: "float64(3.14) to float32",
value: float64(3.14),
typ: float32(0),
ok: true,
expected: float32(3.14),
},
{
name: "float32(3.14) to float64",
value: float32(3.14),
typ: float64(0),
ok: true,
expected: float64(3.14),
},
{
name: "float64(3.14) to float64",
value: float64(3.14),
typ: float64(0),
ok: true,
expected: float64(3.14),
},
} {
v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ))
c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name))
if test.ok {
c.Assert(v.Interface(), hqt.IsSameNumber, test.expected, qt.Commentf("test case: %s", test.name))
}
}
}
func TestConvertIfPossibleMisc(t *testing.T) {
c := qt.New(t)
type s string
var (
i = int32(42)
i64 = int64(i)
iv any = i
ip = &i
inil any = (*int32)(nil)
shello = s("hello")
)
convertOK := func(v any, typ any) any {
rv, ok := ConvertIfPossible(reflect.ValueOf(v), reflect.TypeOf(typ))
c.Assert(ok, qt.IsTrue)
return rv.Interface()
}
c.Assert(convertOK(shello, ""), qt.Equals, "hello")
c.Assert(convertOK(ip, int64(0)), qt.Equals, i64)
c.Assert(convertOK(iv, int64(0)), qt.Equals, i64)
c.Assert(convertOK(inil, int64(0)), qt.Equals, int64(0))
}
func BenchmarkToInt64(b *testing.B) {
v := reflect.ValueOf(int(42))
for b.Loop() {
ToInt64(v)
}
}
+102 -20
View File
@@ -1,6 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
// 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.
@@ -22,11 +20,16 @@ import (
"sync"
"time"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
)
// IsInterfaceOrPointer returns whether the given kind is an interface or a pointer.
func IsInterfaceOrPointer(kind reflect.Kind) bool {
return kind == reflect.Interface || kind == reflect.Ptr
}
// TODO(bep) replace the private versions in /tpl with these.
// IsNumber returns whether the given kind is a number.
func IsNumber(kind reflect.Kind) bool {
@@ -63,6 +66,11 @@ func IsFloat(kind reflect.Kind) bool {
}
}
// IsString returns whether the given kind is a string.
func IsString(kind reflect.Kind) bool {
return kind == reflect.String
}
// IsTruthful returns whether in represents a truthful value.
// See IsTruthfulValue
func IsTruthful(in any) bool {
@@ -86,6 +94,18 @@ func IsSlice(v any) bool {
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
var isZeroCache sync.Map
func implementsIsZero(tp reflect.Type) bool {
v, ok := isZeroCache.Load(tp)
if ok {
return v.(bool)
}
implements := tp.Implements(zeroType)
isZeroCache.Store(tp, implements)
return implements
}
// IsTruthfulValue returns whether the given value has a meaningful truth value.
// This is based on template.IsTrue in Go's stdlib, but also considers
// IsZero and any interface value will be unwrapped before it's considered
@@ -94,14 +114,18 @@ var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
// Based on:
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
func IsTruthfulValue(val reflect.Value) (truth bool) {
val = indirectInterface(val)
val, isNil := Indirect(val)
if !val.IsValid() {
// Something like var x interface{}, never set. It's a form of nil.
// Something like: var x any, never set. It's a form of nil.
return
}
if val.Type().Implements(zeroType) {
if val.Kind() == reflect.Pointer && isNil {
return
}
if implementsIsZero(val.Type()) {
return !val.Interface().(types.Zeroer).IsZero()
}
@@ -134,10 +158,31 @@ type methodKey struct {
name string
}
var methodCache sync.Map
var (
methodIndexCache sync.Map
methodCache sync.Map
)
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup.
// GetMethodByNameForType returns the method with the given name for the given type,
// or a zero Method if no such method exists.
// It panics if tp is an interface type.
// It caches the lookup.
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
if tp.Kind() == reflect.Interface {
// Func field is nil for interface types.
panic("not supported for interface types")
}
k := methodKey{tp, name}
v, found := methodCache.Load(k)
if found {
return v.(reflect.Method)
}
m, _ := tp.MethodByName(name)
methodCache.Store(k, m)
return m
}
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
func GetMethodByName(v reflect.Value, name string) reflect.Value {
index := GetMethodIndexByName(v.Type(), name)
@@ -152,7 +197,7 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
// -1 if no such method exists.
func GetMethodIndexByName(tp reflect.Type, name string) int {
k := methodKey{tp, name}
v, found := methodCache.Load(k)
v, found := methodIndexCache.Load(k)
if found {
return v.(int)
}
@@ -161,7 +206,7 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
if !ok {
index = -1
}
methodCache.Store(k, index)
methodIndexCache.Store(k, index)
if !ok {
return -1
@@ -243,6 +288,7 @@ func ToSliceAny(v any) ([]any, bool) {
return nil, false
}
// CallMethodByName calls the method with the given name on v.
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
fn := v.MethodByName(name)
var args []reflect.Value
@@ -260,20 +306,56 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
return fn.Call(args)
}
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
func indirectInterface(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Interface {
return v
// Indirect unwraps interfaces and pointers until it finds a non-interface/pointer value.
// If a nil is encountered, the second return value is true.
// If a pointer to a struct is encountered, it is not unwrapped.
func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
for ; IsInterfaceOrPointer(v.Kind()); v = v.Elem() {
if IsNil(v) {
return v, true
}
if v.Kind() != reflect.Interface {
// A pointer.
if v.NumMethod() > 0 {
break
}
if v.Elem().Kind() == reflect.Struct {
// Avoid unwrapping pointers to structs.
break
}
}
}
if v.IsNil() {
return reflect.Value{}
return v, false
}
// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
vv, isNil = Indirect(v)
if isNil {
return vv, isNil
}
return v.Elem()
if vv.Kind() == reflect.Pointer {
vv = vv.Elem()
}
return vv, isNil
}
// IsNil reports whether v is nil.
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return v.IsNil()
}
return false
}
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
var isContextCache = maps.NewCache[reflect.Type, bool]()
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
type k string
+126 -16
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// 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.
@@ -20,15 +20,32 @@ import (
"time"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
type zeroStruct struct {
zero bool
}
func (z zeroStruct) IsZero() bool {
return z.zero
}
func TestIsTruthful(t *testing.T) {
c := qt.New(t)
var nilpointerZero *zeroStruct
c.Assert(IsTruthful(true), qt.Equals, true)
c.Assert(IsTruthful(false), qt.Equals, false)
c.Assert(IsTruthful(time.Now()), qt.Equals, true)
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
c.Assert(IsTruthful(&zeroStruct{zero: false}), qt.Equals, true)
c.Assert(IsTruthful(&zeroStruct{zero: true}), qt.Equals, false)
c.Assert(IsTruthful(zeroStruct{zero: false}), qt.Equals, true)
c.Assert(IsTruthful(zeroStruct{zero: true}), qt.Equals, false)
c.Assert(IsTruthful(nil), qt.Equals, false)
c.Assert(IsTruthful(nilpointerZero), qt.Equals, false)
}
func TestGetMethodByName(t *testing.T) {
@@ -63,18 +80,90 @@ func TestToSliceAny(t *testing.T) {
checkOK([]int{1, 2, 3}, []any{1, 2, 3})
}
type testIndirectStruct struct {
S string
}
func (t *testIndirectStruct) GetS() string {
return t.S
}
func (t testIndirectStruct) Foo() string {
return "bar"
}
type testIndirectStructNoMethods struct {
S string
}
func TestIsNil(t *testing.T) {
c := qt.New(t)
var (
nilPtr *testIndirectStruct
nilIface any = nil
nonNilIface any = &testIndirectStruct{S: "hello"}
)
c.Assert(IsNil(reflect.ValueOf(nilPtr)), qt.Equals, true)
c.Assert(IsNil(reflect.ValueOf(nilIface)), qt.Equals, true)
c.Assert(IsNil(reflect.ValueOf(nonNilIface)), qt.Equals, false)
}
func TestIndirectInterface(t *testing.T) {
c := qt.New(t)
var (
structWithMethods = testIndirectStruct{S: "hello"}
structWithMethodsPointer = &testIndirectStruct{S: "hello"}
structWithMethodsPointerAny any = structWithMethodsPointer
structPointerToPointer = &structWithMethodsPointer
structNoMethodsPtr = &testIndirectStructNoMethods{S: "no methods"}
structNoMethods = testIndirectStructNoMethods{S: "no methods"}
intValue = 32
intPtr = &intValue
nilPtr *testIndirectStruct
nilIface any = nil
)
ind := func(v any) any {
c.Helper()
vv, isNil := Indirect(reflect.ValueOf(v))
c.Assert(isNil, qt.IsFalse)
return vv.Interface()
}
c.Assert(ind(intValue), hqt.IsSameType, 32)
c.Assert(ind(intPtr), hqt.IsSameType, 32)
c.Assert(ind(structNoMethodsPtr), hqt.IsSameType, structNoMethodsPtr)
c.Assert(ind(structWithMethods), hqt.IsSameType, structWithMethods)
c.Assert(ind(structNoMethods), hqt.IsSameType, structNoMethods)
c.Assert(ind(structPointerToPointer), hqt.IsSameType, &testIndirectStruct{})
c.Assert(ind(structWithMethodsPointer), hqt.IsSameType, &testIndirectStruct{})
c.Assert(ind(structWithMethodsPointerAny), hqt.IsSameType, structWithMethodsPointer)
vv, isNil := Indirect(reflect.ValueOf(nilPtr))
c.Assert(isNil, qt.IsTrue)
c.Assert(vv, qt.Equals, reflect.ValueOf(nilPtr))
vv, isNil = Indirect(reflect.ValueOf(nilIface))
c.Assert(isNil, qt.IsFalse)
c.Assert(vv, qt.Equals, reflect.ValueOf(nilIface))
}
func BenchmarkIsContextType(b *testing.B) {
const size = 1000
type k string
b.Run("value", func(b *testing.B) {
ctx := context.Background()
ctxs := make([]reflect.Type, b.N)
for i := 0; i < b.N; i++ {
ctxs := make([]reflect.Type, size)
for i := range size {
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !IsContextType(ctxs[i]) {
for i := 0; b.Loop(); i++ {
idx := i % size
if !IsContextType(ctxs[idx]) {
b.Fatal("not context")
}
}
@@ -82,7 +171,7 @@ func BenchmarkIsContextType(b *testing.B) {
b.Run("background", func(b *testing.B) {
var ctxt reflect.Type = reflect.TypeOf(context.Background())
for i := 0; i < b.N; i++ {
for b.Loop() {
if !IsContextType(ctxt) {
b.Fatal("not context")
}
@@ -90,14 +179,25 @@ func BenchmarkIsContextType(b *testing.B) {
})
}
func BenchmarkIsTruthFul(b *testing.B) {
v := reflect.ValueOf("Hugo")
func BenchmarkIsTruthFulValue(b *testing.B) {
var (
stringHugo = reflect.ValueOf("Hugo")
stringEmpty = reflect.ValueOf("")
zero = reflect.ValueOf(time.Time{})
timeNow = reflect.ValueOf(time.Now())
boolTrue = reflect.ValueOf(true)
boolFalse = reflect.ValueOf(false)
nilPointer = reflect.ValueOf((*zeroStruct)(nil))
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !IsTruthfulValue(v) {
b.Fatal("not truthful")
}
for b.Loop() {
IsTruthfulValue(stringHugo)
IsTruthfulValue(stringEmpty)
IsTruthfulValue(zero)
IsTruthfulValue(timeNow)
IsTruthfulValue(boolTrue)
IsTruthfulValue(boolFalse)
IsTruthfulValue(nilPointer)
}
}
@@ -123,12 +223,22 @@ func (t *testStruct) Method5() string {
return "Hugo"
}
func BenchmarkGetMethodByNameForType(b *testing.B) {
tp := reflect.TypeFor[*testStruct]()
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
for b.Loop() {
for _, method := range methods {
_ = GetMethodByNameForType(tp, method)
}
}
}
func BenchmarkGetMethodByName(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
for _, method := range methods {
_ = GetMethodByName(v, method)
}
@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hstore
import (
"reflect"
@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
package hstore
import (
"reflect"
@@ -214,8 +214,8 @@ func TestScratchGetSortedMapValues(t *testing.T) {
func BenchmarkScratchGet(b *testing.B) {
scratch := NewScratch()
scratch.Add("A", 1)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
scratch.Get("A")
}
}
+69 -4
View File
@@ -17,6 +17,7 @@ import (
"fmt"
"regexp"
"slices"
"sort"
"strings"
"sync"
@@ -96,6 +97,16 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
return reCache.getOrCompileRegexp(pattern)
}
// HasAnyPrefix checks if the string s has any of the prefixes given.
func HasAnyPrefix(s string, prefixes ...string) bool {
for _, p := range prefixes {
if strings.HasPrefix(s, p) {
return true
}
}
return false
}
// InSlice checks if a string is an element of a slice of strings
// and returns a boolean value.
func InSlice(arr []string, el string) bool {
@@ -128,7 +139,61 @@ func ToString(v any) (string, bool) {
return "", false
}
type (
Strings2 [2]string
Strings3 [3]string
)
// UniqueStrings returns a new slice with any duplicates removed.
func UniqueStrings(s []string) []string {
unique := make([]string, 0, len(s))
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
unique = append(unique, val)
}
}
return unique
}
// UniqueStringsReuse returns a slice with any duplicates removed.
// It will modify the input slice.
func UniqueStringsReuse(s []string) []string {
result := s[:0]
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
result = append(result, val)
}
}
return result
}
// UniqueStringsSorted returns a sorted slice with any duplicates removed.
// It will modify the input slice.
func UniqueStringsSorted(s []string) []string {
if len(s) == 0 {
return nil
}
ss := sort.StringSlice(s)
ss.Sort()
i := 0
for j := 1; j < len(s); j++ {
if !ss.Less(i, j) {
continue
}
i++
s[i] = s[j]
}
return s[:i+1]
}
+81 -3
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// 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.
@@ -14,6 +14,7 @@
package hstrings
import (
"reflect"
"regexp"
"testing"
@@ -43,14 +44,91 @@ func TestGetOrCompileRegexp(t *testing.T) {
c.Assert(re.MatchString("123"), qt.Equals, true)
}
func TestUniqueStrings(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStrings(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
}
}
func TestUniqueStringsReuse(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStringsReuse(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
}
}
func TestUniqueStringsSorted(t *testing.T) {
c := qt.New(t)
in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
output := UniqueStringsSorted(in)
expected := []string{"", "a", "b", "c", "d"}
c.Assert(output, qt.DeepEquals, expected)
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
}
// Note that these cannot use b.Loop() because of golang/go#27217.
func BenchmarkUniqueStrings(b *testing.B) {
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
b.Run("Safe", func(b *testing.B) {
for b.Loop() {
result := UniqueStrings(input)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
b.Run("Reuse slice", func(b *testing.B) {
inputs := make([][]string, b.N)
for i := 0; i < b.N; i++ {
inputc := make([]string, len(input))
copy(inputc, input)
inputs[i] = inputc
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsReuse(inputc)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
b.Run("Reuse slice sorted", func(b *testing.B) {
inputs := make([][]string, b.N)
for i := 0; i < b.N; i++ {
inputc := make([]string, len(input))
copy(inputc, input)
inputs[i] = inputc
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsSorted(inputc)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
}
func BenchmarkGetOrCompileRegexp(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
GetOrCompileRegexp(`\d+`)
}
}
func BenchmarkCompileRegexp(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
regexp.MustCompile(`\d+`)
}
}
+162
View File
@@ -0,0 +1,162 @@
// 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 hsync
import (
"context"
"sync"
"sync/atomic"
)
// OnceMore is similar to sync.Once.
//
// Additional features are:
// * it can be reset, so the action can be repeated if needed
// * it has methods to check if it's done or in progress
type OnceMore struct {
_ doNotCopy
done atomic.Bool
mu sync.Mutex
}
func (t *OnceMore) Do(f func()) {
if t.Done() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
// Double check
if t.Done() {
return
}
defer t.done.Store(true)
f()
}
func (t *OnceMore) Done() bool {
return t.done.Load()
}
func (t *OnceMore) Reset() {
t.mu.Lock()
t.done.Store(false)
t.mu.Unlock()
}
type ValueResetter[T any] struct {
reset func()
f func(context.Context) T
}
func (v *ValueResetter[T]) Value(ctx context.Context) T {
return v.f(ctx)
}
func (v *ValueResetter[T]) Reset() {
v.reset()
}
// OnceMoreValue returns a function that invokes f only once and returns the value
// returned by f. The returned function may be called concurrently.
//
// If f panics, the returned function will panic with the same value on every call.
func OnceMoreValue[T any](f func(context.Context) T) ValueResetter[T] {
v := struct {
f func(context.Context) T
once OnceMore
ok bool
p any
result T
}{
f: f,
}
ff := func(ctx context.Context) T {
v.once.Do(func() {
v.ok = false
defer func() {
v.p = recover()
if !v.ok {
panic(v.p)
}
}()
v.result = v.f(ctx)
v.ok = true
})
if !v.ok {
panic(v.p)
}
return v.result
}
return ValueResetter[T]{
reset: v.once.Reset,
f: ff,
}
}
type FuncResetter struct {
f func(context.Context) error
reset func()
}
func (v *FuncResetter) Do(ctx context.Context) error {
return v.f(ctx)
}
func (v *FuncResetter) Reset() {
v.reset()
}
func OnceMoreFunc(f func(context.Context) error) FuncResetter {
v := struct {
f func(context.Context) error
once OnceMore
ok bool
err error
p any
}{
f: f,
}
ff := func(ctx context.Context) error {
v.once.Do(func() {
v.ok = false
defer func() {
v.p = recover()
if !v.ok {
panic(v.p)
}
}()
v.err = v.f(ctx)
v.ok = true
})
if !v.ok {
panic(v.p)
}
return v.err
}
return FuncResetter{
f: ff,
reset: v.once.Reset,
}
}
type doNotCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*doNotCopy) Lock() {}
func (*doNotCopy) Unlock() {}
+80
View File
@@ -0,0 +1,80 @@
// 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 hsync
import (
"context"
"testing"
qt "github.com/frankban/quicktest"
)
func TestOnceMoreValue(t *testing.T) {
c := qt.New(t)
var counter int
f := func(context.Context) int {
counter++
return counter
}
omf := OnceMoreValue(f)
for range 10 {
c.Assert(omf.Value(context.Background()), qt.Equals, 1)
}
omf.Reset()
for range 10 {
c.Assert(omf.Value(context.Background()), qt.Equals, 2)
}
}
func TestOnceMoreFunc(t *testing.T) {
c := qt.New(t)
var counter int
f := func(context.Context) error {
counter++
return nil
}
omf := OnceMoreFunc(f)
for range 10 {
c.Assert(omf.Do(context.Background()), qt.IsNil)
c.Assert(counter, qt.Equals, 1)
}
omf.Reset()
for range 10 {
c.Assert(omf.Do(context.Background()), qt.IsNil)
c.Assert(counter, qt.Equals, 2)
}
}
func BenchmarkOnceMoreValue(b *testing.B) {
var counter int
f := func(context.Context) int {
counter++
return counter
}
for b.Loop() {
omf := OnceMoreValue(f)
for range 10 {
omf.Value(context.Background())
}
omf.Reset()
for range 10 {
omf.Value(context.Background())
}
}
}
+2 -2
View File
@@ -24,9 +24,9 @@ func TestApplyWithContext(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-- hugo.toml --
defaultContentLanguage = 'it'
-- layouts/index.html --
-- layouts/home.html --
{{ $dates := slice
"2022-01-03"
"2022-02-01"
+3 -3
View File
@@ -112,7 +112,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
b.Run("Native", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := june06.Format("Monday Jan 2 2006")
if got != "Wednesday Jun 6 2018" {
b.Fatalf("invalid format, got %q", got)
@@ -123,7 +123,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
b.Run("Localized", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
got := f.Format(june06, "Monday Jan 2 2006")
if got != "onsdag juni 6 2018" {
b.Fatalf("invalid format, got %q", got)
@@ -134,7 +134,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
b.Run("Localized Custom", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
got := f.Format(june06, ":date_medium")
if got != "6. juni 2018" {
b.Fatalf("invalid format, got %q", got)
+31
View File
@@ -32,6 +32,37 @@ type ReadSeekCloser interface {
io.Closer
}
// Sizer provides the size of, typically, a io.Reader.
// As implemented by e.g. os.File and io.SectionReader.
type Sizer interface {
Size() int64
}
type SizeReader interface {
io.Reader
Sizer
}
// ToSizeReader converts the given io.Reader to a SizeReader.
// Note that if r is not a SizeReader, the entire content will be read into memory
func ToSizeReader(r io.Reader) (SizeReader, error) {
if sr, ok := r.(SizeReader); ok {
return sr, nil
}
b, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
type CloserFunc func() error
func (f CloserFunc) Close() error {
return f()
}
// ReadSeekCloserProvider provides a ReadSeekCloser.
type ReadSeekCloserProvider interface {
ReadSeekCloser() (ReadSeekCloser, error)
+55 -11
View File
@@ -28,12 +28,14 @@ import (
"github.com/bep/logg"
"github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hstore"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/version"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/bep/helpers/contexthelpers"
"github.com/spf13/afero"
iofs "io/fs"
@@ -54,7 +56,7 @@ var (
vendorInfo string
)
var _ maps.StoreProvider = (*HugoInfo)(nil)
var _ hstore.StoreProvider = (*HugoInfo)(nil)
// HugoInfo contains information about the current Hugo environment
type HugoInfo struct {
@@ -73,14 +75,14 @@ type HugoInfo struct {
conf ConfigProvider
deps []*Dependency
store *maps.Scratch
store *hstore.Scratch
// Context gives access to some of the context scoped variables.
Context Context
}
// Version returns the current version as a comparable version string.
func (i HugoInfo) Version() VersionString {
func (i HugoInfo) Version() version.VersionString {
return CurrentVersion.Version()
}
@@ -119,7 +121,7 @@ func (i HugoInfo) Deps() []*Dependency {
return i.deps
}
func (i HugoInfo) Store() *maps.Scratch {
func (i HugoInfo) Store() *hstore.Scratch {
return i.store
}
@@ -145,7 +147,7 @@ const (
contextKeyMarkupScope contextKey = iota
)
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
type Context struct{}
@@ -196,7 +198,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
Environment: conf.Environment(),
conf: conf,
deps: deps,
store: maps.NewScratch(),
store: hstore.NewScratch(),
GoVersion: goVersion,
}
}
@@ -308,13 +310,12 @@ func GetDependencyList() []string {
// GetDependencyListNonGo returns a list of non-Go dependencies.
func GetDependencyListNonGo() []string {
var deps []string
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
if IsExtended {
deps = append(
deps,
formatDep("github.com/sass/libsass", "3.6.6"),
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
)
}
@@ -451,7 +452,7 @@ func deprecateLevelWithLogger(item, alternative, version string, level logg.Leve
// We want people to run at least the current and previous version without any warnings.
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
func deprecationLogLevelFromVersion(ver string) logg.Level {
from := MustParseVersion(ver)
from := version.MustParseVersion(ver)
to := CurrentVersion
minorDiff := to.Minor - from.Minor
switch {
@@ -465,3 +466,46 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
return logg.LevelInfo
}
}
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
+1 -1
View File
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
---
title: home
---
-- layouts/index.html --
-- layouts/home.html --
multilingual={{ hugo.IsMultilingual }}
multihost={{ hugo.IsMultihost }}
`
+2 -1
View File
@@ -20,6 +20,7 @@ import (
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/version"
)
func TestHugoInfo(t *testing.T) {
@@ -29,7 +30,7 @@ func TestHugoInfo(t *testing.T) {
hugoInfo := NewInfo(conf, nil)
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
bi := getBuildInfo()
+7 -5
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// 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.
@@ -13,11 +13,13 @@
package hugo
import "github.com/gohugoio/hugo/common/version"
// CurrentVersion represents the current build version.
// This should be the only one.
var CurrentVersion = Version{
var CurrentVersion = version.Version{
Major: 0,
Minor: 149,
PatchLevel: 0,
Suffix: "-DEV",
Minor: 155,
PatchLevel: 2,
Suffix: "",
}
+5
View File
@@ -173,6 +173,7 @@ func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
type Logger interface {
Debug() logg.LevelLogger
DebugCommand(command string) logg.LevelLogger
Debugf(format string, v ...any)
Debugln(v ...any)
Error() logg.LevelLogger
@@ -221,6 +222,10 @@ func (l *logAdapter) Debug() logg.LevelLogger {
return l.debugl
}
func (l *logAdapter) DebugCommand(command string) logg.LevelLogger {
return l.debugl.WithField(FieldNameCmd, command)
}
func (l *logAdapter) Debugf(format string, v ...any) {
l.debugl.Logf(format, v...)
}
-1
View File
@@ -52,7 +52,6 @@ func TestPara(t *testing.T) {
var result []int
var mu sync.Mutex
for i := range n {
i := i
r.Run(func() error {
mu.Lock()
defer mu.Unlock()
+2 -2
View File
@@ -115,8 +115,8 @@ func ReplaceExtension(path string, newExt string) string {
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories {
if strings.HasPrefix(inPath, currentPath) {
return strings.TrimPrefix(inPath, currentPath), nil
if after, ok := strings.CutPrefix(inPath, currentPath); ok {
return after, nil
}
}
return inPath, errors.New("can't extract relative path, unknown prefix")
+60 -2
View File
@@ -225,7 +225,7 @@ func BenchmarkSanitize(b *testing.B) {
// This should not allocate any memory.
b.Run("All allowed", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := Sanitize(allAlowedPath)
if got != allAlowedPath {
b.Fatal(got)
@@ -235,7 +235,7 @@ func BenchmarkSanitize(b *testing.B) {
// This will allocate some memory.
b.Run("Spaces", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
got := Sanitize(spacePath)
if got != "foo-bar" {
b.Fatal(got)
@@ -311,3 +311,61 @@ func TestIsSameFilePath(t *testing.T) {
c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}
func BenchmarkAddLeadingSlash(b *testing.B) {
const (
noLeadingSlash = "a/b/c"
withLeadingSlash = "/a/b/c"
)
// This should not allocate any memory.
b.Run("With leading slash", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash(withLeadingSlash)
if got != withLeadingSlash {
b.Fatal(got)
}
}
})
// This will allocate some memory.
b.Run("Without leading slash", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash(noLeadingSlash)
if got != "/a/b/c" {
b.Fatal(got)
}
}
})
b.Run("Blank string", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash("")
if got != "/" {
b.Fatal(got)
}
}
})
}
func TestPathEscape(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
input string
expected string
}{
{"/tags/欢迎", "/tags/%E6%AC%A2%E8%BF%8E"},
{"/path with spaces", "/path%20with%20spaces"},
{"/simple-path", "/simple-path"},
{"/path/with/slash", "/path/with/slash"},
{"/path/with special&chars", "/path/with%20special&chars"},
} {
in := this.input
for range 2 {
result := PathEscape(in)
c.Assert(result, qt.Equals, this.expected, qt.Commentf("input: %q", this.input))
in = result // test idempotency
}
}
}
+86 -7
View File
@@ -14,23 +14,27 @@
package paths
import (
"fmt"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
identifierBaseof = "baseof"
identifierCurstomWrapper = "_"
)
// PathParser parses a path into a Path.
// PathParser parses and manages paths.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
LanguageIndex map[string]int
@@ -44,6 +48,19 @@ type PathParser struct {
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
// The configured sites matrix.
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
pp.initOnce.Do(func() {
pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]()
})
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -57,6 +74,32 @@ func NormalizePathStringBasic(s string) string {
return s
}
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
pp.init()
lang := p.Lang()
v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
if lang != "" {
if idx, ok := pp.LanguageIndex[lang]; ok {
builder.WithLanguageIndices(idx)
}
}
switch p.Component() {
case files.ComponentFolderContent:
builder.WithDefaultsIfNotSet()
case files.ComponentFolderLayouts:
builder.WithAllIfNotSet()
case files.ComponentFolderStatic:
builder.WithDefaultsAndAllLanguagesIfNotSet()
}
return builder.Build(), nil
})
return v
}
// ParseIdentity parses component c with path s into a StringIdentity.
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
p := pp.parsePooled(c, s)
@@ -145,6 +188,12 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierCustom = len(p.identifiersKnown) - 1
found = true
}
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
@@ -155,12 +204,12 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
p.posIdentifierOutputFormat = 0
}
} else {
var langFound bool
if mayHaveLang {
var disabled bool
_, langFound = pp.LanguageIndex[sid]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
if disabled {
@@ -173,6 +222,7 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
@@ -202,8 +252,14 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
}
if !found && mayHaveLayout {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
if p.posIdentifierLayout != -1 {
// Move it to identifiersUnknown.
p.identifiersUnknown = append(p.identifiersUnknown, p.identifiersKnown[p.posIdentifierLayout])
p.identifiersKnown[p.posIdentifierLayout] = id
} else {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
}
found = true
}
@@ -333,7 +389,7 @@ type Type int
const (
// A generic resource, e.g. a JSON file.
// A generic file, e.g. a JSON file.
TypeFile Type = iota
// All below are content files.
@@ -380,6 +436,7 @@ type Path struct {
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
posIdentifierCustom int
disabled bool
trimLeadingSlash bool
@@ -417,6 +474,7 @@ func (p *Path) reset() {
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.posIdentifierCustom = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@@ -580,6 +638,9 @@ func (p *Path) Unnormalized() *Path {
// PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string {
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
return p.Path()
}
return p.base(true, false)
}
@@ -633,7 +694,12 @@ func (p *Path) BaseRel(owner *Path) string {
//
// For other files (Resources), any extension is kept.
func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle())
s := p.base(!p.isContentPage(), p.IsBundle())
if s == "/" && p.isContentPage() {
// The content home page is represented as "".
s = ""
}
return s
}
// Used in template lookups.
@@ -708,6 +774,10 @@ func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
}
func (p *Path) Identifier(i int) string {
return p.identifierAsString(i)
}
@@ -786,3 +856,12 @@ func HasExt(p string) bool {
}
return false
}
// ValidateIdentifier returns true if the given string is a valid identifier according
// to Hugo's basic path normalization rules.
func ValidateIdentifier(s string) error {
if s == NormalizePathStringBasic(s) {
return nil
}
return fmt.Errorf("must be all lower case and no spaces")
}
+81 -30
View File
@@ -18,27 +18,33 @@ import (
"testing"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
var testParser = &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
"fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
func newTestParser() *PathParser {
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
return &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
"fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
ConfiguredDimensions: dims,
}
}
func TestParse(t *testing.T) {
@@ -190,7 +196,7 @@ func TestParse(t *testing.T) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "")
@@ -266,7 +272,7 @@ func TestParse(t *testing.T) {
"Index root no slash",
"_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md")
},
@@ -275,7 +281,7 @@ func TestParse(t *testing.T) {
"Index root",
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md")
},
@@ -373,13 +379,24 @@ func TestParse(t *testing.T) {
c.Assert(p.IsContentData(), qt.IsFalse)
},
},
{
"Custom identifier",
"/a/b/p1._myid_.no.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Custom(), qt.Equals, "myid")
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Home branch cundle" {
// return
if test.name != "Caret up identifier" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
test.assert(c, parser.Parse(files.ComponentFolderContent, test.path))
})
}
}
@@ -437,8 +454,8 @@ func TestParseLayouts(t *testing.T) {
"/mylayout.list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "mylayout")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
c.Assert(p.Base(), qt.Equals, "/mylayout.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
@@ -461,7 +478,8 @@ func TestParseLayouts(t *testing.T) {
"Lang and output format",
"/list.no.amp.not.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list", "amp", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"not"})
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Lang(), qt.Equals, "no")
@@ -583,14 +601,27 @@ func TestParseLayouts(t *testing.T) {
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
},
},
{
"Not lang",
"/foo/index.xy.html",
func(c *qt.C, p *Path) {
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Layout(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.xy.html")
c.Assert(p.PathNoLang(), qt.Equals, "/foo/index.xy.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "index"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Shortcode lang layout" {
// return
if test.name != "Not lang" {
return
}
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
@@ -605,7 +636,27 @@ func TestHasExt(t *testing.T) {
}
func BenchmarkParseIdentity(b *testing.B) {
for i := 0; i < b.N; i++ {
testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
parser := newTestParser()
for b.Loop() {
parser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
}
}
func TestSitesMatrixFromPath(t *testing.T) {
c := qt.New(t)
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
v := parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.LenVectors(), qt.Equals, 1)
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 0})
}
func BenchmarkSitesMatrixFromPath(b *testing.B) {
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
for b.Loop() {
parser.SitesMatrixFromPath(p)
}
}
+4 -4
View File
@@ -35,9 +35,9 @@ weight = 2
removePathAccents = true
-- content/διακριτικός.md --
-- content/διακριτικός.fr.md --
-- layouts/_default/single.html --
-- layouts/single.html --
{{ .Language.Lang }}|Single.
-- layouts/_default/list.html --
-- layouts/list.html --
List
`
b := hugolib.Test(t, files)
@@ -64,9 +64,9 @@ disablePathToLower = true
-- content/MySection/MyPage.fr.md --
-- content/MySection/MyBundle/index.md --
-- content/MySection/MyBundle/index.fr.md --
-- layouts/_default/single.html --
-- layouts/single.html --
{{ .Language.Lang }}|Single.
-- layouts/_default/list.html --
-- layouts/list.html --
{{ .Language.Lang }}|List.
`
b := hugolib.Test(t, files)
+250 -27
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// 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.
@@ -13,52 +13,128 @@
package predicate
import (
"iter"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/hugofs/hglob"
)
// Match represents the result of a predicate evaluation.
type Match interface {
OK() bool
}
var (
// Predefined Match values for common cases.
True = BoolMatch(true)
False = BoolMatch(false)
)
// BoolMatch is a simple Match implementation based on a boolean value.
type BoolMatch bool
func (b BoolMatch) OK() bool {
return bool(b)
}
// breakMatch is a Match implementation that always returns false for OK() and signals to break evaluation.
type breakMatch struct{}
func (b breakMatch) OK() bool {
return false
}
var matchBreak = breakMatch{}
// P is a predicate function that tests whether a value of type T satisfies some condition.
type P[T any] func(T) bool
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
func (p P[T]) And(ps ...P[T]) P[T] {
return func(v T) bool {
for _, pp := range ps {
if !pp(v) {
return false
}
}
if p == nil {
return true
}
return p(v)
}
}
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
// Note that P[T] only supports Or. For chained AND/OR logic, use PR[T].
func (p P[T]) Or(ps ...P[T]) P[T] {
return func(v T) bool {
if p != nil && p(v) {
return true
}
for _, pp := range ps {
if pp(v) {
return true
}
}
if p == nil {
return false
}
return p(v)
return false
}
}
// Negate returns a predicate that is a logical negation of this predicate.
func (p P[T]) Negate() P[T] {
// PR is a predicate function that tests whether a value of type T satisfies some condition and returns a Match result.
type PR[T any] func(T) Match
// BoolFunc returns a P[T] version of this predicate.
func (p PR[T]) BoolFunc() P[T] {
return func(v T) bool {
return !p(v)
if p == nil {
return false
}
return p(v).OK()
}
}
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
func (p PR[T]) And(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(true)
}
}
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
func (p PR[T]) Or(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(false)
}
}
func shouldBreak(m Match) bool {
_, ok := m.(breakMatch)
return ok
}
// Filter returns a new slice holding only the elements of s that satisfy p.
// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length.
func (p P[T]) Filter(s []T) []T {
func (p PR[T]) Filter(s []T) []T {
var n int
for _, v := range s {
if p(v) {
if p(v).OK() {
s[n] = v
n++
}
@@ -67,12 +143,159 @@ func (p P[T]) Filter(s []T) []T {
}
// FilterCopy returns a new slice holding only the elements of s that satisfy p.
func (p P[T]) FilterCopy(s []T) []T {
func (p PR[T]) FilterCopy(s []T) []T {
var result []T
for _, v := range s {
if p(v) {
if p(v).OK() {
result = append(result, v)
}
}
return result
}
const (
rangeOpNone = iota
rangeOpLT
rangeOpLTE
rangeOpGT
rangeOpGTE
rangeOpEQ
rangeOpNE
)
func cutRangeOp(s string) (op int, rest string) {
switch {
case strings.HasPrefix(s, ">= "):
return rangeOpGTE, s[3:]
case strings.HasPrefix(s, "<= "):
return rangeOpLTE, s[3:]
case strings.HasPrefix(s, "== "):
return rangeOpEQ, s[3:]
case strings.HasPrefix(s, "!= "):
return rangeOpNE, s[3:]
case strings.HasPrefix(s, "> "):
return rangeOpGT, s[2:]
case strings.HasPrefix(s, "< "):
return rangeOpLT, s[2:]
default:
return rangeOpNone, s
}
}
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
var p PR[string]
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
if negate {
pattern = pattern[2:]
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.And(func(s string) Match {
return BoolMatch(!g.Match(s))
})
} else {
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.Or(func(s string) Match {
return BoolMatch(g.Match(s))
})
}
}
return p.BoolFunc(), nil
}
// NewIndexStringPredicateFromGlobsAndRanges creates an IndexString predicate from the given glob patterns and range patterns.
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
// A range pattern is one of "> value", ">= value", "< value" or "<= value".
func NewIndexStringPredicateFromGlobsAndRanges(patterns []string, getIndex func(s string) int, getGlob func(pattern string) (glob.Glob, error)) (P[IndexString], error) {
var p PR[IndexString]
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
if negate {
pattern = pattern[2:]
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.And(func(s IndexString) Match {
return BoolMatch(!g.Match(s.String))
})
} else {
// This can be either a glob or a value prefixed with one of >, >=, < or <=.
o, v := cutRangeOp(pattern)
if o != rangeOpNone {
i := getIndex(v)
if i == -1 {
// No match possible.
p = p.And(func(s IndexString) Match {
return BoolMatch(false)
})
continue
}
switch o {
// The greater values starts at the top with index 0.
case rangeOpGT:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index < i)
})
case rangeOpGTE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index <= i)
})
case rangeOpLT:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index > i)
})
case rangeOpLTE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index >= i)
})
case rangeOpEQ:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index == i)
})
case rangeOpNE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index != i)
})
}
} else {
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.Or(func(s IndexString) Match {
return BoolMatch(g.Match(s.String))
})
}
}
}
return p.BoolFunc(), nil
}
type IndexString struct {
Index int
String string
}
type IndexMatcher interface {
IndexMatch(match P[IndexString]) (iter.Seq[int], error)
}
+225 -26
View File
@@ -17,38 +17,66 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/predicate"
)
func TestAdd(t *testing.T) {
func TestPredicate(t *testing.T) {
c := qt.New(t)
var p predicate.P[int] = intP1
n := func() predicate.PR[int] {
var pr predicate.PR[int]
return pr
}
c.Assert(p(1), qt.IsTrue)
var pr predicate.PR[int]
p := pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
pr = n().Or(intP1).Or(intP2)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsTrue) // true || false
c.Assert(p(2), qt.IsTrue) // false || true
c.Assert(p(3), qt.IsFalse) // false || false
pr = pr.And(intP3)
p = pr.BoolFunc()
c.Assert(p(2), qt.IsFalse) // true || true && false
c.Assert(pr(10), qt.IsTrue) // true || true && true
pr = pr.And(intP4)
p = pr.BoolFunc()
c.Assert(p(10), qt.IsTrue) // true || true && true && true
c.Assert(p(2), qt.IsFalse) // true || true && false && false
c.Assert(p(1), qt.IsFalse) // true || false && false && false
c.Assert(p(3), qt.IsFalse) // false || false && false && false
c.Assert(p(4), qt.IsFalse) // false || false && false && false
c.Assert(p(42), qt.IsFalse) // false || false && false && false
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
c.Assert(p(10), qt.IsTrue)
neg := p.Negate()
c.Assert(neg(1), qt.IsFalse)
c.Assert(neg(2), qt.IsTrue)
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
c.Assert(p(10), qt.IsTrue)
and := p.And(intP2)
c.Assert(and(1), qt.IsFalse)
c.Assert(and(2), qt.IsFalse)
c.Assert(and(10), qt.IsTrue)
or := p.Or(intP2)
c.Assert(or(1), qt.IsTrue)
c.Assert(or(2), qt.IsTrue)
c.Assert(or(10), qt.IsTrue)
c.Assert(or(11), qt.IsFalse)
pr = n().Or(intP1).Or(intP2).Or(intP3)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsTrue)
c.Assert(p(10), qt.IsTrue)
c.Assert(p(4), qt.IsFalse)
}
func TestFilter(t *testing.T) {
c := qt.New(t)
var p predicate.P[int] = intP1
p = p.Or(intP2)
var p predicate.PR[int]
p = p.Or(intP1).Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -59,8 +87,8 @@ func TestFilter(t *testing.T) {
func TestFilterCopy(t *testing.T) {
c := qt.New(t)
var p predicate.P[int] = intP1
p = p.Or(intP2)
var p predicate.PR[int]
p = p.Or(intP1).Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -68,16 +96,187 @@ func TestFilterCopy(t *testing.T) {
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2})
}
var intP1 = func(i int) bool {
var intP1 = func(i int) predicate.Match {
if i == 10 {
return true
return predicate.True
}
return i == 1
return predicate.BoolMatch(i == 1)
}
var intP2 = func(i int) bool {
var intP2 = func(i int) predicate.Match {
if i == 10 {
return true
return predicate.True
}
return i == 2
return predicate.BoolMatch(i == 2)
}
var intP3 = func(i int) predicate.Match {
if i == 10 {
return predicate.True
}
return predicate.BoolMatch(i == 3)
}
var intP4 = func(i int) predicate.Match {
if i == 10 {
return predicate.True
}
return predicate.BoolMatch(i == 4)
}
func TestNewStringPredicateFromGlobs(t *testing.T) {
c := qt.New(t)
getGlob := func(pattern string) (glob.Glob, error) {
return glob.Compile(pattern)
}
n := func(patterns ...string) predicate.P[string] {
p, err := predicate.NewStringPredicateFromGlobs(patterns, getGlob)
c.Assert(err, qt.IsNil)
return p
}
m := n("a", "! ab*", "abc")
c.Assert(m("a"), qt.IsTrue)
c.Assert(m("ab"), qt.IsFalse)
c.Assert(m("abc"), qt.IsFalse)
m = n()
c.Assert(m("anything"), qt.IsFalse)
}
func TestNewIndexStringPredicateFromGlobsAndRanges(t *testing.T) {
c := qt.New(t)
// Simulate versions: v4.0.0=0, v3.0.0=1, v2.0.0=2, v1.0.0=3
// Lower index = greater value.
versions := []string{"v4.0.0", "v3.0.0", "v2.0.0", "v1.0.0"}
getIndex := func(s string) int {
for i, v := range versions {
if v == s {
return i
}
}
return -1
}
getGlob := func(pattern string) (glob.Glob, error) {
return glob.Compile(pattern)
}
n := func(patterns ...string) predicate.P[predicate.IndexString] {
p, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(patterns, getIndex, getGlob)
c.Assert(err, qt.IsNil)
return p
}
is := func(i int) predicate.IndexString {
return predicate.IndexString{Index: i, String: versions[i]}
}
// Test >= v2.0.0 (index 2): should match indices <= 2
m := n(">= v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test > v2.0.0 (index 2): should match indices < 2
m = n("> v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test < v3.0.0 (index 1): should match indices > 1
m = n("< v3.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
// Test range: >= v2.0.0 AND <= v3.0.0
m = n(">= v2.0.0", "<= v3.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0 - too high
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - too low
// Test glob pattern
m = n("v2.*.*")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
// Test glob with negation
m = n("v*.*.*", "! v3.*.*")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
// Test range with negation: >= v2.0.0 but not v3.0.0
m = n(">= v2.0.0", "! v3.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
// Test unknown value in range returns no match
m = n(">= v99.0.0")
c.Assert(m(is(0)), qt.IsFalse)
c.Assert(m(is(3)), qt.IsFalse)
// Test == v2.0.0: should only match v2.0.0
m = n("== v2.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test != v2.0.0: should match everything except v2.0.0
m = n("!= v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
// Test != with range: >= v2.0.0 AND != v3.0.0
m = n(">= v2.0.0", "!= v3.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - excluded by !=
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
}
func BenchmarkPredicate(b *testing.B) {
b.Run("and or no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2).Or(intP3)
for b.Loop() {
_ = p(3).OK()
}
})
b.Run("and and no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2)
for b.Loop() {
_ = p(3).OK()
}
})
b.Run("and and match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2)
for b.Loop() {
_ = p(10).OK()
}
})
b.Run("or or match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.Or(intP2).Or(intP3)
for b.Loop() {
_ = p(2).OK()
}
})
}
+52
View File
@@ -0,0 +1,52 @@
// 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 predicate
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCutRangeOp(t *testing.T) {
c := qt.New(t)
op, rest := cutRangeOp(">= value")
c.Assert(op, qt.Equals, rangeOpGTE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("<= value")
c.Assert(op, qt.Equals, rangeOpLTE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("> value")
c.Assert(op, qt.Equals, rangeOpGT)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("< value")
c.Assert(op, qt.Equals, rangeOpLT)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("== value")
c.Assert(op, qt.Equals, rangeOpEQ)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("!= value")
c.Assert(op, qt.Equals, rangeOpNE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("value")
c.Assert(op, qt.Equals, rangeOpNone)
c.Assert(rest, qt.Equals, "value")
}
+25
View File
@@ -16,6 +16,7 @@ package terminal
import (
"fmt"
"io"
"os"
"strings"
@@ -72,3 +73,27 @@ func doublePercent(str string) string {
func singlePercent(str string) string {
return strings.Replace(str, "%%", "%", -1)
}
type ProgressState int
const (
ProgressHidden ProgressState = iota
ProgressNormal
ProgressError
ProgressIntermediate
ProgressWarning
)
// ReportProgress writes OSC 9;4 sequence to w.
func ReportProgress(w io.Writer, state ProgressState, progress float64) {
if progress < 0 {
progress = 0.0
}
if progress > 1 {
progress = 1.0
}
pi := int(progress * 100)
fmt.Fprintf(w, "\033]9;4;%d;%d\007", state, pi)
}
+1 -1
View File
@@ -65,7 +65,7 @@ func BenchmarkVisitLinesAfter(b *testing.B) {
line 3`
for i := 0; i < b.N; i++ {
for b.Loop() {
VisitLinesAfter(lines, func(s string) {
})
}
+18
View File
@@ -139,7 +139,25 @@ func NewBool(b bool) *bool {
return &b
}
// WeightProvider provides a weight.
type WeightProvider interface {
Weight() int
}
// Weight0Provider provides a weight that's considered before the WeightProvider in sorting.
// This allows the weight set on a given term to win.
type Weight0Provider interface {
Weight0() int
}
// PrintableValueProvider is implemented by types that can provide a printable value.
type PrintableValueProvider interface {
PrintableValue() any
}
type (
Strings2 [2]string
Strings3 [3]string
Ints2 [2]int
Ints3 [3]int
)
@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hugo
package version
import (
"fmt"
@@ -37,6 +37,9 @@ type Version struct {
// HugoVersionSuffix is the suffix used in the Hugo version string.
// It will be blank for release versions.
Suffix string
// Set when parsed from a string,
source string
}
var (
@@ -44,6 +47,10 @@ var (
_ compare.Comparer = (*VersionString)(nil)
)
func (v Version) IsZero() bool {
return v.Major == 0 && v.Minor == 0 && v.PatchLevel == 0 && v.Suffix == ""
}
func (v Version) String() string {
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
}
@@ -55,7 +62,7 @@ func (v Version) Version() VersionString {
// Compare implements the compare.Comparer interface.
func (h Version) Compare(other any) int {
return compareVersions(h, other)
return CompareVersions(h, other)
}
// VersionString represents a Hugo version string.
@@ -67,7 +74,7 @@ func (h VersionString) String() string {
// Compare implements the compare.Comparer interface.
func (h VersionString) Compare(other any) int {
return compareVersions(h.Version(), other)
return CompareVersions(h.Version(), other)
}
func (h VersionString) Version() Version {
@@ -83,19 +90,26 @@ func (h VersionString) Eq(other any) bool {
return s == h.String()
}
var versionSuffixes = []string{"-test", "-DEV"}
// ParseVersion parses a version string.
func ParseVersion(s string) (Version, error) {
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
var vv Version
for _, suffix := range versionSuffixes {
if strings.HasSuffix(s, suffix) {
vv.Suffix = suffix
s = strings.TrimSuffix(s, suffix)
hyphen := strings.Index(s, "-")
if hyphen > 0 {
suffix := s[hyphen:]
if len(suffix) > 1 {
if suffix[0] == '-' {
suffix = suffix[1:]
}
if len(suffix) > 0 {
vv.Suffix = suffix
s = s[:hyphen]
}
}
vv.Suffix = suffix
}
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
vv.source = s
return vv, nil
}
@@ -134,107 +148,70 @@ func (v Version) NextPatchLevel(level int) Version {
return prev
}
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
func version(major, minor, patch int, suffix string) string {
if suffix != "" {
if suffix[0] != '-' {
suffix = "-" + suffix
}
}
if patch > 0 || minor > 53 {
return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix)
}
return fmt.Sprintf("%d.%d%s", major, minor, suffix)
}
// CompareVersion compares the given version string or number against the
// running Hugo version.
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
// the running version.
func CompareVersion(version any) int {
return compareVersions(CurrentVersion, version)
}
func compareVersions(inVersion Version, in any) int {
// CompareVersion compares v1 with v2.
// It returns -1 if the v2 is less than, 0 if equal and 1 if greater than
// v1.
func CompareVersions(v1 Version, v2 any) int {
var c int
switch d := in.(type) {
switch d := v2.(type) {
case float64:
c = compareFloatWithVersion(d, inVersion)
c = compareFloatWithVersion(d, v1)
case float32:
c = compareFloatWithVersion(float64(d), inVersion)
c = compareFloatWithVersion(float64(d), v1)
case int:
c = compareFloatWithVersion(float64(d), inVersion)
c = compareFloatWithVersion(float64(d), v1)
case int32:
c = compareFloatWithVersion(float64(d), inVersion)
c = compareFloatWithVersion(float64(d), v1)
case int64:
c = compareFloatWithVersion(float64(d), inVersion)
c = compareFloatWithVersion(float64(d), v1)
case Version:
if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel {
return strings.Compare(inVersion.Suffix, d.Suffix)
if v1.IsZero() && d.IsZero() {
// Fall back to source comparison.
return strings.Compare(v1.source, d.source)
}
if d.Major > inVersion.Major {
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
return strings.Compare(v1.Suffix, d.Suffix)
}
if d.Major > v1.Major {
return 1
} else if d.Major < inVersion.Major {
} else if d.Major < v1.Major {
return -1
}
if d.Minor > inVersion.Minor {
if d.Minor > v1.Minor {
return 1
} else if d.Minor < inVersion.Minor {
} else if d.Minor < v1.Minor {
return -1
}
if d.PatchLevel > inVersion.PatchLevel {
if d.PatchLevel > v1.PatchLevel {
return 1
} else if d.PatchLevel < inVersion.PatchLevel {
} else if d.PatchLevel < v1.PatchLevel {
return -1
}
default:
s, err := cast.ToStringE(in)
s, err := cast.ToStringE(v2)
if err != nil {
return -1
}
v, err := ParseVersion(s)
if err != nil {
return -1
v2v, _ := ParseVersion(s)
if v1.IsZero() && v2v.IsZero() {
// Fall back to source comparison.
return strings.Compare(v1.source, v2v.source)
}
return inVersion.Compare(v)
return v1.Compare(v2v)
}
@@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hugo
package version
import (
"testing"
@@ -50,30 +50,45 @@ func TestHugoVersion(t *testing.T) {
func TestCompareVersions(t *testing.T) {
c := qt.New(t)
c.Assert(compareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
parseIgnoreErr := func(s string) Version {
v, _ := ParseVersion(s)
return v
}
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1)
c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1)
c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1)
}
func TestParseHugoVersion(t *testing.T) {
c := qt.New(t)
c.Assert(MustParseVersion("v2.3.2").String(), qt.Equals, "2.3.2")
c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25")
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// 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.
+1
View File
@@ -63,6 +63,7 @@ func TestLexicographicSort(t *testing.T) {
c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"})
}
// // Note that this cannot use b.Loop() because of golang/go#27217.
func BenchmarkStringSort(b *testing.B) {
prototype := []string{"b", "Bz", "zz", "ba", "αβδ αβδ αβδ", "A", "Ba", "ba", "nnnnasdfnnn", "AAgæåz", "αβδC"}
b.Run("LessStrings", func(b *testing.B) {
@@ -1,4 +1,4 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
// 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.
@@ -11,25 +11,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build extended
package webp
import (
"image"
"io"
"github.com/bep/gowebp/libwebp"
"github.com/bep/gowebp/libwebp/webpoptions"
)
// Encode writes the Image m to w in Webp format with the given
// options.
func Encode(w io.Writer, m image.Image, o webpoptions.EncodingOptions) error {
return libwebp.Encode(w, m, o)
}
// Supports returns whether webp encoding is supported in this build.
func Supports() bool {
return true
}
package compare
+138 -119
View File
@@ -29,9 +29,10 @@ import (
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
@@ -40,7 +41,10 @@ import (
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/hugolib/versions"
"github.com/gohugoio/hugo/langs"
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/markup_config"
@@ -55,8 +59,6 @@ import (
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/spf13/afero"
xmaps "golang.org/x/exp/maps"
)
// InternalConfig is the internal configuration for Hugo, not read from any user provided config file.
@@ -101,7 +103,8 @@ type Config struct {
// For internal use only.
Internal InternalConfig `mapstructure:"-" json:"-"`
// For internal use only.
C *ConfigCompiled `mapstructure:"-" json:"-"`
C *ConfigCompiled `mapstructure:"-" json:"-"`
isLanguageClone bool
RootConfig
@@ -141,16 +144,25 @@ type Config struct {
// The outputformats configuration sections maps a format name (a string) to a configuration object for that format.
OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"`
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
Languages *config.ConfigNamespace[map[string]langs.LanguageConfig, langs.LanguagesInternal] `mapstructure:"-"`
// The versions configuration section contains the top level versions configuration options.
Versions *config.ConfigNamespace[map[string]versions.VersionConfig, versions.VersionsInternal] `mapstructure:"-"`
// The roles configuration section contains the top level roles configuration options.
Roles *config.ConfigNamespace[map[string]roles.RoleConfig, roles.RolesInternal] `mapstructure:"-"`
// The outputs configuration section maps a Page Kind (a string) to a slice of output formats.
// This can be overridden in the front matter.
Outputs map[string][]string `mapstructure:"-"`
// The cascade configuration section contains the top level front matter cascade configuration options,
// a slice of page matcher and params to apply to those pages.
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]] `mapstructure:"-"`
Cascade *page.PageMatcherParamsConfigs `mapstructure:"-"`
// The segments defines segments for the site. Used for partial/segmented builds.
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, *segments.Segments] `mapstructure:"-"`
// Menu configuration.
// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
@@ -200,21 +212,25 @@ type Config struct {
// User provided parameters.
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
Params maps.Params `mapstructure:"-"`
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
Languages map[string]langs.LanguageConfig `mapstructure:"-"`
Params hmaps.Params `mapstructure:"-"`
// UglyURLs configuration. Either a boolean or a sections map.
UglyURLs any `mapstructure:"-"`
}
// Early initialization of config.
type configCompiler interface {
CompileConfig(logger loggers.Logger) error
}
// Late initialization of config.
type configInitializer interface {
InitConfig(logger loggers.Logger, defaultSitesMatrix sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error
}
func (c Config) cloneForLang() *Config {
x := c
x.isLanguageClone = true
x.C = nil
copyStringSlice := func(in []string) []string {
if in == nil {
@@ -321,18 +337,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
for _, lang := range c.DisableLanguages {
disabledLangs[lang] = true
}
for lang, language := range c.Languages {
if !language.Disabled && disabledLangs[lang] {
language.Disabled = true
c.Languages[lang] = language
}
if language.Disabled {
disabledLangs[lang] = true
if lang == c.DefaultContentLanguage {
return fmt.Errorf("cannot disable default content language %q", lang)
}
}
}
for i, s := range c.IgnoreLogs {
c.IgnoreLogs[i] = strings.ToLower(s)
@@ -467,10 +471,10 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
gc.RenderHookUseEmbeddedNever,
}
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) {
return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
return fmt.Errorf("site config markup.goldmark.renderHooks.image.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
}
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) {
return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
return fmt.Errorf("site config markup.goldmark.renderHooks.link.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
}
c.C = &ConfigCompiled{
@@ -485,7 +489,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
MainSections: c.MainSections,
Clock: clock,
HTTPCache: httpCache,
@@ -525,7 +528,6 @@ type ConfigCompiled struct {
CreateTitle func(s string) string
IsUglyURLSection func(section string) bool
IgnoreFile func(filename string) bool
SegmentFilter segments.SegmentFilter
MainSections []string
Clock time.Time
HTTPCache httpcache.ConfigCompiled
@@ -588,13 +590,31 @@ type RootConfig struct {
// Set this to true to put all languages below their language ID.
DefaultContentLanguageInSubdir bool
// The default content role to use for the site.
DefaultContentRole string
// Set this to true to put the default role in a subdirectory.
DefaultContentRoleInSubdir bool
// The default content version to use for the site.
DefaultContentVersion string
// Set to true to render the default version in a subdirectory.
DefaultContentVersionInSubdir bool
// The default output format to use for the site.
// If not set, we will use the first output format.
DefaultOutputFormat string
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
// Note that this currently is an alias for DisableDefaultSiteRedirect introduced in v0.154.5.
// It's not obvious how a more fine grained setup would work.
DisableDefaultLanguageRedirect bool
// Disable generation of redirects to the default site when DefaultContentRoleInSubdir or DefaultContentVersionInSubdir or DefaultContentLanguageInSubdir is enabled.
// The default site is the site is the combination of defaultContentLanguage, defaultContentVersion and defaultContentRole.
DisableDefaultSiteRedirect bool
// Disable creation of alias redirect pages.
DisableAliases bool
@@ -790,33 +810,31 @@ func (c RootConfig) staticDirs() []string {
dirs = append(dirs, c.StaticDir8...)
dirs = append(dirs, c.StaticDir9...)
dirs = append(dirs, c.StaticDir10...)
return helpers.UniqueStringsReuse(dirs)
return hstrings.UniqueStringsReuse(dirs)
}
type Configs struct {
Base *Config
LoadingInfo config.LoadConfigResult
LanguageConfigMap map[string]*Config
LanguageConfigSlice []*Config
Base *Config
LoadingInfo config.LoadConfigResult
LanguageConfigMap map[string]*Config
IsMultihost bool
Modules modules.Modules
ModulesClient *modules.Client
FileCaches filecache.Caches
// All below is set in Init.
Languages langs.Languages
LanguagesDefaultFirst langs.Languages
ContentPathParser *paths.PathParser
Languages langs.Languages
ContentPathParser *paths.PathParser
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
DefaultContentSitesMatrix *sitesmatrix.IntSets
AllSitesMatrix *sitesmatrix.IntSets
configLangs []config.AllProvider
}
func (c *Configs) Validate(logger loggers.Logger) error {
c.Base.Cascade.Config.Range(func(p page.PageMatcher, cfg page.PageMatcherParamsConfig) bool {
page.CheckCascadePattern(logger, p)
return true
})
return nil
}
@@ -835,54 +853,15 @@ func (c *Configs) IsZero() bool {
return c == nil || len(c.Languages) == 0
}
func (c *Configs) Init() error {
func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
var languages langs.Languages
var langKeys []string
var hasEn bool
const en = "en"
for k := range c.LanguageConfigMap {
langKeys = append(langKeys, k)
if k == en {
hasEn = true
for _, f := range c.Base.Languages.Config.Sorted {
v, found := c.LanguageConfigMap[f.Name]
if !found {
return fmt.Errorf("invalid language configuration for %q", f.Name)
}
}
// Sort the LanguageConfigSlice by language weight (if set) or lang.
sort.Slice(langKeys, func(i, j int) bool {
ki := langKeys[i]
kj := langKeys[j]
lki := c.LanguageConfigMap[ki]
lkj := c.LanguageConfigMap[kj]
li := lki.Languages[ki]
lj := lkj.Languages[kj]
if li.Weight != lj.Weight {
return li.Weight < lj.Weight
}
return ki < kj
})
// See issue #13646.
defaultConfigLanguageFallback := en
if !hasEn {
// Pick the first one.
defaultConfigLanguageFallback = langKeys[0]
}
if c.Base.DefaultContentLanguage == "" {
c.Base.DefaultContentLanguage = defaultConfigLanguageFallback
}
for _, k := range langKeys {
v := c.LanguageConfigMap[k]
if v.DefaultContentLanguage == "" {
v.DefaultContentLanguage = defaultConfigLanguageFallback
}
c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
languageConf := v.Languages[k]
language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf)
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
if err != nil {
return err
}
@@ -899,25 +878,29 @@ func (c *Configs) Init() error {
}
languages = languages[:n]
var languagesDefaultFirst langs.Languages
for _, l := range languages {
if l.Lang == c.Base.DefaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
for _, l := range languages {
if l.Lang != c.Base.DefaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
c.Languages = languages
c.ConfiguredDimensions = &sitesmatrix.ConfiguredDimensions{
ConfiguredLanguages: c.Base.Languages.Config,
ConfiguredVersions: c.Base.Versions.Config,
ConfiguredRoles: c.Base.Roles.Config,
}
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
if err := c.ConfiguredDimensions.Init(); err != nil {
return err
}
intSetsCfg := sitesmatrix.IntSetsConfig{
ApplyDefaults: sitesmatrix.IntSetsConfigApplyDefaultsIfNotSet,
}
matrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithConfig(intSetsCfg)
c.DefaultContentSitesMatrix = matrix.Build()
c.AllSitesMatrix = sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithAllIfNotSet().Build()
c.ContentPathParser = &paths.PathParser{
LanguageIndex: languagesDefaultFirst.AsIndexSet(),
IsLangDisabled: c.Base.IsLangDisabled,
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
ConfiguredDimensions: c.ConfiguredDimensions,
LanguageIndex: languages.AsIndexSet(),
IsLangDisabled: c.Base.IsLangDisabled,
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
IsOutputFormat: func(name, ext string) bool {
if name == "" {
return false
@@ -934,12 +917,25 @@ func (c *Configs) Init() error {
}
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
// Config can be shared between languages,
// avoid initializing the same config more than once.
for i, l := range c.Languages {
langConfig := c.LanguageConfigMap[l.Lang]
for _, s := range allDecoderSetups {
if getInitializer := s.getInitializer; getInitializer != nil {
if err := getInitializer(langConfig).InitConfig(logger, nil, c.ConfiguredDimensions); err != nil {
return err
}
}
}
c.configLangs[i] = ConfigLanguage{
m: c,
config: c.LanguageConfigMap[l.Lang],
baseConfig: c.LoadingInfo.BaseConfig,
language: l,
m: c,
config: langConfig,
baseConfig: c.LoadingInfo.BaseConfig,
language: l,
languageIndex: i,
}
}
@@ -948,7 +944,7 @@ func (c *Configs) Init() error {
}
// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil {
if err := modules.ApplyProjectConfigDefaults(logger.Logger(), c.Modules[0], c.configLangs...); err != nil {
return err
}
@@ -957,7 +953,7 @@ func (c *Configs) Init() error {
for _, m := range c.Modules[0].Mounts() {
var found bool
for _, cm := range c.Base.Module.Mounts {
if cm.Source == m.Source && cm.Target == m.Target && cm.Lang == m.Lang {
if cm.Equal(m) {
found = true
break
}
@@ -968,7 +964,7 @@ func (c *Configs) Init() error {
}
// Transfer the changed mounts to the language versions (all share the same mount set, but can be displayed in different languages).
for _, l := range c.LanguageConfigSlice {
for _, l := range c.LanguageConfigMap {
l.Module.Mounts = c.Base.Module.Mounts
}
@@ -985,7 +981,7 @@ func (c Configs) GetFirstLanguageConfig() config.AllProvider {
func (c Configs) GetByLang(lang string) config.AllProvider {
for _, l := range c.configLangs {
if l.Language().Lang == lang {
if l.Language().(*langs.Language).Lang == lang {
return l
}
}
@@ -1028,7 +1024,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
if lang == "" {
lang = "en"
}
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
res.Cfg.Set("languages", hmaps.Params{lang: hmaps.Params{}})
}
bcfg := res.BaseConfig
cfg := res.Cfg
@@ -1054,14 +1050,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
mergedConfig := config.New()
var differentRootKeys []string
switch x := v.(type) {
case maps.Params:
case nil:
case hmaps.Params:
_, found := x["params"]
if !found {
x["params"] = maps.Params{
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
x["params"] = hmaps.Params{
hmaps.MergeStrategyKey: hmaps.ParamsMergeStrategyDeep,
}
}
for kk, vv := range x {
if kk == "_merge" {
continue
@@ -1070,19 +1066,35 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
// baseURL configure don the language level is a multihost setup.
isMultihost = true
}
if p, ok := vv.(hmaps.Params); ok {
// With the introduction of YAML anchor and alias support, language config entries
// may be contain shared references.
// This also break potential cycles.
vv = hmaps.CloneParamsDeep(p)
}
if kk == "cascade" {
// If not set, add the current language to make sure it does not get applied to other languages.
page.AddLangToCascadeTargetMap(k, vv.(hmaps.Params))
// Always clone cascade config to get the sites matrix right.
differentRootKeys = append(differentRootKeys, kk)
}
mergedConfig.Set(kk, vv)
rootv := cfg.Get(kk)
if rootv != nil && cfg.IsSet(kk) {
// This overrides a root key and potentially needs a merge.
if !reflect.DeepEqual(rootv, vv) {
switch vvv := vv.(type) {
case maps.Params:
case hmaps.Params:
differentRootKeys = append(differentRootKeys, kk)
// Use the language value as base.
mergedConfigEntry := xmaps.Clone(vvv)
// Note that this is already cloned above.
mergedConfigEntry := vvv
// Merge in the root value.
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
hmaps.MergeParams(mergedConfigEntry, rootv.(hmaps.Params))
mergedConfig.Set(kk, mergedConfigEntry)
default:
@@ -1092,7 +1104,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
}
} else {
switch vv.(type) {
case maps.Params:
case hmaps.Params:
differentRootKeys = append(differentRootKeys, kk)
default:
// Apply new values to the root.
@@ -1100,7 +1112,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
}
}
}
differentRootKeys = helpers.UniqueStringsSorted(differentRootKeys)
differentRootKeys = hstrings.UniqueStringsSorted(differentRootKeys)
if len(differentRootKeys) == 0 {
langConfigMap[k] = all
@@ -1128,7 +1140,8 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
}
langConfigMap[k] = clone
case maps.ParamsMergeStrategy:
case hmaps.ParamsMergeStrategy:
default:
panic(fmt.Sprintf("unknown type in languages config: %T", v))
@@ -1142,10 +1155,16 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
l.CommonDirs.CacheDir = bcfg.CacheDir
}
caches, err := filecache.NewCaches(all.Caches, fs)
if err != nil {
return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
}
cm := &Configs{
Base: all,
LanguageConfigMap: langConfigMap,
LoadingInfo: res,
FileCaches: caches,
IsMultihost: isMultihost,
}
@@ -1179,7 +1198,7 @@ func decodeConfigFromParams(fs afero.Fs, logger loggers.Logger, bcfg config.Base
})
for _, v := range decoderSetups {
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg}
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg, logger: logger}
if err := v.decode(v, p); err != nil {
return fmt.Errorf("failed to decode %q: %w", v.key, err)
}
+7 -27
View File
@@ -39,7 +39,7 @@ title: "p1"
---
title: "p1"
---
-- layouts/_default/single.html --
-- layouts/single.html --
Title: {{ .Title }}
`
@@ -66,10 +66,12 @@ Title: {{ .Title }}
b.Assert(modConf.Mounts, qt.HasLen, 8)
b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en"))
b.Assert(modConf.Mounts[0].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en")
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "")
b.Assert(modConf.Mounts[0].Sites.Matrix.Languages, qt.DeepEquals, []string{"en"})
b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv"))
b.Assert(modConf.Mounts[1].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "")
b.Assert(modConf.Mounts[1].Sites.Matrix.Languages, qt.DeepEquals, []string{"sv"})
}
func TestConfigAliases(t *testing.T) {
@@ -139,7 +141,7 @@ disableKinds = ["taxonomy", "term"]
[pagination]
disableAliases = true
pagerSize = 2
-- layouts/_default/list.html --
-- layouts/list.html --
{{ $paginator := .Paginate site.RegularPages }}
{{ template "_internal/pagination.html" . }}
{{ range $paginator.Pages }}
@@ -189,7 +191,7 @@ func TestInvalidOutputFormat(t *testing.T) {
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[outputs]
home = ['html','foo']
-- layouts/index.html --
-- layouts/home.html --
x
`
@@ -198,28 +200,6 @@ x
b.Assert(err.Error(), qt.Contains, `failed to create config: unknown output format "foo" for kind "home"`)
}
// Issue 13201
func TestLanguageConfigSlice(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[languages.en]
title = 'TITLE_EN'
weight = 2
[languages.de]
title = 'TITLE_DE'
weight = 1
[languages.fr]
title = 'TITLE_FR'
weight = 3
`
b := hugolib.Test(t, files)
b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`)
}
func TestContentTypesDefault(t *testing.T) {
files := `
-- hugo.toml --
+94 -59
View File
@@ -15,20 +15,25 @@ package allconfig
import (
"fmt"
"sort"
"strings"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/hugolib/versions"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
@@ -46,16 +51,18 @@ import (
)
type decodeConfig struct {
p config.Provider
c *Config
fs afero.Fs
bcfg config.BaseConfig
p config.Provider
c *Config
fs afero.Fs
bcfg config.BaseConfig
logger loggers.Logger
}
type decodeWeight struct {
key string
decode func(decodeWeight, decodeConfig) error
getCompiler func(c *Config) configCompiler
getInitializer func(c *Config) configInitializer
weight int
internalOrDeprecated bool // Hide it from the docs.
}
@@ -69,8 +76,10 @@ var allDecoderSetups = map[string]decodeWeight{
return err
}
// This need to match with Lang which is always lower case.
// This need to match with the map keys, always lower case.
p.c.RootConfig.DefaultContentLanguage = strings.ToLower(p.c.RootConfig.DefaultContentLanguage)
p.c.RootConfig.DefaultContentRole = strings.ToLower(p.c.RootConfig.DefaultContentRole)
p.c.RootConfig.DefaultContentVersion = strings.ToLower(p.c.RootConfig.DefaultContentVersion)
return nil
},
@@ -140,9 +149,12 @@ var allDecoderSetups = map[string]decodeWeight{
key: "segments",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key))
p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key), p.c.RenderSegments, p.logger)
return err
},
getInitializer: func(c *Config) configInitializer {
return c.Segments.Config
},
},
"server": {
key: "server",
@@ -184,7 +196,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "outputs",
decode: func(d decodeWeight, p decodeConfig) error {
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
m := hmaps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
p.c.Outputs = make(map[string][]string)
for k, v := range m {
s := types.ToStringSlicePreserveString(v)
@@ -210,10 +222,74 @@ var allDecoderSetups = map[string]decodeWeight{
return err
},
},
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
if len(m) == 1 {
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
var first hmaps.Params
var ok bool
for _, v := range m {
first, ok = v.(hmaps.Params)
if ok {
break
}
}
if first != nil {
if _, found := first["languagecode"]; !found {
first["languagecode"] = p.p.GetString("languagecode")
}
}
}
var (
err error
defaultContentLanguage string
)
p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, p.c.RootConfig.DisableLanguages, m)
if err != nil {
return fmt.Errorf("failed to decode languages config: %w", err)
}
for k, v := range p.c.Languages.Config.LanguageConfigs {
if v.Disabled {
p.c.RootConfig.DisableLanguages = append(p.c.RootConfig.DisableLanguages, k)
}
}
p.c.RootConfig.DisableLanguages = hstrings.UniqueStringsReuse(p.c.RootConfig.DisableLanguages)
sort.Strings(p.c.RootConfig.DisableLanguages)
p.c.RootConfig.DefaultContentLanguage = defaultContentLanguage
return nil
},
},
"versions": {
key: "versions",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
return err
},
},
"roles": {
key: "roles",
decode: func(d decodeWeight, p decodeConfig) error {
var (
err error
defaultContentRole string
)
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Roles, defaultContentRole, err = roles.DecodeConfig(p.c.RootConfig.DefaultContentRole, m)
p.c.RootConfig.DefaultContentRole = defaultContentRole
return err
},
},
"params": {
key: "params",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params"))
p.c.Params = hmaps.CleanConfigStringMap(p.p.GetStringMap("params"))
if p.c.Params == nil {
p.c.Params = make(map[string]any)
}
@@ -233,7 +309,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "module",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Module, err = modules.DecodeConfig(p.p)
p.c.Module, err = modules.DecodeConfig(p.logger.Logger(), p.p)
return err
},
},
@@ -259,7 +335,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "taxonomies",
decode: func(d decodeWeight, p decodeConfig) error {
if p.p.IsSet(d.key) {
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
p.c.Taxonomies = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
}
return nil
},
@@ -283,56 +359,15 @@ var allDecoderSetups = map[string]decodeWeight{
return nil
},
},
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := p.p.GetStringMap(d.key)
if len(m) == 1 {
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
var first maps.Params
var ok bool
for _, v := range m {
first, ok = v.(maps.Params)
if ok {
break
}
}
if first != nil {
if _, found := first["languagecode"]; !found {
first["languagecode"] = p.p.GetString("languagecode")
}
}
}
p.c.Languages, err = langs.DecodeConfig(m)
if err != nil {
return err
}
// Validate defaultContentLanguage.
if p.c.DefaultContentLanguage != "" {
var found bool
for lang := range p.c.Languages {
if lang == p.c.DefaultContentLanguage {
found = true
break
}
}
if !found {
return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
}
}
return nil
},
},
"cascade": {
key: "cascade",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key))
p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key))
return err
},
getInitializer: func(c *Config) configInitializer { return c.Cascade },
},
"menus": {
key: "menus",
@@ -412,7 +447,7 @@ var allDecoderSetups = map[string]decodeWeight{
"author": {
key: "author",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Author = maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Author = hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
return nil
},
internalOrDeprecated: true,
@@ -420,7 +455,7 @@ var allDecoderSetups = map[string]decodeWeight{
"social": {
key: "social",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Social = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
p.c.Social = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
internalOrDeprecated: true,
@@ -434,8 +469,8 @@ var allDecoderSetups = map[string]decodeWeight{
p.c.UglyURLs = vv
case string:
p.c.UglyURLs = vv == "true"
case maps.Params:
p.c.UglyURLs = cast.ToStringMapBool(maps.CleanConfigStringMap(vv))
case hmaps.Params:
p.c.UglyURLs = cast.ToStringMapBool(hmaps.CleanConfigStringMap(vv))
default:
p.c.UglyURLs = cast.ToStringMapBool(v)
}
+57 -16
View File
@@ -19,28 +19,29 @@ import (
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
)
type ConfigLanguage struct {
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
languageIndex int
}
func (c ConfigLanguage) Language() *langs.Language {
func (c ConfigLanguage) Language() any {
return c.language
}
func (c ConfigLanguage) Languages() langs.Languages {
return c.m.Languages
func (c ConfigLanguage) LanguageIndex() int {
return c.languageIndex
}
func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
return c.m.LanguagesDefaultFirst
func (c ConfigLanguage) Languages() any {
return c.m.Languages
}
func (c ConfigLanguage) PathParser() *paths.PathParser {
@@ -48,14 +49,14 @@ func (c ConfigLanguage) PathParser() *paths.PathParser {
}
func (c ConfigLanguage) LanguagePrefix() string {
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.Language().Lang {
return c.Language().Lang
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.language.Lang {
return c.language.Lang
}
if !c.IsMultilingual() || c.DefaultContentLanguage() == c.Language().Lang {
if !c.IsMultilingual() || c.DefaultContentLanguage() == c.language.Lang {
return ""
}
return c.Language().Lang
return c.language.Lang
}
func (c ConfigLanguage) BaseURL() urls.BaseURL {
@@ -97,6 +98,10 @@ func (c ConfigLanguage) IsLangDisabled(lang string) bool {
return c.config.C.DisabledLanguages[lang]
}
func (c ConfigLanguage) IsKindEnabled(kind string) bool {
return !c.config.C.DisabledKinds[kind]
}
func (c ConfigLanguage) IgnoredLogs() map[string]bool {
return c.config.C.IgnoredLogs
}
@@ -117,6 +122,10 @@ func (c ConfigLanguage) BaseConfig() config.BaseConfig {
return c.baseConfig
}
func (c ConfigLanguage) FileCaches() any {
return c.m.FileCaches
}
func (c ConfigLanguage) Dirs() config.CommonDirs {
return c.config.CommonDirs
}
@@ -129,6 +138,10 @@ func (c ConfigLanguage) WorkingDir() string {
return c.m.Base.WorkingDir
}
func (c ConfigLanguage) CacheDirMisc() string {
return c.config.Caches.CacheDirMisc()
}
func (c ConfigLanguage) Quiet() bool {
return c.m.Base.Internal.Quiet
}
@@ -137,11 +150,11 @@ func (c ConfigLanguage) Watching() bool {
return c.m.Base.Internal.Watch
}
func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager {
func (c ConfigLanguage) NewIdentityManager(opts ...identity.ManagerOption) identity.Manager {
if !c.Watching() {
return identity.NopManager
}
return identity.NewManager(name, opts...)
return identity.NewManager(opts...)
}
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
@@ -155,16 +168,24 @@ func (c ConfigLanguage) GetConfigSection(s string) any {
return c.config.Security
case "build":
return c.config.Build
case "cascade":
return c.config.Cascade
case "frontmatter":
return c.config.Frontmatter
case "caches":
return c.config.Caches
case "markup":
return c.config.Markup
case "module":
return c.config.Module
case "mediaTypes":
return c.config.MediaTypes.Config
case "outputFormats":
return c.config.OutputFormats.Config
case "roles":
return c.config.Roles.Config
case "versions":
return c.config.Versions.Config
case "permalinks":
return c.config.Permalinks
case "minify":
@@ -212,6 +233,14 @@ func (c ConfigLanguage) DefaultContentLanguageInSubdir() bool {
return c.config.DefaultContentLanguageInSubdir
}
func (c ConfigLanguage) DefaultContentRoleInSubdir() bool {
return c.config.DefaultContentRoleInSubdir
}
func (c ConfigLanguage) DefaultContentVersionInSubdir() bool {
return c.config.DefaultContentVersionInSubdir
}
func (c ConfigLanguage) SummaryLength() int {
return c.config.SummaryLength
}
@@ -259,3 +288,15 @@ func (c ConfigLanguage) StaticDirs() []string {
func (c ConfigLanguage) EnableEmoji() bool {
return c.config.EnableEmoji
}
func (c ConfigLanguage) ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions {
return c.m.ConfiguredDimensions
}
func (c ConfigLanguage) DefaultContentsitesMatrix() *sitesmatrix.IntSets {
return c.m.DefaultContentSitesMatrix
}
func (c ConfigLanguage) AllSitesMatrix() *sitesmatrix.IntSets {
return c.m.AllSitesMatrix
}
+6 -6
View File
@@ -14,7 +14,7 @@
package allconfig
import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/docshelper"
)
@@ -27,12 +27,12 @@ func init() {
if v.internalOrDeprecated {
continue
}
cfg.Set(configRoot, make(maps.Params))
cfg.Set(configRoot, make(hmaps.Params))
}
lang := maps.Params{
"en": maps.Params{
"menus": maps.Params{},
"params": maps.Params{},
lang := hmaps.Params{
"en": hmaps.Params{
"menus": hmaps.Params{},
"params": hmaps.Params{},
},
}
cfg.Set("languages", lang)
+40 -17
View File
@@ -19,19 +19,20 @@ import (
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
hglob "github.com/gohugoio/hugo/hugofs/glob"
hglob "github.com/gohugoio/hugo/hugofs/hglob"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
@@ -40,7 +41,14 @@ import (
//lint:ignore ST1005 end user message.
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to load config: %v", r)
debug.PrintStack()
}
}()
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
d.Environ = os.Environ()
}
@@ -59,7 +67,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
return nil, fmt.Errorf("failed to load config: %w", err)
}
configs, err := fromLoadConfigResult(d.Fs, d.Logger, res)
configs, err = fromLoadConfigResult(d.Fs, d.Logger, res)
if err != nil {
return nil, fmt.Errorf("failed to create config from result: %w", err)
}
@@ -87,13 +95,13 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
configs.Modules = moduleConfig.AllModules
configs.ModulesClient = modulesClient
if err := configs.Init(); err != nil {
if err := configs.Init(d.Fs, d.Logger); err != nil {
return nil, fmt.Errorf("failed to init config: %w", err)
}
loggers.SetGlobalLogger(d.Logger)
return configs, nil
return
}
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
@@ -158,7 +166,7 @@ func (l configLoader) applyConfigAliases() error {
}
func (l configLoader) applyDefaultConfig() error {
defaultSettings := maps.Params{
defaultSettings := hmaps.Params{
// These dirs are used early/before we build the config struct.
"themesDir": "themes",
"configDir": "config",
@@ -170,10 +178,16 @@ func (l configLoader) applyDefaultConfig() error {
}
func (l configLoader) normalizeCfg(cfg config.Provider) error {
if b, ok := cfg.Get("minifyOutput").(bool); ok && b {
cfg.Set("minify.minifyOutput", true)
} else if b, ok := cfg.Get("minify").(bool); ok && b {
cfg.Set("minify", maps.Params{"minifyOutput": true})
if b, ok := cfg.Get("minifyOutput").(bool); ok {
hugo.Deprecate("site config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
if b {
cfg.Set("minify.minifyOutput", true)
}
} else if b, ok := cfg.Get("minify").(bool); ok {
hugo.Deprecate("site config minify", "Use minify.minifyOutput instead.", "v0.150.0")
if b {
cfg.Set("minify", hmaps.Params{"minifyOutput": true})
}
}
return nil
@@ -206,8 +220,8 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
var hugoEnv []types.KeyValueStr
for _, v := range environ {
key, val := config.SplitEnvVar(v)
if strings.HasPrefix(key, hugoEnvPrefix) {
delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
if after, ok := strings.CutPrefix(key, hugoEnvPrefix); ok {
delimiterAndKey := after
if len(delimiterAndKey) < 2 {
continue
}
@@ -226,7 +240,7 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
}
for _, env := range hugoEnv {
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
existing, nestedKey, owner, err := hmaps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
if err != nil {
return err
}
@@ -280,7 +294,14 @@ func (l *configLoader) envValToVal(k string, v any) any {
func (l *configLoader) envStringToVal(k, v string) any {
switch k {
case "disablekinds", "disablelanguages":
case "disablekinds", "disablelanguages", "ignorefiles", "ignorelogs":
v = strings.TrimSpace(v)
if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
if parsed, err := metadecoders.Default.UnmarshalStringTo(v, []any{}); err == nil {
return parsed
}
}
if strings.Contains(v, ",") {
return strings.Split(v, ",")
} else {
@@ -456,6 +477,7 @@ func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bo
PublishDir: publishDir,
Environment: l.Environment,
CacheDir: conf.Caches.CacheDirModules(),
ModuleQueriesCache: configs.FileCaches.ModuleQueriesCache(),
ModuleConfig: conf.Module,
IgnoreVendor: ignoreVendor,
IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist,
@@ -525,11 +547,12 @@ func (l configLoader) loadConfig(configName string) (string, error) {
return filename, nil
}
func (l configLoader) deleteMergeStrategies() {
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
func (l configLoader) deleteMergeStrategies() (err error) {
l.cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
return
}
func (l configLoader) wrapFileError(err error, filename string) error {

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