mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-29 17:52:38 +00:00
Compare commits
413 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8f66a7ce | |||
| c4eba92863 | |||
| 8b40a96b6e | |||
| d65af84d15 | |||
| 454450a647 | |||
| 2bfcc6b994 | |||
| 98d396c16a | |||
| d4ae662d59 | |||
| 9ede5fb9e0 | |||
| 833a878eef | |||
| 7622dd86ce | |||
| 0814059bb6 | |||
| 8920d56e95 | |||
| 633cc772e0 | |||
| 90d8bf34ae | |||
| 4c40c6d5ca | |||
| d2594db670 | |||
| ab2de51e07 | |||
| 72b85d5f9c | |||
| 75f61832c2 | |||
| 4c03129fcf | |||
| 080970bc6b | |||
| 1b7495bc49 | |||
| 79f030be5b | |||
| 896bc89ab8 | |||
| 100dde53ad | |||
| bdebb79705 | |||
| a54c398b93 | |||
| f5fce935e7 | |||
| 52123ae23f | |||
| 38b8afdc90 | |||
| 4169c1f70f | |||
| 7574e35b40 | |||
| 927666005c | |||
| 790f4084a1 | |||
| de6955bab3 | |||
| a77bd527fd | |||
| 017a7cd63a | |||
| e3413d927f | |||
| b01cc14703 | |||
| 8ee19ff9a3 | |||
| 6436deb3e1 | |||
| 547ab29ced | |||
| 0d58e4286f | |||
| bbb42b5a6a | |||
| 1eea9fba0b | |||
| 9a5c7e0d24 | |||
| 6613b08eb6 | |||
| 582c26ef42 | |||
| a4f2a8a579 | |||
| 8d6145f3c3 | |||
| ce2a156a4e | |||
| 9747724222 | |||
| a17bdbc5fa | |||
| 8f94d65cac | |||
| d6bc8165e6 | |||
| 8b00030b34 | |||
| c48551677c | |||
| 161d0d4757 | |||
| 45e4596630 | |||
| 58927aa14a | |||
| ce009e3aa9 | |||
| 0755872424 | |||
| 6b5554bac9 | |||
| 652fc5acdd | |||
| bf6e35a755 | |||
| 4e91e14cb0 | |||
| dc9b51d2e2 | |||
| 481baa0896 | |||
| 43aad7118d | |||
| 9f1f1be0be | |||
| 0eda24e65f | |||
| 5d09b5e32a | |||
| 303e443ea7 | |||
| beb57a6839 | |||
| 638262cec3 | |||
| 397980aab3 | |||
| 5f4646acaa | |||
| 479fe6c654 | |||
| 81a5cdca07 | |||
| df520e3150 | |||
| b55d452e46 | |||
| ea7eac6558 | |||
| 458ebdd448 | |||
| 86c7d3afac | |||
| 42289d76f9 | |||
| 0c013c2326 | |||
| bd071d78a5 | |||
| 2ed7d193cf | |||
| d2043cfae5 | |||
| 5997b71db6 | |||
| e7afabb927 | |||
| 4f3c39890e | |||
| 807cae1df1 | |||
| c4fb61d9df | |||
| d88a29e002 | |||
| 3ff9b7f89f | |||
| 9dd9c76027 | |||
| a8fca598e6 | |||
| 182b104571 | |||
| be93ccd304 | |||
| 2669bca6fd | |||
| 753d447f11 | |||
| 4f39d724fe | |||
| eaf4c7515a | |||
| 3315a86d67 | |||
| 38244842e0 | |||
| 7813c5c89d | |||
| eb11c3d0bc | |||
| fa8658111f | |||
| f41be7959a | |||
| 0e46a97e8a | |||
| c27d9e8fcf | |||
| 098eac59a9 | |||
| 3c980c072e | |||
| 404ac00001 | |||
| 06fcb72421 | |||
| 9b5f1d491d | |||
| 87f8de8c7a | |||
| 67ef6c68de | |||
| 623722bb07 | |||
| e431f90bc6 | |||
| a7cbcf15f0 | |||
| 59e0446fe5 | |||
| 36cdb2c789 | |||
| 3e3b849cc7 | |||
| 1f578f160c | |||
| 842d8f1052 | |||
| c47ec23342 | |||
| 4652ae4a44 | |||
| 4eafd9eb7b | |||
| 13a95b9c01 | |||
| e3108225bf | |||
| c9b88e4d15 | |||
| 54c804876a | |||
| 157bfddda5 | |||
| d4f2122dea | |||
| 991d2f9aba | |||
| b29c2f7ade | |||
| e91d191155 | |||
| a8a54bd020 | |||
| f797f84902 | |||
| e8577771c3 | |||
| 44dc3847f4 | |||
| 5594b28229 | |||
| 7747abbb31 | |||
| 3dff7c8c7a | |||
| d98cd4aecf | |||
| 68059972e8 | |||
| 8e28668b09 | |||
| a3ea9cd18f | |||
| 0c2fa2460f | |||
| 49bfb1070b | |||
| b7203bbb3a | |||
| dfece5b674 | |||
| 2d691c7e47 | |||
| b96d58a144 | |||
| 9869e71aaa | |||
| 8f47fe8c87 | |||
| 65b4287c68 | |||
| cc338a9d48 | |||
| 3850881f2a | |||
| a18bec1123 | |||
| 3c9b41f9ec | |||
| 9d914726de | |||
| 86aa62524f | |||
| d8ec0eeeaf | |||
| 4148eded9c | |||
| bba2aed352 | |||
| 29b8e17d29 | |||
| 3c823408ee | |||
| 3f9d0ad2b6 | |||
| ab62320d6b | |||
| 21be4afd49 | |||
| 32027e50a1 | |||
| 8d19f5a78b | |||
| e549016bc5 | |||
| a392906b55 | |||
| e6f01bb464 | |||
| 498431d641 | |||
| 773664b812 | |||
| 7729473500 | |||
| 47e39af2cf | |||
| 4a641d1594 | |||
| 8b7b6af92c | |||
| 30a20122b7 | |||
| 8f3c066d23 | |||
| 7850e7ceb5 | |||
| 86786c91e5 | |||
| 7a7308aec2 | |||
| 2e5132d8d2 | |||
| 19ab3f581c | |||
| 7137714827 | |||
| 3158a51283 | |||
| af5051e759 | |||
| b358a65c8b | |||
| 8ae5e1a8be | |||
| 2d1d7312d0 | |||
| b26fbf80a0 | |||
| 828ee720cc | |||
| 4384f9b8c2 | |||
| 55d136dd66 | |||
| e2776593c0 | |||
| de0c9f3eca | |||
| 73583091e8 | |||
| ee91280412 | |||
| 8a858213b7 | |||
| 6ac7d081d1 | |||
| 24eb84f868 | |||
| 95a3678245 | |||
| 9045797d5f | |||
| 73641aeca1 | |||
| d8c0dfccf7 | |||
| 6bd2bde9d3 | |||
| 0f1c7d1200 | |||
| 10352335e0 | |||
| f23576f2fb | |||
| b5d43cdc17 | |||
| 6ef8017f60 | |||
| d67925f5a1 | |||
| 48dd4f469a | |||
| b1e1eede50 | |||
| f522a7282b | |||
| 6ac72dd3bf | |||
| 74c0bbfc6a | |||
| e66a33d369 | |||
| 8a979d54d1 | |||
| fd49df8f7a | |||
| c7b35c8704 | |||
| 5916b61be9 | |||
| 7eafef2269 | |||
| 00c4228f61 | |||
| 5f5b2f378f | |||
| b3ea2a5fab | |||
| 67b543549b | |||
| ef7a22a2ff | |||
| 8dfcece805 | |||
| 9a6bfe2667 | |||
| 7d45614c6d | |||
| 7f12c0900d | |||
| dab2c332dc | |||
| 5a64551a07 | |||
| 0fc63fbf57 | |||
| 576d55d009 | |||
| b9400b8a95 | |||
| ce0c7f42c1 | |||
| f472dd48a7 | |||
| 50973e7272 | |||
| 48566b6f37 | |||
| b441472b69 | |||
| ac2c3fb20f | |||
| ee91c707ee | |||
| 11f7f39913 | |||
| 192e3c453e | |||
| e569dd59a0 | |||
| 5ba03bf657 | |||
| c1b2e58bfb | |||
| 77214117ae | |||
| 1a94731669 | |||
| 18784711b5 | |||
| 2c611091fd | |||
| 8e9e04e395 | |||
| 32334d098e | |||
| 8e2e60dd45 | |||
| 2bc54df622 | |||
| 608ed09a0a | |||
| 94f1ede3aa | |||
| ab374e3d22 | |||
| 1a9133075c | |||
| d36a8f5601 | |||
| 487b8e3af3 | |||
| a6f99cca22 | |||
| a775488304 | |||
| 66ba63cd5f | |||
| eb06a3cd0c | |||
| a798e3e724 | |||
| fbcf0d52a1 | |||
| a2b2a5a978 | |||
| ec178ea463 | |||
| 2d80b8a741 | |||
| 5ea3e13db6 | |||
| b1c1bd019f | |||
| 866b8e5aa6 | |||
| 409669fa48 | |||
| f66d094446 | |||
| e9b9b36f46 | |||
| b8a2c10d06 | |||
| 601df38834 | |||
| 3bed212a2f | |||
| e2fd6764be | |||
| 8f3527f61c | |||
| 09048aad76 | |||
| f42c422a12 | |||
| 0b71db299a | |||
| 5c7fad23a3 | |||
| 700bb78198 | |||
| d3b5d47a43 | |||
| 86cd1838ff | |||
| 871da3370a | |||
| 2637aa1540 | |||
| bf1d20d735 | |||
| 1deec99bba | |||
| b7bb557c6a | |||
| 7c19c196c3 | |||
| 555443b64a | |||
| 1f2de189ed | |||
| c6ae33c61d | |||
| edeebf0d33 | |||
| ea9675f6e7 | |||
| b1f7e35a98 | |||
| a0d4e1fb26 | |||
| 96777d9b89 | |||
| 25126e5f87 | |||
| 179034abbb | |||
| 96ac146eef | |||
| d4c0e4452c | |||
| 418156ec3e | |||
| 798533a201 | |||
| a8c5d0d00d | |||
| a94a941fe3 | |||
| 385d1a1342 | |||
| d30bd50467 | |||
| 8e6cac8462 | |||
| 4085ee9340 | |||
| 168bf17e70 | |||
| 80ea90c880 | |||
| f740d7cfe0 | |||
| 5f46da6e2a | |||
| fe64a68225 | |||
| b4128babb8 | |||
| 790a5b7014 | |||
| 70ce2446b5 | |||
| 08e1ea5c70 | |||
| 65d43e1d0d | |||
| 65a76664e3 | |||
| da5b1fcf8f | |||
| 39649dce49 | |||
| 614fb16ec6 | |||
| 0637adb239 | |||
| 0bf61353f6 | |||
| b82e496c13 | |||
| 33542d3966 | |||
| 588d20b315 | |||
| b33730293d | |||
| e6a3f1d62b | |||
| 573ecb530f | |||
| 9937a5dcaa | |||
| 1b4514e020 | |||
| 429e57201d | |||
| fa7d37f0c8 | |||
| fae49a22f1 | |||
| 364d237b51 | |||
| 079f3eb8d1 | |||
| 4e6f2b6791 | |||
| dff5e168a9 | |||
| c82a035bf1 | |||
| 86b01eda10 | |||
| e2e64aeec5 | |||
| 9e7182e9e6 | |||
| cf3ad62c3b | |||
| c1cc65fcb9 | |||
| 663075920a | |||
| 596517a259 | |||
| bf42138661 | |||
| 57fecb6b71 | |||
| 6d0ab6a34e | |||
| 6be463b586 | |||
| 9e24b5645d | |||
| 45b67f6c18 | |||
| 5e649eb4d0 | |||
| 0de8f8607b | |||
| 3073fd5ccf | |||
| b9b304a126 | |||
| 8be30ef2cb | |||
| 565a10cbf0 | |||
| 438f113f83 | |||
| dab54058ea | |||
| 1a1b062a01 | |||
| 7a43b928a6 | |||
| 555dfa207a | |||
| 3d21b0687b | |||
| 34b0c15a54 | |||
| 9dc38e917c | |||
| b592d34efe | |||
| 18a2afa519 | |||
| 0b0c827d5b | |||
| 30dc75e620 | |||
| 6bc8e6f491 | |||
| cda4d75187 | |||
| 84950ed2b2 | |||
| 2b337cd212 | |||
| cc61d00df9 | |||
| 8ef2c61985 | |||
| a8ed613742 | |||
| b91688034f | |||
| b971b7f911 | |||
| 24210dcad9 | |||
| b037b9300c | |||
| 268046732e | |||
| 8ce80f755d | |||
| 4baa95ba65 | |||
| 0f94274a28 | |||
| 5f9c3a3e4c | |||
| 51d77a6ca8 | |||
| d25b619ce2 | |||
| b81861fd21 | |||
| 15cce89e0e | |||
| 187e683bb2 | |||
| 432078a6af | |||
| 4a76d8c611 | |||
| d837eff08f | |||
| 323e306bd7 | |||
| 2047638a2c |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22500.20300
|
||||
- image: bepsays/ci-hugoreleaser:1.22600.20100
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20300
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
+4
-4
@@ -2,12 +2,12 @@ have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: true
|
||||
disable: false
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: false
|
||||
summary: true
|
||||
code_review: true
|
||||
help: true
|
||||
summary: false
|
||||
code_review: false
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
You are an expert at detecting AI-generated content. Your main tasks is to analyze pull requests and determine if they were likely generated by AI and if this is disclosed in the pull request description.
|
||||
|
||||
Important: You must start your response with a confidence score line in exactly this format:
|
||||
Confidence Score: X% where X is a number between 0 and 100.
|
||||
|
||||
Then provide your analysis in this format:
|
||||
Reasoning: (explain your analysis)
|
||||
Key Indicators: (list specific elements that suggest AI or human authorship)
|
||||
|
||||
A high confidence score (>80%) means you are very confident the content was AI-generated.
|
||||
A low confidence score (<20%) means you are very confident the content was human-generated.
|
||||
Use scores between 20-80% when there is uncertainty.
|
||||
|
||||
If there's a high confidence score:
|
||||
|
||||
* Add the label "Suspect AI" to the PR
|
||||
* If it's not disclosed that it's AI generated, add a link to [AI Assistance Notice](../CONTRIBUTING.md#ai-assistance-notice) in the contribution gudelines.
|
||||
@@ -1,17 +0,0 @@
|
||||
name: AI Detection
|
||||
on:
|
||||
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review, synchronize]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
detect-ai:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: AI Watchdog
|
||||
uses: Citric-Sheep/ai-watchdog@0db4a635d309db0f8d82642130dfc33e91cf33fa # v0.2.3
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -16,20 +16,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Login to GHCR
|
||||
# Login is only needed when the image is pushed
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
provenance: mode=max
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1
|
||||
- uses: dessant/lock-threads@f5f995c727ac99a91dec92781a8e34e7c839a65e # v6.0.0
|
||||
with:
|
||||
issue-inactive-days: 21
|
||||
add-issue-labels: 'Outdated'
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
operations-per-run: 999
|
||||
days-before-issue-stale: 365
|
||||
|
||||
+17
-21
@@ -16,7 +16,7 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
go-version: [1.25.x, 1.26.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -24,9 +24,7 @@ jobs:
|
||||
name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
tool-cache: true
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
@@ -34,9 +32,9 @@ jobs:
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
@@ -44,25 +42,22 @@ jobs:
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
|
||||
uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
|
||||
with:
|
||||
ruby-version: "3.4.5"
|
||||
- name: Install gems
|
||||
- name: Install Ruby gems
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
gem install asciidoctor-diagram-ditaamini -v "1.0.3"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
- name: Install Java # required by asciidoctor-diagram-ditaamini
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
java-package: jre
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install Mage
|
||||
@@ -115,7 +110,10 @@ jobs:
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Run staticcheck
|
||||
run: staticcheck ./...
|
||||
run: |
|
||||
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
|
||||
staticcheck ./...
|
||||
rm -rf ${{ runner.temp }}/staticcheck
|
||||
- if: matrix.os != 'windows-latest'
|
||||
name: Check
|
||||
run: |
|
||||
@@ -127,16 +125,14 @@ jobs:
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test;
|
||||
mage -v test
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended,withdeploy
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
go clean -i -cache
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
|
||||
+2
-1
@@ -3,4 +3,5 @@
|
||||
imports.*
|
||||
dist/
|
||||
public/
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
cache/filecache/_gen/
|
||||
@@ -0,0 +1 @@
|
||||
tpl/tplimpl/embedded/templates/**
|
||||
@@ -0,0 +1,18 @@
|
||||
* Brevity is good.
|
||||
* Assume that the maintainers and readers of the code you write are Go experts:
|
||||
* Don't use comments to explain the obvious.
|
||||
* Use self-explanatory variable and function names.
|
||||
* Use short variable names when the context is clear.
|
||||
* If you need to add temporary debug printing, use `hdebug.Printf`.[^1]
|
||||
* Never export symbols that's not needed outside of the package.
|
||||
* Avoid global state at (almost) all cost.
|
||||
* This is a project with a long history; assume that a similiar problem has been solved before, look hard for helper functions before creating new ones.
|
||||
* In tests, use `qt` matchers (e.g. `b.Assert(err, qt.ErrorMatches, ...)`) instead of raw `if`/`t.Fatal` checks.
|
||||
* In tests, always use the latest Hugo specification, e.g. for layouts, it's `layouts/page.html` and not `layouts/_default/single.html`, `layouts/list.html` and not `layouts/_default/list.html`
|
||||
* Never name tests `TestIssue1234`; if this e.g. is about fixing an issue, use the issue ID as a suffix, as in `TestUpperCaseTitlesCreatesHavocIssue1234`. And note: No underscores in func names.
|
||||
* Brevity is good. This applies to code, comments and commit messages. Don't write a novel.
|
||||
* Use `./check.sh ./somepackage/...` when iterating.
|
||||
* Use `./check.sh` when you're done.
|
||||
|
||||
|
||||
[^1]: CI build fail if you forget to remove the debug printing.
|
||||
+2
-2
@@ -77,7 +77,7 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Ensure that `./check.sh` succeeds. Note that some tests are skipped when running locally, some because they are slow. To run these locally, do `CI_LOCAL=true ./check.sh ./somepackage/...`.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
## AI Assistance Notice
|
||||
@@ -88,7 +88,7 @@ An example disclosure:
|
||||
|
||||
> This PR was written primarily by Claude Code.
|
||||
|
||||
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it.
|
||||
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it. Also, AI contributions from non-maintainers needs to have a fairly narrow scope (e.g. a bug fix), as we have limited review capacity.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
|
||||
|
||||
+4
-2
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
ARG GO_VERSION="1.25"
|
||||
ARG GO_VERSION="1.26"
|
||||
ARG ALPINE_VERSION="3.22"
|
||||
ARG DART_SASS_VERSION="1.79.3"
|
||||
|
||||
@@ -65,7 +65,9 @@ RUN apk add --no-cache \
|
||||
git \
|
||||
runuser \
|
||||
nodejs \
|
||||
npm
|
||||
npm \
|
||||
openssh-client \
|
||||
tar
|
||||
|
||||
RUN mkdir -p /var/hugo/bin /cache && \
|
||||
addgroup -Sg 1000 hugo && \
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
[Clang]: https://clang.llvm.org/
|
||||
[GCC]: https://gcc.gnu.org/
|
||||
[Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
|
||||
[Go]: https://go.dev/doc/install
|
||||
[bep]: https://github.com/bep
|
||||
[bugs]: https://github.com/gohugoio/hugo/issues?q=is%3Aopen+is%3Aissue+label%3ABug
|
||||
[contributing]: CONTRIBUTING.md
|
||||
[create a proposal]: https://github.com/gohugoio/hugo/issues/new?labels=Proposal%2C+NeedsTriage&template=feature_request.md
|
||||
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
|
||||
[details]: https://gohugo.io/host-and-deploy/deploy-with-hugo-deploy/
|
||||
[documentation repository]: https://github.com/gohugoio/hugoDocs
|
||||
[documentation]: https://gohugo.io/documentation
|
||||
[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd
|
||||
[features]: https://gohugo.io/about/features/
|
||||
[forum]: https://discourse.gohugo.io
|
||||
[friends]: https://github.com/gohugoio/hugo/graphs/contributors
|
||||
[go]: https://go.dev/
|
||||
[hugo modules]: https://gohugo.io/hugo-modules/
|
||||
[installation]: https://gohugo.io/installation
|
||||
[issue queue]: https://github.com/gohugoio/hugo/issues
|
||||
@@ -20,12 +25,13 @@
|
||||
[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator
|
||||
[support]: https://discourse.gohugo.io
|
||||
[themes]: https://themes.gohugo.io/
|
||||
[transpile sass to css]: https://gohugo.io/functions/css/sass/
|
||||
[website]: https://gohugo.io
|
||||
[windows]: https://gohugo.io/installation/windows
|
||||
|
||||
<a href="https://gohugo.io/"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/static/images/hugo-logo-wide.svg?sanitize=true" alt="Hugo" width="565"></a>
|
||||
|
||||
A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go].
|
||||
A fast and flexible static site generator built with love by [bep][], [spf13][], and [friends][] in Go.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,11 +39,11 @@ A fast and flexible static site generator built with love by [bep], [spf13], and
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
[Website][] | [Installation][] | [Documentation][] | [Support][] | [Contributing][] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
|
||||
## Overview
|
||||
|
||||
Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
|
||||
Hugo is a [static site generator][] written in Go, optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
|
||||
|
||||
Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create:
|
||||
|
||||
@@ -52,21 +58,20 @@ Use Hugo's embedded web server during development to instantly see changes to co
|
||||
|
||||
Hugo's fast asset pipelines include:
|
||||
|
||||
- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
|
||||
- CSS Processing – Bundle, transform, minify, create source maps, perform SRI hashing, and integrate with PostCSS.
|
||||
- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract metadata
|
||||
- JavaScript bundling – Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
|
||||
- Sass processing – Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
|
||||
- Tailwind CSS processing – Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
|
||||
|
||||
And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
|
||||
And with [Hugo Modules][], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
|
||||
|
||||
See the [features] section of the documentation for a comprehensive summary of Hugo's capabilities.
|
||||
See the [features][] section of the documentation for a comprehensive summary of Hugo's capabilities.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<p> </p>
|
||||
<p float="left">
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
|
||||
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
|
||||
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/cloudcannon-cms-logo.svg" width="200" alt="CloudCannon"></a>
|
||||
@@ -74,53 +79,63 @@ See the [features] section of the documentation for a comprehensive summary of H
|
||||
|
||||
## Editions
|
||||
|
||||
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
|
||||
Hugo is available in several editions. Use the standard edition unless you need additional features.
|
||||
|
||||
Feature|extended edition|extended/deploy edition
|
||||
:--|:-:|:-:
|
||||
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See [details].|:x:|:heavy_check_mark:
|
||||
Feature|standard|deploy|extended|extended/deploy
|
||||
:--|:-:|:-:|:-:|:-:
|
||||
Core features|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:
|
||||
Direct cloud deployment (1)|:x:|:heavy_check_mark:|:x:|:heavy_check_mark:
|
||||
LibSass support (2)|:x:|:x:|:heavy_check_mark:|:heavy_check_mark:
|
||||
|
||||
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
|
||||
[processing images]: https://gohugo.io/content-management/image-processing/
|
||||
[transpile sass to css]: https://gohugo.io/functions/css/sass/
|
||||
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
|
||||
(1) Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See [details][].
|
||||
|
||||
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
|
||||
(2) [Transpile Sass to CSS][] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass][] transpiler instead, which is compatible with any edition.
|
||||
|
||||
## Installation
|
||||
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
Install Hugo from a [prebuilt binary][], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
|
||||
- [macOS]
|
||||
- [Linux]
|
||||
- [Windows]
|
||||
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD]
|
||||
- [macOS][]
|
||||
- [Linux][]
|
||||
- [Windows][]
|
||||
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD][]
|
||||
|
||||
## Build from source
|
||||
|
||||
Prerequisites to build Hugo from source:
|
||||
To build Hugo from source you must install:
|
||||
|
||||
- Standard edition: Go 1.24.0 or later
|
||||
- Extended edition: Go 1.24.0 or later, and GCC
|
||||
- Extended/deploy edition: Go 1.24.0 or later, and GCC
|
||||
1. [Git][]
|
||||
1. [Go][] version 1.25.0 or later
|
||||
|
||||
Build the standard edition:
|
||||
### Standard edition
|
||||
|
||||
```text
|
||||
go install github.com/gohugoio/hugo@latest
|
||||
To build and install the standard edition:
|
||||
|
||||
```sh
|
||||
CGO_ENABLED=0 go install github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
Build the extended edition:
|
||||
### Deploy edition
|
||||
|
||||
```text
|
||||
To build and install the deploy edition:
|
||||
|
||||
```sh
|
||||
CGO_ENABLED=0 go install -tags withdeploy github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
### Extended edition
|
||||
|
||||
To build and install the extended edition, first install a C compiler such as [GCC][] or [Clang][] and then run the following command.
|
||||
|
||||
```sh
|
||||
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
Build the extended/deploy edition:
|
||||
### Extended/deploy edition
|
||||
|
||||
```text
|
||||
To build and install the extended/deploy edition, first install a C compiler such as [GCC][] or [Clang][] and then run the following command.
|
||||
|
||||
```sh
|
||||
CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
@@ -130,38 +145,48 @@ CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@late
|
||||
|
||||
## Documentation
|
||||
|
||||
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
|
||||
Hugo's [documentation][] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
|
||||
|
||||
Please submit documentation issues and pull requests to the [documentation repository].
|
||||
Please submit documentation issues and pull requests to the [documentation repository][].
|
||||
|
||||
## Support
|
||||
|
||||
Please **do not use the issue queue** for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the [forum].
|
||||
Please **do not use the issue queue** for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the [forum][].
|
||||
|
||||
Hugo’s [forum] is an active community of users and developers who answer questions, share knowledge, and provide examples. A quick search of over 20,000 topics will often answer your question. Please be sure to read about [requesting help] before asking your first question.
|
||||
Hugo's [forum][] is an active community of users and developers who answer questions, share knowledge, and provide examples. A quick search of over 20,000 topics will often answer your question. Please be sure to read about [requesting help][] before asking your first question.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can contribute to the Hugo project by:
|
||||
|
||||
- Answering questions on the [forum]
|
||||
- Improving the [documentation]
|
||||
- Monitoring the [issue queue]
|
||||
- Creating or improving [themes]
|
||||
- Squashing [bugs]
|
||||
- Answering questions on the [forum][]
|
||||
- Improving the [documentation][]
|
||||
- Monitoring the [issue queue][]
|
||||
- Creating or improving [themes][]
|
||||
- Squashing [bugs][]
|
||||
|
||||
Please submit documentation issues and pull requests to the [documentation repository].
|
||||
Please submit documentation issues and pull requests to the [documentation repository][].
|
||||
|
||||
If you have an idea for an enhancement or new feature, create a new topic on the [forum] in the "Feature" category. This will help you to:
|
||||
If you have an idea for an enhancement or new feature, create a new topic on the [forum][] in the "Feature" category. This will help you to:
|
||||
|
||||
- Determine if the capability already exists
|
||||
- Measure interest
|
||||
- Refine the concept
|
||||
|
||||
If there is sufficient interest, [create a proposal]. Do not submit a pull request until the project lead accepts the proposal.
|
||||
If there is sufficient interest, [create a proposal][]. Do not submit a pull request until the project lead accepts the proposal.
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
For the Hugo source code, see [LICENSE](/LICENSE).
|
||||
|
||||
We also bundle some libraries in binary/WASM form:
|
||||
|
||||
- [libwebp](https://github.com/webmproject/libwebp), [BSD-3-Clause license](https://github.com/webmproject/libwebp?tab=BSD-3-Clause-1-ov-file#readme)
|
||||
- [Katex](https://github.com/KaTeX/KaTeX), [MIT license](https://github.com/KaTeX/KaTeX?tab=MIT-1-ov-file#readme)
|
||||
- [QuickJS](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme), [License](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme)
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
|
||||
@@ -171,51 +196,55 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
|
||||
|
||||
```text
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/PuerkitoBio/goquery="v1.10.1"
|
||||
github.com/alecthomas/chroma/v2="v2.15.0"
|
||||
github.com/andybalholm/cascadia="v1.3.3"
|
||||
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
|
||||
github.com/JohannesKaufmann/dom="v0.2.0"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.21.1"
|
||||
github.com/aymerick/douceur="v0.2.0"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.6.0"
|
||||
github.com/bep/gitmap="v1.9.0"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.3.2"
|
||||
github.com/bep/godartsass/v2="v2.5.0"
|
||||
github.com/bep/golibsass="v1.2.0"
|
||||
github.com/bep/gowebp="v0.3.0"
|
||||
github.com/bep/imagemeta="v0.8.4"
|
||||
github.com/bep/lazycache="v0.7.0"
|
||||
github.com/bep/goportabletext="v0.1.0"
|
||||
github.com/bep/helpers="v0.6.0"
|
||||
github.com/bep/imagemeta="v0.12.0"
|
||||
github.com/bep/lazycache="v0.8.0"
|
||||
github.com/bep/logg="v0.4.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.9.2"
|
||||
github.com/bep/simplecobra="v0.5.0"
|
||||
github.com/bep/overlayfs="v0.10.0"
|
||||
github.com/bep/simplecobra="v0.6.1"
|
||||
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/bits-and-blooms/bitset="v1.24.4"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.4"
|
||||
github.com/clipperhouse/displaywidth="v0.6.0"
|
||||
github.com/clipperhouse/stringish="v0.1.1"
|
||||
github.com/clipperhouse/uax29/v2="v2.3.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.6"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.11.5"
|
||||
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
|
||||
github.com/evanw/esbuild="v0.24.2"
|
||||
github.com/evanw/esbuild="v0.27.2"
|
||||
github.com/fatih/color="v1.18.0"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.8.0"
|
||||
github.com/getkin/kin-openapi="v0.129.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/fsnotify/fsnotify="v1.9.0"
|
||||
github.com/getkin/kin-openapi="v0.133.0"
|
||||
github.com/go-openapi/jsonpointer="v0.21.0"
|
||||
github.com/go-openapi/swag="v0.23.0"
|
||||
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible"
|
||||
github.com/gobuffalo/flect="v1.0.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
|
||||
github.com/gohugoio/hashstructure="v0.5.0"
|
||||
github.com/gohugoio/httpcache="v0.7.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
|
||||
github.com/goccy/go-yaml="v1.19.1"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20251018145728-cfcc22d823c6"
|
||||
github.com/gohugoio/go-radix="v1.2.0"
|
||||
github.com/gohugoio/hashstructure="v0.6.0"
|
||||
github.com/gohugoio/httpcache="v0.8.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.5.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.1"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
|
||||
github.com/google/go-cmp="v0.6.0"
|
||||
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941"
|
||||
github.com/google/go-cmp="v0.7.0"
|
||||
github.com/gorilla/css="v1.0.1"
|
||||
github.com/gorilla/websocket="v1.5.3"
|
||||
github.com/hairyhenderson/go-codeowners="v0.7.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.7"
|
||||
@@ -224,58 +253,57 @@ github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.13"
|
||||
github.com/lucasb-eyer/go-colorful="v1.2.0"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/makeworld-the-better-one/dither/v2="v2.4.0"
|
||||
github.com/marekm4/color-extractor="v1.2.1"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.20"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mazznoer/csscolorparser="v0.1.5"
|
||||
github.com/mattn/go-runewidth="v0.0.19"
|
||||
github.com/microcosm-cc/bluemonday="v1.0.27"
|
||||
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
|
||||
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.7.0"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
|
||||
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/niklasfasching/go-org="v1.9.1"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
|
||||
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
|
||||
github.com/olekukonko/cat="v0.0.0-20250911104152-50322a0618f6"
|
||||
github.com/olekukonko/errors="v1.1.0"
|
||||
github.com/olekukonko/ll="v0.1.3"
|
||||
github.com/olekukonko/tablewriter="v1.1.2"
|
||||
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
|
||||
github.com/pelletier/go-toml/v2="v2.2.3"
|
||||
github.com/pelletier/go-toml/v2="v2.2.4"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rivo/uniseg="v0.4.7"
|
||||
github.com/rogpeppe/go-internal="v1.13.1"
|
||||
github.com/rogpeppe/go-internal="v1.14.1"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/sass/libsass="3.6.6"
|
||||
github.com/spf13/afero="v1.11.0"
|
||||
github.com/spf13/cast="v1.7.1"
|
||||
github.com/spf13/cobra="v1.8.1"
|
||||
github.com/sass/dart-sass/compiler="1.97.1"
|
||||
github.com/sass/dart-sass/implementation="1.97.1"
|
||||
github.com/sass/dart-sass/protocol="3.2.0"
|
||||
github.com/spf13/afero="v1.15.0"
|
||||
github.com/spf13/cast="v1.10.0"
|
||||
github.com/spf13/cobra="v1.10.2"
|
||||
github.com/spf13/fsync="v0.10.1"
|
||||
github.com/spf13/pflag="v1.0.6"
|
||||
github.com/tdewolff/minify/v2="v2.20.37"
|
||||
github.com/tdewolff/parse/v2="v2.7.15"
|
||||
github.com/tetratelabs/wazero="v1.8.2"
|
||||
github.com/webmproject/libwebp="v1.3.2"
|
||||
github.com/yuin/goldmark-emoji="v1.0.4"
|
||||
github.com/yuin/goldmark="v1.7.8"
|
||||
github.com/spf13/pflag="v1.0.9"
|
||||
github.com/tdewolff/minify/v2="v2.24.8"
|
||||
github.com/tdewolff/parse/v2="v2.8.5"
|
||||
github.com/tetratelabs/wazero="v1.10.1"
|
||||
github.com/webmproject/libwebp="v1.6.0"
|
||||
github.com/woodsbury/decimal128="v1.3.0"
|
||||
github.com/yuin/goldmark-emoji="v1.0.6"
|
||||
github.com/yuin/goldmark="v1.7.13"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
golang.org/x/crypto="v0.33.0"
|
||||
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac"
|
||||
golang.org/x/image="v0.24.0"
|
||||
golang.org/x/mod="v0.23.0"
|
||||
golang.org/x/net="v0.35.0"
|
||||
golang.org/x/sync="v0.11.0"
|
||||
golang.org/x/sys="v0.30.0"
|
||||
golang.org/x/text="v0.22.0"
|
||||
golang.org/x/tools="v0.30.0"
|
||||
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
|
||||
gonum.org/v1/plot="v0.15.0"
|
||||
google.golang.org/protobuf="v1.36.5"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
go.yaml.in/yaml/v3="v3.0.4"
|
||||
golang.org/x/crypto="v0.46.0"
|
||||
golang.org/x/image="v0.34.0"
|
||||
golang.org/x/mod="v0.31.0"
|
||||
golang.org/x/net="v0.48.0"
|
||||
golang.org/x/sync="v0.19.0"
|
||||
golang.org/x/sys="v0.39.0"
|
||||
golang.org/x/text="v0.32.0"
|
||||
golang.org/x/tools="v0.40.0"
|
||||
google.golang.org/protobuf="v1.36.10"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
oss.terrastruct.com/d2="v0.6.9"
|
||||
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
|
||||
rsc.io/qr="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
|
||||
+6
-2
@@ -1,7 +1,11 @@
|
||||
## Security Policy
|
||||
|
||||
### Before You Report
|
||||
|
||||
Please read [Hugo's Security Model](https://gohugo.io/about/security/) first. If the issue reproduces in an upstream project, please report it there — we cannot triage or patch on their behalf.
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
|
||||
If, after the above, you believe you have found a vulnerability in Hugo itself with a concrete, reproducible impact, report it privately to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. Include a minimal reproducer, the Hugo version, and the observed vs. expected behavior.
|
||||
|
||||
Also see [Hugo's Security Model](https://gohugo.io/about/security/).
|
||||
You should receive an initial response within a few days. Confirmed issues are typically patched within days, depending on complexity.
|
||||
|
||||
Vendored
+2
-2
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
|
||||
|
||||
infol := opts.Log.InfoCommand("dynacache")
|
||||
|
||||
evictedIdentities := collections.NewStack[KeyIdentity]()
|
||||
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
|
||||
|
||||
onEvict := func(k, v any) {
|
||||
if !opts.Watching {
|
||||
@@ -129,7 +129,7 @@ type Cache struct {
|
||||
partitions map[string]PartitionManager
|
||||
|
||||
onEvict func(k, v any)
|
||||
evictedIdentities *collections.Stack[KeyIdentity]
|
||||
evictedIdentities *collections.StackThreadSafe[KeyIdentity]
|
||||
|
||||
opts Options
|
||||
infol logg.LevelLogger
|
||||
|
||||
Vendored
+142
-59
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@ package filecache
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -45,16 +46,12 @@ const (
|
||||
type Cache struct {
|
||||
Fs afero.Fs
|
||||
|
||||
// Max age for items in this cache. Negative duration means forever,
|
||||
// 0 is effectively turning this cache off.
|
||||
maxAge time.Duration
|
||||
cfg FileCacheConfig
|
||||
|
||||
// When set, we just remove this entire root directory on expiration.
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
entryLocker *lockTracker
|
||||
|
||||
initOnce sync.Once
|
||||
isInited bool
|
||||
initErr error
|
||||
}
|
||||
|
||||
@@ -88,12 +85,15 @@ type ItemInfo struct {
|
||||
}
|
||||
|
||||
// NewCache creates a new file cache with the given filesystem and max age.
|
||||
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
|
||||
func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
|
||||
if err := cfg.init(); err != nil {
|
||||
panic(fmt.Sprintf("invalid cache config: %s", err))
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
Fs: fs,
|
||||
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
maxAge: maxAge,
|
||||
pruneAllRootDir: pruneAllRootDir,
|
||||
Fs: fs,
|
||||
entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +109,15 @@ func (l *lockedFile) Close() error {
|
||||
}
|
||||
|
||||
func (c *Cache) init() error {
|
||||
if c == nil {
|
||||
panic("cache is nil")
|
||||
}
|
||||
|
||||
c.initOnce.Do(func() {
|
||||
c.isInited = true
|
||||
// Create the base dir if it does not exist.
|
||||
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
|
||||
err = fmt.Errorf("failled to create base cache directory: %s", err)
|
||||
c.initErr = err
|
||||
}
|
||||
})
|
||||
@@ -126,19 +132,19 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
c.nlocker.Unlock(id)
|
||||
c.entryLocker.Unlock(id)
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
return info, &lockedFile{
|
||||
File: f,
|
||||
unlock: func() { c.nlocker.Unlock(id) },
|
||||
unlock: func() { c.entryLocker.Unlock(id) },
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -156,8 +162,8 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info = ItemInfo{Name: id}
|
||||
|
||||
@@ -186,9 +192,9 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
// NamedLock locks the given id. The lock is released when the returned function is called.
|
||||
func (c *Cache) NamedLock(id string) func() {
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
return func() {
|
||||
c.nlocker.Unlock(id)
|
||||
c.entryLocker.Unlock(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,8 +207,8 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -220,7 +226,7 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching.
|
||||
return info, hugio.ToReadCloser(r), nil
|
||||
}
|
||||
@@ -231,6 +237,40 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
c.writeReader(id, io.TeeReader(r, &buff))
|
||||
}
|
||||
|
||||
// AbsFilenameFromID returns the filename for the given id in the cache.
|
||||
// This will be an absolute path.
|
||||
func (c *Cache) AbsFilenameFromID(id string) string {
|
||||
return filepath.Join(c.cfg.DirCompiled, cleanID(id))
|
||||
}
|
||||
|
||||
// GetOrCreateInfo tries to get the item info with the given id from cache. If not found or expired, create will
|
||||
// be invoked with the id. The create function is expected to create the cache item with the given id. The returned ItemInfo will have the id as Name.
|
||||
// This method is protected by a named lock using the given id as identifier.
|
||||
func (c *Cache) GetOrCreateInfo(id string, create func(id string) error) (ItemInfo, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, err
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if !c.removeIfNeeded(id) {
|
||||
// The file exists and is not expired, so we consider it a cache hit.
|
||||
return info, nil
|
||||
}
|
||||
|
||||
if err := create(id); err != nil {
|
||||
c.remove(id)
|
||||
return info, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *Cache) writeReader(id string, r io.Reader) error {
|
||||
dir := filepath.Dir(id)
|
||||
if dir != "" {
|
||||
@@ -254,8 +294,8 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -275,7 +315,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
@@ -286,15 +326,39 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
// SetBytes sets the file content with the given id in the cache.
|
||||
func (c *Cache) SetBytes(id string, data []byte) error {
|
||||
if err := c.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.writeReader(id, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
func (c *Cache) GetBytes(id string) ([]byte, error) {
|
||||
_, b, err := c.GetItemBytes(id)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// GetItemBytes gets the ItemInfo and file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetItemBytes(id string) (ItemInfo, []byte, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -314,8 +378,8 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
@@ -324,10 +388,23 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
// removeIfNeeded checks if the file with the given id should be re-created.
|
||||
func (c *Cache) removeIfNeeded(id string) bool {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching, remove.
|
||||
c.remove(id)
|
||||
return true
|
||||
}
|
||||
if removed, err := c.removeIfExpired(id); err != nil || removed {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getOrRemove gets the file with the given id. If it's expired, it will
|
||||
// be removed.
|
||||
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
if c.maxAge == 0 {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
@@ -345,7 +422,7 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
}
|
||||
|
||||
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
|
||||
if c.maxAge == 0 {
|
||||
if c.cfg.MaxAge == 0 {
|
||||
// No caching.
|
||||
return nil, false
|
||||
}
|
||||
@@ -370,7 +447,7 @@ func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
|
||||
}
|
||||
|
||||
func (c *Cache) removeIfExpired(id string) (bool, error) {
|
||||
if c.maxAge <= 0 {
|
||||
if c.cfg.MaxAge <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -380,29 +457,37 @@ func (c *Cache) removeIfExpired(id string) (bool, error) {
|
||||
}
|
||||
|
||||
if c.isExpired(fi.ModTime()) {
|
||||
c.Fs.Remove(id)
|
||||
c.remove(id)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *Cache) remove(id string) {
|
||||
if c.cfg.entryIsDir {
|
||||
c.Fs.RemoveAll(id)
|
||||
} else {
|
||||
c.Fs.Remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) isExpired(modTime time.Time) bool {
|
||||
if c.maxAge < 0 {
|
||||
if c.cfg.MaxAge < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Note the use of time.Since here.
|
||||
// We cannot use Hugo's global Clock for this.
|
||||
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
|
||||
return c.cfg.MaxAge == 0 || time.Since(modTime) > c.cfg.MaxAge
|
||||
}
|
||||
|
||||
// For testing
|
||||
func (c *Cache) GetString(id string) string {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
c.entryLocker.Lock(id)
|
||||
defer c.entryLocker.Unlock(id)
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
if err != nil {
|
||||
@@ -417,6 +502,17 @@ func (c *Cache) GetString(id string) string {
|
||||
// Caches is a named set of caches.
|
||||
type Caches map[string]*Cache
|
||||
|
||||
func (f Caches) SetResourceFs(fs afero.Fs) {
|
||||
for _, c := range f {
|
||||
if c.cfg.IsResourceDir {
|
||||
if c.isInited {
|
||||
panic("cannot set resource fs after init")
|
||||
}
|
||||
c.Fs = hugofs.NewBasePathFs(fs, c.cfg.DirCompiled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets a named cache, nil if none found.
|
||||
func (f Caches) Get(name string) *Cache {
|
||||
return f[strings.ToLower(name)]
|
||||
@@ -424,34 +520,21 @@ func (f Caches) Get(name string) *Cache {
|
||||
|
||||
// NewCaches creates a new set of file caches from the given
|
||||
// configuration.
|
||||
func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
|
||||
fs := p.Fs.Source
|
||||
func NewCaches(dcfg Configs, sourceFs afero.Fs) (Caches, error) {
|
||||
fs := sourceFs
|
||||
|
||||
m := make(Caches)
|
||||
for k, v := range dcfg {
|
||||
var cfs afero.Fs
|
||||
|
||||
if v.IsResourceDir {
|
||||
cfs = p.BaseFs.ResourcesCache
|
||||
cfs = nil // Set later. TODO(bep) this needs to be cleanded up.
|
||||
} else {
|
||||
cfs = fs
|
||||
cfs = hugofs.NewBasePathFs(fs, v.DirCompiled)
|
||||
}
|
||||
|
||||
if cfs == nil {
|
||||
panic("nil fs")
|
||||
}
|
||||
c := NewCache(cfs, v)
|
||||
|
||||
baseDir := v.DirCompiled
|
||||
|
||||
bfs := hugofs.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
var pruneAllRootDir string
|
||||
if k == CacheKeyModules {
|
||||
pruneAllRootDir = "pkg"
|
||||
}
|
||||
|
||||
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
|
||||
m[k] = c
|
||||
}
|
||||
|
||||
return m, nil
|
||||
@@ -480,7 +563,7 @@ func (h *httpCache) Get(id string) (resp []byte, ok bool) {
|
||||
}
|
||||
|
||||
func (h *httpCache) Set(id string, resp []byte) {
|
||||
if h.c.maxAge == 0 {
|
||||
if h.c.cfg.MaxAge == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Vendored
+103
-33
@@ -15,6 +15,7 @@
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -34,35 +35,50 @@ const (
|
||||
cacheDirProject = ":cacheDir/:project"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = FileCacheConfig{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: cacheDirProject,
|
||||
}
|
||||
|
||||
const (
|
||||
CacheKeyGetJSON = "getjson"
|
||||
CacheKeyGetCSV = "getcsv"
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyGetResource = "getresource"
|
||||
CacheKeyMisc = "misc"
|
||||
CacheKeyImages = "images"
|
||||
CacheKeyAssets = "assets"
|
||||
CacheKeyModules = "modules"
|
||||
CacheKeyModuleQueries = "modulequeries"
|
||||
CacheKeyModuleGitInfo = "modulegitinfo"
|
||||
CacheKeyGetResource = "getresource"
|
||||
CacheKeyMisc = "misc"
|
||||
)
|
||||
|
||||
type Configs map[string]FileCacheConfig
|
||||
|
||||
// CacheDirModules returns the compiled path to the modules cache.
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirModules() string {
|
||||
return c[CacheKeyModules].DirCompiled
|
||||
}
|
||||
|
||||
// CacheDirMisc returns the compiled path to the misc cache.
|
||||
// For internal use.
|
||||
func (c Configs) CacheDirMisc() string {
|
||||
return c[CacheKeyMisc].DirCompiled
|
||||
}
|
||||
|
||||
var defaultCacheConfigs = Configs{
|
||||
CacheKeyModules: {
|
||||
MaxAge: -1,
|
||||
Dir: ":cacheDir/modules",
|
||||
fileCacheConfigInternal: fileCacheConfigInternal{
|
||||
entryIsDir: true,
|
||||
isReadOnly: true, // we need to make it writable when pruning.
|
||||
},
|
||||
},
|
||||
CacheKeyModuleQueries: {
|
||||
MaxAge: 24 * time.Hour,
|
||||
Dir: ":cacheDir/modules",
|
||||
},
|
||||
CacheKeyModuleGitInfo: {
|
||||
MaxAge: 24 * time.Hour,
|
||||
Dir: ":cacheDir/modules",
|
||||
fileCacheConfigInternal: fileCacheConfigInternal{
|
||||
entryIsDir: true,
|
||||
},
|
||||
},
|
||||
CacheKeyGetJSON: defaultCacheConfig,
|
||||
CacheKeyGetCSV: defaultCacheConfig,
|
||||
CacheKeyImages: {
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
@@ -81,6 +97,13 @@ var defaultCacheConfigs = Configs{
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for k, v := range defaultCacheConfigs {
|
||||
v.name = k
|
||||
defaultCacheConfigs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
type FileCacheConfig struct {
|
||||
// Max age of cache entries in this cache. Any items older than this will
|
||||
// be removed and not returned from the cache.
|
||||
@@ -92,22 +115,50 @@ type FileCacheConfig struct {
|
||||
MaxAge time.Duration
|
||||
|
||||
// The directory where files are stored.
|
||||
Dir string
|
||||
DirCompiled string `json:"-"`
|
||||
Dir string
|
||||
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
IsResourceDir bool `json:"-"`
|
||||
fileCacheConfigInternal `json:"-"`
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[CacheKeyGetJSON]
|
||||
func (cfg *FileCacheConfig) init() error {
|
||||
if cfg.DirCompiled == "" {
|
||||
// From unit tests. Just check that it does not contain any placeholders.
|
||||
if strings.Contains(cfg.Dir, ":") {
|
||||
return fmt.Errorf("cache dir %q contains unresolved placeholders", cfg.Dir)
|
||||
}
|
||||
cfg.DirCompiled = cfg.Dir
|
||||
}
|
||||
// Sanity check the config.
|
||||
if len(cfg.DirCompiled) < 5 {
|
||||
panic(fmt.Sprintf("invalid cache dir: %q", cfg.DirCompiled))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCSVCache gets the file cache for getCSV.
|
||||
func (f Caches) GetCSVCache() *Cache {
|
||||
return f[CacheKeyGetCSV]
|
||||
type fileCacheConfigInternal struct {
|
||||
DirCompiled string
|
||||
|
||||
name string // The name of this cache, e.g. "images", "modules" etc.
|
||||
entryIsDir bool // when set, the cache entries represents directories directly below the base dir.
|
||||
isReadOnly bool // when set, the cache is read only and needs to be pruned differently. This is used for the Go modules cache.
|
||||
IsResourceDir bool // resources/_gen will get its own composite filesystem that also checks any theme. TODO(bep) unexport this.
|
||||
}
|
||||
|
||||
// MarshalJSON marshals FileCacheConfig to JSON with MaxAge as a human-readable string.
|
||||
func (c FileCacheConfig) MarshalJSON() ([]byte, error) {
|
||||
var maxAge any
|
||||
if c.MaxAge == -1 {
|
||||
maxAge = -1
|
||||
} else {
|
||||
maxAge = strings.TrimSuffix(c.MaxAge.String(), "0m0s")
|
||||
}
|
||||
return json.Marshal(&struct {
|
||||
MaxAge any `json:"maxAge"`
|
||||
Dir string `json:"dir"`
|
||||
}{
|
||||
MaxAge: maxAge,
|
||||
Dir: c.Dir,
|
||||
})
|
||||
}
|
||||
|
||||
// ImageCache gets the file cache for processed images.
|
||||
@@ -120,6 +171,25 @@ func (f Caches) ModulesCache() *Cache {
|
||||
return f[CacheKeyModules]
|
||||
}
|
||||
|
||||
// ModuleQueriesCache gets the file cache for Hugo Module version queries.
|
||||
// Returns nil if not found.
|
||||
func (f Caches) ModuleQueriesCache() *Cache {
|
||||
c, ok := f[CacheKeyModuleQueries]
|
||||
if !ok {
|
||||
panic("module queries cache not set")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ModuleGitInfoCache gets the file cache for Hugo Module git info.
|
||||
func (f Caches) ModuleGitInfoCache() *Cache {
|
||||
c, ok := f[CacheKeyModuleGitInfo]
|
||||
if !ok {
|
||||
panic("module git info cache not set")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[CacheKeyAssets]
|
||||
@@ -147,10 +217,14 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
if _, ok := v.(maps.Params); !ok {
|
||||
if _, ok := v.(hmaps.Params); !ok {
|
||||
continue
|
||||
}
|
||||
cc := defaultCacheConfig
|
||||
var ok bool
|
||||
cc, ok := c[k]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not a valid cache name", k)
|
||||
}
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
Result: &cc,
|
||||
@@ -171,12 +245,8 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
return c, errors.New("must provide cache Dir")
|
||||
}
|
||||
|
||||
name := strings.ToLower(k)
|
||||
if !valid[name] {
|
||||
return nil, fmt.Errorf("%q is not a valid cache name", name)
|
||||
}
|
||||
c[k] = cc
|
||||
|
||||
c[name] = cc
|
||||
}
|
||||
|
||||
for k, v := range c {
|
||||
|
||||
+39
-14
@@ -14,6 +14,7 @@
|
||||
package filecache_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -43,12 +44,9 @@ assetDir = "assets"
|
||||
archetypeDir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
[caches.misc]
|
||||
maxAge = "10m"
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = "11h"
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
[caches.getResource]
|
||||
@@ -61,9 +59,9 @@ dir = "/path/to/c4"
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
|
||||
c2 := decoded["getcsv"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
|
||||
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
|
||||
c2 := decoded["misc"]
|
||||
c.Assert(c2.MaxAge.String(), qt.Equals, "10m0s")
|
||||
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c1/filecache/misc"))
|
||||
|
||||
c3 := decoded["images"]
|
||||
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
|
||||
@@ -90,12 +88,9 @@ archeTypedir = "archetypes"
|
||||
|
||||
ignoreCache = true
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
[caches.misc]
|
||||
maxAge = 1234
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = 3456
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
[caches.getResource]
|
||||
@@ -132,15 +127,45 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
c.Assert(len(decoded), qt.Equals, 7)
|
||||
|
||||
imgConfig := decoded[filecache.CacheKeyImages]
|
||||
jsonConfig := decoded[filecache.CacheKeyGetJSON]
|
||||
miscConfig := decoded[filecache.CacheKeyMisc]
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
|
||||
} else {
|
||||
c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images")
|
||||
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
|
||||
c.Assert(miscConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/misc")
|
||||
}
|
||||
|
||||
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
|
||||
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
|
||||
c.Assert(miscConfig.IsResourceDir, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestFileCacheConfigMarshalJSON(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Set("cacheDir", "/cache")
|
||||
cfg.Set("workingDir", "/my/project")
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
|
||||
|
||||
moduleQueriesConfig := decoded[filecache.CacheKeyModuleQueries]
|
||||
c.Assert(moduleQueriesConfig.MaxAge, qt.Equals, 24*time.Hour)
|
||||
|
||||
// Also verify the new moduleGitInfo cache.
|
||||
moduleGitInfoConfig := decoded[filecache.CacheKeyModuleGitInfo]
|
||||
c.Assert(moduleGitInfoConfig.MaxAge, qt.Equals, 24*time.Hour)
|
||||
|
||||
b, err := json.Marshal(moduleQueriesConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(string(b), qt.Contains, `"maxAge":"24h"`)
|
||||
c.Assert(string(b), qt.Not(qt.Contains), "86400000000000")
|
||||
c.Assert(string(b), qt.Not(qt.Contains), "8.64e")
|
||||
|
||||
moduleQueriesConfig.MaxAge = -1
|
||||
b, err = json.Marshal(moduleQueriesConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Contains, `"maxAge":-1`)
|
||||
}
|
||||
|
||||
+7
-8
@@ -18,7 +18,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
@@ -39,9 +38,9 @@ title: "Home"
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files, RunGC: true, NeedsOsFS: true},
|
||||
).Build()
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithConfig(func(c *hugolib.IntegrationTestConfig) {
|
||||
c.RunGC = true
|
||||
}))
|
||||
|
||||
_, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images"))
|
||||
|
||||
@@ -67,7 +66,7 @@ title: "Home"
|
||||
---
|
||||
-- assets/a/pixel.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
{{ warnf "HOME!" }}
|
||||
{{ $img := resources.GetMatch "**.png" }}
|
||||
{{ $img = $img.Resize "3x3" }}
|
||||
@@ -77,9 +76,9 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: logg.LevelInfo},
|
||||
).Build()
|
||||
b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs(), hugolib.TestOptInfo(), hugolib.TestOptWithConfig(func(c *hugolib.IntegrationTestConfig) {
|
||||
c.RunGC = true
|
||||
}))
|
||||
|
||||
b.Assert(b.GCCount, qt.Equals, 0)
|
||||
b.Assert(b.H, qt.IsNotNil)
|
||||
|
||||
Vendored
+43
-7
@@ -50,8 +50,8 @@ func (c Caches) Prune() (int, error) {
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// If force is set, everything will be removed not considering expiry time.
|
||||
func (c *Cache) Prune(force bool) (int, error) {
|
||||
if c.pruneAllRootDir != "" {
|
||||
return c.pruneRootDir(force)
|
||||
if c.cfg.entryIsDir {
|
||||
return c.pruneRootDirs(force)
|
||||
}
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
@@ -93,9 +93,9 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
shouldRemove := force || c.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && len(c.nlocker.seen) > 0 {
|
||||
if !shouldRemove && len(c.entryLocker.seen) > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
_, seen := c.nlocker.seen[name]
|
||||
_, seen := c.entryLocker.seen[name]
|
||||
shouldRemove = !seen
|
||||
}
|
||||
|
||||
@@ -117,11 +117,43 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
return counter, err
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
func (c *Cache) pruneRootDirs(force bool) (int, error) {
|
||||
dirs, err := afero.ReadDir(c.Fs, "")
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
||||
for _, dir := range dirs {
|
||||
if !dir.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
count, err := c.pruneRootDir(dir.Name(), force)
|
||||
if err != nil {
|
||||
return counter, err
|
||||
}
|
||||
counter += count
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
|
||||
// Sanity check.
|
||||
if dirname != "pkg" && len(dirname) < 5 {
|
||||
panic(fmt.Sprintf("invalid cache dir name: %q", dirname))
|
||||
}
|
||||
|
||||
info, err := c.Fs.Stat(dirname)
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
return 0, nil
|
||||
@@ -133,5 +165,9 @@ func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
|
||||
if c.cfg.isReadOnly {
|
||||
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, dirname)
|
||||
}
|
||||
|
||||
return 1, c.Fs.RemoveAll(dirname)
|
||||
}
|
||||
|
||||
+9
-8
@@ -39,12 +39,9 @@ assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
[caches.misc]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/c"
|
||||
[caches.getcsv]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/d"
|
||||
[caches.assets]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
@@ -53,11 +50,14 @@ maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
for _, name := range []string{filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := filecache.NewCaches(p)
|
||||
fs := afero.NewMemMapFs()
|
||||
p := newPathsSpec(t, fs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, fs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache := caches[name]
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
@@ -84,8 +84,9 @@ dir = ":resourceDir/_gen"
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = filecache.NewCaches(p)
|
||||
caches, err = filecache.NewCaches(fileCachConfig, fs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
|
||||
Vendored
+21
-12
@@ -22,6 +22,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -64,7 +66,7 @@ assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
[caches.misc]
|
||||
maxAge = "10h"
|
||||
dir = ":cacheDir/c"
|
||||
|
||||
@@ -78,11 +80,13 @@ dir = ":cacheDir/c"
|
||||
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
|
||||
|
||||
p := newPathsSpec(t, osfs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
|
||||
caches, err := filecache.NewCaches(p)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(p.SourceFs)
|
||||
|
||||
cache := caches.Get("GetJSON")
|
||||
cache := caches.Get("Misc")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
|
||||
cache = caches.Get("Images")
|
||||
@@ -104,7 +108,7 @@ dir = ":cacheDir/c"
|
||||
return []byte("bcd"), nil
|
||||
}
|
||||
|
||||
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache()} {
|
||||
for range 2 {
|
||||
info, r, err := ca.GetOrCreate("a", rf("abc"))
|
||||
c.Assert(err, qt.IsNil)
|
||||
@@ -132,8 +136,6 @@ dir = ":cacheDir/c"
|
||||
}
|
||||
}
|
||||
|
||||
c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil))
|
||||
|
||||
info, w, err := caches.ImageCache().WriteCloser("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
@@ -149,7 +151,7 @@ dir = ":cacheDir/c"
|
||||
r.Close()
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
|
||||
info, b, err = caches.ImageCache().GetBytes("mykey")
|
||||
info, b, err = caches.ImageCache().GetItemBytes("mykey")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(info.Name, qt.Equals, "mykey")
|
||||
c.Assert(string(b), qt.Equals, "Hugo is great!")
|
||||
@@ -158,6 +160,7 @@ dir = ":cacheDir/c"
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
htesting.SkipSlowTestUnlessCI(t)
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
@@ -172,18 +175,19 @@ assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
[caches.misc]
|
||||
maxAge = "1s"
|
||||
dir = "/cache/c"
|
||||
|
||||
`
|
||||
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
|
||||
caches, err := filecache.NewCaches(p)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(p.Fs.Source)
|
||||
|
||||
const cacheName = "getjson"
|
||||
const cacheName = "misc"
|
||||
|
||||
filenameData := func(i int) (string, string) {
|
||||
data := fmt.Sprintf("data: %d", i)
|
||||
@@ -248,7 +252,12 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
|
||||
cfg := filecache.FileCacheConfig{
|
||||
MaxAge: 100 * time.Hour,
|
||||
Dir: "cache/c",
|
||||
}
|
||||
|
||||
cache := filecache.NewCache(afero.NewMemMapFs(), cfg)
|
||||
|
||||
const id = "a32"
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Default to all packages if none specified
|
||||
PACKAGES="${1:-./...}"
|
||||
|
||||
echo "==> Checking packages: $PACKAGES"
|
||||
|
||||
# Timing arrays
|
||||
declare -a STEP_NAMES
|
||||
declare -a STEP_TIMES
|
||||
|
||||
time_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
local start=$(date +%s.%N)
|
||||
"$@"
|
||||
local end=$(date +%s.%N)
|
||||
local elapsed=$(echo "$end - $start" | bc)
|
||||
STEP_NAMES+=("$name")
|
||||
STEP_TIMES+=("$elapsed")
|
||||
}
|
||||
|
||||
# Check gofmt
|
||||
run_gofmt() {
|
||||
echo "==> Running gofmt..."
|
||||
# Convert package pattern to path (e.g., ./hugolib/... -> ./hugolib)
|
||||
local path="${PACKAGES%/...}"
|
||||
GOFMT_OUTPUT=$(gofmt -l "$path" 2>&1) || true
|
||||
if [ -n "$GOFMT_OUTPUT" ]; then
|
||||
echo "gofmt found issues in:"
|
||||
echo "$GOFMT_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run staticcheck
|
||||
run_staticcheck() {
|
||||
# Check if staticcheck is installed, install if not
|
||||
if ! command -v staticcheck &> /dev/null; then
|
||||
echo "==> Installing staticcheck..."
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
fi
|
||||
echo "==> Running staticcheck..."
|
||||
staticcheck $PACKAGES
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run tests
|
||||
run_tests() {
|
||||
echo "==> Running tests..."
|
||||
local output
|
||||
if ! output=$(go test -failfast $PACKAGES 2>&1); then
|
||||
echo "$output"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK"
|
||||
}
|
||||
|
||||
# Run all steps with timing
|
||||
TOTAL_START=$(date +%s.%N)
|
||||
|
||||
time_step "gofmt" run_gofmt
|
||||
time_step "staticcheck" run_staticcheck
|
||||
time_step "tests" run_tests
|
||||
|
||||
TOTAL_END=$(date +%s.%N)
|
||||
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
|
||||
|
||||
# Print timing summary
|
||||
echo ""
|
||||
echo "==> All checks passed!"
|
||||
echo ""
|
||||
echo "Timing summary:"
|
||||
echo "---------------"
|
||||
for i in "${!STEP_NAMES[@]}"; do
|
||||
printf " %-15s %6.2fs\n" "${STEP_NAMES[$i]}" "${STEP_TIMES[$i]}"
|
||||
done
|
||||
echo "---------------"
|
||||
printf " %-15s %6.2fs\n" "Total" "$TOTAL_ELAPSED"
|
||||
+1
-1
@@ -73,7 +73,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
nameAndPackage := func(t reflect.Type) (string, string) {
|
||||
var name, pkg string
|
||||
|
||||
isPointer := t.Kind() == reflect.Ptr
|
||||
isPointer := t.Kind() == reflect.Pointer
|
||||
|
||||
if isPointer {
|
||||
t = t.Elem()
|
||||
|
||||
@@ -26,9 +26,9 @@ import (
|
||||
|
||||
func TestMethods(t *testing.T) {
|
||||
var (
|
||||
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
|
||||
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
|
||||
zeroI = reflect.TypeOf((*I)(nil)).Elem()
|
||||
zeroIE = reflect.TypeFor[IEmbed]()
|
||||
zeroIEOnly = reflect.TypeFor[IEOnly]()
|
||||
zeroI = reflect.TypeFor[I]()
|
||||
)
|
||||
|
||||
dir, _ := os.Getwd()
|
||||
|
||||
+46
-14
@@ -39,6 +39,7 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -97,6 +98,7 @@ type commonConfig struct {
|
||||
type configKey struct {
|
||||
counter int32
|
||||
ignoreModulesDoesNotExists bool
|
||||
skipNpmCheck bool
|
||||
}
|
||||
|
||||
// This is the root command.
|
||||
@@ -131,6 +133,7 @@ type rootCommand struct {
|
||||
gc bool
|
||||
poll string
|
||||
forceSyncStatic bool
|
||||
panicOnWarning bool
|
||||
|
||||
// Profile flags (for debugging of performance problems)
|
||||
cpuprofile string
|
||||
@@ -150,6 +153,23 @@ type rootCommand struct {
|
||||
cfgDir string
|
||||
}
|
||||
|
||||
// resolveEnvironment sets r.environment if not already set.
|
||||
// server indicates whether the server command is running (defaults to development).
|
||||
func (r *rootCommand) resolveEnvironment(server bool) {
|
||||
if r.environment != "" {
|
||||
return
|
||||
}
|
||||
if env := os.Getenv("HUGO_ENVIRONMENT"); env != "" {
|
||||
r.environment = env
|
||||
} else if env := os.Getenv("HUGO_ENV"); env != "" {
|
||||
r.environment = env
|
||||
} else if server {
|
||||
r.environment = hugo.EnvironmentDevelopment
|
||||
} else {
|
||||
r.environment = hugo.EnvironmentProduction
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rootCommand) isVerbose() bool {
|
||||
return r.logger.Level() <= logg.LevelInfo
|
||||
}
|
||||
@@ -194,6 +214,7 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
|
||||
Logger: r.logger,
|
||||
Environment: r.environment,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
SkipNpmCheck: key.skipNpmCheck,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -220,6 +241,7 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
|
||||
if cfg == nil {
|
||||
panic("cfg must be set")
|
||||
}
|
||||
r.resolveEnvironment(false)
|
||||
cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
|
||||
var dir string
|
||||
if r.source != "" {
|
||||
@@ -250,6 +272,7 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
|
||||
Environment: r.environment,
|
||||
Logger: r.logger,
|
||||
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
|
||||
SkipNpmCheck: key.skipNpmCheck,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -331,6 +354,9 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
|
||||
}
|
||||
|
||||
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
|
||||
if conf == nil {
|
||||
return nil, fmt.Errorf("conf must be set")
|
||||
}
|
||||
k := configKey{counter: r.configVersionID.Load()}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
depsCfg := r.newDepsConfig(conf)
|
||||
@@ -401,9 +427,7 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
for _, group := range watchGroups {
|
||||
r.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
|
||||
watcher, err := b.newWatcher(r.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -487,12 +511,18 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
if r.panicOnWarning {
|
||||
logHookLast = loggers.PanicOnWarningHook
|
||||
}
|
||||
|
||||
optsLogger := loggers.Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: level,
|
||||
StdOut: r.StdOut,
|
||||
StdErr: r.StdErr,
|
||||
StoreErrors: running,
|
||||
HandlerPost: logHookLast,
|
||||
}
|
||||
|
||||
return loggers.New(optsLogger), nil
|
||||
@@ -519,8 +549,8 @@ func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Com
|
||||
commandName = subCommandName
|
||||
}
|
||||
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
|
||||
cmd.Short = "Build your site"
|
||||
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
|
||||
cmd.Short = "Build your project"
|
||||
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo project.
|
||||
|
||||
Hugo is a Fast and Flexible Static Site Generator
|
||||
built with love by spf13 and friends in Go.
|
||||
@@ -590,7 +620,7 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
|
||||
_ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions)
|
||||
cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.")
|
||||
@@ -621,13 +651,14 @@ func (r *rootCommand) timeTrack(start time.Time, name string) {
|
||||
}
|
||||
|
||||
type simpleCommand struct {
|
||||
use string
|
||||
name string
|
||||
short string
|
||||
long string
|
||||
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
|
||||
withc func(cmd *cobra.Command, r *rootCommand)
|
||||
initc func(cd *simplecobra.Commandeer) error
|
||||
use string
|
||||
name string
|
||||
short string
|
||||
long string
|
||||
aliases []string
|
||||
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
|
||||
withc func(cmd *cobra.Command, r *rootCommand)
|
||||
initc func(cd *simplecobra.Commandeer) error
|
||||
|
||||
commands []simplecobra.Commander
|
||||
|
||||
@@ -654,6 +685,7 @@ func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = c.short
|
||||
cmd.Long = c.long
|
||||
cmd.Aliases = c.aliases
|
||||
if c.use != "" {
|
||||
cmd.Use = c.use
|
||||
}
|
||||
@@ -671,7 +703,7 @@ func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
|
||||
func mapLegacyArgs(args []string) []string {
|
||||
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") {
|
||||
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "project", "site", "theme", "content") {
|
||||
// Insert "content" as the second argument
|
||||
args = append(args[:1], append([]string{"content"}, args[1:]...)...)
|
||||
}
|
||||
|
||||
+4
-4
@@ -23,7 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
@@ -95,7 +95,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
|
||||
return err
|
||||
}
|
||||
maps.ConvertFloat64WithNoDecimalsToInt(m)
|
||||
hmaps.ConvertFloat64WithNoDecimalsToInt(m)
|
||||
switch format {
|
||||
case "yaml":
|
||||
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
|
||||
@@ -112,8 +112,8 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Display site configuration"
|
||||
cmd.Long = `Display site configuration, both default and custom settings.`
|
||||
cmd.Short = "Display project configuration"
|
||||
cmd.Long = `Display project configuration, both default and custom settings.`
|
||||
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
|
||||
|
||||
@@ -18,10 +18,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -200,6 +202,56 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *convertCommand) copyContentDirsForOutput(pagesBackedByFile page.Pages) error {
|
||||
contentDirs := make(map[string]bool)
|
||||
for _, p := range pagesBackedByFile {
|
||||
filename := p.File().Filename()
|
||||
contentDir := strings.TrimSuffix(filename, p.File().Path())
|
||||
if contentDir == filename {
|
||||
continue
|
||||
}
|
||||
contentDirs[filepath.Clean(contentDir)] = true
|
||||
}
|
||||
|
||||
var contentDirList []string
|
||||
for contentDir := range contentDirs {
|
||||
contentDirList = append(contentDirList, contentDir)
|
||||
}
|
||||
slices.Sort(contentDirList)
|
||||
|
||||
outputDirAbs, err := filepath.Abs(c.outputDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve output path %q: %w", c.outputDir, err)
|
||||
}
|
||||
|
||||
for _, contentDir := range contentDirList {
|
||||
outputContentDirAbs := filepath.Join(outputDirAbs, filepath.Base(contentDir))
|
||||
|
||||
skipDirs := make(map[string]bool)
|
||||
relToOutputDir, err := filepath.Rel(contentDir, outputDirAbs)
|
||||
if err == nil && relToOutputDir != ".." && !strings.HasPrefix(relToOutputDir, ".."+string(filepath.Separator)) {
|
||||
skipDirs[filepath.Clean(outputDirAbs)] = true
|
||||
}
|
||||
relToOutputContentDir, err := filepath.Rel(contentDir, outputContentDirAbs)
|
||||
if err == nil && relToOutputContentDir != ".." && !strings.HasPrefix(relToOutputContentDir, ".."+string(filepath.Separator)) {
|
||||
skipDirs[filepath.Clean(outputContentDirAbs)] = true
|
||||
}
|
||||
|
||||
var shouldCopy func(filename string) bool
|
||||
if len(skipDirs) > 0 {
|
||||
shouldCopy = func(filename string) bool {
|
||||
return !skipDirs[filepath.Clean(filename)]
|
||||
}
|
||||
}
|
||||
|
||||
if err := hugio.CopyDir(hugofs.Os, contentDir, outputContentDirAbs, shouldCopy); err != nil {
|
||||
return fmt.Errorf("failed to copy %q to %q: %w", contentDir, outputContentDirAbs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
if c.outputDir == "" && !c.unsafe {
|
||||
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
@@ -219,6 +271,12 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
pagesBackedByFile = append(pagesBackedByFile, p)
|
||||
}
|
||||
|
||||
if c.outputDir != "" {
|
||||
if err := c.copyContentDirsForOutput(pagesBackedByFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
site.Log.Println("processing", len(pagesBackedByFile), "content files")
|
||||
for _, p := range site.AllPages() {
|
||||
if err := c.convertAndSavePage(p, site, format); err != nil {
|
||||
|
||||
+2
-2
@@ -27,8 +27,8 @@ import (
|
||||
func newDeployCommand() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
short: "Deploy your site to a cloud provider",
|
||||
long: `Deploy your site to a cloud provider
|
||||
short: "Deploy your project to a cloud provider",
|
||||
long: `Deploy your project to a cloud provider
|
||||
|
||||
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
|
||||
documentation.
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ func newGenCommand() *genCommand {
|
||||
short: "Generate CSS stylesheet for the Chroma code highlighter",
|
||||
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
|
||||
|
||||
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
|
||||
See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview of the available styles.`,
|
||||
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
style = strings.ToLower(style)
|
||||
@@ -98,7 +98,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
|
||||
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
"editor": "newContentEditor",
|
||||
}
|
||||
|
||||
// Flags that we for some reason don't want to expose in the site config.
|
||||
// Flags that we for some reason don't want to expose in the project config.
|
||||
internalKeySet := map[string]bool{
|
||||
"quiet": true,
|
||||
"verbose": true,
|
||||
|
||||
+33
-22
@@ -31,11 +31,11 @@ import (
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -56,8 +56,9 @@ import (
|
||||
type hugoBuilder struct {
|
||||
r *rootCommand
|
||||
|
||||
confmu sync.Mutex
|
||||
conf *commonConfig
|
||||
confmu sync.Mutex
|
||||
confOld *commonConfig
|
||||
conf *commonConfig
|
||||
|
||||
// May be nil.
|
||||
s *serverCommand
|
||||
@@ -94,6 +95,27 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
|
||||
fn(c.conf)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) withConfOrOldConf(fn func(conf *commonConfig)) {
|
||||
c.confmu.Lock()
|
||||
defer c.confmu.Unlock()
|
||||
if c.conf != nil {
|
||||
fn(c.conf)
|
||||
} else if c.confOld != nil {
|
||||
fn(c.confOld)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) withConfOrOldConfE(fn func(conf *commonConfig) error) error {
|
||||
c.confmu.Lock()
|
||||
defer c.confmu.Unlock()
|
||||
if c.conf != nil {
|
||||
return fn(c.conf)
|
||||
} else if c.confOld != nil {
|
||||
return fn(c.confOld)
|
||||
}
|
||||
return errConfigNotSet
|
||||
}
|
||||
|
||||
type hugoBuilderErrState struct {
|
||||
mu sync.Mutex
|
||||
paused bool
|
||||
@@ -157,6 +179,7 @@ func (c *hugoBuilder) initCPUProfile() (func(), error) {
|
||||
return nil, fmt.Errorf("failed to create CPU profile: %w", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("failed to start CPU profile: %w", err)
|
||||
}
|
||||
return func() {
|
||||
@@ -1059,25 +1082,11 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
cfg := config.New()
|
||||
cfg.Set("renderToMemory", c.r.renderToMemory)
|
||||
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
|
||||
if c.r.environment == "" {
|
||||
// We need to set the environment as early as possible because we need it to load the correct config.
|
||||
// Check if the user has set it in env.
|
||||
if env := os.Getenv("HUGO_ENVIRONMENT"); env != "" {
|
||||
c.r.environment = env
|
||||
} else if env := os.Getenv("HUGO_ENV"); env != "" {
|
||||
c.r.environment = env
|
||||
} else {
|
||||
if c.s != nil {
|
||||
// The server defaults to development.
|
||||
c.r.environment = hugo.EnvironmentDevelopment
|
||||
} else {
|
||||
c.r.environment = hugo.EnvironmentProduction
|
||||
}
|
||||
}
|
||||
}
|
||||
// We need to set the environment as early as possible because we need it to load the correct config.
|
||||
c.r.resolveEnvironment(c.s != nil)
|
||||
cfg.Set("environment", c.r.environment)
|
||||
|
||||
cfg.Set("internal", maps.Params{
|
||||
cfg.Set("internal", hmaps.Params{
|
||||
"running": running,
|
||||
"watch": watch,
|
||||
"verbose": c.r.isVerbose(),
|
||||
@@ -1091,10 +1100,11 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
|
||||
if len(conf.configs.LoadingInfo.ConfigFiles) == 0 {
|
||||
//lint:ignore ST1005 end user message.
|
||||
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.")
|
||||
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new project.\nRun `hugo help new` for details.")
|
||||
}
|
||||
|
||||
c.conf = conf
|
||||
c.confOld = conf
|
||||
if c.onConfigLoaded != nil {
|
||||
if err := c.onConfigLoaded(false); err != nil {
|
||||
return err
|
||||
@@ -1158,8 +1168,9 @@ func (c *hugoBuilder) reloadConfig() error {
|
||||
c.r.resetLogs()
|
||||
c.r.configVersionID.Add(1)
|
||||
|
||||
if err := c.withConfE(func(conf *commonConfig) error {
|
||||
if err := c.withConfOrOldConfE(func(conf *commonConfig) error {
|
||||
oldConf := conf
|
||||
c.conf = nil
|
||||
newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+11
-11
@@ -29,9 +29,9 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
@@ -49,7 +49,7 @@ func newImportCommand() *importCommand {
|
||||
name: "jekyll",
|
||||
short: "hugo import from Jekyll",
|
||||
long: `hugo import from Jekyll.
|
||||
|
||||
|
||||
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 2 {
|
||||
@@ -90,8 +90,8 @@ func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Import a site from another system"
|
||||
cmd.Long = `Import a site from another system.
|
||||
cmd.Short = "Import a project from another system"
|
||||
cmd.Long = `Import a project from another system.
|
||||
|
||||
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
|
||||
|
||||
@@ -105,7 +105,7 @@ func (c *importCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
|
||||
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
|
||||
title := "My New Hugo Site"
|
||||
title := "My New Hugo Project"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
for key, value := range jekyllConfig {
|
||||
@@ -127,7 +127,7 @@ func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind
|
||||
in := map[string]any{
|
||||
"baseURL": baseURL,
|
||||
"title": title,
|
||||
"languageCode": "en-us",
|
||||
"locale": "en-us",
|
||||
"disablePathToLower": true,
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[st
|
||||
return postDirs, hasAnyPost
|
||||
}
|
||||
|
||||
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
|
||||
func (c *importCommand) createProjectFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
|
||||
fs := &afero.OsFs{}
|
||||
if exists, _ := helpers.Exists(targetDir, fs); exists {
|
||||
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
|
||||
@@ -190,7 +190,7 @@ func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyl
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
|
||||
metadata, _ := maps.ToStringMapE(m)
|
||||
metadata, _ := hmaps.ToStringMapE(m)
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
var resultLines []string
|
||||
@@ -246,7 +246,7 @@ func (c *importCommand) convertJekyllContent(m any, content string) (string, err
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
|
||||
metadata, err := maps.ToStringMapE(m)
|
||||
metadata, err := hmaps.ToStringMapE(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -419,7 +419,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
return errors.New("abort: jekyll root contains neither posts nor drafts")
|
||||
}
|
||||
|
||||
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
|
||||
err = c.createProjectFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
|
||||
if err != nil {
|
||||
return newUserError(err)
|
||||
}
|
||||
@@ -463,7 +463,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
}
|
||||
|
||||
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
|
||||
c.r.Println("Now, start Hugo by yourself:\n")
|
||||
c.r.Println("Now, start Hugo by yourself:")
|
||||
c.r.Println("cd " + args[1])
|
||||
c.r.Println("git init")
|
||||
c.r.Println("git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke")
|
||||
|
||||
+23
-13
@@ -21,12 +21,13 @@ import (
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const commonUsageMod = `
|
||||
Note that Hugo will always start out by resolving the components defined in the site
|
||||
Note that Hugo will always start out by resolving the components defined in the project
|
||||
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
|
||||
Go Modules, or a folder inside the themes directory, in that order.
|
||||
|
||||
@@ -49,28 +50,34 @@ func newModCommands() *modCommands {
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "pack",
|
||||
short: "Experimental: Prepares and writes a composite package.json file for your project",
|
||||
long: `Prepares and writes a composite package.json file for your project.
|
||||
short: "Merges module Node.js dependencies into an npm workspace",
|
||||
long: `Merges Node.js dependencies from all Hugo modules into a "packages/hugoautogen" npm workspace.
|
||||
|
||||
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
|
||||
with the base dependency set.
|
||||
The merged dependencies are written to packages/hugoautogen/package.json, and the root package.json
|
||||
is updated with a "workspaces" entry pointing to "packages/hugoautogen".
|
||||
|
||||
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
|
||||
The source entries are read from either package.hugo.json or package.json in the module root, with package.hugo.json taking precedence if both exist.
|
||||
|
||||
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
|
||||
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
|
||||
so this may/will change in future versions of Hugo.
|
||||
See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more information.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
cfg := flagsToCfg(cd, nil)
|
||||
k := configKey{counter: r.configVersionID.Load(), skipNpmCheck: true}
|
||||
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
|
||||
conf, err := r.ConfigFromProvider(key, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hugolib.NewHugoSites(r.newDepsConfig(conf))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs, h.Configs.Modules)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -166,7 +173,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
if all {
|
||||
modCache := h.ResourceSpec.FileCaches.ModulesCache()
|
||||
count, err := modCache.Prune(true)
|
||||
r.Printf("Deleted %d files from module cache.", count)
|
||||
r.Printf("Deleted %d directories from module cache.", count)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -293,7 +300,10 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
return err
|
||||
}
|
||||
client := conf.configs.ModulesClient
|
||||
return client.Get(args...)
|
||||
if err := client.Get(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
+13
-12
@@ -46,9 +46,9 @@ It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
If archetypes are provided in your theme or project, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
Ensure you run this within the root directory of your project.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
@@ -76,10 +76,11 @@ Ensure you run this within the root directory of your site.`,
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "site",
|
||||
use: "site [path]",
|
||||
short: "Create a new site",
|
||||
long: `Create a new site at the specified path.`,
|
||||
name: "project",
|
||||
use: "project [path]",
|
||||
short: "Create a new project",
|
||||
long: `Create a new project at the specified path.`,
|
||||
aliases: []string{"site"},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("path needs to be provided")
|
||||
@@ -99,13 +100,13 @@ Ensure you run this within the root directory of your site.`,
|
||||
}
|
||||
sourceFs := conf.fs.Source
|
||||
|
||||
err = skeletons.CreateSite(createpath, sourceFs, force, format)
|
||||
err = skeletons.CreateProject(createpath, sourceFs, force, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
|
||||
r.Println(c.newSiteNextStepsText(createpath, format))
|
||||
r.Printf("Congratulations! Your new Hugo project was created in %s.\n\n", createpath)
|
||||
r.Println(c.newProjectNextStepsText(createpath, format))
|
||||
|
||||
return nil
|
||||
},
|
||||
@@ -192,9 +193,9 @@ It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
If archetypes are provided in your theme or project, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your site.`
|
||||
Ensure you run this within the root directory of your project.`
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
@@ -205,7 +206,7 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
|
||||
func (c *newCommand) newProjectNextStepsText(path string, format string) string {
|
||||
format = strings.ToLower(format)
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
|
||||
+3
-1
@@ -28,13 +28,14 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
step int
|
||||
skipPush bool
|
||||
try bool
|
||||
version string
|
||||
)
|
||||
|
||||
return &simpleCommand{
|
||||
name: "release",
|
||||
short: "Release a new version of Hugo",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
rel, err := releaser.New(skipPush, try, step)
|
||||
rel, err := releaser.New(skipPush, try, step, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -47,6 +48,7 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
|
||||
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
|
||||
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
|
||||
cmd.PersistentFlags().StringVarP(&version, "version", "", "", "version to release (derived from branch name if not set)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
},
|
||||
}
|
||||
|
||||
+21
-16
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
@@ -245,7 +246,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
logger := f.c.r.logger
|
||||
|
||||
if i == 0 {
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment())
|
||||
mainTarget := "disk"
|
||||
if f.c.r.renderToMemory {
|
||||
mainTarget = "memory"
|
||||
@@ -281,7 +282,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
}
|
||||
|
||||
port = 1313
|
||||
f.c.withConf(func(conf *commonConfig) {
|
||||
f.c.withConfOrOldConf(func(conf *commonConfig) {
|
||||
if lrport := conf.configs.GetFirstLanguageConfig().BaseURLLiveReload().Port(); lrport != 0 {
|
||||
port = lrport
|
||||
}
|
||||
@@ -370,6 +371,12 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
|
||||
if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
|
||||
if isNavigation(requestURI, r) {
|
||||
// See issue 14240.
|
||||
// Hugo escapes the URL paths when generating them,
|
||||
// that may not be the case when we receive it back from the browser.
|
||||
// PathEscape will escape if it is not already escaped.
|
||||
requestURI = paths.PathEscape(requestURI)
|
||||
|
||||
if !f.c.visitedURLs.Contains(requestURI) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(requestURI); err != nil {
|
||||
@@ -492,9 +499,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
|
||||
|
||||
for _, group := range watchGroups {
|
||||
c.r.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
c.r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
|
||||
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -518,7 +523,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Start the embedded web server"
|
||||
cmd.Long = `Hugo provides its own webserver which builds and serves the site.
|
||||
cmd.Long = `Hugo provides its own webserver which builds and serves the project.
|
||||
While hugo server is high performance, it is a webserver with limited options.
|
||||
|
||||
The ` + "`" + `hugo server` + "`" + ` command will by default write and serve files from disk, but
|
||||
@@ -526,8 +531,8 @@ you can render to memory by using the ` + "`" + `--renderToMemory` + "`" + ` fla
|
||||
faster in some cases, but it will consume more memory.
|
||||
|
||||
By default hugo will also watch your files for any changes you make and
|
||||
automatically rebuild the site. It will then live reload any open browser pages
|
||||
and push the latest content to them. As most Hugo sites are built in a fraction
|
||||
automatically rebuild the project. It will then live reload any open browser pages
|
||||
and push the latest content to them. As most Hugo projects are built in a fraction
|
||||
of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Aliases = []string{"serve"}
|
||||
|
||||
@@ -548,7 +553,7 @@ of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
|
||||
cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
|
||||
cmd.Flags().BoolVarP(&c.navigateToChanged, "navigateToChanged", "N", false, "navigate to changed content file on live browser reload")
|
||||
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the site in a browser after server startup")
|
||||
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the project in a browser after server startup")
|
||||
cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
|
||||
cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
@@ -1155,14 +1160,14 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// chmodFilter is a ChmodFilter for static syncing.
|
||||
// Returns true to skip syncing permissions for directories and files without
|
||||
// owner-write permission. The primary use case is files from the module cache (0444).
|
||||
func chmodFilter(dst, src os.FileInfo) bool {
|
||||
// Hugo publishes data from multiple sources, potentially
|
||||
// with overlapping directory structures. We cannot sync permissions
|
||||
// for directories as that would mean that we might end up with write-protected
|
||||
// directories inside /public.
|
||||
// One example of this would be syncing from the Go Module cache,
|
||||
// which have 0555 directories.
|
||||
return src.IsDir()
|
||||
if src.IsDir() {
|
||||
return true
|
||||
}
|
||||
return src.Mode().Perm()&0o200 == 0
|
||||
}
|
||||
|
||||
func cleanErrorLog(content string) string {
|
||||
|
||||
+55
-11
@@ -21,24 +21,24 @@ import (
|
||||
"github.com/gohugoio/hugo/common/hiter"
|
||||
)
|
||||
|
||||
// Stack is a simple LIFO stack that is safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
|
||||
type StackThreadSafe[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewStack[T any]() *Stack[T] {
|
||||
return &Stack[T]{}
|
||||
func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
|
||||
return &StackThreadSafe[T]{}
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
func (s *StackThreadSafe[T]) Push(item T) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (T, bool) {
|
||||
func (s *StackThreadSafe[T]) Pop() (T, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.items) == 0 {
|
||||
@@ -49,7 +49,7 @@ func (s *Stack[T]) Pop() (T, bool) {
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (T, bool) {
|
||||
func (s *StackThreadSafe[T]) Peek() (T, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.items) == 0 {
|
||||
@@ -58,18 +58,18 @@ func (s *Stack[T]) Peek() (T, bool) {
|
||||
return s.items[len(s.items)-1], true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Len() int {
|
||||
func (s *StackThreadSafe[T]) Len() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
// All returns all items in the stack, from bottom to top.
|
||||
func (s *Stack[T]) All() iter.Seq2[int, T] {
|
||||
func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
|
||||
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
func (s *StackThreadSafe[T]) Drain() []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items := s.items
|
||||
@@ -77,7 +77,7 @@ func (s *Stack[T]) Drain() []T {
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var items []T
|
||||
@@ -89,3 +89,47 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Stack is a simple LIFO stack that is not safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
}
|
||||
|
||||
func NewStack[T any]() *Stack[T] {
|
||||
return &Stack[T]{}
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (T, bool) {
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
item := s.items[len(s.items)-1]
|
||||
s.items = s.items[:len(s.items)-1]
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (T, bool) {
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
return s.items[len(s.items)-1], true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Len() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) All() iter.Seq2[int, T] {
|
||||
return slices.All(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
items := s.items
|
||||
s.items = nil
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestNewStack(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s, qt.IsNotNil)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestStackBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s.Len(), qt.Equals, 0)
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestStackDrain(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[string]()
|
||||
s := NewStackThreadSafe[string]()
|
||||
s.Push("a")
|
||||
s.Push("b")
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestStackDrainMatching(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
s.Push(1)
|
||||
s.Push(2)
|
||||
s.Push(3)
|
||||
|
||||
@@ -16,15 +16,10 @@ package constants
|
||||
// Error/Warning IDs.
|
||||
// Do not change these values.
|
||||
const (
|
||||
// IDs for remote errors in tpl/data.
|
||||
ErrRemoteGetJSON = "error-remote-getjson"
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
|
||||
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
|
||||
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
|
||||
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
|
||||
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
|
||||
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
|
||||
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
|
||||
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
|
||||
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
|
||||
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
|
||||
)
|
||||
|
||||
// Field/method names with special meaning.
|
||||
|
||||
@@ -87,11 +87,13 @@ func XXHashFromString(s string) (uint64, error) {
|
||||
return h.Sum64(), nil
|
||||
}
|
||||
|
||||
// XxHashFromStringHexEncoded calculates the xxHash for the given string
|
||||
// XxHashFromStringHexEncoded calculates the xxHash for the given strings
|
||||
// and returns the hash as a hex encoded string.
|
||||
func XxHashFromStringHexEncoded(f string) string {
|
||||
func XxHashFromStringHexEncoded(s ...string) string {
|
||||
h := xxhash.New()
|
||||
h.WriteString(f)
|
||||
for _, f := range s {
|
||||
h.WriteString(f)
|
||||
}
|
||||
hash := h.Sum(nil)
|
||||
return hex.EncodeToString(hash)
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 10 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
for j := range 100 {
|
||||
s := strings.Repeat("Hello ", i+j+1*42)
|
||||
r := strings.NewReader(s)
|
||||
@@ -50,7 +48,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
|
||||
expect, _ := XXHashFromString(s)
|
||||
c.Assert(got, qt.Equals, expect)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
@@ -288,6 +288,50 @@ func Unwrap(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UnwrapFileErrors returns all FileError contained in err.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errs := Errors(err)
|
||||
var fileErrors []FileError
|
||||
for _, e := range errs {
|
||||
if v, ok := e.(FileError); ok {
|
||||
fileErrors = append(fileErrors, v)
|
||||
}
|
||||
fileErrors = append(fileErrors, UnwrapFileErrors(errors.Unwrap(e))...)
|
||||
}
|
||||
return fileErrors
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
errs := UnwrapFileErrors(err)
|
||||
var n int
|
||||
for _, e := range errs {
|
||||
if e.ErrorContext() != nil {
|
||||
errs[n] = e
|
||||
n++
|
||||
}
|
||||
}
|
||||
return errs[:n]
|
||||
}
|
||||
|
||||
// Errors returns the list of errors contained in err.
|
||||
func Errors(err error) []error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
type unwrapper interface {
|
||||
Unwrap() []error
|
||||
}
|
||||
if u, ok := err.(unwrapper); ok {
|
||||
return u.Unwrap()
|
||||
}
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
func extractFileTypePos(err error) (string, text.Position) {
|
||||
err = Unwrap(err)
|
||||
|
||||
@@ -350,30 +394,6 @@ func UnwrapFileError(err error) FileError {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnwrapFileErrors tries to unwrap all FileError.
|
||||
func UnwrapFileErrors(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
|
||||
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
|
||||
var errs []FileError
|
||||
for err != nil {
|
||||
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
|
||||
errs = append(errs, v)
|
||||
}
|
||||
err = errors.Unwrap(err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func extractOffsetAndType(e error) (int, string) {
|
||||
switch v := e.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
|
||||
@@ -32,15 +32,15 @@ func TestNewFileError(t *testing.T) {
|
||||
fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
|
||||
|
||||
lines := ""
|
||||
var lines strings.Builder
|
||||
for i := 1; i <= 100; i++ {
|
||||
lines += fmt.Sprintf("line %d\n", i)
|
||||
lines.WriteString(fmt.Sprintf("line %d\n", i))
|
||||
}
|
||||
|
||||
fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2})
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`)
|
||||
fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212})
|
||||
fe.UpdateContent(strings.NewReader(lines), nil)
|
||||
fe.UpdateContent(strings.NewReader(lines.String()), nil)
|
||||
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
|
||||
errorContext := fe.ErrorContext()
|
||||
c.Assert(errorContext, qt.IsNotNil)
|
||||
|
||||
+244
-80
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,7 @@
|
||||
package hexec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
@@ -23,12 +24,12 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
@@ -98,11 +99,11 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
|
||||
}
|
||||
|
||||
return &Exec{
|
||||
sc: cfg,
|
||||
workingDir: workingDir,
|
||||
infol: log.InfoCommand("exec"),
|
||||
baseEnviron: baseEnviron,
|
||||
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||
sc: cfg,
|
||||
workingDir: workingDir,
|
||||
infol: log.InfoCommand("exec"),
|
||||
baseEnviron: baseEnviron,
|
||||
nodeRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +122,16 @@ type Exec struct {
|
||||
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
|
||||
baseEnviron []string
|
||||
|
||||
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
|
||||
npxInit sync.Once
|
||||
npxAvailable bool
|
||||
// Additional absolute paths to allow reading from in the Node.js permission model.
|
||||
nodeReadPaths []string
|
||||
|
||||
nodeRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
|
||||
}
|
||||
|
||||
// SetNodeReadPaths sets additional absolute paths to allow reading from
|
||||
// in the Node.js permission model (e.g. Hugo module cache directories).
|
||||
func (e *Exec) SetNodeReadPaths(paths []string) {
|
||||
e.nodeReadPaths = paths
|
||||
}
|
||||
|
||||
func (e *Exec) New(name string, arg ...any) (Runner, error) {
|
||||
@@ -155,8 +163,6 @@ func (b binaryLocation) String() string {
|
||||
switch b {
|
||||
case binaryLocationNodeModules:
|
||||
return "node_modules/.bin"
|
||||
case binaryLocationNpx:
|
||||
return "npx"
|
||||
case binaryLocationPath:
|
||||
return "PATH"
|
||||
}
|
||||
@@ -165,64 +171,55 @@ func (b binaryLocation) String() string {
|
||||
|
||||
const (
|
||||
binaryLocationNodeModules binaryLocation = iota + 1
|
||||
binaryLocationNpx
|
||||
binaryLocationPath
|
||||
)
|
||||
|
||||
// Npx will in order:
|
||||
// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory.
|
||||
// 2. If not found, and npx is available, run npx --no-install <name> <args>.
|
||||
// 3. Fall back to the PATH.
|
||||
// If name is "tailwindcss", we will try the PATH as the second option.
|
||||
// Npx finds and runs a Node.js tool. The binary is located first in
|
||||
// WORKINGDIR/node_modules/.bin, then in PATH. The tool is always invoked via
|
||||
// "node [--permission <flags>] <script> <args>"; the --permission flags are
|
||||
// added when the Node.js permission model is enabled.
|
||||
func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||
if err := e.sc.CheckAllowedExec(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := e.sc.CheckAllowedExec("node"); err != nil {
|
||||
// Legacy path: We replaced npx with node in v0.161.0, and anyone using these tools with a custom security.exec.allow list
|
||||
// would get an error when upgrading. To avoid this, check for npx as well.
|
||||
if err2 := e.sc.CheckAllowedExec("npx"); err2 != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
newRunner, err := e.newNPXRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) {
|
||||
type tryFunc func() func(...any) (Runner, error)
|
||||
tryFuncs := map[binaryLocation]tryFunc{
|
||||
binaryLocationNodeModules: func() func(...any) (Runner, error) {
|
||||
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
|
||||
_, err := exec.LookPath(nodeBinFilename)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return func(arg2 ...any) (Runner, error) {
|
||||
return e.new(name, nodeBinFilename, arg2...)
|
||||
}
|
||||
},
|
||||
binaryLocationNpx: func() func(...any) (Runner, error) {
|
||||
e.checkNpx()
|
||||
if !e.npxAvailable {
|
||||
return nil
|
||||
}
|
||||
return func(arg2 ...any) (Runner, error) {
|
||||
return e.npx(name, arg2...)
|
||||
}
|
||||
},
|
||||
binaryLocationPath: func() func(...any) (Runner, error) {
|
||||
if _, err := exec.LookPath(name); err != nil {
|
||||
return nil
|
||||
}
|
||||
return func(arg2 ...any) (Runner, error) {
|
||||
return e.New(name, arg2...)
|
||||
}
|
||||
},
|
||||
newRunner, err := e.nodeRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) {
|
||||
var resolvedBin string
|
||||
var loc binaryLocation
|
||||
|
||||
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
|
||||
if p, err := exec.LookPath(nodeBinFilename); err == nil {
|
||||
resolvedBin = p
|
||||
loc = binaryLocationNodeModules
|
||||
} else if p, err := exec.LookPath(name); err == nil {
|
||||
resolvedBin = p
|
||||
loc = binaryLocationPath
|
||||
} else {
|
||||
return nil, &NotFoundError{name: name, method: "in PATH"}
|
||||
}
|
||||
|
||||
locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath}
|
||||
if name == "tailwindcss" {
|
||||
// See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253
|
||||
locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx}
|
||||
scriptPath := resolveNodeBin(resolvedBin)
|
||||
|
||||
e.infol.WithFields(logg.Fields{
|
||||
logg.Field{Name: "location", Value: loc},
|
||||
logg.Field{Name: "bin", Value: resolvedBin},
|
||||
logg.Field{Name: "script", Value: scriptPath},
|
||||
}).Logf("resolve %q", name)
|
||||
|
||||
if scriptPath == "" {
|
||||
return nil, fmt.Errorf("binary %q is not a Node.js script", name)
|
||||
}
|
||||
for _, loc := range locations {
|
||||
if f := tryFuncs[loc](); f != nil {
|
||||
e.infol.Logf("resolve %q using %s", name, loc)
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])}
|
||||
|
||||
return func(arg2 ...any) (Runner, error) {
|
||||
return e.newNode(name, scriptPath, arg2...)
|
||||
}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -231,22 +228,195 @@ func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
|
||||
return newRunner(arg...)
|
||||
}
|
||||
|
||||
const (
|
||||
npxNoInstall = "--no-install"
|
||||
npxBinary = "npx"
|
||||
nodeModulesBinPath = "node_modules/.bin"
|
||||
)
|
||||
// newNode runs a Node.js script via "node [--permission <flags>] <scriptPath> <args>".
|
||||
func (e *Exec) newNode(name, scriptPath string, arg ...any) (Runner, error) {
|
||||
var allArgs []any
|
||||
for _, pa := range e.nodePermissionArgs(name, scriptPath) {
|
||||
allArgs = append(allArgs, pa)
|
||||
}
|
||||
allArgs = append(allArgs, scriptPath)
|
||||
allArgs = append(allArgs, arg...)
|
||||
// When the script lives outside the working dir (a globally installed
|
||||
// tool), point NODE_PATH at the script's node_modules ancestor so Node's
|
||||
// resolver (and tools that honor it, e.g. tailwindcss v4) can locate the
|
||||
// tool's sibling packages. tailwindcss v4's CSS resolver treats NODE_PATH
|
||||
// as a single path, not a list, so we don't concatenate with the local
|
||||
// path here. For local installs the caller's NODE_PATH (set by
|
||||
// hugo.GetExecEnviron to <workDir>/node_modules) already covers the need.
|
||||
localNM := filepath.Join(e.workingDir, "node_modules")
|
||||
if p := nodeScriptReadPath(scriptPath); p != "" && p != localNM {
|
||||
allArgs = append(allArgs, WithEnviron([]string{"NODE_PATH=" + p}))
|
||||
}
|
||||
|
||||
func (e *Exec) checkNpx() {
|
||||
e.npxInit.Do(func() {
|
||||
e.npxAvailable = InPath(npxBinary)
|
||||
})
|
||||
return e.New("node", allArgs...)
|
||||
}
|
||||
|
||||
// npx is a convenience method to create a Runner running npx --no-install <name> <args.
|
||||
func (e *Exec) npx(name string, arg ...any) (Runner, error) {
|
||||
arg = append(arg[:0], append([]any{npxNoInstall, name}, arg[0:]...)...)
|
||||
return e.New(npxBinary, arg...)
|
||||
// nodePermissionArgs builds the Node.js --permission flags from the security config.
|
||||
func (e *Exec) nodePermissionArgs(name, scriptPath string) []string {
|
||||
perms := e.sc.Node.Permissions
|
||||
if !perms.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
args := []string{"--permission"}
|
||||
|
||||
for _, p := range e.resolveNodePermPaths(perms.AllowRead) {
|
||||
args = append(args, "--allow-fs-read="+p)
|
||||
}
|
||||
for _, p := range e.nodeReadPaths {
|
||||
args = append(args, "--allow-fs-read="+p)
|
||||
}
|
||||
if p := nodeScriptReadPath(scriptPath); p != "" {
|
||||
args = append(args, "--allow-fs-read="+p)
|
||||
}
|
||||
|
||||
for _, p := range e.resolveNodePermPaths(perms.AllowWrite) {
|
||||
args = append(args, "--allow-fs-write="+p)
|
||||
}
|
||||
|
||||
var silenceSecurityWarnings bool
|
||||
if slices.Contains(perms.AllowAddons, name) {
|
||||
silenceSecurityWarnings = true
|
||||
args = append(args, "--allow-addons")
|
||||
}
|
||||
|
||||
if slices.Contains(perms.AllowWorker, name) {
|
||||
silenceSecurityWarnings = true
|
||||
args = append(args, "--allow-worker")
|
||||
}
|
||||
|
||||
if slices.Contains(perms.AllowChildProcess, name) {
|
||||
silenceSecurityWarnings = true
|
||||
args = append(args, "--allow-child-process")
|
||||
}
|
||||
|
||||
if silenceSecurityWarnings {
|
||||
// There are no more fine grained way to do this, see https://github.com/nodejs/node/issues/59818
|
||||
// If the process is configured to allow either workers or addons, Node will print warnings that's not very helpful.
|
||||
args = append(args, "--disable-warning=SecurityWarning")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// resolveNodePermPaths resolves relative paths against the working directory.
|
||||
func (e *Exec) resolveNodePermPaths(paths []string) []string {
|
||||
resolved := make([]string, len(paths))
|
||||
for i, p := range paths {
|
||||
switch {
|
||||
case p == "*":
|
||||
resolved[i] = "*"
|
||||
case filepath.IsAbs(p):
|
||||
resolved[i] = p
|
||||
default:
|
||||
resolved[i] = filepath.Join(e.workingDir, p)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
const nodeModulesBinPath = "node_modules/.bin"
|
||||
|
||||
// nodeScriptReadPath returns a path to add to the Node.js read allow-list so
|
||||
// a script can load its dependencies. For scripts inside a node_modules tree
|
||||
// it returns the nearest ancestor "node_modules" directory, so both nested
|
||||
// and hoisted deps are reachable. Otherwise the script's own directory.
|
||||
func nodeScriptReadPath(scriptPath string) string {
|
||||
if scriptPath == "" {
|
||||
return ""
|
||||
}
|
||||
dir := filepath.Dir(scriptPath)
|
||||
for {
|
||||
if filepath.Base(dir) == "node_modules" {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return filepath.Dir(scriptPath)
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
// resolveNodeBin resolves a binary path to the underlying Node.js script.
|
||||
// Returns the path to the JS entry point, or "" if the binary is not a Node script.
|
||||
func resolveNodeBin(path string) string {
|
||||
// 1. If the file is a symlink, resolve it (macOS/Linux npm creates symlinks in node_modules/.bin).
|
||||
if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
if resolved, err := filepath.EvalSymlinks(path); err == nil {
|
||||
if hasJSExtension(resolved) || isNodeScript(resolved) {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// 2. Check if the file itself is a Node script (e.g. globally installed with #!/usr/bin/env node).
|
||||
if isNodeScript(path) {
|
||||
return path
|
||||
}
|
||||
// 3. Try extracting JS entry point from an npm wrapper script (.cmd or shell).
|
||||
return extractNodeEntryPoint(path)
|
||||
}
|
||||
|
||||
// nodeEntryPointRe matches a relative path in npm-generated wrapper scripts.
|
||||
// The entry may be a .js/.mjs/.cjs file or an extensionless Node shebang
|
||||
// script (e.g. postcss-cli 7's bin/postcss). Local installs reference the
|
||||
// entry via "..", global installs via "node_modules" (notably on Windows,
|
||||
// where npm does not symlink global binaries).
|
||||
// Examples:
|
||||
//
|
||||
// Local shell: "$basedir/../postcss-cli/index.js"
|
||||
// Local cmd: "%dp0%\..\postcss-cli\index.js"
|
||||
// Scoped: "$basedir/../@babel/cli/bin/babel.js"
|
||||
// No ext: "%dp0%\..\postcss-cli\bin\postcss"
|
||||
// Global shell: "$basedir/node_modules/postcss-cli/index.js"
|
||||
// Global cmd: "%dp0%\node_modules\postcss-cli\index.js"
|
||||
var nodeEntryPointRe = regexp.MustCompile(`[/\\]((?:\.\.|node_modules)[/\\][\w@][\w@./\\-]*)`)
|
||||
|
||||
// extractNodeEntryPoint reads an npm wrapper script and extracts the Node
|
||||
// entry point path, validating that it's a JS file or a Node shebang script.
|
||||
func extractNodeEntryPoint(wrapperPath string) string {
|
||||
data, err := os.ReadFile(wrapperPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
m := nodeEntryPointRe.FindSubmatch(data)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
// Normalize backslashes from Windows .cmd wrappers.
|
||||
relPath := strings.ReplaceAll(string(m[1]), "\\", "/")
|
||||
resolved := filepath.Join(filepath.Dir(wrapperPath), relPath)
|
||||
if _, err := os.Stat(resolved); err != nil {
|
||||
return ""
|
||||
}
|
||||
if !hasJSExtension(resolved) && !isNodeScript(resolved) {
|
||||
return ""
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
func hasJSExtension(path string) bool {
|
||||
switch filepath.Ext(path) {
|
||||
case ".js", ".mjs", ".cjs":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isNodeScript reports whether the file at path has a Node.js shebang.
|
||||
func isNodeScript(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
r := bufio.NewReader(f)
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil && len(line) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(line, "#!") && strings.Contains(line, "node")
|
||||
}
|
||||
|
||||
// Sec returns the security policies this Exec is configured with.
|
||||
@@ -283,14 +453,8 @@ func (c *cmdWrapper) Run() error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
name := c.name
|
||||
method := "in PATH"
|
||||
if name == npxBinary {
|
||||
name = c.c.Args[2]
|
||||
method = "using npx"
|
||||
}
|
||||
if notFoundRe.MatchString(c.outerr.String()) {
|
||||
return &NotFoundError{name: name, method: method}
|
||||
return &NotFoundError{name: c.name, method: "in PATH"}
|
||||
}
|
||||
return fmt.Errorf("failed to execute binary %q with args %v: %s", c.name, c.c.Args[1:], c.outerr.String())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hexec_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestNPMGlobalInstalls(t *testing.T) {
|
||||
if !htesting.IsRealCI() {
|
||||
t.Skip("We only ever want to run this in CI.")
|
||||
}
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
[security.exec]
|
||||
allow = ['^(babel|node|postcss|tailwindcss)$']
|
||||
|
||||
-- package.json --
|
||||
{}
|
||||
-- hugo_stats.json --
|
||||
-- assets/js/main.js --
|
||||
console.log("Hello, world!");
|
||||
-- assets/css/main1.css --
|
||||
body { color: red }
|
||||
-- assets/css/main2.css --
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@source "hugo_stats.json";
|
||||
body { color: blue }
|
||||
-- layouts/home.html --
|
||||
{{ with resources.Get "css/main1.css" }}
|
||||
{{ with . | css.PostCSS }}
|
||||
CSS1 size: {{ .Content | len }}|{{ .RelPermalink }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with resources.Get "css/main2.css" }}
|
||||
{{ with . | css.TailwindCSS }}
|
||||
CSS2 size: {{ .Content | len }}|{{ .RelPermalink }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with resources.Get "js/main.js" }}
|
||||
{{ with . | js.Babel }}
|
||||
JS size: {{ .Content | len }}|{{ .RelPermalink }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstallGlobal(
|
||||
"postcss", "postcss-cli",
|
||||
"@babel/core", "@babel/cli",
|
||||
"tailwindcss", "@tailwindcss/cli", "@tailwindcss/typography",
|
||||
))
|
||||
|
||||
b.AssertFileContent("public/index.html",
|
||||
"CSS1 size: 233|/css/main1.css|",
|
||||
"CSS2 size: 4557|/css/main2.css|",
|
||||
"JS size: 31|/js/main.js|",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hexec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
)
|
||||
|
||||
func TestNodePermissionArgs(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Use t.TempDir() so paths are absolute on any OS (avoids Windows volume assumptions).
|
||||
base := t.TempDir()
|
||||
site := filepath.Join(base, "site")
|
||||
tmp := filepath.Join(base, "tmp")
|
||||
cacheDir := filepath.Join(base, "home", "user", ".cache", "hugo_cache", "modules")
|
||||
|
||||
c.Run("Default config tailwindcss", func(c *qt.C) {
|
||||
e := &Exec{
|
||||
sc: security.DefaultConfig,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("tailwindcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=" + site,
|
||||
"--allow-addons",
|
||||
"--allow-worker",
|
||||
"--allow-child-process",
|
||||
"--disable-warning=SecurityWarning",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Default config postcss", func(c *qt.C) {
|
||||
e := &Exec{
|
||||
sc: security.DefaultConfig,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("postcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=" + site,
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Multiple paths", func(c *qt.C) {
|
||||
cfg := security.DefaultConfig
|
||||
cfg.Node.Permissions.AllowRead = []string{".", tmp}
|
||||
cfg.Node.Permissions.AllowWrite = []string{"."}
|
||||
e := &Exec{
|
||||
sc: cfg,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("tailwindcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=" + site,
|
||||
"--allow-fs-read=" + tmp,
|
||||
"--allow-fs-write=" + site,
|
||||
"--allow-addons",
|
||||
"--allow-worker",
|
||||
"--allow-child-process",
|
||||
"--disable-warning=SecurityWarning",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Wildcard", func(c *qt.C) {
|
||||
cfg := security.DefaultConfig
|
||||
cfg.Node.Permissions.AllowRead = []string{"*"}
|
||||
cfg.Node.Permissions.AllowWrite = []string{"*"}
|
||||
e := &Exec{
|
||||
sc: cfg,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("tailwindcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=*",
|
||||
"--allow-fs-write=*",
|
||||
"--allow-addons",
|
||||
"--allow-worker",
|
||||
"--allow-child-process",
|
||||
"--disable-warning=SecurityWarning",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Disabled", func(c *qt.C) {
|
||||
cfg := security.DefaultConfig
|
||||
cfg.Node.Permissions.Disable = true
|
||||
e := &Exec{
|
||||
sc: cfg,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("tailwindcss", "")
|
||||
c.Assert(args, qt.IsNil)
|
||||
})
|
||||
|
||||
c.Run("No fs flags", func(c *qt.C) {
|
||||
cfg := security.DefaultConfig
|
||||
cfg.Node.Permissions.AllowRead = nil
|
||||
cfg.Node.Permissions.AllowAddons = nil
|
||||
cfg.Node.Permissions.AllowWorker = nil
|
||||
cfg.Node.Permissions.AllowChildProcess = nil
|
||||
e := &Exec{
|
||||
sc: cfg,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("postcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{"--permission"})
|
||||
})
|
||||
|
||||
c.Run("Read only", func(c *qt.C) {
|
||||
cfg := security.DefaultConfig
|
||||
cfg.Node.Permissions.AllowRead = []string{"."}
|
||||
cfg.Node.Permissions.AllowWrite = nil
|
||||
e := &Exec{
|
||||
sc: cfg,
|
||||
workingDir: site,
|
||||
}
|
||||
args := e.nodePermissionArgs("postcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=" + site,
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("With additional read paths", func(c *qt.C) {
|
||||
e := &Exec{
|
||||
sc: security.DefaultConfig,
|
||||
workingDir: site,
|
||||
nodeReadPaths: []string{cacheDir},
|
||||
}
|
||||
args := e.nodePermissionArgs("postcss", "")
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=" + site,
|
||||
"--allow-fs-read=" + cacheDir,
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Global install script path", func(c *qt.C) {
|
||||
e := &Exec{
|
||||
sc: security.DefaultConfig,
|
||||
workingDir: site,
|
||||
}
|
||||
globalNM := filepath.Join(base, "nvm", "lib", "node_modules")
|
||||
script := filepath.Join(globalNM, "postcss-cli", "bin", "postcss")
|
||||
args := e.nodePermissionArgs("postcss", script)
|
||||
c.Assert(args, qt.DeepEquals, []string{
|
||||
"--permission",
|
||||
"--allow-fs-read=" + site,
|
||||
"--allow-fs-read=" + globalNM,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodeScriptReadPath(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
base := t.TempDir()
|
||||
nm := filepath.Join(base, "node_modules")
|
||||
globalNM := filepath.Join(base, "nvm", "lib", "node_modules")
|
||||
|
||||
c.Assert(nodeScriptReadPath(""), qt.Equals, "")
|
||||
c.Assert(nodeScriptReadPath(filepath.Join(nm, "postcss-cli", "index.js")), qt.Equals, nm)
|
||||
c.Assert(nodeScriptReadPath(filepath.Join(nm, "@babel", "cli", "bin", "babel.js")), qt.Equals, nm)
|
||||
c.Assert(nodeScriptReadPath(filepath.Join(globalNM, "postcss-cli", "bin", "postcss")), qt.Equals, globalNM)
|
||||
|
||||
loose := filepath.Join(base, "tools", "script.js")
|
||||
c.Assert(nodeScriptReadPath(loose), qt.Equals, filepath.Dir(loose))
|
||||
}
|
||||
|
||||
func TestResolveNodeBin(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Create a fake node_modules structure.
|
||||
dir := t.TempDir()
|
||||
nodeModules := filepath.Join(dir, "node_modules")
|
||||
binDir := filepath.Join(nodeModules, ".bin")
|
||||
|
||||
// Create target JS files.
|
||||
postcssJS := filepath.Join(nodeModules, "postcss-cli", "index.js")
|
||||
babelJS := filepath.Join(nodeModules, "@babel", "cli", "bin", "babel.js")
|
||||
mkdirAndWrite(t, postcssJS, "#!/usr/bin/env node\nconsole.log('postcss');\n")
|
||||
mkdirAndWrite(t, babelJS, "#!/usr/bin/env node\nconsole.log('babel');\n")
|
||||
os.MkdirAll(binDir, 0o755)
|
||||
|
||||
c.Run("Symlink to JS file", func(c *qt.C) {
|
||||
if runtime.GOOS == "windows" {
|
||||
c.Skip("Symlinks may require elevated privileges on Windows")
|
||||
}
|
||||
link := filepath.Join(binDir, "postcss-link")
|
||||
os.Remove(link)
|
||||
c.Assert(os.Symlink(postcssJS, link), qt.IsNil)
|
||||
|
||||
resolved := resolveNodeBin(link)
|
||||
t.Logf("Symlink: link=%q, resolved=%q", link, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, postcssJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("Symlink to scoped package", func(c *qt.C) {
|
||||
if runtime.GOOS == "windows" {
|
||||
c.Skip("Symlinks may require elevated privileges on Windows")
|
||||
}
|
||||
link := filepath.Join(binDir, "babel-link")
|
||||
os.Remove(link)
|
||||
c.Assert(os.Symlink(babelJS, link), qt.IsNil)
|
||||
|
||||
resolved := resolveNodeBin(link)
|
||||
t.Logf("Scoped symlink: link=%q, resolved=%q", link, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, babelJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("Shell wrapper", func(c *qt.C) {
|
||||
wrapper := filepath.Join(binDir, "postcss-sh")
|
||||
content := "#!/bin/sh\n" +
|
||||
`basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")` + "\n" +
|
||||
`exec node "$basedir/../postcss-cli/index.js" "$@"` + "\n"
|
||||
mkdirAndWrite(t, wrapper, content)
|
||||
|
||||
resolved := resolveNodeBin(wrapper)
|
||||
t.Logf("Shell wrapper: wrapper=%q, resolved=%q", wrapper, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, postcssJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("Cmd wrapper", func(c *qt.C) {
|
||||
wrapper := filepath.Join(binDir, "postcss.cmd")
|
||||
content := "@ECHO off\r\n" +
|
||||
"SETLOCAL\r\n" +
|
||||
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\postcss-cli\index.js" %*` + "\r\n"
|
||||
mkdirAndWrite(t, wrapper, content)
|
||||
|
||||
resolved := resolveNodeBin(wrapper)
|
||||
t.Logf("Cmd wrapper: wrapper=%q, resolved=%q", wrapper, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, postcssJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("Cmd wrapper scoped package", func(c *qt.C) {
|
||||
wrapper := filepath.Join(binDir, "babel.cmd")
|
||||
content := "@ECHO off\r\n" +
|
||||
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@babel\cli\bin\babel.js" %*` + "\r\n"
|
||||
mkdirAndWrite(t, wrapper, content)
|
||||
|
||||
resolved := resolveNodeBin(wrapper)
|
||||
t.Logf("Cmd wrapper (scoped): wrapper=%q, resolved=%q", wrapper, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, babelJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("Node script with shebang", func(c *qt.C) {
|
||||
script := filepath.Join(binDir, "node-global")
|
||||
mkdirAndWrite(t, script, "#!/usr/bin/env node\nconsole.log('global');\n")
|
||||
|
||||
resolved := resolveNodeBin(script)
|
||||
t.Logf("Node script: path=%q, resolved=%q", script, resolved)
|
||||
c.Assert(resolved, qt.Equals, script)
|
||||
})
|
||||
|
||||
c.Run("Native binary", func(c *qt.C) {
|
||||
native := filepath.Join(binDir, "native-tool")
|
||||
mkdirAndWrite(t, native, "\x7fELF\x00\x00\x00")
|
||||
|
||||
resolved := resolveNodeBin(native)
|
||||
t.Logf("Native binary: path=%q, resolved=%q", native, resolved)
|
||||
c.Assert(resolved, qt.Equals, "")
|
||||
})
|
||||
|
||||
c.Run("Nonexistent file", func(c *qt.C) {
|
||||
c.Assert(resolveNodeBin("/nonexistent/path"), qt.Equals, "")
|
||||
})
|
||||
|
||||
c.Run("Wrapper with missing target", func(c *qt.C) {
|
||||
wrapper := filepath.Join(binDir, "missing-target")
|
||||
mkdirAndWrite(t, wrapper, "#!/bin/sh\nexec node \"$basedir/../no-such-pkg/index.js\" \"$@\"\n")
|
||||
|
||||
resolved := resolveNodeBin(wrapper)
|
||||
t.Logf("Missing target: wrapper=%q, resolved=%q", wrapper, resolved)
|
||||
c.Assert(resolved, qt.Equals, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractNodeEntryPointRegex(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
want string // expected capture group (with original separators)
|
||||
}{
|
||||
{"shell postcss", `"$basedir/../postcss-cli/index.js"`, "../postcss-cli/index.js"},
|
||||
{"shell babel", `"$basedir/../@babel/cli/bin/babel.js"`, "../@babel/cli/bin/babel.js"},
|
||||
{"shell tailwind mjs", `"$basedir/../@tailwindcss/cli/dist/index.mjs"`, "../@tailwindcss/cli/dist/index.mjs"},
|
||||
{"cmd postcss", `"%dp0%\..\postcss-cli\index.js"`, `..\postcss-cli\index.js`},
|
||||
{"cmd babel", `"%dp0%\..\@babel\cli\bin\babel.js"`, `..\@babel\cli\bin\babel.js`},
|
||||
{"cmd postcss no ext", `"%dp0%\..\postcss-cli\bin\postcss"`, `..\postcss-cli\bin\postcss`},
|
||||
{"shell postcss no ext", `"$basedir/../postcss-cli/bin/postcss"`, "../postcss-cli/bin/postcss"},
|
||||
{"cmd postcss global", `"%dp0%\node_modules\postcss-cli\index.js"`, `node_modules\postcss-cli\index.js`},
|
||||
{"cmd babel global", `"%dp0%\node_modules\@babel\cli\bin\babel.js"`, `node_modules\@babel\cli\bin\babel.js`},
|
||||
{"shell postcss global", `"$basedir/node_modules/postcss-cli/index.js"`, "node_modules/postcss-cli/index.js"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
c.Run(tc.name, func(c *qt.C) {
|
||||
m := nodeEntryPointRe.FindStringSubmatch(tc.content)
|
||||
t.Logf("regex match for %q: %v", tc.name, m)
|
||||
c.Assert(m, qt.Not(qt.IsNil))
|
||||
c.Assert(m[1], qt.Equals, tc.want)
|
||||
})
|
||||
}
|
||||
|
||||
c.Run("No match", func(c *qt.C) {
|
||||
for _, s := range []string{"@ECHO off", "#!/bin/bash\necho hello", "\x7fELF"} {
|
||||
c.Assert(nodeEntryPointRe.FindStringSubmatch(s), qt.IsNil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestResolveNodeBinWindows tests wrapper resolution on all platforms
|
||||
// by simulating Windows-style wrapper files. On Windows CI, this also
|
||||
// tests the native .cmd resolution path.
|
||||
func TestResolveNodeBinWindows(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
dir := t.TempDir()
|
||||
nodeModules := filepath.Join(dir, "node_modules")
|
||||
binDir := filepath.Join(nodeModules, ".bin")
|
||||
|
||||
// Create target JS file.
|
||||
targetJS := filepath.Join(nodeModules, "postcss-cli", "index.js")
|
||||
mkdirAndWrite(t, targetJS, "#!/usr/bin/env node\nconsole.log('postcss');\n")
|
||||
os.MkdirAll(binDir, 0o755)
|
||||
|
||||
// Simulate what npm creates on Windows: a .cmd wrapper and a shell script.
|
||||
cmdWrapper := filepath.Join(binDir, "postcss.cmd")
|
||||
cmdContent := "@ECHO off\r\n" +
|
||||
"SETLOCAL\r\n" +
|
||||
"CALL :find_dp0\r\n" +
|
||||
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\postcss-cli\index.js" %*` + "\r\n"
|
||||
mkdirAndWrite(t, cmdWrapper, cmdContent)
|
||||
|
||||
shWrapper := filepath.Join(binDir, "postcss")
|
||||
shContent := "#!/bin/sh\n" +
|
||||
`exec node "$basedir/../postcss-cli/index.js" "$@"` + "\n"
|
||||
mkdirAndWrite(t, shWrapper, shContent)
|
||||
|
||||
t.Logf("GOOS=%s", runtime.GOOS)
|
||||
t.Logf("cmd wrapper: %s", cmdWrapper)
|
||||
t.Logf("sh wrapper: %s", shWrapper)
|
||||
t.Logf("target JS: %s", targetJS)
|
||||
|
||||
c.Run("cmd wrapper resolves to JS", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(cmdWrapper)
|
||||
t.Logf("resolveNodeBin(%q) = %q", cmdWrapper, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, targetJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("sh wrapper resolves to JS", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(shWrapper)
|
||||
t.Logf("resolveNodeBin(%q) = %q", shWrapper, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, targetJS), qt.IsTrue)
|
||||
})
|
||||
|
||||
// Simulate postcss-cli 7, whose wrappers point to an extensionless Node
|
||||
// shebang script (bin/postcss) rather than a .js file.
|
||||
targetNoExt := filepath.Join(nodeModules, "postcss-cli", "bin", "postcss")
|
||||
mkdirAndWrite(t, targetNoExt, "#!/usr/bin/env node\nrequire('../');\n")
|
||||
|
||||
cmdNoExt := filepath.Join(binDir, "postcssne.cmd")
|
||||
mkdirAndWrite(t, cmdNoExt, "@ECHO off\r\n"+
|
||||
`"%dp0%\..\postcss-cli\bin\postcss" %*`+"\r\n")
|
||||
|
||||
shNoExt := filepath.Join(binDir, "postcssne")
|
||||
mkdirAndWrite(t, shNoExt, "#!/bin/sh\n"+
|
||||
`exec node "$basedir/../postcss-cli/bin/postcss" "$@"`+"\n")
|
||||
|
||||
c.Run("cmd wrapper resolves to extensionless script", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(cmdNoExt)
|
||||
t.Logf("resolveNodeBin(%q) = %q", cmdNoExt, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, targetNoExt), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("sh wrapper resolves to extensionless script", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(shNoExt)
|
||||
t.Logf("resolveNodeBin(%q) = %q", shNoExt, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, targetNoExt), qt.IsTrue)
|
||||
})
|
||||
|
||||
// Simulate `npm install -g` on Windows: the wrapper sits at the npm
|
||||
// global prefix and references node_modules as a child (no `..`).
|
||||
globalDir := filepath.Join(dir, "global")
|
||||
globalTarget := filepath.Join(globalDir, "node_modules", "postcss-cli", "index.js")
|
||||
mkdirAndWrite(t, globalTarget, "#!/usr/bin/env node\nconsole.log('postcss');\n")
|
||||
|
||||
globalCmd := filepath.Join(globalDir, "postcss.cmd")
|
||||
mkdirAndWrite(t, globalCmd, "@ECHO off\r\n"+
|
||||
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\postcss-cli\index.js" %*`+"\r\n")
|
||||
|
||||
globalSh := filepath.Join(globalDir, "postcss")
|
||||
mkdirAndWrite(t, globalSh, "#!/bin/sh\n"+
|
||||
`exec node "$basedir/node_modules/postcss-cli/index.js" "$@"`+"\n")
|
||||
|
||||
c.Run("global cmd wrapper resolves to JS", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(globalCmd)
|
||||
t.Logf("resolveNodeBin(%q) = %q", globalCmd, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, globalTarget), qt.IsTrue)
|
||||
})
|
||||
|
||||
c.Run("global sh wrapper resolves to JS", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(globalSh)
|
||||
t.Logf("resolveNodeBin(%q) = %q", globalSh, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, globalTarget), qt.IsTrue)
|
||||
})
|
||||
|
||||
globalScopedTarget := filepath.Join(globalDir, "node_modules", "@babel", "cli", "bin", "babel.js")
|
||||
mkdirAndWrite(t, globalScopedTarget, "#!/usr/bin/env node\nconsole.log('babel');\n")
|
||||
globalScopedCmd := filepath.Join(globalDir, "babel.cmd")
|
||||
mkdirAndWrite(t, globalScopedCmd, "@ECHO off\r\n"+
|
||||
`endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\node_modules\@babel\cli\bin\babel.js" %*`+"\r\n")
|
||||
|
||||
c.Run("global cmd wrapper scoped package", func(c *qt.C) {
|
||||
resolved := resolveNodeBin(globalScopedCmd)
|
||||
t.Logf("resolveNodeBin(%q) = %q", globalScopedCmd, resolved)
|
||||
c.Assert(resolved, qt.Not(qt.Equals), "")
|
||||
c.Assert(sameFile(t, resolved, globalScopedTarget), qt.IsTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func sameFile(t *testing.T, a, b string) bool {
|
||||
t.Helper()
|
||||
infoA, errA := os.Stat(a)
|
||||
infoB, errB := os.Stat(b)
|
||||
if errA != nil || errB != nil {
|
||||
t.Logf("sameFile: stat errors: a=%v, b=%v", errA, errB)
|
||||
return false
|
||||
}
|
||||
return os.SameFile(infoA, infoB)
|
||||
}
|
||||
|
||||
func mkdirAndWrite(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package himage provides some high level image types and interfaces.
|
||||
package himage
|
||||
|
||||
import "image"
|
||||
|
||||
// AnimatedImage represents an animated image.
|
||||
// This is currently supported for GIF and WebP images.
|
||||
type AnimatedImage interface {
|
||||
image.Image // The first frame.
|
||||
GetRaw() any // *gif.GIF or *WEBP.
|
||||
GetLoopCount() int // Number of times to loop the animation. 0 means infinite.
|
||||
ImageFrames
|
||||
}
|
||||
|
||||
// ImageFrames provides access to the frames of an animated image.
|
||||
type ImageFrames interface {
|
||||
GetFrames() []image.Image
|
||||
|
||||
// Frame durations in milliseconds.
|
||||
// Note that Gif frame durations are in 100ths of a second,
|
||||
// so they need to be multiplied by 10 to get milliseconds and vice versa.
|
||||
GetFrameDurations() []int
|
||||
|
||||
SetFrames(frames []image.Image)
|
||||
SetWidthHeight(width, height int)
|
||||
}
|
||||
|
||||
// ImageConfigProvider provides access to the image.Config of an image.
|
||||
type ImageConfigProvider interface {
|
||||
GetImageConfig() image.Config
|
||||
}
|
||||
|
||||
// FrameDurationsToGifDelays converts frame durations in milliseconds to
|
||||
// GIF delays in 100ths of a second.
|
||||
func FrameDurationsToGifDelays(frameDurations []int) []int {
|
||||
delays := make([]int, len(frameDurations))
|
||||
for i, fd := range frameDurations {
|
||||
delays[i] = fd / 10
|
||||
if delays[i] == 0 && fd > 0 {
|
||||
delays[i] = 1
|
||||
}
|
||||
}
|
||||
return delays
|
||||
}
|
||||
|
||||
// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to
|
||||
// frame durations in milliseconds.
|
||||
func GifDelaysToFrameDurations(delays []int) []int {
|
||||
frameDurations := make([]int, len(delays))
|
||||
for i, d := range delays {
|
||||
frameDurations[i] = d * 10
|
||||
}
|
||||
return frameDurations
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"slices"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
package hmaps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -96,7 +96,7 @@ func errConvert(v reflect.Value, s string) error {
|
||||
// See Issue 14079.
|
||||
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
|
||||
switch val.Kind() {
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
case reflect.Pointer, reflect.Interface:
|
||||
if val.IsNil() {
|
||||
// Return typ's zero value.
|
||||
return reflect.Zero(typ), true
|
||||
|
||||
+10
-10
@@ -20,14 +20,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
// IsInterfaceOrPointer returns whether the given kind is an interface or a pointer.
|
||||
func IsInterfaceOrPointer(kind reflect.Kind) bool {
|
||||
return kind == reflect.Interface || kind == reflect.Ptr
|
||||
return kind == reflect.Interface || kind == reflect.Pointer
|
||||
}
|
||||
|
||||
// TODO(bep) replace the private versions in /tpl with these.
|
||||
@@ -92,7 +92,7 @@ func IsSlice(v any) bool {
|
||||
return reflect.ValueOf(v).Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
|
||||
var zeroType = reflect.TypeFor[types.Zeroer]()
|
||||
|
||||
var isZeroCache sync.Map
|
||||
|
||||
@@ -136,7 +136,7 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
@@ -216,8 +216,8 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
|
||||
}
|
||||
|
||||
var (
|
||||
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||
asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
|
||||
timeType = reflect.TypeFor[time.Time]()
|
||||
asTimeProviderType = reflect.TypeFor[htime.AsTimeProvider]()
|
||||
)
|
||||
|
||||
// IsTime returns whether tp is a time.Time type or if it can be converted into one
|
||||
@@ -240,7 +240,7 @@ func IsValid(v reflect.Value) bool {
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
return !v.IsNil()
|
||||
}
|
||||
|
||||
@@ -347,15 +347,15 @@ func IsNil(v reflect.Value) bool {
|
||||
return true
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
var contextInterface = reflect.TypeFor[context.Context]()
|
||||
|
||||
var isContextCache = maps.NewCache[reflect.Type, bool]()
|
||||
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
|
||||
|
||||
type k string
|
||||
|
||||
|
||||
@@ -97,6 +97,25 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
return reCache.getOrCompileRegexp(pattern)
|
||||
}
|
||||
|
||||
// HasAnyPrefix checks if the string s has any of the prefixes given.
|
||||
func HasAnyPrefix(s string, prefixes ...string) bool {
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(s, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasUppercase(s string) bool {
|
||||
for _, r := range s {
|
||||
if 'A' <= r && r <= 'Z' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
|
||||
@@ -71,6 +71,16 @@ func TestUniqueStringsSorted(t *testing.T) {
|
||||
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
func TestHasUppercase(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(HasUppercase("abc"), qt.Equals, false)
|
||||
c.Assert(HasUppercase("Abc"), qt.Equals, true)
|
||||
c.Assert(HasUppercase("aBc"), qt.Equals, true)
|
||||
c.Assert(HasUppercase("abC"), qt.Equals, true)
|
||||
c.Assert(HasUppercase("ABC"), qt.Equals, true)
|
||||
}
|
||||
|
||||
// Note that these cannot use b.Loop() because of golang/go#27217.
|
||||
func BenchmarkUniqueStrings(b *testing.B) {
|
||||
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
|
||||
|
||||
@@ -24,9 +24,9 @@ func TestApplyWithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
-- hugo.toml --
|
||||
defaultContentLanguage = 'it'
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
{{ $dates := slice
|
||||
"2022-01-03"
|
||||
"2022-02-01"
|
||||
|
||||
+18
-18
@@ -18,10 +18,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/golocales"
|
||||
|
||||
"github.com/bep/clocks"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -78,9 +78,9 @@ var (
|
||||
Clock = clocks.System()
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
|
||||
if ltr == nil {
|
||||
panic("must provide a locales.Translator")
|
||||
panic("must provide a golocales.Translator")
|
||||
}
|
||||
return TimeFormatter{
|
||||
ltr: ltr,
|
||||
@@ -89,7 +89,7 @@ func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
|
||||
// TimeFormatter is locale aware.
|
||||
type TimeFormatter struct {
|
||||
ltr locales.Translator
|
||||
ltr golocales.Translator
|
||||
}
|
||||
|
||||
func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
@@ -101,39 +101,39 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
// It may be one of Hugo's custom layouts.
|
||||
switch strings.ToLower(layout[1:]) {
|
||||
case "date_full":
|
||||
return f.ltr.FmtDateFull(t)
|
||||
return f.ltr.FormatDateFull(t)
|
||||
case "date_long":
|
||||
return f.ltr.FmtDateLong(t)
|
||||
return f.ltr.FormatDateLong(t)
|
||||
case "date_medium":
|
||||
return f.ltr.FmtDateMedium(t)
|
||||
return f.ltr.FormatDateMedium(t)
|
||||
case "date_short":
|
||||
return f.ltr.FmtDateShort(t)
|
||||
return f.ltr.FormatDateShort(t)
|
||||
case "time_full":
|
||||
return f.ltr.FmtTimeFull(t)
|
||||
return f.ltr.FormatTimeFull(t)
|
||||
case "time_long":
|
||||
return f.ltr.FmtTimeLong(t)
|
||||
return f.ltr.FormatTimeLong(t)
|
||||
case "time_medium":
|
||||
return f.ltr.FmtTimeMedium(t)
|
||||
return f.ltr.FormatTimeMedium(t)
|
||||
case "time_short":
|
||||
return f.ltr.FmtTimeShort(t)
|
||||
return f.ltr.FormatTimeShort(t)
|
||||
}
|
||||
}
|
||||
|
||||
s := t.Format(layout)
|
||||
|
||||
monthIdx := t.Month() - 1 // Month() starts at 1.
|
||||
monthIdx := t.Month() - 1 // time.Month is 1-based, but our month name slices are 0-based.
|
||||
dayIdx := t.Weekday()
|
||||
|
||||
if strings.Contains(layout, "January") {
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthsWide()[monthIdx])
|
||||
} else if strings.Contains(layout, "Jan") {
|
||||
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
|
||||
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthsAbbreviated()[monthIdx])
|
||||
}
|
||||
|
||||
if strings.Contains(layout, "Monday") {
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdaysWide()[dayIdx])
|
||||
} else if strings.Contains(layout, "Mon") {
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdaysAbbreviated()[dayIdx])
|
||||
}
|
||||
|
||||
return s
|
||||
|
||||
+19
-18
@@ -17,8 +17,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bep/golocales"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
translators "github.com/gohugoio/localescompressed"
|
||||
)
|
||||
|
||||
func TestTimeFormatter(t *testing.T) {
|
||||
@@ -34,7 +35,7 @@ func TestTimeFormatter(t *testing.T) {
|
||||
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
|
||||
|
||||
c.Run("Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
|
||||
@@ -42,35 +43,35 @@ func TestTimeFormatter(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.18")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
|
||||
})
|
||||
|
||||
c.Run("Custom layouts English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
f := NewTimeFormatter(golocales.New("en"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37\u202fam UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37\u202fam UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37\u202fam")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09\u202fam")
|
||||
})
|
||||
|
||||
c.Run("English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
f := NewTimeFormatter(golocales.New("en"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
|
||||
@@ -78,31 +79,31 @@ func TestTimeFormatter(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("Weekdays German", func(c *qt.C) {
|
||||
tr := translators.GetTranslator("de")
|
||||
tr := golocales.New("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman)
|
||||
c.Assert(tr.WeekdaysWide()[date.Weekday()], qt.Equals, weekDayWideGerman)
|
||||
c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman)
|
||||
}
|
||||
|
||||
for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
|
||||
c.Assert(tr.WeekdaysAbbreviated()[date.Weekday()], qt.Equals, weekDayAbbreviatedGerman)
|
||||
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
|
||||
}
|
||||
})
|
||||
|
||||
c.Run("Months German", func(c *qt.C) {
|
||||
tr := translators.GetTranslator("de")
|
||||
tr := golocales.New("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} {
|
||||
date := jan06.Add(time.Duration(i*24*31) * time.Hour)
|
||||
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
|
||||
c.Assert(tr.MonthsWide()[date.Month()-1], qt.Equals, monthWideNorway)
|
||||
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
|
||||
}
|
||||
})
|
||||
@@ -121,7 +122,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("Localized", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
got := f.Format(june06, "Monday Jan 2 2006")
|
||||
@@ -132,7 +133,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("Localized Custom", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
f := NewTimeFormatter(golocales.New("nn"))
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
got := f.Format(june06, ":date_medium")
|
||||
|
||||
+43
-9
@@ -19,19 +19,37 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadSeeker wraps io.Reader and io.Seeker.
|
||||
type ReadSeeker interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
}
|
||||
|
||||
// ReadSeekCloser is implemented by afero.File. We use this as the common type for
|
||||
// content in Resource objects, even for strings.
|
||||
type ReadSeekCloser interface {
|
||||
ReadSeeker
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Sizer provides the size of, typically, a io.Reader.
|
||||
// As implemented by e.g. os.File and io.SectionReader.
|
||||
type Sizer interface {
|
||||
Size() int64
|
||||
}
|
||||
|
||||
type SizeReader interface {
|
||||
io.Reader
|
||||
Sizer
|
||||
}
|
||||
|
||||
// ToSizeReader converts the given io.Reader to a SizeReader.
|
||||
// Note that if r is not a SizeReader, the entire content will be read into memory
|
||||
func ToSizeReader(r io.Reader) (SizeReader, error) {
|
||||
if sr, ok := r.(SizeReader); ok {
|
||||
return sr, nil
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(b), nil
|
||||
}
|
||||
|
||||
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
|
||||
type CloserFunc func() error
|
||||
|
||||
@@ -46,7 +64,7 @@ type ReadSeekCloserProvider interface {
|
||||
|
||||
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
type readSeekerNopCloser struct {
|
||||
ReadSeeker
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
// Close does nothing.
|
||||
@@ -55,7 +73,7 @@ func (r readSeekerNopCloser) Close() error {
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
|
||||
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
|
||||
func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
|
||||
return readSeekerNopCloser{r}
|
||||
}
|
||||
|
||||
@@ -87,6 +105,22 @@ func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
|
||||
return readSeekerNopCloser{bytes.NewReader(content)}
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromReader creates a new ReadSeekerNoOpCloser from the given io.Reader.
|
||||
// If the given io.Reader is not an io.ReadSeeker, the entire content will be read into memory.
|
||||
func NewReadSeekerNoOpCloserFromReader(r io.Reader) (readSeekerNopCloser, error) {
|
||||
var rs io.ReadSeeker
|
||||
if s, ok := r.(io.ReadSeeker); ok {
|
||||
rs = s
|
||||
} else {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return readSeekerNopCloser{rs}, err
|
||||
}
|
||||
rs = bytes.NewReader(b)
|
||||
}
|
||||
return readSeekerNopCloser{rs}, nil
|
||||
}
|
||||
|
||||
// NewOpenReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
|
||||
// The ReadSeeker will be seeked to the beginning before returned.
|
||||
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
|
||||
|
||||
+25
-125
@@ -16,7 +16,6 @@ package hugo
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
@@ -25,17 +24,16 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/helpers/contexthelpers"
|
||||
"github.com/bep/logg"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/hstore"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/version"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/bep/helpers/contexthelpers"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
iofs "io/fs"
|
||||
@@ -56,89 +54,24 @@ var (
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
var _ hstore.StoreProvider = (*HugoInfo)(nil)
|
||||
|
||||
// HugoInfo contains information about the current Hugo environment
|
||||
type HugoInfo struct {
|
||||
CommitHash string
|
||||
BuildDate string
|
||||
|
||||
// The build environment.
|
||||
// Defaults are "production" (hugo) and "development" (hugo server).
|
||||
// This can also be set by the user.
|
||||
// It can be any string, but it will be all lower case.
|
||||
Environment string
|
||||
|
||||
// version of go that the Hugo binary was built with
|
||||
GoVersion string
|
||||
|
||||
conf ConfigProvider
|
||||
deps []*Dependency
|
||||
|
||||
store *hstore.Scratch
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
Context Context
|
||||
// BuildInfo holds build information extracted from runtime/debug.
|
||||
type BuildInfo struct {
|
||||
Revision string
|
||||
RevisionTime string
|
||||
GoVersion string
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
func (i HugoInfo) Version() version.VersionString {
|
||||
return CurrentVersion.Version()
|
||||
}
|
||||
|
||||
// Generator a Hugo meta generator HTML tag.
|
||||
func (i HugoInfo) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
|
||||
}
|
||||
|
||||
// IsDevelopment reports whether the current running environment is "development".
|
||||
func (i HugoInfo) IsDevelopment() bool {
|
||||
return i.Environment == EnvironmentDevelopment
|
||||
}
|
||||
|
||||
// IsProduction reports whether the current running environment is "production".
|
||||
func (i HugoInfo) IsProduction() bool {
|
||||
return i.Environment == EnvironmentProduction
|
||||
}
|
||||
|
||||
// IsServer reports whether the built-in server is running.
|
||||
func (i HugoInfo) IsServer() bool {
|
||||
return i.conf.Running()
|
||||
}
|
||||
|
||||
// IsExtended reports whether the Hugo binary is the extended version.
|
||||
func (i HugoInfo) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
|
||||
// WorkingDir returns the project working directory.
|
||||
func (i HugoInfo) WorkingDir() string {
|
||||
return i.conf.WorkingDir()
|
||||
}
|
||||
|
||||
// Deps gets a list of dependencies for this Hugo build.
|
||||
func (i HugoInfo) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
func (i HugoInfo) Store() *hstore.Scratch {
|
||||
return i.store
|
||||
}
|
||||
|
||||
// Deprecated: Use hugo.IsMultihost instead.
|
||||
func (i HugoInfo) IsMultiHost() bool {
|
||||
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
|
||||
return i.conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultihost reports whether each configured language has a unique baseURL.
|
||||
func (i HugoInfo) IsMultihost() bool {
|
||||
return i.conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultilingual reports whether there are two or more configured languages.
|
||||
func (i HugoInfo) IsMultilingual() bool {
|
||||
return i.conf.IsMultilingual()
|
||||
// GetBuildInfo returns the build info for the current binary.
|
||||
func GetBuildInfo() *BuildInfo {
|
||||
bi := getBuildInfo()
|
||||
if bi == nil {
|
||||
return nil
|
||||
}
|
||||
return &BuildInfo{
|
||||
Revision: bi.Revision,
|
||||
RevisionTime: bi.RevisionTime,
|
||||
GoVersion: bi.GoVersion,
|
||||
}
|
||||
}
|
||||
|
||||
type contextKey uint8
|
||||
@@ -149,6 +82,7 @@ const (
|
||||
|
||||
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
type Context struct{}
|
||||
|
||||
func (c Context) MarkupScope(ctx context.Context) string {
|
||||
@@ -165,51 +99,13 @@ func GetMarkupScope(ctx context.Context) string {
|
||||
return markupScope.Get(ctx)
|
||||
}
|
||||
|
||||
// ConfigProvider represents the config options that are relevant for HugoInfo.
|
||||
type ConfigProvider interface {
|
||||
Environment() string
|
||||
Running() bool
|
||||
WorkingDir() string
|
||||
IsMultihost() bool
|
||||
IsMultilingual() bool
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
|
||||
if conf.Environment() == "" {
|
||||
panic("environment not set")
|
||||
}
|
||||
var (
|
||||
commitHash string
|
||||
buildDate string
|
||||
goVersion string
|
||||
)
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
commitHash = bi.Revision
|
||||
buildDate = bi.RevisionTime
|
||||
goVersion = bi.GoVersion
|
||||
}
|
||||
|
||||
return HugoInfo{
|
||||
CommitHash: commitHash,
|
||||
BuildDate: buildDate,
|
||||
Environment: conf.Environment(),
|
||||
conf: conf,
|
||||
deps: deps,
|
||||
store: hstore.NewScratch(),
|
||||
GoVersion: goVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// GetExecEnviron creates and gets the common os/exec environment used in the
|
||||
// external programs we interact with via os/exec, e.g. postcss.
|
||||
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
|
||||
var env []string
|
||||
nodepath := filepath.Join(workDir, "node_modules")
|
||||
if np := os.Getenv("NODE_PATH"); np != "" {
|
||||
nodepath = workDir + string(os.PathListSeparator) + np
|
||||
nodepath = nodepath + string(os.PathListSeparator) + np
|
||||
}
|
||||
config.SetEnvVars(&env, "NODE_PATH", nodepath)
|
||||
config.SetEnvVars(&env, "PWD", workDir)
|
||||
@@ -310,13 +206,12 @@ func GetDependencyList() []string {
|
||||
|
||||
// GetDependencyListNonGo returns a list of non-Go dependencies.
|
||||
func GetDependencyListNonGo() []string {
|
||||
var deps []string
|
||||
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.6"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -439,8 +334,13 @@ func deprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
|
||||
// DeprecateLevel informs about a deprecation logging at the given level.
|
||||
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) {
|
||||
//if strings.Contains(item, "module.mounts.lang") || strings.Contains(item, "includeFiles") {
|
||||
// hdebug.Panicf("Deprecated")
|
||||
//}
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
// Useful to debug deprecation errors that needs to be removedor fixed. Comment out when done debugging.
|
||||
// hdebug.Panicf("deprecation error: %s was removed in Hugo %s. %s", item, version, alternative)
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
|
||||
} else {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
|
||||
|
||||
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
|
||||
---
|
||||
title: home
|
||||
---
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
multilingual={{ hugo.IsMultilingual }}
|
||||
multihost={{ hugo.IsMultihost }}
|
||||
`
|
||||
@@ -75,3 +75,39 @@ multihost={{ hugo.IsMultihost }}
|
||||
"multihost=false",
|
||||
)
|
||||
}
|
||||
|
||||
func TestHugoSites(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
defaultContentLanguage = 'fr'
|
||||
defaultContentLanguageInSubdir = true
|
||||
defaultContentVersionInSubdir = true
|
||||
defaultContentRoleInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
[languages.de]
|
||||
weight = 3
|
||||
[roles]
|
||||
[roles.guest]
|
||||
weight = 1
|
||||
[roles.member]
|
||||
weight = 2
|
||||
[versions]
|
||||
[versions.'v1.0.0']
|
||||
weight = 1
|
||||
[versions.'v2.0.0']
|
||||
weight = 2
|
||||
-- layouts/home.html --
|
||||
{{ range hugo.Sites }}{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}|{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/guest/v1.0.0/en/index.html", "en-guest-v1.0.0|en-member-v1.0.0|en-guest-v2.0.0|en-member-v2.0.0|fr-guest-v1.0.0|fr-member-v1.0.0|fr-guest-v2.0.0|fr-member-v2.0.0|de-guest-v1.0.0|de-member-v1.0.0|de-guest-v2.0.0|de-member-v2.0.0|")
|
||||
}
|
||||
|
||||
+12
-63
@@ -15,43 +15,12 @@ package hugo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/version"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
hugoInfo := NewInfo(conf, nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
|
||||
|
||||
bi := getBuildInfo()
|
||||
if bi != nil {
|
||||
c.Assert(hugoInfo.CommitHash, qt.Equals, bi.Revision)
|
||||
c.Assert(hugoInfo.BuildDate, qt.Equals, bi.RevisionTime)
|
||||
c.Assert(hugoInfo.GoVersion, qt.Equals, bi.GoVersion)
|
||||
}
|
||||
c.Assert(hugoInfo.Environment, qt.Equals, "production")
|
||||
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false)
|
||||
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
|
||||
|
||||
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
|
||||
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
@@ -66,47 +35,27 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
|
||||
// Added just to find the threshold for where we can remove deprecated items.
|
||||
// Subtract 5 from the minor version of the first ERRORed version => 0.122.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.127.0"), qt.Equals, logg.LevelError)
|
||||
// Subtract 5 from the minor version of the first ERRORed version => 0.136.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.141.0"), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
func TestMarkupScope(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
info := NewInfo(conf, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
ctx = SetMarkupScope(ctx, "foo")
|
||||
|
||||
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
|
||||
var hugoCtx Context
|
||||
c.Assert(hugoCtx.MarkupScope(ctx), qt.Equals, "foo")
|
||||
c.Assert(GetMarkupScope(ctx), qt.Equals, "foo")
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
environment string
|
||||
running bool
|
||||
workingDir string
|
||||
multihost bool
|
||||
multilingual bool
|
||||
}
|
||||
func TestGetBuildInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
func (c testConfig) Environment() string {
|
||||
return c.environment
|
||||
}
|
||||
|
||||
func (c testConfig) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c testConfig) WorkingDir() string {
|
||||
return c.workingDir
|
||||
}
|
||||
|
||||
func (c testConfig) IsMultihost() bool {
|
||||
return c.multihost
|
||||
}
|
||||
|
||||
func (c testConfig) IsMultilingual() bool {
|
||||
return c.multilingual
|
||||
bi := GetBuildInfo()
|
||||
// In test mode, build info may or may not be available.
|
||||
if bi != nil {
|
||||
c.Assert(bi.GoVersion, qt.Not(qt.Equals), "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import "github.com/gohugoio/hugo/common/version"
|
||||
// This should be the only one.
|
||||
var CurrentVersion = version.Version{
|
||||
Major: 0,
|
||||
Minor: 153,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
Minor: 161,
|
||||
PatchLevel: 1,
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
@@ -123,8 +124,11 @@ func New(opts Options) Logger {
|
||||
)
|
||||
|
||||
l := logger.WithLevel(opts.Level)
|
||||
logMu := &sync.Mutex{}
|
||||
|
||||
reset := func() {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
logCounters.mu.Lock()
|
||||
defer logCounters.mu.Unlock()
|
||||
logCounters.counters = make(map[logg.Level]int)
|
||||
@@ -135,6 +139,7 @@ func New(opts Options) Logger {
|
||||
}
|
||||
|
||||
return &logAdapter{
|
||||
mu: logMu,
|
||||
logCounters: logCounters,
|
||||
errors: errorsw,
|
||||
reset: reset,
|
||||
@@ -173,6 +178,7 @@ func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
|
||||
|
||||
type Logger interface {
|
||||
Debug() logg.LevelLogger
|
||||
DebugCommand(command string) logg.LevelLogger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Error() logg.LevelLogger
|
||||
@@ -203,6 +209,7 @@ type Logger interface {
|
||||
}
|
||||
|
||||
type logAdapter struct {
|
||||
mu *sync.Mutex
|
||||
logCounters *logLevelCounter
|
||||
errors *strings.Builder
|
||||
reset func()
|
||||
@@ -221,6 +228,10 @@ func (l *logAdapter) Debug() logg.LevelLogger {
|
||||
return l.debugl
|
||||
}
|
||||
|
||||
func (l *logAdapter) DebugCommand(command string) logg.LevelLogger {
|
||||
return l.debugl.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugf(format string, v ...any) {
|
||||
l.debugl.Logf(format, v...)
|
||||
}
|
||||
@@ -275,7 +286,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli)
|
||||
fmt.Fprintf(l.stdErr, "%s in %v ms\n", name, milli)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Printf(format string, v ...any) {
|
||||
@@ -323,6 +334,8 @@ func (l *logAdapter) Errorln(v ...any) {
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errors() string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.errors.String()
|
||||
}
|
||||
|
||||
@@ -339,7 +352,7 @@ func (l *logAdapter) Warnidf(id, format string, v ...any) {
|
||||
}
|
||||
|
||||
func (l *logAdapter) idfInfoStatement(what, id, format string) string {
|
||||
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
|
||||
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your project configuration:\nignoreLogs = ['%s']", what, id)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Trace(s logg.StringFunc) {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewMap[K comparable, T any]() *Map[K, T] {
|
||||
return &Map[K, T]{
|
||||
m: make(map[K]T),
|
||||
}
|
||||
}
|
||||
|
||||
// Map is a thread safe map backed by a Go map.
|
||||
type Map[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Get gets the value for the given key.
|
||||
// It returns the zero value of T if the key is not found.
|
||||
func (m *Map[K, T]) Get(key K) T {
|
||||
v, _ := m.Lookup(key)
|
||||
return v
|
||||
}
|
||||
|
||||
// Lookup looks up the given key in the map.
|
||||
// It returns the value and a boolean indicating whether the key was found.
|
||||
func (m *Map[K, T]) Lookup(key K) (T, bool) {
|
||||
m.mu.RLock()
|
||||
v, found := m.m[key]
|
||||
m.mu.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
|
||||
v, found := m.Lookup(key)
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
v, found = m.m[key]
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
v, err := create()
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
m.m[key] = v
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Set sets the given key to the given value.
|
||||
func (m *Map[K, T]) Set(key K, value T) {
|
||||
m.mu.Lock()
|
||||
m.m[key] = value
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// WithWriteLock executes the given function with a write lock on the map.
|
||||
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
f(m.m)
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
|
||||
// It returns true if the value was set, false otherwise.
|
||||
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
|
||||
m.mu.RLock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.mu.RUnlock()
|
||||
return m.doSetIfAbsent(key, value)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.m[key] = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// All returns an iterator over all key/value pairs in the map.
|
||||
// A read lock is held during the iteration.
|
||||
func (m *Map[K, T]) All() iter.Seq2[K, T] {
|
||||
return func(yield func(K, T) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for k, v := range m.m {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
m := NewMap[string, int]()
|
||||
|
||||
m.Set("b", 42)
|
||||
v, found := m.Lookup("b")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v = m.Get("b")
|
||||
c.Assert(v, qt.Equals, 42)
|
||||
v, found = m.Lookup("c")
|
||||
c.Assert(found, qt.Equals, false)
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v = m.Get("c")
|
||||
c.Assert(v, qt.Equals, 0)
|
||||
v, err := m.GetOrCreate("d", func() (int, error) {
|
||||
return 100, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
v, found = m.Lookup("d")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
v, err = m.GetOrCreate("d", func() (int, error) {
|
||||
return 200, nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, 100)
|
||||
|
||||
wasSet := m.SetIfAbsent("e", 300)
|
||||
c.Assert(wasSet, qt.Equals, true)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
wasSet = m.SetIfAbsent("e", 400)
|
||||
c.Assert(wasSet, qt.Equals, false)
|
||||
v, found = m.Lookup("e")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 300)
|
||||
|
||||
m.WithWriteLock(func(m map[string]int) {
|
||||
m["f"] = 500
|
||||
})
|
||||
v, found = m.Lookup("f")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 500)
|
||||
}
|
||||
@@ -100,19 +100,6 @@ func AddLeadingAndTrailingSlash(path string) string {
|
||||
return AddTrailingSlash(AddLeadingSlash(path))
|
||||
}
|
||||
|
||||
// MakeTitle converts the path given to a suitable title, trimming whitespace
|
||||
// and replacing hyphens with whitespace.
|
||||
func MakeTitle(inpath string) string {
|
||||
return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
|
||||
}
|
||||
|
||||
// ReplaceExtension takes a path and an extension, strips the old extension
|
||||
// and returns the path with the new extension.
|
||||
func ReplaceExtension(path string, newExt string) string {
|
||||
f, _ := fileAndExt(path, fpb)
|
||||
return f + "." + newExt
|
||||
}
|
||||
|
||||
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
|
||||
for _, currentPath := range possibleDirectories {
|
||||
if after, ok := strings.CutPrefix(inPath, currentPath); ok {
|
||||
@@ -201,75 +188,6 @@ func extractFilename(in, ext, base, pathSeparator string) (name string) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetRelativePath returns the relative path of a given path.
|
||||
func GetRelativePath(path, base string) (final string, err error) {
|
||||
if filepath.IsAbs(path) && base == "" {
|
||||
return "", errors.New("source: missing base directory")
|
||||
}
|
||||
name := filepath.Clean(path)
|
||||
base = filepath.Clean(base)
|
||||
|
||||
name, err = filepath.Rel(base, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
|
||||
name += FilePathSeparator
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func prettifyPath(in string, b filepathPathBridge) string {
|
||||
if filepath.Ext(in) == "" {
|
||||
// /section/name/ -> /section/name/index.html
|
||||
if len(in) < 2 {
|
||||
return b.Separator()
|
||||
}
|
||||
return b.Join(in, "index.html")
|
||||
}
|
||||
name, ext := fileAndExt(in, b)
|
||||
if name == "index" {
|
||||
// /section/name/index.html -> /section/name/index.html
|
||||
return b.Clean(in)
|
||||
}
|
||||
// /section/name.html -> /section/name/index.html
|
||||
return b.Join(b.Dir(in), name, "index"+ext)
|
||||
}
|
||||
|
||||
// CommonDirPath returns the common directory of the given paths.
|
||||
func CommonDirPath(path1, path2 string) string {
|
||||
if path1 == "" || path2 == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
hadLeadingSlash := strings.HasPrefix(path1, "/") || strings.HasPrefix(path2, "/")
|
||||
|
||||
path1 = TrimLeading(path1)
|
||||
path2 = TrimLeading(path2)
|
||||
|
||||
p1 := strings.Split(path1, "/")
|
||||
p2 := strings.Split(path2, "/")
|
||||
|
||||
var common []string
|
||||
|
||||
for i := 0; i < len(p1) && i < len(p2); i++ {
|
||||
if p1[i] == p2[i] {
|
||||
common = append(common, p1[i])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s := strings.Join(common, "/")
|
||||
|
||||
if hadLeadingSlash && s != "" {
|
||||
s = "/" + s
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
|
||||
// a predefined set of special Unicode characters.
|
||||
//
|
||||
|
||||
+22
-100
@@ -20,38 +20,6 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestGetRelativePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
base string
|
||||
expect any
|
||||
}{
|
||||
{filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
|
||||
{filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
|
||||
{filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")},
|
||||
{filepath.FromSlash("/c"), "", false},
|
||||
}
|
||||
for i, this := range tests {
|
||||
// ultimately a fancy wrapper around filepath.Rel
|
||||
result, err := GetRelativePath(this.path, this.base)
|
||||
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] GetRelativePath didn't return an expected error", i)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("[%d] GetRelativePath failed: %s", i, err)
|
||||
continue
|
||||
}
|
||||
if result != this.expect {
|
||||
t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePathRelative(t *testing.T) {
|
||||
type test struct {
|
||||
inPath, path1, path2, output string
|
||||
@@ -75,54 +43,6 @@ func TestMakePathRelative(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTitle(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"Make-Title", "Make Title"},
|
||||
{"MakeTitle", "MakeTitle"},
|
||||
{"make_title", "make_title"},
|
||||
}
|
||||
for i, d := range data {
|
||||
output := MakeTitle(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace Extension is probably poorly named, but the intent of the
|
||||
// function is to accept a path and return only the file name with a
|
||||
// new extension. It's intentionally designed to strip out the path
|
||||
// and only provide the name. We should probably rename the function to
|
||||
// be more explicit at some point.
|
||||
func TestReplaceExtension(t *testing.T) {
|
||||
type test struct {
|
||||
input, newext, expected string
|
||||
}
|
||||
data := []test{
|
||||
// These work according to the above definition
|
||||
{"/some/random/path/file.xml", "html", "file.html"},
|
||||
{"/banana.html", "xml", "banana.xml"},
|
||||
{"./banana.html", "xml", "banana.xml"},
|
||||
{"banana/pie/index.html", "xml", "index.xml"},
|
||||
{"../pies/fish/index.html", "xml", "index.xml"},
|
||||
// but these all fail
|
||||
{"filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
|
||||
{"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
|
||||
{"/directory/mydir/", "ext", ".ext"},
|
||||
{"mydir/", "ext", ".ext"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := ReplaceExtension(filepath.FromSlash(d.input), d.newext)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtNoDelimiter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json")
|
||||
@@ -263,26 +183,6 @@ func TestFieldsSlash(t *testing.T) {
|
||||
c.Assert(FieldsSlash(""), qt.DeepEquals, []string{})
|
||||
}
|
||||
|
||||
func TestCommonDirPath(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, this := range []struct {
|
||||
a, b, expected string
|
||||
}{
|
||||
{"/a/b/c", "/a/b/d", "/a/b"},
|
||||
{"/a/b/c", "a/b/d", "/a/b"},
|
||||
{"a/b/c", "/a/b/d", "/a/b"},
|
||||
{"a/b/c", "a/b/d", "a/b"},
|
||||
{"/a/b/c", "/a/b/c", "/a/b/c"},
|
||||
{"/a/b/c", "/a/b/c/d", "/a/b/c"},
|
||||
{"/a/b/c", "/a/b", "/a/b"},
|
||||
{"/a/b/c", "/a", "/a"},
|
||||
{"/a/b/c", "/d/e/f", ""},
|
||||
} {
|
||||
c.Assert(CommonDirPath(this.a, this.b), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSameFilePath(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
@@ -347,3 +247,25 @@ func BenchmarkAddLeadingSlash(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPathEscape(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for _, this := range []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"/tags/欢迎", "/tags/%E6%AC%A2%E8%BF%8E"},
|
||||
{"/path with spaces", "/path%20with%20spaces"},
|
||||
{"/simple-path", "/simple-path"},
|
||||
{"/path/with/slash", "/path/with/slash"},
|
||||
{"/path/with special&chars", "/path/with%20special&chars"},
|
||||
} {
|
||||
in := this.input
|
||||
for range 2 {
|
||||
result := PathEscape(in)
|
||||
c.Assert(result, qt.Equals, this.expected, qt.Commentf("input: %q", this.input))
|
||||
in = result // test idempotency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+286
-60
@@ -21,19 +21,165 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
)
|
||||
|
||||
const (
|
||||
identifierBaseof = "baseof"
|
||||
identifierCurstomWrapper = "_"
|
||||
identifierBaseof = "baseof"
|
||||
identifierCustomWrapper = "_"
|
||||
)
|
||||
|
||||
// Known prefixes for ._prefix_value_. identifiers.
|
||||
const (
|
||||
prefixLanguage = "language_"
|
||||
prefixVersion = "version_"
|
||||
prefixRole = "role_"
|
||||
prefixOutputFormat = "outputformat_"
|
||||
prefixKind = "kind_"
|
||||
prefixLayout = "layout_"
|
||||
)
|
||||
|
||||
// isCustomWrapperIdentifier tells whether a supplied path is of the form _xyz_.
|
||||
// must have non-empty content between the identifierCustomWrapper's to pass.
|
||||
func isCustomWrapperIdentifier(s string) bool {
|
||||
return len(s) > 2*len(identifierCustomWrapper) &&
|
||||
strings.HasPrefix(s, identifierCustomWrapper) &&
|
||||
strings.HasSuffix(s, identifierCustomWrapper)
|
||||
}
|
||||
|
||||
// parsePrefixIdentifier parses inner content of a wrapper block (with outer underscores stripped)
|
||||
// and returns the prefix. The prefix match is case-insensitive.
|
||||
// Returns empty string if not a known prefix.
|
||||
func parsePrefixIdentifier(inner string) string {
|
||||
innerLower := strings.ToLower(inner)
|
||||
for _, p := range []string{prefixLanguage, prefixVersion, prefixRole, prefixOutputFormat, prefixKind, prefixLayout} {
|
||||
if strings.HasPrefix(innerLower, p) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// findWrapperDotPositions returns positions of dots inside ._..._. wrapper blocks.
|
||||
// These dots should be skipped during the main backward dot-scanning loop,
|
||||
// so that wrapper blocks are treated as single opaque segments.
|
||||
func findWrapperDotPositions(s string) []int {
|
||||
lastSlash := strings.LastIndex(s, "/")
|
||||
if lastSlash == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var skipDots []int
|
||||
i := lastSlash
|
||||
for i < len(s) {
|
||||
// Look for ._ which could start a wrapper block.
|
||||
if i+2 < len(s) && s[i] == '.' && s[i+1] == '_' {
|
||||
// Find the closing _. or _ at end of string.
|
||||
end := -1
|
||||
for j := i + 2; j < len(s); j++ {
|
||||
if s[j] == '/' {
|
||||
break
|
||||
}
|
||||
if s[j] == '_' && (j+1 >= len(s) || s[j+1] == '.') {
|
||||
end = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if end != -1 {
|
||||
// Record dot positions inside the wrapper block.
|
||||
for j := i + 2; j < end; j++ {
|
||||
if s[j] == '.' {
|
||||
skipDots = append(skipDots, j)
|
||||
}
|
||||
}
|
||||
i = end + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return skipDots
|
||||
}
|
||||
|
||||
// isSkippedDot reports whether position i is in the sorted skipDots slice.
|
||||
func isSkippedDot(skipDots []int, i int) bool {
|
||||
for _, d := range skipDots {
|
||||
if d == i {
|
||||
return true
|
||||
}
|
||||
if d > i {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// applyPrefixIdentifier validates and stores a prefix identifier.
|
||||
// id is the value-only LowHigh (e.g. pointing at "v1.0.0" in p.s, not the full wrapper).
|
||||
// Returns true if the identifier was recognized and applied.
|
||||
func (pp *PathParser) applyPrefixIdentifier(component, prefix string, p *Path, id types.LowHigh[string]) bool {
|
||||
value := strings.ToLower(p.s[id.Low:id.High])
|
||||
switch prefix {
|
||||
case prefixLanguage:
|
||||
if pp.LanguageIndex != nil {
|
||||
if _, ok := pp.LanguageIndex[value]; ok {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierPrefixLanguages = append(p.posIdentifierPrefixLanguages, len(p.identifiersKnown)-1)
|
||||
return true
|
||||
}
|
||||
if pp.IsLangDisabled != nil && pp.IsLangDisabled(value) {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierPrefixLanguages = append(p.posIdentifierPrefixLanguages, len(p.identifiersKnown)-1)
|
||||
p.disabled = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
case prefixVersion:
|
||||
if pp.ConfiguredDimensions != nil {
|
||||
if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(value); idx >= 0 {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierVersions = append(p.posIdentifierVersions, len(p.identifiersKnown)-1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
case prefixRole:
|
||||
if pp.ConfiguredDimensions != nil {
|
||||
if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(value); idx >= 0 {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierRoles = append(p.posIdentifierRoles, len(p.identifiersKnown)-1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
case prefixOutputFormat:
|
||||
if component == files.ComponentFolderLayouts && pp.IsOutputFormat != nil {
|
||||
if pp.IsOutputFormat(value, "") {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierPrefixOutputFormat = len(p.identifiersKnown) - 1
|
||||
return true
|
||||
}
|
||||
}
|
||||
case prefixKind:
|
||||
if component == files.ComponentFolderLayouts {
|
||||
if kinds.GetKindMain(value) != "" {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierPrefixKind = len(p.identifiersKnown) - 1
|
||||
return true
|
||||
}
|
||||
}
|
||||
case prefixLayout:
|
||||
if component == files.ComponentFolderLayouts {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierPrefixLayout = len(p.identifiersKnown) - 1
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PathParser parses and manages paths.
|
||||
type PathParser struct {
|
||||
// Maps the language code to its index in the languages/sites slice.
|
||||
@@ -54,12 +200,12 @@ type PathParser struct {
|
||||
|
||||
// Below gets created on demand.
|
||||
initOnce sync.Once
|
||||
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
|
||||
sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
|
||||
}
|
||||
|
||||
func (pp *PathParser) init() {
|
||||
pp.initOnce.Do(func() {
|
||||
pp.sitesMatrixCache = maps.NewCache[string, sitesmatrix.VectorStore]()
|
||||
pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,14 +222,36 @@ func NormalizePathStringBasic(s string) string {
|
||||
|
||||
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
|
||||
pp.init()
|
||||
lang := p.Lang()
|
||||
v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
|
||||
langs := p.Langs()
|
||||
versions := p.Versions()
|
||||
roles := p.Roles()
|
||||
lang := p.Lang() // First or dot-based language.
|
||||
// Cache by the full site-selection identity derived from the path:
|
||||
// languages, selected language, versions, and roles.
|
||||
cacheKey := strings.Join(langs, ",") + "/" + lang + "/" + strings.Join(versions, ",") + "|" + strings.Join(roles, ",")
|
||||
v, _ := pp.sitesMatrixCache.GetOrCreate(cacheKey, func() (sitesmatrix.VectorStore, error) {
|
||||
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
|
||||
if lang != "" {
|
||||
if len(langs) > 0 {
|
||||
for _, l := range langs {
|
||||
if idx, ok := pp.LanguageIndex[l]; ok {
|
||||
builder.WithLanguageIndices(idx)
|
||||
}
|
||||
}
|
||||
} else if lang != "" {
|
||||
if idx, ok := pp.LanguageIndex[lang]; ok {
|
||||
builder.WithLanguageIndices(idx)
|
||||
}
|
||||
}
|
||||
for _, version := range versions {
|
||||
if idx := pp.ConfiguredDimensions.ConfiguredVersions.ResolveIndex(version); idx >= 0 {
|
||||
builder.WithVersionIndices(idx)
|
||||
}
|
||||
}
|
||||
for _, role := range roles {
|
||||
if idx := pp.ConfiguredDimensions.ConfiguredRoles.ResolveIndex(role); idx >= 0 {
|
||||
builder.WithRoleIndices(idx)
|
||||
}
|
||||
}
|
||||
|
||||
switch p.Component() {
|
||||
case files.ComponentFolderContent:
|
||||
@@ -100,13 +268,6 @@ func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
|
||||
return v
|
||||
}
|
||||
|
||||
// ParseIdentity parses component c with path s into a StringIdentity.
|
||||
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
|
||||
p := pp.parsePooled(c, s)
|
||||
defer putPath(p)
|
||||
return identity.StringIdentity(p.IdentifierBase())
|
||||
}
|
||||
|
||||
// ParseBaseAndBaseNameNoIdentifier parses component c with path s into a base and a base name without any identifier.
|
||||
func (pp *PathParser) ParseBaseAndBaseNameNoIdentifier(c, s string) (string, string) {
|
||||
p := pp.parsePooled(c, s)
|
||||
@@ -188,13 +349,25 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
sid := p.s[id.Low:id.High]
|
||||
|
||||
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierCustom = len(p.identifiersKnown) - 1
|
||||
found = true
|
||||
if isCustomWrapperIdentifier(sid) {
|
||||
inner := sid[1 : len(sid)-1]
|
||||
if prefix := parsePrefixIdentifier(inner); prefix != "" {
|
||||
// Value-only LowHigh: skip leading _ + prefix, trailing _
|
||||
valueID := types.LowHigh[string]{Low: id.Low + 1 + len(prefix), High: id.High - 1}
|
||||
if pp.applyPrefixIdentifier(component, prefix, p, valueID) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
p.posIdentifierCustom = len(p.identifiersKnown) - 1
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.identifiersKnown) == 0 {
|
||||
if found {
|
||||
// Already handled (e.g. prefix wrapper).
|
||||
} else if len(p.identifiersKnown) == 0 {
|
||||
// The first is always the extension.
|
||||
p.identifiersKnown = append(p.identifiersKnown, id)
|
||||
found = true
|
||||
@@ -268,6 +441,18 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if found {
|
||||
if isLast {
|
||||
// The isLast identifier starts right at the container boundary.
|
||||
// Treat it as part of the name (e.g. layout name "list" in list.no.html).
|
||||
if p.posNameHigh <= 0 {
|
||||
p.posNameHigh = lastDot
|
||||
}
|
||||
} else {
|
||||
p.posNameHigh = i // The '.' before this identifier.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
@@ -292,10 +477,14 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
}
|
||||
|
||||
p.s = s
|
||||
|
||||
// Find dots inside ._..._. wrapper blocks that must be skipped.
|
||||
skipDots := findWrapperDotPositions(s)
|
||||
|
||||
slashCount := 0
|
||||
lastDot := 0
|
||||
lastSlashIdx := strings.LastIndex(s, "/")
|
||||
numDots := strings.Count(s[lastSlashIdx+1:], ".")
|
||||
numDots := strings.Count(s[lastSlashIdx+1:], ".") - len(skipDots)
|
||||
if strings.Contains(s, "/_shortcodes/") {
|
||||
p.pathType = TypeShortcode
|
||||
}
|
||||
@@ -305,6 +494,9 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
|
||||
switch c {
|
||||
case '.':
|
||||
if isSkippedDot(skipDots, i) {
|
||||
continue
|
||||
}
|
||||
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false)
|
||||
lastDot = i
|
||||
case '/':
|
||||
@@ -323,13 +515,19 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the name boundary.
|
||||
if p.posNameHigh >= p.posContainerHigh {
|
||||
p.posIdentifierName = types.LowHigh[string]{Low: p.posContainerHigh, High: p.posNameHigh}
|
||||
} else {
|
||||
p.posIdentifierName = types.LowHigh[string]{Low: p.posContainerHigh, High: len(p.s)}
|
||||
}
|
||||
|
||||
if len(p.identifiersKnown) > 0 {
|
||||
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
|
||||
isContent := isContentComponent && pp.IsContentExt(p.Ext())
|
||||
id := p.identifiersKnown[len(p.identifiersKnown)-1]
|
||||
|
||||
if id.Low > p.posContainerHigh {
|
||||
b := p.s[p.posContainerHigh : id.Low-1]
|
||||
if p.posIdentifierName.Low >= p.posContainerHigh && p.posIdentifierName.High > p.posIdentifierName.Low {
|
||||
b := p.s[p.posIdentifierName.Low:p.posIdentifierName.High]
|
||||
if isContent {
|
||||
switch b {
|
||||
case "index":
|
||||
@@ -437,7 +635,22 @@ type Path struct {
|
||||
posIdentifierLayout int
|
||||
posIdentifierBaseof int
|
||||
posIdentifierCustom int
|
||||
disabled bool
|
||||
|
||||
// Prefix identifier positions (indices into identifiersKnown).
|
||||
posIdentifierPrefixLanguages []int
|
||||
posIdentifierVersions []int
|
||||
posIdentifierRoles []int
|
||||
posIdentifierPrefixOutputFormat int
|
||||
posIdentifierPrefixKind int
|
||||
posIdentifierPrefixLayout int
|
||||
|
||||
// Name boundary, computed during parse.
|
||||
posIdentifierName types.LowHigh[string]
|
||||
// Position of the dot before the leftmost known identifier.
|
||||
// Set during parseIdentifier, used to compute posIdentifierName.
|
||||
posNameHigh int
|
||||
|
||||
disabled bool
|
||||
|
||||
trimLeadingSlash bool
|
||||
|
||||
@@ -475,6 +688,14 @@ func (p *Path) reset() {
|
||||
p.posIdentifierLayout = -1
|
||||
p.posIdentifierBaseof = -1
|
||||
p.posIdentifierCustom = -1
|
||||
p.posIdentifierPrefixLanguages = p.posIdentifierPrefixLanguages[:0]
|
||||
p.posIdentifierVersions = p.posIdentifierVersions[:0]
|
||||
p.posIdentifierRoles = p.posIdentifierRoles[:0]
|
||||
p.posIdentifierPrefixOutputFormat = -1
|
||||
p.posIdentifierPrefixKind = -1
|
||||
p.posIdentifierPrefixLayout = -1
|
||||
p.posIdentifierName = types.LowHigh[string]{}
|
||||
p.posNameHigh = -1
|
||||
p.disabled = false
|
||||
p.trimLeadingSlash = false
|
||||
p.unnormalized = nil
|
||||
@@ -566,16 +787,6 @@ func (p *Path) NameNoExt() string {
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Name returns the last element of path without any language identifier.
|
||||
func (p *Path) NameNoLang() string {
|
||||
i := p.identifierIndex(p.posIdentifierLanguage)
|
||||
if i == -1 {
|
||||
return p.Name()
|
||||
}
|
||||
|
||||
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:]
|
||||
}
|
||||
|
||||
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
|
||||
// For bundles this will be the containing directory's name, e.g. "blog".
|
||||
func (p *Path) BaseNameNoIdentifier() string {
|
||||
@@ -592,21 +803,7 @@ func (p *Path) NameNoIdentifier() string {
|
||||
}
|
||||
|
||||
func (p *Path) nameLowHigh() types.LowHigh[string] {
|
||||
if len(p.identifiersKnown) > 0 {
|
||||
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
|
||||
if p.posContainerHigh == lastID.Low {
|
||||
// The last identifier is the name.
|
||||
return lastID
|
||||
}
|
||||
return types.LowHigh[string]{
|
||||
Low: p.posContainerHigh,
|
||||
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
|
||||
}
|
||||
}
|
||||
return types.LowHigh[string]{
|
||||
Low: p.posContainerHigh,
|
||||
High: len(p.s),
|
||||
}
|
||||
return p.posIdentifierName
|
||||
}
|
||||
|
||||
// Dir returns all but the last element of path, typically the path's directory.
|
||||
@@ -638,6 +835,9 @@ func (p *Path) Unnormalized() *Path {
|
||||
|
||||
// PathNoLang returns the Path but with any language identifier removed.
|
||||
func (p *Path) PathNoLang() string {
|
||||
if len(p.posIdentifierPrefixLanguages) > 0 {
|
||||
return p.base(true, false)
|
||||
}
|
||||
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
|
||||
return p.Path()
|
||||
}
|
||||
@@ -717,11 +917,6 @@ func (p *Path) BaseReTyped(typ string) (d string) {
|
||||
return
|
||||
}
|
||||
|
||||
// BaseNoLeadingSlash returns the base path without the leading slash.
|
||||
func (p *Path) BaseNoLeadingSlash() string {
|
||||
return p.Base()[1:]
|
||||
}
|
||||
|
||||
func (p *Path) base(preserveExt, isBundle bool) string {
|
||||
if len(p.identifiersKnown) == 0 {
|
||||
return p.norm(p.s)
|
||||
@@ -759,27 +954,58 @@ func (p *Path) Ext() string {
|
||||
}
|
||||
|
||||
func (p *Path) OutputFormat() string {
|
||||
if p.posIdentifierPrefixOutputFormat != -1 {
|
||||
return p.identifierAsString(p.posIdentifierPrefixOutputFormat)
|
||||
}
|
||||
return p.identifierAsString(p.posIdentifierOutputFormat)
|
||||
}
|
||||
|
||||
func (p *Path) Kind() string {
|
||||
if p.posIdentifierPrefixKind != -1 {
|
||||
return p.identifierAsString(p.posIdentifierPrefixKind)
|
||||
}
|
||||
return p.identifierAsString(p.posIdentifierKind)
|
||||
}
|
||||
|
||||
func (p *Path) Layout() string {
|
||||
if p.posIdentifierPrefixLayout != -1 {
|
||||
return p.identifierAsString(p.posIdentifierPrefixLayout)
|
||||
}
|
||||
return p.identifierAsString(p.posIdentifierLayout)
|
||||
}
|
||||
|
||||
func (p *Path) Lang() string {
|
||||
if len(p.posIdentifierPrefixLanguages) > 0 {
|
||||
return p.identifierAsString(p.posIdentifierPrefixLanguages[0])
|
||||
}
|
||||
return p.identifierAsString(p.posIdentifierLanguage)
|
||||
}
|
||||
|
||||
func (p *Path) Custom() string {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
|
||||
func (p *Path) Langs() []string {
|
||||
return p.identifiersAsStrings(p.posIdentifierPrefixLanguages)
|
||||
}
|
||||
|
||||
func (p *Path) Identifier(i int) string {
|
||||
return p.identifierAsString(i)
|
||||
func (p *Path) Versions() []string {
|
||||
return p.identifiersAsStrings(p.posIdentifierVersions)
|
||||
}
|
||||
|
||||
func (p *Path) Roles() []string {
|
||||
return p.identifiersAsStrings(p.posIdentifierRoles)
|
||||
}
|
||||
|
||||
func (p *Path) identifiersAsStrings(positions []int) []string {
|
||||
if len(positions) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]string, len(positions))
|
||||
for i, pos := range positions {
|
||||
ids[i] = p.identifierAsString(pos)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) Custom() string {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCustomWrapper), identifierCustomWrapper)
|
||||
}
|
||||
|
||||
func (p *Path) Disabled() bool {
|
||||
|
||||
+273
-18
@@ -15,6 +15,7 @@ package paths
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
func newTestParser() *PathParser {
|
||||
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
|
||||
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3", "v1.0.0"}, []string{"admin", "editor", "viewer", "guest"})
|
||||
|
||||
return &PathParser{
|
||||
LanguageIndex: map[string]int{
|
||||
@@ -141,7 +142,6 @@ func TestParse(t *testing.T) {
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
|
||||
@@ -178,11 +178,9 @@ func TestParse(t *testing.T) {
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
@@ -223,7 +221,6 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
|
||||
c.Assert(p.Section(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
@@ -245,7 +242,6 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "index.no")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
@@ -265,7 +261,6 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "_index.no")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "_index.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -389,6 +384,142 @@ func TestParse(t *testing.T) {
|
||||
c.Assert(p.Custom(), qt.Equals, "myid")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix language",
|
||||
"/a/b/p1._language_no_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix version",
|
||||
"/a/b/p1._version_v1_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "v1"})
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix version with dots",
|
||||
"/a/b/p1._version_v1.0.0_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1.0.0"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix role",
|
||||
"/a/b/p1._role_admin_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple prefixes",
|
||||
"/a/b/p1._language_no_._role_admin_._version_v2_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin"})
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v2"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix version with dots and role",
|
||||
"/a/b/p1._version_v1.0.0_._role_editor_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1.0.0"})
|
||||
c.Assert(p.Roles(), qt.DeepEquals, []string{"editor"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix with dot-based lang",
|
||||
"/a/b/p1._version_v1_.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix leaf bundle with version",
|
||||
"/a/b/index._version_v1_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix branch bundle with role",
|
||||
"/a/b/_index._role_admin_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.IsBranchBundle(), qt.IsTrue)
|
||||
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix with mixed case, unnormalized",
|
||||
"/a/b/My Page._Version_V1_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/my-page")
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v1"})
|
||||
pp := p.Unnormalized()
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "My Page")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple languages",
|
||||
"/a/b/p1._language_en_._language_fr_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
// Right-to-left parse order.
|
||||
c.Assert(p.Lang(), qt.Equals, "fr")
|
||||
c.Assert(p.Langs(), qt.DeepEquals, []string{"fr", "en"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple roles",
|
||||
"/a/b/p1._role_guest_._role_admin_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Roles(), qt.DeepEquals, []string{"admin", "guest"})
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple versions",
|
||||
"/a/b/p1._version_v1_._version_v2_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v2", "v1"})
|
||||
},
|
||||
},
|
||||
{
|
||||
"Unknown prefix not extracted",
|
||||
"/a/b/p1._unknown_foo_.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
// Unknown prefix should be left in path and treated as custom wrapper.
|
||||
c.Assert(p.Custom(), qt.Equals, "unknown_foo")
|
||||
c.Assert(p.Versions(), qt.IsNil)
|
||||
c.Assert(p.Roles(), qt.IsNil)
|
||||
},
|
||||
},
|
||||
}
|
||||
parser := newTestParser()
|
||||
for _, test := range tests {
|
||||
@@ -607,20 +738,81 @@ func TestParseLayouts(t *testing.T) {
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.Layout(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.xy.html")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/foo/index.xy.html")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "index"})
|
||||
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix language layout",
|
||||
"/page._language_no_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "html")
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix outputformat layout",
|
||||
"/page._outputformat_amp_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix kind layout",
|
||||
"/page._kind_section_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix layout layout",
|
||||
"/page._layout_list_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Layout(), qt.Equals, "list")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"All prefix identifiers in layout",
|
||||
"/page._language_fr_._kind_section_._outputformat_amp_._layout_list_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Lang(), qt.Equals, "fr")
|
||||
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
|
||||
c.Assert(p.OutputFormat(), qt.Equals, "amp")
|
||||
c.Assert(p.Layout(), qt.Equals, "list")
|
||||
c.Assert(p.Ext(), qt.Equals, "html")
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix version in layout",
|
||||
"/page._version_v2_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Versions(), qt.DeepEquals, []string{"v2"})
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Prefix role in layout",
|
||||
"/page._role_guest_.html",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Roles(), qt.DeepEquals, []string{"guest"})
|
||||
c.Assert(p.Base(), qt.Equals, "/page.html")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parser := newTestParser()
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
if test.name != "Not lang" {
|
||||
return
|
||||
}
|
||||
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
|
||||
})
|
||||
}
|
||||
@@ -635,22 +827,85 @@ func TestHasExt(t *testing.T) {
|
||||
c.Assert(HasExt("/a/b.c/d"), qt.IsFalse)
|
||||
}
|
||||
|
||||
func BenchmarkParseIdentity(b *testing.B) {
|
||||
parser := newTestParser()
|
||||
for b.Loop() {
|
||||
parser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSitesMatrixFromPath(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
parser := newTestParser()
|
||||
|
||||
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
|
||||
v := parser.SitesMatrixFromPath(p)
|
||||
c.Assert(v.HasLanguage(2), qt.IsTrue)
|
||||
c.Assert(v.LenVectors(), qt.Equals, 1)
|
||||
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 0})
|
||||
|
||||
// With version prefix.
|
||||
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._version_v2_.fr.md")
|
||||
v = parser.SitesMatrixFromPath(p)
|
||||
c.Assert(v.HasLanguage(2), qt.IsTrue)
|
||||
c.Assert(v.HasVersion(1), qt.IsTrue) // v2 is index 1
|
||||
c.Assert(v.LenVectors(), qt.Equals, 1)
|
||||
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 1, 0})
|
||||
|
||||
// With version and role prefixes.
|
||||
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._version_v1_._role_editor_.fr.md")
|
||||
v = parser.SitesMatrixFromPath(p)
|
||||
c.Assert(v.HasLanguage(2), qt.IsTrue)
|
||||
c.Assert(v.HasVersion(0), qt.IsTrue) // v1 is index 0
|
||||
c.Assert(v.HasRole(1), qt.IsTrue) // editor is index 1
|
||||
c.Assert(v.LenVectors(), qt.Equals, 1)
|
||||
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 1})
|
||||
|
||||
// With multiple roles.
|
||||
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._role_guest_._role_admin_.fr.md")
|
||||
v = parser.SitesMatrixFromPath(p)
|
||||
c.Assert(v.HasLanguage(2), qt.IsTrue)
|
||||
c.Assert(v.HasRole(0), qt.IsTrue) // admin is index 0
|
||||
c.Assert(v.HasRole(3), qt.IsTrue) // guest is index 3
|
||||
c.Assert(v.LenVectors(), qt.Equals, 2)
|
||||
|
||||
// With multiple languages via prefix.
|
||||
p = parser.Parse(files.ComponentFolderContent, "/a/b/c._language_en_._language_fr_.md")
|
||||
v = parser.SitesMatrixFromPath(p)
|
||||
c.Assert(v.HasLanguage(0), qt.IsFalse) // no is index 0, not included
|
||||
c.Assert(v.HasLanguage(1), qt.IsTrue) // en is index 1
|
||||
c.Assert(v.HasLanguage(2), qt.IsTrue) // fr is index 2
|
||||
c.Assert(v.LenVectors(), qt.Equals, 2)
|
||||
}
|
||||
|
||||
func FuzzParsePath(f *testing.F) {
|
||||
componentPaths := []struct {
|
||||
component string
|
||||
path string
|
||||
}{
|
||||
{files.ComponentFolderContent, "/a/b/c.fr.md"},
|
||||
{files.ComponentFolderContent, "/a/b/c._version_v2_.fr.md"},
|
||||
{files.ComponentFolderContent, "/a/b/c._version_v1_._role_editor_.fr.md"},
|
||||
{files.ComponentFolderContent, "/a/b/c._role_guest_._role_admin_.fr.md"},
|
||||
{files.ComponentFolderContent, "/a/b/c._language_en_._language_fr_.md"},
|
||||
{files.ComponentFolderLayouts, "/list.no.html"},
|
||||
{files.ComponentFolderLayouts, "/page._language_fr_._kind_section_._outputformat_amp_._layout_list_.html"},
|
||||
}
|
||||
|
||||
for _, cp := range componentPaths {
|
||||
f.Add(cp.component, cp.path)
|
||||
}
|
||||
|
||||
parser := newTestParser()
|
||||
|
||||
f.Fuzz(func(t *testing.T, c, s string) {
|
||||
p := parser.Parse(c, s)
|
||||
if p == nil {
|
||||
t.Fatalf("Parse returned nil for path: %q", s)
|
||||
}
|
||||
// Execute all the methods using reflection to ensure they don't panic.
|
||||
v := reflect.ValueOf(p)
|
||||
for i := 0; i < v.NumMethod(); i++ {
|
||||
method := v.Type().Method(i)
|
||||
if method.Type.NumIn() == 1 {
|
||||
method.Func.Call([]reflect.Value{v})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSitesMatrixFromPath(b *testing.B) {
|
||||
|
||||
@@ -35,9 +35,9 @@ weight = 2
|
||||
removePathAccents = true
|
||||
-- content/διακριτικός.md --
|
||||
-- content/διακριτικός.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
-- layouts/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
-- layouts/list.html --
|
||||
List
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
@@ -64,9 +64,9 @@ disablePathToLower = true
|
||||
-- content/MySection/MyPage.fr.md --
|
||||
-- content/MySection/MyBundle/index.md --
|
||||
-- content/MySection/MyBundle/index.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
-- layouts/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
-- layouts/list.html --
|
||||
{{ .Language.Lang }}|List.
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
@@ -79,6 +79,72 @@ disablePathToLower = true
|
||||
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
|
||||
}
|
||||
|
||||
func TestPrefixIdentifiersContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: p1 default
|
||||
---
|
||||
-- content/p1._language_fr_.md --
|
||||
---
|
||||
title: p1 french
|
||||
---
|
||||
-- layouts/single.html --
|
||||
{{ .Language.Lang }}|{{ .Title }}|Single.
|
||||
-- layouts/list.html --
|
||||
List
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/p1/index.html", "en|p1 default|Single")
|
||||
b.AssertFileContent("public/fr/p1/index.html", "fr|p1 french|Single")
|
||||
}
|
||||
|
||||
func TestPrefixIdentifiersLayouts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: p1
|
||||
---
|
||||
-- content/p1.fr.md --
|
||||
---
|
||||
title: p1 fr
|
||||
---
|
||||
-- layouts/single.html --
|
||||
default|{{ .Title }}|
|
||||
-- layouts/single._language_fr_.html --
|
||||
french layout|{{ .Title }}|
|
||||
-- layouts/list.html --
|
||||
List
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/p1/index.html", "default|p1|")
|
||||
b.AssertFileContent("public/fr/p1/index.html", "french layout|p1 fr|")
|
||||
}
|
||||
|
||||
func TestIssue13596(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
go test fuzz v1
|
||||
string("fr..")
|
||||
@@ -102,64 +102,6 @@ func AddContextRoot(baseURL, relativePath string) string {
|
||||
return newPath
|
||||
}
|
||||
|
||||
// URLizeAn
|
||||
|
||||
// PrettifyURL takes a URL string and returns a semantic, clean URL.
|
||||
func PrettifyURL(in string) string {
|
||||
x := PrettifyURLPath(in)
|
||||
|
||||
if path.Base(x) == "index.html" {
|
||||
return path.Dir(x)
|
||||
}
|
||||
|
||||
if in == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// PrettifyURLPath takes a URL path to a content and converts it
|
||||
// to enable pretty URLs.
|
||||
//
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyURLPath(in string) string {
|
||||
return prettifyPath(in, pb)
|
||||
}
|
||||
|
||||
// Uglify does the opposite of PrettifyURLPath().
|
||||
//
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
func Uglify(in string) string {
|
||||
if path.Ext(in) == "" {
|
||||
if len(in) < 2 {
|
||||
return "/"
|
||||
}
|
||||
// /section/name/ -> /section/name.html
|
||||
return path.Clean(in) + ".html"
|
||||
}
|
||||
|
||||
name, ext := fileAndExt(in, pb)
|
||||
if name == "index" {
|
||||
// /section/name/index.html -> /section/name.html
|
||||
d := path.Dir(in)
|
||||
if len(d) > 1 {
|
||||
return d + ext
|
||||
}
|
||||
return in
|
||||
}
|
||||
// /.xml -> /index.xml
|
||||
if name == "" {
|
||||
return path.Dir(in) + "index" + ext
|
||||
}
|
||||
// /section/name.html -> /section/name.html
|
||||
return path.Clean(in)
|
||||
}
|
||||
|
||||
// URLEscape escapes unicode letters.
|
||||
func URLEscape(uri string) string {
|
||||
// escape unicode letters
|
||||
|
||||
@@ -15,8 +15,6 @@ package paths
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMakePermalink(t *testing.T) {
|
||||
@@ -66,35 +64,3 @@ func TestAddContextRoot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPretty(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html"))
|
||||
c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html"))
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/"))
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html"))
|
||||
c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html"))
|
||||
c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml"))
|
||||
c.Assert("/", qt.Equals, PrettifyURLPath("/"))
|
||||
c.Assert("/", qt.Equals, PrettifyURLPath(""))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html"))
|
||||
c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html"))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/"))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL("/index.html"))
|
||||
c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL("/"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL(""))
|
||||
}
|
||||
|
||||
func TestUgly(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html"))
|
||||
c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html"))
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/"))
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html"))
|
||||
c.Assert("/index.html", qt.Equals, Uglify("/index.html"))
|
||||
c.Assert("/name.xml", qt.Equals, Uglify("/name.xml"))
|
||||
c.Assert("/", qt.Equals, Uglify("/"))
|
||||
c.Assert("/", qt.Equals, Uglify(""))
|
||||
}
|
||||
|
||||
@@ -153,6 +153,35 @@ func (p PR[T]) FilterCopy(s []T) []T {
|
||||
return result
|
||||
}
|
||||
|
||||
const (
|
||||
rangeOpNone = iota
|
||||
rangeOpLT
|
||||
rangeOpLTE
|
||||
rangeOpGT
|
||||
rangeOpGTE
|
||||
rangeOpEQ
|
||||
rangeOpNE
|
||||
)
|
||||
|
||||
func cutRangeOp(s string) (op int, rest string) {
|
||||
switch {
|
||||
case strings.HasPrefix(s, ">= "):
|
||||
return rangeOpGTE, s[3:]
|
||||
case strings.HasPrefix(s, "<= "):
|
||||
return rangeOpLTE, s[3:]
|
||||
case strings.HasPrefix(s, "== "):
|
||||
return rangeOpEQ, s[3:]
|
||||
case strings.HasPrefix(s, "!= "):
|
||||
return rangeOpNE, s[3:]
|
||||
case strings.HasPrefix(s, "> "):
|
||||
return rangeOpGT, s[2:]
|
||||
case strings.HasPrefix(s, "< "):
|
||||
return rangeOpLT, s[2:]
|
||||
default:
|
||||
return rangeOpNone, s
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
|
||||
@@ -187,6 +216,86 @@ func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string)
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
type IndexMatcher interface {
|
||||
IndexMatch(match P[string]) (iter.Seq[int], error)
|
||||
// NewIndexStringPredicateFromGlobsAndRanges creates an IndexString predicate from the given glob patterns and range patterns.
|
||||
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
|
||||
// A range pattern is one of "> value", ">= value", "< value" or "<= value".
|
||||
func NewIndexStringPredicateFromGlobsAndRanges(patterns []string, getIndex func(s string) int, getGlob func(pattern string) (glob.Glob, error)) (P[IndexString], error) {
|
||||
var p PR[IndexString]
|
||||
for _, pattern := range patterns {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
|
||||
if negate {
|
||||
pattern = pattern[2:]
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(!g.Match(s.String))
|
||||
})
|
||||
} else {
|
||||
// This can be either a glob or a value prefixed with one of >, >=, < or <=.
|
||||
o, v := cutRangeOp(pattern)
|
||||
if o != rangeOpNone {
|
||||
i := getIndex(v)
|
||||
if i == -1 {
|
||||
// No match possible.
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(false)
|
||||
})
|
||||
continue
|
||||
}
|
||||
switch o {
|
||||
// The greater values starts at the top with index 0.
|
||||
case rangeOpGT:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index < i)
|
||||
})
|
||||
case rangeOpGTE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index <= i)
|
||||
})
|
||||
case rangeOpLT:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index > i)
|
||||
})
|
||||
case rangeOpLTE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index >= i)
|
||||
})
|
||||
case rangeOpEQ:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index == i)
|
||||
})
|
||||
case rangeOpNE:
|
||||
p = p.And(func(s IndexString) Match {
|
||||
return BoolMatch(s.Index != i)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
g, err := getGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p = p.Or(func(s IndexString) Match {
|
||||
return BoolMatch(g.Match(s.String))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return p.BoolFunc(), nil
|
||||
}
|
||||
|
||||
type IndexString struct {
|
||||
Index int
|
||||
String string
|
||||
}
|
||||
|
||||
type IndexMatcher interface {
|
||||
IndexMatch(match P[IndexString]) (iter.Seq[int], error)
|
||||
}
|
||||
|
||||
@@ -146,6 +146,107 @@ func TestNewStringPredicateFromGlobs(t *testing.T) {
|
||||
c.Assert(m("anything"), qt.IsFalse)
|
||||
}
|
||||
|
||||
func TestNewIndexStringPredicateFromGlobsAndRanges(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Simulate versions: v4.0.0=0, v3.0.0=1, v2.0.0=2, v1.0.0=3
|
||||
// Lower index = greater value.
|
||||
versions := []string{"v4.0.0", "v3.0.0", "v2.0.0", "v1.0.0"}
|
||||
getIndex := func(s string) int {
|
||||
for i, v := range versions {
|
||||
if v == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
getGlob := func(pattern string) (glob.Glob, error) {
|
||||
return glob.Compile(pattern)
|
||||
}
|
||||
|
||||
n := func(patterns ...string) predicate.P[predicate.IndexString] {
|
||||
p, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(patterns, getIndex, getGlob)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return p
|
||||
}
|
||||
|
||||
is := func(i int) predicate.IndexString {
|
||||
return predicate.IndexString{Index: i, String: versions[i]}
|
||||
}
|
||||
|
||||
// Test >= v2.0.0 (index 2): should match indices <= 2
|
||||
m := n(">= v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test > v2.0.0 (index 2): should match indices < 2
|
||||
m = n("> v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test < v3.0.0 (index 1): should match indices > 1
|
||||
m = n("< v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
|
||||
|
||||
// Test range: >= v2.0.0 AND <= v3.0.0
|
||||
m = n(">= v2.0.0", "<= v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0 - too high
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - too low
|
||||
|
||||
// Test glob pattern
|
||||
m = n("v2.*.*")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
|
||||
// Test glob with negation
|
||||
m = n("v*.*.*", "! v3.*.*")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
|
||||
// Test range with negation: >= v2.0.0 but not v3.0.0
|
||||
m = n(">= v2.0.0", "! v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
|
||||
|
||||
// Test unknown value in range returns no match
|
||||
m = n(">= v99.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse)
|
||||
c.Assert(m(is(3)), qt.IsFalse)
|
||||
|
||||
// Test == v2.0.0: should only match v2.0.0
|
||||
m = n("== v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
|
||||
|
||||
// Test != v2.0.0: should match everything except v2.0.0
|
||||
m = n("!= v2.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
|
||||
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
|
||||
|
||||
// Test != with range: >= v2.0.0 AND != v3.0.0
|
||||
m = n(">= v2.0.0", "!= v3.0.0")
|
||||
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
|
||||
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - excluded by !=
|
||||
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
|
||||
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
|
||||
}
|
||||
|
||||
func BenchmarkPredicate(b *testing.B) {
|
||||
b.Run("and or no match", func(b *testing.B) {
|
||||
var p predicate.PR[int] = intP1
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCutRangeOp(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
op, rest := cutRangeOp(">= value")
|
||||
c.Assert(op, qt.Equals, rangeOpGTE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("<= value")
|
||||
c.Assert(op, qt.Equals, rangeOpLTE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("> value")
|
||||
c.Assert(op, qt.Equals, rangeOpGT)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("< value")
|
||||
c.Assert(op, qt.Equals, rangeOpLT)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("== value")
|
||||
c.Assert(op, qt.Equals, rangeOpEQ)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("!= value")
|
||||
c.Assert(op, qt.Equals, rangeOpNE)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
|
||||
op, rest = cutRangeOp("value")
|
||||
c.Assert(op, qt.Equals, rangeOpNone)
|
||||
c.Assert(rest, qt.Equals, "value")
|
||||
}
|
||||
@@ -56,9 +56,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
|
||||
queue := NewEvictingQueue[string](3)
|
||||
|
||||
for range 100 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
queue.Add(val)
|
||||
v := queue.Peek()
|
||||
if v != val {
|
||||
@@ -68,7 +66,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
|
||||
if len(vals) != 1 || vals[0] != val {
|
||||
t.Error("wrong val")
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func IsNil(v any) bool {
|
||||
|
||||
value := reflect.ValueOf(v)
|
||||
switch value.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
|
||||
return value.IsNil()
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ type Version struct {
|
||||
// HugoVersionSuffix is the suffix used in the Hugo version string.
|
||||
// It will be blank for release versions.
|
||||
Suffix string
|
||||
|
||||
// Set when parsed from a string,
|
||||
source string
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -44,6 +47,17 @@ var (
|
||||
_ compare.Comparer = (*VersionString)(nil)
|
||||
)
|
||||
|
||||
// IsAlphaBetaOrRC returns whether this version is an alpha, beta, or release candidate.
|
||||
func (v Version) IsAlphaBetaOrRC() bool {
|
||||
s := strings.ToLower(v.Suffix)
|
||||
// e.g. "alpha.1", "beta.2", "rc.3"
|
||||
return strings.Contains(s, "alpha.") || strings.Contains(s, "beta.") || strings.Contains(s, "rc.")
|
||||
}
|
||||
|
||||
func (v Version) IsZero() bool {
|
||||
return v.Major == 0 && v.Minor == 0 && v.PatchLevel == 0 && v.Suffix == ""
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
|
||||
}
|
||||
@@ -102,6 +116,7 @@ func ParseVersion(s string) (Version, error) {
|
||||
vv.Suffix = suffix
|
||||
}
|
||||
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
|
||||
vv.source = s
|
||||
|
||||
return vv, nil
|
||||
}
|
||||
@@ -169,6 +184,10 @@ func CompareVersions(v1 Version, v2 any) int {
|
||||
case int64:
|
||||
c = compareFloatWithVersion(float64(d), v1)
|
||||
case Version:
|
||||
if v1.IsZero() && d.IsZero() {
|
||||
// Fall back to source comparison.
|
||||
return strings.Compare(v1.source, d.source)
|
||||
}
|
||||
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
|
||||
return strings.Compare(v1.Suffix, d.Suffix)
|
||||
}
|
||||
@@ -193,11 +212,13 @@ func CompareVersions(v1 Version, v2 any) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
v, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
v2v, _ := ParseVersion(s)
|
||||
if v1.IsZero() && v2v.IsZero() {
|
||||
// Fall back to source comparison.
|
||||
return strings.Compare(v1.source, v2v.source)
|
||||
}
|
||||
return v1.Compare(v)
|
||||
|
||||
return v1.Compare(v2v)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ func TestHugoVersion(t *testing.T) {
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
parseIgnoreErr := func(s string) Version {
|
||||
v, _ := ParseVersion(s)
|
||||
return v
|
||||
}
|
||||
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
|
||||
@@ -69,6 +74,15 @@ func TestCompareVersions(t *testing.T) {
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
|
||||
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1)
|
||||
c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1)
|
||||
}
|
||||
|
||||
func TestParseHugoVersion(t *testing.T) {
|
||||
|
||||
+106
-49
@@ -29,10 +29,10 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/cache/httpcache"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -181,7 +181,7 @@ type Config struct {
|
||||
Minify minifiers.MinifyConfig `mapstructure:"-"`
|
||||
|
||||
// Permalink configuration.
|
||||
Permalinks map[string]map[string]string `mapstructure:"-"`
|
||||
Permalinks page.PermalinksConfig `mapstructure:"-"`
|
||||
|
||||
// Taxonomy configuration.
|
||||
Taxonomies map[string]string `mapstructure:"-"`
|
||||
@@ -212,7 +212,7 @@ type Config struct {
|
||||
|
||||
// User provided parameters.
|
||||
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
|
||||
Params maps.Params `mapstructure:"-"`
|
||||
Params hmaps.Params `mapstructure:"-"`
|
||||
|
||||
// UglyURLs configuration. Either a boolean or a sections map.
|
||||
UglyURLs any `mapstructure:"-"`
|
||||
@@ -399,43 +399,93 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Legacy paginate values.
|
||||
if c.Paginate != 0 {
|
||||
hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger())
|
||||
c.Pagination.PagerSize = c.Paginate
|
||||
// Legacy language values.
|
||||
if c.LanguageCode != "" {
|
||||
if !c.isLanguageClone {
|
||||
hugo.DeprecateWithLogger("project config key languageCode", "Use locale instead.", "v0.158.0", logger.Logger())
|
||||
}
|
||||
if c.Locale == "" {
|
||||
c.Locale = c.LanguageCode
|
||||
}
|
||||
c.LanguageCode = ""
|
||||
}
|
||||
if c.PaginatePath != "" {
|
||||
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
|
||||
c.Pagination.Path = c.PaginatePath
|
||||
for k, v := range c.Languages.Config.LanguageConfigs {
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
if v.LanguageCode != "" {
|
||||
if !c.isLanguageClone {
|
||||
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageCode", k), fmt.Sprintf("Use languages.%s.locale instead.", k), "v0.158.0", logger.Logger())
|
||||
}
|
||||
if v.Locale == "" {
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
v.Locale = v.LanguageCode
|
||||
}
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
v.LanguageCode = ""
|
||||
}
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
if v.LanguageName != "" {
|
||||
if !c.isLanguageClone {
|
||||
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageName", k), fmt.Sprintf("Use languages.%s.label instead.", k), "v0.158.0", logger.Logger())
|
||||
}
|
||||
if v.Label == "" {
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
v.Label = v.LanguageName
|
||||
}
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
v.LanguageName = ""
|
||||
}
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
if v.LanguageDirection != "" {
|
||||
if !c.isLanguageClone {
|
||||
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageDirection", k), fmt.Sprintf("Use languages.%s.direction instead.", k), "v0.158.0", logger.Logger())
|
||||
}
|
||||
if v.Direction == "" {
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
v.Direction = v.LanguageDirection
|
||||
}
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
v.LanguageDirection = ""
|
||||
}
|
||||
|
||||
c.Languages.Config.LanguageConfigs[k] = v
|
||||
// Sorted is a snapshot of LanguageConfigs taken at decode time; keep it
|
||||
// in sync so Configs.Init, which reads from Sorted, sees the migrated values.
|
||||
for i, s := range c.Languages.Config.Sorted {
|
||||
if s.Name == k {
|
||||
c.Languages.Config.Sorted[i].LanguageConfig = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy privacy values.
|
||||
if c.Privacy.Twitter.Disable {
|
||||
hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
|
||||
}
|
||||
if c.Privacy.Twitter.EnableDNT {
|
||||
hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
|
||||
}
|
||||
if c.Privacy.Twitter.Simple {
|
||||
hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
|
||||
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
|
||||
}
|
||||
|
||||
// Legacy services values.
|
||||
if c.Services.Twitter.DisableInlineCSS {
|
||||
hugo.DeprecateWithLogger("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
|
||||
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
|
||||
}
|
||||
|
||||
// Legacy permalink tokens
|
||||
vs := fmt.Sprintf("%v", c.Permalinks)
|
||||
if strings.Contains(vs, ":filename") {
|
||||
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
if strings.Contains(vs, ":slugorfilename") {
|
||||
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
for _, pc := range c.Permalinks {
|
||||
if strings.Contains(pc.Pattern, ":filename") {
|
||||
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
if strings.Contains(pc.Pattern, ":slugorfilename") {
|
||||
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy render hook values.
|
||||
@@ -446,7 +496,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
)
|
||||
if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil {
|
||||
alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails
|
||||
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
|
||||
if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault {
|
||||
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
|
||||
} else {
|
||||
@@ -455,7 +505,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
}
|
||||
if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil {
|
||||
alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails
|
||||
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
|
||||
hugo.DeprecateWithLogger("project config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
|
||||
if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault {
|
||||
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
|
||||
} else {
|
||||
@@ -471,10 +521,10 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
gc.RenderHookUseEmbeddedNever,
|
||||
}
|
||||
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) {
|
||||
return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
return fmt.Errorf("project config markup.goldmark.renderHooks.image.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
}
|
||||
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) {
|
||||
return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
return fmt.Errorf("project config markup.goldmark.renderHooks.link.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
|
||||
}
|
||||
|
||||
c.C = &ConfigCompiled{
|
||||
@@ -607,8 +657,14 @@ type RootConfig struct {
|
||||
DefaultOutputFormat string
|
||||
|
||||
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
|
||||
// Note that this currently is an alias for DisableDefaultSiteRedirect introduced in v0.154.5.
|
||||
// It's not obvious how a more fine grained setup would work.
|
||||
DisableDefaultLanguageRedirect bool
|
||||
|
||||
// Disable generation of redirects to the default site when DefaultContentRoleInSubdir or DefaultContentVersionInSubdir or DefaultContentLanguageInSubdir is enabled.
|
||||
// The default site is the site is the combination of defaultContentLanguage, defaultContentVersion and defaultContentRole.
|
||||
DisableDefaultSiteRedirect bool
|
||||
|
||||
// Disable creation of alias redirect pages.
|
||||
DisableAliases bool
|
||||
|
||||
@@ -675,20 +731,15 @@ type RootConfig struct {
|
||||
// The configured environment. Default is "development" for server and "production" for build.
|
||||
Environment string
|
||||
|
||||
// The default language code.
|
||||
LanguageCode string
|
||||
// Deprecated: Use Locale instead.
|
||||
LanguageCode string `json:"-"`
|
||||
|
||||
// The default locale.
|
||||
Locale string
|
||||
|
||||
// Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words.
|
||||
HasCJKLanguage bool
|
||||
|
||||
// The default number of pages per page when paginating.
|
||||
// Deprecated: Use the Pagination struct.
|
||||
Paginate int
|
||||
|
||||
// The path to use when creating pagination URLs, e.g. "page" in /page/2/.
|
||||
// Deprecated: Use the Pagination struct.
|
||||
PaginatePath string
|
||||
|
||||
// Whether to pluralize default list titles.
|
||||
// Note that this currently only works for English, but you can provide your own title in the content file's front matter.
|
||||
PluralizeListTitles bool
|
||||
@@ -816,6 +867,7 @@ type Configs struct {
|
||||
|
||||
Modules modules.Modules
|
||||
ModulesClient *modules.Client
|
||||
FileCaches filecache.Caches
|
||||
|
||||
// All below is set in Init.
|
||||
Languages langs.Languages
|
||||
@@ -846,7 +898,7 @@ func (c *Configs) IsZero() bool {
|
||||
return c == nil || len(c.Languages) == 0
|
||||
}
|
||||
|
||||
func (c *Configs) Init(logger loggers.Logger) error {
|
||||
func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
|
||||
var languages langs.Languages
|
||||
|
||||
for _, f := range c.Base.Languages.Config.Sorted {
|
||||
@@ -854,7 +906,7 @@ func (c *Configs) Init(logger loggers.Logger) error {
|
||||
if !found {
|
||||
return fmt.Errorf("invalid language configuration for %q", f.Name)
|
||||
}
|
||||
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
|
||||
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -915,10 +967,9 @@ func (c *Configs) Init(logger loggers.Logger) error {
|
||||
// avoid initializing the same config more than once.
|
||||
for i, l := range c.Languages {
|
||||
langConfig := c.LanguageConfigMap[l.Lang]
|
||||
sitesMatrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithLanguageIndices(i).WithAllIfNotSet().Build()
|
||||
for _, s := range allDecoderSetups {
|
||||
if getInitializer := s.getInitializer; getInitializer != nil {
|
||||
if err := getInitializer(langConfig).InitConfig(logger, sitesMatrix, c.ConfiguredDimensions); err != nil {
|
||||
if err := getInitializer(langConfig).InitConfig(logger, nil, c.ConfiguredDimensions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1018,7 +1069,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
|
||||
res.Cfg.Set("languages", hmaps.Params{lang: hmaps.Params{}})
|
||||
}
|
||||
bcfg := res.BaseConfig
|
||||
cfg := res.Cfg
|
||||
@@ -1045,11 +1096,11 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
var differentRootKeys []string
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
case maps.Params:
|
||||
case hmaps.Params:
|
||||
_, found := x["params"]
|
||||
if !found {
|
||||
x["params"] = maps.Params{
|
||||
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
|
||||
x["params"] = hmaps.Params{
|
||||
hmaps.MergeStrategyKey: hmaps.ParamsMergeStrategyDeep,
|
||||
}
|
||||
}
|
||||
for kk, vv := range x {
|
||||
@@ -1061,16 +1112,16 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
isMultihost = true
|
||||
}
|
||||
|
||||
if p, ok := vv.(maps.Params); ok {
|
||||
if p, ok := vv.(hmaps.Params); ok {
|
||||
// With the introduction of YAML anchor and alias support, language config entries
|
||||
// may be contain shared references.
|
||||
// This also break potential cycles.
|
||||
vv = maps.CloneParamsDeep(p)
|
||||
vv = hmaps.CloneParamsDeep(p)
|
||||
}
|
||||
|
||||
if kk == "cascade" {
|
||||
// If not set, add the current language to make sure it does not get applied to other languages.
|
||||
page.AddLangToCascadeTargetMap(k, vv.(maps.Params))
|
||||
page.AddLangToCascadeTargetMap(k, vv.(hmaps.Params))
|
||||
// Always clone cascade config to get the sites matrix right.
|
||||
differentRootKeys = append(differentRootKeys, kk)
|
||||
}
|
||||
@@ -1081,14 +1132,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
// This overrides a root key and potentially needs a merge.
|
||||
if !reflect.DeepEqual(rootv, vv) {
|
||||
switch vvv := vv.(type) {
|
||||
case maps.Params:
|
||||
case hmaps.Params:
|
||||
differentRootKeys = append(differentRootKeys, kk)
|
||||
|
||||
// Use the language value as base.
|
||||
// Note that this is already cloned above.
|
||||
mergedConfigEntry := vvv
|
||||
// Merge in the root value.
|
||||
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
|
||||
hmaps.MergeParams(mergedConfigEntry, rootv.(hmaps.Params))
|
||||
|
||||
mergedConfig.Set(kk, mergedConfigEntry)
|
||||
default:
|
||||
@@ -1098,7 +1149,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
} else {
|
||||
switch vv.(type) {
|
||||
case maps.Params:
|
||||
case hmaps.Params:
|
||||
differentRootKeys = append(differentRootKeys, kk)
|
||||
default:
|
||||
// Apply new values to the root.
|
||||
@@ -1134,7 +1185,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
|
||||
langConfigMap[k] = clone
|
||||
case maps.ParamsMergeStrategy:
|
||||
case hmaps.ParamsMergeStrategy:
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type in languages config: %T", v))
|
||||
@@ -1149,10 +1200,16 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
l.CommonDirs.CacheDir = bcfg.CacheDir
|
||||
}
|
||||
|
||||
caches, err := filecache.NewCaches(all.Caches, fs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
|
||||
}
|
||||
|
||||
cm := &Configs{
|
||||
Base: all,
|
||||
LanguageConfigMap: langConfigMap,
|
||||
LoadingInfo: res,
|
||||
FileCaches: caches,
|
||||
IsMultihost: isMultihost,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,11 @@ title: "p1"
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
-- layouts/_default/single.html --
|
||||
-- layouts/single.html --
|
||||
Title: {{ .Title }}
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
|
||||
).Build()
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// b.AssertFileContent("public/p1/index.html", "Title: p1")
|
||||
|
||||
@@ -81,9 +79,7 @@ baseURL = "https://example.com"
|
||||
logI18nWarnings = true
|
||||
logPathWarnings = true
|
||||
`
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
|
||||
).Build()
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
conf := b.H.Configs.Base
|
||||
|
||||
@@ -141,7 +137,7 @@ disableKinds = ["taxonomy", "term"]
|
||||
[pagination]
|
||||
disableAliases = true
|
||||
pagerSize = 2
|
||||
-- layouts/_default/list.html --
|
||||
-- layouts/list.html --
|
||||
{{ $paginator := .Paginate site.RegularPages }}
|
||||
{{ template "_internal/pagination.html" . }}
|
||||
{{ range $paginator.Pages }}
|
||||
@@ -191,7 +187,7 @@ func TestInvalidOutputFormat(t *testing.T) {
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
[outputs]
|
||||
home = ['html','foo']
|
||||
-- layouts/index.html --
|
||||
-- layouts/home.html --
|
||||
x
|
||||
`
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/httpcache"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/privacy"
|
||||
@@ -196,7 +196,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "outputs",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
|
||||
p.c.Outputs = make(map[string][]string)
|
||||
for k, v := range m {
|
||||
s := types.ToStringSlicePreserveString(v)
|
||||
@@ -225,28 +225,18 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"languages": {
|
||||
key: "languages",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
if len(m) == 1 {
|
||||
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
|
||||
var first maps.Params
|
||||
var ok bool
|
||||
for _, v := range m {
|
||||
first, ok = v.(maps.Params)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
if _, found := first["languagecode"]; !found {
|
||||
first["languagecode"] = p.p.GetString("languagecode")
|
||||
}
|
||||
}
|
||||
}
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
// Root-level locale/languageCode is passed to DecodeConfig so it can
|
||||
// be applied to the default content language inside langs.DecodeConfig.
|
||||
// They are passed separately so that an explicit per-lang languageCode
|
||||
// can override a root-level languageCode (but not a root-level locale).
|
||||
rootLocale := p.p.GetString("locale")
|
||||
rootLanguageCode := p.p.GetString("languagecode")
|
||||
var (
|
||||
err error
|
||||
defaultContentLanguage string
|
||||
)
|
||||
p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, p.c.RootConfig.DisableLanguages, m)
|
||||
p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, rootLocale, rootLanguageCode, p.c.RootConfig.DisableLanguages, m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode languages config: %w", err)
|
||||
}
|
||||
@@ -266,7 +256,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "versions",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
|
||||
return err
|
||||
},
|
||||
@@ -278,7 +268,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
err error
|
||||
defaultContentRole string
|
||||
)
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
p.c.Roles, defaultContentRole, err = roles.DecodeConfig(p.c.RootConfig.DefaultContentRole, m)
|
||||
p.c.RootConfig.DefaultContentRole = defaultContentRole
|
||||
|
||||
@@ -289,7 +279,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"params": {
|
||||
key: "params",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params"))
|
||||
p.c.Params = hmaps.CleanConfigStringMap(p.p.GetStringMap("params"))
|
||||
if p.c.Params == nil {
|
||||
p.c.Params = make(map[string]any)
|
||||
}
|
||||
@@ -317,9 +307,10 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "permalinks",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.GetStringMap(d.key))
|
||||
p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.Get(d.key))
|
||||
return err
|
||||
},
|
||||
getInitializer: func(c *Config) configInitializer { return c.Permalinks },
|
||||
},
|
||||
"sitemap": {
|
||||
key: "sitemap",
|
||||
@@ -335,7 +326,14 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "taxonomies",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
if p.p.IsSet(d.key) {
|
||||
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
m := hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
// Remove invalid entries (e.g. non-taxonomy keys placed inside [taxonomies] in TOML).
|
||||
for k, v := range m {
|
||||
if k == "" || v == "" {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
p.c.Taxonomies = m
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -447,7 +445,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"author": {
|
||||
key: "author",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Author = maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
p.c.Author = hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
@@ -455,7 +453,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"social": {
|
||||
key: "social",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Social = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
p.c.Social = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
@@ -469,8 +467,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
p.c.UglyURLs = vv
|
||||
case string:
|
||||
p.c.UglyURLs = vv == "true"
|
||||
case maps.Params:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(maps.CleanConfigStringMap(vv))
|
||||
case hmaps.Params:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(hmaps.CleanConfigStringMap(vv))
|
||||
default:
|
||||
p.c.UglyURLs = cast.ToStringMapBool(v)
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@ import (
|
||||
)
|
||||
|
||||
type ConfigLanguage struct {
|
||||
config *Config
|
||||
baseConfig config.BaseConfig
|
||||
|
||||
config *Config
|
||||
baseConfig config.BaseConfig
|
||||
m *Configs
|
||||
language *langs.Language
|
||||
languageIndex int
|
||||
@@ -123,6 +122,10 @@ func (c ConfigLanguage) BaseConfig() config.BaseConfig {
|
||||
return c.baseConfig
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) FileCaches() any {
|
||||
return c.m.FileCaches
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Dirs() config.CommonDirs {
|
||||
return c.config.CommonDirs
|
||||
}
|
||||
@@ -135,6 +138,10 @@ func (c ConfigLanguage) WorkingDir() string {
|
||||
return c.m.Base.WorkingDir
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) CacheDirMisc() string {
|
||||
return c.config.Caches.CacheDirMisc()
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Quiet() bool {
|
||||
return c.m.Base.Internal.Quiet
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
package allconfig
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
)
|
||||
@@ -27,12 +27,12 @@ func init() {
|
||||
if v.internalOrDeprecated {
|
||||
continue
|
||||
}
|
||||
cfg.Set(configRoot, make(maps.Params))
|
||||
cfg.Set(configRoot, make(hmaps.Params))
|
||||
}
|
||||
lang := maps.Params{
|
||||
"en": maps.Params{
|
||||
"menus": maps.Params{},
|
||||
"params": maps.Params{},
|
||||
lang := hmaps.Params{
|
||||
"en": hmaps.Params{
|
||||
"menus": hmaps.Params{},
|
||||
"params": hmaps.Params{},
|
||||
},
|
||||
}
|
||||
cfg.Set("languages", lang)
|
||||
|
||||
@@ -25,21 +25,22 @@ import (
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
hglob "github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/modules/npm"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
//lint:ignore ST1005 end user message.
|
||||
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
|
||||
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new project.\n Run `hugo help new` for details.\n")
|
||||
|
||||
func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
|
||||
defer func() {
|
||||
@@ -95,7 +96,11 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
|
||||
configs.Modules = moduleConfig.AllModules
|
||||
configs.ModulesClient = modulesClient
|
||||
|
||||
if err := configs.Init(d.Logger); err != nil {
|
||||
if !d.SkipNpmCheck && npm.NpmPackNeedsUpdate(d.Fs, configs.Modules) {
|
||||
d.Logger.Warnln(`npm dependencies are out of sync, please run "hugo mod npm pack" (you may also want to run "npm install" after that)`)
|
||||
}
|
||||
|
||||
if err := configs.Init(d.Fs, d.Logger); err != nil {
|
||||
return nil, fmt.Errorf("failed to init config: %w", err)
|
||||
}
|
||||
|
||||
@@ -127,6 +132,9 @@ type ConfigSourceDescriptor struct {
|
||||
|
||||
// If set, this will be used to ignore the module does not exist error.
|
||||
IgnoreModuleDoesNotExist bool
|
||||
|
||||
// If set, skip the npm pack staleness check (used by hugo mod npm pack).
|
||||
SkipNpmCheck bool
|
||||
}
|
||||
|
||||
func (d ConfigSourceDescriptor) configFilenames() []string {
|
||||
@@ -166,7 +174,7 @@ func (l configLoader) applyConfigAliases() error {
|
||||
}
|
||||
|
||||
func (l configLoader) applyDefaultConfig() error {
|
||||
defaultSettings := maps.Params{
|
||||
defaultSettings := hmaps.Params{
|
||||
// These dirs are used early/before we build the config struct.
|
||||
"themesDir": "themes",
|
||||
"configDir": "config",
|
||||
@@ -179,14 +187,14 @@ func (l configLoader) applyDefaultConfig() error {
|
||||
|
||||
func (l configLoader) normalizeCfg(cfg config.Provider) error {
|
||||
if b, ok := cfg.Get("minifyOutput").(bool); ok {
|
||||
hugo.Deprecate("site config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
|
||||
hugo.Deprecate("project config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
|
||||
if b {
|
||||
cfg.Set("minify.minifyOutput", true)
|
||||
}
|
||||
} else if b, ok := cfg.Get("minify").(bool); ok {
|
||||
hugo.Deprecate("site config minify", "Use minify.minifyOutput instead.", "v0.150.0")
|
||||
hugo.Deprecate("project config minify", "Use minify.minifyOutput instead.", "v0.150.0")
|
||||
if b {
|
||||
cfg.Set("minify", maps.Params{"minifyOutput": true})
|
||||
cfg.Set("minify", hmaps.Params{"minifyOutput": true})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +248,7 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
|
||||
}
|
||||
|
||||
for _, env := range hugoEnv {
|
||||
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
|
||||
existing, nestedKey, owner, err := hmaps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -477,6 +485,7 @@ func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bo
|
||||
PublishDir: publishDir,
|
||||
Environment: l.Environment,
|
||||
CacheDir: conf.Caches.CacheDirModules(),
|
||||
ModuleQueriesCache: configs.FileCaches.ModuleQueriesCache(),
|
||||
ModuleConfig: conf.Module,
|
||||
IgnoreVendor: ignoreVendor,
|
||||
IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist,
|
||||
@@ -547,7 +556,7 @@ func (l configLoader) loadConfig(configName string) (string, error) {
|
||||
}
|
||||
|
||||
func (l configLoader) deleteMergeStrategies() (err error) {
|
||||
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
|
||||
l.cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
|
||||
params[len(params)-1].Params.DeleteMergeStrategy()
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -437,7 +437,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error {
|
||||
return nil
|
||||
}
|
||||
return func(ss string) bool {
|
||||
match = targetRe.MatchString(ss)
|
||||
match := targetRe.MatchString(ss)
|
||||
matchString := "no match"
|
||||
if match {
|
||||
matchString = "match!"
|
||||
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
@@ -222,11 +222,11 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
|
||||
return cfg, dirnames, nil
|
||||
}
|
||||
|
||||
var keyAliases maps.KeyRenamer
|
||||
var keyAliases hmaps.KeyRenamer
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
keyAliases, err = maps.NewKeyRenamer(
|
||||
keyAliases, err = hmaps.NewKeyRenamer(
|
||||
// Before 0.53 we used singular for "menu".
|
||||
"{menu,languages/*/menu}", "menus",
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ package config
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
@@ -41,6 +41,7 @@ type AllProvider interface {
|
||||
Dirs() CommonDirs
|
||||
Quiet() bool
|
||||
DirsBase() CommonDirs
|
||||
FileCaches() any
|
||||
ContentTypes() ContentTypesProvider
|
||||
GetConfigSection(string) any
|
||||
GetConfig() any
|
||||
@@ -79,6 +80,7 @@ type AllProvider interface {
|
||||
WorkingDir() string
|
||||
EnableEmoji() bool
|
||||
ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions
|
||||
CacheDirMisc() string
|
||||
}
|
||||
|
||||
// We cannot import the media package as that would create a circular dependency.
|
||||
@@ -95,7 +97,7 @@ type Provider interface {
|
||||
GetString(key string) string
|
||||
GetInt(key string) int
|
||||
GetBool(key string) bool
|
||||
GetParams(key string) maps.Params
|
||||
GetParams(key string) hmaps.Params
|
||||
GetStringMap(key string) map[string]any
|
||||
GetStringMapString(key string) map[string]string
|
||||
GetStringSlice(key string) []string
|
||||
@@ -103,9 +105,9 @@ type Provider interface {
|
||||
Set(key string, value any)
|
||||
Keys() []string
|
||||
Merge(key string, value any)
|
||||
SetDefaults(params maps.Params)
|
||||
SetDefaults(params hmaps.Params)
|
||||
SetDefaultMergeStrategy()
|
||||
WalkParams(walkFn func(params ...maps.KeyParams) bool)
|
||||
WalkParams(walkFn func(params ...hmaps.KeyParams) bool)
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
|
||||
@@ -20,21 +20,20 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
)
|
||||
|
||||
// New creates a Provider backed by an empty maps.Params.
|
||||
// New creates a Provider backed by an empty hmaps.Params.
|
||||
func New() Provider {
|
||||
return &defaultConfigProvider{
|
||||
root: make(maps.Params),
|
||||
root: make(hmaps.Params),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFrom creates a Provider backed by params.
|
||||
func NewFrom(params maps.Params) Provider {
|
||||
maps.PrepareParams(params)
|
||||
func NewFrom(params hmaps.Params) Provider {
|
||||
hmaps.PrepareParams(params)
|
||||
return &defaultConfigProvider{
|
||||
root: params,
|
||||
}
|
||||
@@ -44,7 +43,7 @@ func NewFrom(params maps.Params) Provider {
|
||||
// All methods are thread safe.
|
||||
type defaultConfigProvider struct {
|
||||
mu sync.RWMutex
|
||||
root maps.Params
|
||||
root hmaps.Params
|
||||
|
||||
keyCache sync.Map
|
||||
}
|
||||
@@ -90,22 +89,22 @@ func (c *defaultConfigProvider) GetString(k string) string {
|
||||
return cast.ToString(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetParams(k string) maps.Params {
|
||||
func (c *defaultConfigProvider) GetParams(k string) hmaps.Params {
|
||||
v := c.Get(k)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.(maps.Params)
|
||||
return v.(hmaps.Params)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMap(k string) map[string]any {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMap(v)
|
||||
return hmaps.ToStringMap(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMapString(v)
|
||||
return hmaps.ToStringMapString(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringSlice(k string) []string {
|
||||
@@ -120,9 +119,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
|
||||
k = strings.ToLower(k)
|
||||
|
||||
if k == "" {
|
||||
if p, err := maps.ToParamsAndPrepare(v); err == nil {
|
||||
if p, err := hmaps.ToParamsAndPrepare(v); err == nil {
|
||||
// Set the values directly in root.
|
||||
maps.SetParams(c.root, p)
|
||||
hmaps.SetParams(c.root, p)
|
||||
} else {
|
||||
c.root[k] = v
|
||||
}
|
||||
@@ -132,7 +131,7 @@ func (c *defaultConfigProvider) Set(k string, v any) {
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]any, map[any]any, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
p := hmaps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
@@ -142,9 +141,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
|
||||
}
|
||||
|
||||
if existing, found := m[key]; found {
|
||||
if p1, ok := existing.(maps.Params); ok {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
maps.SetParams(p1, p2)
|
||||
if p1, ok := existing.(hmaps.Params); ok {
|
||||
if p2, ok := v.(hmaps.Params); ok {
|
||||
hmaps.SetParams(p1, p2)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -154,8 +153,8 @@ func (c *defaultConfigProvider) Set(k string, v any) {
|
||||
}
|
||||
|
||||
// SetDefaults will set values from params if not already set.
|
||||
func (c *defaultConfigProvider) SetDefaults(params maps.Params) {
|
||||
maps.PrepareParams(params)
|
||||
func (c *defaultConfigProvider) SetDefaults(params hmaps.Params) {
|
||||
hmaps.PrepareParams(params)
|
||||
for k, v := range params {
|
||||
if _, found := c.root[k]; !found {
|
||||
c.root[k] = v
|
||||
@@ -170,28 +169,28 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
|
||||
|
||||
if k == "" {
|
||||
rs, f := c.root.GetMergeStrategy()
|
||||
if f && rs == maps.ParamsMergeStrategyNone {
|
||||
if f && rs == hmaps.ParamsMergeStrategyNone {
|
||||
// The user has set a "no merge" strategy on this,
|
||||
// nothing more to do.
|
||||
return
|
||||
}
|
||||
|
||||
if p, err := maps.ToParamsAndPrepare(v); err == nil {
|
||||
if p, err := hmaps.ToParamsAndPrepare(v); err == nil {
|
||||
// As there may be keys in p not in root, we need to handle
|
||||
// those as a special case.
|
||||
var keysToDelete []string
|
||||
for kk, vv := range p {
|
||||
if pp, ok := vv.(maps.Params); ok {
|
||||
if pp, ok := vv.(hmaps.Params); ok {
|
||||
if pppi, ok := c.root[kk]; ok {
|
||||
ppp := pppi.(maps.Params)
|
||||
maps.MergeParamsWithStrategy("", ppp, pp)
|
||||
ppp := pppi.(hmaps.Params)
|
||||
hmaps.MergeParamsWithStrategy("", ppp, pp)
|
||||
} else {
|
||||
// We need to use the default merge strategy for
|
||||
// this key.
|
||||
np := make(maps.Params)
|
||||
strategy := c.determineMergeStrategy(maps.KeyParams{Key: "", Params: c.root}, maps.KeyParams{Key: kk, Params: np})
|
||||
np := make(hmaps.Params)
|
||||
strategy := c.determineMergeStrategy(hmaps.KeyParams{Key: "", Params: c.root}, hmaps.KeyParams{Key: kk, Params: np})
|
||||
np.SetMergeStrategy(strategy)
|
||||
maps.MergeParamsWithStrategy("", np, pp)
|
||||
hmaps.MergeParamsWithStrategy("", np, pp)
|
||||
c.root[kk] = np
|
||||
if np.IsZero() {
|
||||
// Just keep it until merge is done.
|
||||
@@ -201,7 +200,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
|
||||
}
|
||||
}
|
||||
// Merge the rest.
|
||||
maps.MergeParams(c.root, p)
|
||||
hmaps.MergeParams(c.root, p)
|
||||
for _, k := range keysToDelete {
|
||||
delete(c.root, k)
|
||||
}
|
||||
@@ -214,7 +213,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]any, map[any]any, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
p := hmaps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
@@ -224,9 +223,9 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
|
||||
}
|
||||
|
||||
if existing, found := m[key]; found {
|
||||
if p1, ok := existing.(maps.Params); ok {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
maps.MergeParamsWithStrategy("", p1, p2)
|
||||
if p1, ok := existing.(hmaps.Params); ok {
|
||||
if p2, ok := v.(hmaps.Params); ok {
|
||||
hmaps.MergeParamsWithStrategy("", p1, p2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -245,10 +244,10 @@ func (c *defaultConfigProvider) Keys() []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) {
|
||||
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...hmaps.KeyParams) bool) {
|
||||
maxDepth := 1000
|
||||
var walk func(depth int, params ...maps.KeyParams)
|
||||
walk = func(depth int, params ...maps.KeyParams) {
|
||||
var walk func(depth int, params ...hmaps.KeyParams)
|
||||
walk = func(depth int, params ...hmaps.KeyParams) {
|
||||
if depth > maxDepth {
|
||||
panic(errors.New("max depth exceeded"))
|
||||
}
|
||||
@@ -258,24 +257,24 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams)
|
||||
p1 := params[len(params)-1]
|
||||
i := len(params)
|
||||
for k, v := range p1.Params {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
paramsplus1 := make([]maps.KeyParams, i+1)
|
||||
if p2, ok := v.(hmaps.Params); ok {
|
||||
paramsplus1 := make([]hmaps.KeyParams, i+1)
|
||||
copy(paramsplus1, params)
|
||||
paramsplus1[i] = maps.KeyParams{Key: k, Params: p2}
|
||||
paramsplus1[i] = hmaps.KeyParams{Key: k, Params: p2}
|
||||
walk(depth+1, paramsplus1...)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(0, maps.KeyParams{Key: "", Params: c.root})
|
||||
walk(0, hmaps.KeyParams{Key: "", Params: c.root})
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy {
|
||||
func (c *defaultConfigProvider) determineMergeStrategy(params ...hmaps.KeyParams) hmaps.ParamsMergeStrategy {
|
||||
if len(params) == 0 {
|
||||
return maps.ParamsMergeStrategyNone
|
||||
return hmaps.ParamsMergeStrategyNone
|
||||
}
|
||||
|
||||
var (
|
||||
strategy maps.ParamsMergeStrategy
|
||||
strategy hmaps.ParamsMergeStrategy
|
||||
prevIsRoot bool
|
||||
curr = params[len(params)-1]
|
||||
)
|
||||
@@ -299,10 +298,10 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams)
|
||||
// Don't set a merge strategy on the root unless set by user.
|
||||
// This will be handled as a special case.
|
||||
case "params":
|
||||
strategy = maps.ParamsMergeStrategyDeep
|
||||
strategy = hmaps.ParamsMergeStrategyDeep
|
||||
case "outputformats", "mediatypes":
|
||||
if prevIsRoot {
|
||||
strategy = maps.ParamsMergeStrategyShallow
|
||||
strategy = hmaps.ParamsMergeStrategyShallow
|
||||
}
|
||||
case "menus":
|
||||
isMenuKey := prevIsRoot
|
||||
@@ -314,11 +313,11 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams)
|
||||
}
|
||||
}
|
||||
if isMenuKey {
|
||||
strategy = maps.ParamsMergeStrategyShallow
|
||||
strategy = hmaps.ParamsMergeStrategyShallow
|
||||
}
|
||||
default:
|
||||
if strategy == "" {
|
||||
strategy = maps.ParamsMergeStrategyNone
|
||||
strategy = hmaps.ParamsMergeStrategyNone
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +325,7 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
|
||||
c.WalkParams(func(params ...maps.KeyParams) bool {
|
||||
c.WalkParams(func(params ...hmaps.KeyParams) bool {
|
||||
if len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
@@ -344,7 +343,7 @@ func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
|
||||
})
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
|
||||
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, hmaps.Params) {
|
||||
var parts []string
|
||||
v, ok := c.keyCache.Load(key)
|
||||
if ok {
|
||||
@@ -358,14 +357,14 @@ func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (str
|
||||
next, found := current[parts[i]]
|
||||
if !found {
|
||||
if create {
|
||||
next = make(maps.Params)
|
||||
next = make(hmaps.Params)
|
||||
current[parts[i]] = next
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
var ok bool
|
||||
current, ok = next.(maps.Params)
|
||||
current, ok = next.(hmaps.Params)
|
||||
if !ok {
|
||||
// E.g. a string, not a map that we can store values in.
|
||||
return "", nil
|
||||
|
||||
@@ -22,10 +22,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hmaps"
|
||||
"github.com/gohugoio/hugo/common/para"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
@@ -48,7 +47,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Assert(cfg.Get(k), qt.Equals, v)
|
||||
c.Assert(cfg.GetInt(k), qt.Equals, v)
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"foo": 42,
|
||||
})
|
||||
})
|
||||
@@ -60,7 +59,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, hmaps.Params{
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
@@ -76,14 +75,14 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
})
|
||||
cfg.Set("a.c", "cv")
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
|
||||
"b": "bv",
|
||||
"c": "cv",
|
||||
})
|
||||
c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
|
||||
|
||||
cfg.Set("b.a", "av")
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, hmaps.Params{
|
||||
"a": "av",
|
||||
})
|
||||
|
||||
@@ -91,7 +90,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, hmaps.Params{
|
||||
"a": "av",
|
||||
"b": "bv",
|
||||
})
|
||||
@@ -105,7 +104,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
@@ -118,7 +117,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
@@ -137,7 +136,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, hmaps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
@@ -155,7 +154,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"c": "cv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
|
||||
"b": "bv",
|
||||
"c": "cv2",
|
||||
})
|
||||
@@ -169,7 +168,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"a": "av",
|
||||
})
|
||||
})
|
||||
@@ -192,11 +191,11 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"e": "ev2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
|
||||
"e": "ev2",
|
||||
"_merge": maps.ParamsMergeStrategyShallow,
|
||||
"_merge": hmaps.ParamsMergeStrategyShallow,
|
||||
"b": "bv",
|
||||
"c": maps.Params{
|
||||
"c": hmaps.Params{
|
||||
"b": "bv",
|
||||
},
|
||||
})
|
||||
@@ -221,15 +220,15 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
"b": left,
|
||||
})
|
||||
|
||||
cfg.Merge("", maps.Params{
|
||||
"b": maps.Params{
|
||||
cfg.Merge("", hmaps.Params{
|
||||
"b": hmaps.Params{
|
||||
"c": "cv2",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"b": maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"b": hmaps.Params{
|
||||
"c": "cv1",
|
||||
"d": "dv2",
|
||||
},
|
||||
@@ -267,8 +266,8 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
|
||||
cfg.Merge("a", right)
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"a": hmaps.Params{
|
||||
"b": "bv1",
|
||||
"c": "cv2",
|
||||
},
|
||||
@@ -292,7 +291,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
|
||||
"b": "bv",
|
||||
})
|
||||
})
|
||||
@@ -341,7 +340,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
return err
|
||||
}
|
||||
|
||||
m := maps.Params{
|
||||
m := hmaps.Params{
|
||||
"new": 42,
|
||||
}
|
||||
|
||||
@@ -371,8 +370,8 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
cfg := New()
|
||||
k := "foo"
|
||||
|
||||
cfg.Set(k, maps.Params{k: true})
|
||||
c.Assert(cfg.GetParams(k), qt.DeepEquals, maps.Params{
|
||||
cfg.Set(k, hmaps.Params{k: true})
|
||||
c.Assert(cfg.GetParams(k), qt.DeepEquals, hmaps.Params{
|
||||
k: true,
|
||||
})
|
||||
|
||||
@@ -384,8 +383,8 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
k := "foo"
|
||||
k2 := "bar"
|
||||
|
||||
cfg.Set(k, maps.Params{k: struct{}{}})
|
||||
cfg.Set(k2, maps.Params{k2: struct{}{}})
|
||||
cfg.Set(k, hmaps.Params{k: struct{}{}})
|
||||
cfg.Set(k2, hmaps.Params{k2: struct{}{}})
|
||||
|
||||
c.Assert(len(cfg.Keys()), qt.Equals, 2)
|
||||
|
||||
@@ -401,11 +400,11 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("WalkParams", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("x", maps.Params{})
|
||||
cfg.Set("y", maps.Params{})
|
||||
cfg.Set("x", hmaps.Params{})
|
||||
cfg.Set("y", hmaps.Params{})
|
||||
|
||||
var got []string
|
||||
cfg.WalkParams(func(params ...maps.KeyParams) bool {
|
||||
cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
|
||||
got = append(got, params[len(params)-1].Key)
|
||||
return false
|
||||
})
|
||||
@@ -417,7 +416,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Assert(got, qt.DeepEquals, want)
|
||||
|
||||
cfg = New()
|
||||
cfg.WalkParams(func(params ...maps.KeyParams) bool {
|
||||
cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -429,7 +428,7 @@ func TestDefaultConfigProvider(t *testing.T) {
|
||||
c.Run("SetDefaults", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.SetDefaults(maps.Params{
|
||||
cfg.SetDefaults(hmaps.Params{
|
||||
"foo": "bar",
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user