Compare commits

...

413 Commits

Author SHA1 Message Date
hugoreleaser ea8f66a7ce releaser: Bump versions for release of 0.161.1
[ci skip]
2026-04-29 13:56:01 +00:00
Bjørn Erik Pedersen c4eba92863 resources: Honor Retry-After header in resources.GetRemote retries
When the server returns a temporary HTTP error (e.g. 429 or 503)
together with a Retry-After header, use that value as the next sleep
duration instead of the default exponential backoff. The Retry-After
value is also surfaced in the retry-timeout error message.

Fixes #14828
2026-04-29 15:44:16 +02:00
Bjørn Erik Pedersen 8b40a96b6e warpc: Move to parson.c in https://github.com/kgabis/parson
And be specific about which commit we use.

Hugo treat this as an upstream dependency, so we would appreciate that any bugs will be reported and fixed upstream.

See #14823
2026-04-29 13:51:42 +02:00
Bjørn Erik Pedersen d65af84d15 config/security: Add AllowChildProcess to security.node.permissions
Some Linux setups trigger detect-libc's spawnSync('getconf') fallback
when process.report does not expose glibcVersionRuntime, breaking
tailwindcss under the Node permission model. Add AllowChildProcess
mirroring AllowAddons/AllowWorker, default to ["tailwindcss"], and
emit --allow-child-process accordingly.

Fixes #14824
2026-04-29 13:50:37 +02:00
Bjørn Erik Pedersen 454450a647 config/security: Restrict default http.urls "@" deny to userinfo
The previous "! @" deny rule rejected any URL containing "@",
including legitimate version-pinned imports such as
https://cdn.jsdelivr.net/npm/mermaid@latest/dist/mermaid.esm.min.mjs.
Tighten it to "! (?i)^https?://[^/?#]*@" so only "@" inside the
authority section (i.e. real userinfo) is blocked.

Fixes #14825

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:50:37 +02:00
hugoreleaser 2bfcc6b994 releaser: Prepare repository for 0.162.0-DEV
[ci skip]
2026-04-28 12:08:01 +00:00
hugoreleaser 98d396c16a releaser: Bump versions for release of 0.161.0
[ci skip]
2026-04-28 11:46:32 +00:00
dependabot[bot] d4ae662d59 build(deps): bump github.com/getkin/kin-openapi from 0.135.0 to 0.137.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.135.0 to 0.137.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.135.0...v0.137.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 13:40:06 +02:00
dependabot[bot] 9ede5fb9e0 build(deps): bump github.com/mattn/go-isatty from 0.0.21 to 0.0.22
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.21 to 0.0.22.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.21...v0.0.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 20:10:08 +02:00
dependabot[bot] 833a878eef build(deps): bump github.com/tdewolff/minify/v2 from 2.24.12 to 2.24.13
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.12 to 2.24.13.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.12...v2.24.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 20:08:49 +02:00
Bjørn Erik Pedersen 7622dd86ce css: Support nested hugo:vars/<name> imports
Allow CSS variables to be grouped under sub-paths and imported via
@import "hugo:vars/mobile" (or @use for Dart Sass), so callers can pass
nested dicts like:

    {{ dict "primary-color" "blue" "mobile" (dict "primary-color" "red") }}

Top-level "hugo:vars" now skips nested map entries instead of emitting
garbage for them.

Fixes #14705

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:42:50 +02:00
Bjørn Erik Pedersen 0814059bb6 github: Update GitHub actions versions
By running:

```
ghat swot --stable 7 -d .github
```

Closes #14810
2026-04-27 19:15:56 +02:00
Joe Mooring 8920d56e95 hugolib: Do not render aliases if the page is not rendered
Closes #14807
2026-04-25 18:41:27 +02:00
Joe Mooring 633cc772e0 langs/i18n: Improve default content language fallback
The fallback order for translations is now:

1. Current language's locale (e.g., pt-BR → pt-br.toml)
2. Current language's key (e.g., pt → pt.toml)
3. Default language's locale (e.g., es-AR → es-ar.toml) ← new
4. Default language's key (e.g., es → es.toml)

Closes #14243

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 10:43:37 +02:00
Bjørn Erik Pedersen 90d8bf34ae Replace deprecated .Site.Sites/.Page.Sites with hugo.Sites intests 2026-04-24 19:02:46 +02:00
Bjørn Erik Pedersen 4c40c6d5ca helpers: Remove unused code 2026-04-23 20:05:09 +02:00
Bjørn Erik Pedersen d2594db670 common/constants: Remove unused consts 2026-04-23 20:05:09 +02:00
Bjørn Erik Pedersen ab2de51e07 common/paths: Remove unused code
Identified with:

