mirror of
https://github.com/gohugoio/hugo.git
synced 2026-09-01 19:22:38 +00:00
Compare commits
474 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6a4ae4ad5 | |||
| 84de0c3255 | |||
| a074102262 | |||
| da4406ea50 | |||
| 166862a096 | |||
| 82c33c7105 | |||
| 41e9e9fe14 | |||
| ade966b84b | |||
| a93d4ba64e | |||
| 110512a65c | |||
| f503b63957 | |||
| 30aba7fb09 | |||
| 04a3b45db4 | |||
| 0cc39af682 | |||
| ed6fd26ce8 | |||
| 7fa66425aa | |||
| 69210cfdf3 | |||
| 4b36498a85 | |||
| aa5ac36a3e | |||
| 3a977485eb | |||
| 0f248606d5 | |||
| e82cbd746f | |||
| 3339c2bb61 | |||
| 03bbdba8be | |||
| a772b8fc38 | |||
| dce49d1333 | |||
| 61c5b7a2e6 | |||
| 1d60bd1efa | |||
| 75c9b893d9 | |||
| 64e1613fb3 | |||
| b959ecbc81 | |||
| 70e454812e | |||
| 355ff83e74 | |||
| 471ed91c60 | |||
| 94a5bac5b2 | |||
| 9830ca9e31 | |||
| b64fd0577b | |||
| 2706437a7d | |||
| ec7c993cfe | |||
| 32c6f656d9 | |||
| e02e0727e5 | |||
| 096f5e1921 | |||
| ba35e69856 | |||
| c7957c90e8 | |||
| bb05377033 | |||
| 9185e11eff | |||
| 168a3aab46 | |||
| 8bcfa3bdf6 | |||
| d7331aaa7e | |||
| cd4e67af18 | |||
| e6ad1f0e76 | |||
| 625d2c257a | |||
| e6e44b7c41 | |||
| 64abc83fc4 | |||
| f8d132d731 | |||
| 26f1919ae0 | |||
| e55466ce70 | |||
| ecf025f006 | |||
| fab1e43de5 | |||
| e03f82eef2 | |||
| a864ffe9ac | |||
| b49da33280 | |||
| 7c21eca74f | |||
| 13ad8408fc | |||
| 1cabf61ddf | |||
| 268e3069f3 | |||
| 3efc2e2af8 | |||
| 5bc547389a | |||
| bb3254385a | |||
| 9b1d6d7e30 | |||
| e1ead4dbca | |||
| 5b59b9c171 | |||
| acc5eb5b51 | |||
| 6cacfa329c | |||
| 4d03cd780e | |||
| 8b14fdbf89 | |||
| fc21b63b46 | |||
| 6631c9c7e0 | |||
| cf73cc2ece | |||
| 7d1f806ecb | |||
| 04b5959961 | |||
| c278b6e45d | |||
| 107c86febb | |||
| a0489c2dfd | |||
| 0fc2ce9e4b | |||
| 32569285c1 | |||
| 5a46eefbc6 | |||
| 7a15edafe2 | |||
| 2f0945bafe | |||
| f4ffeea71d | |||
| 7ba3f3d201 | |||
| f70165242b | |||
| bc0743ed8e | |||
| ffa2fe6117 | |||
| d966f5d08d | |||
| 3f38c785b7 | |||
| 24589c0814 | |||
| efebd756eb | |||
| 58b6742cfe | |||
| 32ead4b1e7 | |||
| abd969a670 | |||
| 3e11072892 | |||
| 9bba9a3a98 | |||
| d6c8cd7718 | |||
| dfe54d3216 | |||
| b0c541e496 | |||
| 2ed7be295c | |||
| d9d1ef8b07 | |||
| ec1c1c3459 | |||
| c7fd134625 | |||
| a5d2632ebb | |||
| 494f284be3 | |||
| bf738d2f43 | |||
| 8d19850e2d | |||
| 0934983529 | |||
| bd77f6e1c9 | |||
| 1c5b025dd0 | |||
| c13d33dd5e | |||
| d70c485707 | |||
| 9ff17c3324 | |||
| 7aaaf7e33a | |||
| 3a96655592 | |||
| 9a7383caf3 | |||
| 6c70e1f22f | |||
| 4d221ce468 | |||
| e3dc5240f0 | |||
| 268065cb2d | |||
| e0304c06ed | |||
| 7aa8b1cd7d | |||
| f6821b88ab | |||
| 580d320a66 | |||
| b75d4526e4 | |||
| 94b616bdfa | |||
| e90b3591a1 | |||
| 4b7da6a9d7 | |||
| 7907d24ba1 | |||
| 726fe9c3c9 | |||
| b5de37ee79 | |||
| bf301daf15 | |||
| a3701e0931 | |||
| 40b6016cf3 | |||
| 7e1305710f | |||
| efa5760db5 | |||
| a57dda854b | |||
| f9afba9335 | |||
| a5d2ba429d | |||
| 31972f3647 | |||
| 2e58782f96 | |||
| 7b68f6524d | |||
| 81265af2cc | |||
| c102c9719b | |||
| 7c0f904f29 | |||
| b2fbd4d13a | |||
| 90041d1b6d | |||
| 05047096f5 | |||
| a469156ea4 | |||
| 18fdd85bcc | |||
| aeb1935d44 | |||
| c7252224c4 | |||
| e09d7882c8 | |||
| 15c0eed048 | |||
| 91cbb96302 | |||
| a352d19d88 | |||
| 11bb67dcfd | |||
| 41c6c52ead | |||
| d270eaf4fc | |||
| b2e67505f8 | |||
| 32508045d3 | |||
| e521c9a36d | |||
| dfb1cc4315 | |||
| 0294a4a4f8 | |||
| 53a352795a | |||
| 351ed0f569 | |||
| d831d2fce8 | |||
| 634481ba8c | |||
| c19f65f956 | |||
| 022c479551 | |||
| f27e542442 | |||
| ae6cf93c84 | |||
| a70da2b74a | |||
| 805664818d | |||
| ee3d2bb1d3 | |||
| eb2a500367 | |||
| 5cb52c2315 | |||
| 30eea3915b | |||
| 8f40f34cd1 | |||
| 8ddbc95466 | |||
| 724d5db580 | |||
| 875fe4050c | |||
| 56362e4bc7 | |||
| f5cfb9e251 | |||
| 04dc469fbd | |||
| 07919d1ccb | |||
| f75f90079a | |||
| e31b1d1946 | |||
| 43a23239b2 | |||
| 4479f09c9c | |||
| 7eb0e10a80 | |||
| 24ce98b6d1 | |||
| 0019d60f67 | |||
| e451b984cf | |||
| b4d60b3db1 | |||
| 020e4acee4 | |||
| 4c34faf426 | |||
| a339f62664 | |||
| 4c8552b114 | |||
| 34e4742f0c | |||
| bffa2a2a98 | |||
| a1b0353ccb | |||
| bc6f84c581 | |||
| 80410257e3 | |||
| 6c8c0c8b6a | |||
| 49fedbc51c | |||
| 8290720107 | |||
| e0c67958f1 | |||
| f6b9ce6ebd | |||
| 4b03399b78 | |||
| 40dfdd0952 | |||
| 3a6dc6d3f4 | |||
| 6cd2110ab2 | |||
| 6a365c2712 | |||
| 19aa95fc7f | |||
| 923dd9d1c1 | |||
| d9bdd37d35 | |||
| b2eaf4c8c2 | |||
| efb6ee6c1c | |||
| 4bd65e224f | |||
| ff2266300a | |||
| 8677cfb04f | |||
| 093dacab29 | |||
| 4a9d408fe0 | |||
| 9312059888 | |||
| a7e3da242f | |||
| b70a12ec45 | |||
| 4dd90050f1 | |||
| 3d544c9ae4 | |||
| 2c4689f7b8 | |||
| 9c0860f749 | |||
| a1694b06af | |||
| 219ec38007 | |||
| be6b901cf7 | |||
| 643b671931 | |||
| ab4e1dfa4e | |||
| 02f31897b4 | |||
| 402da3f8f3 | |||
| 92405e5b0a | |||
| a074f758b0 | |||
| bb2aa08709 | |||
| 9096842b04 | |||
| 3aa7f0b27f | |||
| 9b870aa788 | |||
| 93aad3c543 | |||
| 5af045ebab | |||
| 8eafe0845d | |||
| 31fb29fb3f | |||
| d392893cd7 | |||
| a886dd53b8 | |||
| a91cd7652f | |||
| 162f41d0ef | |||
| 32ba623541 | |||
| 552cef5c57 | |||
| 73483d0f9e | |||
| 9b5debe4b8 | |||
| 12530519d8 | |||
| f55d2f4376 | |||
| 282f1aa3db | |||
| fcd63de3a5 | |||
| 150d75738b | |||
| 06d295427f | |||
| 26ae12c0c6 | |||
| ee733085b7 | |||
| 01758f99b9 | |||
| 845a7ba4fc | |||
| 10f60de89a | |||
| 785a31b5b8 | |||
| bc1e05286a | |||
| f518b4f71e | |||
| dc6b7a75ff | |||
| 3f515f0e33 | |||
| a9bcd38181 | |||
| ef0f1a7269 | |||
| abbc99d4c6 | |||
| 76c95f55a5 | |||
| 2c7f5b62f6 | |||
| c46fc838a9 | |||
| 504c78da4b | |||
| b660ea8d54 | |||
| 64f88f3011 | |||
| 7a2c10ae60 | |||
| e1c328df25 | |||
| 5afe0a57d0 | |||
| 8900e33918 | |||
| 9753e1b9c5 | |||
| ececd1b122 | |||
| b0ca723eb2 | |||
| 4c65ceccc7 | |||
| 57a471a0db | |||
| 9b63af55bd | |||
| 23fc65832c | |||
| e7b5e36e04 | |||
| a9b52b4175 | |||
| b073a1c972 | |||
| 4227cc1bd3 | |||
| c239c643fe | |||
| d7b22aee46 | |||
| 3cc4fdd6f3 | |||
| 7eb80a9e6f | |||
| 78c1a6a7c6 | |||
| f6745ad358 | |||
| f523e9f0fd | |||
| 63cd05ce5a | |||
| 902535ef11 | |||
| 70aebba04d | |||
| 3e3b7d4474 | |||
| c13d368746 | |||
| 048418ba74 | |||
| eebde0c2ac | |||
| e4dc9a82b5 | |||
| 0d86a32d8f | |||
| 65c502cc81 | |||
| 537c905ec1 | |||
| 243951ebe9 | |||
| fe2ee02802 | |||
| 316d65cd70 | |||
| b95229ab49 | |||
| 0551df090e | |||
| bca40cf0c9 | |||
| 057e5a22af | |||
| fd96f65a3d | |||
| 8f7891e70c | |||
| 07b8d9466d | |||
| d3a64708f4 | |||
| 3b56244f42 | |||
| f5d3d635e6 | |||
| 0d3c42da56 | |||
| ef34dd8f0e | |||
| bc80022e03 | |||
| 2bb9496ce2 | |||
| fda3c4d1ed | |||
| 60618210b8 | |||
| f8b064f3c0 | |||
| 4713e509b6 | |||
| 6e9d2bf0c9 | |||
| e73f7a770d | |||
| 3ddffd064d | |||
| 6fc52d185a | |||
| 73c3ae818a | |||
| 7ca118fdfd | |||
| 33d5f80592 | |||
| 509d39fa6d | |||
| 7725c41d40 | |||
| 5d36d80153 | |||
| 9b34d42bb2 | |||
| c2d8f87cfc | |||
| cbc246616e | |||
| fa432b17b3 | |||
| 0cd55c66d3 | |||
| 0d5cf256e4 | |||
| 36527576b3 | |||
| 9b83f45b6d | |||
| 7fdd2b95e2 | |||
| 3d5dbdcb1a | |||
| 8a308944e4 | |||
| 7b4ade56dd | |||
| 7c7974b711 | |||
| 5656a908d8 | |||
| 2dc222cec4 | |||
| 4d22ad580e | |||
| 5e2f128911 | |||
| 97934779ed | |||
| 9d96078439 | |||
| 1efd93c097 | |||
| 584a9b3e2e | |||
| 86b4fd35e7 | |||
| 195d108da7 | |||
| 81689af799 | |||
| c94aa5cf46 | |||
| df8bb8812f | |||
| 57d8d208ed | |||
| fc06e85082 | |||
| b725253f9e | |||
| 35dedf15c0 | |||
| 7ed56c6941 | |||
| 24c716cac3 | |||
| 137d2dab32 | |||
| ba1d0051b4 | |||
| 1b1dcf586e | |||
| f6612d8bd8 | |||
| 0a2ab3f8fe | |||
| 4d24e2a326 | |||
| 18074d0c23 | |||
| 35bfb66222 | |||
| 6d21559fb5 | |||
| ba16a14c6e | |||
| 782c79ae61 | |||
| 5afcae7e0b | |||
| 60469f429e | |||
| aed7df62a8 | |||
| cd0c5d7ef3 | |||
| e7e194435b | |||
| 9d31f650da | |||
| 88a85dcea9 | |||
| 7f8530039a | |||
| 01dd7c16af | |||
| c8f45d1d86 | |||
| b3504a0ee8 | |||
| 59d15c97d8 | |||
| 9e2d086ca7 | |||
| b65518ac63 | |||
| 0e9cd8128d | |||
| fe77f7434b | |||
| 9e99950c6e | |||
| 1b364b003f | |||
| acb9109df7 | |||
| 7d0a261981 | |||
| c60806550a | |||
| ffd9dac421 | |||
| 88b93a09dc | |||
| 29fb456c9e | |||
| 718fba7d63 | |||
| 9650e56841 | |||
| e77b2e3aa0 | |||
| b5485aeae7 | |||
| ccb822eb5a | |||
| 21e9eb18ac | |||
| ae57ba6a9d | |||
| cf3e077da3 | |||
| 66beac99c6 | |||
| 968dd7a711 | |||
| 38f29e817f | |||
| cd87813aa0 | |||
| 4e815b063e | |||
| 652a59d385 | |||
| 84f0ec7f80 | |||
| bdfbcf6f4b | |||
| b60e9279ab | |||
| a9b0fea6a3 | |||
| e8df097745 | |||
| 3a5ee0d2d6 | |||
| 4ffaeaf155 | |||
| bf55afd71f | |||
| 5f621df257 | |||
| 7118f89cf3 | |||
| e6dd312812 | |||
| 2681633db8 | |||
| 1b24728256 | |||
| 441b11beec | |||
| 4867cd1dea | |||
| 92c6c40419 | |||
| d36fd5b3ee | |||
| a7c515e1b5 | |||
| 216b00f358 | |||
| 144943798c | |||
| 440fdb0eb9 | |||
| b2a48dce58 | |||
| 35def0ae45 | |||
| 2f9dadae40 | |||
| ed3071b753 | |||
| ee9c136763 | |||
| 32b86076ee | |||
| 241b7483ea | |||
| e19a046c4b | |||
| a1fe552fc9 | |||
| a1a9f088b1 | |||
| e48ffb7635 | |||
| 07ad283f68 | |||
| 2c8b5d9165 | |||
| 0004a733c8 | |||
| 8a26ab0bc5 | |||
| 788e50ad3a | |||
| 4d2b6fc4c0 | |||
| 212e5e5542 | |||
| 4fdec67b11 | |||
| 9b681ecfbd |
@@ -1,6 +1,6 @@
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: bepsays/ci-goreleaser:1.15.1
|
||||
- image: bepsays/ci-goreleaser:1.17.2
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support
|
||||
- name: SUPPORT, ISSUES and TROUBLESHOOTING
|
||||
url: https://discourse.gohugo.io/
|
||||
about: Please do not use Github for support requests. Visit https://discourse.gohugo.io for support!
|
||||
about: Please DO NOT use Github for support requests. Please visit https://discourse.gohugo.io for support! You will be helped much faster there. If you ignore this request your issue might be closed with a discourse label.
|
||||
|
||||
+11
-10
@@ -7,7 +7,7 @@ jobs:
|
||||
GO111MODULE: on
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.14.x, 1.15.x]
|
||||
go-version: [1.16.x, 1.17.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -48,23 +48,23 @@ jobs:
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.5/sass_embedded-1.0.0-beta.5-linux-x64.tar.gz;
|
||||
echo "642738beaea4ef1b9168446bc105267a2948a5e939537f5bd5afb48159140a44 sass_embedded-1.0.0-beta.5-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.5-linux-x64.tar.gz;
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-linux-x64.tar.gz;
|
||||
echo "04fc1e5e28d29a4585a701941b6dace56771d94bfbe7f9e4db28d24417ceeec3 sass_embedded-1.0.0-beta.6-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.6-linux-x64.tar.gz;
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Install dart-sass-embedded MacOS
|
||||
run: |
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.5/sass_embedded-1.0.0-beta.5-macos-x64.tar.gz;
|
||||
echo "47b55a39126155f89fdfb8eea7c19ba976b3f6fadbdb6867e5582a18137bd180 sass_embedded-1.0.0-beta.5-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.5-macos-x64.tar.gz;
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-macos-x64.tar.gz;
|
||||
echo "b3b984675a9b04aa22f6f2302dda4191b507ac2ca124467db2dfe7e58e72fbad sass_embedded-1.0.0-beta.6-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf sass_embedded-1.0.0-beta.6-macos-x64.tar.gz;
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'windows-latest'
|
||||
name: Install dart-sass-embedded Windows
|
||||
run: |
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.5/sass_embedded-1.0.0-beta.5-windows-x64.zip;
|
||||
echo "5e65c0d8cbe038b6a120a3e7f390ad731708998f37c2de8ba565c51746a4588c sass_embedded-1.0.0-beta.5-windows-x64.zip" | sha256sum -c;
|
||||
unzip sass_embedded-1.0.0-beta.5-windows-x64.zip;
|
||||
curl -LJO https://github.com/sass/dart-sass-embedded/releases/download/1.0.0-beta.6/sass_embedded-1.0.0-beta.6-windows-x64.zip;
|
||||
echo "6ae442129dbb3334bc21ef851261da6c0c1b560da790ca2e1350871d00ab816d sass_embedded-1.0.0-beta.6-windows-x64.zip" | sha256sum -c;
|
||||
unzip sass_embedded-1.0.0-beta.6-windows-x64.zip;
|
||||
echo "$env:GITHUB_WORKSPACE/sass_embedded/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -75,6 +75,7 @@ jobs:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
HUGO_TIMEOUT: 31000
|
||||
HUGO_IGNOREERRORS: error-remote-getjson
|
||||
HUGO_SERVICES_INSTAGRAM_ACCESSTOKEN: dummytoken
|
||||
run: |
|
||||
mage -v hugo
|
||||
./hugo -s docs/
|
||||
|
||||
+3
-1
@@ -50,7 +50,9 @@ Hugo has become a fully featured static site generator, so any new functionality
|
||||
|
||||
If it is of some complexity, the contributor is expected to maintain and support the new feature in the future (answer questions on the forum, fix any bugs etc.).
|
||||
|
||||
It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin. If you are submitting a complex feature, create a small design proposal on the [Hugo issue tracker](https://github.com/gohugoio/hugo/issues) before you start.
|
||||
It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin.
|
||||
|
||||
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
|
||||
|
||||
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
|
||||
We have one exception to this rule which is LibSASS.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.15-alpine AS build
|
||||
FROM golang:1.16-alpine AS build
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/static/images/hugo-logo-wide.svg?sanitize=true" alt="Hugo" width="565">
|
||||
<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](http://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][].
|
||||
|
||||
@@ -29,7 +29,7 @@ Hugo is designed to work well for any kind of website including blogs, tumbles,
|
||||
|
||||
#### Supported Architectures
|
||||
|
||||
Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD, DragonFly BSD, Open BSD, macOS (Darwin), and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -54,7 +54,7 @@ Use the [installation instructions in the Hugo documentation](https://gohugo.io/
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Go (we test it with the last 2 major versions)](https://golang.org/dl/)
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.81.0 only builds with >= Go 1.16.)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
@@ -119,78 +119,106 @@ If you are unfamiliar with the contribution process, the Hugo team will help you
|
||||
|
||||
For a complete guide to contributing code to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
|
||||
[Go]: https://golang.org/
|
||||
[Hugo Documentation]: https://gohugo.io/overview/introduction/
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries, in lexical order:
|
||||
Hugo stands on the shoulder of many great open source libraries.
|
||||
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
|
||||
In Hugo 0.89.0 that list is, in lexical order:
|
||||
|
||||
```
|
||||
cloud.google.com/go/storage="v1.10.0"
|
||||
cloud.google.com/go="v0.87.0"
|
||||
github.com/Azure/azure-pipeline-go="v0.2.2"
|
||||
github.com/Azure/azure-storage-blob-go="v0.9.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/BurntSushi/toml="v0.3.1"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma="v0.9.4"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go="v1.41.14"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/godartsass="v0.12.0"
|
||||
github.com/bep/golibsass="v1.0.0"
|
||||
github.com/bep/gowebp="v0.1.0"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/cli/safeexec="v1.0.0"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.0"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.4.0"
|
||||
github.com/dustin/go-humanize="v1.0.0"
|
||||
github.com/evanw/esbuild="v0.13.12"
|
||||
github.com/fsnotify/fsnotify="v1.5.1"
|
||||
github.com/getkin/kin-openapi="v0.80.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.2.3"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v0.14.0"
|
||||
github.com/golang/groupcache="v0.0.0-20200121045136-8c9f03a8e57e"
|
||||
github.com/golang/protobuf="v1.5.2"
|
||||
github.com/google/go-cmp="v0.5.6"
|
||||
github.com/google/uuid="v1.1.2"
|
||||
github.com/google/wire="v0.4.0"
|
||||
github.com/googleapis/gax-go/v2="v2.0.5"
|
||||
github.com/googleapis/gax-go="v2.0.2+incompatible"
|
||||
github.com/gorilla/websocket="v1.4.2"
|
||||
github.com/inconshreveable/mousetrap="v1.0.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.8"
|
||||
github.com/mailru/easyjson="v0.0.0-20190626092158-b2ccc519800e"
|
||||
github.com/mattn/go-ieproxy="v0.0.1"
|
||||
github.com/mattn/go-isatty="v0.0.14"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/miekg/mmark="v1.3.6"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.4.2"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.5.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml/v2="v2.0.0-beta.3.0.20210727221244-fa0796069526"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.8.0"
|
||||
github.com/russross/blackfriday/v2="v2.0.1"
|
||||
github.com/russross/blackfriday="v1.5.3-0.20200218234912-41c5fccfd6f6"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.1"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/shurcooL/sanitized_anchor_name="v1.0.0"
|
||||
github.com/spf13/afero="v1.6.0"
|
||||
github.com/spf13/cast="v1.4.1"
|
||||
github.com/spf13/cobra="v1.2.1"
|
||||
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.9.22"
|
||||
github.com/tdewolff/parse/v2="v2.5.21"
|
||||
github.com/webmproject/libwebp="v1.2.0"
|
||||
github.com/yuin/goldmark-highlighting="v0.0.0-20200307114337-60d527fdb691"
|
||||
github.com/yuin/goldmark="v1.4.2"
|
||||
go.opencensus.io="v0.23.0"
|
||||
gocloud.dev="v0.20.0"
|
||||
golang.org/x/image="v0.0.0-20210220032944-ac19c3e999fb"
|
||||
golang.org/x/net="v0.0.0-20210614182718-04defd469f4e"
|
||||
golang.org/x/oauth2="v0.0.0-20210628180205-a41e5a781914"
|
||||
golang.org/x/sync="v0.0.0-20210220032951-036812b2e83c"
|
||||
golang.org/x/sys="v0.0.0-20210908233432-aa78b53d3365"
|
||||
golang.org/x/text="v0.3.7"
|
||||
golang.org/x/xerrors="v0.0.0-20200804184101-5ec99f83aff1"
|
||||
google.golang.org/api="v0.51.0"
|
||||
google.golang.org/genproto="v0.0.0-20210716133855-ce7ef5c701ea"
|
||||
google.golang.org/grpc="v1.39.0"
|
||||
google.golang.org/protobuf="v1.27.1"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
```
|
||||
|
||||
| Dependency | License |
|
||||
| :------------- | :------------- |
|
||||
| [github.com/alecthomas/chroma](https://github.com/alecthomas/chroma) | MIT License |
|
||||
| [github.com/armon/go-radix](https://github.com/armon/go-radix) | MIT License |
|
||||
| [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) | Apache License 2.0 |
|
||||
| [github.com/bep/debounce](https://github.com/bep/debounce) | MIT License |
|
||||
| [github.com/bep/gitmap](https://github.com/bep/gitmap) | MIT License |
|
||||
| [github.com/bep/golibsass](https://github.com/bep/golibsass) | MIT License |
|
||||
| [github.com/bep/tmc](https://github.com/bep/tmc) | MIT License |
|
||||
| [github.com/BurntSushi/locker](https://github.com/BurntSushi/locker) | The Unlicense |
|
||||
| [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | MIT License |
|
||||
| [github.com/cpuguy83/go-md2man](https://github.com/cpuguy83/go-md2man) | MIT License |
|
||||
| [github.com/danwakefield/fnmatch](https://github.com/danwakefield/fnmatch) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/disintegration/gift](https://github.com/disintegration/gift) | MIT License |
|
||||
| [github.com/dustin/go-humanize](https://github.com/dustin/go-humanize) | MIT License |
|
||||
| [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/gobwas/glob](https://github.com/gobwas/glob) | MIT License |
|
||||
| [github.com/gorilla/websocket](https://github.com/gorilla/websocket) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) | Mozilla Public License 2.0 |
|
||||
| [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | Mozilla Public License 2.0 |
|
||||
| [github.com/jdkato/prose](https://github.com/jdkato/prose) | MIT License |
|
||||
| [github.com/kr/pretty](https://github.com/kr/pretty) | MIT License |
|
||||
| [github.com/kyokomi/emoji](https://github.com/kyokomi/emoji) | MIT License |
|
||||
| [github.com/magiconair/properties](https://github.com/magiconair/properties) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/markbates/inflect](https://github.com/markbates/inflect) | MIT License |
|
||||
| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | MIT License |
|
||||
| [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | MIT License |
|
||||
| [github.com/miekg/mmark](https://github.com/miekg/mmark) | Simplified BSD License |
|
||||
| [github.com/mitchellh/hashstructure](https://github.com/mitchellh/hashstructure) | MIT License |
|
||||
| [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | MIT License |
|
||||
| [github.com/muesli/smartcrop](https://github.com/muesli/smartcrop) | MIT License |
|
||||
| [github.com/nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) | MIT License |
|
||||
| [github.com/niklasfasching/go-org](https://github.com/niklasfasching/go-org) | MIT License |
|
||||
| [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) | MIT License |
|
||||
| [github.com/pelletier/go-toml](https://github.com/pelletier/go-toml) | MIT License |
|
||||
| [github.com/pkg/errors](https://github.com/pkg/errors) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/PuerkitoBio/purell](https://github.com/PuerkitoBio/purell) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/PuerkitoBio/urlesc](https://github.com/PuerkitoBio/urlesc) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/russross/blackfriday](https://github.com/russross/blackfriday) | Simplified BSD License |
|
||||
| [github.com/rwcarlsen/goexif](https://github.com/rwcarlsen/goexif) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/spf13/afero](https://github.com/spf13/afero) | Apache License 2.0 |
|
||||
| [github.com/spf13/cast](https://github.com/spf13/cast) | MIT License |
|
||||
| [github.com/spf13/cobra](https://github.com/spf13/cobra) | Apache License 2.0 |
|
||||
| [github.com/spf13/fsync](https://github.com/spf13/fsync) | MIT License |
|
||||
| [github.com/spf13/jwalterweatherman](https://github.com/spf13/jwalterweatherman) | MIT License |
|
||||
| [github.com/spf13/pflag](https://github.com/spf13/pflag) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/spf13/viper](https://github.com/spf13/viper) | MIT License |
|
||||
| [github.com/tdewolff/minify](https://github.com/tdewolff/minify) | MIT License |
|
||||
| [github.com/tdewolff/parse](https://github.com/tdewolff/parse) | MIT License |
|
||||
| [github.com/yuin/goldmark](https://github.com/yuin/goldmark) | MIT License |
|
||||
| [github.com/yuin/goldmark-highlighting](https://github.com/yuin/goldmark-highlighting) | MIT License |
|
||||
| [go.opencensus.io](https://go.opencensus.io) | Apache License 2.0 |
|
||||
| [go.uber.org/atomic](https://go.uber.org/atomic) | MIT License |
|
||||
| [gocloud.dev](https://gocloud.dev) | Apache License 2.0 |
|
||||
| [golang.org/x/image](https://golang.org/x/image) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/net](https://golang.org/x/net) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/oauth2](https://golang.org/x/oauth2) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/sync](https://golang.org/x/sync) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/sys](https://golang.org/x/sys) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/text](https://golang.org/x/text) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/xerrors](https://golang.org/x/xerrors) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [google.golang.org/api](https://google.golang.org/api) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [google.golang.org/genproto](https://google.golang.org/genproto) | Apache License 2.0 |
|
||||
| [gopkg.in/ini.v1](https://gopkg.in/ini.v1) | Apache License 2.0 |
|
||||
| [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) | Apache License 2.0 |
|
||||
|
||||
Vendored
+6
-1
@@ -19,6 +19,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -123,6 +125,9 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
_, isOsFs := fs.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
if _, ok := v.(maps.Params); !ok {
|
||||
continue
|
||||
}
|
||||
cc := defaultCacheConfig
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
@@ -137,7 +142,7 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
|
||||
}
|
||||
|
||||
if err := decoder.Decode(v); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "failed to decode filecache config")
|
||||
}
|
||||
|
||||
if cc.Dir == "" {
|
||||
|
||||
+2
-3
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestDecodeConfig(t *testing.T) {
|
||||
@@ -178,8 +177,8 @@ dir = "/"
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
}
|
||||
|
||||
func newTestConfig() *viper.Viper {
|
||||
cfg := viper.New()
|
||||
func newTestConfig() config.Provider {
|
||||
cfg := config.New()
|
||||
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
|
||||
+34
-14
@@ -61,6 +61,11 @@ type commandeer struct {
|
||||
logger loggers.Logger
|
||||
serverConfig *config.Server
|
||||
|
||||
// Loading state
|
||||
mustHaveConfigFile bool
|
||||
failOnInitErr bool
|
||||
running bool
|
||||
|
||||
// Currently only set when in "fast render mode". But it seems to
|
||||
// be fast enough that we could maybe just add it for all server modes.
|
||||
changeDetector *fileChangeDetector
|
||||
@@ -153,7 +158,7 @@ func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, cfgInit func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
var rebuildDebouncer func(f func())
|
||||
if running {
|
||||
// The time value used is tested with mass content replacements in a fairly big Hugo site.
|
||||
@@ -175,11 +180,17 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla
|
||||
visitedURLs: types.NewEvictingStringQueue(10),
|
||||
debounce: rebuildDebouncer,
|
||||
fullRebuildSem: semaphore.NewWeighted(1),
|
||||
|
||||
// Init state
|
||||
mustHaveConfigFile: mustHaveConfigFile,
|
||||
failOnInitErr: failOnInitErr,
|
||||
running: running,
|
||||
|
||||
// This will be replaced later, but we need something to log to before the configuration is read.
|
||||
logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, ioutil.Discard, running),
|
||||
}
|
||||
|
||||
return c, c.loadConfig(mustHaveConfigFile, running)
|
||||
return c, c.loadConfig()
|
||||
}
|
||||
|
||||
type fileChangeDetector struct {
|
||||
@@ -244,7 +255,7 @@ func (f *fileChangeDetector) PrepareNew() {
|
||||
f.current = make(map[string]string)
|
||||
}
|
||||
|
||||
func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
func (c *commandeer) loadConfig() error {
|
||||
if c.DepsCfg == nil {
|
||||
c.DepsCfg = &deps.DepsCfg{}
|
||||
}
|
||||
@@ -256,7 +267,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
|
||||
cfg := c.DepsCfg
|
||||
c.configured = false
|
||||
cfg.Running = running
|
||||
cfg.Running = c.running
|
||||
|
||||
var dir string
|
||||
if c.h.source != "" {
|
||||
@@ -270,7 +281,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
sourceFs = c.DepsCfg.Fs.Source
|
||||
}
|
||||
|
||||
environment := c.h.getEnvironment(running)
|
||||
environment := c.h.getEnvironment(c.running)
|
||||
|
||||
doWithConfig := func(cfg config.Provider) error {
|
||||
if c.ftch != nil {
|
||||
@@ -303,15 +314,22 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
WorkingDir: dir,
|
||||
Filename: c.h.cfgFile,
|
||||
AbsConfigDir: c.h.getConfigDir(dir),
|
||||
Environ: os.Environ(),
|
||||
Environment: environment,
|
||||
},
|
||||
cfgSetAndInit,
|
||||
doWithConfig)
|
||||
|
||||
if err != nil && mustHaveConfigFile {
|
||||
return err
|
||||
} else if mustHaveConfigFile && len(configFiles) == 0 {
|
||||
if err != nil {
|
||||
// We should improve the error handling here,
|
||||
// but with hugo mod init and similar there is a chicken and egg situation
|
||||
// with modules already configured in config.toml, so ignore those errors.
|
||||
if c.mustHaveConfigFile || (c.failOnInitErr && !moduleNotFoundRe.MatchString(err.Error())) {
|
||||
return err
|
||||
} else {
|
||||
// Just make it a warning.
|
||||
c.logger.Warnln(err)
|
||||
}
|
||||
} else if c.mustHaveConfigFile && len(configFiles) == 0 {
|
||||
return hugolib.ErrNoConfigFile
|
||||
}
|
||||
|
||||
@@ -323,7 +341,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
|
||||
// Set some commonly used flags
|
||||
c.doLiveReload = running && !c.Cfg.GetBool("disableLiveReload")
|
||||
c.doLiveReload = c.running && !c.Cfg.GetBool("disableLiveReload")
|
||||
c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
|
||||
c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
|
||||
|
||||
@@ -335,7 +353,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := c.createLogger(config, running)
|
||||
logger, err := c.createLogger(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -395,7 +413,11 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
|
||||
var h *hugolib.HugoSites
|
||||
|
||||
h, err = hugolib.NewHugoSites(*c.DepsCfg)
|
||||
var createErr error
|
||||
h, createErr = hugolib.NewHugoSites(*c.DepsCfg)
|
||||
if h == nil || c.failOnInitErr {
|
||||
err = createErr
|
||||
}
|
||||
c.hugoSites = h
|
||||
close(c.created)
|
||||
})
|
||||
@@ -410,7 +432,5 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
|
||||
cfg.Logger.Infoln("Using config file:", config.ConfigFileUsed())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+12
-2
@@ -162,13 +162,21 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
// prevent cobra printing error so it can be handled here (before the timeTrack prints)
|
||||
cmd.SilenceErrors = true
|
||||
|
||||
c, err := initializeConfig(true, true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
return err
|
||||
}
|
||||
cc.c = c
|
||||
|
||||
return c.build()
|
||||
err = c.build()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
return err
|
||||
},
|
||||
})
|
||||
|
||||
@@ -204,6 +212,7 @@ type hugoBuilderCommon struct {
|
||||
environment string
|
||||
|
||||
buildWatch bool
|
||||
poll string
|
||||
|
||||
gc bool
|
||||
|
||||
@@ -291,6 +300,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages")
|
||||
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&cc.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().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -29,7 +31,6 @@ import (
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
@@ -120,7 +121,7 @@ func TestExecute(t *testing.T) {
|
||||
resp := Execute([]string{"new", "site", siteDir, "-e=staging"})
|
||||
c.Assert(resp.Err, qt.IsNil)
|
||||
config := readFileFrom(c, filepath.Join(siteDir, "config.toml"))
|
||||
c.Assert(config, qt.Contains, "baseURL = \"http://example.org/\"")
|
||||
c.Assert(config, qt.Contains, "baseURL = 'http://example.org/'")
|
||||
checkNewSiteInited(c, siteDir)
|
||||
})
|
||||
}
|
||||
@@ -166,7 +167,7 @@ func TestFlags(t *testing.T) {
|
||||
name: "ignoreVendor as bool",
|
||||
args: []string{"server", "--ignoreVendor"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := viper.New()
|
||||
cfg := config.New()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendor"), qt.Equals, true)
|
||||
},
|
||||
@@ -176,7 +177,7 @@ func TestFlags(t *testing.T) {
|
||||
name: "ignoreVendorPaths",
|
||||
args: []string{"server", "--ignoreVendorPaths=github.com/**"},
|
||||
check: func(c *qt.C, cmd *serverCmd) {
|
||||
cfg := viper.New()
|
||||
cfg := config.New()
|
||||
cmd.flagsToConfig(cfg)
|
||||
c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
|
||||
},
|
||||
@@ -216,7 +217,7 @@ func TestFlags(t *testing.T) {
|
||||
c.Assert(sc.serverPort, qt.Equals, 1366)
|
||||
c.Assert(sc.environment, qt.Equals, "testing")
|
||||
|
||||
cfg := viper.New()
|
||||
cfg := config.New()
|
||||
sc.flagsToConfig(cfg)
|
||||
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
|
||||
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
|
||||
|
||||
+51
-12
@@ -21,6 +21,9 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
@@ -28,7 +31,6 @@ import (
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*configCmd)(nil)
|
||||
@@ -60,7 +62,7 @@ func (b *commandsBuilder) newConfigCmd() *configCmd {
|
||||
}
|
||||
|
||||
func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, nil)
|
||||
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,7 +70,7 @@ func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
|
||||
allModules := cfg.Cfg.Get("allmodules").(modules.Modules)
|
||||
|
||||
for _, m := range allModules {
|
||||
if err := parser.InterfaceToConfig(&modMounts{m: m}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
if err := parser.InterfaceToConfig(&modMounts{m: m, verbose: c.verbose}, metadecoders.JSON, os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -76,12 +78,12 @@ func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, nil)
|
||||
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allSettings := cfg.Cfg.(*viper.Viper).AllSettings()
|
||||
allSettings := cfg.Cfg.Get("").(maps.Params)
|
||||
|
||||
// We need to clean up this, but we store objects in the config that
|
||||
// isn't really interesting to the end user, so filter these.
|
||||
@@ -114,7 +116,8 @@ func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
type modMounts struct {
|
||||
m modules.Module
|
||||
verbose bool
|
||||
m modules.Module
|
||||
}
|
||||
|
||||
type modMount struct {
|
||||
@@ -134,13 +137,49 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
var ownerPath string
|
||||
if m.m.Owner() != nil {
|
||||
ownerPath = m.m.Owner().Path()
|
||||
}
|
||||
|
||||
if m.verbose {
|
||||
config := m.m.Config()
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Meta map[string]interface{} `json:"meta"`
|
||||
HugoVersion modules.HugoVersion `json:"hugoVersion"`
|
||||
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
Time: m.m.Time(),
|
||||
Owner: ownerPath,
|
||||
Dir: m.m.Dir(),
|
||||
Meta: config.Params,
|
||||
HugoVersion: config.HugoVersion,
|
||||
Mounts: mounts,
|
||||
})
|
||||
}
|
||||
|
||||
return json.Marshal(&struct {
|
||||
Path string `json:"path"`
|
||||
Dir string `json:"dir"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Time time.Time `json:"time"`
|
||||
Owner string `json:"owner"`
|
||||
Dir string `json:"dir"`
|
||||
Mounts []modMount `json:"mounts"`
|
||||
}{
|
||||
Path: m.m.Path(),
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
Path: m.m.Path(),
|
||||
Version: m.m.Version(),
|
||||
Time: m.m.Time(),
|
||||
Owner: ownerPath,
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ func (cc *convertCmd) convertContents(format metadecoders.Format) error {
|
||||
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, nil)
|
||||
c, err := initializeConfig(true, false, false, &cc.hugoBuilderCommon, cc, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ documentation.
|
||||
c.Set("maxDeletes", cc.maxDeletes)
|
||||
return nil
|
||||
}
|
||||
comm, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
comm, err := initializeConfig(true, true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+16
-1
@@ -16,6 +16,8 @@ package commands
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -31,15 +33,28 @@ func newEnvCmd() *envCmd {
|
||||
baseCmd: newBaseCmd(&cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Print Hugo version and environment info",
|
||||
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.`,
|
||||
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.
|
||||
|
||||
If you add the -v flag, you will get a full dependency list.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
printHugoVersion()
|
||||
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
|
||||
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
|
||||
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
|
||||
|
||||
isVerbose, _ := cmd.Flags().GetBool("verbose")
|
||||
|
||||
if isVerbose {
|
||||
deps := hugo.GetDependencyList()
|
||||
for _, dep := range deps {
|
||||
jww.FEEDBACK.Printf("%s\n", dep)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+34
-20
@@ -14,6 +14,9 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -23,7 +26,7 @@ var _ cmder = (*genautocompleteCmd)(nil)
|
||||
type genautocompleteCmd struct {
|
||||
autocompleteTarget string
|
||||
|
||||
// bash for now (zsh and others will come)
|
||||
// bash, zsh, fish or powershell
|
||||
autocompleteType string
|
||||
|
||||
*baseCmd
|
||||
@@ -37,43 +40,54 @@ func newGenautocompleteCmd() *genautocompleteCmd {
|
||||
Short: "Generate shell autocompletion script for Hugo",
|
||||
Long: `Generates a shell autocompletion script for Hugo.
|
||||
|
||||
NOTE: The current version supports Bash only.
|
||||
This should work for *nix systems with Bash installed.
|
||||
The script is written to the console (stdout).
|
||||
|
||||
By default, the file is written directly to /etc/bash_completion.d
|
||||
for convenience, and the command may need superuser rights, e.g.:
|
||||
To write to file, add the ` + "`--completionfile=/path/to/file`" + ` flag.
|
||||
|
||||
$ sudo hugo gen autocomplete
|
||||
|
||||
Add ` + "`--completionfile=/path/to/file`" + ` flag to set alternative
|
||||
file-path and name.
|
||||
Add ` + "`--type={bash, zsh, fish or powershell}`" + ` flag to set alternative
|
||||
shell type.
|
||||
|
||||
Logout and in again to reload the completion scripts,
|
||||
or just source them in directly:
|
||||
|
||||
$ . /etc/bash_completion`,
|
||||
$ . /etc/bash_completion or /path/to/file`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cc.autocompleteType != "bash" {
|
||||
return newUserError("Only Bash is supported for now")
|
||||
var err error
|
||||
var target io.Writer
|
||||
|
||||
if cc.autocompleteTarget == "" {
|
||||
target = os.Stdout
|
||||
} else {
|
||||
target, _ = os.OpenFile(cc.autocompleteTarget, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
switch cc.autocompleteType {
|
||||
case "bash":
|
||||
err = cmd.Root().GenBashCompletion(target)
|
||||
case "zsh":
|
||||
err = cmd.Root().GenZshCompletion(target)
|
||||
case "fish":
|
||||
err = cmd.Root().GenFishCompletion(target, true)
|
||||
case "powershell":
|
||||
err = cmd.Root().GenPowerShellCompletion(target)
|
||||
default:
|
||||
return newUserError("Unsupported completion type")
|
||||
}
|
||||
|
||||
err := cmd.Root().GenBashCompletionFile(cc.autocompleteTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Bash completion file for Hugo saved to", cc.autocompleteTarget)
|
||||
|
||||
if cc.autocompleteTarget != "" {
|
||||
jww.FEEDBACK.Println(cc.autocompleteType+" completion file for Hugo saved to", cc.autocompleteTarget)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)")
|
||||
|
||||
// For bash-completion
|
||||
cc.cmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{})
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "f", "", "autocompletion file, defaults to stdout")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "t", "bash", "autocompletion type (bash, zsh, fish, or powershell)")
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ func createGenChromaStyles() *genChromaStyles {
|
||||
baseCmd: newBaseCmd(&cobra.Command{
|
||||
Use: "chromastyles",
|
||||
Short: "Generate CSS stylesheet for the Chroma code highlighter",
|
||||
Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if pygmentsUseClasses is enabled in config.
|
||||
Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
|
||||
|
||||
See https://help.farbox.com/pygments.html for preview of available styles`,
|
||||
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ See https://help.farbox.com/pygments.html for preview of available styles`,
|
||||
return g.generate()
|
||||
}
|
||||
|
||||
g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://help.farbox.com/pygments.html)")
|
||||
g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
|
||||
|
||||
|
||||
+7
-5
@@ -18,7 +18,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -36,7 +35,6 @@ type genDocCmd struct {
|
||||
|
||||
func newGenDocCmd() *genDocCmd {
|
||||
const gendocFrontmatterTemplate = `---
|
||||
date: %s
|
||||
title: "%s"
|
||||
slug: %s
|
||||
url: %s
|
||||
@@ -57,6 +55,12 @@ It creates one Markdown file per command with front matter suitable
|
||||
for rendering in Hugo.`,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmd.VisitParents(func(c *cobra.Command) {
|
||||
// Disable the "Auto generated by spf13/cobra on DATE"
|
||||
// as it creates a lot of diffs.
|
||||
c.DisableAutoGenTag = true
|
||||
})
|
||||
|
||||
if !strings.HasSuffix(cc.gendocdir, helpers.FilePathSeparator) {
|
||||
cc.gendocdir += helpers.FilePathSeparator
|
||||
}
|
||||
@@ -66,19 +70,17 @@ for rendering in Hugo.`,
|
||||
return err
|
||||
}
|
||||
}
|
||||
now := time.Now().Format("2006-01-02")
|
||||
prepender := func(filename string) string {
|
||||
name := filepath.Base(filename)
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
url := "/commands/" + strings.ToLower(base) + "/"
|
||||
return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.Replace(base, "_", " ", -1), base, url)
|
||||
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
|
||||
}
|
||||
|
||||
linkHandler := func(name string) string {
|
||||
base := strings.TrimSuffix(name, path.Ext(name))
|
||||
return "/commands/" + strings.ToLower(base) + "/"
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", cc.gendocdir, "...")
|
||||
doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler)
|
||||
jww.FEEDBACK.Println("Done.")
|
||||
|
||||
+60
-31
@@ -30,6 +30,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
@@ -37,6 +41,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
|
||||
@@ -113,11 +118,11 @@ func Execute(args []string) Response {
|
||||
}
|
||||
|
||||
// InitializeConfig initializes a config file with sensible default configuration flags.
|
||||
func initializeConfig(mustHaveConfigFile, running bool,
|
||||
func initializeConfig(mustHaveConfigFile, failOnInitErr, running bool,
|
||||
h *hugoBuilderCommon,
|
||||
f flagsToConfigHandler,
|
||||
cfgInit func(c *commandeer) error) (*commandeer, error) {
|
||||
c, err := newCommandeer(mustHaveConfigFile, running, h, f, cfgInit)
|
||||
c, err := newCommandeer(mustHaveConfigFile, failOnInitErr, running, h, f, cfgInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,7 +130,7 @@ func initializeConfig(mustHaveConfigFile, running bool,
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *commandeer) createLogger(cfg config.Provider, running bool) (loggers.Logger, error) {
|
||||
func (c *commandeer) createLogger(cfg config.Provider) (loggers.Logger, error) {
|
||||
var (
|
||||
logHandle = ioutil.Discard
|
||||
logThreshold = jww.LevelWarn
|
||||
@@ -169,7 +174,7 @@ func (c *commandeer) createLogger(cfg config.Provider, running bool) (loggers.Lo
|
||||
loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
|
||||
helpers.InitLoggers()
|
||||
|
||||
return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil
|
||||
return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, c.running), nil
|
||||
}
|
||||
|
||||
func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
||||
@@ -275,7 +280,8 @@ func isTerminal() bool {
|
||||
return terminal.IsTerminal(os.Stdout)
|
||||
}
|
||||
|
||||
func (c *commandeer) fullBuild() error {
|
||||
func (c *commandeer) fullBuild(noBuildLock bool) error {
|
||||
|
||||
var (
|
||||
g errgroup.Group
|
||||
langCount map[string]uint64
|
||||
@@ -283,6 +289,7 @@ func (c *commandeer) fullBuild() error {
|
||||
|
||||
if !c.h.quiet {
|
||||
fmt.Println("Start building sites … ")
|
||||
fmt.Println(hugo.BuildVersionString())
|
||||
if isTerminal() {
|
||||
defer func() {
|
||||
fmt.Print(showCursor + clearLine)
|
||||
@@ -299,7 +306,7 @@ func (c *commandeer) fullBuild() error {
|
||||
return nil
|
||||
}
|
||||
buildSitesFunc := func() error {
|
||||
if err := c.buildSites(); err != nil {
|
||||
if err := c.buildSites(noBuildLock); err != nil {
|
||||
return errors.Wrap(err, "Error building site")
|
||||
}
|
||||
return nil
|
||||
@@ -492,7 +499,7 @@ func (c *commandeer) build() error {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := c.fullBuild(); err != nil {
|
||||
if err := c.fullBuild(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -521,7 +528,7 @@ func (c *commandeer) build() error {
|
||||
|
||||
c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
|
||||
c.logger.Println("Press Ctrl+C to stop")
|
||||
watcher, err := c.newWatcher(watchDirs...)
|
||||
watcher, err := c.newWatcher(c.h.poll, watchDirs...)
|
||||
checkErr(c.Logger, err)
|
||||
defer watcher.Close()
|
||||
|
||||
@@ -535,7 +542,6 @@ func (c *commandeer) build() error {
|
||||
}
|
||||
|
||||
func (c *commandeer) serverBuild() error {
|
||||
defer c.timeTrack(time.Now(), "Built")
|
||||
|
||||
stopProfiling, err := c.initProfiling()
|
||||
if err != nil {
|
||||
@@ -548,7 +554,7 @@ func (c *commandeer) serverBuild() error {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := c.fullBuild(); err != nil {
|
||||
if err := c.fullBuild(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -694,7 +700,7 @@ func (c *commandeer) getDirList() ([]string, error) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
filenames = append(filenames, fi.Meta().Filename())
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -703,7 +709,7 @@ func (c *commandeer) getDirList() ([]string, error) {
|
||||
watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs()
|
||||
for _, fi := range watchFiles {
|
||||
if !fi.IsDir() {
|
||||
filenames = append(filenames, fi.Meta().Filename())
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -718,8 +724,8 @@ func (c *commandeer) getDirList() ([]string, error) {
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
func (c *commandeer) buildSites() (err error) {
|
||||
return c.hugo().Build(hugolib.BuildCfg{})
|
||||
func (c *commandeer) buildSites(noBuildLock bool) (err error) {
|
||||
return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: noBuildLock})
|
||||
}
|
||||
|
||||
func (c *commandeer) handleBuildErr(err error, msg string) {
|
||||
@@ -733,7 +739,6 @@ func (c *commandeer) handleBuildErr(err error, msg string) {
|
||||
}
|
||||
|
||||
func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
|
||||
defer c.timeTrack(time.Now(), "Total")
|
||||
|
||||
c.buildErr = nil
|
||||
visited := c.visitedURLs.PeekAllSet()
|
||||
@@ -748,7 +753,7 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
|
||||
visited[home] = true
|
||||
}
|
||||
}
|
||||
return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
|
||||
return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
|
||||
}
|
||||
|
||||
func (c *commandeer) partialReRender(urls ...string) error {
|
||||
@@ -760,7 +765,7 @@ func (c *commandeer) partialReRender(urls ...string) error {
|
||||
for _, url := range urls {
|
||||
visited[url] = true
|
||||
}
|
||||
return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
|
||||
return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
|
||||
}
|
||||
|
||||
func (c *commandeer) fullRebuild(changeType string) {
|
||||
@@ -790,7 +795,7 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||
defer c.timeTrack(time.Now(), "Rebuilt")
|
||||
|
||||
c.commandeerHugoState = newCommandeerHugoState()
|
||||
err := c.loadConfig(true, true)
|
||||
err := c.loadConfig()
|
||||
if err != nil {
|
||||
// Set the processing on pause until the state is recovered.
|
||||
c.paused = true
|
||||
@@ -807,7 +812,7 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.buildSites()
|
||||
err = c.buildSites(true)
|
||||
if err != nil {
|
||||
c.logger.Errorln(err)
|
||||
} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
|
||||
@@ -818,7 +823,7 @@ func (c *commandeer) fullRebuild(changeType string) {
|
||||
}
|
||||
|
||||
// newWatcher creates a new watcher to watch filesystem events.
|
||||
func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) {
|
||||
if runtime.GOOS == "darwin" {
|
||||
tweakLimit()
|
||||
}
|
||||
@@ -828,7 +833,17 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
watcher, err := watcher.New(1 * time.Second)
|
||||
var pollInterval time.Duration
|
||||
poll := pollIntervalStr != ""
|
||||
if poll {
|
||||
pollInterval, err = types.ToDurationE(pollIntervalStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value for flag poll: %s", err)
|
||||
}
|
||||
c.logger.Printf("Use watcher with poll interval %v", pollInterval)
|
||||
}
|
||||
|
||||
watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -852,13 +867,19 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
for {
|
||||
select {
|
||||
case evs := <-watcher.Events:
|
||||
unlock, err := c.hugo().BaseFs.LockBuild()
|
||||
if err != nil {
|
||||
c.logger.Errorln("Failed to acquire a build lock: %s", err)
|
||||
return
|
||||
}
|
||||
c.handleEvents(watcher, staticSyncer, evs, configSet)
|
||||
if c.showErrorInBrowser && c.errCount() > 0 {
|
||||
// Need to reload browser to show the error
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
if err != nil {
|
||||
unlock()
|
||||
case err := <-watcher.Errors():
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
c.logger.Errorln("Error while watching:", err)
|
||||
}
|
||||
}
|
||||
@@ -1110,9 +1131,13 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
|
||||
|
||||
c.printChangeDetected("")
|
||||
c.changeDetector.PrepareNew()
|
||||
if err := c.rebuildSites(dynamicEvents); err != nil {
|
||||
c.handleBuildErr(err, "Rebuild failed")
|
||||
}
|
||||
|
||||
func() {
|
||||
defer c.timeTrack(time.Now(), "Total")
|
||||
if err := c.rebuildSites(dynamicEvents); err != nil {
|
||||
c.handleBuildErr(err, "Rebuild failed")
|
||||
}
|
||||
}()
|
||||
|
||||
if doLiveReload {
|
||||
if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
|
||||
@@ -1177,12 +1202,16 @@ func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fs
|
||||
func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
|
||||
name := ""
|
||||
|
||||
// Some editors (for example notepad.exe on Windows) triggers a change
|
||||
// both for directory and file. So we pick the longest path, which should
|
||||
// be the file itself.
|
||||
for _, ev := range events {
|
||||
if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) {
|
||||
name = ev.Name
|
||||
if ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create {
|
||||
if files.IsIndexContentFile(ev.Name) {
|
||||
return ev.Name
|
||||
}
|
||||
|
||||
if files.IsContentFile(ev.Name) {
|
||||
name = ev.Name
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,13 @@
|
||||
|
||||
package commands
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
// For time zone lookups on Windows without Go installed.
|
||||
// See #8892
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// This message to show to Windows users if Hugo is opened from explorer.exe
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ func (lc *listCmd) buildSites(config map[string]interface{}) (*hugolib.HugoSites
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, false, &lc.hugoBuilderCommon, lc, cfgInit)
|
||||
c, err := initializeConfig(true, true, false, &lc.hugoBuilderCommon, lc, cfgInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+4
-3
@@ -71,11 +71,12 @@ Also note that if you configure a positive maxAge for the "modules" file cache,
|
||||
if all {
|
||||
com, err := c.initConfig(false)
|
||||
|
||||
if err != nil && !moduleNotFoundRe.MatchString(err.Error()) {
|
||||
if err != nil && com == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = com.hugo().FileCaches.ModulesCache().Prune(true)
|
||||
count, err := com.hugo().FileCaches.ModulesCache().Prune(true)
|
||||
com.logger.Printf("Deleted %d files from module cache.", count)
|
||||
return err
|
||||
}
|
||||
return c.withModsClient(true, func(c *modules.Client) error {
|
||||
@@ -284,7 +285,7 @@ func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
|
||||
}
|
||||
|
||||
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
|
||||
com, err := initializeConfig(failOnNoConfig, false, &c.hugoBuilderCommon, c, nil)
|
||||
com, err := initializeConfig(failOnNoConfig, false, false, &c.hugoBuilderCommon, c, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+3
-13
@@ -71,7 +71,7 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
c, err := initializeConfig(true, true, false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -80,17 +80,7 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
|
||||
createPath := args[0]
|
||||
|
||||
var kind string
|
||||
|
||||
createPath, kind = newContentPathSection(c.hugo(), createPath)
|
||||
|
||||
if n.contentType != "" {
|
||||
kind = n.contentType
|
||||
}
|
||||
|
||||
return create.NewContent(c.hugo(), kind, createPath)
|
||||
return create.NewContent(c.hugo(), n.contentType, args[0])
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
@@ -118,7 +108,7 @@ func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
|
||||
|
||||
if h != nil {
|
||||
for _, dir := range h.BaseFs.Content.Dirs {
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename())
|
||||
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
@@ -29,7 +30,6 @@ import (
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*newSiteCmd)(nil)
|
||||
@@ -102,7 +102,7 @@ func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error
|
||||
|
||||
// Create a default archetype file.
|
||||
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
|
||||
strings.NewReader(create.ArchetypeTemplateTemplate), fs.Source)
|
||||
strings.NewReader(create.DefaultArchetypeTemplateTemplate), fs.Source)
|
||||
|
||||
jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath)
|
||||
jww.FEEDBACK.Println(nextStepsText())
|
||||
@@ -123,7 +123,7 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
|
||||
|
||||
forceNew, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
return n.doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew)
|
||||
return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew)
|
||||
}
|
||||
|
||||
func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
|
||||
|
||||
@@ -38,7 +38,7 @@ func (b *commandsBuilder) newNewThemeCmd() *newThemeCmd {
|
||||
cmd := &cobra.Command{
|
||||
Use: "theme [name]",
|
||||
Short: "Create a new theme",
|
||||
Long: `Create a new theme (skeleton) called [name] in the current directory.
|
||||
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
|
||||
as you see fit.`,
|
||||
@@ -52,7 +52,7 @@ as you see fit.`,
|
||||
|
||||
// newTheme creates a new Hugo theme template
|
||||
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
|
||||
c, err := initializeConfig(false, false, &n.hugoBuilderCommon, n, nil)
|
||||
c, err := initializeConfig(false, false, false, &n.hugoBuilderCommon, n, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+18
-4
@@ -31,6 +31,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
@@ -234,12 +236,24 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
jww.WARN.Println("memstats error:", err)
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit)
|
||||
// silence errors in cobra so we can handle them here
|
||||
cmd.SilenceErrors = true
|
||||
|
||||
c, err := initializeConfig(true, true, true, &sc.hugoBuilderCommon, sc, cfgInit)
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.serverBuild(); err != nil {
|
||||
err = func() error {
|
||||
defer c.timeTrack(time.Now(), "Built")
|
||||
err := c.serverBuild()
|
||||
if err != nil {
|
||||
cmd.PrintErrln("Error:", err.Error())
|
||||
}
|
||||
return err
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -260,7 +274,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
for _, group := range watchGroups {
|
||||
jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
|
||||
}
|
||||
watcher, err := c.newWatcher(watchDirs...)
|
||||
watcher, err := c.newWatcher(sc.poll, watchDirs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -275,7 +289,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
func getRootWatchDirsStr(baseDir string, watchDirs []string) string {
|
||||
relWatchDirs := make([]string, len(watchDirs))
|
||||
for i, dir := range watchDirs {
|
||||
relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseDir)
|
||||
relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir)
|
||||
}
|
||||
|
||||
return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",")
|
||||
|
||||
@@ -22,10 +22,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
@@ -101,7 +101,7 @@ func TestFixURL(t *testing.T) {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
b := newCommandsBuilder()
|
||||
s := b.newServerCmd()
|
||||
v := viper.New()
|
||||
v := config.New()
|
||||
baseURL := test.CLIBaseURL
|
||||
v.Set("baseURL", test.CfgBaseURL)
|
||||
s.serverAppend = test.AppendPort
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
syncer.DestFs = c.Fs.Destination
|
||||
|
||||
// prevent spamming the log on changes
|
||||
logger := helpers.NewDistinctFeedbackLogger()
|
||||
logger := helpers.NewDistinctErrorLogger()
|
||||
|
||||
for _, ev := range staticEvents {
|
||||
// Due to our approach of layering both directories and the content's rendered output
|
||||
|
||||
@@ -100,6 +100,17 @@ func WithFileContextForFile(e error, realFilename, filename string, fs afero.Fs,
|
||||
return WithFileContext(e, realFilename, f, matcher)
|
||||
}
|
||||
|
||||
// WithFileContextForFileDefault tries to add file context using the default line matcher.
|
||||
func WithFileContextForFileDefault(err error, filename string, fs afero.Fs) error {
|
||||
err, _ = WithFileContextForFile(
|
||||
err,
|
||||
filename,
|
||||
filename,
|
||||
fs,
|
||||
SimpleLineMatcher)
|
||||
return err
|
||||
}
|
||||
|
||||
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
|
||||
// If no match could be found, the original error is returned with false as the second return value.
|
||||
func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatcherFn) (error, bool) {
|
||||
|
||||
@@ -36,7 +36,6 @@ func TestToLineNumberError(t *testing.T) {
|
||||
{errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15},
|
||||
{errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1},
|
||||
{errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7},
|
||||
{errors.New("error in front matter: Near line 32 (last key parsed 'title')"), 0, 32, 1},
|
||||
{errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7},
|
||||
} {
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ package herrors
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
@@ -24,8 +28,7 @@ var lineNumberExtractors = []lineNumberExtractor{
|
||||
newLineNumberErrHandlerFromRegexp(".*:(\\d+):"),
|
||||
|
||||
// TOML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(".*Near line (\\d+)(\\s.*)"),
|
||||
|
||||
tomlLineNumberExtractor,
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp("line (\\d+):"),
|
||||
|
||||
@@ -35,6 +38,14 @@ var lineNumberExtractors = []lineNumberExtractor{
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
var tomlLineNumberExtractor = func(e error) (int, int) {
|
||||
e = errors.Cause(e)
|
||||
if terr, ok := e.(*toml.DecodeError); ok {
|
||||
return terr.Position()
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
re := regexp.MustCompile(expression)
|
||||
return extractLineNo(re)
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
// TODO(bep) replace the private versions in /tpl with these.
|
||||
// IsInt returns whether the given kind is a number.
|
||||
// IsNumber returns whether the given kind is a number.
|
||||
func IsNumber(kind reflect.Kind) bool {
|
||||
return IsInt(kind) || IsUint(kind) || IsFloat(kind)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright 2021 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
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
)
|
||||
|
||||
var (
|
||||
longDayNames = []string{
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
}
|
||||
|
||||
shortDayNames = []string{
|
||||
"Sun",
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
}
|
||||
|
||||
shortMonthNames = []string{
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
}
|
||||
|
||||
longMonthNames = []string{
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
}
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
if ltr == nil {
|
||||
panic("must provide a locales.Translator")
|
||||
}
|
||||
return TimeFormatter{
|
||||
ltr: ltr,
|
||||
}
|
||||
}
|
||||
|
||||
// TimeFormatter is locale aware.
|
||||
type TimeFormatter struct {
|
||||
ltr locales.Translator
|
||||
}
|
||||
|
||||
func (f TimeFormatter) Format(t time.Time, layout string) string {
|
||||
if layout == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if layout[0] == ':' {
|
||||
// It may be one of Hugo's custom layouts.
|
||||
switch strings.ToLower(layout[1:]) {
|
||||
case "date_full":
|
||||
return f.ltr.FmtDateFull(t)
|
||||
case "date_long":
|
||||
return f.ltr.FmtDateLong(t)
|
||||
case "date_medium":
|
||||
return f.ltr.FmtDateMedium(t)
|
||||
case "date_short":
|
||||
return f.ltr.FmtDateShort(t)
|
||||
case "time_full":
|
||||
return f.ltr.FmtTimeFull(t)
|
||||
case "time_long":
|
||||
return f.ltr.FmtTimeLong(t)
|
||||
case "time_medium":
|
||||
return f.ltr.FmtTimeMedium(t)
|
||||
case "time_short":
|
||||
return f.ltr.FmtTimeShort(t)
|
||||
}
|
||||
}
|
||||
|
||||
s := t.Format(layout)
|
||||
|
||||
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())) {
|
||||
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())) {
|
||||
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.Time, err error) {
|
||||
switch vv := i.(type) {
|
||||
case toml.LocalDate:
|
||||
return vv.AsTime(location), nil
|
||||
case toml.LocalDateTime:
|
||||
return vv.AsTime(location), nil
|
||||
// issue #8895
|
||||
// datetimes parsed by `go-toml` have empty zone name
|
||||
// convert back them into string and use `cast`
|
||||
case time.Time:
|
||||
i = vv.Format(time.RFC3339)
|
||||
}
|
||||
return cast.ToTimeInDefaultLocationE(i, location)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2021 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
translators "github.com/gohugoio/localescompressed"
|
||||
)
|
||||
|
||||
func TestTimeFormatter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
|
||||
june06 = june06.Add(7777 * time.Second)
|
||||
|
||||
jan06, _ := time.Parse("2006-Jan-02", "2018-Jan-06")
|
||||
jan06 = jan06.Add(32 * time.Second)
|
||||
|
||||
mondayNovemberFirst, _ := time.Parse("2006-Jan-02", "2021-11-01")
|
||||
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
|
||||
|
||||
c.Run("Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "on. on.")
|
||||
})
|
||||
|
||||
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
|
||||
|
||||
})
|
||||
|
||||
c.Run("Custom layouts English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
|
||||
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
|
||||
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
|
||||
|
||||
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
|
||||
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
|
||||
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
|
||||
|
||||
})
|
||||
|
||||
c.Run("English", func(c *qt.C) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("en"))
|
||||
|
||||
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
|
||||
c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "Wed Wed")
|
||||
})
|
||||
|
||||
c.Run("Weekdays German", func(c *qt.C) {
|
||||
tr := translators.GetTranslator("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman)
|
||||
c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman)
|
||||
}
|
||||
|
||||
for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} {
|
||||
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
|
||||
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
|
||||
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
|
||||
}
|
||||
})
|
||||
|
||||
c.Run("Months German", func(c *qt.C) {
|
||||
tr := translators.GetTranslator("de")
|
||||
f := NewTimeFormatter(tr)
|
||||
|
||||
// Issue #9107
|
||||
for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} {
|
||||
date := jan06.Add(time.Duration(i*24*31) * time.Hour)
|
||||
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
|
||||
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkTimeFormatter(b *testing.B) {
|
||||
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
|
||||
|
||||
b.Run("Native", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := june06.Format("Monday Jan 2 2006")
|
||||
if got != "Wednesday Jun 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Localized", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := f.Format(june06, "Monday Jan 2 2006")
|
||||
if got != "onsdag juni 6 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Localized Custom", func(b *testing.B) {
|
||||
f := NewTimeFormatter(translators.GetTranslator("nn"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := f.Format(june06, ":date_medium")
|
||||
if got != "6. juni 2018" {
|
||||
b.Fatalf("invalid format, got %q", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+54
-3
@@ -18,6 +18,8 @@ import (
|
||||
"html/template"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
@@ -34,12 +36,15 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// commitHash contains the current Git revision. Use make to build to make
|
||||
// sure this gets set.
|
||||
// commitHash contains the current Git revision.
|
||||
// Use mage to build to make sure this gets set.
|
||||
commitHash string
|
||||
|
||||
// buildDate contains the date of the current build.
|
||||
buildDate string
|
||||
|
||||
// vendorInfo contains vendor notes about the current build.
|
||||
vendorInfo string
|
||||
)
|
||||
|
||||
// Info contains information about the current Hugo environment
|
||||
@@ -68,6 +73,10 @@ func (i Info) IsProduction() bool {
|
||||
return i.Environment == EnvironmentProduction
|
||||
}
|
||||
|
||||
func (i Info) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(environment string) Info {
|
||||
if environment == "" {
|
||||
@@ -93,10 +102,52 @@ func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
|
||||
if err == nil {
|
||||
for _, fi := range fis {
|
||||
key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
|
||||
value := fi.(hugofs.FileMetaInfo).Meta().Filename()
|
||||
value := fi.(hugofs.FileMetaInfo).Meta().Filename
|
||||
config.SetEnvVars(&env, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// GetDependencyList returns a sorted dependency list on the format package="version".
|
||||
// It includes both Go dependencies and (a manually maintained) list of C(++) dependencies.
|
||||
func GetDependencyList() []string {
|
||||
var deps []string
|
||||
|
||||
formatDep := func(path, version string) string {
|
||||
return fmt.Sprintf("%s=%q", path, version)
|
||||
}
|
||||
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
// TODO(bep) consider adding a DepsNonGo() method to these upstream projects.
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.2.0"),
|
||||
)
|
||||
}
|
||||
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return deps
|
||||
}
|
||||
|
||||
for _, dep := range bi.Deps {
|
||||
deps = append(deps, formatDep(dep.Path, dep.Version))
|
||||
}
|
||||
|
||||
sort.Strings(deps)
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
// IsRunningAsTest reports whether we are running as a test.
|
||||
func IsRunningAsTest() bool {
|
||||
for _, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-test") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ 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.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
|
||||
devHugoInfo := NewInfo("development")
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
|
||||
+11
-3
@@ -127,14 +127,15 @@ func (v Version) NextPatchLevel(level int) Version {
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
program := "Hugo Static Site Generator"
|
||||
// program := "Hugo Static Site Generator"
|
||||
program := "hugo"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
if commitHash != "" {
|
||||
version += "-" + strings.ToUpper(commitHash)
|
||||
}
|
||||
if IsExtended {
|
||||
version += "/extended"
|
||||
version += "+extended"
|
||||
}
|
||||
|
||||
osArch := runtime.GOOS + "/" + runtime.GOARCH
|
||||
@@ -144,7 +145,14 @@ func BuildVersionString() string {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date)
|
||||
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
|
||||
program, version, osArch, date)
|
||||
|
||||
if vendorInfo != "" {
|
||||
versionString += " VendorInfo=" + vendorInfo
|
||||
}
|
||||
|
||||
return versionString
|
||||
}
|
||||
|
||||
func version(version float32, patchVersion int, suffix string) string {
|
||||
|
||||
@@ -16,7 +16,7 @@ package hugo
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Number: 0.80,
|
||||
PatchLevel: 0,
|
||||
Number: 0.89,
|
||||
PatchLevel: 1,
|
||||
Suffix: "",
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
type IgnorableLogger interface {
|
||||
Logger
|
||||
Errorsf(statementID, format string, v ...interface{})
|
||||
Apply(logger Logger) IgnorableLogger
|
||||
}
|
||||
|
||||
type ignorableLogger struct {
|
||||
@@ -55,3 +56,10 @@ ignoreErrors = [%q]`, statementID)
|
||||
|
||||
l.Errorf(format, v...)
|
||||
}
|
||||
|
||||
func (l ignorableLogger) Apply(logger Logger) IgnorableLogger {
|
||||
return ignorableLogger{
|
||||
Logger: logger,
|
||||
statements: l.statements,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ type Logger interface {
|
||||
Println(v ...interface{})
|
||||
PrintTimerIfDelayed(start time.Time, name string)
|
||||
Debug() *log.Logger
|
||||
Debugf(format string, v ...interface{})
|
||||
Debugln(v ...interface{})
|
||||
Info() *log.Logger
|
||||
Infof(format string, v ...interface{})
|
||||
Infoln(v ...interface{})
|
||||
@@ -108,6 +110,14 @@ func (l *logger) Debug() *log.Logger {
|
||||
return l.DEBUG
|
||||
}
|
||||
|
||||
func (l *logger) Debugf(format string, v ...interface{}) {
|
||||
l.DEBUG.Printf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debugln(v ...interface{}) {
|
||||
l.DEBUG.Println(v...)
|
||||
}
|
||||
|
||||
func (l *logger) Infof(format string, v ...interface{}) {
|
||||
l.INFO.Printf(format, v...)
|
||||
}
|
||||
|
||||
+60
-34
@@ -17,54 +17,81 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ToLower makes all the keys in the given map lower cased and will do so
|
||||
// recursively.
|
||||
// Notes:
|
||||
// * This will modify the map given.
|
||||
// * Any nested map[interface{}]interface{} will be converted to Params.
|
||||
func ToLower(m Params) {
|
||||
for k, v := range m {
|
||||
var retyped bool
|
||||
switch v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
var p Params = cast.ToStringMap(v)
|
||||
v = p
|
||||
ToLower(p)
|
||||
retyped = true
|
||||
case map[string]interface{}:
|
||||
var p Params = v.(map[string]interface{})
|
||||
v = p
|
||||
ToLower(p)
|
||||
retyped = true
|
||||
}
|
||||
|
||||
lKey := strings.ToLower(k)
|
||||
if retyped || k != lKey {
|
||||
delete(m, k)
|
||||
m[lKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringMapE converts in to map[string]interface{}.
|
||||
func ToStringMapE(in interface{}) (map[string]interface{}, error) {
|
||||
switch in.(type) {
|
||||
switch vv := in.(type) {
|
||||
case Params:
|
||||
return in.(Params), nil
|
||||
return vv, nil
|
||||
case map[string]string:
|
||||
var m = map[string]interface{}{}
|
||||
for k, v := range vv {
|
||||
m[k] = v
|
||||
}
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
return cast.ToStringMapE(in)
|
||||
}
|
||||
}
|
||||
|
||||
// ToParamsAndPrepare converts in to Params and prepares it for use.
|
||||
// If in is nil, an empty map is returned.
|
||||
// See PrepareParams.
|
||||
func ToParamsAndPrepare(in interface{}) (Params, bool) {
|
||||
if types.IsNil(in) {
|
||||
return Params{}, true
|
||||
}
|
||||
m, err := ToStringMapE(in)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
PrepareParams(m)
|
||||
return m, true
|
||||
}
|
||||
|
||||
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
|
||||
func MustToParamsAndPrepare(in interface{}) Params {
|
||||
if p, ok := ToParamsAndPrepare(in); ok {
|
||||
return p
|
||||
} else {
|
||||
panic(fmt.Sprintf("cannot convert %T to maps.Params", in))
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringMap converts in to map[string]interface{}.
|
||||
func ToStringMap(in interface{}) map[string]interface{} {
|
||||
m, _ := ToStringMapE(in)
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMapStringE converts in to map[string]string.
|
||||
func ToStringMapStringE(in interface{}) (map[string]string, error) {
|
||||
m, err := ToStringMapE(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cast.ToStringMapStringE(m)
|
||||
}
|
||||
|
||||
// ToStringMapString converts in to map[string]string.
|
||||
func ToStringMapString(in interface{}) map[string]string {
|
||||
m, _ := ToStringMapStringE(in)
|
||||
return m
|
||||
}
|
||||
|
||||
// ToStringMapBool converts in to bool.
|
||||
func ToStringMapBool(in interface{}) map[string]bool {
|
||||
m, _ := ToStringMapE(in)
|
||||
return cast.ToStringMapBool(m)
|
||||
}
|
||||
|
||||
// ToSliceStringMap converts in to []map[string]interface{}.
|
||||
func ToSliceStringMap(in interface{}) ([]map[string]interface{}, error) {
|
||||
switch v := in.(type) {
|
||||
case []map[string]interface{}:
|
||||
@@ -127,9 +154,8 @@ func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
|
||||
if k1 == "" {
|
||||
return k2
|
||||
} else {
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestToLower(t *testing.T) {
|
||||
func TestPrepareParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
input Params
|
||||
expected Params
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
@@ -47,6 +47,9 @@ func TestToLower(t *testing.T) {
|
||||
"gHi": map[string]interface{}{
|
||||
"J": 25,
|
||||
},
|
||||
"jKl": map[string]string{
|
||||
"M": "26",
|
||||
},
|
||||
},
|
||||
Params{
|
||||
"abc": 32,
|
||||
@@ -60,14 +63,17 @@ func TestToLower(t *testing.T) {
|
||||
"ghi": Params{
|
||||
"j": 25,
|
||||
},
|
||||
"jkl": Params{
|
||||
"m": "26",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
// ToLower modifies input.
|
||||
ToLower(test.input)
|
||||
// PrepareParams modifies input.
|
||||
PrepareParams(test.input)
|
||||
if !reflect.DeepEqual(test.expected, test.input) {
|
||||
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
|
||||
}
|
||||
@@ -108,6 +114,16 @@ func TestToSliceStringMap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToParamsAndPrepare(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
_, ok := ToParamsAndPrepare(map[string]interface{}{"A": "av"})
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
|
||||
params, ok := ToParamsAndPrepare(nil)
|
||||
c.Assert(ok, qt.IsTrue)
|
||||
c.Assert(params, qt.DeepEquals, Params{})
|
||||
}
|
||||
|
||||
func TestRenameKeys(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package maps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
@@ -29,6 +30,118 @@ func (p Params) Get(indices ...string) interface{} {
|
||||
return v
|
||||
}
|
||||
|
||||
// Set overwrites values in p with values in pp for common or new keys.
|
||||
// This is done recursively.
|
||||
func (p Params) Set(pp Params) {
|
||||
for k, v := range pp {
|
||||
vv, found := p[k]
|
||||
if !found {
|
||||
p[k] = v
|
||||
} else {
|
||||
switch vvv := vv.(type) {
|
||||
case Params:
|
||||
if pv, ok := v.(Params); ok {
|
||||
vvv.Set(pv)
|
||||
} else {
|
||||
p[k] = v
|
||||
}
|
||||
default:
|
||||
p[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsZero returns true if p is considered empty.
|
||||
func (p Params) IsZero() bool {
|
||||
if p == nil || len(p) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(p) > 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, _ := range p {
|
||||
return k == mergeStrategyKey
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// Merge transfers values from pp to p for new keys.
|
||||
// This is done recursively.
|
||||
func (p Params) Merge(pp Params) {
|
||||
p.merge("", pp)
|
||||
}
|
||||
|
||||
// MergeRoot transfers values from pp to p for new keys where p is the
|
||||
// root of the tree.
|
||||
// This is done recursively.
|
||||
func (p Params) MergeRoot(pp Params) {
|
||||
ms, _ := p.GetMergeStrategy()
|
||||
p.merge(ms, pp)
|
||||
}
|
||||
|
||||
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
ns, found := p.GetMergeStrategy()
|
||||
|
||||
var ms = ns
|
||||
if !found && ps != "" {
|
||||
ms = ps
|
||||
}
|
||||
|
||||
noUpdate := ms == ParamsMergeStrategyNone
|
||||
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
|
||||
|
||||
for k, v := range pp {
|
||||
|
||||
if k == mergeStrategyKey {
|
||||
continue
|
||||
}
|
||||
vv, found := p[k]
|
||||
|
||||
if found {
|
||||
// Key matches, if both sides are Params, we try to merge.
|
||||
if vvv, ok := vv.(Params); ok {
|
||||
if pv, ok := v.(Params); ok {
|
||||
vvv.merge(ms, pv)
|
||||
}
|
||||
}
|
||||
} else if !noUpdate {
|
||||
p[k] = v
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
|
||||
if v, found := p[mergeStrategyKey]; found {
|
||||
if s, ok := v.(ParamsMergeStrategy); ok {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return ParamsMergeStrategyShallow, false
|
||||
}
|
||||
|
||||
func (p Params) DeleteMergeStrategy() bool {
|
||||
if _, found := p[mergeStrategyKey]; found {
|
||||
delete(p, mergeStrategyKey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
|
||||
switch s {
|
||||
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid merge strategy %q", s))
|
||||
}
|
||||
p[mergeStrategyKey] = s
|
||||
}
|
||||
|
||||
func getNested(m map[string]interface{}, indices []string) (interface{}, string, map[string]interface{}) {
|
||||
if len(indices) == 0 {
|
||||
return nil, "", nil
|
||||
@@ -108,3 +221,69 @@ func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interf
|
||||
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
|
||||
// ParamsMergeStrategy tells what strategy to use in Params.Merge.
|
||||
type ParamsMergeStrategy string
|
||||
|
||||
const (
|
||||
// Do not merge.
|
||||
ParamsMergeStrategyNone ParamsMergeStrategy = "none"
|
||||
// Only add new keys.
|
||||
ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow"
|
||||
// Add new keys, merge existing.
|
||||
ParamsMergeStrategyDeep ParamsMergeStrategy = "deep"
|
||||
|
||||
mergeStrategyKey = "_merge"
|
||||
)
|
||||
|
||||
func toMergeStrategy(v interface{}) ParamsMergeStrategy {
|
||||
s := ParamsMergeStrategy(cast.ToString(v))
|
||||
switch s {
|
||||
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
|
||||
return s
|
||||
default:
|
||||
return ParamsMergeStrategyDeep
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareParams
|
||||
// * makes all the keys in the given map lower cased and will do so
|
||||
// * This will modify the map given.
|
||||
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
|
||||
// * Any _merge value will be converted to proper type and value.
|
||||
func PrepareParams(m Params) {
|
||||
for k, v := range m {
|
||||
var retyped bool
|
||||
lKey := strings.ToLower(k)
|
||||
if lKey == mergeStrategyKey {
|
||||
v = toMergeStrategy(v)
|
||||
retyped = true
|
||||
} else {
|
||||
switch vv := v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
var p Params = cast.ToStringMap(v)
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
case map[string]interface{}:
|
||||
var p Params = v.(map[string]interface{})
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
case map[string]string:
|
||||
p := make(Params)
|
||||
for k, v := range vv {
|
||||
p[k] = v
|
||||
}
|
||||
v = p
|
||||
PrepareParams(p)
|
||||
retyped = true
|
||||
}
|
||||
}
|
||||
|
||||
if retyped || k != lKey {
|
||||
delete(m, k)
|
||||
m[lKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,3 +69,102 @@ func TestGetNestedParamFnNestedNewKey(t *testing.T) {
|
||||
c.Assert(nestedKey, qt.Equals, "new")
|
||||
c.Assert(owner, qt.DeepEquals, nested)
|
||||
}
|
||||
|
||||
func TestParamsSetAndMerge(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
createParamsPair := func() (Params, Params) {
|
||||
p1 := Params{"a": "av", "c": "cv", "nested": Params{"al2": "al2v", "cl2": "cl2v"}}
|
||||
p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, mergeStrategyKey: ParamsMergeStrategyDeep}
|
||||
return p1, p2
|
||||
}
|
||||
|
||||
p1, p2 := createParamsPair()
|
||||
|
||||
p1.Set(p2)
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "abv",
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2bv",
|
||||
"cl2": "cl2v",
|
||||
"bl2": "bl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
mergeStrategyKey: ParamsMergeStrategyDeep,
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
|
||||
p1.Merge(p2)
|
||||
|
||||
// Default is to do a shallow merge.
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
"a": "av",
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
},
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
p1, p2 = createParamsPair()
|
||||
p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep)
|
||||
p1.Merge(p2)
|
||||
p1.DeleteMergeStrategy()
|
||||
|
||||
c.Assert(p1, qt.DeepEquals, Params{
|
||||
"nested": Params{
|
||||
"al2": "al2v",
|
||||
"cl2": "cl2v",
|
||||
"bl2": "bl2v",
|
||||
},
|
||||
"b": "bv",
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestParamsIsZero(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var nilParams Params
|
||||
|
||||
c.Assert(Params{}.IsZero(), qt.IsTrue)
|
||||
c.Assert(nilParams.IsZero(), qt.IsTrue)
|
||||
c.Assert(Params{"foo": "bar"}.IsZero(), qt.IsFalse)
|
||||
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
|
||||
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue)
|
||||
}
|
||||
|
||||
@@ -129,6 +129,17 @@ func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string
|
||||
return ""
|
||||
}
|
||||
|
||||
// DeleteInMap deletes a value to a map with the given key in the Node context.
|
||||
func (c *Scratch) DeleteInMap(key string, mapKey string) string {
|
||||
c.mu.Lock()
|
||||
_, found := c.values[key]
|
||||
if found {
|
||||
delete(c.values[key].(map[string]interface{}), mapKey)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
|
||||
func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
c.mu.RLock()
|
||||
|
||||
@@ -188,6 +188,20 @@ func TestScratchSetInMap(t *testing.T) {
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"})
|
||||
}
|
||||
|
||||
func TestScratchDeleteInMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.SetInMap("key", "lux", "Lux")
|
||||
scratch.SetInMap("key", "abc", "Abc")
|
||||
scratch.SetInMap("key", "zyx", "Zyx")
|
||||
scratch.DeleteInMap("key", "abc")
|
||||
scratch.SetInMap("key", "def", "Def")
|
||||
scratch.DeleteInMap("key", "lmn") // Do nothing
|
||||
c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []interface{}{0: "Def", 1: "Lux", 2: "Zyx"})
|
||||
}
|
||||
|
||||
func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := NewScratch()
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
// Copyright 2021 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FilePathSeparator as defined by os.Separator.
|
||||
const FilePathSeparator = string(filepath.Separator)
|
||||
|
||||
// filepathPathBridge is a bridge for common functionality in filepath vs path
|
||||
type filepathPathBridge interface {
|
||||
Base(in string) string
|
||||
Clean(in string) string
|
||||
Dir(in string) string
|
||||
Ext(in string) string
|
||||
Join(elem ...string) string
|
||||
Separator() string
|
||||
}
|
||||
|
||||
type filepathBridge struct {
|
||||
}
|
||||
|
||||
func (filepathBridge) Base(in string) string {
|
||||
return filepath.Base(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Clean(in string) string {
|
||||
return filepath.Clean(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Dir(in string) string {
|
||||
return filepath.Dir(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Ext(in string) string {
|
||||
return filepath.Ext(in)
|
||||
}
|
||||
|
||||
func (filepathBridge) Join(elem ...string) string {
|
||||
return filepath.Join(elem...)
|
||||
}
|
||||
|
||||
func (filepathBridge) Separator() string {
|
||||
return FilePathSeparator
|
||||
}
|
||||
|
||||
var fpb filepathBridge
|
||||
|
||||
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
|
||||
func ToSlashTrimLeading(s string) string {
|
||||
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
// MakeTitle converts the path given to a suitable title, trimming whitespace
|
||||
// and replacing hyphens with whitespace.
|
||||
func MakeTitle(inpath string) string {
|
||||
return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
|
||||
}
|
||||
|
||||
// ReplaceExtension takes a path and an extension, strips the old extension
|
||||
// and returns the path with the new extension.
|
||||
func ReplaceExtension(path string, newExt string) string {
|
||||
f, _ := fileAndExt(path, fpb)
|
||||
return f + "." + newExt
|
||||
}
|
||||
|
||||
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
|
||||
for _, currentPath := range possibleDirectories {
|
||||
if strings.HasPrefix(inPath, currentPath) {
|
||||
return strings.TrimPrefix(inPath, currentPath), nil
|
||||
}
|
||||
}
|
||||
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 = filepath.Clean(filepath.FromSlash(inPath))
|
||||
|
||||
if inPath == "." {
|
||||
return "./"
|
||||
}
|
||||
|
||||
if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) {
|
||||
inPath += FilePathSeparator
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(inPath, FilePathSeparator) {
|
||||
inPath = FilePathSeparator + inPath
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(inPath)
|
||||
|
||||
sectionCount := strings.Count(dir, FilePathSeparator)
|
||||
|
||||
if sectionCount == 0 || dir == FilePathSeparator {
|
||||
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), ".")
|
||||
}
|
||||
|
||||
// Ext takes a path and returns the extension, including the delimiter, i.e. ".md".
|
||||
func Ext(in string) string {
|
||||
_, ext := fileAndExt(in, fpb)
|
||||
return ext
|
||||
}
|
||||
|
||||
// PathAndExt is the same as FileAndExt, but it uses the path package.
|
||||
func PathAndExt(in string) (string, string) {
|
||||
return fileAndExt(in, pb)
|
||||
}
|
||||
|
||||
// FileAndExt takes a path and returns the file and extension separated,
|
||||
// the extension including the delimiter, i.e. ".md".
|
||||
func FileAndExt(in string) (string, string) {
|
||||
return fileAndExt(in, fpb)
|
||||
}
|
||||
|
||||
// FileAndExtNoDelimiter takes a path and returns the file and extension separated,
|
||||
// the extension excluding the delimiter, e.g "md".
|
||||
func FileAndExtNoDelimiter(in string) (string, string) {
|
||||
file, ext := fileAndExt(in, fpb)
|
||||
return file, strings.TrimPrefix(ext, ".")
|
||||
}
|
||||
|
||||
// Filename takes a file path, strips out the extension,
|
||||
// and returns the name of the file.
|
||||
func Filename(in string) (name string) {
|
||||
name, _ = fileAndExt(in, fpb)
|
||||
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.
|
||||
//
|
||||
// If the path, in, contains a directory name ending in a slash,
|
||||
// then both name and ext will be empty strings.
|
||||
//
|
||||
// If the path, in, is either the current directory, the parent
|
||||
// directory or the root directory, or an empty string,
|
||||
// then both name and ext will be empty strings.
|
||||
//
|
||||
// If the path, in, represents the path of a file without an extension,
|
||||
// then name will be the name of the file and ext will be an empty string.
|
||||
//
|
||||
// If the path, in, represents a filename with an extension,
|
||||
// then name will be the filename minus any extension - including the dot
|
||||
// and ext will contain the extension - minus the dot.
|
||||
func fileAndExt(in string, b filepathPathBridge) (name string, ext string) {
|
||||
ext = b.Ext(in)
|
||||
base := b.Base(in)
|
||||
|
||||
return extractFilename(in, ext, base, b.Separator()), ext
|
||||
}
|
||||
|
||||
func extractFilename(in, ext, base, pathSeparator string) (name string) {
|
||||
// No file name cases. These are defined as:
|
||||
// 1. any "in" path that ends in a pathSeparator
|
||||
// 2. any "base" consisting of just an pathSeparator
|
||||
// 3. any "base" consisting of just an empty string
|
||||
// 4. any "base" consisting of just the current directory i.e. "."
|
||||
// 5. any "base" consisting of just the parent directory i.e. ".."
|
||||
if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator {
|
||||
name = "" // there is NO filename
|
||||
} else if ext != "" { // there was an Extension
|
||||
// return the filename minus the extension (and the ".")
|
||||
name = base[:strings.LastIndex(base, ".")]
|
||||
} else {
|
||||
// no extension case so just return base, which willi
|
||||
// be the filename
|
||||
name = base
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetRelativePath returns the relative path of a given path.
|
||||
func GetRelativePath(path, base string) (final string, err error) {
|
||||
if filepath.IsAbs(path) && base == "" {
|
||||
return "", errors.New("source: missing base directory")
|
||||
}
|
||||
name := filepath.Clean(path)
|
||||
base = filepath.Clean(base)
|
||||
|
||||
name, err = filepath.Rel(base, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
|
||||
name += FilePathSeparator
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// PathPrep prepares the path using the uglify setting to create paths on
|
||||
// either the form /section/name/index.html or /section/name.html.
|
||||
func PathPrep(ugly bool, in string) string {
|
||||
if ugly {
|
||||
return Uglify(in)
|
||||
}
|
||||
return PrettifyPath(in)
|
||||
}
|
||||
|
||||
// PrettifyPath is the same as PrettifyURLPath but for file paths.
|
||||
// /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 PrettifyPath(in string) string {
|
||||
return prettifyPath(in, fpb)
|
||||
}
|
||||
|
||||
func prettifyPath(in string, b filepathPathBridge) string {
|
||||
if filepath.Ext(in) == "" {
|
||||
// /section/name/ -> /section/name/index.html
|
||||
if len(in) < 2 {
|
||||
return b.Separator()
|
||||
}
|
||||
return b.Join(in, "index.html")
|
||||
}
|
||||
name, ext := fileAndExt(in, b)
|
||||
if name == "index" {
|
||||
// /section/name/index.html -> /section/name/index.html
|
||||
return b.Clean(in)
|
||||
}
|
||||
// /section/name.html -> /section/name/index.html
|
||||
return b.Join(b.Dir(in), name, "index"+ext)
|
||||
}
|
||||
|
||||
type NamedSlice struct {
|
||||
Name string
|
||||
Slice []string
|
||||
}
|
||||
|
||||
func (n NamedSlice) String() string {
|
||||
if len(n.Slice) == 0 {
|
||||
return n.Name
|
||||
}
|
||||
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
|
||||
}
|
||||
|
||||
// FindCWD returns the current working directory from where the Hugo
|
||||
// executable is run.
|
||||
func FindCWD() (string, error) {
|
||||
serverFile, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't get absolute path for executable: %v", err)
|
||||
}
|
||||
|
||||
path := filepath.Dir(serverFile)
|
||||
realFile, err := filepath.EvalSymlinks(serverFile)
|
||||
if err != nil {
|
||||
if _, err = os.Stat(serverFile + ".exe"); err == nil {
|
||||
realFile = filepath.Clean(serverFile + ".exe")
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && realFile != serverFile {
|
||||
path = filepath.Dir(realFile)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddTrailingSlash(path string) string {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright 2021 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"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestGetRelativePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
base string
|
||||
expect interface{}
|
||||
}{
|
||||
{filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
|
||||
{filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
|
||||
{filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")},
|
||||
{filepath.FromSlash("/c"), "", false},
|
||||
}
|
||||
for i, this := range tests {
|
||||
// ultimately a fancy wrapper around filepath.Rel
|
||||
result, err := GetRelativePath(this.path, this.base)
|
||||
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] GetRelativePath didn't return an expected error", i)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("[%d] GetRelativePath failed: %s", i, err)
|
||||
continue
|
||||
}
|
||||
if result != this.expect {
|
||||
t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePathRelative(t *testing.T) {
|
||||
type test struct {
|
||||
inPath, path1, path2, output string
|
||||
}
|
||||
|
||||
data := []test{
|
||||
{"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"},
|
||||
{"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output, _ := makePathRelative(d.inPath, d.path1, d.path2)
|
||||
if d.output != output {
|
||||
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
|
||||
}
|
||||
}
|
||||
_, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
|
||||
|
||||
if error == nil {
|
||||
t.Errorf("Test failed, expected error")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
data := []test{
|
||||
{"Make-Title", "Make Title"},
|
||||
{"MakeTitle", "MakeTitle"},
|
||||
{"make_title", "make_title"},
|
||||
}
|
||||
for i, d := range data {
|
||||
output := MakeTitle(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace Extension is probably poorly named, but the intent of the
|
||||
// function is to accept a path and return only the file name with a
|
||||
// new extension. It's intentionally designed to strip out the path
|
||||
// and only provide the name. We should probably rename the function to
|
||||
// be more explicit at some point.
|
||||
func TestReplaceExtension(t *testing.T) {
|
||||
type test struct {
|
||||
input, newext, expected string
|
||||
}
|
||||
data := []test{
|
||||
// These work according to the above definition
|
||||
{"/some/random/path/file.xml", "html", "file.html"},
|
||||
{"/banana.html", "xml", "banana.xml"},
|
||||
{"./banana.html", "xml", "banana.xml"},
|
||||
{"banana/pie/index.html", "xml", "index.xml"},
|
||||
{"../pies/fish/index.html", "xml", "index.xml"},
|
||||
// but these all fail
|
||||
{"filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
|
||||
{"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
|
||||
{"/directory/mydir/", "ext", ".ext"},
|
||||
{"mydir/", "ext", ".ext"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := ReplaceExtension(filepath.FromSlash(d.input), d.newext)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtNoDelimiter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json")
|
||||
}
|
||||
|
||||
func TestFilename(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"index.html", "index"},
|
||||
{"./index.html", "index"},
|
||||
{"/index.html", "index"},
|
||||
{"index", "index"},
|
||||
{"/tmp/index.html", "index"},
|
||||
{"./filename-no-ext", "filename-no-ext"},
|
||||
{"/filename-no-ext", "filename-no-ext"},
|
||||
{"filename-no-ext", "filename-no-ext"},
|
||||
{"directory/", ""}, // no filename case??
|
||||
{"directory/.hidden.ext", ".hidden"},
|
||||
{"./directory/../~/banana/gold.fish", "gold"},
|
||||
{"../directory/banana.man", "banana"},
|
||||
{"~/mydir/filename.ext", "filename"},
|
||||
{"./directory//tmp/filename.ext", "filename"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := Filename(filepath.FromSlash(d.input))
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileAndExt(t *testing.T) {
|
||||
type test struct {
|
||||
input, expectedFile, expectedExt string
|
||||
}
|
||||
data := []test{
|
||||
{"index.html", "index", ".html"},
|
||||
{"./index.html", "index", ".html"},
|
||||
{"/index.html", "index", ".html"},
|
||||
{"index", "index", ""},
|
||||
{"/tmp/index.html", "index", ".html"},
|
||||
{"./filename-no-ext", "filename-no-ext", ""},
|
||||
{"/filename-no-ext", "filename-no-ext", ""},
|
||||
{"filename-no-ext", "filename-no-ext", ""},
|
||||
{"directory/", "", ""}, // no filename case??
|
||||
{"directory/.hidden.ext", ".hidden", ".ext"},
|
||||
{"./directory/../~/banana/gold.fish", "gold", ".fish"},
|
||||
{"../directory/banana.man", "banana", ".man"},
|
||||
{"~/mydir/filename.ext", "filename", ".ext"},
|
||||
{"./directory//tmp/filename.ext", "filename", ".ext"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
file, ext := fileAndExt(filepath.FromSlash(d.input), fpb)
|
||||
if d.expectedFile != file {
|
||||
t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file)
|
||||
}
|
||||
if d.expectedExt != ext {
|
||||
t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCWD(t *testing.T) {
|
||||
type test struct {
|
||||
expectedDir string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
// cwd, _ := os.Getwd()
|
||||
data := []test{
|
||||
//{cwd, nil},
|
||||
// Commenting this out. It doesn't work properly.
|
||||
// There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to.
|
||||
// I really don't know a better way to test this function. - SPF 2014.11.04
|
||||
}
|
||||
for i, d := range data {
|
||||
dir, err := FindCWD()
|
||||
if d.expectedDir != dir {
|
||||
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir)
|
||||
}
|
||||
if d.expectedErr != err {
|
||||
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2021 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 (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/purell"
|
||||
)
|
||||
|
||||
type pathBridge struct {
|
||||
}
|
||||
|
||||
func (pathBridge) Base(in string) string {
|
||||
return path.Base(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Clean(in string) string {
|
||||
return path.Clean(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Dir(in string) string {
|
||||
return path.Dir(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Ext(in string) string {
|
||||
return path.Ext(in)
|
||||
}
|
||||
|
||||
func (pathBridge) Join(elem ...string) string {
|
||||
return path.Join(elem...)
|
||||
}
|
||||
|
||||
func (pathBridge) Separator() string {
|
||||
return "/"
|
||||
}
|
||||
|
||||
var pb pathBridge
|
||||
|
||||
func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string {
|
||||
s, err := purell.NormalizeURLString(in, f)
|
||||
if err != nil {
|
||||
return in
|
||||
}
|
||||
|
||||
// Temporary workaround for the bug fix and resulting
|
||||
// behavioral change in purell.NormalizeURLString():
|
||||
// a leading '/' was inadvertently added to relative links,
|
||||
// but no longer, see #878.
|
||||
//
|
||||
// I think the real solution is to allow Hugo to
|
||||
// make relative URL with relative path,
|
||||
// e.g. "../../post/hello-again/", as wished by users
|
||||
// in issues #157, #622, etc., without forcing
|
||||
// relative URLs to begin with '/'.
|
||||
// Once the fixes are in, let's remove this kludge
|
||||
// and restore SanitizeURL() to the way it was.
|
||||
// -- @anthonyfok, 2015-02-16
|
||||
//
|
||||
// Begin temporary kludge
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") {
|
||||
u.Path = "/" + u.Path
|
||||
}
|
||||
return u.String()
|
||||
// End temporary kludge
|
||||
|
||||
// return s
|
||||
|
||||
}
|
||||
|
||||
// SanitizeURL sanitizes the input URL string.
|
||||
func SanitizeURL(in string) string {
|
||||
return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
|
||||
}
|
||||
|
||||
// SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash.
|
||||
func SanitizeURLKeepTrailingSlash(in string) string {
|
||||
return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
|
||||
}
|
||||
|
||||
// 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
|
||||
func MakePermalink(host, plink string) *url.URL {
|
||||
base, err := url.Parse(host)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p, err := url.Parse(plink)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if p.Host != "" {
|
||||
panic(fmt.Errorf("can't make permalink from absolute link %q", plink))
|
||||
}
|
||||
|
||||
base.Path = path.Join(base.Path, p.Path)
|
||||
|
||||
// path.Join will strip off the last /, so put it back if it was there.
|
||||
hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/")
|
||||
if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") {
|
||||
base.Path = base.Path + "/"
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
// IsAbsURL determines whether the given path points to an absolute URL.
|
||||
func IsAbsURL(path string) bool {
|
||||
url, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return url.IsAbs() || strings.HasPrefix(path, "//")
|
||||
}
|
||||
|
||||
// AddContextRoot adds the context root to an URL if it's not already set.
|
||||
// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite),
|
||||
// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set.
|
||||
func AddContextRoot(baseURL, relativePath string) string {
|
||||
url, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
newPath := path.Join(url.Path, relativePath)
|
||||
|
||||
// path strips trailing slash, ignore root path.
|
||||
if newPath != "/" && strings.HasSuffix(relativePath, "/") {
|
||||
newPath += "/"
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
// URLizeAn
|
||||
|
||||
// PrettifyURL takes a URL string and returns a semantic, clean URL.
|
||||
func PrettifyURL(in string) string {
|
||||
x := PrettifyURLPath(in)
|
||||
|
||||
if path.Base(x) == "index.html" {
|
||||
return path.Dir(x)
|
||||
}
|
||||
|
||||
if in == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
// PrettifyURLPath takes a URL path to a content and converts it
|
||||
// to enable pretty URLs.
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyURLPath(in string) string {
|
||||
return prettifyPath(in, pb)
|
||||
}
|
||||
|
||||
// Uglify does the opposite of PrettifyURLPath().
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
func Uglify(in string) string {
|
||||
if path.Ext(in) == "" {
|
||||
if len(in) < 2 {
|
||||
return "/"
|
||||
}
|
||||
// /section/name/ -> /section/name.html
|
||||
return path.Clean(in) + ".html"
|
||||
}
|
||||
|
||||
name, ext := fileAndExt(in, pb)
|
||||
if name == "index" {
|
||||
// /section/name/index.html -> /section/name.html
|
||||
d := path.Dir(in)
|
||||
if len(d) > 1 {
|
||||
return d + ext
|
||||
}
|
||||
return in
|
||||
}
|
||||
// /.xml -> /index.xml
|
||||
if name == "" {
|
||||
return path.Dir(in) + "index" + ext
|
||||
}
|
||||
// /section/name.html -> /section/name.html
|
||||
return path.Clean(in)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2021 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 (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestSanitizeURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"http://foo.bar/", "http://foo.bar"},
|
||||
{"http://foo.bar", "http://foo.bar"}, // issue #1105
|
||||
{"http://foo.bar/zoo/", "http://foo.bar/zoo"}, // issue #931
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
o1 := SanitizeURL(test.input)
|
||||
o2 := SanitizeURLKeepTrailingSlash(test.input)
|
||||
|
||||
expected2 := test.expected
|
||||
|
||||
if strings.HasSuffix(test.input, "/") && !strings.HasSuffix(expected2, "/") {
|
||||
expected2 += "/"
|
||||
}
|
||||
|
||||
if o1 != test.expected {
|
||||
t.Errorf("[%d] 1: Expected %#v, got %#v\n", i, test.expected, o1)
|
||||
}
|
||||
if o2 != expected2 {
|
||||
t.Errorf("[%d] 2: Expected %#v, got %#v\n", i, expected2, o2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePermalink(t *testing.T) {
|
||||
type test struct {
|
||||
host, link, output string
|
||||
}
|
||||
|
||||
data := []test{
|
||||
{"http://abc.com/foo", "post/bar", "http://abc.com/foo/post/bar"},
|
||||
{"http://abc.com/foo/", "post/bar", "http://abc.com/foo/post/bar"},
|
||||
{"http://abc.com", "post/bar", "http://abc.com/post/bar"},
|
||||
{"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/"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
output := MakePermalink(d.host, d.link).String()
|
||||
if d.output != output {
|
||||
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddContextRoot(t *testing.T) {
|
||||
tests := []struct {
|
||||
baseURL string
|
||||
url string
|
||||
expected string
|
||||
}{
|
||||
{"http://example.com/sub/", "/foo", "/sub/foo"},
|
||||
{"http://example.com/sub/", "/foo/index.html", "/sub/foo/index.html"},
|
||||
{"http://example.com/sub1/sub2", "/foo", "/sub1/sub2/foo"},
|
||||
{"http://example.com", "/foo", "/foo"},
|
||||
// cannot guess that the context root is already added int the example below
|
||||
{"http://example.com/sub/", "/sub/foo", "/sub/sub/foo"},
|
||||
{"http://example.com/тря", "/трям/", "/тря/трям/"},
|
||||
{"http://example.com", "/", "/"},
|
||||
{"http://example.com/bar", "//", "/bar/"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
output := AddContextRoot(test.baseURL, test.url)
|
||||
if output != test.expected {
|
||||
t.Errorf("Expected %#v, got %#v\n", test.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPretty(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html"))
|
||||
c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html"))
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/"))
|
||||
c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html"))
|
||||
c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html"))
|
||||
c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml"))
|
||||
c.Assert("/", qt.Equals, PrettifyURLPath("/"))
|
||||
c.Assert("/", qt.Equals, PrettifyURLPath(""))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html"))
|
||||
c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html"))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/"))
|
||||
c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL("/index.html"))
|
||||
c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL("/"))
|
||||
c.Assert("/", qt.Equals, PrettifyURL(""))
|
||||
}
|
||||
|
||||
func TestUgly(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html"))
|
||||
c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html"))
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/"))
|
||||
c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html"))
|
||||
c.Assert("/index.html", qt.Equals, Uglify("/index.html"))
|
||||
c.Assert("/name.xml", qt.Equals, Uglify("/name.xml"))
|
||||
c.Assert("/", qt.Equals, Uglify("/"))
|
||||
c.Assert("/", qt.Equals, Uglify(""))
|
||||
}
|
||||
+56
-5
@@ -15,21 +15,72 @@ package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ToStringSlicePreserveString converts v to a string slice.
|
||||
// If v is a string, it will be wrapped in a string slice.
|
||||
// ToDuration converts v to time.Duration.
|
||||
// See ToDurationE if you need to handle errors.
|
||||
func ToDuration(v interface{}) time.Duration {
|
||||
d, _ := ToDurationE(v)
|
||||
return d
|
||||
}
|
||||
|
||||
// ToDurationE converts v to time.Duration.
|
||||
func ToDurationE(v interface{}) (time.Duration, error) {
|
||||
if n := cast.ToInt(v); n > 0 {
|
||||
return time.Duration(n) * time.Millisecond, nil
|
||||
}
|
||||
d, err := time.ParseDuration(cast.ToString(v))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot convert %v to time.Duration", v)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ToStringSlicePreserveString is the same as ToStringSlicePreserveStringE,
|
||||
// but it never fails.
|
||||
func ToStringSlicePreserveString(v interface{}) []string {
|
||||
vv, _ := ToStringSlicePreserveStringE(v)
|
||||
return vv
|
||||
}
|
||||
|
||||
// ToStringSlicePreserveStringE converts v to a string slice.
|
||||
// If v is a string, it will be wrapped in a string slice.
|
||||
func ToStringSlicePreserveStringE(v interface{}) ([]string, error) {
|
||||
if v == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
if sds, ok := v.(string); ok {
|
||||
return []string{sds}
|
||||
return []string{sds}, nil
|
||||
}
|
||||
return cast.ToStringSlice(v)
|
||||
result, err := cast.ToStringSliceE(v)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Probably []int or similar. Fall back to reflect.
|
||||
vv := reflect.ValueOf(v)
|
||||
|
||||
switch vv.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
result = make([]string, vv.Len())
|
||||
for i := 0; i < vv.Len(); i++ {
|
||||
s, err := cast.ToStringE(vv.Index(i).Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = s
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to convert %T to a string slice", v)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TypeToString converts v to a string if it's a valid string type.
|
||||
|
||||
@@ -16,6 +16,7 @@ package types
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
@@ -24,7 +25,9 @@ func TestToStringSlicePreserveString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToStringSlicePreserveString("Hugo"), qt.DeepEquals, []string{"Hugo"})
|
||||
c.Assert(ToStringSlicePreserveString(qt.Commentf("Hugo")), qt.DeepEquals, []string{"Hugo"})
|
||||
c.Assert(ToStringSlicePreserveString([]interface{}{"A", "B"}), qt.DeepEquals, []string{"A", "B"})
|
||||
c.Assert(ToStringSlicePreserveString([]int{1, 3}), qt.DeepEquals, []string{"1", "3"})
|
||||
c.Assert(ToStringSlicePreserveString(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
@@ -34,3 +37,13 @@ func TestToString(t *testing.T) {
|
||||
c.Assert(ToString([]byte("Hugo")), qt.Equals, "Hugo")
|
||||
c.Assert(ToString(json.RawMessage("Hugo")), qt.Equals, "Hugo")
|
||||
}
|
||||
|
||||
func TestToDuration(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(ToDuration("200ms"), qt.Equals, 200*time.Millisecond)
|
||||
c.Assert(ToDuration("200"), qt.Equals, 200*time.Millisecond)
|
||||
c.Assert(ToDuration("4m"), qt.Equals, 4*time.Minute)
|
||||
c.Assert(ToDuration("asdfadf"), qt.Equals, time.Duration(0))
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ type RLocker interface {
|
||||
RUnlock()
|
||||
}
|
||||
|
||||
// KeyValue is a interface{} tuple.
|
||||
type KeyValue struct {
|
||||
Key interface{}
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// KeyValueStr is a string tuple.
|
||||
type KeyValueStr struct {
|
||||
Key string
|
||||
|
||||
@@ -21,14 +21,12 @@ import (
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
v := viper.New()
|
||||
v := New()
|
||||
v.Set("build", map[string]interface{}{
|
||||
"useResourceCacheWhen": "always",
|
||||
})
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2021 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 config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
)
|
||||
|
||||
// NewCompositeConfig creates a new composite Provider with a read-only base
|
||||
// and a writeable layer.
|
||||
func NewCompositeConfig(base, layer Provider) Provider {
|
||||
return &compositeConfig{
|
||||
base: base,
|
||||
layer: layer,
|
||||
}
|
||||
}
|
||||
|
||||
// compositeConfig contains a read only config base with
|
||||
// a possibly writeable config layer on top.
|
||||
type compositeConfig struct {
|
||||
base Provider
|
||||
layer Provider
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetBool(key string) bool {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetBool(key)
|
||||
}
|
||||
return c.base.GetBool(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetInt(key string) int {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetInt(key)
|
||||
}
|
||||
return c.base.GetInt(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Merge(key string, value interface{}) {
|
||||
c.layer.Merge(key, value)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetParams(key string) maps.Params {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetParams(key)
|
||||
}
|
||||
return c.base.GetParams(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringMap(key string) map[string]interface{} {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringMap(key)
|
||||
}
|
||||
return c.base.GetStringMap(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringMapString(key string) map[string]string {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringMapString(key)
|
||||
}
|
||||
return c.base.GetStringMapString(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetStringSlice(key string) []string {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetStringSlice(key)
|
||||
}
|
||||
return c.base.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Get(key string) interface{} {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.Get(key)
|
||||
}
|
||||
return c.base.Get(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) IsSet(key string) bool {
|
||||
if c.layer.IsSet(key) {
|
||||
return true
|
||||
}
|
||||
return c.base.IsSet(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) GetString(key string) string {
|
||||
if c.layer.IsSet(key) {
|
||||
return c.layer.GetString(key)
|
||||
}
|
||||
return c.base.GetString(key)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) Set(key string, value interface{}) {
|
||||
c.layer.Set(key, value)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) SetDefaults(params maps.Params) {
|
||||
c.layer.SetDefaults(params)
|
||||
}
|
||||
|
||||
func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
|
||||
panic("not supported")
|
||||
}
|
||||
|
||||
func (c *compositeConfig) SetDefaultMergeStrategy() {
|
||||
panic("not supported")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2021 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 config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCompositeConfig(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("Set and get", func(c *qt.C) {
|
||||
base, layer := New(), New()
|
||||
cfg := NewCompositeConfig(base, layer)
|
||||
|
||||
layer.Set("a1", "av")
|
||||
base.Set("b1", "bv")
|
||||
cfg.Set("c1", "cv")
|
||||
|
||||
c.Assert(cfg.Get("a1"), qt.Equals, "av")
|
||||
c.Assert(cfg.Get("b1"), qt.Equals, "bv")
|
||||
c.Assert(cfg.Get("c1"), qt.Equals, "cv")
|
||||
c.Assert(cfg.IsSet("c1"), qt.IsTrue)
|
||||
c.Assert(layer.IsSet("c1"), qt.IsTrue)
|
||||
c.Assert(base.IsSet("c1"), qt.IsFalse)
|
||||
})
|
||||
}
|
||||
+106
-22
@@ -14,13 +14,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,32 +49,20 @@ func IsValidConfigFilename(filename string) bool {
|
||||
|
||||
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
|
||||
func FromConfigString(config, configType string) (Provider, error) {
|
||||
v := newViper()
|
||||
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.MergeConfigMap(m)
|
||||
|
||||
return v, nil
|
||||
return NewFrom(m), nil
|
||||
}
|
||||
|
||||
// FromFile loads the configuration from the given filename.
|
||||
func FromFile(fs afero.Fs, filename string) (Provider, error) {
|
||||
m, err := loadConfigFromFile(fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, herrors.WithFileContextForFileDefault(err, filename, fs)
|
||||
}
|
||||
|
||||
v := newViper()
|
||||
|
||||
err = v.MergeConfigMap(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
return NewFrom(m), nil
|
||||
}
|
||||
|
||||
// FromFileToMap is the same as FromFile, but it returns the config values
|
||||
@@ -97,6 +91,102 @@ func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, e
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) {
|
||||
defaultConfigDir := filepath.Join(configDir, "_default")
|
||||
environmentConfigDir := filepath.Join(configDir, environment)
|
||||
cfg := New()
|
||||
|
||||
var configDirs []string
|
||||
// Merge from least to most specific.
|
||||
for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
|
||||
if _, err := sourceFs.Stat(dir); err == nil {
|
||||
configDirs = append(configDirs, dir)
|
||||
}
|
||||
}
|
||||
|
||||
if len(configDirs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Keep track of these so we can watch them for changes.
|
||||
var dirnames []string
|
||||
|
||||
for _, configDir := range configDirs {
|
||||
err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
|
||||
if fi == nil || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
dirnames = append(dirnames, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !IsValidConfigFilename(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := paths.Filename(filepath.Base(path))
|
||||
|
||||
item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
|
||||
if err != nil {
|
||||
// This will be used in error reporting, use the most specific value.
|
||||
dirnames = []string{path}
|
||||
return errors.Wrapf(err, "failed to unmarshl config for path %q", path)
|
||||
}
|
||||
|
||||
var keyPath []string
|
||||
|
||||
if name != "config" {
|
||||
// Can be params.jp, menus.en etc.
|
||||
name, lang := paths.FileAndExtNoDelimiter(name)
|
||||
|
||||
keyPath = []string{name}
|
||||
|
||||
if lang != "" {
|
||||
keyPath = []string{"languages", lang}
|
||||
switch name {
|
||||
case "menu", "menus":
|
||||
keyPath = append(keyPath, "menus")
|
||||
case "params":
|
||||
keyPath = append(keyPath, "params")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root := item
|
||||
if len(keyPath) > 0 {
|
||||
root = make(map[string]interface{})
|
||||
m := root
|
||||
for i, key := range keyPath {
|
||||
if i >= len(keyPath)-1 {
|
||||
m[key] = item
|
||||
} else {
|
||||
nm := make(map[string]interface{})
|
||||
m[key] = nm
|
||||
m = nm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate menu => menus etc.
|
||||
RenameKeys(root)
|
||||
|
||||
// Set will overwrite keys with the same name, recursively.
|
||||
cfg.Set("", root)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, dirnames, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return cfg, dirnames, nil
|
||||
|
||||
}
|
||||
|
||||
var keyAliases maps.KeyRenamer
|
||||
|
||||
func init() {
|
||||
@@ -116,9 +206,3 @@ func init() {
|
||||
func RenameKeys(m map[string]interface{}) {
|
||||
keyAliases.Rename(m)
|
||||
}
|
||||
|
||||
func newViper() *viper.Viper {
|
||||
v := viper.New()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
@@ -22,11 +23,16 @@ type Provider interface {
|
||||
GetString(key string) string
|
||||
GetInt(key string) int
|
||||
GetBool(key string) bool
|
||||
GetParams(key string) maps.Params
|
||||
GetStringMap(key string) map[string]interface{}
|
||||
GetStringMapString(key string) map[string]string
|
||||
GetStringSlice(key string) []string
|
||||
Get(key string) interface{}
|
||||
Set(key string, value interface{})
|
||||
Merge(key string, value interface{})
|
||||
SetDefaults(params maps.Params)
|
||||
SetDefaultMergeStrategy()
|
||||
WalkParams(walkFn func(params ...KeyParams) bool)
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,11 @@ import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestGetStringSlicePreserveString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
cfg := viper.New()
|
||||
cfg := New()
|
||||
|
||||
s := "This is a string"
|
||||
sSlice := []string{"This", "is", "a", "slice"}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
// Copyright 2021 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
// ConfigRootKeysSet contains all of the config map root keys.
|
||||
ConfigRootKeysSet = map[string]bool{
|
||||
"build": true,
|
||||
"caches": true,
|
||||
"cascade": true,
|
||||
"frontmatter": true,
|
||||
"languages": true,
|
||||
"imaging": true,
|
||||
"markup": true,
|
||||
"mediatypes": true,
|
||||
"menus": true,
|
||||
"minify": true,
|
||||
"module": true,
|
||||
"outputformats": true,
|
||||
"params": true,
|
||||
"permalinks": true,
|
||||
"related": true,
|
||||
"sitemap": true,
|
||||
"taxonomies": true,
|
||||
}
|
||||
|
||||
// ConfigRootKeys is a sorted version of ConfigRootKeysSet.
|
||||
ConfigRootKeys []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
for k := range ConfigRootKeysSet {
|
||||
ConfigRootKeys = append(ConfigRootKeys, k)
|
||||
}
|
||||
sort.Strings(ConfigRootKeys)
|
||||
}
|
||||
|
||||
// New creates a Provider backed by an empty maps.Params.
|
||||
func New() Provider {
|
||||
return &defaultConfigProvider{
|
||||
root: make(maps.Params),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFrom creates a Provider backed by params.
|
||||
func NewFrom(params maps.Params) Provider {
|
||||
maps.PrepareParams(params)
|
||||
return &defaultConfigProvider{
|
||||
root: params,
|
||||
}
|
||||
}
|
||||
|
||||
// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
|
||||
// All methods are thread safe.
|
||||
type defaultConfigProvider struct {
|
||||
mu sync.RWMutex
|
||||
root maps.Params
|
||||
|
||||
keyCache sync.Map
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Get(k string) interface{} {
|
||||
if k == "" {
|
||||
return c.root
|
||||
}
|
||||
c.mu.RLock()
|
||||
key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
|
||||
if m == nil {
|
||||
c.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
v := m[key]
|
||||
c.mu.RUnlock()
|
||||
return v
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetBool(k string) bool {
|
||||
v := c.Get(k)
|
||||
return cast.ToBool(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetInt(k string) int {
|
||||
v := c.Get(k)
|
||||
return cast.ToInt(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) IsSet(k string) bool {
|
||||
var found bool
|
||||
c.mu.RLock()
|
||||
key, m := c.getNestedKeyAndMap(strings.ToLower(k), false)
|
||||
if m != nil {
|
||||
_, found = m[key]
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
return found
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetString(k string) string {
|
||||
v := c.Get(k)
|
||||
return cast.ToString(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetParams(k string) maps.Params {
|
||||
v := c.Get(k)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.(maps.Params)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMap(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
|
||||
v := c.Get(k)
|
||||
return maps.ToStringMapString(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) GetStringSlice(k string) []string {
|
||||
v := c.Get(k)
|
||||
return cast.ToStringSlice(v)
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Set(k string, v interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
k = strings.ToLower(k)
|
||||
|
||||
if k == "" {
|
||||
if p, ok := maps.ToParamsAndPrepare(v); ok {
|
||||
// Set the values directly in root.
|
||||
c.root.Set(p)
|
||||
} else {
|
||||
c.root[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}, map[interface{}]interface{}, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
key, m := c.getNestedKeyAndMap(k, true)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if existing, found := m[key]; found {
|
||||
if p1, ok := existing.(maps.Params); ok {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
p1.Set(p2)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m[key] = v
|
||||
}
|
||||
|
||||
// SetDefaults will set values from params if not already set.
|
||||
func (c *defaultConfigProvider) SetDefaults(params maps.Params) {
|
||||
maps.PrepareParams(params)
|
||||
for k, v := range params {
|
||||
if _, found := c.root[k]; !found {
|
||||
c.root[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) Merge(k string, v interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
k = strings.ToLower(k)
|
||||
|
||||
const (
|
||||
languagesKey = "languages"
|
||||
paramsKey = "params"
|
||||
menusKey = "menus"
|
||||
)
|
||||
|
||||
if k == "" {
|
||||
rs, f := c.root.GetMergeStrategy()
|
||||
if f && rs == maps.ParamsMergeStrategyNone {
|
||||
// The user has set a "no merge" strategy on this,
|
||||
// nothing more to do.
|
||||
return
|
||||
}
|
||||
|
||||
if p, ok := maps.ToParamsAndPrepare(v); ok {
|
||||
// As there may be keys in p not in root, we need to handle
|
||||
// those as a special case.
|
||||
var keysToDelete []string
|
||||
for kk, vv := range p {
|
||||
if pp, ok := vv.(maps.Params); ok {
|
||||
if pppi, ok := c.root[kk]; ok {
|
||||
ppp := pppi.(maps.Params)
|
||||
if kk == languagesKey {
|
||||
// Languages is currently a special case.
|
||||
// We may have languages with menus or params in the
|
||||
// right map that is not present in the left map.
|
||||
// With the default merge strategy those items will not
|
||||
// be passed over.
|
||||
var hasParams, hasMenus bool
|
||||
for _, rv := range pp {
|
||||
if lkp, ok := rv.(maps.Params); ok {
|
||||
_, hasMenus = lkp[menusKey]
|
||||
_, hasParams = lkp[paramsKey]
|
||||
}
|
||||
}
|
||||
|
||||
if hasMenus || hasParams {
|
||||
for _, lv := range ppp {
|
||||
if lkp, ok := lv.(maps.Params); ok {
|
||||
if hasMenus {
|
||||
if _, ok := lkp[menusKey]; !ok {
|
||||
p := maps.Params{}
|
||||
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
|
||||
lkp[menusKey] = p
|
||||
}
|
||||
}
|
||||
if hasParams {
|
||||
if _, ok := lkp[paramsKey]; !ok {
|
||||
p := maps.Params{}
|
||||
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
|
||||
lkp[paramsKey] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ppp.Merge(pp)
|
||||
} else {
|
||||
// We need to use the default merge strategy for
|
||||
// this key.
|
||||
np := make(maps.Params)
|
||||
strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
|
||||
np.SetDefaultMergeStrategy(strategy)
|
||||
np.Merge(pp)
|
||||
c.root[kk] = np
|
||||
if np.IsZero() {
|
||||
// Just keep it until merge is done.
|
||||
keysToDelete = append(keysToDelete, kk)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Merge the rest.
|
||||
c.root.MergeRoot(p)
|
||||
for _, k := range keysToDelete {
|
||||
delete(c.root, k)
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("unsupported type %T received in Merge", v))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}, map[interface{}]interface{}, map[string]string:
|
||||
p := maps.MustToParamsAndPrepare(vv)
|
||||
v = p
|
||||
}
|
||||
|
||||
key, m := c.getNestedKeyAndMap(k, true)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if existing, found := m[key]; found {
|
||||
if p1, ok := existing.(maps.Params); ok {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
p1.Merge(p2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
|
||||
var walk func(params ...KeyParams)
|
||||
walk = func(params ...KeyParams) {
|
||||
if walkFn(params...) {
|
||||
return
|
||||
}
|
||||
p1 := params[len(params)-1]
|
||||
i := len(params)
|
||||
for k, v := range p1.Params {
|
||||
if p2, ok := v.(maps.Params); ok {
|
||||
paramsplus1 := make([]KeyParams, i+1)
|
||||
copy(paramsplus1, params)
|
||||
paramsplus1[i] = KeyParams{Key: k, Params: p2}
|
||||
walk(paramsplus1...)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(KeyParams{Key: "", Params: c.root})
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
|
||||
if len(params) == 0 {
|
||||
return maps.ParamsMergeStrategyNone
|
||||
}
|
||||
|
||||
var (
|
||||
strategy maps.ParamsMergeStrategy
|
||||
prevIsRoot bool
|
||||
curr = params[len(params)-1]
|
||||
)
|
||||
|
||||
if len(params) > 1 {
|
||||
prev := params[len(params)-2]
|
||||
prevIsRoot = prev.Key == ""
|
||||
|
||||
// Inherit from parent (but not from the root unless it's set by user).
|
||||
s, found := prev.Params.GetMergeStrategy()
|
||||
if !prevIsRoot && !found {
|
||||
panic("invalid state, merge strategy not set on parent")
|
||||
}
|
||||
if found || !prevIsRoot {
|
||||
strategy = s
|
||||
}
|
||||
}
|
||||
|
||||
switch curr.Key {
|
||||
case "":
|
||||
// Don't set a merge strategy on the root unless set by user.
|
||||
// This will be handled as a special case.
|
||||
case "params":
|
||||
strategy = maps.ParamsMergeStrategyDeep
|
||||
case "outputformats", "mediatypes":
|
||||
if prevIsRoot {
|
||||
strategy = maps.ParamsMergeStrategyShallow
|
||||
}
|
||||
case "menus":
|
||||
isMenuKey := prevIsRoot
|
||||
if !isMenuKey {
|
||||
// Can also be set below languages.
|
||||
// root > languages > en > menus
|
||||
if len(params) == 4 && params[1].Key == "languages" {
|
||||
isMenuKey = true
|
||||
}
|
||||
}
|
||||
if isMenuKey {
|
||||
strategy = maps.ParamsMergeStrategyShallow
|
||||
}
|
||||
default:
|
||||
if strategy == "" {
|
||||
strategy = maps.ParamsMergeStrategyNone
|
||||
}
|
||||
}
|
||||
|
||||
return strategy
|
||||
}
|
||||
|
||||
type KeyParams struct {
|
||||
Key string
|
||||
Params maps.Params
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
|
||||
c.WalkParams(func(params ...KeyParams) bool {
|
||||
if len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
p := params[len(params)-1].Params
|
||||
var found bool
|
||||
if _, found = p.GetMergeStrategy(); found {
|
||||
// Set by user.
|
||||
return false
|
||||
}
|
||||
strategy := c.determineMergeStrategy(params...)
|
||||
if strategy != "" {
|
||||
p.SetDefaultMergeStrategy(strategy)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
|
||||
var parts []string
|
||||
v, ok := c.keyCache.Load(key)
|
||||
if ok {
|
||||
parts = v.([]string)
|
||||
} else {
|
||||
parts = strings.Split(key, ".")
|
||||
c.keyCache.Store(key, parts)
|
||||
}
|
||||
current := c.root
|
||||
for i := 0; i < len(parts)-1; i++ {
|
||||
next, found := current[parts[i]]
|
||||
if !found {
|
||||
if create {
|
||||
next = make(maps.Params)
|
||||
current[parts[i]] = next
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
var ok bool
|
||||
current, ok = next.(maps.Params)
|
||||
if !ok {
|
||||
// E.g. a string, not a map that we can store values in.
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return parts[len(parts)-1], current
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright 2021 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 config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/gohugoio/hugo/common/para"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestDefaultConfigProvider(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("Set and get", func(c *qt.C) {
|
||||
cfg := New()
|
||||
var k string
|
||||
var v interface{}
|
||||
|
||||
k, v = "foo", "bar"
|
||||
cfg.Set(k, v)
|
||||
c.Assert(cfg.Get(k), qt.Equals, v)
|
||||
c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v)
|
||||
c.Assert(cfg.GetString(k), qt.Equals, v)
|
||||
|
||||
k, v = "foo", 42
|
||||
cfg.Set(k, v)
|
||||
c.Assert(cfg.Get(k), qt.Equals, v)
|
||||
c.Assert(cfg.GetInt(k), qt.Equals, v)
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"foo": 42,
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Set and get map", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("foo", map[string]interface{}{
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
|
||||
"bar": "baz",
|
||||
})
|
||||
|
||||
c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")})
|
||||
c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")})
|
||||
})
|
||||
|
||||
c.Run("Set and get nested", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
cfg.Set("a.c", "cv")
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
"b": "bv",
|
||||
"c": "cv",
|
||||
})
|
||||
c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
|
||||
|
||||
cfg.Set("b.a", "av")
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
})
|
||||
|
||||
cfg.Set("b", map[string]interface{}{
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"a": "av",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"b": "bv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
"b": "bv2",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Merge default strategy", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
cfg.Merge("a", map[string]interface{}{
|
||||
"B": "bv2",
|
||||
"c": "cv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
"b": "bv",
|
||||
"c": "cv2",
|
||||
})
|
||||
|
||||
cfg = New()
|
||||
|
||||
cfg.Set("a", "av")
|
||||
|
||||
cfg.Merge("", map[string]interface{}{
|
||||
"a": "av2",
|
||||
"b": "bv2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": "av",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("Merge shallow", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"_merge": "shallow",
|
||||
"B": "bv",
|
||||
"c": map[string]interface{}{
|
||||
"b": "bv",
|
||||
},
|
||||
})
|
||||
|
||||
cfg.Merge("a", map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
"d": "dv2",
|
||||
},
|
||||
"e": "ev2",
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
|
||||
"e": "ev2",
|
||||
"_merge": maps.ParamsMergeStrategyShallow,
|
||||
"b": "bv",
|
||||
"c": maps.Params{
|
||||
"b": "bv",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Issue #8679
|
||||
c.Run("Merge typed maps", func(c *qt.C) {
|
||||
|
||||
for _, left := range []interface{}{
|
||||
map[string]string{
|
||||
"c": "cv1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"c": "cv1",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
"c": "cv1",
|
||||
},
|
||||
} {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"b": left,
|
||||
})
|
||||
|
||||
cfg.Merge("", maps.Params{
|
||||
"b": maps.Params{
|
||||
"c": "cv2",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"b": maps.Params{
|
||||
"c": "cv1",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for _, left := range []interface{}{
|
||||
map[string]string{
|
||||
"b": "bv1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"b": "bv1",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
"b": "bv1",
|
||||
},
|
||||
} {
|
||||
|
||||
for _, right := range []interface{}{
|
||||
map[string]string{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
map[interface{}]interface{}{
|
||||
"b": "bv2",
|
||||
"c": "cv2",
|
||||
},
|
||||
} {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", left)
|
||||
|
||||
cfg.Merge("a", right)
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"a": maps.Params{
|
||||
"b": "bv1",
|
||||
"c": "cv2",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Issue #8701
|
||||
c.Run("Prevent _merge only maps", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
cfg.Merge("", map[string]interface{}{
|
||||
"c": map[string]interface{}{
|
||||
"_merge": "shallow",
|
||||
"d": "dv2",
|
||||
},
|
||||
})
|
||||
|
||||
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
|
||||
"b": "bv",
|
||||
})
|
||||
})
|
||||
|
||||
c.Run("IsSet", func(c *qt.C) {
|
||||
cfg := New()
|
||||
|
||||
cfg.Set("a", map[string]interface{}{
|
||||
"B": "bv",
|
||||
})
|
||||
|
||||
c.Assert(cfg.IsSet("A"), qt.IsTrue)
|
||||
c.Assert(cfg.IsSet("a.b"), qt.IsTrue)
|
||||
c.Assert(cfg.IsSet("z"), qt.IsFalse)
|
||||
})
|
||||
|
||||
c.Run("Para", func(c *qt.C) {
|
||||
cfg := New()
|
||||
p := para.New(4)
|
||||
r, _ := p.Start(context.Background())
|
||||
|
||||
setAndGet := func(k string, v int) error {
|
||||
vs := strconv.Itoa(v)
|
||||
cfg.Set(k, v)
|
||||
err := errors.New("get failed")
|
||||
if cfg.Get(k) != v {
|
||||
return err
|
||||
}
|
||||
if cfg.GetInt(k) != v {
|
||||
return err
|
||||
}
|
||||
if cfg.GetString(k) != vs {
|
||||
return err
|
||||
}
|
||||
if !cfg.IsSet(k) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
i := i
|
||||
r.Run(func() error {
|
||||
const v = 42
|
||||
k := fmt.Sprintf("k%d", i)
|
||||
if err := setAndGet(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := maps.Params{
|
||||
"new": 42,
|
||||
}
|
||||
|
||||
cfg.Merge("", m)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
c.Assert(r.Wait(), qt.IsNil)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkDefaultConfigProvider(b *testing.B) {
|
||||
type cfger interface {
|
||||
Get(key string) interface{}
|
||||
Set(key string, value interface{})
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
newMap := func() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
"b": map[string]interface{}{
|
||||
"c": 32,
|
||||
"d": 43,
|
||||
},
|
||||
},
|
||||
"b": 62,
|
||||
}
|
||||
}
|
||||
|
||||
runMethods := func(b *testing.B, cfg cfger) {
|
||||
m := newMap()
|
||||
cfg.Set("mymap", m)
|
||||
cfg.Set("num", 32)
|
||||
if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) {
|
||||
b.Fatal("IsSet failed")
|
||||
}
|
||||
|
||||
if cfg.Get("num") != 32 {
|
||||
b.Fatal("Get failed")
|
||||
}
|
||||
|
||||
if cfg.Get("mymap.a.b.c") != 32 {
|
||||
b.Fatal("Get failed")
|
||||
}
|
||||
}
|
||||
|
||||
b.Run("Viper", func(b *testing.B) {
|
||||
v := viper.New()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runMethods(b, v)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Custom", func(b *testing.B) {
|
||||
cfg := New()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runMethods(b, cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2021 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 config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"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.Set(configRoot, make(maps.Params))
|
||||
}
|
||||
lang := maps.Params{
|
||||
"en": maps.Params{
|
||||
"menus": maps.Params{},
|
||||
"params": maps.Params{},
|
||||
},
|
||||
}
|
||||
cfg.Set("languages", lang)
|
||||
cfg.SetDefaultMergeStrategy()
|
||||
|
||||
configHelpers := map[string]interface{}{
|
||||
"mergeStrategy": cfg.Get(""),
|
||||
}
|
||||
return docshelper.DocProvider{"config": configHelpers}
|
||||
}
|
||||
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ func SetEnvVars(oldVars *[]string, keyValues ...string) {
|
||||
}
|
||||
|
||||
func SplitEnvVar(v string) (string, string) {
|
||||
parts := strings.Split(v, "=")
|
||||
parts := strings.SplitN(v, "=", 2)
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestDecodeConfigFromTOML(t *testing.T) {
|
||||
@@ -94,7 +93,7 @@ PrivacyENhanced = true
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
pc, err := DecodeConfig(viper.New())
|
||||
pc, err := DecodeConfig(config.New())
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(pc, qt.Not(qt.IsNil))
|
||||
c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, false)
|
||||
|
||||
@@ -53,6 +53,11 @@ type Instagram struct {
|
||||
// This means that if you use Bootstrap 4 or want to provide your own CSS, you want
|
||||
// to disable the inline CSS provided by Hugo.
|
||||
DisableInlineCSS bool
|
||||
|
||||
// App or Client Access Token.
|
||||
// If you are using a Client Access Token, remember that you must combine it with your App ID
|
||||
// using a pipe symbol (<APPID>|<CLIENTTOKEN>) otherwise the request will fail.
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
// Twitter holds the functional configuration settings related to the Twitter shortcodes.
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
)
|
||||
|
||||
func TestDecodeConfigFromTOML(t *testing.T) {
|
||||
@@ -55,7 +55,7 @@ disableInlineCSS = true
|
||||
func TestUseSettingsFromRootIfSet(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
cfg := viper.New()
|
||||
cfg := config.New()
|
||||
cfg.Set("disqusShortname", "root_short")
|
||||
cfg.Set("googleAnalytics", "ga_root")
|
||||
|
||||
|
||||
+245
-219
@@ -16,11 +16,16 @@ package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/glob"
|
||||
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
@@ -31,125 +36,165 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
// NewContent creates a new content file in the content directory based upon the
|
||||
// given kind, which is used to lookup an archetype.
|
||||
func NewContent(
|
||||
sites *hugolib.HugoSites, kind, targetPath string) error {
|
||||
targetPath = filepath.Clean(targetPath)
|
||||
ext := helpers.Ext(targetPath)
|
||||
ps := sites.PathSpec
|
||||
archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs
|
||||
sourceFs := ps.Fs.Source
|
||||
const (
|
||||
// DefaultArchetypeTemplateTemplate is the template used in 'hugo new site'
|
||||
// and the template we use as a fall back.
|
||||
DefaultArchetypeTemplateTemplate = `---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
||||
jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext)
|
||||
`
|
||||
)
|
||||
|
||||
archetypeFilename, isDir := findArchetype(ps, kind, ext)
|
||||
contentPath, s := resolveContentPath(sites, sourceFs, targetPath)
|
||||
|
||||
if isDir {
|
||||
|
||||
langFs, err := hugofs.NewLanguageFs(sites.LanguageSet(), archetypeFs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cm.siteUsed {
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
name := filepath.Base(targetPath)
|
||||
return newContentFromDir(archetypeFilename, sites, sourceFs, cm, name, contentPath)
|
||||
// NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
|
||||
// in targetPath.
|
||||
func NewContent(h *hugolib.HugoSites, kind, targetPath string) error {
|
||||
if h.BaseFs.Content.Dirs == nil {
|
||||
return errors.New("no existing content directory configured for this project")
|
||||
}
|
||||
|
||||
// Building the sites can be expensive, so only do it if really needed.
|
||||
siteUsed := false
|
||||
cf := hugolib.NewContentFactory(h)
|
||||
|
||||
if archetypeFilename != "" {
|
||||
if kind == "" {
|
||||
kind = cf.SectionFromFilename(targetPath)
|
||||
}
|
||||
|
||||
var err error
|
||||
siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename)
|
||||
b := &contentBuilder{
|
||||
archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs,
|
||||
sourceFs: h.PathSpec.Fs.Source,
|
||||
ps: h.PathSpec,
|
||||
h: h,
|
||||
cf: cf,
|
||||
|
||||
kind: kind,
|
||||
targetPath: targetPath,
|
||||
}
|
||||
|
||||
ext := paths.Ext(targetPath)
|
||||
|
||||
b.setArcheTypeFilenameToUse(ext)
|
||||
|
||||
withBuildLock := func() (string, error) {
|
||||
unlock, err := h.BaseFs.LockBuild()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", fmt.Errorf("failed to acquire a build lock: %s", err)
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
if b.isDir {
|
||||
return "", b.buildDir()
|
||||
}
|
||||
|
||||
if ext == "" {
|
||||
return "", errors.Errorf("failed to resolve %q to a archetype template", targetPath)
|
||||
}
|
||||
|
||||
if !files.IsContentFile(b.targetPath) {
|
||||
return "", errors.Errorf("target path %q is not a known content format", b.targetPath)
|
||||
}
|
||||
|
||||
return b.buildFile()
|
||||
|
||||
}
|
||||
|
||||
if siteUsed {
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename)
|
||||
filename, err := withBuildLock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println(contentPath, "created")
|
||||
|
||||
editor := s.Cfg.GetString("newContentEditor")
|
||||
if editor != "" {
|
||||
jww.FEEDBACK.Printf("Editing %s with %q ...\n", targetPath, editor)
|
||||
|
||||
editorCmd := append(strings.Fields(editor), contentPath)
|
||||
cmd, err := hexec.SafeCommand(editorCmd[0], editorCmd[1:]...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
if filename != "" {
|
||||
return b.openInEditorIfConfigured(filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func targetSite(sites *hugolib.HugoSites, fi hugofs.FileMetaInfo) *hugolib.Site {
|
||||
for _, s := range sites.Sites {
|
||||
if fi.Meta().Lang() == s.Language().Lang {
|
||||
return s
|
||||
type contentBuilder struct {
|
||||
archeTypeFs afero.Fs
|
||||
sourceFs afero.Fs
|
||||
|
||||
ps *helpers.PathSpec
|
||||
h *hugolib.HugoSites
|
||||
cf hugolib.ContentFactory
|
||||
|
||||
// Builder state
|
||||
archetypeFilename string
|
||||
targetPath string
|
||||
kind string
|
||||
isDir bool
|
||||
dirMap archetypeMap
|
||||
}
|
||||
|
||||
func (b *contentBuilder) buildDir() error {
|
||||
// Split the dir into content files and the rest.
|
||||
if err := b.mapArcheTypeDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var contentTargetFilenames []string
|
||||
var baseDir string
|
||||
|
||||
for _, fi := range b.dirMap.contentFiles {
|
||||
targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename))
|
||||
abs, err := b.cf.CreateContentPlaceHolder(targetFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if baseDir == "" {
|
||||
baseDir = strings.TrimSuffix(abs, targetFilename)
|
||||
}
|
||||
|
||||
contentTargetFilenames = append(contentTargetFilenames, abs)
|
||||
}
|
||||
|
||||
var contentInclusionFilter *glob.FilenameFilter
|
||||
if !b.dirMap.siteUsed {
|
||||
// We don't need to build everything.
|
||||
contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
|
||||
filename = strings.TrimPrefix(filename, string(os.PathSeparator))
|
||||
for _, cn := range contentTargetFilenames {
|
||||
if strings.Contains(cn, filename) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, filename := range contentTargetFilenames {
|
||||
if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return sites.Sites[0]
|
||||
}
|
||||
|
||||
func newContentFromDir(
|
||||
archetypeDir string,
|
||||
sites *hugolib.HugoSites,
|
||||
targetFs afero.Fs,
|
||||
cm archetypeMap, name, targetPath string) error {
|
||||
for _, f := range cm.otherFiles {
|
||||
// Copy the rest as is.
|
||||
for _, f := range b.dirMap.otherFiles {
|
||||
meta := f.Meta()
|
||||
filename := meta.Path()
|
||||
// Just copy the file to destination.
|
||||
filename := meta.Path
|
||||
|
||||
in, err := meta.Open()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open non-content file")
|
||||
}
|
||||
|
||||
targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
|
||||
|
||||
targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename))
|
||||
targetDir := filepath.Dir(targetFilename)
|
||||
if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "failed to create target directory for %s:", targetDir)
|
||||
|
||||
if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "failed to create target directory for %q", targetDir)
|
||||
}
|
||||
|
||||
out, err := targetFs.Create(targetFilename)
|
||||
out, err := b.sourceFs.Create(targetFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -162,41 +207,82 @@ func newContentFromDir(
|
||||
in.Close()
|
||||
out.Close()
|
||||
}
|
||||
|
||||
for _, f := range cm.contentFiles {
|
||||
filename := f.Meta().Path()
|
||||
s := targetSite(sites, f)
|
||||
targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
|
||||
|
||||
content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to execute archetype template")
|
||||
}
|
||||
|
||||
if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil {
|
||||
return errors.Wrap(err, "failed to save results")
|
||||
}
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println(targetPath, "created")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type archetypeMap struct {
|
||||
// These needs to be parsed and executed as Go templates.
|
||||
contentFiles []hugofs.FileMetaInfo
|
||||
// These are just copied to destination.
|
||||
otherFiles []hugofs.FileMetaInfo
|
||||
// If the templates needs a fully built site. This can potentially be
|
||||
// expensive, so only do when needed.
|
||||
siteUsed bool
|
||||
func (b *contentBuilder) buildFile() (string, error) {
|
||||
contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
usesSite, err := b.usesSiteVar(b.archetypeFilename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var contentInclusionFilter *glob.FilenameFilter
|
||||
if !usesSite {
|
||||
// We don't need to build everything.
|
||||
contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
|
||||
filename = strings.TrimPrefix(filename, string(os.PathSeparator))
|
||||
return strings.Contains(contentPlaceholderAbsFilename, filename)
|
||||
})
|
||||
}
|
||||
|
||||
if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b.h.Log.Infof("Content %q created", contentPlaceholderAbsFilename)
|
||||
|
||||
return contentPlaceholderAbsFilename, nil
|
||||
}
|
||||
|
||||
func mapArcheTypeDir(
|
||||
ps *helpers.PathSpec,
|
||||
fs afero.Fs,
|
||||
archetypeDir string) (archetypeMap, error) {
|
||||
func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
|
||||
var pathsToCheck []string
|
||||
|
||||
if b.kind != "" {
|
||||
pathsToCheck = append(pathsToCheck, b.kind+ext)
|
||||
}
|
||||
pathsToCheck = append(pathsToCheck, "default"+ext, "default")
|
||||
|
||||
for _, p := range pathsToCheck {
|
||||
fi, err := b.archeTypeFs.Stat(p)
|
||||
if err == nil {
|
||||
b.archetypeFilename = p
|
||||
b.isDir = fi.IsDir()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error {
|
||||
p := b.h.GetContentPage(contentFilename)
|
||||
if p == nil {
|
||||
panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
|
||||
}
|
||||
|
||||
f, err := b.sourceFs.Create(contentFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if archetypeFilename == "" {
|
||||
return b.cf.AppplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate)
|
||||
}
|
||||
|
||||
return b.cf.AppplyArchetypeFilename(f, p, b.kind, archetypeFilename)
|
||||
|
||||
}
|
||||
|
||||
func (b *contentBuilder) mapArcheTypeDir() error {
|
||||
var m archetypeMap
|
||||
|
||||
walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
@@ -213,7 +299,7 @@ func mapArcheTypeDir(
|
||||
if files.IsContentFile(path) {
|
||||
m.contentFiles = append(m.contentFiles, fil)
|
||||
if !m.siteUsed {
|
||||
m.siteUsed, err = usesSiteVar(fs, path)
|
||||
m.siteUsed, err = b.usesSiteVar(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,120 +314,60 @@ func mapArcheTypeDir(
|
||||
|
||||
walkCfg := hugofs.WalkwayConfig{
|
||||
WalkFn: walkFn,
|
||||
Fs: fs,
|
||||
Root: archetypeDir,
|
||||
Fs: b.archeTypeFs,
|
||||
Root: b.archetypeFilename,
|
||||
}
|
||||
|
||||
w := hugofs.NewWalkway(walkCfg)
|
||||
|
||||
if err := w.Walk(); err != nil {
|
||||
return m, errors.Wrapf(err, "failed to walk archetype dir %q", archetypeDir)
|
||||
return errors.Wrapf(err, "failed to walk archetype dir %q", b.archetypeFilename)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
b.dirMap = m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func usesSiteVar(fs afero.Fs, filename string) (bool, error) {
|
||||
f, err := fs.Open(filename)
|
||||
func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
|
||||
editor := b.h.Cfg.GetString("newContentEditor")
|
||||
if editor == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.h.Log.Infof("Editing %q with %q ...\n", filename, editor)
|
||||
|
||||
cmd, err := hexec.SafeCommand(editor, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (b *contentBuilder) usesSiteVar(filename string) (bool, error) {
|
||||
if filename == "" {
|
||||
return false, nil
|
||||
}
|
||||
bb, err := afero.ReadFile(b.archeTypeFs, filename)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to open archetype file")
|
||||
}
|
||||
defer f.Close()
|
||||
return helpers.ReaderContains(f, []byte(".Site")), nil
|
||||
|
||||
return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil
|
||||
|
||||
}
|
||||
|
||||
// Resolve the target content path.
|
||||
func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) {
|
||||
targetDir := filepath.Dir(targetPath)
|
||||
first := sites.Sites[0]
|
||||
|
||||
var (
|
||||
s *hugolib.Site
|
||||
siteContentDir string
|
||||
)
|
||||
|
||||
// Try the filename: my-post.en.md
|
||||
for _, ss := range sites.Sites {
|
||||
if strings.Contains(targetPath, "."+ss.Language().Lang+".") {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var dirLang string
|
||||
|
||||
for _, dir := range sites.BaseFs.Content.Dirs {
|
||||
meta := dir.Meta()
|
||||
contentDir := meta.Filename()
|
||||
|
||||
if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) {
|
||||
contentDir += helpers.FilePathSeparator
|
||||
}
|
||||
|
||||
if strings.HasPrefix(targetPath, contentDir) {
|
||||
siteContentDir = contentDir
|
||||
dirLang = meta.Lang()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if s == nil && dirLang != "" {
|
||||
for _, ss := range sites.Sites {
|
||||
if ss.Lang() == dirLang {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
s = first
|
||||
}
|
||||
|
||||
if targetDir != "" && targetDir != "." {
|
||||
exists, _ := helpers.Exists(targetDir, fs)
|
||||
|
||||
if exists {
|
||||
return targetPath, s
|
||||
}
|
||||
}
|
||||
|
||||
if siteContentDir == "" {
|
||||
}
|
||||
|
||||
if siteContentDir != "" {
|
||||
pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir))
|
||||
return s.PathSpec.AbsPathify(pp), s
|
||||
} else {
|
||||
var contentDir string
|
||||
for _, dir := range sites.BaseFs.Content.Dirs {
|
||||
contentDir = dir.Meta().Filename()
|
||||
if dir.Meta().Lang() == s.Lang() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s.PathSpec.AbsPathify(filepath.Join(contentDir, targetPath)), s
|
||||
}
|
||||
}
|
||||
|
||||
// FindArchetype takes a given kind/archetype of content and returns the path
|
||||
// to the archetype in the archetype filesystem, blank if none found.
|
||||
func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) {
|
||||
fs := ps.BaseFs.Archetypes.Fs
|
||||
|
||||
var pathsToCheck []string
|
||||
|
||||
if kind != "" {
|
||||
pathsToCheck = append(pathsToCheck, kind+ext)
|
||||
}
|
||||
pathsToCheck = append(pathsToCheck, "default"+ext, "default")
|
||||
|
||||
for _, p := range pathsToCheck {
|
||||
fi, err := fs.Stat(p)
|
||||
if err == nil {
|
||||
return p, fi.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
type archetypeMap struct {
|
||||
// These needs to be parsed and executed as Go templates.
|
||||
contentFiles []hugofs.FileMetaInfo
|
||||
// These are just copied to destination.
|
||||
otherFiles []hugofs.FileMetaInfo
|
||||
// If the templates needs a fully built site. This can potentially be
|
||||
// expensive, so only do when needed.
|
||||
siteUsed bool
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
// Copyright 2017 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 create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// ArchetypeFileData represents the data available to an archetype template.
|
||||
type ArchetypeFileData struct {
|
||||
// The archetype content type, either given as --kind option or extracted
|
||||
// from the target path's section, i.e. "blog/mypost.md" will resolve to
|
||||
// "blog".
|
||||
Type string
|
||||
|
||||
// The current date and time as a RFC3339 formatted string, suitable for use in front matter.
|
||||
Date string
|
||||
|
||||
// The Site, fully equipped with all the pages etc. Note: This will only be set if it is actually
|
||||
// used in the archetype template. Also, if this is a multilingual setup,
|
||||
// this site is the site that best matches the target content file, based
|
||||
// on the presence of language code in the filename.
|
||||
Site *hugolib.SiteInfo
|
||||
|
||||
// Name will in most cases be the same as TranslationBaseName, e.g. "my-post".
|
||||
// But if that value is "index" (bundles), the Name is instead the owning folder.
|
||||
// This is the value you in most cases would want to use to construct the title in your
|
||||
// archetype template.
|
||||
Name string
|
||||
|
||||
// The target content file. Note that the .Content will be empty, as that
|
||||
// has not been created yet.
|
||||
source.File
|
||||
}
|
||||
|
||||
const (
|
||||
// ArchetypeTemplateTemplate is used as initial template when adding an archetype template.
|
||||
ArchetypeTemplateTemplate = `---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
archetypeShortcodeReplacementsPre = strings.NewReplacer(
|
||||
"{{<", "{x{<",
|
||||
"{{%", "{x{%",
|
||||
">}}", ">}x}",
|
||||
"%}}", "%}x}")
|
||||
|
||||
archetypeShortcodeReplacementsPost = strings.NewReplacer(
|
||||
"{x{<", "{{<",
|
||||
"{x{%", "{{%",
|
||||
">}x}", ">}}",
|
||||
"%}x}", "%}}")
|
||||
)
|
||||
|
||||
func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archetypeFilename string) ([]byte, error) {
|
||||
var (
|
||||
archetypeContent []byte
|
||||
archetypeTemplate []byte
|
||||
err error
|
||||
)
|
||||
|
||||
f, err := s.SourceSpec.NewFileInfoFrom(targetPath, targetPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
name = f.TranslationBaseName()
|
||||
|
||||
if name == "index" || name == "_index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
}
|
||||
}
|
||||
|
||||
data := ArchetypeFileData{
|
||||
Type: kind,
|
||||
Date: time.Now().Format(time.RFC3339),
|
||||
Name: name,
|
||||
File: f,
|
||||
Site: s.Info,
|
||||
}
|
||||
|
||||
if archetypeFilename == "" {
|
||||
// TODO(bep) archetype revive the issue about wrong tpl funcs arg order
|
||||
archetypeTemplate = []byte(ArchetypeTemplateTemplate)
|
||||
} else {
|
||||
archetypeTemplate, err = afero.ReadFile(s.BaseFs.Archetypes.Fs, archetypeFilename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read archetype file %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The archetype template may contain shortcodes, and these does not play well
|
||||
// with the Go templates. Need to set some temporary delimiters.
|
||||
archetypeTemplate = []byte(archetypeShortcodeReplacementsPre.Replace(string(archetypeTemplate)))
|
||||
|
||||
// Reuse the Hugo template setup to get the template funcs properly set up.
|
||||
templateHandler := s.Deps.Tmpl().(tpl.TemplateManager)
|
||||
templateName := helpers.Filename(archetypeFilename)
|
||||
if err := templateHandler.AddTemplate("_text/"+templateName, string(archetypeTemplate)); err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename)
|
||||
}
|
||||
|
||||
templ, _ := templateHandler.Lookup(templateName)
|
||||
|
||||
var buff bytes.Buffer
|
||||
if err := templateHandler.Execute(templ, &buff, data); err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename)
|
||||
}
|
||||
|
||||
archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String()))
|
||||
|
||||
return archetypeContent, nil
|
||||
}
|
||||
+190
-45
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
@@ -30,30 +32,36 @@ import (
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// TODO(bep) clean this up. Export the test site builder in Hugolib or something.
|
||||
func TestNewContent(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
kind string
|
||||
path string
|
||||
expected []string
|
||||
expected interface{}
|
||||
}{
|
||||
{"post", "post/sample-1.md", []string{`title = "Post Arch title"`, `test = "test1"`, "date = \"2015-01-12T19:20:04-07:00\""}},
|
||||
{"post", "post/org-1.org", []string{`#+title: ORG-1`}},
|
||||
{"emptydate", "post/sample-ed.md", []string{`title = "Empty Date Arch title"`, `test = "test1"`}},
|
||||
{"stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file
|
||||
{"", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype
|
||||
{"product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter
|
||||
{"lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}},
|
||||
{"lang", "content/post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}},
|
||||
{"lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}},
|
||||
{"lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"lang", "content/post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}},
|
||||
{"shortcodes", "shortcodes/go.md", []string{
|
||||
{"Post", "post", "post/sample-1.md", []string{`title = "Post Arch title"`, `test = "test1"`, "date = \"2015-01-12T19:20:04-07:00\""}},
|
||||
{"Post org-mode", "post", "post/org-1.org", []string{`#+title: ORG-1`}},
|
||||
{"Post, unknown content filetype", "post", "post/sample-1.pdoc", false},
|
||||
{"Empty date", "emptydate", "post/sample-ed.md", []string{`title = "Empty Date Arch title"`, `test = "test1"`}},
|
||||
{"Archetype file not found", "stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file
|
||||
{"No archetype", "", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype
|
||||
{"Empty archetype", "product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter
|
||||
{"Filenames", "filenames", "content/mypage/index.md", []string{"title = \"INDEX\"\n+++\n\n\nContentBaseName: mypage"}},
|
||||
{"Branch Name", "name", "content/tags/tag-a/_index.md", []string{"+++\ntitle = 'Tag A'\n+++"}},
|
||||
|
||||
{"Lang 1", "lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}},
|
||||
{"Lang 2", "lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}},
|
||||
{"Lang nn file", "lang", "content/post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}},
|
||||
{"Lang nn dir", "lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}},
|
||||
{"Lang en in nn dir", "lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}},
|
||||
{"Lang en default", "lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"Lang en file", "lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"Lang nn bundle", "lang", "content/post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}},
|
||||
{"Site", "site", "content/mypage/index.md", []string{"RegularPages .Site: 10", "RegularPages site: 10"}},
|
||||
{"Shortcodes", "shortcodes", "shortcodes/go.md", []string{
|
||||
`title = "GO"`,
|
||||
"{{< myshortcode >}}",
|
||||
"{{% myshortcode %}}",
|
||||
@@ -61,28 +69,41 @@ func TestNewContent(t *testing.T) {
|
||||
}}, // shortcodes
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
for i, cas := range cases {
|
||||
cas := cas
|
||||
t.Run(fmt.Sprintf("%s-%d", cas.kind, i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run(cas.name, func(c *qt.C) {
|
||||
c.Parallel()
|
||||
|
||||
mm := afero.NewMemMapFs()
|
||||
c.Assert(initFs(mm), qt.IsNil)
|
||||
cfg, fs := newTestCfg(c, mm)
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(create.NewContent(h, cas.kind, cas.path), qt.IsNil)
|
||||
err = create.NewContent(h, cas.kind, cas.path)
|
||||
|
||||
if b, ok := cas.expected.(bool); ok && !b {
|
||||
if !b {
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
fname := filepath.FromSlash(cas.path)
|
||||
if !strings.HasPrefix(fname, "content") {
|
||||
fname = filepath.Join("content", fname)
|
||||
}
|
||||
content := readFileFromFs(t, fs.Source, fname)
|
||||
for _, v := range cas.expected {
|
||||
content := readFileFromFs(c, fs.Source, fname)
|
||||
|
||||
for _, v := range cas.expected.([]string) {
|
||||
found := strings.Contains(content, v)
|
||||
if !found {
|
||||
t.Fatalf("[%d] %q missing from output:\n%q", i, v, content)
|
||||
c.Fatalf("[%d] %q missing from output:\n%q", i, v, content)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -95,10 +116,10 @@ func TestNewContentFromDir(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
archetypeDir := filepath.Join("archetypes", "my-bundle")
|
||||
c.Assert(mm.MkdirAll(archetypeDir, 0755), qt.IsNil)
|
||||
c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil)
|
||||
|
||||
archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle")
|
||||
c.Assert(mm.MkdirAll(archetypeThemeDir, 0755), qt.IsNil)
|
||||
c.Assert(mm.MkdirAll(archetypeThemeDir, 0o755), qt.IsNil)
|
||||
|
||||
contentFile := `
|
||||
File: %s
|
||||
@@ -107,15 +128,15 @@ Name: {{ replace .Name "-" " " | title }}
|
||||
i18n: {{ T "hugo" }}
|
||||
`
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(initFs(mm), qt.IsNil)
|
||||
cfg, fs := newTestCfg(c, mm)
|
||||
@@ -134,15 +155,90 @@ i18n: {{ T "hugo" }}
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Post`, `i18n: Hugo Rocks!`)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Site Lang: nn`, `Name: My Post`, `i18n: Hugo Rokkar!`)
|
||||
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: My Post`)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: Bio`)
|
||||
|
||||
c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"), qt.IsNil)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
}
|
||||
|
||||
func TestNewContentFromDirSiteFunction(t *testing.T) {
|
||||
mm := afero.NewMemMapFs()
|
||||
c := qt.New(t)
|
||||
|
||||
archetypeDir := filepath.Join("archetypes", "my-bundle")
|
||||
c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil)
|
||||
|
||||
contentFile := `
|
||||
File: %s
|
||||
site RegularPages: {{ len site.RegularPages }}
|
||||
|
||||
`
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(initFs(mm), qt.IsNil)
|
||||
cfg, fs := newTestCfg(c, mm)
|
||||
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(len(h.Sites), qt.Equals, 2)
|
||||
|
||||
c.Assert(create.NewContent(h, "my-bundle", "post/my-post"), qt.IsNil)
|
||||
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `site RegularPages: 10`)
|
||||
}
|
||||
|
||||
func TestNewContentFromDirNoSite(t *testing.T) {
|
||||
mm := afero.NewMemMapFs()
|
||||
c := qt.New(t)
|
||||
|
||||
archetypeDir := filepath.Join("archetypes", "my-bundle")
|
||||
c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil)
|
||||
|
||||
archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle")
|
||||
c.Assert(mm.MkdirAll(archetypeThemeDir, 0o755), qt.IsNil)
|
||||
|
||||
contentFile := `
|
||||
File: %s
|
||||
Name: {{ replace .Name "-" " " | title }}
|
||||
i18n: {{ T "hugo" }}
|
||||
`
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(initFs(mm), qt.IsNil)
|
||||
cfg, fs := newTestCfg(c, mm)
|
||||
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(len(h.Sites), qt.Equals, 2)
|
||||
|
||||
c.Assert(create.NewContent(h, "my-bundle", "post/my-post"), qt.IsNil)
|
||||
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`)
|
||||
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Name: My Post`, `i18n: Hugo Rocks!`)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Name: My Post`, `i18n: Hugo Rokkar!`)
|
||||
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Name: Bio`)
|
||||
|
||||
c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"), qt.IsNil)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
|
||||
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
}
|
||||
|
||||
func initFs(fs afero.Fs) error {
|
||||
perm := os.FileMode(0755)
|
||||
perm := os.FileMode(0o755)
|
||||
var err error
|
||||
|
||||
// create directories
|
||||
@@ -158,7 +254,16 @@ func initFs(fs afero.Fs) error {
|
||||
}
|
||||
}
|
||||
|
||||
// create files
|
||||
// create some dummy content
|
||||
for i := 1; i <= 10; i++ {
|
||||
filename := filepath.Join("content", fmt.Sprintf("page%d.md", i))
|
||||
afero.WriteFile(fs, filename, []byte(`---
|
||||
title: Test
|
||||
---
|
||||
`), 0666)
|
||||
}
|
||||
|
||||
// create archetype files
|
||||
for _, v := range []struct {
|
||||
path string
|
||||
content string
|
||||
@@ -171,11 +276,40 @@ func initFs(fs afero.Fs) error {
|
||||
path: filepath.Join("archetypes", "post.org"),
|
||||
content: "#+title: {{ .BaseFileName | upper }}",
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "name.md"),
|
||||
content: `+++
|
||||
title = '{{ replace .Name "-" " " | title }}'
|
||||
+++`,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "product.md"),
|
||||
content: `+++
|
||||
title = "{{ .BaseFileName | upper }}"
|
||||
+++`,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "filenames.md"),
|
||||
content: `...
|
||||
title = "{{ .BaseFileName | upper }}"
|
||||
+++
|
||||
|
||||
|
||||
ContentBaseName: {{ .File.ContentBaseName }}
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "site.md"),
|
||||
content: `...
|
||||
title = "{{ .BaseFileName | upper }}"
|
||||
+++
|
||||
|
||||
Len RegularPages .Site: {{ len .Site.RegularPages }}
|
||||
Len RegularPages site: {{ len site.RegularPages }}
|
||||
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "emptydate.md"),
|
||||
@@ -183,7 +317,7 @@ title = "{{ .BaseFileName | upper }}"
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "lang.md"),
|
||||
content: `Site Lang: {{ .Site.Language.Lang }}|Name: {{ replace .Name "-" " " | title }}|i18n: {{ T "hugo" }}`,
|
||||
content: `Site Lang: {{ site.Language.Lang }}|Name: {{ replace .Name "-" " " | title }}|i18n: {{ T "hugo" }}`,
|
||||
},
|
||||
// #3623x
|
||||
{
|
||||
@@ -226,7 +360,7 @@ func cContains(c *qt.C, v interface{}, matches ...string) {
|
||||
}
|
||||
|
||||
// TODO(bep) extract common testing package with this and some others
|
||||
func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
|
||||
func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
|
||||
t.Helper()
|
||||
filename = filepath.FromSlash(filename)
|
||||
b, err := afero.ReadFile(fs, filename)
|
||||
@@ -245,7 +379,7 @@ func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func newTestCfg(c *qt.C, mm afero.Fs) (*viper.Viper, *hugofs.Fs) {
|
||||
func newTestCfg(c *qt.C, mm afero.Fs) (config.Provider, *hugofs.Fs) {
|
||||
cfg := `
|
||||
|
||||
theme = "mytheme"
|
||||
@@ -256,23 +390,34 @@ languageName = "English"
|
||||
[languages.nn]
|
||||
weight = 2
|
||||
languageName = "Nynorsk"
|
||||
contentDir = "content_nn"
|
||||
|
||||
[module]
|
||||
[[module.mounts]]
|
||||
source = 'archetypes'
|
||||
target = 'archetypes'
|
||||
[[module.mounts]]
|
||||
source = 'content'
|
||||
target = 'content'
|
||||
lang = 'en'
|
||||
[[module.mounts]]
|
||||
source = 'content_nn'
|
||||
target = 'content'
|
||||
lang = 'nn'
|
||||
`
|
||||
if mm == nil {
|
||||
mm = afero.NewMemMapFs()
|
||||
}
|
||||
|
||||
mm.MkdirAll(filepath.FromSlash("content_nn"), 0777)
|
||||
mm.MkdirAll(filepath.FromSlash("content_nn"), 0o777)
|
||||
|
||||
mm.MkdirAll(filepath.FromSlash("themes/mytheme"), 0777)
|
||||
mm.MkdirAll(filepath.FromSlash("themes/mytheme"), 0o777)
|
||||
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join("i18n", "en.toml"), []byte(`[hugo]
|
||||
other = "Hugo Rocks!"`), 0755), qt.IsNil)
|
||||
other = "Hugo Rocks!"`), 0o755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, filepath.Join("i18n", "nn.toml"), []byte(`[hugo]
|
||||
other = "Hugo Rokkar!"`), 0755), qt.IsNil)
|
||||
other = "Hugo Rokkar!"`), 0o755), qt.IsNil)
|
||||
|
||||
c.Assert(afero.WriteFile(mm, "config.toml", []byte(cfg), 0755), qt.IsNil)
|
||||
c.Assert(afero.WriteFile(mm, "config.toml", []byte(cfg), 0o755), qt.IsNil)
|
||||
|
||||
v, _, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
+9
-4
@@ -45,6 +45,7 @@ import (
|
||||
_ "gocloud.dev/blob/fileblob" // import
|
||||
_ "gocloud.dev/blob/gcsblob" // import
|
||||
_ "gocloud.dev/blob/s3blob" // import
|
||||
"gocloud.dev/gcerrors"
|
||||
)
|
||||
|
||||
// Deployer supports deploying the site to target cloud providers.
|
||||
@@ -238,9 +239,13 @@ func (d *Deployer) Deploy(ctx context.Context) error {
|
||||
go func(del string) {
|
||||
jww.INFO.Printf("Deleting %s...\n", del)
|
||||
if err := bucket.Delete(ctx, del); err != nil {
|
||||
errMu.Lock()
|
||||
defer errMu.Unlock()
|
||||
errs = append(errs, err)
|
||||
if gcerrors.Code(err) == gcerrors.NotFound {
|
||||
jww.WARN.Printf("Failed to delete %q because it wasn't found: %v", del, err)
|
||||
} else {
|
||||
errMu.Lock()
|
||||
defer errMu.Unlock()
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
<-sem
|
||||
}(del)
|
||||
@@ -432,7 +437,7 @@ func (lf *localFile) ContentType() string {
|
||||
}
|
||||
|
||||
ext := filepath.Ext(lf.NativePath)
|
||||
if mimeType, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found {
|
||||
if mimeType, _, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found {
|
||||
return mimeType.Type()
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
)
|
||||
|
||||
func TestDecodeConfigFromTOML(t *testing.T) {
|
||||
@@ -164,7 +164,7 @@ Pattern = "[" # invalid regular expression
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
dcfg, err := decodeConfig(viper.New())
|
||||
dcfg, err := decodeConfig(config.New())
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(len(dcfg.Targets), qt.Equals, 0)
|
||||
c.Assert(len(dcfg.Matchers), qt.Equals, 0)
|
||||
|
||||
Vendored
+4
-9
@@ -34,10 +34,7 @@ type Deps struct {
|
||||
Log loggers.Logger `json:"-"`
|
||||
|
||||
// Used to log errors that may repeat itself many times.
|
||||
DistinctErrorLog *helpers.DistinctLogger
|
||||
|
||||
// Used to log warnings that may repeat itself many times.
|
||||
DistinctWarningLog *helpers.DistinctLogger
|
||||
LogDistinct loggers.Logger
|
||||
|
||||
// The templates to use. This will usually implement the full tpl.TemplateManager.
|
||||
tmpl tpl.TemplateHandler
|
||||
@@ -256,7 +253,7 @@ func New(cfg DepsCfg) (*Deps, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sp := source.NewSourceSpec(ps, fs.Source)
|
||||
sp := source.NewSourceSpec(ps, nil, fs.Source)
|
||||
|
||||
timeoutms := cfg.Language.GetInt("timeout")
|
||||
if timeoutms <= 0 {
|
||||
@@ -266,14 +263,12 @@ func New(cfg DepsCfg) (*Deps, error) {
|
||||
ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
|
||||
ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
|
||||
|
||||
distinctErrorLogger := helpers.NewDistinctLogger(logger.Error())
|
||||
distinctWarnLogger := helpers.NewDistinctLogger(logger.Warn())
|
||||
logDistinct := helpers.NewDistinctLogger(logger)
|
||||
|
||||
d := &Deps{
|
||||
Fs: fs,
|
||||
Log: ignorableLogger,
|
||||
DistinctErrorLog: distinctErrorLogger,
|
||||
DistinctWarningLog: distinctWarnLogger,
|
||||
LogDistinct: logDistinct,
|
||||
templateProvider: cfg.TemplateProvider,
|
||||
translationProvider: cfg.TranslationProvider,
|
||||
WithTemplate: cfg.WithTemplate,
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
/.idea
|
||||
/public
|
||||
node_modules
|
||||
nohup.out
|
||||
.DS_Store
|
||||
trace.out
|
||||
trace.out
|
||||
.hugo_build.lock
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre, .pre {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em;
|
||||
margin: 0;
|
||||
@@ -23,8 +17,7 @@ code {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
pre code {
|
||||
pre code {
|
||||
display: block;
|
||||
padding: 1.5em 1.5em;
|
||||
font-size: .875rem;
|
||||
@@ -32,7 +25,6 @@ pre code {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
|
||||
@@ -4083,7 +4083,7 @@ img { max-width: 100%; }
|
||||
max-width: 30em;
|
||||
}
|
||||
.measure-wide-l {
|
||||
max-width: 34em;
|
||||
max-width: 40em;
|
||||
}
|
||||
.measure-narrow-l {
|
||||
max-width: 20em;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
[[banners]]
|
||||
name = "Forestry.io"
|
||||
link = "https://forestry.io/"
|
||||
logo = "/images/sponsors/forestry-logotype.svg"
|
||||
copy = ""
|
||||
|
||||
[[banners]]
|
||||
name = "Linode"
|
||||
link = "https://www.linode.com/"
|
||||
@@ -14,4 +8,12 @@ copy = ""
|
||||
name = "eSolia"
|
||||
link = "https://esolia.com/post/why-did-esolia-choose-hugo/"
|
||||
logo = "/images/sponsors/esolia-logo.svg"
|
||||
copy = ""
|
||||
|
||||
|
||||
[[banners]]
|
||||
name = "Brave"
|
||||
link = "https://brave.com/"
|
||||
logo = "/images/sponsors/brave-logo.svg"
|
||||
bgcolor = "#5A2FA1"
|
||||
copy = ""
|
||||
+3
-4
@@ -39,6 +39,8 @@
|
||||
{{ $.Scratch.Set "stylesheet" . }}
|
||||
{{end}}
|
||||
|
||||
<meta name="description"
|
||||
content="{{ with .Description }}{{ . }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}">
|
||||
|
||||
{{ block "scripts" . }}
|
||||
{{- partial "site-scripts.html" . -}}
|
||||
@@ -53,9 +55,6 @@
|
||||
{{ partial "gtag" . }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body class="ma0 sans-serif bg-primary-color-light{{ with getenv "HUGO_ENV" }} {{ . }}{{ end }}">
|
||||
@@ -69,4 +68,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+24
-24
@@ -1,31 +1,31 @@
|
||||
{{ define "main" }}
|
||||
<article class="w-100 ph4 pb5 pb6-ns pt1 pt5-ns">
|
||||
<div class="flex-l">
|
||||
<article class="w-100 ph4 pb5 pb6-ns pt1 pt5-ns">
|
||||
<div class="flex-l">
|
||||
|
||||
<div class="order-2 w-100 w-20-l ph5-m ph0-l mb4 sticky">
|
||||
<div class="order-2 w-100 w-20-l ph5-m ph0-l mb4 sticky">
|
||||
{{- partial "toc.html" . -}}
|
||||
</div>
|
||||
|
||||
<div class="order-1 w-60-l mw7 ph0 ph5-ns mid-gray nested-copy-line-height no-underline nested-links nested-img nested-copy-seperator nested-blockquote mt0-ns" style="flex-grow:1;">
|
||||
<div class="documentation-copy center measure-wide-l">
|
||||
<div id="readout" class="fixed right-0 bottom-0">
|
||||
</div>
|
||||
{{ .Render "page" }}
|
||||
{{ partial "related.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-0 w-20 dn db-l">
|
||||
{{ partial "nav-links-docs.html" . }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
<div class="order-1 w-60-l mw7 ph0 ph5-ns mid-gray nested-copy-line-height no-underline nested-links nested-img nested-copy-seperator nested-blockquote mt0-ns" style="flex-grow:1;">
|
||||
<div class="documentation-copy center measure-wide-l">
|
||||
<div id="readout" class="fixed right-0 bottom-0">
|
||||
</div>
|
||||
{{ .Render "page" }}
|
||||
{{ partial "related.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-0 w-20 dn db-l">
|
||||
{{ partial "nav-links-docs.html" . }}
|
||||
</div>
|
||||
|
||||
<div class="w-100 bg-light-gray">
|
||||
<div class="mw7 pa4 center nested-lh-copy lh-copy">
|
||||
{{ partial "docs/page-meta-data.html" . }}
|
||||
{{ partial "page-edit.html" . }}
|
||||
{{ partial "tags.html" . }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="w-100 bg-light-gray">
|
||||
<div class="mw7 pa4 center nested-lh-copy lh-copy">
|
||||
{{ partial "docs/page-meta-data.html" . }}
|
||||
{{ partial "page-edit.html" . }}
|
||||
{{ partial "tags.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
gtag('js', new Date());
|
||||
|
||||
{{ $site := $.Site.BaseURL | replaceRE "^https?://(www\\.)?([^/]+).*" "$2" }}
|
||||
gtag('config', '{{ . }}', {'dimension1': '{{ $site }}', 'dimension2': '{{ getenv "BRANCH" }}'});
|
||||
gtag('config', '{{ . }}', {'anonymize_ip': true, 'dimension1': '{{ $site }}', 'dimension2': '{{ getenv "BRANCH" }}'});
|
||||
|
||||
/**
|
||||
* Function that tracks a click on an outbound link in Analytics.
|
||||
|
||||
docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/installation.html
Generated
+1
-2
@@ -14,7 +14,7 @@
|
||||
<div class="h4">
|
||||
<img src="/images/gopher-hero.svg" alt="Hugo Gopher" class="h4">
|
||||
</div>
|
||||
<p class="f6 mid-gray m0 p0 pt3">Mac OS</p>
|
||||
<p class="f6 mid-gray m0 p0 pt3">macOS</p>
|
||||
<div class="code f6 bg-black light-green lh-copy pv3 ph3 br2 w-100 w-50-ns center mb2 tl">
|
||||
$ brew install hugo <br />
|
||||
</div>
|
||||
@@ -35,5 +35,4 @@
|
||||
|
||||
<!-- TODO: Animate the gopher https://github.com/rdwatters/hugo-docs-concept/issues/3#issuecomment-287194777 -->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
{{$classes_box := "ba b--light-gray bg-light-gray br3 flex flex-column flex-wrap items-center justify-center ph3 pv4 mb4 w-100 w-30-l "}}
|
||||
{{$classes_box := "ba b--dark-gray bg-light-gray br3 flex flex-column flex-wrap items-center justify-center ph3 pv4 mb4 w-100 w-30-l "}}
|
||||
{{$gtag := .gtag | default "unknown" }}
|
||||
{{ with .cx.Site.Data.sponsors }}
|
||||
<section class="{{ $.classes_section | default "bg-primary-color-dark b--dark-gray bb bt ph5 pv4 w-100"}}">
|
||||
@@ -8,7 +8,7 @@
|
||||
{{ range .banners }}
|
||||
{{ $banner := . }}
|
||||
{{if .logo}}
|
||||
<div class="{{$classes_box}} o-100">
|
||||
<div class="{{$classes_box}} o-100"{{ with .bgcolor }} style="background-color: {{ . }};"{{ end}}>
|
||||
{{with .link -}}
|
||||
{{ $url := printf "%s?%s" . (querify "utm_source" "homepage" "utm_medium" "banner" "utm_campaign" "hugosponsor") | safeURL }}
|
||||
{{ if eq (getenv "HUGO_ENV") "production" | or (eq $.cx.Site.Params.env "production") }}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<form id="site-search-form" action="" role="search">
|
||||
<fieldset class="bn ma0 pa0">
|
||||
<label class="clip" for="search-input">Search</label>
|
||||
<input type="search" id="search-input" class="needs-js bg-left bn f5 input-reset lh-solid mt3 mt0-ns pl4 pv2 w5 white" placeholder="Search the Docs" name="search-input" value="" style="background: transparent url('/images/icon-search.png') no-repeat 0 8px /16px 16px;">
|
||||
<input type="search" id="search-input" class="needs-js bg-left bn f5 input-reset lh-solid mt3 mt0-ns pl4 pv2 w5 white" placeholder="Search the Docs" name="search-input" value="" style="background: url('/images/icon-search.png') 5px 11px/16px 16px no-repeat; background-color: rgba(255,255,255,0);">
|
||||
</fieldset>
|
||||
</form>
|
||||
</form>
|
||||
@@ -17,7 +17,7 @@
|
||||
{{/* pagination.html: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/template_embedded.go#L117 */}}
|
||||
{{ template "_internal/pagination.html" . }}
|
||||
</div>
|
||||
<div class="dib f3 mt4">The Showcase articles are copyright the content authors. Any open source license will be attached.</div>
|
||||
<div class="dib f3 mt4">The Showcase articles are copyrighted by their respective content authors. Any open source license will be attached.</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "gohugo-default-styles",
|
||||
"version": "1.1.0",
|
||||
"description": "Default Theme for Hugo Sites",
|
||||
"main": "index.js",
|
||||
"homepage": "https://gohugo.io/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/gohugoio/gohugoioTheme/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/gohugoio/gohugoioTheme.git"
|
||||
},
|
||||
"author": "budparr",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack",
|
||||
"build-dev": "NODE_ENV=development webpack --progress --watch",
|
||||
"start": "npm run build-dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clean-webpack-plugin": "^1.0.0",
|
||||
"clipboard": "^2.0.4",
|
||||
"css-loader": "^4.3.0",
|
||||
"docsearch.js": "^2.6.1",
|
||||
"file-loader": "^2.0.0",
|
||||
"glob-all": "^3.1.0",
|
||||
"highlight.js": "^9.13.1",
|
||||
"lazysizes": "^5.2.1",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"postcss": "^7.0.32",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"purgecss-webpack-plugin": "^1.3.1",
|
||||
"scrolldir": "^1.4.0",
|
||||
"tachyons": "^4.7.0",
|
||||
"typeface-muli": "0.0.54",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-command": "^0.4.2"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.6 KiB |
@@ -1 +1 @@
|
||||
# github.com/gohugoio/gohugoioTheme v0.0.0-20200711112515-b0dfe471654f
|
||||
# github.com/gohugoio/gohugoioTheme v0.0.0-20210409071416-c88da48134b7
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
linktitle: ""
|
||||
description: ""
|
||||
godocref: ""
|
||||
publishdate: ""
|
||||
lastmod: ""
|
||||
categories: []
|
||||
@@ -10,4 +9,4 @@ weight: 00
|
||||
slug: ""
|
||||
aliases: []
|
||||
toc: false
|
||||
---
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
linktitle: ""
|
||||
description: ""
|
||||
godocref: ""
|
||||
publishdate: ""
|
||||
lastmod: ""
|
||||
categories: [functions]
|
||||
@@ -14,4 +13,4 @@ aliases: []
|
||||
relatedfuncs: []
|
||||
toc: false
|
||||
deprecated: false
|
||||
---
|
||||
---
|
||||
|
||||
+7
-1
@@ -7,7 +7,7 @@ footnotereturnlinkcontents = "↩"
|
||||
languageCode = "en-us"
|
||||
title = "Hugo"
|
||||
|
||||
ignoreErrors = ["error-remote-getjson"]
|
||||
ignoreErrors = ["error-remote-getjson", "error-missing-instagram-accesstoken"]
|
||||
|
||||
|
||||
googleAnalytics = "UA-7131036-4"
|
||||
@@ -17,6 +17,12 @@ pluralizeListTitles = false
|
||||
# We do redirects via Netlify's _redirects file, generated by Hugo (see "outputs" below).
|
||||
disableAliases = true
|
||||
|
||||
[minify]
|
||||
[minify.tdewolff]
|
||||
[minify.tdewolff.css]
|
||||
[minify.tdewolff.html]
|
||||
keepWhitespace = true
|
||||
|
||||
[module]
|
||||
[module.hugoVersion]
|
||||
min = "0.56.0"
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
defaultMarkdownHandler = "goldmark"
|
||||
|
||||
[goldmark]
|
||||
[goldmark.extensions]
|
||||
definitionList = true
|
||||
footnote = true
|
||||
linkify = true
|
||||
strikethrough = true
|
||||
table = true
|
||||
taskList = true
|
||||
typographer = true
|
||||
[goldmark.parser]
|
||||
autoHeadingID = true
|
||||
autoHeadingIDType = "github"
|
||||
[goldmark.parser.attribute]
|
||||
block = true
|
||||
title = true
|
||||
[goldmark.renderer]
|
||||
hardWraps = false
|
||||
unsafe = false
|
||||
xhtml = false
|
||||
|
||||
[highlight]
|
||||
style = "trac"
|
||||
lineNumbersInTable = true
|
||||
noClasses = false
|
||||
|
||||
[tableOfContents]
|
||||
endLevel = 2
|
||||
ordered = false
|
||||
startLevel = 2
|
||||
@@ -36,7 +36,7 @@ This has many benefits. The most noticeable is performance. HTTP servers are *ve
|
||||
|
||||
|
||||
["An Introduction to Static Site Generators", David Walsh]: https://davidwalsh.name/introduction-static-site-generators
|
||||
["Static Site Generators", O'Reilly]: https://www.oreilly.com/web-platform/free/files/static-site-generators.pdf
|
||||
["Static Site Generators", O'Reilly]: https://github.com/gohugoio/hugoDocs/files/1242701/static-site-generators.pdf
|
||||
["Top 10 Static Website Generators", Netlify blog]: https://www.netlify.com/blog/2016/05/02/top-ten-static-website-generators/
|
||||
[hugovwordpress]: https://gettingthingstech.com/hugo-vs.-wordpress-page-load-speed-comparison-hugo-leaves-wordpress-in-its-dust/
|
||||
[StaticGen: Top Open-Source Static Site Generators (GitHub Stars)]: https://www.staticgen.com/
|
||||
|
||||
@@ -92,6 +92,9 @@ respectDoNotTrack
|
||||
useSessionStorage
|
||||
: Enabling this will disable the use of Cookies and use Session Storage to Store the GA Client ID.
|
||||
|
||||
{{% warning %}}
|
||||
`useSessionStorage` is not supported when using Google Analytics v4 (gtag.js).
|
||||
{{% /warning %}}
|
||||
### Instagram
|
||||
|
||||
simple
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
date: 2020-09-13
|
||||
title: "hugo"
|
||||
slug: hugo
|
||||
url: /commands/hugo/
|
||||
@@ -52,6 +51,7 @@ hugo [flags]
|
||||
--noChmod don't sync permission mode of files
|
||||
--noTimes don't sync modification time of files
|
||||
--path-warnings print warnings on duplicate target paths etc.
|
||||
--poll string set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes
|
||||
--print-mem print memory usage to screen at intervals
|
||||
--quiet build in quiet mode
|
||||
--renderToMemory render to memory (only useful for benchmark testing)
|
||||
@@ -69,6 +69,7 @@ hugo [flags]
|
||||
### SEE ALSO
|
||||
|
||||
* [hugo check](/commands/hugo_check/) - Contains some verification checks
|
||||
* [hugo completion](/commands/hugo_completion/) - generate the autocompletion script for the specified shell
|
||||
* [hugo config](/commands/hugo_config/) - Print the site configuration
|
||||
* [hugo convert](/commands/hugo_convert/) - Convert your content to different formats
|
||||
* [hugo deploy](/commands/hugo_deploy/) - Deploy your site to a Cloud provider.
|
||||
@@ -81,4 +82,3 @@ hugo [flags]
|
||||
* [hugo server](/commands/hugo_server/) - A high performance webserver
|
||||
* [hugo version](/commands/hugo_version/) - Print the version number of Hugo
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Sep-2020
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
date: 2020-09-13
|
||||
title: "hugo check"
|
||||
slug: hugo_check
|
||||
url: /commands/hugo_check/
|
||||
@@ -8,10 +7,6 @@ url: /commands/hugo_check/
|
||||
|
||||
Contains some verification checks
|
||||
|
||||
### Synopsis
|
||||
|
||||
Contains some verification checks
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
@@ -41,4 +36,3 @@ Contains some verification checks
|
||||
* [hugo](/commands/hugo/) - hugo builds your site
|
||||
* [hugo check ulimit](/commands/hugo_check_ulimit/) - Check system ulimit settings
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Sep-2020
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
date: 2020-09-13
|
||||
title: "hugo check ulimit"
|
||||
slug: hugo_check_ulimit
|
||||
url: /commands/hugo_check_ulimit/
|
||||
@@ -45,4 +44,3 @@ hugo check ulimit [flags]
|
||||
|
||||
* [hugo check](/commands/hugo_check/) - Contains some verification checks
|
||||
|
||||
###### Auto generated by spf13/cobra on 13-Sep-2020
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "hugo completion"
|
||||
slug: hugo_completion
|
||||
url: /commands/hugo_completion/
|
||||
---
|
||||
## hugo completion
|
||||
|
||||
generate the autocompletion script for the specified shell
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Generate the autocompletion script for hugo for the specified shell.
|
||||
See each sub-command's help for details on how to use the generated script.
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for completion
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--config string config file (default is path/config.yaml|json|toml)
|
||||
--configDir string config dir (default "config")
|
||||
--debug debug output
|
||||
-e, --environment string build environment
|
||||
--ignoreVendor ignores any _vendor directory
|
||||
--ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern
|
||||
--log enable Logging
|
||||
--logFile string log File path (if set, logging enabled automatically)
|
||||
--quiet build in quiet mode
|
||||
-s, --source string filesystem path to read files relative from
|
||||
--themesDir string filesystem path to themes directory
|
||||
-v, --verbose verbose output
|
||||
--verboseLog verbose logging
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hugo](/commands/hugo/) - hugo builds your site
|
||||
* [hugo completion bash](/commands/hugo_completion_bash/) - generate the autocompletion script for bash
|
||||
* [hugo completion fish](/commands/hugo_completion_fish/) - generate the autocompletion script for fish
|
||||
* [hugo completion powershell](/commands/hugo_completion_powershell/) - generate the autocompletion script for powershell
|
||||
* [hugo completion zsh](/commands/hugo_completion_zsh/) - generate the autocompletion script for zsh
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "hugo completion bash"
|
||||
slug: hugo_completion_bash
|
||||
url: /commands/hugo_completion_bash/
|
||||
---
|
||||
## hugo completion bash
|
||||
|
||||
generate the autocompletion script for bash
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Generate the autocompletion script for the bash shell.
|
||||
|
||||
This script depends on the 'bash-completion' package.
|
||||
If it is not installed already, you can install it via your OS's package manager.
|
||||
|
||||
To load completions in your current shell session:
|
||||
$ source <(hugo completion bash)
|
||||
|
||||
To load completions for every new session, execute once:
|
||||
Linux:
|
||||
$ hugo completion bash > /etc/bash_completion.d/hugo
|
||||
MacOS:
|
||||
$ hugo completion bash > /usr/local/etc/bash_completion.d/hugo
|
||||
|
||||
You will need to start a new shell for this setup to take effect.
|
||||
|
||||
|
||||
```
|
||||
hugo completion bash
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for bash
|
||||
--no-descriptions disable completion descriptions
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--config string config file (default is path/config.yaml|json|toml)
|
||||
--configDir string config dir (default "config")
|
||||
--debug debug output
|
||||
-e, --environment string build environment
|
||||
--ignoreVendor ignores any _vendor directory
|
||||
--ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern
|
||||
--log enable Logging
|
||||
--logFile string log File path (if set, logging enabled automatically)
|
||||
--quiet build in quiet mode
|
||||
-s, --source string filesystem path to read files relative from
|
||||
--themesDir string filesystem path to themes directory
|
||||
-v, --verbose verbose output
|
||||
--verboseLog verbose logging
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hugo completion](/commands/hugo_completion/) - generate the autocompletion script for the specified shell
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "hugo completion fish"
|
||||
slug: hugo_completion_fish
|
||||
url: /commands/hugo_completion_fish/
|
||||
---
|
||||
## hugo completion fish
|
||||
|
||||
generate the autocompletion script for fish
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Generate the autocompletion script for the fish shell.
|
||||
|
||||
To load completions in your current shell session:
|
||||
$ hugo completion fish | source
|
||||
|
||||
To load completions for every new session, execute once:
|
||||
$ hugo completion fish > ~/.config/fish/completions/hugo.fish
|
||||
|
||||
You will need to start a new shell for this setup to take effect.
|
||||
|
||||
|
||||
```
|
||||
hugo completion fish [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for fish
|
||||
--no-descriptions disable completion descriptions
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--config string config file (default is path/config.yaml|json|toml)
|
||||
--configDir string config dir (default "config")
|
||||
--debug debug output
|
||||
-e, --environment string build environment
|
||||
--ignoreVendor ignores any _vendor directory
|
||||
--ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern
|
||||
--log enable Logging
|
||||
--logFile string log File path (if set, logging enabled automatically)
|
||||
--quiet build in quiet mode
|
||||
-s, --source string filesystem path to read files relative from
|
||||
--themesDir string filesystem path to themes directory
|
||||
-v, --verbose verbose output
|
||||
--verboseLog verbose logging
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hugo completion](/commands/hugo_completion/) - generate the autocompletion script for the specified shell
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: "hugo completion powershell"
|
||||
slug: hugo_completion_powershell
|
||||
url: /commands/hugo_completion_powershell/
|
||||
---
|
||||
## hugo completion powershell
|
||||
|
||||
generate the autocompletion script for powershell
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Generate the autocompletion script for powershell.
|
||||
|
||||
To load completions in your current shell session:
|
||||
PS C:\> hugo completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
To load completions for every new session, add the output of the above command
|
||||
to your powershell profile.
|
||||
|
||||
|
||||
```
|
||||
hugo completion powershell [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for powershell
|
||||
--no-descriptions disable completion descriptions
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--config string config file (default is path/config.yaml|json|toml)
|
||||
--configDir string config dir (default "config")
|
||||
--debug debug output
|
||||
-e, --environment string build environment
|
||||
--ignoreVendor ignores any _vendor directory
|
||||
--ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern
|
||||
--log enable Logging
|
||||
--logFile string log File path (if set, logging enabled automatically)
|
||||
--quiet build in quiet mode
|
||||
-s, --source string filesystem path to read files relative from
|
||||
--themesDir string filesystem path to themes directory
|
||||
-v, --verbose verbose output
|
||||
--verboseLog verbose logging
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hugo completion](/commands/hugo_completion/) - generate the autocompletion script for the specified shell
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "hugo completion zsh"
|
||||
slug: hugo_completion_zsh
|
||||
url: /commands/hugo_completion_zsh/
|
||||
---
|
||||
## hugo completion zsh
|
||||
|
||||
generate the autocompletion script for zsh
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Generate the autocompletion script for the zsh shell.
|
||||
|
||||
If shell completion is not already enabled in your environment you will need
|
||||
to enable it. You can execute the following once:
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
To load completions for every new session, execute once:
|
||||
# Linux:
|
||||
$ hugo completion zsh > "${fpath[1]}/_hugo"
|
||||
# macOS:
|
||||
$ hugo completion zsh > /usr/local/share/zsh/site-functions/_hugo
|
||||
|
||||
You will need to start a new shell for this setup to take effect.
|
||||
|
||||
|
||||
```
|
||||
hugo completion zsh [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for zsh
|
||||
--no-descriptions disable completion descriptions
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
--config string config file (default is path/config.yaml|json|toml)
|
||||
--configDir string config dir (default "config")
|
||||
--debug debug output
|
||||
-e, --environment string build environment
|
||||
--ignoreVendor ignores any _vendor directory
|
||||
--ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern
|
||||
--log enable Logging
|
||||
--logFile string log File path (if set, logging enabled automatically)
|
||||
--quiet build in quiet mode
|
||||
-s, --source string filesystem path to read files relative from
|
||||
--themesDir string filesystem path to themes directory
|
||||
-v, --verbose verbose output
|
||||
--verboseLog verbose logging
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [hugo completion](/commands/hugo_completion/) - generate the autocompletion script for the specified shell
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user