mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-30 18:22:37 +00:00
Compare commits
397 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d976efdda | |||
| 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 | |||
| 3aa22b0942 | |||
| 40c3d8233d | |||
| 7ff5ec734c | |||
| 3937ab24d0 | |||
| 9c57af1351 | |||
| d240a705d6 | |||
| bbcc2a7973 | |||
| 98ba786f2f | |||
| 6f42cfbc9b | |||
| a84beee429 | |||
| 65893efd8d | |||
| c0d9bebacc | |||
| 3e2f1cdfdb | |||
| 0a5b870281 | |||
| bba6996e15 | |||
| 94e2c276a8 | |||
| 90d397b142 | |||
| 61e6c730dd | |||
| e4f6b9eef8 | |||
| bb147f91ee | |||
| 266d46dccc | |||
| e77b2ad8fd | |||
| 9487acf6ab | |||
| 84b31721bf | |||
| b8ba33ca95 | |||
| f967212b72 | |||
| 1b4c423667 | |||
| 1e9a0b93e3 | |||
| cfc8d315b4 | |||
| dd6e2c8724 | |||
| 762417617c | |||
| 29bdbde19c | |||
| 6a4a3ab8f8 | |||
| 36f6f987a9 | |||
| 18a9ca7d7a | |||
| b6c8dfa9dc | |||
| 621ea42f3c | |||
| 4ef5720141 | |||
| 34e83789f7 | |||
| 4d3ebe4d21 | |||
| b5c0383bda | |||
| 4217fee4b0 | |||
| fad57964aa | |||
| 10da2bd765 | |||
| 01241d5dc9 | |||
| 8e61f1fe12 | |||
| f37412a575 | |||
| 21a4a9acd7 | |||
| 7a4a4790e5 | |||
| 54065b7ef8 | |||
| e333836f49 | |||
| cc7bfeea32 | |||
| 32eb1a8ad4 | |||
| 32af02cd3e | |||
| 189453612e | |||
| 5273a884d4 | |||
| 6334948515 | |||
| 75259636c8 | |||
| 0df9f3510f | |||
| 302e6a726b | |||
| 202fe0d45c | |||
| 843ffeb48d | |||
| bff5d19121 | |||
| da370d30de | |||
| 6bd328c584 | |||
| 766a2e7868 | |||
| 13e1617557 | |||
| 463e440c7a | |||
| 0a5fd8ebb8 | |||
| e57dcd3795 | |||
| eaf5ace30d | |||
| 9ad26b69ad | |||
| f47193669d | |||
| 013c8cfb25 | |||
| e25db38467 | |||
| 7766fc6241 | |||
| 4a48facef4 | |||
| 0c7b1a3f26 | |||
| 970b887ba1 | |||
| b9b95e5aec | |||
| 84c8426f32 | |||
| a03a245f0c | |||
| 5a81a3a4cf | |||
| 61317821e4 | |||
| 6142bc701c | |||
| e6574cf7a7 | |||
| 05417512bd | |||
| 81426998b8 | |||
| 6def5a1ba9 | |||
| bc98e7a80d | |||
| c745a3e108 | |||
| 9d1d8c8899 | |||
| 84d7a108e8 | |||
| 325a0dba63 | |||
| d70f828e2b | |||
| c7feb15d10 | |||
| 363ab48a24 | |||
| 80f0595311 | |||
| b39b249623 | |||
| d799c045fd | |||
| 95666fc5a4 | |||
| 620fc87b56 | |||
| 8b2124e7c3 | |||
| 5fec7829b1 | |||
| 927d1ec6c1 | |||
| be93d5218b | |||
| a1cb15e1cf | |||
| 673a4d00eb | |||
| 31db7edf6d | |||
| 5857b60cbc | |||
| 7d0039b86d | |||
| 07983e04e2 | |||
| 5c491409d3 | |||
| 75b219db89 | |||
| ad4f63c92f | |||
| 53202314ab | |||
| 2fce0bac03 | |||
| 179aea11ac | |||
| b3d87dd0fd | |||
| 61a286595e | |||
| 6a0e04241a | |||
| 1bd7ac7ed9 | |||
| 41cb880f9a | |||
| df44ee1353 | |||
| 1ad3d39dc4 | |||
| 496730840e | |||
| 6d69dc88a4 | |||
| db72a1f075 | |||
| ad787614e8 | |||
| 9c65b9e88d | |||
| 4eb0e4286a | |||
| 5e62cc6fce | |||
| 1408c156d8 | |||
| be3b147860 | |||
| 99fff2997d | |||
| 1e0b058efe | |||
| 088cd2f996 | |||
| a88b488181 | |||
| d5a8c330cb | |||
| 61328976e1 | |||
| 64cf008880 | |||
| d61b9fc605 | |||
| 01667275d4 | |||
| 985af1c097 | |||
| 65c94c7b23 | |||
| e8e8ce10d2 | |||
| cf9e6904cc | |||
| 8a2830f2dc | |||
| 1e0287f472 | |||
| 915ba3f7f0 | |||
| 05ef8b713a | |||
| 30b9c19c76 | |||
| c8710625b7 | |||
| 53221f88ca | |||
| ff3ab192c2 | |||
| d1c394442b | |||
| 1074e01152 | |||
| c19f1f2363 | |||
| 9221cbca49 | |||
| e3e3f9ae17 | |||
| acc4fe24be | |||
| 3b9f2a7ded | |||
| 648204b3f1 | |||
| 5d1b9d3985 | |||
| 383dd82f95 | |||
| 653f1c1d46 | |||
| 5be51ac3db | |||
| 208a0de6c3 | |||
| 18d2d2f985 | |||
| cd3e3499bd | |||
| 8cd799aac9 | |||
| d4c6dd16b1 | |||
| af0602c343 | |||
| 650fe6cd01 | |||
| d63491beb2 | |||
| 24ac6a9de9 | |||
| e6e18e9122 | |||
| 43af3bc07d | |||
| 10b55439a8 | |||
| 1e00842489 | |||
| 83cfdd78ca | |||
| 812ea0b325 | |||
| 07cbe5701e | |||
| c15ebce2fd | |||
| 6f14dbe24c | |||
| 8d2379bcb3 | |||
| 26d986fc0d | |||
| ebc0693659 | |||
| 7ca6bb06b6 | |||
| bddd2f9001 | |||
| 1c691358f7 | |||
| 17db4edb02 | |||
| 3968f9158e | |||
| e9bd2373a0 | |||
| a6bd67793b | |||
| f34cdc382a | |||
| 52561d561a | |||
| f4f21f5ea3 | |||
| a98ec3bd01 | |||
| d28c84a871 | |||
| 61c39ae63b | |||
| 2037137fbf | |||
| d78d4cf161 | |||
| f9aae1581b | |||
| b9add1c702 | |||
| b0686712ba | |||
| 93d9c0533a | |||
| 93df17661f | |||
| eebea9ec41 | |||
| 3f2e1c08e3 | |||
| 3a11d22da3 | |||
| cb4a097190 | |||
| 666444f0a5 | |||
| e5eecbd9bc | |||
| 4094a1e12c | |||
| 6927e6f048 | |||
| c498d0fe1e | |||
| 521911a576 | |||
| b7ae24b9c2 | |||
| 2f4e666d7e | |||
| 1a4851f138 | |||
| d1f23309bf | |||
| 04f21b4d80 | |||
| ab9e545760 | |||
| 381c0da85d | |||
| 227e429267 | |||
| d0ce942190 | |||
| 4fd7f3233f |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22400.20000
|
||||
- image: bepsays/ci-hugoreleaser:1.22500.20300
|
||||
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.20300
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: true
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: false
|
||||
summary: true
|
||||
code_review: true
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
@@ -0,0 +1,17 @@
|
||||
You are an expert at detecting AI-generated content. Your main tasks is to analyze pull requests and determine if they were likely generated by AI and if this is disclosed in the pull request description.
|
||||
|
||||
Important: You must start your response with a confidence score line in exactly this format:
|
||||
Confidence Score: X% where X is a number between 0 and 100.
|
||||
|
||||
Then provide your analysis in this format:
|
||||
Reasoning: (explain your analysis)
|
||||
Key Indicators: (list specific elements that suggest AI or human authorship)
|
||||
|
||||
A high confidence score (>80%) means you are very confident the content was AI-generated.
|
||||
A low confidence score (<20%) means you are very confident the content was human-generated.
|
||||
Use scores between 20-80% when there is uncertainty.
|
||||
|
||||
If there's a high confidence score:
|
||||
|
||||
* Add the label "Suspect AI" to the PR
|
||||
* If it's not disclosed that it's AI generated, add a link to [AI Assistance Notice](../CONTRIBUTING.md#ai-assistance-notice) in the contribution gudelines.
|
||||
@@ -0,0 +1,17 @@
|
||||
name: AI Detection
|
||||
on:
|
||||
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review, synchronize]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
detect-ai:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: AI Watchdog
|
||||
uses: Citric-Sheep/ai-watchdog@0db4a635d309db0f8d82642130dfc33e91cf33fa # v0.2.3
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -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
|
||||
@@ -46,3 +46,4 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: HUGO_BUILD_TAGS=extended,withdeploy
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
@@ -34,9 +34,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 +45,28 @@ 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 gems
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
gem install asciidoctor-diagram-ditaamini -v "1.0.3"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
- name: Install Java # required by asciidoctor-diagram-ditaamini
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
java-package: jre
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@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
|
||||
|
||||
+13
-2
@@ -1,4 +1,4 @@
|
||||
>**Note:** We would apprecitate if you hold on with any big refactorings (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
|
||||
>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
|
||||
|
||||
# Contributing to Hugo
|
||||
|
||||
@@ -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.**
|
||||
@@ -79,6 +80,16 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* 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,
|
||||
|
||||
+4
-4
@@ -2,8 +2,8 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
ARG GO_VERSION="1.23.2"
|
||||
ARG ALPINE_VERSION="3.20"
|
||||
ARG GO_VERSION="1.25"
|
||||
ARG ALPINE_VERSION="3.22"
|
||||
ARG DART_SASS_VERSION="1.79.3"
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
|
||||
@@ -19,7 +19,7 @@ RUN apk add clang lld
|
||||
COPY --from=xx / /
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
RUN xx-apk add musl-dev gcc g++
|
||||
RUN xx-apk add musl-dev gcc g++
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=withdeploy .
|
||||
@@ -72,7 +72,7 @@ RUN mkdir -p /var/hugo/bin /cache && \
|
||||
adduser -Sg hugo -u 1000 -h /var/hugo hugo && \
|
||||
chown -R hugo: /var/hugo /cache && \
|
||||
# For the Hugo's Git integration to work.
|
||||
runuser -u hugo -- git config --global --add safe.directory /project && \
|
||||
runuser -u hugo -- git config --global --add safe.directory /project && \
|
||||
# See https://github.com/gohugoio/hugo/issues/9810
|
||||
runuser -u hugo -- git config --global core.quotepath false
|
||||
|
||||
|
||||
@@ -65,9 +65,11 @@ 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/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/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/gohugoioTheme/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
|
||||
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></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
|
||||
@@ -80,10 +82,10 @@ Encode to the WebP format when [processing images]. You can decode WebP images w
|
||||
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See [details].|:x:|:heavy_check_mark:
|
||||
|
||||
[dart sass]: /functions/css/sass/#dart-sass
|
||||
[processing images]: /content-management/image-processing/
|
||||
[transpile sass to css]: /functions/css/sass/
|
||||
[details]: /hosting-and-deployment/hugo-deploy/
|
||||
[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.
|
||||
|
||||
@@ -100,9 +102,9 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
|
||||
|
||||
Prerequisites to build Hugo from source:
|
||||
|
||||
- Standard edition: Go 1.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:
|
||||
|
||||
@@ -189,7 +191,6 @@ github.com/bep/simplecobra="v0.5.0"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.4"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.11.5"
|
||||
|
||||
Vendored
+1
-1
@@ -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
+4
-4
@@ -191,16 +191,16 @@ func TestPanicInCreate(t *testing.T) {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for j := 0; j < 3; j++ {
|
||||
for i := range 3 {
|
||||
for range 3 {
|
||||
c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i))
|
||||
c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i))
|
||||
}
|
||||
}
|
||||
|
||||
// Test the same keys again without the panic.
|
||||
for i := 0; i < 3; i++ {
|
||||
for j := 0; j < 3; j++ {
|
||||
for i := range 3 {
|
||||
for range 3 {
|
||||
v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
|
||||
return testItem{
|
||||
name: key,
|
||||
|
||||
+3
-3
@@ -59,7 +59,7 @@ dir = ":resourceDir/_gen"
|
||||
caches, err := filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache := caches[name]
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
@@ -74,7 +74,7 @@ dir = ":resourceDir/_gen"
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 5, msg)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i < 5 {
|
||||
@@ -97,7 +97,7 @@ dir = ":resourceDir/_gen"
|
||||
c.Assert(count, qt.Equals, 4)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i != 5 {
|
||||
|
||||
Vendored
+4
-4
@@ -105,7 +105,7 @@ dir = ":cacheDir/c"
|
||||
}
|
||||
|
||||
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
info, r, err := ca.GetOrCreate("a", rf("abc"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(r, qt.Not(qt.IsNil))
|
||||
@@ -193,11 +193,11 @@ dir = "/cache/c"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
for i := range 50 {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 20; j++ {
|
||||
for range 20 {
|
||||
ca := caches.Get(cacheName)
|
||||
c.Assert(ca, qt.Not(qt.IsNil))
|
||||
filename, data := filenameData(i)
|
||||
@@ -270,7 +270,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
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
+45
-12
@@ -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 behaviour (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
|
||||
|
||||
@@ -52,12 +60,14 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
// Enable HTTP cache behaviour (RFC 9111) for these rsources.
|
||||
// Enable HTTP cache behavior (RFC 9111) for these resources.
|
||||
For GlobMatcher
|
||||
}
|
||||
|
||||
func (c *Config) Compile() (ConfigCompiled, error) {
|
||||
var cc ConfigCompiled
|
||||
cc := ConfigCompiled{
|
||||
Base: *c,
|
||||
}
|
||||
|
||||
p, err := c.Cache.For.CompilePredicate()
|
||||
if err != nil {
|
||||
@@ -122,7 +132,12 @@ type GlobMatcher struct {
|
||||
Includes []string
|
||||
}
|
||||
|
||||
func (gm GlobMatcher) IsZero() bool {
|
||||
return len(gm.Includes) == 0 && len(gm.Excludes) == 0
|
||||
}
|
||||
|
||||
type ConfigCompiled struct {
|
||||
Base Config
|
||||
For predicate.P[string]
|
||||
PollConfigs []PollConfigCompiled
|
||||
}
|
||||
@@ -155,16 +170,19 @@ func (p PollConfigCompiled) IsZero() bool {
|
||||
}
|
||||
|
||||
func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
|
||||
var p predicate.P[string]
|
||||
if gm.IsZero() {
|
||||
panic("no includes or excludes")
|
||||
}
|
||||
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 {
|
||||
@@ -172,16 +190,16 @@ 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(bcfg config.BaseConfig, m map[string]any) (Config, error) {
|
||||
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
|
||||
if len(m) == 0 {
|
||||
return DefaultConfig, nil
|
||||
}
|
||||
@@ -203,5 +221,20 @@ func DecodeConfig(bcfg config.BaseConfig, m map[string]any) (Config, error) {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if c.Cache.For.IsZero() {
|
||||
c.Cache.For = DefaultConfig.Cache.For
|
||||
}
|
||||
|
||||
for pci := range c.Polls {
|
||||
if c.Polls[pci].For.IsZero() {
|
||||
c.Polls[pci].For = DefaultConfig.Cache.For
|
||||
c.Polls[pci].Disable = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Polls) == 0 {
|
||||
c.Polls = DefaultConfig.Polls
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
+31
@@ -22,6 +22,8 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigCustom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[httpcache]
|
||||
@@ -51,6 +53,8 @@ includes = ["**gohugo.io**"]
|
||||
}
|
||||
|
||||
func TestConfigDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
`
|
||||
@@ -62,3 +66,30 @@ func TestConfigDefault(t *testing.T) {
|
||||
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
|
||||
b.Assert(compiled.PollConfigFor("https://gohugo.io/foo.jpg").Config.Disable, qt.IsTrue)
|
||||
}
|
||||
|
||||
func TestConfigPollsOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[httpcache]
|
||||
[[httpcache.polls]]
|
||||
low = "5s"
|
||||
high = "32s"
|
||||
[httpcache.polls.for]
|
||||
includes = ["**gohugo.io**"]
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
compiled := b.H.Configs.Base.C.HTTPCache
|
||||
|
||||
b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse)
|
||||
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
|
||||
|
||||
pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg")
|
||||
b.Assert(pc.Config.Low, qt.Equals, 5*time.Second)
|
||||
b.Assert(pc.Config.High, qt.Equals, 32*time.Second)
|
||||
b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue)
|
||||
}
|
||||
|
||||
Vendored
+31
@@ -17,6 +17,7 @@ import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
)
|
||||
|
||||
func TestGlobMatcher(t *testing.T) {
|
||||
@@ -40,3 +41,33 @@ func TestGlobMatcher(t *testing.T) {
|
||||
c.Assert(p("foo/bar/foo.css"), qt.IsFalse)
|
||||
c.Assert(p("foo/bar/foo.xml"), qt.IsTrue)
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
_, err := DefaultConfig.Compile()
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
|
||||
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]any{})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
|
||||
|
||||
_, err = cfg.Compile()
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cfg, err = DecodeConfig(config.BaseConfig{}, map[string]any{
|
||||
"cache": map[string]any{
|
||||
"polls": []map[string]any{
|
||||
{"disable": true},
|
||||
},
|
||||
},
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
_, err = cfg.Compile()
|
||||
c.Assert(err, qt.IsNil)
|
||||
}
|
||||
|
||||
+8
-7
@@ -26,6 +26,7 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -102,7 +103,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
}
|
||||
|
||||
for _, t := range include {
|
||||
for i := 0; i < t.NumMethod(); i++ {
|
||||
for i := range t.NumMethod() {
|
||||
|
||||
m := t.Method(i)
|
||||
if excludes[m.Name] || seen[m.Name] {
|
||||
@@ -122,7 +123,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
|
||||
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
|
||||
|
||||
for i := 0; i < numIn; i++ {
|
||||
for i := range numIn {
|
||||
in := m.Type.In(i)
|
||||
|
||||
name, pkg := nameAndPackage(in)
|
||||
@@ -137,7 +138,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
numOut := m.Type.NumOut()
|
||||
|
||||
if numOut > 0 {
|
||||
for i := 0; i < numOut; i++ {
|
||||
for i := range numOut {
|
||||
out := m.Type.Out(i)
|
||||
name, pkg := nameAndPackage(out)
|
||||
|
||||
@@ -304,7 +305,7 @@ func (m Method) inOutStr() string {
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := 0; i < len(args); i++ {
|
||||
for i := range args {
|
||||
args[i] = fmt.Sprintf("arg%d", i)
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
@@ -316,7 +317,7 @@ func (m Method) inStr() string {
|
||||
}
|
||||
|
||||
args := make([]string, len(m.In))
|
||||
for i := 0; i < len(args); i++ {
|
||||
for i := range args {
|
||||
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
|
||||
}
|
||||
return "(" + strings.Join(args, ", ") + ")"
|
||||
@@ -339,7 +340,7 @@ func (m Method) outStrNamed() string {
|
||||
}
|
||||
|
||||
outs := make([]string, len(m.Out))
|
||||
for i := 0; i < len(outs); i++ {
|
||||
for i := range outs {
|
||||
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
|
||||
}
|
||||
|
||||
@@ -435,7 +436,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
|
||||
// Exclude self
|
||||
for i, pkgImp := range pkgImports {
|
||||
if pkgImp == pkgPath {
|
||||
pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
|
||||
pkgImports = slices.Delete(pkgImports, i, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,8 +101,8 @@ type configKey struct {
|
||||
|
||||
// This is the root command.
|
||||
type rootCommand struct {
|
||||
Printf func(format string, v ...interface{})
|
||||
Println func(a ...interface{})
|
||||
Printf func(format string, v ...any)
|
||||
Println func(a ...any)
|
||||
StdOut io.Writer
|
||||
StdErr io.Writer
|
||||
|
||||
@@ -431,12 +431,12 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
// Used by mkcert (server).
|
||||
log.SetOutput(r.StdOut)
|
||||
|
||||
r.Printf = func(format string, v ...interface{}) {
|
||||
r.Printf = func(format string, v ...any) {
|
||||
if !r.quiet {
|
||||
fmt.Fprintf(r.StdOut, format, v...)
|
||||
}
|
||||
}
|
||||
r.Println = func(a ...interface{}) {
|
||||
r.Println = func(a ...any) {
|
||||
if !r.quiet {
|
||||
fmt.Fprintln(r.StdOut, a...)
|
||||
}
|
||||
|
||||
+7
-6
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
@@ -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
|
||||
@@ -90,7 +91,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
default:
|
||||
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
|
||||
var m map[string]interface{}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+21
-6
@@ -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 {
|
||||
@@ -50,6 +50,8 @@ func newGenCommand() *genCommand {
|
||||
highlightStyle string
|
||||
lineNumbersInlineStyle string
|
||||
lineNumbersTableStyle string
|
||||
omitEmpty bool
|
||||
omitClassComments bool
|
||||
)
|
||||
|
||||
newChromaStyles := func() simplecobra.Commander {
|
||||
@@ -79,7 +81,16 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatter := html.New(html.WithAllClasses(true))
|
||||
|
||||
if omitEmpty {
|
||||
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
|
||||
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
|
||||
}
|
||||
options := []html.Option{
|
||||
html.WithCSSComments(!omitClassComments),
|
||||
}
|
||||
formatter := html.New(options...)
|
||||
|
||||
w := os.Stdout
|
||||
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
formatter.WriteCSS(w, style)
|
||||
@@ -95,6 +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 (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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -174,13 +189,13 @@ url: %s
|
||||
prepender := func(filename string) string {
|
||||
name := filepath.Base(filename)
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
url := "/commands/" + strings.ToLower(base) + "/"
|
||||
url := "/docs/reference/commands/" + strings.ToLower(base) + "/"
|
||||
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
|
||||
}
|
||||
|
||||
linkHandler := func(name string) string {
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
return "/commands/" + strings.ToLower(base) + "/"
|
||||
return "/docs/reference/commands/" + strings.ToLower(base) + "/"
|
||||
}
|
||||
r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
|
||||
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
|
||||
@@ -222,7 +237,7 @@ url: %s
|
||||
}
|
||||
|
||||
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
|
||||
var m map[string]interface{}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -234,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",
|
||||
}
|
||||
|
||||
+36
-4
@@ -27,9 +27,11 @@ 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/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
@@ -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 {
|
||||
@@ -962,7 +980,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
lrl.Logf("no page to navigate to, force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
} else if len(otherChanges) > 0 {
|
||||
} else if len(otherChanges) > 0 || len(cssChanges) > 0 {
|
||||
if len(otherChanges) == 1 {
|
||||
// Allow single changes to be refreshed without a full page reload.
|
||||
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false)
|
||||
@@ -972,6 +990,9 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
lrl.Logf("force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
} else {
|
||||
lrl.Logf("force refresh")
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
|
||||
if len(cssChanges) > 0 {
|
||||
@@ -1024,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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+17
-12
@@ -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
|
||||
}
|
||||
@@ -76,10 +78,8 @@ Ensure you run this within the root directory of your site.`,
|
||||
&simpleCommand{
|
||||
name: "site",
|
||||
use: "site [path]",
|
||||
short: "Create a new site (skeleton)",
|
||||
long: `Create a new site in the provided directory.
|
||||
The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
short: "Create a new site",
|
||||
long: `Create a new site at the specified path.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
@@ -124,11 +124,9 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
&simpleCommand{
|
||||
name: "theme",
|
||||
use: "theme [name]",
|
||||
short: "Create a new theme (skeleton)",
|
||||
long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
according to your needs.`,
|
||||
short: "Create a new theme",
|
||||
long: `Create a new theme with the specified name in the ./themes directory.
|
||||
This generates a functional theme including template examples and sample content.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
@@ -144,7 +142,7 @@ according to your needs.`,
|
||||
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
|
||||
r.Println("Creating new theme in", createpath)
|
||||
|
||||
err = skeletons.CreateTheme(createpath, sourceFs)
|
||||
err = skeletons.CreateTheme(createpath, sourceFs, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -152,7 +150,14 @@ according to your needs.`,
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return []string{}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
|
||||
}
|
||||
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+12
-14
@@ -23,6 +23,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@@ -48,6 +49,8 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
@@ -57,7 +60,6 @@ import (
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
"github.com/spf13/afero"
|
||||
@@ -195,9 +197,7 @@ func (f *fileChangeDetector) PrepareNew() {
|
||||
}
|
||||
|
||||
f.prev = make(map[string]uint64)
|
||||
for k, v := range f.current {
|
||||
f.prev[k] = v
|
||||
}
|
||||
maps.Copy(f.prev, f.current)
|
||||
f.current = make(map[string]uint64)
|
||||
}
|
||||
|
||||
@@ -759,7 +759,7 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
|
||||
c.serverPorts = make([]serverPortListener, len(conf.configs.Languages))
|
||||
}
|
||||
currentServerPort := c.serverPort
|
||||
for i := 0; i < len(c.serverPorts); i++ {
|
||||
for i := range c.serverPorts {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
|
||||
@@ -880,7 +880,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()
|
||||
@@ -898,16 +898,16 @@ func (c *serverCommand) serve() error {
|
||||
// To allow the en user to change the error template while the server is running, we use
|
||||
// the freshest template we can provide.
|
||||
var (
|
||||
errTempl tpl.Template
|
||||
templHandler tpl.TemplateHandler
|
||||
errTempl *tplimpl.TemplInfo
|
||||
templHandler *tplimpl.TemplateStore
|
||||
)
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
|
||||
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
|
||||
if h == nil {
|
||||
return errTempl, templHandler
|
||||
}
|
||||
templHandler := h.Tmpl()
|
||||
errTempl, found := templHandler.Lookup("_server/error.html")
|
||||
if !found {
|
||||
templHandler := h.GetTemplateStore()
|
||||
errTempl := templHandler.LookupByPath("/_server/error.html")
|
||||
if errTempl == nil {
|
||||
panic("template server/error.html not found")
|
||||
}
|
||||
return errTempl, templHandler
|
||||
@@ -1035,7 +1035,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)
|
||||
})
|
||||
@@ -1167,7 +1166,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)
|
||||
@@ -117,7 +119,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
tos = append(tos, nil)
|
||||
continue
|
||||
}
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
for i := range slice.Len() {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
@@ -128,7 +130,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
|
||||
var tos []any
|
||||
|
||||
for i := 0; i < tov.Len(); i++ {
|
||||
for i := range tov.Len() {
|
||||
tos = append(tos, tov.Index(i).Interface())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ func TestAppend(t *testing.T) {
|
||||
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
|
||||
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
|
||||
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
|
||||
{[]string{"a", "b"}, []any{}, []string{"a", "b"}},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
|
||||
@@ -136,3 +136,37 @@ func TestSortedStringSlice(t *testing.T) {
|
||||
c.Assert(s.Count("z"), qt.Equals, 0)
|
||||
c.Assert(s.Count("a"), qt.Equals, 1)
|
||||
}
|
||||
|
||||
func TestStringSliceToInterfaceSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
want []any
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
in: []string{},
|
||||
want: []any{},
|
||||
},
|
||||
{
|
||||
name: "single element",
|
||||
in: []string{"hello"},
|
||||
want: []any{"hello"},
|
||||
},
|
||||
{
|
||||
name: "multiple elements",
|
||||
in: []string{"a", "b", "c"},
|
||||
want: []any{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := StringSliceToInterfaceSlice(tt.in)
|
||||
c.Assert(got, qt.DeepEquals, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,13 @@
|
||||
|
||||
package collections
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hiter"
|
||||
)
|
||||
|
||||
// Stack is a simple LIFO stack that is safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
@@ -58,6 +64,11 @@ func (s *Stack[T]) Len() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
// All returns all items in the stack, from bottom to top.
|
||||
func (s *Stack[T]) All() iter.Seq2[int, T] {
|
||||
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -73,7 +84,7 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
for i := len(s.items) - 1; i >= 0; i-- {
|
||||
if predicate(s.items[i]) {
|
||||
items = append(items, s.items[i])
|
||||
s.items = append(s.items[:i], s.items[i+1:]...)
|
||||
s.items = slices.Delete(s.items, i, i+1)
|
||||
}
|
||||
}
|
||||
return items
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNewStack(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
|
||||
c.Assert(s, qt.IsNotNil)
|
||||
}
|
||||
|
||||
func TestStackBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
|
||||
c.Assert(s.Len(), qt.Equals, 0)
|
||||
|
||||
s.Push(1)
|
||||
s.Push(2)
|
||||
s.Push(3)
|
||||
|
||||
c.Assert(s.Len(), qt.Equals, 3)
|
||||
|
||||
top, ok := s.Peek()
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
c.Assert(top, qt.Equals, 3)
|
||||
|
||||
popped, ok := s.Pop()
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
c.Assert(popped, qt.Equals, 3)
|
||||
|
||||
c.Assert(s.Len(), qt.Equals, 2)
|
||||
|
||||
_, _ = s.Pop()
|
||||
_, _ = s.Pop()
|
||||
_, ok = s.Pop()
|
||||
|
||||
c.Assert(ok, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestStackDrain(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[string]()
|
||||
s.Push("a")
|
||||
s.Push("b")
|
||||
|
||||
got := s.Drain()
|
||||
|
||||
c.Assert(got, qt.DeepEquals, []string{"a", "b"})
|
||||
c.Assert(s.Len(), qt.Equals, 0)
|
||||
}
|
||||
|
||||
func TestStackDrainMatching(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s.Push(1)
|
||||
s.Push(2)
|
||||
s.Push(3)
|
||||
s.Push(4)
|
||||
|
||||
got := s.DrainMatching(func(v int) bool { return v%2 == 0 })
|
||||
|
||||
c.Assert(got, qt.DeepEquals, []int{4, 2})
|
||||
c.Assert(s.Drain(), qt.DeepEquals, []int{1, 3})
|
||||
}
|
||||
@@ -23,6 +23,8 @@ const (
|
||||
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
|
||||
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
|
||||
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
|
||||
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
|
||||
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
|
||||
)
|
||||
|
||||
// Field/method names with special meaning.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -37,12 +37,11 @@ func TestXxHashFromReaderPara(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
i := i
|
||||
for i := range 10 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
for j := range 100 {
|
||||
s := strings.Repeat("Hello ", i+j+1*42)
|
||||
r := strings.NewReader(s)
|
||||
got, size, err := XXHashFromReader(r)
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -144,14 +143,12 @@ func BenchmarkHashString(b *testing.B) {
|
||||
}
|
||||
|
||||
func BenchmarkHashMap(b *testing.B) {
|
||||
m := map[string]interface{}{}
|
||||
for i := 0; i < 1000; i++ {
|
||||
m := map[string]any{}
|
||||
for i := range 1000 {
|
||||
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!")
|
||||
}
|
||||
}
|
||||
@@ -152,10 +152,7 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
|
||||
}
|
||||
|
||||
if ectx.Position.LineNumber > 0 {
|
||||
low := ectx.Position.LineNumber - 3
|
||||
if low < 0 {
|
||||
low = 0
|
||||
}
|
||||
low := max(ectx.Position.LineNumber-3, 0)
|
||||
|
||||
if ectx.Position.LineNumber > 2 {
|
||||
ectx.LinesPos = 2
|
||||
@@ -163,10 +160,7 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
|
||||
ectx.LinesPos = ectx.Position.LineNumber - 1
|
||||
}
|
||||
|
||||
high := ectx.Position.LineNumber + 2
|
||||
if high > len(lines) {
|
||||
high = len(lines)
|
||||
}
|
||||
high := min(ectx.Position.LineNumber+2, len(lines))
|
||||
|
||||
ectx.Lines = lines[low:high]
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -321,13 +324,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 != "" {
|
||||
|
||||
@@ -19,17 +19,27 @@ import (
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
// YAML parse errors.
|
||||
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
|
||||
|
||||
// Template/shortcode parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
|
||||
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
|
||||
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
|
||||
|
||||
// i18n bundle errors
|
||||
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
|
||||
}
|
||||
|
||||
func commonLineNumberExtractor(e error) (int, int) {
|
||||
for _, handler := range lineNumberExtractors {
|
||||
lno, col := handler(e)
|
||||
if lno > 0 {
|
||||
return lno, col
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
|
||||
@@ -1,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.
|
||||
@@ -11,27 +11,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package doctree
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestDimensionFlag(t *testing.T) {
|
||||
func TestCommonLineNumberExtractor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
var zero DimensionFlag
|
||||
var d DimensionFlag
|
||||
var o DimensionFlag = 1
|
||||
var p DimensionFlag = 12
|
||||
|
||||
c.Assert(d.Has(o), qt.Equals, false)
|
||||
d = d.Set(o)
|
||||
c.Assert(d.Has(o), qt.Equals, true)
|
||||
c.Assert(d.Has(d), qt.Equals, true)
|
||||
c.Assert(func() { zero.Index() }, qt.PanicMatches, "dimension flag not set")
|
||||
c.Assert(DimensionLanguage.Index(), qt.Equals, 0)
|
||||
c.Assert(p.Index(), qt.Equals, 11)
|
||||
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)
|
||||
}
|
||||
+5
-18
@@ -27,7 +27,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -113,18 +112,6 @@ func IsNotFound(err error) bool {
|
||||
return errors.As(err, ¬FoundErr)
|
||||
}
|
||||
|
||||
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
|
||||
// implementation that does not search in current directory before looking in PATH.
|
||||
// See https://github.com/cli/safeexec and the linked issues.
|
||||
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
bin, err := safeexec.LookPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// Exec enforces a security policy for commands run via os/exec.
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
@@ -197,7 +184,7 @@ func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||
tryFuncs := map[binaryLocation]tryFunc{
|
||||
binaryLocationNodeModules: func() func(...any) (Runner, error) {
|
||||
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
|
||||
_, err := safeexec.LookPath(nodeBinFilename)
|
||||
_, err := exec.LookPath(nodeBinFilename)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -215,7 +202,7 @@ func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||
}
|
||||
},
|
||||
binaryLocationPath: func() func(...any) (Runner, error) {
|
||||
if _, err := safeexec.LookPath(name); err != nil {
|
||||
if _, err := exec.LookPath(name); err != nil {
|
||||
return nil
|
||||
}
|
||||
return func(arg2 ...any) (Runner, error) {
|
||||
@@ -346,7 +333,7 @@ func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
|
||||
bin = c.fullyQualifiedName
|
||||
} else {
|
||||
var err error
|
||||
bin, err = safeexec.LookPath(c.name)
|
||||
bin, err = exec.LookPath(c.name)
|
||||
if err != nil {
|
||||
return nil, &NotFoundError{
|
||||
name: c.name,
|
||||
@@ -384,7 +371,7 @@ func InPath(binaryName string) bool {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
_, err := safeexec.LookPath(binaryName)
|
||||
_, err := exec.LookPath(binaryName)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -394,7 +381,7 @@ func LookPath(binaryName string) string {
|
||||
if strings.Contains(binaryName, "/") {
|
||||
panic("binary name should not contain any slash")
|
||||
}
|
||||
s, err := safeexec.LookPath(binaryName)
|
||||
s, err := exec.LookPath(binaryName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
-31
@@ -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.
|
||||
@@ -27,6 +25,11 @@ import (
|
||||
"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,15 +158,31 @@ type methodKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type methods struct {
|
||||
sync.RWMutex
|
||||
cache map[methodKey]int
|
||||
var (
|
||||
methodIndexCache sync.Map
|
||||
methodCache sync.Map
|
||||
)
|
||||
|
||||
// GetMethodByNameForType returns the method with the given name for the given type,
|
||||
// or a zero Method if no such method exists.
|
||||
// It panics if tp is an interface type.
|
||||
// It caches the lookup.
|
||||
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
|
||||
if tp.Kind() == reflect.Interface {
|
||||
// Func field is nil for interface types.
|
||||
panic("not supported for interface types")
|
||||
}
|
||||
k := methodKey{tp, name}
|
||||
v, found := methodCache.Load(k)
|
||||
if found {
|
||||
return v.(reflect.Method)
|
||||
}
|
||||
m, _ := tp.MethodByName(name)
|
||||
methodCache.Store(k, m)
|
||||
return m
|
||||
}
|
||||
|
||||
var methodCache = &methods{cache: make(map[methodKey]int)}
|
||||
|
||||
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
|
||||
// type lookup.
|
||||
// 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)
|
||||
|
||||
@@ -157,22 +197,16 @@ 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}
|
||||
methodCache.RLock()
|
||||
index, found := methodCache.cache[k]
|
||||
methodCache.RUnlock()
|
||||
v, found := methodIndexCache.Load(k)
|
||||
if found {
|
||||
return index
|
||||
return v.(int)
|
||||
}
|
||||
|
||||
methodCache.Lock()
|
||||
defer methodCache.Unlock()
|
||||
|
||||
m, ok := tp.MethodByName(name)
|
||||
index = m.Index
|
||||
index := m.Index
|
||||
if !ok {
|
||||
index = -1
|
||||
}
|
||||
methodCache.cache[k] = index
|
||||
methodIndexCache.Store(k, index)
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
@@ -245,7 +279,7 @@ func ToSliceAny(v any) ([]any, bool) {
|
||||
vvv := reflect.ValueOf(v)
|
||||
if vvv.Kind() == reflect.Slice {
|
||||
out := make([]any, vvv.Len())
|
||||
for i := 0; i < vvv.Len(); i++ {
|
||||
for i := range vvv.Len() {
|
||||
out[i] = vvv.Index(i).Interface()
|
||||
}
|
||||
return out, true
|
||||
@@ -254,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
|
||||
@@ -271,15 +306,51 @@ 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()
|
||||
|
||||
+140
-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,14 +223,38 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetMethodByNamePara(b *testing.B) {
|
||||
v := reflect.ValueOf(&testStruct{})
|
||||
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
for _, method := range methods {
|
||||
_ = GetMethodByName(v, method)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -140,7 +140,7 @@ func TestScratchInParallel(t *testing.T) {
|
||||
for i := 1; i <= 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(j int) {
|
||||
for k := 0; k < 10; k++ {
|
||||
for k := range 10 {
|
||||
newVal := int64(k + j)
|
||||
|
||||
_, err := scratch.Add(key, newVal)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+61
-15
@@ -16,6 +16,8 @@ package hstrings
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -50,12 +52,7 @@ func (s StringEqualFold) Eq(s2 any) bool {
|
||||
|
||||
// EqualAny returns whether a string is equal to any of the given strings.
|
||||
func EqualAny(a string, b ...string) bool {
|
||||
for _, s := range b {
|
||||
if a == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(b, a)
|
||||
}
|
||||
|
||||
// regexpCache represents a cache of regexp objects protected by a mutex.
|
||||
@@ -103,12 +100,7 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
// 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 {
|
||||
for _, v := range arr {
|
||||
if v == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(arr, el)
|
||||
}
|
||||
|
||||
// InSlicEqualFold checks if a string is an element of a slice of strings
|
||||
@@ -137,7 +129,61 @@ func ToString(v any) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
type Tuple struct {
|
||||
First string
|
||||
Second 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestHasBytesWriter(t *testing.T) {
|
||||
return strings.Repeat("ab cfo", r.Intn(33))
|
||||
}
|
||||
|
||||
for i := 0; i < 22; i++ {
|
||||
for range 22 {
|
||||
h, w := neww()
|
||||
fmt.Fprint(w, rndStr()+"abc __foobar"+rndStr())
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, true)
|
||||
|
||||
@@ -32,6 +32,13 @@ type ReadSeekCloser interface {
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
|
||||
type CloserFunc func() error
|
||||
|
||||
func (f CloserFunc) Close() error {
|
||||
return f()
|
||||
}
|
||||
|
||||
// ReadSeekCloserProvider provides a ReadSeekCloser.
|
||||
type ReadSeekCloserProvider interface {
|
||||
ReadSeekCloser() (ReadSeekCloser, error)
|
||||
|
||||
+75
-18
@@ -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
|
||||
}
|
||||
|
||||
@@ -139,9 +141,13 @@ func (i HugoInfo) IsMultilingual() bool {
|
||||
return i.conf.IsMultilingual()
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
type contextKey uint8
|
||||
|
||||
var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope"))
|
||||
const (
|
||||
contextKeyMarkupScope contextKey = iota
|
||||
)
|
||||
|
||||
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
|
||||
|
||||
type Context struct{}
|
||||
|
||||
@@ -192,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,
|
||||
}
|
||||
}
|
||||
@@ -411,20 +417,28 @@ func IsDartSassGeV2() bool {
|
||||
// 2. Their theme to work for at least the last few Hugo versions.
|
||||
func Deprecate(item, alternative string, version string) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
DeprecateLevel(item, alternative, version, level)
|
||||
deprecateLevel(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// See Deprecate for details.
|
||||
func DeprecateWithLogger(item, alternative string, version string, log logg.Logger) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
deprecateLevelWithLogger(item, alternative, version, level, log)
|
||||
}
|
||||
|
||||
// DeprecateLevelMin informs about a deprecation starting at the given version, but with a minimum log level.
|
||||
func DeprecateLevelMin(item, alternative string, version string, minLevel logg.Level) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
if level < minLevel {
|
||||
level = minLevel
|
||||
}
|
||||
DeprecateLevel(item, alternative, version, level)
|
||||
level := max(deprecationLogLevelFromVersion(version), minLevel)
|
||||
deprecateLevel(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// deprecateLevel informs about a deprecation logging at the given level.
|
||||
func deprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
deprecateLevelWithLogger(item, alternative, version, level, loggers.Log().Logger())
|
||||
}
|
||||
|
||||
// DeprecateLevel informs about a deprecation logging at the given level.
|
||||
func DeprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) {
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
|
||||
@@ -432,14 +446,14 @@ func DeprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
|
||||
}
|
||||
|
||||
loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg)
|
||||
log.WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg)
|
||||
}
|
||||
|
||||
// We usually do about one minor version a month.
|
||||
// We want people to run at least the current and previous version without any warnings.
|
||||
// We want people 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 {
|
||||
@@ -453,3 +467,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
|
||||
}
|
||||
|
||||
@@ -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: 145,
|
||||
Minor: 153,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestLogDistinct(t *testing.T) {
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for range 10 {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Warnln("warn 1")
|
||||
@@ -137,7 +137,7 @@ func TestReset(t *testing.T) {
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for range 3 {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Errorln("error 1")
|
||||
|
||||
+45
-3
@@ -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,10 +79,19 @@ 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
|
||||
}
|
||||
|
||||
// Contains returns whether the given key exists in the cache.
|
||||
func (c *Cache[K, T]) Contains(key K) bool {
|
||||
c.RLock()
|
||||
_, found := c.m[key]
|
||||
c.RUnlock()
|
||||
return found
|
||||
}
|
||||
|
||||
// InitAndGet initializes the cache if not already done and returns the value for the given key.
|
||||
// The init state will be reset on Reset or Drain.
|
||||
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
|
||||
@@ -108,7 +131,26 @@ func (c *Cache[K, T]) Set(key K, value T) {
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
|
||||
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
|
||||
c.RLock()
|
||||
if _, found := c.get(key); !found {
|
||||
c.RUnlock()
|
||||
c.Set(key, value)
|
||||
} else {
|
||||
c.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) clearIfNeeded() {
|
||||
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
|
||||
// clear the map
|
||||
clear(c.m)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, T]) set(key K, value T) {
|
||||
c.clearIfNeeded()
|
||||
c.m[key] = value
|
||||
}
|
||||
|
||||
@@ -141,7 +183,7 @@ func (c *Cache[K, T]) Len() int {
|
||||
|
||||
func (c *Cache[K, T]) Reset() {
|
||||
c.Lock()
|
||||
c.m = make(map[K]T)
|
||||
clear(c.m)
|
||||
c.hasBeenInitialized = false
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCacheSize(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})
|
||||
|
||||
for i := range 30 {
|
||||
cache.Set(string(rune('a'+i)), "value")
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
for i := 20; i < 50; i++ {
|
||||
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
|
||||
return "value", nil
|
||||
})
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
for i := 100; i < 200; i++ {
|
||||
cache.SetIfAbsent(string(rune('a'+i)), "value")
|
||||
}
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
|
||||
cache.InitAndGet("foo", func(
|
||||
get func(key string) (string, bool), set func(key string, value string),
|
||||
) error {
|
||||
for i := 50; i < 100; i++ {
|
||||
set(string(rune('a'+i)), "value")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Assert(len(cache.m), qt.Equals, 10)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewMap[K comparable, T any]() *Map[K, T] {
|
||||
return &Map[K, T]{
|
||||
m: make(map[K]T),
|
||||
}
|
||||
}
|
||||
|
||||
// Map is a thread safe map backed by a Go map.
|
||||
type Map[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Get gets the value for the given key.
|
||||
// It returns the zero value of T if the key is not found.
|
||||
func (m *Map[K, T]) Get(key K) T {
|
||||
v, _ := m.Lookup(key)
|
||||
return v
|
||||
}
|
||||
|
||||
// Lookup looks up the given key in the map.
|
||||
// It returns the value and a boolean indicating whether the key was found.
|
||||
func (m *Map[K, T]) Lookup(key K) (T, bool) {
|
||||
m.mu.RLock()
|
||||
v, found := m.m[key]
|
||||
m.mu.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
v, found := m.Lookup(key)
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
v, found = m.m[key]
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
v, err := create()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
m.m[key] = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Set sets the given key to the given value.
|
||||
func (m *Map[K, T]) Set(key K, value T) {
|
||||
m.mu.Lock()
|
||||
m.m[key] = value
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// WithWriteLock executes the given function with a write lock on the map.
|
||||
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
f(m.m)
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
|
||||
// It returns true if the value was set, false otherwise.
|
||||
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
|
||||
m.mu.RLock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.mu.RUnlock()
|
||||
return m.doSetIfAbsent(key, value)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.m[key] = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// All returns an iterator over all key/value pairs in the map.
|
||||
// A read lock is held during the iteration.
|
||||
func (m *Map[K, T]) All() iter.Seq2[K, T] {
|
||||
return func(yield func(K, T) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for k, v := range m.m {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewMap[string, int]()
|
||||
|
||||
m.Set("b", 42)
|
||||
v, found := m.Lookup("b")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v = m.Get("b")
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v, found = m.Lookup("c")
|
||||
c.Assert(found, qt.Equals, false)
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v = m.Get("c")
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v, err := m.GetOrCreate("d", func() (int, error) {
|
||||
return 100, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
v, found = m.Lookup("d")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
v, err = m.GetOrCreate("d", func() (int, error) {
|
||||
return 200, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
wasSet := m.SetIfAbsent("e", 300)
|
||||
c.Assert(wasSet, qt.Equals, true)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
wasSet = m.SetIfAbsent("e", 400)
|
||||
c.Assert(wasSet, qt.Equals, false)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
m.WithWriteLock(func(m map[string]int) {
|
||||
m["f"] = 500
|
||||
})
|
||||
v, found = m.Lookup("f")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 500)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+12
-1
@@ -14,6 +14,8 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hashing"
|
||||
)
|
||||
|
||||
@@ -56,6 +58,15 @@ func (m *Ordered[K, T]) Get(key K) (T, bool) {
|
||||
return value, found
|
||||
}
|
||||
|
||||
// Has returns whether the given key exists in the map.
|
||||
func (m *Ordered[K, T]) Has(key K) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
_, found := m.values[key]
|
||||
return found
|
||||
}
|
||||
|
||||
// Delete deletes the value for the given key.
|
||||
func (m *Ordered[K, T]) Delete(key K) {
|
||||
if m == nil {
|
||||
@@ -64,7 +75,7 @@ func (m *Ordered[K, T]) Delete(key K) {
|
||||
delete(m.values, key)
|
||||
for i, k := range m.keys {
|
||||
if k == key {
|
||||
m.keys = append(m.keys[:i], m.keys[i+1:]...)
|
||||
m.keys = slices.Delete(m.keys, i, i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
)
|
||||
|
||||
type OrderedIntSet struct {
|
||||
keys []int
|
||||
values *bitset.BitSet
|
||||
}
|
||||
|
||||
// NewOrderedIntSet creates a new OrderedIntSet.
|
||||
// Note that this is backed by https://github.com/bits-and-blooms/bitset
|
||||
func NewOrderedIntSet(vals ...int) *OrderedIntSet {
|
||||
m := &OrderedIntSet{
|
||||
keys: make([]int, 0, len(vals)),
|
||||
values: bitset.New(uint(len(vals))),
|
||||
}
|
||||
for _, v := range vals {
|
||||
m.Set(v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Set sets the value for the given key.
|
||||
// Note that insertion order is not affected if a key is re-inserted into the set.
|
||||
func (m *OrderedIntSet) Set(key int) {
|
||||
if m == nil {
|
||||
panic("nil OrderedIntSet")
|
||||
}
|
||||
keyu := uint(key)
|
||||
if m.values.Test(keyu) {
|
||||
return
|
||||
}
|
||||
m.values.Set(keyu)
|
||||
m.keys = append(m.keys, key)
|
||||
}
|
||||
|
||||
// SetFrom sets the values from another OrderedIntSet.
|
||||
func (m *OrderedIntSet) SetFrom(other *OrderedIntSet) {
|
||||
if m == nil || other == nil {
|
||||
return
|
||||
}
|
||||
for _, key := range other.keys {
|
||||
m.Set(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Clone() *OrderedIntSet {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
newSet := &OrderedIntSet{
|
||||
keys: slices.Clone(m.keys),
|
||||
values: m.values.Clone(),
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
// Next returns the next key in the set possibly including the given key.
|
||||
// It returns -1 if the key is not found or if there are no keys greater than the given key.
|
||||
func (m *OrderedIntSet) Next(i int) int {
|
||||
n, ok := m.values.NextSet(uint(i))
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return int(n)
|
||||
}
|
||||
|
||||
// The reason we don't use iter.Seq is https://github.com/golang/go/issues/69015
|
||||
// This is 70% faster than using iter.Seq2[int, int] for the keys.
|
||||
// It returns false if the iteration was stopped early.
|
||||
func (m *OrderedIntSet) ForEachKey(yield func(int) bool) bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
for _, key := range m.keys {
|
||||
if !yield(key) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Has(key int) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
return m.values.Test(uint(key))
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Len() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(m.keys)
|
||||
}
|
||||
|
||||
// KeysSorted returns the keys in sorted order.
|
||||
func (m *OrderedIntSet) KeysSorted() []int {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
keys := slices.Clone(m.keys)
|
||||
slices.Sort(keys)
|
||||
return m.keys
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) String() string {
|
||||
if m == nil {
|
||||
return "[]"
|
||||
}
|
||||
return fmt.Sprintf("%v", m.keys)
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) Values() *bitset.BitSet {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.values
|
||||
}
|
||||
|
||||
func (m *OrderedIntSet) IsSuperSet(other *OrderedIntSet) bool {
|
||||
if m == nil || other == nil {
|
||||
return false
|
||||
}
|
||||
return m.values.IsSuperSet(other.values)
|
||||
}
|
||||
|
||||
// Words returns the bitset as array of 64-bit words, giving direct access to the internal representation.
|
||||
// It is not a copy, so changes to the returned slice will affect the bitset.
|
||||
// It is meant for advanced users.
|
||||
func (m *OrderedIntSet) Words() []uint64 {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m.values.Words()
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestOrderedIntSet(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewOrderedIntSet(2, 1, 3, 7)
|
||||
|
||||
c.Assert(m.Len(), qt.Equals, 4)
|
||||
c.Assert(m.Has(1), qt.Equals, true)
|
||||
c.Assert(m.Has(4), qt.Equals, false)
|
||||
c.Assert(m.String(), qt.Equals, "[2 1 3 7]")
|
||||
m.Set(4)
|
||||
c.Assert(m.Len(), qt.Equals, 5)
|
||||
c.Assert(m.Has(4), qt.Equals, true)
|
||||
c.Assert(m.Next(0), qt.Equals, 1)
|
||||
c.Assert(m.Next(1), qt.Equals, 1)
|
||||
c.Assert(m.Next(2), qt.Equals, 2)
|
||||
c.Assert(m.Next(3), qt.Equals, 3)
|
||||
c.Assert(m.Next(4), qt.Equals, 4)
|
||||
c.Assert(m.Next(7), qt.Equals, 7)
|
||||
c.Assert(m.Next(8), qt.Equals, -1)
|
||||
c.Assert(m.String(), qt.Equals, "[2 1 3 7 4]")
|
||||
|
||||
var nilset *OrderedIntSet
|
||||
c.Assert(nilset.Len(), qt.Equals, 0)
|
||||
c.Assert(nilset.Has(1), qt.Equals, false)
|
||||
c.Assert(nilset.String(), qt.Equals, "[]")
|
||||
|
||||
var collected []int
|
||||
m.ForEachKey(func(key int) bool {
|
||||
collected = append(collected, key)
|
||||
return true
|
||||
})
|
||||
c.Assert(collected, qt.DeepEquals, []int{2, 1, 3, 7, 4})
|
||||
}
|
||||
|
||||
func BenchmarkOrderedIntSet(b *testing.B) {
|
||||
smallSet := NewOrderedIntSet()
|
||||
for i := range 8 {
|
||||
smallSet.Set(i)
|
||||
}
|
||||
mediumSet := NewOrderedIntSet()
|
||||
for i := range 64 {
|
||||
mediumSet.Set(i)
|
||||
}
|
||||
largeSet := NewOrderedIntSet()
|
||||
for i := range 1024 {
|
||||
largeSet.Set(i)
|
||||
}
|
||||
|
||||
b.Run("New", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
NewOrderedIntSet(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Has small", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
smallSet.Has(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Has medium", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
mediumSet.Has(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Next", func(b *testing.B) {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
mediumSet.Next(i % 32)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey small", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
smallSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey medium", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
mediumSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEachKey large", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
largeSet.ForEachKey(func(key int) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
+93
-4
@@ -14,6 +14,7 @@
|
||||
package maps
|
||||
|
||||
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)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestPara(t *testing.T) {
|
||||
c.Run("Order", func(c *qt.C) {
|
||||
n := 500
|
||||
ints := make([]int, n)
|
||||
for i := 0; i < n; i++ {
|
||||
for i := range n {
|
||||
ints[i] = i
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ func TestPara(t *testing.T) {
|
||||
|
||||
var result []int
|
||||
var mu sync.Mutex
|
||||
for i := 0; i < n; i++ {
|
||||
i := i
|
||||
for i := range n {
|
||||
r.Run(func() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -78,7 +77,7 @@ func TestPara(t *testing.T) {
|
||||
|
||||
var counter int64
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
for range n {
|
||||
r.Run(func() error {
|
||||
atomic.AddInt64(&counter, 1)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
@@ -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,39 @@ 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+390
-101
@@ -14,18 +14,27 @@
|
||||
package paths
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"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"
|
||||
)
|
||||
|
||||
// PathParser parses a path into a Path.
|
||||
const (
|
||||
identifierBaseof = "baseof"
|
||||
identifierCurstomWrapper = "_"
|
||||
)
|
||||
|
||||
// PathParser parses and manages paths.
|
||||
type PathParser struct {
|
||||
// Maps the language code to its index in the languages/sites slice.
|
||||
LanguageIndex map[string]int
|
||||
@@ -33,8 +42,25 @@ type PathParser struct {
|
||||
// Reports whether the given language is disabled.
|
||||
IsLangDisabled func(string) bool
|
||||
|
||||
// IsOutputFormat reports whether the given name is a valid output format.
|
||||
// The second argument is optional.
|
||||
IsOutputFormat func(name, ext string) bool
|
||||
|
||||
// Reports whether the given ext is a content file.
|
||||
IsContentExt func(string) bool
|
||||
|
||||
// The configured sites matrix.
|
||||
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
|
||||
|
||||
// Below gets created on demand.
|
||||
initOnce sync.Once
|
||||
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
|
||||
}
|
||||
|
||||
func (pp *PathParser) init() {
|
||||
pp.initOnce.Do(func() {
|
||||
pp.sitesMatrixCache = maps.NewCache[string, sitesmatrix.VectorStore]()
|
||||
})
|
||||
}
|
||||
|
||||
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
|
||||
@@ -48,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)
|
||||
@@ -83,13 +135,10 @@ func (pp *PathParser) Parse(c, s string) *Path {
|
||||
}
|
||||
|
||||
func (pp *PathParser) newPath(component string) *Path {
|
||||
return &Path{
|
||||
component: component,
|
||||
posContainerLow: -1,
|
||||
posContainerHigh: -1,
|
||||
posSectionHigh: -1,
|
||||
posIdentifierLanguage: -1,
|
||||
}
|
||||
p := &Path{}
|
||||
p.reset()
|
||||
p.component = component
|
||||
return p
|
||||
}
|
||||
|
||||
func (pp *PathParser) parse(component, s string) (*Path, error) {
|
||||
@@ -114,10 +163,114 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
hasLang := pp.LanguageIndex != nil
|
||||
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
|
||||
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) {
|
||||
if p.posContainerHigh != -1 {
|
||||
return
|
||||
}
|
||||
mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil
|
||||
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
|
||||
mayHaveOutputFormat := component == files.ComponentFolderLayouts
|
||||
mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat
|
||||
var mayHaveLayout bool
|
||||
if p.pathType == TypeShortcode {
|
||||
mayHaveLayout = !isLast && component == files.ComponentFolderLayouts
|
||||
} else {
|
||||
mayHaveLayout = component == files.ComponentFolderLayouts
|
||||
}
|
||||
|
||||
var found bool
|
||||
var high int
|
||||
if len(p.identifiersKnown) > 0 {
|
||||
high = lastDot
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
sid := p.s[id.Low:id.High]
|
||||
|
||||
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierCustom = len(p.identifiersKnown) - 1
|
||||
found = true
|
||||
}
|
||||
|
||||
if len(p.identifiersKnown) == 0 {
|
||||
// The first is always the extension.
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
found = true
|
||||
|
||||
// May also be the output format.
|
||||
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
|
||||
p.posIdentifierOutputFormat = 0
|
||||
}
|
||||
} else {
|
||||
var langFound bool
|
||||
|
||||
if mayHaveLang {
|
||||
var disabled bool
|
||||
_, langFound = pp.LanguageIndex[sid]
|
||||
|
||||
if !langFound {
|
||||
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
|
||||
if disabled {
|
||||
p.disabled = true
|
||||
langFound = true
|
||||
}
|
||||
}
|
||||
found = langFound
|
||||
if langFound {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !found && mayHaveOutputFormat {
|
||||
// At this point we may already have resolved an output format,
|
||||
// but we need to keep looking for a more specific one, e.g. amp before html.
|
||||
// Use both name and extension to prevent
|
||||
// false positives on the form css.html.
|
||||
if pp.IsOutputFormat(sid, p.Ext()) {
|
||||
found = true
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if !found && mayHaveKind {
|
||||
if kinds.GetKindMain(sid) != "" {
|
||||
found = true
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierKind = len(p.identifiersKnown) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if !found && sid == identifierBaseof {
|
||||
found = true
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierBaseof = len(p.identifiersKnown) - 1
|
||||
}
|
||||
|
||||
if !found && mayHaveLayout {
|
||||
if p.posIdentifierLayout != -1 {
|
||||
// Move it to identifiersUnknown.
|
||||
p.identifiersUnknown = append(p.identifiersUnknown, p.identifiersKnown[p.posIdentifierLayout])
|
||||
p.identifiersKnown[p.posIdentifierLayout] = id
|
||||
} else {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierLayout = len(p.identifiersKnown) - 1
|
||||
}
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
p.identifiersUnknown = append(p.identifiersUnknown, id)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
s = path.Clean(filepath.ToSlash(s))
|
||||
if s == "." {
|
||||
@@ -140,46 +293,26 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
|
||||
p.s = s
|
||||
slashCount := 0
|
||||
lastDot := 0
|
||||
lastSlashIdx := strings.LastIndex(s, "/")
|
||||
numDots := strings.Count(s[lastSlashIdx+1:], ".")
|
||||
if strings.Contains(s, "/_shortcodes/") {
|
||||
p.pathType = TypeShortcode
|
||||
}
|
||||
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
|
||||
switch c {
|
||||
case '.':
|
||||
if p.posContainerHigh == -1 {
|
||||
var high int
|
||||
if len(p.identifiers) > 0 {
|
||||
high = p.identifiers[len(p.identifiers)-1].Low - 1
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
if len(p.identifiers) == 0 {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
} else if len(p.identifiers) == 1 {
|
||||
// Check for a valid language.
|
||||
s := p.s[id.Low:id.High]
|
||||
|
||||
if hasLang {
|
||||
var disabled bool
|
||||
_, langFound := pp.LanguageIndex[s]
|
||||
if !langFound {
|
||||
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
|
||||
if disabled {
|
||||
p.disabled = true
|
||||
langFound = true
|
||||
}
|
||||
}
|
||||
if langFound {
|
||||
p.posIdentifierLanguage = 1
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false)
|
||||
lastDot = i
|
||||
case '/':
|
||||
slashCount++
|
||||
if p.posContainerHigh == -1 {
|
||||
if lastDot > 0 {
|
||||
pp.parseIdentifier(component, s, p, i, lastDot, numDots, true)
|
||||
}
|
||||
p.posContainerHigh = i + 1
|
||||
} else if p.posContainerLow == -1 {
|
||||
p.posContainerLow = i + 1
|
||||
@@ -190,26 +323,52 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.identifiers) > 0 {
|
||||
if len(p.identifiersKnown) > 0 {
|
||||
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
|
||||
isContent := isContentComponent && pp.IsContentExt(p.Ext())
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
b := p.s[p.posContainerHigh : id.Low-1]
|
||||
if isContent {
|
||||
switch b {
|
||||
case "index":
|
||||
p.bundleType = PathTypeLeaf
|
||||
case "_index":
|
||||
p.bundleType = PathTypeBranch
|
||||
default:
|
||||
p.bundleType = PathTypeContentSingle
|
||||
}
|
||||
id := p.identifiersKnown[len(p.identifiersKnown)-1]
|
||||
|
||||
if slashCount == 2 && p.IsLeafBundle() {
|
||||
p.posSectionHigh = 0
|
||||
if id.Low > p.posContainerHigh {
|
||||
b := p.s[p.posContainerHigh : id.Low-1]
|
||||
if isContent {
|
||||
switch b {
|
||||
case "index":
|
||||
p.pathType = TypeLeaf
|
||||
case "_index":
|
||||
p.pathType = TypeBranch
|
||||
default:
|
||||
p.pathType = TypeContentSingle
|
||||
}
|
||||
|
||||
if slashCount == 2 && p.IsLeafBundle() {
|
||||
p.posSectionHigh = 0
|
||||
}
|
||||
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
|
||||
p.pathType = TypeContentData
|
||||
}
|
||||
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
|
||||
p.bundleType = PathTypeContentData
|
||||
}
|
||||
}
|
||||
|
||||
if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts {
|
||||
if p.posIdentifierBaseof != -1 {
|
||||
p.pathType = TypeBaseof
|
||||
} else {
|
||||
pth := p.Path()
|
||||
if strings.Contains(pth, "/_shortcodes/") {
|
||||
p.pathType = TypeShortcode
|
||||
} else if strings.Contains(pth, "/_markup/") {
|
||||
p.pathType = TypeMarkup
|
||||
} else if strings.HasPrefix(pth, "/_partials/") {
|
||||
p.pathType = TypePartial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 {
|
||||
id := p.identifiersKnown[p.posIdentifierLayout]
|
||||
if id.Low == p.posContainerHigh {
|
||||
// First identifier is shortcode name.
|
||||
p.posIdentifierLayout = -1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,35 +377,44 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
|
||||
func ModifyPathBundleTypeResource(p *Path) {
|
||||
if p.IsContent() {
|
||||
p.bundleType = PathTypeContentResource
|
||||
p.pathType = TypeContentResource
|
||||
} else {
|
||||
p.bundleType = PathTypeFile
|
||||
p.pathType = TypeFile
|
||||
}
|
||||
}
|
||||
|
||||
type PathType int
|
||||
//go:generate stringer -type Type
|
||||
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// A generic resource, e.g. a JSON file.
|
||||
PathTypeFile PathType = iota
|
||||
|
||||
// A generic file, e.g. a JSON file.
|
||||
TypeFile Type = iota
|
||||
|
||||
// All below are content files.
|
||||
// A resource of a content type with front matter.
|
||||
PathTypeContentResource
|
||||
TypeContentResource
|
||||
|
||||
// E.g. /blog/my-post.md
|
||||
PathTypeContentSingle
|
||||
TypeContentSingle
|
||||
|
||||
// All below are bundled content files.
|
||||
|
||||
// Leaf bundles, e.g. /blog/my-post/index.md
|
||||
PathTypeLeaf
|
||||
TypeLeaf
|
||||
|
||||
// Branch bundles, e.g. /blog/_index.md
|
||||
PathTypeBranch
|
||||
TypeBranch
|
||||
|
||||
// Content data file, _content.gotmpl.
|
||||
PathTypeContentData
|
||||
TypeContentData
|
||||
|
||||
// Layout types.
|
||||
TypeMarkup
|
||||
TypeShortcode
|
||||
TypePartial
|
||||
TypeBaseof
|
||||
)
|
||||
|
||||
type Path struct {
|
||||
@@ -257,13 +425,19 @@ type Path struct {
|
||||
posContainerHigh int
|
||||
posSectionHigh int
|
||||
|
||||
component string
|
||||
bundleType PathType
|
||||
component string
|
||||
pathType Type
|
||||
|
||||
identifiers []types.LowHigh[string]
|
||||
identifiersKnown []types.LowHigh[string]
|
||||
identifiersUnknown []types.LowHigh[string]
|
||||
|
||||
posIdentifierLanguage int
|
||||
disabled bool
|
||||
posIdentifierLanguage int
|
||||
posIdentifierOutputFormat int
|
||||
posIdentifierKind int
|
||||
posIdentifierLayout int
|
||||
posIdentifierBaseof int
|
||||
posIdentifierCustom int
|
||||
disabled bool
|
||||
|
||||
trimLeadingSlash bool
|
||||
|
||||
@@ -293,9 +467,14 @@ func (p *Path) reset() {
|
||||
p.posContainerHigh = -1
|
||||
p.posSectionHigh = -1
|
||||
p.component = ""
|
||||
p.bundleType = 0
|
||||
p.identifiers = p.identifiers[:0]
|
||||
p.pathType = 0
|
||||
p.identifiersKnown = p.identifiersKnown[:0]
|
||||
p.posIdentifierLanguage = -1
|
||||
p.posIdentifierOutputFormat = -1
|
||||
p.posIdentifierKind = -1
|
||||
p.posIdentifierLayout = -1
|
||||
p.posIdentifierBaseof = -1
|
||||
p.posIdentifierCustom = -1
|
||||
p.disabled = false
|
||||
p.trimLeadingSlash = false
|
||||
p.unnormalized = nil
|
||||
@@ -316,6 +495,9 @@ func (p *Path) norm(s string) string {
|
||||
|
||||
// IdentifierBase satisfies identity.Identity.
|
||||
func (p *Path) IdentifierBase() string {
|
||||
if p.Component() == files.ComponentFolderLayouts {
|
||||
return p.Path()
|
||||
}
|
||||
return p.Base()
|
||||
}
|
||||
|
||||
@@ -332,6 +514,13 @@ func (p *Path) Container() string {
|
||||
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
|
||||
}
|
||||
|
||||
func (p *Path) String() string {
|
||||
if p == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return p.Path()
|
||||
}
|
||||
|
||||
// ContainerDir returns the container directory for this path.
|
||||
// For content bundles this will be the parent directory.
|
||||
func (p *Path) ContainerDir() string {
|
||||
@@ -352,13 +541,13 @@ func (p *Path) Section() string {
|
||||
// IsContent returns true if the path is a content file (e.g. mypost.md).
|
||||
// Note that this will also return true for content files in a bundle.
|
||||
func (p *Path) IsContent() bool {
|
||||
return p.BundleType() >= PathTypeContentResource
|
||||
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
|
||||
}
|
||||
|
||||
// isContentPage returns true if the path is a content file (e.g. mypost.md),
|
||||
// but nof if inside a leaf bundle.
|
||||
func (p *Path) isContentPage() bool {
|
||||
return p.BundleType() >= PathTypeContentSingle
|
||||
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
|
||||
}
|
||||
|
||||
// Name returns the last element of path.
|
||||
@@ -372,7 +561,7 @@ func (p *Path) Name() string {
|
||||
// Name returns the last element of path without any extension.
|
||||
func (p *Path) NameNoExt() string {
|
||||
if i := p.identifierIndex(0); i != -1 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[i].Low-1]
|
||||
return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1]
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
@@ -384,7 +573,7 @@ func (p *Path) NameNoLang() string {
|
||||
return p.Name()
|
||||
}
|
||||
|
||||
return p.s[p.posContainerHigh:p.identifiers[i].Low-1] + p.s[p.identifiers[i].High:]
|
||||
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:]
|
||||
}
|
||||
|
||||
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
|
||||
@@ -398,10 +587,26 @@ func (p *Path) BaseNameNoIdentifier() string {
|
||||
|
||||
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
|
||||
func (p *Path) NameNoIdentifier() string {
|
||||
if len(p.identifiers) > 0 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
|
||||
lowHigh := p.nameLowHigh()
|
||||
return p.s[lowHigh.Low:lowHigh.High]
|
||||
}
|
||||
|
||||
func (p *Path) nameLowHigh() types.LowHigh[string] {
|
||||
if len(p.identifiersKnown) > 0 {
|
||||
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
|
||||
if p.posContainerHigh == lastID.Low {
|
||||
// The last identifier is the name.
|
||||
return lastID
|
||||
}
|
||||
return types.LowHigh[string]{
|
||||
Low: p.posContainerHigh,
|
||||
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
|
||||
}
|
||||
}
|
||||
return types.LowHigh[string]{
|
||||
Low: p.posContainerHigh,
|
||||
High: len(p.s),
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Dir returns all but the last element of path, typically the path's directory.
|
||||
@@ -421,6 +626,11 @@ func (p *Path) Path() (d string) {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
// PathNoLeadingSlash returns the full path without the leading slash.
|
||||
func (p *Path) PathNoLeadingSlash() string {
|
||||
return p.Path()[1:]
|
||||
}
|
||||
|
||||
// Unnormalized returns the Path with the original case preserved.
|
||||
func (p *Path) Unnormalized() *Path {
|
||||
return p.unnormalized
|
||||
@@ -428,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)
|
||||
}
|
||||
|
||||
@@ -436,6 +649,28 @@ func (p *Path) PathNoIdentifier() string {
|
||||
return p.base(false, false)
|
||||
}
|
||||
|
||||
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
|
||||
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
|
||||
if len(p.identifiersKnown) == 0 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
i := p.identifierIndex(0)
|
||||
|
||||
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
|
||||
i = j
|
||||
}
|
||||
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
|
||||
i = j
|
||||
}
|
||||
|
||||
if i == -1 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
id := p.identifiersKnown[i]
|
||||
return p.norm(p.s[:id.Low-1])
|
||||
}
|
||||
|
||||
// PathRel returns the path relative to the given owner.
|
||||
func (p *Path) PathRel(owner *Path) string {
|
||||
ob := owner.Base()
|
||||
@@ -459,7 +694,27 @@ 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.
|
||||
// For pages with Type set, we treat that as the section.
|
||||
func (p *Path) BaseReTyped(typ string) (d string) {
|
||||
base := p.Base()
|
||||
if typ == "" || p.Section() == typ {
|
||||
return base
|
||||
}
|
||||
d = "/" + typ
|
||||
if p.posSectionHigh != -1 {
|
||||
d += base[p.posSectionHigh:]
|
||||
}
|
||||
d = p.norm(d)
|
||||
return
|
||||
}
|
||||
|
||||
// BaseNoLeadingSlash returns the base path without the leading slash.
|
||||
@@ -468,20 +723,21 @@ func (p *Path) BaseNoLeadingSlash() string {
|
||||
}
|
||||
|
||||
func (p *Path) base(preserveExt, isBundle bool) string {
|
||||
if len(p.identifiers) == 0 {
|
||||
if len(p.identifiersKnown) == 0 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
if preserveExt && len(p.identifiers) == 1 {
|
||||
if preserveExt && len(p.identifiersKnown) == 1 {
|
||||
// Preserve extension.
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
high := id.Low - 1
|
||||
var high int
|
||||
|
||||
if isBundle {
|
||||
high = p.posContainerHigh - 1
|
||||
} else {
|
||||
high = p.nameLowHigh().High
|
||||
}
|
||||
|
||||
if high == 0 {
|
||||
@@ -493,7 +749,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
|
||||
}
|
||||
|
||||
// For txt files etc. we want to preserve the extension.
|
||||
id = p.identifiers[0]
|
||||
id := p.identifiersKnown[0]
|
||||
|
||||
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
|
||||
}
|
||||
@@ -502,8 +758,24 @@ func (p *Path) Ext() string {
|
||||
return p.identifierAsString(0)
|
||||
}
|
||||
|
||||
func (p *Path) OutputFormat() string {
|
||||
return p.identifierAsString(p.posIdentifierOutputFormat)
|
||||
}
|
||||
|
||||
func (p *Path) Kind() string {
|
||||
return p.identifierAsString(p.posIdentifierKind)
|
||||
}
|
||||
|
||||
func (p *Path) Layout() string {
|
||||
return p.identifierAsString(p.posIdentifierLayout)
|
||||
}
|
||||
|
||||
func (p *Path) Lang() string {
|
||||
return p.identifierAsString(1)
|
||||
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 {
|
||||
@@ -515,35 +787,43 @@ func (p *Path) Disabled() bool {
|
||||
}
|
||||
|
||||
func (p *Path) Identifiers() []string {
|
||||
ids := make([]string, len(p.identifiers))
|
||||
for i, id := range p.identifiers {
|
||||
ids := make([]string, len(p.identifiersKnown))
|
||||
for i, id := range p.identifiersKnown {
|
||||
ids[i] = p.s[id.Low:id.High]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) BundleType() PathType {
|
||||
return p.bundleType
|
||||
func (p *Path) IdentifiersUnknown() []string {
|
||||
ids := make([]string, len(p.identifiersUnknown))
|
||||
for i, id := range p.identifiersUnknown {
|
||||
ids[i] = p.s[id.Low:id.High]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) Type() Type {
|
||||
return p.pathType
|
||||
}
|
||||
|
||||
func (p *Path) IsBundle() bool {
|
||||
return p.bundleType >= PathTypeLeaf
|
||||
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
|
||||
}
|
||||
|
||||
func (p *Path) IsBranchBundle() bool {
|
||||
return p.bundleType == PathTypeBranch
|
||||
return p.pathType == TypeBranch
|
||||
}
|
||||
|
||||
func (p *Path) IsLeafBundle() bool {
|
||||
return p.bundleType == PathTypeLeaf
|
||||
return p.pathType == TypeLeaf
|
||||
}
|
||||
|
||||
func (p *Path) IsContentData() bool {
|
||||
return p.bundleType == PathTypeContentData
|
||||
return p.pathType == TypeContentData
|
||||
}
|
||||
|
||||
func (p Path) ForBundleType(t PathType) *Path {
|
||||
p.bundleType = t
|
||||
func (p Path) ForType(t Type) *Path {
|
||||
p.pathType = t
|
||||
return &p
|
||||
}
|
||||
|
||||
@@ -553,12 +833,12 @@ func (p *Path) identifierAsString(i int) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
id := p.identifiers[i]
|
||||
id := p.identifiersKnown[i]
|
||||
return p.s[id.Low:id.High]
|
||||
}
|
||||
|
||||
func (p *Path) identifierIndex(i int) int {
|
||||
if i < 0 || i >= len(p.identifiers) {
|
||||
if i < 0 || i >= len(p.identifiersKnown) {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
@@ -576,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")
|
||||
}
|
||||
|
||||
+302
-16
@@ -18,18 +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,
|
||||
},
|
||||
IsContentExt: func(ext string) bool {
|
||||
return ext == "md"
|
||||
},
|
||||
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) {
|
||||
@@ -105,17 +120,19 @@ func TestParse(t *testing.T) {
|
||||
"Basic Markdown file",
|
||||
"/a/b/c.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Type(), qt.Equals, TypeContentSingle)
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.Name(), qt.Equals, "c.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/c")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -130,7 +147,7 @@ func TestParse(t *testing.T) {
|
||||
|
||||
// Reclassify it as a content resource.
|
||||
ModifyPathBundleTypeResource(p)
|
||||
c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
|
||||
c.Assert(p.Type(), qt.Equals, TypeContentResource)
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.Name(), qt.Equals, "b.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.md")
|
||||
@@ -163,8 +180,10 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
|
||||
@@ -174,7 +193,11 @@ func TestParse(t *testing.T) {
|
||||
"Home branch cundle",
|
||||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
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.BaseReTyped("foo"), qt.Equals, "/foo")
|
||||
c.Assert(p.Path(), qt.Equals, "/_index.md")
|
||||
c.Assert(p.Container(), qt.Equals, "")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/")
|
||||
@@ -185,12 +208,14 @@ func TestParse(t *testing.T) {
|
||||
"/a/index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"index"})
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
@@ -208,6 +233,7 @@ func TestParse(t *testing.T) {
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
@@ -220,6 +246,7 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "index.no")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
},
|
||||
@@ -245,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")
|
||||
},
|
||||
@@ -254,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")
|
||||
},
|
||||
@@ -352,10 +379,249 @@ 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) {
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
|
||||
if test.name != "Caret up identifier" {
|
||||
// return
|
||||
}
|
||||
test.assert(c, parser.Parse(files.ComponentFolderContent, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLayouts(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
assert func(c *qt.C, p *Path)
|
||||
}{
|
||||
{
|
||||
"Basic",
|
||||
"/list.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Lang",
|
||||
"/list.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Kind",
|
||||
"/section.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
|
||||
c.Assert(p.Base(), qt.Equals, "/section.html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Layout",
|
||||
"/list.section.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Layout(), qt.Equals, "list")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Layout multiple",
|
||||
"/mylayout.list.section.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Layout(), qt.Equals, "mylayout")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "mylayout"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
|
||||
c.Assert(p.Base(), qt.Equals, "/mylayout.html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Layout shortcode",
|
||||
"/_shortcodes/myshort.list.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Layout(), qt.Equals, "list")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Layout baseof",
|
||||
"/baseof.list.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Layout(), qt.Equals, "list")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Lang and output format",
|
||||
"/list.no.amp.not.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list", "amp", "no"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"not"})
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Base(), qt.Equals, "/list.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Term",
|
||||
"/term.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/term.html")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
|
||||
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Kind(), qt.Equals, "term")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode with layout",
|
||||
"/_shortcodes/myshortcode.list.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/_shortcodes/myshortcode.html")
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list"})
|
||||
c.Assert(p.Layout(), qt.Equals, "list")
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/_shortcodes/myshortcode")
|
||||
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/_shortcodes/myshortcode.list")
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Kind(), qt.Equals, "")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Sub dir",
|
||||
"/pages/home.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Kind(), qt.Equals, "home")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
c.Assert(p.Dir(), qt.Equals, "/pages")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Baseof",
|
||||
"/pages/baseof.list.section.fr.amp.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
|
||||
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
|
||||
c.Assert(p.Lang(), qt.Equals, "fr")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Dir(), qt.Equals, "/pages")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
|
||||
c.Assert(p.Type(), qt.Equals, TypeBaseof)
|
||||
c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Markup",
|
||||
"/_markup/render-link.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeMarkup)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Markup nested",
|
||||
"/foo/_markup/render-link.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeMarkup)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode",
|
||||
"/_shortcodes/myshortcode.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode nested",
|
||||
"/foo/_shortcodes/myshortcode.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode nested sub",
|
||||
"/foo/_shortcodes/foo/myshortcode.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Partials",
|
||||
"/_partials/foo.bar",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypePartial)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode lang in root",
|
||||
"/_shortcodes/no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "no")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Shortcode lang layout",
|
||||
"/_shortcodes/myshortcode.no.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Type(), qt.Equals, TypeShortcode)
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Layout(), qt.Equals, "")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Not lang",
|
||||
"/foo/index.xy.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Layout(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.xy.html")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/foo/index.xy.html")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "index"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parser := newTestParser()
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
if test.name != "Not lang" {
|
||||
return
|
||||
}
|
||||
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -370,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,3 +78,26 @@ disablePathToLower = true
|
||||
b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
|
||||
}
|
||||
|
||||
func TestIssue13596(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
|
||||
-- content/p1/index.md --
|
||||
---
|
||||
title: p1
|
||||
---
|
||||
-- content/p1/a.1.txt --
|
||||
-- content/p1/a.2.txt --
|
||||
-- layouts/all.html --
|
||||
{{ range .Resources.Match "*" }}{{ .Name }}|{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "a.1.txt|a.2.txt|")
|
||||
b.AssertFileExists("public/p1/a.1.txt", true)
|
||||
b.AssertFileExists("public/p1/a.2.txt", true) // fails
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
|
||||
|
||||
package paths
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[PathTypeFile-0]
|
||||
_ = x[PathTypeContentResource-1]
|
||||
_ = x[PathTypeContentSingle-2]
|
||||
_ = x[PathTypeLeaf-3]
|
||||
_ = x[PathTypeBranch-4]
|
||||
}
|
||||
|
||||
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
|
||||
|
||||
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
|
||||
|
||||
func (i PathType) String() string {
|
||||
if i < 0 || i >= PathType(len(_PathType_index)-1) {
|
||||
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by "stringer -type Type"; DO NOT EDIT.
|
||||
|
||||
package paths
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[TypeFile-0]
|
||||
_ = x[TypeContentResource-1]
|
||||
_ = x[TypeContentSingle-2]
|
||||
_ = x[TypeLeaf-3]
|
||||
_ = x[TypeBranch-4]
|
||||
_ = x[TypeContentData-5]
|
||||
_ = x[TypeMarkup-6]
|
||||
_ = x[TypeShortcode-7]
|
||||
_ = x[TypePartial-8]
|
||||
_ = x[TypeBaseof-9]
|
||||
}
|
||||
|
||||
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
|
||||
|
||||
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
|
||||
|
||||
func (i Type) String() string {
|
||||
if i < 0 || i >= Type(len(_Type_index)-1) {
|
||||
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Type_name[_Type_index[i]:_Type_index[i+1]]
|
||||
}
|
||||
+141
-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,50 @@ 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
|
||||
}
|
||||
|
||||
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
|
||||
var p PR[string]
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
|
||||
if negate {
|
||||
pattern = pattern[2:]
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.And(func(s string) Match {
|
||||
return BoolMatch(!g.Match(s))
|
||||
})
|
||||
} else {
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.Or(func(s string) Match {
|
||||
return BoolMatch(g.Match(s))
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
type IndexMatcher interface {
|
||||
IndexMatch(match P[string]) (iter.Seq[int], error)
|
||||
}
|
||||
|
||||
@@ -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,86 @@ 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 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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func Run[T any](ctx context.Context, cfg Config[T]) Group[T] {
|
||||
// Buffered for performance.
|
||||
ch := make(chan T, cfg.NumWorkers)
|
||||
|
||||
for i := 0; i < cfg.NumWorkers; i++ {
|
||||
for range cfg.NumWorkers {
|
||||
g.Go(func() error {
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -103,10 +103,7 @@ func (r *RunEvery) Add(name string, f Func) {
|
||||
f.IntervalHigh = 20 * time.Second
|
||||
}
|
||||
|
||||
start := f.IntervalHigh / 3
|
||||
if start < f.IntervalLow {
|
||||
start = f.IntervalLow
|
||||
}
|
||||
start := max(f.IntervalHigh/3, f.IntervalLow)
|
||||
f.interval = start
|
||||
f.last = time.Now()
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
@@ -41,10 +41,6 @@ func PrintANSIColors(f *os.File) bool {
|
||||
// IsTerminal return true if the file descriptor is terminal and the TERM
|
||||
// environment variable isn't a dumb one.
|
||||
func IsTerminal(f *os.File) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return false
|
||||
}
|
||||
|
||||
fd := f.Fd()
|
||||
return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
|
||||
}
|
||||
@@ -77,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) {
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func ToStringSlicePreserveStringE(v any) ([]string, error) {
|
||||
switch vv.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
result = make([]string, vv.Len())
|
||||
for i := 0; i < vv.Len(); i++ {
|
||||
for i := range vv.Len() {
|
||||
s, err := cast.ToStringE(vv.Index(i).Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -45,7 +46,7 @@ func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] {
|
||||
if len(q.set) == q.size {
|
||||
// Full
|
||||
delete(q.set, q.vals[0])
|
||||
q.vals = append(q.vals[:0], q.vals[1:]...)
|
||||
q.vals = slices.Delete(q.vals, 0, 1)
|
||||
}
|
||||
q.set[v] = true
|
||||
q.vals = append(q.vals, v)
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
|
||||
|
||||
queue := NewEvictingQueue[string](3)
|
||||
|
||||
for j := 0; j < 100; j++ {
|
||||
for range 100 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
+29
-1
@@ -28,6 +28,16 @@ type RLocker interface {
|
||||
RUnlock()
|
||||
}
|
||||
|
||||
type Locker interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
}
|
||||
|
||||
type RWLocker interface {
|
||||
RLocker
|
||||
Locker
|
||||
}
|
||||
|
||||
// KeyValue is a interface{} tuple.
|
||||
type KeyValue struct {
|
||||
Key any
|
||||
@@ -59,7 +69,7 @@ func (k KeyValues) String() string {
|
||||
// KeyValues struct.
|
||||
func NewKeyValuesStrings(key string, values ...string) KeyValues {
|
||||
iv := make([]any, len(values))
|
||||
for i := 0; i < len(values); i++ {
|
||||
for i := range values {
|
||||
iv[i] = values[i]
|
||||
}
|
||||
return KeyValues{Key: key, Values: iv}
|
||||
@@ -129,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"
|
||||
@@ -55,7 +55,7 @@ func (v Version) Version() VersionString {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h Version) Compare(other any) int {
|
||||
return compareVersions(h, other)
|
||||
return CompareVersions(h, other)
|
||||
}
|
||||
|
||||
// VersionString represents a Hugo version string.
|
||||
@@ -67,7 +67,7 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
return compareVersions(h.Version(), other)
|
||||
return CompareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
@@ -83,18 +83,24 @@ 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)
|
||||
|
||||
return vv, nil
|
||||
@@ -134,98 +140,55 @@ 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 d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
|
||||
return strings.Compare(v1.Suffix, d.Suffix)
|
||||
}
|
||||
if d.Major > inVersion.Major {
|
||||
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
|
||||
}
|
||||
@@ -234,7 +197,7 @@ func compareVersions(inVersion Version, in any) int {
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return inVersion.Compare(v)
|
||||
return v1.Compare(v)
|
||||
|
||||
}
|
||||
|
||||
@@ -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,31 @@ 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)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
|
||||
}
|
||||
|
||||
func TestParseHugoVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(MustParseVersion("v2.3.2").String(), qt.Equals, "2.3.2")
|
||||
c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25")
|
||||
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
|
||||
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
|
||||
+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 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,4 +11,4 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tplimpl
|
||||
package compare
|
||||
+220
-101
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -28,11 +29,11 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/cache/httpcache"
|
||||
"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/types"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/privacy"
|
||||
@@ -40,8 +41,12 @@ 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"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/minifiers"
|
||||
@@ -54,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.
|
||||
@@ -82,7 +85,7 @@ func init() {
|
||||
}
|
||||
configLanguageKeys = make(map[string]bool)
|
||||
addKeys := func(v reflect.Value) {
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
for i := range v.NumField() {
|
||||
name := strings.ToLower(v.Type().Field(i).Name)
|
||||
if skip[name] {
|
||||
continue
|
||||
@@ -100,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
|
||||
|
||||
@@ -140,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, maps.Params]] `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>
|
||||
@@ -201,19 +214,23 @@ type Config struct {
|
||||
// <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:"-"`
|
||||
|
||||
// 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 {
|
||||
@@ -320,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)
|
||||
@@ -396,44 +401,80 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
|
||||
// Legacy paginate values.
|
||||
if c.Paginate != 0 {
|
||||
hugo.Deprecate("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0")
|
||||
hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger())
|
||||
c.Pagination.PagerSize = c.Paginate
|
||||
}
|
||||
|
||||
if c.PaginatePath != "" {
|
||||
hugo.Deprecate("site config key paginatePath", "Use pagination.path instead.", "v0.128.0")
|
||||
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
|
||||
c.Pagination.Path = c.PaginatePath
|
||||
}
|
||||
|
||||
// Legacy privacy values.
|
||||
if c.Privacy.Twitter.Disable {
|
||||
hugo.Deprecate("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0")
|
||||
hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
|
||||
}
|
||||
|
||||
if c.Privacy.Twitter.EnableDNT {
|
||||
hugo.Deprecate("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0")
|
||||
hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
|
||||
}
|
||||
|
||||
if c.Privacy.Twitter.Simple {
|
||||
hugo.Deprecate("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0")
|
||||
hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
|
||||
}
|
||||
|
||||
// Legacy services values.
|
||||
if c.Services.Twitter.DisableInlineCSS {
|
||||
hugo.Deprecate("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0")
|
||||
hugo.DeprecateWithLogger("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
|
||||
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
|
||||
}
|
||||
|
||||
// Legacy permalink tokens
|
||||
vs := fmt.Sprintf("%v", c.Permalinks)
|
||||
if strings.Contains(vs, ":filename") {
|
||||
hugo.Deprecate("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0")
|
||||
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
if strings.Contains(vs, ":slugorfilename") {
|
||||
hugo.Deprecate("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0")
|
||||
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
|
||||
// Legacy render hook values.
|
||||
alternativeDetails := fmt.Sprintf(
|
||||
"Set to %q if previous value was false, or set to %q if previous value was true.",
|
||||
gc.RenderHookUseEmbeddedNever,
|
||||
gc.RenderHookUseEmbeddedFallback,
|
||||
)
|
||||
if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil {
|
||||
alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails
|
||||
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
|
||||
if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault {
|
||||
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
|
||||
} else {
|
||||
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedNever
|
||||
}
|
||||
}
|
||||
if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil {
|
||||
alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails
|
||||
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
|
||||
if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault {
|
||||
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
|
||||
} else {
|
||||
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedNever
|
||||
}
|
||||
}
|
||||
|
||||
// Validate render hook configuration.
|
||||
renderHookUseEmbeddedModes := []string{
|
||||
gc.RenderHookUseEmbeddedAlways,
|
||||
gc.RenderHookUseEmbeddedAuto,
|
||||
gc.RenderHookUseEmbeddedFallback,
|
||||
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"))
|
||||
}
|
||||
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"))
|
||||
}
|
||||
|
||||
c.C = &ConfigCompiled{
|
||||
@@ -448,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,
|
||||
@@ -488,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
|
||||
@@ -551,6 +590,18 @@ 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
|
||||
@@ -753,14 +804,13 @@ 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
|
||||
|
||||
@@ -768,18 +818,16 @@ type Configs struct {
|
||||
ModulesClient *modules.Client
|
||||
|
||||
// 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, params maps.Params) bool {
|
||||
page.CheckCascadePattern(logger, p)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -798,32 +846,21 @@ func (c *Configs) IsZero() bool {
|
||||
return c == nil || len(c.Languages) == 0
|
||||
}
|
||||
|
||||
func (c *Configs) Init() error {
|
||||
func (c *Configs) Init(logger loggers.Logger) error {
|
||||
var languages langs.Languages
|
||||
defaultContentLanguage := c.Base.DefaultContentLanguage
|
||||
for k, v := range c.LanguageConfigMap {
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
|
||||
|
||||
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)
|
||||
}
|
||||
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
languages = append(languages, language)
|
||||
}
|
||||
|
||||
// Sort the sites by language weight (if set) or lang.
|
||||
sort.Slice(languages, func(i, j int) bool {
|
||||
li := languages[i]
|
||||
lj := languages[j]
|
||||
if li.Weight != lj.Weight {
|
||||
return li.Weight < lj.Weight
|
||||
}
|
||||
return li.Lang < lj.Lang
|
||||
})
|
||||
|
||||
for _, l := range languages {
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang])
|
||||
}
|
||||
|
||||
// Filter out disabled languages.
|
||||
var n int
|
||||
for _, l := range languages {
|
||||
@@ -834,30 +871,65 @@ func (c *Configs) Init() error {
|
||||
}
|
||||
languages = languages[:n]
|
||||
|
||||
var languagesDefaultFirst langs.Languages
|
||||
for _, l := range languages {
|
||||
if l.Lang == defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
for _, l := range languages {
|
||||
if l.Lang != defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
|
||||
c.Languages = languages
|
||||
c.LanguagesDefaultFirst = languagesDefaultFirst
|
||||
c.ConfiguredDimensions = &sitesmatrix.ConfiguredDimensions{
|
||||
ConfiguredLanguages: c.Base.Languages.Config,
|
||||
ConfiguredVersions: c.Base.Versions.Config,
|
||||
ConfiguredRoles: c.Base.Roles.Config,
|
||||
}
|
||||
|
||||
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
|
||||
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{
|
||||
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
|
||||
}
|
||||
|
||||
if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
|
||||
if ext != "" && !of.MediaType.HasSuffix(ext) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
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]
|
||||
sitesMatrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithLanguageIndices(i).WithAllIfNotSet().Build()
|
||||
for _, s := range allDecoderSetups {
|
||||
if getInitializer := s.getInitializer; getInitializer != nil {
|
||||
if err := getInitializer(langConfig).InitConfig(logger, sitesMatrix, c.ConfiguredDimensions); err != nil {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,7 +938,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
|
||||
}
|
||||
|
||||
@@ -875,7 +947,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
|
||||
}
|
||||
@@ -886,7 +958,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
|
||||
}
|
||||
|
||||
@@ -903,24 +975,55 @@ 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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newDefaultConfig() *Config {
|
||||
return &Config{
|
||||
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
|
||||
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
|
||||
RootConfig: RootConfig{
|
||||
Environment: hugo.EnvironmentProduction,
|
||||
TitleCaseStyle: "AP",
|
||||
PluralizeListTitles: true,
|
||||
CapitalizeListTitles: true,
|
||||
StaticDir: []string{"static"},
|
||||
SummaryLength: 70,
|
||||
Timeout: "60s",
|
||||
|
||||
CommonDirs: config.CommonDirs{
|
||||
ArcheTypeDir: "archetypes",
|
||||
ContentDir: "content",
|
||||
ResourceDir: "resources",
|
||||
PublishDir: "public",
|
||||
ThemesDir: "themes",
|
||||
AssetDir: "assets",
|
||||
LayoutDir: "layouts",
|
||||
I18nDir: "i18n",
|
||||
DataDir: "data",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// fromLoadConfigResult creates a new Config from res.
|
||||
func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) {
|
||||
if !res.Cfg.IsSet("languages") {
|
||||
// We need at least one
|
||||
lang := res.Cfg.GetString("defaultContentLanguage")
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
|
||||
}
|
||||
bcfg := res.BaseConfig
|
||||
cfg := res.Cfg
|
||||
|
||||
all := &Config{}
|
||||
all := newDefaultConfig()
|
||||
|
||||
err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil)
|
||||
if err != nil {
|
||||
@@ -930,6 +1033,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
langConfigMap := make(map[string]*Config)
|
||||
|
||||
languagesConfig := cfg.GetStringMap("languages")
|
||||
|
||||
var isMultihost bool
|
||||
|
||||
if err := all.CompileConfig(logger); err != nil {
|
||||
@@ -940,6 +1044,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
mergedConfig := config.New()
|
||||
var differentRootKeys []string
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
case maps.Params:
|
||||
_, found := x["params"]
|
||||
if !found {
|
||||
@@ -947,7 +1052,6 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
|
||||
}
|
||||
}
|
||||
|
||||
for kk, vv := range x {
|
||||
if kk == "_merge" {
|
||||
continue
|
||||
@@ -956,6 +1060,21 @@ 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.(maps.Params); ok {
|
||||
// With the introduction of YAML anchor and alias support, language config entries
|
||||
// may be contain shared references.
|
||||
// This also break potential cycles.
|
||||
vv = maps.CloneParamsDeep(p)
|
||||
}
|
||||
|
||||
if kk == "cascade" {
|
||||
// If not set, add the current language to make sure it does not get applied to other languages.
|
||||
page.AddLangToCascadeTargetMap(k, vv.(maps.Params))
|
||||
// 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) {
|
||||
@@ -966,7 +1085,8 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
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))
|
||||
|
||||
@@ -986,7 +1106,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
|
||||
@@ -1005,18 +1125,17 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
|
||||
// Adjust Goldmark config defaults for multilingual, single-host sites.
|
||||
if len(languagesConfig) > 1 && !isMultihost && !clone.Markup.Goldmark.DuplicateResourceFiles {
|
||||
if !clone.Markup.Goldmark.DuplicateResourceFiles {
|
||||
if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil {
|
||||
clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true)
|
||||
}
|
||||
if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil {
|
||||
clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true)
|
||||
}
|
||||
if clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto {
|
||||
clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
|
||||
}
|
||||
if clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto {
|
||||
clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
|
||||
}
|
||||
}
|
||||
|
||||
langConfigMap[k] = clone
|
||||
case maps.ParamsMergeStrategy:
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type in languages config: %T", v))
|
||||
|
||||
@@ -1067,7 +1186,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)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package allconfig_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
)
|
||||
|
||||
@@ -63,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) {
|
||||
@@ -195,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 --
|
||||
@@ -234,3 +217,172 @@ baseURL = "https://example.com"
|
||||
b.Assert(c.IsContentFile("foo.md"), qt.Equals, true)
|
||||
b.Assert(len(s), qt.Equals, 6)
|
||||
}
|
||||
|
||||
func TestMergeDeep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
theme = ["theme1", "theme2"]
|
||||
_merge = "deep"
|
||||
-- themes/theme1/hugo.toml --
|
||||
[sitemap]
|
||||
filename = 'mysitemap.xml'
|
||||
[services]
|
||||
[services.googleAnalytics]
|
||||
id = 'foo bar'
|
||||
[taxonomies]
|
||||
foo = 'bars'
|
||||
-- themes/theme2/config/_default/hugo.toml --
|
||||
[taxonomies]
|
||||
bar = 'baz'
|
||||
-- layouts/home.html --
|
||||
GA ID: {{ site.Config.Services.GoogleAnalytics.ID }}.
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
conf := b.H.Configs
|
||||
base := conf.Base
|
||||
|
||||
b.Assert(base.Environment, qt.Equals, hugo.EnvironmentProduction)
|
||||
b.Assert(base.BaseURL, qt.Equals, "https://example.com")
|
||||
b.Assert(base.Sitemap.Filename, qt.Equals, "mysitemap.xml")
|
||||
b.Assert(base.Taxonomies, qt.DeepEquals, map[string]string{"bar": "baz", "foo": "bars"})
|
||||
|
||||
b.AssertFileContent("public/index.html", "GA ID: foo bar.")
|
||||
}
|
||||
|
||||
func TestMergeDeepBuildStats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
title = "Theme 1"
|
||||
_merge = "deep"
|
||||
[module]
|
||||
[module.hugoVersion]
|
||||
[[module.imports]]
|
||||
path = "theme1"
|
||||
-- themes/theme1/hugo.toml --
|
||||
[build]
|
||||
[build.buildStats]
|
||||
disableIDs = true
|
||||
enable = true
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
|
||||
conf := b.H.Configs
|
||||
base := conf.Base
|
||||
|
||||
b.Assert(base.Title, qt.Equals, "Theme 1")
|
||||
b.Assert(len(base.Module.Imports), qt.Equals, 1)
|
||||
b.Assert(base.Build.BuildStats.Enable, qt.Equals, true)
|
||||
b.AssertFileExists("/hugo_stats.json", true)
|
||||
}
|
||||
|
||||
func TestMergeDeepBuildStatsTheme(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
_merge = "deep"
|
||||
theme = ["theme1"]
|
||||
-- themes/theme1/hugo.toml --
|
||||
title = "Theme 1"
|
||||
[build]
|
||||
[build.buildStats]
|
||||
disableIDs = true
|
||||
enable = true
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
|
||||
|
||||
conf := b.H.Configs
|
||||
base := conf.Base
|
||||
|
||||
b.Assert(base.Title, qt.Equals, "Theme 1")
|
||||
b.Assert(len(base.Module.Imports), qt.Equals, 1)
|
||||
b.Assert(base.Build.BuildStats.Enable, qt.Equals, true)
|
||||
b.AssertFileExists("/hugo_stats.json", true)
|
||||
}
|
||||
|
||||
func TestDefaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
[languages]
|
||||
[languages.nn]
|
||||
weight = 20
|
||||
[languages.sv]
|
||||
weight = 10
|
||||
[languages.sv.taxonomies]
|
||||
tag = "taggar"
|
||||
-- layouts/all.html --
|
||||
All.
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv")
|
||||
}
|
||||
|
||||
func TestDefaultConfigEnvDisableLanguagesIssue13707(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableLanguages = []
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.nn]
|
||||
weight = 2
|
||||
[languages.sv]
|
||||
weight = 3
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptWithConfig(func(conf *hugolib.IntegrationTestConfig) {
|
||||
conf.Environ = []string{`HUGO_DISABLELANGUAGES=sv nn`}
|
||||
}))
|
||||
|
||||
b.Assert(len(b.H.Sites), qt.Equals, 1)
|
||||
}
|
||||
|
||||
// Issue 13535
|
||||
// We changed enablement of the embedded link and image render hooks from
|
||||
// booleans to enums in v0.148.0.
|
||||
func TestLegacyEmbeddedRenderHookEnablement(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[markup.goldmark.renderHooks.image]
|
||||
#KEY_VALUE
|
||||
|
||||
[markup.goldmark.renderHooks.link]
|
||||
#KEY_VALUE
|
||||
`
|
||||
f := strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = false")
|
||||
b := hugolib.Test(t, f)
|
||||
c := b.H.Configs.Base.Markup.Goldmark.RenderHooks
|
||||
b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
|
||||
b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
|
||||
|
||||
f = strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = true")
|
||||
b = hugolib.Test(t, f)
|
||||
c = b.H.Configs.Base.Markup.Goldmark.RenderHooks
|
||||
b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
|
||||
b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@ 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/hstrings"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -27,8 +31,9 @@ import (
|
||||
"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",
|
||||
@@ -210,6 +222,70 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
},
|
||||
},
|
||||
"languages": {
|
||||
key: "languages",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
if len(m) == 1 {
|
||||
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
|
||||
var first maps.Params
|
||||
var ok bool
|
||||
for _, v := range m {
|
||||
first, ok = v.(maps.Params)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
if _, found := first["languagecode"]; !found {
|
||||
first["languagecode"] = p.p.GetString("languagecode")
|
||||
}
|
||||
}
|
||||
}
|
||||
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 := maps.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 := maps.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 {
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
@@ -249,14 +325,18 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "sitemap",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key))
|
||||
if p.p.IsSet(d.key) {
|
||||
p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key))
|
||||
}
|
||||
return err
|
||||
},
|
||||
},
|
||||
"taxonomies": {
|
||||
key: "taxonomies",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
if p.p.IsSet(d.key) {
|
||||
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -279,54 +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.
|
||||
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, 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",
|
||||
|
||||
@@ -19,6 +19,7 @@ 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"
|
||||
)
|
||||
@@ -27,20 +28,21 @@ type ConfigLanguage struct {
|
||||
config *Config
|
||||
baseConfig config.BaseConfig
|
||||
|
||||
m *Configs
|
||||
language *langs.Language
|
||||
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 +50,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 +99,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
|
||||
}
|
||||
@@ -137,11 +143,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 +161,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 +226,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 +281,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
|
||||
}
|
||||
|
||||
+75
-96
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
@@ -31,7 +32,7 @@ import (
|
||||
"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.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.).
|
||||
@@ -159,63 +167,9 @@ func (l configLoader) applyConfigAliases() error {
|
||||
|
||||
func (l configLoader) applyDefaultConfig() error {
|
||||
defaultSettings := maps.Params{
|
||||
"baseURL": "",
|
||||
"cleanDestinationDir": false,
|
||||
"watch": false,
|
||||
"contentDir": "content",
|
||||
"resourceDir": "resources",
|
||||
"publishDir": "public",
|
||||
"publishDirOrig": "public",
|
||||
"themesDir": "themes",
|
||||
"assetDir": "assets",
|
||||
"layoutDir": "layouts",
|
||||
"i18nDir": "i18n",
|
||||
"dataDir": "data",
|
||||
"archetypeDir": "archetypes",
|
||||
"configDir": "config",
|
||||
"staticDir": "static",
|
||||
"buildDrafts": false,
|
||||
"buildFuture": false,
|
||||
"buildExpired": false,
|
||||
"params": maps.Params{},
|
||||
"environment": hugo.EnvironmentProduction,
|
||||
"uglyURLs": false,
|
||||
"verbose": false,
|
||||
"ignoreCache": false,
|
||||
"canonifyURLs": false,
|
||||
"relativeURLs": false,
|
||||
"removePathAccents": false,
|
||||
"titleCaseStyle": "AP",
|
||||
"taxonomies": maps.Params{"tag": "tags", "category": "categories"},
|
||||
"permalinks": maps.Params{},
|
||||
"sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"},
|
||||
"menus": maps.Params{},
|
||||
"disableLiveReload": false,
|
||||
"pluralizeListTitles": true,
|
||||
"capitalizeListTitles": true,
|
||||
"forceSyncStatic": false,
|
||||
"footnoteAnchorPrefix": "",
|
||||
"footnoteReturnLinkContents": "",
|
||||
"newContentEditor": "",
|
||||
"paginate": 0, // Moved into the paginator struct in Hugo v0.128.0.
|
||||
"paginatePath": "", // Moved into the paginator struct in Hugo v0.128.0.
|
||||
"summaryLength": 70,
|
||||
"rssLimit": -1,
|
||||
"sectionPagesMenu": "",
|
||||
"disablePathToLower": false,
|
||||
"hasCJKLanguage": false,
|
||||
"enableEmoji": false,
|
||||
"defaultContentLanguage": "en",
|
||||
"defaultContentLanguageInSubdir": false,
|
||||
"enableMissingTranslationPlaceholders": false,
|
||||
"enableGitInfo": false,
|
||||
"ignoreFiles": make([]string, 0),
|
||||
"disableAliases": false,
|
||||
"debug": false,
|
||||
"disableFastRender": false,
|
||||
"timeout": "30s",
|
||||
"timeZone": "",
|
||||
"enableInlineShortcodes": false,
|
||||
// These dirs are used early/before we build the config struct.
|
||||
"themesDir": "themes",
|
||||
"configDir": "config",
|
||||
}
|
||||
|
||||
l.cfg.SetDefaults(defaultSettings)
|
||||
@@ -224,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", maps.Params{"minifyOutput": true})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -260,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
|
||||
}
|
||||
@@ -287,43 +247,61 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
|
||||
|
||||
if existing != nil {
|
||||
val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
val = l.envValToVal(env.Key, val)
|
||||
if owner != nil {
|
||||
owner[nestedKey] = val
|
||||
} else {
|
||||
l.cfg.Set(env.Key, val)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if owner != nil {
|
||||
owner[nestedKey] = val
|
||||
} else {
|
||||
l.cfg.Set(env.Key, val)
|
||||
}
|
||||
} else {
|
||||
if nestedKey != "" {
|
||||
owner[nestedKey] = env.Value
|
||||
} else {
|
||||
var val any
|
||||
key := strings.ReplaceAll(env.Key, delim, ".")
|
||||
_, ok := allDecoderSetups[key]
|
||||
if ok {
|
||||
// A map.
|
||||
if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{}); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
if val == nil {
|
||||
// A string.
|
||||
val = l.envStringToVal(key, env.Value)
|
||||
}
|
||||
l.cfg.Set(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
if owner != nil && nestedKey != "" {
|
||||
owner[nestedKey] = env.Value
|
||||
} else {
|
||||
var val any
|
||||
key := strings.ReplaceAll(env.Key, delim, ".")
|
||||
_, ok := allDecoderSetups[key]
|
||||
if ok {
|
||||
// A map.
|
||||
if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]any{}); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
// A string.
|
||||
val = l.envStringToVal(key, env.Value)
|
||||
}
|
||||
l.cfg.Set(key, val)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *configLoader) envValToVal(k string, v any) any {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return l.envStringToVal(k, v)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -568,11 +546,12 @@ func (l configLoader) loadConfig(configName string) (string, error) {
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
func (l configLoader) deleteMergeStrategies() {
|
||||
func (l configLoader) deleteMergeStrategies() (err error) {
|
||||
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
|
||||
params[len(params)-1].Params.DeleteMergeStrategy()
|
||||
return false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (l configLoader) wrapFileError(err error, filename string) error {
|
||||
|
||||
@@ -58,7 +58,7 @@ weight = 4
|
||||
Filename: configFilename,
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, err := LoadConfig(d)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -128,7 +129,7 @@ func (w BuildStats) Enabled() bool {
|
||||
}
|
||||
|
||||
func (b BuildConfig) clone() BuildConfig {
|
||||
b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
|
||||
b.CacheBusters = slices.Clone(b.CacheBusters)
|
||||
return b
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user