mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-26 16:28:52 +00:00
Compare commits
480 Commits
v0.112.0
...
develop2024
| Author | SHA1 | Date | |
|---|---|---|---|
| 7285e74090 | |||
| 5fd1e74903 | |||
| 9b0050e9aa | |||
| fc7de7136a | |||
| 1083bf7c08 | |||
| b9a03bd59d | |||
| e0021f4961 | |||
| d25902c0d8 | |||
| 50042ee1f4 | |||
| 3758456b31 | |||
| 7125ad401a | |||
| d0d2c6795e | |||
| 2dd608378d | |||
| 45f52be7f8 | |||
| 87bf2b9688 | |||
| f281ef8a4a | |||
| 46f618756f | |||
| 8915343075 | |||
| a541e3b4d4 | |||
| 912c6576bb | |||
| 911bc60a7a | |||
| 5ff6323320 | |||
| 6d5b44305e | |||
| 1ccd3147a0 | |||
| e40b9fbbcf | |||
| 9cd8fbb332 | |||
| abcc61002a | |||
| 648d00c7d8 | |||
| 8adba648cc | |||
| 6f13430d4a | |||
| 00b46fed8e | |||
| eb9f1eb657 | |||
| 5186e762ac | |||
| e321c3502a | |||
| 255e0a9718 | |||
| 558f3258a7 | |||
| 6580cd30ad | |||
| 7617de86cd | |||
| d19ed4d4e6 | |||
| 35dec7c96f | |||
| 9f978d387f | |||
| 14d85ec136 | |||
| 171836cdfa | |||
| 9ea7103db7 | |||
| 3fc42da3d0 | |||
| d24da1712f | |||
| 4583b41305 | |||
| 507f4e356b | |||
| a7e721e02d | |||
| 2627b91d3a | |||
| 6d4b012412 | |||
| e536d461a5 | |||
| bfc325f561 | |||
| 36a60f65d0 | |||
| de2fcc5e1d | |||
| 26a8ec207f | |||
| 9ca889ba49 | |||
| e2a624dd6c | |||
| 4fb40ee873 | |||
| bc93a36133 | |||
| 3e5bc6f3b3 | |||
| 7c47036f18 | |||
| 4d07e1fe8a | |||
| 1c41232e61 | |||
| f11ca0fad4 | |||
| 30a18e8825 | |||
| d7a2f3f980 | |||
| b4c5df42ff | |||
| ef12d169c8 | |||
| a62bbfa9ea | |||
| 8d32ca223d | |||
| 5887230b7d | |||
| a4a66b8216 | |||
| 813390b5a2 | |||
| d528bbd6d0 | |||
| af7f6c8b3e | |||
| e70849ea79 | |||
| dd6cd6268d | |||
| 27620daa20 | |||
| 80d2fdbaa0 | |||
| 805cc1773b | |||
| 0bde6931ac | |||
| ac7cffa7e2 | |||
| f11bca5fec | |||
| 9315a2d2c4 | |||
| cee3a56a91 | |||
| da2a8e6402 | |||
| a4892a07b4 | |||
| cb98e9061b | |||
| 5fa97ee964 | |||
| 4d38f47250 | |||
| 80f793c38d | |||
| a9079d7a63 | |||
| 4914b7f18c | |||
| 9e06fd3394 | |||
| 9c2b2414d2 | |||
| ab21433689 | |||
| 23fcfb7f74 | |||
| 47bf2fcbe2 | |||
| 16fb2cae88 | |||
| 2bedcf3d18 | |||
| 935999e2fa | |||
| 20c3bae2c4 | |||
| 29b6e133a8 | |||
| 7f8ab74681 | |||
| 59bcc098cf | |||
| e26ba752d2 | |||
| bcf07fa639 | |||
| b6a7568131 | |||
| acf01bfb78 | |||
| 27b22cd87e | |||
| a2488b1c95 | |||
| 8f60c0c1ec | |||
| 9dc608084b | |||
| a349aafb7f | |||
| b8fbd4a578 | |||
| e2b2092ce6 | |||
| f4df7b88b3 | |||
| 3d9bd404ee | |||
| 5f5e55aa8d | |||
| aaaf1c8df5 | |||
| 0baa4f9837 | |||
| d3d4ab41a9 | |||
| 28d844642a | |||
| eb5fd3127f | |||
| 3ed28e4bfe | |||
| db14238ba3 | |||
| 3f64b5a3de | |||
| 66904097e0 | |||
| e54139c85b | |||
| 3710a5ec7e | |||
| 705e3cd5f5 | |||
| 8c61fd2541 | |||
| d3145e4e5d | |||
| 743a1da3ef | |||
| 123901b741 | |||
| 71fd79a3f4 | |||
| c4a530f104 | |||
| 272484f8bf | |||
| de4e466036 | |||
| c23a0c4a0f | |||
| a95670b981 | |||
| 46bdc03885 | |||
| 5160c7efa5 | |||
| e2dd4cd05f | |||
| e509cac533 | |||
| fd38171810 | |||
| 2eca1b3cc1 | |||
| 625162674d | |||
| a692278bc6 | |||
| d4016dd5cd | |||
| 4910312ee7 | |||
| 1b5f78b6b7 | |||
| d5d0f420d8 | |||
| 5993afa4c5 | |||
| d1b4458536 | |||
| 274852bcf2 | |||
| 37a2d5eb4e | |||
| 4c95389c25 | |||
| 3af8bded26 | |||
| 46da0b7aaa | |||
| 75f56b4ce6 | |||
| d234a963eb | |||
| b84644c008 | |||
| a9d19dbdd6 | |||
| 12d713176f | |||
| 6b65b2fae6 | |||
| 1768684d8b | |||
| 9aec42c545 | |||
| eb0ed33dfd | |||
| 6a246d1152 | |||
| ef0e7149d6 | |||
| c32094ace1 | |||
| a262fd4ddb | |||
| f0d32455d2 | |||
| e8bc8e6d02 | |||
| f9b3c0f486 | |||
| 11fcda971c | |||
| f31375d4cc | |||
| 6415b599be | |||
| 1e9b87f760 | |||
| 275c0acbf6 | |||
| f9163155d3 | |||
| 79a17d9e55 | |||
| 75c0f88289 | |||
| 18ce854626 | |||
| 69f5bad40f | |||
| 525bed9919 | |||
| 2ae4786ca1 | |||
| da7983ac4b | |||
| df5d76fed9 | |||
| 7362ba2204 | |||
| 0eb480aa1d | |||
| 7e9092eeeb | |||
| 0200eaf58f | |||
| 28b640a220 | |||
| db45dbbee8 | |||
| 77b976dd92 | |||
| e847a98db6 | |||
| 45c9bbc6ca | |||
| 94fbab2a88 | |||
| c3f273b2d7 | |||
| d7dcc76d27 | |||
| 9bf76fd7e8 | |||
| 15d3e48cec | |||
| b2a02c3c5a | |||
| 6821d6f159 | |||
| 93c7ad12ec | |||
| d2ae9e1363 | |||
| 3a8aad6b19 | |||
| a7b93e6564 | |||
| 65871d5cf4 | |||
| 2e4bf89ec7 | |||
| ebaa733d45 | |||
| 24b1be45c1 | |||
| 111f02db2a | |||
| 8a08f91d5b | |||
| dcf425c846 | |||
| 9a8c84d600 | |||
| a19d03b0e6 | |||
| cdf0b3b7a6 | |||
| d979831dbd | |||
| bcf7421ea5 | |||
| b6538532f4 | |||
| 90944aa261 | |||
| db7bc49690 | |||
| a2f6400d65 | |||
| 7d74cd0cc8 | |||
| d139f30234 | |||
| b1b6912412 | |||
| 0de81c6430 | |||
| 851bf3515e | |||
| d4a6c16c17 | |||
| 5d5fb22ead | |||
| 7c62d6ef16 | |||
| 641390f8f5 | |||
| 2e6191b2e0 | |||
| 22861cb4dc | |||
| 16da1ade70 | |||
| a3d42a277d | |||
| 2c20fd557a | |||
| 243736e751 | |||
| bf891c2259 | |||
| da0df0ada3 | |||
| 0885f8ec2b | |||
| 61be050a95 | |||
| 65af75fb00 | |||
| 2d75f74b86 | |||
| 2ac3d61604 | |||
| ade7ec8187 | |||
| 8fa8ce3e48 | |||
| 9dce45c25d | |||
| 239f2e2c99 | |||
| 3e1ea030a5 | |||
| 30885a6c5f | |||
| 58da8554cb | |||
| 92c1594372 | |||
| 5a7e0da84e | |||
| d7db096a9d | |||
| c1df5b1b0b | |||
| be8e2de597 | |||
| d297c8e1b5 | |||
| fbb8eb39ec | |||
| 87d9bffe74 | |||
| d9fdcbe939 | |||
| 295d733883 | |||
| d5247788ee | |||
| 036e260d8d | |||
| a50356b9a2 | |||
| 8859be1c01 | |||
| 87de22d746 | |||
| bec9b80d95 | |||
| 1c97095ac0 | |||
| b3cb6788b2 | |||
| 5542f02fbc | |||
| 0bc7ed9a17 | |||
| 36b5126050 | |||
| 2589b1295c | |||
| ef6e813ca8 | |||
| f4598a0986 | |||
| dc2a544fac | |||
| 916397320b | |||
| b3f10556f0 | |||
| 4d7af757c9 | |||
| 7f058b8bab | |||
| 575d7f8068 | |||
| 739d10e8bd | |||
| 0dbe0f1a07 | |||
| dc95245212 | |||
| d70b6c7d01 | |||
| d947db3713 | |||
| d8c94c354a | |||
| 6bbec90014 | |||
| 5bd22ba85f | |||
| 7ae62f4aa3 | |||
| f1a061e9ed | |||
| 2f11e673c5 | |||
| 387c5f60f9 | |||
| c406fd3a0e | |||
| 286821e360 | |||
| 79f15be5b0 | |||
| 5c2e014a51 | |||
| cc44583cc3 | |||
| 4da672af88 | |||
| f1886f8c37 | |||
| 5bec50838c | |||
| f650e4d751 | |||
| c934a45069 | |||
| 91b02091a5 | |||
| 8966424e0e | |||
| 72510969a6 | |||
| 70c5e485bb | |||
| a78b17d7f1 | |||
| a481942532 | |||
| 0f921ace6f | |||
| 6019953769 | |||
| 92e86702ea | |||
| 6c9ea022a9 | |||
| 12d3469dd1 | |||
| bf7ee8a91a | |||
| d912491f2d | |||
| 857374e693 | |||
| c27639b9a0 | |||
| ceb486f98c | |||
| 5afc89f2bf | |||
| c1eac616d5 | |||
| 3c8256a137 | |||
| 19d76ae96b | |||
| 11ecea6106 | |||
| da98724bc8 | |||
| 4d470bb732 | |||
| 0ff8e13c1c | |||
| ffd37d4f75 | |||
| b4b65245b2 | |||
| 87886f40d1 | |||
| 67caf50698 | |||
| 7917961d59 | |||
| 80ecb95895 | |||
| 58e09cc6c7 | |||
| 635cc346ce | |||
| 12646750aa | |||
| 79639c981c | |||
| 9b313cec1b | |||
| b74b8d6478 | |||
| 793e38f5ce | |||
| 7f698c8934 | |||
| fa0e16f4c7 | |||
| 12e4c4d5dc | |||
| bac03f4076 | |||
| cc14c6a52c | |||
| e3308a0bbc | |||
| 019299b0b0 | |||
| 23ed087c4e | |||
| a018259bcf | |||
| ad5e04daa4 | |||
| e9b716ad98 | |||
| ae31dbdd1e | |||
| 5b4bfc2db7 | |||
| 92f55f1121 | |||
| 078226dd66 | |||
| 06d228aad1 | |||
| b1016d2e26 | |||
| 49336bfc58 | |||
| 941818295d | |||
| 5491e55477 | |||
| bf7af9043d | |||
| 5e12bf7dc3 | |||
| 9df2ec7988 | |||
| 59300faae4 | |||
| 9f98b3e718 | |||
| 3ab84651c6 | |||
| 7241b5fd51 | |||
| 6dfbd24797 | |||
| f59c3c0214 | |||
| 3ca29b1563 | |||
| fdb0b7fb19 | |||
| 49dd53a405 | |||
| 68d9d3ebd7 | |||
| 9009c8cdca | |||
| 12dc9a6e4a | |||
| 1b85303ac2 | |||
| 8a04d47ab7 | |||
| 7c9fada778 | |||
| 0e79446586 | |||
| aaf2e9693a | |||
| ee359df172 | |||
| 0f989d5e21 | |||
| f73c567534 | |||
| d178fe94fe | |||
| 732dcb848f | |||
| 944859f1ad | |||
| 90b2674ddc | |||
| bb9377b5e5 | |||
| e88f1b80b8 | |||
| 516f0cb6ce | |||
| 21d17566a3 | |||
| 35e9b3ed1e | |||
| 3c1deaf201 | |||
| a0009e070a | |||
| 7bed16c30f | |||
| 2ba2271e4a | |||
| 60a2cdf72d | |||
| e08cfc8ca0 | |||
| ef147f4e87 | |||
| 489519566e | |||
| 91c0b0f76b | |||
| baef235d50 | |||
| 0541a1b57d | |||
| 258884f44f | |||
| 254c2b323c | |||
| 84f71ba8ff | |||
| 69f0e88a4a | |||
| 82adc972e8 | |||
| 60533fdc06 | |||
| 261143bbbc | |||
| b8526f32fa | |||
| ed7e250068 | |||
| 6a09e7f28e | |||
| 29e5cbb699 | |||
| ded6866001 | |||
| e1d43021ca | |||
| 5db215d4d6 | |||
| b7dc93ca11 | |||
| f210188da3 | |||
| c782ebd89c | |||
| 73779707a2 | |||
| 085c1b3d61 | |||
| 6c955cc3df | |||
| 5446900dee | |||
| cf38c73f53 | |||
| 536bf71abe | |||
| ea3c95a7b0 | |||
| 5e5ce00d41 | |||
| a191b38ac8 | |||
| 382c726e63 | |||
| 4c46f9400b | |||
| 8c7a4e995b | |||
| 2ca0fcc448 | |||
| a6d774e809 | |||
| dfb1895e4b | |||
| 9e4072ac23 | |||
| 0ef2952846 | |||
| e3ae8f025d | |||
| 06faee5beb | |||
| 409c6c3fcd | |||
| ff77a927f9 | |||
| 9cdca1f958 | |||
| 6462eecfbd | |||
| a7d6b1413f | |||
| 3f497d496f | |||
| 32585696be | |||
| d47225ce9e | |||
| e3dfc76fa8 | |||
| cd59216de4 | |||
| e285153d7f | |||
| 20ea2e0c65 | |||
| 51d0a0ab01 | |||
| fd099331ec | |||
| 43f1282e73 | |||
| e96cdfe966 | |||
| ffdbce5787 | |||
| a838a27e4c | |||
| d3927310d5 | |||
| 273d9f69a6 | |||
| a6257d8a44 | |||
| 6c2db0dfb0 | |||
| 8f293a1855 | |||
| 901cd970db | |||
| f86b5f70a6 | |||
| 3297b395d8 | |||
| ba6f74e404 | |||
| 231374a1fa | |||
| 5adc837904 | |||
| f89108f2b1 | |||
| dd67922013 | |||
| 9a235d0afc | |||
| 99407c39ba | |||
| 7c90c19d28 | |||
| ed906a86e2 | |||
| d666edad71 |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22000.20100
|
||||
- image: bepsays/ci-hugoreleaser:1.22100.20600
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22000.20100
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22100.20600
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -5,7 +5,12 @@ assignees: ''
|
||||
about: Create a report to help us improve
|
||||
---
|
||||
|
||||
|
||||
<!--
|
||||
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:
|
||||
|
||||
https://discourse.gohugo.io
|
||||
-->
|
||||
|
||||
<!-- Please answer these questions before submitting your issue. Thanks! -->
|
||||
|
||||
### What version of Hugo are you using (`hugo version`)?
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
name: TestDartSassV1
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
DART_SASS_VERSION: 1.62.1
|
||||
DART_SASS_SHA_LINUX: 3574da75a7322a539034648b8ff84ff2cca162eb924d72b663d718cd3936f075
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@036ef458ddccddb148a2b9fb67e95a22fdbf728b
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html.py --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y pandoc
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName pandoc
|
||||
- run: pandoc -v
|
||||
- name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- name: Check
|
||||
run: |
|
||||
dart-sass-embedded --version
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
+56
-31
@@ -6,24 +6,24 @@ name: Test
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
DART_SASS_VERSION: 1.56.2
|
||||
DART_SASS_SHA_LINUX: 9e4f455f7b8619959d7878af2862383be58392eb963a14ff87cc512c03701e2a
|
||||
DART_SASS_SHA_MACOS: 5992e979e2c30ec363f8e338822bb2b4443c74232b3340501a76180f5652cb09
|
||||
DART_SASS_SHA_WINDOWS: 8d3d9117c54840e3e6a4919e43acf75ea52f28a64fc87a8e29d80ec72ee36cfb
|
||||
SASS_VERSION: 1.63.2
|
||||
DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
|
||||
DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
|
||||
DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x,1.20.x]
|
||||
go-version: [1.20.x,1.21.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
@@ -32,16 +32,16 @@ jobs:
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4
|
||||
uses: ruby/setup-ruby@036ef458ddccddb148a2b9fb67e95a22fdbf728b
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@07afc7d24f4d6d6442305d49552f04fbda5ccb3e
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Install docutils
|
||||
@@ -58,37 +58,62 @@ jobs:
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName pandoc
|
||||
choco install pandoc
|
||||
- run: pandoc -v
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName mingw -ArgumentList "--version","10.2.0","--allow-downgrade"
|
||||
choco install mingw
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass-embedded Linux
|
||||
name: Install dart-sass Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
echo "Install Dart Sass version ${SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} dart-sass-${SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GOBIN"
|
||||
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Install dart-sass-embedded MacOS
|
||||
name: Install dart-sass MacOS
|
||||
run: |
|
||||
echo "Install Dart Sass version ${DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_MACOS} sass_embedded-${DART_SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf "sass_embedded-${DART_SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
echo "Install Dart Sass version ${SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_MACOS} dart-sass-${SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf "dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'windows-latest'
|
||||
name: Install dart-sass-embedded Windows
|
||||
name: Install dart-sass Windows
|
||||
run: |
|
||||
echo "Install Dart Sass version ${env:DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${env:DART_SASS_VERSION}/sass_embedded-${env:DART_SASS_VERSION}-windows-x64.zip";
|
||||
Expand-Archive -Path "sass_embedded-${env:DART_SASS_VERSION}-windows-x64.zip" -DestinationPath .;
|
||||
echo "$env:GITHUB_WORKSPACE/sass_embedded/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- name: Check
|
||||
echo "Install Dart Sass version ${env:SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${env:SASS_VERSION}/dart-sass-${env:SASS_VERSION}-windows-x64.zip";
|
||||
Expand-Archive -Path "dart-sass-${env:SASS_VERSION}-windows-x64.zip" -DestinationPath .;
|
||||
echo "$env:GITHUB_WORKSPACE/dart-sass/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install staticcheck
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Run staticcheck
|
||||
run: staticcheck ./...
|
||||
- if: matrix.os != 'windows-latest'
|
||||
name: Check
|
||||
run: |
|
||||
sass --version;
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
|
||||
- if: matrix.os == 'windows-latest'
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended,nodeploy
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
|
||||
@@ -122,8 +122,6 @@ cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
>Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`.
|
||||
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
|
||||
```bash
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.19-alpine AS build
|
||||
FROM golang:1.21-alpine AS build
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
@@ -26,7 +26,7 @@ RUN mage hugo && mage install
|
||||
|
||||
# ---
|
||||
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.18
|
||||
|
||||
COPY --from=build /go/bin/hugo /usr/bin/hugo
|
||||
|
||||
|
||||
@@ -1,243 +1,292 @@
|
||||
[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
|
||||
[documentation repository]: https://github.com/gohugoio/hugoDocs
|
||||
[documentation]: https://gohugo.io/documentation
|
||||
[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd
|
||||
[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
|
||||
[linux]: https://gohugo.io/installation/linux
|
||||
[macos]: https://gohugo.io/installation/macos
|
||||
[prebuilt binary]: https://github.com/gohugoio/hugo/releases/latest
|
||||
[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132
|
||||
[spf13]: https://github.com/spf13
|
||||
[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator
|
||||
[support]: https://discourse.gohugo.io
|
||||
[themes]: https://themes.gohugo.io/
|
||||
[twitter]: https://twitter.com/gohugoio
|
||||
[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](https://github.com/bep), [spf13](https://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go](https://go.dev/).
|
||||
A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go].
|
||||
|
||||
[Website](https://gohugo.io) |
|
||||
[Forum](https://discourse.gohugo.io) |
|
||||
[Documentation](https://gohugo.io/getting-started/) |
|
||||
[Installation Guide](https://gohugo.io/getting-started/installing/) |
|
||||
[Contribution Guide](CONTRIBUTING.md) |
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
---
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
* [Overview](#overview)
|
||||
* [Banner Sponsors](#banner-sponsors)
|
||||
* [Supported Architectures](#supported-architectures)
|
||||
* [Choose How to Install](#choose-how-to-install)
|
||||
* [Install Hugo as Your Site Generator (Binary Install)](#install-hugo-as-your-site-generator-binary-install)
|
||||
* [Build and Install the Binary from Source (Using the Go toolchain)](#build-and-install-the-binary-from-source-using-the-go-toolchain)
|
||||
* [The Hugo Documentation](#the-hugo-documentation)
|
||||
* [Contributing to Hugo](#contributing-code-to-hugo)
|
||||
* [Dependencies](#dependencies)
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
|
||||
## Overview
|
||||
|
||||
Hugo is a static HTML and CSS website generator written in [Go](https://go.dev/).
|
||||
It is optimized for speed, ease of use, and configurability.
|
||||
Hugo takes a directory with content and templates and renders them into a full HTML website.
|
||||
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 relies on Markdown files with front matter for metadata, and you can run Hugo from any directory.
|
||||
This works well for shared hosts and other systems where you don’t have a privileged account.
|
||||
Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create:
|
||||
|
||||
Hugo renders a typical website of moderate size in a fraction of a second.
|
||||
A good rule of thumb is that each piece of content renders in around 1 millisecond.
|
||||
- Corporate, government, nonprofit, education, news, event, and project sites
|
||||
- Documentation sites
|
||||
- Image portfolios
|
||||
- Landing pages
|
||||
- Business, professional, and personal blogs
|
||||
- Resumes and CVs
|
||||
|
||||
Hugo is designed to work well for any kind of website including blogs, tumbles, and docs.
|
||||
Use Hugo's embedded web server during development to instantly see changes to content, structure, behavior, and presentation. Then deploy the site to your host, or push changes to your Git provider for automated builds and deployment.
|
||||
|
||||
Hugo's fast asset pipelines include:
|
||||
|
||||
- CSS bundling – transpilation (Sass), tree shaking, minification, source maps, SRI hashing, and PostCSS integration
|
||||
- JavaScript bundling – transpilation (TypeScript, JSX), tree shaking, minification, source maps, and SRI hashing
|
||||
- Image processing – convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
|
||||
|
||||
And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
|
||||
|
||||
## Sponsors
|
||||
|
||||
## Banner Sponsors
|
||||
<p> </p>
|
||||
<p float="left">
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/cloudcannon-blue.svg" width="220" alt="CloudCannon"></a>
|
||||
<p> </p>
|
||||
|
||||
## Supported Architectures
|
||||
## Installation
|
||||
|
||||
Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD, DragonFly BSD, OpenBSD, macOS (Darwin), and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures.
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
|
||||
Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including Plan 9 and Solaris.
|
||||
- [macOS]
|
||||
- [Linux]
|
||||
- [Windows]
|
||||
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD]
|
||||
|
||||
**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
|
||||
## Build from source
|
||||
|
||||
## Choose How to Install
|
||||
Hugo is available in two editions: standard and extended. With the extended edition you can:
|
||||
|
||||
If you want to use Hugo as your site generator, simply install the Hugo binaries.
|
||||
- Encode to the WebP format when processing images. You can decode WebP images with either edition.
|
||||
- Transpile Sass to CSS using the embedded LibSass transpiler. The extended edition is not required to use the Dart Sass transpiler.
|
||||
|
||||
To contribute to the Hugo source code or documentation, you should [fork the Hugo GitHub project](https://github.com/gohugoio/hugo#fork-destination-box) and clone it to your local machine.
|
||||
Prerequisites to build Hugo from source:
|
||||
|
||||
Finally, you can install the Hugo source code with `go`, build the binaries yourself, and run Hugo that way.
|
||||
Building the binaries is an easy task for an experienced `go` getter.
|
||||
- Standard edition: Go 1.20 or later
|
||||
- Extended edition: Go 1.20 or later, and GCC
|
||||
|
||||
### Install Hugo as Your Site Generator (Binary Install)
|
||||
Build the standard edition:
|
||||
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/).
|
||||
|
||||
### Build and Install the Binary from Source (Using the Go toolchain)
|
||||
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.95.0 only builds with >= Go 1.18.)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
To fetch, build and install from the Github source:
|
||||
|
||||
```bash
|
||||
```text
|
||||
go install github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
If you want to compile with Sass/SCSS support use `-tags extended` and make sure `CGO_ENABLED=1` is set in your go environment. If you don't want to have CGO enabled, you may use the following command to temporarily enable CGO only for hugo compilation:
|
||||
Build the extended edition:
|
||||
|
||||
```bash
|
||||
```text
|
||||
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
## The Hugo Documentation
|
||||
## Documentation
|
||||
|
||||
The Hugo documentation now lives in its own repository, see https://github.com/gohugoio/hugoDocs. But we do keep a version of that documentation as a `git subtree` in this repository. To build the sub folder `/docs` as a Hugo site, you need to clone this repo:
|
||||
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:gohugoio/hugo.git
|
||||
```
|
||||
## Contributing code to Hugo
|
||||
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].
|
||||
|
||||
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]
|
||||
|
||||
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:
|
||||
|
||||
- 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.
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
organization, tutorials, blog posts, bug reports, issues, feature requests,
|
||||
feature implementations, pull requests, answering questions on the forum,
|
||||
helping to manage issues, etc.
|
||||
|
||||
The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity.
|
||||
|
||||
## Asking Support Questions
|
||||
|
||||
We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions.
|
||||
Please don't use the GitHub issue tracker to ask questions.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you believe you have found a defect in Hugo or its documentation, use
|
||||
the GitHub issue tracker to report the problem to the Hugo maintainers.
|
||||
If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io).
|
||||
When reporting the issue, please provide the version of Hugo in use (`hugo version`).
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries.
|
||||
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
|
||||
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
<details>
|
||||
<summary>See current dependencies</summary>
|
||||
|
||||
In Hugo 0.111.2 that list is, in lexical order:
|
||||
|
||||
```
|
||||
cloud.google.com/go/compute="v1.6.1"
|
||||
cloud.google.com/go/iam="v0.3.0"
|
||||
cloud.google.com/go/storage="v1.22.0"
|
||||
cloud.google.com/go="v0.101.0"
|
||||
github.com/Azure/azure-pipeline-go="v0.2.3"
|
||||
github.com/Azure/azure-storage-blob-go="v0.14.0"
|
||||
github.com/Azure/go-autorest/autorest/adal="v0.9.15"
|
||||
github.com/Azure/go-autorest/autorest/date="v0.3.0"
|
||||
github.com/Azure/go-autorest/autorest="v0.11.20"
|
||||
github.com/Azure/go-autorest/logger="v0.2.1"
|
||||
github.com/Azure/go-autorest/tracing="v0.6.0"
|
||||
```text
|
||||
cloud.google.com/go/compute/metadata="v0.2.3"
|
||||
cloud.google.com/go/iam="v1.1.3"
|
||||
cloud.google.com/go/storage="v1.31.0"
|
||||
cloud.google.com/go="v0.110.8"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.7.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.1.0"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.0"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.0.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.11.1"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.7.0"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.4.0"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.5.0"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.2.2"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.3.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.4.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.7.0"
|
||||
github.com/aws/aws-sdk-go-v2="v1.9.0"
|
||||
github.com/aws/aws-sdk-go="v1.43.5"
|
||||
github.com/aws/smithy-go="v1.8.0"
|
||||
github.com/bep/clock="v0.3.0"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.4.11"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.18.32"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.13.31"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.13.7"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.11.76"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.1.37"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.4.31"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.3.38"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.1.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.9.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.1.32"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.9.31"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.15.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.38.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.13.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.15.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.21.1"
|
||||
github.com/aws/aws-sdk-go-v2="v1.20.0"
|
||||
github.com/aws/aws-sdk-go="v1.48.2"
|
||||
github.com/aws/smithy-go="v1.14.0"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass="v0.16.0"
|
||||
github.com/bep/golibsass="v1.1.0"
|
||||
github.com/bep/gowebp="v0.2.0"
|
||||
github.com/bep/godartsass/v2="v2.0.0"
|
||||
github.com/bep/godartsass="v1.2.0"
|
||||
github.com/bep/golibsass="v1.1.1"
|
||||
github.com/bep/gowebp="v0.3.0"
|
||||
github.com/bep/lazycache="v0.2.0"
|
||||
github.com/bep/logg="v0.3.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.6.0"
|
||||
github.com/bep/simplecobra="v0.3.2"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/clbanning/mxj/v2="v2.5.7"
|
||||
github.com/cli/safeexec="v1.0.0"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.2"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.7.0"
|
||||
github.com/dustin/go-humanize="v1.0.0"
|
||||
github.com/evanw/esbuild="v0.17.0"
|
||||
github.com/frankban/quicktest="v1.14.4"
|
||||
github.com/fsnotify/fsnotify="v1.6.0"
|
||||
github.com/getkin/kin-openapi="v0.110.0"
|
||||
github.com/dlclark/regexp2="v1.10.0"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/evanw/esbuild="v0.19.7"
|
||||
github.com/fatih/color="v1.16.0"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.7.0"
|
||||
github.com/getkin/kin-openapi="v0.120.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.5"
|
||||
github.com/go-openapi/swag="v0.19.5"
|
||||
github.com/gobuffalo/flect="v0.3.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.6"
|
||||
github.com/go-openapi/swag="v0.22.4"
|
||||
github.com/gobuffalo/flect="v1.0.2"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/golang-jwt/jwt/v4="v4.0.0"
|
||||
github.com/golang-jwt/jwt/v4="v4.5.0"
|
||||
github.com/golang/groupcache="v0.0.0-20210331224755-41bb18bfe9da"
|
||||
github.com/golang/protobuf="v1.5.2"
|
||||
github.com/google/go-cmp="v0.5.9"
|
||||
github.com/google/uuid="v1.3.0"
|
||||
github.com/golang/protobuf="v1.5.3"
|
||||
github.com/google/go-cmp="v0.6.0"
|
||||
github.com/google/s2a-go="v0.1.7"
|
||||
github.com/google/uuid="v1.4.0"
|
||||
github.com/google/wire="v0.5.0"
|
||||
github.com/googleapis/gax-go/v2="v2.3.0"
|
||||
github.com/googleapis/go-type-adapters="v1.0.0"
|
||||
github.com/gorilla/websocket="v1.5.0"
|
||||
github.com/hairyhenderson/go-codeowners="v0.2.3-0.20201026200250-cdc7c0759690"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.3.2"
|
||||
github.com/googleapis/gax-go/v2="v2.12.0"
|
||||
github.com/gorilla/websocket="v1.5.1"
|
||||
github.com/hairyhenderson/go-codeowners="v0.4.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.1"
|
||||
github.com/invopop/yaml="v0.1.0"
|
||||
github.com/invopop/yaml="v0.2.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.11"
|
||||
github.com/mailru/easyjson="v0.0.0-20190626092158-b2ccc519800e"
|
||||
github.com/marekm4/color-extractor="v1.2.0"
|
||||
github.com/mattn/go-ieproxy="v0.0.1"
|
||||
github.com/mattn/go-isatty="v0.0.17"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.12"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/marekm4/color-extractor="v1.2.1"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.20"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.5.0"
|
||||
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.6.5"
|
||||
github.com/niklasfasching/go-org="v1.7.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml/v2="v2.0.6"
|
||||
github.com/rogpeppe/go-internal="v1.9.0"
|
||||
github.com/pelletier/go-toml/v2="v2.1.0"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pkg/browser="v0.0.0-20210911075715-681adbf594b8"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.11.0"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.5"
|
||||
github.com/sass/dart-sass/compiler="1.63.2"
|
||||
github.com/sass/dart-sass/implementation="1.63.2"
|
||||
github.com/sass/dart-sass/protocol="2.0.0"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/spf13/afero="v1.9.3"
|
||||
github.com/spf13/cast="v1.5.0"
|
||||
github.com/spf13/cobra="v1.6.1"
|
||||
github.com/spf13/afero="v1.10.0"
|
||||
github.com/spf13/cast="v1.5.1"
|
||||
github.com/spf13/cobra="v1.7.0"
|
||||
github.com/spf13/fsync="v0.9.0"
|
||||
github.com/spf13/jwalterweatherman="v1.1.0"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/tdewolff/minify/v2="v2.12.4"
|
||||
github.com/tdewolff/parse/v2="v2.6.5"
|
||||
github.com/webmproject/libwebp="v1.2.4"
|
||||
github.com/yuin/goldmark="v1.5.4"
|
||||
github.com/tdewolff/minify/v2="v2.20.7"
|
||||
github.com/tdewolff/parse/v2="v2.7.5"
|
||||
github.com/webmproject/libwebp="v1.3.2"
|
||||
github.com/yuin/goldmark-emoji="v1.0.2"
|
||||
github.com/yuin/goldmark="v1.6.0"
|
||||
go.opencensus.io="v0.24.0"
|
||||
go.uber.org/atomic="v1.10.0"
|
||||
gocloud.dev="v0.24.0"
|
||||
golang.org/x/crypto="v0.3.0"
|
||||
go.uber.org/atomic="v1.11.0"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
gocloud.dev="v0.34.0"
|
||||
golang.org/x/crypto="v0.15.0"
|
||||
golang.org/x/exp="v0.0.0-20221031165847-c99f073a8326"
|
||||
golang.org/x/image="v0.5.0"
|
||||
golang.org/x/net="v0.7.0"
|
||||
golang.org/x/oauth2="v0.2.0"
|
||||
golang.org/x/sync="v0.1.0"
|
||||
golang.org/x/sys="v0.5.0"
|
||||
golang.org/x/text="v0.7.0"
|
||||
golang.org/x/tools="v0.4.0"
|
||||
golang.org/x/image="v0.13.0"
|
||||
golang.org/x/mod="v0.14.0"
|
||||
golang.org/x/net="v0.18.0"
|
||||
golang.org/x/oauth2="v0.13.0"
|
||||
golang.org/x/sync="v0.5.0"
|
||||
golang.org/x/sys="v0.14.0"
|
||||
golang.org/x/text="v0.14.0"
|
||||
golang.org/x/time="v0.3.0"
|
||||
golang.org/x/tools="v0.15.0"
|
||||
golang.org/x/xerrors="v0.0.0-20220907171357-04be3eba64a2"
|
||||
google.golang.org/api="v0.76.0"
|
||||
google.golang.org/genproto="v0.0.0-20220426171045-31bebdecfb46"
|
||||
google.golang.org/grpc="v1.46.0"
|
||||
google.golang.org/protobuf="v1.28.1"
|
||||
google.golang.org/api="v0.151.0"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20231016165738-49dd2c1f3d0b"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20231030173426-d783a09b4405"
|
||||
google.golang.org/genproto="v0.0.0-20231016165738-49dd2c1f3d0b"
|
||||
google.golang.org/grpc="v1.59.0"
|
||||
google.golang.org/protobuf="v1.31.0"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
howett.net/plist="v1.0.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
// Package cache contains the differenct cache implementations.
|
||||
// Package cache contains the different cache implementations.
|
||||
package cache
|
||||
|
||||
Vendored
+550
@@ -0,0 +1,550 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/lazycache"
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/rungroup"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
const minMaxSize = 10
|
||||
|
||||
// New creates a new cache.
|
||||
func New(opts Options) *Cache {
|
||||
if opts.CheckInterval == 0 {
|
||||
opts.CheckInterval = time.Second * 2
|
||||
}
|
||||
|
||||
if opts.MaxSize == 0 {
|
||||
opts.MaxSize = 100000
|
||||
}
|
||||
if opts.Log == nil {
|
||||
panic("nil Log")
|
||||
}
|
||||
|
||||
if opts.MinMaxSize == 0 {
|
||||
opts.MinMaxSize = 30
|
||||
}
|
||||
|
||||
stats := &stats{
|
||||
opts: opts,
|
||||
adjustmentFactor: 1.0,
|
||||
currentMaxSize: opts.MaxSize,
|
||||
availableMemory: config.GetMemoryLimit(),
|
||||
}
|
||||
|
||||
infol := opts.Log.InfoCommand("dynacache")
|
||||
|
||||
c := &Cache{
|
||||
partitions: make(map[string]PartitionManager),
|
||||
opts: opts,
|
||||
stats: stats,
|
||||
infol: infol,
|
||||
}
|
||||
|
||||
c.stop = c.start()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Options for the cache.
|
||||
type Options struct {
|
||||
Log loggers.Logger
|
||||
CheckInterval time.Duration
|
||||
MaxSize int
|
||||
MinMaxSize int
|
||||
Running bool
|
||||
}
|
||||
|
||||
// Options for a partition.
|
||||
type OptionsPartition struct {
|
||||
// When to clear the this partition.
|
||||
ClearWhen ClearWhen
|
||||
|
||||
// Weight is a number between 1 and 100 that indicates how, in general, how big this partition may get.
|
||||
Weight int
|
||||
}
|
||||
|
||||
func (o OptionsPartition) WeightFraction() float64 {
|
||||
return float64(o.Weight) / 100
|
||||
}
|
||||
|
||||
func (o OptionsPartition) CalculateMaxSize(maxSizePerPartition int) int {
|
||||
return int(math.Floor(float64(maxSizePerPartition) * o.WeightFraction()))
|
||||
}
|
||||
|
||||
// A dynamic partitioned cache.
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
partitions map[string]PartitionManager
|
||||
opts Options
|
||||
infol logg.LevelLogger
|
||||
|
||||
stats *stats
|
||||
stopOnce sync.Once
|
||||
stop func()
|
||||
}
|
||||
|
||||
// ClearMatching clears all partition for which the predicate returns true.
|
||||
func (c *Cache) ClearMatching(predicate func(k, v any) bool) {
|
||||
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearMatching(predicate)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
for _, p := range c.partitions {
|
||||
g.Enqueue(p)
|
||||
}
|
||||
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
|
||||
func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
|
||||
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearOnRebuild(changeset...)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
for _, p := range c.partitions {
|
||||
g.Enqueue(p)
|
||||
}
|
||||
|
||||
g.Wait()
|
||||
|
||||
// Clear any entries marked as stale above.
|
||||
g = rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearStale()
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
for _, p := range c.partitions {
|
||||
g.Enqueue(p)
|
||||
}
|
||||
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
type keysProvider interface {
|
||||
Keys() []string
|
||||
}
|
||||
|
||||
// Keys returns a list of keys in all partitions.
|
||||
func (c *Cache) Keys(predicate func(s string) bool) []string {
|
||||
if predicate == nil {
|
||||
predicate = func(s string) bool { return true }
|
||||
}
|
||||
var keys []string
|
||||
for pn, g := range c.partitions {
|
||||
pkeys := g.(keysProvider).Keys()
|
||||
for _, k := range pkeys {
|
||||
p := path.Join(pn, k)
|
||||
if predicate(p) {
|
||||
keys = append(keys, p)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func calculateMaxSizePerPartition(maxItemsTotal, totalWeightQuantity, numPartitions int) int {
|
||||
if numPartitions == 0 {
|
||||
panic("numPartitions must be > 0")
|
||||
}
|
||||
if totalWeightQuantity == 0 {
|
||||
panic("totalWeightQuantity must be > 0")
|
||||
}
|
||||
|
||||
avgWeight := float64(totalWeightQuantity) / float64(numPartitions)
|
||||
return int(math.Floor(float64(maxItemsTotal) / float64(numPartitions) * (100.0 / avgWeight)))
|
||||
}
|
||||
|
||||
// Stop stops the cache.
|
||||
func (c *Cache) Stop() {
|
||||
c.stopOnce.Do(func() {
|
||||
c.stop()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) adjustCurrentMaxSize() {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if len(c.partitions) == 0 {
|
||||
return
|
||||
}
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
s := c.stats
|
||||
s.memstatsCurrent = m
|
||||
// fmt.Printf("\n\nAvailable = %v\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\nMaxSize = %d\nAdjustmentFactor=%f\n\n", helpers.FormatByteCount(s.availableMemory), helpers.FormatByteCount(m.Alloc), helpers.FormatByteCount(m.TotalAlloc), helpers.FormatByteCount(m.Sys), m.NumGC, c.stats.currentMaxSize, s.adjustmentFactor)
|
||||
|
||||
if s.availableMemory >= s.memstatsCurrent.Alloc {
|
||||
if s.adjustmentFactor <= 1.0 {
|
||||
s.adjustmentFactor += 0.2
|
||||
}
|
||||
} else {
|
||||
// We're low on memory.
|
||||
s.adjustmentFactor -= 0.4
|
||||
}
|
||||
|
||||
if s.adjustmentFactor <= 0 {
|
||||
s.adjustmentFactor = 0.05
|
||||
}
|
||||
|
||||
if !s.adjustCurrentMaxSize() {
|
||||
return
|
||||
}
|
||||
|
||||
totalWeight := 0
|
||||
for _, pm := range c.partitions {
|
||||
totalWeight += pm.getOptions().Weight
|
||||
}
|
||||
|
||||
maxSizePerPartition := calculateMaxSizePerPartition(c.stats.currentMaxSize, totalWeight, len(c.partitions))
|
||||
|
||||
evicted := 0
|
||||
for _, p := range c.partitions {
|
||||
evicted += p.adjustMaxSize(p.getOptions().CalculateMaxSize(maxSizePerPartition))
|
||||
}
|
||||
|
||||
if evicted > 0 {
|
||||
c.infol.
|
||||
WithFields(
|
||||
logg.Fields{
|
||||
{Name: "evicted", Value: evicted},
|
||||
{Name: "numGC", Value: m.NumGC},
|
||||
{Name: "limit", Value: helpers.FormatByteCount(c.stats.availableMemory)},
|
||||
{Name: "alloc", Value: helpers.FormatByteCount(m.Alloc)},
|
||||
{Name: "totalAlloc", Value: helpers.FormatByteCount(m.TotalAlloc)},
|
||||
},
|
||||
).Logf("adjusted partitions' max size")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) start() func() {
|
||||
ticker := time.NewTicker(c.opts.CheckInterval)
|
||||
quit := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.adjustCurrentMaxSize()
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
close(quit)
|
||||
}
|
||||
}
|
||||
|
||||
var partitionNameRe = regexp.MustCompile(`^\/[a-zA-Z0-9]{4}(\/[a-zA-Z0-9]+)?(\/[a-zA-Z0-9]+)?`)
|
||||
|
||||
// GetOrCreatePartition gets or creates a partition with the given name.
|
||||
func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts OptionsPartition) *Partition[K, V] {
|
||||
if c == nil {
|
||||
panic("nil Cache")
|
||||
}
|
||||
if opts.Weight < 1 || opts.Weight > 100 {
|
||||
panic("invalid Weight, must be between 1 and 100")
|
||||
}
|
||||
|
||||
if partitionNameRe.FindString(name) != name {
|
||||
panic(fmt.Sprintf("invalid partition name %q", name))
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
p, found := c.partitions[name]
|
||||
c.mu.RUnlock()
|
||||
if found {
|
||||
return p.(*Partition[K, V])
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Double check.
|
||||
p, found = c.partitions[name]
|
||||
if found {
|
||||
return p.(*Partition[K, V])
|
||||
}
|
||||
|
||||
// At this point, we don't know the the number of partitions or their configuration, but
|
||||
// this will be re-adjusted later.
|
||||
const numberOfPartitionsEstimate = 10
|
||||
maxSize := opts.CalculateMaxSize(c.opts.MaxSize / numberOfPartitionsEstimate)
|
||||
|
||||
// Create a new partition and cache it.
|
||||
partition := &Partition[K, V]{
|
||||
c: lazycache.New(lazycache.Options[K, V]{MaxEntries: maxSize}),
|
||||
maxSize: maxSize,
|
||||
trace: c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name),
|
||||
opts: opts,
|
||||
}
|
||||
c.partitions[name] = partition
|
||||
|
||||
return partition
|
||||
}
|
||||
|
||||
// Partition is a partition in the cache.
|
||||
type Partition[K comparable, V any] struct {
|
||||
c *lazycache.Cache[K, V]
|
||||
|
||||
zero V
|
||||
|
||||
trace logg.LevelLogger
|
||||
opts OptionsPartition
|
||||
|
||||
maxSize int
|
||||
}
|
||||
|
||||
// GetOrCreate gets or creates a value for the given key.
|
||||
func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) {
|
||||
v, _, err := p.c.GetOrCreate(key, create)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function
|
||||
// takes too long.
|
||||
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
|
||||
resultch := make(chan V, 1)
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
v, _, err := p.c.GetOrCreate(key, create)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
resultch <- v
|
||||
}()
|
||||
|
||||
select {
|
||||
case v := <-resultch:
|
||||
return v, nil
|
||||
case err := <-errch:
|
||||
return p.zero, err
|
||||
case <-time.After(duration):
|
||||
return p.zero, &herrors.TimeoutError{
|
||||
Duration: duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
if predicate(key, v) {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("clearing cache key %v", key)
|
||||
},
|
||||
),
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
|
||||
opts := p.getOptions()
|
||||
if opts.ClearWhen == ClearNever {
|
||||
return
|
||||
}
|
||||
|
||||
if opts.ClearWhen == ClearOnRebuild {
|
||||
// Clear all.
|
||||
p.Clear()
|
||||
return
|
||||
}
|
||||
|
||||
depsFinder := identity.NewFinder(identity.FinderConfig{})
|
||||
|
||||
shouldDelete := func(key K, v V) bool {
|
||||
// We always clear elements marked as stale.
|
||||
if resource.IsStaleAny(v) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Now check if this entry has changed based on the changeset
|
||||
// based on filesystem events.
|
||||
if len(changeset) == 0 {
|
||||
// Nothing changed.
|
||||
return false
|
||||
}
|
||||
|
||||
var probablyDependent bool
|
||||
identity.WalkIdentitiesShallow(v, func(level int, id2 identity.Identity) bool {
|
||||
for _, id := range changeset {
|
||||
if r := depsFinder.Contains(id, id2, -1); r > 0 {
|
||||
// It's probably dependent, evict from cache.
|
||||
probablyDependent = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return probablyDependent
|
||||
}
|
||||
|
||||
// First pass.
|
||||
// Second pass needs to be done in a separate loop to catch any
|
||||
// elements marked as stale in the other partitions.
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
if shouldDelete(key, v) {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("first pass: clearing cache key %v", key)
|
||||
},
|
||||
),
|
||||
)
|
||||
resource.MarkStale(v)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) Keys() []K {
|
||||
var keys []K
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
keys = append(keys, key)
|
||||
return false
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearStale() {
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
isStale := resource.IsStaleAny(v)
|
||||
if isStale {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("second pass: clearing cache key %v", key)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return isStale
|
||||
})
|
||||
}
|
||||
|
||||
// adjustMaxSize adjusts the max size of the and returns the number of items evicted.
|
||||
func (p *Partition[K, V]) adjustMaxSize(newMaxSize int) int {
|
||||
if newMaxSize < minMaxSize {
|
||||
newMaxSize = minMaxSize
|
||||
}
|
||||
p.maxSize = newMaxSize
|
||||
// fmt.Println("Adjusting max size of partition from", oldMaxSize, "to", newMaxSize)
|
||||
return p.c.Resize(newMaxSize)
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) getMaxSize() int {
|
||||
return p.maxSize
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) getOptions() OptionsPartition {
|
||||
return p.opts
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) Clear() {
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) Get(ctx context.Context, key K) (V, bool) {
|
||||
return p.c.Get(key)
|
||||
}
|
||||
|
||||
type PartitionManager interface {
|
||||
adjustMaxSize(addend int) int
|
||||
getMaxSize() int
|
||||
getOptions() OptionsPartition
|
||||
clearOnRebuild(changeset ...identity.Identity)
|
||||
clearMatching(predicate func(k, v any) bool)
|
||||
clearStale()
|
||||
}
|
||||
|
||||
const (
|
||||
ClearOnRebuild ClearWhen = iota + 1
|
||||
ClearOnChange
|
||||
ClearNever
|
||||
)
|
||||
|
||||
type ClearWhen int
|
||||
|
||||
type stats struct {
|
||||
opts Options
|
||||
memstatsCurrent runtime.MemStats
|
||||
currentMaxSize int
|
||||
availableMemory uint64
|
||||
|
||||
adjustmentFactor float64
|
||||
}
|
||||
|
||||
func (s *stats) adjustCurrentMaxSize() bool {
|
||||
newCurrentMaxSize := int(math.Floor(float64(s.opts.MaxSize) * s.adjustmentFactor))
|
||||
|
||||
if newCurrentMaxSize < s.opts.MaxSize {
|
||||
newCurrentMaxSize = int(s.opts.MinMaxSize)
|
||||
}
|
||||
changed := newCurrentMaxSize != s.currentMaxSize
|
||||
s.currentMaxSize = newCurrentMaxSize
|
||||
return changed
|
||||
}
|
||||
|
||||
// CleanKey turns s into a format suitable for a cache key for this package.
|
||||
// The key will be a Unix-styled path with a leading slash but no trailing slash.
|
||||
func CleanKey(s string) string {
|
||||
return path.Clean(paths.ToSlashPreserveLeading(s))
|
||||
}
|
||||
Vendored
+175
@@ -0,0 +1,175 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.StaleInfo = (*testItem)(nil)
|
||||
_ identity.Identity = (*testItem)(nil)
|
||||
)
|
||||
|
||||
type testItem struct {
|
||||
name string
|
||||
isStale bool
|
||||
}
|
||||
|
||||
func (t testItem) IsStale() bool {
|
||||
return t.isStale
|
||||
}
|
||||
|
||||
func (t testItem) IdentifierBase() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
cache := New(Options{
|
||||
Log: loggers.NewDefault(),
|
||||
})
|
||||
|
||||
c.Cleanup(func() {
|
||||
cache.Stop()
|
||||
})
|
||||
|
||||
opts := OptionsPartition{Weight: 30}
|
||||
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
|
||||
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts)
|
||||
c.Assert(p1, qt.Not(qt.IsNil))
|
||||
|
||||
p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts)
|
||||
|
||||
c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "foo bar", opts) }, qt.PanicMatches, ".*invalid partition name.*")
|
||||
c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 1234}) }, qt.PanicMatches, ".*invalid Weight.*")
|
||||
|
||||
c.Assert(p2, qt.Equals, p1)
|
||||
|
||||
p3 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", opts)
|
||||
c.Assert(p3, qt.Not(qt.IsNil))
|
||||
c.Assert(p3, qt.Not(qt.Equals), p1)
|
||||
|
||||
c.Assert(func() { New(Options{}) }, qt.PanicMatches, ".*nil Log.*")
|
||||
}
|
||||
|
||||
func TestCalculateMaxSizePerPartition(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(calculateMaxSizePerPartition(1000, 500, 5), qt.Equals, 200)
|
||||
c.Assert(calculateMaxSizePerPartition(1000, 250, 5), qt.Equals, 400)
|
||||
c.Assert(func() { calculateMaxSizePerPartition(1000, 250, 0) }, qt.PanicMatches, ".*must be > 0.*")
|
||||
c.Assert(func() { calculateMaxSizePerPartition(1000, 0, 1) }, qt.PanicMatches, ".*must be > 0.*")
|
||||
}
|
||||
|
||||
func TestCleanKey(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(CleanKey("a/b/c"), qt.Equals, "/a/b/c")
|
||||
c.Assert(CleanKey("/a/b/c"), qt.Equals, "/a/b/c")
|
||||
c.Assert(CleanKey("a/b/c/"), qt.Equals, "/a/b/c")
|
||||
c.Assert(CleanKey(filepath.FromSlash("/a/b/c/")), qt.Equals, "/a/b/c")
|
||||
}
|
||||
|
||||
func newTestCache(t *testing.T) *Cache {
|
||||
cache := New(
|
||||
Options{
|
||||
Log: loggers.NewDefault(),
|
||||
},
|
||||
)
|
||||
|
||||
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild})
|
||||
p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 30, ClearWhen: ClearOnChange})
|
||||
|
||||
p1.GetOrCreate("clearOnRebuild", func(string) (testItem, error) {
|
||||
return testItem{}, nil
|
||||
})
|
||||
|
||||
p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) {
|
||||
return testItem{
|
||||
isStale: true,
|
||||
}, nil
|
||||
})
|
||||
|
||||
p2.GetOrCreate("clearBecauseIdentityChanged", func(string) (testItem, error) {
|
||||
return testItem{
|
||||
name: "changed",
|
||||
}, nil
|
||||
})
|
||||
|
||||
p2.GetOrCreate("clearNever", func(string) (testItem, error) {
|
||||
return testItem{
|
||||
isStale: false,
|
||||
}, nil
|
||||
})
|
||||
|
||||
t.Cleanup(func() {
|
||||
cache.Stop()
|
||||
})
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
predicateAll := func(string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
cache := newTestCache(t)
|
||||
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
|
||||
|
||||
cache.ClearOnRebuild()
|
||||
|
||||
// Stale items are always cleared.
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
|
||||
|
||||
cache = newTestCache(t)
|
||||
cache.ClearOnRebuild(identity.StringIdentity("changed"))
|
||||
|
||||
c.Assert(cache.Keys(nil), qt.HasLen, 1)
|
||||
|
||||
cache = newTestCache(t)
|
||||
|
||||
cache.ClearMatching(func(k, v any) bool {
|
||||
return k.(string) == "clearOnRebuild"
|
||||
})
|
||||
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 3)
|
||||
|
||||
cache.adjustCurrentMaxSize()
|
||||
}
|
||||
|
||||
func TestAdjustCurrentMaxSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
cache := newTestCache(t)
|
||||
alloc := cache.stats.memstatsCurrent.Alloc
|
||||
cache.adjustCurrentMaxSize()
|
||||
c.Assert(cache.stats.memstatsCurrent.Alloc, qt.Not(qt.Equals), alloc)
|
||||
}
|
||||
Vendored
+37
-6
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
@@ -51,6 +52,9 @@ type Cache struct {
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
@@ -103,9 +107,23 @@ func (l *lockedFile) Close() error {
|
||||
return l.File.Close()
|
||||
}
|
||||
|
||||
func (c *Cache) init() error {
|
||||
c.initOnce.Do(func() {
|
||||
// Create the base dir if it does not exist.
|
||||
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
|
||||
c.initErr = err
|
||||
}
|
||||
})
|
||||
return c.initErr
|
||||
}
|
||||
|
||||
// WriteCloser returns a transactional writer into the cache.
|
||||
// It's important that it's closed when done.
|
||||
func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
|
||||
@@ -129,7 +147,12 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
// it when done.
|
||||
func (c *Cache) ReadOrCreate(id string,
|
||||
read func(info ItemInfo, r io.ReadSeeker) error,
|
||||
create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
|
||||
create func(info ItemInfo, w io.WriteCloser) error,
|
||||
) (info ItemInfo, err error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, err
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -163,6 +186,9 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
// be invoked and the result cached.
|
||||
// This method is protected by a named lock using the given id as identifier.
|
||||
func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -197,6 +223,9 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
|
||||
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
|
||||
func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -232,6 +261,9 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
|
||||
// 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) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -250,6 +282,9 @@ func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
|
||||
// Get gets the file with the given id from the cache, nil if none found.
|
||||
func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -347,11 +382,7 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
|
||||
baseDir := v.DirCompiled
|
||||
|
||||
if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bfs := afero.NewBasePathFs(cfs, baseDir)
|
||||
bfs := hugofs.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
var pruneAllRootDir string
|
||||
if k == CacheKeyModules {
|
||||
|
||||
Vendored
+2
-2
@@ -81,7 +81,7 @@ 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.
|
||||
// A negative value means forever, 0 means cache is disabled.
|
||||
// Hugo is leninent with what types it accepts here, but we recommend using
|
||||
// Hugo is lenient with what types it accepts here, but we recommend using
|
||||
// a duration string, a sequence of decimal numbers, each with optional fraction and a unit suffix,
|
||||
// such as "300ms", "1.5h" or "2h45m".
|
||||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
@@ -93,7 +93,7 @@ type FileCacheConfig struct {
|
||||
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
IsResourceDir bool
|
||||
IsResourceDir bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
|
||||
Vendored
+6
@@ -53,6 +53,9 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
if c.pruneAllRootDir != "" {
|
||||
return c.pruneRootDir(force)
|
||||
}
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
||||
@@ -117,6 +120,9 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
|
||||
Vendored
+1
-11
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -86,17 +85,8 @@ dir = ":cacheDir/c"
|
||||
cache := caches.Get("GetJSON")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
|
||||
bfs, ok := cache.Fs.(*afero.BasePathFs)
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
filename, err := bfs.RealPath("key")
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cache = caches.Get("Images")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
bfs, ok = cache.Fs.(*afero.BasePathFs)
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
filename, _ = bfs.RealPath("key")
|
||||
c.Assert(filename, qt.Equals, filepath.FromSlash("_gen/images/key"))
|
||||
|
||||
rf := func(s string) func() (io.ReadCloser, error) {
|
||||
return func() (io.ReadCloser, error) {
|
||||
|
||||
Vendored
+5
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,12 +15,10 @@ package filecache_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
@@ -48,7 +46,6 @@ title: "Home"
|
||||
_, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images"))
|
||||
|
||||
b.Assert(err, qt.IsNil)
|
||||
|
||||
}
|
||||
|
||||
func TestPruneImages(t *testing.T) {
|
||||
@@ -56,6 +53,7 @@ func TestPruneImages(t *testing.T) {
|
||||
// TODO(bep)
|
||||
t.Skip("skip flaky test on CI server")
|
||||
}
|
||||
t.Skip("skip flaky test")
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
@@ -80,7 +78,7 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: jww.LevelInfo},
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: logg.LevelInfo},
|
||||
).Build()
|
||||
|
||||
b.Assert(b.GCCount, qt.Equals, 0)
|
||||
@@ -93,7 +91,7 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||
|
||||
// TODO(bep) we need a way to test full rebuilds.
|
||||
// For now, just sleep a little so the cache elements expires.
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
b.RenameFile("assets/a/pixel.png", "assets/b/pixel2.png").Build()
|
||||
|
||||
@@ -105,5 +103,4 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||
b.Assert(err, qt.Not(qt.IsNil))
|
||||
_, err = b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir)
|
||||
b.Assert(err, qt.IsNil)
|
||||
|
||||
}
|
||||
|
||||
Vendored
-78
@@ -1,78 +0,0 @@
|
||||
// Copyright 2018 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 namedmemcache provides a memory cache with a named lock. This is suitable
|
||||
// for situations where creating the cached resource can be time consuming or otherwise
|
||||
// resource hungry, or in situations where a "once only per key" is a requirement.
|
||||
package namedmemcache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
)
|
||||
|
||||
// Cache holds the cached values.
|
||||
type Cache struct {
|
||||
nlocker *locker.Locker
|
||||
cache map[string]cacheEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
value any
|
||||
err error
|
||||
}
|
||||
|
||||
// New creates a new cache.
|
||||
func New() *Cache {
|
||||
return &Cache{
|
||||
nlocker: locker.NewLocker(),
|
||||
cache: make(map[string]cacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the cache state.
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.cache = make(map[string]cacheEntry)
|
||||
c.nlocker = locker.NewLocker()
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the value with the given cache key, if not found
|
||||
// create will be called and cached.
|
||||
// This method is thread safe. It also guarantees that the create func for a given
|
||||
// key is invoked only once for this cache.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (any, error)) (any, error) {
|
||||
c.mu.RLock()
|
||||
entry, found := c.cache[key]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if found {
|
||||
return entry.value, entry.err
|
||||
}
|
||||
|
||||
c.nlocker.Lock(key)
|
||||
defer c.nlocker.Unlock(key)
|
||||
|
||||
// Create it.
|
||||
value, err := create()
|
||||
|
||||
c.mu.Lock()
|
||||
c.cache[key] = cacheEntry{value: value, err: err}
|
||||
c.mu.Unlock()
|
||||
|
||||
return value, err
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
// Copyright 2018 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 namedmemcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNamedCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
cache := New()
|
||||
|
||||
counter := 0
|
||||
create := func() (any, error) {
|
||||
counter++
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
v1, err := cache.GetOrCreate("a1", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v1, qt.Equals, 1)
|
||||
v2, err := cache.GetOrCreate("a2", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v2, qt.Equals, 2)
|
||||
}
|
||||
|
||||
cache.Clear()
|
||||
|
||||
v3, err := cache.GetOrCreate("a2", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v3, qt.Equals, 3)
|
||||
}
|
||||
|
||||
func TestNamedCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cache := New()
|
||||
|
||||
create := func(i int) func() (any, error) {
|
||||
return func() (any, error) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
id := fmt.Sprintf("id%d", j)
|
||||
v, err := cache.GetOrCreate(id, create(j))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, j)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
+97
-105
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -18,25 +18,27 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/bep/clocks"
|
||||
"github.com/bep/lazycache"
|
||||
"github.com/bep/logg"
|
||||
"github.com/bep/overlayfs"
|
||||
"github.com/bep/simplecobra"
|
||||
|
||||
"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/config"
|
||||
@@ -49,9 +51,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
errHelp = errors.New("help requested")
|
||||
)
|
||||
var errHelp = errors.New("help requested")
|
||||
|
||||
// Execute executes a command.
|
||||
func Execute(args []string) error {
|
||||
@@ -108,17 +108,11 @@ type rootCommand struct {
|
||||
buildWatch bool
|
||||
environment string
|
||||
|
||||
// File format to read or write (TOML, YAML, JSON).
|
||||
format string
|
||||
|
||||
// Common build flags.
|
||||
baseURL string
|
||||
gc bool
|
||||
poll string
|
||||
panicOnWarning bool
|
||||
forceSyncStatic bool
|
||||
printPathWarnings bool
|
||||
printUnusedTemplates bool
|
||||
baseURL string
|
||||
gc bool
|
||||
poll string
|
||||
forceSyncStatic bool
|
||||
|
||||
// Profile flags (for debugging of performance problems)
|
||||
cpuprofile string
|
||||
@@ -127,17 +121,20 @@ type rootCommand struct {
|
||||
traceprofile string
|
||||
printm bool
|
||||
|
||||
// TODO(bep) var vs string
|
||||
logging bool
|
||||
verbose bool
|
||||
verboseLog bool
|
||||
debug bool
|
||||
quiet bool
|
||||
logLevel string
|
||||
|
||||
verbose bool
|
||||
debug bool
|
||||
quiet bool
|
||||
|
||||
renderToMemory bool
|
||||
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
logFile string
|
||||
}
|
||||
|
||||
func (r *rootCommand) isVerbose() bool {
|
||||
return r.logger.Level() <= logg.LevelInfo
|
||||
}
|
||||
|
||||
func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) {
|
||||
@@ -175,7 +172,7 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo
|
||||
|
||||
if !configs.Base.C.Clock.IsZero() {
|
||||
// TODO(bep) find a better place for this.
|
||||
htime.Clock = clock.Start(configs.Base.C.Clock)
|
||||
htime.Clock = clocks.Start(configs.Base.C.Clock)
|
||||
}
|
||||
|
||||
return &commonConfig{
|
||||
@@ -184,11 +181,9 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo
|
||||
cfg: oldConf.cfg,
|
||||
fs: fs,
|
||||
}, nil
|
||||
|
||||
})
|
||||
|
||||
return cc, err
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commonConfig, error) {
|
||||
@@ -213,7 +208,7 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
if !cfg.IsSet("workingDir") {
|
||||
cfg.Set("workingDir", dir)
|
||||
} else {
|
||||
if err := os.MkdirAll(cfg.GetString("workingDir"), 0777); err != nil {
|
||||
if err := os.MkdirAll(cfg.GetString("workingDir"), 0o777); err != nil {
|
||||
return nil, fmt.Errorf("failed to create workingDir: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -242,11 +237,11 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
renderStaticToDisk := cfg.GetBool("renderStaticToDisk")
|
||||
|
||||
sourceFs := hugofs.Os
|
||||
var desinationFs afero.Fs
|
||||
var destinationFs afero.Fs
|
||||
if cfg.GetBool("renderToDisk") {
|
||||
desinationFs = hugofs.Os
|
||||
destinationFs = hugofs.Os
|
||||
} else {
|
||||
desinationFs = afero.NewMemMapFs()
|
||||
destinationFs = afero.NewMemMapFs()
|
||||
if renderStaticToDisk {
|
||||
// Hybrid, render dynamic content to Root.
|
||||
cfg.Set("publishDirDynamic", "/")
|
||||
@@ -257,14 +252,14 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
}
|
||||
}
|
||||
|
||||
fs := hugofs.NewFromSourceAndDestination(sourceFs, desinationFs, cfg)
|
||||
fs := hugofs.NewFromSourceAndDestination(sourceFs, destinationFs, cfg)
|
||||
|
||||
if renderStaticToDisk {
|
||||
dynamicFs := fs.PublishDir
|
||||
publishDirStatic := cfg.GetString("publishDirStatic")
|
||||
workingDir := cfg.GetString("workingDir")
|
||||
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
|
||||
staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
|
||||
staticFs := hugofs.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
|
||||
|
||||
// Serve from both the static and dynamic fs,
|
||||
// the first will take priority.
|
||||
@@ -285,10 +280,10 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
|
||||
if !base.C.Clock.IsZero() {
|
||||
// TODO(bep) find a better place for this.
|
||||
htime.Clock = clock.Start(configs.Base.C.Clock)
|
||||
htime.Clock = clocks.Start(configs.Base.C.Clock)
|
||||
}
|
||||
|
||||
if base.LogPathWarnings {
|
||||
if base.PrintPathWarnings {
|
||||
// Note that we only care about the "dynamic creates" here,
|
||||
// so skip the static fs.
|
||||
fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
|
||||
@@ -305,12 +300,11 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
})
|
||||
|
||||
return cc, err
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
|
||||
h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
|
||||
depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, Logger: r.logger}
|
||||
depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level()}
|
||||
return hugolib.NewHugoSites(depsCfg)
|
||||
})
|
||||
return h, err
|
||||
@@ -322,7 +316,7 @@ func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, Logger: r.logger}
|
||||
depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, LogOut: r.logger.Out(), LogLevel: r.logger.Level()}
|
||||
return hugolib.NewHugoSites(depsCfg)
|
||||
})
|
||||
return h, err
|
||||
@@ -339,7 +333,7 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
|
||||
b := newHugoBuilder(r, nil)
|
||||
|
||||
if err := b.loadConfig(cd, true); err != nil {
|
||||
if err := b.loadConfig(cd, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -350,7 +344,6 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
err := b.build()
|
||||
return err
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -392,6 +385,9 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
if r.quiet {
|
||||
r.Out = io.Discard
|
||||
}
|
||||
// Used by mkcert (server).
|
||||
log.SetOutput(r.Out)
|
||||
|
||||
r.Printf = func(format string, v ...interface{}) {
|
||||
if !r.quiet {
|
||||
fmt.Fprintf(r.Out, format, v...)
|
||||
@@ -408,63 +404,62 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch r.format {
|
||||
case "json", "toml", "yaml":
|
||||
// OK
|
||||
default:
|
||||
return fmt.Errorf("unsupported format %q; must be one of json, toml or yaml", r.format)
|
||||
}
|
||||
|
||||
loggers.PanicOnWarning.Store(r.panicOnWarning)
|
||||
r.commonConfigs = lazycache.New[int32, *commonConfig](lazycache.Options{MaxEntries: 5})
|
||||
r.hugoSites = lazycache.New[int32, *hugolib.HugoSites](lazycache.Options{MaxEntries: 5})
|
||||
r.commonConfigs = lazycache.New(lazycache.Options[int32, *commonConfig]{MaxEntries: 5})
|
||||
// We don't want to keep stale HugoSites in memory longer than needed.
|
||||
r.hugoSites = lazycache.New(lazycache.Options[int32, *hugolib.HugoSites]{
|
||||
MaxEntries: 1,
|
||||
OnEvict: func(key int32, value *hugolib.HugoSites) {
|
||||
value.Close()
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
|
||||
var (
|
||||
logHandle = io.Discard
|
||||
logThreshold = jww.LevelWarn
|
||||
outHandle = r.Out
|
||||
stdoutThreshold = jww.LevelWarn
|
||||
)
|
||||
level := logg.LevelWarn
|
||||
|
||||
if r.verboseLog || r.logging || (r.logFile != "") {
|
||||
var err error
|
||||
if r.logFile != "" {
|
||||
logHandle, err = os.OpenFile(r.logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to open log file %q: %s", r.logFile, err)
|
||||
}
|
||||
} else {
|
||||
logHandle, err = os.CreateTemp("", "hugo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.logLevel != "" {
|
||||
switch strings.ToLower(r.logLevel) {
|
||||
case "debug":
|
||||
level = logg.LevelDebug
|
||||
case "info":
|
||||
level = logg.LevelInfo
|
||||
case "warn", "warning":
|
||||
level = logg.LevelWarn
|
||||
case "error":
|
||||
level = logg.LevelError
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
|
||||
}
|
||||
} else {
|
||||
if r.verbose {
|
||||
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
|
||||
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
|
||||
level = logg.LevelInfo
|
||||
}
|
||||
} else if r.verbose {
|
||||
stdoutThreshold = jww.LevelInfo
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
stdoutThreshold = jww.LevelDebug
|
||||
}
|
||||
|
||||
if r.verboseLog {
|
||||
logThreshold = jww.LevelInfo
|
||||
if r.debug {
|
||||
logThreshold = jww.LevelDebug
|
||||
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
|
||||
level = logg.LevelDebug
|
||||
}
|
||||
}
|
||||
|
||||
loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
|
||||
helpers.InitLoggers()
|
||||
return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil
|
||||
optsLogger := loggers.Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: level,
|
||||
Stdout: r.Out,
|
||||
Stderr: r.Out,
|
||||
StoreErrors: running,
|
||||
}
|
||||
|
||||
return loggers.New(optsLogger), nil
|
||||
}
|
||||
|
||||
func (r *rootCommand) Reset() {
|
||||
r.logger.Reset()
|
||||
loggers.Log().Reset()
|
||||
}
|
||||
|
||||
// IsTestRun reports whether the command is running as a test.
|
||||
@@ -485,7 +480,6 @@ Complete documentation is available at https://gohugo.io/.`
|
||||
|
||||
// Configure persistent flags
|
||||
cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.PersistentFlags().StringVar(&r.format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.PersistentFlags().StringP("destination", "d", "", "filesystem path to write files to")
|
||||
cmd.PersistentFlags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
|
||||
@@ -504,17 +498,12 @@ Complete documentation is available at https://gohugo.io/.`
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output")
|
||||
cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output")
|
||||
cmd.PersistentFlags().BoolVar(&r.logging, "log", false, "enable Logging")
|
||||
cmd.PersistentFlags().StringVar(&r.logFile, "logFile", "", "log File path (if set, logging enabled automatically)")
|
||||
cmd.PersistentFlags().BoolVar(&r.verboseLog, "verboseLog", false, "verbose logging")
|
||||
cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
|
||||
cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
|
||||
cmd.Flags().BoolVar(&r.renderToMemory, "renderToMemory", false, "render to memory (only useful for benchmark testing)")
|
||||
|
||||
// Set bash-completion
|
||||
_ = cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
|
||||
|
||||
// Configure local flags
|
||||
applyLocalBuildFlags(cmd, r)
|
||||
applyLocalFlagsBuild(cmd, r)
|
||||
|
||||
// Set bash-completion.
|
||||
// Each flag must first be defined before using the SetAnnotation() call.
|
||||
@@ -523,21 +512,29 @@ Complete documentation is available at https://gohugo.io/.`
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyLocalBuildFlags(cmd *cobra.Command, r *rootCommand) {
|
||||
// A sub set of the complete build flags. These flags are used by new and mod.
|
||||
func applyLocalFlagsBuildConfig(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
|
||||
cmd.Flags().StringVarP(&r.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
|
||||
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory")
|
||||
_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
}
|
||||
|
||||
// Flags needed to do a build (used by hugo and hugo server commands)
|
||||
func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories")
|
||||
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().StringP("contentDir", "c", "", "filesystem path to content directory")
|
||||
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
|
||||
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
|
||||
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
|
||||
cmd.Flags().StringVarP(&r.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
|
||||
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.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.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("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.")
|
||||
@@ -545,8 +542,8 @@ func applyLocalBuildFlags(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
|
||||
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
|
||||
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
|
||||
cmd.Flags().BoolVarP(&r.printPathWarnings, "printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().BoolVarP(&r.printUnusedTemplates, "printUnusedTemplates", "", false, "print warnings on unused templates.")
|
||||
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
|
||||
cmd.Flags().StringVarP(&r.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
|
||||
cmd.Flags().StringVarP(&r.memprofile, "profile-mem", "", "", "write memory profile to `file`")
|
||||
cmd.Flags().BoolVarP(&r.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
|
||||
@@ -559,13 +556,8 @@ func applyLocalBuildFlags(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().MarkHidden("profile-mutex")
|
||||
|
||||
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
|
||||
|
||||
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
|
||||
|
||||
_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) timeTrack(start time.Time, name string) {
|
||||
@@ -579,7 +571,7 @@ type simpleCommand struct {
|
||||
short string
|
||||
long string
|
||||
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
|
||||
withc func(cmd *cobra.Command)
|
||||
withc func(cmd *cobra.Command, r *rootCommand)
|
||||
initc func(cd *simplecobra.Commandeer) error
|
||||
|
||||
commands []simplecobra.Commander
|
||||
@@ -603,6 +595,7 @@ func (c *simpleCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
}
|
||||
|
||||
func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.rootCmd = cd.Root.Command.(*rootCommand)
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = c.short
|
||||
cmd.Long = c.long
|
||||
@@ -610,13 +603,12 @@ func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd.Use = c.use
|
||||
}
|
||||
if c.withc != nil {
|
||||
c.withc(cmd)
|
||||
c.withc(cmd, c.rootCmd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.rootCmd = cd.Root.Command.(*rootCommand)
|
||||
if c.initc != nil {
|
||||
return c.initc(cd)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -37,5 +37,4 @@ func newExec() (*simplecobra.Exec, error) {
|
||||
}
|
||||
|
||||
return simplecobra.New(rootCmd)
|
||||
|
||||
}
|
||||
|
||||
+30
-7
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -17,11 +17,14 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config/allconfig"
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
@@ -34,12 +37,14 @@ func newConfigCommand() *configCommand {
|
||||
&configMountsCommand{},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type configCommand struct {
|
||||
r *rootCommand
|
||||
|
||||
format string
|
||||
lang string
|
||||
|
||||
commands []simplecobra.Commander
|
||||
}
|
||||
|
||||
@@ -56,7 +61,16 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := conf.configs.Base
|
||||
var config *allconfig.Config
|
||||
if c.lang != "" {
|
||||
var found bool
|
||||
config, found = conf.configs.LanguageConfigMap[c.lang]
|
||||
if !found {
|
||||
return fmt.Errorf("language %q not found", c.lang)
|
||||
}
|
||||
} else {
|
||||
config = conf.configs.LanguageConfigSlice[0]
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
dec := json.NewEncoder(&buf)
|
||||
@@ -67,7 +81,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
return err
|
||||
}
|
||||
|
||||
format := strings.ToLower(c.r.format)
|
||||
format := strings.ToLower(c.format)
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
@@ -78,11 +92,14 @@ 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)
|
||||
switch format {
|
||||
case "yaml":
|
||||
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
|
||||
case "toml":
|
||||
return parser.InterfaceToConfig(m, metadecoders.TOML, os.Stdout)
|
||||
default:
|
||||
return fmt.Errorf("unsupported format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,14 +107,18 @@ 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 = "Print the site configuration"
|
||||
cmd.Long = `Print the site configuration, both default and custom settings.`
|
||||
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -168,10 +189,10 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type configMountsCommand struct {
|
||||
r *rootCommand
|
||||
configCmd *configCommand
|
||||
}
|
||||
|
||||
@@ -191,7 +212,7 @@ func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandee
|
||||
}
|
||||
|
||||
for _, m := range conf.configs.Modules {
|
||||
if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.verbose}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.isVerbose()}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -199,8 +220,10 @@ func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandee
|
||||
}
|
||||
|
||||
func (c *configMountsCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Print the configured file mounts"
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+9
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -45,7 +45,7 @@ to use JSON for the front matter.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
return c.convertContents(metadecoders.JSON)
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -56,7 +56,7 @@ to use TOML for the front matter.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
return c.convertContents(metadecoders.TOML)
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -67,7 +67,7 @@ to use YAML for the front matter.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
return c.convertContents(metadecoders.YAML)
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -84,7 +84,7 @@ type convertCommand struct {
|
||||
r *rootCommand
|
||||
h *hugolib.HugoSites
|
||||
|
||||
// Commmands.
|
||||
// Commands.
|
||||
commands []simplecobra.Commander
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ See convert's subcommands toJSON, toTOML and toYAML for more information.`
|
||||
cmd.PersistentFlags().StringVarP(&c.outputDir, "output", "o", "", "filesystem path to write files to")
|
||||
cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first")
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -133,14 +134,14 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
|
||||
}
|
||||
}
|
||||
|
||||
if p.File().IsZero() {
|
||||
if p.File() == nil {
|
||||
// No content file.
|
||||
return nil
|
||||
}
|
||||
|
||||
errMsg := fmt.Errorf("error processing file %q", p.File().Path())
|
||||
|
||||
site.Log.Infoln("ttempting to convert", p.File().Filename())
|
||||
site.Log.Infoln("attempting to convert", p.File().Filename())
|
||||
|
||||
f := p.File()
|
||||
file, err := f.FileInfo().Meta().Open()
|
||||
@@ -208,7 +209,7 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
|
||||
var pagesBackedByFile page.Pages
|
||||
for _, p := range site.AllPages() {
|
||||
if p.File().IsZero() {
|
||||
if p.File() == nil {
|
||||
continue
|
||||
}
|
||||
pagesBackedByFile = append(pagesBackedByFile, p)
|
||||
|
||||
+7
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -38,7 +38,6 @@ import (
|
||||
)
|
||||
|
||||
func newDeployCommand() simplecobra.Commander {
|
||||
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
short: "Deploy your site to a Cloud provider.",
|
||||
@@ -52,20 +51,20 @@ documentation.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.PathSpec.PublishFs)
|
||||
deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.Log, h.PathSpec.PublishFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deployer.Deploy(ctx)
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
|
||||
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
|
||||
cmd.Flags().Bool("dryRun", false, "dry run")
|
||||
cmd.Flags().Bool("force", false, "force upload of all files")
|
||||
cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
|
||||
cmd.Flags().Int("workers", 10, "number of workers to transfer files. defaults to 10")
|
||||
cmd.Flags().Bool("invalidateCDN", deploy.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().Int("maxDeletes", deploy.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable")
|
||||
cmd.Flags().Int("workers", deploy.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
//go:build nodeploy
|
||||
// +build nodeploy
|
||||
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -30,8 +30,9 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -41,7 +42,7 @@ func newDeployCommand() simplecobra.Commander {
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
},
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -32,7 +32,7 @@ func newEnvCommand() simplecobra.Commander {
|
||||
r.Printf("GOARCH=%q\n", runtime.GOARCH)
|
||||
r.Printf("GOVERSION=%q\n", runtime.Version())
|
||||
|
||||
if r.verbose {
|
||||
if r.isVerbose() {
|
||||
deps := hugo.GetDependencyList()
|
||||
for _, dep := range deps {
|
||||
r.Printf("%s\n", dep)
|
||||
|
||||
+39
-17
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -30,8 +31,11 @@ import (
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func newGenCommand() *genCommand {
|
||||
@@ -70,9 +74,9 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
formatter.WriteCSS(os.Stdout, style)
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
|
||||
},
|
||||
}
|
||||
@@ -97,7 +101,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
}
|
||||
if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
|
||||
r.Println("Directory", genmandir, "does not exist, creating...")
|
||||
if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil {
|
||||
if err := hugofs.Os.MkdirAll(genmandir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -110,7 +114,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
|
||||
// For bash-completion
|
||||
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
@@ -132,7 +136,7 @@ url: %s
|
||||
long: `Generate Markdown documentation for the Hugo CLI.
|
||||
This command is, mostly, used to create up-to-date documentation
|
||||
of Hugo's command-line interface for https://gohugo.io/.
|
||||
|
||||
|
||||
It creates one Markdown file per command with front matter suitable
|
||||
for rendering in Hugo.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -146,7 +150,7 @@ url: %s
|
||||
}
|
||||
if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
|
||||
r.Println("Directory", gendocdir, "does not exist, creating...")
|
||||
if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil {
|
||||
if err := hugofs.Os.MkdirAll(gendocdir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -167,13 +171,12 @@ url: %s
|
||||
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
|
||||
// For bash-completion
|
||||
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var docsHelperTarget string
|
||||
@@ -186,25 +189,43 @@ url: %s
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
r.Println("Generate docs data to", docsHelperTarget)
|
||||
|
||||
targetFile := filepath.Join(docsHelperTarget, "docs.json")
|
||||
var buf bytes.Buffer
|
||||
jsonEnc := json.NewEncoder(&buf)
|
||||
|
||||
configProvider := func() docshelper.DocProvider {
|
||||
conf := hugolib.DefaultConfig()
|
||||
conf.CacheDir = "" // The default value does not make sense in the docs.
|
||||
defaultConfig := parser.LowerCaseCamelJSONMarshaller{Value: conf}
|
||||
return docshelper.DocProvider{"config": defaultConfig}
|
||||
}
|
||||
|
||||
docshelper.AddDocProviderFunc(configProvider)
|
||||
if err := jsonEnc.Encode(docshelper.GetDocProvider()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(docsHelperTarget, "docs.yaml")
|
||||
|
||||
f, err := os.Create(targetFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(docshelper.GetDocProvider()); err != nil {
|
||||
yamlEnc := yaml.NewEncoder(f)
|
||||
if err := yamlEnc.Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Println("Done!")
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
cmd.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir")
|
||||
},
|
||||
@@ -219,7 +240,6 @@ url: %s
|
||||
newDocsHelper(),
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type genCommand struct {
|
||||
@@ -243,6 +263,8 @@ func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [
|
||||
func (c *genCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "A collection of several useful generators."
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+11
-26
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -24,8 +23,6 @@ import (
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@@ -79,22 +76,19 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
|
||||
// Flags with a different name in the config.
|
||||
keyMap := map[string]string{
|
||||
"minify": "minifyOutput",
|
||||
"destination": "publishDir",
|
||||
"printI18nWarnings": "logI18nWarnings",
|
||||
"printPathWarnings": "logPathWarnings",
|
||||
"editor": "newContentEditor",
|
||||
"minify": "minifyOutput",
|
||||
"destination": "publishDir",
|
||||
"editor": "newContentEditor",
|
||||
}
|
||||
|
||||
// Flags that we for some reason don't want to expose in the site config.
|
||||
internalKeySet := map[string]bool{
|
||||
"quiet": true,
|
||||
"verbose": true,
|
||||
"watch": true,
|
||||
"disableLiveReload": true,
|
||||
"liveReloadPort": true,
|
||||
"renderToMemory": true,
|
||||
"clock": true,
|
||||
"quiet": true,
|
||||
"verbose": true,
|
||||
"watch": true,
|
||||
"liveReloadPort": true,
|
||||
"renderToMemory": true,
|
||||
"clock": true,
|
||||
}
|
||||
|
||||
cmd := cd.CobraCommand
|
||||
@@ -116,20 +110,11 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
})
|
||||
|
||||
return cfg
|
||||
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
p := filepath.Join(x...)
|
||||
err := os.MkdirAll(p, 0777) // before umask
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func touchFile(fs afero.Fs, filename string) {
|
||||
mkdir(filepath.Dir(filename))
|
||||
err := helpers.WriteToDisk(filename, bytes.NewReader([]byte{}), fs)
|
||||
err := os.MkdirAll(p, 0o777) // before umask
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -25,9 +25,9 @@ func init() {
|
||||
// This message to show to Windows users if Hugo is opened from explorer.exe
|
||||
cobra.MousetrapHelpText = `
|
||||
|
||||
Hugo is a command-line tool for generating static website.
|
||||
Hugo is a command-line tool for generating static websites.
|
||||
|
||||
You need to open PowerShell and run Hugo from there.
|
||||
|
||||
You need to open cmd.exe and run Hugo from there.
|
||||
|
||||
Visit https://gohugo.io/ for more information.`
|
||||
}
|
||||
|
||||
+79
-130
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -24,14 +24,18 @@ import (
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"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"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -41,9 +45,7 @@ import (
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/watcher"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/fsync"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
@@ -68,7 +70,6 @@ type hugoBuilder struct {
|
||||
onConfigLoaded func(reloaded bool) error
|
||||
|
||||
fastRenderMode bool
|
||||
buildWatch bool
|
||||
showErrorInBrowser bool
|
||||
|
||||
errState hugoBuilderErrState
|
||||
@@ -84,7 +85,6 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
|
||||
c.confmu.Lock()
|
||||
defer c.confmu.Unlock()
|
||||
fn(c.conf)
|
||||
|
||||
}
|
||||
|
||||
type hugoBuilderErrState struct {
|
||||
@@ -131,51 +131,17 @@ func (e *hugoBuilderErrState) wasErr() bool {
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) errCount() int {
|
||||
return int(c.r.logger.LogCounters().ErrorCounter.Count())
|
||||
return c.r.logger.LoggCount(logg.LevelError) + loggers.Log().LoggCount(logg.LevelError)
|
||||
}
|
||||
|
||||
// getDirList provides NewWatcher() with a list of directories to watch for changes.
|
||||
func (c *hugoBuilder) getDirList() ([]string, error) {
|
||||
var filenames []string
|
||||
|
||||
walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("walker: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
if fi.Name() == ".git" ||
|
||||
fi.Name() == "node_modules" || fi.Name() == "bower_components" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
h, err := c.hugo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
watchFiles := h.PathSpec.BaseFs.WatchDirs()
|
||||
for _, fi := range watchFiles {
|
||||
if !fi.IsDir() {
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
continue
|
||||
}
|
||||
|
||||
w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.r.logger, Info: fi, WalkFn: walkFn})
|
||||
if err := w.Walk(); err != nil {
|
||||
c.r.logger.Errorln("walker: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
filenames = helpers.UniqueStringsSorted(filenames)
|
||||
|
||||
return filenames, nil
|
||||
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) initCPUProfile() (func(), error) {
|
||||
@@ -363,7 +329,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa
|
||||
configFiles = conf.configs.LoadingInfo.ConfigFiles
|
||||
})
|
||||
|
||||
c.r.logger.Println("Watching for config changes in", strings.Join(configFiles, ", "))
|
||||
c.r.Println("Watching for config changes in", strings.Join(configFiles, ", "))
|
||||
for _, configFile := range configFiles {
|
||||
watcher.Add(configFile)
|
||||
configSet[configFile] = true
|
||||
@@ -418,25 +384,6 @@ func (c *hugoBuilder) build() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.r.printPathWarnings {
|
||||
hugofs.WalkFilesystems(h.Fs.PublishDir, func(fs afero.Fs) bool {
|
||||
if dfs, ok := fs.(hugofs.DuplicatesReporter); ok {
|
||||
dupes := dfs.ReportDuplicates()
|
||||
if dupes != "" {
|
||||
c.r.logger.Warnln("Duplicate target paths:", dupes)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if c.r.printUnusedTemplates {
|
||||
unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
|
||||
for _, unusedTemplate := range unusedTemplates {
|
||||
c.r.logger.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
h.PrintProcessingStats(os.Stdout)
|
||||
c.r.Println()
|
||||
}
|
||||
@@ -461,6 +408,7 @@ func (c *hugoBuilder) copyStatic() (map[string]uint64, error) {
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
|
||||
infol := c.r.logger.InfoCommand("static")
|
||||
publishDir := helpers.FilePathSeparator
|
||||
|
||||
if sourceFs.PublishFolder != "" {
|
||||
@@ -484,13 +432,13 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
|
||||
syncer.SrcFs = fs
|
||||
|
||||
if syncer.Delete {
|
||||
c.r.logger.Infoln("removing all files from destination that don't exist in static dirs")
|
||||
infol.Logf("removing all files from destination that don't exist in static dirs")
|
||||
|
||||
syncer.DeleteFilter = func(f os.FileInfo) bool {
|
||||
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
|
||||
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
|
||||
}
|
||||
}
|
||||
c.r.logger.Infoln("syncing static files to", publishDir)
|
||||
start := time.Now()
|
||||
|
||||
// because we are using a baseFs (to get the union right).
|
||||
// set sync src to root
|
||||
@@ -498,9 +446,10 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
loggers.TimeTrackf(infol, start, nil, "syncing static files to %s", publishDir)
|
||||
|
||||
// Sync runs Stat 3 times for every source file (which sounds much)
|
||||
numFiles := fs.statCounter / 3
|
||||
// Sync runs Stat 2 times for every source file.
|
||||
numFiles := fs.statCounter / 2
|
||||
|
||||
return numFiles, err
|
||||
}
|
||||
@@ -545,14 +494,13 @@ func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
|
||||
langCount map[string]uint64
|
||||
)
|
||||
|
||||
if !c.r.quiet {
|
||||
fmt.Println("Start building sites … ")
|
||||
fmt.Println(hugo.BuildVersionString())
|
||||
if terminal.IsTerminal(os.Stdout) {
|
||||
defer func() {
|
||||
fmt.Print(showCursor + clearLine)
|
||||
}()
|
||||
}
|
||||
c.r.logger.Println("Start building sites … ")
|
||||
c.r.logger.Println(hugo.BuildVersionString())
|
||||
c.r.logger.Println()
|
||||
if terminal.IsTerminal(os.Stdout) {
|
||||
defer func() {
|
||||
fmt.Print(showCursor + clearLine)
|
||||
}()
|
||||
}
|
||||
|
||||
copyStaticFunc := func() error {
|
||||
@@ -672,13 +620,31 @@ func (c *hugoBuilder) handleBuildErr(err error, msg string) {
|
||||
func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
staticSyncer *staticSyncer,
|
||||
evs []fsnotify.Event,
|
||||
configSet map[string]bool) {
|
||||
configSet map[string]bool,
|
||||
) {
|
||||
defer func() {
|
||||
c.errState.setWasErr(false)
|
||||
}()
|
||||
|
||||
var isHandled bool
|
||||
|
||||
// Filter out ghost events (from deleted, renamed directories).
|
||||
// This seems to be a bug in fsnotify, or possibly MacOS.
|
||||
var n int
|
||||
for _, ev := range evs {
|
||||
keep := true
|
||||
if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
|
||||
if _, err := os.Stat(ev.Name); err != nil {
|
||||
keep = false
|
||||
}
|
||||
}
|
||||
if keep {
|
||||
evs[n] = ev
|
||||
n++
|
||||
}
|
||||
}
|
||||
evs = evs[:n]
|
||||
|
||||
for _, ev := range evs {
|
||||
isConfig := configSet[ev.Name]
|
||||
configChangeType := configChangeConfig
|
||||
@@ -746,48 +712,25 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
return
|
||||
}
|
||||
|
||||
c.r.logger.Infoln("Received System Events:", evs)
|
||||
c.r.logger.Debugln("Received System Events:", evs)
|
||||
|
||||
staticEvents := []fsnotify.Event{}
|
||||
dynamicEvents := []fsnotify.Event{}
|
||||
|
||||
filtered := []fsnotify.Event{}
|
||||
h, err := c.hugo()
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("Error getting the Hugo object:", err)
|
||||
return
|
||||
}
|
||||
n = 0
|
||||
for _, ev := range evs {
|
||||
if h.ShouldSkipFileChangeEvent(ev) {
|
||||
continue
|
||||
}
|
||||
// Check the most specific first, i.e. files.
|
||||
contentMapped := h.ContentChanges.GetSymbolicLinkMappings(ev.Name)
|
||||
if len(contentMapped) > 0 {
|
||||
for _, mapped := range contentMapped {
|
||||
filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for any symbolic directory mapping.
|
||||
|
||||
dir, name := filepath.Split(ev.Name)
|
||||
|
||||
contentMapped = h.ContentChanges.GetSymbolicLinkMappings(dir)
|
||||
|
||||
if len(contentMapped) == 0 {
|
||||
filtered = append(filtered, ev)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, mapped := range contentMapped {
|
||||
mappedFilename := filepath.Join(mapped, name)
|
||||
filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
|
||||
}
|
||||
evs[n] = ev
|
||||
n++
|
||||
}
|
||||
|
||||
evs = filtered
|
||||
evs = evs[:n]
|
||||
|
||||
for _, ev := range evs {
|
||||
ext := filepath.Ext(ev.Name)
|
||||
@@ -808,6 +751,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
if istemp {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.Deps.SourceSpec.IgnoreFile(ev.Name) {
|
||||
continue
|
||||
}
|
||||
@@ -831,7 +775,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
continue
|
||||
}
|
||||
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
|
||||
if f.IsDir() {
|
||||
c.r.logger.Println("adding created directory to watchlist", path)
|
||||
if err := watcher.Add(path); err != nil {
|
||||
@@ -847,11 +791,10 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
}
|
||||
|
||||
// recursively add new directories to watch list
|
||||
// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
|
||||
if ev.Op&fsnotify.Create == fsnotify.Create {
|
||||
if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Rename) {
|
||||
c.withConf(func(conf *commonConfig) {
|
||||
if s, err := conf.fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
|
||||
_ = helpers.SymbolicWalk(conf.fs.Source, ev.Name, walkAdder)
|
||||
_ = helpers.Walk(conf.fs.Source, ev.Name, walkAdder)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -892,7 +835,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
return
|
||||
}
|
||||
path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
|
||||
path = h.RelURL(helpers.ToSlashTrimLeading(path), false)
|
||||
path = h.RelURL(paths.ToSlashTrimLeading(path), false)
|
||||
|
||||
livereload.RefreshPath(path)
|
||||
} else {
|
||||
@@ -929,7 +872,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
// Nothing has changed.
|
||||
return
|
||||
} else if len(changed) == 1 {
|
||||
pathToRefresh := h.PathSpec.RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
|
||||
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(changed[0]), false)
|
||||
livereload.RefreshPath(pathToRefresh)
|
||||
} else {
|
||||
livereload.ForceRefresh()
|
||||
@@ -964,7 +907,6 @@ func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) {
|
||||
var err error
|
||||
h, err = c.r.HugFromConfig(conf)
|
||||
return err
|
||||
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -990,14 +932,28 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
cfg := config.New()
|
||||
cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk))
|
||||
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
|
||||
if c.r.environment != "" {
|
||||
cfg.Set("environment", c.r.environment)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
cfg.Set("environment", c.r.environment)
|
||||
|
||||
cfg.Set("internal", maps.Params{
|
||||
"running": running,
|
||||
"watch": watch,
|
||||
"verbose": c.r.verbose,
|
||||
"verbose": c.r.isVerbose(),
|
||||
})
|
||||
|
||||
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
@@ -1005,6 +961,11 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
return err
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
c.conf = conf
|
||||
if c.onConfigLoaded != nil {
|
||||
if err := c.onConfigLoaded(false); err != nil {
|
||||
@@ -1013,15 +974,16 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
var rebuildCounter atomic.Uint64
|
||||
|
||||
func (c *hugoBuilder) printChangeDetected(typ string) {
|
||||
msg := "\nChange"
|
||||
if typ != "" {
|
||||
msg += " of " + typ
|
||||
}
|
||||
msg += " detected, rebuilding site."
|
||||
msg += fmt.Sprintf(" detected, rebuilding site (#%d).", rebuildCounter.Add(1))
|
||||
|
||||
c.r.logger.Println(msg)
|
||||
const layout = "2006-01-02 15:04:05.000 -0700"
|
||||
@@ -1036,25 +998,12 @@ func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
|
||||
}
|
||||
}
|
||||
c.errState.setBuildErr(nil)
|
||||
visited := c.visitedURLs.PeekAllSet()
|
||||
h, err := c.hugo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.fastRenderMode {
|
||||
c.withConf(func(conf *commonConfig) {
|
||||
// Make sure we always render the home pages
|
||||
for _, l := range conf.configs.Languages {
|
||||
langPath := h.GetLangSubDir(l.Lang)
|
||||
if langPath != "" {
|
||||
langPath = langPath + "/"
|
||||
}
|
||||
home := h.PrependBasePath("/"+langPath, false)
|
||||
visited[home] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) reloadConfig() error {
|
||||
|
||||
+17
-21
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -19,12 +19,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -59,7 +57,7 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
}
|
||||
return c.importFromJekyll(args)
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory")
|
||||
},
|
||||
},
|
||||
@@ -67,7 +65,6 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
}
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
type importCommand struct {
|
||||
@@ -97,6 +94,7 @@ func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
|
||||
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -299,7 +297,7 @@ func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate t
|
||||
}
|
||||
|
||||
func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error {
|
||||
jww.TRACE.Println("Converting", path)
|
||||
log.Println("Converting", path)
|
||||
|
||||
filename := filepath.Base(path)
|
||||
postDate, postName, err := c.parseJekyllFilename(filename)
|
||||
@@ -308,11 +306,11 @@ func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft
|
||||
return nil
|
||||
}
|
||||
|
||||
jww.TRACE.Println(filename, postDate, postName)
|
||||
log.Println(filename, postDate, postName)
|
||||
|
||||
targetFile := filepath.Join(targetDir, relPath)
|
||||
targetParentDir := filepath.Dir(targetFile)
|
||||
os.MkdirAll(targetParentDir, 0777)
|
||||
os.MkdirAll(targetParentDir, 0o777)
|
||||
|
||||
contentBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -367,7 +365,7 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl
|
||||
if _, ok := jekyllPostDirs[entry.Name()]; !ok {
|
||||
err = hugio.CopyDir(fs, sfp, dfp, nil)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
c.r.logger.Errorln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,7 +386,7 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl
|
||||
if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' {
|
||||
err = hugio.CopyFile(fs, sfp, dfp)
|
||||
if err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
c.r.logger.Errorln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,7 +396,6 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl
|
||||
}
|
||||
|
||||
func (c *importCommand) importFromJekyll(args []string) error {
|
||||
|
||||
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
return newUserError("path error:", args[0])
|
||||
@@ -429,11 +426,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
c.r.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
callback := func(path string, fi hugofs.FileMetaInfo) error {
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -462,16 +455,19 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
|
||||
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
|
||||
if hasAnyPostInDir {
|
||||
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
|
||||
if err = helpers.Walk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
|
||||
c.r.Println("Now, start Hugo by yourself:\n" +
|
||||
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
|
||||
c.r.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
|
||||
c.r.Println("Now, start Hugo by yourself:\n")
|
||||
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("hugo server")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+6
-10
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
|
||||
// newListCommand creates a new list command and its subcommands.
|
||||
func newListCommand() *listCommand {
|
||||
|
||||
createRecord := func(workingDir string, p page.Page) []string {
|
||||
return []string{
|
||||
filepath.ToSlash(strings.TrimPrefix(p.File().Filename(), workingDir+string(os.PathSeparator))),
|
||||
@@ -83,7 +82,6 @@ func newListCommand() *listCommand {
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
return &listCommand{
|
||||
@@ -94,11 +92,10 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the drafts in your content directory.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !p.Draft() || p.File().IsZero() {
|
||||
if !p.Draft() || p.File() == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildDrafts", true,
|
||||
@@ -113,11 +110,10 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the posts in your content directory which will be posted in the future.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !resource.IsFuture(p) || p.File().IsZero() {
|
||||
if !resource.IsFuture(p) || p.File() == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildFuture", true,
|
||||
@@ -131,7 +127,7 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the posts in your content directory which has already expired.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !resource.IsExpired(p) || p.File().IsZero() {
|
||||
if !resource.IsExpired(p) || p.File() == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -148,14 +144,13 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
return !p.File().IsZero()
|
||||
return p.File() != nil
|
||||
}
|
||||
return list(cd, r, shouldInclude, "buildDrafts", true, "buildFuture", true, "buildExpired", true)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type listCommand struct {
|
||||
@@ -182,6 +177,7 @@ func (c *listCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
|
||||
List requires a subcommand, e.g. hugo list drafts`
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+29
-10
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -61,12 +61,15 @@ This command is marked as 'Experimental'. We think it's a great idea, so it's no
|
||||
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.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -85,6 +88,9 @@ so this may/will change in future versions of Hugo.
|
||||
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
|
||||
inside a subfolder on GitHub, as one example.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
@@ -101,7 +107,8 @@ so this may/will change in future versions of Hugo.
|
||||
name: "verify",
|
||||
short: "Verify dependencies.",
|
||||
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -119,7 +126,8 @@ so this may/will change in future versions of Hugo.
|
||||
long: `Print a module dependency graph with information about module status (disabled, vendored).
|
||||
Note that for vendored modules, that is the version listed and not the one from go.mod.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -135,7 +143,8 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
name: "clean",
|
||||
short: "Delete the Hugo Module cache for the current project.",
|
||||
long: `Delete the Hugo Module cache for the current project.`,
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
|
||||
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
|
||||
},
|
||||
@@ -157,6 +166,9 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
&simpleCommand{
|
||||
name: "tidy",
|
||||
short: "Remove unused entries in go.mod and go.sum.",
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
@@ -171,6 +183,9 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
long: `Vendor all module dependencies into the _vendor directory.
|
||||
If a module is vendored, that is where Hugo will look for it's dependencies.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
@@ -196,20 +211,25 @@ Install a specific version:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
|
||||
|
||||
Install the latest versions of all module dependencies:
|
||||
Install the latest versions of all direct module dependencies:
|
||||
|
||||
hugo mod get
|
||||
hugo mod get ./... (recursive)
|
||||
|
||||
Install the latest versions of all module dependencies (direct and indirect):
|
||||
|
||||
hugo mod get -u
|
||||
hugo mod get -u ./... (recursive)
|
||||
|
||||
Run "go help get" for more information. All flags available for "go get" is also relevant here.
|
||||
` + commonUsageMod,
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.DisableFlagParsing = true
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
// We currently just pass on the flags we get to Go and
|
||||
// need to do the flag handling manually.
|
||||
if len(args) == 1 && args[0] == "-h" {
|
||||
if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") {
|
||||
return errHelp
|
||||
}
|
||||
|
||||
@@ -266,7 +286,6 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
npmCommand,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type modCommands struct {
|
||||
@@ -288,7 +307,7 @@ func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//config := conf.configs.Base
|
||||
// config := conf.configs.Base
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+53
-210
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,19 +16,14 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/gohugoio/hugo/create/skeletons"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -36,6 +31,7 @@ func newNewCommand() *newCommand {
|
||||
var (
|
||||
force bool
|
||||
contentType string
|
||||
format string
|
||||
)
|
||||
|
||||
var c *newCommand
|
||||
@@ -46,16 +42,16 @@ func newNewCommand() *newCommand {
|
||||
use: "content [path]",
|
||||
short: "Create new content for your site",
|
||||
long: `Create a new content file and automatically set the date and title.
|
||||
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.
|
||||
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
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.
|
||||
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("path needs to be provided")
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
@@ -63,10 +59,11 @@ func newNewCommand() *newCommand {
|
||||
}
|
||||
return create.NewContent(h, contentType, args[0], force)
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
|
||||
cmd.Flags().String("editor", "", "edit new content with this editor, if provided")
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite file if it already exists")
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -78,7 +75,7 @@ The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("path needs to be provided")
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
createpath, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
@@ -95,152 +92,49 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
}
|
||||
sourceFs := conf.fs.Source
|
||||
|
||||
archeTypePath := filepath.Join(createpath, "archetypes")
|
||||
dirs := []string{
|
||||
archeTypePath,
|
||||
filepath.Join(createpath, "assets"),
|
||||
filepath.Join(createpath, "content"),
|
||||
filepath.Join(createpath, "data"),
|
||||
filepath.Join(createpath, "layouts"),
|
||||
filepath.Join(createpath, "static"),
|
||||
filepath.Join(createpath, "themes"),
|
||||
err = skeletons.CreateSite(createpath, sourceFs, force, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists, _ := helpers.Exists(createpath, sourceFs); exists {
|
||||
if isDir, _ := helpers.IsDir(createpath, sourceFs); !isDir {
|
||||
return errors.New(createpath + " already exists but not a directory")
|
||||
}
|
||||
|
||||
isEmpty, _ := helpers.IsEmpty(createpath, sourceFs)
|
||||
|
||||
switch {
|
||||
case !isEmpty && !force:
|
||||
return errors.New(createpath + " already exists and is not empty. See --force.")
|
||||
|
||||
case !isEmpty && force:
|
||||
all := append(dirs, filepath.Join(createpath, "hugo."+r.format))
|
||||
for _, path := range all {
|
||||
if exists, _ := helpers.Exists(path, sourceFs); exists {
|
||||
return errors.New(path + " already exists")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := sourceFs.MkdirAll(dir, 0777); err != nil {
|
||||
return fmt.Errorf("failed to create dir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.newSiteCreateConfig(sourceFs, createpath, r.format)
|
||||
|
||||
// Create a default archetype file.
|
||||
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
|
||||
strings.NewReader(create.DefaultArchetypeTemplateTemplate), sourceFs)
|
||||
|
||||
r.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", createpath)
|
||||
r.Println(c.newSiteNextStepsText())
|
||||
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
|
||||
r.Println(c.newSiteNextStepsText(createpath, format))
|
||||
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVar(&force, "force", false, "init inside non-empty directory")
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "init inside non-empty directory")
|
||||
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "theme",
|
||||
use: "theme [path]",
|
||||
short: "Create a new site (skeleton)",
|
||||
long: `Create a new site in the provided directory.
|
||||
The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
use: "theme [name]",
|
||||
short: "Create a new theme (skeleton)",
|
||||
long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
according to your needs.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
cfg := config.New()
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps := h.PathSpec
|
||||
sourceFs := ps.Fs.Source
|
||||
themesDir := h.Configs.LoadingInfo.BaseConfig.ThemesDir
|
||||
createpath := ps.AbsPathify(filepath.Join(themesDir, args[0]))
|
||||
r.Println("Creating theme at", createpath)
|
||||
sourceFs := conf.fs.Source
|
||||
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
|
||||
r.Println("Creating new theme in", createpath)
|
||||
|
||||
if x, _ := helpers.Exists(createpath, sourceFs); x {
|
||||
return errors.New(createpath + " already exists")
|
||||
}
|
||||
|
||||
for _, filename := range []string{
|
||||
"index.html",
|
||||
"404.html",
|
||||
"_default/list.html",
|
||||
"_default/single.html",
|
||||
"partials/head.html",
|
||||
"partials/header.html",
|
||||
"partials/footer.html",
|
||||
} {
|
||||
touchFile(sourceFs, filepath.Join(createpath, "layouts", filename))
|
||||
}
|
||||
|
||||
baseofDefault := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
{{- partial "head.html" . -}}
|
||||
<body>
|
||||
{{- partial "header.html" . -}}
|
||||
<div id="content">
|
||||
{{- block "main" . }}{{- end }}
|
||||
</div>
|
||||
{{- partial "footer.html" . -}}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), sourceFs)
|
||||
err = skeletons.CreateTheme(createpath, sourceFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mkdir(createpath, "archetypes")
|
||||
|
||||
archDefault := []byte("+++\n+++\n")
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), sourceFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mkdir(createpath, "static", "js")
|
||||
mkdir(createpath, "static", "css")
|
||||
|
||||
by := []byte(`The MIT License (MIT)
|
||||
|
||||
Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
`)
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), sourceFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.createThemeMD(ps.Fs.Source, createpath)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -248,7 +142,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
}
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
type newCommand struct {
|
||||
@@ -280,6 +173,8 @@ You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your site.`
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -288,77 +183,25 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *newCommand) newSiteCreateConfig(fs afero.Fs, inpath string, kind string) (err error) {
|
||||
in := map[string]string{
|
||||
"baseURL": "http://example.org/",
|
||||
"title": "My New Hugo Site",
|
||||
"languageCode": "en-us",
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+kind), &buf, fs)
|
||||
}
|
||||
|
||||
func (c *newCommand) newSiteNextStepsText() string {
|
||||
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
|
||||
format = strings.ToLower(format)
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
|
||||
nextStepsText.WriteString(`Just a few more steps...
|
||||
|
||||
1. Download a theme into the same-named folder.
|
||||
Choose a theme from https://themes.gohugo.io/ or
|
||||
create your own with the "hugo new theme <THEMENAME>" command.
|
||||
2. Perhaps you want to add some content. You can add single files
|
||||
with "hugo new `)
|
||||
1. Change the current directory to ` + path + `.
|
||||
2. Create or install a theme:
|
||||
- Create a new theme with the command "hugo new theme <THEMENAME>"
|
||||
- Install a theme from https://themes.gohugo.io/
|
||||
3. Edit hugo.` + format + `, setting the "theme" property to the theme name.
|
||||
4. Create new content with the command "hugo new content `)
|
||||
|
||||
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
|
||||
|
||||
nextStepsText.WriteString(`".
|
||||
3. Start the built-in live server via "hugo server".
|
||||
5. Start the embedded web server with the command "hugo server --buildDrafts".
|
||||
|
||||
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
|
||||
See documentation at https://gohugo.io/.`)
|
||||
|
||||
return nextStepsText.String()
|
||||
}
|
||||
|
||||
func (c *newCommand) createThemeMD(fs afero.Fs, inpath string) (err error) {
|
||||
|
||||
by := []byte(`# theme.toml template for a Hugo theme
|
||||
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
|
||||
|
||||
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
|
||||
license = "MIT"
|
||||
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
|
||||
description = ""
|
||||
homepage = "http://example.com/"
|
||||
tags = []
|
||||
features = []
|
||||
min_version = "0.112.0"
|
||||
|
||||
[author]
|
||||
name = ""
|
||||
homepage = ""
|
||||
|
||||
# If porting an existing theme
|
||||
[original]
|
||||
name = ""
|
||||
homepage = ""
|
||||
repo = ""
|
||||
`)
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(inpath, "hugo.toml"), strings.NewReader("# Theme config.\n"), fs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
// Note: This is a command only meant for internal use and must be run
|
||||
// via "go run -tags release main.go release" on the actual code base that is in the release.
|
||||
func newReleaseCommand() simplecobra.Commander {
|
||||
|
||||
var (
|
||||
step int
|
||||
skipPush bool
|
||||
@@ -42,7 +41,7 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
|
||||
return rel.Run()
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
|
||||
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
|
||||
|
||||
+203
-83
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,27 +16,30 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bep/mclib"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
@@ -55,13 +58,13 @@ import (
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/fsync"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
var (
|
||||
logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
|
||||
logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`)
|
||||
logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`)
|
||||
)
|
||||
@@ -79,10 +82,14 @@ const (
|
||||
)
|
||||
|
||||
func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
|
||||
var visitedURLs *types.EvictingStringQueue
|
||||
if s != nil && !s.disableFastRender {
|
||||
visitedURLs = types.NewEvictingStringQueue(20)
|
||||
}
|
||||
return &hugoBuilder{
|
||||
r: r,
|
||||
s: s,
|
||||
visitedURLs: types.NewEvictingStringQueue(100),
|
||||
visitedURLs: visitedURLs,
|
||||
fullRebuildSem: semaphore.NewWeighted(1),
|
||||
debounce: debounce.New(4 * time.Second),
|
||||
onConfigLoaded: func(reloaded bool) error {
|
||||
@@ -97,13 +104,37 @@ func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(rel
|
||||
}
|
||||
|
||||
func newServerCommand() *serverCommand {
|
||||
var c *serverCommand
|
||||
c = &serverCommand{
|
||||
// Flags.
|
||||
var uninstall bool
|
||||
|
||||
c := &serverCommand{
|
||||
quit: make(chan bool),
|
||||
commands: []simplecobra.Commander{
|
||||
&simpleCommand{
|
||||
name: "trust",
|
||||
short: "Install the local CA in the system trust store.",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
action := "-install"
|
||||
if uninstall {
|
||||
action = "-uninstall"
|
||||
}
|
||||
os.Args = []string{action}
|
||||
return mclib.RunMain()
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVar(&uninstall, "uninstall", false, "Uninstall the local CA (but do not delete it).")
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *serverCommand) Commands() []simplecobra.Commander {
|
||||
return c.commands
|
||||
}
|
||||
|
||||
type countingStatFs struct {
|
||||
afero.Fs
|
||||
statCounter uint64
|
||||
@@ -190,7 +221,7 @@ func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
|
||||
}
|
||||
|
||||
type fileServer struct {
|
||||
baseURLs []string
|
||||
baseURLs []urls.BaseURL
|
||||
roots []string
|
||||
errorTemplate func(err any) (io.Reader, error)
|
||||
c *serverCommand
|
||||
@@ -204,7 +235,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
listener := f.c.serverPorts[i].ln
|
||||
logger := f.c.r.logger
|
||||
|
||||
r.Printf("Environment: %q", f.c.hugoTry().Deps.Site.Hugo().Environment)
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
|
||||
|
||||
if i == 0 {
|
||||
if f.c.renderToDisk {
|
||||
@@ -226,12 +257,6 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
r.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
|
||||
}
|
||||
|
||||
// We're only interested in the path
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, nil, "", "", fmt.Errorf("invalid baseURL: %w", err)
|
||||
}
|
||||
|
||||
decorate := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if f.c.showErrorInBrowser {
|
||||
@@ -251,7 +276,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
port = lrport
|
||||
}
|
||||
})
|
||||
lr := *u
|
||||
lr := baseURL.URL()
|
||||
lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port)
|
||||
fmt.Fprint(w, injectLiveReloadScript(r, lr))
|
||||
|
||||
@@ -282,13 +307,13 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
// This matches Netlify's behaviour and is needed for SPA behaviour.
|
||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||
if !redirect.Force {
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path()))
|
||||
if root != "" {
|
||||
path = filepath.Join(root, path)
|
||||
}
|
||||
var fs afero.Fs
|
||||
f.c.withConf(func(conf *commonConfig) {
|
||||
fs = conf.fs.PublishDir
|
||||
fs = conf.fs.PublishDirServer
|
||||
})
|
||||
|
||||
fi, err := fs.Stat(path)
|
||||
@@ -309,7 +334,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
switch redirect.Status {
|
||||
case 404:
|
||||
w.WriteHeader(404)
|
||||
file, err := fs.Open(strings.TrimPrefix(redirect.To, u.Path))
|
||||
file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path()))
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
@@ -318,7 +343,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
}
|
||||
return
|
||||
case 200:
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil {
|
||||
requestURI = redirect.To
|
||||
r = r2
|
||||
}
|
||||
@@ -356,10 +381,10 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
|
||||
fileserver := decorate(http.FileServer(fs))
|
||||
mu := http.NewServeMux()
|
||||
if u.Path == "" || u.Path == "/" {
|
||||
if baseURL.Path() == "" || baseURL.Path() == "/" {
|
||||
mu.Handle("/", fileserver)
|
||||
} else {
|
||||
mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
|
||||
mu.Handle(baseURL.Path(), http.StripPrefix(baseURL.Path(), fileserver))
|
||||
}
|
||||
if r.IsTestRun() {
|
||||
var shutDownOnce sync.Once
|
||||
@@ -372,7 +397,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
|
||||
endpoint := net.JoinHostPort(f.c.serverInterface, strconv.Itoa(port))
|
||||
|
||||
return mu, listener, u.String(), endpoint, nil
|
||||
return mu, listener, baseURL.String(), endpoint, nil
|
||||
}
|
||||
|
||||
func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
|
||||
@@ -423,6 +448,9 @@ type serverCommand struct {
|
||||
navigateToChanged bool
|
||||
serverAppend bool
|
||||
serverInterface string
|
||||
tlsCertFile string
|
||||
tlsKeyFile string
|
||||
tlsAuto bool
|
||||
serverPort int
|
||||
liveReloadPort int
|
||||
serverWatch bool
|
||||
@@ -432,24 +460,11 @@ type serverCommand struct {
|
||||
disableBrowserError bool
|
||||
}
|
||||
|
||||
func (c *serverCommand) Commands() []simplecobra.Commander {
|
||||
return c.commands
|
||||
}
|
||||
|
||||
func (c *serverCommand) Name() string {
|
||||
return "server"
|
||||
}
|
||||
|
||||
func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
err := func() error {
|
||||
defer c.r.timeTrack(time.Now(), "Built")
|
||||
err := c.build()
|
||||
return err
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch runs its own server as part of the routine
|
||||
if c.serverWatch {
|
||||
|
||||
@@ -472,6 +487,15 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
defer c.r.timeTrack(time.Now(), "Built")
|
||||
err := c.build()
|
||||
return err
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.serve()
|
||||
}
|
||||
|
||||
@@ -480,8 +504,6 @@ func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd.Short = "A high performance webserver"
|
||||
cmd.Long = `Hugo provides its own webserver which builds and serves the site.
|
||||
While hugo server is high performance, it is a webserver with limited options.
|
||||
Many run it in production, but the standard behavior is for people to use it
|
||||
in development and use a more full featured server such as Nginx or Caddy.
|
||||
|
||||
'hugo server' will avoid writing the rendered and served content to disk,
|
||||
preferring to store it in memory.
|
||||
@@ -495,6 +517,9 @@ of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Flags().IntVarP(&c.serverPort, "port", "p", 1313, "port on which the server will listen")
|
||||
cmd.Flags().IntVar(&c.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
|
||||
cmd.Flags().StringVarP(&c.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
|
||||
cmd.Flags().StringVarP(&c.tlsCertFile, "tlsCertFile", "", "", "path to TLS certificate file")
|
||||
cmd.Flags().StringVarP(&c.tlsKeyFile, "tlsKeyFile", "", "", "path to TLS key file")
|
||||
cmd.Flags().BoolVar(&c.tlsAuto, "tlsAuto", false, "generate and use locally-trusted certificates.")
|
||||
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().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
|
||||
@@ -508,8 +533,11 @@ of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Flags().String("memstats", "", "log memory usage to this file")
|
||||
cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
|
||||
|
||||
cmd.Flags().SetAnnotation("tlsCertFile", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.Flags().SetAnnotation("tlsKeyFile", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
r := cd.Root.Command.(*rootCommand)
|
||||
applyLocalBuildFlags(cmd, r)
|
||||
applyLocalFlagsBuild(cmd, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -525,7 +553,14 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
if err := c.createServerPorts(cd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (c.tlsCertFile == "" || c.tlsKeyFile == "") && c.tlsAuto {
|
||||
c.withConfE(func(conf *commonConfig) error {
|
||||
return c.createCertificates(conf)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.setBaseURLsInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -546,9 +581,6 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.doLiveReload = !c.disableLiveReload
|
||||
c.fastRenderMode = !c.disableFastRender
|
||||
c.showErrorInBrowser = c.doLiveReload && !c.disableBrowserError
|
||||
if c.r.environment == "" {
|
||||
c.r.environment = hugo.EnvironmentDevelopment
|
||||
}
|
||||
|
||||
if c.fastRenderMode {
|
||||
// For now, fast render mode only. It should, however, be fast enough
|
||||
@@ -614,8 +646,8 @@ func (c *serverCommand) getErrorWithContext() any {
|
||||
|
||||
m := make(map[string]any)
|
||||
|
||||
//xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors())))
|
||||
m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors())))
|
||||
m["Error"] = cleanErrorLog(c.r.logger.Errors())
|
||||
|
||||
m["Version"] = hugo.BuildVersionString()
|
||||
ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.errState.buildErr())
|
||||
m["Files"] = ferrors
|
||||
@@ -623,6 +655,77 @@ func (c *serverCommand) getErrorWithContext() any {
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *serverCommand) createCertificates(conf *commonConfig) error {
|
||||
hostname := "localhost"
|
||||
if c.r.baseURL != "" {
|
||||
u, err := url.Parse(c.r.baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostname = u.Hostname()
|
||||
}
|
||||
|
||||
// For now, store these in the Hugo cache dir.
|
||||
// Hugo should probably introduce some concept of a less temporary application directory.
|
||||
keyDir := filepath.Join(conf.configs.LoadingInfo.BaseConfig.CacheDir, "_mkcerts")
|
||||
|
||||
// Create the directory if it doesn't exist.
|
||||
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(keyDir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.tlsCertFile = filepath.Join(keyDir, fmt.Sprintf("%s.pem", hostname))
|
||||
c.tlsKeyFile = filepath.Join(keyDir, fmt.Sprintf("%s-key.pem", hostname))
|
||||
|
||||
// Check if the certificate already exists and is valid.
|
||||
certPEM, err := os.ReadFile(c.tlsCertFile)
|
||||
if err == nil {
|
||||
rootPem, err := os.ReadFile(filepath.Join(mclib.GetCAROOT(), "rootCA.pem"))
|
||||
if err == nil {
|
||||
if err := c.verifyCert(rootPem, certPEM, hostname); err == nil {
|
||||
c.r.Println("Using existing", c.tlsCertFile, "and", c.tlsKeyFile)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.r.Println("Creating TLS certificates in", keyDir)
|
||||
|
||||
// Yes, this is unfortunate, but it's currently the only way to use Mkcert as a library.
|
||||
os.Args = []string{"-cert-file", c.tlsCertFile, "-key-file", c.tlsKeyFile, hostname}
|
||||
return mclib.RunMain()
|
||||
}
|
||||
|
||||
func (c *serverCommand) verifyCert(rootPEM, certPEM []byte, name string) error {
|
||||
roots := x509.NewCertPool()
|
||||
ok := roots.AppendCertsFromPEM(rootPEM)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to parse root certificate")
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return fmt.Errorf("failed to parse certificate PEM")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate: %v", err.Error())
|
||||
}
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: name,
|
||||
Roots: roots,
|
||||
}
|
||||
|
||||
if _, err := cert.Verify(opts); err != nil {
|
||||
return fmt.Errorf("failed to verify certificate: %v", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
|
||||
flags := cd.CobraCommand.Flags()
|
||||
var cerr error
|
||||
@@ -665,36 +768,40 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
|
||||
|
||||
// fixURL massages the baseURL into a form needed for serving
|
||||
// all pages correctly.
|
||||
func (c *serverCommand) fixURL(baseURL, s string, port int) (string, error) {
|
||||
func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port int) (string, error) {
|
||||
certsSet := (c.tlsCertFile != "" && c.tlsKeyFile != "") || c.tlsAuto
|
||||
useLocalhost := false
|
||||
if s == "" {
|
||||
s = baseURL
|
||||
baseURL := baseURLFromFlag
|
||||
if baseURL == "" {
|
||||
baseURL = baseURLFromConfig
|
||||
useLocalhost = true
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(s, "/") {
|
||||
s = s + "/"
|
||||
if !strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL + "/"
|
||||
}
|
||||
|
||||
// do an initial parse of the input string
|
||||
u, err := url.Parse(s)
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// if no Host is defined, then assume that no schema or double-slash were
|
||||
// present in the url. Add a double-slash and make a best effort attempt.
|
||||
if u.Host == "" && s != "/" {
|
||||
s = "//" + s
|
||||
if u.Host == "" && baseURL != "/" {
|
||||
baseURL = "//" + baseURL
|
||||
|
||||
u, err = url.Parse(s)
|
||||
u, err = url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if useLocalhost {
|
||||
if u.Scheme == "https" {
|
||||
if certsSet {
|
||||
u.Scheme = "https"
|
||||
} else if u.Scheme == "https" {
|
||||
u.Scheme = "http"
|
||||
}
|
||||
u.Host = "localhost"
|
||||
@@ -718,9 +825,9 @@ func (c *serverCommand) partialReRender(urls ...string) error {
|
||||
c.errState.setWasErr(false)
|
||||
}()
|
||||
c.errState.setBuildErr(nil)
|
||||
visited := make(map[string]bool)
|
||||
visited := types.NewEvictingStringQueue(len(urls))
|
||||
for _, url := range urls {
|
||||
visited[url] = true
|
||||
visited.Add(url)
|
||||
}
|
||||
|
||||
h, err := c.hugo()
|
||||
@@ -733,7 +840,7 @@ func (c *serverCommand) partialReRender(urls ...string) error {
|
||||
|
||||
func (c *serverCommand) serve() error {
|
||||
var (
|
||||
baseURLs []string
|
||||
baseURLs []urls.BaseURL
|
||||
roots []string
|
||||
h *hugolib.HugoSites
|
||||
)
|
||||
@@ -745,20 +852,22 @@ func (c *serverCommand) serve() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need the server to share the same logger as the Hugo build (for error counts etc.)
|
||||
c.r.logger = h.Log
|
||||
|
||||
if isMultiHost {
|
||||
for _, l := range conf.configs.ConfigLangs() {
|
||||
baseURLs = append(baseURLs, l.BaseURL().String())
|
||||
baseURLs = append(baseURLs, l.BaseURL())
|
||||
roots = append(roots, l.Language().Lang)
|
||||
}
|
||||
} else {
|
||||
l := conf.configs.GetFirstLanguageConfig()
|
||||
baseURLs = []string{l.BaseURL().String()}
|
||||
baseURLs = []urls.BaseURL{l.BaseURL()}
|
||||
roots = []string{""}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -811,24 +920,36 @@ func (c *serverCommand) serve() error {
|
||||
|
||||
for i := range baseURLs {
|
||||
mu, listener, serverURL, endpoint, err := srv.createEndpoint(i)
|
||||
srv := &http.Server{
|
||||
Addr: endpoint,
|
||||
Handler: mu,
|
||||
var srv *http.Server
|
||||
if c.tlsCertFile != "" && c.tlsKeyFile != "" {
|
||||
srv = &http.Server{
|
||||
Addr: endpoint,
|
||||
Handler: mu,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
srv = &http.Server{
|
||||
Addr: endpoint,
|
||||
Handler: mu,
|
||||
}
|
||||
}
|
||||
|
||||
servers = append(servers, srv)
|
||||
|
||||
if doLiveReload {
|
||||
u, err := url.Parse(helpers.SanitizeURL(baseURLs[i]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
||||
baseURL := baseURLs[i]
|
||||
mu.HandleFunc(baseURL.Path()+"livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc(baseURL.Path()+"livereload", livereload.Handler)
|
||||
}
|
||||
c.r.Printf("Web Server is available at %s (bind address %s)\n", serverURL, c.serverInterface)
|
||||
c.r.Printf("Web Server is available at %s (bind address %s) %s\n", serverURL, c.serverInterface, roots[i])
|
||||
wg1.Go(func() error {
|
||||
err = srv.Serve(listener)
|
||||
if c.tlsCertFile != "" && c.tlsKeyFile != "" {
|
||||
err = srv.ServeTLS(listener, c.tlsCertFile, c.tlsKeyFile)
|
||||
} else {
|
||||
err = srv.Serve(listener)
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
@@ -839,8 +960,12 @@ func (c *serverCommand) serve() error {
|
||||
if c.r.IsTestRun() {
|
||||
// Write a .ready file to disk to signal ready status.
|
||||
// This is where the test is run from.
|
||||
var baseURLs []string
|
||||
for _, baseURL := range srv.baseURLs {
|
||||
baseURLs = append(baseURLs, baseURL.String())
|
||||
}
|
||||
testInfo := map[string]any{
|
||||
"baseURLs": srv.baseURLs,
|
||||
"baseURLs": baseURLs,
|
||||
}
|
||||
|
||||
dir := os.Getenv("WORK")
|
||||
@@ -851,7 +976,7 @@ func (c *serverCommand) serve() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(readyFile, b, 0777)
|
||||
err = os.WriteFile(readyFile, b, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -934,8 +1059,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
}
|
||||
})
|
||||
|
||||
// prevent spamming the log on changes
|
||||
logger := helpers.NewDistinctErrorLogger()
|
||||
logger := s.c.r.logger
|
||||
|
||||
for _, ev := range staticEvents {
|
||||
// Due to our approach of layering both directories and the content's rendered output
|
||||
@@ -1036,7 +1160,7 @@ func cleanErrorLog(content string) string {
|
||||
return strings.Join(keep, ": ")
|
||||
}
|
||||
|
||||
func injectLiveReloadScript(src io.Reader, baseURL url.URL) string {
|
||||
func injectLiveReloadScript(src io.Reader, baseURL *url.URL) string {
|
||||
var b bytes.Buffer
|
||||
chain := transform.Chain{livereloadinject.New(baseURL)}
|
||||
chain.Apply(&b, src)
|
||||
@@ -1074,10 +1198,6 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func removeErrorPrefixFromLog(content string) string {
|
||||
return logErrorRe.ReplaceAllLiteralString(content, "")
|
||||
}
|
||||
|
||||
func formatByteCount(b uint64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright 2023 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 commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newSimpleTemplateCommand() simplecobra.Commander {
|
||||
return &simpleCommand{
|
||||
name: "template",
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command) {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func newTemplateCommand() *templateCommand {
|
||||
return &templateCommand{
|
||||
commands: []simplecobra.Commander{},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type templateCommand struct {
|
||||
r *rootCommand
|
||||
|
||||
commands []simplecobra.Commander
|
||||
}
|
||||
|
||||
func (c *templateCommand) Commands() []simplecobra.Commander {
|
||||
return c.commands
|
||||
}
|
||||
|
||||
func (c *templateCommand) Name() string {
|
||||
return "template"
|
||||
}
|
||||
|
||||
func (c *templateCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("templateCommand.Run", conf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *templateCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Print the site configuration"
|
||||
cmd.Long = `Print the site configuration, both default and custom settings.`
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *templateCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
return nil
|
||||
}
|
||||
@@ -22,29 +22,64 @@ import (
|
||||
// If length of from is one and the only element is a slice of same type as to,
|
||||
// it will be appended.
|
||||
func Append(to any, from ...any) (any, error) {
|
||||
if len(from) == 0 {
|
||||
return to, nil
|
||||
}
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
var tot reflect.Type
|
||||
|
||||
if !toIsNil {
|
||||
if tov.Kind() == reflect.Slice {
|
||||
// Create a copy of tov, so we don't modify the original.
|
||||
c := reflect.MakeSlice(tov.Type(), tov.Len(), tov.Len()+len(from))
|
||||
reflect.Copy(c, tov)
|
||||
tov = c
|
||||
}
|
||||
|
||||
if tov.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("expected a slice, got %T", to)
|
||||
}
|
||||
|
||||
tot = tov.Type().Elem()
|
||||
if tot.Kind() == reflect.Slice {
|
||||
totvt := tot.Elem()
|
||||
fromvs := make([]reflect.Value, len(from))
|
||||
for i, f := range from {
|
||||
fromv := reflect.ValueOf(f)
|
||||
fromt := fromv.Type()
|
||||
if fromt.Kind() == reflect.Slice {
|
||||
fromt = fromt.Elem()
|
||||
}
|
||||
if totvt != fromt {
|
||||
return nil, fmt.Errorf("cannot append slice of %s to slice of %s", fromt, totvt)
|
||||
} else {
|
||||
fromvs[i] = fromv
|
||||
}
|
||||
}
|
||||
return reflect.Append(tov, fromvs...).Interface(), nil
|
||||
|
||||
}
|
||||
|
||||
toIsNil = tov.Len() == 0
|
||||
|
||||
if len(from) == 1 {
|
||||
fromv := reflect.ValueOf(from[0])
|
||||
if !fromv.IsValid() {
|
||||
// from[0] is nil
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
}
|
||||
fromt := fromv.Type()
|
||||
if fromt.Kind() == reflect.Slice {
|
||||
fromt = fromt.Elem()
|
||||
}
|
||||
if fromv.Kind() == reflect.Slice {
|
||||
if toIsNil {
|
||||
// If we get nil []string, we just return the []string
|
||||
return from[0], nil
|
||||
}
|
||||
|
||||
fromt := reflect.TypeOf(from[0]).Elem()
|
||||
|
||||
// If we get []string []string, we append the from slice to to
|
||||
if tot == fromt {
|
||||
return reflect.AppendSlice(tov, fromv).Interface(), nil
|
||||
@@ -52,6 +87,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +98,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
|
||||
for _, f := range from {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.Type().AssignableTo(tot) {
|
||||
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
tov, _ := indirect(reflect.ValueOf(to))
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
@@ -77,6 +113,10 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
var tos []any
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
if !slice.IsValid() {
|
||||
tos = append(tos, nil)
|
||||
continue
|
||||
}
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
for i, test := range []struct {
|
||||
start any
|
||||
addend []any
|
||||
expected any
|
||||
@@ -74,6 +74,9 @@ func TestAppend(t *testing.T) {
|
||||
[]any{"c"},
|
||||
false,
|
||||
},
|
||||
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
|
||||
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
|
||||
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
@@ -85,6 +88,59 @@ func TestAppend(t *testing.T) {
|
||||
}
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.DeepEquals, test.expected)
|
||||
c.Assert(result, qt.DeepEquals, test.expected, qt.Commentf("test: [%d] %v", i, test))
|
||||
}
|
||||
}
|
||||
|
||||
// #11093
|
||||
func TestAppendToMultiDimensionalSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
for _, test := range []struct {
|
||||
to any
|
||||
from []any
|
||||
expected any
|
||||
}{
|
||||
{[][]string{{"a", "b"}},
|
||||
[]any{[]string{"c", "d"}},
|
||||
[][]string{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
},
|
||||
{[][]string{{"a", "b"}},
|
||||
[]any{[]string{"c", "d"}, []string{"e", "f"}},
|
||||
[][]string{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
{"e", "f"},
|
||||
},
|
||||
},
|
||||
{[][]string{{"a", "b"}},
|
||||
[]any{[]int{1, 2}},
|
||||
false,
|
||||
},
|
||||
} {
|
||||
result, err := Append(test.to, test.from...)
|
||||
if b, ok := test.expected.(bool); ok && !b {
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
} else {
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(result, qt.DeepEquals, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
slice := make([]string, 0, 100)
|
||||
slice = append(slice, "a", "b")
|
||||
result, err := Append(slice, "c")
|
||||
c.Assert(err, qt.IsNil)
|
||||
slice[0] = "d"
|
||||
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
|
||||
}
|
||||
|
||||
@@ -16,10 +16,28 @@ package constants
|
||||
// Error IDs.
|
||||
// Do not change these values.
|
||||
const (
|
||||
ErrIDAmbigousDisableKindTaxonomy = "error-disable-taxonomy"
|
||||
ErrIDAmbigousOutputKindTaxonomy = "error-output-taxonomy"
|
||||
|
||||
// IDs for remote errors in tpl/data.
|
||||
ErrRemoteGetJSON = "error-remote-getjson"
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
)
|
||||
|
||||
// Field/method names with special meaning.
|
||||
const (
|
||||
FieldRelPermalink = "RelPermalink"
|
||||
FieldPermalink = "Permalink"
|
||||
)
|
||||
|
||||
// IsFieldRelOrPermalink returns whether the given name is a RelPermalink or Permalink.
|
||||
func IsFieldRelOrPermalink(name string) bool {
|
||||
return name == FieldRelPermalink || name == FieldPermalink
|
||||
}
|
||||
|
||||
// Resource transformations.
|
||||
const (
|
||||
ResourceTransformationFingerprint = "fingerprint"
|
||||
)
|
||||
|
||||
// IsResourceTransformationLinkChange returns whether the given name is a resource transformation that changes the permalink based on the content.
|
||||
func IsResourceTransformationPermalinkHash(name string) bool {
|
||||
return name == ResourceTransformationFingerprint
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hcontext
|
||||
|
||||
import "context"
|
||||
|
||||
// ContextDispatcher is a generic interface for setting and getting values from a context.
|
||||
type ContextDispatcher[T any] interface {
|
||||
Set(ctx context.Context, value T) context.Context
|
||||
Get(ctx context.Context) T
|
||||
}
|
||||
|
||||
// NewContextDispatcher creates a new ContextDispatcher with the given key.
|
||||
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
|
||||
return keyInContext[T, R]{
|
||||
id: key,
|
||||
}
|
||||
}
|
||||
|
||||
type keyInContext[T any, R comparable] struct {
|
||||
zero T
|
||||
id R
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Get(ctx context.Context) T {
|
||||
v := ctx.Value(f.id)
|
||||
if v == nil {
|
||||
return f.zero
|
||||
}
|
||||
return v.(T)
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
|
||||
return context.WithValue(ctx, f.id, value)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -33,7 +33,7 @@ type LineMatcher struct {
|
||||
}
|
||||
|
||||
// LineMatcherFn is used to match a line with an error.
|
||||
// It returns the column number or 0 if the line was found, but column could not be determinde. Returns -1 if no line match.
|
||||
// It returns the column number or 0 if the line was found, but column could not be determined. Returns -1 if no line match.
|
||||
type LineMatcherFn func(m LineMatcher) int
|
||||
|
||||
// SimpleLineMatcher simply matches by line number.
|
||||
@@ -74,7 +74,6 @@ func ContainsMatcher(text string) func(m LineMatcher) int {
|
||||
// ErrorContext contains contextual information about an error. This will
|
||||
// typically be the lines surrounding some problem in a file.
|
||||
type ErrorContext struct {
|
||||
|
||||
// If a match will contain the matched line and up to 2 lines before and after.
|
||||
// Will be empty if no match.
|
||||
Lines []string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
+42
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,14 +15,13 @@
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PrintStackTrace prints the current stacktrace to w.
|
||||
@@ -49,21 +48,52 @@ func Recover(args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetGID the current goroutine id. Used only for debugging.
|
||||
func GetGID() uint64 {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||
b = b[:bytes.IndexByte(b, ' ')]
|
||||
n, _ := strconv.ParseUint(string(b), 10, 64)
|
||||
return n
|
||||
// IsTimeoutError returns true if the given error is or contains a TimeoutError.
|
||||
func IsTimeoutError(err error) bool {
|
||||
return errors.Is(err, &TimeoutError{})
|
||||
}
|
||||
|
||||
type TimeoutError struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func (e *TimeoutError) Error() string {
|
||||
return fmt.Sprintf("timeout after %s", e.Duration)
|
||||
}
|
||||
|
||||
func (e *TimeoutError) Is(target error) bool {
|
||||
_, ok := target.(*TimeoutError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
|
||||
func IsFeatureNotAvailableError(err error) bool {
|
||||
return errors.Is(err, &FeatureNotAvailableError{})
|
||||
}
|
||||
|
||||
// ErrFeatureNotAvailable denotes that a feature is unavailable.
|
||||
//
|
||||
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
|
||||
// and this error is used to signal those situations.
|
||||
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
|
||||
var ErrFeatureNotAvailable = &FeatureNotAvailableError{Cause: errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")}
|
||||
|
||||
// FeatureNotAvailableError is an error type used to signal that a feature is not available.
|
||||
type FeatureNotAvailableError struct {
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *FeatureNotAvailableError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func (e *FeatureNotAvailableError) Error() string {
|
||||
return e.Cause.Error()
|
||||
}
|
||||
|
||||
func (e *FeatureNotAvailableError) Is(target error) bool {
|
||||
_, ok := target.(*FeatureNotAvailableError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Must panics if err != nil.
|
||||
func Must(err error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,7 @@
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -34,3 +35,11 @@ func TestIsNotExist(t *testing.T) {
|
||||
// os.IsNotExist returns false for wrapped errors.
|
||||
c.Assert(IsNotExist(fmt.Errorf("foo: %w", afero.ErrFileNotFound)), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestIsFeatureNotAvailableError(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(IsFeatureNotAvailableError(ErrFeatureNotAvailable), qt.Equals, true)
|
||||
c.Assert(IsFeatureNotAvailableError(&FeatureNotAvailableError{}), qt.Equals, true)
|
||||
c.Assert(IsFeatureNotAvailableError(errors.New("asdf")), qt.Equals, false)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,19 +15,20 @@ package herrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bep/godartsass"
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/bep/golibsass/libsass/libsasserrors"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/tdewolff/parse/v2"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
// FileError represents an error when handling a file: Parsing a config file,
|
||||
@@ -45,6 +46,9 @@ type FileError interface {
|
||||
|
||||
// UpdateContent updates the error with a new ErrorContext from the content of the file.
|
||||
UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError
|
||||
|
||||
// SetFilename sets the filename of the error.
|
||||
SetFilename(filename string) FileError
|
||||
}
|
||||
|
||||
// Unwrapper can unwrap errors created with fmt.Errorf.
|
||||
@@ -57,6 +61,11 @@ var (
|
||||
_ Unwrapper = (*fileError)(nil)
|
||||
)
|
||||
|
||||
func (fe *fileError) SetFilename(filename string) FileError {
|
||||
fe.position.Filename = filename
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *fileError) UpdatePosition(pos text.Position) FileError {
|
||||
oldFilename := fe.Position().Filename
|
||||
if pos.Filename != "" && fe.fileType == "" {
|
||||
@@ -112,7 +121,6 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
|
||||
}
|
||||
|
||||
return fe
|
||||
|
||||
}
|
||||
|
||||
type fileError struct {
|
||||
@@ -145,6 +153,8 @@ func (e *fileError) causeString() string {
|
||||
// Avoid repeating the file info in the error message.
|
||||
case godartsass.SassError:
|
||||
return v.Message
|
||||
case godartsassv1.SassError:
|
||||
return v.Message
|
||||
case libsasserrors.Error:
|
||||
return v.Message
|
||||
default:
|
||||
@@ -176,7 +186,6 @@ func NewFileErrorFromName(err error, name string) FileError {
|
||||
}
|
||||
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
|
||||
}
|
||||
|
||||
// NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err.
|
||||
@@ -187,7 +196,6 @@ func NewFileErrorFromPos(err error, pos text.Position) FileError {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
|
||||
}
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
|
||||
}
|
||||
|
||||
func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
|
||||
@@ -244,7 +252,6 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
}); ok {
|
||||
realFilename = s.Filename()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
f, err2 := fs.Open(filename)
|
||||
@@ -292,7 +299,7 @@ func extractFileTypePos(err error) (string, text.Position) {
|
||||
}
|
||||
|
||||
// The error type from the minifier contains line number and column number.
|
||||
if line, col := exctractLineNumberAndColumnNumber(err); line >= 0 {
|
||||
if line, col := extractLineNumberAndColumnNumber(err); line >= 0 {
|
||||
pos.LineNumber = line
|
||||
pos.ColumnNumber = col
|
||||
return fileType, pos
|
||||
@@ -364,7 +371,7 @@ func extractOffsetAndType(e error) (int, string) {
|
||||
}
|
||||
}
|
||||
|
||||
func exctractLineNumberAndColumnNumber(e error) (int, int) {
|
||||
func extractLineNumberAndColumnNumber(e error) (int, int) {
|
||||
switch v := e.(type) {
|
||||
case *parse.Error:
|
||||
return v.Line, v.Column
|
||||
@@ -385,6 +392,13 @@ func extractPosition(e error) (pos text.Position) {
|
||||
pos.Filename = filename
|
||||
pos.Offset = start.Offset
|
||||
pos.ColumnNumber = start.Column
|
||||
case godartsassv1.SassError:
|
||||
span := v.Span
|
||||
start := span.Start
|
||||
filename, _ := paths.UrlToFilename(span.Url)
|
||||
pos.Filename = filename
|
||||
pos.Offset = start.Offset
|
||||
pos.ColumnNumber = start.Column
|
||||
case libsasserrors.Error:
|
||||
pos.Filename = v.File
|
||||
pos.LineNumber = v.Line
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,12 +14,11 @@
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -48,7 +47,6 @@ func TestNewFileError(t *testing.T) {
|
||||
c.Assert(errorContext.Lines, qt.DeepEquals, []string{"line 30", "line 31", "line 32", "line 33", "line 34"})
|
||||
c.Assert(errorContext.LinesPos, qt.Equals, 2)
|
||||
c.Assert(errorContext.ChromaLexer, qt.Equals, "go-html-template")
|
||||
|
||||
}
|
||||
|
||||
func TestNewFileErrorExtractFromMessage(t *testing.T) {
|
||||
|
||||
@@ -118,7 +118,7 @@ func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
|
||||
return exec.Command(bin, arg...), nil
|
||||
}
|
||||
|
||||
// Exec encorces a security policy for commands run via os/exec.
|
||||
// Exec enforces a security policy for commands run via os/exec.
|
||||
type Exec struct {
|
||||
sc security.Config
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
@@ -188,6 +189,20 @@ func IsTime(tp reflect.Type) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValid returns whether v is not nil and a valid value.
|
||||
func IsValid(v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return !v.IsNil()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AsTime returns v as a time.Time if possible.
|
||||
// The given location is only used if the value implements AsTimeProvider (e.g. go-toml local).
|
||||
// A zero Time and false is returned if this isn't possible.
|
||||
@@ -217,7 +232,7 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
|
||||
panic("not supported")
|
||||
}
|
||||
first := tp.In(0)
|
||||
if first.Implements(ContextInterface) {
|
||||
if IsContextType(first) {
|
||||
args = append(args, reflect.ValueOf(cxt))
|
||||
}
|
||||
}
|
||||
@@ -236,4 +251,24 @@ func indirectInterface(v reflect.Value) reflect.Value {
|
||||
return v.Elem()
|
||||
}
|
||||
|
||||
var ContextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
var isContextCache = maps.NewCache[reflect.Type, bool]()
|
||||
|
||||
type k string
|
||||
|
||||
var contextTypeValue = reflect.TypeOf(context.WithValue(context.Background(), k("key"), 32))
|
||||
|
||||
// IsContextType returns whether tp is a context.Context type.
|
||||
func IsContextType(tp reflect.Type) bool {
|
||||
if tp == contextTypeValue {
|
||||
return true
|
||||
}
|
||||
if tp == contextInterface {
|
||||
return true
|
||||
}
|
||||
|
||||
return isContextCache.GetOrCreate(tp, func() bool {
|
||||
return tp.Implements(contextInterface)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -40,6 +41,42 @@ func TestGetMethodByName(t *testing.T) {
|
||||
c.Assert(GetMethodIndexByName(tp, "Foo"), qt.Equals, -1)
|
||||
}
|
||||
|
||||
func TestIsContextType(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
type k string
|
||||
ctx := context.Background()
|
||||
valueCtx := context.WithValue(ctx, k("key"), 32)
|
||||
c.Assert(IsContextType(reflect.TypeOf(ctx)), qt.IsTrue)
|
||||
c.Assert(IsContextType(reflect.TypeOf(valueCtx)), qt.IsTrue)
|
||||
}
|
||||
|
||||
func BenchmarkIsContextType(b *testing.B) {
|
||||
type k string
|
||||
b.Run("value", func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
ctxs := make([]reflect.Type, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxs[i]) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("background", func(b *testing.B) {
|
||||
var ctxt reflect.Type = reflect.TypeOf(context.Background())
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxt) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +15,9 @@ package hstrings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
)
|
||||
@@ -55,3 +57,73 @@ func EqualAny(a string, b ...string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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)}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if v == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InSlicEqualFold checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
// It uses strings.EqualFold to compare.
|
||||
func InSlicEqualFold(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if strings.EqualFold(v, el) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Tuple struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,7 @@
|
||||
package hstrings
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -32,5 +33,24 @@ func TestStringEqualFold(t *testing.T) {
|
||||
c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false)
|
||||
c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
func TestGetOrCompileRegexp(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
re, err := GetOrCompileRegexp(`\d+`)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(re.MatchString("123"), qt.Equals, true)
|
||||
}
|
||||
|
||||
func BenchmarkGetOrCompileRegexp(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetOrCompileRegexp(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompileRegexp(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
regexp.MustCompile(`\d+`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package htime_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
// Issue #11267
|
||||
func TestApplyWithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
defaultContentLanguage = 'it'
|
||||
-- layouts/index.html --
|
||||
{{ $dates := slice
|
||||
"2022-01-03"
|
||||
"2022-02-01"
|
||||
"2022-03-02"
|
||||
"2022-04-07"
|
||||
"2022-05-06"
|
||||
"2022-06-04"
|
||||
"2022-07-03"
|
||||
"2022-08-01"
|
||||
"2022-09-06"
|
||||
"2022-10-05"
|
||||
"2022-11-03"
|
||||
"2022-12-02"
|
||||
}}
|
||||
{{ range $dates }}
|
||||
{{ . | time.Format "month: _January_ weekday: _Monday_" }}
|
||||
{{ . | time.Format "month: _Jan_ weekday: _Mon_" }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
month: _gennaio_ weekday: _lunedì_
|
||||
month: _gen_ weekday: _lun_
|
||||
month: _febbraio_ weekday: _martedì_
|
||||
month: _feb_ weekday: _mar_
|
||||
month: _marzo_ weekday: _mercoledì_
|
||||
month: _mar_ weekday: _mer_
|
||||
month: _aprile_ weekday: _giovedì_
|
||||
month: _apr_ weekday: _gio_
|
||||
month: _maggio_ weekday: _venerdì_
|
||||
month: _mag_ weekday: _ven_
|
||||
month: _giugno_ weekday: _sabato_
|
||||
month: _giu_ weekday: _sab_
|
||||
month: _luglio_ weekday: _domenica_
|
||||
month: _lug_ weekday: _dom_
|
||||
month: _agosto_ weekday: _lunedì_
|
||||
month: _ago_ weekday: _lun_
|
||||
month: _settembre_ weekday: _martedì_
|
||||
month: _set_ weekday: _mar_
|
||||
month: _ottobre_ weekday: _mercoledì_
|
||||
month: _ott_ weekday: _mer_
|
||||
month: _novembre_ weekday: _giovedì_
|
||||
month: _nov_ weekday: _gio_
|
||||
month: _dicembre_ weekday: _venerdì_
|
||||
month: _dic_ weekday: _ven_
|
||||
`)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/bep/clocks"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
@@ -75,7 +75,7 @@ var (
|
||||
"December",
|
||||
}
|
||||
|
||||
Clock = clock.System()
|
||||
Clock = clocks.System()
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
@@ -124,12 +124,15 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
monthIdx := t.Month() - 1 // Month() starts at 1.
|
||||
dayIdx := t.Weekday()
|
||||
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
|
||||
if !strings.Contains(s, f.ltr.MonthWide(t.Month())) {
|
||||
if strings.Contains(layout, "January") {
|
||||
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
|
||||
} else if strings.Contains(layout, "Jan") {
|
||||
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
|
||||
}
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
|
||||
if !strings.Contains(s, f.ltr.WeekdayWide(t.Weekday())) {
|
||||
|
||||
if strings.Contains(layout, "Monday") {
|
||||
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
|
||||
} else if strings.Contains(layout, "Mon") {
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package hugio
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
iofs "io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -60,12 +61,16 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool
|
||||
return fmt.Errorf("%q is not a directory", from)
|
||||
}
|
||||
|
||||
err = fs.MkdirAll(to, 0777) // before umask
|
||||
err = fs.MkdirAll(to, 0o777) // before umask
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, _ := afero.ReadDir(fs, from)
|
||||
d, err := fs.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, _ := d.(iofs.ReadDirFile).ReadDir(-1)
|
||||
for _, entry := range entries {
|
||||
fromFilename := filepath.Join(from, entry.Name())
|
||||
toFilename := filepath.Join(to, entry.Name())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package hugio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
@@ -57,3 +58,22 @@ func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekerNoOpCloser {
|
||||
func NewReadSeekerNoOpCloserFromString(content string) ReadSeekerNoOpCloser {
|
||||
return ReadSeekerNoOpCloser{strings.NewReader(content)}
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
|
||||
// from the given bytes slice.
|
||||
func NewReadSeekerNoOpCloserFromBytes(content []byte) ReadSeekerNoOpCloser {
|
||||
return ReadSeekerNoOpCloser{bytes.NewReader(content)}
|
||||
}
|
||||
|
||||
// NewReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
|
||||
// The ReadSeeker will be seeked to the beginning before returned.
|
||||
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
|
||||
return func() (ReadSeekCloser, error) {
|
||||
r.Seek(0, io.SeekStart)
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
// OpenReadSeekCloser allows setting some other way (than reading from a filesystem)
|
||||
// to open or create a ReadSeekCloser.
|
||||
type OpenReadSeekCloser func() (ReadSeekCloser, error)
|
||||
|
||||
+118
-12
@@ -24,12 +24,19 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/godartsass"
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
"github.com/bep/logg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
iofs "io/fs"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
)
|
||||
@@ -74,10 +81,22 @@ 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
|
||||
}
|
||||
@@ -95,6 +114,7 @@ func (i HugoInfo) Deps() []*Dependency {
|
||||
// ConfigProvider represents the config options that are relevant for HugoInfo.
|
||||
type ConfigProvider interface {
|
||||
Environment() string
|
||||
Running() bool
|
||||
WorkingDir() string
|
||||
}
|
||||
|
||||
@@ -141,7 +161,12 @@ func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []strin
|
||||
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir))
|
||||
|
||||
if fs != nil {
|
||||
fis, err := afero.ReadDir(fs, files.FolderJSConfig)
|
||||
var fis []iofs.DirEntry
|
||||
d, err := fs.Open(files.FolderJSConfig)
|
||||
if err == nil {
|
||||
fis, err = d.(iofs.ReadDirFile).ReadDir(-1)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
for _, fi := range fis {
|
||||
key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
|
||||
@@ -166,8 +191,10 @@ type buildInfo struct {
|
||||
*debug.BuildInfo
|
||||
}
|
||||
|
||||
var bInfo *buildInfo
|
||||
var bInfoInit sync.Once
|
||||
var (
|
||||
bInfo *buildInfo
|
||||
bInfoInit sync.Once
|
||||
)
|
||||
|
||||
func getBuildInfo() *buildInfo {
|
||||
bInfoInit.Do(func() {
|
||||
@@ -194,7 +221,6 @@ func getBuildInfo() *buildInfo {
|
||||
bInfo.GoArch = s.Value
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return bInfo
|
||||
@@ -233,12 +259,15 @@ func GetDependencyListNonGo() []string {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.2.4"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
|
||||
)
|
||||
}
|
||||
|
||||
if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" {
|
||||
const dartSassPath = "github.com/sass/dart-sass-embedded"
|
||||
dartSassPath := "github.com/sass/dart-sass-embedded"
|
||||
if IsDartSassV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
deps = append(deps,
|
||||
formatDep(dartSassPath+"/protocol", dartSass.ProtocolVersion),
|
||||
formatDep(dartSassPath+"/compiler", dartSass.CompilerVersion),
|
||||
@@ -283,11 +312,88 @@ type Dependency struct {
|
||||
}
|
||||
|
||||
func dartSassVersion() godartsass.DartSassVersion {
|
||||
// This is also duplicated in the dartsass package.
|
||||
const dartSassEmbeddedBinaryName = "dart-sass-embedded"
|
||||
if !hexec.InPath(dartSassEmbeddedBinaryName) {
|
||||
if DartSassBinaryName == "" {
|
||||
return godartsass.DartSassVersion{}
|
||||
}
|
||||
v, _ := godartsass.Version(dartSassEmbeddedBinaryName)
|
||||
return v
|
||||
if IsDartSassV2() {
|
||||
v, _ := godartsass.Version(DartSassBinaryName)
|
||||
return v
|
||||
}
|
||||
|
||||
v, _ := godartsassv1.Version(DartSassBinaryName)
|
||||
var vv godartsass.DartSassVersion
|
||||
mapstructure.WeakDecode(v, &vv)
|
||||
return vv
|
||||
}
|
||||
|
||||
// DartSassBinaryName is the name of the Dart Sass binary to use.
|
||||
// TODO(beop) find a better place for this.
|
||||
var DartSassBinaryName string
|
||||
|
||||
func init() {
|
||||
DartSassBinaryName = os.Getenv("DART_SASS_BINARY")
|
||||
if DartSassBinaryName == "" {
|
||||
for _, name := range dartSassBinaryNamesV2 {
|
||||
if hexec.InPath(name) {
|
||||
DartSassBinaryName = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if DartSassBinaryName == "" {
|
||||
if hexec.InPath(dartSassBinaryNameV1) {
|
||||
DartSassBinaryName = dartSassBinaryNameV1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
dartSassBinaryNameV1 = "dart-sass-embedded"
|
||||
dartSassBinaryNamesV2 = []string{"dart-sass", "sass"}
|
||||
)
|
||||
|
||||
func IsDartSassV2() bool {
|
||||
return !strings.Contains(DartSassBinaryName, "embedded")
|
||||
}
|
||||
|
||||
// Deprecate informs about a deprecation starting at the given version.
|
||||
//
|
||||
// A deprecation typically needs a simple change in the template, but doing so will make the template incompatible with older versions.
|
||||
// Theme maintainers generally want
|
||||
// 1. No warnings or errors in the console when building a Hugo site.
|
||||
// 2. Their theme to work for at least the last few Hugo versions.
|
||||
func Deprecate(item, alternative string, version string) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
DeprecateLevel(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// DeprecateLevel informs about a deprecation logging at the given level.
|
||||
func DeprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
|
||||
} else {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
|
||||
}
|
||||
|
||||
loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf(msg)
|
||||
}
|
||||
|
||||
// We ususally do about one minor version a month.
|
||||
// We want people to run at least the current and previous version without any warnings.
|
||||
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
|
||||
func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
from := MustParseVersion(ver)
|
||||
to := CurrentVersion
|
||||
minorDiff := to.Minor - from.Minor
|
||||
switch {
|
||||
case minorDiff >= 12:
|
||||
// Start failing the build after about a year.
|
||||
return logg.LevelError
|
||||
case minorDiff >= 6:
|
||||
// Start printing warnings after about six months.
|
||||
return logg.LevelWarn
|
||||
default:
|
||||
return logg.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork"}
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
hugoInfo := NewInfo(conf, nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
@@ -38,15 +39,34 @@ func TestHugoInfo(t *testing.T) {
|
||||
}
|
||||
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"}, nil)
|
||||
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)
|
||||
|
||||
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
|
||||
ver := CurrentVersion
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 1
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 6
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
|
||||
ver.Minor -= 6
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
environment string
|
||||
running bool
|
||||
workingDir string
|
||||
}
|
||||
|
||||
@@ -54,6 +74,10 @@ func (c testConfig) Environment() string {
|
||||
return c.environment
|
||||
}
|
||||
|
||||
func (c testConfig) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c testConfig) WorkingDir() string {
|
||||
return c.workingDir
|
||||
}
|
||||
|
||||
@@ -67,8 +67,11 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
v := MustParseVersion(h.String())
|
||||
return compareVersions(v, other)
|
||||
return compareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
return MustParseVersion(h.String())
|
||||
}
|
||||
|
||||
// Eq implements the compare.Eqer interface.
|
||||
@@ -264,7 +267,6 @@ func compareFloatWithVersion(v1 float64, v2 Version) int {
|
||||
|
||||
if v1maj > v2.Major {
|
||||
return 1
|
||||
|
||||
}
|
||||
|
||||
if v1maj < v2.Major {
|
||||
@@ -276,7 +278,6 @@ func compareFloatWithVersion(v1 float64, v2 Version) int {
|
||||
}
|
||||
|
||||
return -1
|
||||
|
||||
}
|
||||
|
||||
func GoMinorVersion() int {
|
||||
|
||||
@@ -17,7 +17,7 @@ package hugo
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 112,
|
||||
Minor: 123,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 loggers contains some basic logging setup.
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// levelColor mapping.
|
||||
var levelColor = [...]*color.Color{
|
||||
logg.LevelTrace: color.New(color.FgWhite),
|
||||
logg.LevelDebug: color.New(color.FgWhite),
|
||||
logg.LevelInfo: color.New(color.FgBlue),
|
||||
logg.LevelWarn: color.New(color.FgYellow),
|
||||
logg.LevelError: color.New(color.FgRed),
|
||||
}
|
||||
|
||||
// levelString mapping.
|
||||
var levelString = [...]string{
|
||||
logg.LevelTrace: "TRACE",
|
||||
logg.LevelDebug: "DEBUG",
|
||||
logg.LevelInfo: "INFO ",
|
||||
logg.LevelWarn: "WARN ",
|
||||
logg.LevelError: "ERROR",
|
||||
}
|
||||
|
||||
// newDefaultHandler handler.
|
||||
func newDefaultHandler(outWriter, errWriter io.Writer) logg.Handler {
|
||||
return &defaultHandler{
|
||||
outWriter: outWriter,
|
||||
errWriter: errWriter,
|
||||
Padding: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Default Handler implementation.
|
||||
// Based on https://github.com/apex/log/blob/master/handlers/cli/cli.go
|
||||
type defaultHandler struct {
|
||||
mu sync.Mutex
|
||||
outWriter io.Writer // Defaults to os.Stdout.
|
||||
errWriter io.Writer // Defaults to os.Stderr.
|
||||
|
||||
Padding int
|
||||
}
|
||||
|
||||
// HandleLog implements logg.Handler.
|
||||
func (h *defaultHandler) HandleLog(e *logg.Entry) error {
|
||||
color := levelColor[e.Level]
|
||||
level := levelString[e.Level]
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var w io.Writer
|
||||
if e.Level > logg.LevelInfo {
|
||||
w = h.errWriter
|
||||
} else {
|
||||
w = h.outWriter
|
||||
}
|
||||
|
||||
var prefix string
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameCmd {
|
||||
prefix = fmt.Sprint(field.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
prefix = prefix + ": "
|
||||
}
|
||||
|
||||
color.Fprintf(w, "%s %s%s", fmt.Sprintf("%*s", h.Padding+1, level), color.Sprint(prefix), e.Message)
|
||||
|
||||
for _, field := range e.Fields {
|
||||
if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s %v", color.Sprint(field.Name), field.Value)
|
||||
}
|
||||
|
||||
fmt.Fprintln(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
)
|
||||
|
||||
// PanicOnWarningHook panics on warnings.
|
||||
var PanicOnWarningHook = func(e *logg.Entry) error {
|
||||
if e.Level != logg.LevelWarn {
|
||||
return nil
|
||||
}
|
||||
panic(e.Message)
|
||||
}
|
||||
|
||||
func newLogLevelCounter() *logLevelCounter {
|
||||
return &logLevelCounter{
|
||||
counters: make(map[logg.Level]int),
|
||||
}
|
||||
}
|
||||
|
||||
func newLogOnceHandler(threshold logg.Level) *logOnceHandler {
|
||||
return &logOnceHandler{
|
||||
threshold: threshold,
|
||||
seen: make(map[uint64]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func newStopHandler(h ...logg.Handler) *stopHandler {
|
||||
return &stopHandler{
|
||||
handlers: h,
|
||||
}
|
||||
}
|
||||
|
||||
func newSuppressStatementsHandler(statements map[string]bool) *suppressStatementsHandler {
|
||||
return &suppressStatementsHandler{
|
||||
statements: statements,
|
||||
}
|
||||
}
|
||||
|
||||
type logLevelCounter struct {
|
||||
mu sync.RWMutex
|
||||
counters map[logg.Level]int
|
||||
}
|
||||
|
||||
func (h *logLevelCounter) HandleLog(e *logg.Entry) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.counters[e.Level]++
|
||||
return nil
|
||||
}
|
||||
|
||||
var errStop = fmt.Errorf("stop")
|
||||
|
||||
type logOnceHandler struct {
|
||||
threshold logg.Level
|
||||
mu sync.Mutex
|
||||
seen map[uint64]bool
|
||||
}
|
||||
|
||||
func (h *logOnceHandler) HandleLog(e *logg.Entry) error {
|
||||
if e.Level < h.threshold {
|
||||
// We typically only want to enable this for warnings and above.
|
||||
// The common use case is that many go routines may log the same error.
|
||||
return nil
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
hash := identity.HashUint64(e.Level, e.Message, e.Fields)
|
||||
if h.seen[hash] {
|
||||
return errStop
|
||||
}
|
||||
h.seen[hash] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *logOnceHandler) reset() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.seen = make(map[uint64]bool)
|
||||
}
|
||||
|
||||
type stopHandler struct {
|
||||
handlers []logg.Handler
|
||||
}
|
||||
|
||||
// HandleLog implements logg.Handler.
|
||||
func (h *stopHandler) HandleLog(e *logg.Entry) error {
|
||||
for _, handler := range h.handlers {
|
||||
if err := handler.HandleLog(e); err != nil {
|
||||
if err == errStop {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type suppressStatementsHandler struct {
|
||||
statements map[string]bool
|
||||
}
|
||||
|
||||
func (h *suppressStatementsHandler) HandleLog(e *logg.Entry) error {
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameStatementID {
|
||||
if h.statements[field.Value.(string)] {
|
||||
return errStop
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// whiteSpaceTrimmer creates a new log handler that trims whitespace from log messages and string fields.
|
||||
func whiteSpaceTrimmer() logg.Handler {
|
||||
return logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
e.Message = strings.TrimSpace(e.Message)
|
||||
for i, field := range e.Fields {
|
||||
if s, ok := field.Value.(string); ok {
|
||||
e.Fields[i].Value = strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
// newNoColoursHandler creates a new NoColoursHandler
|
||||
func newNoColoursHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noColoursHandler {
|
||||
if predicate == nil {
|
||||
predicate = func(e *logg.Entry) bool { return true }
|
||||
}
|
||||
return &noColoursHandler{
|
||||
noLevelPrefix: noLevelPrefix,
|
||||
outWriter: outWriter,
|
||||
errWriter: errWriter,
|
||||
predicate: predicate,
|
||||
}
|
||||
}
|
||||
|
||||
type noColoursHandler struct {
|
||||
mu sync.Mutex
|
||||
outWriter io.Writer // Defaults to os.Stdout.
|
||||
errWriter io.Writer // Defaults to os.Stderr.
|
||||
predicate func(*logg.Entry) bool
|
||||
noLevelPrefix bool
|
||||
}
|
||||
|
||||
func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
|
||||
if !h.predicate(e) {
|
||||
return nil
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var w io.Writer
|
||||
if e.Level > logg.LevelInfo {
|
||||
w = h.errWriter
|
||||
} else {
|
||||
w = h.outWriter
|
||||
}
|
||||
|
||||
var prefix string
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameCmd {
|
||||
prefix = fmt.Sprint(field.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
prefix = prefix + ": "
|
||||
}
|
||||
|
||||
if h.noLevelPrefix {
|
||||
fmt.Fprintf(w, "%s%s", prefix, e.Message)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, e.Message)
|
||||
}
|
||||
|
||||
for _, field := range e.Fields {
|
||||
if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s %v", field.Name, field.Value)
|
||||
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright 2020 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 loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// IgnorableLogger is a logger that ignores certain log statements.
|
||||
type IgnorableLogger interface {
|
||||
Logger
|
||||
Errorsf(statementID, format string, v ...any)
|
||||
Apply(logger Logger) IgnorableLogger
|
||||
}
|
||||
|
||||
type ignorableLogger struct {
|
||||
Logger
|
||||
statements map[string]bool
|
||||
}
|
||||
|
||||
// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
|
||||
func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger {
|
||||
if statements == nil {
|
||||
statements = make(map[string]bool)
|
||||
}
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: statements,
|
||||
}
|
||||
}
|
||||
|
||||
// Errorsf logs statementID as an ERROR if not configured as ignoreable.
|
||||
func (l ignorableLogger) Errorsf(statementID, format string, v ...any) {
|
||||
if l.statements[statementID] {
|
||||
// Ignore.
|
||||
return
|
||||
}
|
||||
ignoreMsg := fmt.Sprintf(`
|
||||
If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config:
|
||||
ignoreErrors = [%q]`, statementID)
|
||||
|
||||
format += ignoreMsg
|
||||
|
||||
l.Errorf(format, v...)
|
||||
}
|
||||
|
||||
func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: l.statements,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/bep/logg/handlers/multi"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
)
|
||||
|
||||
var (
|
||||
reservedFieldNamePrefix = "__h_field_"
|
||||
// FieldNameCmd is the name of the field that holds the command name.
|
||||
FieldNameCmd = reservedFieldNamePrefix + "_cmd"
|
||||
// Used to suppress statements.
|
||||
FieldNameStatementID = reservedFieldNamePrefix + "__h_field_statement_id"
|
||||
)
|
||||
|
||||
// Options defines options for the logger.
|
||||
type Options struct {
|
||||
Level logg.Level
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
DistinctLevel logg.Level
|
||||
StoreErrors bool
|
||||
HandlerPost func(e *logg.Entry) error
|
||||
SuppressStatements map[string]bool
|
||||
}
|
||||
|
||||
// New creates a new logger with the given options.
|
||||
func New(opts Options) Logger {
|
||||
if opts.Stdout == nil {
|
||||
opts.Stdout = os.Stdout
|
||||
}
|
||||
if opts.Stderr == nil {
|
||||
opts.Stderr = os.Stdout
|
||||
}
|
||||
if opts.Level == 0 {
|
||||
opts.Level = logg.LevelWarn
|
||||
}
|
||||
|
||||
var logHandler logg.Handler
|
||||
if terminal.PrintANSIColors(os.Stdout) {
|
||||
logHandler = newDefaultHandler(opts.Stdout, opts.Stderr)
|
||||
} else {
|
||||
logHandler = newNoColoursHandler(opts.Stdout, opts.Stderr, false, nil)
|
||||
}
|
||||
|
||||
errorsw := &strings.Builder{}
|
||||
logCounters := newLogLevelCounter()
|
||||
handlers := []logg.Handler{
|
||||
logCounters,
|
||||
}
|
||||
|
||||
if opts.Level == logg.LevelTrace {
|
||||
// Trace is used during development only, and it's useful to
|
||||
// only see the trace messages.
|
||||
handlers = append(handlers,
|
||||
logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
if e.Level != logg.LevelTrace {
|
||||
return logg.ErrStopLogEntry
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
handlers = append(handlers, whiteSpaceTrimmer(), logHandler)
|
||||
|
||||
if opts.HandlerPost != nil {
|
||||
var hookHandler logg.HandlerFunc = func(e *logg.Entry) error {
|
||||
opts.HandlerPost(e)
|
||||
return nil
|
||||
}
|
||||
handlers = append(handlers, hookHandler)
|
||||
}
|
||||
|
||||
if opts.StoreErrors {
|
||||
h := newNoColoursHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool {
|
||||
return e.Level >= logg.LevelError
|
||||
})
|
||||
|
||||
handlers = append(handlers, h)
|
||||
}
|
||||
|
||||
logHandler = multi.New(handlers...)
|
||||
|
||||
var logOnce *logOnceHandler
|
||||
if opts.DistinctLevel != 0 {
|
||||
logOnce = newLogOnceHandler(opts.DistinctLevel)
|
||||
logHandler = newStopHandler(logOnce, logHandler)
|
||||
}
|
||||
|
||||
if opts.SuppressStatements != nil && len(opts.SuppressStatements) > 0 {
|
||||
logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppressStatements), logHandler)
|
||||
}
|
||||
|
||||
logger := logg.New(
|
||||
logg.Options{
|
||||
Level: opts.Level,
|
||||
Handler: logHandler,
|
||||
},
|
||||
)
|
||||
|
||||
l := logger.WithLevel(opts.Level)
|
||||
|
||||
reset := func() {
|
||||
logCounters.mu.Lock()
|
||||
defer logCounters.mu.Unlock()
|
||||
logCounters.counters = make(map[logg.Level]int)
|
||||
errorsw.Reset()
|
||||
if logOnce != nil {
|
||||
logOnce.reset()
|
||||
}
|
||||
}
|
||||
|
||||
return &logAdapter{
|
||||
logCounters: logCounters,
|
||||
errors: errorsw,
|
||||
reset: reset,
|
||||
out: opts.Stdout,
|
||||
level: opts.Level,
|
||||
logger: logger,
|
||||
tracel: l.WithLevel(logg.LevelTrace),
|
||||
debugl: l.WithLevel(logg.LevelDebug),
|
||||
infol: l.WithLevel(logg.LevelInfo),
|
||||
warnl: l.WithLevel(logg.LevelWarn),
|
||||
errorl: l.WithLevel(logg.LevelError),
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefault creates a new logger with the default options.
|
||||
func NewDefault() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelWarn,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
|
||||
func NewTrace() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelTrace,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
|
||||
func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
|
||||
return logWriter{l: l}
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug() logg.LevelLogger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Error() logg.LevelLogger
|
||||
Errorf(format string, v ...any)
|
||||
Errorln(v ...any)
|
||||
Errors() string
|
||||
Errorsf(id, format string, v ...any)
|
||||
Info() logg.LevelLogger
|
||||
InfoCommand(command string) logg.LevelLogger
|
||||
Infof(format string, v ...any)
|
||||
Infoln(v ...any)
|
||||
Level() logg.Level
|
||||
LoggCount(logg.Level) int
|
||||
Logger() logg.Logger
|
||||
Out() io.Writer
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Reset()
|
||||
Warn() logg.LevelLogger
|
||||
WarnCommand(command string) logg.LevelLogger
|
||||
Warnf(format string, v ...any)
|
||||
Warnln(v ...any)
|
||||
Deprecatef(fail bool, format string, v ...any)
|
||||
Trace(s logg.StringFunc)
|
||||
}
|
||||
|
||||
type logAdapter struct {
|
||||
logCounters *logLevelCounter
|
||||
errors *strings.Builder
|
||||
reset func()
|
||||
out io.Writer
|
||||
level logg.Level
|
||||
logger logg.Logger
|
||||
tracel logg.LevelLogger
|
||||
debugl logg.LevelLogger
|
||||
infol logg.LevelLogger
|
||||
warnl logg.LevelLogger
|
||||
errorl logg.LevelLogger
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debug() logg.LevelLogger {
|
||||
return l.debugl
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugf(format string, v ...any) {
|
||||
l.debugl.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugln(v ...any) {
|
||||
l.debugl.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Info() logg.LevelLogger {
|
||||
return l.infol
|
||||
}
|
||||
|
||||
func (l *logAdapter) InfoCommand(command string) logg.LevelLogger {
|
||||
return l.infol.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Infof(format string, v ...any) {
|
||||
l.infol.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Infoln(v ...any) {
|
||||
l.infol.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Level() logg.Level {
|
||||
return l.level
|
||||
}
|
||||
|
||||
func (l *logAdapter) LoggCount(level logg.Level) int {
|
||||
l.logCounters.mu.RLock()
|
||||
defer l.logCounters.mu.RUnlock()
|
||||
return l.logCounters.counters[level]
|
||||
}
|
||||
|
||||
func (l *logAdapter) Logger() logg.Logger {
|
||||
return l.logger
|
||||
}
|
||||
|
||||
func (l *logAdapter) Out() io.Writer {
|
||||
return l.out
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
// if considerable time is spent.
|
||||
func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Printf(format string, v ...any) {
|
||||
// Add trailing newline if not present.
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(l.out, format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Println(v ...any) {
|
||||
fmt.Fprintln(l.out, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Reset() {
|
||||
l.reset()
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warn() logg.LevelLogger {
|
||||
return l.warnl
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warnf(format string, v ...any) {
|
||||
l.warnl.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) WarnCommand(command string) logg.LevelLogger {
|
||||
return l.warnl.WithField(FieldNameCmd, command)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warnln(v ...any) {
|
||||
l.warnl.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Error() logg.LevelLogger {
|
||||
return l.errorl
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorf(format string, v ...any) {
|
||||
l.errorl.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorln(v ...any) {
|
||||
l.errorl.Logf(l.sprint(v...))
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errors() string {
|
||||
return l.errors.String()
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorsf(id, format string, v ...any) {
|
||||
l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Trace(s logg.StringFunc) {
|
||||
l.tracel.Log(s)
|
||||
}
|
||||
|
||||
func (l *logAdapter) sprint(v ...any) string {
|
||||
return strings.TrimRight(fmt.Sprintln(v...), "\n")
|
||||
}
|
||||
|
||||
func (l *logAdapter) Deprecatef(fail bool, format string, v ...any) {
|
||||
format = "DEPRECATED: " + format
|
||||
if fail {
|
||||
l.errorl.Logf(format, v...)
|
||||
} else {
|
||||
l.warnl.Logf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
l logg.LevelLogger
|
||||
}
|
||||
|
||||
func (w logWriter) Write(p []byte) (n int, err error) {
|
||||
w.l.Log(logg.String(string(p)))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TimeTrackf(l logg.LevelLogger, start time.Time, fields logg.Fields, format string, a ...any) {
|
||||
elapsed := time.Since(start)
|
||||
if fields != nil {
|
||||
l = l.WithFields(fields)
|
||||
}
|
||||
l.WithField("duration", elapsed).Logf(format, a...)
|
||||
}
|
||||
|
||||
func TimeTrackfn(fn func() (logg.LevelLogger, error)) error {
|
||||
start := time.Now()
|
||||
l, err := fn()
|
||||
elapsed := time.Since(start)
|
||||
l.WithField("duration", elapsed).Logf("")
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 loggers_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
)
|
||||
|
||||
func TestLogDistinct(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
StoreErrors: true,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Warnln("warn 1")
|
||||
}
|
||||
c.Assert(strings.Count(l.Errors(), "error 1"), qt.Equals, 1)
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
|
||||
c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1)
|
||||
}
|
||||
|
||||
func TestHookLast(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
HandlerPost: func(e *logg.Entry) error {
|
||||
panic(e.Message)
|
||||
},
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
c.Assert(func() { l.Warnln("warn 1") }, qt.PanicMatches, "warn 1")
|
||||
}
|
||||
|
||||
func TestOptionStoreErrors(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
Stderr: &sb,
|
||||
Stdout: &sb,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
|
||||
errorsStr := l.Errors()
|
||||
|
||||
c.Assert(errorsStr, qt.Contains, "error 1")
|
||||
c.Assert(errorsStr, qt.Not(qt.Contains), "ERROR")
|
||||
|
||||
c.Assert(sb.String(), qt.Contains, "error 1")
|
||||
c.Assert(sb.String(), qt.Contains, "ERROR")
|
||||
}
|
||||
|
||||
func TestLogCount(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Warnln("warn 1")
|
||||
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
|
||||
c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1)
|
||||
c.Assert(l.LoggCount(logg.LevelInfo), qt.Equals, 0)
|
||||
}
|
||||
|
||||
func TestSuppressStatements(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
SuppressStatements: map[string]bool{
|
||||
"error-1": true,
|
||||
},
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
l.Error().WithField(loggers.FieldNameStatementID, "error-1").Logf("error 1")
|
||||
l.Errorln("error 2")
|
||||
|
||||
errorsStr := l.Errors()
|
||||
|
||||
c.Assert(errorsStr, qt.Not(qt.Contains), "error 1")
|
||||
c.Assert(errorsStr, qt.Contains, "error 2")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 1)
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
l.Errorln("error 1")
|
||||
l.Errorln("error 2")
|
||||
l.Errorln("error 1")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2)
|
||||
|
||||
l.Reset()
|
||||
|
||||
errorsStr := l.Errors()
|
||||
|
||||
c.Assert(errorsStr, qt.Equals, "")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 0)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// 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 loggers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
func InitGlobalLogger(level logg.Level, panicOnWarnings bool) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
if panicOnWarnings {
|
||||
logHookLast = PanicOnWarningHook
|
||||
}
|
||||
|
||||
log = New(
|
||||
Options{
|
||||
Level: level,
|
||||
DistinctLevel: logg.LevelInfo,
|
||||
HandlerPost: logHookLast,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var logMu sync.Mutex
|
||||
|
||||
func Log() Logger {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
return log
|
||||
}
|
||||
|
||||
// The global logger.
|
||||
var log Logger
|
||||
|
||||
func init() {
|
||||
InitGlobalLogger(logg.LevelWarn, false)
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
// Copyright 2020 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 loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var (
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
GlobalErrorCounter *jww.Counter
|
||||
PanicOnWarning atomic.Bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
GlobalErrorCounter = &jww.Counter{}
|
||||
jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError))
|
||||
}
|
||||
|
||||
func LoggerToWriterWithPrefix(logger *log.Logger, prefix string) io.Writer {
|
||||
return prefixWriter{
|
||||
logger: logger,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
logger *log.Logger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (w prefixWriter) Write(p []byte) (n int, err error) {
|
||||
w.logger.Printf("%s: %s", w.prefix, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...any)
|
||||
Println(v ...any)
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Debug() *log.Logger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Info() *log.Logger
|
||||
Infof(format string, v ...any)
|
||||
Infoln(v ...any)
|
||||
Warn() *log.Logger
|
||||
Warnf(format string, v ...any)
|
||||
Warnln(v ...any)
|
||||
Error() *log.Logger
|
||||
Errorf(format string, v ...any)
|
||||
Errorln(v ...any)
|
||||
Errors() string
|
||||
|
||||
Out() io.Writer
|
||||
|
||||
Reset()
|
||||
|
||||
// Used in tests.
|
||||
LogCounters() *LogCounters
|
||||
}
|
||||
|
||||
type LogCounters struct {
|
||||
ErrorCounter *jww.Counter
|
||||
WarnCounter *jww.Counter
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
*jww.Notepad
|
||||
|
||||
// The writer that represents stdout.
|
||||
// Will be io.Discard when in quiet mode.
|
||||
out io.Writer
|
||||
|
||||
logCounters *LogCounters
|
||||
|
||||
// This is only set in server mode.
|
||||
errors *bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *logger) Printf(format string, v ...any) {
|
||||
l.FEEDBACK.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Println(v ...any) {
|
||||
l.FEEDBACK.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debug() *log.Logger {
|
||||
return l.DEBUG
|
||||
}
|
||||
|
||||
func (l *logger) Debugf(format string, v ...any) {
|
||||
l.DEBUG.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debugln(v ...any) {
|
||||
l.DEBUG.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infof(format string, v ...any) {
|
||||
l.INFO.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infoln(v ...any) {
|
||||
l.INFO.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Info() *log.Logger {
|
||||
return l.INFO
|
||||
}
|
||||
|
||||
const panicOnWarningMessage = "Warning trapped. Remove the --panicOnWarning flag to continue."
|
||||
|
||||
func (l *logger) Warnf(format string, v ...any) {
|
||||
l.WARN.Printf(format, v...)
|
||||
if PanicOnWarning.Load() {
|
||||
panic(panicOnWarningMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Warnln(v ...any) {
|
||||
l.WARN.Println(v...)
|
||||
if PanicOnWarning.Load() {
|
||||
panic(panicOnWarningMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Warn() *log.Logger {
|
||||
return l.WARN
|
||||
}
|
||||
|
||||
func (l *logger) Errorf(format string, v ...any) {
|
||||
l.ERROR.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Errorln(v ...any) {
|
||||
l.ERROR.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Error() *log.Logger {
|
||||
return l.ERROR
|
||||
}
|
||||
|
||||
func (l *logger) LogCounters() *LogCounters {
|
||||
return l.logCounters
|
||||
}
|
||||
|
||||
func (l *logger) Out() io.Writer {
|
||||
return l.out
|
||||
}
|
||||
|
||||
// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger
|
||||
// if considerable time is spent.
|
||||
func (l *logger) PrintTimerIfDelayed(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
if milli < 500 {
|
||||
return
|
||||
}
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logger) PrintTimer(start time.Time, name string) {
|
||||
elapsed := time.Since(start)
|
||||
milli := int(1000 * elapsed.Seconds())
|
||||
l.Printf("%s in %v ms", name, milli)
|
||||
}
|
||||
|
||||
func (l *logger) Errors() string {
|
||||
if l.errors == nil {
|
||||
return ""
|
||||
}
|
||||
return ansiColorRe.ReplaceAllString(l.errors.String(), "")
|
||||
}
|
||||
|
||||
// Reset resets the logger's internal state.
|
||||
func (l *logger) Reset() {
|
||||
l.logCounters.ErrorCounter.Reset()
|
||||
if l.errors != nil {
|
||||
l.errors.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogger creates a new Logger for the given thresholds
|
||||
func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger {
|
||||
return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
|
||||
}
|
||||
|
||||
// NewDebugLogger is a convenience function to create a debug logger.
|
||||
func NewDebugLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelDebug)
|
||||
}
|
||||
|
||||
// NewWarningLogger is a convenience function to create a warning logger.
|
||||
func NewWarningLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelWarn)
|
||||
}
|
||||
|
||||
// NewInfoLogger is a convenience function to create a info logger.
|
||||
func NewInfoLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelInfo)
|
||||
}
|
||||
|
||||
// NewErrorLogger is a convenience function to create an error logger.
|
||||
func NewErrorLogger() Logger {
|
||||
return NewBasicLogger(jww.LevelError)
|
||||
}
|
||||
|
||||
// NewBasicLogger creates a new basic logger writing to Stdout.
|
||||
func NewBasicLogger(t jww.Threshold) Logger {
|
||||
return newLogger(t, jww.LevelError, os.Stdout, io.Discard, false)
|
||||
}
|
||||
|
||||
// NewBasicLoggerForWriter creates a new basic logger writing to w.
|
||||
func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger {
|
||||
return newLogger(t, jww.LevelError, w, io.Discard, false)
|
||||
}
|
||||
|
||||
// RemoveANSIColours removes all ANSI colours from the given string.
|
||||
func RemoveANSIColours(s string) string {
|
||||
return ansiColorRe.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
var (
|
||||
ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m")
|
||||
errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)")
|
||||
)
|
||||
|
||||
type ansiCleaner struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a ansiCleaner) Write(p []byte) (n int, err error) {
|
||||
return a.w.Write(ansiColorRe.ReplaceAll(p, []byte("")))
|
||||
}
|
||||
|
||||
type labelColorizer struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a labelColorizer) Write(p []byte) (n int, err error) {
|
||||
replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string {
|
||||
switch m {
|
||||
case "ERROR", "FATAL":
|
||||
return terminal.Error(m)
|
||||
case "WARN":
|
||||
return terminal.Warning(m)
|
||||
default:
|
||||
return m
|
||||
}
|
||||
})
|
||||
// io.MultiWriter will abort if we return a bigger write count than input
|
||||
// bytes, so we lie a little.
|
||||
_, err = a.w.Write([]byte(replaced))
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
// InitGlobalLogger initializes the global logger, used in some rare cases.
|
||||
func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) {
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
jww.SetStdoutOutput(outHandle)
|
||||
jww.SetLogOutput(logHandle)
|
||||
jww.SetLogThreshold(logThreshold)
|
||||
jww.SetStdoutThreshold(stdoutThreshold)
|
||||
}
|
||||
|
||||
func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
|
||||
isTerm := terminal.PrintANSIColors(os.Stdout)
|
||||
if logHandle != io.Discard && isTerm {
|
||||
// Remove any Ansi coloring from log output
|
||||
logHandle = ansiCleaner{w: logHandle}
|
||||
}
|
||||
|
||||
if isTerm {
|
||||
outHandle = labelColorizer{w: outHandle}
|
||||
}
|
||||
|
||||
return outHandle, logHandle
|
||||
}
|
||||
|
||||
type fatalLogWriter int
|
||||
|
||||
func (s fatalLogWriter) Write(p []byte) (n int, err error) {
|
||||
trace := make([]byte, 1500)
|
||||
runtime.Stack(trace, true)
|
||||
fmt.Printf("\n===========\n\n%s\n", trace)
|
||||
os.Exit(-1)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var fatalLogListener = func(t jww.Threshold) io.Writer {
|
||||
if t != jww.LevelError {
|
||||
// Only interested in ERROR
|
||||
return nil
|
||||
}
|
||||
|
||||
return new(fatalLogWriter)
|
||||
}
|
||||
|
||||
func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger {
|
||||
errorCounter := &jww.Counter{}
|
||||
warnCounter := &jww.Counter{}
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)}
|
||||
var errorBuff *bytes.Buffer
|
||||
if saveErrors {
|
||||
errorBuff = new(bytes.Buffer)
|
||||
errorCapture := func(t jww.Threshold) io.Writer {
|
||||
if t != jww.LevelError {
|
||||
// Only interested in ERROR
|
||||
return nil
|
||||
}
|
||||
return errorBuff
|
||||
}
|
||||
|
||||
listeners = append(listeners, errorCapture)
|
||||
}
|
||||
|
||||
return &logger{
|
||||
Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
|
||||
out: outHandle,
|
||||
logCounters: &LogCounters{
|
||||
ErrorCounter: errorCounter,
|
||||
WarnCounter: warnCounter,
|
||||
},
|
||||
errors: errorBuff,
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright 2018 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 loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
l := NewWarningLogger()
|
||||
|
||||
l.Errorln("One error")
|
||||
l.Errorln("Two error")
|
||||
l.Warnln("A warning")
|
||||
|
||||
c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2))
|
||||
}
|
||||
|
||||
func TestLoggerToWriterWithPrefix(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
logger := log.New(&b, "", 0)
|
||||
|
||||
w := LoggerToWriterWithPrefix(logger, "myprefix")
|
||||
|
||||
fmt.Fprint(w, "Hello Hugo!")
|
||||
|
||||
c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n")
|
||||
}
|
||||
|
||||
func TestRemoveANSIColours(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(RemoveANSIColours(""), qt.Equals, "")
|
||||
c.Assert(RemoveANSIColours("\033[31m"), qt.Equals, "")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello"), qt.Equals, "Hello")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello\033[0m"), qt.Equals, "Hello")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello\033[0m World"), qt.Equals, "Hello World")
|
||||
c.Assert(RemoveANSIColours("\033[31mHello\033[0m World\033[31m!"), qt.Equals, "Hello World!")
|
||||
c.Assert(RemoveANSIColours("\x1b[90m 5 |"), qt.Equals, " 5 |")
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import "sync"
|
||||
|
||||
// Cache is a simple thread safe cache backed by a map.
|
||||
type Cache[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCache creates a new Cache.
|
||||
func NewCache[K comparable, T any]() *Cache[K, T] {
|
||||
return &Cache[K, T]{m: make(map[K]T)}
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the cache.
|
||||
func (c *Cache[K, T]) Get(key K) (T, bool) {
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (c *Cache[K, T]) GetOrCreate(key K, create func() T) T {
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
if found {
|
||||
return v
|
||||
}
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
v, found = c.m[key]
|
||||
if found {
|
||||
return v
|
||||
}
|
||||
v = create()
|
||||
c.m[key] = v
|
||||
return v
|
||||
}
|
||||
|
||||
// Set sets the given key to the given value.
|
||||
func (c *Cache[K, T]) Set(key K, value T) {
|
||||
c.Lock()
|
||||
c.m[key] = value
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
// SliceCache is a simple thread safe cache backed by a map.
|
||||
type SliceCache[T any] struct {
|
||||
m map[string][]T
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewSliceCache[T any]() *SliceCache[T] {
|
||||
return &SliceCache[T]{m: make(map[string][]T)}
|
||||
}
|
||||
|
||||
func (c *SliceCache[T]) Get(key string) ([]T, bool) {
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
func (c *SliceCache[T]) Append(key string, values ...T) {
|
||||
c.Lock()
|
||||
c.m[key] = append(c.m[key], values...)
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *SliceCache[T]) Reset() {
|
||||
c.Lock()
|
||||
c.m = make(map[string][]T)
|
||||
c.Unlock()
|
||||
}
|
||||
+33
-9
@@ -29,7 +29,7 @@ func ToStringMapE(in any) (map[string]any, error) {
|
||||
case Params:
|
||||
return vv, nil
|
||||
case map[string]string:
|
||||
var m = map[string]any{}
|
||||
m := map[string]any{}
|
||||
for k, v := range vv {
|
||||
m[k] = v
|
||||
}
|
||||
@@ -192,21 +192,45 @@ func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) {
|
||||
for key, val := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, key)
|
||||
switch val.(type) {
|
||||
for k, v := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, k)
|
||||
switch vv := v.(type) {
|
||||
case map[any]any:
|
||||
val = cast.ToStringMap(val)
|
||||
r.renamePath(keyPath, val.(map[string]any))
|
||||
r.renamePath(keyPath, cast.ToStringMap(vv))
|
||||
case map[string]any:
|
||||
r.renamePath(keyPath, val.(map[string]any))
|
||||
r.renamePath(keyPath, vv)
|
||||
}
|
||||
|
||||
newKey := r.getNewKey(keyPath)
|
||||
|
||||
if newKey != "" {
|
||||
delete(m, key)
|
||||
m[newKey] = val
|
||||
delete(m, k)
|
||||
m[newKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertFloat64WithNoDecimalsToInt converts float64 values with no decimals to int recursively.
|
||||
func ConvertFloat64WithNoDecimalsToInt(m map[string]any) {
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case float64:
|
||||
if v == float64(int64(vv)) {
|
||||
m[k] = int64(vv)
|
||||
}
|
||||
case map[string]any:
|
||||
ConvertFloat64WithNoDecimalsToInt(vv)
|
||||
case []any:
|
||||
for i, vvv := range vv {
|
||||
switch vvvv := vvv.(type) {
|
||||
case float64:
|
||||
if vvv == float64(int64(vvvv)) {
|
||||
vv[i] = int64(vvvv)
|
||||
}
|
||||
case map[string]any:
|
||||
ConvertFloat64WithNoDecimalsToInt(vvvv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func SetParams(dst, src Params) {
|
||||
|
||||
// IsZero returns true if p is considered empty.
|
||||
func (p Params) IsZero() bool {
|
||||
if p == nil || len(p) == 0 {
|
||||
if len(p) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ func (p Params) IsZero() bool {
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given.
|
||||
@@ -93,7 +92,7 @@ func MergeParams(dst, src Params) {
|
||||
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
ns, found := p.GetMergeStrategy()
|
||||
|
||||
var ms = ns
|
||||
ms := ns
|
||||
if !found && ps != "" {
|
||||
ms = ps
|
||||
}
|
||||
@@ -248,7 +247,7 @@ const (
|
||||
// CleanConfigStringMapString removes any processing instructions from m,
|
||||
// m will never be modified.
|
||||
func CleanConfigStringMapString(m map[string]string) map[string]string {
|
||||
if m == nil || len(m) == 0 {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
@@ -267,7 +266,7 @@ func CleanConfigStringMapString(m map[string]string) map[string]string {
|
||||
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
|
||||
// map[string]any.
|
||||
func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
if m == nil || len(m) == 0 {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
@@ -291,7 +290,6 @@ func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
|
||||
}
|
||||
return m2
|
||||
|
||||
}
|
||||
|
||||
func toMergeStrategy(v any) ParamsMergeStrategy {
|
||||
|
||||
@@ -32,8 +32,9 @@ func TestPara(t *testing.T) {
|
||||
t.Skipf("skip para test, CPU count is %d", runtime.NumCPU())
|
||||
}
|
||||
|
||||
if !htesting.IsCI() {
|
||||
t.Skip("skip para test when not running on CI")
|
||||
// TODO(bep)
|
||||
if htesting.IsCI() {
|
||||
t.Skip("skip para test when running on CI")
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
+171
-53
@@ -16,14 +16,18 @@ package paths
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// FilePathSeparator as defined by os.Separator.
|
||||
const FilePathSeparator = string(filepath.Separator)
|
||||
const (
|
||||
FilePathSeparator = string(filepath.Separator)
|
||||
slash = "/"
|
||||
)
|
||||
|
||||
// filepathPathBridge is a bridge for common functionality in filepath vs path
|
||||
type filepathPathBridge interface {
|
||||
@@ -72,6 +76,30 @@ func AbsPathify(workingDir, inPath string) string {
|
||||
return filepath.Join(workingDir, inPath)
|
||||
}
|
||||
|
||||
// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddTrailingSlash(path string) string {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// AddLeadingSlash adds a leading Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddLeadingSlash(path string) string {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// AddTrailingAndLeadingSlash adds a leading and trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
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 {
|
||||
@@ -94,43 +122,6 @@ func makePathRelative(inPath string, possibleDirectories ...string) (string, err
|
||||
return inPath, errors.New("can't extract relative path, unknown prefix")
|
||||
}
|
||||
|
||||
// Should be good enough for Hugo.
|
||||
var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
|
||||
|
||||
// GetDottedRelativePath expects a relative path starting after the content directory.
|
||||
// It returns a relative path with dots ("..") navigating up the path structure.
|
||||
func GetDottedRelativePath(inPath string) string {
|
||||
inPath = path.Clean(filepath.ToSlash(inPath))
|
||||
|
||||
if inPath == "." {
|
||||
return "./"
|
||||
}
|
||||
|
||||
if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, "/") {
|
||||
inPath += "/"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(inPath, "/") {
|
||||
inPath = "/" + inPath
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(inPath)
|
||||
|
||||
sectionCount := strings.Count(dir, "/")
|
||||
|
||||
if sectionCount == 0 || dir == "/" {
|
||||
return "./"
|
||||
}
|
||||
|
||||
var dottedPath string
|
||||
|
||||
for i := 1; i < sectionCount; i++ {
|
||||
dottedPath += "../"
|
||||
}
|
||||
|
||||
return dottedPath
|
||||
}
|
||||
|
||||
// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md".
|
||||
func ExtNoDelimiter(in string) string {
|
||||
return strings.TrimPrefix(Ext(in), ".")
|
||||
@@ -167,12 +158,6 @@ func Filename(in string) (name string) {
|
||||
return
|
||||
}
|
||||
|
||||
// PathNoExt takes a path, strips out the extension,
|
||||
// and returns the name of the file.
|
||||
func PathNoExt(in string) string {
|
||||
return strings.TrimSuffix(in, path.Ext(in))
|
||||
}
|
||||
|
||||
// FileAndExt returns the filename and any extension of a file path as
|
||||
// two separate strings.
|
||||
//
|
||||
@@ -209,7 +194,7 @@ func extractFilename(in, ext, base, pathSeparator string) (name string) {
|
||||
// return the filename minus the extension (and the ".")
|
||||
name = base[:strings.LastIndex(base, ".")]
|
||||
} else {
|
||||
// no extension case so just return base, which willi
|
||||
// no extension case so just return base, which will
|
||||
// be the filename
|
||||
name = base
|
||||
}
|
||||
@@ -252,16 +237,125 @@ func prettifyPath(in string, b filepathPathBridge) string {
|
||||
return b.Join(b.Dir(in), name, "index"+ext)
|
||||
}
|
||||
|
||||
type NamedSlice struct {
|
||||
Name string
|
||||
Slice []string
|
||||
// CommonDir returns the common directory of the given paths.
|
||||
func CommonDir(path1, path2 string) string {
|
||||
if path1 == "" || path2 == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(common, "/")
|
||||
}
|
||||
|
||||
func (n NamedSlice) String() string {
|
||||
if len(n.Slice) == 0 {
|
||||
return n.Name
|
||||
// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
|
||||
// a predefined set of special Unicode characters.
|
||||
//
|
||||
// Spaces will be replaced with a single hyphen.
|
||||
//
|
||||
// This function is the core function used to normalize paths in Hugo.
|
||||
//
|
||||
// Note that this is the first common step for URL/path sanitation,
|
||||
// the final URL/path may end up looking differently if the user has stricter rules defined (e.g. removePathAccents=true).
|
||||
func Sanitize(s string) string {
|
||||
var willChange bool
|
||||
for i, r := range s {
|
||||
willChange = !isAllowedPathCharacter(s, i, r)
|
||||
if willChange {
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
|
||||
|
||||
if !willChange {
|
||||
// Prevent allocation when nothing changes.
|
||||
return s
|
||||
}
|
||||
|
||||
target := make([]rune, 0, len(s))
|
||||
var (
|
||||
prependHyphen bool
|
||||
wasHyphen bool
|
||||
)
|
||||
|
||||
for i, r := range s {
|
||||
isAllowed := isAllowedPathCharacter(s, i, r)
|
||||
|
||||
if isAllowed {
|
||||
// track explicit hyphen in input; no need to add a new hyphen if
|
||||
// we just saw one.
|
||||
wasHyphen = r == '-'
|
||||
|
||||
if prependHyphen {
|
||||
// if currently have a hyphen, don't prepend an extra one
|
||||
if !wasHyphen {
|
||||
target = append(target, '-')
|
||||
}
|
||||
prependHyphen = false
|
||||
}
|
||||
target = append(target, r)
|
||||
} else if len(target) > 0 && !wasHyphen && unicode.IsSpace(r) {
|
||||
prependHyphen = true
|
||||
}
|
||||
}
|
||||
|
||||
return string(target)
|
||||
}
|
||||
|
||||
func isAllowedPathCharacter(s string, i int, r rune) bool {
|
||||
if r == ' ' {
|
||||
return false
|
||||
}
|
||||
// Check for the most likely first (faster).
|
||||
isAllowed := unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
isAllowed = isAllowed || r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' || r == '-' || r == '@'
|
||||
isAllowed = isAllowed || unicode.IsMark(r)
|
||||
isAllowed = isAllowed || (r == '%' && i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2]))
|
||||
return isAllowed
|
||||
}
|
||||
|
||||
// From https://golang.org/src/net/url/url.go
|
||||
func ishex(c byte) bool {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return true
|
||||
case 'a' <= c && c <= 'f':
|
||||
return true
|
||||
case 'A' <= c && c <= 'F':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var slashFunc = func(r rune) bool {
|
||||
return r == '/'
|
||||
}
|
||||
|
||||
// Dir behaves like path.Dir without the path.Clean step.
|
||||
//
|
||||
// The returned path ends in a slash only if it is the root "/".
|
||||
func Dir(s string) string {
|
||||
dir, _ := path.Split(s)
|
||||
if len(dir) > 1 && dir[len(dir)-1] == '/' {
|
||||
return dir[:len(dir)-1]
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// FieldsSlash cuts s into fields separated with '/'.
|
||||
func FieldsSlash(s string) []string {
|
||||
f := strings.FieldsFunc(s, slashFunc)
|
||||
return f
|
||||
}
|
||||
|
||||
// DirFile holds the result from path.Split.
|
||||
@@ -274,3 +368,27 @@ type DirFile struct {
|
||||
func (df DirFile) String() string {
|
||||
return fmt.Sprintf("%s|%s", df.Dir, df.File)
|
||||
}
|
||||
|
||||
// PathEscape escapes unicode letters in pth.
|
||||
// Use URLEscape to escape full URLs including scheme, query etc.
|
||||
// This is slightly faster for the common case.
|
||||
// Note, there is a url.PathEscape function, but that also
|
||||
// escapes /.
|
||||
func PathEscape(pth string) string {
|
||||
u, err := url.Parse(pth)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u.EscapedPath()
|
||||
}
|
||||
|
||||
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
|
||||
func ToSlashTrimLeading(s string) string {
|
||||
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
// ToSlashPreserveLeading converts the path given to a forward slash separated path
|
||||
// and preserves the leading slash if present trimming any trailing slash.
|
||||
func ToSlashPreserveLeading(s string) string {
|
||||
return "/" + strings.Trim(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
+75
-39
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -75,44 +75,6 @@ func TestMakePathRelative(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDottedRelativePath(t *testing.T) {
|
||||
// on Windows this will receive both kinds, both country and western ...
|
||||
for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} {
|
||||
doTestGetDottedRelativePath(f, t)
|
||||
}
|
||||
}
|
||||
|
||||
func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"", "./"},
|
||||
{urlFixer("/"), "./"},
|
||||
{urlFixer("post"), "../"},
|
||||
{urlFixer("/post"), "../"},
|
||||
{urlFixer("post/"), "../"},
|
||||
{urlFixer("tags/foo.html"), "../"},
|
||||
{urlFixer("/tags/foo.html"), "../"},
|
||||
{urlFixer("/post/"), "../"},
|
||||
{urlFixer("////post/////"), "../"},
|
||||
{urlFixer("/foo/bar/index.html"), "../../"},
|
||||
{urlFixer("/foo/bar/foo/"), "../../../"},
|
||||
{urlFixer("/foo/bar/foo"), "../../../"},
|
||||
{urlFixer("foo/bar/foo/"), "../../../"},
|
||||
{urlFixer("foo/bar/foo/bar"), "../../../../"},
|
||||
{"404.html", "./"},
|
||||
{"404.xml", "./"},
|
||||
{"/404.html", "./"},
|
||||
}
|
||||
for i, d := range data {
|
||||
output := GetDottedRelativePath(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTitle(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
@@ -226,3 +188,77 @@ func TestFileAndExt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitize(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{" Foo bar ", "Foo-bar"},
|
||||
{"Foo.Bar/foo_Bar-Foo", "Foo.Bar/foo_Bar-Foo"},
|
||||
{"fOO,bar:foobAR", "fOObarfoobAR"},
|
||||
{"FOo/BaR.html", "FOo/BaR.html"},
|
||||
{"FOo/Ba---R.html", "FOo/Ba---R.html"}, /// See #10104
|
||||
{"FOo/Ba R.html", "FOo/Ba-R.html"},
|
||||
{"трям/трям", "трям/трям"},
|
||||
{"은행", "은행"},
|
||||
{"Банковский кассир", "Банковский-кассир"},
|
||||
// Issue #1488
|
||||
{"संस्कृत", "संस्कृत"},
|
||||
{"a%C3%B1ame", "a%C3%B1ame"}, // Issue #1292
|
||||
{"this+is+a+test", "this+is+a+test"}, // Issue #1290
|
||||
{"~foo", "~foo"}, // Issue #2177
|
||||
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c.Assert(Sanitize(test.input), qt.Equals, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitize(b *testing.B) {
|
||||
const (
|
||||
allAlowedPath = "foo/bar"
|
||||
spacePath = "foo bar"
|
||||
)
|
||||
|
||||
// This should not allocate any memory.
|
||||
b.Run("All allowed", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := Sanitize(allAlowedPath)
|
||||
if got != allAlowedPath {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// This will allocate some memory.
|
||||
b.Run("Spaces", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := Sanitize(spacePath)
|
||||
if got != "foo-bar" {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDir(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(Dir("/a/b/c/d"), qt.Equals, "/a/b/c")
|
||||
c.Assert(Dir("/a"), qt.Equals, "/")
|
||||
c.Assert(Dir("/"), qt.Equals, "/")
|
||||
c.Assert(Dir(""), qt.Equals, "")
|
||||
}
|
||||
|
||||
func TestFieldsSlash(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(FieldsSlash("a/b/c"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("/a/b/c"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("/a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("/"), qt.DeepEquals, []string{})
|
||||
c.Assert(FieldsSlash(""), qt.DeepEquals, []string{})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,494 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
)
|
||||
|
||||
var defaultPathParser PathParser
|
||||
|
||||
// PathParser parses a path into a Path.
|
||||
type PathParser struct {
|
||||
// Maps the language code to its index in the languages/sites slice.
|
||||
LanguageIndex map[string]int
|
||||
}
|
||||
|
||||
// Parse parses component c with path s into Path using the default path parser.
|
||||
func Parse(c, s string) *Path {
|
||||
return defaultPathParser.Parse(c, s)
|
||||
}
|
||||
|
||||
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
|
||||
func NormalizePathStringBasic(s string) string {
|
||||
// All lower case.
|
||||
s = strings.ToLower(s)
|
||||
|
||||
// Replace spaces with hyphens.
|
||||
s = strings.ReplaceAll(s, " ", "-")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Parse parses component c with path s into Path using Hugo's content path rules.
|
||||
func (parser PathParser) Parse(c, s string) *Path {
|
||||
p, err := parser.parse(c, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (pp *PathParser) parse(component, s string) (*Path, error) {
|
||||
ss := NormalizePathStringBasic(s)
|
||||
|
||||
p, err := pp.doParse(component, ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s != ss {
|
||||
var err error
|
||||
// Preserve the original case for titles etc.
|
||||
p.unnormalized, err = pp.doParse(component, s)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
p.unnormalized = p
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string) (*Path, error) {
|
||||
p := &Path{
|
||||
component: component,
|
||||
posContainerLow: -1,
|
||||
posContainerHigh: -1,
|
||||
posSectionHigh: -1,
|
||||
posIdentifierLanguage: -1,
|
||||
}
|
||||
|
||||
hasLang := pp.LanguageIndex != nil
|
||||
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
s = path.Clean(filepath.ToSlash(s))
|
||||
if s == "." {
|
||||
s = ""
|
||||
}
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
s = "/"
|
||||
}
|
||||
|
||||
// Leading slash, no trailing slash.
|
||||
if !strings.HasPrefix(s, "/") {
|
||||
s = "/" + s
|
||||
}
|
||||
|
||||
if s != "/" && s[len(s)-1] == '/' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
|
||||
p.s = s
|
||||
slashCount := 0
|
||||
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
|
||||
switch c {
|
||||
case '.':
|
||||
if p.posContainerHigh == -1 {
|
||||
var high int
|
||||
if len(p.identifiers) > 0 {
|
||||
high = p.identifiers[len(p.identifiers)-1].Low - 1
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh{Low: i + 1, High: high}
|
||||
if len(p.identifiers) == 0 {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
} else if len(p.identifiers) == 1 {
|
||||
// Check for a valid language.
|
||||
s := p.s[id.Low:id.High]
|
||||
|
||||
if hasLang {
|
||||
if _, found := pp.LanguageIndex[s]; found {
|
||||
p.posIdentifierLanguage = 1
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case '/':
|
||||
slashCount++
|
||||
if p.posContainerHigh == -1 {
|
||||
p.posContainerHigh = i + 1
|
||||
} else if p.posContainerLow == -1 {
|
||||
p.posContainerLow = i + 1
|
||||
}
|
||||
if i > 0 {
|
||||
p.posSectionHigh = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
|
||||
isContent := isContentComponent && files.IsContentExt(p.Ext())
|
||||
|
||||
if isContent {
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
b := p.s[p.posContainerHigh : id.Low-1]
|
||||
switch b {
|
||||
case "index":
|
||||
p.bundleType = PathTypeLeaf
|
||||
case "_index":
|
||||
p.bundleType = PathTypeBranch
|
||||
default:
|
||||
p.bundleType = PathTypeContentSingle
|
||||
}
|
||||
|
||||
if slashCount == 2 && p.IsLeafBundle() {
|
||||
p.posSectionHigh = 0
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func ModifyPathBundleTypeResource(p *Path) {
|
||||
if p.IsContent() {
|
||||
p.bundleType = PathTypeContentResource
|
||||
} else {
|
||||
p.bundleType = PathTypeFile
|
||||
}
|
||||
}
|
||||
|
||||
type PathType int
|
||||
|
||||
const (
|
||||
// A generic resource, e.g. a JSON file.
|
||||
PathTypeFile PathType = iota
|
||||
|
||||
// All below are content files.
|
||||
// A resource of a content type with front matter.
|
||||
PathTypeContentResource
|
||||
|
||||
// E.g. /blog/my-post.md
|
||||
PathTypeContentSingle
|
||||
|
||||
// All bewlow are bundled content files.
|
||||
|
||||
// Leaf bundles, e.g. /blog/my-post/index.md
|
||||
PathTypeLeaf
|
||||
|
||||
// Branch bundles, e.g. /blog/_index.md
|
||||
PathTypeBranch
|
||||
)
|
||||
|
||||
type Path struct {
|
||||
s string
|
||||
|
||||
posContainerLow int
|
||||
posContainerHigh int
|
||||
posSectionHigh int
|
||||
|
||||
component string
|
||||
bundleType PathType
|
||||
|
||||
identifiers []types.LowHigh
|
||||
|
||||
posIdentifierLanguage int
|
||||
|
||||
trimLeadingSlash bool
|
||||
|
||||
unnormalized *Path
|
||||
}
|
||||
|
||||
// TrimLeadingSlash returns a copy of the Path with the leading slash removed.
|
||||
func (p Path) TrimLeadingSlash() *Path {
|
||||
p.trimLeadingSlash = true
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Path) norm(s string) string {
|
||||
if p.trimLeadingSlash {
|
||||
s = strings.TrimPrefix(s, "/")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IdentifierBase satifies identity.Identity.
|
||||
func (p *Path) IdentifierBase() string {
|
||||
return p.Base()[1:]
|
||||
}
|
||||
|
||||
// Component returns the component for this path (e.g. "content").
|
||||
func (p *Path) Component() string {
|
||||
return p.component
|
||||
}
|
||||
|
||||
// Container returns the base name of the container directory for this path.
|
||||
func (p *Path) Container() string {
|
||||
if p.posContainerLow == -1 {
|
||||
return ""
|
||||
}
|
||||
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
|
||||
}
|
||||
|
||||
// ContainerDir returns the container directory for this path.
|
||||
// For content bundles this will be the parent directory.
|
||||
func (p *Path) ContainerDir() string {
|
||||
if p.posContainerLow == -1 || !p.IsBundle() {
|
||||
return p.Dir()
|
||||
}
|
||||
return p.norm(p.s[:p.posContainerLow-1])
|
||||
}
|
||||
|
||||
// Section returns the first path element (section).
|
||||
func (p *Path) Section() string {
|
||||
if p.posSectionHigh <= 0 {
|
||||
return ""
|
||||
}
|
||||
return p.norm(p.s[1:p.posSectionHigh])
|
||||
}
|
||||
|
||||
// IsContent returns true if the path is a content file (e.g. mypost.md).
|
||||
// Note that this will also return true for content files in a bundle.
|
||||
func (p *Path) IsContent() bool {
|
||||
return p.BundleType() >= PathTypeContentResource
|
||||
}
|
||||
|
||||
// isContentPage returns true if the path is a content file (e.g. mypost.md),
|
||||
// but nof if inside a leaf bundle.
|
||||
func (p *Path) isContentPage() bool {
|
||||
return p.BundleType() >= PathTypeContentSingle
|
||||
}
|
||||
|
||||
// Name returns the last element of path.
|
||||
func (p *Path) Name() string {
|
||||
if p.posContainerHigh > 0 {
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
return p.s
|
||||
}
|
||||
|
||||
// Name returns the last element of path withhout any extension.
|
||||
func (p *Path) NameNoExt() string {
|
||||
if i := p.identifierIndex(0); i != -1 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[i].Low-1]
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Name returns the last element of path withhout 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.identifiers[i].Low-1] + p.s[p.identifiers[i].High:]
|
||||
}
|
||||
|
||||
// BaseNameNoIdentifier returns the logcical base name for a resource without any idenifier (e.g. no extension).
|
||||
// For bundles this will be the containing directory's name, e.g. "blog".
|
||||
func (p *Path) BaseNameNoIdentifier() string {
|
||||
if p.IsBundle() {
|
||||
return p.Container()
|
||||
}
|
||||
return p.NameNoIdentifier()
|
||||
}
|
||||
|
||||
// NameNoIdentifier returns the last element of path withhout any identifier (e.g. no extension).
|
||||
func (p *Path) NameNoIdentifier() string {
|
||||
if len(p.identifiers) > 0 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Dir returns all but the last element of path, typically the path's directory.
|
||||
func (p *Path) Dir() (d string) {
|
||||
if p.posContainerHigh > 0 {
|
||||
d = p.s[:p.posContainerHigh-1]
|
||||
}
|
||||
if d == "" {
|
||||
d = "/"
|
||||
}
|
||||
d = p.norm(d)
|
||||
return
|
||||
}
|
||||
|
||||
// Path returns the full path.
|
||||
func (p *Path) Path() (d string) {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
// Unmormalized returns the Path with the original case preserved.
|
||||
func (p *Path) Unmormalized() *Path {
|
||||
return p.unnormalized
|
||||
}
|
||||
|
||||
// PathNoLang returns the Path but with any language identifier removed.
|
||||
func (p *Path) PathNoLang() string {
|
||||
return p.base(true, false)
|
||||
}
|
||||
|
||||
// PathNoIdentifier returns the Path but with any identifier (ext, lang) removed.
|
||||
func (p *Path) PathNoIdentifier() string {
|
||||
return p.base(false, false)
|
||||
}
|
||||
|
||||
// PathRel returns the path relativeto the given owner.
|
||||
func (p *Path) PathRel(owner *Path) string {
|
||||
ob := owner.Base()
|
||||
if !strings.HasSuffix(ob, "/") {
|
||||
ob += "/"
|
||||
}
|
||||
return strings.TrimPrefix(p.Path(), ob)
|
||||
}
|
||||
|
||||
// BaseRel returns the base path relative to the given owner.
|
||||
func (p *Path) BaseRel(owner *Path) string {
|
||||
ob := owner.Base()
|
||||
if ob == "/" {
|
||||
ob = ""
|
||||
}
|
||||
return p.Base()[len(ob)+1:]
|
||||
}
|
||||
|
||||
// For content files, Base returns the path without any identifiers (extension, language code etc.).
|
||||
// Any 'index' as the last path element is ignored.
|
||||
//
|
||||
// For other files (Resources), any extension is kept.
|
||||
func (p *Path) Base() string {
|
||||
return p.base(!p.isContentPage(), p.IsBundle())
|
||||
}
|
||||
|
||||
// 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.identifiers) == 0 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
if preserveExt && len(p.identifiers) == 1 {
|
||||
// Preserve extension.
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
high := id.Low - 1
|
||||
|
||||
if isBundle {
|
||||
high = p.posContainerHigh - 1
|
||||
}
|
||||
|
||||
if high == 0 {
|
||||
high++
|
||||
}
|
||||
|
||||
if !preserveExt {
|
||||
return p.norm(p.s[:high])
|
||||
}
|
||||
|
||||
// For txt files etc. we want to preserve the extension.
|
||||
id = p.identifiers[0]
|
||||
|
||||
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
|
||||
}
|
||||
|
||||
func (p *Path) Ext() string {
|
||||
return p.identifierAsString(0)
|
||||
}
|
||||
|
||||
func (p *Path) Lang() string {
|
||||
return p.identifierAsString(1)
|
||||
}
|
||||
|
||||
func (p *Path) Identifier(i int) string {
|
||||
return p.identifierAsString(i)
|
||||
}
|
||||
|
||||
func (p *Path) Identifiers() []string {
|
||||
ids := make([]string, len(p.identifiers))
|
||||
for i, id := range p.identifiers {
|
||||
ids[i] = p.s[id.Low:id.High]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) IsHTML() bool {
|
||||
return files.IsHTML(p.Ext())
|
||||
}
|
||||
|
||||
func (p *Path) BundleType() PathType {
|
||||
return p.bundleType
|
||||
}
|
||||
|
||||
func (p *Path) IsBundle() bool {
|
||||
return p.bundleType >= PathTypeLeaf
|
||||
}
|
||||
|
||||
func (p *Path) IsBranchBundle() bool {
|
||||
return p.bundleType == PathTypeBranch
|
||||
}
|
||||
|
||||
func (p *Path) IsLeafBundle() bool {
|
||||
return p.bundleType == PathTypeLeaf
|
||||
}
|
||||
|
||||
func (p *Path) identifierAsString(i int) string {
|
||||
i = p.identifierIndex(i)
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
id := p.identifiers[i]
|
||||
return p.s[id.Low:id.High]
|
||||
}
|
||||
|
||||
func (p *Path) identifierIndex(i int) int {
|
||||
if i < 0 || i >= len(p.identifiers) {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// HasExt returns true if the Unix styled path has an extension.
|
||||
func HasExt(p string) bool {
|
||||
for i := len(p) - 1; i >= 0; i-- {
|
||||
if p[i] == '.' {
|
||||
return true
|
||||
}
|
||||
if p[i] == '/' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var testParser = &PathParser{
|
||||
LanguageIndex: map[string]int{
|
||||
"no": 0,
|
||||
"en": 1,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
assert func(c *qt.C, p *Path)
|
||||
}{
|
||||
{
|
||||
"Basic text file",
|
||||
"/a/b.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.txt")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.txt")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.IsContent(), qt.IsFalse)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, upper case",
|
||||
"/A/B.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.txt")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "b")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 1 space in dir",
|
||||
"/a b/c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a-b/c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 2 spaces in dir",
|
||||
"/a b/c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a--b/c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 1 space in filename",
|
||||
"/a/b c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b-c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 2 spaces in filename",
|
||||
"/a/b c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b--c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, mixed case and spaces, unnormalized",
|
||||
"/a/Foo BAR.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
pp := p.Unmormalized()
|
||||
c.Assert(pp, qt.IsNotNil)
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "Foo BAR")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic Markdown file",
|
||||
"/a/b/c.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.Name(), qt.Equals, "c.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/c")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Content resource",
|
||||
"/a/b.md",
|
||||
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")
|
||||
|
||||
// Reclassify it as a content resource.
|
||||
ModifyPathBundleTypeResource(p)
|
||||
c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.Name(), qt.Equals, "b.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"No ext",
|
||||
"/a/b",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "b")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"No ext, trailing slash",
|
||||
"/a/b/",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Identifiers",
|
||||
"/a/b.a.b.no.txt",
|
||||
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.Base(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home branch cundle",
|
||||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Path(), qt.Equals, "/_index.md")
|
||||
c.Assert(p.Container(), qt.Equals, "")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index content file in root",
|
||||
"/a/index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
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, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index content file with lang",
|
||||
"/a/b/index.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
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.PathNoLang(), qt.Equals, "/a/b/index.md")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index branch content file",
|
||||
"/a/b/_index.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsTrue)
|
||||
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")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index root no slash",
|
||||
"_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Name(), qt.Equals, "_index.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index root",
|
||||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Name(), qt.Equals, "_index.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index first",
|
||||
"/a/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index text file",
|
||||
"/a/b/index.no.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Empty",
|
||||
"",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
c.Assert(p.Name(), qt.Equals, "")
|
||||
c.Assert(p.Path(), qt.Equals, "/")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Slash",
|
||||
"/",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
c.Assert(p.Name(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Trim Leading Slash bundle",
|
||||
"foo/bar/index.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Path(), qt.Equals, "/foo/bar/index.no.md")
|
||||
pp := p.TrimLeadingSlash()
|
||||
c.Assert(pp.Path(), qt.Equals, "foo/bar/index.no.md")
|
||||
c.Assert(pp.PathNoLang(), qt.Equals, "foo/bar/index.md")
|
||||
c.Assert(pp.Base(), qt.Equals, "foo/bar")
|
||||
c.Assert(pp.Dir(), qt.Equals, "foo/bar")
|
||||
c.Assert(pp.ContainerDir(), qt.Equals, "foo")
|
||||
c.Assert(pp.Container(), qt.Equals, "bar")
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "bar")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Trim Leading Slash file",
|
||||
"foo/bar.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Path(), qt.Equals, "/foo/bar.txt")
|
||||
pp := p.TrimLeadingSlash()
|
||||
c.Assert(pp.Path(), qt.Equals, "foo/bar.txt")
|
||||
c.Assert(pp.PathNoLang(), qt.Equals, "foo/bar.txt")
|
||||
c.Assert(pp.Base(), qt.Equals, "foo/bar.txt")
|
||||
c.Assert(pp.Dir(), qt.Equals, "foo")
|
||||
c.Assert(pp.ContainerDir(), qt.Equals, "foo")
|
||||
c.Assert(pp.Container(), qt.Equals, "foo")
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "bar")
|
||||
},
|
||||
},
|
||||
{
|
||||
"File separator",
|
||||
filepath.FromSlash("/a/b/c.txt"),
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/c.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.Name(), qt.Equals, "c.txt")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/c.txt")
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasExt(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(HasExt("/a/b/c.txt"), qt.IsTrue)
|
||||
c.Assert(HasExt("/a/b.c/d.txt"), qt.IsTrue)
|
||||
c.Assert(HasExt("/a/b/c"), qt.IsFalse)
|
||||
c.Assert(HasExt("/a/b.c/d"), qt.IsFalse)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestRemovePathAccents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
removePathAccents = true
|
||||
-- content/διακριτικός.md --
|
||||
-- content/διακριτικός.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
List
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/διακριτικός/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/διακριτικος/index.html", "fr|Single")
|
||||
}
|
||||
|
||||
func TestDisablePathToLower(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
disablePathToLower = true
|
||||
-- content/MySection/MyPage.md --
|
||||
-- content/MySection/MyPage.fr.md --
|
||||
-- content/MySection/MyBundle/index.md --
|
||||
-- content/MySection/MyBundle/index.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
{{ .Language.Lang }}|List.
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/mysection/index.html", "en|List")
|
||||
b.AssertFileContent("public/en/mysection/mypage/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/MySection/index.html", "fr|List")
|
||||
b.AssertFileContent("public/fr/MySection/MyPage/index.html", "fr|Single")
|
||||
b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
|
||||
|
||||
package paths
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[PathTypeFile-0]
|
||||
_ = x[PathTypeContentResource-1]
|
||||
_ = x[PathTypeContentSingle-2]
|
||||
_ = x[PathTypeLeaf-3]
|
||||
_ = x[PathTypeBranch-4]
|
||||
}
|
||||
|
||||
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
|
||||
|
||||
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
|
||||
|
||||
func (i PathType) String() string {
|
||||
if i < 0 || i >= PathType(len(_PathType_index)-1) {
|
||||
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
|
||||
}
|
||||
+24
-9
@@ -51,9 +51,10 @@ var pb pathBridge
|
||||
|
||||
// MakePermalink combines base URL with content path to create full URL paths.
|
||||
// Example
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
//
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
func MakePermalink(host, plink string) *url.URL {
|
||||
base, err := url.Parse(host)
|
||||
if err != nil {
|
||||
@@ -70,6 +71,8 @@ func MakePermalink(host, plink string) *url.URL {
|
||||
}
|
||||
|
||||
base.Path = path.Join(base.Path, p.Path)
|
||||
base.Fragment = p.Fragment
|
||||
base.RawQuery = p.RawQuery
|
||||
|
||||
// path.Join will strip off the last /, so put it back if it was there.
|
||||
hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/")
|
||||
@@ -117,17 +120,19 @@ func PrettifyURL(in string) string {
|
||||
|
||||
// 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
|
||||
//
|
||||
// /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
|
||||
//
|
||||
// /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 {
|
||||
@@ -179,3 +184,13 @@ func UrlToFilename(s string) (string, bool) {
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
||||
// URLEscape escapes unicode letters.
|
||||
func URLEscape(uri string) string {
|
||||
// escape unicode letters
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func TestMakePermalink(t *testing.T) {
|
||||
{"http://abc.com", "bar", "http://abc.com/bar"},
|
||||
{"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"},
|
||||
{"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"},
|
||||
{"http://abc.com/foo", "post/bar?a=b#c", "http://abc.com/foo/post/bar?a=b#c"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate
|
||||
|
||||
// P is a predicate function that tests whether a value of type T satisfies some condition.
|
||||
type P[T any] func(T) bool
|
||||
|
||||
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
|
||||
func (p P[T]) And(ps ...P[T]) P[T] {
|
||||
return func(v T) bool {
|
||||
for _, pp := range ps {
|
||||
if !pp(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
|
||||
func (p P[T]) Or(ps ...P[T]) P[T] {
|
||||
return func(v T) bool {
|
||||
for _, pp := range ps {
|
||||
if pp(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Negate returns a predicate that is a logical negation of this predicate.
|
||||
func (p P[T]) Negate() P[T] {
|
||||
return func(v T) bool {
|
||||
return !p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a new slice holding only the elements of s that satisfy p.
|
||||
// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length.
|
||||
func (p P[T]) Filter(s []T) []T {
|
||||
var n int
|
||||
for _, v := range s {
|
||||
if p(v) {
|
||||
s[n] = v
|
||||
n++
|
||||
}
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
// FilterCopy returns a new slice holding only the elements of s that satisfy p.
|
||||
func (p P[T]) FilterCopy(s []T) []T {
|
||||
var result []T
|
||||
for _, v := range s {
|
||||
if p(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/predicate"
|
||||
)
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
|
||||
c.Assert(p(1), qt.IsTrue)
|
||||
c.Assert(p(2), qt.IsFalse)
|
||||
|
||||
neg := p.Negate()
|
||||
c.Assert(neg(1), qt.IsFalse)
|
||||
c.Assert(neg(2), qt.IsTrue)
|
||||
|
||||
and := p.And(intP2)
|
||||
c.Assert(and(1), qt.IsFalse)
|
||||
c.Assert(and(2), qt.IsFalse)
|
||||
c.Assert(and(10), qt.IsTrue)
|
||||
|
||||
or := p.Or(intP2)
|
||||
c.Assert(or(1), qt.IsTrue)
|
||||
c.Assert(or(2), qt.IsTrue)
|
||||
c.Assert(or(10), qt.IsTrue)
|
||||
c.Assert(or(11), qt.IsFalse)
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
p = p.Or(intP2)
|
||||
|
||||
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
|
||||
|
||||
c.Assert(p.Filter(ints), qt.DeepEquals, []int{1, 2, 1, 2})
|
||||
c.Assert(ints, qt.DeepEquals, []int{1, 2, 1, 2, 1, 6, 7, 8, 2})
|
||||
}
|
||||
|
||||
func TestFilterCopy(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
p = p.Or(intP2)
|
||||
|
||||
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
|
||||
|
||||
c.Assert(p.FilterCopy(ints), qt.DeepEquals, []int{1, 2, 1, 2})
|
||||
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2})
|
||||
}
|
||||
|
||||
var intP1 = func(i int) bool {
|
||||
if i == 10 {
|
||||
return true
|
||||
}
|
||||
return i == 1
|
||||
}
|
||||
|
||||
var intP2 = func(i int) bool {
|
||||
if i == 10 {
|
||||
return true
|
||||
}
|
||||
return i == 2
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rungroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Group is a group of workers that can be used to enqueue work and wait for
|
||||
// them to finish.
|
||||
type Group[T any] interface {
|
||||
Enqueue(T) error
|
||||
Wait() error
|
||||
}
|
||||
|
||||
type runGroup[T any] struct {
|
||||
ctx context.Context
|
||||
g *errgroup.Group
|
||||
ch chan T
|
||||
}
|
||||
|
||||
// Config is the configuration for a new Group.
|
||||
type Config[T any] struct {
|
||||
NumWorkers int
|
||||
Handle func(context.Context, T) error
|
||||
}
|
||||
|
||||
// Run creates a new Group with the given configuration.
|
||||
func Run[T any](ctx context.Context, cfg Config[T]) Group[T] {
|
||||
if cfg.NumWorkers <= 0 {
|
||||
cfg.NumWorkers = 1
|
||||
}
|
||||
if cfg.Handle == nil {
|
||||
panic("Handle must be set")
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
// Buffered for performance.
|
||||
ch := make(chan T, cfg.NumWorkers)
|
||||
|
||||
for i := 0; i < cfg.NumWorkers; i++ {
|
||||
g.Go(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := cfg.Handle(ctx, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return &runGroup[T]{
|
||||
ctx: ctx,
|
||||
g: g,
|
||||
ch: ch,
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue enqueues a new item to be handled by the workers.
|
||||
func (r *runGroup[T]) Enqueue(t T) error {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return nil
|
||||
case r.ch <- t:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait waits for all workers to finish and returns the first error.
|
||||
func (r *runGroup[T]) Wait() error {
|
||||
close(r.ch)
|
||||
return r.g.Wait()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rungroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var result int
|
||||
adder := func(ctx context.Context, i int) error {
|
||||
result += i
|
||||
return nil
|
||||
}
|
||||
|
||||
g := Run[int](
|
||||
context.Background(),
|
||||
Config[int]{
|
||||
Handle: adder,
|
||||
},
|
||||
)
|
||||
|
||||
c.Assert(g, qt.IsNotNil)
|
||||
g.Enqueue(32)
|
||||
g.Enqueue(33)
|
||||
c.Assert(g.Wait(), qt.IsNil)
|
||||
c.Assert(result, qt.Equals, 65)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -35,11 +35,11 @@ func NewEvictingStringQueue(size int) *EvictingStringQueue {
|
||||
}
|
||||
|
||||
// Add adds a new string to the tail of the queue if it's not already there.
|
||||
func (q *EvictingStringQueue) Add(v string) {
|
||||
func (q *EvictingStringQueue) Add(v string) *EvictingStringQueue {
|
||||
q.mu.Lock()
|
||||
if q.set[v] {
|
||||
q.mu.Unlock()
|
||||
return
|
||||
return q
|
||||
}
|
||||
|
||||
if len(q.set) == q.size {
|
||||
@@ -50,6 +50,17 @@ func (q *EvictingStringQueue) Add(v string) {
|
||||
q.set[v] = true
|
||||
q.vals = append(q.vals, v)
|
||||
q.mu.Unlock()
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *EvictingStringQueue) Len() int {
|
||||
if q == nil {
|
||||
return 0
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return len(q.vals)
|
||||
}
|
||||
|
||||
// Contains returns whether the queue contains v.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -92,5 +92,18 @@ type DevMarker interface {
|
||||
DevOnly()
|
||||
}
|
||||
|
||||
// Unwrapper is implemented by types that can unwrap themselves.
|
||||
type Unwrapper interface {
|
||||
// Unwrapv is for internal use only.
|
||||
// It got its slightly odd name to prevent collisions with user types.
|
||||
Unwrapv() any
|
||||
}
|
||||
|
||||
// LowHigh is typically used to represent a slice boundary.
|
||||
type LowHigh struct {
|
||||
Low int
|
||||
High int
|
||||
}
|
||||
|
||||
// This is only used for debugging purposes.
|
||||
var InvocationCounter atomic.Int64
|
||||
|
||||
+15
-13
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -23,10 +23,12 @@ import (
|
||||
// A BaseURL in Hugo is normally on the form scheme://path, but the
|
||||
// form scheme: is also valid (mailto:hugo@rules.com).
|
||||
type BaseURL struct {
|
||||
url *url.URL
|
||||
WithPath string
|
||||
WithoutPath string
|
||||
BasePath string
|
||||
url *url.URL
|
||||
WithPath string
|
||||
WithPathNoTrailingSlash string
|
||||
WithoutPath string
|
||||
BasePath string
|
||||
BasePathNoTrailingSlash string
|
||||
}
|
||||
|
||||
func (b BaseURL) String() string {
|
||||
@@ -92,19 +94,19 @@ func NewBaseURLFromString(b string) (BaseURL, error) {
|
||||
return BaseURL{}, err
|
||||
}
|
||||
return newBaseURLFromURL(u)
|
||||
|
||||
}
|
||||
|
||||
func newBaseURLFromURL(u *url.URL) (BaseURL, error) {
|
||||
baseURL := BaseURL{url: u, WithPath: u.String()}
|
||||
var baseURLNoPath = baseURL.URL()
|
||||
// A baseURL should always have a trailing slash, see #11669.
|
||||
if !strings.HasSuffix(u.Path, "/") {
|
||||
u.Path += "/"
|
||||
}
|
||||
baseURL := BaseURL{url: u, WithPath: u.String(), WithPathNoTrailingSlash: strings.TrimSuffix(u.String(), "/")}
|
||||
baseURLNoPath := baseURL.URL()
|
||||
baseURLNoPath.Path = ""
|
||||
baseURL.WithoutPath = baseURLNoPath.String()
|
||||
|
||||
basePath := u.Path
|
||||
if basePath != "" && basePath != "/" {
|
||||
baseURL.BasePath = basePath
|
||||
}
|
||||
baseURL.BasePath = u.Path
|
||||
baseURL.BasePathNoTrailingSlash = strings.TrimSuffix(u.Path, "/")
|
||||
|
||||
return baseURL, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -21,17 +21,24 @@ import (
|
||||
|
||||
func TestBaseURL(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
b, err := NewBaseURLFromString("http://example.com")
|
||||
|
||||
b, err := NewBaseURLFromString("http://example.com/")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com")
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/")
|
||||
|
||||
b, err = NewBaseURLFromString("http://example.com")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/")
|
||||
c.Assert(b.WithPathNoTrailingSlash, qt.Equals, "http://example.com")
|
||||
c.Assert(b.BasePath, qt.Equals, "/")
|
||||
|
||||
p, err := b.WithProtocol("webcal://")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com")
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com/")
|
||||
|
||||
p, err = b.WithProtocol("webcal")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com")
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com/")
|
||||
|
||||
_, err = b.WithProtocol("mailto:")
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
@@ -57,11 +64,18 @@ func TestBaseURL(t *testing.T) {
|
||||
|
||||
b, err = NewBaseURLFromString("")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "")
|
||||
c.Assert(b.String(), qt.Equals, "/")
|
||||
|
||||
// BaseURL with sub path
|
||||
b, err = NewBaseURLFromString("http://example.com/sub")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/sub")
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/sub/")
|
||||
c.Assert(b.WithPathNoTrailingSlash, qt.Equals, "http://example.com/sub")
|
||||
c.Assert(b.BasePath, qt.Equals, "/sub/")
|
||||
c.Assert(b.BasePathNoTrailingSlash, qt.Equals, "/sub")
|
||||
|
||||
b, err = NewBaseURLFromString("http://example.com/sub/")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/sub/")
|
||||
c.Assert(b.HostURL(), qt.Equals, "http://example.com")
|
||||
}
|
||||
|
||||
@@ -52,3 +52,16 @@ func Eq(v1, v2 any) bool {
|
||||
|
||||
return v1 == v2
|
||||
}
|
||||
|
||||
// ProbablyEq returns whether v1 is probably equal to v2.
|
||||
func ProbablyEq(v1, v2 any) bool {
|
||||
if Eq(v1, v2) {
|
||||
return true
|
||||
}
|
||||
|
||||
if peqer, ok := v1.(ProbablyEqer); ok {
|
||||
return peqer.ProbablyEq(v2)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
+176
-87
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -27,8 +27,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"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"
|
||||
"github.com/gohugoio/hugo/config/privacy"
|
||||
@@ -45,6 +47,7 @@ import (
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
"github.com/gohugoio/hugo/resources/images"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
||||
"github.com/spf13/afero"
|
||||
@@ -57,12 +60,11 @@ type InternalConfig struct {
|
||||
// Server mode?
|
||||
Running bool
|
||||
|
||||
Quiet bool
|
||||
Verbose bool
|
||||
Clock string
|
||||
Watch bool
|
||||
DisableLiveReload bool
|
||||
LiveReloadPort int
|
||||
Quiet bool
|
||||
Verbose bool
|
||||
Clock string
|
||||
Watch bool
|
||||
LiveReloadPort int
|
||||
}
|
||||
|
||||
// All non-params config keys for language.
|
||||
@@ -150,7 +152,7 @@ type Config struct {
|
||||
Minify minifiers.MinifyConfig `mapstructure:"-"`
|
||||
|
||||
// Permalink configuration.
|
||||
Permalinks map[string]string `mapstructure:"-"`
|
||||
Permalinks map[string]map[string]string `mapstructure:"-"`
|
||||
|
||||
// Taxonomy configuration.
|
||||
Taxonomies map[string]string `mapstructure:"-"`
|
||||
@@ -191,6 +193,22 @@ type configCompiler interface {
|
||||
func (c Config) cloneForLang() *Config {
|
||||
x := c
|
||||
x.C = nil
|
||||
copyStringSlice := func(in []string) []string {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
|
||||
// Copy all the slices to avoid sharing.
|
||||
x.DisableKinds = copyStringSlice(x.DisableKinds)
|
||||
x.DisableLanguages = copyStringSlice(x.DisableLanguages)
|
||||
x.MainSections = copyStringSlice(x.MainSections)
|
||||
x.IgnoreErrors = copyStringSlice(x.IgnoreErrors)
|
||||
x.IgnoreFiles = copyStringSlice(x.IgnoreFiles)
|
||||
x.Theme = copyStringSlice(x.Theme)
|
||||
|
||||
// Collapse all static dirs to one.
|
||||
x.StaticDir = x.staticDirs()
|
||||
@@ -224,9 +242,14 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
disabledKinds := make(map[string]bool)
|
||||
for _, kind := range c.DisableKinds {
|
||||
kind = strings.ToLower(kind)
|
||||
if kind == "taxonomyterm" {
|
||||
if newKind := kinds.IsDeprecatedAndReplacedWith(kind); newKind != "" {
|
||||
logger.Deprecatef(false, "Kind %q used in disableKinds is deprecated, use %q instead.", kind, newKind)
|
||||
// Legacy config.
|
||||
kind = "term"
|
||||
kind = newKind
|
||||
}
|
||||
if kinds.GetKindAny(kind) == "" {
|
||||
logger.Warnf("Unknown kind %q in disableKinds configuration.", kind)
|
||||
continue
|
||||
}
|
||||
disabledKinds[kind] = true
|
||||
}
|
||||
@@ -234,9 +257,17 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
isRssDisabled := disabledKinds["rss"]
|
||||
outputFormats := c.OutputFormats.Config
|
||||
for kind, formats := range c.Outputs {
|
||||
if newKind := kinds.IsDeprecatedAndReplacedWith(kind); newKind != "" {
|
||||
logger.Deprecatef(false, "Kind %q used in outputs configuration is deprecated, use %q instead.", kind, newKind)
|
||||
kind = newKind
|
||||
}
|
||||
if disabledKinds[kind] {
|
||||
continue
|
||||
}
|
||||
if kinds.GetKindAny(kind) == "" {
|
||||
logger.Warnf("Unknown kind %q in outputs configuration.", kind)
|
||||
continue
|
||||
}
|
||||
for _, format := range formats {
|
||||
if isRssDisabled && format == "rss" {
|
||||
// Legacy config.
|
||||
@@ -253,11 +284,20 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
|
||||
disabledLangs := make(map[string]bool)
|
||||
for _, lang := range c.DisableLanguages {
|
||||
if lang == c.DefaultContentLanguage {
|
||||
return fmt.Errorf("cannot disable default content language %q", lang)
|
||||
}
|
||||
disabledLangs[lang] = true
|
||||
}
|
||||
for lang, language := range c.Languages {
|
||||
if !language.Disabled && disabledLangs[lang] {
|
||||
language.Disabled = true
|
||||
c.Languages[lang] = language
|
||||
}
|
||||
if language.Disabled {
|
||||
disabledLangs[lang] = true
|
||||
if lang == c.DefaultContentLanguage {
|
||||
return fmt.Errorf("cannot disable default content language %q", lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ignoredErrors := make(map[string]bool)
|
||||
for _, err := range c.IgnoreErrors {
|
||||
@@ -370,15 +410,19 @@ type ConfigCompiled struct {
|
||||
}
|
||||
|
||||
// This may be set after the config is compiled.
|
||||
func (c *ConfigCompiled) SetMainSectionsIfNotSet(sections []string) {
|
||||
func (c *ConfigCompiled) SetMainSections(sections []string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.MainSections != nil {
|
||||
return
|
||||
}
|
||||
c.MainSections = sections
|
||||
}
|
||||
|
||||
// IsMainSectionsSet returns whether the main sections have been set.
|
||||
func (c *ConfigCompiled) IsMainSectionsSet() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.MainSections != nil
|
||||
}
|
||||
|
||||
// This is set after the config is compiled by the server command.
|
||||
func (c *ConfigCompiled) SetBaseURL(baseURL, baseURLLiveReload urls.BaseURL) {
|
||||
c.BaseURL = baseURL
|
||||
@@ -387,7 +431,6 @@ func (c *ConfigCompiled) SetBaseURL(baseURL, baseURLLiveReload urls.BaseURL) {
|
||||
|
||||
// RootConfig holds all the top-level configuration options in Hugo
|
||||
type RootConfig struct {
|
||||
|
||||
// The base URL of the site.
|
||||
// Note that the default value is empty, but Hugo requires a valid URL (e.g. "https://example.com/") to work properly.
|
||||
// <docsmeta>{"identifiers": ["URL"] }</docsmeta>
|
||||
@@ -408,10 +451,10 @@ type RootConfig struct {
|
||||
// Copyright information.
|
||||
Copyright string
|
||||
|
||||
// The language to apply to content without any Clolanguage indicator.
|
||||
// The language to apply to content without any language indicator.
|
||||
DefaultContentLanguage string
|
||||
|
||||
// By defefault, we put the default content language in the root and the others below their language ID, e.g. /no/.
|
||||
// By default, we put the default content language in the root and the others below their language ID, e.g. /no/.
|
||||
// Set this to true to put all languages below their language ID.
|
||||
DefaultContentLanguageInSubdir bool
|
||||
|
||||
@@ -430,6 +473,9 @@ type RootConfig struct {
|
||||
// Disable the injection of the Hugo generator tag on the home page.
|
||||
DisableHugoGeneratorInject bool
|
||||
|
||||
// Disable live reloading in server mode.
|
||||
DisableLiveReload bool
|
||||
|
||||
// Enable replacement in Pages' Content of Emoji shortcodes with their equivalent Unicode characters.
|
||||
// <docsmeta>{"identifiers": ["Content", "Unicode"] }</docsmeta>
|
||||
EnableEmoji bool
|
||||
@@ -468,11 +514,8 @@ type RootConfig struct {
|
||||
// Enable to print greppable placeholders (on the form "[i18n] TRANSLATIONID") for missing translation strings.
|
||||
EnableMissingTranslationPlaceholders bool
|
||||
|
||||
// Enable to print warnings for missing translation strings.
|
||||
LogI18nWarnings bool
|
||||
|
||||
// ENable to print warnings for multiple files published to the same destination.
|
||||
LogPathWarnings bool
|
||||
// Enable to panic on warning log entries. This may make it easier to detect the source.
|
||||
PanicOnWarning bool
|
||||
|
||||
// The configured environment. Default is "development" for server and "production" for build.
|
||||
Environment string
|
||||
@@ -506,6 +549,12 @@ type RootConfig struct {
|
||||
// Whether to track and print unused templates during the build.
|
||||
PrintUnusedTemplates bool
|
||||
|
||||
// Enable to print warnings for missing translation strings.
|
||||
PrintI18nWarnings bool
|
||||
|
||||
// ENable to print warnings for multiple files published to the same destination.
|
||||
PrintPathWarnings bool
|
||||
|
||||
// URL to be used as a placeholder when a page reference cannot be found in ref or relref. Is used as-is.
|
||||
RefLinksNotFoundURL string
|
||||
|
||||
@@ -526,7 +575,7 @@ type RootConfig struct {
|
||||
// See Modules for more a more flexible way to load themes.
|
||||
Theme []string
|
||||
|
||||
// Timeout for generating page contents, specified as a duration or in milliseconds.
|
||||
// Timeout for generating page contents, specified as a duration or in seconds.
|
||||
Timeout string
|
||||
|
||||
// The time zone (or location), e.g. Europe/Oslo, used to parse front matter dates without such information and in the time function.
|
||||
@@ -604,13 +653,16 @@ type Configs struct {
|
||||
LanguageConfigMap map[string]*Config
|
||||
LanguageConfigSlice []*Config
|
||||
|
||||
IsMultihost bool
|
||||
Languages langs.Languages
|
||||
LanguagesDefaultFirst langs.Languages
|
||||
IsMultihost bool
|
||||
|
||||
Modules modules.Modules
|
||||
ModulesClient *modules.Client
|
||||
|
||||
// All below is set in Init.
|
||||
Languages langs.Languages
|
||||
LanguagesDefaultFirst langs.Languages
|
||||
ContentPathParser paths.PathParser
|
||||
|
||||
configLangs []config.AllProvider
|
||||
}
|
||||
|
||||
@@ -630,6 +682,58 @@ func (c *Configs) IsZero() bool {
|
||||
}
|
||||
|
||||
func (c *Configs) Init() error {
|
||||
var languages langs.Languages
|
||||
defaultContentLanguage := c.Base.DefaultContentLanguage
|
||||
for k, v := range c.LanguageConfigMap {
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
languages = append(languages, language)
|
||||
}
|
||||
|
||||
// Sort the sites by language weight (if set) or lang.
|
||||
sort.Slice(languages, func(i, j int) bool {
|
||||
li := languages[i]
|
||||
lj := languages[j]
|
||||
if li.Weight != lj.Weight {
|
||||
return li.Weight < lj.Weight
|
||||
}
|
||||
return li.Lang < lj.Lang
|
||||
})
|
||||
|
||||
for _, l := range languages {
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang])
|
||||
}
|
||||
|
||||
// Filter out disabled languages.
|
||||
var n int
|
||||
for _, l := range languages {
|
||||
if !l.Disabled {
|
||||
languages[n] = l
|
||||
n++
|
||||
}
|
||||
}
|
||||
languages = languages[:n]
|
||||
|
||||
var languagesDefaultFirst langs.Languages
|
||||
for _, l := range languages {
|
||||
if l.Lang == defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
for _, l := range languages {
|
||||
if l.Lang != defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
|
||||
c.Languages = languages
|
||||
c.LanguagesDefaultFirst = languagesDefaultFirst
|
||||
c.ContentPathParser = paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet()}
|
||||
|
||||
c.configLangs = make([]config.AllProvider, len(c.Languages))
|
||||
for i, l := range c.LanguagesDefaultFirst {
|
||||
c.configLangs[i] = ConfigLanguage{
|
||||
@@ -649,6 +753,26 @@ func (c *Configs) Init() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// We should consolidate this, but to get a full view of the mounts in e.g. "hugo config" we need to
|
||||
// transfer any default mounts added above to the config used to print the config.
|
||||
for _, m := range c.Modules[0].Mounts() {
|
||||
var found bool
|
||||
for _, cm := range c.Base.Module.Mounts {
|
||||
if cm.Source == m.Source && cm.Target == m.Target && cm.Lang == m.Lang {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
c.Base.Module.Mounts = append(c.Base.Module.Mounts, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer the changed mounts to the language versions (all share the same mount set, but can be displayed in different languages).
|
||||
for _, l := range c.LanguageConfigSlice {
|
||||
l.Module.Mounts = c.Base.Module.Mounts
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -680,13 +804,13 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
cfg := res.Cfg
|
||||
|
||||
all := &Config{}
|
||||
err := decodeConfigFromParams(fs, bcfg, cfg, all, nil)
|
||||
|
||||
err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
langConfigMap := make(map[string]*Config)
|
||||
var langConfigs []*Config
|
||||
|
||||
languagesConfig := cfg.GetStringMap("languages")
|
||||
var isMultiHost bool
|
||||
@@ -720,7 +844,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
// We accidentally allowed it in the past, so we need to support it a little longer,
|
||||
// But log a warning.
|
||||
if _, found := params[kk]; !found {
|
||||
helpers.Deprecated(fmt.Sprintf("config: languages.%s.%s: custom params on the language top level", k, kk), fmt.Sprintf("Put the value below [languages.%s.params]. See https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120", k), false)
|
||||
hugo.Deprecate(fmt.Sprintf("config: languages.%s.%s: custom params on the language top level", k, kk), fmt.Sprintf("Put the value below [languages.%s.params]. See https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120", k), "v0.112.0")
|
||||
params[kk] = vv
|
||||
}
|
||||
}
|
||||
@@ -729,8 +853,8 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
isMultiHost = true
|
||||
}
|
||||
mergedConfig.Set(kk, vv)
|
||||
if cfg.IsSet(kk) {
|
||||
rootv := cfg.Get(kk)
|
||||
rootv := cfg.Get(kk)
|
||||
if rootv != nil && cfg.IsSet(kk) {
|
||||
// This overrides a root key and potentially needs a merge.
|
||||
if !reflect.DeepEqual(rootv, vv) {
|
||||
switch vvv := vv.(type) {
|
||||
@@ -767,12 +891,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
|
||||
// Create a copy of the complete config and replace the root keys with the language specific ones.
|
||||
clone := all.cloneForLang()
|
||||
if err := decodeConfigFromParams(fs, bcfg, mergedConfig, clone, differentRootKeys); err != nil {
|
||||
|
||||
if err := decodeConfigFromParams(fs, logger, bcfg, mergedConfig, clone, differentRootKeys); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config for language %q: %w", k, err)
|
||||
}
|
||||
if err := clone.CompileConfig(logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
langConfigMap[k] = clone
|
||||
case maps.ParamsMergeStrategy:
|
||||
default:
|
||||
@@ -781,61 +907,24 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
}
|
||||
|
||||
var languages langs.Languages
|
||||
defaultContentLanguage := all.DefaultContentLanguage
|
||||
for k, v := range langConfigMap {
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
languages = append(languages, language)
|
||||
}
|
||||
|
||||
// Sort the sites by language weight (if set) or lang.
|
||||
sort.Slice(languages, func(i, j int) bool {
|
||||
li := languages[i]
|
||||
lj := languages[j]
|
||||
if li.Weight != lj.Weight {
|
||||
return li.Weight < lj.Weight
|
||||
}
|
||||
return li.Lang < lj.Lang
|
||||
})
|
||||
|
||||
for _, l := range languages {
|
||||
langConfigs = append(langConfigs, langConfigMap[l.Lang])
|
||||
}
|
||||
|
||||
var languagesDefaultFirst langs.Languages
|
||||
for _, l := range languages {
|
||||
if l.Lang == defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
for _, l := range languages {
|
||||
if l.Lang != defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
|
||||
bcfg.PublishDir = all.PublishDir
|
||||
res.BaseConfig = bcfg
|
||||
all.CommonDirs.CacheDir = bcfg.CacheDir
|
||||
for _, l := range langConfigMap {
|
||||
l.CommonDirs.CacheDir = bcfg.CacheDir
|
||||
}
|
||||
|
||||
cm := &Configs{
|
||||
Base: all,
|
||||
LanguageConfigMap: langConfigMap,
|
||||
LanguageConfigSlice: langConfigs,
|
||||
LoadingInfo: res,
|
||||
IsMultihost: isMultiHost,
|
||||
Languages: languages,
|
||||
LanguagesDefaultFirst: languagesDefaultFirst,
|
||||
Base: all,
|
||||
LanguageConfigMap: langConfigMap,
|
||||
LoadingInfo: res,
|
||||
IsMultihost: isMultiHost,
|
||||
}
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func decodeConfigFromParams(fs afero.Fs, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error {
|
||||
|
||||
func decodeConfigFromParams(fs afero.Fs, logger loggers.Logger, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error {
|
||||
var decoderSetups []decodeWeight
|
||||
|
||||
if len(keys) == 0 {
|
||||
@@ -847,7 +936,7 @@ func decodeConfigFromParams(fs afero.Fs, bcfg config.BaseConfig, p config.Provid
|
||||
if v, found := allDecoderSetups[key]; found {
|
||||
decoderSetups = append(decoderSetups, v)
|
||||
} else {
|
||||
return fmt.Errorf("unknown config key %q", key)
|
||||
logger.Warnf("Skip unknown config key %q", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -884,11 +973,11 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string][]string {
|
||||
}
|
||||
|
||||
m := map[string][]string{
|
||||
page.KindPage: {htmlOut.Name},
|
||||
page.KindHome: defaultListTypes,
|
||||
page.KindSection: defaultListTypes,
|
||||
page.KindTerm: defaultListTypes,
|
||||
page.KindTaxonomy: defaultListTypes,
|
||||
kinds.KindPage: {htmlOut.Name},
|
||||
kinds.KindHome: defaultListTypes,
|
||||
kinds.KindSection: defaultListTypes,
|
||||
kinds.KindTerm: defaultListTypes,
|
||||
kinds.KindTaxonomy: defaultListTypes,
|
||||
}
|
||||
|
||||
// May be disabled
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -49,10 +49,11 @@ type decodeConfig struct {
|
||||
}
|
||||
|
||||
type decodeWeight struct {
|
||||
key string
|
||||
decode func(decodeWeight, decodeConfig) error
|
||||
getCompiler func(c *Config) configCompiler
|
||||
weight int
|
||||
key string
|
||||
decode func(decodeWeight, decodeConfig) error
|
||||
getCompiler func(c *Config) configCompiler
|
||||
weight int
|
||||
internalOrDeprecated bool // Hide it from the docs.
|
||||
}
|
||||
|
||||
var allDecoderSetups = map[string]decodeWeight{
|
||||
@@ -60,7 +61,14 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "",
|
||||
weight: -100, // Always first.
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
return mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig)
|
||||
if err := mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This need to match with Lang which is always lower case.
|
||||
p.c.RootConfig.DefaultContentLanguage = strings.ToLower(p.c.RootConfig.DefaultContentLanguage)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"imaging": {
|
||||
@@ -131,8 +139,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
},
|
||||
},
|
||||
"mediaTypes": {
|
||||
key: "mediaTypes",
|
||||
"mediatypes": {
|
||||
key: "mediatypes",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.MediaTypes, err = media.DecodeTypes(p.p.GetStringMap(d.key))
|
||||
@@ -143,7 +151,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "outputs",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
|
||||
m := p.p.GetStringMap("outputs")
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
|
||||
p.c.Outputs = make(map[string][]string)
|
||||
for k, v := range m {
|
||||
s := types.ToStringSlicePreserveString(v)
|
||||
@@ -161,8 +169,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"outputFormats": {
|
||||
key: "outputFormats",
|
||||
"outputformats": {
|
||||
key: "outputformats",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.OutputFormats, err = output.DecodeConfig(p.c.MediaTypes.Config, p.p.Get(d.key))
|
||||
@@ -199,8 +207,9 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"permalinks": {
|
||||
key: "permalinks",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Permalinks = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
return nil
|
||||
var err error
|
||||
p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.GetStringMap(d.key))
|
||||
return err
|
||||
},
|
||||
},
|
||||
"sitemap": {
|
||||
@@ -231,7 +240,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
} else {
|
||||
p.c.Related = related.DefaultConfig
|
||||
if _, found := p.c.Taxonomies["tag"]; found {
|
||||
p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80})
|
||||
p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80, Type: related.TypeBasic})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -241,8 +250,41 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "languages",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.Languages, err = langs.DecodeConfig(p.p.GetStringMap(d.key))
|
||||
return err
|
||||
m := p.p.GetStringMap(d.key)
|
||||
if len(m) == 1 {
|
||||
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
|
||||
var first maps.Params
|
||||
var ok bool
|
||||
for _, v := range m {
|
||||
first, ok = v.(maps.Params)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
if _, found := first["languagecode"]; !found {
|
||||
first["languagecode"] = p.p.GetString("languagecode")
|
||||
}
|
||||
}
|
||||
}
|
||||
p.c.Languages, err = langs.DecodeConfig(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate defaultContentLanguage.
|
||||
var found bool
|
||||
for lang := range p.c.Languages {
|
||||
if lang == p.c.DefaultContentLanguage {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"cascade": {
|
||||
@@ -296,16 +338,18 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"author": {
|
||||
key: "author",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Author = p.p.GetStringMap(d.key)
|
||||
p.c.Author = maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
},
|
||||
"social": {
|
||||
key: "social",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Social = p.p.GetStringMapString(d.key)
|
||||
p.c.Social = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
},
|
||||
"uglyurls": {
|
||||
key: "uglyurls",
|
||||
@@ -321,11 +365,29 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
}
|
||||
return nil
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
},
|
||||
"internal": {
|
||||
key: "internal",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
return mapstructure.WeakDecode(p.p.GetStringMap(d.key), &p.c.Internal)
|
||||
},
|
||||
internalOrDeprecated: true,
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for k, v := range allDecoderSetups {
|
||||
// Verify that k and v.key is all lower case.
|
||||
if k != strings.ToLower(k) {
|
||||
panic(fmt.Sprintf("key %q is not lower case", k))
|
||||
}
|
||||
if v.key != strings.ToLower(v.key) {
|
||||
panic(fmt.Sprintf("key %q is not lower case", v.key))
|
||||
}
|
||||
|
||||
if k != v.key {
|
||||
panic(fmt.Sprintf("key %q is not the same as the map key %q", k, v.key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@ package allconfig
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
@@ -41,6 +42,21 @@ func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
|
||||
return c.m.LanguagesDefaultFirst
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) PathParser() paths.PathParser {
|
||||
return c.m.ContentPathParser
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) LanguagePrefix() string {
|
||||
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.Language().Lang {
|
||||
return c.Language().Lang
|
||||
}
|
||||
|
||||
if !c.IsMultiLingual() || c.DefaultContentLanguage() == c.Language().Lang {
|
||||
return ""
|
||||
}
|
||||
return c.Language().Lang
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) BaseURL() urls.BaseURL {
|
||||
return c.config.C.BaseURL
|
||||
}
|
||||
@@ -109,6 +125,10 @@ func (c ConfigLanguage) Quiet() bool {
|
||||
return c.m.Base.Internal.Quiet
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) Watching() bool {
|
||||
return c.m.Base.Internal.Watch
|
||||
}
|
||||
|
||||
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
|
||||
func (c ConfigLanguage) GetConfigSection(s string) any {
|
||||
switch s {
|
||||
@@ -130,7 +150,7 @@ func (c ConfigLanguage) GetConfigSection(s string) any {
|
||||
return c.config.Permalinks
|
||||
case "minify":
|
||||
return c.config.Minify
|
||||
case "activeModules":
|
||||
case "allModules":
|
||||
return c.m.Modules
|
||||
case "deployment":
|
||||
return c.config.Deployment
|
||||
@@ -199,8 +219,8 @@ func (c ConfigLanguage) EnableMissingTranslationPlaceholders() bool {
|
||||
return c.config.EnableMissingTranslationPlaceholders
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) LogI18nWarnings() bool {
|
||||
return c.config.LogI18nWarnings
|
||||
func (c ConfigLanguage) PrintI18nWarnings() bool {
|
||||
return c.config.PrintI18nWarnings
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) CreateTitle(s string) string {
|
||||
@@ -218,3 +238,7 @@ func (c ConfigLanguage) PaginatePath() string {
|
||||
func (c ConfigLanguage) StaticDirs() []string {
|
||||
return c.config.staticDirs()
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) EnableEmoji() bool {
|
||||
return c.config.EnableEmoji
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -11,19 +11,22 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
package allconfig
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"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.
|
||||
func init() {
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
|
||||
cfg := New()
|
||||
for _, configRoot := range ConfigRootKeys {
|
||||
cfg := config.New()
|
||||
for configRoot, v := range allDecoderSetups {
|
||||
if v.internalOrDeprecated {
|
||||
continue
|
||||
}
|
||||
cfg.Set(configRoot, make(maps.Params))
|
||||
}
|
||||
lang := maps.Params{
|
||||
@@ -38,7 +41,7 @@ func init() {
|
||||
configHelpers := map[string]any{
|
||||
"mergeStrategy": cfg.Get(""),
|
||||
}
|
||||
return docshelper.DocProvider{"config": configHelpers}
|
||||
return docshelper.DocProvider{"config_helpers": configHelpers}
|
||||
}
|
||||
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
func TestDirsMount(t *testing.T) {
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
@@ -44,7 +43,7 @@ Title: {{ .Title }}
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
|
||||
).Build()
|
||||
|
||||
//b.AssertFileContent("public/p1/index.html", "Title: p1")
|
||||
// b.AssertFileContent("public/p1/index.html", "Title: p1")
|
||||
|
||||
sites := b.H.Sites
|
||||
b.Assert(len(sites), qt.Equals, 2)
|
||||
@@ -58,14 +57,30 @@ Title: {{ .Title }}
|
||||
enConcp := sites[0].Conf
|
||||
enConf := enConcp.GetConfig().(*allconfig.Config)
|
||||
|
||||
b.Assert(enConcp.BaseURL().String(), qt.Equals, "https://example.com")
|
||||
b.Assert(enConcp.BaseURL().String(), qt.Equals, "https://example.com/")
|
||||
modConf := enConf.Module
|
||||
b.Assert(modConf.Mounts, qt.HasLen, 2)
|
||||
b.Assert(modConf.Mounts, qt.HasLen, 8)
|
||||
b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en"))
|
||||
b.Assert(modConf.Mounts[0].Target, qt.Equals, "content")
|
||||
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en")
|
||||
b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv"))
|
||||
b.Assert(modConf.Mounts[1].Target, qt.Equals, "content")
|
||||
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
|
||||
|
||||
}
|
||||
|
||||
func TestConfigAliases(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
logI18nWarnings = true
|
||||
logPathWarnings = true
|
||||
`
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
|
||||
).Build()
|
||||
|
||||
conf := b.H.Configs.Base
|
||||
|
||||
b.Assert(conf.PrintI18nWarnings, qt.Equals, true)
|
||||
b.Assert(conf.PrintPathWarnings, qt.Equals, true)
|
||||
}
|
||||
|
||||
+44
-19
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"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")
|
||||
|
||||
func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
@@ -45,7 +46,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
}
|
||||
|
||||
if d.Logger == nil {
|
||||
d.Logger = loggers.NewErrorLogger()
|
||||
d.Logger = loggers.NewDefault()
|
||||
}
|
||||
|
||||
l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
|
||||
@@ -78,19 +79,21 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
if err := configs.transientErr(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config from modules config: %w", err)
|
||||
}
|
||||
configs.LoadingInfo.ConfigFiles = append(configs.LoadingInfo.ConfigFiles, l.ModulesConfigFiles...)
|
||||
} else if err := configs.transientErr(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config: %w", err)
|
||||
}
|
||||
|
||||
configs.Modules = moduleConfig.ActiveModules
|
||||
configs.Modules = moduleConfig.AllModules
|
||||
configs.ModulesClient = modulesClient
|
||||
|
||||
if err := configs.Init(); err != nil {
|
||||
return nil, fmt.Errorf("failed to init config: %w", err)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning)
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
|
||||
@@ -134,7 +137,11 @@ type configLoader struct {
|
||||
|
||||
// Handle some legacy values.
|
||||
func (l configLoader) applyConfigAliases() error {
|
||||
aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}}
|
||||
aliases := []types.KeyValueStr{
|
||||
{Key: "indexes", Value: "taxonomies"},
|
||||
{Key: "logI18nWarnings", Value: "printI18nWarnings"},
|
||||
{Key: "logPathWarnings", Value: "printPathWarnings"},
|
||||
}
|
||||
|
||||
for _, alias := range aliases {
|
||||
if l.cfg.IsSet(alias.Key) {
|
||||
@@ -284,17 +291,44 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
|
||||
} else {
|
||||
l.cfg.Set(env.Key, val)
|
||||
}
|
||||
} else if nestedKey != "" {
|
||||
owner[nestedKey] = env.Value
|
||||
} else {
|
||||
// The container does not exist yet.
|
||||
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
|
||||
if nestedKey != "" {
|
||||
owner[nestedKey] = env.Value
|
||||
} else {
|
||||
var val any
|
||||
key := strings.ReplaceAll(env.Key, delim, ".")
|
||||
_, ok := allDecoderSetups[key]
|
||||
if ok {
|
||||
// A map.
|
||||
if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{}); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
if val == nil {
|
||||
// A string.
|
||||
val = l.envStringToVal(key, env.Value)
|
||||
}
|
||||
l.cfg.Set(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *configLoader) envStringToVal(k, v string) any {
|
||||
switch k {
|
||||
case "disablekinds", "disablelanguages":
|
||||
if strings.Contains(v, ",") {
|
||||
return strings.Split(v, ",")
|
||||
} else {
|
||||
return strings.Fields(v)
|
||||
}
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConfigResult, modules.ModulesConfig, error) {
|
||||
var res config.LoadConfigResult
|
||||
|
||||
@@ -433,7 +467,7 @@ func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *mo
|
||||
ex := hexec.New(conf.Security)
|
||||
|
||||
hook := func(m *modules.ModulesConfig) error {
|
||||
for _, tc := range m.ActiveModules {
|
||||
for _, tc := range m.AllModules {
|
||||
if len(tc.ConfigFilenames()) > 0 {
|
||||
if tc.Watch() {
|
||||
l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...)
|
||||
@@ -533,15 +567,6 @@ func (l configLoader) deleteMergeStrategies() {
|
||||
})
|
||||
}
|
||||
|
||||
func (l configLoader) loadModulesConfig() (modules.Config, error) {
|
||||
modConfig, err := modules.DecodeConfig(l.cfg)
|
||||
if err != nil {
|
||||
return modules.Config{}, err
|
||||
}
|
||||
|
||||
return modConfig, nil
|
||||
}
|
||||
|
||||
func (l configLoader) wrapFileError(err error, filename string) error {
|
||||
fe := herrors.UnwrapFileError(err)
|
||||
if fe != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user