Compare commits

..

1 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen 518eefee9b Skip flakey test on CI 2025-08-05 17:03:12 +02:00
1442 changed files with 30610 additions and 53539 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22600.20100
- image: bepsays/ci-hugoreleaser:1.22400.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
steps:
- *restore-cache
- &attach-workspace
-13
View File
@@ -1,13 +0,0 @@
have_fun: false
memory_config:
disabled: false
code_review:
disable: false
comment_severity_threshold: HIGH
max_review_comments: -1
pull_request_opened:
help: true
summary: false
code_review: false
include_drafts: false
ignore_patterns: []
+5 -5
View File
@@ -16,20 +16,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- 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@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.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@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
with:
context: .
provenance: mode=max
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
operations-per-run: 999
days-before-issue-stale: 365
@@ -43,7 +43,7 @@ jobs:
stale-pr-message: This PR has been automatically marked as stale because it has not had
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell us in a comment.
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'Stale'
+17 -19
View File
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.25.x, 1.26.x]
go-version: [1.23.x, 1.24.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
runs-on: ${{ matrix.os }}
steps:
@@ -24,7 +24,9 @@ jobs:
name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
@@ -32,9 +34,9 @@ jobs:
docker-images: true
swap-storage: true
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ matrix.go-version }}
check-latest: true
@@ -43,21 +45,18 @@ jobs:
**/go.sum
**/go.mod
- name: Install Ruby
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
ruby-version: "3.4.5"
- name: Install Ruby gems
run: |
gem install asciidoctor -v "2.0.26"
gem install asciidoctor-diagram -v "3.1.0"
- name: Install GoAT
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
ruby-version: "2.7"
bundler-cache: true #
- name: Install Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: "3.x"
- name: Install Mage
run: go install github.com/magefile/mage@v1.15.0
- name: Install asciidoctor
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
- name: Install docutils
run: |
pip install docutils
@@ -106,10 +105,7 @@ jobs:
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- if: matrix.os == 'ubuntu-latest'
name: Run staticcheck
run: |
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
staticcheck ./...
rm -rf ${{ runner.temp }}/staticcheck
run: staticcheck ./...
- if: matrix.os != 'windows-latest'
name: Check
run: |
@@ -121,14 +117,16 @@ 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
+1 -2
View File
@@ -3,5 +3,4 @@
imports.*
dist/
public/
.DS_Store
cache/filecache/_gen/
.DS_Store
-1
View File
@@ -1 +0,0 @@
tpl/tplimpl/embedded/templates/**
-17
View File
@@ -1,17 +0,0 @@
* 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`
* 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
@@ -1 +0,0 @@
@AGENTS.md
+2 -13
View File
@@ -19,7 +19,6 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
* [Reporting Issues](#reporting-issues)
* [Submitting Patches](#submitting-patches)
* [Code Contribution Guidelines](#code-contribution-guidelines)
* [AI Assistance Notice](#ai-assistance-notice)
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
@@ -55,7 +54,7 @@ If it is of some complexity, the contributor is expected to maintain and support
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
Note that we do not accept new features that require [CGO](https://go.dev/wiki/cgo).
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
We have one exception to this rule which is LibSASS.
**Bug fixes are, of course, always welcome.**
@@ -77,19 +76,9 @@ 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 `./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/...`.
* 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.
* Follow the **Git Commit Message Guidelines** below.
## AI Assistance Notice
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
An example disclosure:
> This PR was written primarily by Claude Code.
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it. 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
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
+2 -4
View File
@@ -2,7 +2,7 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.26"
ARG GO_VERSION="1.24"
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
@@ -65,9 +65,7 @@ RUN apk add --no-cache \
git \
runuser \
nodejs \
npm \
openssh-client \
tar
npm
RUN mkdir -p /var/hugo/bin /cache && \
addgroup -Sg 1000 hugo && \
+75 -82
View File
@@ -52,7 +52,7 @@ Use Hugo's embedded web server during development to instantly see changes to co
Hugo's fast asset pipelines include:
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract metadata
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
- JavaScript bundling &ndash; Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
- Sass processing &ndash; Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
- Tailwind CSS processing &ndash; Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
@@ -65,24 +65,30 @@ See the [features] section of the documentation for a comprehensive summary of H
<p>&nbsp;</p>
<p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
&nbsp;&nbsp;&nbsp;
<a href="https://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>
<a href="https://pinme.eth.limo/?s=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/logo-pinme.svg" width="200" alt="PinMe."></a>
</p>
## Editions
Hugo offers a standard edition with core features, plus extended and extended/deploy editions with more. Use the standard edition unless you need the features below.
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.
Feature|extended edition|extended/deploy edition
:--|:-:|:-:
[Transpile Sass to CSS] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass] transpiler instead, which is compatible with any edition.|:heavy_check_mark:|:heavy_check_mark:
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:
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[processing images]: https://gohugo.io/content-management/image-processing/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
## Installation
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
@@ -96,9 +102,9 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
Prerequisites to build Hugo from source:
- Standard edition: Go 1.25.0 or later
- Extended edition: Go 1.25.0 or later, and GCC
- Extended/deploy edition: Go 1.25.0 or later, and GCC
- Standard edition: Go 1.23.0 or later
- Extended edition: Go 1.23.0 or later, and GCC
- Extended/deploy edition: Go 1.23.0 or later, and GCC
Build the standard edition:
@@ -156,16 +162,6 @@ If there is sufficient interest, [create a proposal]. Do not submit a pull reque
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
## License
For the Hugo source code, see [LICENSE](/LICENSE).
We also bundle some libraries in binary/WASM form:
* [libwebp](https://github.com/webmproject/libwebp), [BSD-3-Clause license](https://github.com/webmproject/libwebp?tab=BSD-3-Clause-1-ov-file#readme)
* [Katex](https://github.com/KaTeX/KaTeX), [MIT license](https://github.com/KaTeX/KaTeX?tab=MIT-1-ov-file#readme)
* [QuickJS](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme), [License](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme)
## Dependencies
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
@@ -175,55 +171,51 @@ 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/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/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/bep/clocks="v0.5.0"
github.com/bep/debounce="v1.2.0"
github.com/bep/gitmap="v1.9.0"
github.com/bep/gitmap="v1.6.0"
github.com/bep/goat="v0.5.0"
github.com/bep/godartsass/v2="v2.5.0"
github.com/bep/godartsass/v2="v2.3.2"
github.com/bep/golibsass="v1.2.0"
github.com/bep/goportabletext="v0.1.0"
github.com/bep/helpers="v0.6.0"
github.com/bep/imagemeta="v0.12.0"
github.com/bep/lazycache="v0.8.0"
github.com/bep/gowebp="v0.3.0"
github.com/bep/imagemeta="v0.8.4"
github.com/bep/lazycache="v0.7.0"
github.com/bep/logg="v0.4.0"
github.com/bep/mclib="v1.20400.20402"
github.com/bep/overlayfs="v0.10.0"
github.com/bep/simplecobra="v0.6.1"
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
github.com/bep/overlayfs="v0.9.2"
github.com/bep/simplecobra="v0.5.0"
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/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/cpuguy83/go-md2man/v2="v2.0.4"
github.com/disintegration/gift="v1.2.1"
github.com/dlclark/regexp2="v1.11.5"
github.com/evanw/esbuild="v0.27.2"
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
github.com/evanw/esbuild="v0.24.2"
github.com/fatih/color="v1.18.0"
github.com/frankban/quicktest="v1.14.6"
github.com/fsnotify/fsnotify="v1.9.0"
github.com/getkin/kin-openapi="v0.133.0"
github.com/fsnotify/fsnotify="v1.8.0"
github.com/getkin/kin-openapi="v0.129.0"
github.com/ghodss/yaml="v1.0.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/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/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/gohugoio/locales="v0.14.0"
github.com/gohugoio/localescompressed="v1.0.1"
github.com/google/go-cmp="v0.7.0"
github.com/gorilla/css="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/gorilla/websocket="v1.5.3"
github.com/hairyhenderson/go-codeowners="v0.7.0"
github.com/hashicorp/golang-lru/v2="v2.0.7"
@@ -232,57 +224,58 @@ 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.19"
github.com/microcosm-cc/bluemonday="v1.0.27"
github.com/mattn/go-runewidth="v0.0.9"
github.com/mazznoer/csscolorparser="v0.1.5"
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
github.com/muesli/smartcrop="v0.3.0"
github.com/niklasfasching/go-org="v1.9.1"
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
github.com/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/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/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
github.com/pelletier/go-toml/v2="v2.2.4"
github.com/pelletier/go-toml/v2="v2.2.3"
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/rogpeppe/go-internal="v1.14.1"
github.com/rivo/uniseg="v0.4.7"
github.com/rogpeppe/go-internal="v1.13.1"
github.com/russross/blackfriday/v2="v2.1.0"
github.com/sass/dart-sass/compiler="1.97.1"
github.com/sass/dart-sass/implementation="1.97.1"
github.com/sass/dart-sass/protocol="3.2.0"
github.com/spf13/afero="v1.15.0"
github.com/spf13/cast="v1.10.0"
github.com/spf13/cobra="v1.10.2"
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/spf13/fsync="v0.10.1"
github.com/spf13/pflag="v1.0.9"
github.com/tdewolff/minify/v2="v2.24.8"
github.com/tdewolff/parse/v2="v2.8.5"
github.com/tetratelabs/wazero="v1.10.1"
github.com/webmproject/libwebp="v1.6.0"
github.com/woodsbury/decimal128="v1.3.0"
github.com/yuin/goldmark-emoji="v1.0.6"
github.com/yuin/goldmark="v1.7.13"
github.com/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"
go.uber.org/automaxprocs="v1.5.3"
go.yaml.in/yaml/v3="v3.0.4"
golang.org/x/crypto="v0.46.0"
golang.org/x/image="v0.34.0"
golang.org/x/mod="v0.31.0"
golang.org/x/net="v0.48.0"
golang.org/x/sync="v0.19.0"
golang.org/x/sys="v0.39.0"
golang.org/x/text="v0.32.0"
golang.org/x/tools="v0.40.0"
google.golang.org/protobuf="v1.36.10"
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"
gopkg.in/yaml.v3="v3.0.1"
oss.terrastruct.com/d2="v0.6.9"
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
rsc.io/qr="v0.2.0"
software.sslmate.com/src/go-pkcs12="v0.2.0"
```
+3 -3
View File
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
infol := opts.Log.InfoCommand("dynacache")
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
evictedIdentities := collections.NewStack[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.StackThreadSafe[KeyIdentity]
evictedIdentities *collections.Stack[KeyIdentity]
opts Options
infol logg.LevelLogger
@@ -340,7 +340,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
panic("invalid Weight, must be between 1 and 100")
}
if !partitionNameRe.MatchString(name) {
if partitionNameRe.FindString(name) != name {
panic(fmt.Sprintf("invalid partition name %q", name))
}
+59 -142
View File
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@ package filecache
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@@ -46,12 +45,16 @@ const (
type Cache struct {
Fs afero.Fs
cfg FileCacheConfig
// Max age for items in this cache. Negative duration means forever,
// 0 is effectively turning this cache off.
maxAge time.Duration
entryLocker *lockTracker
// When set, we just remove this entire root directory on expiration.
pruneAllRootDir string
nlocker *lockTracker
initOnce sync.Once
isInited bool
initErr error
}
@@ -85,15 +88,12 @@ type ItemInfo struct {
}
// NewCache creates a new file cache with the given filesystem and max age.
func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
if err := cfg.init(); err != nil {
panic(fmt.Sprintf("invalid cache config: %s", err))
}
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
return &Cache{
Fs: fs,
entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
cfg: cfg,
Fs: fs,
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
maxAge: maxAge,
pruneAllRootDir: pruneAllRootDir,
}
}
@@ -109,15 +109,9 @@ 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
}
})
@@ -132,19 +126,19 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
}
id = cleanID(id)
c.entryLocker.Lock(id)
c.nlocker.Lock(id)
info := ItemInfo{Name: id}
f, err := helpers.OpenFileForWriting(c.Fs, id)
if err != nil {
c.entryLocker.Unlock(id)
c.nlocker.Unlock(id)
return info, nil, err
}
return info, &lockedFile{
File: f,
unlock: func() { c.entryLocker.Unlock(id) },
unlock: func() { c.nlocker.Unlock(id) },
}, nil
}
@@ -162,8 +156,8 @@ func (c *Cache) ReadOrCreate(id string,
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info = ItemInfo{Name: id}
@@ -192,9 +186,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.entryLocker.Lock(id)
c.nlocker.Lock(id)
return func() {
c.entryLocker.Unlock(id)
c.nlocker.Unlock(id)
}
}
@@ -207,8 +201,8 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -226,7 +220,7 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
return info, nil, err
}
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
// No caching.
return info, hugio.ToReadCloser(r), nil
}
@@ -237,40 +231,6 @@ 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 != "" {
@@ -294,8 +254,8 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -315,7 +275,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
return info, nil, err
}
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
return info, b, nil
}
@@ -326,39 +286,15 @@ 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) ([]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) {
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -378,8 +314,8 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -388,23 +324,10 @@ 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.cfg.MaxAge == 0 {
if c.maxAge == 0 {
// No caching.
return nil
}
@@ -422,7 +345,7 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
}
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
// No caching.
return nil, false
}
@@ -447,7 +370,7 @@ func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
}
func (c *Cache) removeIfExpired(id string) (bool, error) {
if c.cfg.MaxAge <= 0 {
if c.maxAge <= 0 {
return false, nil
}
@@ -457,37 +380,29 @@ func (c *Cache) removeIfExpired(id string) (bool, error) {
}
if c.isExpired(fi.ModTime()) {
c.remove(id)
c.Fs.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.cfg.MaxAge < 0 {
if c.maxAge < 0 {
return false
}
// Note the use of time.Since here.
// We cannot use Hugo's global Clock for this.
return c.cfg.MaxAge == 0 || time.Since(modTime) > c.cfg.MaxAge
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
}
// For testing
func (c *Cache) GetString(id string) string {
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
f, err := c.Fs.Open(id)
if err != nil {
@@ -502,17 +417,6 @@ 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)]
@@ -520,21 +424,34 @@ func (f Caches) Get(name string) *Cache {
// NewCaches creates a new set of file caches from the given
// configuration.
func NewCaches(dcfg Configs, sourceFs afero.Fs) (Caches, error) {
fs := sourceFs
func NewCaches(p *helpers.PathSpec) (Caches, error) {
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
fs := p.Fs.Source
m := make(Caches)
for k, v := range dcfg {
var cfs afero.Fs
if v.IsResourceDir {
cfs = nil // Set later. TODO(bep) this needs to be cleanded up.
cfs = p.BaseFs.ResourcesCache
} else {
cfs = hugofs.NewBasePathFs(fs, v.DirCompiled)
cfs = fs
}
c := NewCache(cfs, v)
if cfs == nil {
panic("nil fs")
}
m[k] = c
baseDir := v.DirCompiled
bfs := hugofs.NewBasePathFs(cfs, baseDir)
var pruneAllRootDir string
if k == CacheKeyModules {
pruneAllRootDir = "pkg"
}
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
}
return m, nil
@@ -563,7 +480,7 @@ func (h *httpCache) Get(id string) (resp []byte, ok bool) {
}
func (h *httpCache) Set(id string, resp []byte) {
if h.c.cfg.MaxAge == 0 {
if h.c.maxAge == 0 {
return
}
+33 -103
View File
@@ -15,7 +15,6 @@
package filecache
import (
"encoding/json"
"errors"
"fmt"
"path"
@@ -23,7 +22,7 @@ import (
"strings"
"time"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
@@ -35,50 +34,35 @@ const (
cacheDirProject = ":cacheDir/:project"
)
var defaultCacheConfig = FileCacheConfig{
MaxAge: -1, // Never expire
Dir: cacheDirProject,
}
const (
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyModuleQueries = "modulequeries"
CacheKeyModuleGitInfo = "modulegitinfo"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
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,
@@ -97,13 +81,6 @@ 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.
@@ -115,50 +92,22 @@ type FileCacheConfig struct {
MaxAge time.Duration
// The directory where files are stored.
Dir string
Dir string
DirCompiled string `json:"-"`
fileCacheConfigInternal `json:"-"`
// Will resources/_gen will get its own composite filesystem that
// also checks any theme.
IsResourceDir bool `json:"-"`
}
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
// GetJSONCache gets the file cache for getJSON.
func (f Caches) GetJSONCache() *Cache {
return f[CacheKeyGetJSON]
}
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,
})
// GetCSVCache gets the file cache for getCSV.
func (f Caches) GetCSVCache() *Cache {
return f[CacheKeyGetCSV]
}
// ImageCache gets the file cache for processed images.
@@ -171,25 +120,6 @@ 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]
@@ -217,14 +147,10 @@ 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.(hmaps.Params); !ok {
if _, ok := v.(maps.Params); !ok {
continue
}
var ok bool
cc, ok := c[k]
if !ok {
return nil, fmt.Errorf("%q is not a valid cache name", k)
}
cc := defaultCacheConfig
dc := &mapstructure.DecoderConfig{
Result: &cc,
@@ -245,8 +171,12 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
return c, errors.New("must provide cache Dir")
}
c[k] = cc
name := strings.ToLower(k)
if !valid[name] {
return nil, fmt.Errorf("%q is not a valid cache name", name)
}
c[name] = cc
}
for k, v := range c {
+14 -39
View File
@@ -14,7 +14,6 @@
package filecache_test
import (
"encoding/json"
"path/filepath"
"runtime"
"testing"
@@ -44,9 +43,12 @@ assetDir = "assets"
archetypeDir = "archetypes"
[caches]
[caches.misc]
[caches.getJSON]
maxAge = "10m"
dir = "/path/to/c1"
[caches.getCSV]
maxAge = "11h"
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
@@ -59,9 +61,9 @@ dir = "/path/to/c4"
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c2 := decoded["misc"]
c.Assert(c2.MaxAge.String(), qt.Equals, "10m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c1/filecache/misc"))
c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
c3 := decoded["images"]
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
@@ -88,9 +90,12 @@ archeTypedir = "archetypes"
ignoreCache = true
[caches]
[caches.misc]
[caches.getJSON]
maxAge = 1234
dir = "/path/to/c1"
[caches.getCSV]
maxAge = 3456
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
@@ -127,45 +132,15 @@ func TestDecodeConfigDefault(t *testing.T) {
c.Assert(len(decoded), qt.Equals, 7)
imgConfig := decoded[filecache.CacheKeyImages]
miscConfig := decoded[filecache.CacheKeyMisc]
jsonConfig := decoded[filecache.CacheKeyGetJSON]
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(miscConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/misc")
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
}
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
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`)
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
}
+1 -1
View File
@@ -67,7 +67,7 @@ title: "Home"
---
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/home.html --
-- layouts/index.html --
{{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }}
+7 -43
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.cfg.entryIsDir {
return c.pruneRootDirs(force)
if c.pruneAllRootDir != "" {
return c.pruneRootDir(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.entryLocker.seen) > 0 {
if !shouldRemove && len(c.nlocker.seen) > 0 {
// Remove it if it's not been touched/used in the last build.
_, seen := c.entryLocker.seen[name]
_, seen := c.nlocker.seen[name]
shouldRemove = !seen
}
@@ -117,43 +117,11 @@ func (c *Cache) Prune(force bool) (int, error) {
return counter, err
}
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) {
func (c *Cache) pruneRootDir(force bool) (int, error) {
if err := c.init(); err != nil {
return 0, err
}
// Sanity check.
if dirname != "pkg" && len(dirname) < 5 {
panic(fmt.Sprintf("invalid cache dir name: %q", dirname))
}
info, err := c.Fs.Stat(dirname)
info, err := c.Fs.Stat(c.pruneAllRootDir)
if err != nil {
if herrors.IsNotExist(err) {
return 0, nil
@@ -165,9 +133,5 @@ func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
return 0, nil
}
if c.cfg.isReadOnly {
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, dirname)
}
return 1, c.Fs.RemoveAll(dirname)
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
}
+8 -9
View File
@@ -39,9 +39,12 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.misc]
[caches.getjson]
maxAge = "200ms"
dir = "/cache/c"
[caches.getcsv]
maxAge = "200ms"
dir = "/cache/d"
[caches.assets]
maxAge = "200ms"
dir = ":resourceDir/_gen"
@@ -50,14 +53,11 @@ maxAge = "200ms"
dir = ":resourceDir/_gen"
`
for _, name := range []string{filecache.CacheKeyAssets, filecache.CacheKeyImages} {
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
msg := qt.Commentf("cache: %s", name)
fs := afero.NewMemMapFs()
p := newPathsSpec(t, fs, configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, fs)
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(fs)
cache := caches[name]
for i := range 10 {
id := fmt.Sprintf("i%d", i)
@@ -84,9 +84,8 @@ dir = ":resourceDir/_gen"
}
}
caches, err = filecache.NewCaches(fileCachConfig, fs)
caches, err = filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(fs)
cache = caches[name]
// Touch one and then prune.
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
+14 -23
View File
@@ -22,8 +22,6 @@ 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"
@@ -66,7 +64,7 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.misc]
[caches.getJSON]
maxAge = "10h"
dir = ":cacheDir/c"
@@ -80,13 +78,11 @@ dir = ":cacheDir/c"
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
p := newPathsSpec(t, osfs, configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(p.SourceFs)
cache := caches.Get("Misc")
cache := caches.Get("GetJSON")
c.Assert(cache, qt.Not(qt.IsNil))
cache = caches.Get("Images")
@@ -108,7 +104,7 @@ dir = ":cacheDir/c"
return []byte("bcd"), nil
}
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache()} {
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for range 2 {
info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil)
@@ -136,6 +132,8 @@ 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")
@@ -151,7 +149,7 @@ dir = ":cacheDir/c"
r.Close()
c.Assert(string(b), qt.Equals, "Hugo is great!")
info, b, err = caches.ImageCache().GetItemBytes("mykey")
info, b, err = caches.ImageCache().GetBytes("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
c.Assert(string(b), qt.Equals, "Hugo is great!")
@@ -160,7 +158,6 @@ dir = ":cacheDir/c"
}
func TestFileCacheConcurrent(t *testing.T) {
htesting.SkipSlowTestUnlessCI(t)
t.Parallel()
c := qt.New(t)
@@ -175,19 +172,18 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.misc]
[caches.getjson]
maxAge = "1s"
dir = "/cache/c"
`
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
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 = "misc"
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
const cacheName = "getjson"
filenameData := func(i int) (string, string) {
data := fmt.Sprintf("data: %d", i)
@@ -252,12 +248,7 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
}
}
cfg := filecache.FileCacheConfig{
MaxAge: 100 * time.Hour,
Dir: "cache/c",
}
cache := filecache.NewCache(afero.NewMemMapFs(), cfg)
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
const id = "a32"
@@ -279,7 +270,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
acfg := testconfig.GetTestConfig(fs, cfg)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil, nil)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
c.Assert(err, qt.IsNil)
return p
}
+10 -21
View File
@@ -25,8 +25,6 @@ import (
// DefaultConfig holds the default configuration for the HTTP cache.
var DefaultConfig = Config{
RespectCacheControlNoStoreInRequest: true,
RespectCacheControlNoStoreInResponse: false,
Cache: Cache{
For: GlobMatcher{
Excludes: []string{"**"},
@@ -44,13 +42,7 @@ var DefaultConfig = Config{
// Config holds the configuration for the HTTP cache.
type Config struct {
// When enabled and there's a Cache-Control: no-store directive in the request, response will never be stored in disk cache.
RespectCacheControlNoStoreInRequest bool
// When enabled and there's a Cache-Control: no-store directive in the response, response will never be stored in disk cache.
RespectCacheControlNoStoreInResponse bool
// Enables HTTP cache behavior (RFC 9111) for these resources.
// Configures the HTTP cache behavior (RFC 9111).
// When this is not enabled for a resource, Hugo will go straight to the file cache.
Cache Cache
@@ -65,9 +57,7 @@ type Cache struct {
}
func (c *Config) Compile() (ConfigCompiled, error) {
cc := ConfigCompiled{
Base: *c,
}
var cc ConfigCompiled
p, err := c.Cache.For.CompilePredicate()
if err != nil {
@@ -137,7 +127,6 @@ func (gm GlobMatcher) IsZero() bool {
}
type ConfigCompiled struct {
Base Config
For predicate.P[string]
PollConfigs []PollConfigCompiled
}
@@ -173,16 +162,16 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if gm.IsZero() {
panic("no includes or excludes")
}
var b predicate.PR[string]
var p predicate.P[string]
for _, include := range gm.Includes {
g, err := glob.Compile(include, '/')
if err != nil {
return nil, err
}
fn := func(s string) predicate.Match {
return predicate.BoolMatch(g.Match(s))
fn := func(s string) bool {
return g.Match(s)
}
b = b.Or(fn)
p = p.Or(fn)
}
for _, exclude := range gm.Excludes {
@@ -190,13 +179,13 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if err != nil {
return nil, err
}
fn := func(s string) predicate.Match {
return predicate.BoolMatch(!g.Match(s))
fn := func(s string) bool {
return !g.Match(s)
}
b = b.And(fn)
p = p.And(fn)
}
return b.BoolFunc(), nil
return p, nil
}
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
+1 -1
View File
@@ -52,7 +52,7 @@ func TestDefaultConfig(t *testing.T) {
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]any{})
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{})
c.Assert(err, qt.IsNil)
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
-82
View File
@@ -1,82 +0,0 @@
#!/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.Pointer
isPointer := t.Kind() == reflect.Ptr
if isPointer {
t = t.Elem()
+3 -3
View File
@@ -26,9 +26,9 @@ import (
func TestMethods(t *testing.T) {
var (
zeroIE = reflect.TypeFor[IEmbed]()
zeroIEOnly = reflect.TypeFor[IEOnly]()
zeroI = reflect.TypeFor[I]()
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
zeroI = reflect.TypeOf((*I)(nil)).Elem()
)
dir, _ := os.Getwd()
+14 -27
View File
@@ -97,7 +97,6 @@ type commonConfig struct {
type configKey struct {
counter int32
ignoreModulesDoesNotExists bool
skipNpmCheck bool
}
// This is the root command.
@@ -132,7 +131,6 @@ type rootCommand struct {
gc bool
poll string
forceSyncStatic bool
panicOnWarning bool
// Profile flags (for debugging of performance problems)
cpuprofile string
@@ -196,7 +194,6 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
Logger: r.logger,
Environment: r.environment,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
@@ -253,7 +250,6 @@ 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 {
@@ -335,9 +331,6 @@ 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)
@@ -408,7 +401,9 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
for _, group := range watchGroups {
r.Printf("Watching for changes in %s\n", group)
}
watcher, err := b.newWatcher(r.poll, watchDirs...)
if err != nil {
return err
@@ -492,18 +487,12 @@ 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
@@ -530,8 +519,8 @@ func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Com
commandName = subCommandName
}
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
cmd.Short = "Build your project"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo project.
cmd.Short = "Build your site"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
@@ -601,7 +590,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().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("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.")
@@ -632,14 +621,13 @@ func (r *rootCommand) timeTrack(start time.Time, name string) {
}
type simpleCommand struct {
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
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
commands []simplecobra.Commander
@@ -666,7 +654,6 @@ 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
}
@@ -684,7 +671,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], "project", "site", "theme", "content") {
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") {
// Insert "content" as the second argument
args = append(args[:1], append([]string{"content"}, args[1:]...)...)
}
+9 -10
View File
@@ -23,9 +23,8 @@ import (
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
@@ -72,7 +71,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
return fmt.Errorf("language %q not found", c.lang)
}
} else {
config = conf.configs.LanguageConfigMap[conf.configs.Base.DefaultContentLanguage]
config = conf.configs.LanguageConfigSlice[0]
}
var buf bytes.Buffer
@@ -95,7 +94,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
hmaps.ConvertFloat64WithNoDecimalsToInt(m)
maps.ConvertFloat64WithNoDecimalsToInt(m)
switch format {
case "yaml":
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
@@ -112,8 +111,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 project configuration"
cmd.Long = `Display project configuration, both default and custom settings.`
cmd.Short = "Display site configuration"
cmd.Long = `Display site 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.")
@@ -129,9 +128,9 @@ func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
type configModMount struct {
Source string `json:"source"`
Target string `json:"target"`
Sites sitesmatrix.Sites `json:"sites,omitzero"`
Source string `json:"source"`
Target string `json:"target"`
Lang string `json:"lang,omitempty"`
}
type configModMounts struct {
@@ -147,7 +146,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
mounts = append(mounts, configModMount{
Source: mount.Source,
Target: mount.Target,
Sites: mount.Sites,
Lang: mount.Lang,
})
}
-58
View File
@@ -18,12 +18,10 @@ 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"
@@ -202,56 +200,6 @@ 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")
@@ -271,12 +219,6 @@ 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 project to a cloud provider",
long: `Deploy your project to a cloud provider
short: "Deploy your site to a cloud provider",
long: `Deploy your site to a cloud provider
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
+9 -14
View File
@@ -28,7 +28,6 @@ import (
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra"
"github.com/goccy/go-yaml"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/helpers"
@@ -37,6 +36,7 @@ import (
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"gopkg.in/yaml.v2"
)
func newGenCommand() *genCommand {
@@ -51,7 +51,6 @@ func newGenCommand() *genCommand {
lineNumbersInlineStyle string
lineNumbersTableStyle string
omitEmpty bool
omitClassComments bool
)
newChromaStyles := func() simplecobra.Commander {
@@ -60,7 +59,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://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview of the available styles.`,
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
style = strings.ToLower(style)
@@ -82,14 +81,12 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
return err
}
var formatter *html.Formatter
if omitEmpty {
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
formatter = html.New(html.WithClasses(true))
} else {
formatter = html.New(html.WithAllClasses(true))
}
options := []html.Option{
html.WithCSSComments(!omitClassComments),
}
formatter := html.New(options...)
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
@@ -98,7 +95,7 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
_ = 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)
@@ -106,10 +103,8 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules (deprecated, no longer needed)`)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
},
}
}
@@ -249,7 +244,7 @@ url: %s
return err
}
defer f.Close()
yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt())
yamlEnc := yaml.NewEncoder(f)
if err := yamlEnc.Encode(m); err != nil {
return err
}
+2 -2
View File
@@ -76,12 +76,12 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
// Flags with a different name in the config.
keyMap := map[string]string{
"minify": "minify.minifyOutput",
"minify": "minifyOutput",
"destination": "publishDir",
"editor": "newContentEditor",
}
// Flags that we for some reason don't want to expose in the project config.
// Flags that we for some reason don't want to expose in the site config.
internalKeySet := map[string]bool{
"quiet": true,
"verbose": true,
+9 -63
View File
@@ -27,15 +27,13 @@ import (
"sync/atomic"
"time"
"github.com/bep/debounce"
"github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/terminal"
"github.com/gohugoio/hugo/common/types"
@@ -56,9 +54,8 @@ import (
type hugoBuilder struct {
r *rootCommand
confmu sync.Mutex
confOld *commonConfig
conf *commonConfig
confmu sync.Mutex
conf *commonConfig
// May be nil.
s *serverCommand
@@ -95,27 +92,6 @@ 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
@@ -166,7 +142,7 @@ func (c *hugoBuilder) getDirList() ([]string, error) {
return nil, err
}
return hstrings.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
}
func (c *hugoBuilder) initCPUProfile() (func(), error) {
@@ -179,7 +155,6 @@ 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() {
@@ -487,15 +462,7 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
infol.Logf("removing all files from destination that don't exist in static dirs")
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
name := f.Name()
// Keep .gitignore and .gitattributes anywhere
if name == ".gitignore" || name == ".gitattributes" {
return true
}
// Keep Hugo's original dot-directory behavior
return f.IsDir() && strings.HasPrefix(name, ".")
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
}
}
start := time.Now()
@@ -548,14 +515,6 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
return langCount, nil
}
func (c *hugoBuilder) progressIntermediate() {
terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
}
func (c *hugoBuilder) progressHidden() {
terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
}
func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
var (
g errgroup.Group
@@ -859,7 +818,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
continue
}
walkAdder := func(ctx context.Context, path string, f hugofs.FileMetaInfo) error {
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
if f.IsDir() {
c.r.logger.Println("adding created directory to watchlist", path)
if err := watcher.Add(path); err != nil {
@@ -1068,17 +1027,6 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
}
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
if terminal.PrintANSIColors(os.Stdout) {
defer c.progressHidden()
// If the configuration takes a while to load, we want to show some progress.
// This is typically loading of external modules.
d := debounce.New(500 * time.Millisecond)
d(func() {
c.progressIntermediate()
})
defer d(func() {})
}
cfg := config.New()
cfg.Set("renderToMemory", c.r.renderToMemory)
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
@@ -1100,7 +1048,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
}
cfg.Set("environment", c.r.environment)
cfg.Set("internal", hmaps.Params{
cfg.Set("internal", maps.Params{
"running": running,
"watch": watch,
"verbose": c.r.isVerbose(),
@@ -1114,11 +1062,10 @@ 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 project.\nRun `hugo help new` for details.")
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.")
}
c.conf = conf
c.confOld = conf
if c.onConfigLoaded != nil {
if err := c.onConfigLoaded(false); err != nil {
return err
@@ -1182,9 +1129,8 @@ func (c *hugoBuilder) reloadConfig() error {
c.r.resetLogs()
c.r.configVersionID.Add(1)
if err := c.withConfOrOldConfE(func(conf *commonConfig) error {
if err := c.withConfE(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
+12 -12
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 project from another system"
cmd.Long = `Import a project from another system.
cmd.Short = "Import a site from another system"
cmd.Long = `Import a site 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 Project"
title := "My New Hugo Site"
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,
"locale": "en-us",
"languageCode": "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) createProjectFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
func (c *importCommand) createSiteFromJekyll(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) createProjectFromJekyll(jekyllRoot, targetDir string, je
}
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
metadata, _ := hmaps.ToStringMapE(m)
metadata, _ := maps.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 := hmaps.ToStringMapE(m)
metadata, err := maps.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.createProjectFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
if err != nil {
return newUserError(err)
}
@@ -427,7 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
c.r.Println("Importing...")
fileCount := 0
callback := func(ctx context.Context, path string, fi hugofs.FileMetaInfo) error {
callback := func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
}
@@ -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:")
c.r.Println("Now, start Hugo by yourself:\n")
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")
+13 -23
View File
@@ -21,13 +21,12 @@ 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 project
Note that Hugo will always start out by resolving the components defined in the site
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
Go Modules, or a folder inside the themes directory, in that order.
@@ -50,34 +49,28 @@ func newModCommands() *modCommands {
commands: []simplecobra.Commander{
&simpleCommand{
name: "pack",
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.
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.
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".
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 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 set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more information.
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.
`,
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 {
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))
})
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs, h.Configs.Modules)
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
},
},
},
@@ -173,7 +166,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 directories from module cache.", count)
r.Printf("Deleted %d files from module cache.", count)
return err
}
@@ -300,10 +293,7 @@ Run "go help get" for more information. All flags available for "go get" is also
return err
}
client := conf.configs.ModulesClient
if err := client.Get(args...); err != nil {
return err
}
return nil
return client.Get(args...)
}
},
},
+13 -16
View File
@@ -46,16 +46,14 @@ 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 project, they will be used.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your project.`,
Ensure you run this within the root directory of your 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")
}
cfg := flagsToCfg(cd, nil)
cfg.Set("BuildFuture", true)
h, err := r.Hugo(cfg)
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
@@ -76,11 +74,10 @@ Ensure you run this within the root directory of your project.`,
},
},
&simpleCommand{
name: "project",
use: "project [path]",
short: "Create a new project",
long: `Create a new project at the specified path.`,
aliases: []string{"site"},
name: "site",
use: "site [path]",
short: "Create a new site",
long: `Create a new site at the specified path.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
@@ -100,13 +97,13 @@ Ensure you run this within the root directory of your project.`,
}
sourceFs := conf.fs.Source
err = skeletons.CreateProject(createpath, sourceFs, force, format)
err = skeletons.CreateSite(createpath, sourceFs, force, format)
if err != nil {
return err
}
r.Printf("Congratulations! Your new Hugo project was created in %s.\n\n", createpath)
r.Println(c.newProjectNextStepsText(createpath, format))
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
r.Println(c.newSiteNextStepsText(createpath, format))
return nil
},
@@ -193,9 +190,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 project, they will be used.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your project.`
Ensure you run this within the root directory of your site.`
cmd.RunE = nil
return nil
@@ -206,7 +203,7 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return nil
}
func (c *newCommand) newProjectNextStepsText(path string, format string) string {
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
format = strings.ToLower(format)
var nextStepsText bytes.Buffer
+1 -3
View File
@@ -28,14 +28,13 @@ 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, version)
rel, err := releaser.New(skipPush, try, step)
if err != nil {
return err
}
@@ -48,7 +47,6 @@ 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))
},
}
+20 -24
View File
@@ -49,8 +49,6 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
@@ -246,7 +244,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"
@@ -282,7 +280,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
}
port = 1313
f.c.withConfOrOldConf(func(conf *commonConfig) {
f.c.withConf(func(conf *commonConfig) {
if lrport := conf.configs.GetFirstLanguageConfig().BaseURLLiveReload().Port(); lrport != 0 {
port = lrport
}
@@ -371,12 +369,6 @@ 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 {
@@ -499,7 +491,9 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
c.r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
for _, group := range watchGroups {
c.r.Printf("Watching for changes in %s\n", group)
}
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
if err != nil {
return err
@@ -523,7 +517,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 project.
cmd.Long = `Hugo provides its own webserver which builds and serves the site.
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
@@ -531,8 +525,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 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
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
of a second, you will be able to save and see your changes nearly instantly.`
cmd.Aliases = []string{"serve"}
@@ -553,7 +547,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 project in a browser after server startup")
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the site 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")
@@ -633,7 +627,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
panic("no server ports set")
}
return c.withConfE(func(conf *commonConfig) error {
for i, language := range conf.configs.Languages {
for i, language := range conf.configs.LanguagesDefaultFirst {
isMultihost := conf.configs.IsMultihost
var serverPort int
if isMultihost {
@@ -885,7 +879,7 @@ func (c *serverCommand) serve() error {
if isMultihost {
for _, l := range conf.configs.ConfigLangs() {
baseURLs = append(baseURLs, l.BaseURL())
roots = append(roots, l.Language().(*langs.Language).Lang)
roots = append(roots, l.Language().Lang)
}
} else {
l := conf.configs.GetFirstLanguageConfig()
@@ -1040,6 +1034,7 @@ func (c *serverCommand) serve() error {
defer cancel()
wg2, ctx := errgroup.WithContext(ctx)
for _, srv := range servers {
srv := srv
wg2.Go(func() error {
return srv.Shutdown(ctx)
})
@@ -1160,17 +1155,18 @@ 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 {
if src.IsDir() {
return true
}
return src.Mode().Perm()&0o200 == 0
// 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()
}
func cleanErrorLog(content string) string {
content = strings.ReplaceAll(content, "\n", " ")
content = logReplacer.Replace(content)
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
+16 -4
View File
@@ -16,8 +16,6 @@ package collections
import (
"fmt"
"reflect"
"github.com/gohugoio/hugo/common/hreflect"
)
// Append appends from to a slice to and returns the resulting slice.
@@ -27,7 +25,7 @@ func Append(to any, from ...any) (any, error) {
if len(from) == 0 {
return to, nil
}
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to))
tov, toIsNil := indirect(reflect.ValueOf(to))
toIsNil = toIsNil || to == nil
var tot reflect.Type
@@ -102,7 +100,7 @@ func Append(to any, from ...any) (any, error) {
fv := reflect.ValueOf(f)
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
// Fall back to a []interface{} slice.
tov, _ := hreflect.Indirect(reflect.ValueOf(to))
tov, _ := indirect(reflect.ValueOf(to))
return appendToInterfaceSlice(tov, from...)
}
tov = reflect.Append(tov, fv)
@@ -138,3 +136,17 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
return tos, nil
}
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
// TODO(bep) consolidate
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}
+64
View File
@@ -15,6 +15,7 @@ package collections
import (
"html/template"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
@@ -147,3 +148,66 @@ func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
}
func TestIndirect(t *testing.T) {
t.Parallel()
c := qt.New(t)
type testStruct struct {
Field string
}
var (
nilPtr *testStruct
nilIface interface{} = nil
nonNilIface interface{} = &testStruct{Field: "hello"}
)
tests := []struct {
name string
input any
wantKind reflect.Kind
wantNil bool
}{
{
name: "nil pointer",
input: nilPtr,
wantKind: reflect.Ptr,
wantNil: true,
},
{
name: "nil interface",
input: nilIface,
wantKind: reflect.Invalid,
wantNil: false,
},
{
name: "non-nil pointer to struct",
input: &testStruct{Field: "abc"},
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "non-nil interface holding pointer",
input: nonNilIface,
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "plain value",
input: testStruct{Field: "xyz"},
wantKind: reflect.Struct,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := reflect.ValueOf(tt.input)
got, isNil := indirect(v)
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
c.Assert(isNil, qt.Equals, tt.wantNil)
})
}
}
+12 -65
View File
@@ -13,32 +13,28 @@
package collections
import (
"iter"
"slices"
"sync"
import "slices"
"github.com/gohugoio/hugo/common/hiter"
)
import "sync"
// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
type StackThreadSafe[T any] struct {
// Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct {
items []T
zero T
mu sync.RWMutex
}
func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
return &StackThreadSafe[T]{}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *StackThreadSafe[T]) Push(item T) {
func (s *Stack[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *StackThreadSafe[T]) Pop() (T, bool) {
func (s *Stack[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
@@ -49,7 +45,7 @@ func (s *StackThreadSafe[T]) Pop() (T, bool) {
return item, true
}
func (s *StackThreadSafe[T]) Peek() (T, bool) {
func (s *Stack[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
@@ -58,18 +54,13 @@ func (s *StackThreadSafe[T]) Peek() (T, bool) {
return s.items[len(s.items)-1], true
}
func (s *StackThreadSafe[T]) Len() int {
func (s *Stack[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 *StackThreadSafe[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
func (s *StackThreadSafe[T]) Drain() []T {
func (s *Stack[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
@@ -77,7 +68,7 @@ func (s *StackThreadSafe[T]) Drain() []T {
return items
}
func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
@@ -89,47 +80,3 @@ func (s *StackThreadSafe[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 := NewStackThreadSafe[int]()
s := NewStack[int]()
c.Assert(s, qt.IsNotNil)
}
@@ -19,7 +19,7 @@ func TestStackBasic(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[int]()
s := NewStack[int]()
c.Assert(s.Len(), qt.Equals, 0)
@@ -50,7 +50,7 @@ func TestStackDrain(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[string]()
s := NewStack[string]()
s.Push("a")
s.Push("b")
@@ -64,7 +64,7 @@ func TestStackDrainMatching(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[int]()
s := NewStack[int]()
s.Push(1)
s.Push(2)
s.Push(3)
+3 -34
View File
@@ -23,7 +23,6 @@ import (
"github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity"
)
@@ -39,34 +38,6 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
return h.Sum64(), size, nil
}
type Hasher interface {
io.StringWriter
io.Writer
io.ReaderFrom
Sum64() uint64
}
type HashCloser interface {
Hasher
io.Closer
}
// XxHasher returns a Hasher that uses xxHash.
// Remember to call Close when done.
func XxHasher() HashCloser {
h := getXxHashReadFrom()
return struct {
Hasher
io.Closer
}{
Hasher: h,
Closer: hugio.CloserFunc(func() error {
putXxHashReadFrom(h)
return nil
}),
}
}
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
// and returns the hash as a hex encoded string.
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
@@ -87,13 +58,11 @@ func XXHashFromString(s string) (uint64, error) {
return h.Sum64(), nil
}
// XxHashFromStringHexEncoded calculates the xxHash for the given strings
// XxHashFromStringHexEncoded calculates the xxHash for the given string
// and returns the hash as a hex encoded string.
func XxHashFromStringHexEncoded(s ...string) string {
func XxHashFromStringHexEncoded(f string) string {
h := xxhash.New()
for _, f := range s {
h.WriteString(f)
}
h.WriteString(f)
hash := h.Sum(nil)
return hex.EncodeToString(hash)
}
+15 -10
View File
@@ -38,7 +38,10 @@ func TestXxHashFromReaderPara(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Go(func() {
i := i
wg.Add(1)
go func() {
defer wg.Done()
for j := range 100 {
s := strings.Repeat("Hello ", i+j+1*42)
r := strings.NewReader(s)
@@ -48,7 +51,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
expect, _ := XXHashFromString(s)
c.Assert(got, qt.Equals, expect)
}
})
}()
}
wg.Wait()
@@ -72,8 +75,8 @@ func TestXxHashFromStringHexEncoded(t *testing.T) {
func BenchmarkXXHashFromReader(b *testing.B) {
r := strings.NewReader("Hello World")
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFromReader(r)
r.Seek(0, 0)
}
@@ -81,16 +84,16 @@ func BenchmarkXXHashFromReader(b *testing.B) {
func BenchmarkXXHashFromString(b *testing.B) {
s := "Hello World"
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFromString(s)
}
}
func BenchmarkXXHashFromStringHexEncoded(b *testing.B) {
s := "The quick brown fox jumps over the lazy dog"
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
XxHashFromStringHexEncoded(s)
}
}
@@ -133,7 +136,7 @@ func BenchmarkHashString(b *testing.B) {
for _, test := range tests {
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
HashString(test)
}
})
@@ -146,7 +149,9 @@ func BenchmarkHashMap(b *testing.B) {
m[fmt.Sprintf("key%d", i)] = i
}
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
HashString(m)
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hcontext
import "context"
// ContextDispatcher is a generic interface for setting and getting values from a context.
type ContextDispatcher[T any] interface {
Set(ctx context.Context, value T) context.Context
Get(ctx context.Context) T
}
// NewContextDispatcher creates a new ContextDispatcher with the given key.
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
return keyInContext[T, R]{
id: key,
}
}
type keyInContext[T any, R comparable] struct {
zero T
id R
}
func (f keyInContext[T, R]) Get(ctx context.Context) T {
v := ctx.Value(f.id)
if v == nil {
return f.zero
}
return v.(T)
}
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
return context.WithValue(ctx, f.id, value)
}
-61
View File
@@ -1,61 +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 hdebug
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/htesting"
)
// Printf is a debug print function that should be removed before committing code to the repository.
func Printf(format string, args ...any) {
panicIfRealCI()
if len(args) == 1 && !strings.Contains(format, "%") {
format = format + ": %v"
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
fmt.Printf(format, args...)
}
func AssertNotNil(a ...any) {
panicIfRealCI()
for _, v := range a {
if types.IsNil(v) {
panic("hdebug.AssertNotNil: value is nil")
}
}
}
func Panicf(format string, args ...any) {
panicIfRealCI()
// fmt.Println(stack())
if len(args) == 1 && !strings.Contains(format, "%") {
format = format + ": %v"
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
panic(fmt.Sprintf(format, args...))
}
func panicIfRealCI() {
if htesting.IsRealCI() {
panic("This debug statement should be removed before committing code!")
}
}
+10 -3
View File
@@ -17,13 +17,22 @@ package herrors
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"
)
// PrintStackTrace prints the current stacktrace to w.
func PrintStackTrace(w io.Writer) {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Fprintf(w, "%s", buf)
}
// ErrorSender is a, typically, non-blocking error handler.
type ErrorSender interface {
SendError(err error)
@@ -36,9 +45,7 @@ type ErrorSender interface {
func Recover(args ...any) {
if r := recover(); r != nil {
fmt.Println("ERR:", r)
buf := make([]byte, 64<<10)
buf = buf[:runtime.Stack(buf, false)]
args = append(args, "stacktrace from panic: \n"+string(buf), "\n")
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
fmt.Println(args...)
}
}
+34 -53
View File
@@ -110,11 +110,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
fe.errorContext = ectx
if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
if ectx.Position.LineNumber > 0 {
fe.position.LineNumber = ectx.Position.LineNumber
}
if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
if ectx.Position.ColumnNumber > 0 {
fe.position.ColumnNumber = ectx.Position.ColumnNumber
}
@@ -177,7 +177,6 @@ func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
}
@@ -235,9 +234,7 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
fe := NewFileErrorFromName(err, realFilename)
fe = fe.UpdateContent(f, linematcher)
return fe
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -288,50 +285,6 @@ 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)
@@ -368,9 +321,13 @@ func extractFileTypePos(err error) (string, text.Position) {
}
// Look in the error message for the line number.
if lno, col := commonLineNumberExtractor(err); lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
for _, handle := range lineNumberExtractors {
lno, col := handle(err)
if lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
break
}
}
if fileType == "" && pos.Filename != "" {
@@ -394,6 +351,30 @@ 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`)
var lines strings.Builder
lines := ""
for i := 1; i <= 100; i++ {
lines.WriteString(fmt.Sprintf("line %d\n", i))
lines += 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.String()), nil)
fe.UpdateContent(strings.NewReader(lines), nil)
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
errorContext := fe.ErrorContext()
c.Assert(errorContext, qt.IsNotNil)
+3 -13
View File
@@ -19,27 +19,17 @@ import (
)
var lineNumberExtractors = []lineNumberExtractor{
// YAML parse errors.
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
// YAML parse errors
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
// i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
}
func commonLineNumberExtractor(e error) (int, int) {
for _, handler := range lineNumberExtractors {
lno, col := handler(e)
if lno > 0 {
return lno, col
}
}
return 0, 0
}
type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
+3 -3
View File
@@ -27,8 +27,8 @@ import (
"sync"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/security"
)
@@ -102,7 +102,7 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
workingDir: workingDir,
infol: log.InfoCommand("exec"),
baseEnviron: baseEnviron,
newNPXRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
}
}
@@ -121,7 +121,7 @@ type Exec struct {
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
baseEnviron []string
newNPXRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
npxInit sync.Once
npxAvailable bool
}
-67
View File
@@ -1,67 +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 himage provides some high level image types and interfaces.
package himage
import "image"
// AnimatedImage represents an animated image.
// This is currently supported for GIF and WebP images.
type AnimatedImage interface {
image.Image // The first frame.
GetRaw() any // *gif.GIF or *WEBP.
GetLoopCount() int // Number of times to loop the animation. 0 means infinite.
ImageFrames
}
// ImageFrames provides access to the frames of an animated image.
type ImageFrames interface {
GetFrames() []image.Image
// Frame durations in milliseconds.
// Note that Gif frame durations are in 100ths of a second,
// so they need to be multiplied by 10 to get milliseconds and vice versa.
GetFrameDurations() []int
SetFrames(frames []image.Image)
SetWidthHeight(width, height int)
}
// ImageConfigProvider provides access to the image.Config of an image.
type ImageConfigProvider interface {
GetImageConfig() image.Config
}
// FrameDurationsToGifDelays converts frame durations in milliseconds to
// GIF delays in 100ths of a second.
func FrameDurationsToGifDelays(frameDurations []int) []int {
delays := make([]int, len(frameDurations))
for i, fd := range frameDurations {
delays[i] = fd / 10
if delays[i] == 0 && fd > 0 {
delays[i] = 1
}
}
return delays
}
// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to
// frame durations in milliseconds.
func GifDelaysToFrameDurations(delays []int) []int {
frameDurations := make([]int, len(delays))
for i, d := range delays {
frameDurations[i] = d * 10
}
return frameDurations
}
-66
View File
@@ -1,66 +0,0 @@
package hiter
// Common iterator functions.
// Some of these are are based on this discsussion: https://github.com/golang/go/issues/61898
import "iter"
// Concat returns an iterator over the concatenation of the sequences.
// Any nil sequences are ignored.
func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] {
return func(yield func(V) bool) {
for _, seq := range seqs {
if seq == nil {
continue
}
for e := range seq {
if !yield(e) {
return
}
}
}
}
}
// Concat2 returns an iterator over the concatenation of the sequences.
// Any nil sequences are ignored.
func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for _, seq := range seqs {
if seq == nil {
continue
}
for k, v := range seq {
if !yield(k, v) {
return
}
}
}
}
}
// Lock returns an iterator that locks before iterating and unlocks after.
func Lock[V any](seq iter.Seq[V], lock, unlock func()) iter.Seq[V] {
return func(yield func(V) bool) {
lock()
defer unlock()
for e := range seq {
if !yield(e) {
return
}
}
}
}
// Lock2 returns an iterator that locks before iterating and unlocks after.
func Lock2[K, V any](seq iter.Seq2[K, V], lock, unlock func()) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
lock()
defer unlock()
for k, v := range seq {
if !yield(k, v) {
return
}
}
}
}
-57
View File
@@ -1,57 +0,0 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCacheSize(t *testing.T) {
c := qt.New(t)
cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})
for i := range 30 {
cache.Set(string(rune('a'+i)), "value")
}
c.Assert(len(cache.m), qt.Equals, 10)
for i := 20; i < 50; i++ {
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
return "value", nil
})
}
c.Assert(len(cache.m), qt.Equals, 10)
for i := 100; i < 200; i++ {
cache.SetIfAbsent(string(rune('a'+i)), "value")
}
c.Assert(len(cache.m), qt.Equals, 10)
cache.InitAndGet("foo", func(
get func(key string) (string, bool), set func(key string, value string),
) error {
for i := 50; i < 100; i++ {
set(string(rune('a'+i)), "value")
}
return nil
})
c.Assert(len(cache.m), qt.Equals, 10)
}
-129
View File
@@ -1,129 +0,0 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
import (
"iter"
"sync"
)
func NewMap[K comparable, T any]() *Map[K, T] {
return &Map[K, T]{
m: make(map[K]T),
}
}
// Map is a thread safe map backed by a Go map.
type Map[K comparable, T any] struct {
m map[K]T
mu sync.RWMutex
}
// Get gets the value for the given key.
// It returns the zero value of T if the key is not found.
func (m *Map[K, T]) Get(key K) T {
v, _ := m.Lookup(key)
return v
}
// Lookup looks up the given key in the map.
// It returns the value and a boolean indicating whether the key was found.
func (m *Map[K, T]) Lookup(key K) (T, bool) {
m.mu.RLock()
v, found := m.m[key]
m.mu.RUnlock()
return v, found
}
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
v, found := m.Lookup(key)
if found {
return v, nil
}
m.mu.Lock()
defer m.mu.Unlock()
v, found = m.m[key]
if found {
return v, nil
}
v, err := create()
if err != nil {
return v, err
}
m.m[key] = v
return v, nil
}
// Set sets the given key to the given value.
func (m *Map[K, T]) Set(key K, value T) {
m.mu.Lock()
m.m[key] = value
m.mu.Unlock()
}
// Delete deletes the given key from the map.
// It returns true if the key was found and deleted, false otherwise.
func (m *Map[K, T]) Delete(key K) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; found {
delete(m.m, key)
return true
}
return false
}
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
m.mu.Lock()
defer m.mu.Unlock()
return f(m.m)
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
// It returns true if the value was set, false otherwise.
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
m.mu.RLock()
if _, found := m.m[key]; !found {
m.mu.RUnlock()
return m.doSetIfAbsent(key, value)
}
m.mu.RUnlock()
return false
}
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; !found {
m.m[key] = value
return true
}
return false
}
// All returns an iterator over all key/value pairs in the map.
// A read lock is held during the iteration.
func (m *Map[K, T]) All() iter.Seq2[K, T] {
return func(yield func(K, T) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for k, v := range m.m {
if !yield(k, v) {
return
}
}
}
}
-72
View File
@@ -1,72 +0,0 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestMap(t *testing.T) {
c := qt.New(t)
m := NewMap[string, int]()
m.Set("b", 42)
v, found := m.Lookup("b")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 42)
v = m.Get("b")
c.Assert(v, qt.Equals, 42)
v, found = m.Lookup("c")
c.Assert(found, qt.Equals, false)
c.Assert(v, qt.Equals, 0)
v = m.Get("c")
c.Assert(v, qt.Equals, 0)
v, err := m.GetOrCreate("d", func() (int, error) {
return 100, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
v, found = m.Lookup("d")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 100)
v, err = m.GetOrCreate("d", func() (int, error) {
return 200, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
wasSet := m.SetIfAbsent("e", 300)
c.Assert(wasSet, qt.Equals, true)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
wasSet = m.SetIfAbsent("e", 400)
c.Assert(wasSet, qt.Equals, false)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
m.WithWriteLock(func(m map[string]int) error {
m["f"] = 500
return nil
})
v, found = m.Lookup("f")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 500)
}
-154
View File
@@ -1,154 +0,0 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
import (
"fmt"
"slices"
"github.com/bits-and-blooms/bitset"
)
type OrderedIntSet struct {
keys []int
values *bitset.BitSet
}
// NewOrderedIntSet creates a new OrderedIntSet.
// Note that this is backed by https://github.com/bits-and-blooms/bitset
func NewOrderedIntSet(vals ...int) *OrderedIntSet {
m := &OrderedIntSet{
keys: make([]int, 0, len(vals)),
values: bitset.New(uint(len(vals))),
}
for _, v := range vals {
m.Set(v)
}
return m
}
// Set sets the value for the given key.
// Note that insertion order is not affected if a key is re-inserted into the set.
func (m *OrderedIntSet) Set(key int) {
if m == nil {
panic("nil OrderedIntSet")
}
keyu := uint(key)
if m.values.Test(keyu) {
return
}
m.values.Set(keyu)
m.keys = append(m.keys, key)
}
// SetFrom sets the values from another OrderedIntSet.
func (m *OrderedIntSet) SetFrom(other *OrderedIntSet) {
if m == nil || other == nil {
return
}
for _, key := range other.keys {
m.Set(key)
}
}
func (m *OrderedIntSet) Clone() *OrderedIntSet {
if m == nil {
return nil
}
newSet := &OrderedIntSet{
keys: slices.Clone(m.keys),
values: m.values.Clone(),
}
return newSet
}
// Next returns the next key in the set possibly including the given key.
// It returns -1 if the key is not found or if there are no keys greater than the given key.
func (m *OrderedIntSet) Next(i int) int {
n, ok := m.values.NextSet(uint(i))
if !ok {
return -1
}
return int(n)
}
// The reason we don't use iter.Seq is https://github.com/golang/go/issues/69015
// This is 70% faster than using iter.Seq2[int, int] for the keys.
// It returns false if the iteration was stopped early.
func (m *OrderedIntSet) ForEachKey(yield func(int) bool) bool {
if m == nil {
return true
}
for _, key := range m.keys {
if !yield(key) {
return false
}
}
return true
}
func (m *OrderedIntSet) Has(key int) bool {
if m == nil {
return false
}
return m.values.Test(uint(key))
}
func (m *OrderedIntSet) Len() int {
if m == nil {
return 0
}
return len(m.keys)
}
// KeysSorted returns the keys in sorted order.
func (m *OrderedIntSet) KeysSorted() []int {
if m == nil {
return nil
}
keys := slices.Clone(m.keys)
slices.Sort(keys)
return m.keys
}
func (m *OrderedIntSet) String() string {
if m == nil {
return "[]"
}
return fmt.Sprintf("%v", m.keys)
}
func (m *OrderedIntSet) Values() *bitset.BitSet {
if m == nil {
return nil
}
return m.values
}
func (m *OrderedIntSet) IsSuperSet(other *OrderedIntSet) bool {
if m == nil || other == nil {
return false
}
return m.values.IsSuperSet(other.values)
}
// Words returns the bitset as array of 64-bit words, giving direct access to the internal representation.
// It is not a copy, so changes to the returned slice will affect the bitset.
// It is meant for advanced users.
func (m *OrderedIntSet) Words() []uint64 {
if m == nil {
return nil
}
return m.values.Words()
}
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestOrderedIntSet(t *testing.T) {
c := qt.New(t)
m := NewOrderedIntSet(2, 1, 3, 7)
c.Assert(m.Len(), qt.Equals, 4)
c.Assert(m.Has(1), qt.Equals, true)
c.Assert(m.Has(4), qt.Equals, false)
c.Assert(m.String(), qt.Equals, "[2 1 3 7]")
m.Set(4)
c.Assert(m.Len(), qt.Equals, 5)
c.Assert(m.Has(4), qt.Equals, true)
c.Assert(m.Next(0), qt.Equals, 1)
c.Assert(m.Next(1), qt.Equals, 1)
c.Assert(m.Next(2), qt.Equals, 2)
c.Assert(m.Next(3), qt.Equals, 3)
c.Assert(m.Next(4), qt.Equals, 4)
c.Assert(m.Next(7), qt.Equals, 7)
c.Assert(m.Next(8), qt.Equals, -1)
c.Assert(m.String(), qt.Equals, "[2 1 3 7 4]")
var nilset *OrderedIntSet
c.Assert(nilset.Len(), qt.Equals, 0)
c.Assert(nilset.Has(1), qt.Equals, false)
c.Assert(nilset.String(), qt.Equals, "[]")
var collected []int
m.ForEachKey(func(key int) bool {
collected = append(collected, key)
return true
})
c.Assert(collected, qt.DeepEquals, []int{2, 1, 3, 7, 4})
}
func BenchmarkOrderedIntSet(b *testing.B) {
smallSet := NewOrderedIntSet()
for i := range 8 {
smallSet.Set(i)
}
mediumSet := NewOrderedIntSet()
for i := range 64 {
mediumSet.Set(i)
}
largeSet := NewOrderedIntSet()
for i := range 1024 {
largeSet.Set(i)
}
b.Run("New", func(b *testing.B) {
for b.Loop() {
NewOrderedIntSet(1, 2, 3, 4, 5, 6, 7, 8)
}
})
b.Run("Has small", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
smallSet.Has(i % 32)
}
})
b.Run("Has medium", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
mediumSet.Has(i % 32)
}
})
b.Run("Next", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
mediumSet.Next(i % 32)
}
})
b.Run("ForEachKey small", func(b *testing.B) {
for b.Loop() {
smallSet.ForEachKey(func(key int) bool {
return true
})
}
})
b.Run("ForEachKey medium", func(b *testing.B) {
for b.Loop() {
mediumSet.ForEachKey(func(key int) bool {
return true
})
}
})
b.Run("ForEachKey large", func(b *testing.B) {
for b.Loop() {
largeSet.ForEachKey(func(key int) bool {
return true
})
}
})
}
-222
View File
@@ -1,222 +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 hreflect
import (
"fmt"
"math"
"reflect"
)
var (
typeInt64 = reflect.TypeFor[int64]()
typeFloat64 = reflect.TypeFor[float64]()
typeString = reflect.TypeFor[string]()
)
// ToInt64 converts v to int64 if possible, returning an error if not.
func ToInt64E(v reflect.Value) (int64, error) {
if v, ok := ConvertIfPossible(v, typeInt64); ok {
return v.Int(), nil
}
return 0, errConvert(v, "int64")
}
// ToInt64 converts v to int64 if possible. It panics if the conversion is not possible.
func ToInt64(v reflect.Value) int64 {
vv, err := ToInt64E(v)
if err != nil {
panic(err)
}
return vv
}
// ToFloat64E converts v to float64 if possible, returning an error if not.
func ToFloat64E(v reflect.Value) (float64, error) {
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
return v.Float(), nil
}
return 0, errConvert(v, "float64")
}
// ToFloat64 converts v to float64 if possible, panicking if not.
func ToFloat64(v reflect.Value) float64 {
vv, err := ToFloat64E(v)
if err != nil {
panic(err)
}
return vv
}
// ToStringE converts v to string if possible, returning an error if not.
func ToStringE(v reflect.Value) (string, error) {
vv, err := ToStringValueE(v)
if err != nil {
return "", err
}
return vv.String(), nil
}
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
if v, ok := ConvertIfPossible(v, typeString); ok {
return v, nil
}
return reflect.Value{}, errConvert(v, "string")
}
// ToString converts v to string if possible, panicking if not.
func ToString(v reflect.Value) string {
vv, err := ToStringE(v)
if err != nil {
panic(err)
}
return vv
}
func errConvert(v reflect.Value, s string) error {
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
}
// ConvertIfPossible tries to convert val to typ if possible.
// This is currently only implemented for int kinds,
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
// We can expand on this later if needed.
// This conversion is lossless.
// See Issue 14079.
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
switch val.Kind() {
case reflect.Pointer, reflect.Interface:
if val.IsNil() {
// Return typ's zero value.
return reflect.Zero(typ), true
}
val = val.Elem()
}
if val.Type().AssignableTo(typ) {
// No conversion needed.
return val, true
}
if IsInt(typ.Kind()) {
return convertToIntIfPossible(val, typ)
}
if IsFloat(typ.Kind()) {
return convertToFloatIfPossible(val, typ)
}
if IsUint(typ.Kind()) {
return convertToUintIfPossible(val, typ)
}
if IsString(typ.Kind()) && IsString(val.Kind()) {
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
if i < 0 {
return reflect.Value{}, false
}
u := uint64(i)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
if IsUint(val.Kind()) {
if typ.OverflowUint(val.Uint()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < 0 || f > float64(math.MaxUint64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
u := uint64(f)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
return reflect.Value{}, false
}
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
f := float64(i)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsUint(val.Kind()) {
u := val.Uint()
f := float64(u)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsFloat(val.Kind()) {
if typ.OverflowFloat(val.Float()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
if typ.OverflowInt(val.Int()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsUint(val.Kind()) {
if val.Uint() > uint64(math.MaxInt64) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(val.Uint())) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(f)) {
return reflect.Value{}, false
}
return reflect.ValueOf(int64(f)).Convert(typ), true
}
return reflect.Value{}, false
}
-284
View File
@@ -1,284 +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 hreflect
import (
"math"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
func TestToFuncs(t *testing.T) {
c := qt.New(t)
c.Assert(ToInt64(reflect.ValueOf(int(42))), qt.Equals, int64(42))
c.Assert(ToFloat64(reflect.ValueOf(float32(3.14))), hqt.IsSameFloat64, float64(3.14))
c.Assert(ToString(reflect.ValueOf("hello")), qt.Equals, "hello")
}
func TestConvertIfPossible(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {
name string
value any
typ any
expected any
ok bool
}{
// From uint to int.
{
name: "uint64(math.MaxUint64) to int16",
value: uint64(math.MaxUint64),
typ: int16(0),
ok: false, // overflow
},
{
name: "uint64(math.MaxUint64) to int64",
value: uint64(math.MaxUint64),
typ: int64(0),
ok: false, // overflow
},
{
name: "uint64(math.MaxInt16) to int16",
value: uint64(math.MaxInt16),
typ: int64(0),
ok: true,
expected: int64(math.MaxInt16),
},
// From int to int.
{
name: "int64(math.MaxInt64) to int16",
value: int64(math.MaxInt64),
typ: int16(0),
ok: false, // overflow
},
{
name: "int64(math.MaxInt16) to int",
value: int64(math.MaxInt16),
typ: int(0),
ok: true,
expected: int(math.MaxInt16),
},
{
name: "int64(math.MaxInt16) to int",
value: int64(math.MaxInt16),
typ: int(0),
ok: true,
expected: int(math.MaxInt16),
},
// From float64 to int.
{
name: "float64(1.5) to int",
value: float64(1.5),
typ: int(0),
ok: false, // loss of precision
},
{
name: "float64(1.0) to int",
value: float64(1.0),
typ: int(0),
ok: true,
expected: int(1),
},
{
name: "float64(math.MaxInt16+1) to int16",
value: float64(math.MaxInt16 + 1),
typ: int16(0),
ok: false, // overflow
},
{
name: "float64(math.MaxFloat64) to int64",
value: float64(math.MaxFloat64),
typ: int64(0),
ok: false, // overflow
},
{
name: "float64(32767) to int16",
value: float64(32767),
typ: int16(0),
ok: true,
expected: int16(32767),
},
// From float32 to int.
{
name: "float32(1.5) to int",
value: float32(1.5),
typ: int(0),
ok: false, // loss of precision
},
{
name: "float32(1.0) to int",
value: float32(1.0),
typ: int(0),
ok: true,
expected: int(1),
},
{
name: "float32(math.MaxFloat32) to int16",
value: float32(math.MaxFloat32),
typ: int16(0),
ok: false, // overflow
},
{
name: "float32(math.MaxFloat32) to int64",
value: float32(math.MaxFloat32),
typ: int64(0),
ok: false, // overflow
},
{
name: "float32(math.MaxInt16) to int16",
value: float32(math.MaxInt16),
typ: int16(0),
ok: true,
expected: int16(32767),
},
{
name: "float32(math.MaxInt16+1) to int16",
value: float32(math.MaxInt16 + 1),
typ: int16(0),
ok: false, // overflow
},
// Int to float.
{
name: "int16(32767) to float32",
value: int16(32767),
typ: float32(0),
ok: true,
expected: float32(32767),
},
{
name: "int64(32767) to float32",
value: int64(32767),
typ: float32(0),
ok: true,
expected: float32(32767),
},
{
name: "int64(math.MaxInt64) to float32",
value: int64(math.MaxInt64),
typ: float32(0),
ok: true,
expected: float32(math.MaxInt64),
},
{
name: "int64(math.MaxInt64) to float64",
value: int64(math.MaxInt64),
typ: float64(0),
ok: true,
expected: float64(math.MaxInt64),
},
// Int to uint.
{
name: "int16(32767) to uint16",
value: int16(32767),
typ: uint16(0),
ok: true,
expected: uint16(32767),
},
{
name: "int16(32767) to uint8",
value: int16(32767),
typ: uint8(0),
ok: false,
},
{
name: "float64(3.14) to uint64",
value: float64(3.14),
typ: uint64(0),
ok: false,
},
{
name: "float64(3.0) to uint64",
value: float64(3.0),
typ: uint64(0),
ok: true,
expected: uint64(3),
},
// From uint to float.
{
name: "uint64(math.MaxInt16) to float64",
value: uint64(math.MaxInt16),
typ: float64(0),
ok: true,
expected: float64(math.MaxInt16),
},
// Float to float.
{
name: "float64(3.14) to float32",
value: float64(3.14),
typ: float32(0),
ok: true,
expected: float32(3.14),
},
{
name: "float32(3.14) to float64",
value: float32(3.14),
typ: float64(0),
ok: true,
expected: float64(3.14),
},
{
name: "float64(3.14) to float64",
value: float64(3.14),
typ: float64(0),
ok: true,
expected: float64(3.14),
},
} {
v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ))
c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name))
if test.ok {
c.Assert(v.Interface(), hqt.IsSameNumber, test.expected, qt.Commentf("test case: %s", test.name))
}
}
}
func TestConvertIfPossibleMisc(t *testing.T) {
c := qt.New(t)
type s string
var (
i = int32(42)
i64 = int64(i)
iv any = i
ip = &i
inil any = (*int32)(nil)
shello = s("hello")
)
convertOK := func(v any, typ any) any {
rv, ok := ConvertIfPossible(reflect.ValueOf(v), reflect.TypeOf(typ))
c.Assert(ok, qt.IsTrue)
return rv.Interface()
}
c.Assert(convertOK(shello, ""), qt.Equals, "hello")
c.Assert(convertOK(ip, int64(0)), qt.Equals, i64)
c.Assert(convertOK(iv, int64(0)), qt.Equals, i64)
c.Assert(convertOK(inil, int64(0)), qt.Equals, int64(0))
}
func BenchmarkToInt64(b *testing.B) {
v := reflect.ValueOf(int(42))
for b.Loop() {
ToInt64(v)
}
}
+27 -109
View File
@@ -1,4 +1,6 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,16 +22,11 @@ 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.Pointer
}
// TODO(bep) replace the private versions in /tpl with these.
// IsNumber returns whether the given kind is a number.
func IsNumber(kind reflect.Kind) bool {
@@ -66,11 +63,6 @@ func IsFloat(kind reflect.Kind) bool {
}
}
// IsString returns whether the given kind is a string.
func IsString(kind reflect.Kind) bool {
return kind == reflect.String
}
// IsTruthful returns whether in represents a truthful value.
// See IsTruthfulValue
func IsTruthful(in any) bool {
@@ -92,19 +84,7 @@ func IsSlice(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Slice
}
var zeroType = reflect.TypeFor[types.Zeroer]()
var isZeroCache sync.Map
func implementsIsZero(tp reflect.Type) bool {
v, ok := isZeroCache.Load(tp)
if ok {
return v.(bool)
}
implements := tp.Implements(zeroType)
isZeroCache.Store(tp, implements)
return implements
}
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
// IsTruthfulValue returns whether the given value has a meaningful truth value.
// This is based on template.IsTrue in Go's stdlib, but also considers
@@ -114,18 +94,14 @@ func implementsIsZero(tp reflect.Type) bool {
// Based on:
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
func IsTruthfulValue(val reflect.Value) (truth bool) {
val, isNil := Indirect(val)
val = indirectInterface(val)
if !val.IsValid() {
// Something like: var x any, never set. It's a form of nil.
// Something like var x interface{}, never set. It's a form of nil.
return
}
if val.Kind() == reflect.Pointer && isNil {
return
}
if implementsIsZero(val.Type()) {
if val.Type().Implements(zeroType) {
return !val.Interface().(types.Zeroer).IsZero()
}
@@ -136,7 +112,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.Pointer, reflect.UnsafePointer, reflect.Interface:
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
@@ -158,31 +134,10 @@ type methodKey struct {
name string
}
var (
methodIndexCache sync.Map
methodCache sync.Map
)
var methodCache sync.Map
// GetMethodByNameForType returns the method with the given name for the given type,
// or a zero Method if no such method exists.
// It panics if tp is an interface type.
// It caches the lookup.
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
if tp.Kind() == reflect.Interface {
// Func field is nil for interface types.
panic("not supported for interface types")
}
k := methodKey{tp, name}
v, found := methodCache.Load(k)
if found {
return v.(reflect.Method)
}
m, _ := tp.MethodByName(name)
methodCache.Store(k, m)
return m
}
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup.
func GetMethodByName(v reflect.Value, name string) reflect.Value {
index := GetMethodIndexByName(v.Type(), name)
@@ -197,7 +152,7 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
// -1 if no such method exists.
func GetMethodIndexByName(tp reflect.Type, name string) int {
k := methodKey{tp, name}
v, found := methodIndexCache.Load(k)
v, found := methodCache.Load(k)
if found {
return v.(int)
}
@@ -206,7 +161,7 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
if !ok {
index = -1
}
methodIndexCache.Store(k, index)
methodCache.Store(k, index)
if !ok {
return -1
@@ -216,8 +171,8 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
}
var (
timeType = reflect.TypeFor[time.Time]()
asTimeProviderType = reflect.TypeFor[htime.AsTimeProvider]()
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
)
// IsTime returns whether tp is a time.Time type or if it can be converted into one
@@ -240,7 +195,7 @@ func IsValid(v reflect.Value) bool {
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return !v.IsNil()
}
@@ -288,7 +243,6 @@ func ToSliceAny(v any) ([]any, bool) {
return nil, false
}
// CallMethodByName calls the method with the given name on v.
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
fn := v.MethodByName(name)
var args []reflect.Value
@@ -306,56 +260,20 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
return fn.Call(args)
}
// Indirect unwraps interfaces and pointers until it finds a non-interface/pointer value.
// If a nil is encountered, the second return value is true.
// If a pointer to a struct is encountered, it is not unwrapped.
func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
for ; IsInterfaceOrPointer(v.Kind()); v = v.Elem() {
if IsNil(v) {
return v, true
}
if v.Kind() != reflect.Interface {
// A pointer.
if v.NumMethod() > 0 {
break
}
if v.Elem().Kind() == reflect.Struct {
// Avoid unwrapping pointers to structs.
break
}
}
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
func indirectInterface(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Interface {
return v
}
return v, false
if v.IsNil() {
return reflect.Value{}
}
return v.Elem()
}
// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
vv, isNil = Indirect(v)
if isNil {
return vv, isNil
}
if vv.Kind() == reflect.Pointer {
vv = vv.Elem()
}
return vv, isNil
}
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
// IsNil reports whether v is nil.
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return v.IsNil()
}
return false
}
var contextInterface = reflect.TypeFor[context.Context]()
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
var isContextCache = maps.NewCache[reflect.Type, bool]()
type k string
+16 -126
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,32 +20,15 @@ import (
"time"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
type zeroStruct struct {
zero bool
}
func (z zeroStruct) IsZero() bool {
return z.zero
}
func TestIsTruthful(t *testing.T) {
c := qt.New(t)
var nilpointerZero *zeroStruct
c.Assert(IsTruthful(true), qt.Equals, true)
c.Assert(IsTruthful(false), qt.Equals, false)
c.Assert(IsTruthful(time.Now()), qt.Equals, true)
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
c.Assert(IsTruthful(&zeroStruct{zero: false}), qt.Equals, true)
c.Assert(IsTruthful(&zeroStruct{zero: true}), qt.Equals, false)
c.Assert(IsTruthful(zeroStruct{zero: false}), qt.Equals, true)
c.Assert(IsTruthful(zeroStruct{zero: true}), qt.Equals, false)
c.Assert(IsTruthful(nil), qt.Equals, false)
c.Assert(IsTruthful(nilpointerZero), qt.Equals, false)
}
func TestGetMethodByName(t *testing.T) {
@@ -80,90 +63,18 @@ func TestToSliceAny(t *testing.T) {
checkOK([]int{1, 2, 3}, []any{1, 2, 3})
}
type testIndirectStruct struct {
S string
}
func (t *testIndirectStruct) GetS() string {
return t.S
}
func (t testIndirectStruct) Foo() string {
return "bar"
}
type testIndirectStructNoMethods struct {
S string
}
func TestIsNil(t *testing.T) {
c := qt.New(t)
var (
nilPtr *testIndirectStruct
nilIface any = nil
nonNilIface any = &testIndirectStruct{S: "hello"}
)
c.Assert(IsNil(reflect.ValueOf(nilPtr)), qt.Equals, true)
c.Assert(IsNil(reflect.ValueOf(nilIface)), qt.Equals, true)
c.Assert(IsNil(reflect.ValueOf(nonNilIface)), qt.Equals, false)
}
func TestIndirectInterface(t *testing.T) {
c := qt.New(t)
var (
structWithMethods = testIndirectStruct{S: "hello"}
structWithMethodsPointer = &testIndirectStruct{S: "hello"}
structWithMethodsPointerAny any = structWithMethodsPointer
structPointerToPointer = &structWithMethodsPointer
structNoMethodsPtr = &testIndirectStructNoMethods{S: "no methods"}
structNoMethods = testIndirectStructNoMethods{S: "no methods"}
intValue = 32
intPtr = &intValue
nilPtr *testIndirectStruct
nilIface any = nil
)
ind := func(v any) any {
c.Helper()
vv, isNil := Indirect(reflect.ValueOf(v))
c.Assert(isNil, qt.IsFalse)
return vv.Interface()
}
c.Assert(ind(intValue), hqt.IsSameType, 32)
c.Assert(ind(intPtr), hqt.IsSameType, 32)
c.Assert(ind(structNoMethodsPtr), hqt.IsSameType, structNoMethodsPtr)
c.Assert(ind(structWithMethods), hqt.IsSameType, structWithMethods)
c.Assert(ind(structNoMethods), hqt.IsSameType, structNoMethods)
c.Assert(ind(structPointerToPointer), hqt.IsSameType, &testIndirectStruct{})
c.Assert(ind(structWithMethodsPointer), hqt.IsSameType, &testIndirectStruct{})
c.Assert(ind(structWithMethodsPointerAny), hqt.IsSameType, structWithMethodsPointer)
vv, isNil := Indirect(reflect.ValueOf(nilPtr))
c.Assert(isNil, qt.IsTrue)
c.Assert(vv, qt.Equals, reflect.ValueOf(nilPtr))
vv, isNil = Indirect(reflect.ValueOf(nilIface))
c.Assert(isNil, qt.IsFalse)
c.Assert(vv, qt.Equals, reflect.ValueOf(nilIface))
}
func BenchmarkIsContextType(b *testing.B) {
const size = 1000
type k string
b.Run("value", func(b *testing.B) {
ctx := context.Background()
ctxs := make([]reflect.Type, size)
for i := range size {
ctxs := make([]reflect.Type, b.N)
for i := 0; i < b.N; i++ {
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
}
for i := 0; b.Loop(); i++ {
idx := i % size
if !IsContextType(ctxs[idx]) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !IsContextType(ctxs[i]) {
b.Fatal("not context")
}
}
@@ -171,7 +82,7 @@ func BenchmarkIsContextType(b *testing.B) {
b.Run("background", func(b *testing.B) {
var ctxt reflect.Type = reflect.TypeOf(context.Background())
for b.Loop() {
for i := 0; i < b.N; i++ {
if !IsContextType(ctxt) {
b.Fatal("not context")
}
@@ -179,25 +90,14 @@ func BenchmarkIsContextType(b *testing.B) {
})
}
func BenchmarkIsTruthFulValue(b *testing.B) {
var (
stringHugo = reflect.ValueOf("Hugo")
stringEmpty = reflect.ValueOf("")
zero = reflect.ValueOf(time.Time{})
timeNow = reflect.ValueOf(time.Now())
boolTrue = reflect.ValueOf(true)
boolFalse = reflect.ValueOf(false)
nilPointer = reflect.ValueOf((*zeroStruct)(nil))
)
func BenchmarkIsTruthFul(b *testing.B) {
v := reflect.ValueOf("Hugo")
for b.Loop() {
IsTruthfulValue(stringHugo)
IsTruthfulValue(stringEmpty)
IsTruthfulValue(zero)
IsTruthfulValue(timeNow)
IsTruthfulValue(boolTrue)
IsTruthfulValue(boolFalse)
IsTruthfulValue(nilPointer)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !IsTruthfulValue(v) {
b.Fatal("not truthful")
}
}
}
@@ -223,22 +123,12 @@ func (t *testStruct) Method5() string {
return "Hugo"
}
func BenchmarkGetMethodByNameForType(b *testing.B) {
tp := reflect.TypeFor[*testStruct]()
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
for b.Loop() {
for _, method := range methods {
_ = GetMethodByNameForType(tp, method)
}
}
}
func BenchmarkGetMethodByName(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, method := range methods {
_ = GetMethodByName(v, method)
}
+4 -69
View File
@@ -17,7 +17,6 @@ import (
"fmt"
"regexp"
"slices"
"sort"
"strings"
"sync"
@@ -97,16 +96,6 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
return reCache.getOrCompileRegexp(pattern)
}
// HasAnyPrefix checks if the string s has any of the prefixes given.
func HasAnyPrefix(s string, prefixes ...string) bool {
for _, p := range prefixes {
if strings.HasPrefix(s, p) {
return true
}
}
return false
}
// InSlice checks if a string is an element of a slice of strings
// and returns a boolean value.
func InSlice(arr []string, el string) bool {
@@ -139,61 +128,7 @@ func ToString(v any) (string, bool) {
return "", false
}
// UniqueStrings returns a new slice with any duplicates removed.
func UniqueStrings(s []string) []string {
unique := make([]string, 0, len(s))
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
unique = append(unique, val)
}
}
return unique
}
// UniqueStringsReuse returns a slice with any duplicates removed.
// It will modify the input slice.
func UniqueStringsReuse(s []string) []string {
result := s[:0]
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
result = append(result, val)
}
}
return result
}
// UniqueStringsSorted returns a sorted slice with any duplicates removed.
// It will modify the input slice.
func UniqueStringsSorted(s []string) []string {
if len(s) == 0 {
return nil
}
ss := sort.StringSlice(s)
ss.Sort()
i := 0
for j := 1; j < len(s); j++ {
if !ss.Less(i, j) {
continue
}
i++
s[i] = s[j]
}
return s[:i+1]
}
type (
Strings2 [2]string
Strings3 [3]string
)
+3 -81
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@
package hstrings
import (
"reflect"
"regexp"
"testing"
@@ -44,91 +43,14 @@ func TestGetOrCompileRegexp(t *testing.T) {
c.Assert(re.MatchString("123"), qt.Equals, true)
}
func TestUniqueStrings(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStrings(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
}
}
func TestUniqueStringsReuse(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStringsReuse(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
}
}
func TestUniqueStringsSorted(t *testing.T) {
c := qt.New(t)
in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
output := UniqueStringsSorted(in)
expected := []string{"", "a", "b", "c", "d"}
c.Assert(output, qt.DeepEquals, expected)
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
}
// Note that these cannot use b.Loop() because of golang/go#27217.
func BenchmarkUniqueStrings(b *testing.B) {
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
b.Run("Safe", func(b *testing.B) {
for b.Loop() {
result := UniqueStrings(input)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
b.Run("Reuse slice", func(b *testing.B) {
inputs := make([][]string, b.N)
for i := 0; i < b.N; i++ {
inputc := make([]string, len(input))
copy(inputc, input)
inputs[i] = inputc
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsReuse(inputc)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
b.Run("Reuse slice sorted", func(b *testing.B) {
inputs := make([][]string, b.N)
for i := 0; i < b.N; i++ {
inputc := make([]string, len(input))
copy(inputc, input)
inputs[i] = inputc
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsSorted(inputc)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
}
func BenchmarkGetOrCompileRegexp(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
GetOrCompileRegexp(`\d+`)
}
}
func BenchmarkCompileRegexp(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
regexp.MustCompile(`\d+`)
}
}
-162
View File
@@ -1,162 +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 hsync
import (
"context"
"sync"
"sync/atomic"
)
// OnceMore is similar to sync.Once.
//
// Additional features are:
// * it can be reset, so the action can be repeated if needed
// * it has methods to check if it's done or in progress
type OnceMore struct {
_ doNotCopy
done atomic.Bool
mu sync.Mutex
}
func (t *OnceMore) Do(f func()) {
if t.Done() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
// Double check
if t.Done() {
return
}
defer t.done.Store(true)
f()
}
func (t *OnceMore) Done() bool {
return t.done.Load()
}
func (t *OnceMore) Reset() {
t.mu.Lock()
t.done.Store(false)
t.mu.Unlock()
}
type ValueResetter[T any] struct {
reset func()
f func(context.Context) T
}
func (v *ValueResetter[T]) Value(ctx context.Context) T {
return v.f(ctx)
}
func (v *ValueResetter[T]) Reset() {
v.reset()
}
// OnceMoreValue returns a function that invokes f only once and returns the value
// returned by f. The returned function may be called concurrently.
//
// If f panics, the returned function will panic with the same value on every call.
func OnceMoreValue[T any](f func(context.Context) T) ValueResetter[T] {
v := struct {
f func(context.Context) T
once OnceMore
ok bool
p any
result T
}{
f: f,
}
ff := func(ctx context.Context) T {
v.once.Do(func() {
v.ok = false
defer func() {
v.p = recover()
if !v.ok {
panic(v.p)
}
}()
v.result = v.f(ctx)
v.ok = true
})
if !v.ok {
panic(v.p)
}
return v.result
}
return ValueResetter[T]{
reset: v.once.Reset,
f: ff,
}
}
type FuncResetter struct {
f func(context.Context) error
reset func()
}
func (v *FuncResetter) Do(ctx context.Context) error {
return v.f(ctx)
}
func (v *FuncResetter) Reset() {
v.reset()
}
func OnceMoreFunc(f func(context.Context) error) FuncResetter {
v := struct {
f func(context.Context) error
once OnceMore
ok bool
err error
p any
}{
f: f,
}
ff := func(ctx context.Context) error {
v.once.Do(func() {
v.ok = false
defer func() {
v.p = recover()
if !v.ok {
panic(v.p)
}
}()
v.err = v.f(ctx)
v.ok = true
})
if !v.ok {
panic(v.p)
}
return v.err
}
return FuncResetter{
f: ff,
reset: v.once.Reset,
}
}
type doNotCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*doNotCopy) Lock() {}
func (*doNotCopy) Unlock() {}
-80
View File
@@ -1,80 +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 hsync
import (
"context"
"testing"
qt "github.com/frankban/quicktest"
)
func TestOnceMoreValue(t *testing.T) {
c := qt.New(t)
var counter int
f := func(context.Context) int {
counter++
return counter
}
omf := OnceMoreValue(f)
for range 10 {
c.Assert(omf.Value(context.Background()), qt.Equals, 1)
}
omf.Reset()
for range 10 {
c.Assert(omf.Value(context.Background()), qt.Equals, 2)
}
}
func TestOnceMoreFunc(t *testing.T) {
c := qt.New(t)
var counter int
f := func(context.Context) error {
counter++
return nil
}
omf := OnceMoreFunc(f)
for range 10 {
c.Assert(omf.Do(context.Background()), qt.IsNil)
c.Assert(counter, qt.Equals, 1)
}
omf.Reset()
for range 10 {
c.Assert(omf.Do(context.Background()), qt.IsNil)
c.Assert(counter, qt.Equals, 2)
}
}
func BenchmarkOnceMoreValue(b *testing.B) {
var counter int
f := func(context.Context) int {
counter++
return counter
}
for b.Loop() {
omf := OnceMoreValue(f)
for range 10 {
omf.Value(context.Background())
}
omf.Reset()
for range 10 {
omf.Value(context.Background())
}
}
}
+2 -2
View File
@@ -24,9 +24,9 @@ func TestApplyWithContext(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- config.toml --
defaultContentLanguage = 'it'
-- layouts/home.html --
-- layouts/index.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 golocales.Translator) TimeFormatter {
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
if ltr == nil {
panic("must provide a golocales.Translator")
panic("must provide a locales.Translator")
}
return TimeFormatter{
ltr: ltr,
@@ -89,7 +89,7 @@ func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
// TimeFormatter is locale aware.
type TimeFormatter struct {
ltr golocales.Translator
ltr locales.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.FormatDateFull(t)
return f.ltr.FmtDateFull(t)
case "date_long":
return f.ltr.FormatDateLong(t)
return f.ltr.FmtDateLong(t)
case "date_medium":
return f.ltr.FormatDateMedium(t)
return f.ltr.FmtDateMedium(t)
case "date_short":
return f.ltr.FormatDateShort(t)
return f.ltr.FmtDateShort(t)
case "time_full":
return f.ltr.FormatTimeFull(t)
return f.ltr.FmtTimeFull(t)
case "time_long":
return f.ltr.FormatTimeLong(t)
return f.ltr.FmtTimeLong(t)
case "time_medium":
return f.ltr.FormatTimeMedium(t)
return f.ltr.FmtTimeMedium(t)
case "time_short":
return f.ltr.FormatTimeShort(t)
return f.ltr.FmtTimeShort(t)
}
}
s := t.Format(layout)
monthIdx := t.Month() - 1 // time.Month is 1-based, but our month name slices are 0-based.
monthIdx := t.Month() - 1 // Month() starts at 1.
dayIdx := t.Weekday()
if strings.Contains(layout, "January") {
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthsWide()[monthIdx])
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
} else if strings.Contains(layout, "Jan") {
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthsAbbreviated()[monthIdx])
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
}
if strings.Contains(layout, "Monday") {
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdaysWide()[dayIdx])
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
} else if strings.Contains(layout, "Mon") {
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdaysAbbreviated()[dayIdx])
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
}
return s
+21 -22
View File
@@ -17,9 +17,8 @@ import (
"testing"
"time"
"github.com/bep/golocales"
qt "github.com/frankban/quicktest"
translators "github.com/gohugoio/localescompressed"
)
func TestTimeFormatter(t *testing.T) {
@@ -35,7 +34,7 @@ func TestTimeFormatter(t *testing.T) {
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
c.Run("Norsk nynorsk", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("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")
@@ -43,35 +42,35 @@ func TestTimeFormatter(t *testing.T) {
})
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("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.18")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "02:09:37 UTC")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 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(golocales.New("en"))
f := NewTimeFormatter(translators.GetTranslator("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\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.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.Run("English", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("en"))
f := NewTimeFormatter(translators.GetTranslator("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")
@@ -79,31 +78,31 @@ func TestTimeFormatter(t *testing.T) {
})
c.Run("Weekdays German", func(c *qt.C) {
tr := golocales.New("de")
tr := translators.GetTranslator("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.WeekdaysWide()[date.Weekday()], qt.Equals, weekDayWideGerman)
c.Assert(tr.WeekdayWide(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.WeekdaysAbbreviated()[date.Weekday()], qt.Equals, weekDayAbbreviatedGerman)
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
}
})
c.Run("Months German", func(c *qt.C) {
tr := golocales.New("de")
tr := translators.GetTranslator("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.MonthsWide()[date.Month()-1], qt.Equals, monthWideNorway)
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
}
})
@@ -113,7 +112,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
b.Run("Native", func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
got := june06.Format("Monday Jan 2 2006")
if got != "Wednesday Jun 6 2018" {
b.Fatalf("invalid format, got %q", got)
@@ -122,9 +121,9 @@ func BenchmarkTimeFormatter(b *testing.B) {
})
b.Run("Localized", func(b *testing.B) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for b.Loop() {
for i := 0; i < b.N; i++ {
got := f.Format(june06, "Monday Jan 2 2006")
if got != "onsdag juni 6 2018" {
b.Fatalf("invalid format, got %q", got)
@@ -133,9 +132,9 @@ func BenchmarkTimeFormatter(b *testing.B) {
})
b.Run("Localized Custom", func(b *testing.B) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for b.Loop() {
for i := 0; i < b.N; i++ {
got := f.Format(june06, ":date_medium")
if got != "6. juni 2018" {
b.Fatalf("invalid format, got %q", got)
+9 -50
View File
@@ -19,44 +19,19 @@ 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 {
io.ReadSeeker
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
func (f CloserFunc) Close() error {
return f()
}
// ReadSeekCloserProvider provides a ReadSeekCloser.
type ReadSeekCloserProvider interface {
ReadSeekCloser() (ReadSeekCloser, error)
@@ -64,7 +39,7 @@ type ReadSeekCloserProvider interface {
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
type readSeekerNopCloser struct {
io.ReadSeeker
ReadSeeker
}
// Close does nothing.
@@ -73,7 +48,7 @@ func (r readSeekerNopCloser) Close() error {
}
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
return readSeekerNopCloser{r}
}
@@ -105,22 +80,6 @@ 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 {
+126 -71
View File
@@ -16,6 +16,7 @@ package hugo
import (
"context"
"fmt"
"html/template"
"os"
"path/filepath"
"runtime/debug"
@@ -24,14 +25,13 @@ import (
"sync"
"time"
"github.com/bep/helpers/contexthelpers"
"github.com/bep/logg"
"github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/version"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/spf13/afero"
@@ -54,24 +54,89 @@ var (
vendorInfo string
)
// BuildInfo holds build information extracted from runtime/debug.
type BuildInfo struct {
Revision string
RevisionTime string
GoVersion string
var _ maps.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 *maps.Scratch
// Context gives access to some of the context scoped variables.
Context Context
}
// 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,
}
// Version returns the current version as a comparable version string.
func (i HugoInfo) 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() *maps.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()
}
type contextKey uint8
@@ -80,9 +145,8 @@ const (
contextKeyMarkupScope contextKey = iota
)
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
// Context gives access to some of the context scoped variables.
type Context struct{}
func (c Context) MarkupScope(ctx context.Context) string {
@@ -99,6 +163,44 @@ 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: maps.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 {
@@ -206,12 +308,13 @@ func GetDependencyList() []string {
// GetDependencyListNonGo returns a list of non-Go dependencies.
func GetDependencyListNonGo() []string {
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
var deps []string
if IsExtended {
deps = append(
deps,
formatDep("github.com/sass/libsass", "3.6.6"),
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
)
}
@@ -334,13 +437,8 @@ 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)
@@ -353,7 +451,7 @@ func deprecateLevelWithLogger(item, alternative, version string, level logg.Leve
// We want people to run at least the current and previous version without any warnings.
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
func deprecationLogLevelFromVersion(ver string) logg.Level {
from := version.MustParseVersion(ver)
from := MustParseVersion(ver)
to := CurrentVersion
minorDiff := to.Minor - from.Minor
switch {
@@ -367,46 +465,3 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
return logg.LevelInfo
}
}
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
+1 -37
View File
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
---
title: home
---
-- layouts/home.html --
-- layouts/index.html --
multilingual={{ hugo.IsMultilingual }}
multihost={{ hugo.IsMultihost }}
`
@@ -75,39 +75,3 @@ 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|")
}
+63 -13
View File
@@ -15,12 +15,42 @@ package hugo
import (
"context"
"fmt"
"testing"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
)
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", 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)
@@ -35,27 +65,47 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
// Added just to find the threshold for where we can remove deprecated items.
// Subtract 5 from the minor version of the first ERRORed version => 0.136.0.
c.Assert(deprecationLogLevelFromVersion("0.141.0"), qt.Equals, logg.LevelError)
// Subtract 5 from the minor version of the first ERRORed version => 0.122.0.
c.Assert(deprecationLogLevelFromVersion("0.127.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")
var hugoCtx Context
c.Assert(hugoCtx.MarkupScope(ctx), qt.Equals, "foo")
c.Assert(GetMarkupScope(ctx), qt.Equals, "foo")
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
}
func TestGetBuildInfo(t *testing.T) {
c := qt.New(t)
bi := GetBuildInfo()
// In test mode, build info may or may not be available.
if bi != nil {
c.Assert(bi.GoVersion, qt.Not(qt.Equals), "")
}
type testConfig struct {
environment string
running bool
workingDir string
multihost bool
multilingual bool
}
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
}
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 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 version
package hugo
import (
"fmt"
@@ -37,9 +37,6 @@ 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 (
@@ -47,17 +44,6 @@ 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)
}
@@ -69,7 +55,7 @@ func (v Version) Version() VersionString {
// Compare implements the compare.Comparer interface.
func (h Version) Compare(other any) int {
return CompareVersions(h, other)
return compareVersions(h, other)
}
// VersionString represents a Hugo version string.
@@ -81,7 +67,7 @@ func (h VersionString) String() string {
// Compare implements the compare.Comparer interface.
func (h VersionString) Compare(other any) int {
return CompareVersions(h.Version(), other)
return compareVersions(h.Version(), other)
}
func (h VersionString) Version() Version {
@@ -97,26 +83,19 @@ func (h VersionString) Eq(other any) bool {
return s == h.String()
}
var versionSuffixes = []string{"-test", "-DEV"}
// ParseVersion parses a version string.
func ParseVersion(s string) (Version, error) {
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
var vv Version
hyphen := strings.Index(s, "-")
if hyphen > 0 {
suffix := s[hyphen:]
if len(suffix) > 1 {
if suffix[0] == '-' {
suffix = suffix[1:]
}
if len(suffix) > 0 {
vv.Suffix = suffix
s = s[:hyphen]
}
for _, suffix := range versionSuffixes {
if strings.HasSuffix(s, suffix) {
vv.Suffix = suffix
s = strings.TrimSuffix(s, suffix)
}
vv.Suffix = suffix
}
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
vv.source = s
return vv, nil
}
@@ -155,70 +134,107 @@ func (v Version) NextPatchLevel(level int) Version {
return prev
}
func version(major, minor, patch int, suffix string) string {
if suffix != "" {
if suffix[0] != '-' {
suffix = "-" + suffix
}
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
func version(major, minor, patch int, suffix string) string {
if patch > 0 || minor > 53 {
return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix)
}
return fmt.Sprintf("%d.%d%s", major, minor, suffix)
}
// CompareVersion compares v1 with v2.
// It returns -1 if the v2 is less than, 0 if equal and 1 if greater than
// v1.
func CompareVersions(v1 Version, v2 any) int {
// CompareVersion compares the given version string or number against the
// running Hugo version.
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
// the running version.
func CompareVersion(version any) int {
return compareVersions(CurrentVersion, version)
}
func compareVersions(inVersion Version, in any) int {
var c int
switch d := v2.(type) {
switch d := in.(type) {
case float64:
c = compareFloatWithVersion(d, v1)
c = compareFloatWithVersion(d, inVersion)
case float32:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case int:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case int32:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case int64:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case Version:
if v1.IsZero() && d.IsZero() {
// Fall back to source comparison.
return strings.Compare(v1.source, d.source)
if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel {
return strings.Compare(inVersion.Suffix, d.Suffix)
}
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
return strings.Compare(v1.Suffix, d.Suffix)
}
if d.Major > v1.Major {
if d.Major > inVersion.Major {
return 1
} else if d.Major < v1.Major {
} else if d.Major < inVersion.Major {
return -1
}
if d.Minor > v1.Minor {
if d.Minor > inVersion.Minor {
return 1
} else if d.Minor < v1.Minor {
} else if d.Minor < inVersion.Minor {
return -1
}
if d.PatchLevel > v1.PatchLevel {
if d.PatchLevel > inVersion.PatchLevel {
return 1
} else if d.PatchLevel < v1.PatchLevel {
} else if d.PatchLevel < inVersion.PatchLevel {
return -1
}
default:
s, err := cast.ToStringE(v2)
s, err := cast.ToStringE(in)
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)
v, err := ParseVersion(s)
if err != nil {
return -1
}
return v1.Compare(v2v)
return inVersion.Compare(v)
}
+3 -5
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -13,13 +13,11 @@
package hugo
import "github.com/gohugoio/hugo/common/version"
// CurrentVersion represents the current build version.
// This should be the only one.
var CurrentVersion = version.Version{
var CurrentVersion = Version{
Major: 0,
Minor: 160,
Minor: 149,
PatchLevel: 0,
Suffix: "-DEV",
}
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2015 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 version
package hugo
import (
"testing"
@@ -50,45 +50,30 @@ 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)
c.Assert(CompareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1)
c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1)
c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
}
func TestParseHugoVersion(t *testing.T) {
c := qt.New(t)
c.Assert(MustParseVersion("v2.3.2").String(), qt.Equals, "2.3.2")
c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25")
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
+2 -15
View File
@@ -20,7 +20,6 @@ import (
"io"
"os"
"strings"
"sync"
"time"
"github.com/bep/logg"
@@ -124,11 +123,8 @@ 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)
@@ -139,7 +135,6 @@ func New(opts Options) Logger {
}
return &logAdapter{
mu: logMu,
logCounters: logCounters,
errors: errorsw,
reset: reset,
@@ -178,7 +173,6 @@ 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
@@ -209,7 +203,6 @@ type Logger interface {
}
type logAdapter struct {
mu *sync.Mutex
logCounters *logLevelCounter
errors *strings.Builder
reset func()
@@ -228,10 +221,6 @@ 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...)
}
@@ -286,7 +275,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
if milli < 500 {
return
}
fmt.Fprintf(l.stdErr, "%s in %v ms\n", name, milli)
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli)
}
func (l *logAdapter) Printf(format string, v ...any) {
@@ -334,8 +323,6 @@ func (l *logAdapter) Errorln(v ...any) {
}
func (l *logAdapter) Errors() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.errors.String()
}
@@ -352,7 +339,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 project configuration:\nignoreLogs = ['%s']", what, id)
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
}
func (l *logAdapter) Trace(s logg.StringFunc) {
+4 -27
View File
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"sync"
@@ -20,27 +20,13 @@ import (
// Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct {
m map[K]T
opts CacheOptions
hasBeenInitialized bool
sync.RWMutex
}
// CacheOptions are the options for the Cache.
type CacheOptions struct {
// If set, the cache will not grow beyond this size.
Size uint64
}
var defaultCacheOptions = CacheOptions{}
// NewCache creates a new Cache with default options.
// NewCache creates a new Cache.
func NewCache[K comparable, T any]() *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions}
}
// NewCacheWithOptions creates a new Cache with the given options.
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: opts}
return &Cache[K, T]{m: make(map[K]T)}
}
// Delete deletes the given key from the cache.
@@ -79,7 +65,6 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
if err != nil {
return v, err
}
c.clearIfNeeded()
c.m[key] = v
return v, nil
}
@@ -142,15 +127,7 @@ func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
}
}
func (c *Cache[K, T]) clearIfNeeded() {
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
// clear the map
clear(c.m)
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.clearIfNeeded()
c.m[key] = value
}
+2 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2018 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 hmaps
package maps
import (
"fmt"
@@ -98,8 +98,6 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
return v, nil
case Params:
return []map[string]any{v}, nil
case map[string]any:
return []map[string]any{v}, nil
case []any:
var s []map[string]any
for _, entry := range v {
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2018 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 hmaps
package maps
import (
"fmt"
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"slices"
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"testing"
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2019 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,10 +11,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"errors"
"fmt"
"strings"
@@ -41,14 +40,6 @@ func (p Params) GetNested(indices ...string) any {
// SetParams overwrites values in dst with values in src for common or new keys.
// This is done recursively.
func SetParams(dst, src Params) {
setParams(dst, src, 0)
}
func setParams(dst, src Params, depth int) {
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
for k, v := range src {
vv, found := dst[k]
if !found {
@@ -57,7 +48,7 @@ func setParams(dst, src Params, depth int) {
switch vvv := vv.(type) {
case Params:
if pv, ok := v.(Params); ok {
setParams(vvv, pv, depth+1)
SetParams(vvv, pv)
} else {
dst[k] = v
}
@@ -110,6 +101,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
for k, v := range pp {
if k == MergeStrategyKey {
continue
}
@@ -187,56 +179,6 @@ func getNested(m map[string]any, indices []string) (any, string, map[string]any)
}
}
// CreateNestedParamsFromSegements creates empty nested maps for the given keySegments in the target map.
func CreateNestedParamsFromSegements(target Params, keySegments ...string) Params {
if len(keySegments) == 0 {
return target
}
m := target
for i, key := range keySegments {
v, found := m[key]
if !found {
nm := Params{}
m[key] = nm
m = nm
if i == len(keySegments)-1 {
return nm
}
continue
}
m = v.(Params)
}
return m
}
// CreateNestedParamsSepString creates empty nested maps for the given keyStr in the target map
// It returns the last map created.
func CreateNestedParamsSepString(keyStr, separator string, target Params) Params {
keySegments := strings.Split(keyStr, separator)
return CreateNestedParamsFromSegements(target, keySegments...)
}
// SetNestedParamIfNotSet sets the value for the given keyStr in the target map if it does not exist.
// It assumes that all but the last key in keyStr is a Params map or should be one.
func SetNestedParamIfNotSet(keyStr, separator string, value any, target Params) Params {
keySegments := strings.Split(keyStr, separator)
if len(keySegments) == 0 {
return target
}
base := keySegments[:len(keySegments)-1]
last := keySegments[len(keySegments)-1]
m := CreateNestedParamsFromSegements(target, base...)
if _, ok := m[last]; !ok {
m[last] = value
}
return target
}
// GetNestedParam gets the first match of the keyStr in the candidates given.
// It will first try the exact match and then try to find it as a nested map value,
// using the given separator, e.g. "mymap.name".
@@ -324,17 +266,9 @@ func CleanConfigStringMapString(m map[string]string) map[string]string {
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any.
func CleanConfigStringMap(m map[string]any) map[string]any {
return doCleanConfigStringMap(m, 0)
}
func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
if len(m) == 0 {
return m
}
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
if _, found := m[MergeStrategyKey]; !found {
return m
}
@@ -346,9 +280,9 @@ func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
}
switch v2 := v.(type) {
case map[string]any:
m2[k] = doCleanConfigStringMap(v2, depth+1)
m2[k] = CleanConfigStringMap(v2)
case Params:
var p Params = doCleanConfigStringMap(v2, depth+1)
var p Params = CleanConfigStringMap(v2)
m2[k] = p
case map[string]string:
m2[k] = CleanConfigStringMapString(v2)
@@ -410,29 +344,6 @@ func PrepareParams(m Params) {
}
}
// CloneParamsDeep does a deep clone of the given Params,
// meaning that any nested Params will be cloned as well.
func CloneParamsDeep(m Params) Params {
return cloneParamsDeep(m, 0)
}
func cloneParamsDeep(m Params, depth int) Params {
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
m2 := make(Params)
for k, v := range m {
switch vv := v.(type) {
case Params:
m2[k] = cloneParamsDeep(vv, depth+1)
default:
m2[k] = v
}
}
return m2
}
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
func PrepareParamsClone(m Params) Params {
m2 := make(Params)
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// Copyright 2019 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 hmaps
package maps
import (
"testing"
@@ -167,35 +167,3 @@ func TestParamsIsZero(t *testing.T) {
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue)
}
func TestSetNestedParamIfNotSet(t *testing.T) {
c := qt.New(t)
m := Params{}
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
c.Assert(m, qt.DeepEquals, Params{
"a": Params{
"b": Params{
"c": "value",
},
},
})
m = Params{
"a": Params{
"b": Params{
"c": "existingValue",
},
},
}
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
c.Assert(m, qt.DeepEquals, Params{
"a": Params{
"b": Params{
"c": "existingValue",
},
},
})
}
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2019 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 hstore
package maps
import (
"reflect"
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 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 hstore
package maps
import (
"reflect"
@@ -214,8 +214,8 @@ func TestScratchGetSortedMapValues(t *testing.T) {
func BenchmarkScratchGet(b *testing.B) {
scratch := NewScratch()
scratch.Add("A", 1)
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
scratch.Get("A")
}
}
+1
View File
@@ -52,6 +52,7 @@ func TestPara(t *testing.T) {
var result []int
var mu sync.Mutex
for i := range n {
i := i
r.Run(func() error {
mu.Lock()
defer mu.Unlock()
+2 -2
View File
@@ -115,8 +115,8 @@ func ReplaceExtension(path string, newExt string) string {
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories {
if after, ok := strings.CutPrefix(inPath, currentPath); ok {
return after, nil
if strings.HasPrefix(inPath, currentPath) {
return strings.TrimPrefix(inPath, currentPath), nil
}
}
return inPath, errors.New("can't extract relative path, unknown prefix")
+2 -60
View File
@@ -225,7 +225,7 @@ func BenchmarkSanitize(b *testing.B) {
// This should not allocate any memory.
b.Run("All allowed", func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
got := Sanitize(allAlowedPath)
if got != allAlowedPath {
b.Fatal(got)
@@ -235,7 +235,7 @@ func BenchmarkSanitize(b *testing.B) {
// This will allocate some memory.
b.Run("Spaces", func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
got := Sanitize(spacePath)
if got != "foo-bar" {
b.Fatal(got)
@@ -311,61 +311,3 @@ func TestIsSameFilePath(t *testing.T) {
c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}
func BenchmarkAddLeadingSlash(b *testing.B) {
const (
noLeadingSlash = "a/b/c"
withLeadingSlash = "/a/b/c"
)
// This should not allocate any memory.
b.Run("With leading slash", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash(withLeadingSlash)
if got != withLeadingSlash {
b.Fatal(got)
}
}
})
// This will allocate some memory.
b.Run("Without leading slash", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash(noLeadingSlash)
if got != "/a/b/c" {
b.Fatal(got)
}
}
})
b.Run("Blank string", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash("")
if got != "/" {
b.Fatal(got)
}
}
})
}
func TestPathEscape(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
input string
expected string
}{
{"/tags/欢迎", "/tags/%E6%AC%A2%E8%BF%8E"},
{"/path with spaces", "/path%20with%20spaces"},
{"/simple-path", "/simple-path"},
{"/path/with/slash", "/path/with/slash"},
{"/path/with special&chars", "/path/with%20special&chars"},
} {
in := this.input
for range 2 {
result := PathEscape(in)
c.Assert(result, qt.Equals, this.expected, qt.Commentf("input: %q", this.input))
in = result // test idempotency
}
}
}
+7 -94
View File
@@ -14,35 +14,23 @@
package paths
import (
"fmt"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
identifierCustomWrapper = "_"
identifierBaseof = "baseof"
)
// 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)
}
// PathParser parses and manages paths.
// PathParser parses a path into a Path.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
LanguageIndex map[string]int
@@ -56,19 +44,6 @@ type PathParser struct {
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
// The configured sites matrix.
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
pp.initOnce.Do(func() {
pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]()
})
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -82,32 +57,6 @@ func NormalizePathStringBasic(s string) string {
return s
}
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
pp.init()
lang := p.Lang()
v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
if lang != "" {
if idx, ok := pp.LanguageIndex[lang]; ok {
builder.WithLanguageIndices(idx)
}
}
switch p.Component() {
case files.ComponentFolderContent:
builder.WithDefaultsIfNotSet()
case files.ComponentFolderLayouts:
builder.WithAllIfNotSet()
case files.ComponentFolderStatic:
builder.WithDefaultsAndAllLanguagesIfNotSet()
}
return builder.Build(), nil
})
return v
}
// ParseIdentity parses component c with path s into a StringIdentity.
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
p := pp.parsePooled(c, s)
@@ -196,12 +145,6 @@ 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 isCustomWrapperIdentifier(sid) {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierCustom = len(p.identifiersKnown) - 1
found = true
}
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
@@ -212,12 +155,12 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
p.posIdentifierOutputFormat = 0
}
} else {
var langFound bool
if mayHaveLang {
var disabled bool
_, langFound = pp.LanguageIndex[sid]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
if disabled {
@@ -230,7 +173,6 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
@@ -260,14 +202,8 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
}
if !found && mayHaveLayout {
if p.posIdentifierLayout != -1 {
// Move it to identifiersUnknown.
p.identifiersUnknown = append(p.identifiersUnknown, p.identifiersKnown[p.posIdentifierLayout])
p.identifiersKnown[p.posIdentifierLayout] = id
} else {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
}
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
found = true
}
@@ -397,7 +333,7 @@ type Type int
const (
// A generic file, e.g. a JSON file.
// A generic resource, e.g. a JSON file.
TypeFile Type = iota
// All below are content files.
@@ -444,7 +380,6 @@ type Path struct {
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
posIdentifierCustom int
disabled bool
trimLeadingSlash bool
@@ -482,7 +417,6 @@ func (p *Path) reset() {
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.posIdentifierCustom = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@@ -646,9 +580,6 @@ func (p *Path) Unnormalized() *Path {
// PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string {
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
return p.Path()
}
return p.base(true, false)
}
@@ -702,12 +633,7 @@ func (p *Path) BaseRel(owner *Path) string {
//
// For other files (Resources), any extension is kept.
func (p *Path) Base() string {
s := p.base(!p.isContentPage(), p.IsBundle())
if s == "/" && p.isContentPage() {
// The content home page is represented as "".
s = ""
}
return s
return p.base(!p.isContentPage(), p.IsBundle())
}
// Used in template lookups.
@@ -782,10 +708,6 @@ func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCustomWrapper), identifierCustomWrapper)
}
func (p *Path) Identifier(i int) string {
return p.identifierAsString(i)
}
@@ -864,12 +786,3 @@ func HasExt(p string) bool {
}
return false
}
// ValidateIdentifier returns true if the given string is a valid identifier according
// to Hugo's basic path normalization rules.
func ValidateIdentifier(s string) error {
if s == NormalizePathStringBasic(s) {
return nil
}
return fmt.Errorf("must be all lower case and no spaces")
}
+30 -81
View File
@@ -18,33 +18,27 @@ import (
"testing"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
func newTestParser() *PathParser {
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
return &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
"fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
ConfiguredDimensions: dims,
}
var testParser = &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
"fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
}
func TestParse(t *testing.T) {
@@ -196,7 +190,7 @@ func TestParse(t *testing.T) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "")
@@ -272,7 +266,7 @@ func TestParse(t *testing.T) {
"Index root no slash",
"_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md")
},
@@ -281,7 +275,7 @@ func TestParse(t *testing.T) {
"Index root",
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md")
},
@@ -379,24 +373,13 @@ func TestParse(t *testing.T) {
c.Assert(p.IsContentData(), qt.IsFalse)
},
},
{
"Custom identifier",
"/a/b/p1._myid_.no.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Custom(), qt.Equals, "myid")
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Caret up identifier" {
// return
if test.name != "Home branch cundle" {
// return
}
test.assert(c, parser.Parse(files.ComponentFolderContent, test.path))
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
})
}
}
@@ -454,8 +437,8 @@ func TestParseLayouts(t *testing.T) {
"/mylayout.list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "mylayout")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/mylayout.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
@@ -478,8 +461,7 @@ func TestParseLayouts(t *testing.T) {
"Lang and output format",
"/list.no.amp.not.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list", "amp", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"not"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Lang(), qt.Equals, "no")
@@ -601,27 +583,14 @@ func TestParseLayouts(t *testing.T) {
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
},
},
{
"Not lang",
"/foo/index.xy.html",
func(c *qt.C, p *Path) {
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Layout(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.xy.html")
c.Assert(p.PathNoLang(), qt.Equals, "/foo/index.xy.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "index"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Not lang" {
return
if test.name != "Shortcode lang layout" {
// return
}
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
@@ -636,27 +605,7 @@ func TestHasExt(t *testing.T) {
}
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})
}
func BenchmarkSitesMatrixFromPath(b *testing.B) {
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
for b.Loop() {
parser.SitesMatrixFromPath(p)
for i := 0; i < b.N; i++ {
testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
}
}
+4 -4
View File
@@ -35,9 +35,9 @@ weight = 2
removePathAccents = true
-- content/διακριτικός.md --
-- content/διακριτικός.fr.md --
-- layouts/single.html --
-- layouts/_default/single.html --
{{ .Language.Lang }}|Single.
-- layouts/list.html --
-- layouts/_default/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/single.html --
-- layouts/_default/single.html --
{{ .Language.Lang }}|Single.
-- layouts/list.html --
-- layouts/_default/list.html --
{{ .Language.Lang }}|List.
`
b := hugolib.Test(t, files)
+25 -248
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -13,128 +13,52 @@
package predicate
import (
"iter"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/hugofs/hglob"
)
// Match represents the result of a predicate evaluation.
type Match interface {
OK() bool
}
var (
// Predefined Match values for common cases.
True = BoolMatch(true)
False = BoolMatch(false)
)
// BoolMatch is a simple Match implementation based on a boolean value.
type BoolMatch bool
func (b BoolMatch) OK() bool {
return bool(b)
}
// breakMatch is a Match implementation that always returns false for OK() and signals to break evaluation.
type breakMatch struct{}
func (b breakMatch) OK() bool {
return false
}
var matchBreak = breakMatch{}
// P is a predicate function that tests whether a value of type T satisfies some condition.
type P[T any] func(T) bool
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
// Note that P[T] only supports Or. For chained AND/OR logic, use PR[T].
func (p P[T]) Or(ps ...P[T]) P[T] {
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
func (p P[T]) And(ps ...P[T]) P[T] {
return func(v T) bool {
if p != nil && p(v) {
for _, pp := range ps {
if !pp(v) {
return false
}
}
if p == nil {
return true
}
return p(v)
}
}
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
func (p P[T]) Or(ps ...P[T]) P[T] {
return func(v T) bool {
for _, pp := range ps {
if pp(v) {
return true
}
}
return false
}
}
// PR is a predicate function that tests whether a value of type T satisfies some condition and returns a Match result.
type PR[T any] func(T) Match
// BoolFunc returns a P[T] version of this predicate.
func (p PR[T]) BoolFunc() P[T] {
return func(v T) bool {
if p == nil {
return false
}
return p(v).OK()
return p(v)
}
}
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
func (p PR[T]) And(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(true)
// Negate returns a predicate that is a logical negation of this predicate.
func (p P[T]) Negate() P[T] {
return func(v T) bool {
return !p(v)
}
}
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
func (p PR[T]) Or(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(false)
}
}
func shouldBreak(m Match) bool {
_, ok := m.(breakMatch)
return ok
}
// Filter returns a new slice holding only the elements of s that satisfy p.
// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length.
func (p PR[T]) Filter(s []T) []T {
func (p P[T]) Filter(s []T) []T {
var n int
for _, v := range s {
if p(v).OK() {
if p(v) {
s[n] = v
n++
}
@@ -143,159 +67,12 @@ func (p PR[T]) Filter(s []T) []T {
}
// FilterCopy returns a new slice holding only the elements of s that satisfy p.
func (p PR[T]) FilterCopy(s []T) []T {
func (p P[T]) FilterCopy(s []T) []T {
var result []T
for _, v := range s {
if p(v).OK() {
if p(v) {
result = append(result, v)
}
}
return result
}
const (
rangeOpNone = iota
rangeOpLT
rangeOpLTE
rangeOpGT
rangeOpGTE
rangeOpEQ
rangeOpNE
)
func cutRangeOp(s string) (op int, rest string) {
switch {
case strings.HasPrefix(s, ">= "):
return rangeOpGTE, s[3:]
case strings.HasPrefix(s, "<= "):
return rangeOpLTE, s[3:]
case strings.HasPrefix(s, "== "):
return rangeOpEQ, s[3:]
case strings.HasPrefix(s, "!= "):
return rangeOpNE, s[3:]
case strings.HasPrefix(s, "> "):
return rangeOpGT, s[2:]
case strings.HasPrefix(s, "< "):
return rangeOpLT, s[2:]
default:
return rangeOpNone, s
}
}
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
var p PR[string]
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
if negate {
pattern = pattern[2:]
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.And(func(s string) Match {
return BoolMatch(!g.Match(s))
})
} else {
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.Or(func(s string) Match {
return BoolMatch(g.Match(s))
})
}
}
return p.BoolFunc(), nil
}
// NewIndexStringPredicateFromGlobsAndRanges creates an IndexString predicate from the given glob patterns and range patterns.
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
// A range pattern is one of "> value", ">= value", "< value" or "<= value".
func NewIndexStringPredicateFromGlobsAndRanges(patterns []string, getIndex func(s string) int, getGlob func(pattern string) (glob.Glob, error)) (P[IndexString], error) {
var p PR[IndexString]
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
if negate {
pattern = pattern[2:]
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.And(func(s IndexString) Match {
return BoolMatch(!g.Match(s.String))
})
} else {
// This can be either a glob or a value prefixed with one of >, >=, < or <=.
o, v := cutRangeOp(pattern)
if o != rangeOpNone {
i := getIndex(v)
if i == -1 {
// No match possible.
p = p.And(func(s IndexString) Match {
return BoolMatch(false)
})
continue
}
switch o {
// The greater values starts at the top with index 0.
case rangeOpGT:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index < i)
})
case rangeOpGTE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index <= i)
})
case rangeOpLT:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index > i)
})
case rangeOpLTE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index >= i)
})
case rangeOpEQ:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index == i)
})
case rangeOpNE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index != i)
})
}
} else {
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.Or(func(s IndexString) Match {
return BoolMatch(g.Match(s.String))
})
}
}
}
return p.BoolFunc(), nil
}
type IndexString struct {
Index int
String string
}
type IndexMatcher interface {
IndexMatch(match P[IndexString]) (iter.Seq[int], error)
}
+28 -227
View File
@@ -17,66 +17,38 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/predicate"
)
func TestPredicate(t *testing.T) {
func TestAdd(t *testing.T) {
c := qt.New(t)
n := func() predicate.PR[int] {
var pr predicate.PR[int]
return pr
}
var p predicate.P[int] = intP1
var pr predicate.PR[int]
p := pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
pr = n().Or(intP1).Or(intP2)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsTrue) // true || false
c.Assert(p(2), qt.IsTrue) // false || true
c.Assert(p(3), qt.IsFalse) // false || false
pr = pr.And(intP3)
p = pr.BoolFunc()
c.Assert(p(2), qt.IsFalse) // true || true && false
c.Assert(pr(10), qt.IsTrue) // true || true && true
pr = pr.And(intP4)
p = pr.BoolFunc()
c.Assert(p(10), qt.IsTrue) // true || true && true && true
c.Assert(p(2), qt.IsFalse) // true || true && false && false
c.Assert(p(1), qt.IsFalse) // true || false && false && false
c.Assert(p(3), qt.IsFalse) // false || false && false && false
c.Assert(p(4), qt.IsFalse) // false || false && false && false
c.Assert(p(42), qt.IsFalse) // false || false && false && false
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
c.Assert(p(10), qt.IsTrue)
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
c.Assert(p(10), qt.IsTrue)
pr = n().Or(intP1).Or(intP2).Or(intP3)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsTrue)
c.Assert(p(10), qt.IsTrue)
c.Assert(p(4), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
neg := p.Negate()
c.Assert(neg(1), qt.IsFalse)
c.Assert(neg(2), qt.IsTrue)
and := p.And(intP2)
c.Assert(and(1), qt.IsFalse)
c.Assert(and(2), qt.IsFalse)
c.Assert(and(10), qt.IsTrue)
or := p.Or(intP2)
c.Assert(or(1), qt.IsTrue)
c.Assert(or(2), qt.IsTrue)
c.Assert(or(10), qt.IsTrue)
c.Assert(or(11), qt.IsFalse)
}
func TestFilter(t *testing.T) {
c := qt.New(t)
var p predicate.PR[int]
p = p.Or(intP1).Or(intP2)
var p predicate.P[int] = intP1
p = p.Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -87,8 +59,8 @@ func TestFilter(t *testing.T) {
func TestFilterCopy(t *testing.T) {
c := qt.New(t)
var p predicate.PR[int]
p = p.Or(intP1).Or(intP2)
var p predicate.P[int] = intP1
p = p.Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -96,187 +68,16 @@ func TestFilterCopy(t *testing.T) {
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2})
}
var intP1 = func(i int) predicate.Match {
var intP1 = func(i int) bool {
if i == 10 {
return predicate.True
return true
}
return predicate.BoolMatch(i == 1)
return i == 1
}
var intP2 = func(i int) predicate.Match {
var intP2 = func(i int) bool {
if i == 10 {
return predicate.True
return true
}
return predicate.BoolMatch(i == 2)
}
var intP3 = func(i int) predicate.Match {
if i == 10 {
return predicate.True
}
return predicate.BoolMatch(i == 3)
}
var intP4 = func(i int) predicate.Match {
if i == 10 {
return predicate.True
}
return predicate.BoolMatch(i == 4)
}
func TestNewStringPredicateFromGlobs(t *testing.T) {
c := qt.New(t)
getGlob := func(pattern string) (glob.Glob, error) {
return glob.Compile(pattern)
}
n := func(patterns ...string) predicate.P[string] {
p, err := predicate.NewStringPredicateFromGlobs(patterns, getGlob)
c.Assert(err, qt.IsNil)
return p
}
m := n("a", "! ab*", "abc")
c.Assert(m("a"), qt.IsTrue)
c.Assert(m("ab"), qt.IsFalse)
c.Assert(m("abc"), qt.IsFalse)
m = n()
c.Assert(m("anything"), qt.IsFalse)
}
func TestNewIndexStringPredicateFromGlobsAndRanges(t *testing.T) {
c := qt.New(t)
// Simulate versions: v4.0.0=0, v3.0.0=1, v2.0.0=2, v1.0.0=3
// Lower index = greater value.
versions := []string{"v4.0.0", "v3.0.0", "v2.0.0", "v1.0.0"}
getIndex := func(s string) int {
for i, v := range versions {
if v == s {
return i
}
}
return -1
}
getGlob := func(pattern string) (glob.Glob, error) {
return glob.Compile(pattern)
}
n := func(patterns ...string) predicate.P[predicate.IndexString] {
p, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(patterns, getIndex, getGlob)
c.Assert(err, qt.IsNil)
return p
}
is := func(i int) predicate.IndexString {
return predicate.IndexString{Index: i, String: versions[i]}
}
// Test >= v2.0.0 (index 2): should match indices <= 2
m := n(">= v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test > v2.0.0 (index 2): should match indices < 2
m = n("> v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test < v3.0.0 (index 1): should match indices > 1
m = n("< v3.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
// Test range: >= v2.0.0 AND <= v3.0.0
m = n(">= v2.0.0", "<= v3.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0 - too high
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - too low
// Test glob pattern
m = n("v2.*.*")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
// Test glob with negation
m = n("v*.*.*", "! v3.*.*")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
// Test range with negation: >= v2.0.0 but not v3.0.0
m = n(">= v2.0.0", "! v3.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
// Test unknown value in range returns no match
m = n(">= v99.0.0")
c.Assert(m(is(0)), qt.IsFalse)
c.Assert(m(is(3)), qt.IsFalse)
// Test == v2.0.0: should only match v2.0.0
m = n("== v2.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test != v2.0.0: should match everything except v2.0.0
m = n("!= v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
// Test != with range: >= v2.0.0 AND != v3.0.0
m = n(">= v2.0.0", "!= v3.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - excluded by !=
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
}
func BenchmarkPredicate(b *testing.B) {
b.Run("and or no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2).Or(intP3)
for b.Loop() {
_ = p(3).OK()
}
})
b.Run("and and no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2)
for b.Loop() {
_ = p(3).OK()
}
})
b.Run("and and match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2)
for b.Loop() {
_ = p(10).OK()
}
})
b.Run("or or match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.Or(intP2).Or(intP3)
for b.Loop() {
_ = p(2).OK()
}
})
return i == 2
}
-52
View File
@@ -1,52 +0,0 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package predicate
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCutRangeOp(t *testing.T) {
c := qt.New(t)
op, rest := cutRangeOp(">= value")
c.Assert(op, qt.Equals, rangeOpGTE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("<= value")
c.Assert(op, qt.Equals, rangeOpLTE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("> value")
c.Assert(op, qt.Equals, rangeOpGT)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("< value")
c.Assert(op, qt.Equals, rangeOpLT)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("== value")
c.Assert(op, qt.Equals, rangeOpEQ)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("!= value")
c.Assert(op, qt.Equals, rangeOpNE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("value")
c.Assert(op, qt.Equals, rangeOpNone)
c.Assert(rest, qt.Equals, "value")
}
-25
View File
@@ -16,7 +16,6 @@ package terminal
import (
"fmt"
"io"
"os"
"strings"
@@ -73,27 +72,3 @@ func doublePercent(str string) string {
func singlePercent(str string) string {
return strings.Replace(str, "%%", "%", -1)
}
type ProgressState int
const (
ProgressHidden ProgressState = iota
ProgressNormal
ProgressError
ProgressIntermediate
ProgressWarning
)
// ReportProgress writes OSC 9;4 sequence to w.
func ReportProgress(w io.Writer, state ProgressState, progress float64) {
if progress < 0 {
progress = 0.0
}
if progress > 1 {
progress = 1.0
}
pi := int(progress * 100)
fmt.Fprintf(w, "\033]9;4;%d;%d\007", state, pi)
}
+1 -1
View File
@@ -65,7 +65,7 @@ func BenchmarkVisitLinesAfter(b *testing.B) {
line 3`
for b.Loop() {
for i := 0; i < b.N; i++ {
VisitLinesAfter(lines, func(s string) {
})
}
+4 -2
View File
@@ -56,7 +56,9 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
queue := NewEvictingQueue[string](3)
for range 100 {
wg.Go(func() {
wg.Add(1)
go func() {
defer wg.Done()
queue.Add(val)
v := queue.Peek()
if v != val {
@@ -66,7 +68,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
if len(vals) != 1 || vals[0] != val {
t.Error("wrong val")
}
})
}()
}
wg.Wait()
}
+1 -19
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.Pointer, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return value.IsNil()
}
@@ -139,25 +139,7 @@ func NewBool(b bool) *bool {
return &b
}
// WeightProvider provides a weight.
type WeightProvider interface {
Weight() int
}
// Weight0Provider provides a weight that's considered before the WeightProvider in sorting.
// This allows the weight set on a given term to win.
type Weight0Provider interface {
Weight0() int
}
// PrintableValueProvider is implemented by types that can provide a printable value.
type PrintableValueProvider interface {
PrintableValue() any
}
type (
Strings2 [2]string
Strings3 [3]string
Ints2 [2]int
Ints3 [3]int
)
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
-1
View File
@@ -63,7 +63,6 @@ func TestLexicographicSort(t *testing.T) {
c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"})
}
// // Note that this cannot use b.Loop() because of golang/go#27217.
func BenchmarkStringSort(b *testing.B) {
prototype := []string{"b", "Bz", "zz", "ba", "αβδ αβδ αβδ", "A", "Ba", "ba", "nnnnasdfnnn", "AAgæåz", "αβδC"}
b.Run("LessStrings", func(b *testing.B) {

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