mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
369 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8c0dfccf7 | |||
| 6bd2bde9d3 | |||
| 0f1c7d1200 | |||
| 10352335e0 | |||
| f23576f2fb | |||
| b5d43cdc17 | |||
| 6ef8017f60 | |||
| d67925f5a1 | |||
| 48dd4f469a | |||
| b1e1eede50 | |||
| f522a7282b | |||
| 6ac72dd3bf | |||
| 74c0bbfc6a | |||
| e66a33d369 | |||
| 8a979d54d1 | |||
| fd49df8f7a | |||
| c7b35c8704 | |||
| 5916b61be9 | |||
| 7eafef2269 | |||
| 00c4228f61 | |||
| 5f5b2f378f | |||
| b3ea2a5fab | |||
| 67b543549b | |||
| ef7a22a2ff | |||
| 8dfcece805 | |||
| 9a6bfe2667 | |||
| 7d45614c6d | |||
| 7f12c0900d | |||
| dab2c332dc | |||
| 5a64551a07 | |||
| 0fc63fbf57 | |||
| 576d55d009 | |||
| b9400b8a95 | |||
| ce0c7f42c1 | |||
| f472dd48a7 | |||
| 50973e7272 | |||
| 48566b6f37 | |||
| b441472b69 | |||
| ac2c3fb20f | |||
| ee91c707ee | |||
| 11f7f39913 | |||
| 192e3c453e | |||
| e569dd59a0 | |||
| 5ba03bf657 | |||
| c1b2e58bfb | |||
| 77214117ae | |||
| 1a94731669 | |||
| 18784711b5 | |||
| 2c611091fd | |||
| 8e9e04e395 | |||
| 32334d098e | |||
| 8e2e60dd45 | |||
| 2bc54df622 | |||
| 608ed09a0a | |||
| 94f1ede3aa | |||
| ab374e3d22 | |||
| 1a9133075c | |||
| d36a8f5601 | |||
| 487b8e3af3 | |||
| a6f99cca22 | |||
| a775488304 | |||
| 66ba63cd5f | |||
| eb06a3cd0c | |||
| a798e3e724 | |||
| fbcf0d52a1 | |||
| a2b2a5a978 | |||
| ec178ea463 | |||
| 2d80b8a741 | |||
| 5ea3e13db6 | |||
| b1c1bd019f | |||
| 866b8e5aa6 | |||
| 409669fa48 | |||
| f66d094446 | |||
| e9b9b36f46 | |||
| b8a2c10d06 | |||
| 601df38834 | |||
| 3bed212a2f | |||
| e2fd6764be | |||
| 8f3527f61c | |||
| 09048aad76 | |||
| f42c422a12 | |||
| 0b71db299a | |||
| 5c7fad23a3 | |||
| 700bb78198 | |||
| d3b5d47a43 | |||
| 86cd1838ff | |||
| 871da3370a | |||
| 2637aa1540 | |||
| bf1d20d735 | |||
| 1deec99bba | |||
| b7bb557c6a | |||
| 7c19c196c3 | |||
| 555443b64a | |||
| 1f2de189ed | |||
| c6ae33c61d | |||
| edeebf0d33 | |||
| ea9675f6e7 | |||
| b1f7e35a98 | |||
| a0d4e1fb26 | |||
| 96777d9b89 | |||
| 25126e5f87 | |||
| 179034abbb | |||
| 96ac146eef | |||
| d4c0e4452c | |||
| 418156ec3e | |||
| 798533a201 | |||
| a8c5d0d00d | |||
| a94a941fe3 | |||
| 385d1a1342 | |||
| d30bd50467 | |||
| 8e6cac8462 | |||
| 4085ee9340 | |||
| 168bf17e70 | |||
| 80ea90c880 | |||
| f740d7cfe0 | |||
| 5f46da6e2a | |||
| fe64a68225 | |||
| b4128babb8 | |||
| 790a5b7014 | |||
| 70ce2446b5 | |||
| 08e1ea5c70 | |||
| 65d43e1d0d | |||
| 65a76664e3 | |||
| da5b1fcf8f | |||
| 39649dce49 | |||
| 614fb16ec6 | |||
| 0637adb239 | |||
| 0bf61353f6 | |||
| b82e496c13 | |||
| 33542d3966 | |||
| 588d20b315 | |||
| b33730293d | |||
| e6a3f1d62b | |||
| 573ecb530f | |||
| 9937a5dcaa | |||
| 1b4514e020 | |||
| 429e57201d | |||
| fa7d37f0c8 | |||
| fae49a22f1 | |||
| 364d237b51 | |||
| 079f3eb8d1 | |||
| 4e6f2b6791 | |||
| dff5e168a9 | |||
| c82a035bf1 | |||
| 86b01eda10 | |||
| e2e64aeec5 | |||
| 9e7182e9e6 | |||
| cf3ad62c3b | |||
| c1cc65fcb9 | |||
| 663075920a | |||
| 596517a259 | |||
| bf42138661 | |||
| 57fecb6b71 | |||
| 6d0ab6a34e | |||
| 6be463b586 | |||
| 9e24b5645d | |||
| 45b67f6c18 | |||
| 5e649eb4d0 | |||
| 0de8f8607b | |||
| 3073fd5ccf | |||
| b9b304a126 | |||
| 8be30ef2cb | |||
| 565a10cbf0 | |||
| 438f113f83 | |||
| dab54058ea | |||
| 1a1b062a01 | |||
| 7a43b928a6 | |||
| 555dfa207a | |||
| 3d21b0687b | |||
| 34b0c15a54 | |||
| 9dc38e917c | |||
| b592d34efe | |||
| 18a2afa519 | |||
| 0b0c827d5b | |||
| 30dc75e620 | |||
| 6bc8e6f491 | |||
| cda4d75187 | |||
| 84950ed2b2 | |||
| 2b337cd212 | |||
| cc61d00df9 | |||
| 8ef2c61985 | |||
| a8ed613742 | |||
| b91688034f | |||
| b971b7f911 | |||
| 24210dcad9 | |||
| b037b9300c | |||
| 268046732e | |||
| 8ce80f755d | |||
| 4baa95ba65 | |||
| 0f94274a28 | |||
| 5f9c3a3e4c | |||
| 51d77a6ca8 | |||
| d25b619ce2 | |||
| b81861fd21 | |||
| 15cce89e0e | |||
| 187e683bb2 | |||
| 432078a6af | |||
| 4a76d8c611 | |||
| d837eff08f | |||
| 323e306bd7 | |||
| 2047638a2c | |||
| 38efb702aa | |||
| bdf8b1a915 | |||
| f33c1a3fdb | |||
| fee095768c | |||
| 56d7925a97 | |||
| 38608c7c14 | |||
| d75f0dc6a5 | |||
| 579362efd4 | |||
| c6b6910a79 | |||
| ca4025405c | |||
| 44b5f13f86 | |||
| 5af311284a | |||
| 9d80c3978f | |||
| 62c47407b2 | |||
| 2c6574ebdb | |||
| 60c4245e67 | |||
| 5e6b269c41 | |||
| 94a6233aab | |||
| 26f31ff6ce | |||
| bca171b691 | |||
| 9289aa43aa | |||
| 606415ec18 | |||
| dc2f6ae27a | |||
| a0944ac7a4 | |||
| 22d0c17e82 | |||
| 25c7c18f9f | |||
| 0e8f88fa72 | |||
| a2469d504e | |||
| 0efcb24c37 | |||
| e1236e3d0d | |||
| e24b604c30 | |||
| 4c7a78f5ca | |||
| 91eac9e573 | |||
| 04650ce778 | |||
| 264022a75a | |||
| ff0f67ea7f | |||
| 2c80dee358 | |||
| e9bda21ce9 | |||
| 3893e70510 | |||
| b95f7d2552 | |||
| 6abdacad3f | |||
| 1c8c21e45e | |||
| 809ebe01fa | |||
| 08a0679a89 | |||
| 524b98665b | |||
| 5869cbddd8 | |||
| e08278d165 | |||
| df4f80d54f | |||
| d4c78885ae | |||
| 29e2c2fa92 | |||
| 0579afc3c5 | |||
| 0ccbc63ab8 | |||
| 06bc2c16c0 | |||
| d51adca26a | |||
| a1307700dd | |||
| 9425b939c6 | |||
| bd50c9c7e7 | |||
| a8e0ca9254 | |||
| 559a029de7 | |||
| 184b10ed45 | |||
| 5bad0d523e | |||
| a3d9548469 | |||
| 9e344bbe49 | |||
| 1ec0e2ebec | |||
| a3574f6f70 | |||
| 989454a52d | |||
| 1e91e46520 | |||
| 8a57d0f15f | |||
| b76c50ac18 | |||
| 1cdd17882c | |||
| e2fb0b0e80 | |||
| 88aea56683 | |||
| f4c11571bb | |||
| 54075acc29 | |||
| 8b52303e31 | |||
| 3d45d30a41 | |||
| 095157cd65 | |||
| 29cf87444f | |||
| 1b4dd436dd | |||
| 4414ef73f3 | |||
| a133393eda | |||
| 9197debbfe | |||
| c29897fac0 | |||
| c70ab27ceb | |||
| 510d98b778 | |||
| 7fd6762c16 | |||
| 584f052f39 | |||
| b76d71761e | |||
| c5dca3bdee | |||
| ec463c0977 | |||
| 4d2743e4c8 | |||
| c20f70d9a3 | |||
| 1b556216a8 | |||
| 106c8e6de6 | |||
| 03b33ecb5e | |||
| 9928122eb8 | |||
| 105d3bc32f | |||
| 3e46ba5ce2 | |||
| 4d1303512b | |||
| 9943c1bef0 | |||
| 766757310a | |||
| d71c07cf3c | |||
| b462980ac2 | |||
| 47678d8cbd | |||
| 18b9b6488b | |||
| ce44a8e835 | |||
| 64f40731f1 | |||
| 1140314be1 | |||
| 404fd9e512 | |||
| b1b0cdee3a | |||
| 3eea082903 | |||
| 0f18cbe990 | |||
| 3f5473b7d4 | |||
| d1f6a1dc59 | |||
| 747cf4ad65 | |||
| d8774d7fc3 | |||
| 3b8947d821 | |||
| 321a66ef19 | |||
| 57a784e027 | |||
| 25c0f2408a | |||
| 4f2d2b2cc4 | |||
| b8eb45c9df | |||
| 1d90afff1b | |||
| e751afa9bd | |||
| a09b8a60eb | |||
| 0071b47b8b | |||
| 70d62993ee | |||
| 66240338f1 | |||
| 84b5123912 | |||
| 2912415955 | |||
| bb4e66cd7c | |||
| 45ec2f88bb | |||
| 1ba80874e4 | |||
| 84dd495f2b | |||
| 327bbc613f | |||
| 2447138f1d | |||
| 61ec7a20a5 | |||
| c289fcaaaa | |||
| cfc38ecfed | |||
| 22e579e050 | |||
| b886615d1b | |||
| ecdef2be70 | |||
| ff3ae62933 | |||
| bfa7453792 | |||
| 12ace3ad5c | |||
| c14fdddada | |||
| 186934feb4 | |||
| 13b43e6117 | |||
| bff4dddb12 | |||
| 885cd299b7 | |||
| 80e973ea5c | |||
| debf3c559a | |||
| 01b0eda96c | |||
| 87e100e61f | |||
| 1649f3126f | |||
| ccd6a4b71e | |||
| 6dc1a1752b | |||
| 806d4848c3 | |||
| 348aae91e8 | |||
| 61482cfab6 | |||
| 04ee1b9784 | |||
| 7a86fe9905 | |||
| 5fdcc09062 | |||
| f5245a7d5f | |||
| 5029676aca | |||
| 2216028620 | |||
| ecc3dd1f53 | |||
| de4a7f1e04 |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22400.20000
|
||||
- image: bepsays/ci-hugoreleaser:1.22500.20400
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20400
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: false
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: true
|
||||
summary: false
|
||||
code_review: false
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
@@ -0,0 +1,10 @@
|
||||
🛑 Before submitting this PR
|
||||
|
||||
All PRs must be linked to an Issue in this repository, and that Issue must be approved by a project maintainer:
|
||||
|
||||
- For a bug, approval means that the "needs triage" label is removed.
|
||||
- For a proposal, approval means that the "proposal" label is replaced by an "enhancement" label.
|
||||
|
||||
PRs without a linked, approved Issue will be closed immediately.
|
||||
|
||||
Please review the contribution guide (CONTRIBUTING.md) to understand commit message guidelines and AI assistance disclosures.
|
||||
@@ -0,0 +1,22 @@
|
||||
name: AI Detection
|
||||
on:
|
||||
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
detect-ai:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: AI Watchdog
|
||||
uses: bep/ai-watchdog@995475dd81767f8035dc2c943e9230918d06caca # temporary fork (probably)
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
pr-label: AI Suspect
|
||||
fail-when-confident: false
|
||||
# CSV list.
|
||||
pr-user-logins-skip: dependabot[bot],bep,jmooring
|
||||
@@ -16,20 +16,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to GHCR
|
||||
# Login is only needed when the image is pushed
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
provenance: mode=max
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 365
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
stale-pr-message: This PR has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
|
||||
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell us in a comment.
|
||||
|
||||
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
stale-issue-label: 'Stale'
|
||||
|
||||
+19
-17
@@ -16,7 +16,7 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.23.x, 1.24.x]
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -24,9 +24,7 @@ jobs:
|
||||
name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
@@ -34,9 +32,9 @@ jobs:
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
@@ -45,18 +43,21 @@ jobs:
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
|
||||
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
|
||||
with:
|
||||
ruby-version: "2.7"
|
||||
bundler-cache: true #
|
||||
ruby-version: "3.4.5"
|
||||
- name: Install Ruby gems
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
@@ -105,7 +106,10 @@ jobs:
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Run staticcheck
|
||||
run: staticcheck ./...
|
||||
run: |
|
||||
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
|
||||
staticcheck ./...
|
||||
rm -rf ${{ runner.temp }}/staticcheck
|
||||
- if: matrix.os != 'windows-latest'
|
||||
name: Check
|
||||
run: |
|
||||
@@ -117,16 +121,14 @@ jobs:
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test;
|
||||
mage -v test
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended,withdeploy
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
go clean -i -cache
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
|
||||
+2
-1
@@ -3,4 +3,5 @@
|
||||
imports.*
|
||||
dist/
|
||||
public/
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
cache/filecache/_gen/
|
||||
@@ -0,0 +1 @@
|
||||
tpl/tplimpl/embedded/templates/**
|
||||
@@ -0,0 +1,14 @@
|
||||
* Brevity is good.
|
||||
* Assume that the maintainers and readers of the code you write are Go experts:
|
||||
* Don't use comments to explain the obvious.
|
||||
* Use self-explanatory variable and function names.
|
||||
* Use short variable names when the context is clear.
|
||||
* If you need to add temporary debug printing, use `hdebug.Printf`.[^1]
|
||||
* Never export symbols that's not needed outside of the package.
|
||||
* Avoid global state at (almost) all cost.
|
||||
* This is a project with a long history; assume that a similiar problem has been solved before, look hard for helper functions before creating new ones.
|
||||
* Use `./check.sh ./somepackage/...` when iterating.
|
||||
* Use `./check.sh` when you're done.
|
||||
|
||||
|
||||
[^1]: CI build fail if you forget to remove the debug printing.
|
||||
+13
-2
@@ -19,6 +19,7 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
|
||||
* [Reporting Issues](#reporting-issues)
|
||||
* [Submitting Patches](#submitting-patches)
|
||||
* [Code Contribution Guidelines](#code-contribution-guidelines)
|
||||
* [AI Assistance Notice](#ai-assistance-notice)
|
||||
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
|
||||
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
|
||||
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
|
||||
@@ -54,7 +55,7 @@ If it is of some complexity, the contributor is expected to maintain and support
|
||||
|
||||
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
|
||||
|
||||
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
|
||||
Note that we do not accept new features that require [CGO](https://go.dev/wiki/cgo).
|
||||
We have one exception to this rule which is LibSASS.
|
||||
|
||||
**Bug fixes are, of course, always welcome.**
|
||||
@@ -76,9 +77,19 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Ensure that `./check.sh` succeeds. Note that some tests are skipped when running locally, some because they are slow. To run these locally, do `CI_LOCAL=true ./check.sh ./somepackage/...`.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
## AI Assistance Notice
|
||||
|
||||
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
> This PR was written primarily by Claude Code.
|
||||
|
||||
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
|
||||
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
ARG GO_VERSION="1.24"
|
||||
ARG GO_VERSION="1.25"
|
||||
ARG ALPINE_VERSION="3.22"
|
||||
ARG DART_SASS_VERSION="1.79.3"
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Use Hugo's embedded web server during development to instantly see changes to co
|
||||
|
||||
Hugo's fast asset pipelines include:
|
||||
|
||||
- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
|
||||
- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract metadata
|
||||
- JavaScript bundling – Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
|
||||
- Sass processing – Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
|
||||
- Tailwind CSS processing – Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
|
||||
@@ -65,30 +65,24 @@ See the [features] section of the documentation for a comprehensive summary of H
|
||||
|
||||
<p> </p>
|
||||
<p float="left">
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
|
||||
<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://pinme.eth.limo/?s=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/logo-pinme.svg" width="200" alt="PinMe."></a>
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/cloudcannon-cms-logo.svg" width="200" alt="CloudCannon"></a>
|
||||
</p>
|
||||
|
||||
## Editions
|
||||
|
||||
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
|
||||
Hugo offers a standard edition with core features, plus extended and extended/deploy editions with more. Use the standard edition unless you need the features below.
|
||||
|
||||
Feature|extended edition|extended/deploy edition
|
||||
:--|:-:|:-:
|
||||
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
[Transpile Sass to CSS] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass] transpiler instead, which is compatible with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See [details].|:x:|:heavy_check_mark:
|
||||
|
||||
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
|
||||
[processing images]: https://gohugo.io/content-management/image-processing/
|
||||
[transpile sass to css]: https://gohugo.io/functions/css/sass/
|
||||
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
|
||||
|
||||
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
|
||||
|
||||
## Installation
|
||||
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
@@ -102,9 +96,9 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
|
||||
|
||||
Prerequisites to build Hugo from source:
|
||||
|
||||
- Standard edition: Go 1.23.0 or later
|
||||
- Extended edition: Go 1.23.0 or later, and GCC
|
||||
- Extended/deploy edition: Go 1.23.0 or later, and GCC
|
||||
- Standard edition: Go 1.24.0 or later
|
||||
- Extended edition: Go 1.24.0 or later, and GCC
|
||||
- Extended/deploy edition: Go 1.24.0 or later, and GCC
|
||||
|
||||
Build the standard edition:
|
||||
|
||||
@@ -162,6 +156,16 @@ If there is sufficient interest, [create a proposal]. Do not submit a pull reque
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
For the Hugo source code, see [LICENSE](/LICENSE).
|
||||
|
||||
We also bundle some libraries in binary/WASM form:
|
||||
|
||||
* [libwebp](https://github.com/webmproject/libwebp), [BSD-3-Clause license](https://github.com/webmproject/libwebp?tab=BSD-3-Clause-1-ov-file#readme)
|
||||
* [Katex](https://github.com/KaTeX/KaTeX), [MIT license](https://github.com/KaTeX/KaTeX?tab=MIT-1-ov-file#readme)
|
||||
* [QuickJS](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme), [License](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme)
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
|
||||
@@ -171,51 +175,55 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
|
||||
|
||||
```text
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/PuerkitoBio/goquery="v1.10.1"
|
||||
github.com/alecthomas/chroma/v2="v2.15.0"
|
||||
github.com/andybalholm/cascadia="v1.3.3"
|
||||
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
|
||||
github.com/JohannesKaufmann/dom="v0.2.0"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.21.1"
|
||||
github.com/aymerick/douceur="v0.2.0"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.6.0"
|
||||
github.com/bep/gitmap="v1.9.0"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.3.2"
|
||||
github.com/bep/godartsass/v2="v2.5.0"
|
||||
github.com/bep/golibsass="v1.2.0"
|
||||
github.com/bep/gowebp="v0.3.0"
|
||||
github.com/bep/imagemeta="v0.8.4"
|
||||
github.com/bep/lazycache="v0.7.0"
|
||||
github.com/bep/goportabletext="v0.1.0"
|
||||
github.com/bep/helpers="v0.6.0"
|
||||
github.com/bep/imagemeta="v0.12.0"
|
||||
github.com/bep/lazycache="v0.8.0"
|
||||
github.com/bep/logg="v0.4.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.9.2"
|
||||
github.com/bep/simplecobra="v0.5.0"
|
||||
github.com/bep/overlayfs="v0.10.0"
|
||||
github.com/bep/simplecobra="v0.6.1"
|
||||
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/bits-and-blooms/bitset="v1.24.4"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.4"
|
||||
github.com/clipperhouse/displaywidth="v0.6.0"
|
||||
github.com/clipperhouse/stringish="v0.1.1"
|
||||
github.com/clipperhouse/uax29/v2="v2.3.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.6"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.11.5"
|
||||
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
|
||||
github.com/evanw/esbuild="v0.24.2"
|
||||
github.com/evanw/esbuild="v0.27.2"
|
||||
github.com/fatih/color="v1.18.0"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.8.0"
|
||||
github.com/getkin/kin-openapi="v0.129.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/fsnotify/fsnotify="v1.9.0"
|
||||
github.com/getkin/kin-openapi="v0.133.0"
|
||||
github.com/go-openapi/jsonpointer="v0.21.0"
|
||||
github.com/go-openapi/swag="v0.23.0"
|
||||
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible"
|
||||
github.com/gobuffalo/flect="v1.0.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
|
||||
github.com/gohugoio/hashstructure="v0.5.0"
|
||||
github.com/gohugoio/httpcache="v0.7.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
|
||||
github.com/goccy/go-yaml="v1.19.1"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20251018145728-cfcc22d823c6"
|
||||
github.com/gohugoio/go-radix="v1.2.0"
|
||||
github.com/gohugoio/hashstructure="v0.6.0"
|
||||
github.com/gohugoio/httpcache="v0.8.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.5.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.1"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
|
||||
github.com/google/go-cmp="v0.6.0"
|
||||
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941"
|
||||
github.com/google/go-cmp="v0.7.0"
|
||||
github.com/gorilla/css="v1.0.1"
|
||||
github.com/gorilla/websocket="v1.5.3"
|
||||
github.com/hairyhenderson/go-codeowners="v0.7.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.7"
|
||||
@@ -224,58 +232,57 @@ github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.13"
|
||||
github.com/lucasb-eyer/go-colorful="v1.2.0"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/makeworld-the-better-one/dither/v2="v2.4.0"
|
||||
github.com/marekm4/color-extractor="v1.2.1"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.20"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mazznoer/csscolorparser="v0.1.5"
|
||||
github.com/mattn/go-runewidth="v0.0.19"
|
||||
github.com/microcosm-cc/bluemonday="v1.0.27"
|
||||
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
|
||||
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.7.0"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
|
||||
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/niklasfasching/go-org="v1.9.1"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
|
||||
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
|
||||
github.com/olekukonko/cat="v0.0.0-20250911104152-50322a0618f6"
|
||||
github.com/olekukonko/errors="v1.1.0"
|
||||
github.com/olekukonko/ll="v0.1.3"
|
||||
github.com/olekukonko/tablewriter="v1.1.2"
|
||||
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
|
||||
github.com/pelletier/go-toml/v2="v2.2.3"
|
||||
github.com/pelletier/go-toml/v2="v2.2.4"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rivo/uniseg="v0.4.7"
|
||||
github.com/rogpeppe/go-internal="v1.13.1"
|
||||
github.com/rogpeppe/go-internal="v1.14.1"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/sass/libsass="3.6.6"
|
||||
github.com/spf13/afero="v1.11.0"
|
||||
github.com/spf13/cast="v1.7.1"
|
||||
github.com/spf13/cobra="v1.8.1"
|
||||
github.com/sass/dart-sass/compiler="1.97.1"
|
||||
github.com/sass/dart-sass/implementation="1.97.1"
|
||||
github.com/sass/dart-sass/protocol="3.2.0"
|
||||
github.com/spf13/afero="v1.15.0"
|
||||
github.com/spf13/cast="v1.10.0"
|
||||
github.com/spf13/cobra="v1.10.2"
|
||||
github.com/spf13/fsync="v0.10.1"
|
||||
github.com/spf13/pflag="v1.0.6"
|
||||
github.com/tdewolff/minify/v2="v2.20.37"
|
||||
github.com/tdewolff/parse/v2="v2.7.15"
|
||||
github.com/tetratelabs/wazero="v1.8.2"
|
||||
github.com/webmproject/libwebp="v1.3.2"
|
||||
github.com/yuin/goldmark-emoji="v1.0.4"
|
||||
github.com/yuin/goldmark="v1.7.8"
|
||||
github.com/spf13/pflag="v1.0.9"
|
||||
github.com/tdewolff/minify/v2="v2.24.8"
|
||||
github.com/tdewolff/parse/v2="v2.8.5"
|
||||
github.com/tetratelabs/wazero="v1.10.1"
|
||||
github.com/webmproject/libwebp="v1.6.0"
|
||||
github.com/woodsbury/decimal128="v1.3.0"
|
||||
github.com/yuin/goldmark-emoji="v1.0.6"
|
||||
github.com/yuin/goldmark="v1.7.13"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
golang.org/x/crypto="v0.33.0"
|
||||
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac"
|
||||
golang.org/x/image="v0.24.0"
|
||||
golang.org/x/mod="v0.23.0"
|
||||
golang.org/x/net="v0.35.0"
|
||||
golang.org/x/sync="v0.11.0"
|
||||
golang.org/x/sys="v0.30.0"
|
||||
golang.org/x/text="v0.22.0"
|
||||
golang.org/x/tools="v0.30.0"
|
||||
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
|
||||
gonum.org/v1/plot="v0.15.0"
|
||||
google.golang.org/protobuf="v1.36.5"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
go.yaml.in/yaml/v3="v3.0.4"
|
||||
golang.org/x/crypto="v0.46.0"
|
||||
golang.org/x/image="v0.34.0"
|
||||
golang.org/x/mod="v0.31.0"
|
||||
golang.org/x/net="v0.48.0"
|
||||
golang.org/x/sync="v0.19.0"
|
||||
golang.org/x/sys="v0.39.0"
|
||||
golang.org/x/text="v0.32.0"
|
||||
golang.org/x/tools="v0.40.0"
|
||||
google.golang.org/protobuf="v1.36.10"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
oss.terrastruct.com/d2="v0.6.9"
|
||||
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
|
||||
rsc.io/qr="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
|
||||
Vendored
+3
-3
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
|
||||
|
||||
infol := opts.Log.InfoCommand("dynacache")
|
||||
|
||||
evictedIdentities := collections.NewStack[KeyIdentity]()
|
||||
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
|
||||
|
||||
onEvict := func(k, v any) {
|
||||
if !opts.Watching {
|
||||
@@ -129,7 +129,7 @@ type Cache struct {
|
||||
partitions map[string]PartitionManager
|
||||
|
||||
onEvict func(k, v any)
|
||||
evictedIdentities *collections.Stack[KeyIdentity]
|
||||
evictedIdentities *collections.StackThreadSafe[KeyIdentity]
|
||||
|
||||
opts Options
|
||||
infol logg.LevelLogger
|
||||
@@ -340,7 +340,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
|
||||
panic("invalid Weight, must be between 1 and 100")
|
||||
}
|
||||
|
||||
if partitionNameRe.FindString(name) != name {
|
||||
if !partitionNameRe.MatchString(name) {
|
||||
panic(fmt.Sprintf("invalid partition name %q", name))
|
||||
}
|
||||
|
||||
|
||||
Vendored
+55
-11
@@ -16,6 +16,7 @@ package filecache
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -54,7 +55,11 @@ type Cache struct {
|
||||
|
||||
nlocker *lockTracker
|
||||
|
||||
isResourceDir bool
|
||||
baseDir string
|
||||
|
||||
initOnce sync.Once
|
||||
isInited bool
|
||||
initErr error
|
||||
}
|
||||
|
||||
@@ -109,9 +114,14 @@ func (l *lockedFile) Close() error {
|
||||
}
|
||||
|
||||
func (c *Cache) init() error {
|
||||
if c == nil {
|
||||
panic("cache is nil")
|
||||
}
|
||||
c.initOnce.Do(func() {
|
||||
c.isInited = true
|
||||
// Create the base dir if it does not exist.
|
||||
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
|
||||
err = fmt.Errorf("failled to create base cache directory: %s", err)
|
||||
c.initErr = err
|
||||
}
|
||||
})
|
||||
@@ -286,8 +296,32 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
// SetBytes sets the file content with the given id in the cache.
|
||||
func (c *Cache) SetBytes(id string, data []byte) error {
|
||||
if err := c.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.writeReader(id, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
func (c *Cache) GetBytes(id string) ([]byte, error) {
|
||||
_, b, err := c.GetItemBytes(id)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// GetItemBytes gets the ItemInfo and file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetItemBytes(id string) (ItemInfo, []byte, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
@@ -417,6 +451,17 @@ func (c *Cache) GetString(id string) string {
|
||||
// Caches is a named set of caches.
|
||||
type Caches map[string]*Cache
|
||||
|
||||
func (f Caches) SetResourceFs(fs afero.Fs) {
|
||||
for _, c := range f {
|
||||
if c.isResourceDir {
|
||||
if c.isInited {
|
||||
panic("cannot set resource fs after init")
|
||||
}
|
||||
c.Fs = hugofs.NewBasePathFs(fs, c.baseDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets a named cache, nil if none found.
|
||||
func (f Caches) Get(name string) *Cache {
|
||||
return f[strings.ToLower(name)]
|
||||
@@ -424,24 +469,19 @@ func (f Caches) Get(name string) *Cache {
|
||||
|
||||
// NewCaches creates a new set of file caches from the given
|
||||
// configuration.
|
||||
func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
|
||||
fs := p.Fs.Source
|
||||
func NewCaches(dcfg Configs, sourceFs afero.Fs) (Caches, error) {
|
||||
// dcfg := p.Cfg.GetConfigSection("caches").(Configs)
|
||||
fs := sourceFs
|
||||
|
||||
m := make(Caches)
|
||||
for k, v := range dcfg {
|
||||
var cfs afero.Fs
|
||||
|
||||
if v.IsResourceDir {
|
||||
cfs = p.BaseFs.ResourcesCache
|
||||
cfs = nil // Set later. TODO(bep) this needs to be cleanded up.
|
||||
} else {
|
||||
cfs = fs
|
||||
}
|
||||
|
||||
if cfs == nil {
|
||||
panic("nil fs")
|
||||
}
|
||||
|
||||
baseDir := v.DirCompiled
|
||||
|
||||
bfs := hugofs.NewBasePathFs(cfs, baseDir)
|
||||
@@ -451,7 +491,11 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
pruneAllRootDir = "pkg"
|
||||
}
|
||||
|
||||
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
|
||||
c := NewCache(bfs, v.MaxAge, pruneAllRootDir)
|
||||
c.isResourceDir = v.IsResourceDir
|
||||
c.baseDir = baseDir
|
||||
|
||||
m[k] = c
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
||||
Vendored
+49
-9
@@ -15,6 +15,7 @@
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -40,27 +41,39 @@ var defaultCacheConfig = FileCacheConfig{
|
||||
}
|
||||
|
||||
const (
|
||||
CacheKeyGetJSON = "getjson"
|
||||
CacheKeyGetCSV = "getcsv"
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyGetResource = "getresource"
|
||||
CacheKeyMisc = "misc"
|
||||
CacheKeyGetJSON = "getjson"
|
||||
CacheKeyGetCSV = "getcsv"
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyModuleQueries = "modulequeries"
|
||||
CacheKeyGetResource = "getresource"
|
||||
CacheKeyMisc = "misc"
|
||||
)
|
||||
|
||||
type Configs map[string]FileCacheConfig
|
||||
|
||||
// CacheDirModules returns the compiled path to the modules cache.
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirModules() string {
|
||||
return c[CacheKeyModules].DirCompiled
|
||||
}
|
||||
|
||||
// CacheDirMisc returns the compiled path to the misc cache.
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirMisc() string {
|
||||
return c[CacheKeyMisc].DirCompiled
|
||||
}
|
||||
|
||||
var defaultCacheConfigs = Configs{
|
||||
CacheKeyModules: {
|
||||
MaxAge: -1,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
CacheKeyModuleQueries: {
|
||||
MaxAge: 24 * time.Hour,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
CacheKeyGetJSON: defaultCacheConfig,
|
||||
CacheKeyGetCSV: defaultCacheConfig,
|
||||
CacheKeyImages: {
|
||||
@@ -100,6 +113,23 @@ type FileCacheConfig struct {
|
||||
IsResourceDir bool `json:"-"`
|
||||
}
|
||||
|
||||
// MarshalJSON marshals FileCacheConfig to JSON with MaxAge as a human-readable string.
|
||||
func (c FileCacheConfig) MarshalJSON() ([]byte, error) {
|
||||
var maxAge any
|
||||
if c.MaxAge == -1 {
|
||||
maxAge = -1
|
||||
} else {
|
||||
maxAge = strings.TrimSuffix(c.MaxAge.String(), "0m0s")
|
||||
}
|
||||
return json.Marshal(&struct {
|
||||
MaxAge any `json:"maxAge"`
|
||||
Dir string `json:"dir"`
|
||||
}{
|
||||
MaxAge: maxAge,
|
||||
Dir: c.Dir,
|
||||
})
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[CacheKeyGetJSON]
|
||||
@@ -120,6 +150,16 @@ func (f Caches) ModulesCache() *Cache {
|
||||
return f[CacheKeyModules]
|
||||
}
|
||||
|
||||
// ModuleQueriesCache gets the file cache for Hugo Module version queries.
|
||||
// Returns nil if not found.
|
||||
func (f Caches) ModuleQueriesCache() *Cache {
|
||||
c, ok := f[CacheKeyModuleQueries]
|
||||
if !ok {
|
||||
panic("module queries cache not set")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[CacheKeyAssets]
|
||||
@@ -147,7 +187,7 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
if _, ok := v.(maps.Params); !ok {
|
||||
if _, ok := v.(hmaps.Params); !ok {
|
||||
continue
|
||||
}
|
||||
cc := defaultCacheConfig
|
||||
|
||||
+30
-3
@@ -14,6 +14,7 @@
|
||||
package filecache_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -59,7 +60,7 @@ dir = "/path/to/c4"
|
||||
c.Assert(err, qt.IsNil)
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
c.Assert(len(decoded), qt.Equals, 8)
|
||||
|
||||
c2 := decoded["getcsv"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
|
||||
@@ -106,7 +107,7 @@ dir = "/path/to/c4"
|
||||
c.Assert(err, qt.IsNil)
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
c.Assert(len(decoded), qt.Equals, 8)
|
||||
|
||||
for _, v := range decoded {
|
||||
c.Assert(v.MaxAge, qt.Equals, time.Duration(0))
|
||||
@@ -129,7 +130,7 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
c.Assert(len(decoded), qt.Equals, 8)
|
||||
|
||||
imgConfig := decoded[filecache.CacheKeyImages]
|
||||
jsonConfig := decoded[filecache.CacheKeyGetJSON]
|
||||
@@ -144,3 +145,29 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
|
||||
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestFileCacheConfigMarshalJSON(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("cacheDir", "/cache")
|
||||
cfg.Set("workingDir", "/my/project")
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
|
||||
moduleQueriesConfig := decoded[filecache.CacheKeyModuleQueries]
|
||||
c.Assert(moduleQueriesConfig.MaxAge, qt.Equals, 24*time.Hour)
|
||||
|
||||
b, err := json.Marshal(moduleQueriesConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(string(b), qt.Contains, `"maxAge":"24h"`)
|
||||
c.Assert(string(b), qt.Not(qt.Contains), "86400000000000")
|
||||
c.Assert(string(b), qt.Not(qt.Contains), "8.64e")
|
||||
|
||||
moduleQueriesConfig.MaxAge = -1
|
||||
b, err = json.Marshal(moduleQueriesConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Contains, `"maxAge":-1`)
|
||||
}
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ title: "Home"
|
||||
---
|
||||
-- assets/a/pixel.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
{{ warnf "HOME!" }}
|
||||
{{ $img := resources.GetMatch "**.png" }}
|
||||
{{ $img = $img.Resize "3x3" }}
|
||||
|
||||
+7
-3
@@ -55,9 +55,12 @@ dir = ":resourceDir/_gen"
|
||||
|
||||
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := filecache.NewCaches(p)
|
||||
fs := afero.NewMemMapFs()
|
||||
p := newPathsSpec(t, fs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, fs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache := caches[name]
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
@@ -84,8 +87,9 @@ dir = ":resourceDir/_gen"
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = filecache.NewCaches(p)
|
||||
caches, err = filecache.NewCaches(fileCachConfig, fs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
|
||||
Vendored
+11
-5
@@ -18,6 +18,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -78,9 +80,11 @@ dir = ":cacheDir/c"
|
||||
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
|
||||
|
||||
p := newPathsSpec(t, osfs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
|
||||
caches, err := filecache.NewCaches(p)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(p.SourceFs)
|
||||
|
||||
cache := caches.Get("GetJSON")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
@@ -149,7 +153,7 @@ dir = ":cacheDir/c"
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
info, b, err = caches.ImageCache().GetBytes("mykey")
|
||||
info, b, err = caches.ImageCache().GetItemBytes("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
@@ -158,6 +162,7 @@ dir = ":cacheDir/c"
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
htesting.SkipSlowTestUnlessCI(t)
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
@@ -179,9 +184,10 @@ dir = "/cache/c"
|
||||
`
|
||||
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
|
||||
caches, err := filecache.NewCaches(p)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(p.Fs.Source)
|
||||
|
||||
const cacheName = "getjson"
|
||||
|
||||
@@ -270,7 +276,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
c.Assert(err, qt.IsNil)
|
||||
acfg := testconfig.GetTestConfig(fs, cfg)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
|
||||
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
Vendored
+21
-10
@@ -25,6 +25,8 @@ import (
|
||||
|
||||
// DefaultConfig holds the default configuration for the HTTP cache.
|
||||
var DefaultConfig = Config{
|
||||
RespectCacheControlNoStoreInRequest: true,
|
||||
RespectCacheControlNoStoreInResponse: false,
|
||||
Cache: Cache{
|
||||
For: GlobMatcher{
|
||||
Excludes: []string{"**"},
|
||||
@@ -42,7 +44,13 @@ var DefaultConfig = Config{
|
||||
|
||||
// Config holds the configuration for the HTTP cache.
|
||||
type Config struct {
|
||||
// Configures the HTTP cache behavior (RFC 9111).
|
||||
// When enabled and there's a Cache-Control: no-store directive in the request, response will never be stored in disk cache.
|
||||
RespectCacheControlNoStoreInRequest bool
|
||||
|
||||
// When enabled and there's a Cache-Control: no-store directive in the response, response will never be stored in disk cache.
|
||||
RespectCacheControlNoStoreInResponse bool
|
||||
|
||||
// Enables HTTP cache behavior (RFC 9111) for these resources.
|
||||
// When this is not enabled for a resource, Hugo will go straight to the file cache.
|
||||
Cache Cache
|
||||
|
||||
@@ -57,7 +65,9 @@ type Cache struct {
|
||||
}
|
||||
|
||||
func (c *Config) Compile() (ConfigCompiled, error) {
|
||||
var cc ConfigCompiled
|
||||
cc := ConfigCompiled{
|
||||
Base: *c,
|
||||
}
|
||||
|
||||
p, err := c.Cache.For.CompilePredicate()
|
||||
if err != nil {
|
||||
@@ -127,6 +137,7 @@ func (gm GlobMatcher) IsZero() bool {
|
||||
}
|
||||
|
||||
type ConfigCompiled struct {
|
||||
Base Config
|
||||
For predicate.P[string]
|
||||
PollConfigs []PollConfigCompiled
|
||||
}
|
||||
@@ -162,16 +173,16 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
|
||||
if gm.IsZero() {
|
||||
panic("no includes or excludes")
|
||||
}
|
||||
var p predicate.P[string]
|
||||
var b predicate.PR[string]
|
||||
for _, include := range gm.Includes {
|
||||
g, err := glob.Compile(include, '/')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fn := func(s string) bool {
|
||||
return g.Match(s)
|
||||
fn := func(s string) predicate.Match {
|
||||
return predicate.BoolMatch(g.Match(s))
|
||||
}
|
||||
p = p.Or(fn)
|
||||
b = b.Or(fn)
|
||||
}
|
||||
|
||||
for _, exclude := range gm.Excludes {
|
||||
@@ -179,13 +190,13 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fn := func(s string) bool {
|
||||
return !g.Match(s)
|
||||
fn := func(s string) predicate.Match {
|
||||
return predicate.BoolMatch(!g.Match(s))
|
||||
}
|
||||
p = p.And(fn)
|
||||
b = b.And(fn)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
return b.BoolFunc(), nil
|
||||
}
|
||||
|
||||
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
|
||||
|
||||
Vendored
+1
-1
@@ -52,7 +52,7 @@ func TestDefaultConfig(t *testing.T) {
|
||||
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{})
|
||||
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]any{})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Default to all packages if none specified
|
||||
PACKAGES="${1:-./...}"
|
||||
|
||||
echo "==> Checking packages: $PACKAGES"
|
||||
|
||||
# Timing arrays
|
||||
declare -a STEP_NAMES
|
||||
declare -a STEP_TIMES
|
||||
|
||||
time_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local start=$(date +%s.%N)
|
||||
"$@"
|
||||
local end=$(date +%s.%N)
|
||||
local elapsed=$(echo "$end - $start" | bc)
|
||||
STEP_NAMES+=("$name")
|
||||
STEP_TIMES+=("$elapsed")
|
||||
}
|
||||
|
||||
# Check gofmt
|
||||
run_gofmt() {
|
||||
echo "==> Running gofmt..."
|
||||
# Convert package pattern to path (e.g., ./hugolib/... -> ./hugolib)
|
||||
local path="${PACKAGES%/...}"
|
||||
GOFMT_OUTPUT=$(gofmt -l "$path" 2>&1) || true
|
||||
if [ -n "$GOFMT_OUTPUT" ]; then
|
||||
echo "gofmt found issues in:"
|
||||
echo "$GOFMT_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run staticcheck
|
||||
run_staticcheck() {
|
||||
# Check if staticcheck is installed, install if not
|
||||
if ! command -v staticcheck &> /dev/null; then
|
||||
echo "==> Installing staticcheck..."
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
fi
|
||||
echo "==> Running staticcheck..."
|
||||
staticcheck $PACKAGES
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run tests
|
||||
run_tests() {
|
||||
echo "==> Running tests..."
|
||||
local output
|
||||
if ! output=$(go test $PACKAGES 2>&1); then
|
||||
echo "$output"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run all steps with timing
|
||||
TOTAL_START=$(date +%s.%N)
|
||||
|
||||
time_step "gofmt" run_gofmt
|
||||
time_step "staticcheck" run_staticcheck
|
||||
time_step "tests" run_tests
|
||||
|
||||
TOTAL_END=$(date +%s.%N)
|
||||
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
|
||||
|
||||
# Print timing summary
|
||||
echo ""
|
||||
echo "==> All checks passed!"
|
||||
echo ""
|
||||
echo "Timing summary:"
|
||||
echo "---------------"
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
printf " %-15s %6.2fs\n" "${STEP_NAMES[$i]}" "${STEP_TIMES[$i]}"
|
||||
done
|
||||
echo "---------------"
|
||||
printf " %-15s %6.2fs\n" "Total" "$TOTAL_ELAPSED"
|
||||
@@ -401,9 +401,7 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
for _, group := range watchGroups {
|
||||
r.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
|
||||
watcher, err := b.newWatcher(r.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+8
-7
@@ -23,8 +23,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
@@ -71,7 +72,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
return fmt.Errorf("language %q not found", c.lang)
|
||||
}
|
||||
} else {
|
||||
config = conf.configs.LanguageConfigSlice[0]
|
||||
config = conf.configs.LanguageConfigMap[conf.configs.Base.DefaultContentLanguage]
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -94,7 +95,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
|
||||
return err
|
||||
}
|
||||
maps.ConvertFloat64WithNoDecimalsToInt(m)
|
||||
hmaps.ConvertFloat64WithNoDecimalsToInt(m)
|
||||
switch format {
|
||||
case "yaml":
|
||||
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
|
||||
@@ -128,9 +129,9 @@ func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
|
||||
type configModMount struct {
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Lang string `json:"lang,omitempty"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Sites sitesmatrix.Sites `json:"sites,omitzero"`
|
||||
}
|
||||
|
||||
type configModMounts struct {
|
||||
@@ -146,7 +147,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
mounts = append(mounts, configModMount{
|
||||
Source: mount.Source,
|
||||
Target: mount.Target,
|
||||
Lang: mount.Lang,
|
||||
Sites: mount.Sites,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -36,7 +37,6 @@ import (
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func newGenCommand() *genCommand {
|
||||
@@ -51,6 +51,7 @@ func newGenCommand() *genCommand {
|
||||
lineNumbersInlineStyle string
|
||||
lineNumbersTableStyle string
|
||||
omitEmpty bool
|
||||
omitClassComments bool
|
||||
)
|
||||
|
||||
newChromaStyles := func() simplecobra.Commander {
|
||||
@@ -81,12 +82,14 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
return err
|
||||
}
|
||||
|
||||
var formatter *html.Formatter
|
||||
if omitEmpty {
|
||||
formatter = html.New(html.WithClasses(true))
|
||||
} else {
|
||||
formatter = html.New(html.WithAllClasses(true))
|
||||
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
|
||||
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
|
||||
}
|
||||
options := []html.Option{
|
||||
html.WithCSSComments(!omitClassComments),
|
||||
}
|
||||
formatter := html.New(options...)
|
||||
|
||||
w := os.Stdout
|
||||
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
@@ -103,8 +106,10 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
|
||||
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules (deprecated, no longer needed)`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -244,7 +249,7 @@ url: %s
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
yamlEnc := yaml.NewEncoder(f)
|
||||
yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt())
|
||||
if err := yamlEnc.Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
|
||||
// Flags with a different name in the config.
|
||||
keyMap := map[string]string{
|
||||
"minify": "minifyOutput",
|
||||
"minify": "minify.minifyOutput",
|
||||
"destination": "publishDir",
|
||||
"editor": "newContentEditor",
|
||||
}
|
||||
|
||||
+34
-5
@@ -27,13 +27,15 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -142,7 +144,7 @@ func (c *hugoBuilder) getDirList() ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
|
||||
return hstrings.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) initCPUProfile() (func(), error) {
|
||||
@@ -462,7 +464,15 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
|
||||
infol.Logf("removing all files from destination that don't exist in static dirs")
|
||||
|
||||
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
|
||||
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
|
||||
name := f.Name()
|
||||
|
||||
// Keep .gitignore and .gitattributes anywhere
|
||||
if name == ".gitignore" || name == ".gitattributes" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Keep Hugo's original dot-directory behavior
|
||||
return f.IsDir() && strings.HasPrefix(name, ".")
|
||||
}
|
||||
}
|
||||
start := time.Now()
|
||||
@@ -515,6 +525,14 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
|
||||
return langCount, nil
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) progressIntermediate() {
|
||||
terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) progressHidden() {
|
||||
terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
|
||||
var (
|
||||
g errgroup.Group
|
||||
@@ -818,7 +836,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
continue
|
||||
}
|
||||
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
|
||||
walkAdder := func(ctx context.Context, path string, f hugofs.FileMetaInfo) error {
|
||||
if f.IsDir() {
|
||||
c.r.logger.Println("adding created directory to watchlist", path)
|
||||
if err := watcher.Add(path); err != nil {
|
||||
@@ -1027,6 +1045,17 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
|
||||
if terminal.PrintANSIColors(os.Stdout) {
|
||||
defer c.progressHidden()
|
||||
// If the configuration takes a while to load, we want to show some progress.
|
||||
// This is typically loading of external modules.
|
||||
d := debounce.New(500 * time.Millisecond)
|
||||
d(func() {
|
||||
c.progressIntermediate()
|
||||
})
|
||||
defer d(func() {})
|
||||
}
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("renderToMemory", c.r.renderToMemory)
|
||||
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
|
||||
@@ -1048,7 +1077,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
}
|
||||
cfg.Set("environment", c.r.environment)
|
||||
|
||||
cfg.Set("internal", maps.Params{
|
||||
cfg.Set("internal", hmaps.Params{
|
||||
"running": running,
|
||||
"watch": watch,
|
||||
"verbose": c.r.isVerbose(),
|
||||
|
||||
+4
-4
@@ -29,9 +29,9 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
@@ -190,7 +190,7 @@ func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyl
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
|
||||
metadata, _ := maps.ToStringMapE(m)
|
||||
metadata, _ := hmaps.ToStringMapE(m)
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
var resultLines []string
|
||||
@@ -246,7 +246,7 @@ func (c *importCommand) convertJekyllContent(m any, content string) (string, err
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
|
||||
metadata, err := maps.ToStringMapE(m)
|
||||
metadata, err := hmaps.ToStringMapE(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -427,7 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
c.r.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi hugofs.FileMetaInfo) error {
|
||||
callback := func(ctx context.Context, path string, fi hugofs.FileMetaInfo) error {
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
+3
-1
@@ -53,7 +53,9 @@ Ensure you run this within the root directory of your site.`,
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
cfg := flagsToCfg(cd, nil)
|
||||
cfg.Set("BuildFuture", true)
|
||||
h, err := r.Hugo(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+11
-7
@@ -49,6 +49,8 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -369,6 +371,12 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
|
||||
if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
|
||||
if isNavigation(requestURI, r) {
|
||||
// See issue 14240.
|
||||
// Hugo escapes the URL paths when generating them,
|
||||
// that may not be the case when we receive it back from the browser.
|
||||
// PathEscape will escape if it is not already escaped.
|
||||
requestURI = paths.PathEscape(requestURI)
|
||||
|
||||
if !f.c.visitedURLs.Contains(requestURI) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(requestURI); err != nil {
|
||||
@@ -491,9 +499,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
for _, group := range watchGroups {
|
||||
c.r.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
c.r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
|
||||
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -627,7 +633,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
|
||||
panic("no server ports set")
|
||||
}
|
||||
return c.withConfE(func(conf *commonConfig) error {
|
||||
for i, language := range conf.configs.LanguagesDefaultFirst {
|
||||
for i, language := range conf.configs.Languages {
|
||||
isMultihost := conf.configs.IsMultihost
|
||||
var serverPort int
|
||||
if isMultihost {
|
||||
@@ -879,7 +885,7 @@ func (c *serverCommand) serve() error {
|
||||
if isMultihost {
|
||||
for _, l := range conf.configs.ConfigLangs() {
|
||||
baseURLs = append(baseURLs, l.BaseURL())
|
||||
roots = append(roots, l.Language().Lang)
|
||||
roots = append(roots, l.Language().(*langs.Language).Lang)
|
||||
}
|
||||
} else {
|
||||
l := conf.configs.GetFirstLanguageConfig()
|
||||
@@ -1034,7 +1040,6 @@ func (c *serverCommand) serve() error {
|
||||
defer cancel()
|
||||
wg2, ctx := errgroup.WithContext(ctx)
|
||||
for _, srv := range servers {
|
||||
srv := srv
|
||||
wg2.Go(func() error {
|
||||
return srv.Shutdown(ctx)
|
||||
})
|
||||
@@ -1166,7 +1171,6 @@ func chmodFilter(dst, src os.FileInfo) bool {
|
||||
}
|
||||
|
||||
func cleanErrorLog(content string) string {
|
||||
content = strings.ReplaceAll(content, "\n", " ")
|
||||
content = logReplacer.Replace(content)
|
||||
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
|
||||
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
|
||||
|
||||
@@ -16,6 +16,8 @@ package collections
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hreflect"
|
||||
)
|
||||
|
||||
// Append appends from to a slice to and returns the resulting slice.
|
||||
@@ -25,7 +27,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
if len(from) == 0 {
|
||||
return to, nil
|
||||
}
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
var tot reflect.Type
|
||||
@@ -100,7 +102,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
tov, _ := indirect(reflect.ValueOf(to))
|
||||
tov, _ := hreflect.Indirect(reflect.ValueOf(to))
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
}
|
||||
tov = reflect.Append(tov, fv)
|
||||
@@ -136,17 +138,3 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
|
||||
// TODO(bep) consolidate
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ package collections
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -148,66 +147,3 @@ func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
|
||||
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
|
||||
}
|
||||
|
||||
func TestIndirect(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
type testStruct struct {
|
||||
Field string
|
||||
}
|
||||
|
||||
var (
|
||||
nilPtr *testStruct
|
||||
nilIface interface{} = nil
|
||||
nonNilIface interface{} = &testStruct{Field: "hello"}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input any
|
||||
wantKind reflect.Kind
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "nil pointer",
|
||||
input: nilPtr,
|
||||
wantKind: reflect.Ptr,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "nil interface",
|
||||
input: nilIface,
|
||||
wantKind: reflect.Invalid,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "non-nil pointer to struct",
|
||||
input: &testStruct{Field: "abc"},
|
||||
wantKind: reflect.Struct,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "non-nil interface holding pointer",
|
||||
input: nonNilIface,
|
||||
wantKind: reflect.Struct,
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "plain value",
|
||||
input: testStruct{Field: "xyz"},
|
||||
wantKind: reflect.Struct,
|
||||
wantNil: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := reflect.ValueOf(tt.input)
|
||||
got, isNil := indirect(v)
|
||||
|
||||
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
|
||||
c.Assert(isNil, qt.Equals, tt.wantNil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+65
-12
@@ -13,28 +13,32 @@
|
||||
|
||||
package collections
|
||||
|
||||
import "slices"
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
import "sync"
|
||||
"github.com/gohugoio/hugo/common/hiter"
|
||||
)
|
||||
|
||||
// Stack is a simple LIFO stack that is safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
|
||||
type StackThreadSafe[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewStack[T any]() *Stack[T] {
|
||||
return &Stack[T]{}
|
||||
func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
|
||||
return &StackThreadSafe[T]{}
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
func (s *StackThreadSafe[T]) Push(item T) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (T, bool) {
|
||||
func (s *StackThreadSafe[T]) Pop() (T, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.items) == 0 {
|
||||
@@ -45,7 +49,7 @@ func (s *Stack[T]) Pop() (T, bool) {
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (T, bool) {
|
||||
func (s *StackThreadSafe[T]) Peek() (T, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.items) == 0 {
|
||||
@@ -54,13 +58,18 @@ func (s *Stack[T]) Peek() (T, bool) {
|
||||
return s.items[len(s.items)-1], true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Len() int {
|
||||
func (s *StackThreadSafe[T]) Len() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
// All returns all items in the stack, from bottom to top.
|
||||
func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
|
||||
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
|
||||
}
|
||||
|
||||
func (s *StackThreadSafe[T]) Drain() []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items := s.items
|
||||
@@ -68,7 +77,7 @@ func (s *Stack[T]) Drain() []T {
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var items []T
|
||||
@@ -80,3 +89,47 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Stack is a simple LIFO stack that is not safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
}
|
||||
|
||||
func NewStack[T any]() *Stack[T] {
|
||||
return &Stack[T]{}
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (T, bool) {
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
item := s.items[len(s.items)-1]
|
||||
s.items = s.items[:len(s.items)-1]
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (T, bool) {
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
return s.items[len(s.items)-1], true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Len() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) All() iter.Seq2[int, T] {
|
||||
return slices.All(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
items := s.items
|
||||
s.items = nil
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestNewStack(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s, qt.IsNotNil)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestStackBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s.Len(), qt.Equals, 0)
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestStackDrain(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[string]()
|
||||
s := NewStackThreadSafe[string]()
|
||||
s.Push("a")
|
||||
s.Push("b")
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestStackDrainMatching(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
s.Push(1)
|
||||
s.Push(2)
|
||||
s.Push(3)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/gohugoio/hashstructure"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
)
|
||||
|
||||
@@ -38,6 +39,34 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
|
||||
return h.Sum64(), size, nil
|
||||
}
|
||||
|
||||
type Hasher interface {
|
||||
io.StringWriter
|
||||
io.Writer
|
||||
io.ReaderFrom
|
||||
Sum64() uint64
|
||||
}
|
||||
|
||||
type HashCloser interface {
|
||||
Hasher
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// XxHasher returns a Hasher that uses xxHash.
|
||||
// Remember to call Close when done.
|
||||
func XxHasher() HashCloser {
|
||||
h := getXxHashReadFrom()
|
||||
return struct {
|
||||
Hasher
|
||||
io.Closer
|
||||
}{
|
||||
Hasher: h,
|
||||
Closer: hugio.CloserFunc(func() error {
|
||||
putXxHashReadFrom(h)
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
|
||||
// and returns the hash as a hex encoded string.
|
||||
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
|
||||
@@ -58,11 +87,13 @@ func XXHashFromString(s string) (uint64, error) {
|
||||
return h.Sum64(), nil
|
||||
}
|
||||
|
||||
// XxHashFromStringHexEncoded calculates the xxHash for the given string
|
||||
// XxHashFromStringHexEncoded calculates the xxHash for the given strings
|
||||
// and returns the hash as a hex encoded string.
|
||||
func XxHashFromStringHexEncoded(f string) string {
|
||||
func XxHashFromStringHexEncoded(s ...string) string {
|
||||
h := xxhash.New()
|
||||
h.WriteString(f)
|
||||
for _, f := range s {
|
||||
h.WriteString(f)
|
||||
}
|
||||
hash := h.Sum(nil)
|
||||
return hex.EncodeToString(hash)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ func TestXxHashFromReaderPara(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 10 {
|
||||
i := i
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -75,8 +74,8 @@ func TestXxHashFromStringHexEncoded(t *testing.T) {
|
||||
|
||||
func BenchmarkXXHashFromReader(b *testing.B) {
|
||||
r := strings.NewReader("Hello World")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
XXHashFromReader(r)
|
||||
r.Seek(0, 0)
|
||||
}
|
||||
@@ -84,16 +83,16 @@ func BenchmarkXXHashFromReader(b *testing.B) {
|
||||
|
||||
func BenchmarkXXHashFromString(b *testing.B) {
|
||||
s := "Hello World"
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
XXHashFromString(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkXXHashFromStringHexEncoded(b *testing.B) {
|
||||
s := "The quick brown fox jumps over the lazy dog"
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
XxHashFromStringHexEncoded(s)
|
||||
}
|
||||
}
|
||||
@@ -136,7 +135,7 @@ func BenchmarkHashString(b *testing.B) {
|
||||
|
||||
for _, test := range tests {
|
||||
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
HashString(test)
|
||||
}
|
||||
})
|
||||
@@ -149,9 +148,7 @@ func BenchmarkHashMap(b *testing.B) {
|
||||
m[fmt.Sprintf("key%d", i)] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
HashString(m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hcontext
|
||||
|
||||
import "context"
|
||||
|
||||
// ContextDispatcher is a generic interface for setting and getting values from a context.
|
||||
type ContextDispatcher[T any] interface {
|
||||
Set(ctx context.Context, value T) context.Context
|
||||
Get(ctx context.Context) T
|
||||
}
|
||||
|
||||
// NewContextDispatcher creates a new ContextDispatcher with the given key.
|
||||
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
|
||||
return keyInContext[T, R]{
|
||||
id: key,
|
||||
}
|
||||
}
|
||||
|
||||
type keyInContext[T any, R comparable] struct {
|
||||
zero T
|
||||
id R
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Get(ctx context.Context) T {
|
||||
v := ctx.Value(f.id)
|
||||
if v == nil {
|
||||
return f.zero
|
||||
}
|
||||
return v.(T)
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
|
||||
return context.WithValue(ctx, f.id, value)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hdebug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
)
|
||||
|
||||
// Printf is a debug print function that should be removed before committing code to the repository.
|
||||
func Printf(format string, args ...any) {
|
||||
panicIfRealCI()
|
||||
if len(args) == 1 && !strings.Contains(format, "%") {
|
||||
format = format + ": %v"
|
||||
}
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format = format + "\n"
|
||||
}
|
||||
fmt.Printf(format, args...)
|
||||
}
|
||||
|
||||
func AssertNotNil(a ...any) {
|
||||
panicIfRealCI()
|
||||
for _, v := range a {
|
||||
if types.IsNil(v) {
|
||||
panic("hdebug.AssertNotNil: value is nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Panicf(format string, args ...any) {
|
||||
panicIfRealCI()
|
||||
// fmt.Println(stack())
|
||||
if len(args) == 1 && !strings.Contains(format, "%") {
|
||||
format = format + ": %v"
|
||||
}
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format = format + "\n"
|
||||
}
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func panicIfRealCI() {
|
||||
if htesting.IsRealCI() {
|
||||
panic("This debug statement should be removed before committing code!")
|
||||
}
|
||||
}
|
||||
@@ -17,22 +17,13 @@ package herrors
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PrintStackTrace prints the current stacktrace to w.
|
||||
func PrintStackTrace(w io.Writer) {
|
||||
buf := make([]byte, 1<<16)
|
||||
runtime.Stack(buf, true)
|
||||
fmt.Fprintf(w, "%s", buf)
|
||||
}
|
||||
|
||||
// ErrorSender is a, typically, non-blocking error handler.
|
||||
type ErrorSender interface {
|
||||
SendError(err error)
|
||||
@@ -45,7 +36,9 @@ type ErrorSender interface {
|
||||
func Recover(args ...any) {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("ERR:", r)
|
||||
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
|
||||
buf := make([]byte, 64<<10)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
args = append(args, "stacktrace from panic: \n"+string(buf), "\n")
|
||||
fmt.Println(args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
|
||||
|
||||
fe.errorContext = ectx
|
||||
|
||||
if ectx.Position.LineNumber > 0 {
|
||||
if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
|
||||
fe.position.LineNumber = ectx.Position.LineNumber
|
||||
}
|
||||
|
||||
if ectx.Position.ColumnNumber > 0 {
|
||||
if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
|
||||
fe.position.ColumnNumber = ectx.Position.ColumnNumber
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ func NewFileErrorFromName(err error, name string) FileError {
|
||||
// Filetype is used to determine the Chroma lexer to use.
|
||||
fileType, pos := extractFileTypePos(err)
|
||||
pos.Filename = name
|
||||
|
||||
if fileType == "" {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
|
||||
}
|
||||
@@ -234,7 +235,9 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
|
||||
return NewFileErrorFromName(err, realFilename)
|
||||
}
|
||||
defer f.Close()
|
||||
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
|
||||
fe := NewFileErrorFromName(err, realFilename)
|
||||
fe = fe.UpdateContent(f, linematcher)
|
||||
return fe
|
||||
}
|
||||
|
||||
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
@@ -285,6 +288,50 @@ func Unwrap(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UnwrapFileErrors returns all FileError contained in err.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errs := Errors(err)
|
||||
var fileErrors []FileError
|
||||
for _, e := range errs {
|
||||
if v, ok := e.(FileError); ok {
|
||||
fileErrors = append(fileErrors, v)
|
||||
}
|
||||
fileErrors = append(fileErrors, UnwrapFileErrors(errors.Unwrap(e))...)
|
||||
}
|
||||
return fileErrors
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
errs := UnwrapFileErrors(err)
|
||||
var n int
|
||||
for _, e := range errs {
|
||||
if e.ErrorContext() != nil {
|
||||
errs[n] = e
|
||||
n++
|
||||
}
|
||||
}
|
||||
return errs[:n]
|
||||
}
|
||||
|
||||
// Errors returns the list of errors contained in err.
|
||||
func Errors(err error) []error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
type unwrapper interface {
|
||||
Unwrap() []error
|
||||
}
|
||||
if u, ok := err.(unwrapper); ok {
|
||||
return u.Unwrap()
|
||||
}
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
func extractFileTypePos(err error) (string, text.Position) {
|
||||
err = Unwrap(err)
|
||||
|
||||
@@ -321,13 +368,9 @@ func extractFileTypePos(err error) (string, text.Position) {
|
||||
}
|
||||
|
||||
// Look in the error message for the line number.
|
||||
for _, handle := range lineNumberExtractors {
|
||||
lno, col := handle(err)
|
||||
if lno > 0 {
|
||||
pos.ColumnNumber = col
|
||||
pos.LineNumber = lno
|
||||
break
|
||||
}
|
||||
if lno, col := commonLineNumberExtractor(err); lno > 0 {
|
||||
pos.ColumnNumber = col
|
||||
pos.LineNumber = lno
|
||||
}
|
||||
|
||||
if fileType == "" && pos.Filename != "" {
|
||||
@@ -351,30 +394,6 @@ func UnwrapFileError(err error) FileError {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnwrapFileErrors tries to unwrap all FileError.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func extractOffsetAndType(e error) (int, string) {
|
||||
switch v := e.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
|
||||
@@ -19,17 +19,27 @@ import (
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
// YAML parse errors.
|
||||
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
|
||||
|
||||
// Template/shortcode parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
|
||||
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
|
||||
|
||||
// i18n bundle errors
|
||||
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
|
||||
}
|
||||
|
||||
func commonLineNumberExtractor(e error) (int, int) {
|
||||
for _, handler := range lineNumberExtractors {
|
||||
lno, col := handler(e)
|
||||
if lno > 0 {
|
||||
return lno, col
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
|
||||
+8
-15
@@ -1,7 +1,5 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Portions Copyright The Go Authors.
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
@@ -13,26 +11,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tplimpl_test
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestLegacyPartialIssue13599(t *testing.T) {
|
||||
func TestCommonLineNumberExtractor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/partials/mypartial.html --
|
||||
Mypartial.
|
||||
-- layouts/_default/index.html --
|
||||
mypartial: {{ template "partials/mypartial.html" . }}
|
||||
c := qt.New(t)
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Mypartial.")
|
||||
lno, col := commonLineNumberExtractor(errors.New("[4:9] value is not allowed in this context"))
|
||||
c.Assert(lno, qt.Equals, 4)
|
||||
c.Assert(col, qt.Equals, 9)
|
||||
}
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
@@ -102,7 +102,7 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
|
||||
workingDir: workingDir,
|
||||
infol: log.InfoCommand("exec"),
|
||||
baseEnviron: baseEnviron,
|
||||
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||
newNPXRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ type Exec struct {
|
||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||
baseEnviron []string
|
||||
|
||||
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
|
||||
newNPXRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
|
||||
npxInit sync.Once
|
||||
npxAvailable bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package himage provides some high level image types and interfaces.
|
||||
package himage
|
||||
|
||||
import "image"
|
||||
|
||||
// AnimatedImage represents an animated image.
|
||||
// This is currently supported for GIF and WebP images.
|
||||
type AnimatedImage interface {
|
||||
image.Image // The first frame.
|
||||
GetRaw() any // *gif.GIF or *WEBP.
|
||||
GetLoopCount() int // Number of times to loop the animation. 0 means infinite.
|
||||
ImageFrames
|
||||
}
|
||||
|
||||
// ImageFrames provides access to the frames of an animated image.
|
||||
type ImageFrames interface {
|
||||
GetFrames() []image.Image
|
||||
|
||||
// Frame durations in milliseconds.
|
||||
// Note that Gif frame durations are in 100ths of a second,
|
||||
// so they need to be multiplied by 10 to get milliseconds and vice versa.
|
||||
GetFrameDurations() []int
|
||||
|
||||
SetFrames(frames []image.Image)
|
||||
SetWidthHeight(width, height int)
|
||||
}
|
||||
|
||||
// ImageConfigProvider provides access to the image.Config of an image.
|
||||
type ImageConfigProvider interface {
|
||||
GetImageConfig() image.Config
|
||||
}
|
||||
|
||||
// FrameDurationsToGifDelays converts frame durations in milliseconds to
|
||||
// GIF delays in 100ths of a second.
|
||||
func FrameDurationsToGifDelays(frameDurations []int) []int {
|
||||
delays := make([]int, len(frameDurations))
|
||||
for i, fd := range frameDurations {
|
||||
delays[i] = fd / 10
|
||||
if delays[i] == 0 && fd > 0 {
|
||||
delays[i] = 1
|
||||
}
|
||||
}
|
||||
return delays
|
||||
}
|
||||
|
||||
// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to
|
||||
// frame durations in milliseconds.
|
||||
func GifDelaysToFrameDurations(delays []int) []int {
|
||||
frameDurations := make([]int, len(delays))
|
||||
for i, d := range delays {
|
||||
frameDurations[i] = d * 10
|
||||
}
|
||||
return frameDurations
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package hiter
|
||||
|
||||
// Common iterator functions.
|
||||
// Some of these are are based on this discsussion: https://github.com/golang/go/issues/61898
|
||||
|
||||
import "iter"
|
||||
|
||||
// Concat returns an iterator over the concatenation of the sequences.
|
||||
// Any nil sequences are ignored.
|
||||
func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
for _, seq := range seqs {
|
||||
if seq == nil {
|
||||
continue
|
||||
}
|
||||
for e := range seq {
|
||||
if !yield(e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concat2 returns an iterator over the concatenation of the sequences.
|
||||
// Any nil sequences are ignored.
|
||||
func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] {
|
||||
return func(yield func(K, V) bool) {
|
||||
for _, seq := range seqs {
|
||||
if seq == nil {
|
||||
continue
|
||||
}
|
||||
for k, v := range seq {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock returns an iterator that locks before iterating and unlocks after.
|
||||
func Lock[V any](seq iter.Seq[V], lock, unlock func()) iter.Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
lock()
|
||||
defer unlock()
|
||||
for e := range seq {
|
||||
if !yield(e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lock2 returns an iterator that locks before iterating and unlocks after.
|
||||
func Lock2[K, V any](seq iter.Seq2[K, V], lock, unlock func()) iter.Seq2[K, V] {
|
||||
return func(yield func(K, V) bool) {
|
||||
lock()
|
||||
defer unlock()
|
||||
for k, v := range seq {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -20,13 +20,27 @@ import (
|
||||
// Cache is a simple thread safe cache backed by a map.
|
||||
type Cache[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
opts CacheOptions
|
||||
hasBeenInitialized bool
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCache creates a new Cache.
|
||||
// CacheOptions are the options for the Cache.
|
||||
type CacheOptions struct {
|
||||
// If set, the cache will not grow beyond this size.
|
||||
Size uint64
|
||||
}
|
||||
|
||||
var defaultCacheOptions = CacheOptions{}
|
||||
|
||||
// NewCache creates a new Cache with default options.
|
||||
func NewCache[K comparable, T any]() *Cache[K, T] {
|
||||
return &Cache[K, T]{m: make(map[K]T)}
|
||||
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions}
|
||||
}
|
||||
|
||||
// NewCacheWithOptions creates a new Cache with the given options.
|
||||
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
|
||||
return &Cache[K, T]{m: make(map[K]T), opts: opts}
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the cache.
|
||||
@@ -65,6 +79,7 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
c.clearIfNeeded()
|
||||
c.m[key] = v
|
||||
return v, nil
|
||||
}
|
||||
@@ -127,7 +142,15 @@ func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) clearIfNeeded() {
|
||||
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
|
||||
// clear the map
|
||||
clear(c.m)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) set(key K, value T) {
|
||||
c.clearIfNeeded()
|
||||
c.m[key] = value
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCacheSize(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})
|
||||
|
||||
for i := range 30 {
|
||||
cache.Set(string(rune('a'+i)), "value")
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
for i := 20; i < 50; i++ {
|
||||
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
|
||||
return "value", nil
|
||||
})
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
for i := 100; i < 200; i++ {
|
||||
cache.SetIfAbsent(string(rune('a'+i)), "value")
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
cache.InitAndGet("foo", func(
|
||||
get func(key string) (string, bool), set func(key string, value string),
|
||||
) error {
|
||||
for i := 50; i < 100; i++ {
|
||||
set(string(rune('a'+i)), "value")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewMap[K comparable, T any]() *Map[K, T] {
|
||||
return &Map[K, T]{
|
||||
m: make(map[K]T),
|
||||
}
|
||||
}
|
||||
|
||||
// Map is a thread safe map backed by a Go map.
|
||||
type Map[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Get gets the value for the given key.
|
||||
// It returns the zero value of T if the key is not found.
|
||||
func (m *Map[K, T]) Get(key K) T {
|
||||
v, _ := m.Lookup(key)
|
||||
return v
|
||||
}
|
||||
|
||||
// Lookup looks up the given key in the map.
|
||||
// It returns the value and a boolean indicating whether the key was found.
|
||||
func (m *Map[K, T]) Lookup(key K) (T, bool) {
|
||||
m.mu.RLock()
|
||||
v, found := m.m[key]
|
||||
m.mu.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
v, found := m.Lookup(key)
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
v, found = m.m[key]
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
v, err := create()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
m.m[key] = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Set sets the given key to the given value.
|
||||
func (m *Map[K, T]) Set(key K, value T) {
|
||||
m.mu.Lock()
|
||||
m.m[key] = value
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the map.
|
||||
// It returns true if the key was found and deleted, false otherwise.
|
||||
func (m *Map[K, T]) Delete(key K) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; found {
|
||||
delete(m.m, key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithWriteLock executes the given function with a write lock on the map.
|
||||
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return f(m.m)
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
|
||||
// It returns true if the value was set, false otherwise.
|
||||
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
|
||||
m.mu.RLock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.mu.RUnlock()
|
||||
return m.doSetIfAbsent(key, value)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.m[key] = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// All returns an iterator over all key/value pairs in the map.
|
||||
// A read lock is held during the iteration.
|
||||
func (m *Map[K, T]) All() iter.Seq2[K, T] {
|
||||
return func(yield func(K, T) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for k, v := range m.m {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewMap[string, int]()
|
||||
|
||||
m.Set("b", 42)
|
||||
v, found := m.Lookup("b")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v = m.Get("b")
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v, found = m.Lookup("c")
|
||||
c.Assert(found, qt.Equals, false)
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v = m.Get("c")
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v, err := m.GetOrCreate("d", func() (int, error) {
|
||||
return 100, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
v, found = m.Lookup("d")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
v, err = m.GetOrCreate("d", func() (int, error) {
|
||||
return 200, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
wasSet := m.SetIfAbsent("e", 300)
|
||||
c.Assert(wasSet, qt.Equals, true)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
wasSet = m.SetIfAbsent("e", 400)
|
||||
c.Assert(wasSet, qt.Equals, false)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
m.WithWriteLock(func(m map[string]int) error {
|
||||
m["f"] = 500
|
||||
return nil
|
||||
})
|
||||
v, found = m.Lookup("f")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 500)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -98,6 +98,8 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
|
||||
return v, nil
|
||||
case Params:
|
||||
return []map[string]any{v}, nil
|
||||
case map[string]any:
|
||||
return []map[string]any{v}, nil
|
||||
case []any:
|
||||
var s []map[string]any
|
||||
for _, entry := range v {
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"slices"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
)
|
||||
|
||||
type OrderedIntSet struct {
|
||||
keys []int
|
||||
values *bitset.BitSet
|
||||
}
|
||||
|
||||
// NewOrderedIntSet creates a new OrderedIntSet.
|
||||
// Note that this is backed by https://github.com/bits-and-blooms/bitset
|
||||
func NewOrderedIntSet(vals ...int) *OrderedIntSet {
|
||||
m := &OrderedIntSet{
|
||||
keys: make([]int, 0, len(vals)),
|
||||
values: bitset.New(uint(len(vals))),
|
||||
}
|
||||
for _, v := range vals {
|
||||
m.Set(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Set sets the value for the given key.
|
||||
// Note that insertion order is not affected if a key is re-inserted into the set.
|
||||
func (m *OrderedIntSet) Set(key int) {
|
||||
if m == nil {
|
||||
panic("nil OrderedIntSet")
|
||||
}
|
||||
keyu := uint(key)
|
||||
if m.values.Test(keyu) {
|
||||
return
|
||||
}
|
||||
m.values.Set(keyu)
|
||||
m.keys = append(m.keys, key)
|
||||
}
|
||||
|
||||
// SetFrom sets the values from another OrderedIntSet.
|
||||
func (m *OrderedIntSet) SetFrom(other *OrderedIntSet) {
|
||||
if m == nil || other == nil {
|
||||
return
|
||||
}
|
||||
for _, key := range other.keys {
|
||||
m.Set(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Clone() *OrderedIntSet {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
newSet := &OrderedIntSet{
|
||||
keys: slices.Clone(m.keys),
|
||||
values: m.values.Clone(),
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
// Next returns the next key in the set possibly including the given key.
|
||||
// It returns -1 if the key is not found or if there are no keys greater than the given key.
|
||||
func (m *OrderedIntSet) Next(i int) int {
|
||||
n, ok := m.values.NextSet(uint(i))
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return int(n)
|
||||
}
|
||||
|
||||
// The reason we don't use iter.Seq is https://github.com/golang/go/issues/69015
|
||||
// This is 70% faster than using iter.Seq2[int, int] for the keys.
|
||||
// It returns false if the iteration was stopped early.
|
||||
func (m *OrderedIntSet) ForEachKey(yield func(int) bool) bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
for _, key := range m.keys {
|
||||
if !yield(key) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Has(key int) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
return m.values.Test(uint(key))
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Len() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(m.keys)
|
||||
}
|
||||
|
||||
// KeysSorted returns the keys in sorted order.
|
||||
func (m *OrderedIntSet) KeysSorted() []int {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
keys := slices.Clone(m.keys)
|
||||
slices.Sort(keys)
|
||||
return m.keys
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) String() string {
|
||||
if m == nil {
|
||||
return "[]"
|
||||
}
|
||||
return fmt.Sprintf("%v", m.keys)
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Values() *bitset.BitSet {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.values
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) IsSuperSet(other *OrderedIntSet) bool {
|
||||
if m == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
return m.values.IsSuperSet(other.values)
|
||||
}
|
||||
|
||||
// Words returns the bitset as array of 64-bit words, giving direct access to the internal representation.
|
||||
// It is not a copy, so changes to the returned slice will affect the bitset.
|
||||
// It is meant for advanced users.
|
||||
func (m *OrderedIntSet) Words() []uint64 {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.values.Words()
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestOrderedIntSet(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewOrderedIntSet(2, 1, 3, 7)
|
||||
|
||||
c.Assert(m.Len(), qt.Equals, 4)
|
||||
c.Assert(m.Has(1), qt.Equals, true)
|
||||
c.Assert(m.Has(4), qt.Equals, false)
|
||||
c.Assert(m.String(), qt.Equals, "[2 1 3 7]")
|
||||
m.Set(4)
|
||||
c.Assert(m.Len(), qt.Equals, 5)
|
||||
c.Assert(m.Has(4), qt.Equals, true)
|
||||
c.Assert(m.Next(0), qt.Equals, 1)
|
||||
c.Assert(m.Next(1), qt.Equals, 1)
|
||||
c.Assert(m.Next(2), qt.Equals, 2)
|
||||
c.Assert(m.Next(3), qt.Equals, 3)
|
||||
c.Assert(m.Next(4), qt.Equals, 4)
|
||||
c.Assert(m.Next(7), qt.Equals, 7)
|
||||
c.Assert(m.Next(8), qt.Equals, -1)
|
||||
c.Assert(m.String(), qt.Equals, "[2 1 3 7 4]")
|
||||
|
||||
var nilset *OrderedIntSet
|
||||
c.Assert(nilset.Len(), qt.Equals, 0)
|
||||
c.Assert(nilset.Has(1), qt.Equals, false)
|
||||
c.Assert(nilset.String(), qt.Equals, "[]")
|
||||
|
||||
var collected []int
|
||||
m.ForEachKey(func(key int) bool {
|
||||
collected = append(collected, key)
|
||||
return true
|
||||
})
|
||||
c.Assert(collected, qt.DeepEquals, []int{2, 1, 3, 7, 4})
|
||||
}
|
||||
|
||||
func BenchmarkOrderedIntSet(b *testing.B) {
|
||||
smallSet := NewOrderedIntSet()
|
||||
for i := range 8 {
|
||||
smallSet.Set(i)
|
||||
}
|
||||
mediumSet := NewOrderedIntSet()
|
||||
for i := range 64 {
|
||||
mediumSet.Set(i)
|
||||
}
|
||||
largeSet := NewOrderedIntSet()
|
||||
for i := range 1024 {
|
||||
largeSet.Set(i)
|
||||
}
|
||||
|
||||
b.Run("New", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
NewOrderedIntSet(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Has small", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
smallSet.Has(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Has medium", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
mediumSet.Has(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Next", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
mediumSet.Next(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey small", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
smallSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey medium", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
mediumSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey large", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
largeSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,9 +11,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -40,6 +41,14 @@ func (p Params) GetNested(indices ...string) any {
|
||||
// SetParams overwrites values in dst with values in src for common or new keys.
|
||||
// This is done recursively.
|
||||
func SetParams(dst, src Params) {
|
||||
setParams(dst, src, 0)
|
||||
}
|
||||
|
||||
func setParams(dst, src Params, depth int) {
|
||||
const maxDepth = 1000
|
||||
if depth > maxDepth {
|
||||
panic(errors.New("max depth exceeded"))
|
||||
}
|
||||
for k, v := range src {
|
||||
vv, found := dst[k]
|
||||
if !found {
|
||||
@@ -48,7 +57,7 @@ func SetParams(dst, src Params) {
|
||||
switch vvv := vv.(type) {
|
||||
case Params:
|
||||
if pv, ok := v.(Params); ok {
|
||||
SetParams(vvv, pv)
|
||||
setParams(vvv, pv, depth+1)
|
||||
} else {
|
||||
dst[k] = v
|
||||
}
|
||||
@@ -101,7 +110,6 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
|
||||
|
||||
for k, v := range pp {
|
||||
|
||||
if k == MergeStrategyKey {
|
||||
continue
|
||||
}
|
||||
@@ -179,6 +187,56 @@ func getNested(m map[string]any, indices []string) (any, string, map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNestedParamsFromSegements creates empty nested maps for the given keySegments in the target map.
|
||||
func CreateNestedParamsFromSegements(target Params, keySegments ...string) Params {
|
||||
if len(keySegments) == 0 {
|
||||
return target
|
||||
}
|
||||
|
||||
m := target
|
||||
for i, key := range keySegments {
|
||||
v, found := m[key]
|
||||
if !found {
|
||||
nm := Params{}
|
||||
m[key] = nm
|
||||
m = nm
|
||||
if i == len(keySegments)-1 {
|
||||
return nm
|
||||
}
|
||||
continue
|
||||
}
|
||||
m = v.(Params)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// CreateNestedParamsSepString creates empty nested maps for the given keyStr in the target map
|
||||
// It returns the last map created.
|
||||
func CreateNestedParamsSepString(keyStr, separator string, target Params) Params {
|
||||
keySegments := strings.Split(keyStr, separator)
|
||||
return CreateNestedParamsFromSegements(target, keySegments...)
|
||||
}
|
||||
|
||||
// SetNestedParamIfNotSet sets the value for the given keyStr in the target map if it does not exist.
|
||||
// It assumes that all but the last key in keyStr is a Params map or should be one.
|
||||
func SetNestedParamIfNotSet(keyStr, separator string, value any, target Params) Params {
|
||||
keySegments := strings.Split(keyStr, separator)
|
||||
if len(keySegments) == 0 {
|
||||
return target
|
||||
}
|
||||
base := keySegments[:len(keySegments)-1]
|
||||
last := keySegments[len(keySegments)-1]
|
||||
|
||||
m := CreateNestedParamsFromSegements(target, base...)
|
||||
|
||||
if _, ok := m[last]; !ok {
|
||||
m[last] = value
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
// GetNestedParam gets the first match of the keyStr in the candidates given.
|
||||
// It will first try the exact match and then try to find it as a nested map value,
|
||||
// using the given separator, e.g. "mymap.name".
|
||||
@@ -266,9 +324,17 @@ func CleanConfigStringMapString(m map[string]string) map[string]string {
|
||||
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
|
||||
// map[string]any.
|
||||
func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
return doCleanConfigStringMap(m, 0)
|
||||
}
|
||||
|
||||
func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
const maxDepth = 1000
|
||||
if depth > maxDepth {
|
||||
panic(errors.New("max depth exceeded"))
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
return m
|
||||
}
|
||||
@@ -280,9 +346,9 @@ func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
}
|
||||
switch v2 := v.(type) {
|
||||
case map[string]any:
|
||||
m2[k] = CleanConfigStringMap(v2)
|
||||
m2[k] = doCleanConfigStringMap(v2, depth+1)
|
||||
case Params:
|
||||
var p Params = CleanConfigStringMap(v2)
|
||||
var p Params = doCleanConfigStringMap(v2, depth+1)
|
||||
m2[k] = p
|
||||
case map[string]string:
|
||||
m2[k] = CleanConfigStringMapString(v2)
|
||||
@@ -344,6 +410,29 @@ func PrepareParams(m Params) {
|
||||
}
|
||||
}
|
||||
|
||||
// CloneParamsDeep does a deep clone of the given Params,
|
||||
// meaning that any nested Params will be cloned as well.
|
||||
func CloneParamsDeep(m Params) Params {
|
||||
return cloneParamsDeep(m, 0)
|
||||
}
|
||||
|
||||
func cloneParamsDeep(m Params, depth int) Params {
|
||||
const maxDepth = 1000
|
||||
if depth > maxDepth {
|
||||
panic(errors.New("max depth exceeded"))
|
||||
}
|
||||
m2 := make(Params)
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case Params:
|
||||
m2[k] = cloneParamsDeep(vv, depth+1)
|
||||
default:
|
||||
m2[k] = v
|
||||
}
|
||||
}
|
||||
return m2
|
||||
}
|
||||
|
||||
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
|
||||
func PrepareParamsClone(m Params) Params {
|
||||
m2 := make(Params)
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -167,3 +167,35 @@ func TestParamsIsZero(t *testing.T) {
|
||||
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
|
||||
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue)
|
||||
}
|
||||
|
||||
func TestSetNestedParamIfNotSet(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := Params{}
|
||||
|
||||
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
|
||||
c.Assert(m, qt.DeepEquals, Params{
|
||||
"a": Params{
|
||||
"b": Params{
|
||||
"c": "value",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
m = Params{
|
||||
"a": Params{
|
||||
"b": Params{
|
||||
"c": "existingValue",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
|
||||
c.Assert(m, qt.DeepEquals, Params{
|
||||
"a": Params{
|
||||
"b": Params{
|
||||
"c": "existingValue",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
typeInt64 = reflect.TypeFor[int64]()
|
||||
typeFloat64 = reflect.TypeFor[float64]()
|
||||
typeString = reflect.TypeFor[string]()
|
||||
)
|
||||
|
||||
// ToInt64 converts v to int64 if possible, returning an error if not.
|
||||
func ToInt64E(v reflect.Value) (int64, error) {
|
||||
if v, ok := ConvertIfPossible(v, typeInt64); ok {
|
||||
return v.Int(), nil
|
||||
}
|
||||
return 0, errConvert(v, "int64")
|
||||
}
|
||||
|
||||
// ToInt64 converts v to int64 if possible. It panics if the conversion is not possible.
|
||||
func ToInt64(v reflect.Value) int64 {
|
||||
vv, err := ToInt64E(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToFloat64E converts v to float64 if possible, returning an error if not.
|
||||
func ToFloat64E(v reflect.Value) (float64, error) {
|
||||
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
|
||||
return v.Float(), nil
|
||||
}
|
||||
return 0, errConvert(v, "float64")
|
||||
}
|
||||
|
||||
// ToFloat64 converts v to float64 if possible, panicking if not.
|
||||
func ToFloat64(v reflect.Value) float64 {
|
||||
vv, err := ToFloat64E(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToStringE converts v to string if possible, returning an error if not.
|
||||
func ToStringE(v reflect.Value) (string, error) {
|
||||
vv, err := ToStringValueE(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return vv.String(), nil
|
||||
}
|
||||
|
||||
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
|
||||
if v, ok := ConvertIfPossible(v, typeString); ok {
|
||||
return v, nil
|
||||
}
|
||||
return reflect.Value{}, errConvert(v, "string")
|
||||
}
|
||||
|
||||
// ToString converts v to string if possible, panicking if not.
|
||||
func ToString(v reflect.Value) string {
|
||||
vv, err := ToStringE(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vv
|
||||
}
|
||||
|
||||
func errConvert(v reflect.Value, s string) error {
|
||||
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
|
||||
}
|
||||
|
||||
// ConvertIfPossible tries to convert val to typ if possible.
|
||||
// This is currently only implemented for int kinds,
|
||||
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
|
||||
// We can expand on this later if needed.
|
||||
// This conversion is lossless.
|
||||
// See Issue 14079.
|
||||
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
switch val.Kind() {
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
if val.IsNil() {
|
||||
// Return typ's zero value.
|
||||
return reflect.Zero(typ), true
|
||||
}
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Type().AssignableTo(typ) {
|
||||
// No conversion needed.
|
||||
return val, true
|
||||
}
|
||||
|
||||
if IsInt(typ.Kind()) {
|
||||
return convertToIntIfPossible(val, typ)
|
||||
}
|
||||
if IsFloat(typ.Kind()) {
|
||||
return convertToFloatIfPossible(val, typ)
|
||||
}
|
||||
if IsUint(typ.Kind()) {
|
||||
return convertToUintIfPossible(val, typ)
|
||||
}
|
||||
if IsString(typ.Kind()) && IsString(val.Kind()) {
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
if IsInt(val.Kind()) {
|
||||
i := val.Int()
|
||||
if i < 0 {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
u := uint64(i)
|
||||
if typ.OverflowUint(u) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(u).Convert(typ), true
|
||||
}
|
||||
if IsUint(val.Kind()) {
|
||||
if typ.OverflowUint(val.Uint()) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
if IsFloat(val.Kind()) {
|
||||
f := val.Float()
|
||||
if f < 0 || f > float64(math.MaxUint64) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if f != math.Trunc(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
u := uint64(f)
|
||||
if typ.OverflowUint(u) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(u).Convert(typ), true
|
||||
}
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
if IsInt(val.Kind()) {
|
||||
i := val.Int()
|
||||
f := float64(i)
|
||||
if typ.OverflowFloat(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(f).Convert(typ), true
|
||||
}
|
||||
if IsUint(val.Kind()) {
|
||||
u := val.Uint()
|
||||
f := float64(u)
|
||||
if typ.OverflowFloat(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(f).Convert(typ), true
|
||||
}
|
||||
if IsFloat(val.Kind()) {
|
||||
if typ.OverflowFloat(val.Float()) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
|
||||
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
if IsInt(val.Kind()) {
|
||||
if typ.OverflowInt(val.Int()) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
if IsUint(val.Kind()) {
|
||||
if val.Uint() > uint64(math.MaxInt64) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if typ.OverflowInt(int64(val.Uint())) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return val.Convert(typ), true
|
||||
}
|
||||
if IsFloat(val.Kind()) {
|
||||
f := val.Float()
|
||||
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if f != math.Trunc(f) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
if typ.OverflowInt(int64(f)) {
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
return reflect.ValueOf(int64(f)).Convert(typ), true
|
||||
|
||||
}
|
||||
|
||||
return reflect.Value{}, false
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
)
|
||||
|
||||
func TestToFuncs(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToInt64(reflect.ValueOf(int(42))), qt.Equals, int64(42))
|
||||
c.Assert(ToFloat64(reflect.ValueOf(float32(3.14))), hqt.IsSameFloat64, float64(3.14))
|
||||
c.Assert(ToString(reflect.ValueOf("hello")), qt.Equals, "hello")
|
||||
}
|
||||
|
||||
func TestConvertIfPossible(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value any
|
||||
typ any
|
||||
expected any
|
||||
ok bool
|
||||
}{
|
||||
// From uint to int.
|
||||
{
|
||||
name: "uint64(math.MaxUint64) to int16",
|
||||
value: uint64(math.MaxUint64),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
|
||||
{
|
||||
name: "uint64(math.MaxUint64) to int64",
|
||||
value: uint64(math.MaxUint64),
|
||||
typ: int64(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "uint64(math.MaxInt16) to int16",
|
||||
value: uint64(math.MaxInt16),
|
||||
typ: int64(0),
|
||||
ok: true,
|
||||
expected: int64(math.MaxInt16),
|
||||
},
|
||||
// From int to int.
|
||||
{
|
||||
name: "int64(math.MaxInt64) to int16",
|
||||
value: int64(math.MaxInt64),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "int64(math.MaxInt16) to int",
|
||||
value: int64(math.MaxInt16),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(math.MaxInt16),
|
||||
},
|
||||
|
||||
{
|
||||
name: "int64(math.MaxInt16) to int",
|
||||
value: int64(math.MaxInt16),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(math.MaxInt16),
|
||||
},
|
||||
// From float64 to int.
|
||||
{
|
||||
name: "float64(1.5) to int",
|
||||
value: float64(1.5),
|
||||
typ: int(0),
|
||||
ok: false, // loss of precision
|
||||
},
|
||||
{
|
||||
name: "float64(1.0) to int",
|
||||
value: float64(1.0),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(1),
|
||||
},
|
||||
{
|
||||
name: "float64(math.MaxInt16+1) to int16",
|
||||
value: float64(math.MaxInt16 + 1),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float64(math.MaxFloat64) to int64",
|
||||
value: float64(math.MaxFloat64),
|
||||
typ: int64(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float64(32767) to int16",
|
||||
value: float64(32767),
|
||||
typ: int16(0),
|
||||
ok: true,
|
||||
expected: int16(32767),
|
||||
},
|
||||
// From float32 to int.
|
||||
{
|
||||
name: "float32(1.5) to int",
|
||||
value: float32(1.5),
|
||||
typ: int(0),
|
||||
ok: false, // loss of precision
|
||||
},
|
||||
{
|
||||
name: "float32(1.0) to int",
|
||||
value: float32(1.0),
|
||||
typ: int(0),
|
||||
ok: true,
|
||||
expected: int(1),
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxFloat32) to int16",
|
||||
value: float32(math.MaxFloat32),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxFloat32) to int64",
|
||||
value: float32(math.MaxFloat32),
|
||||
typ: int64(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxInt16) to int16",
|
||||
value: float32(math.MaxInt16),
|
||||
typ: int16(0),
|
||||
ok: true,
|
||||
expected: int16(32767),
|
||||
},
|
||||
{
|
||||
name: "float32(math.MaxInt16+1) to int16",
|
||||
value: float32(math.MaxInt16 + 1),
|
||||
typ: int16(0),
|
||||
ok: false, // overflow
|
||||
},
|
||||
// Int to float.
|
||||
{
|
||||
name: "int16(32767) to float32",
|
||||
value: int16(32767),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(32767),
|
||||
},
|
||||
{
|
||||
name: "int64(32767) to float32",
|
||||
value: int64(32767),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(32767),
|
||||
},
|
||||
{
|
||||
name: "int64(math.MaxInt64) to float32",
|
||||
value: int64(math.MaxInt64),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(math.MaxInt64),
|
||||
},
|
||||
{
|
||||
name: "int64(math.MaxInt64) to float64",
|
||||
value: int64(math.MaxInt64),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(math.MaxInt64),
|
||||
},
|
||||
// Int to uint.
|
||||
{
|
||||
name: "int16(32767) to uint16",
|
||||
value: int16(32767),
|
||||
typ: uint16(0),
|
||||
ok: true,
|
||||
expected: uint16(32767),
|
||||
},
|
||||
{
|
||||
name: "int16(32767) to uint8",
|
||||
value: int16(32767),
|
||||
typ: uint8(0),
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "float64(3.14) to uint64",
|
||||
value: float64(3.14),
|
||||
typ: uint64(0),
|
||||
ok: false,
|
||||
},
|
||||
{
|
||||
name: "float64(3.0) to uint64",
|
||||
value: float64(3.0),
|
||||
typ: uint64(0),
|
||||
ok: true,
|
||||
expected: uint64(3),
|
||||
},
|
||||
// From uint to float.
|
||||
{
|
||||
name: "uint64(math.MaxInt16) to float64",
|
||||
value: uint64(math.MaxInt16),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(math.MaxInt16),
|
||||
},
|
||||
// Float to float.
|
||||
{
|
||||
name: "float64(3.14) to float32",
|
||||
value: float64(3.14),
|
||||
typ: float32(0),
|
||||
ok: true,
|
||||
expected: float32(3.14),
|
||||
},
|
||||
{
|
||||
name: "float32(3.14) to float64",
|
||||
value: float32(3.14),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(3.14),
|
||||
},
|
||||
{
|
||||
name: "float64(3.14) to float64",
|
||||
value: float64(3.14),
|
||||
typ: float64(0),
|
||||
ok: true,
|
||||
expected: float64(3.14),
|
||||
},
|
||||
} {
|
||||
|
||||
v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ))
|
||||
c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name))
|
||||
if test.ok {
|
||||
c.Assert(v.Interface(), hqt.IsSameNumber, test.expected, qt.Commentf("test case: %s", test.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertIfPossibleMisc(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
type s string
|
||||
|
||||
var (
|
||||
i = int32(42)
|
||||
i64 = int64(i)
|
||||
iv any = i
|
||||
ip = &i
|
||||
inil any = (*int32)(nil)
|
||||
shello = s("hello")
|
||||
)
|
||||
|
||||
convertOK := func(v any, typ any) any {
|
||||
rv, ok := ConvertIfPossible(reflect.ValueOf(v), reflect.TypeOf(typ))
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
return rv.Interface()
|
||||
}
|
||||
|
||||
c.Assert(convertOK(shello, ""), qt.Equals, "hello")
|
||||
c.Assert(convertOK(ip, int64(0)), qt.Equals, i64)
|
||||
c.Assert(convertOK(iv, int64(0)), qt.Equals, i64)
|
||||
c.Assert(convertOK(inil, int64(0)), qt.Equals, int64(0))
|
||||
}
|
||||
|
||||
func BenchmarkToInt64(b *testing.B) {
|
||||
v := reflect.ValueOf(int(42))
|
||||
for b.Loop() {
|
||||
ToInt64(v)
|
||||
}
|
||||
}
|
||||
+102
-20
@@ -1,6 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -22,11 +20,16 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
// IsInterfaceOrPointer returns whether the given kind is an interface or a pointer.
|
||||
func IsInterfaceOrPointer(kind reflect.Kind) bool {
|
||||
return kind == reflect.Interface || kind == reflect.Ptr
|
||||
}
|
||||
|
||||
// TODO(bep) replace the private versions in /tpl with these.
|
||||
// IsNumber returns whether the given kind is a number.
|
||||
func IsNumber(kind reflect.Kind) bool {
|
||||
@@ -63,6 +66,11 @@ func IsFloat(kind reflect.Kind) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsString returns whether the given kind is a string.
|
||||
func IsString(kind reflect.Kind) bool {
|
||||
return kind == reflect.String
|
||||
}
|
||||
|
||||
// IsTruthful returns whether in represents a truthful value.
|
||||
// See IsTruthfulValue
|
||||
func IsTruthful(in any) bool {
|
||||
@@ -86,6 +94,18 @@ func IsSlice(v any) bool {
|
||||
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
|
||||
var isZeroCache sync.Map
|
||||
|
||||
func implementsIsZero(tp reflect.Type) bool {
|
||||
v, ok := isZeroCache.Load(tp)
|
||||
if ok {
|
||||
return v.(bool)
|
||||
}
|
||||
implements := tp.Implements(zeroType)
|
||||
isZeroCache.Store(tp, implements)
|
||||
return implements
|
||||
}
|
||||
|
||||
// IsTruthfulValue returns whether the given value has a meaningful truth value.
|
||||
// This is based on template.IsTrue in Go's stdlib, but also considers
|
||||
// IsZero and any interface value will be unwrapped before it's considered
|
||||
@@ -94,14 +114,18 @@ var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
// Based on:
|
||||
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
|
||||
func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
val = indirectInterface(val)
|
||||
val, isNil := Indirect(val)
|
||||
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
// Something like: var x any, never set. It's a form of nil.
|
||||
return
|
||||
}
|
||||
|
||||
if val.Type().Implements(zeroType) {
|
||||
if val.Kind() == reflect.Pointer && isNil {
|
||||
return
|
||||
}
|
||||
|
||||
if implementsIsZero(val.Type()) {
|
||||
return !val.Interface().(types.Zeroer).IsZero()
|
||||
}
|
||||
|
||||
@@ -134,10 +158,31 @@ type methodKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var methodCache sync.Map
|
||||
var (
|
||||
methodIndexCache sync.Map
|
||||
methodCache sync.Map
|
||||
)
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
|
||||
// type lookup.
|
||||
// GetMethodByNameForType returns the method with the given name for the given type,
|
||||
// or a zero Method if no such method exists.
|
||||
// It panics if tp is an interface type.
|
||||
// It caches the lookup.
|
||||
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
|
||||
if tp.Kind() == reflect.Interface {
|
||||
// Func field is nil for interface types.
|
||||
panic("not supported for interface types")
|
||||
}
|
||||
k := methodKey{tp, name}
|
||||
v, found := methodCache.Load(k)
|
||||
if found {
|
||||
return v.(reflect.Method)
|
||||
}
|
||||
m, _ := tp.MethodByName(name)
|
||||
methodCache.Store(k, m)
|
||||
return m
|
||||
}
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
|
||||
func GetMethodByName(v reflect.Value, name string) reflect.Value {
|
||||
index := GetMethodIndexByName(v.Type(), name)
|
||||
|
||||
@@ -152,7 +197,7 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
|
||||
// -1 if no such method exists.
|
||||
func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
k := methodKey{tp, name}
|
||||
v, found := methodCache.Load(k)
|
||||
v, found := methodIndexCache.Load(k)
|
||||
if found {
|
||||
return v.(int)
|
||||
}
|
||||
@@ -161,7 +206,7 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
if !ok {
|
||||
index = -1
|
||||
}
|
||||
methodCache.Store(k, index)
|
||||
methodIndexCache.Store(k, index)
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
@@ -243,6 +288,7 @@ func ToSliceAny(v any) ([]any, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// CallMethodByName calls the method with the given name on v.
|
||||
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
|
||||
fn := v.MethodByName(name)
|
||||
var args []reflect.Value
|
||||
@@ -260,20 +306,56 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
|
||||
return fn.Call(args)
|
||||
}
|
||||
|
||||
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
|
||||
func indirectInterface(v reflect.Value) reflect.Value {
|
||||
if v.Kind() != reflect.Interface {
|
||||
return v
|
||||
// Indirect unwraps interfaces and pointers until it finds a non-interface/pointer value.
|
||||
// If a nil is encountered, the second return value is true.
|
||||
// If a pointer to a struct is encountered, it is not unwrapped.
|
||||
func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
|
||||
for ; IsInterfaceOrPointer(v.Kind()); v = v.Elem() {
|
||||
if IsNil(v) {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() != reflect.Interface {
|
||||
// A pointer.
|
||||
if v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
if v.Elem().Kind() == reflect.Struct {
|
||||
// Avoid unwrapping pointers to structs.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.IsNil() {
|
||||
return reflect.Value{}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
|
||||
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
|
||||
vv, isNil = Indirect(v)
|
||||
if isNil {
|
||||
return vv, isNil
|
||||
}
|
||||
return v.Elem()
|
||||
if vv.Kind() == reflect.Pointer {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
return vv, isNil
|
||||
}
|
||||
|
||||
// IsNil reports whether v is nil.
|
||||
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
|
||||
func IsNil(v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return true
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
var isContextCache = maps.NewCache[reflect.Type, bool]()
|
||||
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
|
||||
|
||||
type k string
|
||||
|
||||
|
||||
+126
-16
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,15 +20,32 @@ import (
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
)
|
||||
|
||||
type zeroStruct struct {
|
||||
zero bool
|
||||
}
|
||||
|
||||
func (z zeroStruct) IsZero() bool {
|
||||
return z.zero
|
||||
}
|
||||
|
||||
func TestIsTruthful(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var nilpointerZero *zeroStruct
|
||||
|
||||
c.Assert(IsTruthful(true), qt.Equals, true)
|
||||
c.Assert(IsTruthful(false), qt.Equals, false)
|
||||
c.Assert(IsTruthful(time.Now()), qt.Equals, true)
|
||||
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
|
||||
c.Assert(IsTruthful(&zeroStruct{zero: false}), qt.Equals, true)
|
||||
c.Assert(IsTruthful(&zeroStruct{zero: true}), qt.Equals, false)
|
||||
c.Assert(IsTruthful(zeroStruct{zero: false}), qt.Equals, true)
|
||||
c.Assert(IsTruthful(zeroStruct{zero: true}), qt.Equals, false)
|
||||
c.Assert(IsTruthful(nil), qt.Equals, false)
|
||||
c.Assert(IsTruthful(nilpointerZero), qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestGetMethodByName(t *testing.T) {
|
||||
@@ -63,18 +80,90 @@ func TestToSliceAny(t *testing.T) {
|
||||
checkOK([]int{1, 2, 3}, []any{1, 2, 3})
|
||||
}
|
||||
|
||||
type testIndirectStruct struct {
|
||||
S string
|
||||
}
|
||||
|
||||
func (t *testIndirectStruct) GetS() string {
|
||||
return t.S
|
||||
}
|
||||
|
||||
func (t testIndirectStruct) Foo() string {
|
||||
return "bar"
|
||||
}
|
||||
|
||||
type testIndirectStructNoMethods struct {
|
||||
S string
|
||||
}
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var (
|
||||
nilPtr *testIndirectStruct
|
||||
nilIface any = nil
|
||||
nonNilIface any = &testIndirectStruct{S: "hello"}
|
||||
)
|
||||
|
||||
c.Assert(IsNil(reflect.ValueOf(nilPtr)), qt.Equals, true)
|
||||
c.Assert(IsNil(reflect.ValueOf(nilIface)), qt.Equals, true)
|
||||
c.Assert(IsNil(reflect.ValueOf(nonNilIface)), qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestIndirectInterface(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var (
|
||||
structWithMethods = testIndirectStruct{S: "hello"}
|
||||
structWithMethodsPointer = &testIndirectStruct{S: "hello"}
|
||||
structWithMethodsPointerAny any = structWithMethodsPointer
|
||||
structPointerToPointer = &structWithMethodsPointer
|
||||
structNoMethodsPtr = &testIndirectStructNoMethods{S: "no methods"}
|
||||
structNoMethods = testIndirectStructNoMethods{S: "no methods"}
|
||||
intValue = 32
|
||||
intPtr = &intValue
|
||||
nilPtr *testIndirectStruct
|
||||
nilIface any = nil
|
||||
)
|
||||
|
||||
ind := func(v any) any {
|
||||
c.Helper()
|
||||
vv, isNil := Indirect(reflect.ValueOf(v))
|
||||
c.Assert(isNil, qt.IsFalse)
|
||||
return vv.Interface()
|
||||
}
|
||||
|
||||
c.Assert(ind(intValue), hqt.IsSameType, 32)
|
||||
c.Assert(ind(intPtr), hqt.IsSameType, 32)
|
||||
c.Assert(ind(structNoMethodsPtr), hqt.IsSameType, structNoMethodsPtr)
|
||||
c.Assert(ind(structWithMethods), hqt.IsSameType, structWithMethods)
|
||||
c.Assert(ind(structNoMethods), hqt.IsSameType, structNoMethods)
|
||||
c.Assert(ind(structPointerToPointer), hqt.IsSameType, &testIndirectStruct{})
|
||||
c.Assert(ind(structWithMethodsPointer), hqt.IsSameType, &testIndirectStruct{})
|
||||
c.Assert(ind(structWithMethodsPointerAny), hqt.IsSameType, structWithMethodsPointer)
|
||||
|
||||
vv, isNil := Indirect(reflect.ValueOf(nilPtr))
|
||||
c.Assert(isNil, qt.IsTrue)
|
||||
c.Assert(vv, qt.Equals, reflect.ValueOf(nilPtr))
|
||||
|
||||
vv, isNil = Indirect(reflect.ValueOf(nilIface))
|
||||
c.Assert(isNil, qt.IsFalse)
|
||||
c.Assert(vv, qt.Equals, reflect.ValueOf(nilIface))
|
||||
}
|
||||
|
||||
func BenchmarkIsContextType(b *testing.B) {
|
||||
const size = 1000
|
||||
type k string
|
||||
b.Run("value", func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
ctxs := make([]reflect.Type, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
ctxs := make([]reflect.Type, size)
|
||||
for i := range size {
|
||||
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxs[i]) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
idx := i % size
|
||||
if !IsContextType(ctxs[idx]) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
}
|
||||
@@ -82,7 +171,7 @@ func BenchmarkIsContextType(b *testing.B) {
|
||||
|
||||
b.Run("background", func(b *testing.B) {
|
||||
var ctxt reflect.Type = reflect.TypeOf(context.Background())
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
if !IsContextType(ctxt) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
@@ -90,14 +179,25 @@ func BenchmarkIsContextType(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
func BenchmarkIsTruthFulValue(b *testing.B) {
|
||||
var (
|
||||
stringHugo = reflect.ValueOf("Hugo")
|
||||
stringEmpty = reflect.ValueOf("")
|
||||
zero = reflect.ValueOf(time.Time{})
|
||||
timeNow = reflect.ValueOf(time.Now())
|
||||
boolTrue = reflect.ValueOf(true)
|
||||
boolFalse = reflect.ValueOf(false)
|
||||
nilPointer = reflect.ValueOf((*zeroStruct)(nil))
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsTruthfulValue(v) {
|
||||
b.Fatal("not truthful")
|
||||
}
|
||||
for b.Loop() {
|
||||
IsTruthfulValue(stringHugo)
|
||||
IsTruthfulValue(stringEmpty)
|
||||
IsTruthfulValue(zero)
|
||||
IsTruthfulValue(timeNow)
|
||||
IsTruthfulValue(boolTrue)
|
||||
IsTruthfulValue(boolFalse)
|
||||
IsTruthfulValue(nilPointer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +223,22 @@ func (t *testStruct) Method5() string {
|
||||
return "Hugo"
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByNameForType(b *testing.B) {
|
||||
tp := reflect.TypeFor[*testStruct]()
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
for b.Loop() {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByNameForType(tp, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByName(b *testing.B) {
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByName(v, method)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hstore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hstore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -214,8 +214,8 @@ func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
func BenchmarkScratchGet(b *testing.B) {
|
||||
scratch := NewScratch()
|
||||
scratch.Add("A", 1)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
scratch.Get("A")
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -96,6 +97,16 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
return reCache.getOrCompileRegexp(pattern)
|
||||
}
|
||||
|
||||
// HasAnyPrefix checks if the string s has any of the prefixes given.
|
||||
func HasAnyPrefix(s string, prefixes ...string) bool {
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(s, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
@@ -128,7 +139,61 @@ func ToString(v any) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
type (
|
||||
Strings2 [2]string
|
||||
Strings3 [3]string
|
||||
)
|
||||
// UniqueStrings returns a new slice with any duplicates removed.
|
||||
func UniqueStrings(s []string) []string {
|
||||
unique := make([]string, 0, len(s))
|
||||
for i, val := range s {
|
||||
var seen bool
|
||||
for j := range i {
|
||||
if s[j] == val {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
unique = append(unique, val)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// UniqueStringsReuse returns a slice with any duplicates removed.
|
||||
// It will modify the input slice.
|
||||
func UniqueStringsReuse(s []string) []string {
|
||||
result := s[:0]
|
||||
for i, val := range s {
|
||||
var seen bool
|
||||
|
||||
for j := range i {
|
||||
if s[j] == val {
|
||||
seen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !seen {
|
||||
result = append(result, val)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqueStringsSorted returns a sorted slice with any duplicates removed.
|
||||
// It will modify the input slice.
|
||||
func UniqueStringsSorted(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
ss := sort.StringSlice(s)
|
||||
ss.Sort()
|
||||
i := 0
|
||||
for j := 1; j < len(s); j++ {
|
||||
if !ss.Less(i, j) {
|
||||
continue
|
||||
}
|
||||
i++
|
||||
s[i] = s[j]
|
||||
}
|
||||
|
||||
return s[:i+1]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,7 @@
|
||||
package hstrings
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@@ -43,14 +44,91 @@ func TestGetOrCompileRegexp(t *testing.T) {
|
||||
c.Assert(re.MatchString("123"), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestUniqueStrings(t *testing.T) {
|
||||
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
|
||||
output := UniqueStrings(in)
|
||||
expected := []string{"a", "b", "c", "", "d"}
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Errorf("Expected %#v, got %#v\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueStringsReuse(t *testing.T) {
|
||||
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
|
||||
output := UniqueStringsReuse(in)
|
||||
expected := []string{"a", "b", "c", "", "d"}
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Errorf("Expected %#v, got %#v\n", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueStringsSorted(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
|
||||
output := UniqueStringsSorted(in)
|
||||
expected := []string{"", "a", "b", "c", "d"}
|
||||
c.Assert(output, qt.DeepEquals, expected)
|
||||
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
// Note that these cannot use b.Loop() because of golang/go#27217.
|
||||
func BenchmarkUniqueStrings(b *testing.B) {
|
||||
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
|
||||
|
||||
b.Run("Safe", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
result := UniqueStrings(input)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Reuse slice", func(b *testing.B) {
|
||||
inputs := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := make([]string, len(input))
|
||||
copy(inputc, input)
|
||||
inputs[i] = inputc
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := inputs[i]
|
||||
|
||||
result := UniqueStringsReuse(inputc)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Reuse slice sorted", func(b *testing.B) {
|
||||
inputs := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := make([]string, len(input))
|
||||
copy(inputc, input)
|
||||
inputs[i] = inputc
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := inputs[i]
|
||||
|
||||
result := UniqueStringsSorted(inputc)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkGetOrCompileRegexp(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
GetOrCompileRegexp(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompileRegexp(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
regexp.MustCompile(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// OnceMore is similar to sync.Once.
|
||||
//
|
||||
// Additional features are:
|
||||
// * it can be reset, so the action can be repeated if needed
|
||||
// * it has methods to check if it's done or in progress
|
||||
type OnceMore struct {
|
||||
_ doNotCopy
|
||||
done atomic.Bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *OnceMore) Do(f func()) {
|
||||
if t.Done() {
|
||||
return
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
// Double check
|
||||
if t.Done() {
|
||||
return
|
||||
}
|
||||
|
||||
defer t.done.Store(true)
|
||||
f()
|
||||
}
|
||||
|
||||
func (t *OnceMore) Done() bool {
|
||||
return t.done.Load()
|
||||
}
|
||||
|
||||
func (t *OnceMore) Reset() {
|
||||
t.mu.Lock()
|
||||
t.done.Store(false)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
type ValueResetter[T any] struct {
|
||||
reset func()
|
||||
f func(context.Context) T
|
||||
}
|
||||
|
||||
func (v *ValueResetter[T]) Value(ctx context.Context) T {
|
||||
return v.f(ctx)
|
||||
}
|
||||
|
||||
func (v *ValueResetter[T]) Reset() {
|
||||
v.reset()
|
||||
}
|
||||
|
||||
// OnceMoreValue returns a function that invokes f only once and returns the value
|
||||
// returned by f. The returned function may be called concurrently.
|
||||
//
|
||||
// If f panics, the returned function will panic with the same value on every call.
|
||||
func OnceMoreValue[T any](f func(context.Context) T) ValueResetter[T] {
|
||||
v := struct {
|
||||
f func(context.Context) T
|
||||
once OnceMore
|
||||
ok bool
|
||||
p any
|
||||
result T
|
||||
}{
|
||||
f: f,
|
||||
}
|
||||
ff := func(ctx context.Context) T {
|
||||
v.once.Do(func() {
|
||||
v.ok = false
|
||||
defer func() {
|
||||
v.p = recover()
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
}()
|
||||
v.result = v.f(ctx)
|
||||
v.ok = true
|
||||
})
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
return v.result
|
||||
}
|
||||
|
||||
return ValueResetter[T]{
|
||||
reset: v.once.Reset,
|
||||
f: ff,
|
||||
}
|
||||
}
|
||||
|
||||
type FuncResetter struct {
|
||||
f func(context.Context) error
|
||||
reset func()
|
||||
}
|
||||
|
||||
func (v *FuncResetter) Do(ctx context.Context) error {
|
||||
return v.f(ctx)
|
||||
}
|
||||
|
||||
func (v *FuncResetter) Reset() {
|
||||
v.reset()
|
||||
}
|
||||
|
||||
func OnceMoreFunc(f func(context.Context) error) FuncResetter {
|
||||
v := struct {
|
||||
f func(context.Context) error
|
||||
once OnceMore
|
||||
ok bool
|
||||
err error
|
||||
p any
|
||||
}{
|
||||
f: f,
|
||||
}
|
||||
ff := func(ctx context.Context) error {
|
||||
v.once.Do(func() {
|
||||
v.ok = false
|
||||
defer func() {
|
||||
v.p = recover()
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
}()
|
||||
v.err = v.f(ctx)
|
||||
v.ok = true
|
||||
})
|
||||
if !v.ok {
|
||||
panic(v.p)
|
||||
}
|
||||
return v.err
|
||||
}
|
||||
|
||||
return FuncResetter{
|
||||
f: ff,
|
||||
reset: v.once.Reset,
|
||||
}
|
||||
}
|
||||
|
||||
type doNotCopy struct{}
|
||||
|
||||
// Lock is a no-op used by -copylocks checker from `go vet`.
|
||||
func (*doNotCopy) Lock() {}
|
||||
func (*doNotCopy) Unlock() {}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestOnceMoreValue(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var counter int
|
||||
f := func(context.Context) int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
|
||||
omf := OnceMoreValue(f)
|
||||
for range 10 {
|
||||
c.Assert(omf.Value(context.Background()), qt.Equals, 1)
|
||||
}
|
||||
omf.Reset()
|
||||
for range 10 {
|
||||
c.Assert(omf.Value(context.Background()), qt.Equals, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnceMoreFunc(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var counter int
|
||||
f := func(context.Context) error {
|
||||
counter++
|
||||
return nil
|
||||
}
|
||||
|
||||
omf := OnceMoreFunc(f)
|
||||
for range 10 {
|
||||
c.Assert(omf.Do(context.Background()), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, 1)
|
||||
}
|
||||
omf.Reset()
|
||||
for range 10 {
|
||||
c.Assert(omf.Do(context.Background()), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, 2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOnceMoreValue(b *testing.B) {
|
||||
var counter int
|
||||
f := func(context.Context) int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
omf := OnceMoreValue(f)
|
||||
for range 10 {
|
||||
omf.Value(context.Background())
|
||||
}
|
||||
omf.Reset()
|
||||
for range 10 {
|
||||
omf.Value(context.Background())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ func TestApplyWithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
-- hugo.toml --
|
||||
defaultContentLanguage = 'it'
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
{{ $dates := slice
|
||||
"2022-01-03"
|
||||
"2022-02-01"
|
||||
|
||||
@@ -112,7 +112,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
|
||||
|
||||
b.Run("Native", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
got := june06.Format("Monday Jan 2 2006")
|
||||
if got != "Wednesday Jun 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
@@ -123,7 +123,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
b.Run("Localized", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
got := f.Format(june06, "Monday Jan 2 2006")
|
||||
if got != "onsdag juni 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
@@ -134,7 +134,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
b.Run("Localized Custom", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
got := f.Format(june06, ":date_medium")
|
||||
if got != "6. juni 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
|
||||
@@ -32,6 +32,37 @@ type ReadSeekCloser interface {
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Sizer provides the size of, typically, a io.Reader.
|
||||
// As implemented by e.g. os.File and io.SectionReader.
|
||||
type Sizer interface {
|
||||
Size() int64
|
||||
}
|
||||
|
||||
type SizeReader interface {
|
||||
io.Reader
|
||||
Sizer
|
||||
}
|
||||
|
||||
// ToSizeReader converts the given io.Reader to a SizeReader.
|
||||
// Note that if r is not a SizeReader, the entire content will be read into memory
|
||||
func ToSizeReader(r io.Reader) (SizeReader, error) {
|
||||
if sr, ok := r.(SizeReader); ok {
|
||||
return sr, nil
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(b), nil
|
||||
}
|
||||
|
||||
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
|
||||
type CloserFunc func() error
|
||||
|
||||
func (f CloserFunc) Close() error {
|
||||
return f()
|
||||
}
|
||||
|
||||
// ReadSeekCloserProvider provides a ReadSeekCloser.
|
||||
type ReadSeekCloserProvider interface {
|
||||
ReadSeekCloser() (ReadSeekCloser, error)
|
||||
|
||||
+55
-11
@@ -28,12 +28,14 @@ import (
|
||||
"github.com/bep/logg"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/gohugoio/hugo/common/hcontext"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/hstore"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/version"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/bep/helpers/contexthelpers"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
iofs "io/fs"
|
||||
@@ -54,7 +56,7 @@ var (
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
var _ maps.StoreProvider = (*HugoInfo)(nil)
|
||||
var _ hstore.StoreProvider = (*HugoInfo)(nil)
|
||||
|
||||
// HugoInfo contains information about the current Hugo environment
|
||||
type HugoInfo struct {
|
||||
@@ -73,14 +75,14 @@ type HugoInfo struct {
|
||||
conf ConfigProvider
|
||||
deps []*Dependency
|
||||
|
||||
store *maps.Scratch
|
||||
store *hstore.Scratch
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
Context Context
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
func (i HugoInfo) Version() VersionString {
|
||||
func (i HugoInfo) Version() version.VersionString {
|
||||
return CurrentVersion.Version()
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ func (i HugoInfo) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
func (i HugoInfo) Store() *maps.Scratch {
|
||||
func (i HugoInfo) Store() *hstore.Scratch {
|
||||
return i.store
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ const (
|
||||
contextKeyMarkupScope contextKey = iota
|
||||
)
|
||||
|
||||
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
|
||||
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
|
||||
|
||||
type Context struct{}
|
||||
|
||||
@@ -196,7 +198,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
Environment: conf.Environment(),
|
||||
conf: conf,
|
||||
deps: deps,
|
||||
store: maps.NewScratch(),
|
||||
store: hstore.NewScratch(),
|
||||
GoVersion: goVersion,
|
||||
}
|
||||
}
|
||||
@@ -308,13 +310,12 @@ func GetDependencyList() []string {
|
||||
|
||||
// GetDependencyListNonGo returns a list of non-Go dependencies.
|
||||
func GetDependencyListNonGo() []string {
|
||||
var deps []string
|
||||
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.6"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -451,7 +452,7 @@ func deprecateLevelWithLogger(item, alternative, version string, level logg.Leve
|
||||
// We want people to run at least the current and previous version without any warnings.
|
||||
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
|
||||
func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
from := MustParseVersion(ver)
|
||||
from := version.MustParseVersion(ver)
|
||||
to := CurrentVersion
|
||||
minorDiff := to.Minor - from.Minor
|
||||
switch {
|
||||
@@ -465,3 +466,46 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
return logg.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
// program := "Hugo Static Site Generator"
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return version
|
||||
}
|
||||
if bi.Revision != "" {
|
||||
version += "-" + bi.Revision
|
||||
}
|
||||
if IsExtended {
|
||||
version += "+extended"
|
||||
}
|
||||
if IsWithdeploy {
|
||||
version += "+withdeploy"
|
||||
}
|
||||
|
||||
osArch := bi.GoOS + "/" + bi.GoArch
|
||||
|
||||
date := bi.RevisionTime
|
||||
if date == "" {
|
||||
// Accept vendor-specified build date if .git/ is unavailable.
|
||||
date = buildDate
|
||||
}
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
|
||||
program, version, osArch, date)
|
||||
|
||||
if vendorInfo != "" {
|
||||
versionString += " VendorInfo=" + vendorInfo
|
||||
}
|
||||
|
||||
return versionString
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
|
||||
---
|
||||
title: home
|
||||
---
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
multilingual={{ hugo.IsMultilingual }}
|
||||
multihost={{ hugo.IsMultihost }}
|
||||
`
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/version"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
@@ -29,7 +30,7 @@ func TestHugoInfo(t *testing.T) {
|
||||
hugoInfo := NewInfo(conf, nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
|
||||
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
|
||||
|
||||
bi := getBuildInfo()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,11 +13,13 @@
|
||||
|
||||
package hugo
|
||||
|
||||
import "github.com/gohugoio/hugo/common/version"
|
||||
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
var CurrentVersion = version.Version{
|
||||
Major: 0,
|
||||
Minor: 149,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
Minor: 155,
|
||||
PatchLevel: 2,
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -173,6 +173,7 @@ func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
|
||||
|
||||
type Logger interface {
|
||||
Debug() logg.LevelLogger
|
||||
DebugCommand(command string) logg.LevelLogger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Error() logg.LevelLogger
|
||||
@@ -221,6 +222,10 @@ func (l *logAdapter) Debug() logg.LevelLogger {
|
||||
return l.debugl
|
||||
}
|
||||
|
||||
func (l *logAdapter) DebugCommand(command string) logg.LevelLogger {
|
||||
return l.debugl.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugf(format string, v ...any) {
|
||||
l.debugl.Logf(format, v...)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ func TestPara(t *testing.T) {
|
||||
var result []int
|
||||
var mu sync.Mutex
|
||||
for i := range n {
|
||||
i := i
|
||||
r.Run(func() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
@@ -115,8 +115,8 @@ func ReplaceExtension(path string, newExt string) string {
|
||||
|
||||
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
|
||||
for _, currentPath := range possibleDirectories {
|
||||
if strings.HasPrefix(inPath, currentPath) {
|
||||
return strings.TrimPrefix(inPath, currentPath), nil
|
||||
if after, ok := strings.CutPrefix(inPath, currentPath); ok {
|
||||
return after, nil
|
||||
}
|
||||
}
|
||||
return inPath, errors.New("can't extract relative path, unknown prefix")
|
||||
|
||||
@@ -225,7 +225,7 @@ func BenchmarkSanitize(b *testing.B) {
|
||||
|
||||
// This should not allocate any memory.
|
||||
b.Run("All allowed", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
got := Sanitize(allAlowedPath)
|
||||
if got != allAlowedPath {
|
||||
b.Fatal(got)
|
||||
@@ -235,7 +235,7 @@ func BenchmarkSanitize(b *testing.B) {
|
||||
|
||||
// This will allocate some memory.
|
||||
b.Run("Spaces", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
got := Sanitize(spacePath)
|
||||
if got != "foo-bar" {
|
||||
b.Fatal(got)
|
||||
@@ -311,3 +311,61 @@ func TestIsSameFilePath(t *testing.T) {
|
||||
c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAddLeadingSlash(b *testing.B) {
|
||||
const (
|
||||
noLeadingSlash = "a/b/c"
|
||||
withLeadingSlash = "/a/b/c"
|
||||
)
|
||||
|
||||
// This should not allocate any memory.
|
||||
b.Run("With leading slash", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
got := AddLeadingSlash(withLeadingSlash)
|
||||
if got != withLeadingSlash {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// This will allocate some memory.
|
||||
b.Run("Without leading slash", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
got := AddLeadingSlash(noLeadingSlash)
|
||||
if got != "/a/b/c" {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Blank string", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
got := AddLeadingSlash("")
|
||||
if got != "/" {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPathEscape(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, this := range []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"/tags/欢迎", "/tags/%E6%AC%A2%E8%BF%8E"},
|
||||
{"/path with spaces", "/path%20with%20spaces"},
|
||||
{"/simple-path", "/simple-path"},
|
||||
{"/path/with/slash", "/path/with/slash"},
|
||||
{"/path/with special&chars", "/path/with%20special&chars"},
|
||||
} {
|
||||
in := this.input
|
||||
for range 2 {
|
||||
result := PathEscape(in)
|
||||
c.Assert(result, qt.Equals, this.expected, qt.Commentf("input: %q", this.input))
|
||||
in = result // test idempotency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,23 +14,27 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
)
|
||||
|
||||
const (
|
||||
identifierBaseof = "baseof"
|
||||
identifierBaseof = "baseof"
|
||||
identifierCurstomWrapper = "_"
|
||||
)
|
||||
|
||||
// PathParser parses a path into a Path.
|
||||
// PathParser parses and manages paths.
|
||||
type PathParser struct {
|
||||
// Maps the language code to its index in the languages/sites slice.
|
||||
LanguageIndex map[string]int
|
||||
@@ -44,6 +48,19 @@ type PathParser struct {
|
||||
|
||||
// Reports whether the given ext is a content file.
|
||||
IsContentExt func(string) bool
|
||||
|
||||
// The configured sites matrix.
|
||||
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
|
||||
|
||||
// Below gets created on demand.
|
||||
initOnce sync.Once
|
||||
sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
|
||||
}
|
||||
|
||||
func (pp *PathParser) init() {
|
||||
pp.initOnce.Do(func() {
|
||||
pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]()
|
||||
})
|
||||
}
|
||||
|
||||
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
|
||||
@@ -57,6 +74,32 @@ func NormalizePathStringBasic(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
|
||||
pp.init()
|
||||
lang := p.Lang()
|
||||
v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
|
||||
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
|
||||
if lang != "" {
|
||||
if idx, ok := pp.LanguageIndex[lang]; ok {
|
||||
builder.WithLanguageIndices(idx)
|
||||
}
|
||||
}
|
||||
|
||||
switch p.Component() {
|
||||
case files.ComponentFolderContent:
|
||||
builder.WithDefaultsIfNotSet()
|
||||
case files.ComponentFolderLayouts:
|
||||
builder.WithAllIfNotSet()
|
||||
case files.ComponentFolderStatic:
|
||||
builder.WithDefaultsAndAllLanguagesIfNotSet()
|
||||
}
|
||||
|
||||
return builder.Build(), nil
|
||||
})
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// ParseIdentity parses component c with path s into a StringIdentity.
|
||||
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
|
||||
p := pp.parsePooled(c, s)
|
||||
@@ -145,6 +188,12 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
sid := p.s[id.Low:id.High]
|
||||
|
||||
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierCustom = len(p.identifiersKnown) - 1
|
||||
found = true
|
||||
}
|
||||
|
||||
if len(p.identifiersKnown) == 0 {
|
||||
// The first is always the extension.
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
@@ -155,12 +204,12 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
|
||||
p.posIdentifierOutputFormat = 0
|
||||
}
|
||||
} else {
|
||||
|
||||
var langFound bool
|
||||
|
||||
if mayHaveLang {
|
||||
var disabled bool
|
||||
_, langFound = pp.LanguageIndex[sid]
|
||||
|
||||
if !langFound {
|
||||
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
|
||||
if disabled {
|
||||
@@ -173,6 +222,7 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !found && mayHaveOutputFormat {
|
||||
@@ -202,8 +252,14 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
|
||||
}
|
||||
|
||||
if !found && mayHaveLayout {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierLayout = len(p.identifiersKnown) - 1
|
||||
if p.posIdentifierLayout != -1 {
|
||||
// Move it to identifiersUnknown.
|
||||
p.identifiersUnknown = append(p.identifiersUnknown, p.identifiersKnown[p.posIdentifierLayout])
|
||||
p.identifiersKnown[p.posIdentifierLayout] = id
|
||||
} else {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierLayout = len(p.identifiersKnown) - 1
|
||||
}
|
||||
found = true
|
||||
}
|
||||
|
||||
@@ -333,7 +389,7 @@ type Type int
|
||||
|
||||
const (
|
||||
|
||||
// A generic resource, e.g. a JSON file.
|
||||
// A generic file, e.g. a JSON file.
|
||||
TypeFile Type = iota
|
||||
|
||||
// All below are content files.
|
||||
@@ -380,6 +436,7 @@ type Path struct {
|
||||
posIdentifierKind int
|
||||
posIdentifierLayout int
|
||||
posIdentifierBaseof int
|
||||
posIdentifierCustom int
|
||||
disabled bool
|
||||
|
||||
trimLeadingSlash bool
|
||||
@@ -417,6 +474,7 @@ func (p *Path) reset() {
|
||||
p.posIdentifierKind = -1
|
||||
p.posIdentifierLayout = -1
|
||||
p.posIdentifierBaseof = -1
|
||||
p.posIdentifierCustom = -1
|
||||
p.disabled = false
|
||||
p.trimLeadingSlash = false
|
||||
p.unnormalized = nil
|
||||
@@ -580,6 +638,9 @@ func (p *Path) Unnormalized() *Path {
|
||||
|
||||
// PathNoLang returns the Path but with any language identifier removed.
|
||||
func (p *Path) PathNoLang() string {
|
||||
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
|
||||
return p.Path()
|
||||
}
|
||||
return p.base(true, false)
|
||||
}
|
||||
|
||||
@@ -633,7 +694,12 @@ func (p *Path) BaseRel(owner *Path) string {
|
||||
//
|
||||
// For other files (Resources), any extension is kept.
|
||||
func (p *Path) Base() string {
|
||||
return p.base(!p.isContentPage(), p.IsBundle())
|
||||
s := p.base(!p.isContentPage(), p.IsBundle())
|
||||
if s == "/" && p.isContentPage() {
|
||||
// The content home page is represented as "".
|
||||
s = ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Used in template lookups.
|
||||
@@ -708,6 +774,10 @@ func (p *Path) Lang() string {
|
||||
return p.identifierAsString(p.posIdentifierLanguage)
|
||||
}
|
||||
|
||||
func (p *Path) Custom() string {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
|
||||
}
|
||||
|
||||
func (p *Path) Identifier(i int) string {
|
||||
return p.identifierAsString(i)
|
||||
}
|
||||
@@ -786,3 +856,12 @@ func HasExt(p string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateIdentifier returns true if the given string is a valid identifier according
|
||||
// to Hugo's basic path normalization rules.
|
||||
func ValidateIdentifier(s string) error {
|
||||
if s == NormalizePathStringBasic(s) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("must be all lower case and no spaces")
|
||||
}
|
||||
|
||||
@@ -18,27 +18,33 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var testParser = &PathParser{
|
||||
LanguageIndex: map[string]int{
|
||||
"no": 0,
|
||||
"en": 1,
|
||||
"fr": 2,
|
||||
},
|
||||
IsContentExt: func(ext string) bool {
|
||||
return ext == "md"
|
||||
},
|
||||
IsOutputFormat: func(name, ext string) bool {
|
||||
switch name {
|
||||
case "html", "amp", "csv", "rss":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
func newTestParser() *PathParser {
|
||||
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
|
||||
|
||||
return &PathParser{
|
||||
LanguageIndex: map[string]int{
|
||||
"no": 0,
|
||||
"en": 1,
|
||||
"fr": 2,
|
||||
},
|
||||
IsContentExt: func(ext string) bool {
|
||||
return ext == "md"
|
||||
},
|
||||
IsOutputFormat: func(name, ext string) bool {
|
||||
switch name {
|
||||
case "html", "amp", "csv", "rss":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
ConfiguredDimensions: dims,
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
@@ -190,7 +196,7 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Base(), qt.Equals, "")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
|
||||
c.Assert(p.Path(), qt.Equals, "/_index.md")
|
||||
c.Assert(p.Container(), qt.Equals, "")
|
||||
@@ -266,7 +272,7 @@ func TestParse(t *testing.T) {
|
||||
"Index root no slash",
|
||||
"_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Base(), qt.Equals, "")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Name(), qt.Equals, "_index.md")
|
||||
},
|
||||
@@ -275,7 +281,7 @@ func TestParse(t *testing.T) {
|
||||
"Index root",
|
||||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Base(), qt.Equals, "")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Name(), qt.Equals, "_index.md")
|
||||
},
|
||||
@@ -373,13 +379,24 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.IsContentData(), qt.IsFalse)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Custom identifier",
|
||||
"/a/b/p1._myid_.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Custom(), qt.Equals, "myid")
|
||||
},
|
||||
},
|
||||
}
|
||||
parser := newTestParser()
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
if test.name != "Home branch cundle" {
|
||||
// return
|
||||
if test.name != "Caret up identifier" {
|
||||
// return
|
||||
}
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
|
||||
test.assert(c, parser.Parse(files.ComponentFolderContent, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -437,8 +454,8 @@ func TestParseLayouts(t *testing.T) {
|
||||
"/mylayout.list.section.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Layout(), qt.Equals, "mylayout")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "mylayout"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "mylayout"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
|
||||
c.Assert(p.Base(), qt.Equals, "/mylayout.html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
},
|
||||
@@ -461,7 +478,8 @@ func TestParseLayouts(t *testing.T) {
|
||||
"Lang and output format",
|
||||
"/list.no.amp.not.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list", "amp", "no"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"not"})
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
@@ -583,14 +601,27 @@ func TestParseLayouts(t *testing.T) {
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Not lang",
|
||||
"/foo/index.xy.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Layout(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.xy.html")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/foo/index.xy.html")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "index"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parser := newTestParser()
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
if test.name != "Shortcode lang layout" {
|
||||
// return
|
||||
if test.name != "Not lang" {
|
||||
return
|
||||
}
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
|
||||
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -605,7 +636,27 @@ func TestHasExt(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkParseIdentity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
|
||||
parser := newTestParser()
|
||||
for b.Loop() {
|
||||
parser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSitesMatrixFromPath(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
parser := newTestParser()
|
||||
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
|
||||
v := parser.SitesMatrixFromPath(p)
|
||||
c.Assert(v.HasLanguage(2), qt.IsTrue)
|
||||
c.Assert(v.LenVectors(), qt.Equals, 1)
|
||||
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 0})
|
||||
}
|
||||
|
||||
func BenchmarkSitesMatrixFromPath(b *testing.B) {
|
||||
parser := newTestParser()
|
||||
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
|
||||
for b.Loop() {
|
||||
parser.SitesMatrixFromPath(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ weight = 2
|
||||
removePathAccents = true
|
||||
-- content/διακριτικός.md --
|
||||
-- content/διακριτικός.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
-- layouts/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
-- layouts/list.html --
|
||||
List
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
@@ -64,9 +64,9 @@ disablePathToLower = true
|
||||
-- content/MySection/MyPage.fr.md --
|
||||
-- content/MySection/MyBundle/index.md --
|
||||
-- content/MySection/MyBundle/index.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
-- layouts/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
-- layouts/list.html --
|
||||
{{ .Language.Lang }}|List.
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
+250
-27
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -13,52 +13,128 @@
|
||||
|
||||
package predicate
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/hugofs/hglob"
|
||||
)
|
||||
|
||||
// Match represents the result of a predicate evaluation.
|
||||
type Match interface {
|
||||
OK() bool
|
||||
}
|
||||
|
||||
var (
|
||||
// Predefined Match values for common cases.
|
||||
True = BoolMatch(true)
|
||||
False = BoolMatch(false)
|
||||
)
|
||||
|
||||
// BoolMatch is a simple Match implementation based on a boolean value.
|
||||
type BoolMatch bool
|
||||
|
||||
func (b BoolMatch) OK() bool {
|
||||
return bool(b)
|
||||
}
|
||||
|
||||
// breakMatch is a Match implementation that always returns false for OK() and signals to break evaluation.
|
||||
type breakMatch struct{}
|
||||
|
||||
func (b breakMatch) OK() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var matchBreak = breakMatch{}
|
||||
|
||||
// P is a predicate function that tests whether a value of type T satisfies some condition.
|
||||
type P[T any] func(T) bool
|
||||
|
||||
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
|
||||
func (p P[T]) And(ps ...P[T]) P[T] {
|
||||
return func(v T) bool {
|
||||
for _, pp := range ps {
|
||||
if !pp(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if p == nil {
|
||||
return true
|
||||
}
|
||||
return p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
|
||||
// Note that P[T] only supports Or. For chained AND/OR logic, use PR[T].
|
||||
func (p P[T]) Or(ps ...P[T]) P[T] {
|
||||
return func(v T) bool {
|
||||
if p != nil && p(v) {
|
||||
return true
|
||||
}
|
||||
for _, pp := range ps {
|
||||
if pp(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
return p(v)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Negate returns a predicate that is a logical negation of this predicate.
|
||||
func (p P[T]) Negate() P[T] {
|
||||
// PR is a predicate function that tests whether a value of type T satisfies some condition and returns a Match result.
|
||||
type PR[T any] func(T) Match
|
||||
|
||||
// BoolFunc returns a P[T] version of this predicate.
|
||||
func (p PR[T]) BoolFunc() P[T] {
|
||||
return func(v T) bool {
|
||||
return !p(v)
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
return p(v).OK()
|
||||
}
|
||||
}
|
||||
|
||||
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
|
||||
func (p PR[T]) And(ps ...PR[T]) PR[T] {
|
||||
return func(v T) Match {
|
||||
if p != nil {
|
||||
m := p(v)
|
||||
if !m.OK() || shouldBreak(m) {
|
||||
return matchBreak
|
||||
}
|
||||
}
|
||||
for _, pp := range ps {
|
||||
m := pp(v)
|
||||
if !m.OK() || shouldBreak(m) {
|
||||
return matchBreak
|
||||
}
|
||||
}
|
||||
return BoolMatch(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
|
||||
func (p PR[T]) Or(ps ...PR[T]) PR[T] {
|
||||
return func(v T) Match {
|
||||
if p != nil {
|
||||
m := p(v)
|
||||
if m.OK() {
|
||||
return m
|
||||
}
|
||||
if shouldBreak(m) {
|
||||
return matchBreak
|
||||
}
|
||||
}
|
||||
for _, pp := range ps {
|
||||
m := pp(v)
|
||||
if m.OK() {
|
||||
return m
|
||||
}
|
||||
if shouldBreak(m) {
|
||||
return matchBreak
|
||||
}
|
||||
}
|
||||
return BoolMatch(false)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldBreak(m Match) bool {
|
||||
_, ok := m.(breakMatch)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Filter returns a new slice holding only the elements of s that satisfy p.
|
||||
// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length.
|
||||
func (p P[T]) Filter(s []T) []T {
|
||||
func (p PR[T]) Filter(s []T) []T {
|
||||
var n int
|
||||
for _, v := range s {
|
||||
if p(v) {
|
||||
if p(v).OK() {
|
||||
s[n] = v
|
||||
n++
|
||||
}
|
||||
@@ -67,12 +143,159 @@ func (p P[T]) Filter(s []T) []T {
|
||||
}
|
||||
|
||||
// FilterCopy returns a new slice holding only the elements of s that satisfy p.
|
||||
func (p P[T]) FilterCopy(s []T) []T {
|
||||
func (p PR[T]) FilterCopy(s []T) []T {
|
||||
var result []T
|
||||
for _, v := range s {
|
||||
if p(v) {
|
||||
if p(v).OK() {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const (
|
||||
rangeOpNone = iota
|
||||
rangeOpLT
|
||||
rangeOpLTE
|
||||
rangeOpGT
|
||||
rangeOpGTE
|
||||
rangeOpEQ
|
||||
rangeOpNE
|
||||
)
|
||||
|
||||
func cutRangeOp(s string) (op int, rest string) {
|
||||
switch {
|
||||
case strings.HasPrefix(s, ">= "):
|
||||
return rangeOpGTE, s[3:]
|
||||
case strings.HasPrefix(s, "<= "):
|
||||
return rangeOpLTE, s[3:]
|
||||
case strings.HasPrefix(s, "== "):
|
||||
return rangeOpEQ, s[3:]
|
||||
case strings.HasPrefix(s, "!= "):
|
||||
return rangeOpNE, s[3:]
|
||||
case strings.HasPrefix(s, "> "):
|
||||
return rangeOpGT, s[2:]
|
||||
case strings.HasPrefix(s, "< "):
|
||||
return rangeOpLT, s[2:]
|
||||
default:
|
||||
return rangeOpNone, s
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
|
||||
var p PR[string]
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
|
||||
if negate {
|
||||
pattern = pattern[2:]
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.And(func(s string) Match {
|
||||
return BoolMatch(!g.Match(s))
|
||||
})
|
||||
} else {
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.Or(func(s string) Match {
|
||||
return BoolMatch(g.Match(s))
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
// NewIndexStringPredicateFromGlobsAndRanges creates an IndexString predicate from the given glob patterns and range patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
// A range pattern is one of "> value", ">= value", "< value" or "<= value".
|
||||
func NewIndexStringPredicateFromGlobsAndRanges(patterns []string, getIndex func(s string) int, getGlob func(pattern string) (glob.Glob, error)) (P[IndexString], error) {
|
||||
var p PR[IndexString]
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
|
||||
if negate {
|
||||
pattern = pattern[2:]
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(!g.Match(s.String))
|
||||
})
|
||||
} else {
|
||||
// This can be either a glob or a value prefixed with one of >, >=, < or <=.
|
||||
o, v := cutRangeOp(pattern)
|
||||
if o != rangeOpNone {
|
||||
i := getIndex(v)
|
||||
if i == -1 {
|
||||
// No match possible.
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(false)
|
||||
})
|
||||
continue
|
||||
}
|
||||
switch o {
|
||||
// The greater values starts at the top with index 0.
|
||||
case rangeOpGT:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index < i)
|
||||
})
|
||||
case rangeOpGTE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index <= i)
|
||||
})
|
||||
case rangeOpLT:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index > i)
|
||||
})
|
||||
case rangeOpLTE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index >= i)
|
||||
})
|
||||
case rangeOpEQ:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index == i)
|
||||
})
|
||||
case rangeOpNE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index != i)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.Or(func(s IndexString) Match {
|
||||
return BoolMatch(g.Match(s.String))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
type IndexString struct {
|
||||
Index int
|
||||
String string
|
||||
}
|
||||
|
||||
type IndexMatcher interface {
|
||||
IndexMatch(match P[IndexString]) (iter.Seq[int], error)
|
||||
}
|
||||
|
||||
@@ -17,38 +17,66 @@ import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/predicate"
|
||||
)
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
func TestPredicate(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
n := func() predicate.PR[int] {
|
||||
var pr predicate.PR[int]
|
||||
return pr
|
||||
}
|
||||
|
||||
c.Assert(p(1), qt.IsTrue)
|
||||
var pr predicate.PR[int]
|
||||
p := pr.BoolFunc()
|
||||
c.Assert(p(1), qt.IsFalse)
|
||||
|
||||
pr = n().Or(intP1).Or(intP2)
|
||||
p = pr.BoolFunc()
|
||||
c.Assert(p(1), qt.IsTrue) // true || false
|
||||
c.Assert(p(2), qt.IsTrue) // false || true
|
||||
c.Assert(p(3), qt.IsFalse) // false || false
|
||||
|
||||
pr = pr.And(intP3)
|
||||
p = pr.BoolFunc()
|
||||
c.Assert(p(2), qt.IsFalse) // true || true && false
|
||||
c.Assert(pr(10), qt.IsTrue) // true || true && true
|
||||
|
||||
pr = pr.And(intP4)
|
||||
p = pr.BoolFunc()
|
||||
c.Assert(p(10), qt.IsTrue) // true || true && true && true
|
||||
c.Assert(p(2), qt.IsFalse) // true || true && false && false
|
||||
c.Assert(p(1), qt.IsFalse) // true || false && false && false
|
||||
c.Assert(p(3), qt.IsFalse) // false || false && false && false
|
||||
c.Assert(p(4), qt.IsFalse) // false || false && false && false
|
||||
c.Assert(p(42), qt.IsFalse) // false || false && false && false
|
||||
|
||||
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
|
||||
p = pr.BoolFunc()
|
||||
c.Assert(p(1), qt.IsFalse)
|
||||
c.Assert(p(2), qt.IsFalse)
|
||||
c.Assert(p(10), qt.IsTrue)
|
||||
|
||||
neg := p.Negate()
|
||||
c.Assert(neg(1), qt.IsFalse)
|
||||
c.Assert(neg(2), qt.IsTrue)
|
||||
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
|
||||
p = pr.BoolFunc()
|
||||
c.Assert(p(1), qt.IsFalse)
|
||||
c.Assert(p(2), qt.IsFalse)
|
||||
c.Assert(p(10), qt.IsTrue)
|
||||
|
||||
and := p.And(intP2)
|
||||
c.Assert(and(1), qt.IsFalse)
|
||||
c.Assert(and(2), qt.IsFalse)
|
||||
c.Assert(and(10), qt.IsTrue)
|
||||
|
||||
or := p.Or(intP2)
|
||||
c.Assert(or(1), qt.IsTrue)
|
||||
c.Assert(or(2), qt.IsTrue)
|
||||
c.Assert(or(10), qt.IsTrue)
|
||||
c.Assert(or(11), qt.IsFalse)
|
||||
pr = n().Or(intP1).Or(intP2).Or(intP3)
|
||||
p = pr.BoolFunc()
|
||||
c.Assert(p(1), qt.IsTrue)
|
||||
c.Assert(p(10), qt.IsTrue)
|
||||
c.Assert(p(4), qt.IsFalse)
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
p = p.Or(intP2)
|
||||
var p predicate.PR[int]
|
||||
p = p.Or(intP1).Or(intP2)
|
||||
|
||||
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
|
||||
|
||||
@@ -59,8 +87,8 @@ func TestFilter(t *testing.T) {
|
||||
func TestFilterCopy(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
p = p.Or(intP2)
|
||||
var p predicate.PR[int]
|
||||
p = p.Or(intP1).Or(intP2)
|
||||
|
||||
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
|
||||
|
||||
@@ -68,16 +96,187 @@ func TestFilterCopy(t *testing.T) {
|
||||
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2})
|
||||
}
|
||||
|
||||
var intP1 = func(i int) bool {
|
||||
var intP1 = func(i int) predicate.Match {
|
||||
if i == 10 {
|
||||
return true
|
||||
return predicate.True
|
||||
}
|
||||
return i == 1
|
||||
return predicate.BoolMatch(i == 1)
|
||||
}
|
||||
|
||||
var intP2 = func(i int) bool {
|
||||
var intP2 = func(i int) predicate.Match {
|
||||
if i == 10 {
|
||||
return true
|
||||
return predicate.True
|
||||
}
|
||||
return i == 2
|
||||
return predicate.BoolMatch(i == 2)
|
||||
}
|
||||
|
||||
var intP3 = func(i int) predicate.Match {
|
||||
if i == 10 {
|
||||
return predicate.True
|
||||
}
|
||||
return predicate.BoolMatch(i == 3)
|
||||
}
|
||||
|
||||
var intP4 = func(i int) predicate.Match {
|
||||
if i == 10 {
|
||||
return predicate.True
|
||||
}
|
||||
return predicate.BoolMatch(i == 4)
|
||||
}
|
||||
|
||||
func TestNewStringPredicateFromGlobs(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
getGlob := func(pattern string) (glob.Glob, error) {
|
||||
return glob.Compile(pattern)
|
||||
}
|
||||
|
||||
n := func(patterns ...string) predicate.P[string] {
|
||||
p, err := predicate.NewStringPredicateFromGlobs(patterns, getGlob)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
m := n("a", "! ab*", "abc")
|
||||
c.Assert(m("a"), qt.IsTrue)
|
||||
c.Assert(m("ab"), qt.IsFalse)
|
||||
c.Assert(m("abc"), qt.IsFalse)
|
||||
|
||||
m = n()
|
||||
c.Assert(m("anything"), qt.IsFalse)
|
||||
}
|
||||
|
||||
func TestNewIndexStringPredicateFromGlobsAndRanges(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Simulate versions: v4.0.0=0, v3.0.0=1, v2.0.0=2, v1.0.0=3
|
||||
// Lower index = greater value.
|
||||
versions := []string{"v4.0.0", "v3.0.0", "v2.0.0", "v1.0.0"}
|
||||
getIndex := func(s string) int {
|
||||
for i, v := range versions {
|
||||
if v == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
getGlob := func(pattern string) (glob.Glob, error) {
|
||||
return glob.Compile(pattern)
|
||||
}
|
||||
|
||||
n := func(patterns ...string) predicate.P[predicate.IndexString] {
|
||||
p, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(patterns, getIndex, getGlob)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
is := func(i int) predicate.IndexString {
|
||||
return predicate.IndexString{Index: i, String: versions[i]}
|
||||
}
|
||||
|
||||
// Test >= v2.0.0 (index 2): should match indices <= 2
|
||||
m := n(">= v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test > v2.0.0 (index 2): should match indices < 2
|
||||
m = n("> v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test < v3.0.0 (index 1): should match indices > 1
|
||||
m = n("< v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
|
||||
|
||||
// Test range: >= v2.0.0 AND <= v3.0.0
|
||||
m = n(">= v2.0.0", "<= v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0 - too high
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - too low
|
||||
|
||||
// Test glob pattern
|
||||
m = n("v2.*.*")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
|
||||
// Test glob with negation
|
||||
m = n("v*.*.*", "! v3.*.*")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
|
||||
// Test range with negation: >= v2.0.0 but not v3.0.0
|
||||
m = n(">= v2.0.0", "! v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
|
||||
|
||||
// Test unknown value in range returns no match
|
||||
m = n(">= v99.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse)
|
||||
c.Assert(m(is(3)), qt.IsFalse)
|
||||
|
||||
// Test == v2.0.0: should only match v2.0.0
|
||||
m = n("== v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test != v2.0.0: should match everything except v2.0.0
|
||||
m = n("!= v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
|
||||
|
||||
// Test != with range: >= v2.0.0 AND != v3.0.0
|
||||
m = n(">= v2.0.0", "!= v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - excluded by !=
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
|
||||
}
|
||||
|
||||
func BenchmarkPredicate(b *testing.B) {
|
||||
b.Run("and or no match", func(b *testing.B) {
|
||||
var p predicate.PR[int] = intP1
|
||||
p = p.And(intP2).Or(intP3)
|
||||
for b.Loop() {
|
||||
_ = p(3).OK()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("and and no match", func(b *testing.B) {
|
||||
var p predicate.PR[int] = intP1
|
||||
p = p.And(intP2)
|
||||
for b.Loop() {
|
||||
_ = p(3).OK()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("and and match", func(b *testing.B) {
|
||||
var p predicate.PR[int] = intP1
|
||||
p = p.And(intP2)
|
||||
for b.Loop() {
|
||||
_ = p(10).OK()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("or or match", func(b *testing.B) {
|
||||
var p predicate.PR[int] = intP1
|
||||
p = p.Or(intP2).Or(intP3)
|
||||
for b.Loop() {
|
||||
_ = p(2).OK()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCutRangeOp(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
op, rest := cutRangeOp(">= value")
|
||||
c.Assert(op, qt.Equals, rangeOpGTE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("<= value")
|
||||
c.Assert(op, qt.Equals, rangeOpLTE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("> value")
|
||||
c.Assert(op, qt.Equals, rangeOpGT)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("< value")
|
||||
c.Assert(op, qt.Equals, rangeOpLT)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("== value")
|
||||
c.Assert(op, qt.Equals, rangeOpEQ)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("!= value")
|
||||
c.Assert(op, qt.Equals, rangeOpNE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("value")
|
||||
c.Assert(op, qt.Equals, rangeOpNone)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
}
|
||||
@@ -16,6 +16,7 @@ package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -72,3 +73,27 @@ func doublePercent(str string) string {
|
||||
func singlePercent(str string) string {
|
||||
return strings.Replace(str, "%%", "%", -1)
|
||||
}
|
||||
|
||||
type ProgressState int
|
||||
|
||||
const (
|
||||
ProgressHidden ProgressState = iota
|
||||
ProgressNormal
|
||||
ProgressError
|
||||
ProgressIntermediate
|
||||
ProgressWarning
|
||||
)
|
||||
|
||||
// ReportProgress writes OSC 9;4 sequence to w.
|
||||
func ReportProgress(w io.Writer, state ProgressState, progress float64) {
|
||||
if progress < 0 {
|
||||
progress = 0.0
|
||||
}
|
||||
if progress > 1 {
|
||||
progress = 1.0
|
||||
}
|
||||
|
||||
pi := int(progress * 100)
|
||||
|
||||
fmt.Fprintf(w, "\033]9;4;%d;%d\007", state, pi)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func BenchmarkVisitLinesAfter(b *testing.B) {
|
||||
|
||||
line 3`
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
VisitLinesAfter(lines, func(s string) {
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,7 +139,25 @@ func NewBool(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
// WeightProvider provides a weight.
|
||||
type WeightProvider interface {
|
||||
Weight() int
|
||||
}
|
||||
|
||||
// Weight0Provider provides a weight that's considered before the WeightProvider in sorting.
|
||||
// This allows the weight set on a given term to win.
|
||||
type Weight0Provider interface {
|
||||
Weight0() int
|
||||
}
|
||||
|
||||
// PrintableValueProvider is implemented by types that can provide a printable value.
|
||||
type PrintableValueProvider interface {
|
||||
PrintableValue() any
|
||||
}
|
||||
|
||||
type (
|
||||
Strings2 [2]string
|
||||
Strings3 [3]string
|
||||
Ints2 [2]int
|
||||
Ints3 [3]int
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugo
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -37,6 +37,9 @@ type Version struct {
|
||||
// HugoVersionSuffix is the suffix used in the Hugo version string.
|
||||
// It will be blank for release versions.
|
||||
Suffix string
|
||||
|
||||
// Set when parsed from a string,
|
||||
source string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -44,6 +47,10 @@ var (
|
||||
_ compare.Comparer = (*VersionString)(nil)
|
||||
)
|
||||
|
||||
func (v Version) IsZero() bool {
|
||||
return v.Major == 0 && v.Minor == 0 && v.PatchLevel == 0 && v.Suffix == ""
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
|
||||
}
|
||||
@@ -55,7 +62,7 @@ func (v Version) Version() VersionString {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h Version) Compare(other any) int {
|
||||
return compareVersions(h, other)
|
||||
return CompareVersions(h, other)
|
||||
}
|
||||
|
||||
// VersionString represents a Hugo version string.
|
||||
@@ -67,7 +74,7 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
return compareVersions(h.Version(), other)
|
||||
return CompareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
@@ -83,19 +90,26 @@ func (h VersionString) Eq(other any) bool {
|
||||
return s == h.String()
|
||||
}
|
||||
|
||||
var versionSuffixes = []string{"-test", "-DEV"}
|
||||
|
||||
// ParseVersion parses a version string.
|
||||
func ParseVersion(s string) (Version, error) {
|
||||
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
|
||||
var vv Version
|
||||
for _, suffix := range versionSuffixes {
|
||||
if strings.HasSuffix(s, suffix) {
|
||||
vv.Suffix = suffix
|
||||
s = strings.TrimSuffix(s, suffix)
|
||||
hyphen := strings.Index(s, "-")
|
||||
if hyphen > 0 {
|
||||
suffix := s[hyphen:]
|
||||
if len(suffix) > 1 {
|
||||
if suffix[0] == '-' {
|
||||
suffix = suffix[1:]
|
||||
}
|
||||
if len(suffix) > 0 {
|
||||
vv.Suffix = suffix
|
||||
s = s[:hyphen]
|
||||
}
|
||||
}
|
||||
vv.Suffix = suffix
|
||||
}
|
||||
|
||||
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
|
||||
vv.source = s
|
||||
|
||||
return vv, nil
|
||||
}
|
||||
@@ -134,107 +148,70 @@ func (v Version) NextPatchLevel(level int) Version {
|
||||
return prev
|
||||
}
|
||||
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
// program := "Hugo Static Site Generator"
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return version
|
||||
}
|
||||
if bi.Revision != "" {
|
||||
version += "-" + bi.Revision
|
||||
}
|
||||
if IsExtended {
|
||||
version += "+extended"
|
||||
}
|
||||
if IsWithdeploy {
|
||||
version += "+withdeploy"
|
||||
}
|
||||
|
||||
osArch := bi.GoOS + "/" + bi.GoArch
|
||||
|
||||
date := bi.RevisionTime
|
||||
if date == "" {
|
||||
// Accept vendor-specified build date if .git/ is unavailable.
|
||||
date = buildDate
|
||||
}
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
|
||||
program, version, osArch, date)
|
||||
|
||||
if vendorInfo != "" {
|
||||
versionString += " VendorInfo=" + vendorInfo
|
||||
}
|
||||
|
||||
return versionString
|
||||
}
|
||||
|
||||
func version(major, minor, patch int, suffix string) string {
|
||||
if suffix != "" {
|
||||
if suffix[0] != '-' {
|
||||
suffix = "-" + suffix
|
||||
}
|
||||
}
|
||||
if patch > 0 || minor > 53 {
|
||||
return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix)
|
||||
}
|
||||
return fmt.Sprintf("%d.%d%s", major, minor, suffix)
|
||||
}
|
||||
|
||||
// CompareVersion compares the given version string or number against the
|
||||
// running Hugo version.
|
||||
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
func CompareVersion(version any) int {
|
||||
return compareVersions(CurrentVersion, version)
|
||||
}
|
||||
|
||||
func compareVersions(inVersion Version, in any) int {
|
||||
// CompareVersion compares v1 with v2.
|
||||
// It returns -1 if the v2 is less than, 0 if equal and 1 if greater than
|
||||
// v1.
|
||||
func CompareVersions(v1 Version, v2 any) int {
|
||||
var c int
|
||||
switch d := in.(type) {
|
||||
switch d := v2.(type) {
|
||||
case float64:
|
||||
c = compareFloatWithVersion(d, inVersion)
|
||||
c = compareFloatWithVersion(d, v1)
|
||||
case float32:
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
case int:
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
case int32:
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
case int64:
|
||||
c = compareFloatWithVersion(float64(d), inVersion)
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
case Version:
|
||||
if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel {
|
||||
return strings.Compare(inVersion.Suffix, d.Suffix)
|
||||
if v1.IsZero() && d.IsZero() {
|
||||
// Fall back to source comparison.
|
||||
return strings.Compare(v1.source, d.source)
|
||||
}
|
||||
if d.Major > inVersion.Major {
|
||||
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
|
||||
return strings.Compare(v1.Suffix, d.Suffix)
|
||||
}
|
||||
if d.Major > v1.Major {
|
||||
return 1
|
||||
} else if d.Major < inVersion.Major {
|
||||
} else if d.Major < v1.Major {
|
||||
return -1
|
||||
}
|
||||
if d.Minor > inVersion.Minor {
|
||||
if d.Minor > v1.Minor {
|
||||
return 1
|
||||
} else if d.Minor < inVersion.Minor {
|
||||
} else if d.Minor < v1.Minor {
|
||||
return -1
|
||||
}
|
||||
if d.PatchLevel > inVersion.PatchLevel {
|
||||
if d.PatchLevel > v1.PatchLevel {
|
||||
return 1
|
||||
} else if d.PatchLevel < inVersion.PatchLevel {
|
||||
} else if d.PatchLevel < v1.PatchLevel {
|
||||
return -1
|
||||
}
|
||||
default:
|
||||
s, err := cast.ToStringE(in)
|
||||
s, err := cast.ToStringE(v2)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
v, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
v2v, _ := ParseVersion(s)
|
||||
if v1.IsZero() && v2v.IsZero() {
|
||||
// Fall back to source comparison.
|
||||
return strings.Compare(v1.source, v2v.source)
|
||||
}
|
||||
return inVersion.Compare(v)
|
||||
|
||||
return v1.Compare(v2v)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugo
|
||||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -50,30 +50,45 @@ func TestHugoVersion(t *testing.T) {
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
|
||||
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
|
||||
parseIgnoreErr := func(s string) Version {
|
||||
v, _ := ParseVersion(s)
|
||||
return v
|
||||
}
|
||||
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
|
||||
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1)
|
||||
}
|
||||
|
||||
func TestParseHugoVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(MustParseVersion("v2.3.2").String(), qt.Equals, "2.3.2")
|
||||
c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25")
|
||||
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
|
||||
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -63,6 +63,7 @@ func TestLexicographicSort(t *testing.T) {
|
||||
c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"})
|
||||
}
|
||||
|
||||
// // Note that this cannot use b.Loop() because of golang/go#27217.
|
||||
func BenchmarkStringSort(b *testing.B) {
|
||||
prototype := []string{"b", "Bz", "zz", "ba", "αβδ αβδ αβδ", "A", "Ba", "ba", "nnnnasdfnnn", "AAgæåz", "αβδC"}
|
||||
b.Run("LessStrings", func(b *testing.B) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,25 +11,4 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build extended
|
||||
|
||||
package webp
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/bep/gowebp/libwebp"
|
||||
"github.com/bep/gowebp/libwebp/webpoptions"
|
||||
)
|
||||
|
||||
// Encode writes the Image m to w in Webp format with the given
|
||||
// options.
|
||||
func Encode(w io.Writer, m image.Image, o webpoptions.EncodingOptions) error {
|
||||
return libwebp.Encode(w, m, o)
|
||||
}
|
||||
|
||||
// Supports returns whether webp encoding is supported in this build.
|
||||
func Supports() bool {
|
||||
return true
|
||||
}
|
||||
package compare
|
||||
+138
-119
@@ -29,9 +29,10 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/cache/httpcache"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -40,7 +41,10 @@ import (
|
||||
"github.com/gohugoio/hugo/config/services"
|
||||
"github.com/gohugoio/hugo/deploy/deployconfig"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib/roles"
|
||||
"github.com/gohugoio/hugo/hugolib/segments"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/hugolib/versions"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
|
||||
"github.com/gohugoio/hugo/markup/markup_config"
|
||||
@@ -55,8 +59,6 @@ import (
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// InternalConfig is the internal configuration for Hugo, not read from any user provided config file.
|
||||
@@ -101,7 +103,8 @@ type Config struct {
|
||||
// For internal use only.
|
||||
Internal InternalConfig `mapstructure:"-" json:"-"`
|
||||
// For internal use only.
|
||||
C *ConfigCompiled `mapstructure:"-" json:"-"`
|
||||
C *ConfigCompiled `mapstructure:"-" json:"-"`
|
||||
isLanguageClone bool
|
||||
|
||||
RootConfig
|
||||
|
||||
@@ -141,16 +144,25 @@ type Config struct {
|
||||
// The outputformats configuration sections maps a format name (a string) to a configuration object for that format.
|
||||
OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"`
|
||||
|
||||
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
|
||||
Languages *config.ConfigNamespace[map[string]langs.LanguageConfig, langs.LanguagesInternal] `mapstructure:"-"`
|
||||
|
||||
// The versions configuration section contains the top level versions configuration options.
|
||||
Versions *config.ConfigNamespace[map[string]versions.VersionConfig, versions.VersionsInternal] `mapstructure:"-"`
|
||||
|
||||
// The roles configuration section contains the top level roles configuration options.
|
||||
Roles *config.ConfigNamespace[map[string]roles.RoleConfig, roles.RolesInternal] `mapstructure:"-"`
|
||||
|
||||
// The outputs configuration section maps a Page Kind (a string) to a slice of output formats.
|
||||
// This can be overridden in the front matter.
|
||||
Outputs map[string][]string `mapstructure:"-"`
|
||||
|
||||
// The cascade configuration section contains the top level front matter cascade configuration options,
|
||||
// a slice of page matcher and params to apply to those pages.
|
||||
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]] `mapstructure:"-"`
|
||||
Cascade *page.PageMatcherParamsConfigs `mapstructure:"-"`
|
||||
|
||||
// The segments defines segments for the site. Used for partial/segmented builds.
|
||||
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
|
||||
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, *segments.Segments] `mapstructure:"-"`
|
||||
|
||||
// Menu configuration.
|
||||
// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
|
||||
@@ -200,21 +212,25 @@ type Config struct {
|
||||
|
||||
// User provided parameters.
|
||||
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
|
||||
Params maps.Params `mapstructure:"-"`
|
||||
|
||||
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
|
||||
Languages map[string]langs.LanguageConfig `mapstructure:"-"`
|
||||
Params hmaps.Params `mapstructure:"-"`
|
||||
|
||||
// UglyURLs configuration. Either a boolean or a sections map.
|
||||
UglyURLs any `mapstructure:"-"`
|
||||
}
|
||||
|
||||
// Early initialization of config.
|
||||
type configCompiler interface {
|
||||
CompileConfig(logger loggers.Logger) error
|
||||
}
|
||||
|
||||
// Late initialization of config.
|
||||
type configInitializer interface {
|
||||
InitConfig(logger loggers.Logger, defaultSitesMatrix sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error
|
||||
}
|
||||
|
||||
func (c Config) cloneForLang() *Config {
|
||||
x := c
|
||||
x.isLanguageClone = true
|
||||
x.C = nil
|
||||
copyStringSlice := func(in []string) []string {
|
||||
if in == nil {
|
||||
@@ -321,18 +337,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
for _, lang := range c.DisableLanguages {
|
||||
disabledLangs[lang] = true
|
||||
}
|
||||
for lang, language := range c.Languages {
|
||||
if !language.Disabled && disabledLangs[lang] {
|
||||
language.Disabled = true
|
||||
c.Languages[lang] = language
|
||||
}
|
||||
if language.Disabled {
|
||||
disabledLangs[lang] = true
|
||||
if lang == c.DefaultContentLanguage {
|
||||
return fmt.Errorf("cannot disable default content language %q", lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, s := range c.IgnoreLogs {
|
||||
c.IgnoreLogs[i] = strings.ToLower(s)
|
||||
@@ -467,10 +471,10 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
gc.RenderHookUseEmbeddedNever,
|
||||
}
|
||||
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) {
|
||||
return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
return fmt.Errorf("site config markup.goldmark.renderHooks.image.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
}
|
||||
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) {
|
||||
return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
return fmt.Errorf("site config markup.goldmark.renderHooks.link.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
}
|
||||
|
||||
c.C = &ConfigCompiled{
|
||||
@@ -485,7 +489,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
|
||||
IsUglyURLSection: isUglyURL,
|
||||
IgnoreFile: ignoreFile,
|
||||
SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
|
||||
MainSections: c.MainSections,
|
||||
Clock: clock,
|
||||
HTTPCache: httpCache,
|
||||
@@ -525,7 +528,6 @@ type ConfigCompiled struct {
|
||||
CreateTitle func(s string) string
|
||||
IsUglyURLSection func(section string) bool
|
||||
IgnoreFile func(filename string) bool
|
||||
SegmentFilter segments.SegmentFilter
|
||||
MainSections []string
|
||||
Clock time.Time
|
||||
HTTPCache httpcache.ConfigCompiled
|
||||
@@ -588,13 +590,31 @@ type RootConfig struct {
|
||||
// Set this to true to put all languages below their language ID.
|
||||
DefaultContentLanguageInSubdir bool
|
||||
|
||||
// The default content role to use for the site.
|
||||
DefaultContentRole string
|
||||
|
||||
// Set this to true to put the default role in a subdirectory.
|
||||
DefaultContentRoleInSubdir bool
|
||||
|
||||
// The default content version to use for the site.
|
||||
DefaultContentVersion string
|
||||
|
||||
// Set to true to render the default version in a subdirectory.
|
||||
DefaultContentVersionInSubdir bool
|
||||
|
||||
// The default output format to use for the site.
|
||||
// If not set, we will use the first output format.
|
||||
DefaultOutputFormat string
|
||||
|
||||
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
|
||||
// Note that this currently is an alias for DisableDefaultSiteRedirect introduced in v0.154.5.
|
||||
// It's not obvious how a more fine grained setup would work.
|
||||
DisableDefaultLanguageRedirect bool
|
||||
|
||||
// Disable generation of redirects to the default site when DefaultContentRoleInSubdir or DefaultContentVersionInSubdir or DefaultContentLanguageInSubdir is enabled.
|
||||
// The default site is the site is the combination of defaultContentLanguage, defaultContentVersion and defaultContentRole.
|
||||
DisableDefaultSiteRedirect bool
|
||||
|
||||
// Disable creation of alias redirect pages.
|
||||
DisableAliases bool
|
||||
|
||||
@@ -790,33 +810,31 @@ func (c RootConfig) staticDirs() []string {
|
||||
dirs = append(dirs, c.StaticDir8...)
|
||||
dirs = append(dirs, c.StaticDir9...)
|
||||
dirs = append(dirs, c.StaticDir10...)
|
||||
return helpers.UniqueStringsReuse(dirs)
|
||||
return hstrings.UniqueStringsReuse(dirs)
|
||||
}
|
||||
|
||||
type Configs struct {
|
||||
Base *Config
|
||||
LoadingInfo config.LoadConfigResult
|
||||
LanguageConfigMap map[string]*Config
|
||||
LanguageConfigSlice []*Config
|
||||
Base *Config
|
||||
LoadingInfo config.LoadConfigResult
|
||||
LanguageConfigMap map[string]*Config
|
||||
|
||||
IsMultihost bool
|
||||
|
||||
Modules modules.Modules
|
||||
ModulesClient *modules.Client
|
||||
FileCaches filecache.Caches
|
||||
|
||||
// All below is set in Init.
|
||||
Languages langs.Languages
|
||||
LanguagesDefaultFirst langs.Languages
|
||||
ContentPathParser *paths.PathParser
|
||||
Languages langs.Languages
|
||||
ContentPathParser *paths.PathParser
|
||||
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
|
||||
DefaultContentSitesMatrix *sitesmatrix.IntSets
|
||||
AllSitesMatrix *sitesmatrix.IntSets
|
||||
|
||||
configLangs []config.AllProvider
|
||||
}
|
||||
|
||||
func (c *Configs) Validate(logger loggers.Logger) error {
|
||||
c.Base.Cascade.Config.Range(func(p page.PageMatcher, cfg page.PageMatcherParamsConfig) bool {
|
||||
page.CheckCascadePattern(logger, p)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -835,54 +853,15 @@ func (c *Configs) IsZero() bool {
|
||||
return c == nil || len(c.Languages) == 0
|
||||
}
|
||||
|
||||
func (c *Configs) Init() error {
|
||||
func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
|
||||
var languages langs.Languages
|
||||
|
||||
var langKeys []string
|
||||
var hasEn bool
|
||||
|
||||
const en = "en"
|
||||
|
||||
for k := range c.LanguageConfigMap {
|
||||
langKeys = append(langKeys, k)
|
||||
if k == en {
|
||||
hasEn = true
|
||||
for _, f := range c.Base.Languages.Config.Sorted {
|
||||
v, found := c.LanguageConfigMap[f.Name]
|
||||
if !found {
|
||||
return fmt.Errorf("invalid language configuration for %q", f.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the LanguageConfigSlice by language weight (if set) or lang.
|
||||
sort.Slice(langKeys, func(i, j int) bool {
|
||||
ki := langKeys[i]
|
||||
kj := langKeys[j]
|
||||
lki := c.LanguageConfigMap[ki]
|
||||
lkj := c.LanguageConfigMap[kj]
|
||||
li := lki.Languages[ki]
|
||||
lj := lkj.Languages[kj]
|
||||
if li.Weight != lj.Weight {
|
||||
return li.Weight < lj.Weight
|
||||
}
|
||||
return ki < kj
|
||||
})
|
||||
|
||||
// See issue #13646.
|
||||
defaultConfigLanguageFallback := en
|
||||
if !hasEn {
|
||||
// Pick the first one.
|
||||
defaultConfigLanguageFallback = langKeys[0]
|
||||
}
|
||||
|
||||
if c.Base.DefaultContentLanguage == "" {
|
||||
c.Base.DefaultContentLanguage = defaultConfigLanguageFallback
|
||||
}
|
||||
|
||||
for _, k := range langKeys {
|
||||
v := c.LanguageConfigMap[k]
|
||||
if v.DefaultContentLanguage == "" {
|
||||
v.DefaultContentLanguage = defaultConfigLanguageFallback
|
||||
}
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf)
|
||||
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -899,25 +878,29 @@ func (c *Configs) Init() error {
|
||||
}
|
||||
languages = languages[:n]
|
||||
|
||||
var languagesDefaultFirst langs.Languages
|
||||
for _, l := range languages {
|
||||
if l.Lang == c.Base.DefaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
for _, l := range languages {
|
||||
if l.Lang != c.Base.DefaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
c.Languages = languages
|
||||
c.ConfiguredDimensions = &sitesmatrix.ConfiguredDimensions{
|
||||
ConfiguredLanguages: c.Base.Languages.Config,
|
||||
ConfiguredVersions: c.Base.Versions.Config,
|
||||
ConfiguredRoles: c.Base.Roles.Config,
|
||||
}
|
||||
|
||||
c.Languages = languages
|
||||
c.LanguagesDefaultFirst = languagesDefaultFirst
|
||||
if err := c.ConfiguredDimensions.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intSetsCfg := sitesmatrix.IntSetsConfig{
|
||||
ApplyDefaults: sitesmatrix.IntSetsConfigApplyDefaultsIfNotSet,
|
||||
}
|
||||
matrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithConfig(intSetsCfg)
|
||||
c.DefaultContentSitesMatrix = matrix.Build()
|
||||
c.AllSitesMatrix = sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithAllIfNotSet().Build()
|
||||
|
||||
c.ContentPathParser = &paths.PathParser{
|
||||
LanguageIndex: languagesDefaultFirst.AsIndexSet(),
|
||||
IsLangDisabled: c.Base.IsLangDisabled,
|
||||
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
|
||||
ConfiguredDimensions: c.ConfiguredDimensions,
|
||||
LanguageIndex: languages.AsIndexSet(),
|
||||
IsLangDisabled: c.Base.IsLangDisabled,
|
||||
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
|
||||
IsOutputFormat: func(name, ext string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
@@ -934,12 +917,25 @@ func (c *Configs) Init() error {
|
||||
}
|
||||
|
||||
c.configLangs = make([]config.AllProvider, len(c.Languages))
|
||||
for i, l := range c.LanguagesDefaultFirst {
|
||||
|
||||
// Config can be shared between languages,
|
||||
// avoid initializing the same config more than once.
|
||||
for i, l := range c.Languages {
|
||||
langConfig := c.LanguageConfigMap[l.Lang]
|
||||
for _, s := range allDecoderSetups {
|
||||
if getInitializer := s.getInitializer; getInitializer != nil {
|
||||
if err := getInitializer(langConfig).InitConfig(logger, nil, c.ConfiguredDimensions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.configLangs[i] = ConfigLanguage{
|
||||
m: c,
|
||||
config: c.LanguageConfigMap[l.Lang],
|
||||
baseConfig: c.LoadingInfo.BaseConfig,
|
||||
language: l,
|
||||
m: c,
|
||||
config: langConfig,
|
||||
baseConfig: c.LoadingInfo.BaseConfig,
|
||||
language: l,
|
||||
languageIndex: i,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -948,7 +944,7 @@ func (c *Configs) Init() error {
|
||||
}
|
||||
|
||||
// Apply default project mounts.
|
||||
if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil {
|
||||
if err := modules.ApplyProjectConfigDefaults(logger.Logger(), c.Modules[0], c.configLangs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -957,7 +953,7 @@ func (c *Configs) Init() error {
|
||||
for _, m := range c.Modules[0].Mounts() {
|
||||
var found bool
|
||||
for _, cm := range c.Base.Module.Mounts {
|
||||
if cm.Source == m.Source && cm.Target == m.Target && cm.Lang == m.Lang {
|
||||
if cm.Equal(m) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -968,7 +964,7 @@ func (c *Configs) Init() error {
|
||||
}
|
||||
|
||||
// Transfer the changed mounts to the language versions (all share the same mount set, but can be displayed in different languages).
|
||||
for _, l := range c.LanguageConfigSlice {
|
||||
for _, l := range c.LanguageConfigMap {
|
||||
l.Module.Mounts = c.Base.Module.Mounts
|
||||
}
|
||||
|
||||
@@ -985,7 +981,7 @@ func (c Configs) GetFirstLanguageConfig() config.AllProvider {
|
||||
|
||||
func (c Configs) GetByLang(lang string) config.AllProvider {
|
||||
for _, l := range c.configLangs {
|
||||
if l.Language().Lang == lang {
|
||||
if l.Language().(*langs.Language).Lang == lang {
|
||||
return l
|
||||
}
|
||||
}
|
||||
@@ -1028,7 +1024,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
|
||||
res.Cfg.Set("languages", hmaps.Params{lang: hmaps.Params{}})
|
||||
}
|
||||
bcfg := res.BaseConfig
|
||||
cfg := res.Cfg
|
||||
@@ -1054,14 +1050,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
mergedConfig := config.New()
|
||||
var differentRootKeys []string
|
||||
switch x := v.(type) {
|
||||
case maps.Params:
|
||||
case nil:
|
||||
case hmaps.Params:
|
||||
_, found := x["params"]
|
||||
if !found {
|
||||
x["params"] = maps.Params{
|
||||
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
|
||||
x["params"] = hmaps.Params{
|
||||
hmaps.MergeStrategyKey: hmaps.ParamsMergeStrategyDeep,
|
||||
}
|
||||
}
|
||||
|
||||
for kk, vv := range x {
|
||||
if kk == "_merge" {
|
||||
continue
|
||||
@@ -1070,19 +1066,35 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
// baseURL configure don the language level is a multihost setup.
|
||||
isMultihost = true
|
||||
}
|
||||
|
||||
if p, ok := vv.(hmaps.Params); ok {
|
||||
// With the introduction of YAML anchor and alias support, language config entries
|
||||
// may be contain shared references.
|
||||
// This also break potential cycles.
|
||||
vv = hmaps.CloneParamsDeep(p)
|
||||
}
|
||||
|
||||
if kk == "cascade" {
|
||||
// If not set, add the current language to make sure it does not get applied to other languages.
|
||||
page.AddLangToCascadeTargetMap(k, vv.(hmaps.Params))
|
||||
// Always clone cascade config to get the sites matrix right.
|
||||
differentRootKeys = append(differentRootKeys, kk)
|
||||
}
|
||||
|
||||
mergedConfig.Set(kk, vv)
|
||||
rootv := cfg.Get(kk)
|
||||
if rootv != nil && cfg.IsSet(kk) {
|
||||
// This overrides a root key and potentially needs a merge.
|
||||
if !reflect.DeepEqual(rootv, vv) {
|
||||
switch vvv := vv.(type) {
|
||||
case maps.Params:
|
||||
case hmaps.Params:
|
||||
differentRootKeys = append(differentRootKeys, kk)
|
||||
|
||||
// Use the language value as base.
|
||||
mergedConfigEntry := xmaps.Clone(vvv)
|
||||
// Note that this is already cloned above.
|
||||
mergedConfigEntry := vvv
|
||||
// Merge in the root value.
|
||||
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
|
||||
hmaps.MergeParams(mergedConfigEntry, rootv.(hmaps.Params))
|
||||
|
||||
mergedConfig.Set(kk, mergedConfigEntry)
|
||||
default:
|
||||
@@ -1092,7 +1104,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
} else {
|
||||
switch vv.(type) {
|
||||
case maps.Params:
|
||||
case hmaps.Params:
|
||||
differentRootKeys = append(differentRootKeys, kk)
|
||||
default:
|
||||
// Apply new values to the root.
|
||||
@@ -1100,7 +1112,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
}
|
||||
}
|
||||
differentRootKeys = helpers.UniqueStringsSorted(differentRootKeys)
|
||||
differentRootKeys = hstrings.UniqueStringsSorted(differentRootKeys)
|
||||
|
||||
if len(differentRootKeys) == 0 {
|
||||
langConfigMap[k] = all
|
||||
@@ -1128,7 +1140,8 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
|
||||
langConfigMap[k] = clone
|
||||
case maps.ParamsMergeStrategy:
|
||||
case hmaps.ParamsMergeStrategy:
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type in languages config: %T", v))
|
||||
|
||||
@@ -1142,10 +1155,16 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
l.CommonDirs.CacheDir = bcfg.CacheDir
|
||||
}
|
||||
|
||||
caches, err := filecache.NewCaches(all.Caches, fs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
|
||||
}
|
||||
|
||||
cm := &Configs{
|
||||
Base: all,
|
||||
LanguageConfigMap: langConfigMap,
|
||||
LoadingInfo: res,
|
||||
FileCaches: caches,
|
||||
IsMultihost: isMultihost,
|
||||
}
|
||||
|
||||
@@ -1179,7 +1198,7 @@ func decodeConfigFromParams(fs afero.Fs, logger loggers.Logger, bcfg config.Base
|
||||
})
|
||||
|
||||
for _, v := range decoderSetups {
|
||||
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg}
|
||||
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg, logger: logger}
|
||||
if err := v.decode(v, p); err != nil {
|
||||
return fmt.Errorf("failed to decode %q: %w", v.key, err)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ title: "p1"
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
-- layouts/_default/single.html --
|
||||
-- layouts/single.html --
|
||||
Title: {{ .Title }}
|
||||
`
|
||||
|
||||
@@ -66,10 +66,12 @@ Title: {{ .Title }}
|
||||
b.Assert(modConf.Mounts, qt.HasLen, 8)
|
||||
b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en"))
|
||||
b.Assert(modConf.Mounts[0].Target, qt.Equals, "content")
|
||||
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en")
|
||||
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "")
|
||||
b.Assert(modConf.Mounts[0].Sites.Matrix.Languages, qt.DeepEquals, []string{"en"})
|
||||
b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv"))
|
||||
b.Assert(modConf.Mounts[1].Target, qt.Equals, "content")
|
||||
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
|
||||
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "")
|
||||
b.Assert(modConf.Mounts[1].Sites.Matrix.Languages, qt.DeepEquals, []string{"sv"})
|
||||
}
|
||||
|
||||
func TestConfigAliases(t *testing.T) {
|
||||
@@ -139,7 +141,7 @@ disableKinds = ["taxonomy", "term"]
|
||||
[pagination]
|
||||
disableAliases = true
|
||||
pagerSize = 2
|
||||
-- layouts/_default/list.html --
|
||||
-- layouts/list.html --
|
||||
{{ $paginator := .Paginate site.RegularPages }}
|
||||
{{ template "_internal/pagination.html" . }}
|
||||
{{ range $paginator.Pages }}
|
||||
@@ -189,7 +191,7 @@ func TestInvalidOutputFormat(t *testing.T) {
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
[outputs]
|
||||
home = ['html','foo']
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
x
|
||||
`
|
||||
|
||||
@@ -198,28 +200,6 @@ x
|
||||
b.Assert(err.Error(), qt.Contains, `failed to create config: unknown output format "foo" for kind "home"`)
|
||||
}
|
||||
|
||||
// Issue 13201
|
||||
func TestLanguageConfigSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
[languages.en]
|
||||
title = 'TITLE_EN'
|
||||
weight = 2
|
||||
[languages.de]
|
||||
title = 'TITLE_DE'
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
title = 'TITLE_FR'
|
||||
weight = 3
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`)
|
||||
}
|
||||
|
||||
func TestContentTypesDefault(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
|
||||
@@ -15,20 +15,25 @@ package allconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/httpcache"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/privacy"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
"github.com/gohugoio/hugo/config/services"
|
||||
"github.com/gohugoio/hugo/deploy/deployconfig"
|
||||
"github.com/gohugoio/hugo/hugolib/roles"
|
||||
"github.com/gohugoio/hugo/hugolib/segments"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/hugolib/versions"
|
||||
"github.com/gohugoio/hugo/markup/markup_config"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/minifiers"
|
||||
@@ -46,16 +51,18 @@ import (
|
||||
)
|
||||
|
||||
type decodeConfig struct {
|
||||
p config.Provider
|
||||
c *Config
|
||||
fs afero.Fs
|
||||
bcfg config.BaseConfig
|
||||
p config.Provider
|
||||
c *Config
|
||||
fs afero.Fs
|
||||
bcfg config.BaseConfig
|
||||
logger loggers.Logger
|
||||
}
|
||||
|
||||
type decodeWeight struct {
|
||||
key string
|
||||
decode func(decodeWeight, decodeConfig) error
|
||||
getCompiler func(c *Config) configCompiler
|
||||
getInitializer func(c *Config) configInitializer
|
||||
weight int
|
||||
internalOrDeprecated bool // Hide it from the docs.
|
||||
}
|
||||
@@ -69,8 +76,10 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
}
|
||||
|
||||
// This need to match with Lang which is always lower case.
|
||||
// This need to match with the map keys, always lower case.
|
||||
p.c.RootConfig.DefaultContentLanguage = strings.ToLower(p.c.RootConfig.DefaultContentLanguage)
|
||||
p.c.RootConfig.DefaultContentRole = strings.ToLower(p.c.RootConfig.DefaultContentRole)
|
||||
p.c.RootConfig.DefaultContentVersion = strings.ToLower(p.c.RootConfig.DefaultContentVersion)
|
||||
|
||||
return nil
|
||||
},
|
||||
@@ -140,9 +149,12 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "segments",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key))
|
||||
p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key), p.c.RenderSegments, p.logger)
|
||||
return err
|
||||
},
|
||||
getInitializer: func(c *Config) configInitializer {
|
||||
return c.Segments.Config
|
||||
},
|
||||
},
|
||||
"server": {
|
||||
key: "server",
|
||||
@@ -184,7 +196,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "outputs",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
|
||||
p.c.Outputs = make(map[string][]string)
|
||||
for k, v := range m {
|
||||
s := types.ToStringSlicePreserveString(v)
|
||||
@@ -210,10 +222,74 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
},
|
||||
},
|
||||
"languages": {
|
||||
key: "languages",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
if len(m) == 1 {
|
||||
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
|
||||
var first hmaps.Params
|
||||
var ok bool
|
||||
for _, v := range m {
|
||||
first, ok = v.(hmaps.Params)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
if _, found := first["languagecode"]; !found {
|
||||
first["languagecode"] = p.p.GetString("languagecode")
|
||||
}
|
||||
}
|
||||
}
|
||||
var (
|
||||
err error
|
||||
defaultContentLanguage string
|
||||
)
|
||||
p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, p.c.RootConfig.DisableLanguages, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode languages config: %w", err)
|
||||
}
|
||||
for k, v := range p.c.Languages.Config.LanguageConfigs {
|
||||
if v.Disabled {
|
||||
p.c.RootConfig.DisableLanguages = append(p.c.RootConfig.DisableLanguages, k)
|
||||
}
|
||||
}
|
||||
|
||||
p.c.RootConfig.DisableLanguages = hstrings.UniqueStringsReuse(p.c.RootConfig.DisableLanguages)
|
||||
sort.Strings(p.c.RootConfig.DisableLanguages)
|
||||
p.c.RootConfig.DefaultContentLanguage = defaultContentLanguage
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"versions": {
|
||||
key: "versions",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
|
||||
return err
|
||||
},
|
||||
},
|
||||
"roles": {
|
||||
key: "roles",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var (
|
||||
err error
|
||||
defaultContentRole string
|
||||
)
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
p.c.Roles, defaultContentRole, err = roles.DecodeConfig(p.c.RootConfig.DefaultContentRole, m)
|
||||
p.c.RootConfig.DefaultContentRole = defaultContentRole
|
||||
|
||||
return err
|
||||
},
|
||||
},
|
||||
|
||||
"params": {
|
||||
key: "params",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params"))
|
||||
p.c.Params = hmaps.CleanConfigStringMap(p.p.GetStringMap("params"))
|
||||
if p.c.Params == nil {
|
||||
p.c.Params = make(map[string]any)
|
||||
}
|
||||
@@ -233,7 +309,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "module",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.Module, err = modules.DecodeConfig(p.p)
|
||||
p.c.Module, err = modules.DecodeConfig(p.logger.Logger(), p.p)
|
||||
return err
|
||||
},
|
||||
},
|
||||
@@ -259,7 +335,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "taxonomies",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
if p.p.IsSet(d.key) {
|
||||
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
p.c.Taxonomies = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -283,56 +359,15 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"languages": {
|
||||
key: "languages",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
m := p.p.GetStringMap(d.key)
|
||||
if len(m) == 1 {
|
||||
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
|
||||
var first maps.Params
|
||||
var ok bool
|
||||
for _, v := range m {
|
||||
first, ok = v.(maps.Params)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
if _, found := first["languagecode"]; !found {
|
||||
first["languagecode"] = p.p.GetString("languagecode")
|
||||
}
|
||||
}
|
||||
}
|
||||
p.c.Languages, err = langs.DecodeConfig(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate defaultContentLanguage.
|
||||
if p.c.DefaultContentLanguage != "" {
|
||||
var found bool
|
||||
for lang := range p.c.Languages {
|
||||
if lang == p.c.DefaultContentLanguage {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"cascade": {
|
||||
key: "cascade",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key))
|
||||
p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key))
|
||||
return err
|
||||
},
|
||||
getInitializer: func(c *Config) configInitializer { return c.Cascade },
|
||||
},
|
||||
"menus": {
|
||||
key: "menus",
|
||||
@@ -412,7 +447,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"author": {
|
||||
key: "author",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Author = maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
p.c.Author = hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
@@ -420,7 +455,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"social": {
|
||||
key: "social",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Social = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
p.c.Social = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
@@ -434,8 +469,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
p.c.UglyURLs = vv
|
||||
case string:
|
||||
p.c.UglyURLs = vv == "true"
|
||||
case maps.Params:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(maps.CleanConfigStringMap(vv))
|
||||
case hmaps.Params:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(hmaps.CleanConfigStringMap(vv))
|
||||
default:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(v)
|
||||
}
|
||||
|
||||
@@ -19,28 +19,29 @@ import (
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
)
|
||||
|
||||
type ConfigLanguage struct {
|
||||
config *Config
|
||||
baseConfig config.BaseConfig
|
||||
|
||||
m *Configs
|
||||
language *langs.Language
|
||||
config *Config
|
||||
baseConfig config.BaseConfig
|
||||
m *Configs
|
||||
language *langs.Language
|
||||
languageIndex int
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Language() *langs.Language {
|
||||
func (c ConfigLanguage) Language() any {
|
||||
return c.language
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Languages() langs.Languages {
|
||||
return c.m.Languages
|
||||
func (c ConfigLanguage) LanguageIndex() int {
|
||||
return c.languageIndex
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
|
||||
return c.m.LanguagesDefaultFirst
|
||||
func (c ConfigLanguage) Languages() any {
|
||||
return c.m.Languages
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) PathParser() *paths.PathParser {
|
||||
@@ -48,14 +49,14 @@ func (c ConfigLanguage) PathParser() *paths.PathParser {
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) LanguagePrefix() string {
|
||||
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.Language().Lang {
|
||||
return c.Language().Lang
|
||||
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.language.Lang {
|
||||
return c.language.Lang
|
||||
}
|
||||
|
||||
if !c.IsMultilingual() || c.DefaultContentLanguage() == c.Language().Lang {
|
||||
if !c.IsMultilingual() || c.DefaultContentLanguage() == c.language.Lang {
|
||||
return ""
|
||||
}
|
||||
return c.Language().Lang
|
||||
return c.language.Lang
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) BaseURL() urls.BaseURL {
|
||||
@@ -97,6 +98,10 @@ func (c ConfigLanguage) IsLangDisabled(lang string) bool {
|
||||
return c.config.C.DisabledLanguages[lang]
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) IsKindEnabled(kind string) bool {
|
||||
return !c.config.C.DisabledKinds[kind]
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) IgnoredLogs() map[string]bool {
|
||||
return c.config.C.IgnoredLogs
|
||||
}
|
||||
@@ -117,6 +122,10 @@ func (c ConfigLanguage) BaseConfig() config.BaseConfig {
|
||||
return c.baseConfig
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) FileCaches() any {
|
||||
return c.m.FileCaches
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Dirs() config.CommonDirs {
|
||||
return c.config.CommonDirs
|
||||
}
|
||||
@@ -129,6 +138,10 @@ func (c ConfigLanguage) WorkingDir() string {
|
||||
return c.m.Base.WorkingDir
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) CacheDirMisc() string {
|
||||
return c.config.Caches.CacheDirMisc()
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Quiet() bool {
|
||||
return c.m.Base.Internal.Quiet
|
||||
}
|
||||
@@ -137,11 +150,11 @@ func (c ConfigLanguage) Watching() bool {
|
||||
return c.m.Base.Internal.Watch
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager {
|
||||
func (c ConfigLanguage) NewIdentityManager(opts ...identity.ManagerOption) identity.Manager {
|
||||
if !c.Watching() {
|
||||
return identity.NopManager
|
||||
}
|
||||
return identity.NewManager(name, opts...)
|
||||
return identity.NewManager(opts...)
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
|
||||
@@ -155,16 +168,24 @@ func (c ConfigLanguage) GetConfigSection(s string) any {
|
||||
return c.config.Security
|
||||
case "build":
|
||||
return c.config.Build
|
||||
case "cascade":
|
||||
return c.config.Cascade
|
||||
case "frontmatter":
|
||||
return c.config.Frontmatter
|
||||
case "caches":
|
||||
return c.config.Caches
|
||||
case "markup":
|
||||
return c.config.Markup
|
||||
case "module":
|
||||
return c.config.Module
|
||||
case "mediaTypes":
|
||||
return c.config.MediaTypes.Config
|
||||
case "outputFormats":
|
||||
return c.config.OutputFormats.Config
|
||||
case "roles":
|
||||
return c.config.Roles.Config
|
||||
case "versions":
|
||||
return c.config.Versions.Config
|
||||
case "permalinks":
|
||||
return c.config.Permalinks
|
||||
case "minify":
|
||||
@@ -212,6 +233,14 @@ func (c ConfigLanguage) DefaultContentLanguageInSubdir() bool {
|
||||
return c.config.DefaultContentLanguageInSubdir
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) DefaultContentRoleInSubdir() bool {
|
||||
return c.config.DefaultContentRoleInSubdir
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) DefaultContentVersionInSubdir() bool {
|
||||
return c.config.DefaultContentVersionInSubdir
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) SummaryLength() int {
|
||||
return c.config.SummaryLength
|
||||
}
|
||||
@@ -259,3 +288,15 @@ func (c ConfigLanguage) StaticDirs() []string {
|
||||
func (c ConfigLanguage) EnableEmoji() bool {
|
||||
return c.config.EnableEmoji
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions {
|
||||
return c.m.ConfiguredDimensions
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) DefaultContentsitesMatrix() *sitesmatrix.IntSets {
|
||||
return c.m.DefaultContentSitesMatrix
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) AllSitesMatrix() *sitesmatrix.IntSets {
|
||||
return c.m.AllSitesMatrix
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
package allconfig
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
)
|
||||
@@ -27,12 +27,12 @@ func init() {
|
||||
if v.internalOrDeprecated {
|
||||
continue
|
||||
}
|
||||
cfg.Set(configRoot, make(maps.Params))
|
||||
cfg.Set(configRoot, make(hmaps.Params))
|
||||
}
|
||||
lang := maps.Params{
|
||||
"en": maps.Params{
|
||||
"menus": maps.Params{},
|
||||
"params": maps.Params{},
|
||||
lang := hmaps.Params{
|
||||
"en": hmaps.Params{
|
||||
"menus": hmaps.Params{},
|
||||
"params": hmaps.Params{},
|
||||
},
|
||||
}
|
||||
cfg.Set("languages", lang)
|
||||
|
||||
+40
-17
@@ -19,19 +19,20 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
hglob "github.com/gohugoio/hugo/hugofs/glob"
|
||||
hglob "github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
@@ -40,7 +41,14 @@ import (
|
||||
//lint:ignore ST1005 end user message.
|
||||
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
|
||||
|
||||
func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("failed to load config: %v", r)
|
||||
debug.PrintStack()
|
||||
}
|
||||
}()
|
||||
|
||||
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
|
||||
d.Environ = os.Environ()
|
||||
}
|
||||
@@ -59,7 +67,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
configs, err := fromLoadConfigResult(d.Fs, d.Logger, res)
|
||||
configs, err = fromLoadConfigResult(d.Fs, d.Logger, res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create config from result: %w", err)
|
||||
}
|
||||
@@ -87,13 +95,13 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
configs.Modules = moduleConfig.AllModules
|
||||
configs.ModulesClient = modulesClient
|
||||
|
||||
if err := configs.Init(); err != nil {
|
||||
if err := configs.Init(d.Fs, d.Logger); err != nil {
|
||||
return nil, fmt.Errorf("failed to init config: %w", err)
|
||||
}
|
||||
|
||||
loggers.SetGlobalLogger(d.Logger)
|
||||
|
||||
return configs, nil
|
||||
return
|
||||
}
|
||||
|
||||
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
|
||||
@@ -158,7 +166,7 @@ func (l configLoader) applyConfigAliases() error {
|
||||
}
|
||||
|
||||
func (l configLoader) applyDefaultConfig() error {
|
||||
defaultSettings := maps.Params{
|
||||
defaultSettings := hmaps.Params{
|
||||
// These dirs are used early/before we build the config struct.
|
||||
"themesDir": "themes",
|
||||
"configDir": "config",
|
||||
@@ -170,10 +178,16 @@ func (l configLoader) applyDefaultConfig() error {
|
||||
}
|
||||
|
||||
func (l configLoader) normalizeCfg(cfg config.Provider) error {
|
||||
if b, ok := cfg.Get("minifyOutput").(bool); ok && b {
|
||||
cfg.Set("minify.minifyOutput", true)
|
||||
} else if b, ok := cfg.Get("minify").(bool); ok && b {
|
||||
cfg.Set("minify", maps.Params{"minifyOutput": true})
|
||||
if b, ok := cfg.Get("minifyOutput").(bool); ok {
|
||||
hugo.Deprecate("site config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
|
||||
if b {
|
||||
cfg.Set("minify.minifyOutput", true)
|
||||
}
|
||||
} else if b, ok := cfg.Get("minify").(bool); ok {
|
||||
hugo.Deprecate("site config minify", "Use minify.minifyOutput instead.", "v0.150.0")
|
||||
if b {
|
||||
cfg.Set("minify", hmaps.Params{"minifyOutput": true})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -206,8 +220,8 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
|
||||
var hugoEnv []types.KeyValueStr
|
||||
for _, v := range environ {
|
||||
key, val := config.SplitEnvVar(v)
|
||||
if strings.HasPrefix(key, hugoEnvPrefix) {
|
||||
delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
|
||||
if after, ok := strings.CutPrefix(key, hugoEnvPrefix); ok {
|
||||
delimiterAndKey := after
|
||||
if len(delimiterAndKey) < 2 {
|
||||
continue
|
||||
}
|
||||
@@ -226,7 +240,7 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
|
||||
}
|
||||
|
||||
for _, env := range hugoEnv {
|
||||
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
|
||||
existing, nestedKey, owner, err := hmaps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -280,7 +294,14 @@ func (l *configLoader) envValToVal(k string, v any) any {
|
||||
|
||||
func (l *configLoader) envStringToVal(k, v string) any {
|
||||
switch k {
|
||||
case "disablekinds", "disablelanguages":
|
||||
case "disablekinds", "disablelanguages", "ignorefiles", "ignorelogs":
|
||||
v = strings.TrimSpace(v)
|
||||
if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
|
||||
if parsed, err := metadecoders.Default.UnmarshalStringTo(v, []any{}); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") {
|
||||
return strings.Split(v, ",")
|
||||
} else {
|
||||
@@ -456,6 +477,7 @@ func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bo
|
||||
PublishDir: publishDir,
|
||||
Environment: l.Environment,
|
||||
CacheDir: conf.Caches.CacheDirModules(),
|
||||
ModuleQueriesCache: configs.FileCaches.ModuleQueriesCache(),
|
||||
ModuleConfig: conf.Module,
|
||||
IgnoreVendor: ignoreVendor,
|
||||
IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist,
|
||||
@@ -525,11 +547,12 @@ func (l configLoader) loadConfig(configName string) (string, error) {
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func (l configLoader) deleteMergeStrategies() {
|
||||
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
|
||||
func (l configLoader) deleteMergeStrategies() (err error) {
|
||||
l.cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
|
||||
params[len(params)-1].Params.DeleteMergeStrategy()
|
||||
return false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (l configLoader) wrapFileError(err error, filename string) error {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user