Compare commits

..

1 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen 75f383065f Fix server refresh on 404 template changes
Fixes #13209
2025-01-01 17:24:30 +01:00
2425 changed files with 59392 additions and 65473 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults defaults: &defaults
resource_class: large resource_class: large
docker: docker:
- image: bepsays/ci-hugoreleaser:1.22500.20400 - image: bepsays/ci-hugoreleaser:1.22301.20401
environment: &buildenv environment: &buildenv
GOMODCACHE: /root/project/gomodcache GOMODCACHE: /root/project/gomodcache
version: 2 version: 2
@@ -58,7 +58,7 @@ jobs:
environment: environment:
<<: [*buildenv] <<: [*buildenv]
docker: docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20400 - image: bepsays/ci-hugoreleaser-linux-arm64:1.22301.20401
steps: steps:
- *restore-cache - *restore-cache
- &attach-workspace - &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: []
-10
View File
@@ -1,10 +0,0 @@
🛑 Before submitting this PR
All PRs must be linked to an Issue in this repository, and that Issue must be approved by a project maintainer:
- For a bug, approval means that the "needs triage" label is removed.
- For a proposal, approval means that the "proposal" label is replaced by an "enhancement" label.
PRs without a linked, approved Issue will be closed immediately.
Please review the contribution guide (CONTRIBUTING.md) to understand commit message guidelines and AI assistance disclosures.
-22
View File
@@ -1,22 +0,0 @@
name: AI Detection
on:
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
pull_request_target:
types: [opened, ready_for_review]
permissions:
contents: read
pull-requests: write
jobs:
detect-ai:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: AI Watchdog
uses: bep/ai-watchdog@995475dd81767f8035dc2c943e9230918d06caca # temporary fork (probably)
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
pr-label: AI Suspect
fail-when-confident: false
# CSV list.
pr-user-logins-skip: dependabot[bot]
+5 -6
View File
@@ -16,20 +16,20 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
with: with:
images: ${{ env.REGISTRY_IMAGE }} images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx - 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 - name: Login to GHCR
# Login is only needed when the image is pushed # 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: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -37,7 +37,7 @@ jobs:
- name: Build and push - name: Build and push
id: build id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
with: with:
context: . context: .
provenance: mode=max provenance: mode=max
@@ -46,4 +46,3 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: HUGO_BUILD_TAGS=extended,withdeploy
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
This pull request has been automatically locked since there This pull request has been automatically locked since there
has not been any recent activity after it was closed. has not been any recent activity after it was closed.
Please open a new issue for related bugs. Please open a new issue for related bugs.
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with: with:
operations-per-run: 999 operations-per-run: 999
days-before-issue-stale: 365 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 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. 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. 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' stale-issue-label: 'Stale'
+12 -13
View File
@@ -16,7 +16,7 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.24.x, 1.25.x] go-version: [1.22.x, 1.23.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues. os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@@ -24,7 +24,9 @@ jobs:
name: Free Disk Space (Ubuntu) name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with: 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 android: true
dotnet: true dotnet: true
haskell: true haskell: true
@@ -32,9 +34,9 @@ jobs:
docker-images: true docker-images: true
swap-storage: true swap-storage: true
- name: Checkout code - name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Install Go - name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
check-latest: true check-latest: true
@@ -43,21 +45,18 @@ jobs:
**/go.sum **/go.sum
**/go.mod **/go.mod
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0 uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with: with:
ruby-version: "3.4.5" ruby-version: "2.7"
- name: Install Ruby gems bundler-cache: true #
run: |
gem install asciidoctor -v "2.0.26"
gem install asciidoctor-diagram -v "3.1.0"
- name: Install GoAT
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
- name: Install Python - name: Install Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install Mage - name: Install Mage
run: go install github.com/magefile/mage@v1.15.0 run: go install github.com/magefile/mage@v1.15.0
- name: Install asciidoctor
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
- name: Install docutils - name: Install docutils
run: | run: |
pip install docutils pip install docutils
-1
View File
@@ -3,4 +3,3 @@
imports.* imports.*
dist/ dist/
public/ public/
.DS_Store
+2 -14
View File
@@ -1,4 +1,4 @@
>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future. >**Note:** We would apprecitate if you hold on with any big refactorings (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
# Contributing to Hugo # Contributing to Hugo
@@ -19,7 +19,6 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
* [Reporting Issues](#reporting-issues) * [Reporting Issues](#reporting-issues)
* [Submitting Patches](#submitting-patches) * [Submitting Patches](#submitting-patches)
* [Code Contribution Guidelines](#code-contribution-guidelines) * [Code Contribution Guidelines](#code-contribution-guidelines)
* [AI Assistance Notice](#ai-assistance-notice)
* [Git Commit Message Guidelines](#git-commit-message-guidelines) * [Git Commit Message Guidelines](#git-commit-message-guidelines)
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github) * [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
* [Building Hugo with Your Changes](#building-hugo-with-your-changes) * [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. 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. We have one exception to this rule which is LibSASS.
**Bug fixes are, of course, always welcome.** **Bug fixes are, of course, always welcome.**
@@ -80,16 +79,6 @@ To make the contribution process as seamless as possible, we ask for the followi
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails. * Ensure that `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. * Follow the **Git Commit Message Guidelines** below.
## AI Assistance Notice
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
An example disclosure:
> This PR was written primarily by Claude Code.
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it.
### Git Commit Message Guidelines ### Git Commit Message Guidelines
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages, This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
@@ -104,7 +93,6 @@ Most title/subjects should have a lower-cased prefix with a colon and one whites
* If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`) * If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`)
* If this is a documentation update, prefix with `docs:`. * If this is a documentation update, prefix with `docs:`.
* If nothing of the above applies, just leave the prefix out. * If nothing of the above applies, just leave the prefix out.
* Note that the above excludes nouns seen in other repositories, e.g. "chore:".
Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*. Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*.
Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*. Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*.
+4 -4
View File
@@ -2,8 +2,8 @@
# Twitter: https://twitter.com/gohugoio # Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/ # Website: https://gohugo.io/
ARG GO_VERSION="1.25" ARG GO_VERSION="1.23.2"
ARG ALPINE_VERSION="3.22" ARG ALPINE_VERSION="3.20"
ARG DART_SASS_VERSION="1.79.3" ARG DART_SASS_VERSION="1.79.3"
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
@@ -19,7 +19,7 @@ RUN apk add clang lld
COPY --from=xx / / COPY --from=xx / /
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN xx-apk add musl-dev gcc g++ RUN xx-apk add musl-dev gcc g++
# Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so: # Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so:
# docker build --build-arg HUGO_BUILD_TAGS=withdeploy . # docker build --build-arg HUGO_BUILD_TAGS=withdeploy .
@@ -72,7 +72,7 @@ RUN mkdir -p /var/hugo/bin /cache && \
adduser -Sg hugo -u 1000 -h /var/hugo hugo && \ adduser -Sg hugo -u 1000 -h /var/hugo hugo && \
chown -R hugo: /var/hugo /cache && \ chown -R hugo: /var/hugo /cache && \
# For the Hugo's Git integration to work. # For the Hugo's Git integration to work.
runuser -u hugo -- git config --global --add safe.directory /project && \ runuser -u hugo -- git config --global --add safe.directory /project && \
# See https://github.com/gohugoio/hugo/issues/9810 # See https://github.com/gohugoio/hugo/issues/9810
runuser -u hugo -- git config --global core.quotepath false runuser -u hugo -- git config --global core.quotepath false
+41 -76
View File
@@ -34,7 +34,6 @@ A fast and flexible static site generator built with love by [bep], [spf13], and
[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo) [![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo)
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a> [Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
## Overview ## Overview
Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less. Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
@@ -65,30 +64,11 @@ See the [features] section of the documentation for a comprehensive summary of H
<p>&nbsp;</p> <p>&nbsp;</p>
<p float="left"> <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> <a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
&nbsp;&nbsp;&nbsp; &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> <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/gohugoioTheme/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>
</p> </p>
## Editions
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
Feature|extended edition|extended/deploy edition
:--|:-:|:-:
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details].|:x:|:heavy_check_mark:
[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 ## Installation
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system: Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
@@ -100,11 +80,15 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
## Build from source ## Build from source
Hugo is available in two editions: standard and extended. With the extended edition you can:
- Encode to the WebP format when processing images. You can decode WebP images with either edition.
- Transpile Sass to CSS using the embedded LibSass transpiler. The extended edition is not required to use the Dart Sass transpiler.
Prerequisites to build Hugo from source: Prerequisites to build Hugo from source:
- Standard edition: Go 1.24.0 or later - Standard edition: Go 1.20 or later
- Extended edition: Go 1.24.0 or later, and GCC - Extended edition: Go 1.20 or later, and GCC
- Extended/deploy edition: Go 1.24.0 or later, and GCC
Build the standard edition: Build the standard edition:
@@ -117,13 +101,6 @@ Build the extended edition:
```text ```text
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
``` ```
Build the extended/deploy edition:
```text
CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest
```
## Star History ## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=gohugoio/hugo&type=Timeline)](https://star-history.com/#gohugoio/hugo&Timeline) [![Star History Chart](https://api.star-history.com/svg?repos=gohugoio/hugo&type=Timeline)](https://star-history.com/#gohugoio/hugo&Timeline)
@@ -171,112 +148,100 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
```text ```text
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69" github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
github.com/PuerkitoBio/goquery="v1.10.1" github.com/alecthomas/chroma/v2="v2.14.0"
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/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
github.com/bep/clocks="v0.5.0" github.com/bep/clocks="v0.5.0"
github.com/bep/debounce="v1.2.0" github.com/bep/debounce="v1.2.0"
github.com/bep/gitmap="v1.6.0" github.com/bep/gitmap="v1.6.0"
github.com/bep/goat="v0.5.0" github.com/bep/goat="v0.5.0"
github.com/bep/godartsass/v2="v2.3.2" github.com/bep/godartsass/v2="v2.3.0"
github.com/bep/golibsass="v1.2.0" github.com/bep/golibsass="v1.2.0"
github.com/bep/gowebp="v0.3.0" github.com/bep/gowebp="v0.3.0"
github.com/bep/imagemeta="v0.8.4" github.com/bep/imagemeta="v0.8.3"
github.com/bep/lazycache="v0.7.0" github.com/bep/lazycache="v0.7.0"
github.com/bep/logg="v0.4.0" github.com/bep/logg="v0.4.0"
github.com/bep/mclib="v1.20400.20402" github.com/bep/mclib="v1.20400.20402"
github.com/bep/overlayfs="v0.9.2" github.com/bep/overlayfs="v0.9.2"
github.com/bep/simplecobra="v0.5.0" github.com/bep/simplecobra="v0.4.0"
github.com/bep/tmc="v0.5.1" github.com/bep/tmc="v0.5.1"
github.com/cespare/xxhash/v2="v2.3.0" github.com/cespare/xxhash/v2="v2.3.0"
github.com/clbanning/mxj/v2="v2.7.0" github.com/clbanning/mxj/v2="v2.7.0"
github.com/cli/safeexec="v1.0.1"
github.com/cpuguy83/go-md2man/v2="v2.0.4" github.com/cpuguy83/go-md2man/v2="v2.0.4"
github.com/disintegration/gift="v1.2.1" github.com/disintegration/gift="v1.2.1"
github.com/dlclark/regexp2="v1.11.5" github.com/dlclark/regexp2="v1.11.0"
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17" github.com/evanw/esbuild="v0.24.0"
github.com/evanw/esbuild="v0.24.2"
github.com/fatih/color="v1.18.0" github.com/fatih/color="v1.18.0"
github.com/frankban/quicktest="v1.14.6" github.com/frankban/quicktest="v1.14.6"
github.com/fsnotify/fsnotify="v1.8.0" github.com/fsnotify/fsnotify="v1.8.0"
github.com/getkin/kin-openapi="v0.129.0" github.com/getkin/kin-openapi="v0.123.0"
github.com/ghodss/yaml="v1.0.0" github.com/ghodss/yaml="v1.0.0"
github.com/go-openapi/jsonpointer="v0.21.0" github.com/go-openapi/jsonpointer="v0.20.2"
github.com/go-openapi/swag="v0.23.0" github.com/go-openapi/swag="v0.22.8"
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible"
github.com/gobuffalo/flect="v1.0.3" github.com/gobuffalo/flect="v1.0.3"
github.com/gobwas/glob="v0.2.3" github.com/gobwas/glob="v0.2.3"
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e" github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
github.com/gohugoio/hashstructure="v0.5.0" github.com/gohugoio/hashstructure="v0.1.0"
github.com/gohugoio/httpcache="v0.7.0" github.com/gohugoio/httpcache="v0.7.0"
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0" github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0" github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
github.com/gohugoio/locales="v0.14.0" github.com/gohugoio/locales="v0.14.0"
github.com/gohugoio/localescompressed="v1.0.1" github.com/gohugoio/localescompressed="v1.0.1"
github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
github.com/google/go-cmp="v0.6.0" github.com/google/go-cmp="v0.6.0"
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941"
github.com/gorilla/websocket="v1.5.3" github.com/gorilla/websocket="v1.5.3"
github.com/hairyhenderson/go-codeowners="v0.7.0" github.com/hairyhenderson/go-codeowners="v0.6.1"
github.com/hashicorp/golang-lru/v2="v2.0.7" github.com/hashicorp/golang-lru/v2="v2.0.7"
github.com/invopop/yaml="v0.2.0"
github.com/jdkato/prose="v1.2.1" github.com/jdkato/prose="v1.2.1"
github.com/josharian/intern="v1.0.0" github.com/josharian/intern="v1.0.0"
github.com/kr/pretty="v0.3.1" github.com/kr/pretty="v0.3.1"
github.com/kr/text="v0.2.0" github.com/kr/text="v0.2.0"
github.com/kyokomi/emoji/v2="v2.2.13" 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/mailru/easyjson="v0.7.7"
github.com/makeworld-the-better-one/dither/v2="v2.4.0" github.com/makeworld-the-better-one/dither/v2="v2.4.0"
github.com/marekm4/color-extractor="v1.2.1" github.com/marekm4/color-extractor="v1.2.1"
github.com/mattn/go-colorable="v0.1.13" github.com/mattn/go-colorable="v0.1.13"
github.com/mattn/go-isatty="v0.0.20" github.com/mattn/go-isatty="v0.0.20"
github.com/mattn/go-runewidth="v0.0.9" 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/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826" github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
github.com/muesli/smartcrop="v0.3.0" github.com/muesli/smartcrop="v0.3.0"
github.com/niklasfasching/go-org="v1.7.0" github.com/niklasfasching/go-org="v1.7.0"
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
github.com/olekukonko/tablewriter="v0.0.5" github.com/olekukonko/tablewriter="v0.0.5"
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58" github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
github.com/pelletier/go-toml/v2="v2.2.3" github.com/pelletier/go-toml/v2="v2.2.3"
github.com/perimeterx/marshmallow="v1.1.5" github.com/perimeterx/marshmallow="v1.1.5"
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c" github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
github.com/pkg/errors="v0.9.1" github.com/pkg/errors="v0.9.1"
github.com/rivo/uniseg="v0.4.7"
github.com/rogpeppe/go-internal="v1.13.1" github.com/rogpeppe/go-internal="v1.13.1"
github.com/russross/blackfriday/v2="v2.1.0" github.com/russross/blackfriday/v2="v2.1.0"
github.com/sass/libsass="3.6.6" github.com/sass/dart-sass/compiler="1.81.0"
github.com/sass/dart-sass/implementation="1.81.0"
github.com/sass/dart-sass/protocol="3.1.0"
github.com/spf13/afero="v1.11.0" github.com/spf13/afero="v1.11.0"
github.com/spf13/cast="v1.7.1" github.com/spf13/cast="v1.7.0"
github.com/spf13/cobra="v1.8.1" github.com/spf13/cobra="v1.8.1"
github.com/spf13/fsync="v0.10.1" github.com/spf13/fsync="v0.10.1"
github.com/spf13/pflag="v1.0.6" github.com/spf13/pflag="v1.0.5"
github.com/tdewolff/minify/v2="v2.20.37" github.com/tdewolff/minify/v2="v2.21.1"
github.com/tdewolff/parse/v2="v2.7.15" github.com/tdewolff/parse/v2="v2.7.18"
github.com/tetratelabs/wazero="v1.8.2" github.com/tetratelabs/wazero="v1.8.1"
github.com/webmproject/libwebp="v1.3.2"
github.com/yuin/goldmark-emoji="v1.0.4" github.com/yuin/goldmark-emoji="v1.0.4"
github.com/yuin/goldmark="v1.7.8" github.com/yuin/goldmark="v1.7.8"
go.uber.org/automaxprocs="v1.5.3" go.uber.org/automaxprocs="v1.5.3"
golang.org/x/crypto="v0.33.0" golang.org/x/crypto="v0.29.0"
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac" golang.org/x/exp="v0.0.0-20221031165847-c99f073a8326"
golang.org/x/image="v0.24.0" golang.org/x/image="v0.22.0"
golang.org/x/mod="v0.23.0" golang.org/x/mod="v0.22.0"
golang.org/x/net="v0.35.0" golang.org/x/net="v0.31.0"
golang.org/x/sync="v0.11.0" golang.org/x/sync="v0.9.0"
golang.org/x/sys="v0.30.0" golang.org/x/sys="v0.27.0"
golang.org/x/text="v0.22.0" golang.org/x/text="v0.20.0"
golang.org/x/tools="v0.30.0" golang.org/x/tools="v0.27.0"
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da" google.golang.org/protobuf="v1.35.1"
gonum.org/v1/plot="v0.15.0"
google.golang.org/protobuf="v1.36.5"
gopkg.in/yaml.v2="v2.4.0" gopkg.in/yaml.v2="v2.4.0"
gopkg.in/yaml.v3="v3.0.1" gopkg.in/yaml.v3="v3.0.1"
oss.terrastruct.com/d2="v0.6.9" howett.net/plist="v1.0.0"
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" software.sslmate.com/src/go-pkcs12="v0.2.0"
``` ```
</details> </details>
+6 -12
View File
@@ -176,12 +176,11 @@ func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManag
} }
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account. // ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
// predicate is optional and will clear any entry for which it returns true. func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{ g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions), NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error { Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearOnRebuild(predicate, changeset...) partition.clearOnRebuild(changeset...)
return nil return nil
}, },
}) })
@@ -340,7 +339,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
panic("invalid Weight, must be between 1 and 100") 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)) panic(fmt.Sprintf("invalid partition name %q", name))
} }
@@ -480,12 +479,7 @@ func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
}) })
} }
func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) { func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
if predicate == nil {
predicate = func(k, v any) bool {
return false
}
}
opts := p.getOptions() opts := p.getOptions()
if opts.ClearWhen == ClearNever { if opts.ClearWhen == ClearNever {
return return
@@ -531,7 +525,7 @@ func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changese
// Second pass needs to be done in a separate loop to catch any // Second pass needs to be done in a separate loop to catch any
// elements marked as stale in the other partitions. // elements marked as stale in the other partitions.
p.c.DeleteFunc(func(key K, v V) bool { p.c.DeleteFunc(func(key K, v V) bool {
if predicate(key, v) || shouldDelete(key, v) { if shouldDelete(key, v) {
p.trace.Log( p.trace.Log(
logg.StringFunc( logg.StringFunc(
func() string { func() string {
@@ -607,7 +601,7 @@ type PartitionManager interface {
adjustMaxSize(addend int) int adjustMaxSize(addend int) int
getMaxSize() int getMaxSize() int
getOptions() OptionsPartition getOptions() OptionsPartition
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) clearOnRebuild(changeset ...identity.Identity)
clearMatching(predicate func(k, v any) bool) clearMatching(predicate func(k, v any) bool)
clearStale() clearStale()
} }
+6 -6
View File
@@ -147,13 +147,13 @@ func TestClear(t *testing.T) {
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4) c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
cache.ClearOnRebuild(nil) cache.ClearOnRebuild()
// Stale items are always cleared. // Stale items are always cleared.
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2) c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
cache = newTestCache(t) cache = newTestCache(t)
cache.ClearOnRebuild(nil, identity.StringIdentity("changed")) cache.ClearOnRebuild(identity.StringIdentity("changed"))
c.Assert(cache.Keys(nil), qt.HasLen, 1) c.Assert(cache.Keys(nil), qt.HasLen, 1)
@@ -191,16 +191,16 @@ func TestPanicInCreate(t *testing.T) {
return err return err
} }
for i := range 3 { for i := 0; i < 3; i++ {
for range 3 { for j := 0; j < 3; j++ {
c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i)) c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i))
c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i)) c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i))
} }
} }
// Test the same keys again without the panic. // Test the same keys again without the panic.
for i := range 3 { for i := 0; i < 3; i++ {
for range 3 { for j := 0; j < 3; j++ {
v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) { v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
return testItem{ return testItem{
name: key, name: key,
-7
View File
@@ -51,18 +51,11 @@ const (
type Configs map[string]FileCacheConfig type Configs map[string]FileCacheConfig
// CacheDirModules returns the compiled path to the modules cache.
// For internal use. // For internal use.
func (c Configs) CacheDirModules() string { func (c Configs) CacheDirModules() string {
return c[CacheKeyModules].DirCompiled 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{ var defaultCacheConfigs = Configs{
CacheKeyModules: { CacheKeyModules: {
MaxAge: -1, MaxAge: -1,
+1 -1
View File
@@ -67,7 +67,7 @@ title: "Home"
--- ---
-- assets/a/pixel.png -- -- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/home.html -- -- layouts/index.html --
{{ warnf "HOME!" }} {{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }} {{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }} {{ $img = $img.Resize "3x3" }}
+3 -3
View File
@@ -59,7 +59,7 @@ dir = ":resourceDir/_gen"
caches, err := filecache.NewCaches(p) caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
cache := caches[name] cache := caches[name]
for i := range 10 { for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i) id := fmt.Sprintf("i%d", i)
cache.GetOrCreateBytes(id, func() ([]byte, error) { cache.GetOrCreateBytes(id, func() ([]byte, error) {
return []byte("abc"), nil return []byte("abc"), nil
@@ -74,7 +74,7 @@ dir = ":resourceDir/_gen"
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(count, qt.Equals, 5, msg) c.Assert(count, qt.Equals, 5, msg)
for i := range 10 { for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i) id := fmt.Sprintf("i%d", i)
v := cache.GetString(id) v := cache.GetString(id)
if i < 5 { if i < 5 {
@@ -97,7 +97,7 @@ dir = ":resourceDir/_gen"
c.Assert(count, qt.Equals, 4) c.Assert(count, qt.Equals, 4)
// Now only the i5 should be left. // Now only the i5 should be left.
for i := range 10 { for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i) id := fmt.Sprintf("i%d", i)
v := cache.GetString(id) v := cache.GetString(id)
if i != 5 { if i != 5 {
+4 -4
View File
@@ -105,7 +105,7 @@ dir = ":cacheDir/c"
} }
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} { for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for range 2 { for i := 0; i < 2; i++ {
info, r, err := ca.GetOrCreate("a", rf("abc")) info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(r, qt.Not(qt.IsNil)) c.Assert(r, qt.Not(qt.IsNil))
@@ -193,11 +193,11 @@ dir = "/cache/c"
var wg sync.WaitGroup var wg sync.WaitGroup
for i := range 50 { for i := 0; i < 50; i++ {
wg.Add(1) wg.Add(1)
go func(i int) { go func(i int) {
defer wg.Done() defer wg.Done()
for range 20 { for j := 0; j < 20; j++ {
ca := caches.Get(cacheName) ca := caches.Get(cacheName)
c.Assert(ca, qt.Not(qt.IsNil)) c.Assert(ca, qt.Not(qt.IsNil))
filename, data := filenameData(i) filename, data := filenameData(i)
@@ -270,7 +270,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
cfg, err := config.FromConfigString(configStr, "toml") cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
acfg := testconfig.GetTestConfig(fs, cfg) 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) c.Assert(err, qt.IsNil)
return p return p
} }
+12 -45
View File
@@ -25,8 +25,6 @@ import (
// DefaultConfig holds the default configuration for the HTTP cache. // DefaultConfig holds the default configuration for the HTTP cache.
var DefaultConfig = Config{ var DefaultConfig = Config{
RespectCacheControlNoStoreInRequest: true,
RespectCacheControlNoStoreInResponse: false,
Cache: Cache{ Cache: Cache{
For: GlobMatcher{ For: GlobMatcher{
Excludes: []string{"**"}, Excludes: []string{"**"},
@@ -44,13 +42,7 @@ var DefaultConfig = Config{
// Config holds the configuration for the HTTP cache. // Config holds the configuration for the HTTP cache.
type Config struct { 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. // Configures the HTTP cache behaviour (RFC 9111).
RespectCacheControlNoStoreInRequest bool
// When enabled and there's a Cache-Control: no-store directive in the response, response will never be stored in disk cache.
RespectCacheControlNoStoreInResponse bool
// Enables HTTP cache behavior (RFC 9111) for these resources.
// When this is not enabled for a resource, Hugo will go straight to the file cache. // When this is not enabled for a resource, Hugo will go straight to the file cache.
Cache Cache Cache Cache
@@ -60,14 +52,12 @@ type Config struct {
} }
type Cache struct { type Cache struct {
// Enable HTTP cache behavior (RFC 9111) for these resources. // Enable HTTP cache behaviour (RFC 9111) for these rsources.
For GlobMatcher For GlobMatcher
} }
func (c *Config) Compile() (ConfigCompiled, error) { func (c *Config) Compile() (ConfigCompiled, error) {
cc := ConfigCompiled{ var cc ConfigCompiled
Base: *c,
}
p, err := c.Cache.For.CompilePredicate() p, err := c.Cache.For.CompilePredicate()
if err != nil { if err != nil {
@@ -132,12 +122,7 @@ type GlobMatcher struct {
Includes []string Includes []string
} }
func (gm GlobMatcher) IsZero() bool {
return len(gm.Includes) == 0 && len(gm.Excludes) == 0
}
type ConfigCompiled struct { type ConfigCompiled struct {
Base Config
For predicate.P[string] For predicate.P[string]
PollConfigs []PollConfigCompiled PollConfigs []PollConfigCompiled
} }
@@ -170,19 +155,16 @@ func (p PollConfigCompiled) IsZero() bool {
} }
func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) { func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if gm.IsZero() { var p predicate.P[string]
panic("no includes or excludes")
}
var b predicate.PR[string]
for _, include := range gm.Includes { for _, include := range gm.Includes {
g, err := glob.Compile(include, '/') g, err := glob.Compile(include, '/')
if err != nil { if err != nil {
return nil, err return nil, err
} }
fn := func(s string) predicate.Match { fn := func(s string) bool {
return predicate.BoolMatch(g.Match(s)) return g.Match(s)
} }
b = b.Or(fn) p = p.Or(fn)
} }
for _, exclude := range gm.Excludes { for _, exclude := range gm.Excludes {
@@ -190,16 +172,16 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
fn := func(s string) predicate.Match { fn := func(s string) bool {
return predicate.BoolMatch(!g.Match(s)) 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) { func DecodeConfig(bcfg config.BaseConfig, m map[string]any) (Config, error) {
if len(m) == 0 { if len(m) == 0 {
return DefaultConfig, nil return DefaultConfig, nil
} }
@@ -221,20 +203,5 @@ func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
return c, err return c, err
} }
if c.Cache.For.IsZero() {
c.Cache.For = DefaultConfig.Cache.For
}
for pci := range c.Polls {
if c.Polls[pci].For.IsZero() {
c.Polls[pci].For = DefaultConfig.Cache.For
c.Polls[pci].Disable = true
}
}
if len(c.Polls) == 0 {
c.Polls = DefaultConfig.Polls
}
return c, nil return c, nil
} }
-31
View File
@@ -22,8 +22,6 @@ import (
) )
func TestConfigCustom(t *testing.T) { func TestConfigCustom(t *testing.T) {
t.Parallel()
files := ` files := `
-- hugo.toml -- -- hugo.toml --
[httpcache] [httpcache]
@@ -53,8 +51,6 @@ includes = ["**gohugo.io**"]
} }
func TestConfigDefault(t *testing.T) { func TestConfigDefault(t *testing.T) {
t.Parallel()
files := ` files := `
-- hugo.toml -- -- hugo.toml --
` `
@@ -66,30 +62,3 @@ func TestConfigDefault(t *testing.T) {
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse) b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
b.Assert(compiled.PollConfigFor("https://gohugo.io/foo.jpg").Config.Disable, qt.IsTrue) b.Assert(compiled.PollConfigFor("https://gohugo.io/foo.jpg").Config.Disable, qt.IsTrue)
} }
func TestConfigPollsOnly(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[httpcache]
[[httpcache.polls]]
low = "5s"
high = "32s"
[httpcache.polls.for]
includes = ["**gohugo.io**"]
`
b := hugolib.Test(t, files)
compiled := b.H.Configs.Base.C.HTTPCache
b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse)
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg")
b.Assert(pc.Config.Low, qt.Equals, 5*time.Second)
b.Assert(pc.Config.High, qt.Equals, 32*time.Second)
b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue)
}
-31
View File
@@ -17,7 +17,6 @@ import (
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
) )
func TestGlobMatcher(t *testing.T) { func TestGlobMatcher(t *testing.T) {
@@ -41,33 +40,3 @@ func TestGlobMatcher(t *testing.T) {
c.Assert(p("foo/bar/foo.css"), qt.IsFalse) c.Assert(p("foo/bar/foo.css"), qt.IsFalse)
c.Assert(p("foo/bar/foo.xml"), qt.IsTrue) c.Assert(p("foo/bar/foo.xml"), qt.IsTrue)
} }
func TestDefaultConfig(t *testing.T) {
c := qt.New(t)
_, err := DefaultConfig.Compile()
c.Assert(err, qt.IsNil)
}
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]any{})
c.Assert(err, qt.IsNil)
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
_, err = cfg.Compile()
c.Assert(err, qt.IsNil)
cfg, err = DecodeConfig(config.BaseConfig{}, map[string]any{
"cache": map[string]any{
"polls": []map[string]any{
{"disable": true},
},
},
})
c.Assert(err, qt.IsNil)
_, err = cfg.Compile()
c.Assert(err, qt.IsNil)
}
+7 -8
View File
@@ -26,7 +26,6 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"slices"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -103,7 +102,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
} }
for _, t := range include { for _, t := range include {
for i := range t.NumMethod() { for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i) m := t.Method(i)
if excludes[m.Name] || seen[m.Name] { if excludes[m.Name] || seen[m.Name] {
@@ -123,7 +122,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name} method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
for i := range numIn { for i := 0; i < numIn; i++ {
in := m.Type.In(i) in := m.Type.In(i)
name, pkg := nameAndPackage(in) name, pkg := nameAndPackage(in)
@@ -138,7 +137,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
numOut := m.Type.NumOut() numOut := m.Type.NumOut()
if numOut > 0 { if numOut > 0 {
for i := range numOut { for i := 0; i < numOut; i++ {
out := m.Type.Out(i) out := m.Type.Out(i)
name, pkg := nameAndPackage(out) name, pkg := nameAndPackage(out)
@@ -305,7 +304,7 @@ func (m Method) inOutStr() string {
} }
args := make([]string, len(m.In)) args := make([]string, len(m.In))
for i := range args { for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d", i) args[i] = fmt.Sprintf("arg%d", i)
} }
return "(" + strings.Join(args, ", ") + ")" return "(" + strings.Join(args, ", ") + ")"
@@ -317,7 +316,7 @@ func (m Method) inStr() string {
} }
args := make([]string, len(m.In)) args := make([]string, len(m.In))
for i := range args { for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i]) args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
} }
return "(" + strings.Join(args, ", ") + ")" return "(" + strings.Join(args, ", ") + ")"
@@ -340,7 +339,7 @@ func (m Method) outStrNamed() string {
} }
outs := make([]string, len(m.Out)) outs := make([]string, len(m.Out))
for i := range outs { for i := 0; i < len(outs); i++ {
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i]) outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
} }
@@ -436,7 +435,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
// Exclude self // Exclude self
for i, pkgImp := range pkgImports { for i, pkgImp := range pkgImports {
if pkgImp == pkgPath { if pkgImp == pkgPath {
pkgImports = slices.Delete(pkgImports, i, i+1) pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
} }
} }
} }
+8 -8
View File
@@ -101,8 +101,8 @@ type configKey struct {
// This is the root command. // This is the root command.
type rootCommand struct { type rootCommand struct {
Printf func(format string, v ...any) Printf func(format string, v ...interface{})
Println func(a ...any) Println func(a ...interface{})
StdOut io.Writer StdOut io.Writer
StdErr io.Writer StdErr io.Writer
@@ -401,7 +401,9 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs) 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...) watcher, err := b.newWatcher(r.poll, watchDirs...)
if err != nil { if err != nil {
return err return err
@@ -429,12 +431,12 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
// Used by mkcert (server). // Used by mkcert (server).
log.SetOutput(r.StdOut) log.SetOutput(r.StdOut)
r.Printf = func(format string, v ...any) { r.Printf = func(format string, v ...interface{}) {
if !r.quiet { if !r.quiet {
fmt.Fprintf(r.StdOut, format, v...) fmt.Fprintf(r.StdOut, format, v...)
} }
} }
r.Println = func(a ...any) { r.Println = func(a ...interface{}) {
if !r.quiet { if !r.quiet {
fmt.Fprintln(r.StdOut, a...) fmt.Fprintln(r.StdOut, a...)
} }
@@ -445,8 +447,6 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
if err != nil { if err != nil {
return err return err
} }
// Set up the global logger early to allow info deprecations during config load.
loggers.SetGlobalLogger(r.logger)
r.changesFromBuild = make(chan []identity.Identity, 10) r.changesFromBuild = make(chan []identity.Identity, 10)
@@ -538,7 +538,6 @@ Complete documentation is available at https://gohugo.io/.`
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory") cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
_ = cmd.MarkFlagDirname("themesDir") _ = cmd.MarkFlagDirname("themesDir")
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern") cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
cmd.PersistentFlags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
_ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions)
cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00") cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
_ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions)
@@ -594,6 +593,7 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.") cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.")
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files") cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files") cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations") cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.") cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.") cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
+9 -12
View File
@@ -25,7 +25,6 @@ import (
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/parser/metadecoders"
@@ -44,9 +43,8 @@ func newConfigCommand() *configCommand {
type configCommand struct { type configCommand struct {
r *rootCommand r *rootCommand
format string format string
lang string lang string
printZero bool
commands []simplecobra.Commander commands []simplecobra.Commander
} }
@@ -72,7 +70,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
return fmt.Errorf("language %q not found", c.lang) return fmt.Errorf("language %q not found", c.lang)
} }
} else { } else {
config = conf.configs.LanguageConfigMap[conf.configs.Base.DefaultContentLanguage] config = conf.configs.LanguageConfigSlice[0]
} }
var buf bytes.Buffer var buf bytes.Buffer
@@ -80,7 +78,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
dec.SetIndent("", " ") dec.SetIndent("", " ")
dec.SetEscapeHTML(false) dec.SetEscapeHTML(false)
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: !c.printZero}); err != nil { if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: true}); err != nil {
return err return err
} }
@@ -91,7 +89,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
os.Stdout.Write(buf.Bytes()) os.Stdout.Write(buf.Bytes())
default: default:
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format. // Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any var m map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil { if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err return err
} }
@@ -117,7 +115,6 @@ func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)") 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.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.") cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`)
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
applyLocalFlagsBuildConfig(cmd, c.r) applyLocalFlagsBuildConfig(cmd, c.r)
@@ -129,9 +126,9 @@ func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
} }
type configModMount struct { type configModMount struct {
Source string `json:"source"` Source string `json:"source"`
Target string `json:"target"` Target string `json:"target"`
Sites sitesmatrix.Sites `json:"sites,omitzero"` Lang string `json:"lang,omitempty"`
} }
type configModMounts struct { type configModMounts struct {
@@ -147,7 +144,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
mounts = append(mounts, configModMount{ mounts = append(mounts, configModMount{
Source: mount.Source, Source: mount.Source,
Target: mount.Target, Target: mount.Target,
Sites: mount.Sites, Lang: mount.Lang,
}) })
} }
+1
View File
@@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build withdeploy //go:build withdeploy
// +build withdeploy
package commands package commands
+1
View File
@@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build !withdeploy //go:build !withdeploy
// +build !withdeploy
// Copyright 2024 The Hugo Authors. All rights reserved. // Copyright 2024 The Hugo Authors. All rights reserved.
// //
+8 -28
View File
@@ -21,14 +21,12 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles" "github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/goccy/go-yaml"
"github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/docshelper" "github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
@@ -37,6 +35,7 @@ import (
"github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"gopkg.in/yaml.v2"
) )
func newGenCommand() *genCommand { func newGenCommand() *genCommand {
@@ -50,8 +49,6 @@ func newGenCommand() *genCommand {
highlightStyle string highlightStyle string
lineNumbersInlineStyle string lineNumbersInlineStyle string
lineNumbersTableStyle string lineNumbersTableStyle string
omitEmpty bool
omitClassComments bool
) )
newChromaStyles := func() simplecobra.Commander { newChromaStyles := func() simplecobra.Commander {
@@ -63,10 +60,6 @@ func newGenCommand() *genCommand {
See https://xyproto.github.io/splash/docs/all.html 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 { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
style = strings.ToLower(style)
if !slices.Contains(styles.Names(), style) {
return fmt.Errorf("invalid style: %s", style)
}
builder := styles.Get(style).Builder() builder := styles.Get(style).Builder()
if highlightStyle != "" { if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle) builder.Add(chroma.LineHighlight, highlightStyle)
@@ -81,16 +74,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
if err != nil { if err != nil {
return err return err
} }
formatter := html.New(html.WithAllClasses(true))
if omitEmpty {
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
}
options := []html.Option{
html.WithCSSComments(!omitClassComments),
}
formatter := html.New(options...)
w := os.Stdout w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " ")) fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
formatter.WriteCSS(w, style) formatter.WriteCSS(w, style)
@@ -106,10 +90,6 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions) _ = 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.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions) _ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules (deprecated, no longer needed)`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
}, },
} }
} }
@@ -164,7 +144,7 @@ url: %s
return &simpleCommand{ return &simpleCommand{
name: "doc", name: "doc",
short: "Generate Markdown documentation for the Hugo CLI", short: "Generate Markdown documentation for the Hugo CLI.",
long: `Generate Markdown documentation for the Hugo CLI. long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/. of Hugo's command-line interface for https://gohugo.io/.
@@ -189,13 +169,13 @@ url: %s
prepender := func(filename string) string { prepender := func(filename string) string {
name := filepath.Base(filename) name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name)) base := strings.TrimSuffix(name, path.Ext(name))
url := "/docs/reference/commands/" + strings.ToLower(base) + "/" url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url) return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
} }
linkHandler := func(name string) string { linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name)) base := strings.TrimSuffix(name, path.Ext(name))
return "/docs/reference/commands/" + strings.ToLower(base) + "/" return "/commands/" + strings.ToLower(base) + "/"
} }
r.Println("Generating Hugo command-line documentation in", gendocdir, "...") r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler) doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
@@ -216,7 +196,7 @@ url: %s
newDocsHelper := func() simplecobra.Commander { newDocsHelper := func() simplecobra.Commander {
return &simpleCommand{ return &simpleCommand{
name: "docshelper", name: "docshelper",
short: "Generate some data files for the Hugo docs", short: "Generate some data files for the Hugo docs.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Println("Generate docs data to", docsHelperTarget) r.Println("Generate docs data to", docsHelperTarget)
@@ -237,7 +217,7 @@ url: %s
} }
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format. // Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any var m map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil { if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err return err
} }
@@ -249,7 +229,7 @@ url: %s
return err return err
} }
defer f.Close() defer f.Close()
yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt()) yamlEnc := yaml.NewEncoder(f)
if err := yamlEnc.Encode(m); err != nil { if err := yamlEnc.Encode(m); err != nil {
return err return err
} }
+1 -1
View File
@@ -76,7 +76,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
// Flags with a different name in the config. // Flags with a different name in the config.
keyMap := map[string]string{ keyMap := map[string]string{
"minify": "minify.minifyOutput", "minify": "minifyOutput",
"destination": "publishDir", "destination": "publishDir",
"editor": "newContentEditor", "editor": "newContentEditor",
} }
+23 -53
View File
@@ -27,11 +27,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/bep/debounce"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
@@ -64,7 +62,7 @@ type hugoBuilder struct {
// Currently only set when in "fast render mode". // Currently only set when in "fast render mode".
changeDetector *fileChangeDetector changeDetector *fileChangeDetector
visitedURLs *types.EvictingQueue[string] visitedURLs *types.EvictingStringQueue
fullRebuildSem *semaphore.Weighted fullRebuildSem *semaphore.Weighted
debounce func(f func()) debounce func(f func())
@@ -144,7 +142,7 @@ func (c *hugoBuilder) getDirList() ([]string, error) {
return nil, err 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) { func (c *hugoBuilder) initCPUProfile() (func(), error) {
@@ -464,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") infol.Logf("removing all files from destination that don't exist in static dirs")
syncer.DeleteFilter = func(f fsync.FileInfo) bool { syncer.DeleteFilter = func(f fsync.FileInfo) bool {
name := f.Name() return f.IsDir() && strings.HasPrefix(f.Name(), ".")
// Keep .gitignore and .gitattributes anywhere
if name == ".gitignore" || name == ".gitattributes" {
return true
}
// Keep Hugo's original dot-directory behavior
return f.IsDir() && strings.HasPrefix(name, ".")
} }
} }
start := time.Now() start := time.Now()
@@ -525,14 +515,6 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
return langCount, nil 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 { func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
var ( var (
g errgroup.Group g errgroup.Group
@@ -681,20 +663,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
var n int var n int
for _, ev := range evs { for _, ev := range evs {
keep := true keep := true
// Write and rename operations are often followed by CHMOD. if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
// There may be valid use cases for rebuilding the site on CHMOD,
// but that will require more complex logic than this simple conditional.
// On OS X this seems to be related to Spotlight, see:
// https://github.com/go-fsnotify/fsnotify/issues/15
// A workaround is to put your site(s) on the Spotlight exception list,
// but that may be a little mysterious for most end users.
// So, for now, we skip reload on CHMOD.
// We do have to check for WRITE though. On slower laptops a Chmod
// could be aggregated with other important events, and we still want
// to rebuild on those
if ev.Op == fsnotify.Chmod {
keep = false
} else if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
if _, err := os.Stat(ev.Name); err != nil { if _, err := os.Stat(ev.Name); err != nil {
keep = false keep = false
} }
@@ -836,7 +805,22 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
continue continue
} }
walkAdder := func(ctx context.Context, path string, f hugofs.FileMetaInfo) error { // Write and rename operations are often followed by CHMOD.
// There may be valid use cases for rebuilding the site on CHMOD,
// but that will require more complex logic than this simple conditional.
// On OS X this seems to be related to Spotlight, see:
// https://github.com/go-fsnotify/fsnotify/issues/15
// A workaround is to put your site(s) on the Spotlight exception list,
// but that may be a little mysterious for most end users.
// So, for now, we skip reload on CHMOD.
// We do have to check for WRITE though. On slower laptops a Chmod
// could be aggregated with other important events, and we still want
// to rebuild on those
if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
continue
}
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
if f.IsDir() { if f.IsDir() {
c.r.logger.Println("adding created directory to watchlist", path) c.r.logger.Println("adding created directory to watchlist", path)
if err := watcher.Add(path); err != nil { if err := watcher.Add(path); err != nil {
@@ -980,7 +964,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
lrl.Logf("no page to navigate to, force refresh") lrl.Logf("no page to navigate to, force refresh")
livereload.ForceRefresh() livereload.ForceRefresh()
} }
} else if len(otherChanges) > 0 || len(cssChanges) > 0 { } else if len(otherChanges) > 0 {
if len(otherChanges) == 1 { if len(otherChanges) == 1 {
// Allow single changes to be refreshed without a full page reload. // Allow single changes to be refreshed without a full page reload.
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false) pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false)
@@ -990,9 +974,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
lrl.Logf("force refresh") lrl.Logf("force refresh")
livereload.ForceRefresh() livereload.ForceRefresh()
} }
} else {
lrl.Logf("force refresh")
livereload.ForceRefresh()
} }
if len(cssChanges) > 0 { if len(cssChanges) > 0 {
@@ -1045,17 +1026,6 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
} }
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error { 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 := config.New()
cfg.Set("renderToMemory", c.r.renderToMemory) cfg.Set("renderToMemory", c.r.renderToMemory)
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch) watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
@@ -1133,7 +1103,7 @@ func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) (err error) {
if err != nil { if err != nil {
return return
} }
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...) err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
return return
} }
@@ -1149,7 +1119,7 @@ func (c *hugoBuilder) rebuildSitesForChanges(ids []identity.Identity) (err error
} }
whatChanged := &hugolib.WhatChanged{} whatChanged := &hugolib.WhatChanged{}
whatChanged.Add(ids...) whatChanged.Add(ids...)
err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}) err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()})
return return
} }
+1 -1
View File
@@ -427,7 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
c.r.Println("Importing...") c.r.Println("Importing...")
fileCount := 0 fileCount := 0
callback := func(ctx context.Context, path string, fi hugofs.FileMetaInfo) error { callback := func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() { if fi.IsDir() {
return nil return nil
} }
+14 -14
View File
@@ -44,16 +44,16 @@ func newModCommands() *modCommands {
npmCommand := &simpleCommand{ npmCommand := &simpleCommand{
name: "npm", name: "npm",
short: "Various npm helpers", short: "Various npm helpers.",
long: `Various npm (Node package manager) helpers.`, long: `Various npm (Node package manager) helpers.`,
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
&simpleCommand{ &simpleCommand{
name: "pack", name: "pack",
short: "Experimental: Prepares and writes a composite package.json file for your project", 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. long: `Prepares and writes a composite package.json file for your project.
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 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. with the base dependency set.
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project. This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
@@ -80,12 +80,12 @@ so this may/will change in future versions of Hugo.
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
&simpleCommand{ &simpleCommand{
name: "init", name: "init",
short: "Initialize this project as a Hugo Module", short: "Initialize this project as a Hugo Module.",
long: `Initialize this project as a Hugo Module. long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g: It will try to guess the module path, but you may help by passing it as an argument, e.g:
hugo mod init github.com/gohugoio/testshortcodes hugo mod init github.com/gohugoio/testshortcodes
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
inside a subfolder on GitHub, as one example. inside a subfolder on GitHub, as one example.
`, `,
@@ -111,7 +111,7 @@ so this may/will change in future versions of Hugo.
}, },
&simpleCommand{ &simpleCommand{
name: "verify", name: "verify",
short: "Verify dependencies", short: "Verify dependencies.",
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`, long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
@@ -129,7 +129,7 @@ so this may/will change in future versions of Hugo.
}, },
&simpleCommand{ &simpleCommand{
name: "graph", name: "graph",
short: "Print a module dependency graph", short: "Print a module dependency graph.",
long: `Print a module dependency graph with information about module status (disabled, vendored). long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod. Note that for vendored modules, that is the version listed and not the one from go.mod.
`, `,
@@ -149,7 +149,7 @@ Note that for vendored modules, that is the version listed and not the one from
}, },
&simpleCommand{ &simpleCommand{
name: "clean", name: "clean",
short: "Delete the Hugo Module cache for the current project", short: "Delete the Hugo Module cache for the current project.",
long: `Delete the Hugo Module cache for the current project.`, long: `Delete the Hugo Module cache for the current project.`,
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
@@ -175,7 +175,7 @@ Note that for vendored modules, that is the version listed and not the one from
}, },
&simpleCommand{ &simpleCommand{
name: "tidy", name: "tidy",
short: "Remove unused entries in go.mod and go.sum", short: "Remove unused entries in go.mod and go.sum.",
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r) applyLocalFlagsBuildConfig(cmd, r)
@@ -190,7 +190,7 @@ Note that for vendored modules, that is the version listed and not the one from
}, },
&simpleCommand{ &simpleCommand{
name: "vendor", name: "vendor",
short: "Vendor all module dependencies into the _vendor directory", short: "Vendor all module dependencies into the _vendor directory.",
long: `Vendor all module dependencies into the _vendor directory. long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies. If a module is vendored, that is where Hugo will look for it's dependencies.
`, `,
@@ -209,16 +209,16 @@ Note that for vendored modules, that is the version listed and not the one from
&simpleCommand{ &simpleCommand{
name: "get", name: "get",
short: "Resolves dependencies in your current Hugo project", short: "Resolves dependencies in your current Hugo Project.",
long: ` long: `
Resolves dependencies in your current Hugo project. Resolves dependencies in your current Hugo Project.
Some examples: Some examples:
Install the latest version possible for a given module: Install the latest version possible for a given module:
hugo mod get github.com/gohugoio/testshortcodes hugo mod get github.com/gohugoio/testshortcodes
Install a specific version: Install a specific version:
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0 hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
+12 -17
View File
@@ -53,9 +53,7 @@ Ensure you run this within the root directory of your site.`,
if len(args) < 1 { if len(args) < 1 {
return newUserError("path needs to be provided") return newUserError("path needs to be provided")
} }
cfg := flagsToCfg(cd, nil) h, err := r.Hugo(flagsToCfg(cd, nil))
cfg.Set("BuildFuture", true)
h, err := r.Hugo(cfg)
if err != nil { if err != nil {
return err return err
} }
@@ -78,8 +76,10 @@ Ensure you run this within the root directory of your site.`,
&simpleCommand{ &simpleCommand{
name: "site", name: "site",
use: "site [path]", use: "site [path]",
short: "Create a new site", short: "Create a new site (skeleton)",
long: `Create a new site at the specified path.`, long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return newUserError("path needs to be provided") return newUserError("path needs to be provided")
@@ -124,9 +124,11 @@ Ensure you run this within the root directory of your site.`,
&simpleCommand{ &simpleCommand{
name: "theme", name: "theme",
use: "theme [name]", use: "theme [name]",
short: "Create a new theme", short: "Create a new theme (skeleton)",
long: `Create a new theme with the specified name in the ./themes directory. long: `Create a new theme (skeleton) called [name] in ./themes.
This generates a functional theme including template examples and sample content.`, New theme is a skeleton. Please add content to the touched files. Add your
name to the copyright line in the license and adjust the theme.toml file
according to your needs.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return newUserError("theme name needs to be provided") return newUserError("theme name needs to be provided")
@@ -142,7 +144,7 @@ This generates a functional theme including template examples and sample content
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0])) createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
r.Println("Creating new theme in", createpath) r.Println("Creating new theme in", createpath)
err = skeletons.CreateTheme(createpath, sourceFs, format) err = skeletons.CreateTheme(createpath, sourceFs)
if err != nil { if err != nil {
return err return err
} }
@@ -150,14 +152,7 @@ This generates a functional theme including template examples and sample content
return nil return nil
}, },
withc: func(cmd *cobra.Command, r *rootCommand) { withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { cmd.ValidArgsFunction = cobra.NoFileCompletions
if len(args) != 0 {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
}
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
}, },
}, },
}, },
+2 -4
View File
@@ -28,14 +28,13 @@ func newReleaseCommand() simplecobra.Commander {
step int step int
skipPush bool skipPush bool
try bool try bool
version string
) )
return &simpleCommand{ return &simpleCommand{
name: "release", name: "release",
short: "Release a new version of Hugo", short: "Release a new version of Hugo.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { 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 { if err != nil {
return err return err
} }
@@ -48,7 +47,6 @@ func newReleaseCommand() simplecobra.Commander {
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote") cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes") 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().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)) _ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
}, },
} }
+71 -100
View File
@@ -23,7 +23,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
@@ -49,9 +48,6 @@ import (
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo" "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" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/common/urls"
@@ -61,6 +57,7 @@ import (
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems" "github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload" "github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/transform" "github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject" "github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero" "github.com/spf13/afero"
@@ -87,14 +84,10 @@ const (
configChangeGoWork = "go work file" configChangeGoWork = "go work file"
) )
const (
hugoHeaderRedirect = "X-Hugo-Redirect"
)
func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder { func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
var visitedURLs *types.EvictingQueue[string] var visitedURLs *types.EvictingStringQueue
if s != nil && !s.disableFastRender { if s != nil && !s.disableFastRender {
visitedURLs = types.NewEvictingQueue[string](20) visitedURLs = types.NewEvictingStringQueue(20)
} }
return &hugoBuilder{ return &hugoBuilder{
r: r, r: r,
@@ -122,7 +115,7 @@ func newServerCommand() *serverCommand {
commands: []simplecobra.Commander{ commands: []simplecobra.Commander{
&simpleCommand{ &simpleCommand{
name: "trust", name: "trust",
short: "Install the local CA in the system trust store", short: "Install the local CA in the system trust store.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
action := "-install" action := "-install"
if uninstall { if uninstall {
@@ -198,7 +191,9 @@ func (f *fileChangeDetector) PrepareNew() {
} }
f.prev = make(map[string]uint64) f.prev = make(map[string]uint64)
maps.Copy(f.prev, f.current) for k, v := range f.current {
f.prev[k] = v
}
f.current = make(map[string]uint64) f.current = make(map[string]uint64)
} }
@@ -312,71 +307,64 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
w.Header().Set(header.Key, header.Value) w.Header().Set(header.Key, header.Value)
} }
if canRedirect(requestURI, r) { if redirect := serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
if redirect := serverConfig.MatchRedirect(requestURI, r.Header); !redirect.IsZero() { // fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
doRedirect := true doRedirect := true
// This matches Netlify's behavior and is needed for SPA behavior. // This matches Netlify's behavior and is needed for SPA behavior.
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/ // See https://docs.netlify.com/routing/redirects/rewrites-proxies/
if !redirect.Force { if !redirect.Force {
path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path())) path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path()))
if root != "" { if root != "" {
path = filepath.Join(root, path) path = filepath.Join(root, path)
}
var fs afero.Fs
f.c.withConf(func(conf *commonConfig) {
fs = conf.fs.PublishDirServer
})
fi, err := fs.Stat(path)
if err == nil {
if fi.IsDir() {
// There will be overlapping directories, so we
// need to check for a file.
_, err = fs.Stat(filepath.Join(path, "index.html"))
doRedirect = err != nil
} else {
doRedirect = false
}
}
} }
var fs afero.Fs
f.c.withConf(func(conf *commonConfig) {
fs = conf.fs.PublishDirServer
})
if doRedirect { fi, err := fs.Stat(path)
w.Header().Set(hugoHeaderRedirect, "true")
switch redirect.Status {
case 404:
w.WriteHeader(404)
file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path()))
if err == nil {
defer file.Close()
io.Copy(w, file)
} else {
fmt.Fprintln(w, "<h1>Page Not Found</h1>")
}
return
case 200:
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil {
requestURI = redirect.To
r = r2
}
default:
w.Header().Set("Content-Type", "")
http.Redirect(w, r, redirect.To, redirect.Status)
return
if err == nil {
if fi.IsDir() {
// There will be overlapping directories, so we
// need to check for a file.
_, err = fs.Stat(filepath.Join(path, "index.html"))
doRedirect = err != nil
} else {
doRedirect = false
} }
} }
} }
if doRedirect {
switch redirect.Status {
case 404:
w.WriteHeader(404)
file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path()))
if err == nil {
defer file.Close()
io.Copy(w, file)
} else {
fmt.Fprintln(w, "<h1>Page Not Found</h1>")
}
return
case 200:
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil {
requestURI = redirect.To
r = r2
}
default:
w.Header().Set("Content-Type", "")
http.Redirect(w, r, redirect.To, redirect.Status)
return
}
}
} }
if f.c.fastRenderMode && f.c.errState.buildErr() == nil { if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
if isNavigation(requestURI, r) { if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
// 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 !f.c.visitedURLs.Contains(requestURI) {
// If not already on stack, re-render that single page. // If not already on stack, re-render that single page.
if err := f.c.partialReRender(requestURI); err != nil { if err := f.c.partialReRender(requestURI); err != nil {
@@ -499,7 +487,9 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs) 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...) watcher, err := c.newWatcher(c.r.poll, watchDirs...)
if err != nil { if err != nil {
return err return err
@@ -764,7 +754,7 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
c.serverPorts = make([]serverPortListener, len(conf.configs.Languages)) c.serverPorts = make([]serverPortListener, len(conf.configs.Languages))
} }
currentServerPort := c.serverPort currentServerPort := c.serverPort
for i := range c.serverPorts { for i := 0; i < len(c.serverPorts); i++ {
l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort))) l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort)))
if err == nil { if err == nil {
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort} c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
@@ -848,7 +838,7 @@ func (c *serverCommand) partialReRender(urls ...string) (err error) {
defer func() { defer func() {
c.errState.setWasErr(false) c.errState.setWasErr(false)
}() }()
visited := types.NewEvictingQueue[string](len(urls)) visited := types.NewEvictingStringQueue(len(urls))
for _, url := range urls { for _, url := range urls {
visited.Add(url) visited.Add(url)
} }
@@ -860,7 +850,7 @@ func (c *serverCommand) partialReRender(urls ...string) (err error) {
} }
// Note: We do not set NoBuildLock as the file lock is not acquired at this stage. // Note: We do not set NoBuildLock as the file lock is not acquired at this stage.
err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyTouched: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()}) err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()})
return return
} }
@@ -885,7 +875,7 @@ func (c *serverCommand) serve() error {
if isMultihost { if isMultihost {
for _, l := range conf.configs.ConfigLangs() { for _, l := range conf.configs.ConfigLangs() {
baseURLs = append(baseURLs, l.BaseURL()) baseURLs = append(baseURLs, l.BaseURL())
roots = append(roots, l.Language().(*langs.Language).Lang) roots = append(roots, l.Language().Lang)
} }
} else { } else {
l := conf.configs.GetFirstLanguageConfig() l := conf.configs.GetFirstLanguageConfig()
@@ -903,16 +893,16 @@ func (c *serverCommand) serve() error {
// To allow the en user to change the error template while the server is running, we use // To allow the en user to change the error template while the server is running, we use
// the freshest template we can provide. // the freshest template we can provide.
var ( var (
errTempl *tplimpl.TemplInfo errTempl tpl.Template
templHandler *tplimpl.TemplateStore templHandler tpl.TemplateHandler
) )
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) { getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
if h == nil { if h == nil {
return errTempl, templHandler return errTempl, templHandler
} }
templHandler := h.GetTemplateStore() templHandler := h.Tmpl()
errTempl := templHandler.LookupByPath("/_server/error.html") errTempl, found := templHandler.Lookup("_server/error.html")
if errTempl == nil { if !found {
panic("template server/error.html not found") panic("template server/error.html not found")
} }
return errTempl, templHandler return errTempl, templHandler
@@ -1040,6 +1030,7 @@ func (c *serverCommand) serve() error {
defer cancel() defer cancel()
wg2, ctx := errgroup.WithContext(ctx) wg2, ctx := errgroup.WithContext(ctx)
for _, srv := range servers { for _, srv := range servers {
srv := srv
wg2.Go(func() error { wg2.Go(func() error {
return srv.Shutdown(ctx) return srv.Shutdown(ctx)
}) })
@@ -1171,6 +1162,7 @@ func chmodFilter(dst, src os.FileInfo) bool {
} }
func cleanErrorLog(content string) string { func cleanErrorLog(content string) string {
content = strings.ReplaceAll(content, "\n", " ")
content = logReplacer.Replace(content) content = logReplacer.Replace(content)
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "") content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "") content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
@@ -1238,24 +1230,3 @@ func formatByteCount(b uint64) string {
return fmt.Sprintf("%.1f %cB", return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "kMGTPE"[exp]) float64(b)/float64(div), "kMGTPE"[exp])
} }
func canRedirect(requestURIWithoutQuery string, r *http.Request) bool {
if r.Header.Get(hugoHeaderRedirect) != "" {
return false
}
return isNavigation(requestURIWithoutQuery, r)
}
// Sec-Fetch-Mode should be sent by all recent browser versions, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode#navigate
// Fall back to the file extension if not set.
// The main take here is that we don't want to have CSS/JS files etc. partake in this logic.
func isNavigation(requestURIWithoutQuery string, r *http.Request) bool {
return r.Header.Get("Sec-Fetch-Mode") == "navigate" || isPropablyHTMLRequest(requestURIWithoutQuery)
}
func isPropablyHTMLRequest(requestURIWithoutQuery string) bool {
if strings.HasSuffix(requestURIWithoutQuery, "/") || strings.HasSuffix(requestURIWithoutQuery, "html") || strings.HasSuffix(requestURIWithoutQuery, "htm") {
return true
}
return !strings.Contains(requestURIWithoutQuery, ".")
}
+18 -6
View File
@@ -16,8 +16,6 @@ package collections
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/gohugoio/hugo/common/hreflect"
) )
// Append appends from to a slice to and returns the resulting slice. // 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 { if len(from) == 0 {
return to, nil return to, nil
} }
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to)) tov, toIsNil := indirect(reflect.ValueOf(to))
toIsNil = toIsNil || to == nil toIsNil = toIsNil || to == nil
var tot reflect.Type var tot reflect.Type
@@ -102,7 +100,7 @@ func Append(to any, from ...any) (any, error) {
fv := reflect.ValueOf(f) fv := reflect.ValueOf(f)
if !fv.IsValid() || !fv.Type().AssignableTo(tot) { if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
// Fall back to a []interface{} slice. // Fall back to a []interface{} slice.
tov, _ := hreflect.Indirect(reflect.ValueOf(to)) tov, _ := indirect(reflect.ValueOf(to))
return appendToInterfaceSlice(tov, from...) return appendToInterfaceSlice(tov, from...)
} }
tov = reflect.Append(tov, fv) tov = reflect.Append(tov, fv)
@@ -119,7 +117,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
tos = append(tos, nil) tos = append(tos, nil)
continue continue
} }
for i := range slice.Len() { for i := 0; i < slice.Len(); i++ {
tos = append(tos, slice.Index(i).Interface()) tos = append(tos, slice.Index(i).Interface())
} }
} }
@@ -130,7 +128,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) { func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
var tos []any var tos []any
for i := range tov.Len() { for i := 0; i < tov.Len(); i++ {
tos = append(tos, tov.Index(i).Interface()) tos = append(tos, tov.Index(i).Interface())
} }
@@ -138,3 +136,17 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
return tos, nil 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
}
-1
View File
@@ -77,7 +77,6 @@ func TestAppend(t *testing.T) {
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}}, {[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}}, {[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}}, {[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
{[]string{"a", "b"}, []any{}, []string{"a", "b"}},
} { } {
result, err := Append(test.start, test.addend...) result, err := Append(test.start, test.addend...)
-34
View File
@@ -136,37 +136,3 @@ func TestSortedStringSlice(t *testing.T) {
c.Assert(s.Count("z"), qt.Equals, 0) c.Assert(s.Count("z"), qt.Equals, 0)
c.Assert(s.Count("a"), qt.Equals, 1) c.Assert(s.Count("a"), qt.Equals, 1)
} }
func TestStringSliceToInterfaceSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
tests := []struct {
name string
in []string
want []any
}{
{
name: "empty slice",
in: []string{},
want: []any{},
},
{
name: "single element",
in: []string{"hello"},
want: []any{"hello"},
},
{
name: "multiple elements",
in: []string{"a", "b", "c"},
want: []any{"a", "b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := StringSliceToInterfaceSlice(tt.in)
c.Assert(got, qt.DeepEquals, tt.want)
})
}
}
+2 -13
View File
@@ -13,13 +13,7 @@
package collections package collections
import ( import "sync"
"iter"
"slices"
"sync"
"github.com/gohugoio/hugo/common/hiter"
)
// Stack is a simple LIFO stack that is safe for concurrent use. // Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct { type Stack[T any] struct {
@@ -64,11 +58,6 @@ func (s *Stack[T]) Len() int {
return len(s.items) return len(s.items)
} }
// All returns all items in the stack, from bottom to top.
func (s *Stack[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
func (s *Stack[T]) Drain() []T { func (s *Stack[T]) Drain() []T {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -84,7 +73,7 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
for i := len(s.items) - 1; i >= 0; i-- { for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) { if predicate(s.items[i]) {
items = append(items, s.items[i]) items = append(items, s.items[i])
s.items = slices.Delete(s.items, i, i+1) s.items = append(s.items[:i], s.items[i+1:]...)
} }
} }
return items return items
-77
View File
@@ -1,77 +0,0 @@
package collections
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestNewStack(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
c.Assert(s, qt.IsNotNil)
}
func TestStackBasic(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
c.Assert(s.Len(), qt.Equals, 0)
s.Push(1)
s.Push(2)
s.Push(3)
c.Assert(s.Len(), qt.Equals, 3)
top, ok := s.Peek()
c.Assert(ok, qt.Equals, true)
c.Assert(top, qt.Equals, 3)
popped, ok := s.Pop()
c.Assert(ok, qt.Equals, true)
c.Assert(popped, qt.Equals, 3)
c.Assert(s.Len(), qt.Equals, 2)
_, _ = s.Pop()
_, _ = s.Pop()
_, ok = s.Pop()
c.Assert(ok, qt.Equals, false)
}
func TestStackDrain(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[string]()
s.Push("a")
s.Push("b")
got := s.Drain()
c.Assert(got, qt.DeepEquals, []string{"a", "b"})
c.Assert(s.Len(), qt.Equals, 0)
}
func TestStackDrainMatching(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
s.Push(1)
s.Push(2)
s.Push(3)
s.Push(4)
got := s.DrainMatching(func(v int) bool { return v%2 == 0 })
c.Assert(got, qt.DeepEquals, []int{4, 2})
c.Assert(s.Drain(), qt.DeepEquals, []int{1, 3})
}
-2
View File
@@ -23,8 +23,6 @@ const (
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides" WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html" WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html" WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
) )
// Field/method names with special meaning. // Field/method names with special meaning.
+4 -61
View File
@@ -23,7 +23,6 @@ import (
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure" "github.com/gohugoio/hashstructure"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
) )
@@ -39,47 +38,6 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
return h.Sum64(), size, nil 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) {
h := getXxHashReadFrom()
defer putXxHashReadFrom(h)
_, err := io.Copy(h, r)
if err != nil {
return "", err
}
hash := h.Sum(nil)
return hex.EncodeToString(hash), nil
}
// XXHashFromString calculates the xxHash for the given string. // XXHashFromString calculates the xxHash for the given string.
func XXHashFromString(s string) (uint64, error) { func XXHashFromString(s string) (uint64, error) {
h := xxhash.New() h := xxhash.New()
@@ -112,13 +70,6 @@ func HashString(vs ...any) string {
return strconv.FormatUint(hash, 10) return strconv.FormatUint(hash, 10)
} }
// HashStringHex returns a hash from the given elements as a hex encoded string.
// See HashString for more information.
func HashStringHex(vs ...any) string {
hash := HashUint64(vs...)
return strconv.FormatUint(hash, 16)
}
var hashOptsPool = sync.Pool{ var hashOptsPool = sync.Pool{
New: func() any { New: func() any {
return &hashstructure.HashOptions{ return &hashstructure.HashOptions{
@@ -152,24 +103,16 @@ func HashUint64(vs ...any) uint64 {
o = elements o = elements
} }
hash, err := Hash(o) hashOpts := getHashOpts()
defer putHashOpts(hashOpts)
hash, err := hashstructure.Hash(o, hashOpts)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return hash return hash
} }
// Hash returns a hash from vs.
func Hash(vs ...any) (uint64, error) {
hashOpts := getHashOpts()
defer putHashOpts(hashOpts)
var v any = vs
if len(vs) == 1 {
v = vs[0]
}
return hashstructure.Hash(v, hashOpts)
}
type keyer interface { type keyer interface {
Key() string Key() string
} }
+10 -20
View File
@@ -37,11 +37,12 @@ func TestXxHashFromReaderPara(t *testing.T) {
c := qt.New(t) c := qt.New(t)
var wg sync.WaitGroup var wg sync.WaitGroup
for i := range 10 { for i := 0; i < 10; i++ {
i := i
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
for j := range 100 { for j := 0; j < 100; j++ {
s := strings.Repeat("Hello ", i+j+1*42) s := strings.Repeat("Hello ", i+j+1*42)
r := strings.NewReader(s) r := strings.NewReader(s)
got, size, err := XXHashFromReader(r) got, size, err := XXHashFromReader(r)
@@ -74,8 +75,8 @@ func TestXxHashFromStringHexEncoded(t *testing.T) {
func BenchmarkXXHashFromReader(b *testing.B) { func BenchmarkXXHashFromReader(b *testing.B) {
r := strings.NewReader("Hello World") r := strings.NewReader("Hello World")
b.ResetTimer()
for b.Loop() { for i := 0; i < b.N; i++ {
XXHashFromReader(r) XXHashFromReader(r)
r.Seek(0, 0) r.Seek(0, 0)
} }
@@ -83,16 +84,16 @@ func BenchmarkXXHashFromReader(b *testing.B) {
func BenchmarkXXHashFromString(b *testing.B) { func BenchmarkXXHashFromString(b *testing.B) {
s := "Hello World" s := "Hello World"
b.ResetTimer()
for b.Loop() { for i := 0; i < b.N; i++ {
XXHashFromString(s) XXHashFromString(s)
} }
} }
func BenchmarkXXHashFromStringHexEncoded(b *testing.B) { func BenchmarkXXHashFromStringHexEncoded(b *testing.B) {
s := "The quick brown fox jumps over the lazy dog" s := "The quick brown fox jumps over the lazy dog"
b.ResetTimer()
for b.Loop() { for i := 0; i < b.N; i++ {
XxHashFromStringHexEncoded(s) XxHashFromStringHexEncoded(s)
} }
} }
@@ -135,20 +136,9 @@ func BenchmarkHashString(b *testing.B) {
for _, test := range tests { for _, test := range tests {
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) { b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
HashString(test) HashString(test)
} }
}) })
} }
} }
func BenchmarkHashMap(b *testing.B) {
m := map[string]any{}
for i := range 1000 {
m[fmt.Sprintf("key%d", i)] = i
}
for b.Loop() {
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!")
}
}
+8 -2
View File
@@ -152,7 +152,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
} }
if ectx.Position.LineNumber > 0 { if ectx.Position.LineNumber > 0 {
low := max(ectx.Position.LineNumber-3, 0) low := ectx.Position.LineNumber - 3
if low < 0 {
low = 0
}
if ectx.Position.LineNumber > 2 { if ectx.Position.LineNumber > 2 {
ectx.LinesPos = 2 ectx.LinesPos = 2
@@ -160,7 +163,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
ectx.LinesPos = ectx.Position.LineNumber - 1 ectx.LinesPos = ectx.Position.LineNumber - 1
} }
high := min(ectx.Position.LineNumber+2, len(lines)) high := ectx.Position.LineNumber + 2
if high > len(lines) {
high = len(lines)
}
ectx.Lines = lines[low:high] ectx.Lines = lines[low:high]
+10 -3
View File
@@ -17,13 +17,22 @@ package herrors
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"regexp" "regexp"
"runtime" "runtime"
"runtime/debug"
"strings" "strings"
"time" "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. // ErrorSender is a, typically, non-blocking error handler.
type ErrorSender interface { type ErrorSender interface {
SendError(err error) SendError(err error)
@@ -36,9 +45,7 @@ type ErrorSender interface {
func Recover(args ...any) { func Recover(args ...any) {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Println("ERR:", r) fmt.Println("ERR:", r)
buf := make([]byte, 64<<10) args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
buf = buf[:runtime.Stack(buf, false)]
args = append(args, "stacktrace from panic: \n"+string(buf), "\n")
fmt.Println(args...) fmt.Println(args...)
} }
} }
+12 -30
View File
@@ -110,11 +110,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
fe.errorContext = ectx 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 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 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 is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err) fileType, pos := extractFileTypePos(err)
pos.Filename = name pos.Filename = name
if fileType == "" { if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name)) _, 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) return NewFileErrorFromName(err, realFilename)
} }
defer f.Close() defer f.Close()
fe := NewFileErrorFromName(err, realFilename) return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
fe = fe.UpdateContent(f, linematcher)
return fe
} }
func openFile(filename string, fs afero.Fs) (afero.File, string, error) { func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -261,27 +258,8 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
return f, realFilename, nil return f, realFilename, nil
} }
// Cause returns the underlying error, that is, // Cause returns the underlying error or itself if it does not implement Unwrap.
// it unwraps errors until it finds one that does not implement
// the Unwrap method.
// For a shallow variant, see Unwrap.
func Cause(err error) error { func Cause(err error) error {
type unwrapper interface {
Unwrap() error
}
for err != nil {
cause, ok := err.(unwrapper)
if !ok {
break
}
err = cause.Unwrap()
}
return err
}
// Unwrap returns the underlying error or itself if it does not implement Unwrap.
func Unwrap(err error) error {
if u := errors.Unwrap(err); u != nil { if u := errors.Unwrap(err); u != nil {
return u return u
} }
@@ -289,7 +267,7 @@ func Unwrap(err error) error {
} }
func extractFileTypePos(err error) (string, text.Position) { func extractFileTypePos(err error) (string, text.Position) {
err = Unwrap(err) err = Cause(err)
var fileType string var fileType string
@@ -324,9 +302,13 @@ func extractFileTypePos(err error) (string, text.Position) {
} }
// Look in the error message for the line number. // Look in the error message for the line number.
if lno, col := commonLineNumberExtractor(err); lno > 0 { for _, handle := range lineNumberExtractors {
pos.ColumnNumber = col lno, col := handle(err)
pos.LineNumber = lno if lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
break
}
} }
if fileType == "" && pos.Filename != "" { if fileType == "" && pos.Filename != "" {
+3 -13
View File
@@ -19,27 +19,17 @@ import (
) )
var lineNumberExtractors = []lineNumberExtractor{ var lineNumberExtractors = []lineNumberExtractor{
// YAML parse errors.
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
// Template/shortcode parse errors // Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`), newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`), newLineNumberErrHandlerFromRegexp(`:(\d+):`),
// YAML parse errors
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
// i18n bundle errors // i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`), 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) type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor { func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
+33 -88
View File
@@ -26,9 +26,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/bep/logg" "github.com/cli/safeexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/security"
) )
@@ -88,7 +86,7 @@ var WithEnviron = func(env []string) func(c *commandeer) {
} }
// New creates a new Exec using the provided security config. // New creates a new Exec using the provided security config.
func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec { func New(cfg security.Config, workingDir string) *Exec {
var baseEnviron []string var baseEnviron []string
for _, v := range os.Environ() { for _, v := range os.Environ() {
k, _ := config.SplitEnvVar(v) k, _ := config.SplitEnvVar(v)
@@ -98,11 +96,9 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
} }
return &Exec{ return &Exec{
sc: cfg, sc: cfg,
workingDir: workingDir, workingDir: workingDir,
infol: log.InfoCommand("exec"), baseEnviron: baseEnviron,
baseEnviron: baseEnviron,
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
} }
} }
@@ -112,18 +108,28 @@ func IsNotFound(err error) bool {
return errors.As(err, &notFoundErr) return errors.As(err, &notFoundErr)
} }
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
// implementation that does not search in current directory before looking in PATH.
// See https://github.com/cli/safeexec and the linked issues.
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
bin, err := safeexec.LookPath(name)
if err != nil {
return nil, err
}
return exec.Command(bin, arg...), nil
}
// Exec enforces a security policy for commands run via os/exec. // Exec enforces a security policy for commands run via os/exec.
type Exec struct { type Exec struct {
sc security.Config sc security.Config
workingDir string workingDir string
infol logg.LevelLogger
// os.Environ filtered by the Exec.OsEnviron whitelist filter. // os.Environ filtered by the Exec.OsEnviron whitelist filter.
baseEnviron []string baseEnviron []string
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)] npxInit sync.Once
npxInit sync.Once npxAvailable bool
npxAvailable bool
} }
func (e *Exec) New(name string, arg ...any) (Runner, error) { func (e *Exec) New(name string, arg ...any) (Runner, error) {
@@ -149,86 +155,25 @@ func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner,
return cm.command(arg...) return cm.command(arg...)
} }
type binaryLocation int
func (b binaryLocation) String() string {
switch b {
case binaryLocationNodeModules:
return "node_modules/.bin"
case binaryLocationNpx:
return "npx"
case binaryLocationPath:
return "PATH"
}
return "unknown"
}
const (
binaryLocationNodeModules binaryLocation = iota + 1
binaryLocationNpx
binaryLocationPath
)
// Npx will in order: // Npx will in order:
// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory. // 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
// 2. If not found, and npx is available, run npx --no-install <name> <args>. // 2. If not found, and npx is available, run npx --no-install <name> <args>.
// 3. Fall back to the PATH. // 3. Fall back to the PATH.
// If name is "tailwindcss", we will try the PATH as the second option.
func (e *Exec) Npx(name string, arg ...any) (Runner, error) { func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
if err := e.sc.CheckAllowedExec(name); err != nil { // npx is slow, so first try the common case.
return nil, err nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
_, err := safeexec.LookPath(nodeBinFilename)
if err == nil {
return e.new(name, nodeBinFilename, arg...)
} }
e.checkNpx()
newRunner, err := e.newNPXRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) { if e.npxAvailable {
type tryFunc func() func(...any) (Runner, error) r, err := e.npx(name, arg...)
tryFuncs := map[binaryLocation]tryFunc{ if err == nil {
binaryLocationNodeModules: func() func(...any) (Runner, error) { return r, nil
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
_, err := exec.LookPath(nodeBinFilename)
if err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.new(name, nodeBinFilename, arg2...)
}
},
binaryLocationNpx: func() func(...any) (Runner, error) {
e.checkNpx()
if !e.npxAvailable {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.npx(name, arg2...)
}
},
binaryLocationPath: func() func(...any) (Runner, error) {
if _, err := exec.LookPath(name); err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
return e.New(name, arg2...)
}
},
} }
locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath}
if name == "tailwindcss" {
// See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx}
}
for _, loc := range locations {
if f := tryFuncs[loc](); f != nil {
e.infol.Logf("resolve %q using %s", name, loc)
return f, nil
}
}
return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
})
if err != nil {
return nil, err
} }
return e.New(name, arg...)
return newRunner(arg...)
} }
const ( const (
@@ -333,7 +278,7 @@ func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
bin = c.fullyQualifiedName bin = c.fullyQualifiedName
} else { } else {
var err error var err error
bin, err = exec.LookPath(c.name) bin, err = safeexec.LookPath(c.name)
if err != nil { if err != nil {
return nil, &NotFoundError{ return nil, &NotFoundError{
name: c.name, name: c.name,
@@ -371,7 +316,7 @@ func InPath(binaryName string) bool {
if strings.Contains(binaryName, "/") { if strings.Contains(binaryName, "/") {
panic("binary name should not contain any slash") panic("binary name should not contain any slash")
} }
_, err := exec.LookPath(binaryName) _, err := safeexec.LookPath(binaryName)
return err == nil return err == nil
} }
@@ -381,7 +326,7 @@ func LookPath(binaryName string) string {
if strings.Contains(binaryName, "/") { if strings.Contains(binaryName, "/") {
panic("binary name should not contain any slash") panic("binary name should not contain any slash")
} }
s, err := exec.LookPath(binaryName) s, err := safeexec.LookPath(binaryName)
if err != nil { if err != nil {
return "" return ""
} }
-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
}
}
}
}
-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.Ptr, reflect.Interface:
if val.IsNil() {
// Return typ's zero value.
return reflect.Zero(typ), true
}
val = val.Elem()
}
if val.Type().AssignableTo(typ) {
// No conversion needed.
return val, true
}
if IsInt(typ.Kind()) {
return convertToIntIfPossible(val, typ)
}
if IsFloat(typ.Kind()) {
return convertToFloatIfPossible(val, typ)
}
if IsUint(typ.Kind()) {
return convertToUintIfPossible(val, typ)
}
if IsString(typ.Kind()) && IsString(val.Kind()) {
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
if i < 0 {
return reflect.Value{}, false
}
u := uint64(i)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
if IsUint(val.Kind()) {
if typ.OverflowUint(val.Uint()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < 0 || f > float64(math.MaxUint64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
u := uint64(f)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
return reflect.Value{}, false
}
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
f := float64(i)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsUint(val.Kind()) {
u := val.Uint()
f := float64(u)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsFloat(val.Kind()) {
if typ.OverflowFloat(val.Float()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
if typ.OverflowInt(val.Int()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsUint(val.Kind()) {
if val.Uint() > uint64(math.MaxInt64) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(val.Uint())) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(f)) {
return reflect.Value{}, false
}
return reflect.ValueOf(int64(f)).Convert(typ), true
}
return reflect.Value{}, false
}
-284
View File
@@ -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)
}
}
+31 -112
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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -25,11 +27,6 @@ import (
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
) )
// IsInterfaceOrPointer returns whether the given kind is an interface or a pointer.
func IsInterfaceOrPointer(kind reflect.Kind) bool {
return kind == reflect.Interface || kind == reflect.Ptr
}
// TODO(bep) replace the private versions in /tpl with these. // TODO(bep) replace the private versions in /tpl with these.
// IsNumber returns whether the given kind is a number. // IsNumber returns whether the given kind is a number.
func IsNumber(kind reflect.Kind) bool { 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. // IsTruthful returns whether in represents a truthful value.
// See IsTruthfulValue // See IsTruthfulValue
func IsTruthful(in any) bool { func IsTruthful(in any) bool {
@@ -82,30 +74,8 @@ func IsTruthful(in any) bool {
} }
} }
// IsMap reports whether v is a map.
func IsMap(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Map
}
// IsSlice reports whether v is a slice.
func IsSlice(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Slice
}
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem() var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
var isZeroCache sync.Map
func implementsIsZero(tp reflect.Type) bool {
v, ok := isZeroCache.Load(tp)
if ok {
return v.(bool)
}
implements := tp.Implements(zeroType)
isZeroCache.Store(tp, implements)
return implements
}
// IsTruthfulValue returns whether the given value has a meaningful truth value. // IsTruthfulValue returns whether the given value has a meaningful truth value.
// This is based on template.IsTrue in Go's stdlib, but also considers // This is based on template.IsTrue in Go's stdlib, but also considers
// IsZero and any interface value will be unwrapped before it's considered // IsZero and any interface value will be unwrapped before it's considered
@@ -114,18 +84,14 @@ func implementsIsZero(tp reflect.Type) bool {
// Based on: // Based on:
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306 // https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
func IsTruthfulValue(val reflect.Value) (truth bool) { func IsTruthfulValue(val reflect.Value) (truth bool) {
val, isNil := Indirect(val) val = indirectInterface(val)
if !val.IsValid() { 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 return
} }
if val.Kind() == reflect.Pointer && isNil { if val.Type().Implements(zeroType) {
return
}
if implementsIsZero(val.Type()) {
return !val.Interface().(types.Zeroer).IsZero() return !val.Interface().(types.Zeroer).IsZero()
} }
@@ -158,31 +124,15 @@ type methodKey struct {
name string name string
} }
var ( type methods struct {
methodIndexCache sync.Map sync.RWMutex
methodCache sync.Map cache map[methodKey]int
)
// 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. var methodCache = &methods{cache: make(map[methodKey]int)}
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup.
func GetMethodByName(v reflect.Value, name string) reflect.Value { func GetMethodByName(v reflect.Value, name string) reflect.Value {
index := GetMethodIndexByName(v.Type(), name) index := GetMethodIndexByName(v.Type(), name)
@@ -197,16 +147,22 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
// -1 if no such method exists. // -1 if no such method exists.
func GetMethodIndexByName(tp reflect.Type, name string) int { func GetMethodIndexByName(tp reflect.Type, name string) int {
k := methodKey{tp, name} k := methodKey{tp, name}
v, found := methodIndexCache.Load(k) methodCache.RLock()
index, found := methodCache.cache[k]
methodCache.RUnlock()
if found { if found {
return v.(int) return index
} }
methodCache.Lock()
defer methodCache.Unlock()
m, ok := tp.MethodByName(name) m, ok := tp.MethodByName(name)
index := m.Index index = m.Index
if !ok { if !ok {
index = -1 index = -1
} }
methodIndexCache.Store(k, index) methodCache.cache[k] = index
if !ok { if !ok {
return -1 return -1
@@ -279,7 +235,7 @@ func ToSliceAny(v any) ([]any, bool) {
vvv := reflect.ValueOf(v) vvv := reflect.ValueOf(v)
if vvv.Kind() == reflect.Slice { if vvv.Kind() == reflect.Slice {
out := make([]any, vvv.Len()) out := make([]any, vvv.Len())
for i := range vvv.Len() { for i := 0; i < vvv.Len(); i++ {
out[i] = vvv.Index(i).Interface() out[i] = vvv.Index(i).Interface()
} }
return out, true return out, true
@@ -288,7 +244,6 @@ func ToSliceAny(v any) ([]any, bool) {
return nil, false 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 { func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
fn := v.MethodByName(name) fn := v.MethodByName(name)
var args []reflect.Value var args []reflect.Value
@@ -306,51 +261,15 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
return fn.Call(args) return fn.Call(args)
} }
// Indirect unwraps interfaces and pointers until it finds a non-interface/pointer value. // Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
// If a nil is encountered, the second return value is true. func indirectInterface(v reflect.Value) reflect.Value {
// If a pointer to a struct is encountered, it is not unwrapped. if v.Kind() != reflect.Interface {
func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) { return v
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
}
}
} }
return v, false if v.IsNil() {
} return reflect.Value{}
// 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 { return v.Elem()
vv = vv.Elem()
}
return vv, isNil
}
// IsNil reports whether v is nil.
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return v.IsNil()
}
return false
} }
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem() var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
+16 -140
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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -20,32 +20,15 @@ import (
"time" "time"
qt "github.com/frankban/quicktest" 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) { func TestIsTruthful(t *testing.T) {
c := qt.New(t) c := qt.New(t)
var nilpointerZero *zeroStruct
c.Assert(IsTruthful(true), qt.Equals, true) c.Assert(IsTruthful(true), qt.Equals, true)
c.Assert(IsTruthful(false), qt.Equals, false) c.Assert(IsTruthful(false), qt.Equals, false)
c.Assert(IsTruthful(time.Now()), qt.Equals, true) c.Assert(IsTruthful(time.Now()), qt.Equals, true)
c.Assert(IsTruthful(time.Time{}), qt.Equals, false) 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) { func TestGetMethodByName(t *testing.T) {
@@ -80,90 +63,18 @@ func TestToSliceAny(t *testing.T) {
checkOK([]int{1, 2, 3}, []any{1, 2, 3}) 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) { func BenchmarkIsContextType(b *testing.B) {
const size = 1000
type k string type k string
b.Run("value", func(b *testing.B) { b.Run("value", func(b *testing.B) {
ctx := context.Background() ctx := context.Background()
ctxs := make([]reflect.Type, size) ctxs := make([]reflect.Type, b.N)
for i := range size { for i := 0; i < b.N; i++ {
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i)) ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
} }
for i := 0; b.Loop(); i++ { b.ResetTimer()
idx := i % size for i := 0; i < b.N; i++ {
if !IsContextType(ctxs[idx]) { if !IsContextType(ctxs[i]) {
b.Fatal("not context") b.Fatal("not context")
} }
} }
@@ -171,7 +82,7 @@ func BenchmarkIsContextType(b *testing.B) {
b.Run("background", func(b *testing.B) { b.Run("background", func(b *testing.B) {
var ctxt reflect.Type = reflect.TypeOf(context.Background()) var ctxt reflect.Type = reflect.TypeOf(context.Background())
for b.Loop() { for i := 0; i < b.N; i++ {
if !IsContextType(ctxt) { if !IsContextType(ctxt) {
b.Fatal("not context") b.Fatal("not context")
} }
@@ -179,25 +90,14 @@ func BenchmarkIsContextType(b *testing.B) {
}) })
} }
func BenchmarkIsTruthFulValue(b *testing.B) { func BenchmarkIsTruthFul(b *testing.B) {
var ( v := reflect.ValueOf("Hugo")
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))
)
for b.Loop() { b.ResetTimer()
IsTruthfulValue(stringHugo) for i := 0; i < b.N; i++ {
IsTruthfulValue(stringEmpty) if !IsTruthfulValue(v) {
IsTruthfulValue(zero) b.Fatal("not truthful")
IsTruthfulValue(timeNow) }
IsTruthfulValue(boolTrue)
IsTruthfulValue(boolFalse)
IsTruthfulValue(nilPointer)
} }
} }
@@ -223,38 +123,14 @@ func (t *testStruct) Method5() string {
return "Hugo" 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) { func BenchmarkGetMethodByName(b *testing.B) {
v := reflect.ValueOf(&testStruct{}) v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"} methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
for b.Loop() { b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, method := range methods { for _, method := range methods {
_ = GetMethodByName(v, method) _ = GetMethodByName(v, method)
} }
} }
} }
func BenchmarkGetMethodByNamePara(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for _, method := range methods {
_ = GetMethodByName(v, method)
}
}
})
}
+14 -70
View File
@@ -16,8 +16,6 @@ package hstrings
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"slices"
"sort"
"strings" "strings"
"sync" "sync"
@@ -52,7 +50,12 @@ func (s StringEqualFold) Eq(s2 any) bool {
// EqualAny returns whether a string is equal to any of the given strings. // EqualAny returns whether a string is equal to any of the given strings.
func EqualAny(a string, b ...string) bool { func EqualAny(a string, b ...string) bool {
return slices.Contains(b, a) for _, s := range b {
if a == s {
return true
}
}
return false
} }
// regexpCache represents a cache of regexp objects protected by a mutex. // regexpCache represents a cache of regexp objects protected by a mutex.
@@ -97,22 +100,17 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
return reCache.getOrCompileRegexp(pattern) return reCache.getOrCompileRegexp(pattern)
} }
// HasAnyPrefix checks if the string s has any of the prefixes given. // InSlice checks if a string is an element of a slice of strings
func HasAnyPrefix(s string, prefixes ...string) bool { // and returns a boolean value.
for _, p := range prefixes { func InSlice(arr []string, el string) bool {
if strings.HasPrefix(s, p) { for _, v := range arr {
if v == el {
return true return true
} }
} }
return false 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 {
return slices.Contains(arr, el)
}
// InSlicEqualFold checks if a string is an element of a slice of strings // InSlicEqualFold checks if a string is an element of a slice of strings
// and returns a boolean value. // and returns a boolean value.
// It uses strings.EqualFold to compare. // It uses strings.EqualFold to compare.
@@ -139,61 +137,7 @@ func ToString(v any) (string, bool) {
return "", false return "", false
} }
// UniqueStrings returns a new slice with any duplicates removed. type Tuple struct {
func UniqueStrings(s []string) []string { First string
unique := make([]string, 0, len(s)) Second string
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]
} }
+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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@
package hstrings package hstrings
import ( import (
"reflect"
"regexp" "regexp"
"testing" "testing"
@@ -44,91 +43,14 @@ func TestGetOrCompileRegexp(t *testing.T) {
c.Assert(re.MatchString("123"), qt.Equals, true) 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) { func BenchmarkGetOrCompileRegexp(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
GetOrCompileRegexp(`\d+`) GetOrCompileRegexp(`\d+`)
} }
} }
func BenchmarkCompileRegexp(b *testing.B) { func BenchmarkCompileRegexp(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
regexp.MustCompile(`\d+`) 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() t.Parallel()
files := ` files := `
-- hugo.toml -- -- config.toml --
defaultContentLanguage = 'it' defaultContentLanguage = 'it'
-- layouts/home.html -- -- layouts/index.html --
{{ $dates := slice {{ $dates := slice
"2022-01-03" "2022-01-03"
"2022-02-01" "2022-02-01"
+3 -3
View File
@@ -112,7 +112,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06") june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
b.Run("Native", func(b *testing.B) { b.Run("Native", func(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
got := june06.Format("Monday Jan 2 2006") got := june06.Format("Monday Jan 2 2006")
if got != "Wednesday Jun 6 2018" { if got != "Wednesday Jun 6 2018" {
b.Fatalf("invalid format, got %q", got) b.Fatalf("invalid format, got %q", got)
@@ -123,7 +123,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
b.Run("Localized", func(b *testing.B) { b.Run("Localized", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn")) f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer() b.ResetTimer()
for b.Loop() { for i := 0; i < b.N; i++ {
got := f.Format(june06, "Monday Jan 2 2006") got := f.Format(june06, "Monday Jan 2 2006")
if got != "onsdag juni 6 2018" { if got != "onsdag juni 6 2018" {
b.Fatalf("invalid format, got %q", got) b.Fatalf("invalid format, got %q", got)
@@ -134,7 +134,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
b.Run("Localized Custom", func(b *testing.B) { b.Run("Localized Custom", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn")) f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer() b.ResetTimer()
for b.Loop() { for i := 0; i < b.N; i++ {
got := f.Format(june06, ":date_medium") got := f.Format(june06, ":date_medium")
if got != "6. juni 2018" { if got != "6. juni 2018" {
b.Fatalf("invalid format, got %q", got) b.Fatalf("invalid format, got %q", got)
+5 -5
View File
@@ -46,18 +46,18 @@ func TestHasBytesWriter(t *testing.T) {
return strings.Repeat("ab cfo", r.Intn(33)) return strings.Repeat("ab cfo", r.Intn(33))
} }
for range 22 { for i := 0; i < 22; i++ {
h, w := neww() h, w := neww()
fmt.Fprint(w, rndStr()+"abc __foobar"+rndStr()) fmt.Fprintf(w, rndStr()+"abc __foobar"+rndStr())
c.Assert(h.Patterns[0].Match, qt.Equals, true) c.Assert(h.Patterns[0].Match, qt.Equals, true)
h, w = neww() h, w = neww()
fmt.Fprint(w, rndStr()+"abc __f") fmt.Fprintf(w, rndStr()+"abc __f")
fmt.Fprint(w, "oo bar"+rndStr()) fmt.Fprintf(w, "oo bar"+rndStr())
c.Assert(h.Patterns[0].Match, qt.Equals, true) c.Assert(h.Patterns[0].Match, qt.Equals, true)
h, w = neww() h, w = neww()
fmt.Fprint(w, rndStr()+"abc __moo bar") fmt.Fprintf(w, rndStr()+"abc __moo bar")
c.Assert(h.Patterns[0].Match, qt.Equals, false) c.Assert(h.Patterns[0].Match, qt.Equals, false)
} }
-7
View File
@@ -32,13 +32,6 @@ type ReadSeekCloser interface {
io.Closer io.Closer
} }
// 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. // ReadSeekCloserProvider provides a ReadSeekCloser.
type ReadSeekCloserProvider interface { type ReadSeekCloserProvider interface {
ReadSeekCloser() (ReadSeekCloser, error) ReadSeekCloser() (ReadSeekCloser, error)
+18 -84
View File
@@ -28,14 +28,12 @@ import (
"github.com/bep/logg" "github.com/bep/logg"
"github.com/bep/godartsass/v2" "github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hstore"
"github.com/gohugoio/hugo/common/loggers" "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/gohugoio/hugo/hugofs/files"
"github.com/bep/helpers/contexthelpers"
"github.com/spf13/afero" "github.com/spf13/afero"
iofs "io/fs" iofs "io/fs"
@@ -56,7 +54,7 @@ var (
vendorInfo string vendorInfo string
) )
var _ hstore.StoreProvider = (*HugoInfo)(nil) var _ maps.StoreProvider = (*HugoInfo)(nil)
// HugoInfo contains information about the current Hugo environment // HugoInfo contains information about the current Hugo environment
type HugoInfo struct { type HugoInfo struct {
@@ -75,14 +73,14 @@ type HugoInfo struct {
conf ConfigProvider conf ConfigProvider
deps []*Dependency deps []*Dependency
store *hstore.Scratch store *maps.Scratch
// Context gives access to some of the context scoped variables. // Context gives access to some of the context scoped variables.
Context Context Context Context
} }
// Version returns the current version as a comparable version string. // Version returns the current version as a comparable version string.
func (i HugoInfo) Version() version.VersionString { func (i HugoInfo) Version() VersionString {
return CurrentVersion.Version() return CurrentVersion.Version()
} }
@@ -121,7 +119,7 @@ func (i HugoInfo) Deps() []*Dependency {
return i.deps return i.deps
} }
func (i HugoInfo) Store() *hstore.Scratch { func (i HugoInfo) Store() *maps.Scratch {
return i.store return i.store
} }
@@ -141,13 +139,9 @@ func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual() return i.conf.IsMultilingual()
} }
type contextKey uint8 type contextKey string
const ( var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope"))
contextKeyMarkupScope contextKey = iota
)
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
type Context struct{} type Context struct{}
@@ -198,7 +192,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
Environment: conf.Environment(), Environment: conf.Environment(),
conf: conf, conf: conf,
deps: deps, deps: deps,
store: hstore.NewScratch(), store: maps.NewScratch(),
GoVersion: goVersion, GoVersion: goVersion,
} }
} }
@@ -417,96 +411,36 @@ func IsDartSassGeV2() bool {
// 2. Their theme to work for at least the last few Hugo versions. // 2. Their theme to work for at least the last few Hugo versions.
func Deprecate(item, alternative string, version string) { func Deprecate(item, alternative string, version string) {
level := deprecationLogLevelFromVersion(version) level := deprecationLogLevelFromVersion(version)
deprecateLevel(item, alternative, version, level) DeprecateLevel(item, alternative, version, level)
}
// See Deprecate for details.
func DeprecateWithLogger(item, alternative string, version string, log logg.Logger) {
level := deprecationLogLevelFromVersion(version)
deprecateLevelWithLogger(item, alternative, version, level, log)
}
// DeprecateLevelMin informs about a deprecation starting at the given version, but with a minimum log level.
func DeprecateLevelMin(item, alternative string, version string, minLevel logg.Level) {
level := max(deprecationLogLevelFromVersion(version), minLevel)
deprecateLevel(item, alternative, version, level)
}
// deprecateLevel informs about a deprecation logging at the given level.
func deprecateLevel(item, alternative, version string, level logg.Level) {
deprecateLevelWithLogger(item, alternative, version, level, loggers.Log().Logger())
} }
// DeprecateLevel informs about a deprecation logging at the given level. // DeprecateLevel informs about a deprecation logging at the given level.
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) { func DeprecateLevel(item, alternative, version string, level logg.Level) {
var msg string var msg string
if level == logg.LevelError { if level == logg.LevelError {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative) msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
} else { } else {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative) msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
} }
log.WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg) loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf(msg)
} }
// We usually do about one minor version a month. // We usually do about one minor version a month.
// We want people to run at least the current and previous version without any warnings. // 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. // 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 { func deprecationLogLevelFromVersion(ver string) logg.Level {
from := version.MustParseVersion(ver) from := MustParseVersion(ver)
to := CurrentVersion to := CurrentVersion
minorDiff := to.Minor - from.Minor minorDiff := to.Minor - from.Minor
switch { switch {
case minorDiff >= 15: case minorDiff >= 12:
// Start failing the build after about 15 months. // Start failing the build after about a year.
return logg.LevelError return logg.LevelError
case minorDiff >= 3: case minorDiff >= 6:
// Start printing warnings after about 3 months. // Start printing warnings after about six months.
return logg.LevelWarn return logg.LevelWarn
default: default:
return logg.LevelInfo return logg.LevelInfo
} }
} }
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
+1 -1
View File
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
--- ---
title: home title: home
--- ---
-- layouts/home.html -- -- layouts/index.html --
multilingual={{ hugo.IsMultilingual }} multilingual={{ hugo.IsMultilingual }}
multihost={{ hugo.IsMultihost }} multihost={{ hugo.IsMultihost }}
` `
+5 -6
View File
@@ -20,7 +20,6 @@ import (
"github.com/bep/logg" "github.com/bep/logg"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/version"
) )
func TestHugoInfo(t *testing.T) { func TestHugoInfo(t *testing.T) {
@@ -30,7 +29,7 @@ func TestHugoInfo(t *testing.T) {
hugoInfo := NewInfo(conf, nil) hugoInfo := NewInfo(conf, nil)
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version()) c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version())) c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork") c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
bi := getBuildInfo() bi := getBuildInfo()
@@ -58,11 +57,11 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError) c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
ver := CurrentVersion ver := CurrentVersion
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo) c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
ver.Minor -= 3 ver.Minor -= 1
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
ver.Minor -= 6
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn) c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
ver.Minor -= 4 ver.Minor -= 6
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
ver.Minor -= 13
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError) c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
// Added just to find the threshold for where we can remove deprecated items. // Added just to find the threshold for where we can remove deprecated items.
+1
View File
@@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build extended //go:build extended
// +build extended
package hugo package hugo
+1
View File
@@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build !extended //go:build !extended
// +build !extended
package hugo package hugo
+1
View File
@@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build withdeploy //go:build withdeploy
// +build withdeploy
package hugo package hugo
+1
View File
@@ -12,6 +12,7 @@
// limitations under the License. // limitations under the License.
//go:build !withdeploy //go:build !withdeploy
// +build !withdeploy
package hugo package hugo
@@ -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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package version package hugo
import ( import (
"fmt" "fmt"
@@ -44,13 +44,6 @@ var (
_ compare.Comparer = (*VersionString)(nil) _ 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) String() string { func (v Version) String() string {
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix) return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
} }
@@ -62,7 +55,7 @@ func (v Version) Version() VersionString {
// Compare implements the compare.Comparer interface. // Compare implements the compare.Comparer interface.
func (h Version) Compare(other any) int { func (h Version) Compare(other any) int {
return CompareVersions(h, other) return compareVersions(h, other)
} }
// VersionString represents a Hugo version string. // VersionString represents a Hugo version string.
@@ -74,7 +67,7 @@ func (h VersionString) String() string {
// Compare implements the compare.Comparer interface. // Compare implements the compare.Comparer interface.
func (h VersionString) Compare(other any) int { func (h VersionString) Compare(other any) int {
return CompareVersions(h.Version(), other) return compareVersions(h.Version(), other)
} }
func (h VersionString) Version() Version { func (h VersionString) Version() Version {
@@ -90,24 +83,18 @@ func (h VersionString) Eq(other any) bool {
return s == h.String() return s == h.String()
} }
var versionSuffixes = []string{"-test", "-DEV"}
// ParseVersion parses a version string. // ParseVersion parses a version string.
func ParseVersion(s string) (Version, error) { func ParseVersion(s string) (Version, error) {
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
var vv Version var vv Version
hyphen := strings.Index(s, "-") for _, suffix := range versionSuffixes {
if hyphen > 0 { if strings.HasSuffix(s, suffix) {
suffix := s[hyphen:] vv.Suffix = suffix
if len(suffix) > 1 { s = strings.TrimSuffix(s, suffix)
if suffix[0] == '-' {
suffix = suffix[1:]
}
if len(suffix) > 0 {
vv.Suffix = suffix
s = s[:hyphen]
}
} }
vv.Suffix = suffix
} }
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s) vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
return vv, nil return vv, nil
@@ -147,55 +134,98 @@ func (v Version) NextPatchLevel(level int) Version {
return prev return prev
} }
func version(major, minor, patch int, suffix string) string { // BuildVersionString creates a version string. This is what you see when
if suffix != "" { // running "hugo version".
if suffix[0] != '-' { func BuildVersionString() string {
suffix = "-" + suffix // 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 { if patch > 0 || minor > 53 {
return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix) return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix)
} }
return fmt.Sprintf("%d.%d%s", major, minor, suffix) return fmt.Sprintf("%d.%d%s", major, minor, suffix)
} }
// CompareVersion compares v1 with v2. // CompareVersion compares the given version string or number against the
// It returns -1 if the v2 is less than, 0 if equal and 1 if greater than // running Hugo version.
// v1. // It returns -1 if the given version is less than, 0 if equal and 1 if greater than
func CompareVersions(v1 Version, v2 any) int { // the running version.
func CompareVersion(version any) int {
return compareVersions(CurrentVersion, version)
}
func compareVersions(inVersion Version, in any) int {
var c int var c int
switch d := v2.(type) { switch d := in.(type) {
case float64: case float64:
c = compareFloatWithVersion(d, v1) c = compareFloatWithVersion(d, inVersion)
case float32: case float32:
c = compareFloatWithVersion(float64(d), v1) c = compareFloatWithVersion(float64(d), inVersion)
case int: case int:
c = compareFloatWithVersion(float64(d), v1) c = compareFloatWithVersion(float64(d), inVersion)
case int32: case int32:
c = compareFloatWithVersion(float64(d), v1) c = compareFloatWithVersion(float64(d), inVersion)
case int64: case int64:
c = compareFloatWithVersion(float64(d), v1) c = compareFloatWithVersion(float64(d), inVersion)
case Version: case Version:
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel { if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel {
return strings.Compare(v1.Suffix, d.Suffix) return strings.Compare(inVersion.Suffix, d.Suffix)
} }
if d.Major > v1.Major { if d.Major > inVersion.Major {
return 1 return 1
} else if d.Major < v1.Major { } else if d.Major < inVersion.Major {
return -1 return -1
} }
if d.Minor > v1.Minor { if d.Minor > inVersion.Minor {
return 1 return 1
} else if d.Minor < v1.Minor { } else if d.Minor < inVersion.Minor {
return -1 return -1
} }
if d.PatchLevel > v1.PatchLevel { if d.PatchLevel > inVersion.PatchLevel {
return 1 return 1
} else if d.PatchLevel < v1.PatchLevel { } else if d.PatchLevel < inVersion.PatchLevel {
return -1 return -1
} }
default: default:
s, err := cast.ToStringE(v2) s, err := cast.ToStringE(in)
if err != nil { if err != nil {
return -1 return -1
} }
@@ -204,7 +234,7 @@ func CompareVersions(v1 Version, v2 any) int {
if err != nil { if err != nil {
return -1 return -1
} }
return v1.Compare(v) 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -13,13 +13,11 @@
package hugo package hugo
import "github.com/gohugoio/hugo/common/version"
// CurrentVersion represents the current build version. // CurrentVersion represents the current build version.
// This should be the only one. // This should be the only one.
var CurrentVersion = version.Version{ var CurrentVersion = Version{
Major: 0, Major: 0,
Minor: 153, Minor: 141,
PatchLevel: 0, PatchLevel: 0,
Suffix: "-DEV", 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package version package hugo
import ( import (
"testing" "testing"
@@ -50,31 +50,30 @@ func TestHugoVersion(t *testing.T) {
func TestCompareVersions(t *testing.T) { func TestCompareVersions(t *testing.T) {
c := qt.New(t) c := qt.New(t)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0) 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"), float32(0.20)), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(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.1"), 0.20), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), 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"), 3), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(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.1"), int64(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0) 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.1"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1) 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.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.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.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-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"), "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.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"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0) c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
} }
func TestParseHugoVersion(t *testing.T) { func TestParseHugoVersion(t *testing.T) {
c := qt.New(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").String(), qt.Equals, "0.25")
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2") c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test") c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
+2 -2
View File
@@ -37,7 +37,7 @@ func TestLogDistinct(t *testing.T) {
l := loggers.New(opts) l := loggers.New(opts)
for range 10 { for i := 0; i < 10; i++ {
l.Errorln("error 1") l.Errorln("error 1")
l.Errorln("error 2") l.Errorln("error 2")
l.Warnln("warn 1") l.Warnln("warn 1")
@@ -137,7 +137,7 @@ func TestReset(t *testing.T) {
l := loggers.New(opts) l := loggers.New(opts)
for range 3 { for i := 0; i < 3; i++ {
l.Errorln("error 1") l.Errorln("error 1")
l.Errorln("error 2") l.Errorln("error 2")
l.Errorln("error 1") l.Errorln("error 1")
+2 -10
View File
@@ -21,15 +21,7 @@ import (
"github.com/bep/logg" "github.com/bep/logg"
) )
// SetGlobalLogger sets the global logger. func InitGlobalLogger(level logg.Level, panicOnWarnings bool) {
// This is used in a few places in Hugo, e.g. deprecated functions.
func SetGlobalLogger(logger Logger) {
logMu.Lock()
defer logMu.Unlock()
log = logger
}
func initGlobalLogger(level logg.Level, panicOnWarnings bool) {
logMu.Lock() logMu.Lock()
defer logMu.Unlock() defer logMu.Unlock()
var logHookLast func(e *logg.Entry) error var logHookLast func(e *logg.Entry) error
@@ -58,5 +50,5 @@ func Log() Logger {
var log Logger var log Logger
func init() { func init() {
initGlobalLogger(logg.LevelWarn, false) InitGlobalLogger(logg.LevelWarn, false)
} }
+3 -45
View File
@@ -20,27 +20,13 @@ import (
// Cache is a simple thread safe cache backed by a map. // Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct { type Cache[K comparable, T any] struct {
m map[K]T m map[K]T
opts CacheOptions
hasBeenInitialized bool hasBeenInitialized bool
sync.RWMutex sync.RWMutex
} }
// CacheOptions are the options for the Cache. // NewCache creates a new Cache.
type CacheOptions struct {
// If set, the cache will not grow beyond this size.
Size uint64
}
var defaultCacheOptions = CacheOptions{}
// NewCache creates a new Cache with default options.
func NewCache[K comparable, T any]() *Cache[K, T] { func NewCache[K comparable, T any]() *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions} return &Cache[K, T]{m: make(map[K]T)}
}
// NewCacheWithOptions creates a new Cache with the given options.
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: opts}
} }
// Delete deletes the given key from the cache. // Delete deletes the given key from the cache.
@@ -79,19 +65,10 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
if err != nil { if err != nil {
return v, err return v, err
} }
c.clearIfNeeded()
c.m[key] = v c.m[key] = v
return v, nil return v, nil
} }
// Contains returns whether the given key exists in the cache.
func (c *Cache[K, T]) Contains(key K) bool {
c.RLock()
_, found := c.m[key]
c.RUnlock()
return found
}
// InitAndGet initializes the cache if not already done and returns the value for the given key. // InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain. // The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) { func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
@@ -131,26 +108,7 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock() c.Unlock()
} }
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
c.RLock()
if _, found := c.get(key); !found {
c.RUnlock()
c.Set(key, value)
} else {
c.RUnlock()
}
}
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) { func (c *Cache[K, T]) set(key K, value T) {
c.clearIfNeeded()
c.m[key] = value c.m[key] = value
} }
@@ -183,7 +141,7 @@ func (c *Cache[K, T]) Len() int {
func (c *Cache[K, T]) Reset() { func (c *Cache[K, T]) Reset() {
c.Lock() c.Lock()
clear(c.m) c.m = make(map[K]T)
c.hasBeenInitialized = false c.hasBeenInitialized = false
c.Unlock() c.Unlock()
} }
-57
View File
@@ -1,57 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func 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)
}
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"iter"
"sync"
)
func NewMap[K comparable, T any]() *Map[K, T] {
return &Map[K, T]{
m: make(map[K]T),
}
}
// Map is a thread safe map backed by a Go map.
type Map[K comparable, T any] struct {
m map[K]T
mu sync.RWMutex
}
// Get gets the value for the given key.
// It returns the zero value of T if the key is not found.
func (m *Map[K, T]) Get(key K) T {
v, _ := m.Lookup(key)
return v
}
// Lookup looks up the given key in the map.
// It returns the value and a boolean indicating whether the key was found.
func (m *Map[K, T]) Lookup(key K) (T, bool) {
m.mu.RLock()
v, found := m.m[key]
m.mu.RUnlock()
return v, found
}
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
v, found := m.Lookup(key)
if found {
return v, nil
}
m.mu.Lock()
defer m.mu.Unlock()
v, found = m.m[key]
if found {
return v, nil
}
v, err := create()
if err != nil {
return v, err
}
m.m[key] = v
return v, nil
}
// Set sets the given key to the given value.
func (m *Map[K, T]) Set(key K, value T) {
m.mu.Lock()
m.m[key] = value
m.mu.Unlock()
}
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) {
m.mu.Lock()
defer m.mu.Unlock()
f(m.m)
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
// It returns true if the value was set, false otherwise.
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
m.mu.RLock()
if _, found := m.m[key]; !found {
m.mu.RUnlock()
return m.doSetIfAbsent(key, value)
}
m.mu.RUnlock()
return false
}
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; !found {
m.m[key] = value
return true
}
return false
}
// All returns an iterator over all key/value pairs in the map.
// A read lock is held during the iteration.
func (m *Map[K, T]) All() iter.Seq2[K, T] {
return func(yield func(K, T) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for k, v := range m.m {
if !yield(k, v) {
return
}
}
}
}
-71
View File
@@ -1,71 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestMap(t *testing.T) {
c := qt.New(t)
m := NewMap[string, int]()
m.Set("b", 42)
v, found := m.Lookup("b")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 42)
v = m.Get("b")
c.Assert(v, qt.Equals, 42)
v, found = m.Lookup("c")
c.Assert(found, qt.Equals, false)
c.Assert(v, qt.Equals, 0)
v = m.Get("c")
c.Assert(v, qt.Equals, 0)
v, err := m.GetOrCreate("d", func() (int, error) {
return 100, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
v, found = m.Lookup("d")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 100)
v, err = m.GetOrCreate("d", func() (int, error) {
return 200, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
wasSet := m.SetIfAbsent("e", 300)
c.Assert(wasSet, qt.Equals, true)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
wasSet = m.SetIfAbsent("e", 400)
c.Assert(wasSet, qt.Equals, false)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
m.WithWriteLock(func(m map[string]int) {
m["f"] = 500
})
v, found = m.Lookup("f")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 500)
}
-2
View File
@@ -98,8 +98,6 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
return v, nil return v, nil
case Params: case Params:
return []map[string]any{v}, nil return []map[string]any{v}, nil
case map[string]any:
return []map[string]any{v}, nil
case []any: case []any:
var s []map[string]any var s []map[string]any
for _, entry := range v { for _, entry := range v {
-4
View File
@@ -73,14 +73,10 @@ func TestPrepareParams(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) {
// PrepareParams modifies input. // PrepareParams modifies input.
prepareClone := PrepareParamsClone(test.input)
PrepareParams(test.input) PrepareParams(test.input)
if !reflect.DeepEqual(test.expected, test.input) { if !reflect.DeepEqual(test.expected, test.input) {
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
} }
if !reflect.DeepEqual(test.expected, prepareClone) {
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, prepareClone)
}
}) })
} }
} }
-144
View File
@@ -1,144 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"slices"
"github.com/gohugoio/hugo/common/hashing"
)
// Ordered is a map that can be iterated in the order of insertion.
// Note that insertion order is not affected if a key is re-inserted into the map.
// In a nil map, all operations are no-ops.
// This is not thread safe.
type Ordered[K comparable, T any] struct {
// The keys in the order they were added.
keys []K
// The values.
values map[K]T
}
// NewOrdered creates a new Ordered map.
func NewOrdered[K comparable, T any]() *Ordered[K, T] {
return &Ordered[K, T]{values: make(map[K]T)}
}
// Set sets the value for the given key.
// Note that insertion order is not affected if a key is re-inserted into the map.
func (m *Ordered[K, T]) Set(key K, value T) {
if m == nil {
return
}
// Check if key already exists.
if _, found := m.values[key]; !found {
m.keys = append(m.keys, key)
}
m.values[key] = value
}
// Get gets the value for the given key.
func (m *Ordered[K, T]) Get(key K) (T, bool) {
if m == nil {
var v T
return v, false
}
value, found := m.values[key]
return value, found
}
// Has returns whether the given key exists in the map.
func (m *Ordered[K, T]) Has(key K) bool {
if m == nil {
return false
}
_, found := m.values[key]
return found
}
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {
return
}
delete(m.values, key)
for i, k := range m.keys {
if k == key {
m.keys = slices.Delete(m.keys, i, i+1)
break
}
}
}
// Clone creates a shallow copy of the map.
func (m *Ordered[K, T]) Clone() *Ordered[K, T] {
if m == nil {
return nil
}
clone := NewOrdered[K, T]()
for _, k := range m.keys {
clone.Set(k, m.values[k])
}
return clone
}
// Keys returns the keys in the order they were added.
func (m *Ordered[K, T]) Keys() []K {
if m == nil {
return nil
}
return m.keys
}
// Values returns the values in the order they were added.
func (m *Ordered[K, T]) Values() []T {
if m == nil {
return nil
}
var values []T
for _, k := range m.keys {
values = append(values, m.values[k])
}
return values
}
// Len returns the number of items in the map.
func (m *Ordered[K, T]) Len() int {
if m == nil {
return 0
}
return len(m.keys)
}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
// TODO(bep) replace with iter.Seq2 when we bump go Go 1.24.
func (m *Ordered[K, T]) Range(f func(key K, value T) bool) {
if m == nil {
return
}
for _, k := range m.keys {
if !f(k, m.values[k]) {
return
}
}
}
// Hash calculates a hash from the values.
func (m *Ordered[K, T]) Hash() (uint64, error) {
if m == nil {
return 0, nil
}
return hashing.Hash(m.values)
}
-99
View File
@@ -1,99 +0,0 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestOrdered(t *testing.T) {
c := qt.New(t)
m := NewOrdered[string, int]()
m.Set("a", 1)
m.Set("b", 2)
m.Set("c", 3)
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(m.Values(), qt.DeepEquals, []int{1, 2, 3})
v, found := m.Get("b")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 2)
m.Set("b", 22)
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(m.Values(), qt.DeepEquals, []int{1, 22, 3})
m.Delete("b")
c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "c"})
c.Assert(m.Values(), qt.DeepEquals, []int{1, 3})
}
func TestOrderedHash(t *testing.T) {
c := qt.New(t)
m := NewOrdered[string, int]()
m.Set("a", 1)
m.Set("b", 2)
m.Set("c", 3)
h1, err := m.Hash()
c.Assert(err, qt.IsNil)
m.Set("d", 4)
h2, err := m.Hash()
c.Assert(err, qt.IsNil)
c.Assert(h1, qt.Not(qt.Equals), h2)
m = NewOrdered[string, int]()
m.Set("b", 2)
m.Set("a", 1)
m.Set("c", 3)
h3, err := m.Hash()
c.Assert(err, qt.IsNil)
// Order does not matter.
c.Assert(h1, qt.Equals, h3)
}
func TestOrderedNil(t *testing.T) {
c := qt.New(t)
var m *Ordered[string, int]
m.Set("a", 1)
c.Assert(m.Keys(), qt.IsNil)
c.Assert(m.Values(), qt.IsNil)
v, found := m.Get("a")
c.Assert(found, qt.Equals, false)
c.Assert(v, qt.Equals, 0)
m.Delete("a")
var b bool
m.Range(func(k string, v int) bool {
b = true
return true
})
c.Assert(b, qt.Equals, false)
c.Assert(m.Len(), qt.Equals, 0)
c.Assert(m.Clone(), qt.IsNil)
h, err := m.Hash()
c.Assert(err, qt.IsNil)
c.Assert(h, qt.Equals, uint64(0))
}
-154
View File
@@ -1,154 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"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 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func 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
})
}
})
}
+5 -133
View File
@@ -14,7 +14,6 @@
package maps package maps
import ( import (
"errors"
"fmt" "fmt"
"strings" "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. // SetParams overwrites values in dst with values in src for common or new keys.
// This is done recursively. // This is done recursively.
func SetParams(dst, src Params) { 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 { for k, v := range src {
vv, found := dst[k] vv, found := dst[k]
if !found { if !found {
@@ -57,7 +48,7 @@ func setParams(dst, src Params, depth int) {
switch vvv := vv.(type) { switch vvv := vv.(type) {
case Params: case Params:
if pv, ok := v.(Params); ok { if pv, ok := v.(Params); ok {
setParams(vvv, pv, depth+1) SetParams(vvv, pv)
} else { } else {
dst[k] = v dst[k] = v
} }
@@ -110,6 +101,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow) noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
for k, v := range pp { for k, v := range pp {
if k == MergeStrategyKey { if k == MergeStrategyKey {
continue 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. // 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, // 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". // 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 // CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any. // map[string]any.
func CleanConfigStringMap(m map[string]any) 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 { if len(m) == 0 {
return m return m
} }
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
if _, found := m[MergeStrategyKey]; !found { if _, found := m[MergeStrategyKey]; !found {
return m return m
} }
@@ -346,9 +280,9 @@ func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
} }
switch v2 := v.(type) { switch v2 := v.(type) {
case map[string]any: case map[string]any:
m2[k] = doCleanConfigStringMap(v2, depth+1) m2[k] = CleanConfigStringMap(v2)
case Params: case Params:
var p Params = doCleanConfigStringMap(v2, depth+1) var p Params = CleanConfigStringMap(v2)
m2[k] = p m2[k] = p
case map[string]string: case map[string]string:
m2[k] = CleanConfigStringMapString(v2) m2[k] = CleanConfigStringMapString(v2)
@@ -369,7 +303,7 @@ func toMergeStrategy(v any) ParamsMergeStrategy {
} }
// PrepareParams // PrepareParams
// * makes all the keys in the given map lower cased and will do so recursively. // * makes all the keys in the given map lower cased and will do so
// * This will modify the map given. // * This will modify the map given.
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params. // * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
// * Any _merge value will be converted to proper type and value. // * Any _merge value will be converted to proper type and value.
@@ -409,65 +343,3 @@ 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)
for k, v := range m {
var retyped bool
lKey := strings.ToLower(k)
if lKey == MergeStrategyKey {
v = toMergeStrategy(v)
retyped = true
} else {
switch vv := v.(type) {
case map[any]any:
var p Params = cast.ToStringMap(v)
v = PrepareParamsClone(p)
retyped = true
case map[string]any:
var p Params = v.(map[string]any)
v = PrepareParamsClone(p)
retyped = true
case map[string]string:
p := make(Params)
for k, v := range vv {
p[k] = v
}
v = p
PrepareParams(p)
retyped = true
}
}
if retyped || k != lKey {
m2[lKey] = v
} else {
m2[k] = v
}
}
return m2
}
-32
View File
@@ -167,35 +167,3 @@ func TestParamsIsZero(t *testing.T) {
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse) c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue) 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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package hstore package maps
import ( import (
"reflect" "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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with 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 // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package hstore package maps
import ( import (
"reflect" "reflect"
@@ -140,7 +140,7 @@ func TestScratchInParallel(t *testing.T) {
for i := 1; i <= 10; i++ { for i := 1; i <= 10; i++ {
wg.Add(1) wg.Add(1)
go func(j int) { go func(j int) {
for k := range 10 { for k := 0; k < 10; k++ {
newVal := int64(k + j) newVal := int64(k + j)
_, err := scratch.Add(key, newVal) _, err := scratch.Add(key, newVal)
@@ -214,8 +214,8 @@ func TestScratchGetSortedMapValues(t *testing.T) {
func BenchmarkScratchGet(b *testing.B) { func BenchmarkScratchGet(b *testing.B) {
scratch := NewScratch() scratch := NewScratch()
scratch.Add("A", 1) scratch.Add("A", 1)
b.ResetTimer()
for b.Loop() { for i := 0; i < b.N; i++ {
scratch.Get("A") scratch.Get("A")
} }
} }
+4 -3
View File
@@ -42,7 +42,7 @@ func TestPara(t *testing.T) {
c.Run("Order", func(c *qt.C) { c.Run("Order", func(c *qt.C) {
n := 500 n := 500
ints := make([]int, n) ints := make([]int, n)
for i := range n { for i := 0; i < n; i++ {
ints[i] = i ints[i] = i
} }
@@ -51,7 +51,8 @@ func TestPara(t *testing.T) {
var result []int var result []int
var mu sync.Mutex var mu sync.Mutex
for i := range n { for i := 0; i < n; i++ {
i := i
r.Run(func() error { r.Run(func() error {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@@ -77,7 +78,7 @@ func TestPara(t *testing.T) {
var counter int64 var counter int64
for range n { for i := 0; i < n; i++ {
r.Run(func() error { r.Run(func() error {
atomic.AddInt64(&counter, 1) atomic.AddInt64(&counter, 1)
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
+2 -2
View File
@@ -115,8 +115,8 @@ func ReplaceExtension(path string, newExt string) string {
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories { for _, currentPath := range possibleDirectories {
if after, ok := strings.CutPrefix(inPath, currentPath); ok { if strings.HasPrefix(inPath, currentPath) {
return after, nil return strings.TrimPrefix(inPath, currentPath), nil
} }
} }
return inPath, errors.New("can't extract relative path, unknown prefix") 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. // This should not allocate any memory.
b.Run("All allowed", func(b *testing.B) { b.Run("All allowed", func(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
got := Sanitize(allAlowedPath) got := Sanitize(allAlowedPath)
if got != allAlowedPath { if got != allAlowedPath {
b.Fatal(got) b.Fatal(got)
@@ -235,7 +235,7 @@ func BenchmarkSanitize(b *testing.B) {
// This will allocate some memory. // This will allocate some memory.
b.Run("Spaces", func(b *testing.B) { b.Run("Spaces", func(b *testing.B) {
for b.Loop() { for i := 0; i < b.N; i++ {
got := Sanitize(spacePath) got := Sanitize(spacePath)
if got != "foo-bar" { if got != "foo-bar" {
b.Fatal(got) 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)) 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
}
}
}
+100 -389
View File
@@ -14,27 +14,18 @@
package paths package paths
import ( import (
"fmt"
"path" "path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
) )
const ( // PathParser parses a path into a Path.
identifierBaseof = "baseof"
identifierCurstomWrapper = "_"
)
// PathParser parses and manages paths.
type PathParser struct { type PathParser struct {
// Maps the language code to its index in the languages/sites slice. // Maps the language code to its index in the languages/sites slice.
LanguageIndex map[string]int LanguageIndex map[string]int
@@ -42,25 +33,8 @@ type PathParser struct {
// Reports whether the given language is disabled. // Reports whether the given language is disabled.
IsLangDisabled func(string) bool IsLangDisabled func(string) bool
// IsOutputFormat reports whether the given name is a valid output format.
// The second argument is optional.
IsOutputFormat func(name, ext string) bool
// Reports whether the given ext is a content file. // Reports whether the given ext is a content file.
IsContentExt func(string) bool IsContentExt func(string) bool
// The configured sites matrix.
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
pp.initOnce.Do(func() {
pp.sitesMatrixCache = maps.NewCache[string, sitesmatrix.VectorStore]()
})
} }
// NormalizePathString returns a normalized path string using the very basic Hugo rules. // NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -74,32 +48,6 @@ func NormalizePathStringBasic(s string) string {
return s 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. // ParseIdentity parses component c with path s into a StringIdentity.
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity { func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
p := pp.parsePooled(c, s) p := pp.parsePooled(c, s)
@@ -135,10 +83,13 @@ func (pp *PathParser) Parse(c, s string) *Path {
} }
func (pp *PathParser) newPath(component string) *Path { func (pp *PathParser) newPath(component string) *Path {
p := &Path{} return &Path{
p.reset() component: component,
p.component = component posContainerLow: -1,
return p posContainerHigh: -1,
posSectionHigh: -1,
posIdentifierLanguage: -1,
}
} }
func (pp *PathParser) parse(component, s string) (*Path, error) { func (pp *PathParser) parse(component, s string) (*Path, error) {
@@ -163,114 +114,10 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil return p, nil
} }
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) {
if p.posContainerHigh != -1 {
return
}
mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
mayHaveOutputFormat := component == files.ComponentFolderLayouts
mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat
var mayHaveLayout bool
if p.pathType == TypeShortcode {
mayHaveLayout = !isLast && component == files.ComponentFolderLayouts
} else {
mayHaveLayout = component == files.ComponentFolderLayouts
}
var found bool
var high int
if len(p.identifiersKnown) > 0 {
high = lastDot
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierCustom = len(p.identifiersKnown) - 1
found = true
}
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
found = true
// May also be the output format.
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
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 {
p.disabled = true
langFound = true
}
}
found = langFound
if langFound {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
// At this point we may already have resolved an output format,
// but we need to keep looking for a more specific one, e.g. amp before html.
// Use both name and extension to prevent
// false positives on the form css.html.
if pp.IsOutputFormat(sid, p.Ext()) {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveKind {
if kinds.GetKindMain(sid) != "" {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierKind = len(p.identifiersKnown) - 1
}
}
if !found && sid == identifierBaseof {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierBaseof = len(p.identifiersKnown) - 1
}
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
}
found = true
}
if !found {
p.identifiersUnknown = append(p.identifiersUnknown, id)
}
}
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
hasLang := pp.LanguageIndex != nil
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s)) s = path.Clean(filepath.ToSlash(s))
if s == "." { if s == "." {
@@ -293,26 +140,46 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p.s = s p.s = s
slashCount := 0 slashCount := 0
lastDot := 0
lastSlashIdx := strings.LastIndex(s, "/")
numDots := strings.Count(s[lastSlashIdx+1:], ".")
if strings.Contains(s, "/_shortcodes/") {
p.pathType = TypeShortcode
}
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {
c := s[i] c := s[i]
switch c { switch c {
case '.': case '.':
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false) if p.posContainerHigh == -1 {
lastDot = i var high int
if len(p.identifiers) > 0 {
high = p.identifiers[len(p.identifiers)-1].Low - 1
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
if len(p.identifiers) == 0 {
p.identifiers = append(p.identifiers, id)
} else if len(p.identifiers) == 1 {
// Check for a valid language.
s := p.s[id.Low:id.High]
if hasLang {
var disabled bool
_, langFound := pp.LanguageIndex[s]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
if disabled {
p.disabled = true
langFound = true
}
}
if langFound {
p.posIdentifierLanguage = 1
p.identifiers = append(p.identifiers, id)
}
}
}
}
case '/': case '/':
slashCount++ slashCount++
if p.posContainerHigh == -1 { if p.posContainerHigh == -1 {
if lastDot > 0 {
pp.parseIdentifier(component, s, p, i, lastDot, numDots, true)
}
p.posContainerHigh = i + 1 p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 { } else if p.posContainerLow == -1 {
p.posContainerLow = i + 1 p.posContainerLow = i + 1
@@ -323,52 +190,26 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
} }
} }
if len(p.identifiersKnown) > 0 { if len(p.identifiers) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext()) isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiersKnown[len(p.identifiersKnown)-1] id := p.identifiers[len(p.identifiers)-1]
b := p.s[p.posContainerHigh : id.Low-1]
if id.Low > p.posContainerHigh { if isContent {
b := p.s[p.posContainerHigh : id.Low-1] switch b {
if isContent { case "index":
switch b { p.bundleType = PathTypeLeaf
case "index": case "_index":
p.pathType = TypeLeaf p.bundleType = PathTypeBranch
case "_index": default:
p.pathType = TypeBranch p.bundleType = PathTypeContentSingle
default:
p.pathType = TypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.pathType = TypeContentData
} }
}
}
if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts { if slashCount == 2 && p.IsLeafBundle() {
if p.posIdentifierBaseof != -1 { p.posSectionHigh = 0
p.pathType = TypeBaseof
} else {
pth := p.Path()
if strings.Contains(pth, "/_shortcodes/") {
p.pathType = TypeShortcode
} else if strings.Contains(pth, "/_markup/") {
p.pathType = TypeMarkup
} else if strings.HasPrefix(pth, "/_partials/") {
p.pathType = TypePartial
} }
} } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
} p.bundleType = PathTypeContentData
if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 {
id := p.identifiersKnown[p.posIdentifierLayout]
if id.Low == p.posContainerHigh {
// First identifier is shortcode name.
p.posIdentifierLayout = -1
} }
} }
@@ -377,44 +218,35 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
func ModifyPathBundleTypeResource(p *Path) { func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() { if p.IsContent() {
p.pathType = TypeContentResource p.bundleType = PathTypeContentResource
} else { } else {
p.pathType = TypeFile p.bundleType = PathTypeFile
} }
} }
//go:generate stringer -type Type type PathType int
type Type int
const ( const (
// A generic resource, e.g. a JSON file.
// A generic file, e.g. a JSON file. PathTypeFile PathType = iota
TypeFile Type = iota
// All below are content files. // All below are content files.
// A resource of a content type with front matter. // A resource of a content type with front matter.
TypeContentResource PathTypeContentResource
// E.g. /blog/my-post.md // E.g. /blog/my-post.md
TypeContentSingle PathTypeContentSingle
// All below are bundled content files. // All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md // Leaf bundles, e.g. /blog/my-post/index.md
TypeLeaf PathTypeLeaf
// Branch bundles, e.g. /blog/_index.md // Branch bundles, e.g. /blog/_index.md
TypeBranch PathTypeBranch
// Content data file, _content.gotmpl. // Content data file, _content.gotmpl.
TypeContentData PathTypeContentData
// Layout types.
TypeMarkup
TypeShortcode
TypePartial
TypeBaseof
) )
type Path struct { type Path struct {
@@ -425,19 +257,13 @@ type Path struct {
posContainerHigh int posContainerHigh int
posSectionHigh int posSectionHigh int
component string component string
pathType Type bundleType PathType
identifiersKnown []types.LowHigh[string] identifiers []types.LowHigh[string]
identifiersUnknown []types.LowHigh[string]
posIdentifierLanguage int posIdentifierLanguage int
posIdentifierOutputFormat int disabled bool
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
posIdentifierCustom int
disabled bool
trimLeadingSlash bool trimLeadingSlash bool
@@ -467,14 +293,9 @@ func (p *Path) reset() {
p.posContainerHigh = -1 p.posContainerHigh = -1
p.posSectionHigh = -1 p.posSectionHigh = -1
p.component = "" p.component = ""
p.pathType = 0 p.bundleType = 0
p.identifiersKnown = p.identifiersKnown[:0] p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1 p.posIdentifierLanguage = -1
p.posIdentifierOutputFormat = -1
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.posIdentifierCustom = -1
p.disabled = false p.disabled = false
p.trimLeadingSlash = false p.trimLeadingSlash = false
p.unnormalized = nil p.unnormalized = nil
@@ -495,9 +316,6 @@ func (p *Path) norm(s string) string {
// IdentifierBase satisfies identity.Identity. // IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string { func (p *Path) IdentifierBase() string {
if p.Component() == files.ComponentFolderLayouts {
return p.Path()
}
return p.Base() return p.Base()
} }
@@ -514,13 +332,6 @@ func (p *Path) Container() string {
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1]) return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
} }
func (p *Path) String() string {
if p == nil {
return "<nil>"
}
return p.Path()
}
// ContainerDir returns the container directory for this path. // ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory. // For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string { func (p *Path) ContainerDir() string {
@@ -541,13 +352,13 @@ func (p *Path) Section() string {
// IsContent returns true if the path is a content file (e.g. mypost.md). // IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle. // Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool { func (p *Path) IsContent() bool {
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData return p.BundleType() >= PathTypeContentResource
} }
// isContentPage returns true if the path is a content file (e.g. mypost.md), // isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle. // but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool { func (p *Path) isContentPage() bool {
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData return p.BundleType() >= PathTypeContentSingle
} }
// Name returns the last element of path. // Name returns the last element of path.
@@ -561,7 +372,7 @@ func (p *Path) Name() string {
// Name returns the last element of path without any extension. // Name returns the last element of path without any extension.
func (p *Path) NameNoExt() string { func (p *Path) NameNoExt() string {
if i := p.identifierIndex(0); i != -1 { if i := p.identifierIndex(0); i != -1 {
return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1] return p.s[p.posContainerHigh : p.identifiers[i].Low-1]
} }
return p.s[p.posContainerHigh:] return p.s[p.posContainerHigh:]
} }
@@ -573,7 +384,7 @@ func (p *Path) NameNoLang() string {
return p.Name() return p.Name()
} }
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:] return p.s[p.posContainerHigh:p.identifiers[i].Low-1] + p.s[p.identifiers[i].High:]
} }
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension). // BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
@@ -587,26 +398,10 @@ func (p *Path) BaseNameNoIdentifier() string {
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension). // NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string { func (p *Path) NameNoIdentifier() string {
lowHigh := p.nameLowHigh() if len(p.identifiers) > 0 {
return p.s[lowHigh.Low:lowHigh.High] return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiersKnown) > 0 {
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
if p.posContainerHigh == lastID.Low {
// The last identifier is the name.
return lastID
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
}
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: len(p.s),
} }
return p.s[p.posContainerHigh:]
} }
// Dir returns all but the last element of path, typically the path's directory. // Dir returns all but the last element of path, typically the path's directory.
@@ -626,11 +421,6 @@ func (p *Path) Path() (d string) {
return p.norm(p.s) return p.norm(p.s)
} }
// PathNoLeadingSlash returns the full path without the leading slash.
func (p *Path) PathNoLeadingSlash() string {
return p.Path()[1:]
}
// Unnormalized returns the Path with the original case preserved. // Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path { func (p *Path) Unnormalized() *Path {
return p.unnormalized return p.unnormalized
@@ -638,9 +428,6 @@ func (p *Path) Unnormalized() *Path {
// PathNoLang returns the Path but with any language identifier removed. // PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string { func (p *Path) PathNoLang() string {
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
return p.Path()
}
return p.base(true, false) return p.base(true, false)
} }
@@ -649,28 +436,6 @@ func (p *Path) PathNoIdentifier() string {
return p.base(false, false) return p.base(false, false)
} }
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
if len(p.identifiersKnown) == 0 {
return p.norm(p.s)
}
i := p.identifierIndex(0)
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
i = j
}
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
i = j
}
if i == -1 {
return p.norm(p.s)
}
id := p.identifiersKnown[i]
return p.norm(p.s[:id.Low-1])
}
// PathRel returns the path relative to the given owner. // PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string { func (p *Path) PathRel(owner *Path) string {
ob := owner.Base() ob := owner.Base()
@@ -694,27 +459,7 @@ func (p *Path) BaseRel(owner *Path) string {
// //
// For other files (Resources), any extension is kept. // For other files (Resources), any extension is kept.
func (p *Path) Base() string { func (p *Path) Base() string {
s := p.base(!p.isContentPage(), p.IsBundle()) return p.base(!p.isContentPage(), p.IsBundle())
if s == "/" && p.isContentPage() {
// The content home page is represented as "".
s = ""
}
return s
}
// Used in template lookups.
// For pages with Type set, we treat that as the section.
func (p *Path) BaseReTyped(typ string) (d string) {
base := p.Base()
if typ == "" || p.Section() == typ {
return base
}
d = "/" + typ
if p.posSectionHigh != -1 {
d += base[p.posSectionHigh:]
}
d = p.norm(d)
return
} }
// BaseNoLeadingSlash returns the base path without the leading slash. // BaseNoLeadingSlash returns the base path without the leading slash.
@@ -723,21 +468,20 @@ func (p *Path) BaseNoLeadingSlash() string {
} }
func (p *Path) base(preserveExt, isBundle bool) string { func (p *Path) base(preserveExt, isBundle bool) string {
if len(p.identifiersKnown) == 0 { if len(p.identifiers) == 0 {
return p.norm(p.s) return p.norm(p.s)
} }
if preserveExt && len(p.identifiersKnown) == 1 { if preserveExt && len(p.identifiers) == 1 {
// Preserve extension. // Preserve extension.
return p.norm(p.s) return p.norm(p.s)
} }
var high int id := p.identifiers[len(p.identifiers)-1]
high := id.Low - 1
if isBundle { if isBundle {
high = p.posContainerHigh - 1 high = p.posContainerHigh - 1
} else {
high = p.nameLowHigh().High
} }
if high == 0 { if high == 0 {
@@ -749,7 +493,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
} }
// For txt files etc. we want to preserve the extension. // For txt files etc. we want to preserve the extension.
id := p.identifiersKnown[0] id = p.identifiers[0]
return p.norm(p.s[:high] + p.s[id.Low-1:id.High]) return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
} }
@@ -758,24 +502,8 @@ func (p *Path) Ext() string {
return p.identifierAsString(0) return p.identifierAsString(0)
} }
func (p *Path) OutputFormat() string {
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Layout() string {
return p.identifierAsString(p.posIdentifierLayout)
}
func (p *Path) Lang() string { func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage) return p.identifierAsString(1)
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
} }
func (p *Path) Identifier(i int) string { func (p *Path) Identifier(i int) string {
@@ -787,43 +515,35 @@ func (p *Path) Disabled() bool {
} }
func (p *Path) Identifiers() []string { func (p *Path) Identifiers() []string {
ids := make([]string, len(p.identifiersKnown)) ids := make([]string, len(p.identifiers))
for i, id := range p.identifiersKnown { for i, id := range p.identifiers {
ids[i] = p.s[id.Low:id.High] ids[i] = p.s[id.Low:id.High]
} }
return ids return ids
} }
func (p *Path) IdentifiersUnknown() []string { func (p *Path) BundleType() PathType {
ids := make([]string, len(p.identifiersUnknown)) return p.bundleType
for i, id := range p.identifiersUnknown {
ids[i] = p.s[id.Low:id.High]
}
return ids
}
func (p *Path) Type() Type {
return p.pathType
} }
func (p *Path) IsBundle() bool { func (p *Path) IsBundle() bool {
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData return p.bundleType >= PathTypeLeaf
} }
func (p *Path) IsBranchBundle() bool { func (p *Path) IsBranchBundle() bool {
return p.pathType == TypeBranch return p.bundleType == PathTypeBranch
} }
func (p *Path) IsLeafBundle() bool { func (p *Path) IsLeafBundle() bool {
return p.pathType == TypeLeaf return p.bundleType == PathTypeLeaf
} }
func (p *Path) IsContentData() bool { func (p *Path) IsContentData() bool {
return p.pathType == TypeContentData return p.bundleType == PathTypeContentData
} }
func (p Path) ForType(t Type) *Path { func (p Path) ForBundleType(t PathType) *Path {
p.pathType = t p.bundleType = t
return &p return &p
} }
@@ -833,12 +553,12 @@ func (p *Path) identifierAsString(i int) string {
return "" return ""
} }
id := p.identifiersKnown[i] id := p.identifiers[i]
return p.s[id.Low:id.High] return p.s[id.Low:id.High]
} }
func (p *Path) identifierIndex(i int) int { func (p *Path) identifierIndex(i int) int {
if i < 0 || i >= len(p.identifiersKnown) { if i < 0 || i >= len(p.identifiers) {
return -1 return -1
} }
return i return i
@@ -856,12 +576,3 @@ func HasExt(p string) bool {
} }
return false 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")
}
+16 -302
View File
@@ -18,33 +18,18 @@ import (
"testing" "testing"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
func newTestParser() *PathParser { var testParser = &PathParser{
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"}) LanguageIndex: map[string]int{
"no": 0,
return &PathParser{ "en": 1,
LanguageIndex: map[string]int{ },
"no": 0, IsContentExt: func(ext string) bool {
"en": 1, return ext == "md"
"fr": 2, },
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
ConfiguredDimensions: dims,
}
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
@@ -120,19 +105,17 @@ func TestParse(t *testing.T) {
"Basic Markdown file", "Basic Markdown file",
"/a/b/c.md", "/a/b/c.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Type(), qt.Equals, TypeContentSingle)
c.Assert(p.IsContent(), qt.IsTrue) c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse) c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md") c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c") c.Assert(p.Base(), qt.Equals, "/a/b/c")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
c.Assert(p.Section(), qt.Equals, "a") c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md") c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b") c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/b") c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
}, },
}, },
{ {
@@ -147,7 +130,7 @@ func TestParse(t *testing.T) {
// Reclassify it as a content resource. // Reclassify it as a content resource.
ModifyPathBundleTypeResource(p) ModifyPathBundleTypeResource(p)
c.Assert(p.Type(), qt.Equals, TypeContentResource) c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
c.Assert(p.IsContent(), qt.IsTrue) c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md") c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b.md") c.Assert(p.Base(), qt.Equals, "/a/b.md")
@@ -180,10 +163,8 @@ func TestParse(t *testing.T) {
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b") c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt") c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt") c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt") c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt") c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.Ext(), qt.Equals, "txt") c.Assert(p.Ext(), qt.Equals, "txt")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b") c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
@@ -193,11 +174,7 @@ func TestParse(t *testing.T) {
"Home branch cundle", "Home branch cundle",
"/_index.md", "/_index.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
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.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "") c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/") c.Assert(p.ContainerDir(), qt.Equals, "/")
@@ -208,14 +185,12 @@ func TestParse(t *testing.T) {
"/a/index.md", "/a/index.md",
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a") c.Assert(p.Base(), qt.Equals, "/a")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a") c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a") c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.ContainerDir(), qt.Equals, "") c.Assert(p.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a") c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md") c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"index"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsFalse) c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue) c.Assert(p.IsBundle(), qt.IsTrue)
@@ -233,7 +208,6 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) { func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b") c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b") c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
c.Assert(p.Container(), qt.Equals, "b") c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a") c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b") c.Assert(p.Dir(), qt.Equals, "/a/b")
@@ -246,7 +220,6 @@ func TestParse(t *testing.T) {
c.Assert(p.NameNoExt(), qt.Equals, "index.no") c.Assert(p.NameNoExt(), qt.Equals, "index.no")
c.Assert(p.NameNoIdentifier(), qt.Equals, "index") c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.md") c.Assert(p.NameNoLang(), qt.Equals, "index.md")
c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md") c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
c.Assert(p.Section(), qt.Equals, "a") c.Assert(p.Section(), qt.Equals, "a")
}, },
@@ -272,7 +245,7 @@ func TestParse(t *testing.T) {
"Index root no slash", "Index root no slash",
"_index.md", "_index.md",
func(c *qt.C, p *Path) { 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.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md") c.Assert(p.Name(), qt.Equals, "_index.md")
}, },
@@ -281,7 +254,7 @@ func TestParse(t *testing.T) {
"Index root", "Index root",
"/_index.md", "/_index.md",
func(c *qt.C, p *Path) { 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.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md") c.Assert(p.Name(), qt.Equals, "_index.md")
}, },
@@ -379,249 +352,10 @@ func TestParse(t *testing.T) {
c.Assert(p.IsContentData(), qt.IsFalse) 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 { for _, test := range tests {
c.Run(test.name, func(c *qt.C) { c.Run(test.name, func(c *qt.C) {
if test.name != "Caret up identifier" { test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
// return
}
test.assert(c, parser.Parse(files.ComponentFolderContent, test.path))
})
}
}
func TestParseLayouts(t *testing.T) {
c := qt.New(t)
tests := []struct {
name string
path string
assert func(c *qt.C, p *Path)
}{
{
"Basic",
"/list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Lang",
"/list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Kind",
"/section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/section.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout",
"/list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout multiple",
"/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.Base(), qt.Equals, "/mylayout.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout shortcode",
"/_shortcodes/myshort.list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
},
},
{
"Layout baseof",
"/baseof.list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
},
},
{
"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.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Base(), qt.Equals, "/list.html")
},
},
{
"Term",
"/term.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/term.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "term")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Shortcode with layout",
"/_shortcodes/myshortcode.list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/_shortcodes/myshortcode.html")
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list"})
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/_shortcodes/myshortcode")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/_shortcodes/myshortcode.list")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Sub dir",
"/pages/home.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "home")
c.Assert(p.OutputFormat(), qt.Equals, "html")
c.Assert(p.Dir(), qt.Equals, "/pages")
},
},
{
"Baseof",
"/pages/baseof.list.section.fr.amp.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Lang(), qt.Equals, "fr")
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Dir(), qt.Equals, "/pages")
c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
c.Assert(p.Type(), qt.Equals, TypeBaseof)
c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
},
},
{
"Markup",
"/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Markup nested",
"/foo/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Shortcode",
"/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested",
"/foo/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested sub",
"/foo/_shortcodes/foo/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Partials",
"/_partials/foo.bar",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypePartial)
},
},
{
"Shortcode lang in root",
"/_shortcodes/no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.NameNoIdentifier(), qt.Equals, "no")
},
},
{
"Shortcode lang layout",
"/_shortcodes/myshortcode.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Layout(), qt.Equals, "")
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
}
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
}) })
} }
} }
@@ -636,27 +370,7 @@ func TestHasExt(t *testing.T) {
} }
func BenchmarkParseIdentity(b *testing.B) { func BenchmarkParseIdentity(b *testing.B) {
parser := newTestParser() for i := 0; i < b.N; i++ {
for b.Loop() { testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
parser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
}
}
func TestSitesMatrixFromPath(t *testing.T) {
c := qt.New(t)
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
v := parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.LenVectors(), qt.Equals, 1)
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 0})
}
func BenchmarkSitesMatrixFromPath(b *testing.B) {
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
for b.Loop() {
parser.SitesMatrixFromPath(p)
} }
} }
+4 -27
View File
@@ -35,9 +35,9 @@ weight = 2
removePathAccents = true removePathAccents = true
-- content/διακριτικός.md -- -- content/διακριτικός.md --
-- content/διακριτικός.fr.md -- -- content/διακριτικός.fr.md --
-- layouts/single.html -- -- layouts/_default/single.html --
{{ .Language.Lang }}|Single. {{ .Language.Lang }}|Single.
-- layouts/list.html -- -- layouts/_default/list.html --
List List
` `
b := hugolib.Test(t, files) b := hugolib.Test(t, files)
@@ -64,9 +64,9 @@ disablePathToLower = true
-- content/MySection/MyPage.fr.md -- -- content/MySection/MyPage.fr.md --
-- content/MySection/MyBundle/index.md -- -- content/MySection/MyBundle/index.md --
-- content/MySection/MyBundle/index.fr.md -- -- content/MySection/MyBundle/index.fr.md --
-- layouts/single.html -- -- layouts/_default/single.html --
{{ .Language.Lang }}|Single. {{ .Language.Lang }}|Single.
-- layouts/list.html -- -- layouts/_default/list.html --
{{ .Language.Lang }}|List. {{ .Language.Lang }}|List.
` `
b := hugolib.Test(t, files) b := hugolib.Test(t, files)
@@ -78,26 +78,3 @@ disablePathToLower = true
b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single") b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single")
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single") b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
} }
func TestIssue13596(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/p1/index.md --
---
title: p1
---
-- content/p1/a.1.txt --
-- content/p1/a.2.txt --
-- layouts/all.html --
{{ range .Resources.Match "*" }}{{ .Name }}|{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "a.1.txt|a.2.txt|")
b.AssertFileExists("public/p1/a.1.txt", true)
b.AssertFileExists("public/p1/a.2.txt", true) // fails
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[PathTypeFile-0]
_ = x[PathTypeContentResource-1]
_ = x[PathTypeContentSingle-2]
_ = x[PathTypeLeaf-3]
_ = x[PathTypeBranch-4]
}
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
func (i PathType) String() string {
if i < 0 || i >= PathType(len(_PathType_index)-1) {
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
}
-32
View File
@@ -1,32 +0,0 @@
// Code generated by "stringer -type Type"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TypeFile-0]
_ = x[TypeContentResource-1]
_ = x[TypeContentSingle-2]
_ = x[TypeLeaf-3]
_ = x[TypeBranch-4]
_ = x[TypeContentData-5]
_ = x[TypeMarkup-6]
_ = x[TypeShortcode-7]
_ = x[TypePartial-8]
_ = x[TypeBaseof-9]
}
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
func (i Type) String() string {
if i < 0 || i >= Type(len(_Type_index)-1) {
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Type_name[_Type_index[i]:_Type_index[i+1]]
}
+1 -1
View File
@@ -228,7 +228,7 @@ func UrlFromFilename(filename string) (*url.URL, error) {
}, nil }, nil
} }
// UrlStringToFilename converts the URL s to a filename. // UrlToFilename converts the URL s to a filename.
// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned. // If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
func UrlStringToFilename(s string) (string, bool) { func UrlStringToFilename(s string) (string, bool) {
u, err := url.ParseRequestURI(s) u, err := url.ParseRequestURI(s)
+25 -139
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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@@ -13,128 +13,52 @@
package predicate 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. // P is a predicate function that tests whether a value of type T satisfies some condition.
type P[T any] func(T) bool type P[T any] func(T) bool
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates. // And returns a predicate that is a short-circuiting logical AND 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]) And(ps ...P[T]) P[T] {
func (p P[T]) Or(ps ...P[T]) P[T] {
return func(v T) bool { 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 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 { for _, pp := range ps {
if pp(v) { if pp(v) {
return true 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 { if p == nil {
return false 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. // Negate returns a predicate that is a logical negation of this predicate.
func (p PR[T]) And(ps ...PR[T]) PR[T] { func (p P[T]) Negate() P[T] {
return func(v T) Match { return func(v T) bool {
if p != nil { return !p(v)
m := p(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(true)
} }
} }
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
func (p PR[T]) Or(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(false)
}
}
func shouldBreak(m Match) bool {
_, ok := m.(breakMatch)
return ok
}
// Filter returns a new slice holding only the elements of s that satisfy p. // Filter 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. // 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 var n int
for _, v := range s { for _, v := range s {
if p(v).OK() { if p(v) {
s[n] = v s[n] = v
n++ n++
} }
@@ -143,50 +67,12 @@ func (p PR[T]) Filter(s []T) []T {
} }
// FilterCopy returns a new slice holding only the elements of s that satisfy p. // 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 var result []T
for _, v := range s { for _, v := range s {
if p(v).OK() { if p(v) {
result = append(result, v) result = append(result, v)
} }
} }
return result return result
} }
// 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
}
type IndexMatcher interface {
IndexMatch(match P[string]) (iter.Seq[int], error)
}
+28 -126
View File
@@ -17,66 +17,38 @@ import (
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/predicate" "github.com/gohugoio/hugo/common/predicate"
) )
func TestPredicate(t *testing.T) { func TestAdd(t *testing.T) {
c := qt.New(t) c := qt.New(t)
n := func() predicate.PR[int] { var p predicate.P[int] = intP1
var pr predicate.PR[int]
return pr
}
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(1), qt.IsTrue)
c.Assert(p(10), qt.IsTrue) c.Assert(p(2), qt.IsFalse)
c.Assert(p(4), 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) { func TestFilter(t *testing.T) {
c := qt.New(t) c := qt.New(t)
var p predicate.PR[int] var p predicate.P[int] = intP1
p = p.Or(intP1).Or(intP2) p = p.Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2} 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) { func TestFilterCopy(t *testing.T) {
c := qt.New(t) c := qt.New(t)
var p predicate.PR[int] var p predicate.P[int] = intP1
p = p.Or(intP1).Or(intP2) p = p.Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2} ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -96,86 +68,16 @@ func TestFilterCopy(t *testing.T) {
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2}) 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 { 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 { if i == 10 {
return predicate.True return true
} }
return predicate.BoolMatch(i == 2) return 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 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()
}
})
} }
+1 -1
View File
@@ -51,7 +51,7 @@ func Run[T any](ctx context.Context, cfg Config[T]) Group[T] {
// Buffered for performance. // Buffered for performance.
ch := make(chan T, cfg.NumWorkers) ch := make(chan T, cfg.NumWorkers)
for range cfg.NumWorkers { for i := 0; i < cfg.NumWorkers; i++ {
g.Go(func() error { g.Go(func() error {
for { for {
select { select {
+4 -1
View File
@@ -103,7 +103,10 @@ func (r *RunEvery) Add(name string, f Func) {
f.IntervalHigh = 20 * time.Second f.IntervalHigh = 20 * time.Second
} }
start := max(f.IntervalHigh/3, f.IntervalLow) start := f.IntervalHigh / 3
if start < f.IntervalLow {
start = f.IntervalLow
}
f.interval = start f.interval = start
f.last = time.Now() f.last = time.Now()

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