mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
481 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a25af7facf | |||
| 723579ff54 | |||
| 85ad5e48eb | |||
| 7b5199fdef | |||
| e31ff547d2 | |||
| 87260e4a60 | |||
| 8405b802cf | |||
| bf05832d14 | |||
| a05736cb9f | |||
| 49dceb19f5 | |||
| 5e7099256e | |||
| 0805c734a4 | |||
| 76a5e1880a | |||
| 0bb337b22a | |||
| 03dc9170b3 | |||
| c829b736cc | |||
| 995a2159e5 | |||
| f88f0a9f59 | |||
| dd3f2731e0 | |||
| f772998fa3 | |||
| 8a55df7af2 | |||
| 52c9bd7908 | |||
| 44da086082 | |||
| 2ffaf1fc1e | |||
| a808f6e40d | |||
| 94f3908ec6 | |||
| 75fcc7524a | |||
| 33d1f2c853 | |||
| 64da6d7c62 | |||
| 70db201ed4 | |||
| 8a468df065 | |||
| b5fa03d3aa | |||
| 9da472dd3b | |||
| 615e45d6e8 | |||
| 635532a20f | |||
| a243a6154c | |||
| 9c71f600bf | |||
| 420527fc6b | |||
| 7fe786e3b0 | |||
| 03b244fc0b | |||
| 961181334f | |||
| e35b7f049d | |||
| 7d90277a28 | |||
| 0796fa7ace | |||
| 6bf15241a1 | |||
| 861ede6d10 | |||
| f228c87d41 | |||
| 1b701b72ca | |||
| a32d70b712 | |||
| 948cfb98f9 | |||
| 8930802ef2 | |||
| 7df45f615a | |||
| f961093ea9 | |||
| 89b8c32200 | |||
| 984358f047 | |||
| d1f191c58d | |||
| a198116669 | |||
| ce2470e701 | |||
| 921db7b52c | |||
| 786ce71e59 | |||
| 5ad2846161 | |||
| 36ad9f583a | |||
| 7c0a0bc979 | |||
| d83ce27ae0 | |||
| 5a5f4a5495 | |||
| c6acc246ab | |||
| 29ed932513 | |||
| 7b4ddd1863 | |||
| 671897ae91 | |||
| a879ebfaaa | |||
| 499794d19e | |||
| feb3d494b9 | |||
| 65c82178b7 | |||
| 332d5ec823 | |||
| 212cc11ada | |||
| dfb35dcd7a | |||
| ef053faa6a | |||
| af91eb1997 | |||
| a5ec542393 | |||
| 884439b9a2 | |||
| 128fb17c2b | |||
| e46d37a984 | |||
| 790a8aa474 | |||
| fe06735214 | |||
| d15baf53a9 | |||
| 4d22555aeb | |||
| ce1a7e0bce | |||
| c86d9f4aa8 | |||
| e8988c3141 | |||
| 70a9068aa6 | |||
| 9d66d513ce | |||
| f013346667 | |||
| 96e06e1ab8 | |||
| 19a5cec0b9 | |||
| 134674f00d | |||
| 147f605f7d | |||
| 1f35beb918 | |||
| 2a4fd58818 | |||
| 93c8c7d345 | |||
| 95e5e9f4ab | |||
| a00b5c72ac | |||
| cf9c8f93ca | |||
| 11f09f59fc | |||
| 2602796cf1 | |||
| 72495f9fba | |||
| 4a9485336a | |||
| 1d018ef857 | |||
| 121bc6ceb2 | |||
| 0d29fc81bb | |||
| bb57404f3d | |||
| 781fabf4e4 | |||
| cf18b827e2 | |||
| 98ad9b3c03 | |||
| ff2903a931 | |||
| 7d1b1fb33d | |||
| b89e7fe675 | |||
| e8fefc8388 | |||
| a043d3ec63 | |||
| 77a1147056 | |||
| 341f575d2d | |||
| 248241b6e1 | |||
| 4e47d95db9 | |||
| 03b4b54220 | |||
| 79be0532f1 | |||
| 0f440460c8 | |||
| 4e17421ec2 | |||
| b01ecd4cd4 | |||
| ca68936d61 | |||
| 45c00b7c16 | |||
| 28d882ab70 | |||
| b0fd7ee2e1 | |||
| bba860e3ed | |||
| 59f35cd985 | |||
| 21e9868fe2 | |||
| 2760288c6d | |||
| c270975049 | |||
| ea8b48af64 | |||
| 076dfe13d0 | |||
| e41a06447d | |||
| 90d9f812b2 | |||
| 80e60847fb | |||
| df5421918a | |||
| aeb9a5cc02 | |||
| c4bbc2805c | |||
| d8c70218b7 | |||
| ee4f1acd93 | |||
| b6133657e0 | |||
| 4bc7caea14 | |||
| 7711ba4d0c | |||
| d2c821b5c7 | |||
| 4f444c810c | |||
| fe6c72652f | |||
| 4ed7600fd6 | |||
| cbe4339a5f | |||
| 6475d308ec | |||
| 67aede4364 | |||
| 87f194b249 | |||
| 5f01b0603d | |||
| c23d97904f | |||
| 5d51b82a7e | |||
| d81e3c29c0 | |||
| 7c65a4dbc0 | |||
| d31a9275c6 | |||
| 6a2a03806d | |||
| cf1de598d9 | |||
| c36608c584 | |||
| 1e4037cade | |||
| 2f361a8e25 | |||
| 81d77620c6 | |||
| 5559263326 | |||
| 97f990cc4f | |||
| 656fc04035 | |||
| a20cb5b1c0 | |||
| b99634e253 | |||
| 4d775cbe95 | |||
| fdd977e95d | |||
| ae7bf74b3e | |||
| 123018de21 | |||
| ba5d812673 | |||
| be4a0df3a2 | |||
| e4cf565c0d | |||
| 9e64953338 | |||
| f0cfc28c00 | |||
| 16e854a437 | |||
| f8b5fa09a6 | |||
| 86fbb0f7a8 | |||
| 7d4af7a179 | |||
| 28147cb040 | |||
| db40fada48 | |||
| b88fa8cc66 | |||
| 88d838a971 | |||
| e51e761d9c | |||
| 7011239205 | |||
| 694906f6f1 | |||
| d27b9c06bd | |||
| 62cef3678b | |||
| ff22c62a32 | |||
| 7fd65e16e4 | |||
| 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 |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22500.20400
|
||||
- image: bepsays/ci-hugoreleaser:1.22700.20000
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20400
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22700.20000
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: false
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: true
|
||||
summary: false
|
||||
code_review: false
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
@@ -1,10 +0,0 @@
|
||||
🛑 Before submitting this PR
|
||||
|
||||
All PRs must be linked to an Issue in this repository, and that Issue must be approved by a project maintainer:
|
||||
|
||||
- For a bug, approval means that the "needs triage" label is removed.
|
||||
- For a proposal, approval means that the "proposal" label is replaced by an "enhancement" label.
|
||||
|
||||
PRs without a linked, approved Issue will be closed immediately.
|
||||
|
||||
Please review the contribution guide (CONTRIBUTING.md) to understand commit message guidelines and AI assistance disclosures.
|
||||
@@ -1,22 +0,0 @@
|
||||
name: AI Detection
|
||||
on:
|
||||
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
|
||||
pull_request_target:
|
||||
types: [opened, ready_for_review]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
detect-ai:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: AI Watchdog
|
||||
uses: bep/ai-watchdog@995475dd81767f8035dc2c943e9230918d06caca # temporary fork (probably)
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
pr-label: AI Suspect
|
||||
fail-when-confident: false
|
||||
# CSV list.
|
||||
pr-user-logins-skip: dependabot[bot],bep,jmooring
|
||||
@@ -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
|
||||
|
||||
+23
-15
@@ -16,8 +16,8 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
go-version: [1.27.x]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
@@ -32,35 +32,39 @@ 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
|
||||
cache: true
|
||||
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 Ruby gems
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
gem install asciidoctor-html5s -v "0.5.1"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
- 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
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install gotmplfmt
|
||||
run: go install github.com/gohugoio/gotmplfmt@latest
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
pip install docutils Pygments
|
||||
rst2html --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
@@ -104,9 +108,15 @@ jobs:
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install staticcheck
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Check embedded go template formatting
|
||||
run: "diff <(gotmplfmt -d tpl/tplimpl/embedded/templates) <(printf '')"
|
||||
- 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: |
|
||||
@@ -118,16 +128,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
|
||||
|
||||
+4
-1
@@ -3,4 +3,7 @@
|
||||
imports.*
|
||||
dist/
|
||||
public/
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
cache/filecache/_gen/
|
||||
.claude/
|
||||
dump.txt
|
||||
@@ -0,0 +1 @@
|
||||
tpl/tplimpl/embedded/templates/**
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
* 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, almost always write end-to-end integration tests using `hugolib.Test` or one of its siblings. Write unit tests only for isolated utilities.
|
||||
* 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`; always give the test function a descriptive name, e.g. `TestDisablePathToLower`, and add any issue reference as a Go doc function comment, e.g. `// See issue 1234.`.
|
||||
* If you borrow a test case (e.g. from the issue), that test's author must be added as co-author in the commit.
|
||||
* If you're a security researcher, read @SECURITY.md carefully.
|
||||
* 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.
|
||||
+7
-5
@@ -1,5 +1,3 @@
|
||||
>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
|
||||
|
||||
# Contributing to Hugo
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
@@ -74,21 +72,25 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* When you’re ready to create a pull request, be sure to:
|
||||
* Sign the [CLA](https://cla-assistant.io/gohugoio/hugo).
|
||||
* Have test cases for the new code. If you have questions about how to do this, please ask in your pull request.
|
||||
* If you borrow a test case (e.g. from the issue), that test's author must be added as [co-author](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/creating-a-commit-with-multiple-authors) in the commit.
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Ensure that `./check.sh` succeeds. Note that some tests are skipped when running locally, some because they are slow. To run these locally, do `CI_LOCAL=true ./check.sh ./somepackage/...`.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
## AI Assistance Notice
|
||||
|
||||
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
|
||||
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used. AI contributions from non-maintainers needs to have a fairly narrow scope (e.g. a bug fix), as we have limited review capacity.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
> This PR was written primarily by Claude Code.
|
||||
|
||||
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it.
|
||||
Also, When using AI assistance:
|
||||
|
||||
* We expect contributors to manually verify that the state of the pull request is OK (e.g. that the CLI is signed).
|
||||
* We expect contributors to understand the code that is produced and be able to answer critical questions about it
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
|
||||
|
||||
+5
-3
@@ -2,8 +2,8 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
ARG GO_VERSION="1.25"
|
||||
ARG ALPINE_VERSION="3.22"
|
||||
ARG GO_VERSION="1.27"
|
||||
ARG ALPINE_VERSION="3.24"
|
||||
ARG DART_SASS_VERSION="1.79.3"
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
|
||||
@@ -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,24 +25,24 @@
|
||||
[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.
|
||||
|
||||
---
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](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,14 +57,15 @@ 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
|
||||
|
||||
@@ -72,87 +78,97 @@ See the [features] section of the documentation for a comprehensive summary of H
|
||||
|
||||
## Editions
|
||||
|
||||
Hugo offers a standard edition with core features, plus extended and extended/deploy editions with more. Use the standard edition unless you need the features below.
|
||||
Hugo is available in several editions. Use the standard edition unless you need additional features.
|
||||
|
||||
Feature|extended edition|extended/deploy edition
|
||||
:--|:-:|:-:
|
||||
[Transpile Sass to CSS] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass] transpiler instead, which is compatible with any edition.|:heavy_check_mark:|:heavy_check_mark:
|
||||
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
|
||||
[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][].
|
||||
|
||||
(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.26.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
|
||||
```
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#gohugoio/hugo&Timeline)
|
||||
|
||||
## 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).
|
||||
|
||||
@@ -162,9 +178,9 @@ 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)
|
||||
- [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
|
||||
|
||||
@@ -174,10 +190,48 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
|
||||
<summary>See current dependencies</summary>
|
||||
|
||||
```text
|
||||
cel.dev/expr="v0.25.1"
|
||||
cloud.google.com/go/auth/oauth2adapt="v0.2.8"
|
||||
cloud.google.com/go/auth="v0.20.0"
|
||||
cloud.google.com/go/compute/metadata="v0.9.0"
|
||||
cloud.google.com/go/iam="v1.5.3"
|
||||
cloud.google.com/go/monitoring="v1.24.3"
|
||||
cloud.google.com/go/storage="v1.57.2"
|
||||
cloud.google.com/go="v0.123.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.20.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.13.1"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.11.2"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.6.3"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.1"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.6.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/JohannesKaufmann/dom="v0.2.0"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.21.1"
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp="v1.31.0"
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric="v0.54.0"
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping="v0.54.0"
|
||||
github.com/JohannesKaufmann/dom="v0.3.1"
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.2"
|
||||
github.com/alecthomas/chroma/v2="v2.27.0"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.7.8"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.32.2"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.19.2"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.18.14"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.20.12"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.4.22"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.7.22"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.8.4"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.4.22"
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront="v1.61.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.13.7"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.9.13"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.13.21"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.19.21"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.97.3"
|
||||
github.com/aws/aws-sdk-go-v2/service/signin="v1.0.2"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.30.5"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.35.10"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.41.2"
|
||||
github.com/aws/aws-sdk-go-v2="v1.41.6"
|
||||
github.com/aws/smithy-go="v1.25.0"
|
||||
github.com/aymerick/douceur="v0.2.0"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
@@ -185,105 +239,129 @@ github.com/bep/gitmap="v1.9.0"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.5.0"
|
||||
github.com/bep/golibsass="v1.2.0"
|
||||
github.com/bep/goportabletext="v0.1.0"
|
||||
github.com/bep/helpers="v0.6.0"
|
||||
github.com/bep/imagemeta="v0.12.0"
|
||||
github.com/bep/lazycache="v0.8.0"
|
||||
github.com/bep/golocales="v0.2.0"
|
||||
github.com/bep/goportabletext="v0.2.0"
|
||||
github.com/bep/helpers="v0.12.0"
|
||||
github.com/bep/imagemeta="v0.17.2"
|
||||
github.com/bep/lazycache="v0.8.1"
|
||||
github.com/bep/logg="v0.4.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.10.0"
|
||||
github.com/bep/simplecobra="v0.6.1"
|
||||
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/bits-and-blooms/bitset="v1.24.4"
|
||||
github.com/bep/mclib="v1.20401.20400"
|
||||
github.com/bep/overlayfs="v0.11.0"
|
||||
github.com/bep/simplecobra="v0.7.0"
|
||||
github.com/bep/textandbinarywriter="v0.1.0"
|
||||
github.com/bep/tmc="v0.6.0"
|
||||
github.com/bits-and-blooms/bitset="v1.24.5"
|
||||
github.com/cespare/xxhash/v2="v2.3.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/clipperhouse/displaywidth="v0.6.0"
|
||||
github.com/clipperhouse/stringish="v0.1.1"
|
||||
github.com/clipperhouse/uax29/v2="v2.3.0"
|
||||
github.com/clipperhouse/displaywidth="v0.10.0"
|
||||
github.com/clipperhouse/uax29/v2="v2.6.0"
|
||||
github.com/cncf/xds/go="v0.0.0-20251210132809-ee656c7534f5"
|
||||
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/evanw/esbuild="v0.27.2"
|
||||
github.com/dlclark/regexp2/v2="v2.2.1"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/envoyproxy/go-control-plane/envoy="v1.36.0"
|
||||
github.com/envoyproxy/protoc-gen-validate="v1.3.0"
|
||||
github.com/evanw/esbuild="v0.28.1"
|
||||
github.com/fatih/color="v1.18.0"
|
||||
github.com/felixge/httpsnoop="v1.0.4"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
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/getkin/kin-openapi="v0.140.0"
|
||||
github.com/go-jose/go-jose/v4="v4.1.4"
|
||||
github.com/go-logr/logr="v1.4.3"
|
||||
github.com/go-logr/stdr="v1.2.2"
|
||||
github.com/go-openapi/jsonpointer="v0.22.5"
|
||||
github.com/go-openapi/swag/jsonname="v0.25.5"
|
||||
github.com/gobuffalo/flect="v1.0.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/goccy/go-yaml="v1.19.1"
|
||||
github.com/goccy/go-yaml="v1.19.2"
|
||||
github.com/gohugoio/gift="v0.2.0"
|
||||
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/gohugoio/hugo-goldmark-extensions/extras="v0.7.0"
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.5.0"
|
||||
github.com/golang-jwt/jwt/v5="v5.3.0"
|
||||
github.com/google/go-cmp="v0.7.0"
|
||||
github.com/google/s2a-go="v0.1.9"
|
||||
github.com/google/uuid="v1.6.0"
|
||||
github.com/google/wire="v0.7.0"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.3.14"
|
||||
github.com/googleapis/gax-go/v2="v2.21.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"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.13"
|
||||
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-colorable="v0.1.14"
|
||||
github.com/mattn/go-isatty="v0.0.22"
|
||||
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.9.1"
|
||||
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
|
||||
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
|
||||
github.com/oasdiff/yaml3="v0.0.13"
|
||||
github.com/oasdiff/yaml="v0.1.0"
|
||||
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/olekukonko/errors="v1.2.0"
|
||||
github.com/olekukonko/ll="v0.1.6"
|
||||
github.com/olekukonko/tablewriter="v1.1.4"
|
||||
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
|
||||
github.com/pelletier/go-toml/v2="v2.2.4"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pelletier/go-toml/v2="v2.4.3"
|
||||
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.14.1"
|
||||
github.com/rogpeppe/go-internal="v1.15.0"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/sass/dart-sass/compiler="1.97.1"
|
||||
github.com/sass/dart-sass/implementation="1.97.1"
|
||||
github.com/sass/dart-sass/protocol="3.2.0"
|
||||
github.com/santhosh-tekuri/jsonschema/v6="v6.0.2"
|
||||
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.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/spf13/pflag="v1.0.10"
|
||||
github.com/spiffe/go-spiffe/v2="v2.6.0"
|
||||
github.com/tdewolff/minify/v2="v2.24.13"
|
||||
github.com/tdewolff/parse/v2="v2.8.12"
|
||||
github.com/tetratelabs/wazero="v1.12.0"
|
||||
github.com/webmproject/libwebp="v1.6.0"
|
||||
github.com/woodsbury/decimal128="v1.3.0"
|
||||
github.com/yuin/goldmark-emoji="v1.0.6"
|
||||
github.com/yuin/goldmark="v1.7.13"
|
||||
github.com/yuin/goldmark="v1.8.2"
|
||||
go.opentelemetry.io/auto/sdk="v1.2.1"
|
||||
go.opentelemetry.io/contrib/detectors/gcp="v1.39.0"
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc="v0.67.0"
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp="v0.67.0"
|
||||
go.opentelemetry.io/otel/metric="v1.43.0"
|
||||
go.opentelemetry.io/otel/sdk/metric="v1.43.0"
|
||||
go.opentelemetry.io/otel/sdk="v1.43.0"
|
||||
go.opentelemetry.io/otel/trace="v1.43.0"
|
||||
go.opentelemetry.io/otel="v1.43.0"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
go.yaml.in/yaml/v3="v3.0.4"
|
||||
golang.org/x/crypto="v0.46.0"
|
||||
golang.org/x/image="v0.34.0"
|
||||
golang.org/x/mod="v0.31.0"
|
||||
golang.org/x/net="v0.48.0"
|
||||
golang.org/x/sync="v0.19.0"
|
||||
golang.org/x/sys="v0.39.0"
|
||||
golang.org/x/text="v0.32.0"
|
||||
golang.org/x/tools="v0.40.0"
|
||||
google.golang.org/protobuf="v1.36.10"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
gocloud.dev="v0.45.0"
|
||||
golang.org/x/crypto="v0.53.0"
|
||||
golang.org/x/image="v0.43.0"
|
||||
golang.org/x/mod="v0.37.0"
|
||||
golang.org/x/net="v0.56.0"
|
||||
golang.org/x/oauth2="v0.36.0"
|
||||
golang.org/x/sync="v0.21.0"
|
||||
golang.org/x/sys="v0.46.0"
|
||||
golang.org/x/text="v0.38.0"
|
||||
golang.org/x/time="v0.15.0"
|
||||
golang.org/x/tools="v0.47.0"
|
||||
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
|
||||
google.golang.org/api="v0.276.0"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20260319201613-d00831a3d3e7"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20260401024825-9d38bb4040a9"
|
||||
google.golang.org/genproto="v0.0.0-20260319201613-d00831a3d3e7"
|
||||
google.golang.org/grpc="v1.80.0"
|
||||
google.golang.org/protobuf="v1.36.11"
|
||||
rsc.io/qr="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.7.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
+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
+145
-71
@@ -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"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
"github.com/bep/helpers/maphelpers"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
@@ -45,22 +47,17 @@ 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
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
seenMu sync.RWMutex
|
||||
seen map[string]struct{}
|
||||
seen *maphelpers.ConcurrentSet[string]
|
||||
|
||||
*locker.Locker
|
||||
}
|
||||
@@ -68,16 +65,7 @@ type lockTracker struct {
|
||||
// Lock tracks the ids in use. We use this information to do garbage collection
|
||||
// after a Hugo build.
|
||||
func (l *lockTracker) Lock(id string) {
|
||||
l.seenMu.RLock()
|
||||
if _, seen := l.seen[id]; !seen {
|
||||
l.seenMu.RUnlock()
|
||||
l.seenMu.Lock()
|
||||
l.seen[id] = struct{}{}
|
||||
l.seenMu.Unlock()
|
||||
} else {
|
||||
l.seenMu.RUnlock()
|
||||
}
|
||||
|
||||
l.seen.AddIfAbsent(id)
|
||||
l.Locker.Lock(id)
|
||||
}
|
||||
|
||||
@@ -88,12 +76,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: maphelpers.NewConcurrentSet[string]()},
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +100,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 +123,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 +153,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 +183,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 +198,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 +217,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 +228,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 +285,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 +306,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 +317,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 +369,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 +379,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 +413,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 +438,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 +448,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 +493,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 +511,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 +554,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
+96
-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,19 +35,14 @@ 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
|
||||
@@ -67,9 +63,22 @@ 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,
|
||||
@@ -88,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.
|
||||
@@ -99,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.
|
||||
@@ -127,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]
|
||||
@@ -154,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,
|
||||
@@ -178,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`)
|
||||
}
|
||||
|
||||
+6
-7
@@ -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"))
|
||||
|
||||
@@ -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
+95
-19
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -50,8 +51,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
|
||||
@@ -59,6 +60,29 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
counter := 0
|
||||
|
||||
seen := c.entryLocker.seen
|
||||
seenByLower := make(map[string]string, seen.Len())
|
||||
for id := range seen.All() {
|
||||
seenByLower[strings.ToLower(id)] = id
|
||||
}
|
||||
|
||||
// Names on disk matching a used cache key except for the case, and the used keys
|
||||
// actually walked. See the note about case-insensitive filesystems below.
|
||||
var candidates map[string]string
|
||||
visited := make(map[string]bool, seen.Len())
|
||||
|
||||
remove := func(name string) error {
|
||||
err := c.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
return nil
|
||||
}
|
||||
if !herrors.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil
|
||||
@@ -93,35 +117,83 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
shouldRemove := force || c.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && len(c.nlocker.seen) > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
_, seen := c.nlocker.seen[name]
|
||||
shouldRemove = !seen
|
||||
if seen.Has(name) {
|
||||
visited[name] = true
|
||||
} else if !shouldRemove && seen.Len() > 0 {
|
||||
if id, found := seenByLower[strings.ToLower(name)]; found {
|
||||
// On case-insensitive filesystems this is the same file as id; e.g. an
|
||||
// entry created before Hugo started lowercasing the content paths in
|
||||
// v0.123 (content/MyBundle => _gen/images/MyBundle). Decided once the
|
||||
// walk is done: if id is walked too, they are distinct files and this
|
||||
// one is stale. See issue 15101.
|
||||
if candidates == nil {
|
||||
candidates = make(map[string]string)
|
||||
}
|
||||
candidates[name] = id
|
||||
} else {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
shouldRemove = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldRemove {
|
||||
err := c.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
}
|
||||
|
||||
if err != nil && !herrors.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return remove(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return counter, err
|
||||
}
|
||||
|
||||
return counter, err
|
||||
for name, id := range candidates {
|
||||
if visited[id] {
|
||||
if err := remove(name); err != nil {
|
||||
return counter, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
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 +205,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)
|
||||
}
|
||||
|
||||
+122
-57
@@ -16,20 +16,83 @@ package filecache_test
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
// A cache entry created before Hugo started lowercasing content paths in v0.123
|
||||
// (e.g. _gen/images/MyBundle) is on a case-insensitive filesystem the same file as
|
||||
// the lowercased cache key used today, and must not be pruned.
|
||||
// See issue 15101.
|
||||
func TestPruneCacheEntryWithOtherCase(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
dir := t.TempDir()
|
||||
if isCaseInsensitive, err := htesting.IsCaseInsensitiveFs(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !isCaseInsensitive {
|
||||
t.Skip("skip test on case-sensitive filesystem")
|
||||
}
|
||||
|
||||
fs := afero.NewBasePathFs(afero.NewOsFs(), dir)
|
||||
newCache := func() *filecache.Cache {
|
||||
return filecache.NewCache(fs, filecache.FileCacheConfig{Dir: "cache", MaxAge: -1})
|
||||
}
|
||||
|
||||
c.Assert(newCache().SetBytes("MyBundle/i1", []byte("abc")), qt.IsNil)
|
||||
|
||||
cache := newCache()
|
||||
_, b, err := cache.GetOrCreateBytes("mybundle/i1", func() ([]byte, error) {
|
||||
return []byte("def"), nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Equals, "abc")
|
||||
|
||||
count, err := cache.Prune(false)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 0)
|
||||
c.Assert(cache.GetString("MyBundle/i1"), qt.Equals, "abc")
|
||||
}
|
||||
|
||||
// On a case-sensitive filesystem the entries above are distinct files,
|
||||
// and the one not used in this build should be pruned.
|
||||
func TestPruneCacheEntryWithOtherCaseCaseSensitiveFs(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
fs := afero.NewMemMapFs()
|
||||
cache := filecache.NewCache(fs, filecache.FileCacheConfig{Dir: "cache", MaxAge: -1})
|
||||
|
||||
c.Assert(cache.SetBytes("MyBundle/i1", []byte("abc")), qt.IsNil)
|
||||
|
||||
cache = filecache.NewCache(fs, filecache.FileCacheConfig{Dir: "cache", MaxAge: -1})
|
||||
_, b, err := cache.GetOrCreateBytes("mybundle/i1", func() ([]byte, error) {
|
||||
return []byte("def"), nil
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(string(b), qt.Equals, "def")
|
||||
|
||||
count, err := cache.Prune(false)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 1)
|
||||
c.Assert(cache.GetString("MyBundle/i1"), qt.Equals, "")
|
||||
c.Assert(cache.GetString("mybundle/i1"), qt.Equals, "def")
|
||||
}
|
||||
|
||||
func TestPrune(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
configStr := `
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
@@ -39,12 +102,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,59 +113,64 @@ maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
|
||||
caches, err := filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache := caches[name]
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
for _, name := range []string{filecache.CacheKeyAssets, filecache.CacheKeyImages} {
|
||||
msg := qt.Commentf("cache: %s", name)
|
||||
fs := afero.NewMemMapFs()
|
||||
p := newPathsSpec(t, fs, configStr)
|
||||
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
|
||||
caches, err := filecache.NewCaches(fileCachConfig, fs)
|
||||
c.Assert(err, qt.IsNil)
|
||||
caches.SetResourceFs(fs)
|
||||
cache := caches[name]
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
if i == 4 {
|
||||
// This will expire the first 5
|
||||
time.Sleep(201 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := caches.Prune()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 5, msg)
|
||||
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i < 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
c.Assert(v, qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
if i == 4 {
|
||||
// This will expire the first 5
|
||||
time.Sleep(201 * time.Millisecond)
|
||||
|
||||
count, err = caches.Prune()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 4)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i != 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
c.Assert(v, qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
count, err := caches.Prune()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 5, msg)
|
||||
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i < 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
c.Assert(v, qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = filecache.NewCaches(p)
|
||||
c.Assert(err, qt.IsNil)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
|
||||
count, err = caches.Prune()
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(count, qt.Equals, 4)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := range 10 {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.GetString(id)
|
||||
if i != 5 {
|
||||
c.Assert(v, qt.Equals, "")
|
||||
} else {
|
||||
c.Assert(v, qt.Equals, "abc")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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,89 @@
|
||||
#!/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() {
|
||||
local staticcheck_bin
|
||||
if command -v staticcheck &> /dev/null; then
|
||||
staticcheck_bin=$(command -v staticcheck)
|
||||
else
|
||||
echo "==> Installing staticcheck..."
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
staticcheck_bin="$(go env GOBIN)"
|
||||
if [ -z "$staticcheck_bin" ]; then
|
||||
staticcheck_bin="$(go env GOPATH)/bin"
|
||||
fi
|
||||
staticcheck_bin="$staticcheck_bin/staticcheck"
|
||||
fi
|
||||
echo "==> Running staticcheck..."
|
||||
"$staticcheck_bin" $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"
|
||||
+2
-4
@@ -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()
|
||||
@@ -103,9 +103,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
|
||||
}
|
||||
|
||||
for _, t := range include {
|
||||
for i := range t.NumMethod() {
|
||||
|
||||
m := t.Method(i)
|
||||
for m := range t.Methods() {
|
||||
if excludes[m.Name] || seen[m.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -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
-12
@@ -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)
|
||||
@@ -485,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
|
||||
@@ -517,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.
|
||||
@@ -581,14 +613,14 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft")
|
||||
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
|
||||
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignore the configured file caches")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
|
||||
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
|
||||
_ = cmd.MarkFlagDirname("layoutDir")
|
||||
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.")
|
||||
@@ -619,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
|
||||
|
||||
@@ -652,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
|
||||
}
|
||||
@@ -669,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:]...)...)
|
||||
}
|
||||
|
||||
+5
-5
@@ -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,11 +112,11 @@ 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.")
|
||||
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for (default is the default content language)")
|
||||
cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
|
||||
+89
-3
@@ -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")
|
||||
@@ -211,16 +263,50 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
|
||||
site := c.h.Sites[0]
|
||||
|
||||
var pagesBackedByFile page.Pages
|
||||
for _, p := range site.AllPages() {
|
||||
workingDir := c.h.Sites[0].Deps.Conf.WorkingDir() + string(filepath.Separator)
|
||||
|
||||
isConvertible := func(p page.Page) bool {
|
||||
// Skip pages not backed by a content file.
|
||||
if p.File() == nil {
|
||||
return false
|
||||
}
|
||||
// Skip content adapters.
|
||||
if p.File().IsContentAdapter() {
|
||||
return false
|
||||
}
|
||||
// Skip content files provided by modules, including vendored modules.
|
||||
if !p.File().FileInfo().Meta().IsProject {
|
||||
return false
|
||||
}
|
||||
// Skip content files in project mounts outside the working directory.
|
||||
if !strings.HasPrefix(p.File().Filename(), workingDir) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
var pagesBackedByFile page.Pages
|
||||
for _, p := range c.h.Pages() {
|
||||
if !isConvertible(p) {
|
||||
continue
|
||||
}
|
||||
filename := p.File().Filename()
|
||||
if seen[filename] {
|
||||
continue
|
||||
}
|
||||
seen[filename] = true
|
||||
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() {
|
||||
for _, p := range pagesBackedByFile {
|
||||
if err := c.convertAndSavePage(p, site, format); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+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.
|
||||
|
||||
+30
-31
@@ -21,12 +21,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
@@ -34,6 +30,7 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/markup/highlight"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
@@ -47,6 +44,10 @@ func newGenCommand() *genCommand {
|
||||
|
||||
// Chroma flags.
|
||||
style string
|
||||
mode string
|
||||
modeSelector bool
|
||||
classDark string
|
||||
classLight string
|
||||
highlightStyle string
|
||||
lineNumbersInlineStyle string
|
||||
lineNumbersTableStyle string
|
||||
@@ -60,46 +61,44 @@ 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)
|
||||
if !slices.Contains(styles.Names(), style) {
|
||||
return fmt.Errorf("invalid style: %s", style)
|
||||
}
|
||||
builder := styles.Get(style).Builder()
|
||||
if highlightStyle != "" {
|
||||
builder.Add(chroma.LineHighlight, highlightStyle)
|
||||
}
|
||||
if lineNumbersInlineStyle != "" {
|
||||
builder.Add(chroma.LineNumbers, lineNumbersInlineStyle)
|
||||
}
|
||||
if lineNumbersTableStyle != "" {
|
||||
builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle)
|
||||
}
|
||||
style, err := builder.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if omitEmpty {
|
||||
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
|
||||
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
|
||||
}
|
||||
options := []html.Option{
|
||||
html.WithCSSComments(!omitClassComments),
|
||||
css, err := highlight.ChromaStylesCSS(highlight.ChromaStylesOptions{
|
||||
Style: style,
|
||||
Mode: mode,
|
||||
ModeSelector: modeSelector,
|
||||
ClassDark: classDark,
|
||||
ClassLight: classLight,
|
||||
HighlightStyle: highlightStyle,
|
||||
LineNumbersInlineStyle: lineNumbersInlineStyle,
|
||||
LineNumbersTableStyle: lineNumbersTableStyle,
|
||||
OmitClassComments: omitClassComments,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatter := html.New(options...)
|
||||
|
||||
w := os.Stdout
|
||||
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
formatter.WriteCSS(w, style)
|
||||
fmt.Printf("/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
|
||||
fmt.Print(css)
|
||||
return nil
|
||||
},
|
||||
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(&mode, "mode", "", `style mode ("light", "dark")`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("mode", cobra.FixedCompletions([]string{"light", "dark"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
cmd.PersistentFlags().BoolVar(&modeSelector, "modeSelector", false, `scope selectors under a top level mode class, e.g. ".dark .chroma"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("modeSelector", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&classDark, "classDark", "dark", `class name used by --modeSelector for dark styles`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("classDark", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&classLight, "classLight", "light", `class name used by --modeSelector for light styles`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("classLight", 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)
|
||||
cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
|
||||
|
||||
+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,
|
||||
|
||||
+35
-24
@@ -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() {
|
||||
@@ -344,7 +367,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
|
||||
case changes := <-c.r.changesFromBuild:
|
||||
unlock, err := h.LockBuild()
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("Failed to acquire a build lock: %s", err)
|
||||
c.r.logger.Errorf("Failed to acquire a build lock: %s", err)
|
||||
return
|
||||
}
|
||||
c.changeDetector.PrepareNew()
|
||||
@@ -364,7 +387,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
|
||||
case evs := <-watcher.Events:
|
||||
unlock, err := h.LockBuild()
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("Failed to acquire a build lock: %s", err)
|
||||
c.r.logger.Errorf("Failed to acquire a build lock: %s", err)
|
||||
return
|
||||
}
|
||||
c.handleEvents(watcher, staticSyncer, evs, configSet)
|
||||
@@ -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
|
||||
|
||||
+43
-36
@@ -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,44 +463,51 @@ 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")
|
||||
c.r.Println("echo \"theme = 'ananke'\" > hugo.toml")
|
||||
c.r.Println("git submodule add https://github.com/gohugo-ananke/ananke themes/ananke")
|
||||
c.r.Println("echo \"theme: ananke\" >> hugo.yaml")
|
||||
c.r.Println("hugo server")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *importCommand) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
|
||||
path := filepath.Join(jekyllRoot, "_config.yml")
|
||||
for _, candidate := range []struct {
|
||||
filename string
|
||||
format metadecoders.Format
|
||||
}{
|
||||
{"_config.yml", metadecoders.YAML},
|
||||
{"_config.yaml", metadecoders.YAML},
|
||||
{"_config.toml", metadecoders.TOML},
|
||||
} {
|
||||
path := filepath.Join(jekyllRoot, candidate.filename)
|
||||
exists, err := helpers.Exists(path, fs)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
exists, err := helpers.Exists(path, fs)
|
||||
f, err := fs.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
b, err := io.ReadAll(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil || !exists {
|
||||
c.r.Println("_config.yaml not found: Is the specified Jekyll root correct?")
|
||||
return nil
|
||||
m, err := metadecoders.Default.UnmarshalToMap(b, candidate.format)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
f, err := fs.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m
|
||||
c.r.Println("no config file (_config.yml, _config.yaml, or _config.toml) found: is the specified Jekyll root correct?")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *importCommand) parseJekyllFilename(filename string) (time.Time, string, error) {
|
||||
|
||||
+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
|
||||
|
||||
|
||||
+15
-15
@@ -246,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"
|
||||
@@ -282,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
|
||||
}
|
||||
@@ -523,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
|
||||
@@ -531,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"}
|
||||
|
||||
@@ -549,11 +549,11 @@ of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Flags().BoolVar(&c.tlsAuto, "tlsAuto", false, "generate and use locally-trusted certificates.")
|
||||
cmd.Flags().BoolVar(&c.pprof, "pprof", false, "enable the pprof server (port 8080)")
|
||||
cmd.Flags().BoolVarP(&c.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
|
||||
cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
|
||||
cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "disable browser caching of pages served by the embedded web server")
|
||||
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")
|
||||
@@ -1108,7 +1108,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
|
||||
fromPath := ev.Name
|
||||
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath, true)
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath, false)
|
||||
|
||||
if !found {
|
||||
// Not member of this virtual host.
|
||||
@@ -1160,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 {
|
||||
|
||||
@@ -81,9 +81,9 @@ func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var items []T
|
||||
for i := len(s.items) - 1; i >= 0; i-- {
|
||||
if predicate(s.items[i]) {
|
||||
items = append(items, s.items[i])
|
||||
for i, v := range slices.Backward(s.items) {
|
||||
if predicate(v) {
|
||||
items = append(items, v)
|
||||
s.items = slices.Delete(s.items, i, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+46
-24
@@ -18,6 +18,7 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
@@ -87,11 +88,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)
|
||||
}
|
||||
@@ -103,6 +106,16 @@ func MD5FromStringHexEncoded(f string) string {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// MD5FromReaderHexEncoded returns the MD5 hash of the given reader.
|
||||
func MD5FromReaderHexEncoded(r io.Reader) string {
|
||||
h := md5.New()
|
||||
_, err := io.Copy(h, r)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// HashString returns a hash from the given elements.
|
||||
// It will panic if the hash cannot be calculated.
|
||||
// Note that this hash should be used primarily for identity, not for change detection as
|
||||
@@ -122,11 +135,38 @@ func HashStringHex(vs ...any) string {
|
||||
var hashOptsPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &hashstructure.HashOptions{
|
||||
Hasher: xxhash.New(),
|
||||
Hasher: xxhash.New(),
|
||||
UnwrapFunc: unwrapForHashing,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// hashstructure only sees exported struct fields, so rewrite known identity types before hashing,
|
||||
// e.g. a Resource or Page nested in an options map hashes by its Key.
|
||||
func unwrapForHashing(v reflect.Value) (reflect.Value, error) {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return v, nil
|
||||
}
|
||||
var in any
|
||||
if v.CanAddr() {
|
||||
// The common case; pointer receiver methods on a struct
|
||||
// reached through a pointer.
|
||||
in = v.Addr().Interface()
|
||||
} else {
|
||||
in = v.Interface()
|
||||
}
|
||||
switch t := in.(type) {
|
||||
case hashstructure.Hashable:
|
||||
// Let hashstructure handle it.
|
||||
return v, nil
|
||||
case keyer:
|
||||
return reflect.ValueOf(t.Key()), nil
|
||||
case identity.IdentityProvider:
|
||||
return reflect.ValueOf(t.GetIdentity()), nil
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func getHashOpts() *hashstructure.HashOptions {
|
||||
return hashOptsPool.Get().(*hashstructure.HashOptions)
|
||||
}
|
||||
@@ -143,15 +183,10 @@ func putHashOpts(opts *hashstructure.HashOptions) {
|
||||
func HashUint64(vs ...any) uint64 {
|
||||
var o any
|
||||
if len(vs) == 1 {
|
||||
o = toHashable(vs[0])
|
||||
o = vs[0]
|
||||
} else {
|
||||
elements := make([]any, len(vs))
|
||||
for i, e := range vs {
|
||||
elements[i] = toHashable(e)
|
||||
}
|
||||
o = elements
|
||||
o = vs
|
||||
}
|
||||
|
||||
hash, err := Hash(o)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -174,19 +209,6 @@ type keyer interface {
|
||||
Key() string
|
||||
}
|
||||
|
||||
// For structs, hashstructure.Hash only works on the exported fields,
|
||||
// so rewrite the input slice for known identity types.
|
||||
func toHashable(v any) any {
|
||||
switch t := v.(type) {
|
||||
case keyer:
|
||||
return t.Key()
|
||||
case identity.IdentityProvider:
|
||||
return t.GetIdentity()
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
type xxhashReadFrom struct {
|
||||
buff []byte
|
||||
*xxhash.Digest
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -178,3 +178,14 @@ func improveIfNilPointerMsg(inErr error) string {
|
||||
s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
|
||||
return nilPointerErrRe.ReplaceAllString(inErr.Error(), s)
|
||||
}
|
||||
|
||||
// Or returns the first non-nil error from the given list of errors.
|
||||
// If all errors are nil, it returns nil.
|
||||
func Or(errs ...error) error {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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 (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed esmloader.mjs
|
||||
var esmLoaderSource string
|
||||
|
||||
// nodeESMLoaderImportArg returns a "--import=data:..." argument that installs
|
||||
// a Node.js ESM resolver hook making NODE_PATH a fallback for failed bare
|
||||
// imports. See esmloader.mjs for the rationale.
|
||||
var nodeESMLoaderImportArg = sync.OnceValue(func() string {
|
||||
return "--import=data:text/javascript;base64," + base64.StdEncoding.EncodeToString([]byte(esmLoaderSource))
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
// Node.js ESM resolver hook installed by Hugo.
|
||||
//
|
||||
// Node's ESM resolver does not consult NODE_PATH, unlike CJS require().
|
||||
// That breaks postcss.config.js / babel.config.js / etc. files written in
|
||||
// ESM and loaded from outside the project tree (typically the Hugo module
|
||||
// cache): bare imports like `import x from "postcss-import"` cannot be
|
||||
// resolved by walking up from the file's location.
|
||||
//
|
||||
// This hook makes the ESM resolver fall back to NODE_PATH for bare
|
||||
// specifiers when Node's normal resolution fails. It is a no-op for
|
||||
// relative/absolute paths and URL-scheme specifiers, and it never fires
|
||||
// unless Node would itself have thrown ERR_MODULE_NOT_FOUND or
|
||||
// ERR_ACCESS_DENIED.
|
||||
//
|
||||
// ERR_ACCESS_DENIED is handled because Node's resolver walks up the
|
||||
// directory tree looking for node_modules. Under the permission model that
|
||||
// walk can hit a node_modules outside the allow-list (e.g. Netlify stores
|
||||
// its node_modules cache in the same tree as the Hugo file cache), aborting
|
||||
// resolution even though the package is reachable via NODE_PATH. If the
|
||||
// NODE_PATH fallback also fails we re-throw the original error so the
|
||||
// access-denied resource is still reported.
|
||||
//
|
||||
// Uses the synchronous registerHooks API so it runs on the main thread and
|
||||
// does not require --allow-worker under the Node permission model.
|
||||
|
||||
import { registerHooks, createRequire } from 'node:module';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const resolvers = [];
|
||||
const np = process.env.NODE_PATH;
|
||||
if (np) {
|
||||
const sep = process.platform === 'win32' ? ';' : ':';
|
||||
for (const p of np.split(sep)) {
|
||||
if (p) resolvers.push(createRequire(p + '/_'));
|
||||
}
|
||||
}
|
||||
|
||||
function isBareSpecifier(s) {
|
||||
if (!s) return false;
|
||||
if (s.startsWith('.') || s.startsWith('/') || s.startsWith('#')) return false;
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(s)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
try {
|
||||
return nextResolve(specifier, context);
|
||||
} catch (err) {
|
||||
if (err?.code !== 'ERR_MODULE_NOT_FOUND' && err?.code !== 'ERR_ACCESS_DENIED') throw err;
|
||||
if (!isBareSpecifier(specifier)) throw err;
|
||||
for (const r of resolvers) {
|
||||
try {
|
||||
const resolved = r.resolve(specifier);
|
||||
return { url: pathToFileURL(resolved).href, shortCircuit: true, format: null };
|
||||
} catch (_) { /* try next */ }
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
});
|
||||
+248
-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,199 @@ 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)
|
||||
}
|
||||
// Install an ESM resolver hook that makes NODE_PATH a fallback for failed
|
||||
// bare imports, so postcss.config.js / babel.config.js / etc. written in
|
||||
// ESM work when loaded from the Hugo module cache. See esmloader.mjs.
|
||||
allArgs = append(allArgs, nodeESMLoaderImportArg())
|
||||
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 +457,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: {{ .RelPermalink }}|{{ gt (.Content | len) 10 }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with resources.Get "css/main2.css" }}
|
||||
{{ with . | css.TailwindCSS }}
|
||||
CSS2: {{ .RelPermalink }}|{{ gt (.Content | len) 10 }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with resources.Get "js/main.js" }}
|
||||
{{ with . | js.Babel }}
|
||||
JS: {{ .RelPermalink }}|{{ gt (.Content | len) 10 }}|
|
||||
{{ 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: /css/main1.css|true|",
|
||||
"CSS2: /css/main2.css|true|",
|
||||
"JS: /js/main.js|true|",
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,23 @@ type ImageConfigProvider interface {
|
||||
GetImageConfig() image.Config
|
||||
}
|
||||
|
||||
// ColorPropertiesProvider provides access to CICP color properties (for HDR images).
|
||||
// Images implementing this interface preserve color space information through processing.
|
||||
type ColorPropertiesProvider interface {
|
||||
GetColorPrimaries() int
|
||||
GetTransferCharacteristics() int
|
||||
GetMatrixCoefficients() int
|
||||
}
|
||||
|
||||
// HasColorProperties returns true if the image has non-zero color properties that should be preserved.
|
||||
func HasColorProperties(img image.Image) bool {
|
||||
if cpp, ok := img.(ColorPropertiesProvider); ok {
|
||||
// Consider it as having properties if any value is non-zero.
|
||||
return cpp.GetColorPrimaries() > 0 || cpp.GetTransferCharacteristics() > 0 || cpp.GetMatrixCoefficients() > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FrameDurationsToGifDelays converts frame durations in milliseconds to
|
||||
// GIF delays in 100ths of a second.
|
||||
func FrameDurationsToGifDelays(frameDurations []int) []int {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package hiter
|
||||
|
||||
// Common iterator functions.
|
||||
// Some of these are are based on this discsussion: https://github.com/golang/go/issues/61898
|
||||
// Some of these are based on this discsussion: https://github.com/golang/go/issues/61898
|
||||
|
||||
import "iter"
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -120,6 +120,14 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
if vvv, ok := vv.(Params); ok {
|
||||
if pv, ok := v.(Params); ok {
|
||||
vvv.merge(ms, pv)
|
||||
} else if vvv.IsZero() {
|
||||
// The existing value is an empty Params (just merge metadata)
|
||||
// and the incoming value is a non-Params type (e.g. a slice).
|
||||
// If the user has set an explicit non-none merge strategy,
|
||||
// honor it by using the incoming value.
|
||||
if s, found := vvv.GetMergeStrategy(); found && s != ParamsMergeStrategyNone {
|
||||
p[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !noUpdate {
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
package hstore
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
@@ -69,9 +68,8 @@ func TestScratchAddSlice(t *testing.T) {
|
||||
sl := scratch.Get("intSlice")
|
||||
expected := []int{1, 2, 3}
|
||||
|
||||
if !reflect.DeepEqual(expected, sl) {
|
||||
t.Errorf("Slice difference, go %q expected %q", sl, expected)
|
||||
}
|
||||
c.Assert(sl, qt.DeepEquals, expected)
|
||||
|
||||
_, err = scratch.Add("intSlice", []int{4, 5})
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
@@ -79,9 +77,7 @@ func TestScratchAddSlice(t *testing.T) {
|
||||
sl = scratch.Get("intSlice")
|
||||
expected = []int{1, 2, 3, 4, 5}
|
||||
|
||||
if !reflect.DeepEqual(expected, sl) {
|
||||
t.Errorf("Slice difference, go %q expected %q", sl, expected)
|
||||
}
|
||||
c.Assert(sl, qt.DeepEquals, expected)
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5275
|
||||
|
||||
+16
-36
@@ -19,8 +19,8 @@ import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/helpers/maphelpers"
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
)
|
||||
|
||||
@@ -55,46 +55,17 @@ func EqualAny(a string, b ...string) bool {
|
||||
return slices.Contains(b, a)
|
||||
}
|
||||
|
||||
// regexpCache represents a cache of regexp objects protected by a mutex.
|
||||
type regexpCache struct {
|
||||
mu sync.RWMutex
|
||||
re map[string]*regexp.Regexp
|
||||
}
|
||||
|
||||
func (rc *regexpCache) getOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
var ok bool
|
||||
|
||||
if re, ok = rc.get(pattern); !ok {
|
||||
re, err = regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.set(pattern, re)
|
||||
}
|
||||
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) {
|
||||
rc.mu.RLock()
|
||||
re, ok = rc.re[key]
|
||||
rc.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (rc *regexpCache) set(key string, re *regexp.Regexp) {
|
||||
rc.mu.Lock()
|
||||
rc.re[key] = re
|
||||
rc.mu.Unlock()
|
||||
}
|
||||
|
||||
var reCache = regexpCache{re: make(map[string]*regexp.Regexp)}
|
||||
var reCache = *maphelpers.NewConcurrentMap[string, *regexp.Regexp]()
|
||||
|
||||
// GetOrCompileRegexp retrieves a regexp object from the cache based upon the pattern.
|
||||
// If the pattern is not found in the cache, the pattern is compiled and added to
|
||||
// the cache.
|
||||
func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
return reCache.getOrCompileRegexp(pattern)
|
||||
return reCache.GetOrCreate(pattern,
|
||||
func() (*regexp.Regexp, error) {
|
||||
return regexp.Compile(pattern)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// HasAnyPrefix checks if the string s has any of the prefixes given.
|
||||
@@ -107,6 +78,15 @@ func HasAnyPrefix(s string, prefixes ...string) bool {
|
||||
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"}
|
||||
|
||||
+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")
|
||||
|
||||
@@ -82,6 +82,9 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if shouldCopy != nil && !shouldCopy(fromFilename) {
|
||||
continue
|
||||
}
|
||||
if err := CopyFile(fs, fromFilename, toFilename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ import (
|
||||
type HasBytesWriter struct {
|
||||
Patterns []*HasBytesPattern
|
||||
|
||||
i int
|
||||
done bool
|
||||
// The tail of the bytes written so far, retained so we can detect a
|
||||
// pattern that straddles the boundary between two Write calls.
|
||||
buff []byte
|
||||
}
|
||||
|
||||
@@ -31,10 +32,13 @@ type HasBytesPattern struct {
|
||||
Pattern []byte
|
||||
}
|
||||
|
||||
func (h *HasBytesWriter) patternLen() int {
|
||||
// maxPatternLen returns the length of the longest pattern.
|
||||
func (h *HasBytesWriter) maxPatternLen() int {
|
||||
l := 0
|
||||
for _, p := range h.Patterns {
|
||||
l += len(p.Pattern)
|
||||
if len(p.Pattern) > l {
|
||||
l = len(p.Pattern)
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
@@ -44,36 +48,55 @@ func (h *HasBytesWriter) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
if len(h.buff) == 0 {
|
||||
h.buff = make([]byte, h.patternLen()*2)
|
||||
keep := h.maxPatternLen() - 1
|
||||
|
||||
// Join the tail retained from previous Writes with the head of this chunk
|
||||
// so a pattern straddling the boundary is still detected. Only the
|
||||
// boundary window is copied; the chunk itself is scanned in place below.
|
||||
var boundary []byte
|
||||
if keep > 0 && len(h.buff) > 0 {
|
||||
head := p
|
||||
if len(head) > keep {
|
||||
head = head[:keep]
|
||||
}
|
||||
boundary = make([]byte, 0, len(h.buff)+len(head))
|
||||
boundary = append(boundary, h.buff...)
|
||||
boundary = append(boundary, head...)
|
||||
}
|
||||
|
||||
for i := range p {
|
||||
h.buff[h.i] = p[i]
|
||||
h.i++
|
||||
if h.i == len(h.buff) {
|
||||
// Shift left.
|
||||
copy(h.buff, h.buff[len(h.buff)/2:])
|
||||
h.i = len(h.buff) / 2
|
||||
// Scan each not-yet-matched pattern once per Write instead of once per byte.
|
||||
done := true
|
||||
for _, pp := range h.Patterns {
|
||||
if pp.Match {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, pp := range h.Patterns {
|
||||
if bytes.Contains(h.buff, pp.Pattern) {
|
||||
pp.Match = true
|
||||
done := true
|
||||
for _, ppp := range h.Patterns {
|
||||
if !ppp.Match {
|
||||
done = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if done {
|
||||
h.done = true
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
if bytes.Contains(p, pp.Pattern) || bytes.Contains(boundary, pp.Pattern) {
|
||||
pp.Match = true
|
||||
continue
|
||||
}
|
||||
done = false
|
||||
}
|
||||
|
||||
if done {
|
||||
// All patterns found; no need to look at any more data.
|
||||
h.done = true
|
||||
h.buff = nil
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Retain the last keep bytes of (previous tail + this chunk) to detect a
|
||||
// pattern straddling into the next Write.
|
||||
switch {
|
||||
case keep <= 0:
|
||||
h.buff = h.buff[:0]
|
||||
case len(p) >= keep:
|
||||
h.buff = append(h.buff[:0], p[len(p)-keep:]...)
|
||||
default:
|
||||
// Chunk shorter than keep: slide the window over the retained tail.
|
||||
if total := len(h.buff) + len(p); total > keep {
|
||||
h.buff = h.buff[total-keep:]
|
||||
}
|
||||
h.buff = append(h.buff, p...)
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
|
||||
@@ -65,3 +65,61 @@ func TestHasBytesWriter(t *testing.T) {
|
||||
fmt.Fprintf(w, "__foo")
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestHasBytesWriterMultiplePatterns(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
neww := func() (*HasBytesWriter, io.Writer) {
|
||||
var b bytes.Buffer
|
||||
h := &HasBytesWriter{
|
||||
Patterns: []*HasBytesPattern{
|
||||
{Pattern: []byte("__hdeferred/")},
|
||||
{Pattern: []byte("__h_pp_l1")},
|
||||
},
|
||||
}
|
||||
return h, io.MultiWriter(&b, h)
|
||||
}
|
||||
|
||||
// Neither pattern present.
|
||||
h, w := neww()
|
||||
fmt.Fprint(w, "the quick brown fox jumps over the lazy dog")
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, false)
|
||||
c.Assert(h.Patterns[1].Match, qt.Equals, false)
|
||||
c.Assert(h.done, qt.Equals, false)
|
||||
|
||||
// Only the second pattern present; the writer must not report a match
|
||||
// for the first, and must not prematurely mark itself done.
|
||||
h, w = neww()
|
||||
fmt.Fprint(w, "prefix __h_pp_l1 suffix")
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, false)
|
||||
c.Assert(h.Patterns[1].Match, qt.Equals, true)
|
||||
c.Assert(h.done, qt.Equals, false)
|
||||
|
||||
// Both patterns present across multiple writes; done once all match.
|
||||
h, w = neww()
|
||||
fmt.Fprint(w, "aaa __hdef")
|
||||
fmt.Fprint(w, "erred/xyz bbb __h_p")
|
||||
fmt.Fprint(w, "p_l1 ccc")
|
||||
c.Assert(h.Patterns[0].Match, qt.Equals, true)
|
||||
c.Assert(h.Patterns[1].Match, qt.Equals, true)
|
||||
c.Assert(h.done, qt.Equals, true)
|
||||
}
|
||||
|
||||
func BenchmarkHasBytesWriter(b *testing.B) {
|
||||
// A large chunk of output containing neither pattern is the common case
|
||||
// (a normal rendered page): the writer must scan all of it.
|
||||
content := []byte(strings.Repeat("<div class=\"nav\"><a href=\"/foo/bar\">baz</a></div>\n", 4000))
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
h := &HasBytesWriter{
|
||||
Patterns: []*HasBytesPattern{
|
||||
{Pattern: []byte("__hdeferred/")},
|
||||
{Pattern: []byte("__h_pp_l1")},
|
||||
},
|
||||
}
|
||||
if _, err := h.Write(content); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-9
@@ -19,16 +19,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -70,7 +64,7 @@ type ReadSeekCloserProvider interface {
|
||||
|
||||
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
type readSeekerNopCloser struct {
|
||||
ReadSeeker
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
// Close does nothing.
|
||||
@@ -79,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}
|
||||
}
|
||||
|
||||
@@ -111,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 {
|
||||
|
||||
+34
-123
@@ -16,7 +16,6 @@ package hugo
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
@@ -25,17 +24,17 @@ 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/gohugoio/hugo/internal/warpc"
|
||||
|
||||
"github.com/bep/helpers/contexthelpers"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
iofs "io/fs"
|
||||
@@ -56,89 +55,29 @@ 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()
|
||||
// GetBuildDate returns the build date if set by -ldflags="-X github.com/gohugoio/hugo/common/hugo.buildDate="
|
||||
func GetBuildDate() string {
|
||||
return buildDate
|
||||
}
|
||||
|
||||
// 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 +88,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 +105,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,7 +212,10 @@ func GetDependencyList() []string {
|
||||
|
||||
// GetDependencyListNonGo returns a list of non-Go dependencies.
|
||||
func GetDependencyListNonGo() []string {
|
||||
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
|
||||
var deps []string
|
||||
for _, dep := range warpc.GetWASMDeps() {
|
||||
deps = append(deps, formatDep(dep[0], dep[1]))
|
||||
}
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
@@ -324,7 +229,8 @@ func GetDependencyListNonGo() []string {
|
||||
if IsDartSassGeV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
deps = append(deps,
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep(dartSassPath+"/protocol", dartSass.ProtocolVersion),
|
||||
formatDep(dartSassPath+"/compiler", dartSass.CompilerVersion),
|
||||
formatDep(dartSassPath+"/implementation", dartSass.ImplementationVersion),
|
||||
@@ -438,8 +344,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)
|
||||
|
||||
@@ -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.145.0.
|
||||
c.Assert(deprecationLogLevelFromVersion("0.150.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: 155,
|
||||
Minor: 166,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -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,129 +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()
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the map.
|
||||
// It returns true if the key was found and deleted, false otherwise.
|
||||
func (m *Map[K, T]) Delete(key K) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; found {
|
||||
delete(m.m, key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithWriteLock executes the given function with a write lock on the map.
|
||||
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return f(m.m)
|
||||
}
|
||||
|
||||
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
|
||||
// It returns true if the value was set, false otherwise.
|
||||
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
|
||||
m.mu.RLock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.mu.RUnlock()
|
||||
return m.doSetIfAbsent(key, value)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, found := m.m[key]; !found {
|
||||
m.m[key] = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// All returns an iterator over all key/value pairs in the map.
|
||||
// A read lock is held during the iteration.
|
||||
func (m *Map[K, T]) All() iter.Seq2[K, T] {
|
||||
return func(yield func(K, T) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for k, v := range m.m {
|
||||
if !yield(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +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) error {
|
||||
m["f"] = 500
|
||||
return nil
|
||||
})
|
||||
v, found = m.Lookup("f")
|
||||
c.Assert(found, qt.Equals, true)
|
||||
c.Assert(v, qt.Equals, 500)
|
||||
}
|
||||
+28
-26
@@ -20,6 +20,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
@@ -28,18 +29,16 @@ import (
|
||||
)
|
||||
|
||||
func TestPara(t *testing.T) {
|
||||
if runtime.NumCPU() < 4 {
|
||||
t.Skipf("skip para test, CPU count is %d", runtime.NumCPU())
|
||||
}
|
||||
|
||||
// TODO(bep)
|
||||
if htesting.IsCI() {
|
||||
t.Skip("skip para test when running on CI")
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("Order", func(c *qt.C) {
|
||||
if runtime.NumCPU() < 4 {
|
||||
c.Skipf("skip Order subtest, CPU count is %d", runtime.NumCPU())
|
||||
}
|
||||
if htesting.IsCI() {
|
||||
c.Skip("skip Order subtest when running on CI")
|
||||
}
|
||||
|
||||
n := 500
|
||||
ints := make([]int, n)
|
||||
for i := range n {
|
||||
@@ -68,28 +67,31 @@ func TestPara(t *testing.T) {
|
||||
})
|
||||
|
||||
c.Run("Time", func(c *qt.C) {
|
||||
const n = 100
|
||||
synctest.Test(c.TB.(*testing.T), func(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
const n = 100
|
||||
|
||||
p := New(5)
|
||||
r, _ := p.Start(context.Background())
|
||||
p := New(5)
|
||||
r, _ := p.Start(context.Background())
|
||||
|
||||
start := time.Now()
|
||||
start := time.Now()
|
||||
|
||||
var counter int64
|
||||
var counter int64
|
||||
|
||||
for range n {
|
||||
r.Run(func() error {
|
||||
atomic.AddInt64(&counter, 1)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
for range n {
|
||||
r.Run(func() error {
|
||||
atomic.AddInt64(&counter, 1)
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
c.Assert(r.Wait(), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, int64(n))
|
||||
c.Assert(r.Wait(), qt.IsNil)
|
||||
c.Assert(counter, qt.Equals, int64(n))
|
||||
|
||||
since := time.Since(start)
|
||||
limit := n / 2 * time.Millisecond
|
||||
c.Assert(since < limit, qt.Equals, true, qt.Commentf("%s >= %s", since, limit))
|
||||
since := time.Since(start)
|
||||
limit := n / 2 * time.Millisecond
|
||||
c.Assert(since < limit, qt.Equals, true, qt.Commentf("%s >= %s", since, limit))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -134,11 +134,6 @@ func (l LowHigh[S]) Value(source S) S {
|
||||
// This is only used for debugging purposes.
|
||||
var InvocationCounter atomic.Int64
|
||||
|
||||
// NewTrue returns a pointer to b.
|
||||
func NewBool(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
// WeightProvider provides a weight.
|
||||
type WeightProvider interface {
|
||||
Weight() int
|
||||
|
||||
@@ -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 (
|
||||
@@ -51,6 +54,10 @@ func (v Version) IsAlphaBetaOrRC() bool {
|
||||
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)
|
||||
}
|
||||
@@ -109,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
|
||||
}
|
||||
@@ -176,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)
|
||||
}
|
||||
@@ -200,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) {
|
||||
|
||||
+116
-87
@@ -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,63 @@ 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
|
||||
}
|
||||
if c.PaginatePath != "" {
|
||||
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
|
||||
c.Pagination.Path = c.PaginatePath
|
||||
// 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 = ""
|
||||
}
|
||||
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 = ""
|
||||
}
|
||||
|
||||
// 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())
|
||||
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())
|
||||
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())
|
||||
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())
|
||||
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())
|
||||
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 render hook values.
|
||||
@@ -446,7 +466,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 +475,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 +491,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.useEmbedded 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.useEmbedded 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 +627,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 +701,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 +837,7 @@ type Configs struct {
|
||||
|
||||
Modules modules.Modules
|
||||
ModulesClient *modules.Client
|
||||
FileCaches filecache.Caches
|
||||
|
||||
// All below is set in Init.
|
||||
Languages langs.Languages
|
||||
@@ -846,7 +868,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 +876,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
|
||||
}
|
||||
@@ -983,29 +1005,30 @@ func (c Configs) GetByLang(lang string) config.AllProvider {
|
||||
|
||||
func newDefaultConfig() *Config {
|
||||
return &Config{
|
||||
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
|
||||
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
|
||||
RootConfig: RootConfig{
|
||||
Environment: hugo.EnvironmentProduction,
|
||||
TitleCaseStyle: "AP",
|
||||
PluralizeListTitles: true,
|
||||
CapitalizeListTitles: true,
|
||||
StaticDir: []string{"static"},
|
||||
SummaryLength: 70,
|
||||
Timeout: "60s",
|
||||
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
|
||||
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
|
||||
Environment: hugo.EnvironmentProduction,
|
||||
TitleCaseStyle: "AP",
|
||||
PluralizeListTitles: true,
|
||||
CapitalizeListTitles: true,
|
||||
StaticDir: []string{"static"},
|
||||
SummaryLength: 70,
|
||||
Timeout: "60s",
|
||||
|
||||
CommonDirs: config.CommonDirs{
|
||||
ArcheTypeDir: "archetypes",
|
||||
ContentDir: "content",
|
||||
ResourceDir: "resources",
|
||||
PublishDir: "public",
|
||||
ThemesDir: "themes",
|
||||
AssetDir: "assets",
|
||||
LayoutDir: "layouts",
|
||||
I18nDir: "i18n",
|
||||
DataDir: "data",
|
||||
},
|
||||
},
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
ArcheTypeDir: "archetypes",
|
||||
ContentDir: "content",
|
||||
ResourceDir: "resources",
|
||||
PublishDir: "public",
|
||||
ThemesDir: "themes",
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
AssetDir: "assets",
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
LayoutDir: "layouts",
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
I18nDir: "i18n",
|
||||
//lint:ignore SA1019 Keep as adapter for now.
|
||||
DataDir: "data",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,7 +1040,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
|
||||
@@ -1044,11 +1067,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 {
|
||||
@@ -1060,16 +1083,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)
|
||||
}
|
||||
@@ -1080,14 +1103,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:
|
||||
@@ -1097,7 +1120,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.
|
||||
@@ -1133,7 +1156,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))
|
||||
@@ -1148,10 +1171,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,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
)
|
||||
|
||||
@@ -26,11 +25,13 @@ weight = 2
|
||||
[[module.mounts]]
|
||||
source = 'content/en'
|
||||
target = 'content'
|
||||
lang = 'en'
|
||||
[module.mounts.sites.matrix]
|
||||
languages = 'en'
|
||||
[[module.mounts]]
|
||||
source = 'content/sv'
|
||||
target = 'content'
|
||||
lang = 'sv'
|
||||
[module.mounts.sites.matrix]
|
||||
languages = 'sv'
|
||||
-- content/en/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
@@ -43,9 +44,7 @@ title: "p1"
|
||||
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 +80,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
|
||||
|
||||
@@ -364,25 +361,46 @@ weight = 3
|
||||
|
||||
// Issue 13535
|
||||
// We changed enablement of the embedded link and image render hooks from
|
||||
// booleans to enums in v0.148.0.
|
||||
// booleans to enums in v0.148.0. This should throw error with v0.163.0 and later.
|
||||
func TestLegacyEmbeddedRenderHookEnablement(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[markup.goldmark.renderHooks.image]
|
||||
#KEY_VALUE
|
||||
#KEY_VALUE_IMAGE
|
||||
|
||||
[markup.goldmark.renderHooks.link]
|
||||
#KEY_VALUE
|
||||
#KEY_VALUE_LINK
|
||||
`
|
||||
f := strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = false")
|
||||
b := hugolib.Test(t, f)
|
||||
c := b.H.Configs.Base.Markup.Goldmark.RenderHooks
|
||||
b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
|
||||
b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
|
||||
|
||||
f = strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = true")
|
||||
b = hugolib.Test(t, f)
|
||||
c = b.H.Configs.Base.Markup.Goldmark.RenderHooks
|
||||
b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
|
||||
b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
|
||||
replacer := strings.NewReplacer(
|
||||
"#KEY_VALUE_IMAGE", "enableDefault = false",
|
||||
"#KEY_VALUE_LINK", "",
|
||||
)
|
||||
f := replacer.Replace(files)
|
||||
b, _ := hugolib.TestE(t, f)
|
||||
b.AssertLogContains("ERROR deprecated")
|
||||
|
||||
replacer = strings.NewReplacer(
|
||||
"#KEY_VALUE_IMAGE", "enableDefault = true",
|
||||
"#KEY_VALUE_LINK", "",
|
||||
)
|
||||
f = replacer.Replace(files)
|
||||
b, _ = hugolib.TestE(t, f)
|
||||
b.AssertLogContains("ERROR deprecated")
|
||||
|
||||
replacer = strings.NewReplacer(
|
||||
"#KEY_VALUE_IMAGE", "",
|
||||
"#KEY_VALUE_LINK", "enableDefault = false",
|
||||
)
|
||||
f = replacer.Replace(files)
|
||||
b, _ = hugolib.TestE(t, f)
|
||||
b.AssertLogContains("ERROR deprecated")
|
||||
|
||||
replacer = strings.NewReplacer(
|
||||
"#KEY_VALUE_IMAGE", "",
|
||||
"#KEY_VALUE_LINK", "enableDefault = true",
|
||||
)
|
||||
f = replacer.Replace(files)
|
||||
b, _ = hugolib.TestE(t, f)
|
||||
b.AssertLogContains("ERROR deprecated")
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ 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/hugo"
|
||||
"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"
|
||||
@@ -87,8 +88,18 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"imaging": {
|
||||
key: "imaging",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
m := p.p.GetStringMap(d.key)
|
||||
if _, found := m["quality"]; found {
|
||||
hugo.DeprecateWithLogger("project config key imaging.quality", "Set the quality per format instead with imaging.jpeg.quality, imaging.webp.quality and/or imaging.avif.quality.", "v0.163.0", p.logger.Logger())
|
||||
}
|
||||
if _, found := m["compression"]; found {
|
||||
hugo.DeprecateWithLogger("project config key imaging.compression", "Set the compression type per format instead with imaging.webp.compression and/or imaging.avif.compression.", "v0.163.0", p.logger.Logger())
|
||||
}
|
||||
if _, found := m["hint"]; found {
|
||||
hugo.DeprecateWithLogger("project config key imaging.hint", "Set the hint per format instead with imaging.webp.hint and/or imaging.avif.hint.", "v0.163.0", p.logger.Logger())
|
||||
}
|
||||
var err error
|
||||
p.c.Imaging, err = images.DecodeConfig(p.p.GetStringMap(d.key))
|
||||
p.c.Imaging, err = images.DecodeConfig(m)
|
||||
return err
|
||||
},
|
||||
},
|
||||
@@ -196,7 +207,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 +236,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 +267,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 +279,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 +290,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 +318,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 +337,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 +456,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 +464,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 +478,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
|
||||
@@ -68,6 +67,15 @@ func (c ConfigLanguage) BaseURLLiveReload() urls.BaseURL {
|
||||
return c.config.C.BaseURLLiveReload
|
||||
}
|
||||
|
||||
// AllBaseURLs returns the BaseURL for each enabled language, ordered as Languages().
|
||||
func (c ConfigLanguage) AllBaseURLs() []urls.BaseURL {
|
||||
bs := make([]urls.BaseURL, len(c.m.configLangs))
|
||||
for i, p := range c.m.configLangs {
|
||||
bs[i] = p.BaseURL()
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Environment() string {
|
||||
return c.config.Environment
|
||||
}
|
||||
@@ -123,6 +131,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
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// This is is just some helpers used to create some JSON used in the Hugo docs.
|
||||
// This is just a helper used to create some JSON used in the Hugo docs.
|
||||
func init() {
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
cfg := config.New()
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -19,19 +19,23 @@ defaultContentLanguage = 'en'
|
||||
[[module.mounts]]
|
||||
source = 'content/en'
|
||||
target = 'content/en'
|
||||
lang = 'en'
|
||||
[module.mounts.sites.matrix]
|
||||
languages = 'en'
|
||||
[[module.mounts]]
|
||||
source = 'content/nn'
|
||||
target = 'content/nn'
|
||||
lang = 'nn'
|
||||
[module.mounts.sites.matrix]
|
||||
languages = 'nn'
|
||||
[[module.mounts]]
|
||||
source = 'content/no'
|
||||
target = 'content/no'
|
||||
lang = 'no'
|
||||
[module.mounts.sites.matrix]
|
||||
languages = 'no'
|
||||
[[module.mounts]]
|
||||
source = 'content/sv'
|
||||
target = 'content/sv'
|
||||
lang = 'sv'
|
||||
[module.mounts.sites.matrix]
|
||||
languages = 'sv'
|
||||
[[module.mounts]]
|
||||
source = 'layouts'
|
||||
target = 'layouts'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user