```
punused "common/paths/**.go"
 ````
2026-04-23 20:05:09 +02:00
Joe Mooring 72b85d5f9c langs/i18n: Fix translation lookup when using language variants
Closes #7982

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 18:47:12 +02:00
Joe Mooring 75f61832c2 tests: Update Ruby setup action to v1.305.0 2026-04-23 18:46:49 +02:00
dependabot[bot] 4c03129fcf build(deps): bump github.com/magefile/mage from 1.17.1 to 1.17.2
Bumps [github.com/magefile/mage](https://github.com/magefile/mage) from 1.17.1 to 1.17.2.
- [Release notes](https://github.com/magefile/mage/releases)
- [Commits](https://github.com/magefile/mage/compare/v1.17.1...v1.17.2)

---
updated-dependencies:
- dependency-name: github.com/magefile/mage
  dependency-version: 1.17.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-23 15:02:42 +02:00
Bjørn Erik Pedersen 080970bc6b deps: Upgrade github.com/bep/imagemeta v0.17.1 => v0.17.2 2026-04-23 15:02:28 +02:00
Joe Mooring 1b7495bc49 langs: Use Language.Locale as primary localization key
Localization now uses Language.Locale as the golocales lookup key,
falling back to Language.Lang, then defaultContentLanguage, then "en".

Closes #9109
2026-04-22 22:41:51 +02:00
Bjørn Erik Pedersen 79f030be5b config/security: Add "! " negation to Whitelist, harden default http.urls
Whitelist now treats any pattern prefixed with "! " (the same negation
prefix used by hglob/predicate) as a deny rule. Deny matches take
precedence over allow, and a whitelist made up exclusively of deny
rules implicitly allows everything it does not deny.

The default security.http.urls now reads:

    urls = ['(?i)^https?://[a-z]', '! (?i)localhost', '! @']

i.e. allow URLs whose host starts with a letter (the common
"https://example.com" shape), deny anything that looks like localhost,
and deny URLs with userinfo to foil "http://user@127.0.0.1/" bypasses.
Public IP literals are collateral blocks; users who need them (or their
own private hosts) override security.http.urls as before, mixing allow
and deny rules with the same "! " prefix, e.g.

    [security.http]
    urls = ['.*', '! ^https?://evil\.example\.com']

Fixes #14792
2026-04-22 20:15:19 +02:00
dependabot[bot] 896bc89ab8 build(deps): bump github.com/aws/aws-sdk-go-v2/service/cloudfront (#14789)
Bumps [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://github.com/aws/aws-sdk-go-v2) from 1.59.0 to 1.61.1.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.59.0...service/s3/v1.61.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 15:51:30 +02:00
dependabot[bot] 100dde53ad build(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.21 (#14788)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.21.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 15:51:14 +02:00
dependabot[bot] bdebb79705 build(deps): bump github.com/bep/mclib (#14787)
Bumps [github.com/bep/mclib](https://github.com/bep/mclib) from 1.20400.20402 to 1.20401.20400.
- [Release notes](https://github.com/bep/mclib/releases)
- [Commits](https://github.com/bep/mclib/compare/v1.20400.20402...v1.20401.20400)

---
updated-dependencies:
- dependency-name: github.com/bep/mclib
  dependency-version: 1.20401.20400
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 15:51:00 +02:00
Bjørn Erik Pedersen a54c398b93 Harden Node tool execution with --permission flag
Add security.node.permissions config to run Node tools (PostCSS, Babel,
TailwindCSS) under Node's permission model, restricting file system access
to the working directory by default.

The binary resolution is simplified to node_modules/.bin → PATH (npx removed).
For both locations, the actual JS entry point is resolved via symlinks (macOS/Linux)
or by parsing npm wrapper scripts (Windows .cmd), then executed as
"node --permission --allow-fs-read=<path> --allow-fs-write=<path> <script>".

Users can opt out by removing "node" from security.exec.allow.

Closes #7287
2026-04-22 15:47:34 +02:00
Bjørn Erik Pedersen f5fce935e7 tpl/collections: Honor the Eqer interface in where comparisons
The where function previously fell through to a no-op when comparing
two values whose kinds were not handled by the primitive type switches
(e.g. two Page interface values). This made `where pages "Parent" $page`
return an empty list, while the equivalent `range pages` + `if eq` worked.

Use compare.Eqer for equality operators when either side implements it,
matching the behavior of the eq/ne template funcs.

Fixes #14777

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:45:06 +02:00
dependabot[bot] 52123ae23f build(deps): bump google.golang.org/api from 0.267.0 to 0.276.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.267.0 to 0.276.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.267.0...v0.276.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 19:12:24 +02:00
dependabot[bot] 38b8afdc90 build(deps): bump github.com/aws/aws-sdk-go-v2 from 1.41.5 to 1.41.6
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.41.5 to 1.41.6.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.5...v1.41.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 19:12:13 +02:00
Bjørn Erik Pedersen 4169c1f70f modules: Ignore non-require blocks in go.mod rewrite
The Go 1.24 tool directive uses single-token entries inside a
tool ( ... ) block. The previous splitter treated any tab-indented
line as a require entry, causing an index out of range panic when
running hugo mod tidy on a module with a tool block.

Track the require block state explicitly so other blocks (tool,
replace, exclude, retract) are left untouched.

Fixes #14783
2026-04-20 19:11:41 +02:00
Bjørn Erik Pedersen 7574e35b40 Replace the concurrent map with an identical upstream version 2026-04-20 18:33:59 +02:00
dependabot[bot] 927666005c build(deps): bump github.com/getkin/kin-openapi from 0.134.0 to 0.135.0 (#14781)
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.134.0 to 0.135.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.134.0...v0.135.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 17:45:53 +02:00
dependabot[bot] 790f4084a1 build(deps): bump github.com/bep/goportabletext from 0.1.0 to 0.2.0 (#14779)
Bumps [github.com/bep/goportabletext](https://github.com/bep/goportabletext) from 0.1.0 to 0.2.0.
- [Release notes](https://github.com/bep/goportabletext/releases)
- [Commits](https://github.com/bep/goportabletext/compare/v0.1.0...v0.2.0)

---
updated-dependencies:
- dependency-name: github.com/bep/goportabletext
  dependency-version: 0.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 17:45:36 +02:00
dependabot[bot] de6955bab3 build(deps): bump golang.org/x/image from 0.38.0 to 0.39.0 (#14780)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/image/compare/v0.38.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 15:19:28 +02:00
Bjørn Erik Pedersen a77bd527fd deps: Upgrade github.com/bep/imagemeta v0.17.0 => v0.17.1 (#14775)
Close #14758
2026-04-20 10:35:22 +02:00
Bjørn Erik Pedersen 017a7cd63a Add slice-based permalinks config with PageMatcher target
Closes #14744
Clses #4641

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 19:45:45 +02:00
Bjørn Erik Pedersen e3413d927f commands: Add missing import 2026-04-19 15:35:46 +02:00
Bjørn Erik Pedersen b01cc14703 Revert "common/hugo: Deprecate extended and extended_withdeploy editions"
This reverts commit a17bdbc5fa.

Close #14771
2026-04-19 11:15:49 +02:00
Bjørn Erik Pedersen 8ee19ff9a3 Adjust the SECURITY.md slightly 2026-04-18 23:05:27 +02:00
Joe Mooring 6436deb3e1 create: Fix non-deterministic conflict detection in hugo new content
The contentInclusionFilter used strings.Contains to match filenames
against the target path. Because strings.Contains is a substring check,
a directory entry like "content/about" matches "content/about.md",
causing unrelated files to be pulled into the mini-build. Whether the
conflict was then detected depended on whether the filesystem walker
delivered a directory entry or a full file path.

Also adds an upfront check for the directory-conflict case, since
a corrected filter alone would allow about.md to be created alongside
an existing about/ directory.

Closes #12602
Closes #12786
Closes #14112
Closes #14769

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 15:06:28 +02:00
dependabot[bot] 547ab29ced build(deps): bump golang.org/x/tools from 0.43.0 to 0.44.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.43.0 to 0.44.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.43.0...v0.44.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 15:03:47 +02:00
Joe Mooring 0d58e4286f resources/page: Add passing test for Issue #14325 2026-04-18 15:02:47 +02:00
Bjørn Erik Pedersen bbb42b5a6a agents: Add a note about having the issue ID in test names 2026-04-17 21:59:11 +02:00
Joe Mooring 1eea9fba0b commands: Fix environment isolation for configuration settings
Closes #14763

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:40:23 +02:00
dependabot[bot] 9a5c7e0d24 build(deps): bump github.com/evanw/esbuild from 0.27.4 to 0.28.0
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.27.4 to 0.28.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.27.4...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 19:56:49 +02:00
dependabot[bot] 6613b08eb6 build(deps): bump github.com/aws/aws-sdk-go-v2 from 1.41.1 to 1.41.5
Bumps [github.com/aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) from 1.41.1 to 1.41.5.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/v1.41.1...v1.41.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 19:55:55 +02:00
dependabot[bot] 582c26ef42 build(deps): bump github.com/pelletier/go-toml/v2 from 2.2.4 to 2.3.0
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.4 to 2.3.0.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.4...v2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 19:55:18 +02:00
dependabot[bot] a4f2a8a579 build(deps): bump github.com/tdewolff/minify/v2 from 2.24.11 to 2.24.12
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.11 to 2.24.12.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.11...v2.24.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 13:57:04 +02:00
Bjørn Erik Pedersen 8d6145f3c3 Fix filename dimension identifiers (_role_X_, _version_X_) to replace mount config
Filename identifiers for roles and versions were parsed but never applied
to the SitesMatrix. Now they replace the mount's configuration for that
dimension, matching how language identifiers already worked.

Fixes #14756

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:34:34 +02:00
Bjørn Erik Pedersen ce2a156a4e Add a more flexible filename identifier scheme that also allows setting roles and versions (#14754)
Fixes #14750
2026-04-15 16:44:19 +02:00
Bjørn Erik Pedersen 9747724222 Fix it so we never auto-fallback to page resources in other roles/versions
This is some logic that's left behind from when we had only one dimension (language) where the common case would be to have one resource set (e.g. an image) and many content translation.

After this commit:

* For sites matrix defined in the content filename (e.g. data.en.js) or in its mount definition, we may use that as a fallback for e.g. German languages if we don't find a better match.
* For content adapters, this is not relevant: Here you must be explicit about this.
* We never auto-fallback on resources from a role/version to another.
* When a page bundle spans multiple roles (e.g. via roles = "*"), we clone its resources to all roles so each gets role-specific paths.

Fixes #14749
Fixes #14752
2026-04-14 16:33:38 +02:00
Joe Mooring a17bdbc5fa common/hugo: Deprecate extended and extended_withdeploy editions
Closes #14696
2026-04-13 23:18:23 +02:00
Bjørn Erik Pedersen 8f94d65cac parser/pageparser: Add a parser fuzz test
Ran it for 40 minutes on my MacBook Pro, and it found no issues.
2026-04-09 12:25:19 +02:00
hugoreleaser d6bc8165e6 releaser: Bump versions for release of 0.160.1
[ci skip]
2026-04-08 14:02:42 +00:00
Bjørn Erik Pedersen 8b00030b34 Fix panic when passthrough elements are used in headings
Fixes #14677

Co-Authored-By: xingzihai <1315258019@qq.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:03:05 +02:00
Bjørn Erik Pedersen c48551677c Fix panic on edit of legacy mapped template names that's also a valid path in the new setup
This mapping was added in Hugo `v0.146.0`.

Fixes #14740
2026-04-08 13:19:45 +02:00
Bjørn Erik Pedersen 161d0d4757 Fix RenderShortcodes leaking context markers when indented
Strip leading whitespace from Hugo context marker lines before
Goldmark parsing to prevent them from being treated as indented
code blocks.

Fixes #12457

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:05:42 +02:00
Bjørn Erik Pedersen 45e4596630 Strip nested page context markers from standalone RenderShortcodes
Fixes #14732

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 19:27:40 +02:00
Bjørn Erik Pedersen 58927aa14a Rename deprecated cascade._target to cascade.target in tests 2026-04-07 13:58:06 +02:00
Bjørn Erik Pedersen ce009e3aa9 Fix auto-creation of root sections in multilingual sites
Fixes #14681

Co-authored-by: Joe Mooring <joe@mooring.com>
2026-04-07 13:58:06 +02:00
Christopher Hicks 0755872424 readme: Fix links
* docs: fix broken links in top README.md

* point at the docs site instead of github

* fix typo

Co-authored-by: Joe Mooring <joe@mooring.com>

---------

Co-authored-by: Joe Mooring <joe@mooring.com>
2026-04-06 08:48:45 -07:00
hugoreleaser 6b5554bac9 releaser: Prepare repository for 0.161.0-DEV
[ci skip]
2026-04-04 13:53:16 +00:00
hugoreleaser 652fc5acdd releaser: Bump versions for release of 0.160.0
[ci skip]
2026-04-04 13:32:34 +00:00
dependabot[bot] bf6e35a755 build(deps): bump github.com/magefile/mage from 1.16.1 to 1.17.1
Bumps [github.com/magefile/mage](https://github.com/magefile/mage) from 1.16.1 to 1.17.1.
- [Release notes](https://github.com/magefile/mage/releases)
- [Commits](https://github.com/magefile/mage/compare/v1.16.1...v1.17.1)

---
updated-dependencies:
- dependency-name: github.com/magefile/mage
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-04 14:56:33 +02:00
Bjørn Erik Pedersen 4e91e14cb0 Fix some recently introduced Position issues
Fixes #14710
2026-04-04 14:53:20 +02:00
Bjørn Erik Pedersen dc9b51d2e2 markup/goldmark: Fix double-escaping of ampersands in link URLs
The XSS fix in 479fe6c65 accidentally called util.EscapeHTML twice
on link destinations, causing `&` to render as `&amp;amp;` instead
of `&amp;`.

Fixes #14715

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:52:29 +02:00
Bjørn Erik Pedersen 481baa0896 all: Replace NewIntegrationTestBuilder with Test/TestE/TestRunning 2026-04-03 21:17:45 +02:00
Bjørn Erik Pedersen 43aad7118d tpl: Fix stray quotes from partial decorator in script context
Use template.JS for the falsy return value of _PopPartialDecorator
so Go's html/template JS escaper doesn't wrap the empty string in
quotes inside <script> tags.

Fixes #14711

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:30:26 +02:00
Joe Mooring 9f1f1be0be readme: Revise edition descriptions and installation instructions 2026-04-03 11:54:31 +02:00
dependabot[bot] 0eda24e65f build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.3 to 4.1.4.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 11:32:24 +02:00
Bjørn Erik Pedersen 5d09b5e32a tpl/css: Support @import "hugo:vars" for CSS custom properties in css.Build
Closes #14699

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:02:13 +02:00
Bjørn Erik Pedersen 303e443ea7 Improve and extend .Position handling in Goldmark render hooks
Goldmark v1.8 reports source positions in render hooks.
Use this to build a source map that translates Goldmark positions back to
the original content source.

Now all render hooks' context object implenent the `BaseContext` interface:

```go
type BaseContext interface {
	Position() Position
	Page() any
	PageInner() any
	Ordinal() int
}
```

Closes #14663

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:30:28 +02:00
dependabot[bot] beb57a6839 build(deps): bump golang.org/x/image from 0.37.0 to 0.38.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/image/compare/v0.37.0...v0.38.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 19:16:47 +02:00
Bjørn Erik Pedersen 638262cec3 markup/goldmark: Clean up test 2026-04-01 17:05:08 +02:00
hugoreleaser 397980aab3 releaser: Prepare repository for 0.160.0-DEV
[ci skip]
2026-04-01 13:21:05 +00:00
hugoreleaser 5f4646acaa releaser: Bump versions for release of 0.159.2
[ci skip]
2026-04-01 12:59:20 +00:00
Bjørn Erik Pedersen 479fe6c654 Fix potential content XSS by escaping dangerous URLs in links and images
* This issue in question was fixed upstream in https://github.com/yuin/goldmark/releases/tag/v1.8.2
* We do, however, have a copy of the affected functions, used as fallbacks when no render hook are defined for e.g. links and images, so we need to port these fixes to our copy of the affected functions.
2026-04-01 14:23:24 +02:00
Bjørn Erik Pedersen 81a5cdca07 releaser: Add standard withdeploy release assets
Closes #14687

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:01:53 +02:00
Joe Mooring df520e3150 resources/page: Fix shared reader in Source.ValueAsOpenReadSeekCloser
Closes #14684

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:32:28 +01:00
Bjørn Erik Pedersen b55d452e46 testing: Simplify line ending handling in tests 2026-03-28 19:48:28 +01:00
Joe Mooring ea7eac6558 readme: Update Go version to 1.25.0 2026-03-27 08:43:24 -07:00
hugoreleaser 458ebdd448 releaser: Prepare repository for 0.160.0-DEV
[ci skip]
2026-03-26 10:13:11 +00:00
hugoreleaser 86c7d3afac releaser: Bump versions for release of 0.159.1
[ci skip]
2026-03-26 09:54:15 +00:00
Bjørn Erik Pedersen 42289d76f9 minifiers: Keep x-bind and blank namespace in SVG minification
Update tdewolff/minify to v2.24.11 and add "" and "x-bind" to KeepNamespaces
to prevent Alpine.js directives from being stripped in SVGs.

This is a new option in tdewolff/minify v2.24.11, and it is needed to prevent breakage of Alpine.js, and possilby others, directives in SVGs.

Fixes #14669
2026-03-26 10:49:53 +01:00
Bjørn Erik Pedersen 0c013c2326 Adjust depreceated syntax in tests 2026-03-24 11:07:01 +01:00
hugoreleaser bd071d78a5 releaser: Prepare repository for 0.160.0-DEV
[ci skip]
2026-03-23 18:34:59 +00:00
hugoreleaser 2ed7d193cf releaser: Bump versions for release of 0.159.0
[ci skip]
2026-03-23 18:16:59 +00:00
Bjørn Erik Pedersen d2043cfae5 docs: Update docs.yaml 2026-03-23 18:27:14 +01:00
Bjørn Erik Pedersen 5997b71db6 Merge commit 'e7afabb927a79b179ae57013fd5f49e32829671e' 2026-03-23 18:25:55 +01:00
Bjørn Erik Pedersen e7afabb927 Squashed 'docs/' changes from 80dd7b067..0755fb534
0755fb534 misc: Update docs.yaml
f0c69a38c content: Remove outdated content
43ce895b9 content: Remove outdated new-in badges
c3a382fb7 content: Improve GitLab Pages workflow example
f2a851966 content: Update Vimeo example
044565ab3 content: Fix typo
9f5e8b5f4 content: Update version references
9dc30f225 theme: Address deprecations in v0.158.0
ea0787a6b content: Document css.Build function
f98e4f55c content: Standardize Node.js references
bf683e36f content: Use consistent file system terminology
54ed4c12b content: Document strings.ReplacePairs
779d37978 all: Miscellaneous updates for v0.158.0
be1a20c27 Update HUGO_VERSION to 0.158.0
24cdd7bf7 Fix typos in module configuration documentation
ce37e7b05 content: Address Markdown linting error
592049f02 content: Remove unnecessary workflow customization section
b17b18ce1 content: Update version references in hosting guides
32ec89596 content: Improve urls.PathEscape/PathUnescape examples
9297bb6fa content: Clarify page collection sort order
60192a16f content: Add reference link to glossary term
63d4a992a content: Update documentation contribution guide
946273d67 theme: Adjust feature state notices
d70dfd651 theme: Simplify border classes per Tailwind linter
fc53c7263 theme: Refactor feature status shortcodes
850dc5b2b content: Clean up content for deprecated features
35da1aa3c content: Miscellaneous udpates for v0.157.0
05b69f84e content: More site-to-project changes
291ffc3f6 content: Add blogger2hugo migration tool
53a1bdee3 Update HUGO_VERSION to 0.157.0
808f0431d Merge branch 'tempv0.157.0'
f02222e46 docs: Regen and fix the imaging docshelper output
b0d3364f1 Merge commit '0c2fa2460f485e0eca564dcccf36d34538374922'
d1f37cc6d resources/images: Adjust WebP processing defaults
9a9f02fb8 Add per-request timeout option to `resources.GetRemote`
900c10201 docs: Fix lineNos default value in docs.yaml
0052c975a docs: Regenerate docs.yaml
3e2f98235 Merge commit '8f3c066d23f431fb2c53d97ea489e4c28b42bd82'
dbfd34a1a docs: Update docs.yaml
7e1a08e54 Merge commit '08e1ea5c709d3d49bdc3ce3c21e8fa05a33150d0'
cb6e0a42a tpl: Add missing functions to init files
05544e400 markup/asciidocext: Improve Asciidoctor integration
820b5536a github: Add ai-watchdog workflow and update other workflows' versions
3848aec5f markup/goldmark: Enhance footnote extension with backlinkHTML option
070074dac markup/goldmark: Enhance footnote extension with auto-prefixing option
f92f7ac5c config/security: Add PROGRAMDATA to the osenv allowlist
d1ab84530 minifiers: Update deprecation handling
6c911d1e5 Merge commit 'bfa74537929f409fca841540b971125b7678963a'
61916ab4c resources/page: Add :sectionslug and :sectionslugs permalink tokens
80667667c Add Ancestors (plural) method to GitInfo, rename Ancestor field to Parent
e8fdc1d4d source: Expose Ancestor in GitInfo
079671b41 Merge commit 'bb147f91ee9078e6a55e8c32ab4b2e5dbc5cee45'
9e8a08520 images: Add option for vertical alignment to images.Text
f7c795479 Merge commit 'b3d87dd0fd746f07f9afa6e6a2969aea41da6a38'
283e97783 Merge commit '5be51ac3db225d5df501ed1fa1499c41d97dbf65'
4c6ec5c19 resources/page: Revise the new contentbasename permalinks tokens
fc69d3981 resources/page: Add :contentbasename and :contentbasenameorslug permalink tokens
c5f654716 modules: Add GOAUTH to module config
2a1a832fb js/esbuild: Add drop option
d9fb55352 Merge commit 'a024bc7d76fcc5e49e8210f9b0896db9ef21861a'
84678f5af helpers: Add Chroma styles to docs.yaml
fc8d9e344 Merge commit '346b60358dd8ec2ca228e6635bff9d7914b398b7'
4e4da5e47 Merge commit '81a7b6390036138356773c87a886679c81c524e1'
5df38fde0 docs: Regen CLI docs
3b0f8034b tpl/images: Change signature of images.QR to images.QR TEXT OPTIONS
68187036f images.Text: Add "alignx" option for horizontal alignment
240a85734 docs: Regen CLI docs
21b2917c6 Merge commit 'e9fbadacc3f09191e2e19f112a49777eeb8df06c'
5cc7ad562 tpl/images: Add images.QR function
9b588ee9e Update gocloud and docs for S3-Compatible Endpoints
b24377040 tpl/tplimpl: Update details shortcode
0535f4b22 tpl/tplimpl: Add details shortcode
2dd21cf90 Merge commit 'e477373487abcccdbed95688e37aa74b9b8fc198'
869321ffa dartsass: Add silenceDeprecations option
492981933 Merge commit '838bd312b1a287bb33962ad478dbc54737654f35'
c79b7d840 docs: Regen CLI docs
101410f08 docs: Regenerate CLI docs
700846bc9 Merge commit 'de0df119b504a91c9e1f442b07954f366ffb2932'
1fb46bb7c docs: Regen CLI docs
923fd9044 commands: Add "hugo build" as an alias for "hugo"
244ea3067 Merge commit '39fd3b557014e339bc6c68a7ff34a7734a735ee0'
f2e8913d2 Add support for Obsidian type blockquote alerts
9777cfa83 Merge commit 'dec8cd4ada29218971743333f8ac662a9c06aad8'
b3f6d546c output: Fix docshelper template lookup order for AMP pages
476507712 Add config options page.nextPrevSortOrder/nextPrevInSectionSortOrder
b2507b8ab tpl/transform: Make Plainify and ToMath return template.HTML
154525780 docs: Regen docshelper
9d9a0848c Merge commit 'a6e635ca7d905d9ec3ffd708db2694f680b03aae'
b56a8baa1 math: Add trigonometric functions and some angle helper functions
59f708c0e source: Expose GitInfo Body
7a330c378 Merge commit '8b9803425e63e1b1801f8d5d676e96368d706722'
1ea10926d deploy: Add stripIndexHtml target option
903da161a markup/goldmark: Add the Hugo Goldmark Extras "delete" extension
63cf1ea35 deps: Upgrade github.com/alecthomas/chroma v2.13.0 => v2.14.0
6dcbebe11 config: Remove extraneous BuildConfig setting
1c89b76c2 docs: Regen docshelper
976b30877 markup/goldmark: Support extras extension
0da4eee49 commands: Add gen chromastyles --lineNumbersTableStyle flag
daf545b56 docs: Regen docshelper
a46a8ac97 all: Fix duplicate words in comments
9a3c76650 tpl/tplimpl: Optionally exclude content from sitemap
737b3b210 tpl/tplimpl: Update Google Analytics template and config
9283d3071 docs: Regen CLI docs
fa0437be3 docs: Regen docshelper
c318cd850 docs: Fix hyphens and grammar in synopsis of command 'hugo server'
5896f23a9 js: Support JSX and JSXImportSourceOptions
08b0bca77 Merge commit '2658a71e1b6fe24a8b754a62ce0398a09d270d86'
c4c5be6c2 docs: Regen docshelper
ec07d66fe Add images.Dither filter
3b23f2b89 docs: Regen CLI docs
d1c5d6a5e docs: Regenerate docshelper
6c8c9da77 Merge commit '6efb279bfacbd7304cef994be8181c6f804e7dd4'
5ce83ee91 docs: Make null booleans falsy in the docs helper
1185cfc71 docs: Regen docs helper
aab11aad7 Merge commit '9b0050e9aabe4be65c78ccf292a348f309d50ccd' as 'docs'

git-subtree-dir: docs
git-subtree-split: 0755fb534d8b4cde8a0227698da634e3536283ff
2026-03-23 18:21:25 +01:00
Bjørn Erik Pedersen 4f3c39890e commands: Update docs linke to Node.js docs 2026-03-23 18:13:50 +01:00
eason 807cae1df1 create: Return error instead of panic when page not found
When `hugo new` creates a content file on a case-insensitive filesystem
where a file with the same name but different case already exists (e.g.
Floop.md vs floop.md), the internal page lookup fails because it uses
exact string comparison. This previously caused an unhelpful panic.

Replace the panic with a descriptive error message that tells the user
what went wrong and suggests checking for case-variant filenames.

Fixes #14112

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
2026-03-23 17:50:15 +01:00
Alexandre Vaz c4fb61d9df commands: Preserve non-content files in convert output
When running `hugo convert` with `--output`, copy the content tree first so non-content bundle resources are kept in the destination, then overwrite converted content files.

Also avoid recursive self-copy when the output path points inside the content tree by skipping output directories during copy.

Fixes #4621
2026-03-23 17:49:13 +01:00
Bjørn Erik Pedersen d88a29e002 npm: Use workspaces to simplify hugo mod npm pack
Rewrite `hugo mod npm pack` to use npm workspaces. Module deps are now
written to packages/hugoautogen/package.json and the root package.json
gets a "workspaces" reference. A hugo_packagemeta.json sidecar stores a
hash of all input package files so regular commands can warn when npm
deps are out of sync.

Other changes:
- Workspace glob patterns (*, **, {a,b}) are resolved via gobwas/glob.
- Workspaces defined in package.hugo.json are supported.
- package.hugo.json is only recognised at module roots, not in workspaces.
- When package.hugo.json exists, package.json is not mounted or vendored.
- packages/hugoautogen is not mounted or vendored from dependencies.
- Add usePackageJSON import option (auto/always/never) to control whether
  a module's npm deps are included. "auto" checks for Hugo config files
  or package.hugo.json.
- The staleness check is skipped when running `hugo mod npm pack` itself.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 16:00:50 +01:00
dependabot[bot] 3ff9b7f89f build(deps): bump github.com/olekukonko/tablewriter from 1.1.3 to 1.1.4 (#14641)
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.1.3...v1.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 21:19:50 +01:00
Tay 9dd9c76027 commands: Close cpu profile file when StartCPUProfile fails 2026-03-21 21:17:12 +01:00
Bjørn Erik Pedersen a8fca598e6 Replace deprecated site.Data with hugo.Data in tests
This also fixes a subtle server reload issue when changing files inside /data and using the recently added hugo.Data method.
2026-03-21 19:09:14 +01:00
Bjørn Erik Pedersen 182b104571 Replace deprecated excludeFiles and includeFiles with files in tests 2026-03-21 16:59:13 +01:00
dependabot[bot] be93ccd304 build(deps): bump github.com/yuin/goldmark from 1.7.16 to 1.7.17
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.16 to 1.7.17.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.16...v1.7.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 11:37:30 +01:00
dependabot[bot] 2669bca6fd build(deps): bump github.com/magefile/mage from 1.15.0 to 1.16.1
Bumps [github.com/magefile/mage](https://github.com/magefile/mage) from 1.15.0 to 1.16.1.
- [Release notes](https://github.com/magefile/mage/releases)
- [Commits](https://github.com/magefile/mage/compare/v1.15.0...v1.16.1)

---
updated-dependencies:
- dependency-name: github.com/magefile/mage
  dependency-version: 1.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 20:39:12 +01:00
dependabot[bot] 753d447f11 build(deps): bump golang.org/x/image from 0.36.0 to 0.37.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/image/compare/v0.36.0...v0.37.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 20:38:51 +01:00
dependabot[bot] 4f39d724fe build(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.3
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.78.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-19 20:21:44 +01:00
Joe Mooring eaf4c7515a tpl/tplimpl: Fix Vimeo shortcode test
Closes #14649
2026-03-19 20:21:21 +01:00
Bjørn Erik Pedersen 3315a86d67 Remove the AI Watchdog workflow for now
It's failing with a 404 that I don't know how to fix (or don't have the time to investigate). It works fine when run locally.
2026-03-19 17:28:20 +01:00
Bjørn Erik Pedersen 38244842e0 Remove 'bep' from PR user logins skip list
To test it and see why it fails.
2026-03-19 17:02:16 +01:00
Bjørn Erik Pedersen 7813c5c89d tpl/tplimpl: Comment out the Vimeo simple shortcode tests
See #14649
2026-03-19 13:52:36 +01:00
Bjørn Erik Pedersen eb11c3d0bc Replace deprecated :filename with :contentbasename in the permalinks test 2026-03-18 08:47:26 +01:00
hugoreleaser fa8658111f releaser: Prepare repository for 0.159.0-DEV
[ci skip]
2026-03-16 17:56:18 +00:00
hugoreleaser f41be7959a releaser: Bump versions for release of 0.158.0
[ci skip]
2026-03-16 17:42:04 +00:00
Bjørn Erik Pedersen 0e46a97e8a deps: Upgrade github.com/evanw/esbuild v0.27.3 => v0.27.4 2026-03-16 18:39:08 +01:00
dependabot[bot] c27d9e8fcf build(deps): bump github.com/getkin/kin-openapi from 0.133.0 to 0.134.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.133.0 to 0.134.0.
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](https://github.com/getkin/kin-openapi/compare/v0.133.0...v0.134.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 11:59:42 +01:00
dependabot[bot] 098eac59a9 build(deps): bump golang.org/x/tools from 0.42.0 to 0.43.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.42.0 to 0.43.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.42.0...v0.43.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 11:56:42 +01:00
Bjørn Erik Pedersen 3c980c072e resources: Re-publish on transformation cache hit
When a resource transformation result was served from cache (same
options as a previous build), the output file was not re-written to
disk. This caused incorrect output when toggling transformation
options (e.g. minify) back to a previously seen value in server mode.

Fixes #14629
2026-03-15 18:32:41 +01:00
Joe Mooring 404ac00001 create/skeletons: Use css.Build in theme skeleton
Closes #14626
2026-03-15 16:51:24 +01:00
Bjørn Erik Pedersen 06fcb72421 tpl/css: Add a test case for rebuilds on CSS options changes 2026-03-15 11:09:02 +01:00
Joe Mooring 9b5f1d491d hugolib: Allow regular pages to cascade to self
Closes #14627
2026-03-15 10:17:45 +01:00
dependabot[bot] 87f8de8c7a build(deps): bump gocloud.dev from 0.44.0 to 0.45.0
Bumps [gocloud.dev](https://github.com/google/go-cloud) from 0.44.0 to 0.45.0.
- [Release notes](https://github.com/google/go-cloud/releases)
- [Commits](https://github.com/google/go-cloud/compare/v0.44.0...v0.45.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 16:06:09 +01:00
dependabot[bot] 67ef6c68de build(deps): bump golang.org/x/sync from 0.19.0 to 0.20.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/sync/compare/v0.19.0...v0.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 16:05:19 +01:00
Bjørn Erik Pedersen 623722bb07 tpl/css: Allow the user to override single loader entries
Fixes #14623

 Co-authored-by: Joe Mooring <joe.mooring@veriphor.com>
2026-03-14 15:17:21 +01:00
Bjørn Erik Pedersen e431f90bc6 tpl/css: Fix external source maps
Fixes #14620
2026-03-14 12:08:18 +01:00
Bjørn Erik Pedersen a7cbcf15f0 tpl/css: Make default loader resolution for CSS @import and url() always behave the same
Before this commit, we did dynamic loader resolution for CSS bundling for resources resolved by Hugo while any fallback to ESBuild would fall back to a (potentially) empty loaders config.

This revises the logic to always use a static list (see below) if `loaders` is not set. This should be easier do document and less confusing for the end user.

````
".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".avif",
".woff", ".woff2", ".ttf", ".eot", ".otf"
````

Fixes #14619
2026-03-13 20:24:32 +01:00
Joe Mooring 59e0446fe5 hugolib: Fix server no watch
Closes #14615

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 16:28:37 +01:00
Joe Mooring 36cdb2c789 internal/js: Add default mainFields for CSS builds
Closes #14614
2026-03-13 08:33:17 +01:00
Bjørn Erik Pedersen 3e3b849cc7 Add css.Build
Fixes #14609
Fixes #14613
2026-03-12 13:32:40 +01:00
Bjørn Erik Pedersen 1f578f160c Upgrade to to Go 1.26.1 (#14597) (note)
This fixes a security issue: https://www.cve.org/CVERecord?id=CVE-2026-27142

Closes #14595
2026-03-11 12:36:13 +01:00
Bjørn Erik Pedersen 842d8f1052 resources: Fix context canceled on GetRemote with per-request timeout
The per-request timeout context was cancelled via defer in the getRes
closure, before io.ReadAll(res.Body) in the outer scope could read
the response body. Fix this by returning the cancel function from
getRes so each caller manages its own cancel lifecycle.

Fixes #14611

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:45:14 +01:00
Bjørn Erik Pedersen c47ec23342 resources: Use full path for Exif etc. decoding error/warning messages
Fixes #12693
2026-03-08 23:03:57 +01:00
Bjørn Erik Pedersen 4652ae4a44 Move to new locales library and upgrade CLDR from v36.1 to v48.1
* I had all kinds of issues upgrading CLDR on the old library, so I decided to start fresh in github.com/bep/golocales
* It shaves off about 3 MB of the binary, and package init cost is zero compared to the old localescompressed (5000x faster).
* We will probably move to the `text/**` packages at this point, but this will have to do for now.
2026-03-08 21:43:37 +01:00
Bjørn Erik Pedersen 4eafd9eb7b tpl/tplimpl: Prefer early suffixes when media type matches
Fixes #13877
Close #14601

Co-Authored-By: Joe Mooring <joe.mooring@veriphor.com>
2026-03-08 21:42:36 +01:00
Joe Mooring 13a95b9c01 tpl/strings: Add strings.ReplacePairs function
Closes #14594
2026-03-07 20:21:02 +01:00
Bjørn Erik Pedersen e3108225bf all: Run go fix ./... 2026-03-07 18:30:42 +01:00
Bjørn Erik Pedersen c9b88e4d15 internal/warpc: Fix SIGSEGV in Close() when dispatcher fails to start
Only set started=true when Start() succeeds, so Close() doesn't
attempt to close a nil dispatcher.

Fixes #14536
2026-03-07 15:30:07 +01:00
Bjørn Erik Pedersen 54c804876a github: Remove pull_request_template.md
* Main reason is that it slows down my own `gh` workflow (I also frequently accidentally select that template).
* We also don't have a large volume of PRs from new contributors, so the template isn't as necessary.
2026-03-07 12:48:15 +01:00
Bjørn Erik Pedersen 157bfddda5 testing: Make commands tests pass in Go 1.26.1 2026-03-06 12:23:25 +01:00
Joe Mooring d4f2122dea refactor: Deprecate language configuration and template methods
Configuration:

- languageCode      -> locale (either in root or per-language)
- languageName      -> label
- languageDirection -> direction

Methods:

- .Language.LanguageCode      -> .Language.Locale
- .Language.LanguageName      -> .Language.Label
- .Language.LanguageDirection -> .Language.Direction
- .Site.LanguageCode          -> .Site.Language.Locale

Example configuration:

[languages.en]
  direction = 'ltr'
  label     = 'English'
  locale    = 'en-US'
  weight    = 1

Closes #14269

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 11:45:52 +01:00
Bjørn Erik Pedersen 991d2f9aba Replace Exif with Meta in tests 2026-03-04 11:38:28 +01:00
dependabot[bot] b29c2f7ade build(deps): bump golang.org/x/net from 0.50.0 to 0.51.0 (#14569)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.51.0.
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.51.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 13:01:37 +01:00
dependabot[bot] e91d191155 build(deps): bump github.com/tdewolff/minify/v2 from 2.24.9 to 2.24.10 (#14585)
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.9 to 2.24.10.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.9...v2.24.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 12:57:29 +01:00
dependabot[bot] a8a54bd020 build(deps): bump github.com/bep/imagemeta from 0.15.0 to 0.17.0 (#14584)
Bumps [github.com/bep/imagemeta](https://github.com/bep/imagemeta) from 0.15.0 to 0.17.0.
- [Release notes](https://github.com/bep/imagemeta/releases)
- [Commits](https://github.com/bep/imagemeta/compare/v0.15.0...v0.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 12:57:10 +01:00
Bjørn Erik Pedersen f797f84902 Fix index out of range panic in fileEventsContentPaths
The nested loop had dirs as the outer loop and others as the inner loop
with a single counter, causing n to exceed len(others) when multiple
dirs existed. Swap the loop order so each file in others is checked
against all dirs exactly once.

Fixes #14573

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 12:05:13 +01:00
Joe Mooring e8577771c3 resources: Improve getImageOps error message
Closes #14571
2026-02-26 22:20:08 +01:00
Bjørn Erik Pedersen 44dc3847f4 resources/images: Add IsImageResourceWithMeta etc. tests for bmp and gif
Closes #14568
2026-02-26 16:49:11 +01:00
hugoreleaser 5594b28229 releaser: Prepare repository for 0.158.0-DEV
[ci skip]
2026-02-25 16:57:59 +00:00
hugoreleaser 7747abbb31 releaser: Bump versions for release of 0.157.0
[ci skip]
2026-02-25 16:38:33 +00:00
Bjørn Erik Pedersen 3dff7c8c7a Fix menu pageRef resolution in multidimensional setups
Fixes #14566
2026-02-25 17:35:20 +01:00
Bjørn Erik Pedersen d98cd4aecf Handle GitInfo for modules where Origin is not set when running go list
This is an upstream bug, but this workaround seem to work fine.

Fixes #14564
2026-02-25 14:26:50 +01:00
Joe Mooring 68059972e8 commands: Update link to highlighting style examples
Closes #14556
2026-02-25 12:57:25 +01:00
Bjørn Erik Pedersen 8e28668b09 docs: Regen and fix the imaging docshelper output
Fixes #14562
2026-02-25 10:43:58 +01:00
Bjørn Erik Pedersen a3ea9cd18f Merge commit '0c2fa2460f485e0eca564dcccf36d34538374922' 2026-02-25 10:29:05 +01:00
Bjørn Erik Pedersen 0c2fa2460f Squashed 'docs/' changes from 42914c50e..80dd7b067
80dd7b067 theme: Fix border class in render-codeblock template
0d3fde6c9 content: Fix typo
23a4adb29 content: More site => project changes
9243e9f6b content: Rename site configuration to project configuration (phase 2)
330aa2249 content: Miscellaneous edits
25ce893be content: Clarify sort order with PAGE.Rotate
b398506dc content: Various dimension improvements
c54573d13 content: Use singular form in method section descriptions
d9a95fe54 content: Improve description of PAGE.Rotate
154f30600 content: Fix link
c71bd2776 content: Address Markdown linting error
72c88e68e content: Improve pages related to content dimensions
b85afa645 content: Fix formatting
7aaca8947 content: More site-to-project changes
8e1579030 content: Update quick start guide
f7ba1fde2 content: Make new-in into a note
8b9d51584 theme: Address TailwindCSS warnings
3a4c2a25c theme: Address Hugo v0.156.0 deprecations
4f1a2bd7b content: Improve glossary entries
21bcd23a6 content: Update glossary entry
c72ef1116 content: Update quick start guide
c0737ffab content: Add typography plugin to TailwindCSS example
80bcb868e content: Remove Site.AllPages from examples
2b9ba5831 content: Replace Page.Sites and Site.Sites with hugo.Sites
2cdcc2556 content: Remove outdated badges
5a4beb530 content: Replace Site.Data with hugo.Data
de4651b5d content: Update glossary entry
2302825a7 content: Add hugo.Data and note other deprecations
bac5da4b5 content: Add hugo.Sites and update the other Sites methods
0f7d99153 content: Add Site.IsDefault
f888f33bb content: Update glossary entries
65311c2de content: Include the "build" subcommand where appropriate
4991320d2 content: Standard shell language code in info strings
b16d9f162 theme: Style todo lists
f121450c6 content: Update version references
5a81dd8c2 content: Update CLI documentation
f3ae0ce86 content: Rename site configuration to project configuration (phase 1)
9c8327119 content: Miscellaneous edits
fbff91e22 Update HUGO_VERSION to 0.156.0
2ec625ae8 content: Restore snap installation instructions
fc8c246b8 content: Clarify module initialization

git-subtree-dir: docs
git-subtree-split: 80dd7b067c31c28b13c51f3ac4636890509a4bb7
2026-02-24 21:38:42 +01:00
Bjørn Erik Pedersen 49bfb1070b Add AVIF, HEIF and HEIC partial support (only metadata for now)
* Add AVIF, HEIF and HEIC partial support

* Add them as media types.
* Support reading metadata (Width, Height, Exif, etc.) from these formats.
* Add a new template function IsImageResourceMeta to check if a resource supports image metadata operations, which will return true for AVIF, HEIF and HEIC resources even if they don't support full image operations yet.

Fixes #14549
2026-02-24 19:45:12 +01:00
Joe Mooring b7203bbb3a resources/images: Adjust WebP processing defaults
defaultWebpUseSharpYuv: true => false
defaultWebpMethod: 4 => 2
2026-02-24 10:02:48 +01:00
Bjørn Erik Pedersen dfece5b674 Add Page.GitInfo support for content from Git modules
Use blobless git clone (--filter=blob:none --no-checkout) to fetch
commit history for content from Git modules, then map it using the
existing gitmap library.  Cloned repos are cached in the modulegitinfo file cache.

Closes #14431
Fixes #5533
2026-02-23 17:43:17 +01:00
Paul van Brouwershaven 2d691c7e47 Add per-request timeout option to resources.GetRemote 2026-02-23 16:38:13 +01:00
Bjørn Erik Pedersen b96d58a144 Update AI Watchdog action version in workflow 2026-02-23 15:05:07 +01:00
dependabot[bot] 9869e71aaa build(deps): bump github.com/tdewolff/minify/v2 from 2.24.8 to 2.24.9
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.24.8 to 2.24.9.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.24.8...v2.24.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 12:23:59 +01:00
dependabot[bot] 8f47fe8c87 build(deps): bump github.com/bep/imagemeta from 0.14.0 to 0.15.0
Bumps [github.com/bep/imagemeta](https://github.com/bep/imagemeta) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/bep/imagemeta/releases)
- [Commits](https://github.com/bep/imagemeta/compare/v0.14.0...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 12:23:01 +01:00
Bjørn Erik Pedersen 65b4287c68 config: Skip taxonomy entries with empty keys or values
When non-taxonomy keys (e.g. disableKinds = []) are placed after
[taxonomies] in TOML, they become part of the taxonomies table.
An empty-valued entry creates a phantom taxonomy with an empty
pluralTreeKey, causing .Ancestors to loop indefinitely.

Fixes #14550

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 12:22:39 +01:00
Bjørn Erik Pedersen cc338a9d48 Add guideline for brevity in code and comments
Added a guideline emphasizing brevity in code, comments, and commit messages.
2026-02-21 18:53:41 +01:00
Bjørn Erik Pedersen 3850881f2a modules: Include JSON error info from go mod download in error messages
Fixes #14543
2026-02-21 17:21:08 +01:00
Bjørn Erik Pedersen a18bec1123 hugolib: Fix automatic section pages not replaced by sites.complements
When a home page or section page in language A is backed by a file and
language B has no content file for that section, the automatic page for
language B was not replaced by the complement from language A. This
happened because:

1. The content node shifter returned auto pages (not backed by files) as
   exact matches without trying complement fallback.
2. findContentNodeForSiteVector immediately returned auto pages as exact
   matches without considering file-backed complements.
3. getPagesInSection used Get (no fallback) for IncludeSelf, preventing
   complement resolution for home/section pages.

Fix by preferring file-backed complement pages over auto pages in the
shift and lookup mechanisms, and using fallback for self-inclusion.

Fixes #14540

Co-Authored-By: Joe Mooring <joe.mooring@veriphor.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:23:20 +01:00
hugoreleaser 3c9b41f9ec releaser: Prepare repository for 0.157.0-DEV
[ci skip]
2026-02-18 16:58:30 +00:00
hugoreleaser 9d914726de releaser: Bump versions for release of 0.156.0
[ci skip]
2026-02-18 16:39:55 +00:00
Bjørn Erik Pedersen 86aa62524f hugolib: Move site.Data to hugo.Data, deprecate Site.AllPages/BuildDrafts/Languages
This moves Site.Data to the hugo namespace as hugo.Data, following the
same pattern used for hugo.Sites. Also deprecate Site.AllPages,
Site.BuildDrafts, and Site.Languages.

Closes #14521

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 14:34:47 +01:00
dependabot[bot] d8ec0eeeaf build(deps): bump google.golang.org/api from 0.255.0 to 0.267.0
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.255.0 to 0.267.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.255.0...v0.267.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-18 10:54:40 +01:00
Joe Mooring 4148eded9c hugolib: Add Page.Sites to Site.Sites deprecation notice
Closes #14532
2026-02-17 20:33:18 +01:00
Bjørn Erik Pedersen bba2aed352 hugolib: Simplify sites collection 2026-02-17 19:10:47 +01:00
Bjørn Erik Pedersen 29b8e17d29 hugolib: Adjust hugo.Sites.Default
Fixes #14531
2026-02-17 19:10:47 +01:00
Bjørn Erik Pedersen 3c823408ee Move common/hugo/HugoInfo to resources/page
* This break the circular dependency between hugolib and resources/page, and allow HugoInfo to use the proper Page ans Site interfaces.
* It's a little misplaced, but it makes everything easier to maintain and test, so that's well worth it.
2026-02-17 19:10:47 +01:00
Bjørn Erik Pedersen 3f9d0ad2b6 commands: Fix --panicOnWarning flag having no effect with module version warnings
The --panicOnWarning flag was not wired into the root command's logger,
so warnings emitted during config loading (such as module version
incompatibility warnings) would not trigger a panic.

Bind the panicOnWarning flag to the rootCommand struct and set the
PanicOnWarningHook on the logger created in createLogger.

Fixes #14524

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:34:29 +01:00
Joe Mooring ab62320d6b hugolib: Add hugo.Sites and .Site.IsDefault(), modify .Site.Sites
Changes:

- Add hugo.Sites to return all sites for all dimensions
- Modify .Site.Sites to return all sites for all dimensions
- Deprecate .Site.Sites in favor of hugo.Sites
- Add .Site.IsDefault() to report whether the current site is the
  default site across all dimensions
- Consolidate tests

Closes #14479
Closes #14481

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 18:57:26 +01:00
dependabot[bot] 21be4afd49 build(deps): bump github.com/bep/textandbinarywriter
Bumps [github.com/bep/textandbinarywriter](https://github.com/bep/textandbinarywriter) from 0.0.0-20251212174530-cd9f0732f60f to 0.1.0.
- [Release notes](https://github.com/bep/textandbinarywriter/releases)
- [Commits](https://github.com/bep/textandbinarywriter/commits/v0.1.0)

---
updated-dependencies:
- dependency-name: github.com/bep/textandbinarywriter
  dependency-version: 0.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 17:48:37 +01:00
khayyam 32027e50a1 paths: Fix handling of _ as a path name
Fixes #14344
2026-02-16 17:32:45 +01:00
Bjørn Erik Pedersen 8d19f5a78b output: Remove unused method
Closes #14522
2026-02-16 11:00:33 +01:00
dependabot[bot] e549016bc5 build(deps): bump github.com/bep/simplecobra from 0.6.1 to 0.7.0
Bumps [github.com/bep/simplecobra](https://github.com/bep/simplecobra) from 0.6.1 to 0.7.0.
- [Release notes](https://github.com/bep/simplecobra/releases)
- [Commits](https://github.com/bep/simplecobra/compare/v0.6.1...v0.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 10:56:28 +01:00
dependabot[bot] a392906b55 build(deps): bump github.com/bep/tmc from 0.5.1 to 0.6.0
Bumps [github.com/bep/tmc](https://github.com/bep/tmc) from 0.5.1 to 0.6.0.
- [Release notes](https://github.com/bep/tmc/releases)
- [Commits](https://github.com/bep/tmc/compare/v0.5.1...v0.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 10:55:54 +01:00
Joe Mooring e6f01bb464 snap: Stop building for ppc64el and s390x
Closes #14519
2026-02-14 19:59:26 +01:00
Joe Mooring 498431d641 docs: Fix lineNos default value in docs.yaml 2026-02-14 19:15:02 +01:00
Bjørn Erik Pedersen 773664b812 hugolib: Fix term title when taxonomy name contains spaces
The pluralTreeKey is normalized (spaces become hyphens), but the
unnormalized path preserves spaces. Use the plural name directly
for prefix trimming on the unnormalized path.

Fixes #13422

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 19:14:40 +01:00
Bjørn Erik Pedersen 7729473500 Update AI assistance guidelines in CONTRIBUTING.md
Clarified expectations for AI contributions from non-maintainers.
2026-02-14 18:21:47 +01:00
Bjørn Erik Pedersen 47e39af2cf tpl/collections: Speed up where and sort performance
```bash
goos: darwin
goarch: arm64
pkg: github.com/gohugoio/hugo/tpl/collections
cpu: Apple M1 Pro
                                        │ master.bench │       fix-whereperfalloc.bench       │
                                        │    sec/op    │   sec/op     vs base                 │
WhereOps/eq-10                            139.62µ ± 3%   66.04µ ± 2%  -52.70% (p=0.002 n=6)
WhereOps/ne-10                            139.54µ ± 1%   65.68µ ± 2%  -52.93% (p=0.002 n=6)
WhereOps/like-10                           179.8µ ± 3%   106.7µ ± 1%  -40.66% (p=0.002 n=6)
WhereSliceOfStructPointersWithMethod-10    350.7µ ± 1%   252.2µ ± 2%  -28.09% (p=0.002 n=6)
WhereAndSortPages/Where-10                120.93µ ± 1%   81.69µ ± 1%  -32.45% (p=0.002 n=6)
WhereAndSortPages/Sort-10                  1.608m ± 1%   1.165m ± 1%  -27.52% (p=0.002 n=6)
geomean                                    117.2µ        149.4µ       -40.03%               ¹

                                        │ master.bench  │       fix-whereperfalloc.bench        │
                                        │     B/op      │     B/op      vs base                 │
WhereOps/eq-10                             52.18Ki ± 0%   36.62Ki ± 0%  -29.82% (p=0.002 n=6)
WhereOps/ne-10                             52.18Ki ± 0%   36.62Ki ± 0%  -29.82% (p=0.002 n=6)
WhereOps/like-10                           52.20Ki ± 0%   36.63Ki ± 0%  -29.83% (p=0.002 n=6)
WhereSliceOfStructPointersWithMethod-10    64.52Ki ± 0%   64.59Ki ± 0%   +0.12% (p=0.002 n=6)
WhereAndSortPages/Where-10                 21.05Ki ± 0%   21.11Ki ± 0%   +0.30% (p=0.002 n=6)
WhereAndSortPages/Sort-10                 591.15Ki ± 0%   51.82Ki ± 0%  -91.23% (p=0.002 n=6)
geomean                                    7.690Ki        38.91Ki       -44.13%               ¹

                                        │ master.bench │       fix-whereperfalloc.bench       │
                                        │  allocs/op   │  allocs/op   vs base                 │
WhereOps/eq-10                             2.515k ± 0%   1.517k ± 0%  -39.68% (p=0.002 n=6)
WhereOps/ne-10                             2.515k ± 0%   1.517k ± 0%  -39.68% (p=0.002 n=6)
WhereOps/like-10                           2.515k ± 0%   1.517k ± 0%  -39.68% (p=0.002 n=6)
WhereSliceOfStructPointersWithMethod-10    4.503k ± 0%   4.504k ± 0%   +0.02% (p=0.002 n=6)
WhereAndSortPages/Where-10                 1.029k ± 0%   1.030k ± 0%   +0.10% (p=0.002 n=6)
WhereAndSortPages/Sort-10                 20.736k ± 0%   1.012k ± 0%  -95.12% (p=0.002 n=6)
geomean                                     358.5        1.594k       -53.04%               ¹
```
2026-02-14 16:33:15 +01:00
Bjørn Erik Pedersen 4a641d1594 tpl/internal: Replace deprecated parser.ParseDir and doc.New
Replace parser.ParseDir (deprecated since Go 1.25) with manual
file iteration using parser.ParseFile, and replace doc.New with
doc.NewFromFiles.

Fixes #14513

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 14:49:09 +01:00
Bjørn Erik Pedersen 8b7b6af92c docs: Regenerate docs.yaml
Mostly so I can test #14513
2026-02-14 12:15:58 +01:00
Bjørn Erik Pedersen 30a20122b7 Merge commit '8f3c066d23f431fb2c53d97ea489e4c28b42bd82'
# Conflicts:
#	docs/data/docs.yaml
2026-02-14 12:14:33 +01:00
Bjørn Erik Pedersen 8f3c066d23 Squashed 'docs/' changes from 1ad3c75ad..42914c50e
42914c50e content: Correct module mount example
d7b80be5f content: Add a "vendor" glossary entry
e1bd65866 content: Miscellaneous edits
deb1c4b1c content: Fix formatting
7ff740aae content: Update capitalizeListTitles documentation
a9f40164f content: Remove data namespace from functions
b3386a13a content: Remove expired content
e21f66fa5 content: Clean up deprecated service configurations
39ab42192 content: Change default value of respectDoNotTrack to true
067cbb771 Update partial-decorators.md
b419af329 content: Minor edits to collections functions
b12dd9366 content: Improve documentation for partial decorators
573b8610f content: Improve descriptions and signatures for collections functions
81befbae8 content: Update version references
684dbcedb Update HUGO_VERSION to 0.155.3
198e5f1df misc: Update README.md
4d20bbc04 content: Correct and improve glob patterns documentation
39d369ddc content: Update disablePathToLower documentation
be2da46e9 content: Miscellaneous edits to configuration documentation
19a580828 content: Add Commentix to the list of commercial commenting systems
d350725cd theme: Pin npm dependencies and adjust Netlify build commands
9b33bb5fa content: Define canonical output format and default site
84b72792e content: Document glob pattern matching logic
1839dd114 content: Create glob pattern quick reference
30f8f9ca0 content: Revise multi-dimensional content model description
b7e36e479 content: Fix formatting
b1722ac6b content: Indicate data types returned by the or function
1bbe3a566 content: Fix typo
b7e1ac7bb content: Improve example of _redirects rendering
37c71ef80 content: Improve and/or descriptions
0226c14ed content: Updates for v0.155.0
301fa208b content: Change GitHub Pages workflow to support dot files
5d07a679c Update HUGO_VERSION to 0.155.2
adb8e8b2c theme: Pin turbo
9206cb64b Update netlify.toml
84e712633 content: Refine ToMath admonition and provide example
a099af3cd content: Warn about conditional KaTeX CSS on list pages
c3ca7ac4c misc: Address build warnings
80a9139f4 Update HUGO_VERSION to 0.155.0
b096f6279 content: Restore homebrew on linux instructions
05132aa29 content: Hide snap installation instructions
d0e448a14 content: Refactor pages related to image processing
d141bc04f content: Clarify default site redirect description
1d6a0bed5 content: Adjust paragraph order
fa53534cb content: Clarify usage of quote characters in templates
1d62aa670 content: Miscellaneous edits
579c0678b content: Fix description of disableDefaultLanguageRedirect
cd12583ee Update all.md
37b01c320 Update HUGO_VERSION to 0.154.5
66053b6df content: Update version references
3f6fb8737 content: Refresh deployment dependencies to latest versions
c2915ee98 theme: Add a mobile menu variant
6457c3f59 Remove trailing whitespace from content/en/configuration/all.md
46c4d117a config: Mention disableDefaultDimensionRedirect
338500de0 Regen docs helper
98b9d25f3 Updates for 0.154.4 release
8306956e2 Update HUGO_VERSION to 0.154.4
af3f62992 Update HUGO_VERSION to 0.154.3
d06de65bd content: Fix typo
0d841a59e content: Fix typo
ed6fa87cf content: Fix typo
dcf80dbea content: Describe Markdown attribute access in render hook templates
70d3513f1 content: Update version references
7e581c529 Update netlify.toml
c6a706943 content: Remove expired pages
e64ee7d54 misc: Update workflows
bbec5a198 Update HUGO_VERSION to 0.154.1
e8717cb72 content: Fix formatting of glossary entry
0da233817 Add a link to templates.Inner from the partial decorator glossary entry
d55f7ad5e Update Inner.md
b674dfd48 Some more about templates.Inner
ef354e664 Remove some old new-in markers
7d1e407fd content: Misc updates for v0.154.0
43589a017 Update HUGO_VERSION to 0.154.0
46f832b9b content: Change shortcode usage note to an admonition
71ea41250 Adjust sponstors.toml
a5dc99838 Update netlify.toml
c3edd4d5b Update netlify.toml
c1d306d6b Add Zoomment to comments management options
cb222be99 Update HUGO_VERSION to 0.153.3
92ee87b4f content: Update CI/CD and version references
170bf3693 content: Note disabled extended check in v0.153.2 and later
34b5ef8b3 Update HUGO_VERSION to 0.153.2
c3c42b171 content: Add Dart Sass installation to SourceHut CI workflow
1768cc2f5 content: Fix links
63ba04562 theme: Modernize sans-serif font stack for cross-platform consistency
287da5c1c content: Update edition comparison and guidance
44b5010ca content: Update min version required for quick start
ec4f60cb7 content: Update version references
fafa69e7c Update HUGO_VERSION to 0.153.1
37bedd7d3 content: Fix formatting
ed9056088 content: Miscellaneous edits
fa4d61004 content: Miscellaneous post-release updates for v0.153.0
b99f47449 Misc v0.153.0 related edits
018c3e969 Regen docs.yml
be80ec4c7 content: Document new dimensions
ba397d5aa content: Miscellaneous updates for v0.153.0
59ca8fa3f Update HUGO_VERSION to 0.153.0

git-subtree-dir: docs
git-subtree-split: 42914c50ee338b00d0b1cda2c3af8066cd9ad65c
2026-02-14 12:13:44 +01:00
Joe Mooring 7850e7ceb5 commands: Skip chmod for files without owner-write permission
Closes #14507

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-13 18:23:56 +01:00
dependabot[bot] 86786c91e5 build(deps): bump github.com/gohugoio/hugo-goldmark-extensions/extras
Bumps [github.com/gohugoio/hugo-goldmark-extensions/extras](https://github.com/gohugoio/hugo-goldmark-extensions) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/gohugoio/hugo-goldmark-extensions/releases)
- [Commits](https://github.com/gohugoio/hugo-goldmark-extensions/compare/extras/v0.5.0...extras/v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hugo-goldmark-extensions/extras
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 13:43:28 +01:00
dependabot[bot] 7a7308aec2 build(deps): bump github.com/gohugoio/hugo-goldmark-extensions/passthrough
Bumps [github.com/gohugoio/hugo-goldmark-extensions/passthrough](https://github.com/gohugoio/hugo-goldmark-extensions) from 0.3.1 to 0.4.0.
- [Release notes](https://github.com/gohugoio/hugo-goldmark-extensions/releases)
- [Commits](https://github.com/gohugoio/hugo-goldmark-extensions/compare/passthrough/v0.3.1...extras/v0.4.0)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hugo-goldmark-extensions/passthrough
  dependency-version: 0.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 12:42:44 +01:00
Bjørn Erik Pedersen 2e5132d8d2 tpl/collections: Add some more benchmarks for where and sort 2026-02-13 10:34:39 +01:00
Joe Mooring 19ab3f581c all: Change site to project where appropriate
Closes #14504
2026-02-12 20:52:56 +01:00
Joe Mooring 7137714827 docker: Add full tar and openssh-client to support GitHub Actions
Closes #14502
2026-02-12 19:35:14 +01:00
Joe Mooring 3158a51283 markup/highlight: Allow lineNos to be true, false, "inline", or "table"
Closes #13481
2026-02-12 19:34:39 +01:00
Bjørn Erik Pedersen af5051e759 Remove items deprecated <= v0.136.0 (note)
Template functions:
- data.GetCSV / getCSV (use resources.GetRemote)
- data.GetJSON / getJSON (use resources.GetRemote)
- crypto.FNV32a (use hash.FNV32a)
- resources.Babel (use js.Babel)
- resources.PostCSS (use css.PostCSS)
- resources.ToCSS (use css.Sass)

Page methods:
- .Page.NextPage (use .Page.Next)
- .Page.PrevPage (use .Page.Prev)

Paginator:
- .Paginator.PageSize (use .Paginator.PagerSize)

Site methods:
- .Site.LastChange (use .Site.Lastmod)
- .Site.Author (use .Site.Params.Author)
- .Site.Authors (use .Site.Params.Authors)
- .Site.Social (use .Site.Params.Social)
- .Site.IsMultiLingual (use hugo.IsMultilingual)
- .Sites.First (use .Sites.Default)

Site config:
- paginate (use pagination.pagerSize)
- paginatePath (use pagination.path)

File caches:
- getjson cache
- getcsv cache
2026-02-12 19:33:58 +01:00
Bjørn Erik Pedersen b358a65c8b Upgrade to Go 1.26 2026-02-12 15:34:33 +01:00
Bjørn Erik Pedersen 8ae5e1a8be tpl: Move from md5 to xxhash for some in memory keys 2026-02-12 15:27:00 +01:00
dependabot[bot] 2d1d7312d0 build(deps): bump golang.org/x/tools from 0.41.0 to 0.42.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.41.0 to 0.42.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.41.0...v0.42.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 14:10:05 +01:00
dependabot[bot] b26fbf80a0 build(deps): bump github.com/bep/helpers from 0.6.0 to 0.7.0
Bumps [github.com/bep/helpers](https://github.com/bep/helpers) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/bep/helpers/releases)
- [Commits](https://github.com/bep/helpers/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/bep/helpers
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 14:09:09 +01:00
Joe Mooring 828ee720cc resources/page: Deprecate cascade._target in favor of cascade.target
Closes #14341
2026-02-12 13:50:37 +01:00
Joe Mooring 4384f9b8c2 tpl/tplimpl: Throw error when calling gist shortcode
Closes #14491
2026-02-12 13:49:23 +01:00
Joe Mooring 55d136dd66 tpl/tplimpl: Throw error when calling twitter/twitter_simple shortcodes
Closes #14488
2026-02-12 11:59:09 +01:00
Dr. Tobias Quathamer e2776593c0 testscripts/commands: Update 'future' date to far future
Fixes #14486
2026-02-11 13:34:10 +01:00
Joe Mooring de0c9f3eca Reapply "release: Support alpha, beta, and RC releases"
This reverts commit e66a33d369.

Closes #14466
2026-02-10 10:40:29 +01:00
dependabot[bot] 73583091e8 build(deps): bump golang.org/x/image from 0.35.0 to 0.36.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/image/compare/v0.35.0...v0.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 10:39:02 +01:00
hugoreleaser ee91280412 releaser: Prepare repository for 0.156.0-DEV
[ci skip]
2026-02-08 17:03:48 +00:00
hugoreleaser 8a858213b7 releaser: Bump versions for release of 0.155.3
[ci skip]
2026-02-08 16:40:42 +00:00
Bjørn Erik Pedersen 6ac7d081d1 hugolib: Don't render default site redirect for non-primary isHTML output formats
When a custom output format has isHTML=true but no Path, renderDefaultSiteRedirect
would derive intermediate redirect paths from homeLink (e.g. /en/foo.html),
creating a spurious redirect at /en/ that overwrites the actual HTML content.

Only render the default site redirect for the canonical "html" format
or for formats with their own Path prefix (e.g. AMP).

Fixes #14482

Co-Authored-By: Joe Mooring <joe.mooring@veriphor.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 17:35:32 +01:00
Bjørn Erik Pedersen 24eb84f868 server: Fix stuck server global error logging
Fixes #14469
2026-02-06 15:06:19 +01:00
dependabot[bot] 95a3678245 build(deps): bump github.com/evanw/esbuild from 0.27.2 to 0.27.3
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.27.2 to 0.27.3.
- [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.2...v0.27.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 10:50:39 +01:00
Bjørn Erik Pedersen 9045797d5f server: Fix panic when the server browser error handler tried to use a config in a state of flux
I tried and failed to create a test for this, but I have tested this manually.,

Fixes #14470
2026-02-05 20:11:48 +01:00
hugoreleaser 73641aeca1 releaser: Prepare repository for 0.156.0-DEV
[ci skip]
2026-02-02 10:27:50 +00:00
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
1238 changed files with 34644 additions and 15314 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22500.20300
- image: bepsays/ci-hugoreleaser:1.22600.20100
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20300
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100
steps:
- *restore-cache
- &attach-workspace
+4 -4
View File
@@ -2,12 +2,12 @@ have_fun: false
memory_config:
disabled: false
code_review:
disable: true
disable: false
comment_severity_threshold: HIGH
max_review_comments: -1
pull_request_opened:
help: false
summary: true
code_review: true
help: true
summary: false
code_review: false
include_drafts: false
ignore_patterns: []
-17
View File
@@ -1,17 +0,0 @@
You are an expert at detecting AI-generated content. Your main tasks is to analyze pull requests and determine if they were likely generated by AI and if this is disclosed in the pull request description.
Important: You must start your response with a confidence score line in exactly this format:
Confidence Score: X% where X is a number between 0 and 100.
Then provide your analysis in this format:
Reasoning: (explain your analysis)
Key Indicators: (list specific elements that suggest AI or human authorship)
A high confidence score (>80%) means you are very confident the content was AI-generated.
A low confidence score (<20%) means you are very confident the content was human-generated.
Use scores between 20-80% when there is uncertainty.
If there's a high confidence score:
* Add the label "Suspect AI" to the PR
* If it's not disclosed that it's AI generated, add a link to [AI Assistance Notice](../CONTRIBUTING.md#ai-assistance-notice) in the contribution gudelines.
-17
View File
@@ -1,17 +0,0 @@
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, reopened, ready_for_review, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
detect-ai:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: AI Watchdog
uses: Citric-Sheep/ai-watchdog@0db4a635d309db0f8d82642130dfc33e91cf33fa # v0.2.3
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
+5 -5
View File
@@ -16,20 +16,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GHCR
# Login is only needed when the image is pushed
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
provenance: mode=max
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1
- uses: dessant/lock-threads@f5f995c727ac99a91dec92781a8e34e7c839a65e # v6.0.0
with:
issue-inactive-days: 21
add-issue-labels: 'Outdated'
@@ -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@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
operations-per-run: 999
days-before-issue-stale: 365
+17 -21
View File
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
go-version: [1.25.x, 1.26.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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ matrix.go-version }}
check-latest: true
@@ -44,25 +42,22 @@ jobs:
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- name: Install Ruby
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
with:
ruby-version: "3.4.5"
- name: Install gems
- name: Install Ruby gems
run: |
gem install asciidoctor -v "2.0.26"
gem install asciidoctor-diagram -v "3.1.0"
gem install asciidoctor-diagram-ditaamini -v "1.0.3"
- name: Install GoAT
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
- name: Install Java # required by asciidoctor-diagram-ditaamini
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
distribution: temurin
java-version: "25"
java-package: jre
- name: Install Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Install Mage
@@ -115,7 +110,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: |
@@ -127,16 +125,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/**
+18
View File
@@ -0,0 +1,18 @@
* 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.
* In tests, use `qt` matchers (e.g. `b.Assert(err, qt.ErrorMatches, ...)`) instead of raw `if`/`t.Fatal` checks.
* In tests, always use the latest Hugo specification, e.g. for layouts, it's `layouts/page.html` and not `layouts/_default/single.html`, `layouts/list.html` and not `layouts/_default/list.html`
* Never name tests `TestIssue1234`; if this e.g. is about fixing an issue, use the issue ID as a suffix, as in `TestUpperCaseTitlesCreatesHavocIssue1234`. And note: No underscores in func names.
* Brevity is good. This applies to code, comments and commit messages. Don't write a novel.
* 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
+2 -2
View File
@@ -77,7 +77,7 @@ 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
@@ -88,7 +88,7 @@ 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.
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it. Also, AI contributions from non-maintainers needs to have a fairly narrow scope (e.g. a bug fix), as we have limited review capacity.
### Git Commit Message Guidelines
+4 -2
View File
@@ -2,7 +2,7 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.25"
ARG GO_VERSION="1.26"
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
@@ -65,7 +65,9 @@ RUN apk add --no-cache \
git \
runuser \
nodejs \
npm
npm \
openssh-client \
tar
RUN mkdir -p /var/hugo/bin /cache && \
addgroup -Sg 1000 hugo && \
+138 -110
View File
@@ -1,14 +1,19 @@
[Clang]: https://clang.llvm.org/
[GCC]: https://gcc.gnu.org/
[Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
[Go]: https://go.dev/doc/install
[bep]: https://github.com/bep
[bugs]: https://github.com/gohugoio/hugo/issues?q=is%3Aopen+is%3Aissue+label%3ABug
[contributing]: CONTRIBUTING.md
[create a proposal]: https://github.com/gohugoio/hugo/issues/new?labels=Proposal%2C+NeedsTriage&template=feature_request.md
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[details]: https://gohugo.io/host-and-deploy/deploy-with-hugo-deploy/
[documentation repository]: https://github.com/gohugoio/hugoDocs
[documentation]: https://gohugo.io/documentation
[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd
[features]: https://gohugo.io/about/features/
[forum]: https://discourse.gohugo.io
[friends]: https://github.com/gohugoio/hugo/graphs/contributors
[go]: https://go.dev/
[hugo modules]: https://gohugo.io/hugo-modules/
[installation]: https://gohugo.io/installation
[issue queue]: https://github.com/gohugoio/hugo/issues
@@ -20,12 +25,13 @@
[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator
[support]: https://discourse.gohugo.io
[themes]: https://themes.gohugo.io/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[website]: https://gohugo.io
[windows]: https://gohugo.io/installation/windows
<a href="https://gohugo.io/"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/static/images/hugo-logo-wide.svg?sanitize=true" alt="Hugo" width="565"></a>
A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go].
A fast and flexible static site generator built with love by [bep][], [spf13][], and [friends][] in Go.
---
@@ -33,11 +39,11 @@ A fast and flexible static site generator built with love by [bep], [spf13], and
[![Tests on Linux, MacOS and Windows](https://github.com/gohugoio/hugo/workflows/Test/badge.svg)](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo)
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
[Website][] | [Installation][] | [Documentation][] | [Support][] | [Contributing][] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
## Overview
Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
Hugo is a [static site generator][] written in Go, optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create:
@@ -52,21 +58,20 @@ 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
- CSS Processing &ndash; Bundle, transform, minify, create source maps, perform SRI hashing, and integrate with PostCSS.
- 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
And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
And with [Hugo Modules][], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
See the [features] section of the documentation for a comprehensive summary of Hugo's capabilities.
See the [features][] section of the documentation for a comprehensive summary of Hugo's capabilities.
## Sponsors
<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://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>
@@ -74,53 +79,63 @@ See the [features] section of the documentation for a comprehensive summary of H
## 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 is available in several editions. Use the standard edition unless you need additional features.
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:
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:
Feature|standard|deploy|extended|extended/deploy
:--|:-:|:-:|:-:|:-:
Core features|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:
Direct cloud deployment (1)|:x:|:heavy_check_mark:|:x:|:heavy_check_mark:
LibSass support (2)|:x:|:x:|:heavy_check_mark:|: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/
(1) Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details][].
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
(2) [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.
## Installation
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
Install Hugo from a [prebuilt binary][], package manager, or package repository. Please see the installation instructions for your operating system:
- [macOS]
- [Linux]
- [Windows]
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD]
- [macOS][]
- [Linux][]
- [Windows][]
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD][]
## Build from source
Prerequisites to build Hugo from source:
To build Hugo from source you must install:
- 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
1. [Git][]
1. [Go][] version 1.25.0 or later
Build the standard edition:
### Standard edition
```text
go install github.com/gohugoio/hugo@latest
To build and install the standard edition:
```sh
CGO_ENABLED=0 go install github.com/gohugoio/hugo@latest
```
Build the extended edition:
### Deploy edition
```text
To build and install the deploy edition:
```sh
CGO_ENABLED=0 go install -tags withdeploy github.com/gohugoio/hugo@latest
```
### Extended edition
To build and install the extended edition, first install a C compiler such as [GCC][] or [Clang][] and then run the following command.
```sh
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
```
Build the extended/deploy edition:
### Extended/deploy edition
```text
To build and install the extended/deploy edition, first install a C compiler such as [GCC][] or [Clang][] and then run the following command.
```sh
CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest
```
@@ -130,38 +145,48 @@ CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@late
## Documentation
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
Hugo's [documentation][] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
Please submit documentation issues and pull requests to the [documentation repository].
Please submit documentation issues and pull requests to the [documentation repository][].
## Support
Please **do not use the issue queue** for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the [forum].
Please **do not use the issue queue** for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the [forum][].
Hugos [forum] is an active community of users and developers who answer questions, share knowledge, and provide examples. A quick search of over 20,000 topics will often answer your question. Please be sure to read about [requesting help] before asking your first question.
Hugo's [forum][] is an active community of users and developers who answer questions, share knowledge, and provide examples. A quick search of over 20,000 topics will often answer your question. Please be sure to read about [requesting help][] before asking your first question.
## Contributing
You can contribute to the Hugo project by:
- Answering questions on the [forum]
- Improving the [documentation]
- Monitoring the [issue queue]
- Creating or improving [themes]
- Squashing [bugs]
- Answering questions on the [forum][]
- Improving the [documentation][]
- Monitoring the [issue queue][]
- Creating or improving [themes][]
- Squashing [bugs][]
Please submit documentation issues and pull requests to the [documentation repository].
Please submit documentation issues and pull requests to the [documentation repository][].
If you have an idea for an enhancement or new feature, create a new topic on the [forum] in the "Feature" category. This will help you to:
If you have an idea for an enhancement or new feature, create a new topic on the [forum][] in the "Feature" category. This will help you to:
- Determine if the capability already exists
- Measure interest
- Refine the concept
If there is sufficient interest, [create a proposal]. Do not submit a pull request until the project lead accepts the proposal.
If there is sufficient interest, [create a proposal][]. Do not submit a pull request until the project lead accepts the proposal.
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 +196,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 +253,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"
```
+6 -2
View File
@@ -1,7 +1,11 @@
## Security Policy
### Before You Report
Please read [Hugo's Security Model](https://gohugo.io/about/security/) first. If the issue reproduces in an upstream project, please report it there — we cannot triage or patch on their behalf.
### Reporting a Vulnerability
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
If, after the above, you believe you have found a vulnerability in Hugo itself with a concrete, reproducible impact, report it privately to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. Include a minimal reproducer, the Hugo version, and the observed vs. expected behavior.
Also see [Hugo's Security Model](https://gohugo.io/about/security/).
You should receive an initial response within a few days. Confirmed issues are typically patched within days, depending on complexity.
+2 -2
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
+142 -59
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.
@@ -16,6 +16,7 @@ package filecache
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@@ -45,16 +46,12 @@ const (
type Cache struct {
Fs afero.Fs
// Max age for items in this cache. Negative duration means forever,
// 0 is effectively turning this cache off.
maxAge time.Duration
cfg FileCacheConfig
// When set, we just remove this entire root directory on expiration.
pruneAllRootDir string
nlocker *lockTracker
entryLocker *lockTracker
initOnce sync.Once
isInited bool
initErr error
}
@@ -88,12 +85,15 @@ type ItemInfo struct {
}
// NewCache creates a new file cache with the given filesystem and max age.
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
if err := cfg.init(); err != nil {
panic(fmt.Sprintf("invalid cache config: %s", err))
}
return &Cache{
Fs: fs,
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
maxAge: maxAge,
pruneAllRootDir: pruneAllRootDir,
Fs: fs,
entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
cfg: cfg,
}
}
@@ -109,9 +109,15 @@ 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
}
})
@@ -126,19 +132,19 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
}
id = cleanID(id)
c.nlocker.Lock(id)
c.entryLocker.Lock(id)
info := ItemInfo{Name: id}
f, err := helpers.OpenFileForWriting(c.Fs, id)
if err != nil {
c.nlocker.Unlock(id)
c.entryLocker.Unlock(id)
return info, nil, err
}
return info, &lockedFile{
File: f,
unlock: func() { c.nlocker.Unlock(id) },
unlock: func() { c.entryLocker.Unlock(id) },
}, nil
}
@@ -156,8 +162,8 @@ func (c *Cache) ReadOrCreate(id string,
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info = ItemInfo{Name: id}
@@ -186,9 +192,9 @@ func (c *Cache) ReadOrCreate(id string,
// NamedLock locks the given id. The lock is released when the returned function is called.
func (c *Cache) NamedLock(id string) func() {
id = cleanID(id)
c.nlocker.Lock(id)
c.entryLocker.Lock(id)
return func() {
c.nlocker.Unlock(id)
c.entryLocker.Unlock(id)
}
}
@@ -201,8 +207,8 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -220,7 +226,7 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
return info, nil, err
}
if c.maxAge == 0 {
if c.cfg.MaxAge == 0 {
// No caching.
return info, hugio.ToReadCloser(r), nil
}
@@ -231,6 +237,40 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
c.writeReader(id, io.TeeReader(r, &buff))
}
// AbsFilenameFromID returns the filename for the given id in the cache.
// This will be an absolute path.
func (c *Cache) AbsFilenameFromID(id string) string {
return filepath.Join(c.cfg.DirCompiled, cleanID(id))
}
// GetOrCreateInfo tries to get the item info with the given id from cache. If not found or expired, create will
// be invoked with the id. The create function is expected to create the cache item with the given id. The returned ItemInfo will have the id as Name.
// This method is protected by a named lock using the given id as identifier.
func (c *Cache) GetOrCreateInfo(id string, create func(id string) error) (ItemInfo, error) {
if err := c.init(); err != nil {
return ItemInfo{}, err
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info := ItemInfo{Name: id}
if !c.removeIfNeeded(id) {
// The file exists and is not expired, so we consider it a cache hit.
return info, nil
}
if err := create(id); err != nil {
c.remove(id)
return info, err
}
return info, nil
}
func (c *Cache) writeReader(id string, r io.Reader) error {
dir := filepath.Dir(id)
if dir != "" {
@@ -254,8 +294,8 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -275,7 +315,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
return info, nil, err
}
if c.maxAge == 0 {
if c.cfg.MaxAge == 0 {
return info, b, nil
}
@@ -286,15 +326,39 @@ 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.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
if c.cfg.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
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -314,8 +378,8 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
}
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -324,10 +388,23 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
return info, r, nil
}
// removeIfNeeded checks if the file with the given id should be re-created.
func (c *Cache) removeIfNeeded(id string) bool {
if c.cfg.MaxAge == 0 {
// No caching, remove.
c.remove(id)
return true
}
if removed, err := c.removeIfExpired(id); err != nil || removed {
return true
}
return false
}
// getOrRemove gets the file with the given id. If it's expired, it will
// be removed.
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
if c.maxAge == 0 {
if c.cfg.MaxAge == 0 {
// No caching.
return nil
}
@@ -345,7 +422,7 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
}
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
if c.maxAge == 0 {
if c.cfg.MaxAge == 0 {
// No caching.
return nil, false
}
@@ -370,7 +447,7 @@ func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
}
func (c *Cache) removeIfExpired(id string) (bool, error) {
if c.maxAge <= 0 {
if c.cfg.MaxAge <= 0 {
return false, nil
}
@@ -380,29 +457,37 @@ func (c *Cache) removeIfExpired(id string) (bool, error) {
}
if c.isExpired(fi.ModTime()) {
c.Fs.Remove(id)
c.remove(id)
return true, nil
}
return false, nil
}
func (c *Cache) remove(id string) {
if c.cfg.entryIsDir {
c.Fs.RemoveAll(id)
} else {
c.Fs.Remove(id)
}
}
func (c *Cache) isExpired(modTime time.Time) bool {
if c.maxAge < 0 {
if c.cfg.MaxAge < 0 {
return false
}
// Note the use of time.Since here.
// We cannot use Hugo's global Clock for this.
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
return c.cfg.MaxAge == 0 || time.Since(modTime) > c.cfg.MaxAge
}
// For testing
func (c *Cache) GetString(id string) string {
id = cleanID(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
f, err := c.Fs.Open(id)
if err != nil {
@@ -417,6 +502,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.cfg.IsResourceDir {
if c.isInited {
panic("cannot set resource fs after init")
}
c.Fs = hugofs.NewBasePathFs(fs, c.cfg.DirCompiled)
}
}
}
// Get gets a named cache, nil if none found.
func (f Caches) Get(name string) *Cache {
return f[strings.ToLower(name)]
@@ -424,34 +520,21 @@ 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) {
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
cfs = hugofs.NewBasePathFs(fs, v.DirCompiled)
}
if cfs == nil {
panic("nil fs")
}
c := NewCache(cfs, v)
baseDir := v.DirCompiled
bfs := hugofs.NewBasePathFs(cfs, baseDir)
var pruneAllRootDir string
if k == CacheKeyModules {
pruneAllRootDir = "pkg"
}
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
m[k] = c
}
return m, nil
@@ -480,7 +563,7 @@ func (h *httpCache) Get(id string) (resp []byte, ok bool) {
}
func (h *httpCache) Set(id string, resp []byte) {
if h.c.maxAge == 0 {
if h.c.cfg.MaxAge == 0 {
return
}
+103 -33
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"
@@ -34,35 +35,50 @@ const (
cacheDirProject = ":cacheDir/:project"
)
var defaultCacheConfig = FileCacheConfig{
MaxAge: -1, // Never expire
Dir: cacheDirProject,
}
const (
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyModuleQueries = "modulequeries"
CacheKeyModuleGitInfo = "modulegitinfo"
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",
fileCacheConfigInternal: fileCacheConfigInternal{
entryIsDir: true,
isReadOnly: true, // we need to make it writable when pruning.
},
},
CacheKeyModuleQueries: {
MaxAge: 24 * time.Hour,
Dir: ":cacheDir/modules",
},
CacheKeyModuleGitInfo: {
MaxAge: 24 * time.Hour,
Dir: ":cacheDir/modules",
fileCacheConfigInternal: fileCacheConfigInternal{
entryIsDir: true,
},
},
CacheKeyGetJSON: defaultCacheConfig,
CacheKeyGetCSV: defaultCacheConfig,
CacheKeyImages: {
MaxAge: -1,
Dir: resourcesGenDir,
@@ -81,6 +97,13 @@ var defaultCacheConfigs = Configs{
},
}
func init() {
for k, v := range defaultCacheConfigs {
v.name = k
defaultCacheConfigs[k] = v
}
}
type FileCacheConfig struct {
// Max age of cache entries in this cache. Any items older than this will
// be removed and not returned from the cache.
@@ -92,22 +115,50 @@ type FileCacheConfig struct {
MaxAge time.Duration
// The directory where files are stored.
Dir string
DirCompiled string `json:"-"`
Dir string
// Will resources/_gen will get its own composite filesystem that
// also checks any theme.
IsResourceDir bool `json:"-"`
fileCacheConfigInternal `json:"-"`
}
// GetJSONCache gets the file cache for getJSON.
func (f Caches) GetJSONCache() *Cache {
return f[CacheKeyGetJSON]
func (cfg *FileCacheConfig) init() error {
if cfg.DirCompiled == "" {
// From unit tests. Just check that it does not contain any placeholders.
if strings.Contains(cfg.Dir, ":") {
return fmt.Errorf("cache dir %q contains unresolved placeholders", cfg.Dir)
}
cfg.DirCompiled = cfg.Dir
}
// Sanity check the config.
if len(cfg.DirCompiled) < 5 {
panic(fmt.Sprintf("invalid cache dir: %q", cfg.DirCompiled))
}
return nil
}
// GetCSVCache gets the file cache for getCSV.
func (f Caches) GetCSVCache() *Cache {
return f[CacheKeyGetCSV]
type fileCacheConfigInternal struct {
DirCompiled string
name string // The name of this cache, e.g. "images", "modules" etc.
entryIsDir bool // when set, the cache entries represents directories directly below the base dir.
isReadOnly bool // when set, the cache is read only and needs to be pruned differently. This is used for the Go modules cache.
IsResourceDir bool // resources/_gen will get its own composite filesystem that also checks any theme. TODO(bep) unexport this.
}
// 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,
})
}
// ImageCache gets the file cache for processed images.
@@ -120,6 +171,25 @@ 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
}
// ModuleGitInfoCache gets the file cache for Hugo Module git info.
func (f Caches) ModuleGitInfoCache() *Cache {
c, ok := f[CacheKeyModuleGitInfo]
if !ok {
panic("module git info 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,10 +217,14 @@ 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
var ok bool
cc, ok := c[k]
if !ok {
return nil, fmt.Errorf("%q is not a valid cache name", k)
}
dc := &mapstructure.DecoderConfig{
Result: &cc,
@@ -171,12 +245,8 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
return c, errors.New("must provide cache Dir")
}
name := strings.ToLower(k)
if !valid[name] {
return nil, fmt.Errorf("%q is not a valid cache name", name)
}
c[k] = cc
c[name] = cc
}
for k, v := range c {
+39 -14
View File
@@ -14,6 +14,7 @@
package filecache_test
import (
"encoding/json"
"path/filepath"
"runtime"
"testing"
@@ -43,12 +44,9 @@ assetDir = "assets"
archetypeDir = "archetypes"
[caches]
[caches.getJSON]
[caches.misc]
maxAge = "10m"
dir = "/path/to/c1"
[caches.getCSV]
maxAge = "11h"
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
@@ -61,9 +59,9 @@ dir = "/path/to/c4"
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
c2 := decoded["misc"]
c.Assert(c2.MaxAge.String(), qt.Equals, "10m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c1/filecache/misc"))
c3 := decoded["images"]
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
@@ -90,12 +88,9 @@ archeTypedir = "archetypes"
ignoreCache = true
[caches]
[caches.getJSON]
[caches.misc]
maxAge = 1234
dir = "/path/to/c1"
[caches.getCSV]
maxAge = 3456
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
@@ -132,15 +127,45 @@ func TestDecodeConfigDefault(t *testing.T) {
c.Assert(len(decoded), qt.Equals, 7)
imgConfig := decoded[filecache.CacheKeyImages]
jsonConfig := decoded[filecache.CacheKeyGetJSON]
miscConfig := decoded[filecache.CacheKeyMisc]
if runtime.GOOS == "windows" {
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
} else {
c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images")
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
c.Assert(miscConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/misc")
}
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
c.Assert(miscConfig.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)
// Also verify the new moduleGitInfo cache.
moduleGitInfoConfig := decoded[filecache.CacheKeyModuleGitInfo]
c.Assert(moduleGitInfoConfig.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`)
}
+7 -8
View File
@@ -18,7 +18,6 @@ import (
"testing"
"time"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
@@ -39,9 +38,9 @@ title: "Home"
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files, RunGC: true, NeedsOsFS: true},
).Build()
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithConfig(func(c *hugolib.IntegrationTestConfig) {
c.RunGC = true
}))
_, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images"))
@@ -67,7 +66,7 @@ title: "Home"
---
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/index.html --
-- layouts/home.html --
{{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }}
@@ -77,9 +76,9 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: logg.LevelInfo},
).Build()
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs(), hugolib.TestOptInfo(), hugolib.TestOptWithConfig(func(c *hugolib.IntegrationTestConfig) {
c.RunGC = true
}))
b.Assert(b.GCCount, qt.Equals, 0)
b.Assert(b.H, qt.IsNotNil)
+43 -7
View File
@@ -50,8 +50,8 @@ func (c Caches) Prune() (int, error) {
// Prune removes expired and unused items from this cache.
// If force is set, everything will be removed not considering expiry time.
func (c *Cache) Prune(force bool) (int, error) {
if c.pruneAllRootDir != "" {
return c.pruneRootDir(force)
if c.cfg.entryIsDir {
return c.pruneRootDirs(force)
}
if err := c.init(); err != nil {
return 0, err
@@ -93,9 +93,9 @@ func (c *Cache) Prune(force bool) (int, error) {
shouldRemove := force || c.isExpired(info.ModTime())
if !shouldRemove && len(c.nlocker.seen) > 0 {
if !shouldRemove && len(c.entryLocker.seen) > 0 {
// Remove it if it's not been touched/used in the last build.
_, seen := c.nlocker.seen[name]
_, seen := c.entryLocker.seen[name]
shouldRemove = !seen
}
@@ -117,11 +117,43 @@ func (c *Cache) Prune(force bool) (int, error) {
return counter, err
}
func (c *Cache) pruneRootDir(force bool) (int, error) {
func (c *Cache) pruneRootDirs(force bool) (int, error) {
dirs, err := afero.ReadDir(c.Fs, "")
if err != nil {
if herrors.IsNotExist(err) {
return 0, nil
}
return 0, err
}
counter := 0
for _, dir := range dirs {
if !dir.IsDir() {
continue
}
count, err := c.pruneRootDir(dir.Name(), force)
if err != nil {
return counter, err
}
counter += count
}
return counter, nil
}
func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
if err := c.init(); err != nil {
return 0, err
}
info, err := c.Fs.Stat(c.pruneAllRootDir)
// Sanity check.
if dirname != "pkg" && len(dirname) < 5 {
panic(fmt.Sprintf("invalid cache dir name: %q", dirname))
}
info, err := c.Fs.Stat(dirname)
if err != nil {
if herrors.IsNotExist(err) {
return 0, nil
@@ -133,5 +165,9 @@ func (c *Cache) pruneRootDir(force bool) (int, error) {
return 0, nil
}
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
if c.cfg.isReadOnly {
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, dirname)
}
return 1, c.Fs.RemoveAll(dirname)
}
+9 -8
View File
@@ -39,12 +39,9 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getjson]
[caches.misc]
maxAge = "200ms"
dir = "/cache/c"
[caches.getcsv]
maxAge = "200ms"
dir = "/cache/d"
[caches.assets]
maxAge = "200ms"
dir = ":resourceDir/_gen"
@@ -53,11 +50,14 @@ maxAge = "200ms"
dir = ":resourceDir/_gen"
`
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
for _, name := range []string{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 +84,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) {
+21 -12
View File
@@ -22,6 +22,8 @@ import (
"testing"
"time"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config"
@@ -64,7 +66,7 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getJSON]
[caches.misc]
maxAge = "10h"
dir = ":cacheDir/c"
@@ -78,11 +80,13 @@ 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")
cache := caches.Get("Misc")
c.Assert(cache, qt.Not(qt.IsNil))
cache = caches.Get("Images")
@@ -104,7 +108,7 @@ dir = ":cacheDir/c"
return []byte("bcd"), nil
}
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache()} {
for range 2 {
info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil)
@@ -132,8 +136,6 @@ dir = ":cacheDir/c"
}
}
c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil))
info, w, err := caches.ImageCache().WriteCloser("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
@@ -149,7 +151,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 +160,7 @@ dir = ":cacheDir/c"
}
func TestFileCacheConcurrent(t *testing.T) {
htesting.SkipSlowTestUnlessCI(t)
t.Parallel()
c := qt.New(t)
@@ -172,18 +175,19 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getjson]
[caches.misc]
maxAge = "1s"
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"
const cacheName = "misc"
filenameData := func(i int) (string, string) {
data := fmt.Sprintf("data: %d", i)
@@ -248,7 +252,12 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
}
}
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
cfg := filecache.FileCacheConfig{
MaxAge: 100 * time.Hour,
Dir: "cache/c",
}
cache := filecache.NewCache(afero.NewMemMapFs(), cfg)
const id = "a32"
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 -failfast $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 -1
View File
@@ -73,7 +73,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
nameAndPackage := func(t reflect.Type) (string, string) {
var name, pkg string
isPointer := t.Kind() == reflect.Ptr
isPointer := t.Kind() == reflect.Pointer
if isPointer {
t = t.Elem()
+3 -3
View File
@@ -26,9 +26,9 @@ import (
func TestMethods(t *testing.T) {
var (
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
zeroI = reflect.TypeOf((*I)(nil)).Elem()
zeroIE = reflect.TypeFor[IEmbed]()
zeroIEOnly = reflect.TypeFor[IEOnly]()
zeroI = reflect.TypeFor[I]()
)
dir, _ := os.Getwd()
+46 -14
View File
@@ -39,6 +39,7 @@ import (
"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/paths"
"github.com/gohugoio/hugo/common/types"
@@ -97,6 +98,7 @@ type commonConfig struct {
type configKey struct {
counter int32
ignoreModulesDoesNotExists bool
skipNpmCheck bool
}
// This is the root command.
@@ -131,6 +133,7 @@ type rootCommand struct {
gc bool
poll string
forceSyncStatic bool
panicOnWarning bool
// Profile flags (for debugging of performance problems)
cpuprofile string
@@ -150,6 +153,23 @@ type rootCommand struct {
cfgDir string
}
// resolveEnvironment sets r.environment if not already set.
// server indicates whether the server command is running (defaults to development).
func (r *rootCommand) resolveEnvironment(server bool) {
if r.environment != "" {
return
}
if env := os.Getenv("HUGO_ENVIRONMENT"); env != "" {
r.environment = env
} else if env := os.Getenv("HUGO_ENV"); env != "" {
r.environment = env
} else if server {
r.environment = hugo.EnvironmentDevelopment
} else {
r.environment = hugo.EnvironmentProduction
}
}
func (r *rootCommand) isVerbose() bool {
return r.logger.Level() <= logg.LevelInfo
}
@@ -194,6 +214,7 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
Logger: r.logger,
Environment: r.environment,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
@@ -220,6 +241,7 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
if cfg == nil {
panic("cfg must be set")
}
r.resolveEnvironment(false)
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
var dir string
if r.source != "" {
@@ -250,6 +272,7 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
Environment: r.environment,
Logger: r.logger,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
@@ -331,6 +354,9 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
}
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
if conf == nil {
return nil, fmt.Errorf("conf must be set")
}
k := configKey{counter: r.configVersionID.Load()}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
depsCfg := r.newDepsConfig(conf)
@@ -401,9 +427,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
@@ -487,12 +511,18 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
}
}
var logHookLast func(e *logg.Entry) error
if r.panicOnWarning {
logHookLast = loggers.PanicOnWarningHook
}
optsLogger := loggers.Options{
DistinctLevel: logg.LevelWarn,
Level: level,
StdOut: r.StdOut,
StdErr: r.StdErr,
StoreErrors: running,
HandlerPost: logHookLast,
}
return loggers.New(optsLogger), nil
@@ -519,8 +549,8 @@ func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Com
commandName = subCommandName
}
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
cmd.Short = "Build your site"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
cmd.Short = "Build your project"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo project.
Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
@@ -590,7 +620,7 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
_ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions)
cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.")
@@ -621,13 +651,14 @@ func (r *rootCommand) timeTrack(start time.Time, name string) {
}
type simpleCommand struct {
use string
name string
short string
long string
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
withc func(cmd *cobra.Command, r *rootCommand)
initc func(cd *simplecobra.Commandeer) error
use string
name string
short string
long string
aliases []string
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
withc func(cmd *cobra.Command, r *rootCommand)
initc func(cd *simplecobra.Commandeer) error
commands []simplecobra.Commander
@@ -654,6 +685,7 @@ func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = c.short
cmd.Long = c.long
cmd.Aliases = c.aliases
if c.use != "" {
cmd.Use = c.use
}
@@ -671,7 +703,7 @@ func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
func mapLegacyArgs(args []string) []string {
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") {
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "project", "site", "theme", "content") {
// Insert "content" as the second argument
args = append(args[:1], append([]string{"content"}, args[1:]...)...)
}
+4 -4
View File
@@ -23,7 +23,7 @@ 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"
@@ -95,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)
@@ -112,8 +112,8 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cmd := cd.CobraCommand
cmd.Short = "Display site configuration"
cmd.Long = `Display site configuration, both default and custom settings.`
cmd.Short = "Display project configuration"
cmd.Long = `Display project configuration, both default and custom settings.`
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
+58
View File
@@ -18,10 +18,12 @@ import (
"context"
"fmt"
"path/filepath"
"slices"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
@@ -200,6 +202,56 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
return nil
}
func (c *convertCommand) copyContentDirsForOutput(pagesBackedByFile page.Pages) error {
contentDirs := make(map[string]bool)
for _, p := range pagesBackedByFile {
filename := p.File().Filename()
contentDir := strings.TrimSuffix(filename, p.File().Path())
if contentDir == filename {
continue
}
contentDirs[filepath.Clean(contentDir)] = true
}
var contentDirList []string
for contentDir := range contentDirs {
contentDirList = append(contentDirList, contentDir)
}
slices.Sort(contentDirList)
outputDirAbs, err := filepath.Abs(c.outputDir)
if err != nil {
return fmt.Errorf("failed to resolve output path %q: %w", c.outputDir, err)
}
for _, contentDir := range contentDirList {
outputContentDirAbs := filepath.Join(outputDirAbs, filepath.Base(contentDir))
skipDirs := make(map[string]bool)
relToOutputDir, err := filepath.Rel(contentDir, outputDirAbs)
if err == nil && relToOutputDir != ".." && !strings.HasPrefix(relToOutputDir, ".."+string(filepath.Separator)) {
skipDirs[filepath.Clean(outputDirAbs)] = true
}
relToOutputContentDir, err := filepath.Rel(contentDir, outputContentDirAbs)
if err == nil && relToOutputContentDir != ".." && !strings.HasPrefix(relToOutputContentDir, ".."+string(filepath.Separator)) {
skipDirs[filepath.Clean(outputContentDirAbs)] = true
}
var shouldCopy func(filename string) bool
if len(skipDirs) > 0 {
shouldCopy = func(filename string) bool {
return !skipDirs[filepath.Clean(filename)]
}
}
if err := hugio.CopyDir(hugofs.Os, contentDir, outputContentDirAbs, shouldCopy); err != nil {
return fmt.Errorf("failed to copy %q to %q: %w", contentDir, outputContentDirAbs, err)
}
}
return nil
}
func (c *convertCommand) convertContents(format metadecoders.Format) error {
if c.outputDir == "" && !c.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
@@ -219,6 +271,12 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
pagesBackedByFile = append(pagesBackedByFile, p)
}
if c.outputDir != "" {
if err := c.copyContentDirsForOutput(pagesBackedByFile); err != nil {
return err
}
}
site.Log.Println("processing", len(pagesBackedByFile), "content files")
for _, p := range site.AllPages() {
if err := c.convertAndSavePage(p, site, format); err != nil {
+2 -2
View File
@@ -27,8 +27,8 @@ import (
func newDeployCommand() simplecobra.Commander {
return &simpleCommand{
name: "deploy",
short: "Deploy your site to a cloud provider",
long: `Deploy your site to a cloud provider
short: "Deploy your project to a cloud provider",
long: `Deploy your project to a cloud provider
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
+2 -2
View File
@@ -60,7 +60,7 @@ func newGenCommand() *genCommand {
short: "Generate CSS stylesheet for the Chroma code highlighter",
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview of the available styles.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
style = strings.ToLower(style)
@@ -98,7 +98,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
+1 -1
View File
@@ -81,7 +81,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
"editor": "newContentEditor",
}
// Flags that we for some reason don't want to expose in the site config.
// Flags that we for some reason don't want to expose in the project config.
internalKeySet := map[string]bool{
"quiet": true,
"verbose": true,
+33 -22
View File
@@ -31,11 +31,11 @@ import (
"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"
@@ -56,8 +56,9 @@ import (
type hugoBuilder struct {
r *rootCommand
confmu sync.Mutex
conf *commonConfig
confmu sync.Mutex
confOld *commonConfig
conf *commonConfig
// May be nil.
s *serverCommand
@@ -94,6 +95,27 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
fn(c.conf)
}
func (c *hugoBuilder) withConfOrOldConf(fn func(conf *commonConfig)) {
c.confmu.Lock()
defer c.confmu.Unlock()
if c.conf != nil {
fn(c.conf)
} else if c.confOld != nil {
fn(c.confOld)
}
}
func (c *hugoBuilder) withConfOrOldConfE(fn func(conf *commonConfig) error) error {
c.confmu.Lock()
defer c.confmu.Unlock()
if c.conf != nil {
return fn(c.conf)
} else if c.confOld != nil {
return fn(c.confOld)
}
return errConfigNotSet
}
type hugoBuilderErrState struct {
mu sync.Mutex
paused bool
@@ -157,6 +179,7 @@ func (c *hugoBuilder) initCPUProfile() (func(), error) {
return nil, fmt.Errorf("failed to create CPU profile: %w", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
f.Close()
return nil, fmt.Errorf("failed to start CPU profile: %w", err)
}
return func() {
@@ -1059,25 +1082,11 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
cfg := config.New()
cfg.Set("renderToMemory", c.r.renderToMemory)
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
if c.r.environment == "" {
// We need to set the environment as early as possible because we need it to load the correct config.
// Check if the user has set it in env.
if env := os.Getenv("HUGO_ENVIRONMENT"); env != "" {
c.r.environment = env
} else if env := os.Getenv("HUGO_ENV"); env != "" {
c.r.environment = env
} else {
if c.s != nil {
// The server defaults to development.
c.r.environment = hugo.EnvironmentDevelopment
} else {
c.r.environment = hugo.EnvironmentProduction
}
}
}
// We need to set the environment as early as possible because we need it to load the correct config.
c.r.resolveEnvironment(c.s != nil)
cfg.Set("environment", c.r.environment)
cfg.Set("internal", maps.Params{
cfg.Set("internal", hmaps.Params{
"running": running,
"watch": watch,
"verbose": c.r.isVerbose(),
@@ -1091,10 +1100,11 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
if len(conf.configs.LoadingInfo.ConfigFiles) == 0 {
//lint:ignore ST1005 end user message.
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.")
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new project.\nRun `hugo help new` for details.")
}
c.conf = conf
c.confOld = conf
if c.onConfigLoaded != nil {
if err := c.onConfigLoaded(false); err != nil {
return err
@@ -1158,8 +1168,9 @@ func (c *hugoBuilder) reloadConfig() error {
c.r.resetLogs()
c.r.configVersionID.Add(1)
if err := c.withConfE(func(conf *commonConfig) error {
if err := c.withConfOrOldConfE(func(conf *commonConfig) error {
oldConf := conf
c.conf = nil
newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf)
if err != nil {
return err
+11 -11
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"
@@ -49,7 +49,7 @@ func newImportCommand() *importCommand {
name: "jekyll",
short: "hugo import from Jekyll",
long: `hugo import from Jekyll.
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 2 {
@@ -90,8 +90,8 @@ func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Import a site from another system"
cmd.Long = `Import a site from another system.
cmd.Short = "Import a project from another system"
cmd.Long = `Import a project from another system.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
@@ -105,7 +105,7 @@ func (c *importCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
title := "My New Hugo Site"
title := "My New Hugo Project"
baseURL := "http://example.org/"
for key, value := range jekyllConfig {
@@ -127,7 +127,7 @@ func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind
in := map[string]any{
"baseURL": baseURL,
"title": title,
"languageCode": "en-us",
"locale": "en-us",
"disablePathToLower": true,
}
@@ -159,7 +159,7 @@ func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[st
return postDirs, hasAnyPost
}
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
func (c *importCommand) createProjectFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
@@ -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
}
@@ -419,7 +419,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
err = c.createProjectFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
if err != nil {
return newUserError(err)
}
@@ -463,7 +463,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
}
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
c.r.Println("Now, start Hugo by yourself:\n")
c.r.Println("Now, start Hugo by yourself:")
c.r.Println("cd " + args[1])
c.r.Println("git init")
c.r.Println("git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke")
+23 -13
View File
@@ -21,12 +21,13 @@ import (
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
const commonUsageMod = `
Note that Hugo will always start out by resolving the components defined in the site
Note that Hugo will always start out by resolving the components defined in the project
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
Go Modules, or a folder inside the themes directory, in that order.
@@ -49,28 +50,34 @@ func newModCommands() *modCommands {
commands: []simplecobra.Commander{
&simpleCommand{
name: "pack",
short: "Experimental: Prepares and writes a composite package.json file for your project",
long: `Prepares and writes a composite package.json file for your project.
short: "Merges module Node.js dependencies into an npm workspace",
long: `Merges Node.js dependencies from all Hugo modules into a "packages/hugoautogen" npm workspace.
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
The merged dependencies are written to packages/hugoautogen/package.json, and the root package.json
is updated with a "workspaces" entry pointing to "packages/hugoautogen".
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
The source entries are read from either package.hugo.json or package.json in the module root, with package.hugo.json taking precedence if both exist.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more information.
`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
cfg := flagsToCfg(cd, nil)
k := configKey{counter: r.configVersionID.Load(), skipNpmCheck: true}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
conf, err := r.ConfigFromProvider(key, cfg)
if err != nil {
return nil, err
}
return hugolib.NewHugoSites(r.newDepsConfig(conf))
})
if err != nil {
return err
}
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs, h.Configs.Modules)
},
},
},
@@ -166,7 +173,7 @@ Note that for vendored modules, that is the version listed and not the one from
if all {
modCache := h.ResourceSpec.FileCaches.ModulesCache()
count, err := modCache.Prune(true)
r.Printf("Deleted %d files from module cache.", count)
r.Printf("Deleted %d directories from module cache.", count)
return err
}
@@ -293,7 +300,10 @@ Run "go help get" for more information. All flags available for "go get" is also
return err
}
client := conf.configs.ModulesClient
return client.Get(args...)
if err := client.Get(args...); err != nil {
return err
}
return nil
}
},
},
+13 -12
View File
@@ -46,9 +46,9 @@ It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
If archetypes are provided in your theme or project, they will be used.
Ensure you run this within the root directory of your site.`,
Ensure you run this within the root directory of your project.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
@@ -76,10 +76,11 @@ Ensure you run this within the root directory of your site.`,
},
},
&simpleCommand{
name: "site",
use: "site [path]",
short: "Create a new site",
long: `Create a new site at the specified path.`,
name: "project",
use: "project [path]",
short: "Create a new project",
long: `Create a new project at the specified path.`,
aliases: []string{"site"},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
@@ -99,13 +100,13 @@ Ensure you run this within the root directory of your site.`,
}
sourceFs := conf.fs.Source
err = skeletons.CreateSite(createpath, sourceFs, force, format)
err = skeletons.CreateProject(createpath, sourceFs, force, format)
if err != nil {
return err
}
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
r.Println(c.newSiteNextStepsText(createpath, format))
r.Printf("Congratulations! Your new Hugo project was created in %s.\n\n", createpath)
r.Println(c.newProjectNextStepsText(createpath, format))
return nil
},
@@ -192,9 +193,9 @@ It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
If archetypes are provided in your theme or project, they will be used.
Ensure you run this within the root directory of your site.`
Ensure you run this within the root directory of your project.`
cmd.RunE = nil
return nil
@@ -205,7 +206,7 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return nil
}
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
func (c *newCommand) newProjectNextStepsText(path string, format string) string {
format = strings.ToLower(format)
var nextStepsText bytes.Buffer
+3 -1
View File
@@ -28,13 +28,14 @@ func newReleaseCommand() simplecobra.Commander {
step int
skipPush bool
try bool
version string
)
return &simpleCommand{
name: "release",
short: "Release a new version of Hugo",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
rel, err := releaser.New(skipPush, try, step)
rel, err := releaser.New(skipPush, try, step, version)
if err != nil {
return err
}
@@ -47,6 +48,7 @@ func newReleaseCommand() simplecobra.Commander {
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
cmd.PersistentFlags().StringVarP(&version, "version", "", "", "version to release (derived from branch name if not set)")
_ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
},
}
+21 -16
View File
@@ -49,6 +49,7 @@ 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"
@@ -245,7 +246,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
logger := f.c.r.logger
if i == 0 {
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment())
mainTarget := "disk"
if f.c.r.renderToMemory {
mainTarget = "memory"
@@ -281,7 +282,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
}
port = 1313
f.c.withConf(func(conf *commonConfig) {
f.c.withConfOrOldConf(func(conf *commonConfig) {
if lrport := conf.configs.GetFirstLanguageConfig().BaseURLLiveReload().Port(); lrport != 0 {
port = lrport
}
@@ -370,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 {
@@ -492,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
@@ -518,7 +523,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Start the embedded web server"
cmd.Long = `Hugo provides its own webserver which builds and serves the site.
cmd.Long = `Hugo provides its own webserver which builds and serves the project.
While hugo server is high performance, it is a webserver with limited options.
The ` + "`" + `hugo server` + "`" + ` command will by default write and serve files from disk, but
@@ -526,8 +531,8 @@ you can render to memory by using the ` + "`" + `--renderToMemory` + "`" + ` fla
faster in some cases, but it will consume more memory.
By default hugo will also watch your files for any changes you make and
automatically rebuild the site. It will then live reload any open browser pages
and push the latest content to them. As most Hugo sites are built in a fraction
automatically rebuild the project. It will then live reload any open browser pages
and push the latest content to them. As most Hugo projects are built in a fraction
of a second, you will be able to save and see your changes nearly instantly.`
cmd.Aliases = []string{"serve"}
@@ -548,7 +553,7 @@ of a second, you will be able to save and see your changes nearly instantly.`
cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
cmd.Flags().BoolVarP(&c.navigateToChanged, "navigateToChanged", "N", false, "navigate to changed content file on live browser reload")
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the site in a browser after server startup")
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the project in a browser after server startup")
cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
@@ -1155,14 +1160,14 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
return err
}
// chmodFilter is a ChmodFilter for static syncing.
// Returns true to skip syncing permissions for directories and files without
// owner-write permission. The primary use case is files from the module cache (0444).
func chmodFilter(dst, src os.FileInfo) bool {
// Hugo publishes data from multiple sources, potentially
// with overlapping directory structures. We cannot sync permissions
// for directories as that would mean that we might end up with write-protected
// directories inside /public.
// One example of this would be syncing from the Go Module cache,
// which have 0555 directories.
return src.IsDir()
if src.IsDir() {
return true
}
return src.Mode().Perm()&0o200 == 0
}
func cleanErrorLog(content string) string {
+55 -11
View File
@@ -21,24 +21,24 @@ import (
"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 {
@@ -49,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 {
@@ -58,18 +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)
}
// All returns all items in the stack, from bottom to top.
func (s *Stack[T]) All() iter.Seq2[int, T] {
func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
func (s *Stack[T]) Drain() []T {
func (s *StackThreadSafe[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
@@ -77,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
@@ -89,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)
+4 -9
View File
@@ -16,15 +16,10 @@ package constants
// Error/Warning IDs.
// Do not change these values.
const (
// IDs for remote errors in tpl/data.
ErrRemoteGetJSON = "error-remote-getjson"
ErrRemoteGetCSV = "error-remote-getcsv"
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
)
// Field/method names with special meaning.
+5 -3
View File
@@ -87,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)
}
+2 -4
View File
@@ -38,9 +38,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for j := range 100 {
s := strings.Repeat("Hello ", i+j+1*42)
r := strings.NewReader(s)
@@ -50,7 +48,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
expect, _ := XXHashFromString(s)
c.Assert(got, qt.Equals, expect)
}
}()
})
}
wg.Wait()
+44 -24
View File
@@ -288,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)
@@ -350,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:
+3 -3
View File
@@ -32,15 +32,15 @@ func TestNewFileError(t *testing.T) {
fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
lines := ""
var lines strings.Builder
for i := 1; i <= 100; i++ {
lines += fmt.Sprintf("line %d\n", i)
lines.WriteString(fmt.Sprintf("line %d\n", i))
}
fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2})
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`)
fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212})
fe.UpdateContent(strings.NewReader(lines), nil)
fe.UpdateContent(strings.NewReader(lines.String()), nil)
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
errorContext := fe.ErrorContext()
c.Assert(errorContext, qt.IsNotNil)
+244 -80
View File
@@ -1,4 +1,4 @@
// Copyright 2020 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.
@@ -14,6 +14,7 @@
package hexec
import (
"bufio"
"bytes"
"context"
"errors"
@@ -23,12 +24,12 @@ import (
"os/exec"
"path/filepath"
"regexp"
"slices"
"strings"
"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"
)
@@ -98,11 +99,11 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
}
return &Exec{
sc: cfg,
workingDir: workingDir,
infol: log.InfoCommand("exec"),
baseEnviron: baseEnviron,
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
sc: cfg,
workingDir: workingDir,
infol: log.InfoCommand("exec"),
baseEnviron: baseEnviron,
nodeRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
}
}
@@ -121,9 +122,16 @@ type Exec struct {
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
baseEnviron []string
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
npxInit sync.Once
npxAvailable bool
// Additional absolute paths to allow reading from in the Node.js permission model.
nodeReadPaths []string
nodeRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
}
// SetNodeReadPaths sets additional absolute paths to allow reading from
// in the Node.js permission model (e.g. Hugo module cache directories).
func (e *Exec) SetNodeReadPaths(paths []string) {
e.nodeReadPaths = paths
}
func (e *Exec) New(name string, arg ...any) (Runner, error) {
@@ -155,8 +163,6 @@ func (b binaryLocation) String() string {
switch b {
case binaryLocationNodeModules:
return "node_modules/.bin"
case binaryLocationNpx:
return "npx"
case binaryLocationPath:
return "PATH"
}
@@ -165,64 +171,55 @@ func (b binaryLocation) String() string {
const (
binaryLocationNodeModules binaryLocation = iota + 1
binaryLocationNpx
binaryLocationPath
)
// Npx will in order:
// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
// 2. If not found, and npx is available, run npx --no-install <name> <args>.
// 3. Fall back to the PATH.
// If name is "tailwindcss", we will try the PATH as the second option.
// Npx finds and runs a Node.js tool. The binary is located first in
// WORKINGDIR/node_modules/.bin, then in PATH. The tool is always invoked via
// "node [--permission <flags>] <script> <args>"; the --permission flags are
// added when the Node.js permission model is enabled.
func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
if err := e.sc.CheckAllowedExec(name); err != nil {
return nil, err
}
if err := e.sc.CheckAllowedExec("node"); err != nil {
// Legacy path: We replaced npx with node in v0.161.0, and anyone using these tools with a custom security.exec.allow list
// would get an error when upgrading. To avoid this, check for npx as well.
if err2 := e.sc.CheckAllowedExec("npx"); err2 != nil {
return nil, err
}
}
newRunner, err := e.newNPXRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) {
type tryFunc func() func(...any) (Runner, error)
tryFuncs := map[binaryLocation]tryFunc{
binaryLocationNodeModules: func() func(...any) (Runner, error) {
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
_, err := exec.LookPath(nodeBinFilename)
if err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.new(name, nodeBinFilename, arg2...)
}
},
binaryLocationNpx: func() func(...any) (Runner, error) {
e.checkNpx()
if !e.npxAvailable {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.npx(name, arg2...)
}
},
binaryLocationPath: func() func(...any) (Runner, error) {
if _, err := exec.LookPath(name); err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.New(name, arg2...)
}
},
newRunner, err := e.nodeRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) {
var resolvedBin string
var loc binaryLocation
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
if p, err := exec.LookPath(nodeBinFilename); err == nil {
resolvedBin = p
loc = binaryLocationNodeModules
} else if p, err := exec.LookPath(name); err == nil {
resolvedBin = p
loc = binaryLocationPath
} else {
return nil, &NotFoundError{name: name, method: "in PATH"}
}
locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath}
if name == "tailwindcss" {
// See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx}
scriptPath := resolveNodeBin(resolvedBin)
e.infol.WithFields(logg.Fields{
logg.Field{Name: "location", Value: loc},
logg.Field{Name: "bin", Value: resolvedBin},
logg.Field{Name: "script", Value: scriptPath},
}).Logf("resolve %q", name)
if scriptPath == "" {
return nil, fmt.Errorf("binary %q is not a Node.js script", name)
}
for _, loc := range locations {
if f := tryFuncs[loc](); f != nil {
e.infol.Logf("resolve %q using %s", name, loc)
return f, nil
}
}
return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
return func(arg2 ...any) (Runner, error) {
return e.newNode(name, scriptPath, arg2...)
}, nil
})
if err != nil {
return nil, err
@@ -231,22 +228,195 @@ func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
return newRunner(arg...)
}
const (
npxNoInstall = "--no-install"
npxBinary = "npx"
nodeModulesBinPath = "node_modules/.bin"
)
// newNode runs a Node.js script via "node [--permission <flags>] <scriptPath> <args>".
func (e *Exec) newNode(name, scriptPath string, arg ...any) (Runner, error) {
var allArgs []any
for _, pa := range e.nodePermissionArgs(name, scriptPath) {
allArgs = append(allArgs, pa)
}
allArgs = append(allArgs, scriptPath)
allArgs = append(allArgs, arg...)
// When the script lives outside the working dir (a globally installed
// tool), point NODE_PATH at the script's node_modules ancestor so Node's
// resolver (and tools that honor it, e.g. tailwindcss v4) can locate the
// tool's sibling packages. tailwindcss v4's CSS resolver treats NODE_PATH
// as a single path, not a list, so we don't concatenate with the local
// path here. For local installs the caller's NODE_PATH (set by
// hugo.GetExecEnviron to <workDir>/node_modules) already covers the need.
localNM := filepath.Join(e.workingDir, "node_modules")
if p := nodeScriptReadPath(scriptPath); p != "" && p != localNM {
allArgs = append(allArgs, WithEnviron([]string{"NODE_PATH=" + p}))
}
func (e *Exec) checkNpx() {
e.npxInit.Do(func() {
e.npxAvailable = InPath(npxBinary)
})
return e.New("node", allArgs...)
}
// npx is a convenience method to create a Runner running npx --no-install <name> <args.
func (e *Exec) npx(name string, arg ...any) (Runner, error) {
arg = append(arg[:0], append([]any{npxNoInstall, name}, arg[0:]...)...)
return e.New(npxBinary, arg...)
// nodePermissionArgs builds the Node.js --permission flags from the security config.
func (e *Exec) nodePermissionArgs(name, scriptPath string) []string {
perms := e.sc.Node.Permissions
if !perms.IsEnabled() {
return nil
}
args := []string{"--permission"}
for _, p := range e.resolveNodePermPaths(perms.AllowRead) {
args = append(args, "--allow-fs-read="+p)
}
for _, p := range e.nodeReadPaths {
args = append(args, "--allow-fs-read="+p)
}
if p := nodeScriptReadPath(scriptPath); p != "" {
args = append(args, "--allow-fs-read="+p)
}
for _, p := range e.resolveNodePermPaths(perms.AllowWrite) {
args = append(args, "--allow-fs-write="+p)
}
var silenceSecurityWarnings bool
if slices.Contains(perms.AllowAddons, name) {
silenceSecurityWarnings = true
args = append(args, "--allow-addons")
}
if slices.Contains(perms.AllowWorker, name) {
silenceSecurityWarnings = true
args = append(args, "--allow-worker")
}
if slices.Contains(perms.AllowChildProcess, name) {
silenceSecurityWarnings = true
args = append(args, "--allow-child-process")
}
if silenceSecurityWarnings {
// There are no more fine grained way to do this, see https://github.com/nodejs/node/issues/59818
// If the process is configured to allow either workers or addons, Node will print warnings that's not very helpful.
args = append(args, "--disable-warning=SecurityWarning")
}
return args
}
// resolveNodePermPaths resolves relative paths against the working directory.
func (e *Exec) resolveNodePermPaths(paths []string) []string {
resolved := make([]string, len(paths))
for i, p := range paths {
switch {
case p == "*":
resolved[i] = "*"
case filepath.IsAbs(p):
resolved[i] = p
default:
resolved[i] = filepath.Join(e.workingDir, p)
}
}
return resolved
}
const nodeModulesBinPath = "node_modules/.bin"
// nodeScriptReadPath returns a path to add to the Node.js read allow-list so
// a script can load its dependencies. For scripts inside a node_modules tree
// it returns the nearest ancestor "node_modules" directory, so both nested
// and hoisted deps are reachable. Otherwise the script's own directory.
func nodeScriptReadPath(scriptPath string) string {
if scriptPath == "" {
return ""
}
dir := filepath.Dir(scriptPath)
for {
if filepath.Base(dir) == "node_modules" {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
return filepath.Dir(scriptPath)
}
dir = parent
}
}
// resolveNodeBin resolves a binary path to the underlying Node.js script.
// Returns the path to the JS entry point, or "" if the binary is not a Node script.
func resolveNodeBin(path string) string {
// 1. If the file is a symlink, resolve it (macOS/Linux npm creates symlinks in node_modules/.bin).
if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 {
if resolved, err := filepath.EvalSymlinks(path); err == nil {
if hasJSExtension(resolved) || isNodeScript(resolved) {
return resolved
}
}
return ""
}
// 2. Check if the file itself is a Node script (e.g. globally installed with #!/usr/bin/env node).
if isNodeScript(path) {
return path
}
// 3. Try extracting JS entry point from an npm wrapper script (.cmd or shell).
return extractNodeEntryPoint(path)
}
// nodeEntryPointRe matches a relative path in npm-generated wrapper scripts.
// The entry may be a .js/.mjs/.cjs file or an extensionless Node shebang
// script (e.g. postcss-cli 7's bin/postcss). Local installs reference the
// entry via "..", global installs via "node_modules" (notably on Windows,
// where npm does not symlink global binaries).
// Examples:
//
// Local shell: "$basedir/../postcss-cli/index.js"
// Local cmd: "%dp0%\..\postcss-cli\index.js"
// Scoped: "$basedir/../@babel/cli/bin/babel.js"
// No ext: "%dp0%\..\postcss-cli\bin\postcss"
// Global shell: "$basedir/node_modules/postcss-cli/index.js"
// Global cmd: "%dp0%\node_modules\postcss-cli\index.js"
var nodeEntryPointRe = regexp.MustCompile(`[/\\]((?:\.\.|node_modules)[/\\][\w@][\w@./\\-]*)`)
// extractNodeEntryPoint reads an npm wrapper script and extracts the Node
// entry point path, validating that it's a JS file or a Node shebang script.
func extractNodeEntryPoint(wrapperPath string) string {
data, err := os.ReadFile(wrapperPath)
if err != nil {
return ""
}
m := nodeEntryPointRe.FindSubmatch(data)
if m == nil {
return ""
}
// Normalize backslashes from Windows .cmd wrappers.
relPath := strings.ReplaceAll(string(m[1]), "\\", "/")
resolved := filepath.Join(filepath.Dir(wrapperPath), relPath)
if _, err := os.Stat(resolved); err != nil {
return ""
}
if !hasJSExtension(resolved) && !isNodeScript(resolved) {
return ""
}
return resolved
}
func hasJSExtension(path string) bool {
switch filepath.Ext(path) {
case ".js", ".mjs", ".cjs":
return true
}
return false
}
// isNodeScript reports whether the file at path has a Node.js shebang.
func isNodeScript(path string) bool {
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
r := bufio.NewReader(f)
line, err := r.ReadString('\n')
if err != nil && len(line) == 0 {
return false
}
return strings.HasPrefix(line, "#!") && strings.Contains(line, "node")
}
// Sec returns the security policies this Exec is configured with.
@@ -283,14 +453,8 @@ func (c *cmdWrapper) Run() error {
if err == nil {
return nil
}
name := c.name
method := "in PATH"
if name == npxBinary {
name = c.c.Args[2]
method = "using npx"
}
if notFoundRe.MatchString(c.outerr.String()) {
return &NotFoundError{name: name, method: method}
return &NotFoundError{name: c.name, method: "in PATH"}
}
return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
}
+74
View File
@@ -0,0 +1,74 @@
// 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 hexec_test
import (
"testing"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
)
func TestNPMGlobalInstalls(t *testing.T) {
if !htesting.IsRealCI() {
t.Skip("We only ever want to run this in CI.")
}
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
[security.exec]
allow = ['^(babel|node|postcss|tailwindcss)$']
-- package.json --
{}
-- hugo_stats.json --
-- assets/js/main.js --
console.log("Hello, world!");
-- assets/css/main1.css --
body { color: red }
-- assets/css/main2.css --
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@source "hugo_stats.json";
body { color: blue }
-- layouts/home.html --
{{ with resources.Get "css/main1.css" }}
{{ with . | css.PostCSS }}
CSS1 size: {{ .Content | len }}|{{ .RelPermalink }}|
{{ end }}
{{ end }}
{{ with resources.Get "css/main2.css" }}
{{ with . | css.TailwindCSS }}
CSS2 size: {{ .Content | len }}|{{ .RelPermalink }}|
{{ end }}
{{ end }}
{{ with resources.Get "js/main.js" }}
{{ with . | js.Babel }}
JS size: {{ .Content | len }}|{{ .RelPermalink }}|
{{ end }}
{{ end }}
`
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstallGlobal(
"postcss", "postcss-cli",
"@babel/core", "@babel/cli",
"tailwindcss", "@tailwindcss/cli", "@tailwindcss/typography",
))
b.AssertFileContent("public/index.html",
"CSS1 size: 233|/css/main1.css|",
"CSS2 size: 4557|/css/main2.css|",
"JS size: 31|/js/main.js|",
)
}
+474
View File
@@ -0,0 +1,474 @@
// 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 hexec
import (
"os"
"path/filepath"
"runtime"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config/security"
)
func TestNodePermissionArgs(t *testing.T) {
c := qt.New(t)
// Use t.TempDir() so paths are absolute on any OS (avoids Windows volume assumptions).
base := t.TempDir()
site := filepath.Join(base, "site")
tmp := filepath.Join(base, "tmp")
cacheDir := filepath.Join(base, "home", "user", ".cache", "hugo_cache", "modules")
c.Run("Default config tailwindcss", func(c *qt.C) {
e := &Exec{
sc: security.DefaultConfig,
workingDir: site,
}
args := e.nodePermissionArgs("tailwindcss", "")
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=" + site,
"--allow-addons",
"--allow-worker",
"--allow-child-process",
"--disable-warning=SecurityWarning",
})
})
c.Run("Default config postcss", func(c *qt.C) {
e := &Exec{
sc: security.DefaultConfig,
workingDir: site,
}
args := e.nodePermissionArgs("postcss", "")
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=" + site,
})
})
c.Run("Multiple paths", func(c *qt.C) {
cfg := security.DefaultConfig
cfg.Node.Permissions.AllowRead = []string{".", tmp}
cfg.Node.Permissions.AllowWrite = []string{"."}
e := &Exec{
sc: cfg,
workingDir: site,
}
args := e.nodePermissionArgs("tailwindcss", "")
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=" + site,
"--allow-fs-read=" + tmp,
"--allow-fs-write=" + site,
"--allow-addons",
"--allow-worker",
"--allow-child-process",
"--disable-warning=SecurityWarning",
})
})
c.Run("Wildcard", func(c *qt.C) {
cfg := security.DefaultConfig
cfg.Node.Permissions.AllowRead = []string{"*"}
cfg.Node.Permissions.AllowWrite = []string{"*"}
e := &Exec{
sc: cfg,
workingDir: site,
}
args := e.nodePermissionArgs("tailwindcss", "")
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=*",
"--allow-fs-write=*",
"--allow-addons",
"--allow-worker",
"--allow-child-process",
"--disable-warning=SecurityWarning",
})
})
c.Run("Disabled", func(c *qt.C) {
cfg := security.DefaultConfig
cfg.Node.Permissions.Disable = true
e := &Exec{
sc: cfg,
workingDir: site,
}
args := e.nodePermissionArgs("tailwindcss", "")
c.Assert(args, qt.IsNil)
})
c.Run("No fs flags", func(c *qt.C) {
cfg := security.DefaultConfig
cfg.Node.Permissions.AllowRead = nil
cfg.Node.Permissions.AllowAddons = nil
cfg.Node.Permissions.AllowWorker = nil
cfg.Node.Permissions.AllowChildProcess = nil
e := &Exec{
sc: cfg,
workingDir: site,
}
args := e.nodePermissionArgs("postcss", "")
c.Assert(args, qt.DeepEquals, []string{"--permission"})
})
c.Run("Read only", func(c *qt.C) {
cfg := security.DefaultConfig
cfg.Node.Permissions.AllowRead = []string{"."}
cfg.Node.Permissions.AllowWrite = nil
e := &Exec{
sc: cfg,
workingDir: site,
}
args := e.nodePermissionArgs("postcss", "")
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=" + site,
})
})
c.Run("With additional read paths", func(c *qt.C) {
e := &Exec{
sc: security.DefaultConfig,
workingDir: site,
nodeReadPaths: []string{cacheDir},
}
args := e.nodePermissionArgs("postcss", "")
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=" + site,
"--allow-fs-read=" + cacheDir,
})
})
c.Run("Global install script path", func(c *qt.C) {
e := &Exec{
sc: security.DefaultConfig,
workingDir: site,
}
globalNM := filepath.Join(base, "nvm", "lib", "node_modules")
script := filepath.Join(globalNM, "postcss-cli", "bin", "postcss")
args := e.nodePermissionArgs("postcss", script)
c.Assert(args, qt.DeepEquals, []string{
"--permission",
"--allow-fs-read=" + site,
"--allow-fs-read=" + globalNM,
})
})
}
func TestNodeScriptReadPath(t *testing.T) {
c := qt.New(t)
base := t.TempDir()
nm := filepath.Join(base, "node_modules")
globalNM := filepath.Join(base, "nvm", "lib", "node_modules")
c.Assert(nodeScriptReadPath(""), qt.Equals, "")
c.Assert(nodeScriptReadPath(filepath.Join(nm, "postcss-cli", "index.js")), qt.Equals, nm)
c.Assert(nodeScriptReadPath(filepath.Join(nm, "@babel", "cli", "bin", "babel.js")), qt.Equals, nm)
c.Assert(nodeScriptReadPath(filepath.Join(globalNM, "postcss-cli", "bin", "postcss")), qt.Equals, globalNM)
loose := filepath.Join(base, "tools", "script.js")
c.Assert(nodeScriptReadPath(loose), qt.Equals, filepath.Dir(loose))
}
func TestResolveNodeBin(t *testing.T) {
c := qt.New(t)
// Create a fake node_modules structure.
dir := t.TempDir()
nodeModules := filepath.Join(dir, "node_modules")
binDir := filepath.Join(nodeModules, ".bin")
// Create target JS files.
postcssJS := filepath.Join(nodeModules, "postcss-cli", "index.js")
babelJS := filepath.Join(nodeModules, "@babel", "cli", "bin", "babel.js")
mkdirAndWrite(t, postcssJS, "#!/usr/bin/env node\nconsole.log('postcss');\n")
mkdirAndWrite(t, babelJS, "#!/usr/bin/env node\nconsole.log('babel');\n")
os.MkdirAll(binDir, 0o755)
c.Run("Symlink to JS file", func(c *qt.C) {
if runtime.GOOS == "windows" {
c.Skip("Symlinks may require elevated privileges on Windows")
}
link := filepath.Join(binDir, "postcss-link")
os.Remove(link)
c.Assert(os.Symlink(postcssJS, link), qt.IsNil)
resolved := resolveNodeBin(link)
t.Logf("Symlink: link=%q, resolved=%q", link, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, postcssJS), qt.IsTrue)
})
c.Run("Symlink to scoped package", func(c *qt.C) {
if runtime.GOOS == "windows" {
c.Skip("Symlinks may require elevated privileges on Windows")
}
link := filepath.Join(binDir, "babel-link")
os.Remove(link)
c.Assert(os.Symlink(babelJS, link), qt.IsNil)
resolved := resolveNodeBin(link)
t.Logf("Scoped symlink: link=%q, resolved=%q", link, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, babelJS), qt.IsTrue)
})
c.Run("Shell wrapper", func(c *qt.C) {
wrapper := filepath.Join(binDir, "postcss-sh")
content := "#!/bin/sh\n" +
`basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")` + "\n" +
`exec node "$basedir/../postcss-cli/index.js" "$@"` + "\n"
mkdirAndWrite(t, wrapper, content)
resolved := resolveNodeBin(wrapper)
t.Logf("Shell wrapper: wrapper=%q, resolved=%q", wrapper, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, postcssJS), qt.IsTrue)
})
c.Run("Cmd wrapper", func(c *qt.C) {
wrapper := filepath.Join(binDir, "postcss.cmd")
content := "@ECHO off\r\n" +
"SETLOCAL\r\n" +
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\postcss-cli\index.js" %*` + "\r\n"
mkdirAndWrite(t, wrapper, content)
resolved := resolveNodeBin(wrapper)
t.Logf("Cmd wrapper: wrapper=%q, resolved=%q", wrapper, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, postcssJS), qt.IsTrue)
})
c.Run("Cmd wrapper scoped package", func(c *qt.C) {
wrapper := filepath.Join(binDir, "babel.cmd")
content := "@ECHO off\r\n" +
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@babel\cli\bin\babel.js" %*` + "\r\n"
mkdirAndWrite(t, wrapper, content)
resolved := resolveNodeBin(wrapper)
t.Logf("Cmd wrapper (scoped): wrapper=%q, resolved=%q", wrapper, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, babelJS), qt.IsTrue)
})
c.Run("Node script with shebang", func(c *qt.C) {
script := filepath.Join(binDir, "node-global")
mkdirAndWrite(t, script, "#!/usr/bin/env node\nconsole.log('global');\n")
resolved := resolveNodeBin(script)
t.Logf("Node script: path=%q, resolved=%q", script, resolved)
c.Assert(resolved, qt.Equals, script)
})
c.Run("Native binary", func(c *qt.C) {
native := filepath.Join(binDir, "native-tool")
mkdirAndWrite(t, native, "\x7fELF\x00\x00\x00")
resolved := resolveNodeBin(native)
t.Logf("Native binary: path=%q, resolved=%q", native, resolved)
c.Assert(resolved, qt.Equals, "")
})
c.Run("Nonexistent file", func(c *qt.C) {
c.Assert(resolveNodeBin("/nonexistent/path"), qt.Equals, "")
})
c.Run("Wrapper with missing target", func(c *qt.C) {
wrapper := filepath.Join(binDir, "missing-target")
mkdirAndWrite(t, wrapper, "#!/bin/sh\nexec node \"$basedir/../no-such-pkg/index.js\" \"$@\"\n")
resolved := resolveNodeBin(wrapper)
t.Logf("Missing target: wrapper=%q, resolved=%q", wrapper, resolved)
c.Assert(resolved, qt.Equals, "")
})
}
func TestExtractNodeEntryPointRegex(t *testing.T) {
c := qt.New(t)
cases := []struct {
name string
content string
want string // expected capture group (with original separators)
}{
{"shell postcss", `"$basedir/../postcss-cli/index.js"`, "../postcss-cli/index.js"},
{"shell babel", `"$basedir/../@babel/cli/bin/babel.js"`, "../@babel/cli/bin/babel.js"},
{"shell tailwind mjs", `"$basedir/../@tailwindcss/cli/dist/index.mjs"`, "../@tailwindcss/cli/dist/index.mjs"},
{"cmd postcss", `"%dp0%\..\postcss-cli\index.js"`, `..\postcss-cli\index.js`},
{"cmd babel", `"%dp0%\..\@babel\cli\bin\babel.js"`, `..\@babel\cli\bin\babel.js`},
{"cmd postcss no ext", `"%dp0%\..\postcss-cli\bin\postcss"`, `..\postcss-cli\bin\postcss`},
{"shell postcss no ext", `"$basedir/../postcss-cli/bin/postcss"`, "../postcss-cli/bin/postcss"},
{"cmd postcss global", `"%dp0%\node_modules\postcss-cli\index.js"`, `node_modules\postcss-cli\index.js`},
{"cmd babel global", `"%dp0%\node_modules\@babel\cli\bin\babel.js"`, `node_modules\@babel\cli\bin\babel.js`},
{"shell postcss global", `"$basedir/node_modules/postcss-cli/index.js"`, "node_modules/postcss-cli/index.js"},
}
for _, tc := range cases {
c.Run(tc.name, func(c *qt.C) {
m := nodeEntryPointRe.FindStringSubmatch(tc.content)
t.Logf("regex match for %q: %v", tc.name, m)
c.Assert(m, qt.Not(qt.IsNil))
c.Assert(m[1], qt.Equals, tc.want)
})
}
c.Run("No match", func(c *qt.C) {
for _, s := range []string{"@ECHO off", "#!/bin/bash\necho hello", "\x7fELF"} {
c.Assert(nodeEntryPointRe.FindStringSubmatch(s), qt.IsNil)
}
})
}
// TestResolveNodeBinWindows tests wrapper resolution on all platforms
// by simulating Windows-style wrapper files. On Windows CI, this also
// tests the native .cmd resolution path.
func TestResolveNodeBinWindows(t *testing.T) {
c := qt.New(t)
dir := t.TempDir()
nodeModules := filepath.Join(dir, "node_modules")
binDir := filepath.Join(nodeModules, ".bin")
// Create target JS file.
targetJS := filepath.Join(nodeModules, "postcss-cli", "index.js")
mkdirAndWrite(t, targetJS, "#!/usr/bin/env node\nconsole.log('postcss');\n")
os.MkdirAll(binDir, 0o755)
// Simulate what npm creates on Windows: a .cmd wrapper and a shell script.
cmdWrapper := filepath.Join(binDir, "postcss.cmd")
cmdContent := "@ECHO off\r\n" +
"SETLOCAL\r\n" +
"CALL :find_dp0\r\n" +
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\postcss-cli\index.js" %*` + "\r\n"
mkdirAndWrite(t, cmdWrapper, cmdContent)
shWrapper := filepath.Join(binDir, "postcss")
shContent := "#!/bin/sh\n" +
`exec node "$basedir/../postcss-cli/index.js" "$@"` + "\n"
mkdirAndWrite(t, shWrapper, shContent)
t.Logf("GOOS=%s", runtime.GOOS)
t.Logf("cmd wrapper: %s", cmdWrapper)
t.Logf("sh wrapper: %s", shWrapper)
t.Logf("target JS: %s", targetJS)
c.Run("cmd wrapper resolves to JS", func(c *qt.C) {
resolved := resolveNodeBin(cmdWrapper)
t.Logf("resolveNodeBin(%q) = %q", cmdWrapper, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, targetJS), qt.IsTrue)
})
c.Run("sh wrapper resolves to JS", func(c *qt.C) {
resolved := resolveNodeBin(shWrapper)
t.Logf("resolveNodeBin(%q) = %q", shWrapper, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, targetJS), qt.IsTrue)
})
// Simulate postcss-cli 7, whose wrappers point to an extensionless Node
// shebang script (bin/postcss) rather than a .js file.
targetNoExt := filepath.Join(nodeModules, "postcss-cli", "bin", "postcss")
mkdirAndWrite(t, targetNoExt, "#!/usr/bin/env node\nrequire('../');\n")
cmdNoExt := filepath.Join(binDir, "postcssne.cmd")
mkdirAndWrite(t, cmdNoExt, "@ECHO off\r\n"+
`"%dp0%\..\postcss-cli\bin\postcss" %*`+"\r\n")
shNoExt := filepath.Join(binDir, "postcssne")
mkdirAndWrite(t, shNoExt, "#!/bin/sh\n"+
`exec node "$basedir/../postcss-cli/bin/postcss" "$@"`+"\n")
c.Run("cmd wrapper resolves to extensionless script", func(c *qt.C) {
resolved := resolveNodeBin(cmdNoExt)
t.Logf("resolveNodeBin(%q) = %q", cmdNoExt, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, targetNoExt), qt.IsTrue)
})
c.Run("sh wrapper resolves to extensionless script", func(c *qt.C) {
resolved := resolveNodeBin(shNoExt)
t.Logf("resolveNodeBin(%q) = %q", shNoExt, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, targetNoExt), qt.IsTrue)
})
// Simulate `npm install -g` on Windows: the wrapper sits at the npm
// global prefix and references node_modules as a child (no `..`).
globalDir := filepath.Join(dir, "global")
globalTarget := filepath.Join(globalDir, "node_modules", "postcss-cli", "index.js")
mkdirAndWrite(t, globalTarget, "#!/usr/bin/env node\nconsole.log('postcss');\n")
globalCmd := filepath.Join(globalDir, "postcss.cmd")
mkdirAndWrite(t, globalCmd, "@ECHO off\r\n"+
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\postcss-cli\index.js" %*`+"\r\n")
globalSh := filepath.Join(globalDir, "postcss")
mkdirAndWrite(t, globalSh, "#!/bin/sh\n"+
`exec node "$basedir/node_modules/postcss-cli/index.js" "$@"`+"\n")
c.Run("global cmd wrapper resolves to JS", func(c *qt.C) {
resolved := resolveNodeBin(globalCmd)
t.Logf("resolveNodeBin(%q) = %q", globalCmd, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, globalTarget), qt.IsTrue)
})
c.Run("global sh wrapper resolves to JS", func(c *qt.C) {
resolved := resolveNodeBin(globalSh)
t.Logf("resolveNodeBin(%q) = %q", globalSh, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, globalTarget), qt.IsTrue)
})
globalScopedTarget := filepath.Join(globalDir, "node_modules", "@babel", "cli", "bin", "babel.js")
mkdirAndWrite(t, globalScopedTarget, "#!/usr/bin/env node\nconsole.log('babel');\n")
globalScopedCmd := filepath.Join(globalDir, "babel.cmd")
mkdirAndWrite(t, globalScopedCmd, "@ECHO off\r\n"+
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\@babel\cli\bin\babel.js" %*`+"\r\n")
c.Run("global cmd wrapper scoped package", func(c *qt.C) {
resolved := resolveNodeBin(globalScopedCmd)
t.Logf("resolveNodeBin(%q) = %q", globalScopedCmd, resolved)
c.Assert(resolved, qt.Not(qt.Equals), "")
c.Assert(sameFile(t, resolved, globalScopedTarget), qt.IsTrue)
})
}
func sameFile(t *testing.T, a, b string) bool {
t.Helper()
infoA, errA := os.Stat(a)
infoB, errB := os.Stat(b)
if errA != nil || errB != nil {
t.Logf("sameFile: stat errors: a=%v, b=%v", errA, errB)
return false
}
return os.SameFile(infoA, infoB)
}
func mkdirAndWrite(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
t.Fatal(err)
}
}
+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
}
@@ -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"
@@ -1,4 +1,4 @@
// Copyright 2025 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"
+2 -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"
@@ -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"
@@ -1,4 +1,4 @@
// Copyright 2025 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 2025 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"
@@ -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 (
"errors"
@@ -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"
+1 -1
View File
@@ -96,7 +96,7 @@ func errConvert(v reflect.Value, s string) error {
// See Issue 14079.
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
switch val.Kind() {
case reflect.Ptr, reflect.Interface:
case reflect.Pointer, reflect.Interface:
if val.IsNil() {
// Return typ's zero value.
return reflect.Zero(typ), true
+10 -10
View File
@@ -20,14 +20,14 @@ 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
return kind == reflect.Interface || kind == reflect.Pointer
}
// TODO(bep) replace the private versions in /tpl with these.
@@ -92,7 +92,7 @@ func IsSlice(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Slice
}
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
var zeroType = reflect.TypeFor[types.Zeroer]()
var isZeroCache sync.Map
@@ -136,7 +136,7 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
@@ -216,8 +216,8 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
}
var (
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
timeType = reflect.TypeFor[time.Time]()
asTimeProviderType = reflect.TypeFor[htime.AsTimeProvider]()
)
// IsTime returns whether tp is a time.Time type or if it can be converted into one
@@ -240,7 +240,7 @@ func IsValid(v reflect.Value) bool {
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return !v.IsNil()
}
@@ -347,15 +347,15 @@ func IsNil(v reflect.Value) bool {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
}
return false
}
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
var contextInterface = reflect.TypeFor[context.Context]()
var isContextCache = maps.NewCache[reflect.Type, bool]()
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
type k string
+19
View File
@@ -97,6 +97,25 @@ 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
}
func HasUppercase(s string) bool {
for _, r := range s {
if 'A' <= r && r <= 'Z' {
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 {
+10
View File
@@ -71,6 +71,16 @@ func TestUniqueStringsSorted(t *testing.T) {
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
}
func TestHasUppercase(t *testing.T) {
c := qt.New(t)
c.Assert(HasUppercase("abc"), qt.Equals, false)
c.Assert(HasUppercase("Abc"), qt.Equals, true)
c.Assert(HasUppercase("aBc"), qt.Equals, true)
c.Assert(HasUppercase("abC"), qt.Equals, true)
c.Assert(HasUppercase("ABC"), qt.Equals, true)
}
// 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"}
+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"
+18 -18
View File
@@ -18,10 +18,10 @@ import (
"strings"
"time"
"github.com/bep/golocales"
"github.com/bep/clocks"
"github.com/spf13/cast"
"github.com/gohugoio/locales"
)
var (
@@ -78,9 +78,9 @@ var (
Clock = clocks.System()
)
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
if ltr == nil {
panic("must provide a locales.Translator")
panic("must provide a golocales.Translator")
}
return TimeFormatter{
ltr: ltr,
@@ -89,7 +89,7 @@ func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
// TimeFormatter is locale aware.
type TimeFormatter struct {
ltr locales.Translator
ltr golocales.Translator
}
func (f TimeFormatter) Format(t time.Time, layout string) string {
@@ -101,39 +101,39 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
// It may be one of Hugo's custom layouts.
switch strings.ToLower(layout[1:]) {
case "date_full":
return f.ltr.FmtDateFull(t)
return f.ltr.FormatDateFull(t)
case "date_long":
return f.ltr.FmtDateLong(t)
return f.ltr.FormatDateLong(t)
case "date_medium":
return f.ltr.FmtDateMedium(t)
return f.ltr.FormatDateMedium(t)
case "date_short":
return f.ltr.FmtDateShort(t)
return f.ltr.FormatDateShort(t)
case "time_full":
return f.ltr.FmtTimeFull(t)
return f.ltr.FormatTimeFull(t)
case "time_long":
return f.ltr.FmtTimeLong(t)
return f.ltr.FormatTimeLong(t)
case "time_medium":
return f.ltr.FmtTimeMedium(t)
return f.ltr.FormatTimeMedium(t)
case "time_short":
return f.ltr.FmtTimeShort(t)
return f.ltr.FormatTimeShort(t)
}
}
s := t.Format(layout)
monthIdx := t.Month() - 1 // Month() starts at 1.
monthIdx := t.Month() - 1 // time.Month is 1-based, but our month name slices are 0-based.
dayIdx := t.Weekday()
if strings.Contains(layout, "January") {
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthsWide()[monthIdx])
} else if strings.Contains(layout, "Jan") {
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthsAbbreviated()[monthIdx])
}
if strings.Contains(layout, "Monday") {
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdaysWide()[dayIdx])
} else if strings.Contains(layout, "Mon") {
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdaysAbbreviated()[dayIdx])
}
return s
+19 -18
View File
@@ -17,8 +17,9 @@ import (
"testing"
"time"
"github.com/bep/golocales"
qt "github.com/frankban/quicktest"
translators "github.com/gohugoio/localescompressed"
)
func TestTimeFormatter(t *testing.T) {
@@ -34,7 +35,7 @@ func TestTimeFormatter(t *testing.T) {
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
c.Run("Norsk nynorsk", func(c *qt.C) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
f := NewTimeFormatter(golocales.New("nn"))
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
@@ -42,35 +43,35 @@ func TestTimeFormatter(t *testing.T) {
})
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
f := NewTimeFormatter(golocales.New("nn"))
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.18")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "02:09:37 UTC")
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
})
c.Run("Custom layouts English", func(c *qt.C) {
f := NewTimeFormatter(translators.GetTranslator("en"))
f := NewTimeFormatter(golocales.New("en"))
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37\u202fam UTC")
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37\u202fam UTC")
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37\u202fam")
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09\u202fam")
})
c.Run("English", func(c *qt.C) {
f := NewTimeFormatter(translators.GetTranslator("en"))
f := NewTimeFormatter(golocales.New("en"))
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
@@ -78,31 +79,31 @@ func TestTimeFormatter(t *testing.T) {
})
c.Run("Weekdays German", func(c *qt.C) {
tr := translators.GetTranslator("de")
tr := golocales.New("de")
f := NewTimeFormatter(tr)
// Issue #9107
for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} {
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman)
c.Assert(tr.WeekdaysWide()[date.Weekday()], qt.Equals, weekDayWideGerman)
c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman)
}
for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} {
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
c.Assert(tr.WeekdaysAbbreviated()[date.Weekday()], qt.Equals, weekDayAbbreviatedGerman)
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
}
})
c.Run("Months German", func(c *qt.C) {
tr := translators.GetTranslator("de")
tr := golocales.New("de")
f := NewTimeFormatter(tr)
// Issue #9107
for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} {
date := jan06.Add(time.Duration(i*24*31) * time.Hour)
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
c.Assert(tr.MonthsWide()[date.Month()-1], qt.Equals, monthWideNorway)
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
}
})
@@ -121,7 +122,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
})
b.Run("Localized", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
f := NewTimeFormatter(golocales.New("nn"))
b.ResetTimer()
for b.Loop() {
got := f.Format(june06, "Monday Jan 2 2006")
@@ -132,7 +133,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
})
b.Run("Localized Custom", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
f := NewTimeFormatter(golocales.New("nn"))
b.ResetTimer()
for b.Loop() {
got := f.Format(june06, ":date_medium")
+43 -9
View File
@@ -19,19 +19,37 @@ import (
"strings"
)
// ReadSeeker wraps io.Reader and io.Seeker.
type ReadSeeker interface {
io.Reader
io.Seeker
}
// ReadSeekCloser is implemented by afero.File. We use this as the common type for
// content in Resource objects, even for strings.
type ReadSeekCloser interface {
ReadSeeker
io.ReadSeeker
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
@@ -46,7 +64,7 @@ type ReadSeekCloserProvider interface {
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
type readSeekerNopCloser struct {
ReadSeeker
io.ReadSeeker
}
// Close does nothing.
@@ -55,7 +73,7 @@ func (r readSeekerNopCloser) Close() error {
}
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
return readSeekerNopCloser{r}
}
@@ -87,6 +105,22 @@ func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
return readSeekerNopCloser{bytes.NewReader(content)}
}
// NewReadSeekerNoOpCloserFromReader creates a new ReadSeekerNoOpCloser from the given io.Reader.
// If the given io.Reader is not an io.ReadSeeker, the entire content will be read into memory.
func NewReadSeekerNoOpCloserFromReader(r io.Reader) (readSeekerNopCloser, error) {
var rs io.ReadSeeker
if s, ok := r.(io.ReadSeeker); ok {
rs = s
} else {
b, err := io.ReadAll(r)
if err != nil {
return readSeekerNopCloser{rs}, err
}
rs = bytes.NewReader(b)
}
return readSeekerNopCloser{rs}, nil
}
// NewOpenReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
// The ReadSeeker will be seeked to the beginning before returned.
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
+25 -125
View File
@@ -16,7 +16,6 @@ package hugo
import (
"context"
"fmt"
"html/template"
"os"
"path/filepath"
"runtime/debug"
@@ -25,17 +24,16 @@ import (
"sync"
"time"
"github.com/bep/helpers/contexthelpers"
"github.com/bep/logg"
"github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hstore"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/version"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/bep/helpers/contexthelpers"
"github.com/spf13/afero"
iofs "io/fs"
@@ -56,89 +54,24 @@ var (
vendorInfo string
)
var _ hstore.StoreProvider = (*HugoInfo)(nil)
// HugoInfo contains information about the current Hugo environment
type HugoInfo struct {
CommitHash string
BuildDate string
// The build environment.
// Defaults are "production" (hugo) and "development" (hugo server).
// This can also be set by the user.
// It can be any string, but it will be all lower case.
Environment string
// version of go that the Hugo binary was built with
GoVersion string
conf ConfigProvider
deps []*Dependency
store *hstore.Scratch
// Context gives access to some of the context scoped variables.
Context Context
// BuildInfo holds build information extracted from runtime/debug.
type BuildInfo struct {
Revision string
RevisionTime string
GoVersion string
}
// Version returns the current version as a comparable version string.
func (i HugoInfo) Version() version.VersionString {
return CurrentVersion.Version()
}
// Generator a Hugo meta generator HTML tag.
func (i HugoInfo) Generator() template.HTML {
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
}
// IsDevelopment reports whether the current running environment is "development".
func (i HugoInfo) IsDevelopment() bool {
return i.Environment == EnvironmentDevelopment
}
// IsProduction reports whether the current running environment is "production".
func (i HugoInfo) IsProduction() bool {
return i.Environment == EnvironmentProduction
}
// IsServer reports whether the built-in server is running.
func (i HugoInfo) IsServer() bool {
return i.conf.Running()
}
// IsExtended reports whether the Hugo binary is the extended version.
func (i HugoInfo) IsExtended() bool {
return IsExtended
}
// WorkingDir returns the project working directory.
func (i HugoInfo) WorkingDir() string {
return i.conf.WorkingDir()
}
// Deps gets a list of dependencies for this Hugo build.
func (i HugoInfo) Deps() []*Dependency {
return i.deps
}
func (i HugoInfo) Store() *hstore.Scratch {
return i.store
}
// Deprecated: Use hugo.IsMultihost instead.
func (i HugoInfo) IsMultiHost() bool {
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
return i.conf.IsMultihost()
}
// IsMultihost reports whether each configured language has a unique baseURL.
func (i HugoInfo) IsMultihost() bool {
return i.conf.IsMultihost()
}
// IsMultilingual reports whether there are two or more configured languages.
func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual()
// GetBuildInfo returns the build info for the current binary.
func GetBuildInfo() *BuildInfo {
bi := getBuildInfo()
if bi == nil {
return nil
}
return &BuildInfo{
Revision: bi.Revision,
RevisionTime: bi.RevisionTime,
GoVersion: bi.GoVersion,
}
}
type contextKey uint8
@@ -149,6 +82,7 @@ const (
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
// Context gives access to some of the context scoped variables.
type Context struct{}
func (c Context) MarkupScope(ctx context.Context) string {
@@ -165,51 +99,13 @@ func GetMarkupScope(ctx context.Context) string {
return markupScope.Get(ctx)
}
// ConfigProvider represents the config options that are relevant for HugoInfo.
type ConfigProvider interface {
Environment() string
Running() bool
WorkingDir() string
IsMultihost() bool
IsMultilingual() bool
}
// NewInfo creates a new Hugo Info object.
func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
if conf.Environment() == "" {
panic("environment not set")
}
var (
commitHash string
buildDate string
goVersion string
)
bi := getBuildInfo()
if bi != nil {
commitHash = bi.Revision
buildDate = bi.RevisionTime
goVersion = bi.GoVersion
}
return HugoInfo{
CommitHash: commitHash,
BuildDate: buildDate,
Environment: conf.Environment(),
conf: conf,
deps: deps,
store: hstore.NewScratch(),
GoVersion: goVersion,
}
}
// GetExecEnviron creates and gets the common os/exec environment used in the
// external programs we interact with via os/exec, e.g. postcss.
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
var env []string
nodepath := filepath.Join(workDir, "node_modules")
if np := os.Getenv("NODE_PATH"); np != "" {
nodepath = workDir + string(os.PathListSeparator) + np
nodepath = nodepath + string(os.PathListSeparator) + np
}
config.SetEnvVars(&env, "NODE_PATH", nodepath)
config.SetEnvVars(&env, "PWD", workDir)
@@ -310,13 +206,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"),
)
}
@@ -439,8 +334,13 @@ func deprecateLevel(item, alternative, version string, level logg.Level) {
// DeprecateLevel informs about a deprecation logging at the given level.
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) {
//if strings.Contains(item, "module.mounts.lang") || strings.Contains(item, "includeFiles") {
// hdebug.Panicf("Deprecated")
//}
var msg string
if level == logg.LevelError {
// Useful to debug deprecation errors that needs to be removedor fixed. Comment out when done debugging.
// hdebug.Panicf("deprecation error: %s was removed in Hugo %s. %s", item, version, alternative)
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
} else {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
+37 -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 }}
`
@@ -75,3 +75,39 @@ multihost={{ hugo.IsMultihost }}
"multihost=false",
)
}
func TestHugoSites(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
defaultContentLanguage = 'fr'
defaultContentLanguageInSubdir = true
defaultContentVersionInSubdir = true
defaultContentRoleInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.de]
weight = 3
[roles]
[roles.guest]
weight = 1
[roles.member]
weight = 2
[versions]
[versions.'v1.0.0']
weight = 1
[versions.'v2.0.0']
weight = 2
-- layouts/home.html --
{{ range hugo.Sites }}{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}|{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/guest/v1.0.0/en/index.html", "en-guest-v1.0.0|en-member-v1.0.0|en-guest-v2.0.0|en-member-v2.0.0|fr-guest-v1.0.0|fr-member-v1.0.0|fr-guest-v2.0.0|fr-member-v2.0.0|de-guest-v1.0.0|de-member-v1.0.0|de-guest-v2.0.0|de-member-v2.0.0|")
}
+12 -63
View File
@@ -15,43 +15,12 @@ package hugo
import (
"context"
"fmt"
"testing"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/version"
)
func TestHugoInfo(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
hugoInfo := NewInfo(conf, nil)
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
bi := getBuildInfo()
if bi != nil {
c.Assert(hugoInfo.CommitHash, qt.Equals, bi.Revision)
c.Assert(hugoInfo.BuildDate, qt.Equals, bi.RevisionTime)
c.Assert(hugoInfo.GoVersion, qt.Equals, bi.GoVersion)
}
c.Assert(hugoInfo.Environment, qt.Equals, "production")
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false)
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
}
func TestDeprecationLogLevelFromVersion(t *testing.T) {
c := qt.New(t)
@@ -66,47 +35,27 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
// Added just to find the threshold for where we can remove deprecated items.
// Subtract 5 from the minor version of the first ERRORed version => 0.122.0.
c.Assert(deprecationLogLevelFromVersion("0.127.0"), qt.Equals, logg.LevelError)
// Subtract 5 from the minor version of the first ERRORed version => 0.136.0.
c.Assert(deprecationLogLevelFromVersion("0.141.0"), qt.Equals, logg.LevelError)
}
func TestMarkupScope(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
info := NewInfo(conf, nil)
ctx := context.Background()
ctx = SetMarkupScope(ctx, "foo")
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
var hugoCtx Context
c.Assert(hugoCtx.MarkupScope(ctx), qt.Equals, "foo")
c.Assert(GetMarkupScope(ctx), qt.Equals, "foo")
}
type testConfig struct {
environment string
running bool
workingDir string
multihost bool
multilingual bool
}
func TestGetBuildInfo(t *testing.T) {
c := qt.New(t)
func (c testConfig) Environment() string {
return c.environment
}
func (c testConfig) Running() bool {
return c.running
}
func (c testConfig) WorkingDir() string {
return c.workingDir
}
func (c testConfig) IsMultihost() bool {
return c.multihost
}
func (c testConfig) IsMultilingual() bool {
return c.multilingual
bi := GetBuildInfo()
// In test mode, build info may or may not be available.
if bi != nil {
c.Assert(bi.GoVersion, qt.Not(qt.Equals), "")
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ import "github.com/gohugoio/hugo/common/version"
// This should be the only one.
var CurrentVersion = version.Version{
Major: 0,
Minor: 153,
PatchLevel: 0,
Suffix: "-DEV",
Minor: 161,
PatchLevel: 1,
Suffix: "",
}
+15 -2
View File
@@ -20,6 +20,7 @@ import (
"io"
"os"
"strings"
"sync"
"time"
"github.com/bep/logg"
@@ -123,8 +124,11 @@ func New(opts Options) Logger {
)
l := logger.WithLevel(opts.Level)
logMu := &sync.Mutex{}
reset := func() {
logMu.Lock()
defer logMu.Unlock()
logCounters.mu.Lock()
defer logCounters.mu.Unlock()
logCounters.counters = make(map[logg.Level]int)
@@ -135,6 +139,7 @@ func New(opts Options) Logger {
}
return &logAdapter{
mu: logMu,
logCounters: logCounters,
errors: errorsw,
reset: reset,
@@ -173,6 +178,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
@@ -203,6 +209,7 @@ type Logger interface {
}
type logAdapter struct {
mu *sync.Mutex
logCounters *logLevelCounter
errors *strings.Builder
reset func()
@@ -221,6 +228,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...)
}
@@ -275,7 +286,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
if milli < 500 {
return
}
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli)
fmt.Fprintf(l.stdErr, "%s in %v ms\n", name, milli)
}
func (l *logAdapter) Printf(format string, v ...any) {
@@ -323,6 +334,8 @@ func (l *logAdapter) Errorln(v ...any) {
}
func (l *logAdapter) Errors() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.errors.String()
}
@@ -339,7 +352,7 @@ func (l *logAdapter) Warnidf(id, format string, v ...any) {
}
func (l *logAdapter) idfInfoStatement(what, id, format string) string {
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your project configuration:\nignoreLogs = ['%s']", what, id)
}
func (l *logAdapter) Trace(s logg.StringFunc) {
-117
View File
@@ -1,117 +0,0 @@
// 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 maps
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()
}
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) {
m.mu.Lock()
defer m.mu.Unlock()
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
}
}
}
}
-71
View File
@@ -1,71 +0,0 @@
// 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 maps
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) {
m["f"] = 500
})
v, found = m.Lookup("f")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 500)
}
-82
View File
@@ -100,19 +100,6 @@ func AddLeadingAndTrailingSlash(path string) string {
return AddTrailingSlash(AddLeadingSlash(path))
}
// MakeTitle converts the path given to a suitable title, trimming whitespace
// and replacing hyphens with whitespace.
func MakeTitle(inpath string) string {
return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
}
// ReplaceExtension takes a path and an extension, strips the old extension
// and returns the path with the new extension.
func ReplaceExtension(path string, newExt string) string {
f, _ := fileAndExt(path, fpb)
return f + "." + newExt
}
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories {
if after, ok := strings.CutPrefix(inPath, currentPath); ok {
@@ -201,75 +188,6 @@ func extractFilename(in, ext, base, pathSeparator string) (name string) {
return
}
// GetRelativePath returns the relative path of a given path.
func GetRelativePath(path, base string) (final string, err error) {
if filepath.IsAbs(path) && base == "" {
return "", errors.New("source: missing base directory")
}
name := filepath.Clean(path)
base = filepath.Clean(base)
name, err = filepath.Rel(base, name)
if err != nil {
return "", err
}
if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
name += FilePathSeparator
}
return name, nil
}
func prettifyPath(in string, b filepathPathBridge) string {
if filepath.Ext(in) == "" {
// /section/name/ -> /section/name/index.html
if len(in) < 2 {
return b.Separator()
}
return b.Join(in, "index.html")
}
name, ext := fileAndExt(in, b)
if name == "index" {
// /section/name/index.html -> /section/name/index.html
return b.Clean(in)
}
// /section/name.html -> /section/name/index.html
return b.Join(b.Dir(in), name, "index"+ext)
}
// CommonDirPath returns the common directory of the given paths.
func CommonDirPath(path1, path2 string) string {
if path1 == "" || path2 == "" {
return ""
}
hadLeadingSlash := strings.HasPrefix(path1, "/") || strings.HasPrefix(path2, "/")
path1 = TrimLeading(path1)
path2 = TrimLeading(path2)
p1 := strings.Split(path1, "/")
p2 := strings.Split(path2, "/")
var common []string
for i := 0; i < len(p1) && i < len(p2); i++ {
if p1[i] == p2[i] {
common = append(common, p1[i])
} else {
break
}
}
s := strings.Join(common, "/")
if hadLeadingSlash && s != "" {
s = "/" + s
}
return s
}
// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
// a predefined set of special Unicode characters.
//
+22 -100
View File
@@ -20,38 +20,6 @@ import (
qt "github.com/frankban/quicktest"
)
func TestGetRelativePath(t *testing.T) {
tests := []struct {
path string
base string
expect any
}{
{filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
{filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
{filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")},
{filepath.FromSlash("/c"), "", false},
}
for i, this := range tests {
// ultimately a fancy wrapper around filepath.Rel
result, err := GetRelativePath(this.path, this.base)
if b, ok := this.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] GetRelativePath didn't return an expected error", i)
}
} else {
if err != nil {
t.Errorf("[%d] GetRelativePath failed: %s", i, err)
continue
}
if result != this.expect {
t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect)
}
}
}
}
func TestMakePathRelative(t *testing.T) {
type test struct {
inPath, path1, path2, output string
@@ -75,54 +43,6 @@ func TestMakePathRelative(t *testing.T) {
}
}
func TestMakeTitle(t *testing.T) {
type test struct {
input, expected string
}
data := []test{
{"Make-Title", "Make Title"},
{"MakeTitle", "MakeTitle"},
{"make_title", "make_title"},
}
for i, d := range data {
output := MakeTitle(d.input)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
}
}
// Replace Extension is probably poorly named, but the intent of the
// function is to accept a path and return only the file name with a
// new extension. It's intentionally designed to strip out the path
// and only provide the name. We should probably rename the function to
// be more explicit at some point.
func TestReplaceExtension(t *testing.T) {
type test struct {
input, newext, expected string
}
data := []test{
// These work according to the above definition
{"/some/random/path/file.xml", "html", "file.html"},
{"/banana.html", "xml", "banana.xml"},
{"./banana.html", "xml", "banana.xml"},
{"banana/pie/index.html", "xml", "index.xml"},
{"../pies/fish/index.html", "xml", "index.xml"},
// but these all fail
{"filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
{"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
{"/directory/mydir/", "ext", ".ext"},
{"mydir/", "ext", ".ext"},
}
for i, d := range data {
output := ReplaceExtension(filepath.FromSlash(d.input), d.newext)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
}
}
func TestExtNoDelimiter(t *testing.T) {
c := qt.New(t)
c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json")
@@ -263,26 +183,6 @@ func TestFieldsSlash(t *testing.T) {
c.Assert(FieldsSlash(""), qt.DeepEquals, []string{})
}
func TestCommonDirPath(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
a, b, expected string
}{
{"/a/b/c", "/a/b/d", "/a/b"},
{"/a/b/c", "a/b/d", "/a/b"},
{"a/b/c", "/a/b/d", "/a/b"},
{"a/b/c", "a/b/d", "a/b"},
{"/a/b/c", "/a/b/c", "/a/b/c"},
{"/a/b/c", "/a/b/c/d", "/a/b/c"},
{"/a/b/c", "/a/b", "/a/b"},
{"/a/b/c", "/a", "/a"},
{"/a/b/c", "/d/e/f", ""},
} {
c.Assert(CommonDirPath(this.a, this.b), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}
func TestIsSameFilePath(t *testing.T) {
c := qt.New(t)
@@ -347,3 +247,25 @@ func BenchmarkAddLeadingSlash(b *testing.B) {
}
})
}
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
}
}
}
+286 -60
View File
@@ -21,19 +21,165 @@ import (
"strings"
"sync"
"github.com/gohugoio/hugo/common/maps"
"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"
identifierCurstomWrapper = "_"
identifierBaseof = "baseof"
identifierCustomWrapper = "_"
)
// Known prefixes for ._prefix_value_. identifiers.
const (
prefixLanguage = "language_"
prefixVersion = "version_"
prefixRole = "role_"
prefixOutputFormat = "outputformat_"
prefixKind = "kind_"
prefixLayout = "layout_"
)
// isCustomWrapperIdentifier tells whether a supplied path is of the form _xyz_.
// must have non-empty content between the identifierCustomWrapper's to pass.
func isCustomWrapperIdentifier(s string) bool {
return len(s) > 2*len(identifierCustomWrapper) &&
strings.HasPrefix(s, identifierCustomWrapper) &&
strings.HasSuffix(s, identifierCustomWrapper)
}
// parsePrefixIdentifier parses inner content of a wrapper block (with outer underscores stripped)
// and returns the prefix. The prefix match is case-insensitive.
// Returns empty string if not a known prefix.
func parsePrefixIdentifier(inner string) string {
innerLower := strings.ToLower(inner)
for _, p := range []string{prefixLanguage, prefixVersion, prefixRole, prefixOutputFormat, prefixKind, prefixLayout} {
if strings.HasPrefix(innerLower, p) {
return p
}
}
return ""
}
// findWrapperDotPositions returns positions of dots inside ._..._. wrapper blocks.
// These dots should be skipped during the main backward dot-scanning loop,
// so that wrapper blocks are treated as single opaque segments.
func findWrapperDotPositions(s string) []int {
lastSlash := strings.LastIndex(s, "/")
if lastSlash == -1 {
return nil
}
var skipDots []int
i := lastSlash
for i < len(s) {
// Look for ._ which could start a wrapper block.
if i+2 < len(s) && s[i] == '.' && s[i+1] == '_' {
// Find the closing _. or _ at end of string.
end := -1
for j := i + 2; j < len(s); j++ {
if s[j] == '/' {
break
}
if s[j] == '_' && (j+1 >= len(s) || s[j+1] == '.') {
end = j
break
}
}
if end != -1 {
// Record dot positions inside the wrapper block.
for j := i + 2; j < end; j++ {
if s[j] == '.' {
skipDots = append(skipDots, j)
}
}
i = end + 1
continue
}
}
i++
}
return skipDots
}
// isSkippedDot reports whether position i is in the sorted skipDots slice.
func isSkippedDot(skipDots []int, i int) bool {
for _, d := range skipDots {
if d == i {
return true
}
if d > i {
break
}
}
return false
}
// applyPrefixIdentifier validates and stores a prefix identifier.
// id is the value-only LowHigh (e.g. pointing at "v1.0.0" in p.s, not the full wrapper).
// Returns true if the identifier was recognized and applied.
func (pp *PathParser) applyPrefixIdentifier(component, prefix string, p *Path, id types.LowHigh[string]) bool {
value := strings.ToLower(p.s[id.Low:id.High])
switch prefix {
case prefixLanguage:
if pp.LanguageIndex != nil {
if _, ok := pp.LanguageIndex[value]; ok {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierPrefixLanguages = append(p.posIdentifierPrefixLanguages, len(p.identifiersKnown)-1)
return true
}
if pp.IsLangDisabled != nil && pp.IsLangDisabled(value) {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierPrefixLanguages = append(p.posIdentifierPrefixLanguages, len(p.identifiersKnown)-1)
p.disabled = true
return true
}
}
case prefixVersion:
if pp.ConfiguredDimensions != nil {
if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(value); idx >= 0 {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierVersions = append(p.posIdentifierVersions, len(p.identifiersKnown)-1)
return true
}
}
case prefixRole:
if pp.ConfiguredDimensions != nil {
if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(value); idx >= 0 {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierRoles = append(p.posIdentifierRoles, len(p.identifiersKnown)-1)
return true
}
}
case prefixOutputFormat:
if component == files.ComponentFolderLayouts && pp.IsOutputFormat != nil {
if pp.IsOutputFormat(value, "") {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierPrefixOutputFormat = len(p.identifiersKnown) - 1
return true
}
}
case prefixKind:
if component == files.ComponentFolderLayouts {
if kinds.GetKindMain(value) != "" {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierPrefixKind = len(p.identifiersKnown) - 1
return true
}
}
case prefixLayout:
if component == files.ComponentFolderLayouts {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierPrefixLayout = len(p.identifiersKnown) - 1
return true
}
}
return false
}
// PathParser parses and manages paths.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
@@ -54,12 +200,12 @@ type PathParser struct {
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
pp.initOnce.Do(func() {
pp.sitesMatrixCache = maps.NewCache[string, sitesmatrix.VectorStore]()
pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]()
})
}
@@ -76,14 +222,36 @@ func NormalizePathStringBasic(s string) string {
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
pp.init()
lang := p.Lang()
v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
langs := p.Langs()
versions := p.Versions()
roles := p.Roles()
lang := p.Lang() // First or dot-based language.
// Cache by the full site-selection identity derived from the path:
// languages, selected language, versions, and roles.
cacheKey := strings.Join(langs, ",") + "/" + lang + "/" + strings.Join(versions, ",") + "|" + strings.Join(roles, ",")
v, _ := pp.sitesMatrixCache.GetOrCreate(cacheKey, func() (sitesmatrix.VectorStore, error) {
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
if lang != "" {
if len(langs) > 0 {
for _, l := range langs {
if idx, ok := pp.LanguageIndex[l]; ok {
builder.WithLanguageIndices(idx)
}
}
} else if lang != "" {
if idx, ok := pp.LanguageIndex[lang]; ok {
builder.WithLanguageIndices(idx)
}
}
for _, version := range versions {
if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(version); idx >= 0 {
builder.WithVersionIndices(idx)
}
}
for _, role := range roles {
if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(role); idx >= 0 {
builder.WithRoleIndices(idx)
}
}
switch p.Component() {
case files.ComponentFolderContent:
@@ -100,13 +268,6 @@ func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
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)
defer putPath(p)
return identity.StringIdentity(p.IdentifierBase())
}
// ParseBaseAndBaseNameNoIdentifier parses component c with path s into a base and a base name without any identifier.
func (pp *PathParser) ParseBaseAndBaseNameNoIdentifier(c, s string) (string, string) {
p := pp.parsePooled(c, s)
@@ -188,13 +349,25 @@ 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 isCustomWrapperIdentifier(sid) {
inner := sid[1 : len(sid)-1]
if prefix := parsePrefixIdentifier(inner); prefix != "" {
// Value-only LowHigh: skip leading _ + prefix, trailing _
valueID := types.LowHigh[string]{Low: id.Low + 1 + len(prefix), High: id.High - 1}
if pp.applyPrefixIdentifier(component, prefix, p, valueID) {
found = true
}
}
if !found {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierCustom = len(p.identifiersKnown) - 1
found = true
}
}
if len(p.identifiersKnown) == 0 {
if found {
// Already handled (e.g. prefix wrapper).
} else if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
found = true
@@ -268,6 +441,18 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
}
}
if found {
if isLast {
// The isLast identifier starts right at the container boundary.
// Treat it as part of the name (e.g. layout name "list" in list.no.html).
if p.posNameHigh <= 0 {
p.posNameHigh = lastDot
}
} else {
p.posNameHigh = i // The '.' before this identifier.
}
}
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
@@ -292,10 +477,14 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
}
p.s = s
// Find dots inside ._..._. wrapper blocks that must be skipped.
skipDots := findWrapperDotPositions(s)
slashCount := 0
lastDot := 0
lastSlashIdx := strings.LastIndex(s, "/")
numDots := strings.Count(s[lastSlashIdx+1:], ".")
numDots := strings.Count(s[lastSlashIdx+1:], ".") - len(skipDots)
if strings.Contains(s, "/_shortcodes/") {
p.pathType = TypeShortcode
}
@@ -305,6 +494,9 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
switch c {
case '.':
if isSkippedDot(skipDots, i) {
continue
}
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false)
lastDot = i
case '/':
@@ -323,13 +515,19 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
}
}
// Compute the name boundary.
if p.posNameHigh >= p.posContainerHigh {
p.posIdentifierName = types.LowHigh[string]{Low: p.posContainerHigh, High: p.posNameHigh}
} else {
p.posIdentifierName = types.LowHigh[string]{Low: p.posContainerHigh, High: len(p.s)}
}
if len(p.identifiersKnown) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiersKnown[len(p.identifiersKnown)-1]
if id.Low > p.posContainerHigh {
b := p.s[p.posContainerHigh : id.Low-1]
if p.posIdentifierName.Low >= p.posContainerHigh && p.posIdentifierName.High > p.posIdentifierName.Low {
b := p.s[p.posIdentifierName.Low:p.posIdentifierName.High]
if isContent {
switch b {
case "index":
@@ -437,7 +635,22 @@ type Path struct {
posIdentifierLayout int
posIdentifierBaseof int
posIdentifierCustom int
disabled bool
// Prefix identifier positions (indices into identifiersKnown).
posIdentifierPrefixLanguages []int
posIdentifierVersions []int
posIdentifierRoles []int
posIdentifierPrefixOutputFormat int
posIdentifierPrefixKind int
posIdentifierPrefixLayout int
// Name boundary, computed during parse.
posIdentifierName types.LowHigh[string]
// Position of the dot before the leftmost known identifier.
// Set during parseIdentifier, used to compute posIdentifierName.
posNameHigh int
disabled bool
trimLeadingSlash bool
@@ -475,6 +688,14 @@ func (p *Path) reset() {
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.posIdentifierCustom = -1
p.posIdentifierPrefixLanguages = p.posIdentifierPrefixLanguages[:0]
p.posIdentifierVersions = p.posIdentifierVersions[:0]
p.posIdentifierRoles = p.posIdentifierRoles[:0]
p.posIdentifierPrefixOutputFormat = -1
p.posIdentifierPrefixKind = -1
p.posIdentifierPrefixLayout = -1
p.posIdentifierName = types.LowHigh[string]{}
p.posNameHigh = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@@ -566,16 +787,6 @@ func (p *Path) NameNoExt() string {
return p.s[p.posContainerHigh:]
}
// Name returns the last element of path without any language identifier.
func (p *Path) NameNoLang() string {
i := p.identifierIndex(p.posIdentifierLanguage)
if i == -1 {
return p.Name()
}
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:]
}
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
// For bundles this will be the containing directory's name, e.g. "blog".
func (p *Path) BaseNameNoIdentifier() string {
@@ -592,21 +803,7 @@ func (p *Path) NameNoIdentifier() string {
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiersKnown) > 0 {
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
if p.posContainerHigh == lastID.Low {
// The last identifier is the name.
return lastID
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
}
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: len(p.s),
}
return p.posIdentifierName
}
// Dir returns all but the last element of path, typically the path's directory.
@@ -638,6 +835,9 @@ func (p *Path) Unnormalized() *Path {
// PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string {
if len(p.posIdentifierPrefixLanguages) > 0 {
return p.base(true, false)
}
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
return p.Path()
}
@@ -717,11 +917,6 @@ func (p *Path) BaseReTyped(typ string) (d string) {
return
}
// BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:]
}
func (p *Path) base(preserveExt, isBundle bool) string {
if len(p.identifiersKnown) == 0 {
return p.norm(p.s)
@@ -759,27 +954,58 @@ func (p *Path) Ext() string {
}
func (p *Path) OutputFormat() string {
if p.posIdentifierPrefixOutputFormat != -1 {
return p.identifierAsString(p.posIdentifierPrefixOutputFormat)
}
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
if p.posIdentifierPrefixKind != -1 {
return p.identifierAsString(p.posIdentifierPrefixKind)
}
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Layout() string {
if p.posIdentifierPrefixLayout != -1 {
return p.identifierAsString(p.posIdentifierPrefixLayout)
}
return p.identifierAsString(p.posIdentifierLayout)
}
func (p *Path) Lang() string {
if len(p.posIdentifierPrefixLanguages) > 0 {
return p.identifierAsString(p.posIdentifierPrefixLanguages[0])
}
return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
func (p *Path) Langs() []string {
return p.identifiersAsStrings(p.posIdentifierPrefixLanguages)
}
func (p *Path) Identifier(i int) string {
return p.identifierAsString(i)
func (p *Path) Versions() []string {
return p.identifiersAsStrings(p.posIdentifierVersions)
}
func (p *Path) Roles() []string {
return p.identifiersAsStrings(p.posIdentifierRoles)
}
func (p *Path) identifiersAsStrings(positions []int) []string {
if len(positions) == 0 {
return nil
}
ids := make([]string, len(positions))
for i, pos := range positions {
ids[i] = p.identifierAsString(pos)
}
return ids
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCustomWrapper), identifierCustomWrapper)
}
func (p *Path) Disabled() bool {
+273 -18
View File
@@ -15,6 +15,7 @@ package paths
import (
"path/filepath"
"reflect"
"testing"
"github.com/gohugoio/hugo/hugofs/files"
@@ -25,7 +26,7 @@ import (
)
func newTestParser() *PathParser {
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3", "v1.0.0"}, []string{"admin", "editor", "viewer", "guest"})
return &PathParser{
LanguageIndex: map[string]int{
@@ -141,7 +142,6 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b")
c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
@@ -178,11 +178,9 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
@@ -223,7 +221,6 @@ func TestParse(t *testing.T) {
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.NameNoExt(), qt.Equals, "index")
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
c.Assert(p.Section(), qt.Equals, "")
},
},
@@ -245,7 +242,6 @@ func TestParse(t *testing.T) {
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.NameNoExt(), qt.Equals, "index.no")
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
c.Assert(p.Section(), qt.Equals, "a")
@@ -265,7 +261,6 @@ func TestParse(t *testing.T) {
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.NameNoExt(), qt.Equals, "_index.no")
c.Assert(p.NameNoLang(), qt.Equals, "_index.md")
},
},
{
@@ -389,6 +384,142 @@ func TestParse(t *testing.T) {
c.Assert(p.Custom(), qt.Equals, "myid")
},
},
{
"Prefix language",
"/a/b/p1._language_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.Identifiers(), qt.DeepEquals, []string{"md", "no"})
},
},
{
"Prefix version",
"/a/b/p1._version_v1_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "v1"})
},
},
{
"Prefix version with dots",
"/a/b/p1._version_v1.0.0_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1.0.0"})
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Prefix role",
"/a/b/p1._role_admin_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin"})
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Multiple prefixes",
"/a/b/p1._language_no_._role_admin_._version_v2_.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.Roles(), qt.DeepEquals, []string{"admin"})
c.Assert(p.Versions(), qt.DeepEquals, []string{"v2"})
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Prefix version with dots and role",
"/a/b/p1._version_v1.0.0_._role_editor_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1.0.0"})
c.Assert(p.Roles(), qt.DeepEquals, []string{"editor"})
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Prefix with dot-based lang",
"/a/b/p1._version_v1_.no.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Prefix leaf bundle with version",
"/a/b/index._version_v1_.md",
func(c *qt.C, p *Path) {
c.Assert(p.IsLeafBundle(), qt.IsTrue)
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
},
},
{
"Prefix branch bundle with role",
"/a/b/_index._role_admin_.md",
func(c *qt.C, p *Path) {
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin"})
c.Assert(p.Base(), qt.Equals, "/a/b")
},
},
{
"Prefix with mixed case, unnormalized",
"/a/b/My Page._Version_V1_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/my-page")
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
pp := p.Unnormalized()
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "My Page")
},
},
{
"Multiple languages",
"/a/b/p1._language_en_._language_fr_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
// Right-to-left parse order.
c.Assert(p.Lang(), qt.Equals, "fr")
c.Assert(p.Langs(), qt.DeepEquals, []string{"fr", "en"})
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Multiple roles",
"/a/b/p1._role_guest_._role_admin_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin", "guest"})
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
"Multiple versions",
"/a/b/p1._version_v1_._version_v2_.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Versions(), qt.DeepEquals, []string{"v2", "v1"})
},
},
{
"Unknown prefix not extracted",
"/a/b/p1._unknown_foo_.md",
func(c *qt.C, p *Path) {
// Unknown prefix should be left in path and treated as custom wrapper.
c.Assert(p.Custom(), qt.Equals, "unknown_foo")
c.Assert(p.Versions(), qt.IsNil)
c.Assert(p.Roles(), qt.IsNil)
},
},
}
parser := newTestParser()
for _, test := range tests {
@@ -607,20 +738,81 @@ func TestParseLayouts(t *testing.T) {
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"})
},
},
{
"Prefix language layout",
"/page._language_no_.html",
func(c *qt.C, p *Path) {
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.OutputFormat(), qt.Equals, "html")
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
{
"Prefix outputformat layout",
"/page._outputformat_amp_.html",
func(c *qt.C, p *Path) {
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
{
"Prefix kind layout",
"/page._kind_section_.html",
func(c *qt.C, p *Path) {
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
{
"Prefix layout layout",
"/page._layout_list_.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
{
"All prefix identifiers in layout",
"/page._language_fr_._kind_section_._outputformat_amp_._layout_list_.html",
func(c *qt.C, p *Path) {
c.Assert(p.Lang(), qt.Equals, "fr")
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
{
"Prefix version in layout",
"/page._version_v2_.html",
func(c *qt.C, p *Path) {
c.Assert(p.Versions(), qt.DeepEquals, []string{"v2"})
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
{
"Prefix role in layout",
"/page._role_guest_.html",
func(c *qt.C, p *Path) {
c.Assert(p.Roles(), qt.DeepEquals, []string{"guest"})
c.Assert(p.Base(), qt.Equals, "/page.html")
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Not lang" {
return
}
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
})
}
@@ -635,22 +827,85 @@ func TestHasExt(t *testing.T) {
c.Assert(HasExt("/a/b.c/d"), qt.IsFalse)
}
func BenchmarkParseIdentity(b *testing.B) {
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})
// With version prefix.
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._version_v2_.fr.md")
v = parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.HasVersion(1), qt.IsTrue) // v2 is index 1
c.Assert(v.LenVectors(), qt.Equals, 1)
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 1, 0})
// With version and role prefixes.
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._version_v1_._role_editor_.fr.md")
v = parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.HasVersion(0), qt.IsTrue) // v1 is index 0
c.Assert(v.HasRole(1), qt.IsTrue) // editor is index 1
c.Assert(v.LenVectors(), qt.Equals, 1)
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 1})
// With multiple roles.
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._role_guest_._role_admin_.fr.md")
v = parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.HasRole(0), qt.IsTrue) // admin is index 0
c.Assert(v.HasRole(3), qt.IsTrue) // guest is index 3
c.Assert(v.LenVectors(), qt.Equals, 2)
// With multiple languages via prefix.
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._language_en_._language_fr_.md")
v = parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(0), qt.IsFalse) // no is index 0, not included
c.Assert(v.HasLanguage(1), qt.IsTrue) // en is index 1
c.Assert(v.HasLanguage(2), qt.IsTrue) // fr is index 2
c.Assert(v.LenVectors(), qt.Equals, 2)
}
func FuzzParsePath(f *testing.F) {
componentPaths := []struct {
component string
path string
}{
{files.ComponentFolderContent, "/a/b/c.fr.md"},
{files.ComponentFolderContent, "/a/b/c._version_v2_.fr.md"},
{files.ComponentFolderContent, "/a/b/c._version_v1_._role_editor_.fr.md"},
{files.ComponentFolderContent, "/a/b/c._role_guest_._role_admin_.fr.md"},
{files.ComponentFolderContent, "/a/b/c._language_en_._language_fr_.md"},
{files.ComponentFolderLayouts, "/list.no.html"},
{files.ComponentFolderLayouts, "/page._language_fr_._kind_section_._outputformat_amp_._layout_list_.html"},
}
for _, cp := range componentPaths {
f.Add(cp.component, cp.path)
}
parser := newTestParser()
f.Fuzz(func(t *testing.T, c, s string) {
p := parser.Parse(c, s)
if p == nil {
t.Fatalf("Parse returned nil for path: %q", s)
}
// Execute all the methods using reflection to ensure they don't panic.
v := reflect.ValueOf(p)
for i := 0; i < v.NumMethod(); i++ {
method := v.Type().Method(i)
if method.Type.NumIn() == 1 {
method.Func.Call([]reflect.Value{v})
}
}
})
}
func BenchmarkSitesMatrixFromPath(b *testing.B) {
+70 -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)
@@ -79,6 +79,72 @@ disablePathToLower = true
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
}
func TestPrefixIdentifiersContent(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
-- content/p1.md --
---
title: p1 default
---
-- content/p1._language_fr_.md --
---
title: p1 french
---
-- layouts/single.html --
{{ .Language.Lang }}|{{ .Title }}|Single.
-- layouts/list.html --
List
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/en/p1/index.html", "en|p1 default|Single")
b.AssertFileContent("public/fr/p1/index.html", "fr|p1 french|Single")
}
func TestPrefixIdentifiersLayouts(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term"]
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
-- content/p1.md --
---
title: p1
---
-- content/p1.fr.md --
---
title: p1 fr
---
-- layouts/single.html --
default|{{ .Title }}|
-- layouts/single._language_fr_.html --
french layout|{{ .Title }}|
-- layouts/list.html --
List
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/en/p1/index.html", "default|p1|")
b.AssertFileContent("public/fr/p1/index.html", "french layout|p1 fr|")
}
func TestIssue13596(t *testing.T) {
t.Parallel()
@@ -0,0 +1,2 @@
go test fuzz v1
string("fr..")
-58
View File
@@ -102,64 +102,6 @@ func AddContextRoot(baseURL, relativePath string) string {
return newPath
}
// URLizeAn
// PrettifyURL takes a URL string and returns a semantic, clean URL.
func PrettifyURL(in string) string {
x := PrettifyURLPath(in)
if path.Base(x) == "index.html" {
return path.Dir(x)
}
if in == "" {
return "/"
}
return x
}
// PrettifyURLPath takes a URL path to a content and converts it
// to enable pretty URLs.
//
// /section/name.html becomes /section/name/index.html
// /section/name/ becomes /section/name/index.html
// /section/name/index.html becomes /section/name/index.html
func PrettifyURLPath(in string) string {
return prettifyPath(in, pb)
}
// Uglify does the opposite of PrettifyURLPath().
//
// /section/name/index.html becomes /section/name.html
// /section/name/ becomes /section/name.html
// /section/name.html becomes /section/name.html
func Uglify(in string) string {
if path.Ext(in) == "" {
if len(in) < 2 {
return "/"
}
// /section/name/ -> /section/name.html
return path.Clean(in) + ".html"
}
name, ext := fileAndExt(in, pb)
if name == "index" {
// /section/name/index.html -> /section/name.html
d := path.Dir(in)
if len(d) > 1 {
return d + ext
}
return in
}
// /.xml -> /index.xml
if name == "" {
return path.Dir(in) + "index" + ext
}
// /section/name.html -> /section/name.html
return path.Clean(in)
}
// URLEscape escapes unicode letters.
func URLEscape(uri string) string {
// escape unicode letters
-34
View File
@@ -15,8 +15,6 @@ package paths
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestMakePermalink(t *testing.T) {
@@ -66,35 +64,3 @@ func TestAddContextRoot(t *testing.T) {
}
}
}
func TestPretty(t *testing.T) {
c := qt.New(t)
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html"))
c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html"))
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/"))
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html"))
c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html"))
c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml"))
c.Assert("/", qt.Equals, PrettifyURLPath("/"))
c.Assert("/", qt.Equals, PrettifyURLPath(""))
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html"))
c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html"))
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/"))
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html"))
c.Assert("/", qt.Equals, PrettifyURL("/index.html"))
c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml"))
c.Assert("/", qt.Equals, PrettifyURL("/"))
c.Assert("/", qt.Equals, PrettifyURL(""))
}
func TestUgly(t *testing.T) {
c := qt.New(t)
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html"))
c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html"))
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/"))
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html"))
c.Assert("/index.html", qt.Equals, Uglify("/index.html"))
c.Assert("/name.xml", qt.Equals, Uglify("/name.xml"))
c.Assert("/", qt.Equals, Uglify("/"))
c.Assert("/", qt.Equals, Uglify(""))
}
+111 -2
View File
@@ -153,6 +153,35 @@ func (p PR[T]) FilterCopy(s []T) []T {
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) {
@@ -187,6 +216,86 @@ func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string)
return p.BoolFunc(), nil
}
type IndexMatcher interface {
IndexMatch(match P[string]) (iter.Seq[int], error)
// 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)
}
+101
View File
@@ -146,6 +146,107 @@ func TestNewStringPredicateFromGlobs(t *testing.T) {
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
+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")
}
+2 -4
View File
@@ -56,9 +56,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
queue := NewEvictingQueue[string](3)
for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
queue.Add(val)
v := queue.Peek()
if v != val {
@@ -68,7 +66,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
if len(vals) != 1 || vals[0] != val {
t.Error("wrong val")
}
}()
})
}
wg.Wait()
}
+1 -1
View File
@@ -89,7 +89,7 @@ func IsNil(v any) bool {
value := reflect.ValueOf(v)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return value.IsNil()
}
+25 -4
View File
@@ -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,17 @@ var (
_ compare.Comparer = (*VersionString)(nil)
)
// IsAlphaBetaOrRC returns whether this version is an alpha, beta, or release candidate.
func (v Version) IsAlphaBetaOrRC() bool {
s := strings.ToLower(v.Suffix)
// e.g. "alpha.1", "beta.2", "rc.3"
return strings.Contains(s, "alpha.") || strings.Contains(s, "beta.") || strings.Contains(s, "rc.")
}
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)
}
@@ -102,6 +116,7 @@ func ParseVersion(s string) (Version, error) {
vv.Suffix = suffix
}
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
vv.source = s
return vv, nil
}
@@ -169,6 +184,10 @@ func CompareVersions(v1 Version, v2 any) int {
case int64:
c = compareFloatWithVersion(float64(d), v1)
case Version:
if v1.IsZero() && d.IsZero() {
// Fall back to source comparison.
return strings.Compare(v1.source, d.source)
}
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
return strings.Compare(v1.Suffix, d.Suffix)
}
@@ -193,11 +212,13 @@ func CompareVersions(v1 Version, v2 any) int {
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 v1.Compare(v)
return v1.Compare(v2v)
}
+14
View File
@@ -50,6 +50,11 @@ func TestHugoVersion(t *testing.T) {
func TestCompareVersions(t *testing.T) {
c := qt.New(t)
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)
@@ -69,6 +74,15 @@ func TestCompareVersions(t *testing.T) {
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) {
+106 -49
View File
@@ -29,10 +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"
@@ -181,7 +181,7 @@ type Config struct {
Minify minifiers.MinifyConfig `mapstructure:"-"`
// Permalink configuration.
Permalinks map[string]map[string]string `mapstructure:"-"`
Permalinks page.PermalinksConfig `mapstructure:"-"`
// Taxonomy configuration.
Taxonomies map[string]string `mapstructure:"-"`
@@ -212,7 +212,7 @@ type Config struct {
// User provided parameters.
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
Params maps.Params `mapstructure:"-"`
Params hmaps.Params `mapstructure:"-"`
// UglyURLs configuration. Either a boolean or a sections map.
UglyURLs any `mapstructure:"-"`
@@ -399,43 +399,93 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
return err
}
// Legacy paginate values.
if c.Paginate != 0 {
hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger())
c.Pagination.PagerSize = c.Paginate
// Legacy language values.
if c.LanguageCode != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger("project config key languageCode", "Use locale instead.", "v0.158.0", logger.Logger())
}
if c.Locale == "" {
c.Locale = c.LanguageCode
}
c.LanguageCode = ""
}
if c.PaginatePath != "" {
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
c.Pagination.Path = c.PaginatePath
for k, v := range c.Languages.Config.LanguageConfigs {
//lint:ignore SA1019 Keep as adapter for now.
if v.LanguageCode != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageCode", k), fmt.Sprintf("Use languages.%s.locale instead.", k), "v0.158.0", logger.Logger())
}
if v.Locale == "" {
//lint:ignore SA1019 Keep as adapter for now.
v.Locale = v.LanguageCode
}
//lint:ignore SA1019 Keep as adapter for now.
v.LanguageCode = ""
}
//lint:ignore SA1019 Keep as adapter for now.
if v.LanguageName != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageName", k), fmt.Sprintf("Use languages.%s.label instead.", k), "v0.158.0", logger.Logger())
}
if v.Label == "" {
//lint:ignore SA1019 Keep as adapter for now.
v.Label = v.LanguageName
}
//lint:ignore SA1019 Keep as adapter for now.
v.LanguageName = ""
}
//lint:ignore SA1019 Keep as adapter for now.
if v.LanguageDirection != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageDirection", k), fmt.Sprintf("Use languages.%s.direction instead.", k), "v0.158.0", logger.Logger())
}
if v.Direction == "" {
//lint:ignore SA1019 Keep as adapter for now.
v.Direction = v.LanguageDirection
}
//lint:ignore SA1019 Keep as adapter for now.
v.LanguageDirection = ""
}
c.Languages.Config.LanguageConfigs[k] = v
// Sorted is a snapshot of LanguageConfigs taken at decode time; keep it
// in sync so Configs.Init, which reads from Sorted, sees the migrated values.
for i, s := range c.Languages.Config.Sorted {
if s.Name == k {
c.Languages.Config.Sorted[i].LanguageConfig = v
break
}
}
}
// Legacy privacy values.
if c.Privacy.Twitter.Disable {
hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("project config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
}
if c.Privacy.Twitter.EnableDNT {
hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("project config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
}
if c.Privacy.Twitter.Simple {
hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("project config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
}
// Legacy services values.
if c.Services.Twitter.DisableInlineCSS {
hugo.DeprecateWithLogger("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("project config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
}
// Legacy permalink tokens
vs := fmt.Sprintf("%v", c.Permalinks)
if strings.Contains(vs, ":filename") {
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
}
if strings.Contains(vs, ":slugorfilename") {
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
for _, pc := range c.Permalinks {
if strings.Contains(pc.Pattern, ":filename") {
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
}
if strings.Contains(pc.Pattern, ":slugorfilename") {
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
}
}
// Legacy render hook values.
@@ -446,7 +496,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
)
if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil {
alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
hugo.DeprecateWithLogger("project config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault {
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
} else {
@@ -455,7 +505,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
}
if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil {
alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
hugo.DeprecateWithLogger("project config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault {
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
} else {
@@ -471,10 +521,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("project 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("project config markup.goldmark.renderHooks.link.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
}
c.C = &ConfigCompiled{
@@ -607,8 +657,14 @@ type RootConfig struct {
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
@@ -675,20 +731,15 @@ type RootConfig struct {
// The configured environment. Default is "development" for server and "production" for build.
Environment string
// The default language code.
LanguageCode string
// Deprecated: Use Locale instead.
LanguageCode string `json:"-"`
// The default locale.
Locale string
// Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words.
HasCJKLanguage bool
// The default number of pages per page when paginating.
// Deprecated: Use the Pagination struct.
Paginate int
// The path to use when creating pagination URLs, e.g. "page" in /page/2/.
// Deprecated: Use the Pagination struct.
PaginatePath string
// Whether to pluralize default list titles.
// Note that this currently only works for English, but you can provide your own title in the content file's front matter.
PluralizeListTitles bool
@@ -816,6 +867,7 @@ type Configs struct {
Modules modules.Modules
ModulesClient *modules.Client
FileCaches filecache.Caches
// All below is set in Init.
Languages langs.Languages
@@ -846,7 +898,7 @@ func (c *Configs) IsZero() bool {
return c == nil || len(c.Languages) == 0
}
func (c *Configs) Init(logger loggers.Logger) error {
func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
var languages langs.Languages
for _, f := range c.Base.Languages.Config.Sorted {
@@ -854,7 +906,7 @@ func (c *Configs) Init(logger loggers.Logger) error {
if !found {
return fmt.Errorf("invalid language configuration for %q", f.Name)
}
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig, logger)
if err != nil {
return err
}
@@ -915,10 +967,9 @@ func (c *Configs) Init(logger loggers.Logger) error {
// avoid initializing the same config more than once.
for i, l := range c.Languages {
langConfig := c.LanguageConfigMap[l.Lang]
sitesMatrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithLanguageIndices(i).WithAllIfNotSet().Build()
for _, s := range allDecoderSetups {
if getInitializer := s.getInitializer; getInitializer != nil {
if err := getInitializer(langConfig).InitConfig(logger, sitesMatrix, c.ConfiguredDimensions); err != nil {
if err := getInitializer(langConfig).InitConfig(logger, nil, c.ConfiguredDimensions); err != nil {
return err
}
}
@@ -1018,7 +1069,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
@@ -1045,11 +1096,11 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
var differentRootKeys []string
switch x := v.(type) {
case nil:
case maps.Params:
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 {
@@ -1061,16 +1112,16 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
isMultihost = true
}
if p, ok := vv.(maps.Params); ok {
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 = maps.CloneParamsDeep(p)
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.(maps.Params))
page.AddLangToCascadeTargetMap(k, vv.(hmaps.Params))
// Always clone cascade config to get the sites matrix right.
differentRootKeys = append(differentRootKeys, kk)
}
@@ -1081,14 +1132,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
// 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.
// 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:
@@ -1098,7 +1149,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.
@@ -1134,7 +1185,7 @@ 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))
@@ -1149,10 +1200,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,
}
@@ -39,13 +39,11 @@ title: "p1"
---
title: "p1"
---
-- layouts/_default/single.html --
-- layouts/single.html --
Title: {{ .Title }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
).Build()
b := hugolib.Test(t, files)
// b.AssertFileContent("public/p1/index.html", "Title: p1")
@@ -81,9 +79,7 @@ baseURL = "https://example.com"
logI18nWarnings = true
logPathWarnings = true
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
).Build()
b := hugolib.Test(t, files)
conf := b.H.Configs.Base
@@ -141,7 +137,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 }}
@@ -191,7 +187,7 @@ func TestInvalidOutputFormat(t *testing.T) {
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[outputs]
home = ['html','foo']
-- layouts/index.html --
-- layouts/home.html --
x
`
+27 -29
View File
@@ -22,9 +22,9 @@ import (
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
@@ -196,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)
@@ -225,28 +225,18 @@ var allDecoderSetups = map[string]decodeWeight{
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
m := maps.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 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")
}
}
}
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
// Root-level locale/languageCode is passed to DecodeConfig so it can
// be applied to the default content language inside langs.DecodeConfig.
// They are passed separately so that an explicit per-lang languageCode
// can override a root-level languageCode (but not a root-level locale).
rootLocale := p.p.GetString("locale")
rootLanguageCode := 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)
p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, rootLocale, rootLanguageCode, p.c.RootConfig.DisableLanguages, m)
if err != nil {
return fmt.Errorf("failed to decode languages config: %w", err)
}
@@ -266,7 +256,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "versions",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
return err
},
@@ -278,7 +268,7 @@ var allDecoderSetups = map[string]decodeWeight{
err error
defaultContentRole string
)
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
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
@@ -289,7 +279,7 @@ var allDecoderSetups = map[string]decodeWeight{
"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)
}
@@ -317,9 +307,10 @@ var allDecoderSetups = map[string]decodeWeight{
key: "permalinks",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.GetStringMap(d.key))
p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.Get(d.key))
return err
},
getInitializer: func(c *Config) configInitializer { return c.Permalinks },
},
"sitemap": {
key: "sitemap",
@@ -335,7 +326,14 @@ 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))
m := hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
// Remove invalid entries (e.g. non-taxonomy keys placed inside [taxonomies] in TOML).
for k, v := range m {
if k == "" || v == "" {
delete(m, k)
}
}
p.c.Taxonomies = m
}
return nil
},
@@ -447,7 +445,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,
@@ -455,7 +453,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,
@@ -469,8 +467,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)
}
+10 -3
View File
@@ -25,9 +25,8 @@ import (
)
type ConfigLanguage struct {
config *Config
baseConfig config.BaseConfig
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
languageIndex int
@@ -123,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
}
@@ -135,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
}
+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)
+18 -9
View File
@@ -25,21 +25,22 @@ import (
"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/hglob"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/modules/npm"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
)
//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")
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new project.\n Run `hugo help new` for details.\n")
func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
defer func() {
@@ -95,7 +96,11 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
configs.Modules = moduleConfig.AllModules
configs.ModulesClient = modulesClient
if err := configs.Init(d.Logger); err != nil {
if !d.SkipNpmCheck && npm.NpmPackNeedsUpdate(d.Fs, configs.Modules) {
d.Logger.Warnln(`npm dependencies are out of sync, please run "hugo mod npm pack" (you may also want to run "npm install" after that)`)
}
if err := configs.Init(d.Fs, d.Logger); err != nil {
return nil, fmt.Errorf("failed to init config: %w", err)
}
@@ -127,6 +132,9 @@ type ConfigSourceDescriptor struct {
// If set, this will be used to ignore the module does not exist error.
IgnoreModuleDoesNotExist bool
// If set, skip the npm pack staleness check (used by hugo mod npm pack).
SkipNpmCheck bool
}
func (d ConfigSourceDescriptor) configFilenames() []string {
@@ -166,7 +174,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",
@@ -179,14 +187,14 @@ func (l configLoader) applyDefaultConfig() error {
func (l configLoader) normalizeCfg(cfg config.Provider) error {
if b, ok := cfg.Get("minifyOutput").(bool); ok {
hugo.Deprecate("site config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
hugo.Deprecate("project 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")
hugo.Deprecate("project config minify", "Use minify.minifyOutput instead.", "v0.150.0")
if b {
cfg.Set("minify", maps.Params{"minifyOutput": true})
cfg.Set("minify", hmaps.Params{"minifyOutput": true})
}
}
@@ -240,7 +248,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
}
@@ -477,6 +485,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,
@@ -547,7 +556,7 @@ func (l configLoader) loadConfig(configName string) (string, error) {
}
func (l configLoader) deleteMergeStrategies() (err error) {
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
l.cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
+1 -1
View File
@@ -437,7 +437,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error {
return nil
}
return func(ss string) bool {
match = targetRe.MatchString(ss)
match := targetRe.MatchString(ss)
matchString := "no match"
if match {
matchString = "match!"
+3 -3
View File
@@ -20,11 +20,11 @@ import (
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
)
@@ -222,11 +222,11 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
return cfg, dirnames, nil
}
var keyAliases maps.KeyRenamer
var keyAliases hmaps.KeyRenamer
func init() {
var err error
keyAliases, err = maps.NewKeyRenamer(
keyAliases, err = hmaps.NewKeyRenamer(
// Before 0.53 we used singular for "menu".
"{menu,languages/*/menu}", "menus",
)
+6 -4
View File
@@ -16,7 +16,7 @@ package config
import (
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
@@ -41,6 +41,7 @@ type AllProvider interface {
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
FileCaches() any
ContentTypes() ContentTypesProvider
GetConfigSection(string) any
GetConfig() any
@@ -79,6 +80,7 @@ type AllProvider interface {
WorkingDir() string
EnableEmoji() bool
ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions
CacheDirMisc() string
}
// We cannot import the media package as that would create a circular dependency.
@@ -95,7 +97,7 @@ type Provider interface {
GetString(key string) string
GetInt(key string) int
GetBool(key string) bool
GetParams(key string) maps.Params
GetParams(key string) hmaps.Params
GetStringMap(key string) map[string]any
GetStringMapString(key string) map[string]string
GetStringSlice(key string) []string
@@ -103,9 +105,9 @@ type Provider interface {
Set(key string, value any)
Keys() []string
Merge(key string, value any)
SetDefaults(params maps.Params)
SetDefaults(params hmaps.Params)
SetDefaultMergeStrategy()
WalkParams(walkFn func(params ...maps.KeyParams) bool)
WalkParams(walkFn func(params ...hmaps.KeyParams) bool)
IsSet(key string) bool
}
+49 -50
View File
@@ -20,21 +20,20 @@ import (
"strings"
"sync"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
)
// New creates a Provider backed by an empty maps.Params.
// New creates a Provider backed by an empty hmaps.Params.
func New() Provider {
return &defaultConfigProvider{
root: make(maps.Params),
root: make(hmaps.Params),
}
}
// NewFrom creates a Provider backed by params.
func NewFrom(params maps.Params) Provider {
maps.PrepareParams(params)
func NewFrom(params hmaps.Params) Provider {
hmaps.PrepareParams(params)
return &defaultConfigProvider{
root: params,
}
@@ -44,7 +43,7 @@ func NewFrom(params maps.Params) Provider {
// All methods are thread safe.
type defaultConfigProvider struct {
mu sync.RWMutex
root maps.Params
root hmaps.Params
keyCache sync.Map
}
@@ -90,22 +89,22 @@ func (c *defaultConfigProvider) GetString(k string) string {
return cast.ToString(v)
}
func (c *defaultConfigProvider) GetParams(k string) maps.Params {
func (c *defaultConfigProvider) GetParams(k string) hmaps.Params {
v := c.Get(k)
if v == nil {
return nil
}
return v.(maps.Params)
return v.(hmaps.Params)
}
func (c *defaultConfigProvider) GetStringMap(k string) map[string]any {
v := c.Get(k)
return maps.ToStringMap(v)
return hmaps.ToStringMap(v)
}
func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
v := c.Get(k)
return maps.ToStringMapString(v)
return hmaps.ToStringMapString(v)
}
func (c *defaultConfigProvider) GetStringSlice(k string) []string {
@@ -120,9 +119,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
k = strings.ToLower(k)
if k == "" {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
if p, err := hmaps.ToParamsAndPrepare(v); err == nil {
// Set the values directly in root.
maps.SetParams(c.root, p)
hmaps.SetParams(c.root, p)
} else {
c.root[k] = v
}
@@ -132,7 +131,7 @@ func (c *defaultConfigProvider) Set(k string, v any) {
switch vv := v.(type) {
case map[string]any, map[any]any, map[string]string:
p := maps.MustToParamsAndPrepare(vv)
p := hmaps.MustToParamsAndPrepare(vv)
v = p
}
@@ -142,9 +141,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
}
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
maps.SetParams(p1, p2)
if p1, ok := existing.(hmaps.Params); ok {
if p2, ok := v.(hmaps.Params); ok {
hmaps.SetParams(p1, p2)
return
}
}
@@ -154,8 +153,8 @@ func (c *defaultConfigProvider) Set(k string, v any) {
}
// SetDefaults will set values from params if not already set.
func (c *defaultConfigProvider) SetDefaults(params maps.Params) {
maps.PrepareParams(params)
func (c *defaultConfigProvider) SetDefaults(params hmaps.Params) {
hmaps.PrepareParams(params)
for k, v := range params {
if _, found := c.root[k]; !found {
c.root[k] = v
@@ -170,28 +169,28 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
if k == "" {
rs, f := c.root.GetMergeStrategy()
if f && rs == maps.ParamsMergeStrategyNone {
if f && rs == hmaps.ParamsMergeStrategyNone {
// The user has set a "no merge" strategy on this,
// nothing more to do.
return
}
if p, err := maps.ToParamsAndPrepare(v); err == nil {
if p, err := hmaps.ToParamsAndPrepare(v); err == nil {
// As there may be keys in p not in root, we need to handle
// those as a special case.
var keysToDelete []string
for kk, vv := range p {
if pp, ok := vv.(maps.Params); ok {
if pp, ok := vv.(hmaps.Params); ok {
if pppi, ok := c.root[kk]; ok {
ppp := pppi.(maps.Params)
maps.MergeParamsWithStrategy("", ppp, pp)
ppp := pppi.(hmaps.Params)
hmaps.MergeParamsWithStrategy("", ppp, pp)
} else {
// We need to use the default merge strategy for
// this key.
np := make(maps.Params)
strategy := c.determineMergeStrategy(maps.KeyParams{Key: "", Params: c.root}, maps.KeyParams{Key: kk, Params: np})
np := make(hmaps.Params)
strategy := c.determineMergeStrategy(hmaps.KeyParams{Key: "", Params: c.root}, hmaps.KeyParams{Key: kk, Params: np})
np.SetMergeStrategy(strategy)
maps.MergeParamsWithStrategy("", np, pp)
hmaps.MergeParamsWithStrategy("", np, pp)
c.root[kk] = np
if np.IsZero() {
// Just keep it until merge is done.
@@ -201,7 +200,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
}
// Merge the rest.
maps.MergeParams(c.root, p)
hmaps.MergeParams(c.root, p)
for _, k := range keysToDelete {
delete(c.root, k)
}
@@ -214,7 +213,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
switch vv := v.(type) {
case map[string]any, map[any]any, map[string]string:
p := maps.MustToParamsAndPrepare(vv)
p := hmaps.MustToParamsAndPrepare(vv)
v = p
}
@@ -224,9 +223,9 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
maps.MergeParamsWithStrategy("", p1, p2)
if p1, ok := existing.(hmaps.Params); ok {
if p2, ok := v.(hmaps.Params); ok {
hmaps.MergeParamsWithStrategy("", p1, p2)
}
}
} else {
@@ -245,10 +244,10 @@ func (c *defaultConfigProvider) Keys() []string {
return keys
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) {
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...hmaps.KeyParams) bool) {
maxDepth := 1000
var walk func(depth int, params ...maps.KeyParams)
walk = func(depth int, params ...maps.KeyParams) {
var walk func(depth int, params ...hmaps.KeyParams)
walk = func(depth int, params ...hmaps.KeyParams) {
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
@@ -258,24 +257,24 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams)
p1 := params[len(params)-1]
i := len(params)
for k, v := range p1.Params {
if p2, ok := v.(maps.Params); ok {
paramsplus1 := make([]maps.KeyParams, i+1)
if p2, ok := v.(hmaps.Params); ok {
paramsplus1 := make([]hmaps.KeyParams, i+1)
copy(paramsplus1, params)
paramsplus1[i] = maps.KeyParams{Key: k, Params: p2}
paramsplus1[i] = hmaps.KeyParams{Key: k, Params: p2}
walk(depth+1, paramsplus1...)
}
}
}
walk(0, maps.KeyParams{Key: "", Params: c.root})
walk(0, hmaps.KeyParams{Key: "", Params: c.root})
}
func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy {
func (c *defaultConfigProvider) determineMergeStrategy(params ...hmaps.KeyParams) hmaps.ParamsMergeStrategy {
if len(params) == 0 {
return maps.ParamsMergeStrategyNone
return hmaps.ParamsMergeStrategyNone
}
var (
strategy maps.ParamsMergeStrategy
strategy hmaps.ParamsMergeStrategy
prevIsRoot bool
curr = params[len(params)-1]
)
@@ -299,10 +298,10 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams)
// Don't set a merge strategy on the root unless set by user.
// This will be handled as a special case.
case "params":
strategy = maps.ParamsMergeStrategyDeep
strategy = hmaps.ParamsMergeStrategyDeep
case "outputformats", "mediatypes":
if prevIsRoot {
strategy = maps.ParamsMergeStrategyShallow
strategy = hmaps.ParamsMergeStrategyShallow
}
case "menus":
isMenuKey := prevIsRoot
@@ -314,11 +313,11 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams)
}
}
if isMenuKey {
strategy = maps.ParamsMergeStrategyShallow
strategy = hmaps.ParamsMergeStrategyShallow
}
default:
if strategy == "" {
strategy = maps.ParamsMergeStrategyNone
strategy = hmaps.ParamsMergeStrategyNone
}
}
@@ -326,7 +325,7 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams)
}
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
c.WalkParams(func(params ...maps.KeyParams) bool {
c.WalkParams(func(params ...hmaps.KeyParams) bool {
if len(params) == 0 {
return false
}
@@ -344,7 +343,7 @@ func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
})
}
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, hmaps.Params) {
var parts []string
v, ok := c.keyCache.Load(key)
if ok {
@@ -358,14 +357,14 @@ func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (str
next, found := current[parts[i]]
if !found {
if create {
next = make(maps.Params)
next = make(hmaps.Params)
current[parts[i]] = next
} else {
return "", nil
}
}
var ok bool
current, ok = next.(maps.Params)
current, ok = next.(hmaps.Params)
if !ok {
// E.g. a string, not a map that we can store values in.
return "", nil
+31 -32
View File
@@ -22,10 +22,9 @@ import (
"strings"
"testing"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/common/maps"
qt "github.com/frankban/quicktest"
)
@@ -48,7 +47,7 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Assert(cfg.Get(k), qt.Equals, v)
c.Assert(cfg.GetInt(k), qt.Equals, v)
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"foo": 42,
})
})
@@ -60,7 +59,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"bar": "baz",
})
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("foo"), qt.DeepEquals, hmaps.Params{
"bar": "baz",
})
@@ -76,14 +75,14 @@ func TestDefaultConfigProvider(t *testing.T) {
})
cfg.Set("a.c", "cv")
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
"b": "bv",
"c": "cv",
})
c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
cfg.Set("b.a", "av")
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("b"), qt.DeepEquals, hmaps.Params{
"a": "av",
})
@@ -91,7 +90,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv",
})
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("b"), qt.DeepEquals, hmaps.Params{
"a": "av",
"b": "bv",
})
@@ -105,7 +104,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"a": "av2",
"b": "bv2",
})
@@ -118,7 +117,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"a": "av",
"b": "bv2",
})
@@ -137,7 +136,7 @@ func TestDefaultConfigProvider(t *testing.T) {
},
})
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("foo"), qt.DeepEquals, hmaps.Params{
"a": "av",
"b": "bv2",
})
@@ -155,7 +154,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"c": "cv2",
})
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
"b": "bv",
"c": "cv2",
})
@@ -169,7 +168,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"a": "av",
})
})
@@ -192,11 +191,11 @@ func TestDefaultConfigProvider(t *testing.T) {
"e": "ev2",
})
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
"e": "ev2",
"_merge": maps.ParamsMergeStrategyShallow,
"_merge": hmaps.ParamsMergeStrategyShallow,
"b": "bv",
"c": maps.Params{
"c": hmaps.Params{
"b": "bv",
},
})
@@ -221,15 +220,15 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": left,
})
cfg.Merge("", maps.Params{
"b": maps.Params{
cfg.Merge("", hmaps.Params{
"b": hmaps.Params{
"c": "cv2",
"d": "dv2",
},
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"b": maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"b": hmaps.Params{
"c": "cv1",
"d": "dv2",
},
@@ -267,8 +266,8 @@ func TestDefaultConfigProvider(t *testing.T) {
cfg.Merge("a", right)
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"a": hmaps.Params{
"b": "bv1",
"c": "cv2",
},
@@ -292,7 +291,7 @@ func TestDefaultConfigProvider(t *testing.T) {
},
})
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"b": "bv",
})
})
@@ -341,7 +340,7 @@ func TestDefaultConfigProvider(t *testing.T) {
return err
}
m := maps.Params{
m := hmaps.Params{
"new": 42,
}
@@ -371,8 +370,8 @@ func TestDefaultConfigProvider(t *testing.T) {
cfg := New()
k := "foo"
cfg.Set(k, maps.Params{k: true})
c.Assert(cfg.GetParams(k), qt.DeepEquals, maps.Params{
cfg.Set(k, hmaps.Params{k: true})
c.Assert(cfg.GetParams(k), qt.DeepEquals, hmaps.Params{
k: true,
})
@@ -384,8 +383,8 @@ func TestDefaultConfigProvider(t *testing.T) {
k := "foo"
k2 := "bar"
cfg.Set(k, maps.Params{k: struct{}{}})
cfg.Set(k2, maps.Params{k2: struct{}{}})
cfg.Set(k, hmaps.Params{k: struct{}{}})
cfg.Set(k2, hmaps.Params{k2: struct{}{}})
c.Assert(len(cfg.Keys()), qt.Equals, 2)
@@ -401,11 +400,11 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Run("WalkParams", func(c *qt.C) {
cfg := New()
cfg.Set("x", maps.Params{})
cfg.Set("y", maps.Params{})
cfg.Set("x", hmaps.Params{})
cfg.Set("y", hmaps.Params{})
var got []string
cfg.WalkParams(func(params ...maps.KeyParams) bool {
cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
got = append(got, params[len(params)-1].Key)
return false
})
@@ -417,7 +416,7 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Assert(got, qt.DeepEquals, want)
cfg = New()
cfg.WalkParams(func(params ...maps.KeyParams) bool {
cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
return true
})
@@ -429,7 +428,7 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Run("SetDefaults", func(c *qt.C) {
cfg := New()
cfg.SetDefaults(maps.Params{
cfg.SetDefaults(hmaps.Params{
"foo": "bar",
"bar": "baz",
})

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