mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
458 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c8232e3bd | |||
| bfc3122f8e | |||
| 00ae8e8c72 | |||
| e423e56273 | |||
| 09eb822822 | |||
| a6e8439176 | |||
| 38f68cd162 | |||
| a67650b6f7 | |||
| 2a060b37a3 | |||
| 97df6be59f | |||
| 9323376dfa | |||
| bf0b140364 | |||
| e9b8bec433 | |||
| 888cc1e61e | |||
| 17765a7451 | |||
| 92de8625c7 | |||
| 7907935a42 | |||
| 02d5ec14f3 | |||
| 26640525a3 | |||
| 488b21d15b | |||
| 4500b0e423 | |||
| 060cce0a91 | |||
| 5608ba1f75 | |||
| 7bf1abfc55 | |||
| 2fedca6c8a | |||
| 07873b74bd | |||
| 8a0ea12d8a | |||
| 6f07e5976d | |||
| 2da4ec5738 | |||
| 6624979e1b | |||
| 983b8d537c | |||
| 6738a3e79d | |||
| 2f7df4b926 | |||
| f0a26cf58e | |||
| 74ce5dc841 | |||
| 54a8f0ce21 | |||
| 38e05bd3c7 | |||
| ebfca61ac4 | |||
| e1917740af | |||
| 27414d43a0 | |||
| c837f36ab4 | |||
| a2f67152b3 | |||
| db083b05f1 | |||
| 758a876f90 | |||
| 19937a20ad | |||
| c1ea22a232 | |||
| 0750a9ec91 | |||
| 90bc1f802a | |||
| 629f84e8ed | |||
| 76ef3f42fa | |||
| 0ccb6cdc04 | |||
| 1f1c62e6c7 | |||
| f1d755965f | |||
| 558f74f009 | |||
| ba03114aa9 | |||
| 3935faa417 | |||
| d4d49e0f0e | |||
| 78178d0c2a | |||
| 68d92ef9dd | |||
| b40f3c7df6 | |||
| 57206e7274 | |||
| b1f8676347 | |||
| 07b2e535be | |||
| f038a51b3e | |||
| b4bff6190c | |||
| d2cebee273 | |||
| be914ff34d | |||
| 48a0fea87a | |||
| 9ca1de09dd | |||
| e62675002e | |||
| 9668759ad8 | |||
| 9e9b1f110c | |||
| dc6a292133 | |||
| 4f92f949ea | |||
| d24ffdde5b | |||
| ca31b95f30 | |||
| 5fed9c591b | |||
| 134e7d1d3d | |||
| 1f48b717c7 | |||
| ada3fceea8 | |||
| 05e23bd55c | |||
| 7afac3f1ad | |||
| a4b17470a8 | |||
| 4d5e173cf8 | |||
| 4271b6be0f | |||
| 0567a3e6f1 | |||
| 632ad74fc5 | |||
| b1de03f398 | |||
| 312735366b | |||
| 2b2f2b75ef | |||
| 0d6e593ffb | |||
| 7023cf0f07 | |||
| 9dfa9e70e6 | |||
| 3f217fd664 | |||
| 92684f9a26 | |||
| fce8d82b79 | |||
| 4a502f7eb4 | |||
| be1dbba0f7 | |||
| 1007bcdf49 | |||
| 1904ba40e8 | |||
| 6a8b25e291 | |||
| a322282e70 | |||
| 6bc0d745a5 | |||
| 218690328c | |||
| 673c634131 | |||
| 21a41003c4 | |||
| 1736ef7459 | |||
| d4be1643a0 | |||
| f27e578718 | |||
| d310595a2b | |||
| 049dd1d7e0 | |||
| 189b72331e | |||
| b2b7bfdd3a | |||
| 80e69344da | |||
| 7508a16470 | |||
| a75a659f6f | |||
| 03b88c6ddb | |||
| 7ee0f70507 | |||
| 564bae06f6 | |||
| bf14d0cb26 | |||
| f521336c81 | |||
| c4fe45ff4c | |||
| 554aa58db6 | |||
| 36bf3cb98c | |||
| ea0ba9751d | |||
| 929b91fe75 | |||
| db3bb623d4 | |||
| f5fd7ec8cc | |||
| 16406d9d77 | |||
| e75784930d | |||
| b8ab5c9be6 | |||
| cf7023cb90 | |||
| fd0185a84a | |||
| 6da1d8f370 | |||
| 3f8434a62f | |||
| c9f7ebf004 | |||
| eceeb19751 | |||
| 621194a319 | |||
| 48eec2a4e6 | |||
| a118cb4138 | |||
| e8cc785a58 | |||
| 2658a71e1b | |||
| 8406a3e9dc | |||
| f54ba6f9ef | |||
| 4019b177e6 | |||
| 5dbc29dc6c | |||
| 5ada27bf65 | |||
| 60b176cb57 | |||
| afe5b6d7df | |||
| 168d375784 | |||
| 4a53fd56b7 | |||
| fc6aabe938 | |||
| 9cc65757a1 | |||
| 4835f9e898 | |||
| 5bdda0bdbf | |||
| 9679443c15 | |||
| 2d1681d915 | |||
| 68f67c9aeb | |||
| 43ea2cd660 | |||
| f1491c900e | |||
| 639073e4fe | |||
| 21d9057dbf | |||
| 0672b5c766 | |||
| e309f82efe | |||
| 6f59d96bba | |||
| caba6ba6e7 | |||
| f4575e5f29 | |||
| 0851c175ad | |||
| 676e6875da | |||
| 068ccde4c7 | |||
| 7cb447ab8e | |||
| 6efb279bfa | |||
| b8d5090452 | |||
| 301bafabe0 | |||
| 9571246bcc | |||
| 0257eb50a4 | |||
| a65622a13e | |||
| 146aedd7aa | |||
| bd0200da6e | |||
| a80c3021ba | |||
| 9df7b295bc | |||
| c37bf19c89 | |||
| 9c6d377872 | |||
| 58d7f83390 | |||
| 609d798e34 | |||
| 53f204310e | |||
| 7f82461407 | |||
| b72f909725 | |||
| 3a665ddbf9 | |||
| 46575baa02 | |||
| 058f230a1b | |||
| a66480f70c | |||
| e33a632551 | |||
| 2873324898 | |||
| d0788b96ae | |||
| 034fbef50d | |||
| 54ad51e8a6 | |||
| bd1bcc0f91 | |||
| 8d42a7942a | |||
| 4174a7866b | |||
| 5dd06b4136 | |||
| f5ec75db36 | |||
| 6cb3bda3d1 | |||
| 963cecc12c | |||
| d8f0e30715 | |||
| b6def61727 | |||
| 1891d5e6b5 | |||
| 156f08de35 | |||
| b332f243fd | |||
| d8c2734178 | |||
| d8e1e82188 | |||
| a1c64989df | |||
| 6c3b6ba3e6 | |||
| 4d98b0ed6a | |||
| 15b9976b7a | |||
| 34d63c8d1c | |||
| 51615440bf | |||
| bd66d30295 | |||
| 7caa5b3e50 | |||
| 309d61b220 | |||
| 5b7cb258ec | |||
| 80595bbe3e | |||
| afee781f03 | |||
| 4e84f57efb | |||
| f31a6db797 | |||
| ec22bb31a8 | |||
| a795acbcd8 | |||
| 982d9513e7 | |||
| 6dedb4efc7 | |||
| 292626e679 | |||
| 60d954c785 | |||
| 63e0a92894 | |||
| ce7daa6156 | |||
| 2a0329423c | |||
| 50dc327d1a | |||
| 7285e74090 | |||
| 5fd1e74903 | |||
| 9b0050e9aa | |||
| fc7de7136a | |||
| 1083bf7c08 | |||
| b9a03bd59d | |||
| e0021f4961 | |||
| d25902c0d8 | |||
| 50042ee1f4 | |||
| 3758456b31 | |||
| 7125ad401a | |||
| d0d2c6795e | |||
| 2dd608378d | |||
| 45f52be7f8 | |||
| 87bf2b9688 | |||
| f281ef8a4a | |||
| 46f618756f | |||
| 8915343075 | |||
| a541e3b4d4 | |||
| 912c6576bb | |||
| 911bc60a7a | |||
| 5ff6323320 | |||
| 6d5b44305e | |||
| 1ccd3147a0 | |||
| e40b9fbbcf | |||
| 9cd8fbb332 | |||
| abcc61002a | |||
| 648d00c7d8 | |||
| 8adba648cc | |||
| 6f13430d4a | |||
| 00b46fed8e | |||
| eb9f1eb657 | |||
| 5186e762ac | |||
| e321c3502a | |||
| 255e0a9718 | |||
| 558f3258a7 | |||
| 6580cd30ad | |||
| 7617de86cd | |||
| d19ed4d4e6 | |||
| 35dec7c96f | |||
| 9f978d387f | |||
| 14d85ec136 | |||
| 171836cdfa | |||
| 9ea7103db7 | |||
| 3fc42da3d0 | |||
| d24da1712f | |||
| 4583b41305 | |||
| 507f4e356b | |||
| a7e721e02d | |||
| 2627b91d3a | |||
| 6d4b012412 | |||
| e536d461a5 | |||
| bfc325f561 | |||
| 36a60f65d0 | |||
| de2fcc5e1d | |||
| 26a8ec207f | |||
| 9ca889ba49 | |||
| e2a624dd6c | |||
| 4fb40ee873 | |||
| bc93a36133 | |||
| 3e5bc6f3b3 | |||
| 7c47036f18 | |||
| 4d07e1fe8a | |||
| 1c41232e61 | |||
| f11ca0fad4 | |||
| 30a18e8825 | |||
| d7a2f3f980 | |||
| b4c5df42ff | |||
| ef12d169c8 | |||
| a62bbfa9ea | |||
| 8d32ca223d | |||
| 5887230b7d | |||
| a4a66b8216 | |||
| 813390b5a2 | |||
| d528bbd6d0 | |||
| af7f6c8b3e | |||
| e70849ea79 | |||
| dd6cd6268d | |||
| 27620daa20 | |||
| 80d2fdbaa0 | |||
| 805cc1773b | |||
| 0bde6931ac | |||
| ac7cffa7e2 | |||
| f11bca5fec | |||
| 9315a2d2c4 | |||
| cee3a56a91 | |||
| da2a8e6402 | |||
| a4892a07b4 | |||
| cb98e9061b | |||
| 5fa97ee964 | |||
| 4d38f47250 | |||
| 80f793c38d | |||
| a9079d7a63 | |||
| 4914b7f18c | |||
| 9e06fd3394 | |||
| 9c2b2414d2 | |||
| ab21433689 | |||
| 23fcfb7f74 | |||
| 47bf2fcbe2 | |||
| 16fb2cae88 | |||
| 2bedcf3d18 | |||
| 935999e2fa | |||
| 20c3bae2c4 | |||
| 29b6e133a8 | |||
| 7f8ab74681 | |||
| 59bcc098cf | |||
| e26ba752d2 | |||
| bcf07fa639 | |||
| b6a7568131 | |||
| acf01bfb78 | |||
| 27b22cd87e | |||
| a2488b1c95 | |||
| 8f60c0c1ec | |||
| 9dc608084b | |||
| a349aafb7f | |||
| b8fbd4a578 | |||
| e2b2092ce6 | |||
| f4df7b88b3 | |||
| 3d9bd404ee | |||
| 5f5e55aa8d | |||
| aaaf1c8df5 | |||
| 0baa4f9837 | |||
| d3d4ab41a9 | |||
| 28d844642a | |||
| eb5fd3127f | |||
| 3ed28e4bfe | |||
| db14238ba3 | |||
| 3f64b5a3de | |||
| 66904097e0 | |||
| e54139c85b | |||
| 3710a5ec7e | |||
| 705e3cd5f5 | |||
| 8c61fd2541 | |||
| d3145e4e5d | |||
| 743a1da3ef | |||
| 123901b741 | |||
| 71fd79a3f4 | |||
| c4a530f104 | |||
| 272484f8bf | |||
| de4e466036 | |||
| c23a0c4a0f | |||
| a95670b981 | |||
| 46bdc03885 | |||
| 5160c7efa5 | |||
| e2dd4cd05f | |||
| e509cac533 | |||
| fd38171810 | |||
| 2eca1b3cc1 | |||
| 625162674d | |||
| a692278bc6 | |||
| d4016dd5cd | |||
| 4910312ee7 | |||
| 1b5f78b6b7 | |||
| d5d0f420d8 | |||
| 5993afa4c5 | |||
| d1b4458536 | |||
| 274852bcf2 | |||
| 37a2d5eb4e | |||
| 4c95389c25 | |||
| 3af8bded26 | |||
| 46da0b7aaa | |||
| 75f56b4ce6 | |||
| d234a963eb | |||
| b84644c008 | |||
| a9d19dbdd6 | |||
| 12d713176f | |||
| 6b65b2fae6 | |||
| 1768684d8b | |||
| 9aec42c545 | |||
| eb0ed33dfd | |||
| 6a246d1152 | |||
| ef0e7149d6 | |||
| c32094ace1 | |||
| a262fd4ddb | |||
| f0d32455d2 | |||
| e8bc8e6d02 | |||
| f9b3c0f486 | |||
| 11fcda971c | |||
| f31375d4cc | |||
| 6415b599be | |||
| 1e9b87f760 | |||
| 275c0acbf6 | |||
| f9163155d3 | |||
| 79a17d9e55 | |||
| 75c0f88289 | |||
| 18ce854626 | |||
| 69f5bad40f | |||
| 525bed9919 | |||
| 2ae4786ca1 | |||
| da7983ac4b | |||
| df5d76fed9 | |||
| 7362ba2204 | |||
| 0eb480aa1d | |||
| 7e9092eeeb | |||
| 0200eaf58f | |||
| 28b640a220 | |||
| db45dbbee8 | |||
| 77b976dd92 | |||
| e847a98db6 | |||
| 45c9bbc6ca | |||
| 94fbab2a88 | |||
| c3f273b2d7 | |||
| d7dcc76d27 | |||
| 9bf76fd7e8 | |||
| 15d3e48cec | |||
| b2a02c3c5a | |||
| 6821d6f159 | |||
| 93c7ad12ec | |||
| d2ae9e1363 | |||
| 3a8aad6b19 | |||
| a7b93e6564 | |||
| 65871d5cf4 | |||
| 2e4bf89ec7 | |||
| ebaa733d45 | |||
| 24b1be45c1 | |||
| 111f02db2a | |||
| 8a08f91d5b | |||
| dcf425c846 | |||
| 9a8c84d600 | |||
| a19d03b0e6 | |||
| cdf0b3b7a6 | |||
| d979831dbd | |||
| bcf7421ea5 |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22000.20100
|
||||
- image: bepsays/ci-hugoreleaser:1.22200.20200
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22000.20100
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20200
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -1,73 +1,86 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
name: TestDartSassV1
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
DART_SASS_VERSION: 1.62.1
|
||||
DART_SASS_SHA_LINUX: 3574da75a7322a539034648b8ff84ff2cca162eb924d72b663d718cd3936f075
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
DART_SASS_VERSION: 1.62.1
|
||||
DART_SASS_SHA_LINUX: 3574da75a7322a539034648b8ff84ff2cca162eb924d72b663d718cd3936f075
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.20.x]
|
||||
go-version: [1.22.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html.py --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
run: |
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@5f19ec79cedfadb78ab837f95b87734d0003c899
|
||||
with:
|
||||
ruby-version: "2.7"
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@03fcc74cd74880b697950c4930c9ec8a67c69ecc
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y pandoc
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName pandoc
|
||||
- run: pandoc -v
|
||||
- name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- name: Check
|
||||
run: |
|
||||
dart-sass-embedded --version
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName pandoc
|
||||
- run: pandoc -v
|
||||
- name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${DART_SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH
|
||||
- name: Check
|
||||
run: |
|
||||
dart-sass-embedded --version
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
|
||||
+116
-97
@@ -1,113 +1,132 @@
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
name: Test
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
SASS_VERSION: 1.63.2
|
||||
DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
|
||||
DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
|
||||
DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
|
||||
GOPROXY: https://proxy.golang.org
|
||||
GO111MODULE: on
|
||||
SASS_VERSION: 1.63.2
|
||||
DART_SASS_SHA_LINUX: 3ea33c95ad5c35fda6e9a0956199eef38a398f496cfb8750e02479d7d1dd42af
|
||||
DART_SASS_SHA_MACOS: 11c70f259836b250b44a9cb57fed70e030f21f45069b467d371685855f1eb4f0
|
||||
DART_SASS_SHA_WINDOWS: cd8cd36a619dd8e27f93d3186c52d70eb7d69472aa6c85f5094b29693e773f64
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x,1.20.x]
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4
|
||||
with:
|
||||
ruby-version: '2.7'
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html.py --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
run: |
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
cache: true
|
||||
cache-dependency-path: |
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@5f19ec79cedfadb78ab837f95b87734d0003c899
|
||||
with:
|
||||
ruby-version: "2.7"
|
||||
bundler-cache: true #
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install Mage
|
||||
run: go install github.com/magefile/mage@v1.15.0
|
||||
- name: Install asciidoctor
|
||||
uses: reitzig/actions-asciidoctor@03fcc74cd74880b697950c4930c9ec8a67c69ecc
|
||||
- name: Install docutils
|
||||
run: |
|
||||
pip install docutils
|
||||
rst2html --version
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install pandoc on Linux
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y pandoc
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName pandoc
|
||||
- run: pandoc -v
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
Choco-Install -PackageName mingw -ArgumentList "--version","12.2.0","--allow-downgrade"
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass-embedded Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} dart-sass-${SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GOBIN"
|
||||
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Install dart-sass-embedded MacOS
|
||||
run: |
|
||||
echo "Install Dart Sass version ${SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_MACOS} dart-sass-${SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf "dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'windows-latest'
|
||||
name: Install dart-sass-embedded Windows
|
||||
run: |
|
||||
echo "Install Dart Sass version ${env:SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${env:SASS_VERSION}/dart-sass-${env:SASS_VERSION}-windows-x64.zip";
|
||||
Expand-Archive -Path "dart-sass-${env:SASS_VERSION}-windows-x64.zip" -DestinationPath .;
|
||||
echo "$env:GITHUB_WORKSPACE/dart-sass/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- if: matrix.os != 'windows-latest'
|
||||
name: Check
|
||||
run: |
|
||||
sass --version;
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- if: matrix.os == 'windows-latest'
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended,nodeploy
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
- if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install pandoc
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
choco install pandoc
|
||||
- run: pandoc -v
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
choco install mingw
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install dart-sass Linux
|
||||
run: |
|
||||
echo "Install Dart Sass version ${SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_LINUX} dart-sass-${SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c;
|
||||
tar -xvf "dart-sass-${SASS_VERSION}-linux-x64.tar.gz";
|
||||
echo "$GOBIN"
|
||||
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'macos-latest'
|
||||
name: Install dart-sass MacOS
|
||||
run: |
|
||||
echo "Install Dart Sass version ${SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "${DART_SASS_SHA_MACOS} dart-sass-${SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c;
|
||||
tar -xvf "dart-sass-${SASS_VERSION}-macos-x64.tar.gz";
|
||||
echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH
|
||||
- if: matrix.os == 'windows-latest'
|
||||
name: Install dart-sass Windows
|
||||
run: |
|
||||
echo "Install Dart Sass version ${env:SASS_VERSION} ..."
|
||||
curl -LJO "https://github.com/sass/dart-sass/releases/download/${env:SASS_VERSION}/dart-sass-${env:SASS_VERSION}-windows-x64.zip";
|
||||
Expand-Archive -Path "dart-sass-${env:SASS_VERSION}-windows-x64.zip" -DestinationPath .;
|
||||
echo "$env:GITHUB_WORKSPACE/dart-sass/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Install staticcheck
|
||||
run: go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Run staticcheck
|
||||
run: staticcheck ./...
|
||||
- if: matrix.os != 'windows-latest'
|
||||
name: Check
|
||||
run: |
|
||||
sass --version;
|
||||
mage -v check;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- if: matrix.os == 'windows-latest'
|
||||
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
|
||||
name: Test
|
||||
run: |
|
||||
mage -v test;
|
||||
env:
|
||||
HUGO_BUILD_TAGS: extended
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended,nodeploy
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
|
||||
@@ -122,8 +122,6 @@ cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
>Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`.
|
||||
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
|
||||
```bash
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
FROM golang:1.19-alpine AS build
|
||||
FROM golang:1.21-alpine AS build
|
||||
|
||||
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
|
||||
# docker build --build-arg HUGO_BUILD_TAGS=extended .
|
||||
@@ -26,7 +26,7 @@ RUN mage hugo && mage install
|
||||
|
||||
# ---
|
||||
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.18
|
||||
|
||||
COPY --from=build /go/bin/hugo /usr/bin/hugo
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ A fast and flexible static site generator built with love by [bep], [spf13], and
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | [Twitter]
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | <a rel="me" href="https://fosstodon.org/@gohugoio">Mastodon</a>
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -84,8 +84,8 @@ Hugo is available in two editions: standard and extended. With the extended edit
|
||||
|
||||
Prerequisites to build Hugo from source:
|
||||
|
||||
- Standard edition: Go 1.19 or later
|
||||
- Extended edition: Go 1.19 or later, and GCC
|
||||
- Standard edition: Go 1.20 or later
|
||||
- Extended edition: Go 1.20 or later, and GCC
|
||||
|
||||
Build the standard edition:
|
||||
|
||||
@@ -142,40 +142,40 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
|
||||
|
||||
```text
|
||||
cloud.google.com/go/compute/metadata="v0.2.3"
|
||||
cloud.google.com/go/iam="v1.1.0"
|
||||
cloud.google.com/go/storage="v1.30.1"
|
||||
cloud.google.com/go="v0.110.2"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.6.1"
|
||||
cloud.google.com/go/iam="v1.1.3"
|
||||
cloud.google.com/go/storage="v1.31.0"
|
||||
cloud.google.com/go="v0.110.8"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.7.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.0.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.1.0"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.0"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.0.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma/v2="v2.7.0"
|
||||
github.com/alecthomas/chroma/v2="v2.11.1"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.4.10"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.18.27"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.13.26"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.13.4"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.11.70"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.1.34"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.4.28"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.3.35"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.0.26"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.9.11"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.1.29"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.9.28"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.14.3"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.35.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.12.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.14.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.19.2"
|
||||
github.com/aws/aws-sdk-go-v2="v1.18.1"
|
||||
github.com/aws/aws-sdk-go="v1.44.284"
|
||||
github.com/aws/smithy-go="v1.13.5"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.4.11"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.18.32"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.13.31"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.13.7"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.11.76"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.1.37"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.4.31"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.3.38"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.1.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.9.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.1.32"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.9.31"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.15.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.38.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.13.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.15.1"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.21.1"
|
||||
github.com/aws/aws-sdk-go-v2="v1.20.0"
|
||||
github.com/aws/aws-sdk-go="v1.48.2"
|
||||
github.com/aws/smithy-go="v1.14.0"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
@@ -183,45 +183,45 @@ github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass/v2="v2.0.0"
|
||||
github.com/bep/godartsass="v1.2.0"
|
||||
github.com/bep/golibsass="v1.1.1"
|
||||
github.com/bep/gowebp="v0.2.0"
|
||||
github.com/bep/gowebp="v0.3.0"
|
||||
github.com/bep/lazycache="v0.2.0"
|
||||
github.com/bep/logg="v0.2.0"
|
||||
github.com/bep/logg="v0.3.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.6.0"
|
||||
github.com/bep/simplecobra="v0.3.2"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/clbanning/mxj/v2="v2.5.7"
|
||||
github.com/clbanning/mxj/v2="v2.7.0"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.2"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.10.0"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/evanw/esbuild="v0.18.5"
|
||||
github.com/fatih/color="v1.15.0"
|
||||
github.com/frankban/quicktest="v1.14.5"
|
||||
github.com/fsnotify/fsnotify="v1.6.0"
|
||||
github.com/getkin/kin-openapi="v0.118.0"
|
||||
github.com/evanw/esbuild="v0.19.7"
|
||||
github.com/fatih/color="v1.16.0"
|
||||
github.com/frankban/quicktest="v1.14.6"
|
||||
github.com/fsnotify/fsnotify="v1.7.0"
|
||||
github.com/getkin/kin-openapi="v0.120.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.6"
|
||||
github.com/go-openapi/swag="v0.22.3"
|
||||
github.com/go-openapi/swag="v0.22.4"
|
||||
github.com/gobuffalo/flect="v1.0.2"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/golang-jwt/jwt/v4="v4.5.0"
|
||||
github.com/golang/groupcache="v0.0.0-20210331224755-41bb18bfe9da"
|
||||
github.com/golang/protobuf="v1.5.3"
|
||||
github.com/google/go-cmp="v0.5.9"
|
||||
github.com/google/s2a-go="v0.1.4"
|
||||
github.com/google/uuid="v1.3.0"
|
||||
github.com/google/go-cmp="v0.6.0"
|
||||
github.com/google/s2a-go="v0.1.7"
|
||||
github.com/google/uuid="v1.4.0"
|
||||
github.com/google/wire="v0.5.0"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.2.5"
|
||||
github.com/googleapis/gax-go/v2="v2.11.0"
|
||||
github.com/gorilla/websocket="v1.5.0"
|
||||
github.com/hairyhenderson/go-codeowners="v0.3.0"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.3.2"
|
||||
github.com/googleapis/gax-go/v2="v2.12.0"
|
||||
github.com/gorilla/websocket="v1.5.1"
|
||||
github.com/hairyhenderson/go-codeowners="v0.4.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.1"
|
||||
github.com/invopop/yaml="v0.1.0"
|
||||
github.com/invopop/yaml="v0.2.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
@@ -230,9 +230,9 @@ github.com/kr/text="v0.2.0"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.12"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/marekm4/color-extractor="v1.2.0"
|
||||
github.com/marekm4/color-extractor="v1.2.1"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.19"
|
||||
github.com/mattn/go-isatty="v0.0.20"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.5.0"
|
||||
@@ -240,51 +240,53 @@ github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.7.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml/v2="v2.0.8"
|
||||
github.com/perimeterx/marshmallow="v1.1.4"
|
||||
github.com/pelletier/go-toml/v2="v2.1.0"
|
||||
github.com/perimeterx/marshmallow="v1.1.5"
|
||||
github.com/pkg/browser="v0.0.0-20210911075715-681adbf594b8"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.10.1-0.20230508101108-a4f6fabd84c5"
|
||||
github.com/rogpeppe/go-internal="v1.11.0"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.5"
|
||||
github.com/sass/dart-sass/compiler="1.63.6"
|
||||
github.com/sass/dart-sass/implementation="1.63.6"
|
||||
github.com/sass/dart-sass/protocol="2.1.0"
|
||||
github.com/sass/dart-sass/compiler="1.63.2"
|
||||
github.com/sass/dart-sass/implementation="1.63.2"
|
||||
github.com/sass/dart-sass/protocol="2.0.0"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/spf13/afero="v1.9.5"
|
||||
github.com/spf13/afero="v1.10.0"
|
||||
github.com/spf13/cast="v1.5.1"
|
||||
github.com/spf13/cobra="v1.7.0"
|
||||
github.com/spf13/fsync="v0.9.0"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/tdewolff/minify/v2="v2.12.7"
|
||||
github.com/tdewolff/parse/v2="v2.6.6"
|
||||
github.com/webmproject/libwebp="v1.2.4"
|
||||
github.com/yuin/goldmark="v1.5.4"
|
||||
github.com/tdewolff/minify/v2="v2.20.7"
|
||||
github.com/tdewolff/parse/v2="v2.7.5"
|
||||
github.com/webmproject/libwebp="v1.3.2"
|
||||
github.com/yuin/goldmark-emoji="v1.0.2"
|
||||
github.com/yuin/goldmark="v1.6.0"
|
||||
go.opencensus.io="v0.24.0"
|
||||
go.uber.org/atomic="v1.11.0"
|
||||
go.uber.org/automaxprocs="v1.5.2"
|
||||
gocloud.dev="v0.30.0"
|
||||
golang.org/x/crypto="v0.10.0"
|
||||
golang.org/x/exp="v0.0.0-20230321023759-10a507213a29"
|
||||
golang.org/x/image="v0.8.0"
|
||||
golang.org/x/mod="v0.10.0"
|
||||
golang.org/x/net="v0.11.0"
|
||||
golang.org/x/oauth2="v0.9.0"
|
||||
golang.org/x/sync="v0.3.0"
|
||||
golang.org/x/sys="v0.9.0"
|
||||
golang.org/x/text="v0.10.0"
|
||||
golang.org/x/tools="v0.9.3"
|
||||
go.uber.org/automaxprocs="v1.5.3"
|
||||
gocloud.dev="v0.34.0"
|
||||
golang.org/x/crypto="v0.15.0"
|
||||
golang.org/x/exp="v0.0.0-20221031165847-c99f073a8326"
|
||||
golang.org/x/image="v0.13.0"
|
||||
golang.org/x/mod="v0.14.0"
|
||||
golang.org/x/net="v0.18.0"
|
||||
golang.org/x/oauth2="v0.13.0"
|
||||
golang.org/x/sync="v0.5.0"
|
||||
golang.org/x/sys="v0.14.0"
|
||||
golang.org/x/text="v0.14.0"
|
||||
golang.org/x/time="v0.3.0"
|
||||
golang.org/x/tools="v0.15.0"
|
||||
golang.org/x/xerrors="v0.0.0-20220907171357-04be3eba64a2"
|
||||
google.golang.org/api="v0.128.0"
|
||||
google.golang.org/appengine="v1.6.7"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/genproto="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/grpc="v1.56.0"
|
||||
google.golang.org/protobuf="v1.30.0"
|
||||
google.golang.org/api="v0.151.0"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20231016165738-49dd2c1f3d0b"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20231030173426-d783a09b4405"
|
||||
google.golang.org/genproto="v0.0.0-20231016165738-49dd2c1f3d0b"
|
||||
google.golang.org/grpc="v1.59.0"
|
||||
google.golang.org/protobuf="v1.31.0"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
howett.net/plist="v1.0.0"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
Vendored
+584
@@ -0,0 +1,584 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bep/lazycache"
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/common/collections"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/rungroup"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
const minMaxSize = 10
|
||||
|
||||
// New creates a new cache.
|
||||
func New(opts Options) *Cache {
|
||||
if opts.CheckInterval == 0 {
|
||||
opts.CheckInterval = time.Second * 2
|
||||
}
|
||||
|
||||
if opts.MaxSize == 0 {
|
||||
opts.MaxSize = 100000
|
||||
}
|
||||
if opts.Log == nil {
|
||||
panic("nil Log")
|
||||
}
|
||||
|
||||
if opts.MinMaxSize == 0 {
|
||||
opts.MinMaxSize = 30
|
||||
}
|
||||
|
||||
stats := &stats{
|
||||
opts: opts,
|
||||
adjustmentFactor: 1.0,
|
||||
currentMaxSize: opts.MaxSize,
|
||||
availableMemory: config.GetMemoryLimit(),
|
||||
}
|
||||
|
||||
infol := opts.Log.InfoCommand("dynacache")
|
||||
|
||||
evictedIdentities := collections.NewStack[identity.Identity]()
|
||||
|
||||
onEvict := func(k, v any) {
|
||||
if !opts.Running {
|
||||
return
|
||||
}
|
||||
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
|
||||
evictedIdentities.Push(id)
|
||||
return false
|
||||
})
|
||||
resource.MarkStale(v)
|
||||
}
|
||||
|
||||
c := &Cache{
|
||||
partitions: make(map[string]PartitionManager),
|
||||
onEvict: onEvict,
|
||||
evictedIdentities: evictedIdentities,
|
||||
opts: opts,
|
||||
stats: stats,
|
||||
infol: infol,
|
||||
}
|
||||
|
||||
c.stop = c.start()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Options for the cache.
|
||||
type Options struct {
|
||||
Log loggers.Logger
|
||||
CheckInterval time.Duration
|
||||
MaxSize int
|
||||
MinMaxSize int
|
||||
Running bool
|
||||
}
|
||||
|
||||
// Options for a partition.
|
||||
type OptionsPartition struct {
|
||||
// When to clear the this partition.
|
||||
ClearWhen ClearWhen
|
||||
|
||||
// Weight is a number between 1 and 100 that indicates how, in general, how big this partition may get.
|
||||
Weight int
|
||||
}
|
||||
|
||||
func (o OptionsPartition) WeightFraction() float64 {
|
||||
return float64(o.Weight) / 100
|
||||
}
|
||||
|
||||
func (o OptionsPartition) CalculateMaxSize(maxSizePerPartition int) int {
|
||||
return int(math.Floor(float64(maxSizePerPartition) * o.WeightFraction()))
|
||||
}
|
||||
|
||||
// A dynamic partitioned cache.
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
partitions map[string]PartitionManager
|
||||
|
||||
onEvict func(k, v any)
|
||||
evictedIdentities *collections.Stack[identity.Identity]
|
||||
|
||||
opts Options
|
||||
infol logg.LevelLogger
|
||||
|
||||
stats *stats
|
||||
stopOnce sync.Once
|
||||
stop func()
|
||||
}
|
||||
|
||||
// DrainEvictedIdentities drains the evicted identities from the cache.
|
||||
func (c *Cache) DrainEvictedIdentities() []identity.Identity {
|
||||
return c.evictedIdentities.Drain()
|
||||
}
|
||||
|
||||
// ClearMatching clears all partition for which the predicate returns true.
|
||||
func (c *Cache) ClearMatching(predicate func(k, v any) bool) {
|
||||
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearMatching(predicate)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
for _, p := range c.partitions {
|
||||
g.Enqueue(p)
|
||||
}
|
||||
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
|
||||
func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
|
||||
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearOnRebuild(changeset...)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
for _, p := range c.partitions {
|
||||
g.Enqueue(p)
|
||||
}
|
||||
|
||||
g.Wait()
|
||||
|
||||
// Clear any entries marked as stale above.
|
||||
g = rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
|
||||
NumWorkers: len(c.partitions),
|
||||
Handle: func(ctx context.Context, partition PartitionManager) error {
|
||||
partition.clearStale()
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
for _, p := range c.partitions {
|
||||
g.Enqueue(p)
|
||||
}
|
||||
|
||||
g.Wait()
|
||||
}
|
||||
|
||||
type keysProvider interface {
|
||||
Keys() []string
|
||||
}
|
||||
|
||||
// Keys returns a list of keys in all partitions.
|
||||
func (c *Cache) Keys(predicate func(s string) bool) []string {
|
||||
if predicate == nil {
|
||||
predicate = func(s string) bool { return true }
|
||||
}
|
||||
var keys []string
|
||||
for pn, g := range c.partitions {
|
||||
pkeys := g.(keysProvider).Keys()
|
||||
for _, k := range pkeys {
|
||||
p := path.Join(pn, k)
|
||||
if predicate(p) {
|
||||
keys = append(keys, p)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func calculateMaxSizePerPartition(maxItemsTotal, totalWeightQuantity, numPartitions int) int {
|
||||
if numPartitions == 0 {
|
||||
panic("numPartitions must be > 0")
|
||||
}
|
||||
if totalWeightQuantity == 0 {
|
||||
panic("totalWeightQuantity must be > 0")
|
||||
}
|
||||
|
||||
avgWeight := float64(totalWeightQuantity) / float64(numPartitions)
|
||||
return int(math.Floor(float64(maxItemsTotal) / float64(numPartitions) * (100.0 / avgWeight)))
|
||||
}
|
||||
|
||||
// Stop stops the cache.
|
||||
func (c *Cache) Stop() {
|
||||
c.stopOnce.Do(func() {
|
||||
c.stop()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) adjustCurrentMaxSize() {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if len(c.partitions) == 0 {
|
||||
return
|
||||
}
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
s := c.stats
|
||||
s.memstatsCurrent = m
|
||||
// fmt.Printf("\n\nAvailable = %v\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\nMaxSize = %d\nAdjustmentFactor=%f\n\n", helpers.FormatByteCount(s.availableMemory), helpers.FormatByteCount(m.Alloc), helpers.FormatByteCount(m.TotalAlloc), helpers.FormatByteCount(m.Sys), m.NumGC, c.stats.currentMaxSize, s.adjustmentFactor)
|
||||
|
||||
if s.availableMemory >= s.memstatsCurrent.Alloc {
|
||||
if s.adjustmentFactor <= 1.0 {
|
||||
s.adjustmentFactor += 0.2
|
||||
}
|
||||
} else {
|
||||
// We're low on memory.
|
||||
s.adjustmentFactor -= 0.4
|
||||
}
|
||||
|
||||
if s.adjustmentFactor <= 0 {
|
||||
s.adjustmentFactor = 0.05
|
||||
}
|
||||
|
||||
if !s.adjustCurrentMaxSize() {
|
||||
return
|
||||
}
|
||||
|
||||
totalWeight := 0
|
||||
for _, pm := range c.partitions {
|
||||
totalWeight += pm.getOptions().Weight
|
||||
}
|
||||
|
||||
maxSizePerPartition := calculateMaxSizePerPartition(c.stats.currentMaxSize, totalWeight, len(c.partitions))
|
||||
|
||||
evicted := 0
|
||||
for _, p := range c.partitions {
|
||||
evicted += p.adjustMaxSize(p.getOptions().CalculateMaxSize(maxSizePerPartition))
|
||||
}
|
||||
|
||||
if evicted > 0 {
|
||||
c.infol.
|
||||
WithFields(
|
||||
logg.Fields{
|
||||
{Name: "evicted", Value: evicted},
|
||||
{Name: "numGC", Value: m.NumGC},
|
||||
{Name: "limit", Value: helpers.FormatByteCount(c.stats.availableMemory)},
|
||||
{Name: "alloc", Value: helpers.FormatByteCount(m.Alloc)},
|
||||
{Name: "totalAlloc", Value: helpers.FormatByteCount(m.TotalAlloc)},
|
||||
},
|
||||
).Logf("adjusted partitions' max size")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) start() func() {
|
||||
ticker := time.NewTicker(c.opts.CheckInterval)
|
||||
quit := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.adjustCurrentMaxSize()
|
||||
// Reset the ticker to avoid drift.
|
||||
ticker.Reset(c.opts.CheckInterval)
|
||||
case <-quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
close(quit)
|
||||
}
|
||||
}
|
||||
|
||||
var partitionNameRe = regexp.MustCompile(`^\/[a-zA-Z0-9]{4}(\/[a-zA-Z0-9]+)?(\/[a-zA-Z0-9]+)?`)
|
||||
|
||||
// GetOrCreatePartition gets or creates a partition with the given name.
|
||||
func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts OptionsPartition) *Partition[K, V] {
|
||||
if c == nil {
|
||||
panic("nil Cache")
|
||||
}
|
||||
if opts.Weight < 1 || opts.Weight > 100 {
|
||||
panic("invalid Weight, must be between 1 and 100")
|
||||
}
|
||||
|
||||
if partitionNameRe.FindString(name) != name {
|
||||
panic(fmt.Sprintf("invalid partition name %q", name))
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
p, found := c.partitions[name]
|
||||
c.mu.RUnlock()
|
||||
if found {
|
||||
return p.(*Partition[K, V])
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Double check.
|
||||
p, found = c.partitions[name]
|
||||
if found {
|
||||
return p.(*Partition[K, V])
|
||||
}
|
||||
|
||||
// At this point, we don't know the number of partitions or their configuration, but
|
||||
// this will be re-adjusted later.
|
||||
const numberOfPartitionsEstimate = 10
|
||||
maxSize := opts.CalculateMaxSize(c.opts.MaxSize / numberOfPartitionsEstimate)
|
||||
|
||||
onEvict := func(k K, v V) {
|
||||
c.onEvict(k, v)
|
||||
}
|
||||
|
||||
// Create a new partition and cache it.
|
||||
partition := &Partition[K, V]{
|
||||
c: lazycache.New(lazycache.Options[K, V]{MaxEntries: maxSize, OnEvict: onEvict}),
|
||||
maxSize: maxSize,
|
||||
trace: c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name),
|
||||
opts: opts,
|
||||
}
|
||||
c.partitions[name] = partition
|
||||
|
||||
return partition
|
||||
}
|
||||
|
||||
// Partition is a partition in the cache.
|
||||
type Partition[K comparable, V any] struct {
|
||||
c *lazycache.Cache[K, V]
|
||||
|
||||
zero V
|
||||
|
||||
trace logg.LevelLogger
|
||||
opts OptionsPartition
|
||||
|
||||
maxSize int
|
||||
}
|
||||
|
||||
// GetOrCreate gets or creates a value for the given key.
|
||||
func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) {
|
||||
v, _, err := p.c.GetOrCreate(key, create)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function
|
||||
// takes too long.
|
||||
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
|
||||
resultch := make(chan V, 1)
|
||||
errch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
v, _, err := p.c.GetOrCreate(key, create)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
resultch <- v
|
||||
}()
|
||||
|
||||
select {
|
||||
case v := <-resultch:
|
||||
return v, nil
|
||||
case err := <-errch:
|
||||
return p.zero, err
|
||||
case <-time.After(duration):
|
||||
return p.zero, &herrors.TimeoutError{
|
||||
Duration: duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
if predicate(key, v) {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("clearing cache key %v", key)
|
||||
},
|
||||
),
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
|
||||
opts := p.getOptions()
|
||||
if opts.ClearWhen == ClearNever {
|
||||
return
|
||||
}
|
||||
|
||||
if opts.ClearWhen == ClearOnRebuild {
|
||||
// Clear all.
|
||||
p.Clear()
|
||||
return
|
||||
}
|
||||
|
||||
depsFinder := identity.NewFinder(identity.FinderConfig{})
|
||||
|
||||
shouldDelete := func(key K, v V) bool {
|
||||
// We always clear elements marked as stale.
|
||||
if resource.IsStaleAny(v) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Now check if this entry has changed based on the changeset
|
||||
// based on filesystem events.
|
||||
if len(changeset) == 0 {
|
||||
// Nothing changed.
|
||||
return false
|
||||
}
|
||||
|
||||
var probablyDependent bool
|
||||
identity.WalkIdentitiesShallow(v, func(level int, id2 identity.Identity) bool {
|
||||
for _, id := range changeset {
|
||||
if r := depsFinder.Contains(id, id2, -1); r > 0 {
|
||||
// It's probably dependent, evict from cache.
|
||||
probablyDependent = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return probablyDependent
|
||||
}
|
||||
|
||||
// First pass.
|
||||
// Second pass needs to be done in a separate loop to catch any
|
||||
// elements marked as stale in the other partitions.
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
if shouldDelete(key, v) {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("first pass: clearing cache key %v", key)
|
||||
},
|
||||
),
|
||||
)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) Keys() []K {
|
||||
var keys []K
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
keys = append(keys, key)
|
||||
return false
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) clearStale() {
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
isStale := resource.IsStaleAny(v)
|
||||
if isStale {
|
||||
p.trace.Log(
|
||||
logg.StringFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("second pass: clearing cache key %v", key)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return isStale
|
||||
})
|
||||
}
|
||||
|
||||
// adjustMaxSize adjusts the max size of the and returns the number of items evicted.
|
||||
func (p *Partition[K, V]) adjustMaxSize(newMaxSize int) int {
|
||||
if newMaxSize < minMaxSize {
|
||||
newMaxSize = minMaxSize
|
||||
}
|
||||
oldMaxSize := p.maxSize
|
||||
if newMaxSize == oldMaxSize {
|
||||
return 0
|
||||
}
|
||||
p.maxSize = newMaxSize
|
||||
// fmt.Println("Adjusting max size of partition from", oldMaxSize, "to", newMaxSize)
|
||||
return p.c.Resize(newMaxSize)
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) getMaxSize() int {
|
||||
return p.maxSize
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) getOptions() OptionsPartition {
|
||||
return p.opts
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) Clear() {
|
||||
p.c.DeleteFunc(func(key K, v V) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Partition[K, V]) Get(ctx context.Context, key K) (V, bool) {
|
||||
return p.c.Get(key)
|
||||
}
|
||||
|
||||
type PartitionManager interface {
|
||||
adjustMaxSize(addend int) int
|
||||
getMaxSize() int
|
||||
getOptions() OptionsPartition
|
||||
clearOnRebuild(changeset ...identity.Identity)
|
||||
clearMatching(predicate func(k, v any) bool)
|
||||
clearStale()
|
||||
}
|
||||
|
||||
const (
|
||||
ClearOnRebuild ClearWhen = iota + 1
|
||||
ClearOnChange
|
||||
ClearNever
|
||||
)
|
||||
|
||||
type ClearWhen int
|
||||
|
||||
type stats struct {
|
||||
opts Options
|
||||
memstatsCurrent runtime.MemStats
|
||||
currentMaxSize int
|
||||
availableMemory uint64
|
||||
|
||||
adjustmentFactor float64
|
||||
}
|
||||
|
||||
func (s *stats) adjustCurrentMaxSize() bool {
|
||||
newCurrentMaxSize := int(math.Floor(float64(s.opts.MaxSize) * s.adjustmentFactor))
|
||||
|
||||
if newCurrentMaxSize < s.opts.MinMaxSize {
|
||||
newCurrentMaxSize = int(s.opts.MinMaxSize)
|
||||
}
|
||||
changed := newCurrentMaxSize != s.currentMaxSize
|
||||
s.currentMaxSize = newCurrentMaxSize
|
||||
return changed
|
||||
}
|
||||
|
||||
// CleanKey turns s into a format suitable for a cache key for this package.
|
||||
// The key will be a Unix-styled path with a leading slash but no trailing slash.
|
||||
func CleanKey(s string) string {
|
||||
return path.Clean(paths.ToSlashPreserveLeading(s))
|
||||
}
|
||||
Vendored
+175
@@ -0,0 +1,175 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.StaleInfo = (*testItem)(nil)
|
||||
_ identity.Identity = (*testItem)(nil)
|
||||
)
|
||||
|
||||
type testItem struct {
|
||||
name string
|
||||
isStale bool
|
||||
}
|
||||
|
||||
func (t testItem) IsStale() bool {
|
||||
return t.isStale
|
||||
}
|
||||
|
||||
func (t testItem) IdentifierBase() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
cache := New(Options{
|
||||
Log: loggers.NewDefault(),
|
||||
})
|
||||
|
||||
c.Cleanup(func() {
|
||||
cache.Stop()
|
||||
})
|
||||
|
||||
opts := OptionsPartition{Weight: 30}
|
||||
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
|
||||
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts)
|
||||
c.Assert(p1, qt.Not(qt.IsNil))
|
||||
|
||||
p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts)
|
||||
|
||||
c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "foo bar", opts) }, qt.PanicMatches, ".*invalid partition name.*")
|
||||
c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 1234}) }, qt.PanicMatches, ".*invalid Weight.*")
|
||||
|
||||
c.Assert(p2, qt.Equals, p1)
|
||||
|
||||
p3 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", opts)
|
||||
c.Assert(p3, qt.Not(qt.IsNil))
|
||||
c.Assert(p3, qt.Not(qt.Equals), p1)
|
||||
|
||||
c.Assert(func() { New(Options{}) }, qt.PanicMatches, ".*nil Log.*")
|
||||
}
|
||||
|
||||
func TestCalculateMaxSizePerPartition(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(calculateMaxSizePerPartition(1000, 500, 5), qt.Equals, 200)
|
||||
c.Assert(calculateMaxSizePerPartition(1000, 250, 5), qt.Equals, 400)
|
||||
c.Assert(func() { calculateMaxSizePerPartition(1000, 250, 0) }, qt.PanicMatches, ".*must be > 0.*")
|
||||
c.Assert(func() { calculateMaxSizePerPartition(1000, 0, 1) }, qt.PanicMatches, ".*must be > 0.*")
|
||||
}
|
||||
|
||||
func TestCleanKey(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(CleanKey("a/b/c"), qt.Equals, "/a/b/c")
|
||||
c.Assert(CleanKey("/a/b/c"), qt.Equals, "/a/b/c")
|
||||
c.Assert(CleanKey("a/b/c/"), qt.Equals, "/a/b/c")
|
||||
c.Assert(CleanKey(filepath.FromSlash("/a/b/c/")), qt.Equals, "/a/b/c")
|
||||
}
|
||||
|
||||
func newTestCache(t *testing.T) *Cache {
|
||||
cache := New(
|
||||
Options{
|
||||
Log: loggers.NewDefault(),
|
||||
},
|
||||
)
|
||||
|
||||
p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild})
|
||||
p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 30, ClearWhen: ClearOnChange})
|
||||
|
||||
p1.GetOrCreate("clearOnRebuild", func(string) (testItem, error) {
|
||||
return testItem{}, nil
|
||||
})
|
||||
|
||||
p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) {
|
||||
return testItem{
|
||||
isStale: true,
|
||||
}, nil
|
||||
})
|
||||
|
||||
p2.GetOrCreate("clearBecauseIdentityChanged", func(string) (testItem, error) {
|
||||
return testItem{
|
||||
name: "changed",
|
||||
}, nil
|
||||
})
|
||||
|
||||
p2.GetOrCreate("clearNever", func(string) (testItem, error) {
|
||||
return testItem{
|
||||
isStale: false,
|
||||
}, nil
|
||||
})
|
||||
|
||||
t.Cleanup(func() {
|
||||
cache.Stop()
|
||||
})
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
predicateAll := func(string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
cache := newTestCache(t)
|
||||
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
|
||||
|
||||
cache.ClearOnRebuild()
|
||||
|
||||
// Stale items are always cleared.
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
|
||||
|
||||
cache = newTestCache(t)
|
||||
cache.ClearOnRebuild(identity.StringIdentity("changed"))
|
||||
|
||||
c.Assert(cache.Keys(nil), qt.HasLen, 1)
|
||||
|
||||
cache = newTestCache(t)
|
||||
|
||||
cache.ClearMatching(func(k, v any) bool {
|
||||
return k.(string) == "clearOnRebuild"
|
||||
})
|
||||
|
||||
c.Assert(cache.Keys(predicateAll), qt.HasLen, 3)
|
||||
|
||||
cache.adjustCurrentMaxSize()
|
||||
}
|
||||
|
||||
func TestAdjustCurrentMaxSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
cache := newTestCache(t)
|
||||
alloc := cache.stats.memstatsCurrent.Alloc
|
||||
cache.adjustCurrentMaxSize()
|
||||
c.Assert(cache.stats.memstatsCurrent.Alloc, qt.Not(qt.Equals), alloc)
|
||||
}
|
||||
Vendored
+37
-6
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
@@ -51,6 +52,9 @@ type Cache struct {
|
||||
pruneAllRootDir string
|
||||
|
||||
nlocker *lockTracker
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
@@ -103,9 +107,23 @@ func (l *lockedFile) Close() error {
|
||||
return l.File.Close()
|
||||
}
|
||||
|
||||
func (c *Cache) init() error {
|
||||
c.initOnce.Do(func() {
|
||||
// Create the base dir if it does not exist.
|
||||
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
|
||||
c.initErr = err
|
||||
}
|
||||
})
|
||||
return c.initErr
|
||||
}
|
||||
|
||||
// WriteCloser returns a transactional writer into the cache.
|
||||
// It's important that it's closed when done.
|
||||
func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
|
||||
@@ -129,7 +147,12 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
|
||||
// it when done.
|
||||
func (c *Cache) ReadOrCreate(id string,
|
||||
read func(info ItemInfo, r io.ReadSeeker) error,
|
||||
create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
|
||||
create func(info ItemInfo, w io.WriteCloser) error,
|
||||
) (info ItemInfo, err error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, err
|
||||
}
|
||||
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -163,6 +186,9 @@ func (c *Cache) ReadOrCreate(id string,
|
||||
// be invoked and the result cached.
|
||||
// This method is protected by a named lock using the given id as identifier.
|
||||
func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -197,6 +223,9 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
|
||||
|
||||
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
|
||||
func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -232,6 +261,9 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
|
||||
|
||||
// GetBytes gets the file content with the given id from the cache, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -250,6 +282,9 @@ func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
|
||||
// Get gets the file with the given id from the cache, nil if none found.
|
||||
func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return ItemInfo{}, nil, err
|
||||
}
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
@@ -347,11 +382,7 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
|
||||
baseDir := v.DirCompiled
|
||||
|
||||
if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bfs := afero.NewBasePathFs(cfs, baseDir)
|
||||
bfs := hugofs.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
var pruneAllRootDir string
|
||||
if k == CacheKeyModules {
|
||||
|
||||
Vendored
+1
-3
@@ -15,6 +15,7 @@
|
||||
package filecache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -24,8 +25,6 @@ import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
@@ -225,7 +224,6 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
|
||||
|
||||
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
|
||||
func resolveDirPlaceholder(fs afero.Fs, bcfg config.BaseConfig, placeholder string) (cacheDir string, isResource bool, err error) {
|
||||
|
||||
switch strings.ToLower(placeholder) {
|
||||
case ":resourcedir":
|
||||
return "", true, nil
|
||||
|
||||
+3
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +15,6 @@ package filecache_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -47,7 +46,6 @@ title: "Home"
|
||||
_, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images"))
|
||||
|
||||
b.Assert(err, qt.IsNil)
|
||||
|
||||
}
|
||||
|
||||
func TestPruneImages(t *testing.T) {
|
||||
@@ -55,6 +53,7 @@ func TestPruneImages(t *testing.T) {
|
||||
// TODO(bep)
|
||||
t.Skip("skip flaky test on CI server")
|
||||
}
|
||||
t.Skip("skip flaky test")
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
@@ -92,7 +91,7 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||
|
||||
// TODO(bep) we need a way to test full rebuilds.
|
||||
// For now, just sleep a little so the cache elements expires.
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
b.RenameFile("assets/a/pixel.png", "assets/b/pixel2.png").Build()
|
||||
|
||||
@@ -104,5 +103,4 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
|
||||
b.Assert(err, qt.Not(qt.IsNil))
|
||||
_, err = b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir)
|
||||
b.Assert(err, qt.IsNil)
|
||||
|
||||
}
|
||||
Vendored
+6
-2
@@ -53,11 +53,13 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
if c.pruneAllRootDir != "" {
|
||||
return c.pruneRootDir(force)
|
||||
}
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
counter := 0
|
||||
|
||||
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
|
||||
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -66,7 +68,6 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
|
||||
if info.IsDir() {
|
||||
f, err := c.Fs.Open(name)
|
||||
|
||||
if err != nil {
|
||||
// This cache dir may not exist.
|
||||
return nil
|
||||
@@ -117,6 +118,9 @@ func (c *Cache) Prune(force bool) (int, error) {
|
||||
}
|
||||
|
||||
func (c *Cache) pruneRootDir(force bool) (int, error) {
|
||||
if err := c.init(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
info, err := c.Fs.Stat(c.pruneAllRootDir)
|
||||
if err != nil {
|
||||
if herrors.IsNotExist(err) {
|
||||
|
||||
Vendored
+1
-11
@@ -1,4 +1,4 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -86,17 +85,8 @@ dir = ":cacheDir/c"
|
||||
cache := caches.Get("GetJSON")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
|
||||
bfs, ok := cache.Fs.(*afero.BasePathFs)
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
filename, err := bfs.RealPath("key")
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cache = caches.Get("Images")
|
||||
c.Assert(cache, qt.Not(qt.IsNil))
|
||||
bfs, ok = cache.Fs.(*afero.BasePathFs)
|
||||
c.Assert(ok, qt.Equals, true)
|
||||
filename, _ = bfs.RealPath("key")
|
||||
c.Assert(filename, qt.Equals, filepath.FromSlash("_gen/images/key"))
|
||||
|
||||
rf := func(s string) func() (io.ReadCloser, error) {
|
||||
return func() (io.ReadCloser, error) {
|
||||
|
||||
Vendored
-78
@@ -1,78 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package namedmemcache provides a memory cache with a named lock. This is suitable
|
||||
// for situations where creating the cached resource can be time consuming or otherwise
|
||||
// resource hungry, or in situations where a "once only per key" is a requirement.
|
||||
package namedmemcache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
)
|
||||
|
||||
// Cache holds the cached values.
|
||||
type Cache struct {
|
||||
nlocker *locker.Locker
|
||||
cache map[string]cacheEntry
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
value any
|
||||
err error
|
||||
}
|
||||
|
||||
// New creates a new cache.
|
||||
func New() *Cache {
|
||||
return &Cache{
|
||||
nlocker: locker.NewLocker(),
|
||||
cache: make(map[string]cacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the cache state.
|
||||
func (c *Cache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.cache = make(map[string]cacheEntry)
|
||||
c.nlocker = locker.NewLocker()
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the value with the given cache key, if not found
|
||||
// create will be called and cached.
|
||||
// This method is thread safe. It also guarantees that the create func for a given
|
||||
// key is invoked only once for this cache.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (any, error)) (any, error) {
|
||||
c.mu.RLock()
|
||||
entry, found := c.cache[key]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if found {
|
||||
return entry.value, entry.err
|
||||
}
|
||||
|
||||
c.nlocker.Lock(key)
|
||||
defer c.nlocker.Unlock(key)
|
||||
|
||||
// Create it.
|
||||
value, err := create()
|
||||
|
||||
c.mu.Lock()
|
||||
c.cache[key] = cacheEntry{value: value, err: err}
|
||||
c.mu.Unlock()
|
||||
|
||||
return value, err
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package namedmemcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNamedCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
cache := New()
|
||||
|
||||
counter := 0
|
||||
create := func() (any, error) {
|
||||
counter++
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
v1, err := cache.GetOrCreate("a1", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v1, qt.Equals, 1)
|
||||
v2, err := cache.GetOrCreate("a2", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v2, qt.Equals, 2)
|
||||
}
|
||||
|
||||
cache.Clear()
|
||||
|
||||
v3, err := cache.GetOrCreate("a2", create)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v3, qt.Equals, 3)
|
||||
}
|
||||
|
||||
func TestNamedCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cache := New()
|
||||
|
||||
create := func(i int) func() (any, error) {
|
||||
return func() (any, error) {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 100; j++ {
|
||||
id := fmt.Sprintf("id%d", j)
|
||||
v, err := cache.GetOrCreate(id, create(j))
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(v, qt.Equals, j)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
diff <(gofmt -d .) <(printf '')
|
||||
@@ -461,7 +461,6 @@ func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
|
||||
pkg,
|
||||
tt.Methods.List)...)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Embedded, but in a different file/package. Return the
|
||||
// package.Name and deal with that later.
|
||||
|
||||
+47
-42
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -38,6 +39,7 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -46,13 +48,12 @@ import (
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/resources/kinds"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
errHelp = errors.New("help requested")
|
||||
)
|
||||
var errHelp = errors.New("help requested")
|
||||
|
||||
// Execute executes a command.
|
||||
func Execute(args []string) error {
|
||||
@@ -182,11 +183,9 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo
|
||||
cfg: oldConf.cfg,
|
||||
fs: fs,
|
||||
}, nil
|
||||
|
||||
})
|
||||
|
||||
return cc, err
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commonConfig, error) {
|
||||
@@ -205,13 +204,10 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
cfg = config.New()
|
||||
}
|
||||
|
||||
if !cfg.IsSet("renderToDisk") {
|
||||
cfg.Set("renderToDisk", true)
|
||||
}
|
||||
if !cfg.IsSet("workingDir") {
|
||||
cfg.Set("workingDir", dir)
|
||||
} else {
|
||||
if err := os.MkdirAll(cfg.GetString("workingDir"), 0777); err != nil {
|
||||
if err := os.MkdirAll(cfg.GetString("workingDir"), 0o777); err != nil {
|
||||
return nil, fmt.Errorf("failed to create workingDir: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -241,9 +237,7 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
|
||||
sourceFs := hugofs.Os
|
||||
var destinationFs afero.Fs
|
||||
if cfg.GetBool("renderToDisk") {
|
||||
destinationFs = hugofs.Os
|
||||
} else {
|
||||
if cfg.GetBool("renderToMemory") {
|
||||
destinationFs = afero.NewMemMapFs()
|
||||
if renderStaticToDisk {
|
||||
// Hybrid, render dynamic content to Root.
|
||||
@@ -253,6 +247,8 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
cfg.Set("publishDirDynamic", "/")
|
||||
cfg.Set("publishDirStatic", "/")
|
||||
}
|
||||
} else {
|
||||
destinationFs = hugofs.Os
|
||||
}
|
||||
|
||||
fs := hugofs.NewFromSourceAndDestination(sourceFs, destinationFs, cfg)
|
||||
@@ -262,7 +258,7 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
publishDirStatic := cfg.GetString("publishDirStatic")
|
||||
workingDir := cfg.GetString("workingDir")
|
||||
absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
|
||||
staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
|
||||
staticFs := hugofs.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic)
|
||||
|
||||
// Serve from both the static and dynamic fs,
|
||||
// the first will take priority.
|
||||
@@ -303,7 +299,6 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
})
|
||||
|
||||
return cc, err
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
|
||||
@@ -346,9 +341,11 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
defer r.timeTrack(time.Now(), "Built")
|
||||
}
|
||||
err := b.build()
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -410,8 +407,15 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
r.commonConfigs = lazycache.New[int32, *commonConfig](lazycache.Options{MaxEntries: 5})
|
||||
r.hugoSites = lazycache.New[int32, *hugolib.HugoSites](lazycache.Options{MaxEntries: 5})
|
||||
r.commonConfigs = lazycache.New(lazycache.Options[int32, *commonConfig]{MaxEntries: 5})
|
||||
// We don't want to keep stale HugoSites in memory longer than needed.
|
||||
r.hugoSites = lazycache.New(lazycache.Options[int32, *hugolib.HugoSites]{
|
||||
MaxEntries: 1,
|
||||
OnEvict: func(key int32, value *hugolib.HugoSites) {
|
||||
value.Close()
|
||||
runtime.GC()
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -434,26 +438,26 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
|
||||
}
|
||||
} else {
|
||||
if r.verbose {
|
||||
helpers.Deprecated("--verbose", "use --logLevel info", false)
|
||||
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
|
||||
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
|
||||
level = logg.LevelInfo
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
helpers.Deprecated("--debug", "use --logLevel debug", false)
|
||||
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
|
||||
level = logg.LevelDebug
|
||||
}
|
||||
}
|
||||
|
||||
optsLogger := loggers.Options{
|
||||
Distinct: true,
|
||||
Level: level,
|
||||
Stdout: r.Out,
|
||||
Stderr: r.Out,
|
||||
StoreErrors: running,
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: level,
|
||||
Stdout: r.Out,
|
||||
Stderr: r.Out,
|
||||
StoreErrors: running,
|
||||
}
|
||||
|
||||
return loggers.New(optsLogger), nil
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) Reset() {
|
||||
@@ -479,47 +483,47 @@ Complete documentation is available at https://gohugo.io/.`
|
||||
|
||||
// Configure persistent flags
|
||||
cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.MarkFlagDirname("source")
|
||||
cmd.PersistentFlags().StringP("destination", "d", "", "filesystem path to write files to")
|
||||
cmd.PersistentFlags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.MarkFlagDirname("destination")
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&r.environment, "environment", "e", "", "build environment")
|
||||
_ = cmd.RegisterFlagCompletionFunc("environment", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
_ = cmd.MarkFlagDirname("themesDir")
|
||||
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
|
||||
_ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
|
||||
_ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions)
|
||||
|
||||
cmd.PersistentFlags().StringVar(&r.cfgFile, "config", "", "config file (default is hugo.yaml|json|toml)")
|
||||
_ = cmd.MarkFlagFilename("config", config.ValidConfigFileExtensions...)
|
||||
cmd.PersistentFlags().StringVar(&r.cfgDir, "configDir", "config", "config dir")
|
||||
_ = cmd.MarkFlagDirname("configDir")
|
||||
cmd.PersistentFlags().BoolVar(&r.quiet, "quiet", false, "build in quiet mode")
|
||||
|
||||
// Set bash-completion
|
||||
_ = cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions)
|
||||
cmd.PersistentFlags().BoolVar(&r.renderToMemory, "renderToMemory", false, "render to memory (mostly useful when running the server)")
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output")
|
||||
cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output")
|
||||
cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
|
||||
cmd.Flags().BoolVar(&r.renderToMemory, "renderToMemory", false, "render to memory (only useful for benchmark testing)")
|
||||
|
||||
// Configure local flags
|
||||
applyLocalFlagsBuild(cmd, r)
|
||||
|
||||
// Set bash-completion.
|
||||
// Each flag must first be defined before using the SetAnnotation() call.
|
||||
_ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A sub set of the complete build flags. These flags are used by new and mod.
|
||||
func applyLocalFlagsBuildConfig(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
|
||||
_ = cmd.MarkFlagDirname("theme")
|
||||
cmd.Flags().StringVarP(&r.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
|
||||
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory")
|
||||
_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.MarkFlagDirname("cacheDir")
|
||||
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
|
||||
cmd.Flags().StringSliceP("renderSegments", "", []string{}, "named segments to render (configured in the segments config)")
|
||||
}
|
||||
|
||||
// Flags needed to do a build (used by hugo and hugo server commands)
|
||||
@@ -532,8 +536,10 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
|
||||
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
|
||||
_ = cmd.MarkFlagDirname("layoutDir")
|
||||
cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
|
||||
_ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions)
|
||||
cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
@@ -556,9 +562,8 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().MarkHidden("profile-mutex")
|
||||
|
||||
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("disableKinds", cobra.FixedCompletions(kinds.AllKinds, cobra.ShellCompDirectiveNoFileComp))
|
||||
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
|
||||
_ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
}
|
||||
|
||||
func (r *rootCommand) timeTrack(start time.Time, name string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -37,5 +37,4 @@ func newExec() (*simplecobra.Exec, error) {
|
||||
}
|
||||
|
||||
return simplecobra.New(rootCmd)
|
||||
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/gohugoio/hugo/modules"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newConfigCommand creates a new config command and its subcommands.
|
||||
@@ -37,7 +38,6 @@ func newConfigCommand() *configCommand {
|
||||
&configMountsCommand{},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type configCommand struct {
|
||||
@@ -113,7 +113,9 @@ func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd.Short = "Print the site configuration"
|
||||
cmd.Long = `Print the site configuration, both default and custom settings.`
|
||||
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
|
||||
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
|
||||
return nil
|
||||
@@ -190,7 +192,6 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
|
||||
Dir: m.m.Dir(),
|
||||
Mounts: mounts,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type configMountsCommand struct {
|
||||
@@ -225,6 +226,7 @@ func (c *configMountsCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
c.r = cd.Root.Command.(*rootCommand)
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "Print the configured file mounts"
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, c.r)
|
||||
return nil
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -46,6 +46,7 @@ to use JSON for the front matter.`,
|
||||
return c.convertContents(metadecoders.JSON)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -57,6 +58,7 @@ to use TOML for the front matter.`,
|
||||
return c.convertContents(metadecoders.TOML)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -68,6 +70,7 @@ to use YAML for the front matter.`,
|
||||
return c.convertContents(metadecoders.YAML)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -108,6 +111,7 @@ func (c *convertCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
See convert's subcommands toJSON, toTOML and toYAML for more information.`
|
||||
|
||||
cmd.PersistentFlags().StringVarP(&c.outputDir, "output", "o", "", "filesystem path to write files to")
|
||||
_ = cmd.MarkFlagDirname("output")
|
||||
cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first")
|
||||
|
||||
cmd.RunE = nil
|
||||
@@ -134,7 +138,7 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
|
||||
}
|
||||
}
|
||||
|
||||
if p.File().IsZero() {
|
||||
if p.File() == nil {
|
||||
// No content file.
|
||||
return nil
|
||||
}
|
||||
@@ -209,7 +213,7 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
|
||||
|
||||
var pagesBackedByFile page.Pages
|
||||
for _, p := range site.AllPages() {
|
||||
if p.File().IsZero() {
|
||||
if p.File() == nil {
|
||||
continue
|
||||
}
|
||||
pagesBackedByFile = append(pagesBackedByFile, p)
|
||||
|
||||
+12
-7
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -32,13 +32,14 @@ package commands
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/gohugoio/hugo/deploy/deployconfig"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDeployCommand() simplecobra.Commander {
|
||||
|
||||
return &simpleCommand{
|
||||
name: "deploy",
|
||||
short: "Deploy your site to a Cloud provider.",
|
||||
@@ -59,13 +60,17 @@ documentation.
|
||||
return deployer.Deploy(ctx)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
|
||||
_ = cmd.RegisterFlagCompletionFunc("target", cobra.NoFileCompletions)
|
||||
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
|
||||
cmd.Flags().Bool("dryRun", false, "dry run")
|
||||
cmd.Flags().Bool("force", false, "force upload of all files")
|
||||
cmd.Flags().Bool("invalidateCDN", deploy.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().Int("maxDeletes", deploy.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable")
|
||||
cmd.Flags().Int("workers", deploy.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10")
|
||||
cmd.Flags().Bool("invalidateCDN", deployconfig.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target")
|
||||
cmd.Flags().Int("maxDeletes", deployconfig.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable")
|
||||
_ = cmd.RegisterFlagCompletionFunc("maxDeletes", cobra.NoFileCompletions)
|
||||
cmd.Flags().Int("workers", deployconfig.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10")
|
||||
_ = cmd.RegisterFlagCompletionFunc("workers", cobra.NoFileCompletions)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +14,7 @@
|
||||
//go:build nodeploy
|
||||
// +build nodeploy
|
||||
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
+8
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newEnvCommand() simplecobra.Commander {
|
||||
@@ -47,6 +48,9 @@ func newEnvCommand() simplecobra.Commander {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,5 +63,8 @@ func newVersionCmd() simplecobra.Commander {
|
||||
},
|
||||
short: "Print Hugo version and environment info",
|
||||
long: "Print Hugo version and environment info. This is useful in Hugo bug reports.",
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+15
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -75,9 +75,13 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
|
||||
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("linesStyle", cobra.NoFileCompletions)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -101,7 +105,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
}
|
||||
if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
|
||||
r.Println("Directory", genmandir, "does not exist, creating...")
|
||||
if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil {
|
||||
if err := hugofs.Os.MkdirAll(genmandir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -115,9 +119,9 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
|
||||
// For bash-completion
|
||||
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.MarkFlagDirname("dir")
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -136,7 +140,7 @@ url: %s
|
||||
long: `Generate Markdown documentation for the Hugo CLI.
|
||||
This command is, mostly, used to create up-to-date documentation
|
||||
of Hugo's command-line interface for https://gohugo.io/.
|
||||
|
||||
|
||||
It creates one Markdown file per command with front matter suitable
|
||||
for rendering in Hugo.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -150,7 +154,7 @@ url: %s
|
||||
}
|
||||
if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
|
||||
r.Println("Directory", gendocdir, "does not exist, creating...")
|
||||
if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil {
|
||||
if err := hugofs.Os.MkdirAll(gendocdir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -172,12 +176,11 @@ url: %s
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
|
||||
// For bash-completion
|
||||
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.MarkFlagDirname("dir")
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var docsHelperTarget string
|
||||
@@ -196,7 +199,7 @@ url: %s
|
||||
configProvider := func() docshelper.DocProvider {
|
||||
conf := hugolib.DefaultConfig()
|
||||
conf.CacheDir = "" // The default value does not make sense in the docs.
|
||||
defaultConfig := parser.LowerCaseCamelJSONMarshaller{Value: conf}
|
||||
defaultConfig := parser.NullBoolJSONMarshaller{Wrapped: parser.LowerCaseCamelJSONMarshaller{Value: conf}}
|
||||
return docshelper.DocProvider{"config": defaultConfig}
|
||||
}
|
||||
|
||||
@@ -228,6 +231,7 @@ url: %s
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir")
|
||||
},
|
||||
}
|
||||
@@ -241,7 +245,6 @@ url: %s
|
||||
newDocsHelper(),
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type genCommand struct {
|
||||
|
||||
+2
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -110,12 +110,11 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
})
|
||||
|
||||
return cfg
|
||||
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
p := filepath.Join(x...)
|
||||
err := os.MkdirAll(p, 0777) // before umask
|
||||
err := os.MkdirAll(p, 0o777) // before umask
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -25,9 +25,9 @@ func init() {
|
||||
// This message to show to Windows users if Hugo is opened from explorer.exe
|
||||
cobra.MousetrapHelpText = `
|
||||
|
||||
Hugo is a command-line tool for generating static website.
|
||||
Hugo is a command-line tool for generating static websites.
|
||||
|
||||
You need to open PowerShell and run Hugo from there.
|
||||
|
||||
You need to open cmd.exe and run Hugo from there.
|
||||
|
||||
Visit https://gohugo.io/ for more information.`
|
||||
}
|
||||
|
||||
+51
-99
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bep/logg"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -83,7 +85,6 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
|
||||
c.confmu.Lock()
|
||||
defer c.confmu.Unlock()
|
||||
fn(c.conf)
|
||||
|
||||
}
|
||||
|
||||
type hugoBuilderErrState struct {
|
||||
@@ -135,46 +136,12 @@ func (c *hugoBuilder) errCount() int {
|
||||
|
||||
// getDirList provides NewWatcher() with a list of directories to watch for changes.
|
||||
func (c *hugoBuilder) getDirList() ([]string, error) {
|
||||
var filenames []string
|
||||
|
||||
walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("walker: ", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
if fi.Name() == ".git" ||
|
||||
fi.Name() == "node_modules" || fi.Name() == "bower_components" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
h, err := c.hugo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
watchFiles := h.PathSpec.BaseFs.WatchDirs()
|
||||
for _, fi := range watchFiles {
|
||||
if !fi.IsDir() {
|
||||
filenames = append(filenames, fi.Meta().Filename)
|
||||
continue
|
||||
}
|
||||
|
||||
w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.r.logger, Info: fi, WalkFn: walkFn})
|
||||
if err := w.Walk(); err != nil {
|
||||
c.r.logger.Errorln("walker: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
filenames = helpers.UniqueStringsSorted(filenames)
|
||||
|
||||
return filenames, nil
|
||||
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) initCPUProfile() (func(), error) {
|
||||
@@ -441,7 +408,7 @@ func (c *hugoBuilder) copyStatic() (map[string]uint64, error) {
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
|
||||
infol := c.r.logger.InfoCommand("copy static")
|
||||
infol := c.r.logger.InfoCommand("static")
|
||||
publishDir := helpers.FilePathSeparator
|
||||
|
||||
if sourceFs.PublishFolder != "" {
|
||||
@@ -467,11 +434,11 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
|
||||
if syncer.Delete {
|
||||
infol.Logf("removing all files from destination that don't exist in static dirs")
|
||||
|
||||
syncer.DeleteFilter = func(f os.FileInfo) bool {
|
||||
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
|
||||
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
|
||||
}
|
||||
}
|
||||
infol.Logf("syncing static files to %s", publishDir)
|
||||
start := time.Now()
|
||||
|
||||
// because we are using a baseFs (to get the union right).
|
||||
// set sync src to root
|
||||
@@ -479,9 +446,10 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
loggers.TimeTrackf(infol, start, nil, "syncing static files to %s", publishDir)
|
||||
|
||||
// Sync runs Stat 3 times for every source file (which sounds much)
|
||||
numFiles := fs.statCounter / 3
|
||||
// Sync runs Stat 2 times for every source file.
|
||||
numFiles := fs.statCounter / 2
|
||||
|
||||
return numFiles, err
|
||||
}
|
||||
@@ -652,13 +620,31 @@ func (c *hugoBuilder) handleBuildErr(err error, msg string) {
|
||||
func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
staticSyncer *staticSyncer,
|
||||
evs []fsnotify.Event,
|
||||
configSet map[string]bool) {
|
||||
configSet map[string]bool,
|
||||
) {
|
||||
defer func() {
|
||||
c.errState.setWasErr(false)
|
||||
}()
|
||||
|
||||
var isHandled bool
|
||||
|
||||
// Filter out ghost events (from deleted, renamed directories).
|
||||
// This seems to be a bug in fsnotify, or possibly MacOS.
|
||||
var n int
|
||||
for _, ev := range evs {
|
||||
keep := true
|
||||
if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {
|
||||
if _, err := os.Stat(ev.Name); err != nil {
|
||||
keep = false
|
||||
}
|
||||
}
|
||||
if keep {
|
||||
evs[n] = ev
|
||||
n++
|
||||
}
|
||||
}
|
||||
evs = evs[:n]
|
||||
|
||||
for _, ev := range evs {
|
||||
isConfig := configSet[ev.Name]
|
||||
configChangeType := configChangeConfig
|
||||
@@ -726,48 +712,25 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
return
|
||||
}
|
||||
|
||||
c.r.logger.Infoln("Received System Events:", evs)
|
||||
c.r.logger.Debugln("Received System Events:", evs)
|
||||
|
||||
staticEvents := []fsnotify.Event{}
|
||||
dynamicEvents := []fsnotify.Event{}
|
||||
|
||||
filtered := []fsnotify.Event{}
|
||||
h, err := c.hugo()
|
||||
if err != nil {
|
||||
c.r.logger.Errorln("Error getting the Hugo object:", err)
|
||||
return
|
||||
}
|
||||
n = 0
|
||||
for _, ev := range evs {
|
||||
if h.ShouldSkipFileChangeEvent(ev) {
|
||||
continue
|
||||
}
|
||||
// Check the most specific first, i.e. files.
|
||||
contentMapped := h.ContentChanges.GetSymbolicLinkMappings(ev.Name)
|
||||
if len(contentMapped) > 0 {
|
||||
for _, mapped := range contentMapped {
|
||||
filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for any symbolic directory mapping.
|
||||
|
||||
dir, name := filepath.Split(ev.Name)
|
||||
|
||||
contentMapped = h.ContentChanges.GetSymbolicLinkMappings(dir)
|
||||
|
||||
if len(contentMapped) == 0 {
|
||||
filtered = append(filtered, ev)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, mapped := range contentMapped {
|
||||
mappedFilename := filepath.Join(mapped, name)
|
||||
filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
|
||||
}
|
||||
evs[n] = ev
|
||||
n++
|
||||
}
|
||||
|
||||
evs = filtered
|
||||
evs = evs[:n]
|
||||
|
||||
for _, ev := range evs {
|
||||
ext := filepath.Ext(ev.Name)
|
||||
@@ -788,6 +751,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
if istemp {
|
||||
continue
|
||||
}
|
||||
|
||||
if h.Deps.SourceSpec.IgnoreFile(ev.Name) {
|
||||
continue
|
||||
}
|
||||
@@ -811,7 +775,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
continue
|
||||
}
|
||||
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
|
||||
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
|
||||
if f.IsDir() {
|
||||
c.r.logger.Println("adding created directory to watchlist", path)
|
||||
if err := watcher.Add(path); err != nil {
|
||||
@@ -827,11 +791,10 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
}
|
||||
|
||||
// recursively add new directories to watch list
|
||||
// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
|
||||
if ev.Op&fsnotify.Create == fsnotify.Create {
|
||||
if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Rename) {
|
||||
c.withConf(func(conf *commonConfig) {
|
||||
if s, err := conf.fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
|
||||
_ = helpers.SymbolicWalk(conf.fs.Source, ev.Name, walkAdder)
|
||||
_ = helpers.Walk(conf.fs.Source, ev.Name, walkAdder)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -872,7 +835,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
return
|
||||
}
|
||||
path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
|
||||
path = h.RelURL(helpers.ToSlashTrimLeading(path), false)
|
||||
path = h.RelURL(paths.ToSlashTrimLeading(path), false)
|
||||
|
||||
livereload.RefreshPath(path)
|
||||
} else {
|
||||
@@ -909,7 +872,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
|
||||
// Nothing has changed.
|
||||
return
|
||||
} else if len(changed) == 1 {
|
||||
pathToRefresh := h.PathSpec.RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
|
||||
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(changed[0]), false)
|
||||
livereload.RefreshPath(pathToRefresh)
|
||||
} else {
|
||||
livereload.ForceRefresh()
|
||||
@@ -944,7 +907,6 @@ func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) {
|
||||
var err error
|
||||
h, err = c.r.HugFromConfig(conf)
|
||||
return err
|
||||
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -968,7 +930,7 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
|
||||
|
||||
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
|
||||
cfg := config.New()
|
||||
cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk))
|
||||
cfg.Set("renderToMemory", c.r.renderToMemory)
|
||||
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
|
||||
if c.r.environment == "" {
|
||||
// We need to set the environment as early as possible because we need it to load the correct config.
|
||||
@@ -989,9 +951,10 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
cfg.Set("environment", c.r.environment)
|
||||
|
||||
cfg.Set("internal", maps.Params{
|
||||
"running": running,
|
||||
"watch": watch,
|
||||
"verbose": c.r.isVerbose(),
|
||||
"running": running,
|
||||
"watch": watch,
|
||||
"verbose": c.r.isVerbose(),
|
||||
"fastRenderMode": c.fastRenderMode,
|
||||
})
|
||||
|
||||
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
@@ -1000,6 +963,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
}
|
||||
|
||||
if len(conf.configs.LoadingInfo.ConfigFiles) == 0 {
|
||||
//lint:ignore ST1005 end user message.
|
||||
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.")
|
||||
}
|
||||
|
||||
@@ -1011,15 +975,16 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
var rebuildCounter atomic.Uint64
|
||||
|
||||
func (c *hugoBuilder) printChangeDetected(typ string) {
|
||||
msg := "\nChange"
|
||||
if typ != "" {
|
||||
msg += " of " + typ
|
||||
}
|
||||
msg += " detected, rebuilding site."
|
||||
msg += fmt.Sprintf(" detected, rebuilding site (#%d).", rebuildCounter.Add(1))
|
||||
|
||||
c.r.logger.Println(msg)
|
||||
const layout = "2006-01-02 15:04:05.000 -0700"
|
||||
@@ -1034,25 +999,12 @@ func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
|
||||
}
|
||||
}
|
||||
c.errState.setBuildErr(nil)
|
||||
visited := c.visitedURLs.PeekAllSet()
|
||||
h, err := c.hugo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.fastRenderMode {
|
||||
c.withConf(func(conf *commonConfig) {
|
||||
// Make sure we always render the home pages
|
||||
for _, l := range conf.configs.ConfigLangs() {
|
||||
langPath := l.LanguagePrefix()
|
||||
if langPath != "" {
|
||||
langPath = langPath + "/"
|
||||
}
|
||||
home := h.PrependBasePath("/"+langPath, false)
|
||||
visited[home] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
|
||||
return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...)
|
||||
}
|
||||
|
||||
func (c *hugoBuilder) reloadConfig() error {
|
||||
|
||||
+5
-11
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -59,6 +58,7 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
return c.importFromJekyll(args)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory")
|
||||
},
|
||||
},
|
||||
@@ -66,7 +66,6 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
}
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
type importCommand struct {
|
||||
@@ -312,7 +311,7 @@ func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft
|
||||
|
||||
targetFile := filepath.Join(targetDir, relPath)
|
||||
targetParentDir := filepath.Dir(targetFile)
|
||||
os.MkdirAll(targetParentDir, 0777)
|
||||
os.MkdirAll(targetParentDir, 0o777)
|
||||
|
||||
contentBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -398,7 +397,6 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl
|
||||
}
|
||||
|
||||
func (c *importCommand) importFromJekyll(args []string) error {
|
||||
|
||||
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
return newUserError("path error:", args[0])
|
||||
@@ -429,11 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
c.r.Println("Importing...")
|
||||
|
||||
fileCount := 0
|
||||
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
callback := func(path string, fi hugofs.FileMetaInfo) error {
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -462,7 +456,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
|
||||
|
||||
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
|
||||
if hasAnyPostInDir {
|
||||
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
|
||||
if err = helpers.Walk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -23,15 +23,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newListCommand creates a new list command and its subcommands.
|
||||
func newListCommand() *listCommand {
|
||||
|
||||
createRecord := func(workingDir string, p page.Page) []string {
|
||||
return []string{
|
||||
filepath.ToSlash(strings.TrimPrefix(p.File().Filename(), workingDir+string(os.PathSeparator))),
|
||||
@@ -47,7 +46,7 @@ func newListCommand() *listCommand {
|
||||
|
||||
list := func(cd *simplecobra.Commandeer, r *rootCommand, shouldInclude func(page.Page) bool, opts ...any) error {
|
||||
bcfg := hugolib.BuildCfg{SkipRender: true}
|
||||
cfg := config.New()
|
||||
cfg := flagsToCfg(cd, nil)
|
||||
for i := 0; i < len(opts); i += 2 {
|
||||
cfg.Set(opts[i].(string), opts[i+1])
|
||||
}
|
||||
@@ -83,7 +82,6 @@ func newListCommand() *listCommand {
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
return &listCommand{
|
||||
@@ -94,11 +92,10 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the drafts in your content directory.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !p.Draft() || p.File().IsZero() {
|
||||
if !p.Draft() || p.File() == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildDrafts", true,
|
||||
@@ -106,6 +103,9 @@ func newListCommand() *listCommand {
|
||||
"buildExpired", true,
|
||||
)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "future",
|
||||
@@ -113,17 +113,19 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the posts in your content directory which will be posted in the future.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !resource.IsFuture(p) || p.File().IsZero() {
|
||||
if !resource.IsFuture(p) || p.File() == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
return list(cd, r, shouldInclude,
|
||||
"buildFuture", true,
|
||||
"buildDrafts", true,
|
||||
)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "expired",
|
||||
@@ -131,7 +133,7 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the posts in your content directory which has already expired.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
if !resource.IsExpired(p) || p.File().IsZero() {
|
||||
if !resource.IsExpired(p) || p.File() == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -141,6 +143,9 @@ func newListCommand() *listCommand {
|
||||
"buildDrafts", true,
|
||||
)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "all",
|
||||
@@ -148,14 +153,16 @@ func newListCommand() *listCommand {
|
||||
long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
shouldInclude := func(p page.Page) bool {
|
||||
return !p.File().IsZero()
|
||||
return p.File() != nil
|
||||
}
|
||||
return list(cd, r, shouldInclude, "buildDrafts", true, "buildFuture", true, "buildExpired", true)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type listCommand struct {
|
||||
|
||||
+18
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -62,6 +62,7 @@ removed from Hugo, but we need to test this out in "real life" to get a feel of
|
||||
so this may/will change in future versions of Hugo.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -69,7 +70,7 @@ so this may/will change in future versions of Hugo.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
|
||||
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -89,6 +90,7 @@ so this may/will change in future versions of Hugo.
|
||||
inside a subfolder on GitHub, as one example.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -108,6 +110,7 @@ so this may/will change in future versions of Hugo.
|
||||
short: "Verify dependencies.",
|
||||
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
},
|
||||
@@ -127,6 +130,7 @@ so this may/will change in future versions of Hugo.
|
||||
Note that for vendored modules, that is the version listed and not the one from go.mod.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
|
||||
},
|
||||
@@ -144,8 +148,10 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
short: "Delete the Hugo Module cache for the current project.",
|
||||
long: `Delete the Hugo Module cache for the current project.`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
|
||||
_ = cmd.RegisterFlagCompletionFunc("pattern", cobra.NoFileCompletions)
|
||||
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -167,6 +173,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
name: "tidy",
|
||||
short: "Remove unused entries in go.mod and go.sum.",
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -184,6 +191,7 @@ Note that for vendored modules, that is the version listed and not the one from
|
||||
If a module is vendored, that is where Hugo will look for it's dependencies.
|
||||
`,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
@@ -211,7 +219,12 @@ Install a specific version:
|
||||
|
||||
hugo mod get github.com/gohugoio/testshortcodes@v0.3.0
|
||||
|
||||
Install the latest versions of all module dependencies:
|
||||
Install the latest versions of all direct module dependencies:
|
||||
|
||||
hugo mod get
|
||||
hugo mod get ./... (recursive)
|
||||
|
||||
Install the latest versions of all module dependencies (direct and indirect):
|
||||
|
||||
hugo mod get -u
|
||||
hugo mod get -u ./... (recursive)
|
||||
@@ -220,6 +233,7 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
` + commonUsageMod,
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.DisableFlagParsing = true
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
// We currently just pass on the flags we get to Go and
|
||||
@@ -281,7 +295,6 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
npmCommand,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type modCommands struct {
|
||||
@@ -303,7 +316,7 @@ func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//config := conf.configs.Base
|
||||
// config := conf.configs.Base
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+25
-9
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/create/skeletons"
|
||||
@@ -59,12 +60,17 @@ Ensure you run this within the root directory of your site.`,
|
||||
return create.NewContent(h, contentType, args[0], force)
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return []string{}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
|
||||
}
|
||||
cmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
|
||||
cmd.Flags().String("editor", "", "edit new content with this editor, if provided")
|
||||
_ = cmd.RegisterFlagCompletionFunc("editor", cobra.NoFileCompletions)
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite file if it already exists")
|
||||
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
applyLocalFlagsBuildConfig(cmd, r)
|
||||
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -104,8 +110,15 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return []string{}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
|
||||
}
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "init inside non-empty directory")
|
||||
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
@@ -120,14 +133,15 @@ according to your needs.`,
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
cfg := config.New()
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps := h.PathSpec
|
||||
sourceFs := ps.Fs.Source
|
||||
themesDir := h.Configs.LoadingInfo.BaseConfig.ThemesDir
|
||||
createpath := ps.AbsPathify(filepath.Join(themesDir, args[0]))
|
||||
sourceFs := conf.fs.Source
|
||||
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
|
||||
r.Println("Creating new theme in", createpath)
|
||||
|
||||
err = skeletons.CreateTheme(createpath, sourceFs)
|
||||
@@ -137,12 +151,14 @@ according to your needs.`,
|
||||
|
||||
return nil
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
type newCommand struct {
|
||||
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
// Note: This is a command only meant for internal use and must be run
|
||||
// via "go run -tags release main.go release" on the actual code base that is in the release.
|
||||
func newReleaseCommand() simplecobra.Commander {
|
||||
|
||||
var (
|
||||
step int
|
||||
skipPush bool
|
||||
@@ -44,9 +43,11 @@ func newReleaseCommand() simplecobra.Commander {
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Hidden = true
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
|
||||
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
|
||||
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+76
-74
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -25,22 +25,22 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/bep/mclib"
|
||||
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bep/mclib"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
@@ -83,10 +83,14 @@ const (
|
||||
)
|
||||
|
||||
func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
|
||||
var visitedURLs *types.EvictingStringQueue
|
||||
if s != nil && !s.disableFastRender {
|
||||
visitedURLs = types.NewEvictingStringQueue(20)
|
||||
}
|
||||
return &hugoBuilder{
|
||||
r: r,
|
||||
s: s,
|
||||
visitedURLs: types.NewEvictingStringQueue(100),
|
||||
visitedURLs: visitedURLs,
|
||||
fullRebuildSem: semaphore.NewWeighted(1),
|
||||
debounce: debounce.New(4 * time.Second),
|
||||
onConfigLoaded: func(reloaded bool) error {
|
||||
@@ -119,8 +123,8 @@ func newServerCommand() *serverCommand {
|
||||
return mclib.RunMain()
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.ValidArgsFunction = cobra.NoFileCompletions
|
||||
cmd.Flags().BoolVar(&uninstall, "uninstall", false, "Uninstall the local CA (but do not delete it).")
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -219,7 +223,7 @@ func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
|
||||
}
|
||||
|
||||
type fileServer struct {
|
||||
baseURLs []string
|
||||
baseURLs []urls.BaseURL
|
||||
roots []string
|
||||
errorTemplate func(err any) (io.Reader, error)
|
||||
c *serverCommand
|
||||
@@ -233,15 +237,16 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
listener := f.c.serverPorts[i].ln
|
||||
logger := f.c.r.logger
|
||||
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
|
||||
|
||||
if i == 0 {
|
||||
if f.c.renderToDisk {
|
||||
r.Println("Serving pages from disk")
|
||||
} else if f.c.renderStaticToDisk {
|
||||
r.Println("Serving pages from memory and static files from disk")
|
||||
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
|
||||
mainTarget := "disk"
|
||||
if f.c.r.renderToMemory {
|
||||
mainTarget = "memory"
|
||||
}
|
||||
if f.c.renderStaticToDisk {
|
||||
r.Printf("Serving pages from %s and static files from disk\n", mainTarget)
|
||||
} else {
|
||||
r.Println("Serving pages from memory")
|
||||
r.Printf("Serving pages from %s\n", mainTarget)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,12 +260,6 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
r.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
|
||||
}
|
||||
|
||||
// We're only interested in the path
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, nil, "", "", fmt.Errorf("invalid baseURL: %w", err)
|
||||
}
|
||||
|
||||
decorate := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if f.c.showErrorInBrowser {
|
||||
@@ -280,7 +279,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
port = lrport
|
||||
}
|
||||
})
|
||||
lr := *u
|
||||
lr := baseURL.URL()
|
||||
lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port)
|
||||
fmt.Fprint(w, injectLiveReloadScript(r, lr))
|
||||
|
||||
@@ -308,10 +307,10 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
if redirect := serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
|
||||
// fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
|
||||
doRedirect := true
|
||||
// This matches Netlify's behaviour and is needed for SPA behaviour.
|
||||
// This matches Netlify's behavior and is needed for SPA behavior.
|
||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||
if !redirect.Force {
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
|
||||
path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path()))
|
||||
if root != "" {
|
||||
path = filepath.Join(root, path)
|
||||
}
|
||||
@@ -338,7 +337,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
switch redirect.Status {
|
||||
case 404:
|
||||
w.WriteHeader(404)
|
||||
file, err := fs.Open(strings.TrimPrefix(redirect.To, u.Path))
|
||||
file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path()))
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
@@ -347,7 +346,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
}
|
||||
return
|
||||
case 200:
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
|
||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil {
|
||||
requestURI = redirect.To
|
||||
r = r2
|
||||
}
|
||||
@@ -385,10 +384,10 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
|
||||
fileserver := decorate(http.FileServer(fs))
|
||||
mu := http.NewServeMux()
|
||||
if u.Path == "" || u.Path == "/" {
|
||||
if baseURL.Path() == "" || baseURL.Path() == "/" {
|
||||
mu.Handle("/", fileserver)
|
||||
} else {
|
||||
mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
|
||||
mu.Handle(baseURL.Path(), http.StripPrefix(baseURL.Path(), fileserver))
|
||||
}
|
||||
if r.IsTestRun() {
|
||||
var shutDownOnce sync.Once
|
||||
@@ -401,7 +400,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||
|
||||
endpoint := net.JoinHostPort(f.c.serverInterface, strconv.Itoa(port))
|
||||
|
||||
return mu, listener, u.String(), endpoint, nil
|
||||
return mu, listener, baseURL.String(), endpoint, nil
|
||||
}
|
||||
|
||||
func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
|
||||
@@ -447,7 +446,6 @@ type serverCommand struct {
|
||||
doLiveReload bool
|
||||
|
||||
// Flags.
|
||||
renderToDisk bool
|
||||
renderStaticToDisk bool
|
||||
navigateToChanged bool
|
||||
serverAppend bool
|
||||
@@ -455,6 +453,7 @@ type serverCommand struct {
|
||||
tlsCertFile string
|
||||
tlsKeyFile string
|
||||
tlsAuto bool
|
||||
pprof bool
|
||||
serverPort int
|
||||
liveReloadPort int
|
||||
serverWatch bool
|
||||
@@ -469,7 +468,11 @@ func (c *serverCommand) Name() string {
|
||||
}
|
||||
|
||||
func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
|
||||
|
||||
if c.pprof {
|
||||
go func() {
|
||||
http.ListenAndServe("localhost:8080", nil)
|
||||
}()
|
||||
}
|
||||
// Watch runs its own server as part of the routine
|
||||
if c.serverWatch {
|
||||
|
||||
@@ -494,8 +497,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
|
||||
|
||||
err := func() error {
|
||||
defer c.r.timeTrack(time.Now(), "Built")
|
||||
err := c.build()
|
||||
return err
|
||||
return c.build()
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -510,8 +512,9 @@ func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd.Long = `Hugo provides its own webserver which builds and serves the site.
|
||||
While hugo server is high performance, it is a webserver with limited options.
|
||||
|
||||
'hugo server' will avoid writing the rendered and served content to disk,
|
||||
preferring to store it in memory.
|
||||
The ` + "`" + `hugo server` + "`" + ` command will by default write and serve files from disk, but
|
||||
you can render to memory by using the ` + "`" + `--renderToMemory` + "`" + ` flag. This can be
|
||||
faster in some cases, but it will consume more memory.
|
||||
|
||||
By default hugo will also watch your files for any changes you make and
|
||||
automatically rebuild the site. It will then live reload any open browser pages
|
||||
@@ -520,27 +523,26 @@ of a second, you will be able to save and see your changes nearly instantly.`
|
||||
cmd.Aliases = []string{"serve"}
|
||||
|
||||
cmd.Flags().IntVarP(&c.serverPort, "port", "p", 1313, "port on which the server will listen")
|
||||
_ = cmd.RegisterFlagCompletionFunc("port", cobra.NoFileCompletions)
|
||||
cmd.Flags().IntVar(&c.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("liveReloadPort", cobra.NoFileCompletions)
|
||||
cmd.Flags().StringVarP(&c.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
|
||||
_ = cmd.RegisterFlagCompletionFunc("bind", cobra.NoFileCompletions)
|
||||
cmd.Flags().StringVarP(&c.tlsCertFile, "tlsCertFile", "", "", "path to TLS certificate file")
|
||||
_ = cmd.MarkFlagFilename("tlsCertFile", "pem")
|
||||
cmd.Flags().StringVarP(&c.tlsKeyFile, "tlsKeyFile", "", "", "path to TLS key file")
|
||||
_ = cmd.MarkFlagFilename("tlsKeyFile", "pem")
|
||||
cmd.Flags().BoolVar(&c.tlsAuto, "tlsAuto", false, "generate and use locally-trusted certificates.")
|
||||
cmd.Flags().BoolVar(&c.pprof, "pprof", false, "enable the pprof server (port 8080)")
|
||||
cmd.Flags().BoolVarP(&c.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
|
||||
cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
|
||||
cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
|
||||
cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
|
||||
cmd.Flags().BoolVar(&c.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
|
||||
cmd.Flags().BoolVar(&c.renderToDisk, "renderToDisk", false, "serve all files from disk (default is from memory)")
|
||||
cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
|
||||
cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
|
||||
cmd.Flags().String("memstats", "", "log memory usage to this file")
|
||||
cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
|
||||
|
||||
cmd.Flags().SetAnnotation("tlsCertFile", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.Flags().SetAnnotation("tlsKeyFile", cobra.BashCompSubdirsInDir, []string{})
|
||||
|
||||
r := cd.Root.Command.(*rootCommand)
|
||||
applyLocalFlagsBuild(cmd, r)
|
||||
|
||||
@@ -566,7 +568,7 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.setBaseURLsInConfig(); err != nil {
|
||||
if err := c.setServerInfoInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -582,7 +584,9 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
)
|
||||
|
||||
destinationFlag := cd.CobraCommand.Flags().Lookup("destination")
|
||||
c.renderToDisk = c.renderToDisk || (destinationFlag != nil && destinationFlag.Changed)
|
||||
if c.r.renderToMemory && (destinationFlag != nil && destinationFlag.Changed) {
|
||||
return fmt.Errorf("cannot use --renderToMemory with --destination")
|
||||
}
|
||||
c.doLiveReload = !c.disableLiveReload
|
||||
c.fastRenderMode = !c.disableFastRender
|
||||
c.showErrorInBrowser = c.doLiveReload && !c.disableBrowserError
|
||||
@@ -609,15 +613,15 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverCommand) setBaseURLsInConfig() error {
|
||||
func (c *serverCommand) setServerInfoInConfig() error {
|
||||
if len(c.serverPorts) == 0 {
|
||||
panic("no server ports set")
|
||||
}
|
||||
return c.withConfE(func(conf *commonConfig) error {
|
||||
for i, language := range conf.configs.Languages {
|
||||
isMultiHost := conf.configs.IsMultihost
|
||||
isMultihost := conf.configs.IsMultihost
|
||||
var serverPort int
|
||||
if isMultiHost {
|
||||
if isMultihost {
|
||||
serverPort = c.serverPorts[i].p
|
||||
} else {
|
||||
serverPort = c.serverPorts[0].p
|
||||
@@ -636,7 +640,8 @@ func (c *serverCommand) setBaseURLsInConfig() error {
|
||||
if c.liveReloadPort != -1 {
|
||||
baseURLLiveReload, _ = baseURLLiveReload.WithPort(c.liveReloadPort)
|
||||
}
|
||||
langConfig.C.SetBaseURL(baseURL, baseURLLiveReload)
|
||||
langConfig.C.SetServerInfo(baseURL, baseURLLiveReload, c.serverInterface)
|
||||
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -676,7 +681,7 @@ func (c *serverCommand) createCertificates(conf *commonConfig) error {
|
||||
|
||||
// Create the directory if it doesn't exist.
|
||||
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(keyDir, 0777); err != nil {
|
||||
if err := os.MkdirAll(keyDir, 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -701,7 +706,6 @@ func (c *serverCommand) createCertificates(conf *commonConfig) error {
|
||||
// Yes, this is unfortunate, but it's currently the only way to use Mkcert as a library.
|
||||
os.Args = []string{"-cert-file", c.tlsCertFile, "-key-file", c.tlsKeyFile, hostname}
|
||||
return mclib.RunMain()
|
||||
|
||||
}
|
||||
|
||||
func (c *serverCommand) verifyCert(rootPEM, certPEM []byte, name string) error {
|
||||
@@ -736,9 +740,9 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
|
||||
flags := cd.CobraCommand.Flags()
|
||||
var cerr error
|
||||
c.withConf(func(conf *commonConfig) {
|
||||
isMultiHost := conf.configs.IsMultihost
|
||||
isMultihost := conf.configs.IsMultihost
|
||||
c.serverPorts = make([]serverPortListener, 1)
|
||||
if isMultiHost {
|
||||
if isMultihost {
|
||||
if !c.serverAppend {
|
||||
cerr = errors.New("--appendPort=false not supported when in multihost mode")
|
||||
return
|
||||
@@ -831,9 +835,9 @@ func (c *serverCommand) partialReRender(urls ...string) error {
|
||||
c.errState.setWasErr(false)
|
||||
}()
|
||||
c.errState.setBuildErr(nil)
|
||||
visited := make(map[string]bool)
|
||||
visited := types.NewEvictingStringQueue(len(urls))
|
||||
for _, url := range urls {
|
||||
visited[url] = true
|
||||
visited.Add(url)
|
||||
}
|
||||
|
||||
h, err := c.hugo()
|
||||
@@ -846,12 +850,12 @@ func (c *serverCommand) partialReRender(urls ...string) error {
|
||||
|
||||
func (c *serverCommand) serve() error {
|
||||
var (
|
||||
baseURLs []string
|
||||
baseURLs []urls.BaseURL
|
||||
roots []string
|
||||
h *hugolib.HugoSites
|
||||
)
|
||||
err := c.withConfE(func(conf *commonConfig) error {
|
||||
isMultiHost := conf.configs.IsMultihost
|
||||
isMultihost := conf.configs.IsMultihost
|
||||
var err error
|
||||
h, err = c.r.HugFromConfig(conf)
|
||||
if err != nil {
|
||||
@@ -861,20 +865,19 @@ func (c *serverCommand) serve() error {
|
||||
// We need the server to share the same logger as the Hugo build (for error counts etc.)
|
||||
c.r.logger = h.Log
|
||||
|
||||
if isMultiHost {
|
||||
if isMultihost {
|
||||
for _, l := range conf.configs.ConfigLangs() {
|
||||
baseURLs = append(baseURLs, l.BaseURL().String())
|
||||
baseURLs = append(baseURLs, l.BaseURL())
|
||||
roots = append(roots, l.Language().Lang)
|
||||
}
|
||||
} else {
|
||||
l := conf.configs.GetFirstLanguageConfig()
|
||||
baseURLs = []string{l.BaseURL().String()}
|
||||
baseURLs = []urls.BaseURL{l.BaseURL()}
|
||||
roots = []string{""}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -946,15 +949,11 @@ func (c *serverCommand) serve() error {
|
||||
servers = append(servers, srv)
|
||||
|
||||
if doLiveReload {
|
||||
u, err := url.Parse(helpers.SanitizeURL(baseURLs[i]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc(u.Path+"/livereload", livereload.Handler)
|
||||
baseURL := baseURLs[i]
|
||||
mu.HandleFunc(baseURL.Path()+"livereload.js", livereload.ServeJS)
|
||||
mu.HandleFunc(baseURL.Path()+"livereload", livereload.Handler)
|
||||
}
|
||||
c.r.Printf("Web Server is available at %s (bind address %s)\n", serverURL, c.serverInterface)
|
||||
c.r.Printf("Web Server is available at %s (bind address %s) %s\n", serverURL, c.serverInterface, roots[i])
|
||||
wg1.Go(func() error {
|
||||
if c.tlsCertFile != "" && c.tlsKeyFile != "" {
|
||||
err = srv.ServeTLS(listener, c.tlsCertFile, c.tlsKeyFile)
|
||||
@@ -971,8 +970,12 @@ func (c *serverCommand) serve() error {
|
||||
if c.r.IsTestRun() {
|
||||
// Write a .ready file to disk to signal ready status.
|
||||
// This is where the test is run from.
|
||||
var baseURLs []string
|
||||
for _, baseURL := range srv.baseURLs {
|
||||
baseURLs = append(baseURLs, baseURL.String())
|
||||
}
|
||||
testInfo := map[string]any{
|
||||
"baseURLs": srv.baseURLs,
|
||||
"baseURLs": baseURLs,
|
||||
}
|
||||
|
||||
dir := os.Getenv("WORK")
|
||||
@@ -983,7 +986,7 @@ func (c *serverCommand) serve() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(readyFile, b, 0777)
|
||||
err = os.WriteFile(readyFile, b, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1005,7 +1008,6 @@ func (c *serverCommand) serve() error {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
c.r.Println("Error:", err)
|
||||
}
|
||||
@@ -1087,7 +1089,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
|
||||
fromPath := ev.Name
|
||||
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath)
|
||||
relPath, found := sourceFs.MakePathRelative(fromPath, true)
|
||||
|
||||
if !found {
|
||||
// Not member of this virtual host.
|
||||
@@ -1167,7 +1169,7 @@ func cleanErrorLog(content string) string {
|
||||
return strings.Join(keep, ": ")
|
||||
}
|
||||
|
||||
func injectLiveReloadScript(src io.Reader, baseURL url.URL) string {
|
||||
func injectLiveReloadScript(src io.Reader, baseURL *url.URL) string {
|
||||
var b bytes.Buffer
|
||||
chain := transform.Chain{livereloadinject.New(baseURL)}
|
||||
chain.Apply(&b, src)
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestAppend(t *testing.T) {
|
||||
[]any{&tstSlicerIn1{"c"}},
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}},
|
||||
},
|
||||
//https://github.com/gohugoio/hugo/issues/5361
|
||||
// https://github.com/gohugoio/hugo/issues/5361
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
[]any{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
@@ -102,14 +102,16 @@ func TestAppendToMultiDimensionalSlice(t *testing.T) {
|
||||
from []any
|
||||
expected any
|
||||
}{
|
||||
{[][]string{{"a", "b"}},
|
||||
{
|
||||
[][]string{{"a", "b"}},
|
||||
[]any{[]string{"c", "d"}},
|
||||
[][]string{
|
||||
{"a", "b"},
|
||||
{"c", "d"},
|
||||
},
|
||||
},
|
||||
{[][]string{{"a", "b"}},
|
||||
{
|
||||
[][]string{{"a", "b"}},
|
||||
[]any{[]string{"c", "d"}, []string{"e", "f"}},
|
||||
[][]string{
|
||||
{"a", "b"},
|
||||
@@ -117,7 +119,8 @@ func TestAppendToMultiDimensionalSlice(t *testing.T) {
|
||||
{"e", "f"},
|
||||
},
|
||||
},
|
||||
{[][]string{{"a", "b"}},
|
||||
{
|
||||
[][]string{{"a", "b"}},
|
||||
[]any{[]int{1, 2}},
|
||||
false,
|
||||
},
|
||||
@@ -130,7 +133,6 @@ func TestAppendToMultiDimensionalSlice(t *testing.T) {
|
||||
c.Assert(result, qt.DeepEquals, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
|
||||
|
||||
@@ -73,7 +73,6 @@ func StringSliceToInterfaceSlice(ss []string) []any {
|
||||
result[i] = s
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
type SortedStringSlice []string
|
||||
|
||||
@@ -135,5 +135,4 @@ func TestSortedStringSlice(t *testing.T) {
|
||||
c.Assert(s.Count("b"), qt.Equals, 3)
|
||||
c.Assert(s.Count("z"), qt.Equals, 0)
|
||||
c.Assert(s.Count("a"), qt.Equals, 1)
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collections
|
||||
|
||||
import "sync"
|
||||
|
||||
// Stack is a simple LIFO stack that is safe for concurrent use.
|
||||
type Stack[T any] struct {
|
||||
items []T
|
||||
zero T
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewStack[T any]() *Stack[T] {
|
||||
return &Stack[T]{}
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Push(item T) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.items = append(s.items, item)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Pop() (T, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
item := s.items[len(s.items)-1]
|
||||
s.items = s.items[:len(s.items)-1]
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Peek() (T, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.items) == 0 {
|
||||
return s.zero, false
|
||||
}
|
||||
return s.items[len(s.items)-1], true
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Len() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
func (s *Stack[T]) Drain() []T {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
items := s.items
|
||||
s.items = nil
|
||||
return items
|
||||
}
|
||||
@@ -13,13 +13,33 @@
|
||||
|
||||
package constants
|
||||
|
||||
// Error IDs.
|
||||
// Error/Warning IDs.
|
||||
// Do not change these values.
|
||||
const (
|
||||
ErrIDAmbigousDisableKindTaxonomy = "error-disable-taxonomy"
|
||||
ErrIDAmbigousOutputKindTaxonomy = "error-output-taxonomy"
|
||||
|
||||
// IDs for remote errors in tpl/data.
|
||||
ErrRemoteGetJSON = "error-remote-getjson"
|
||||
ErrRemoteGetCSV = "error-remote-getcsv"
|
||||
|
||||
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
|
||||
)
|
||||
|
||||
// Field/method names with special meaning.
|
||||
const (
|
||||
FieldRelPermalink = "RelPermalink"
|
||||
FieldPermalink = "Permalink"
|
||||
)
|
||||
|
||||
// IsFieldRelOrPermalink returns whether the given name is a RelPermalink or Permalink.
|
||||
func IsFieldRelOrPermalink(name string) bool {
|
||||
return name == FieldRelPermalink || name == FieldPermalink
|
||||
}
|
||||
|
||||
// Resource transformations.
|
||||
const (
|
||||
ResourceTransformationFingerprint = "fingerprint"
|
||||
)
|
||||
|
||||
// IsResourceTransformationLinkChange returns whether the given name is a resource transformation that changes the permalink based on the content.
|
||||
func IsResourceTransformationPermalinkHash(name string) bool {
|
||||
return name == ResourceTransformationFingerprint
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hcontext
|
||||
|
||||
import "context"
|
||||
|
||||
// ContextDispatcher is a generic interface for setting and getting values from a context.
|
||||
type ContextDispatcher[T any] interface {
|
||||
Set(ctx context.Context, value T) context.Context
|
||||
Get(ctx context.Context) T
|
||||
}
|
||||
|
||||
// NewContextDispatcher creates a new ContextDispatcher with the given key.
|
||||
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
|
||||
return keyInContext[T, R]{
|
||||
id: key,
|
||||
}
|
||||
}
|
||||
|
||||
type keyInContext[T any, R comparable] struct {
|
||||
zero T
|
||||
id R
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Get(ctx context.Context) T {
|
||||
v := ctx.Value(f.id)
|
||||
if v == nil {
|
||||
return f.zero
|
||||
}
|
||||
return v.(T)
|
||||
}
|
||||
|
||||
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
|
||||
return context.WithValue(ctx, f.id, value)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -74,7 +74,6 @@ func ContainsMatcher(text string) func(m LineMatcher) int {
|
||||
// ErrorContext contains contextual information about an error. This will
|
||||
// typically be the lines surrounding some problem in a file.
|
||||
type ErrorContext struct {
|
||||
|
||||
// If a match will contain the matched line and up to 2 lines before and after.
|
||||
// Will be empty if no match.
|
||||
Lines []string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
+39
-11
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,14 +15,15 @@
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PrintStackTrace prints the current stacktrace to w.
|
||||
@@ -49,14 +50,22 @@ func Recover(args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetGID the current goroutine id. Used only for debugging.
|
||||
func GetGID() uint64 {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||
b = b[:bytes.IndexByte(b, ' ')]
|
||||
n, _ := strconv.ParseUint(string(b), 10, 64)
|
||||
return n
|
||||
// IsTimeoutError returns true if the given error is or contains a TimeoutError.
|
||||
func IsTimeoutError(err error) bool {
|
||||
return errors.Is(err, &TimeoutError{})
|
||||
}
|
||||
|
||||
type TimeoutError struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func (e *TimeoutError) Error() string {
|
||||
return fmt.Sprintf("timeout after %s", e.Duration)
|
||||
}
|
||||
|
||||
func (e *TimeoutError) Is(target error) bool {
|
||||
_, ok := target.(*TimeoutError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
|
||||
@@ -109,3 +118,22 @@ func IsNotExist(err error) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`)
|
||||
|
||||
func ImproveIfNilPointer(inErr error) (outErr error) {
|
||||
outErr = inErr
|
||||
|
||||
m := nilPointerErrRe.FindStringSubmatch(inErr.Error())
|
||||
if len(m) == 0 {
|
||||
return
|
||||
}
|
||||
call := m[1]
|
||||
field := m[2]
|
||||
parts := strings.Split(call, ".")
|
||||
receiverName := parts[len(parts)-2]
|
||||
receiver := strings.Join(parts[:len(parts)-1], ".")
|
||||
s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field)
|
||||
outErr = errors.New(nilPointerErrRe.ReplaceAllString(inErr.Error(), s))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -42,5 +42,4 @@ func TestIsFeatureNotAvailableError(t *testing.T) {
|
||||
c.Assert(IsFeatureNotAvailableError(ErrFeatureNotAvailable), qt.Equals, true)
|
||||
c.Assert(IsFeatureNotAvailableError(&FeatureNotAvailableError{}), qt.Equals, true)
|
||||
c.Assert(IsFeatureNotAvailableError(errors.New("asdf")), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,13 +15,13 @@ package herrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/bep/golibsass/libsass/libsasserrors"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
@@ -29,8 +29,6 @@ import (
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/tdewolff/parse/v2"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
// FileError represents an error when handling a file: Parsing a config file,
|
||||
@@ -48,6 +46,9 @@ type FileError interface {
|
||||
|
||||
// UpdateContent updates the error with a new ErrorContext from the content of the file.
|
||||
UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError
|
||||
|
||||
// SetFilename sets the filename of the error.
|
||||
SetFilename(filename string) FileError
|
||||
}
|
||||
|
||||
// Unwrapper can unwrap errors created with fmt.Errorf.
|
||||
@@ -60,6 +61,11 @@ var (
|
||||
_ Unwrapper = (*fileError)(nil)
|
||||
)
|
||||
|
||||
func (fe *fileError) SetFilename(filename string) FileError {
|
||||
fe.position.Filename = filename
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *fileError) UpdatePosition(pos text.Position) FileError {
|
||||
oldFilename := fe.Position().Filename
|
||||
if pos.Filename != "" && fe.fileType == "" {
|
||||
@@ -115,7 +121,6 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
|
||||
}
|
||||
|
||||
return fe
|
||||
|
||||
}
|
||||
|
||||
type fileError struct {
|
||||
@@ -181,7 +186,6 @@ func NewFileErrorFromName(err error, name string) FileError {
|
||||
}
|
||||
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
|
||||
}
|
||||
|
||||
// NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err.
|
||||
@@ -192,7 +196,6 @@ func NewFileErrorFromPos(err error, pos text.Position) FileError {
|
||||
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename))
|
||||
}
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
|
||||
}
|
||||
|
||||
func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError {
|
||||
@@ -249,7 +252,6 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
|
||||
}); ok {
|
||||
realFilename = s.Filename()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
f, err2 := fs.Open(filename)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -14,12 +14,11 @@
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -48,7 +47,6 @@ func TestNewFileError(t *testing.T) {
|
||||
c.Assert(errorContext.Lines, qt.DeepEquals, []string{"line 30", "line 31", "line 32", "line 33", "line 34"})
|
||||
c.Assert(errorContext.LinesPos, qt.Equals, 2)
|
||||
c.Assert(errorContext.ChromaLexer, qt.Equals, "go-html-template")
|
||||
|
||||
}
|
||||
|
||||
func TestNewFileErrorExtractFromMessage(t *testing.T) {
|
||||
|
||||
@@ -19,11 +19,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -142,7 +141,6 @@ func (e *Exec) New(name string, arg ...any) (Runner, error) {
|
||||
}
|
||||
|
||||
return cm.command(arg...)
|
||||
|
||||
}
|
||||
|
||||
// Npx is a convenience method to create a Runner running npx --no-install <name> <args.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/htime"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
@@ -188,6 +189,20 @@ func IsTime(tp reflect.Type) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValid returns whether v is not nil and a valid value.
|
||||
func IsValid(v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return !v.IsNil()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AsTime returns v as a time.Time if possible.
|
||||
// The given location is only used if the value implements AsTimeProvider (e.g. go-toml local).
|
||||
// A zero Time and false is returned if this isn't possible.
|
||||
@@ -217,7 +232,7 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
|
||||
panic("not supported")
|
||||
}
|
||||
first := tp.In(0)
|
||||
if first.Implements(ContextInterface) {
|
||||
if IsContextType(first) {
|
||||
args = append(args, reflect.ValueOf(cxt))
|
||||
}
|
||||
}
|
||||
@@ -236,4 +251,24 @@ func indirectInterface(v reflect.Value) reflect.Value {
|
||||
return v.Elem()
|
||||
}
|
||||
|
||||
var ContextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||
|
||||
var isContextCache = maps.NewCache[reflect.Type, bool]()
|
||||
|
||||
type k string
|
||||
|
||||
var contextTypeValue = reflect.TypeOf(context.WithValue(context.Background(), k("key"), 32))
|
||||
|
||||
// IsContextType returns whether tp is a context.Context type.
|
||||
func IsContextType(tp reflect.Type) bool {
|
||||
if tp == contextTypeValue {
|
||||
return true
|
||||
}
|
||||
if tp == contextInterface {
|
||||
return true
|
||||
}
|
||||
|
||||
return isContextCache.GetOrCreate(tp, func() bool {
|
||||
return tp.Implements(contextInterface)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package hreflect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -40,6 +41,42 @@ func TestGetMethodByName(t *testing.T) {
|
||||
c.Assert(GetMethodIndexByName(tp, "Foo"), qt.Equals, -1)
|
||||
}
|
||||
|
||||
func TestIsContextType(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
type k string
|
||||
ctx := context.Background()
|
||||
valueCtx := context.WithValue(ctx, k("key"), 32)
|
||||
c.Assert(IsContextType(reflect.TypeOf(ctx)), qt.IsTrue)
|
||||
c.Assert(IsContextType(reflect.TypeOf(valueCtx)), qt.IsTrue)
|
||||
}
|
||||
|
||||
func BenchmarkIsContextType(b *testing.B) {
|
||||
type k string
|
||||
b.Run("value", func(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
ctxs := make([]reflect.Type, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxs[i]) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("background", func(b *testing.B) {
|
||||
var ctxt reflect.Type = reflect.TypeOf(context.Background())
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !IsContextType(ctxt) {
|
||||
b.Fatal("not context")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkIsTruthFul(b *testing.B) {
|
||||
v := reflect.ValueOf("Hugo")
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -99,3 +99,31 @@ var reCache = regexpCache{re: make(map[string]*regexp.Regexp)}
|
||||
func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
|
||||
return reCache.getOrCompileRegexp(pattern)
|
||||
}
|
||||
|
||||
// InSlice checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
func InSlice(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if v == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InSlicEqualFold checks if a string is an element of a slice of strings
|
||||
// and returns a boolean value.
|
||||
// It uses strings.EqualFold to compare.
|
||||
func InSlicEqualFold(arr []string, el string) bool {
|
||||
for _, v := range arr {
|
||||
if strings.EqualFold(v, el) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Tuple struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -33,7 +33,6 @@ func TestStringEqualFold(t *testing.T) {
|
||||
c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false)
|
||||
c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true)
|
||||
c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
func TestGetOrCompileRegexp(t *testing.T) {
|
||||
@@ -42,7 +41,6 @@ func TestGetOrCompileRegexp(t *testing.T) {
|
||||
re, err := GetOrCompileRegexp(`\d+`)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(re.MatchString("123"), qt.Equals, true)
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkGetOrCompileRegexp(b *testing.B) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -47,12 +47,7 @@ defaultContentLanguage = 'it'
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
month: _gennaio_ weekday: _lunedì_
|
||||
@@ -53,7 +53,6 @@ func TestTimeFormatter(t *testing.T) {
|
||||
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) {
|
||||
@@ -68,7 +67,6 @@ func TestTimeFormatter(t *testing.T) {
|
||||
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) {
|
||||
@@ -107,9 +105,7 @@ func TestTimeFormatter(t *testing.T) {
|
||||
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
|
||||
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkTimeFormatter(b *testing.B) {
|
||||
|
||||
@@ -16,6 +16,7 @@ package hugio
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
iofs "io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
@@ -60,12 +61,16 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool
|
||||
return fmt.Errorf("%q is not a directory", from)
|
||||
}
|
||||
|
||||
err = fs.MkdirAll(to, 0777) // before umask
|
||||
err = fs.MkdirAll(to, 0o777) // before umask
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, _ := afero.ReadDir(fs, from)
|
||||
d, err := fs.Open(from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries, _ := d.(iofs.ReadDirFile).ReadDir(-1)
|
||||
for _, entry := range entries {
|
||||
fromFilename := filepath.Join(from, entry.Name())
|
||||
toFilename := filepath.Join(to, entry.Name())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
+55
-8
@@ -14,6 +14,7 @@
|
||||
package hugio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
@@ -36,24 +37,70 @@ type ReadSeekCloserProvider interface {
|
||||
ReadSeekCloser() (ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
// TODO(bep) rename this and similar to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense.
|
||||
type ReadSeekerNoOpCloser struct {
|
||||
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
type readSeekerNopCloser struct {
|
||||
ReadSeeker
|
||||
}
|
||||
|
||||
// Close does nothing.
|
||||
func (r ReadSeekerNoOpCloser) Close() error {
|
||||
func (r readSeekerNopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
|
||||
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekerNoOpCloser {
|
||||
return ReadSeekerNoOpCloser{r}
|
||||
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
|
||||
return readSeekerNopCloser{r}
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
|
||||
// from the given string.
|
||||
func NewReadSeekerNoOpCloserFromString(content string) ReadSeekerNoOpCloser {
|
||||
return ReadSeekerNoOpCloser{strings.NewReader(content)}
|
||||
func NewReadSeekerNoOpCloserFromString(content string) ReadSeekCloser {
|
||||
return stringReadSeeker{s: content, readSeekerNopCloser: readSeekerNopCloser{strings.NewReader(content)}}
|
||||
}
|
||||
|
||||
var _ StringReader = (*stringReadSeeker)(nil)
|
||||
|
||||
type stringReadSeeker struct {
|
||||
s string
|
||||
readSeekerNopCloser
|
||||
}
|
||||
|
||||
func (s *stringReadSeeker) ReadString() string {
|
||||
return s.s
|
||||
}
|
||||
|
||||
// StringReader provides a way to read a string.
|
||||
type StringReader interface {
|
||||
ReadString() string
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
|
||||
// from the given bytes slice.
|
||||
func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
|
||||
return readSeekerNopCloser{bytes.NewReader(content)}
|
||||
}
|
||||
|
||||
// NewReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
|
||||
// The ReadSeeker will be seeked to the beginning before returned.
|
||||
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
|
||||
return func() (ReadSeekCloser, error) {
|
||||
r.Seek(0, io.SeekStart)
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
// OpenReadSeekCloser allows setting some other way (than reading from a filesystem)
|
||||
// to open or create a ReadSeekCloser.
|
||||
type OpenReadSeekCloser func() (ReadSeekCloser, error)
|
||||
|
||||
// ReadString reads from the given reader and returns the content as a string.
|
||||
func ReadString(r io.Reader) (string, error) {
|
||||
if sr, ok := r.(StringReader); ok {
|
||||
return sr.ReadString(), nil
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
+90
-8
@@ -22,18 +22,21 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
godartsassv1 "github.com/bep/godartsass"
|
||||
"github.com/bep/logg"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
iofs "io/fs"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
)
|
||||
@@ -78,10 +81,22 @@ func (i HugoInfo) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
|
||||
}
|
||||
|
||||
// IsDevelopment reports whether the current running environment is "development".
|
||||
func (i HugoInfo) IsDevelopment() bool {
|
||||
return i.Environment == EnvironmentDevelopment
|
||||
}
|
||||
|
||||
// IsProduction reports whether the current running environment is "production".
|
||||
func (i HugoInfo) IsProduction() bool {
|
||||
return i.Environment == EnvironmentProduction
|
||||
}
|
||||
|
||||
// IsServer reports whether the built-in server is running.
|
||||
func (i HugoInfo) IsServer() bool {
|
||||
return i.conf.Running()
|
||||
}
|
||||
|
||||
// IsExtended reports whether the Hugo binary is the extended version.
|
||||
func (i HugoInfo) IsExtended() bool {
|
||||
return IsExtended
|
||||
}
|
||||
@@ -96,10 +111,29 @@ func (i HugoInfo) Deps() []*Dependency {
|
||||
return i.deps
|
||||
}
|
||||
|
||||
// Deprecated: Use hugo.IsMultihost instead.
|
||||
func (i HugoInfo) IsMultiHost() bool {
|
||||
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
|
||||
return i.conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultihost reports whether each configured language has a unique baseURL.
|
||||
func (i HugoInfo) IsMultihost() bool {
|
||||
return i.conf.IsMultihost()
|
||||
}
|
||||
|
||||
// IsMultilingual reports whether there are two or more configured languages.
|
||||
func (i HugoInfo) IsMultilingual() bool {
|
||||
return i.conf.IsMultilingual()
|
||||
}
|
||||
|
||||
// ConfigProvider represents the config options that are relevant for HugoInfo.
|
||||
type ConfigProvider interface {
|
||||
Environment() string
|
||||
Running() bool
|
||||
WorkingDir() string
|
||||
IsMultihost() bool
|
||||
IsMultilingual() bool
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
@@ -145,7 +179,12 @@ func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []strin
|
||||
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir))
|
||||
|
||||
if fs != nil {
|
||||
fis, err := afero.ReadDir(fs, files.FolderJSConfig)
|
||||
var fis []iofs.DirEntry
|
||||
d, err := fs.Open(files.FolderJSConfig)
|
||||
if err == nil {
|
||||
fis, err = d.(iofs.ReadDirFile).ReadDir(-1)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
for _, fi := range fis {
|
||||
key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_"))
|
||||
@@ -170,8 +209,10 @@ type buildInfo struct {
|
||||
*debug.BuildInfo
|
||||
}
|
||||
|
||||
var bInfo *buildInfo
|
||||
var bInfoInit sync.Once
|
||||
var (
|
||||
bInfo *buildInfo
|
||||
bInfoInit sync.Once
|
||||
)
|
||||
|
||||
func getBuildInfo() *buildInfo {
|
||||
bInfoInit.Do(func() {
|
||||
@@ -198,7 +239,6 @@ func getBuildInfo() *buildInfo {
|
||||
bInfo.GoArch = s.Value
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return bInfo
|
||||
@@ -237,12 +277,12 @@ func GetDependencyListNonGo() []string {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.2.4"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
|
||||
)
|
||||
}
|
||||
|
||||
if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" {
|
||||
var dartSassPath = "github.com/sass/dart-sass-embedded"
|
||||
dartSassPath := "github.com/sass/dart-sass-embedded"
|
||||
if IsDartSassV2() {
|
||||
dartSassPath = "github.com/sass/dart-sass"
|
||||
}
|
||||
@@ -333,3 +373,45 @@ var (
|
||||
func IsDartSassV2() bool {
|
||||
return !strings.Contains(DartSassBinaryName, "embedded")
|
||||
}
|
||||
|
||||
// Deprecate informs about a deprecation starting at the given version.
|
||||
//
|
||||
// A deprecation typically needs a simple change in the template, but doing so will make the template incompatible with older versions.
|
||||
// Theme maintainers generally want
|
||||
// 1. No warnings or errors in the console when building a Hugo site.
|
||||
// 2. Their theme to work for at least the last few Hugo versions.
|
||||
func Deprecate(item, alternative string, version string) {
|
||||
level := deprecationLogLevelFromVersion(version)
|
||||
DeprecateLevel(item, alternative, version, level)
|
||||
}
|
||||
|
||||
// DeprecateLevel informs about a deprecation logging at the given level.
|
||||
func DeprecateLevel(item, alternative, version string, level logg.Level) {
|
||||
var msg string
|
||||
if level == logg.LevelError {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
|
||||
} else {
|
||||
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
|
||||
}
|
||||
|
||||
loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf(msg)
|
||||
}
|
||||
|
||||
// We ususally do about one minor version a month.
|
||||
// We want people to run at least the current and previous version without any warnings.
|
||||
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
|
||||
func deprecationLogLevelFromVersion(ver string) logg.Level {
|
||||
from := MustParseVersion(ver)
|
||||
to := CurrentVersion
|
||||
minorDiff := to.Minor - from.Minor
|
||||
switch {
|
||||
case minorDiff >= 12:
|
||||
// Start failing the build after about a year.
|
||||
return logg.LevelError
|
||||
case minorDiff >= 6:
|
||||
// Start printing warnings after about six months.
|
||||
return logg.LevelWarn
|
||||
default:
|
||||
return logg.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugo_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestIsMultilingualAndIsMultihost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages.de]
|
||||
baseURL = 'https://de.example.org/'
|
||||
[languages.en]
|
||||
baseURL = 'https://en.example.org/'
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: home
|
||||
---
|
||||
-- layouts/index.html --
|
||||
multilingual={{ hugo.IsMultilingual }}
|
||||
multihost={{ hugo.IsMultihost }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/de/index.html",
|
||||
"multilingual=true",
|
||||
"multihost=true",
|
||||
)
|
||||
b.AssertFileContent("public/en/index.html",
|
||||
"multilingual=true",
|
||||
"multihost=true",
|
||||
)
|
||||
|
||||
files = strings.ReplaceAll(files, "baseURL = 'https://de.example.org/'", "")
|
||||
files = strings.ReplaceAll(files, "baseURL = 'https://en.example.org/'", "")
|
||||
|
||||
b = hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/de/index.html",
|
||||
"multilingual=true",
|
||||
"multihost=false",
|
||||
)
|
||||
b.AssertFileContent("public/en/index.html",
|
||||
"multilingual=true",
|
||||
"multihost=false",
|
||||
)
|
||||
|
||||
files = strings.ReplaceAll(files, "[languages.de]", "")
|
||||
files = strings.ReplaceAll(files, "[languages.en]", "")
|
||||
|
||||
b = hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/index.html",
|
||||
"multilingual=false",
|
||||
"multihost=false",
|
||||
)
|
||||
}
|
||||
@@ -17,13 +17,14 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork"}
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
hugoInfo := NewInfo(conf, nil)
|
||||
|
||||
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
|
||||
@@ -38,22 +39,55 @@ func TestHugoInfo(t *testing.T) {
|
||||
}
|
||||
c.Assert(hugoInfo.Environment, qt.Equals, "production")
|
||||
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false)
|
||||
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
|
||||
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
|
||||
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
|
||||
|
||||
devHugoInfo := NewInfo(testConfig{environment: "development"}, nil)
|
||||
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
|
||||
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
|
||||
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
|
||||
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
|
||||
ver := CurrentVersion
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 1
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
|
||||
ver.Minor -= 6
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
|
||||
ver.Minor -= 6
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
environment string
|
||||
workingDir string
|
||||
environment string
|
||||
running bool
|
||||
workingDir string
|
||||
multihost bool
|
||||
multilingual bool
|
||||
}
|
||||
|
||||
func (c testConfig) Environment() string {
|
||||
return c.environment
|
||||
}
|
||||
|
||||
func (c testConfig) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c testConfig) WorkingDir() string {
|
||||
return c.workingDir
|
||||
}
|
||||
|
||||
func (c testConfig) IsMultihost() bool {
|
||||
return c.multihost
|
||||
}
|
||||
|
||||
func (c testConfig) IsMultilingual() bool {
|
||||
return c.multilingual
|
||||
}
|
||||
|
||||
@@ -67,8 +67,11 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
v := MustParseVersion(h.String())
|
||||
return compareVersions(v, other)
|
||||
return compareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
return MustParseVersion(h.String())
|
||||
}
|
||||
|
||||
// Eq implements the compare.Eqer interface.
|
||||
@@ -264,7 +267,6 @@ func compareFloatWithVersion(v1 float64, v2 Version) int {
|
||||
|
||||
if v1maj > v2.Major {
|
||||
return 1
|
||||
|
||||
}
|
||||
|
||||
if v1maj < v2.Major {
|
||||
@@ -276,7 +278,6 @@ func compareFloatWithVersion(v1 float64, v2 Version) int {
|
||||
}
|
||||
|
||||
return -1
|
||||
|
||||
}
|
||||
|
||||
func GoMinorVersion() int {
|
||||
|
||||
@@ -17,7 +17,7 @@ package hugo
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 117,
|
||||
Minor: 125,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -27,10 +27,9 @@ import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var bold = color.New(color.Bold)
|
||||
|
||||
// levelColor mapping.
|
||||
var levelColor = [...]*color.Color{
|
||||
logg.LevelTrace: color.New(color.FgWhite),
|
||||
logg.LevelDebug: color.New(color.FgWhite),
|
||||
logg.LevelInfo: color.New(color.FgBlue),
|
||||
logg.LevelWarn: color.New(color.FgYellow),
|
||||
@@ -39,6 +38,7 @@ var levelColor = [...]*color.Color{
|
||||
|
||||
// levelString mapping.
|
||||
var levelString = [...]string{
|
||||
logg.LevelTrace: "TRACE",
|
||||
logg.LevelDebug: "DEBUG",
|
||||
logg.LevelInfo: "INFO ",
|
||||
logg.LevelWarn: "WARN ",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -69,7 +69,7 @@ func (h *logLevelCounter) HandleLog(e *logg.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var stopError = fmt.Errorf("stop")
|
||||
var errStop = fmt.Errorf("stop")
|
||||
|
||||
type logOnceHandler struct {
|
||||
threshold logg.Level
|
||||
@@ -87,7 +87,7 @@ func (h *logOnceHandler) HandleLog(e *logg.Entry) error {
|
||||
defer h.mu.Unlock()
|
||||
hash := identity.HashUint64(e.Level, e.Message, e.Fields)
|
||||
if h.seen[hash] {
|
||||
return stopError
|
||||
return errStop
|
||||
}
|
||||
h.seen[hash] = true
|
||||
return nil
|
||||
@@ -107,7 +107,7 @@ type stopHandler struct {
|
||||
func (h *stopHandler) HandleLog(e *logg.Entry) error {
|
||||
for _, handler := range h.handlers {
|
||||
if err := handler.HandleLog(e); err != nil {
|
||||
if err == stopError {
|
||||
if err == errStop {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -124,26 +124,13 @@ func (h *suppressStatementsHandler) HandleLog(e *logg.Entry) error {
|
||||
for _, field := range e.Fields {
|
||||
if field.Name == FieldNameStatementID {
|
||||
if h.statements[field.Value.(string)] {
|
||||
return stopError
|
||||
return errStop
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// replacer creates a new log handler that does string replacement in log messages.
|
||||
func replacer(repl *strings.Replacer) logg.Handler {
|
||||
return logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
e.Message = repl.Replace(e.Message)
|
||||
for i, field := range e.Fields {
|
||||
if s, ok := field.Value.(string); ok {
|
||||
e.Fields[i].Value = repl.Replace(s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// whiteSpaceTrimmer creates a new log handler that trims whitespace from log messages and string fields.
|
||||
func whiteSpaceTrimmer() logg.Handler {
|
||||
return logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -81,7 +81,7 @@ func (h *noColoursHandler) HandleLog(e *logg.Entry) error {
|
||||
if strings.HasPrefix(field.Name, reservedFieldNamePrefix) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s %q", field.Name, field.Value)
|
||||
fmt.Fprintf(w, " %s %v", field.Name, field.Value)
|
||||
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
+82
-20
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -37,13 +37,13 @@ var (
|
||||
|
||||
// Options defines options for the logger.
|
||||
type Options struct {
|
||||
Level logg.Level
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Distinct bool
|
||||
StoreErrors bool
|
||||
HandlerPost func(e *logg.Entry) error
|
||||
SuppresssStatements map[string]bool
|
||||
Level logg.Level
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
DistinctLevel logg.Level
|
||||
StoreErrors bool
|
||||
HandlerPost func(e *logg.Entry) error
|
||||
SuppressStatements map[string]bool
|
||||
}
|
||||
|
||||
// New creates a new logger with the given options.
|
||||
@@ -68,11 +68,24 @@ func New(opts Options) Logger {
|
||||
errorsw := &strings.Builder{}
|
||||
logCounters := newLogLevelCounter()
|
||||
handlers := []logg.Handler{
|
||||
whiteSpaceTrimmer(),
|
||||
logHandler,
|
||||
logCounters,
|
||||
}
|
||||
|
||||
if opts.Level == logg.LevelTrace {
|
||||
// Trace is used during development only, and it's useful to
|
||||
// only see the trace messages.
|
||||
handlers = append(handlers,
|
||||
logg.HandlerFunc(func(e *logg.Entry) error {
|
||||
if e.Level != logg.LevelTrace {
|
||||
return logg.ErrStopLogEntry
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
handlers = append(handlers, whiteSpaceTrimmer(), logHandler)
|
||||
|
||||
if opts.HandlerPost != nil {
|
||||
var hookHandler logg.HandlerFunc = func(e *logg.Entry) error {
|
||||
opts.HandlerPost(e)
|
||||
@@ -92,13 +105,13 @@ func New(opts Options) Logger {
|
||||
logHandler = multi.New(handlers...)
|
||||
|
||||
var logOnce *logOnceHandler
|
||||
if opts.Distinct {
|
||||
logOnce = newLogOnceHandler(logg.LevelWarn)
|
||||
if opts.DistinctLevel != 0 {
|
||||
logOnce = newLogOnceHandler(opts.DistinctLevel)
|
||||
logHandler = newStopHandler(logOnce, logHandler)
|
||||
}
|
||||
|
||||
if opts.SuppresssStatements != nil && len(opts.SuppresssStatements) > 0 {
|
||||
logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppresssStatements), logHandler)
|
||||
if opts.SuppressStatements != nil && len(opts.SuppressStatements) > 0 {
|
||||
logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppressStatements), logHandler)
|
||||
}
|
||||
|
||||
logger := logg.New(
|
||||
@@ -127,6 +140,7 @@ func New(opts Options) Logger {
|
||||
out: opts.Stdout,
|
||||
level: opts.Level,
|
||||
logger: logger,
|
||||
tracel: l.WithLevel(logg.LevelTrace),
|
||||
debugl: l.WithLevel(logg.LevelDebug),
|
||||
infol: l.WithLevel(logg.LevelInfo),
|
||||
warnl: l.WithLevel(logg.LevelWarn),
|
||||
@@ -137,10 +151,20 @@ func New(opts Options) Logger {
|
||||
// NewDefault creates a new logger with the default options.
|
||||
func NewDefault() Logger {
|
||||
opts := Options{
|
||||
Distinct: true,
|
||||
Level: logg.LevelWarn,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelWarn,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
|
||||
func NewTrace() Logger {
|
||||
opts := Options{
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Level: logg.LevelTrace,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stdout,
|
||||
}
|
||||
return New(opts)
|
||||
}
|
||||
@@ -150,13 +174,14 @@ func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug() logg.LevelLogger
|
||||
Debugf(format string, v ...any)
|
||||
Debugln(v ...any)
|
||||
Error() logg.LevelLogger
|
||||
Errorf(format string, v ...any)
|
||||
Erroridf(id, format string, v ...any)
|
||||
Errorln(v ...any)
|
||||
Errors() string
|
||||
Errorsf(id, format string, v ...any)
|
||||
Info() logg.LevelLogger
|
||||
InfoCommand(command string) logg.LevelLogger
|
||||
Infof(format string, v ...any)
|
||||
@@ -172,8 +197,10 @@ type Logger interface {
|
||||
Warn() logg.LevelLogger
|
||||
WarnCommand(command string) logg.LevelLogger
|
||||
Warnf(format string, v ...any)
|
||||
Warnidf(id, format string, v ...any)
|
||||
Warnln(v ...any)
|
||||
Deprecatef(fail bool, format string, v ...any)
|
||||
Trace(s logg.StringFunc)
|
||||
}
|
||||
|
||||
type logAdapter struct {
|
||||
@@ -183,12 +210,17 @@ type logAdapter struct {
|
||||
out io.Writer
|
||||
level logg.Level
|
||||
logger logg.Logger
|
||||
tracel logg.LevelLogger
|
||||
debugl logg.LevelLogger
|
||||
infol logg.LevelLogger
|
||||
warnl logg.LevelLogger
|
||||
errorl logg.LevelLogger
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debug() logg.LevelLogger {
|
||||
return l.debugl
|
||||
}
|
||||
|
||||
func (l *logAdapter) Debugf(format string, v ...any) {
|
||||
l.debugl.Logf(format, v...)
|
||||
}
|
||||
@@ -290,10 +322,24 @@ func (l *logAdapter) Errors() string {
|
||||
return l.errors.String()
|
||||
}
|
||||
|
||||
func (l *logAdapter) Errorsf(id, format string, v ...any) {
|
||||
func (l *logAdapter) Erroridf(id, format string, v ...any) {
|
||||
format += l.idfInfoStatement("error", id, format)
|
||||
l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Warnidf(id, format string, v ...any) {
|
||||
format += l.idfInfoStatement("warning", id, format)
|
||||
l.warnl.WithField(FieldNameStatementID, id).Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *logAdapter) idfInfoStatement(what, id, format string) string {
|
||||
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
|
||||
}
|
||||
|
||||
func (l *logAdapter) Trace(s logg.StringFunc) {
|
||||
l.tracel.Log(s)
|
||||
}
|
||||
|
||||
func (l *logAdapter) sprint(v ...any) string {
|
||||
return strings.TrimRight(fmt.Sprintln(v...), "\n")
|
||||
}
|
||||
@@ -315,3 +361,19 @@ func (w logWriter) Write(p []byte) (n int, err error) {
|
||||
w.l.Log(logg.String(string(p)))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TimeTrackf(l logg.LevelLogger, start time.Time, fields logg.Fields, format string, a ...any) {
|
||||
elapsed := time.Since(start)
|
||||
if fields != nil {
|
||||
l = l.WithFields(fields)
|
||||
}
|
||||
l.WithField("duration", elapsed).Logf(format, a...)
|
||||
}
|
||||
|
||||
func TimeTrackfn(fn func() (logg.LevelLogger, error)) error {
|
||||
start := time.Now()
|
||||
l, err := fn()
|
||||
elapsed := time.Since(start)
|
||||
l.WithField("duration", elapsed).Logf("")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -29,10 +29,10 @@ func TestLogDistinct(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
Distinct: true,
|
||||
StoreErrors: true,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
StoreErrors: true,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
@@ -85,7 +85,6 @@ func TestOptionStoreErrors(t *testing.T) {
|
||||
|
||||
c.Assert(sb.String(), qt.Contains, "error 1")
|
||||
c.Assert(sb.String(), qt.Contains, "ERROR")
|
||||
|
||||
}
|
||||
|
||||
func TestLogCount(t *testing.T) {
|
||||
@@ -110,7 +109,7 @@ func TestSuppressStatements(t *testing.T) {
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
SuppresssStatements: map[string]bool{
|
||||
SuppressStatements: map[string]bool{
|
||||
"error-1": true,
|
||||
},
|
||||
}
|
||||
@@ -124,17 +123,16 @@ func TestSuppressStatements(t *testing.T) {
|
||||
c.Assert(errorsStr, qt.Not(qt.Contains), "error 1")
|
||||
c.Assert(errorsStr, qt.Contains, "error 2")
|
||||
c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 1)
|
||||
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := loggers.Options{
|
||||
StoreErrors: true,
|
||||
Distinct: true,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
StoreErrors: true,
|
||||
DistinctLevel: logg.LevelWarn,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
l := loggers.New(opts)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
// Some functions in this file (see comments) is based on the Go source code,
|
||||
// copyright The Go Authors and governed by a BSD-style license.
|
||||
//
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/bep/logg"
|
||||
)
|
||||
|
||||
func InitGlobalLogger(panicOnWarnings bool) {
|
||||
func InitGlobalLogger(level logg.Level, panicOnWarnings bool) {
|
||||
logMu.Lock()
|
||||
defer logMu.Unlock()
|
||||
var logHookLast func(e *logg.Entry) error
|
||||
@@ -31,8 +31,9 @@ func InitGlobalLogger(panicOnWarnings bool) {
|
||||
|
||||
log = New(
|
||||
Options{
|
||||
Distinct: true,
|
||||
HandlerPost: logHookLast,
|
||||
Level: level,
|
||||
DistinctLevel: logg.LevelInfo,
|
||||
HandlerPost: logHookLast,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -49,5 +50,5 @@ func Log() Logger {
|
||||
var log Logger
|
||||
|
||||
func init() {
|
||||
InitGlobalLogger(false)
|
||||
InitGlobalLogger(logg.LevelWarn, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package maps
|
||||
|
||||
import "sync"
|
||||
|
||||
// Cache is a simple thread safe cache backed by a map.
|
||||
type Cache[K comparable, T any] struct {
|
||||
m map[K]T
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCache creates a new Cache.
|
||||
func NewCache[K comparable, T any]() *Cache[K, T] {
|
||||
return &Cache[K, T]{m: make(map[K]T)}
|
||||
}
|
||||
|
||||
// Delete deletes the given key from the cache.
|
||||
func (c *Cache[K, T]) Get(key K) (T, bool) {
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
|
||||
func (c *Cache[K, T]) GetOrCreate(key K, create func() T) T {
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
if found {
|
||||
return v
|
||||
}
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
v, found = c.m[key]
|
||||
if found {
|
||||
return v
|
||||
}
|
||||
v = create()
|
||||
c.m[key] = v
|
||||
return v
|
||||
}
|
||||
|
||||
// Set sets the given key to the given value.
|
||||
func (c *Cache[K, T]) Set(key K, value T) {
|
||||
c.Lock()
|
||||
c.m[key] = value
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
// SliceCache is a simple thread safe cache backed by a map.
|
||||
type SliceCache[T any] struct {
|
||||
m map[string][]T
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewSliceCache[T any]() *SliceCache[T] {
|
||||
return &SliceCache[T]{m: make(map[string][]T)}
|
||||
}
|
||||
|
||||
func (c *SliceCache[T]) Get(key string) ([]T, bool) {
|
||||
c.RLock()
|
||||
v, found := c.m[key]
|
||||
c.RUnlock()
|
||||
return v, found
|
||||
}
|
||||
|
||||
func (c *SliceCache[T]) Append(key string, values ...T) {
|
||||
c.Lock()
|
||||
c.m[key] = append(c.m[key], values...)
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *SliceCache[T]) Reset() {
|
||||
c.Lock()
|
||||
c.m = make(map[string][]T)
|
||||
c.Unlock()
|
||||
}
|
||||
+8
-9
@@ -29,7 +29,7 @@ func ToStringMapE(in any) (map[string]any, error) {
|
||||
case Params:
|
||||
return vv, nil
|
||||
case map[string]string:
|
||||
var m = map[string]any{}
|
||||
m := map[string]any{}
|
||||
for k, v := range vv {
|
||||
m[k] = v
|
||||
}
|
||||
@@ -192,21 +192,20 @@ func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) {
|
||||
for key, val := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, key)
|
||||
switch val.(type) {
|
||||
for k, v := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, k)
|
||||
switch vv := v.(type) {
|
||||
case map[any]any:
|
||||
val = cast.ToStringMap(val)
|
||||
r.renamePath(keyPath, val.(map[string]any))
|
||||
r.renamePath(keyPath, cast.ToStringMap(vv))
|
||||
case map[string]any:
|
||||
r.renamePath(keyPath, val.(map[string]any))
|
||||
r.renamePath(keyPath, vv)
|
||||
}
|
||||
|
||||
newKey := r.getNewKey(keyPath)
|
||||
|
||||
if newKey != "" {
|
||||
delete(m, key)
|
||||
m[newKey] = val
|
||||
delete(m, k)
|
||||
m[newKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,5 +192,4 @@ func TestLookupEqualFold(t *testing.T) {
|
||||
v, found = LookupEqualFold(m2, "b")
|
||||
c.Assert(found, qt.IsTrue)
|
||||
c.Assert(v, qt.Equals, "bv")
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func SetParams(dst, src Params) {
|
||||
|
||||
// IsZero returns true if p is considered empty.
|
||||
func (p Params) IsZero() bool {
|
||||
if p == nil || len(p) == 0 {
|
||||
if len(p) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ func (p Params) IsZero() bool {
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given.
|
||||
@@ -93,7 +92,7 @@ func MergeParams(dst, src Params) {
|
||||
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
|
||||
ns, found := p.GetMergeStrategy()
|
||||
|
||||
var ms = ns
|
||||
ms := ns
|
||||
if !found && ps != "" {
|
||||
ms = ps
|
||||
}
|
||||
@@ -248,7 +247,7 @@ const (
|
||||
// CleanConfigStringMapString removes any processing instructions from m,
|
||||
// m will never be modified.
|
||||
func CleanConfigStringMapString(m map[string]string) map[string]string {
|
||||
if m == nil || len(m) == 0 {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
@@ -267,7 +266,7 @@ func CleanConfigStringMapString(m map[string]string) map[string]string {
|
||||
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
|
||||
// map[string]any.
|
||||
func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
if m == nil || len(m) == 0 {
|
||||
if len(m) == 0 {
|
||||
return m
|
||||
}
|
||||
if _, found := m[MergeStrategyKey]; !found {
|
||||
@@ -291,7 +290,6 @@ func CleanConfigStringMap(m map[string]any) map[string]any {
|
||||
|
||||
}
|
||||
return m2
|
||||
|
||||
}
|
||||
|
||||
func toMergeStrategy(v any) ParamsMergeStrategy {
|
||||
|
||||
@@ -154,7 +154,6 @@ func TestParamsSetAndMerge(t *testing.T) {
|
||||
"a": "av",
|
||||
"c": "cv",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestParamsIsZero(t *testing.T) {
|
||||
|
||||
@@ -32,8 +32,9 @@ func TestPara(t *testing.T) {
|
||||
t.Skipf("skip para test, CPU count is %d", runtime.NumCPU())
|
||||
}
|
||||
|
||||
if !htesting.IsCI() {
|
||||
t.Skip("skip para test when not running on CI")
|
||||
// TODO(bep)
|
||||
if htesting.IsCI() {
|
||||
t.Skip("skip para test when running on CI")
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
+175
-52
@@ -16,14 +16,18 @@ package paths
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// FilePathSeparator as defined by os.Separator.
|
||||
const FilePathSeparator = string(filepath.Separator)
|
||||
const (
|
||||
FilePathSeparator = string(filepath.Separator)
|
||||
slash = "/"
|
||||
)
|
||||
|
||||
// filepathPathBridge is a bridge for common functionality in filepath vs path
|
||||
type filepathPathBridge interface {
|
||||
@@ -72,6 +76,30 @@ func AbsPathify(workingDir, inPath string) string {
|
||||
return filepath.Join(workingDir, inPath)
|
||||
}
|
||||
|
||||
// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddTrailingSlash(path string) string {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// AddLeadingSlash adds a leading Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddLeadingSlash(path string) string {
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// AddTrailingAndLeadingSlash adds a leading and trailing Unix styled slash (/) if not already
|
||||
// there.
|
||||
func AddLeadingAndTrailingSlash(path string) string {
|
||||
return AddTrailingSlash(AddLeadingSlash(path))
|
||||
}
|
||||
|
||||
// MakeTitle converts the path given to a suitable title, trimming whitespace
|
||||
// and replacing hyphens with whitespace.
|
||||
func MakeTitle(inpath string) string {
|
||||
@@ -94,43 +122,6 @@ func makePathRelative(inPath string, possibleDirectories ...string) (string, err
|
||||
return inPath, errors.New("can't extract relative path, unknown prefix")
|
||||
}
|
||||
|
||||
// Should be good enough for Hugo.
|
||||
var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
|
||||
|
||||
// GetDottedRelativePath expects a relative path starting after the content directory.
|
||||
// It returns a relative path with dots ("..") navigating up the path structure.
|
||||
func GetDottedRelativePath(inPath string) string {
|
||||
inPath = path.Clean(filepath.ToSlash(inPath))
|
||||
|
||||
if inPath == "." {
|
||||
return "./"
|
||||
}
|
||||
|
||||
if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, "/") {
|
||||
inPath += "/"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(inPath, "/") {
|
||||
inPath = "/" + inPath
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(inPath)
|
||||
|
||||
sectionCount := strings.Count(dir, "/")
|
||||
|
||||
if sectionCount == 0 || dir == "/" {
|
||||
return "./"
|
||||
}
|
||||
|
||||
var dottedPath string
|
||||
|
||||
for i := 1; i < sectionCount; i++ {
|
||||
dottedPath += "../"
|
||||
}
|
||||
|
||||
return dottedPath
|
||||
}
|
||||
|
||||
// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md".
|
||||
func ExtNoDelimiter(in string) string {
|
||||
return strings.TrimPrefix(Ext(in), ".")
|
||||
@@ -167,12 +158,6 @@ func Filename(in string) (name string) {
|
||||
return
|
||||
}
|
||||
|
||||
// PathNoExt takes a path, strips out the extension,
|
||||
// and returns the name of the file.
|
||||
func PathNoExt(in string) string {
|
||||
return strings.TrimSuffix(in, path.Ext(in))
|
||||
}
|
||||
|
||||
// FileAndExt returns the filename and any extension of a file path as
|
||||
// two separate strings.
|
||||
//
|
||||
@@ -252,16 +237,125 @@ func prettifyPath(in string, b filepathPathBridge) string {
|
||||
return b.Join(b.Dir(in), name, "index"+ext)
|
||||
}
|
||||
|
||||
type NamedSlice struct {
|
||||
Name string
|
||||
Slice []string
|
||||
// CommonDir returns the common directory of the given paths.
|
||||
func CommonDir(path1, path2 string) string {
|
||||
if path1 == "" || path2 == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
p1 := strings.Split(path1, "/")
|
||||
p2 := strings.Split(path2, "/")
|
||||
|
||||
var common []string
|
||||
|
||||
for i := 0; i < len(p1) && i < len(p2); i++ {
|
||||
if p1[i] == p2[i] {
|
||||
common = append(common, p1[i])
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(common, "/")
|
||||
}
|
||||
|
||||
func (n NamedSlice) String() string {
|
||||
if len(n.Slice) == 0 {
|
||||
return n.Name
|
||||
// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
|
||||
// a predefined set of special Unicode characters.
|
||||
//
|
||||
// Spaces will be replaced with a single hyphen.
|
||||
//
|
||||
// This function is the core function used to normalize paths in Hugo.
|
||||
//
|
||||
// Note that this is the first common step for URL/path sanitation,
|
||||
// the final URL/path may end up looking differently if the user has stricter rules defined (e.g. removePathAccents=true).
|
||||
func Sanitize(s string) string {
|
||||
var willChange bool
|
||||
for i, r := range s {
|
||||
willChange = !isAllowedPathCharacter(s, i, r)
|
||||
if willChange {
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
|
||||
|
||||
if !willChange {
|
||||
// Prevent allocation when nothing changes.
|
||||
return s
|
||||
}
|
||||
|
||||
target := make([]rune, 0, len(s))
|
||||
var (
|
||||
prependHyphen bool
|
||||
wasHyphen bool
|
||||
)
|
||||
|
||||
for i, r := range s {
|
||||
isAllowed := isAllowedPathCharacter(s, i, r)
|
||||
|
||||
if isAllowed {
|
||||
// track explicit hyphen in input; no need to add a new hyphen if
|
||||
// we just saw one.
|
||||
wasHyphen = r == '-'
|
||||
|
||||
if prependHyphen {
|
||||
// if currently have a hyphen, don't prepend an extra one
|
||||
if !wasHyphen {
|
||||
target = append(target, '-')
|
||||
}
|
||||
prependHyphen = false
|
||||
}
|
||||
target = append(target, r)
|
||||
} else if len(target) > 0 && !wasHyphen && unicode.IsSpace(r) {
|
||||
prependHyphen = true
|
||||
}
|
||||
}
|
||||
|
||||
return string(target)
|
||||
}
|
||||
|
||||
func isAllowedPathCharacter(s string, i int, r rune) bool {
|
||||
if r == ' ' {
|
||||
return false
|
||||
}
|
||||
// Check for the most likely first (faster).
|
||||
isAllowed := unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
isAllowed = isAllowed || r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' || r == '-' || r == '@'
|
||||
isAllowed = isAllowed || unicode.IsMark(r)
|
||||
isAllowed = isAllowed || (r == '%' && i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2]))
|
||||
return isAllowed
|
||||
}
|
||||
|
||||
// From https://golang.org/src/net/url/url.go
|
||||
func ishex(c byte) bool {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return true
|
||||
case 'a' <= c && c <= 'f':
|
||||
return true
|
||||
case 'A' <= c && c <= 'F':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var slashFunc = func(r rune) bool {
|
||||
return r == '/'
|
||||
}
|
||||
|
||||
// Dir behaves like path.Dir without the path.Clean step.
|
||||
//
|
||||
// The returned path ends in a slash only if it is the root "/".
|
||||
func Dir(s string) string {
|
||||
dir, _ := path.Split(s)
|
||||
if len(dir) > 1 && dir[len(dir)-1] == '/' {
|
||||
return dir[:len(dir)-1]
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// FieldsSlash cuts s into fields separated with '/'.
|
||||
func FieldsSlash(s string) []string {
|
||||
f := strings.FieldsFunc(s, slashFunc)
|
||||
return f
|
||||
}
|
||||
|
||||
// DirFile holds the result from path.Split.
|
||||
@@ -274,3 +368,32 @@ type DirFile struct {
|
||||
func (df DirFile) String() string {
|
||||
return fmt.Sprintf("%s|%s", df.Dir, df.File)
|
||||
}
|
||||
|
||||
// PathEscape escapes unicode letters in pth.
|
||||
// Use URLEscape to escape full URLs including scheme, query etc.
|
||||
// This is slightly faster for the common case.
|
||||
// Note, there is a url.PathEscape function, but that also
|
||||
// escapes /.
|
||||
func PathEscape(pth string) string {
|
||||
u, err := url.Parse(pth)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u.EscapedPath()
|
||||
}
|
||||
|
||||
// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
|
||||
func ToSlashTrimLeading(s string) string {
|
||||
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
// ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer.
|
||||
func ToSlashTrimTrailing(s string) string {
|
||||
return strings.TrimSuffix(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
// ToSlashPreserveLeading converts the path given to a forward slash separated path
|
||||
// and preserves the leading slash if present trimming any trailing slash.
|
||||
func ToSlashPreserveLeading(s string) string {
|
||||
return "/" + strings.Trim(filepath.ToSlash(s), "/")
|
||||
}
|
||||
|
||||
+75
-39
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -75,44 +75,6 @@ func TestMakePathRelative(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDottedRelativePath(t *testing.T) {
|
||||
// on Windows this will receive both kinds, both country and western ...
|
||||
for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} {
|
||||
doTestGetDottedRelativePath(f, t)
|
||||
}
|
||||
}
|
||||
|
||||
func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
}
|
||||
data := []test{
|
||||
{"", "./"},
|
||||
{urlFixer("/"), "./"},
|
||||
{urlFixer("post"), "../"},
|
||||
{urlFixer("/post"), "../"},
|
||||
{urlFixer("post/"), "../"},
|
||||
{urlFixer("tags/foo.html"), "../"},
|
||||
{urlFixer("/tags/foo.html"), "../"},
|
||||
{urlFixer("/post/"), "../"},
|
||||
{urlFixer("////post/////"), "../"},
|
||||
{urlFixer("/foo/bar/index.html"), "../../"},
|
||||
{urlFixer("/foo/bar/foo/"), "../../../"},
|
||||
{urlFixer("/foo/bar/foo"), "../../../"},
|
||||
{urlFixer("foo/bar/foo/"), "../../../"},
|
||||
{urlFixer("foo/bar/foo/bar"), "../../../../"},
|
||||
{"404.html", "./"},
|
||||
{"404.xml", "./"},
|
||||
{"/404.html", "./"},
|
||||
}
|
||||
for i, d := range data {
|
||||
output := GetDottedRelativePath(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeTitle(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
@@ -226,3 +188,77 @@ func TestFileAndExt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitize(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{" Foo bar ", "Foo-bar"},
|
||||
{"Foo.Bar/foo_Bar-Foo", "Foo.Bar/foo_Bar-Foo"},
|
||||
{"fOO,bar:foobAR", "fOObarfoobAR"},
|
||||
{"FOo/BaR.html", "FOo/BaR.html"},
|
||||
{"FOo/Ba---R.html", "FOo/Ba---R.html"}, /// See #10104
|
||||
{"FOo/Ba R.html", "FOo/Ba-R.html"},
|
||||
{"трям/трям", "трям/трям"},
|
||||
{"은행", "은행"},
|
||||
{"Банковский кассир", "Банковский-кассир"},
|
||||
// Issue #1488
|
||||
{"संस्कृत", "संस्कृत"},
|
||||
{"a%C3%B1ame", "a%C3%B1ame"}, // Issue #1292
|
||||
{"this+is+a+test", "this+is+a+test"}, // Issue #1290
|
||||
{"~foo", "~foo"}, // Issue #2177
|
||||
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
c.Assert(Sanitize(test.input), qt.Equals, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitize(b *testing.B) {
|
||||
const (
|
||||
allAlowedPath = "foo/bar"
|
||||
spacePath = "foo bar"
|
||||
)
|
||||
|
||||
// This should not allocate any memory.
|
||||
b.Run("All allowed", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := Sanitize(allAlowedPath)
|
||||
if got != allAlowedPath {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// This will allocate some memory.
|
||||
b.Run("Spaces", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := Sanitize(spacePath)
|
||||
if got != "foo-bar" {
|
||||
b.Fatal(got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDir(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Assert(Dir("/a/b/c/d"), qt.Equals, "/a/b/c")
|
||||
c.Assert(Dir("/a"), qt.Equals, "/")
|
||||
c.Assert(Dir("/"), qt.Equals, "/")
|
||||
c.Assert(Dir(""), qt.Equals, "")
|
||||
}
|
||||
|
||||
func TestFieldsSlash(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(FieldsSlash("a/b/c"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("/a/b/c"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("/a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(FieldsSlash("/"), qt.DeepEquals, []string{})
|
||||
c.Assert(FieldsSlash(""), qt.DeepEquals, []string{})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
)
|
||||
|
||||
var defaultPathParser PathParser
|
||||
|
||||
// PathParser parses a path into a Path.
|
||||
type PathParser struct {
|
||||
// Maps the language code to its index in the languages/sites slice.
|
||||
LanguageIndex map[string]int
|
||||
|
||||
// Reports whether the given language is disabled.
|
||||
IsLangDisabled func(string) bool
|
||||
}
|
||||
|
||||
// Parse parses component c with path s into Path using the default path parser.
|
||||
func Parse(c, s string) *Path {
|
||||
return defaultPathParser.Parse(c, s)
|
||||
}
|
||||
|
||||
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
|
||||
func NormalizePathStringBasic(s string) string {
|
||||
// All lower case.
|
||||
s = strings.ToLower(s)
|
||||
|
||||
// Replace spaces with hyphens.
|
||||
s = strings.ReplaceAll(s, " ", "-")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseIdentity parses component c with path s into a StringIdentity.
|
||||
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
|
||||
p := pp.parsePooled(c, s)
|
||||
defer putPath(p)
|
||||
return identity.StringIdentity(p.IdentifierBase())
|
||||
}
|
||||
|
||||
// ParseBaseAndBaseNameNoIdentifier parses component c with path s into a base and a base name without any identifier.
|
||||
func (pp *PathParser) ParseBaseAndBaseNameNoIdentifier(c, s string) (string, string) {
|
||||
p := pp.parsePooled(c, s)
|
||||
defer putPath(p)
|
||||
return p.Base(), p.BaseNameNoIdentifier()
|
||||
}
|
||||
|
||||
func (pp *PathParser) parsePooled(c, s string) *Path {
|
||||
s = NormalizePathStringBasic(s)
|
||||
p := getPath()
|
||||
p.component = c
|
||||
p, err := pp.doParse(c, s, p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Parse parses component c with path s into Path using Hugo's content path rules.
|
||||
func (pp *PathParser) Parse(c, s string) *Path {
|
||||
p, err := pp.parse(c, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (pp *PathParser) newPath(component string) *Path {
|
||||
return &Path{
|
||||
component: component,
|
||||
posContainerLow: -1,
|
||||
posContainerHigh: -1,
|
||||
posSectionHigh: -1,
|
||||
posIdentifierLanguage: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *PathParser) parse(component, s string) (*Path, error) {
|
||||
ss := NormalizePathStringBasic(s)
|
||||
|
||||
p, err := pp.doParse(component, ss, pp.newPath(component))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s != ss {
|
||||
var err error
|
||||
// Preserve the original case for titles etc.
|
||||
p.unnormalized, err = pp.doParse(component, s, pp.newPath(component))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
p.unnormalized = p
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
hasLang := pp.LanguageIndex != nil
|
||||
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
s = path.Clean(filepath.ToSlash(s))
|
||||
if s == "." {
|
||||
s = ""
|
||||
}
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
s = "/"
|
||||
}
|
||||
|
||||
// Leading slash, no trailing slash.
|
||||
if !strings.HasPrefix(s, "/") {
|
||||
s = "/" + s
|
||||
}
|
||||
|
||||
if s != "/" && s[len(s)-1] == '/' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
|
||||
p.s = s
|
||||
slashCount := 0
|
||||
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
c := s[i]
|
||||
|
||||
switch c {
|
||||
case '.':
|
||||
if p.posContainerHigh == -1 {
|
||||
var high int
|
||||
if len(p.identifiers) > 0 {
|
||||
high = p.identifiers[len(p.identifiers)-1].Low - 1
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh{Low: i + 1, High: high}
|
||||
if len(p.identifiers) == 0 {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
} else if len(p.identifiers) == 1 {
|
||||
// Check for a valid language.
|
||||
s := p.s[id.Low:id.High]
|
||||
|
||||
if hasLang {
|
||||
var disabled bool
|
||||
_, langFound := pp.LanguageIndex[s]
|
||||
if !langFound {
|
||||
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
|
||||
if disabled {
|
||||
p.disabled = true
|
||||
langFound = true
|
||||
}
|
||||
}
|
||||
if langFound {
|
||||
p.posIdentifierLanguage = 1
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case '/':
|
||||
slashCount++
|
||||
if p.posContainerHigh == -1 {
|
||||
p.posContainerHigh = i + 1
|
||||
} else if p.posContainerLow == -1 {
|
||||
p.posContainerLow = i + 1
|
||||
}
|
||||
if i > 0 {
|
||||
p.posSectionHigh = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
|
||||
isContent := isContentComponent && files.IsContentExt(p.Ext())
|
||||
|
||||
if isContent {
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
b := p.s[p.posContainerHigh : id.Low-1]
|
||||
switch b {
|
||||
case "index":
|
||||
p.bundleType = PathTypeLeaf
|
||||
case "_index":
|
||||
p.bundleType = PathTypeBranch
|
||||
default:
|
||||
p.bundleType = PathTypeContentSingle
|
||||
}
|
||||
|
||||
if slashCount == 2 && p.IsLeafBundle() {
|
||||
p.posSectionHigh = 0
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func ModifyPathBundleTypeResource(p *Path) {
|
||||
if p.IsContent() {
|
||||
p.bundleType = PathTypeContentResource
|
||||
} else {
|
||||
p.bundleType = PathTypeFile
|
||||
}
|
||||
}
|
||||
|
||||
type PathType int
|
||||
|
||||
const (
|
||||
// A generic resource, e.g. a JSON file.
|
||||
PathTypeFile PathType = iota
|
||||
|
||||
// All below are content files.
|
||||
// A resource of a content type with front matter.
|
||||
PathTypeContentResource
|
||||
|
||||
// E.g. /blog/my-post.md
|
||||
PathTypeContentSingle
|
||||
|
||||
// All below are bundled content files.
|
||||
|
||||
// Leaf bundles, e.g. /blog/my-post/index.md
|
||||
PathTypeLeaf
|
||||
|
||||
// Branch bundles, e.g. /blog/_index.md
|
||||
PathTypeBranch
|
||||
)
|
||||
|
||||
type Path struct {
|
||||
// Note: Any additions to this struct should also be added to the pathPool.
|
||||
s string
|
||||
|
||||
posContainerLow int
|
||||
posContainerHigh int
|
||||
posSectionHigh int
|
||||
|
||||
component string
|
||||
bundleType PathType
|
||||
|
||||
identifiers []types.LowHigh
|
||||
|
||||
posIdentifierLanguage int
|
||||
disabled bool
|
||||
|
||||
trimLeadingSlash bool
|
||||
|
||||
unnormalized *Path
|
||||
}
|
||||
|
||||
var pathPool = &sync.Pool{
|
||||
New: func() any {
|
||||
p := &Path{}
|
||||
p.reset()
|
||||
return p
|
||||
},
|
||||
}
|
||||
|
||||
func getPath() *Path {
|
||||
return pathPool.Get().(*Path)
|
||||
}
|
||||
|
||||
func putPath(p *Path) {
|
||||
p.reset()
|
||||
pathPool.Put(p)
|
||||
}
|
||||
|
||||
func (p *Path) reset() {
|
||||
p.s = ""
|
||||
p.posContainerLow = -1
|
||||
p.posContainerHigh = -1
|
||||
p.posSectionHigh = -1
|
||||
p.component = ""
|
||||
p.bundleType = 0
|
||||
p.identifiers = p.identifiers[:0]
|
||||
p.posIdentifierLanguage = -1
|
||||
p.disabled = false
|
||||
p.trimLeadingSlash = false
|
||||
p.unnormalized = nil
|
||||
}
|
||||
|
||||
// TrimLeadingSlash returns a copy of the Path with the leading slash removed.
|
||||
func (p Path) TrimLeadingSlash() *Path {
|
||||
p.trimLeadingSlash = true
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Path) norm(s string) string {
|
||||
if p.trimLeadingSlash {
|
||||
s = strings.TrimPrefix(s, "/")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IdentifierBase satisfies identity.Identity.
|
||||
func (p *Path) IdentifierBase() string {
|
||||
return p.Base()
|
||||
}
|
||||
|
||||
// Component returns the component for this path (e.g. "content").
|
||||
func (p *Path) Component() string {
|
||||
return p.component
|
||||
}
|
||||
|
||||
// Container returns the base name of the container directory for this path.
|
||||
func (p *Path) Container() string {
|
||||
if p.posContainerLow == -1 {
|
||||
return ""
|
||||
}
|
||||
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
|
||||
}
|
||||
|
||||
// ContainerDir returns the container directory for this path.
|
||||
// For content bundles this will be the parent directory.
|
||||
func (p *Path) ContainerDir() string {
|
||||
if p.posContainerLow == -1 || !p.IsBundle() {
|
||||
return p.Dir()
|
||||
}
|
||||
return p.norm(p.s[:p.posContainerLow-1])
|
||||
}
|
||||
|
||||
// Section returns the first path element (section).
|
||||
func (p *Path) Section() string {
|
||||
if p.posSectionHigh <= 0 {
|
||||
return ""
|
||||
}
|
||||
return p.norm(p.s[1:p.posSectionHigh])
|
||||
}
|
||||
|
||||
// IsContent returns true if the path is a content file (e.g. mypost.md).
|
||||
// Note that this will also return true for content files in a bundle.
|
||||
func (p *Path) IsContent() bool {
|
||||
return p.BundleType() >= PathTypeContentResource
|
||||
}
|
||||
|
||||
// isContentPage returns true if the path is a content file (e.g. mypost.md),
|
||||
// but nof if inside a leaf bundle.
|
||||
func (p *Path) isContentPage() bool {
|
||||
return p.BundleType() >= PathTypeContentSingle
|
||||
}
|
||||
|
||||
// Name returns the last element of path.
|
||||
func (p *Path) Name() string {
|
||||
if p.posContainerHigh > 0 {
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
return p.s
|
||||
}
|
||||
|
||||
// Name returns the last element of path without any extension.
|
||||
func (p *Path) NameNoExt() string {
|
||||
if i := p.identifierIndex(0); i != -1 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[i].Low-1]
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Name returns the last element of path without any language identifier.
|
||||
func (p *Path) NameNoLang() string {
|
||||
i := p.identifierIndex(p.posIdentifierLanguage)
|
||||
if i == -1 {
|
||||
return p.Name()
|
||||
}
|
||||
|
||||
return p.s[p.posContainerHigh:p.identifiers[i].Low-1] + p.s[p.identifiers[i].High:]
|
||||
}
|
||||
|
||||
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
|
||||
// For bundles this will be the containing directory's name, e.g. "blog".
|
||||
func (p *Path) BaseNameNoIdentifier() string {
|
||||
if p.IsBundle() {
|
||||
return p.Container()
|
||||
}
|
||||
return p.NameNoIdentifier()
|
||||
}
|
||||
|
||||
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
|
||||
func (p *Path) NameNoIdentifier() string {
|
||||
if len(p.identifiers) > 0 {
|
||||
return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
|
||||
}
|
||||
return p.s[p.posContainerHigh:]
|
||||
}
|
||||
|
||||
// Dir returns all but the last element of path, typically the path's directory.
|
||||
func (p *Path) Dir() (d string) {
|
||||
if p.posContainerHigh > 0 {
|
||||
d = p.s[:p.posContainerHigh-1]
|
||||
}
|
||||
if d == "" {
|
||||
d = "/"
|
||||
}
|
||||
d = p.norm(d)
|
||||
return
|
||||
}
|
||||
|
||||
// Path returns the full path.
|
||||
func (p *Path) Path() (d string) {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
// Unnormalized returns the Path with the original case preserved.
|
||||
func (p *Path) Unnormalized() *Path {
|
||||
return p.unnormalized
|
||||
}
|
||||
|
||||
// PathNoLang returns the Path but with any language identifier removed.
|
||||
func (p *Path) PathNoLang() string {
|
||||
return p.base(true, false)
|
||||
}
|
||||
|
||||
// PathNoIdentifier returns the Path but with any identifier (ext, lang) removed.
|
||||
func (p *Path) PathNoIdentifier() string {
|
||||
return p.base(false, false)
|
||||
}
|
||||
|
||||
// PathRel returns the path relative to the given owner.
|
||||
func (p *Path) PathRel(owner *Path) string {
|
||||
ob := owner.Base()
|
||||
if !strings.HasSuffix(ob, "/") {
|
||||
ob += "/"
|
||||
}
|
||||
return strings.TrimPrefix(p.Path(), ob)
|
||||
}
|
||||
|
||||
// BaseRel returns the base path relative to the given owner.
|
||||
func (p *Path) BaseRel(owner *Path) string {
|
||||
ob := owner.Base()
|
||||
if ob == "/" {
|
||||
ob = ""
|
||||
}
|
||||
return p.Base()[len(ob)+1:]
|
||||
}
|
||||
|
||||
// For content files, Base returns the path without any identifiers (extension, language code etc.).
|
||||
// Any 'index' as the last path element is ignored.
|
||||
//
|
||||
// For other files (Resources), any extension is kept.
|
||||
func (p *Path) Base() string {
|
||||
return p.base(!p.isContentPage(), p.IsBundle())
|
||||
}
|
||||
|
||||
// BaseNoLeadingSlash returns the base path without the leading slash.
|
||||
func (p *Path) BaseNoLeadingSlash() string {
|
||||
return p.Base()[1:]
|
||||
}
|
||||
|
||||
func (p *Path) base(preserveExt, isBundle bool) string {
|
||||
if len(p.identifiers) == 0 {
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
if preserveExt && len(p.identifiers) == 1 {
|
||||
// Preserve extension.
|
||||
return p.norm(p.s)
|
||||
}
|
||||
|
||||
id := p.identifiers[len(p.identifiers)-1]
|
||||
high := id.Low - 1
|
||||
|
||||
if isBundle {
|
||||
high = p.posContainerHigh - 1
|
||||
}
|
||||
|
||||
if high == 0 {
|
||||
high++
|
||||
}
|
||||
|
||||
if !preserveExt {
|
||||
return p.norm(p.s[:high])
|
||||
}
|
||||
|
||||
// For txt files etc. we want to preserve the extension.
|
||||
id = p.identifiers[0]
|
||||
|
||||
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
|
||||
}
|
||||
|
||||
func (p *Path) Ext() string {
|
||||
return p.identifierAsString(0)
|
||||
}
|
||||
|
||||
func (p *Path) Lang() string {
|
||||
return p.identifierAsString(1)
|
||||
}
|
||||
|
||||
func (p *Path) Identifier(i int) string {
|
||||
return p.identifierAsString(i)
|
||||
}
|
||||
|
||||
func (p *Path) Disabled() bool {
|
||||
return p.disabled
|
||||
}
|
||||
|
||||
func (p *Path) Identifiers() []string {
|
||||
ids := make([]string, len(p.identifiers))
|
||||
for i, id := range p.identifiers {
|
||||
ids[i] = p.s[id.Low:id.High]
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (p *Path) IsHTML() bool {
|
||||
return files.IsHTML(p.Ext())
|
||||
}
|
||||
|
||||
func (p *Path) BundleType() PathType {
|
||||
return p.bundleType
|
||||
}
|
||||
|
||||
func (p *Path) IsBundle() bool {
|
||||
return p.bundleType >= PathTypeLeaf
|
||||
}
|
||||
|
||||
func (p *Path) IsBranchBundle() bool {
|
||||
return p.bundleType == PathTypeBranch
|
||||
}
|
||||
|
||||
func (p *Path) IsLeafBundle() bool {
|
||||
return p.bundleType == PathTypeLeaf
|
||||
}
|
||||
|
||||
func (p Path) ForBundleType(t PathType) *Path {
|
||||
p.bundleType = t
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *Path) identifierAsString(i int) string {
|
||||
i = p.identifierIndex(i)
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
id := p.identifiers[i]
|
||||
return p.s[id.Low:id.High]
|
||||
}
|
||||
|
||||
func (p *Path) identifierIndex(i int) int {
|
||||
if i < 0 || i >= len(p.identifiers) {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// HasExt returns true if the Unix styled path has an extension.
|
||||
func HasExt(p string) bool {
|
||||
for i := len(p) - 1; i >= 0; i-- {
|
||||
if p[i] == '.' {
|
||||
return true
|
||||
}
|
||||
if p[i] == '/' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var testParser = &PathParser{
|
||||
LanguageIndex: map[string]int{
|
||||
"no": 0,
|
||||
"en": 1,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
assert func(c *qt.C, p *Path)
|
||||
}{
|
||||
{
|
||||
"Basic text file",
|
||||
"/a/b.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.txt")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.txt")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.IsContent(), qt.IsFalse)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, upper case",
|
||||
"/A/B.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.txt")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "b")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 1 space in dir",
|
||||
"/a b/c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a-b/c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 2 spaces in dir",
|
||||
"/a b/c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a--b/c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 1 space in filename",
|
||||
"/a/b c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b-c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, 2 spaces in filename",
|
||||
"/a/b c.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b--c.txt")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic text file, mixed case and spaces, unnormalized",
|
||||
"/a/Foo BAR.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
pp := p.Unnormalized()
|
||||
c.Assert(pp, qt.IsNotNil)
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "Foo BAR")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Basic Markdown file",
|
||||
"/a/b/c.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.Name(), qt.Equals, "c.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/c")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Content resource",
|
||||
"/a/b.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
|
||||
// Reclassify it as a content resource.
|
||||
ModifyPathBundleTypeResource(p)
|
||||
c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
|
||||
c.Assert(p.IsContent(), qt.IsTrue)
|
||||
c.Assert(p.Name(), qt.Equals, "b.md")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"No ext",
|
||||
"/a/b",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "b")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"No ext, trailing slash",
|
||||
"/a/b/",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b")
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Identifiers",
|
||||
"/a/b.a.b.no.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Home branch cundle",
|
||||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Path(), qt.Equals, "/_index.md")
|
||||
c.Assert(p.Container(), qt.Equals, "")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index content file in root",
|
||||
"/a/index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.Container(), qt.Equals, "a")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
c.Assert(p.Lang(), qt.Equals, "")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
|
||||
c.Assert(p.Section(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index content file with lang",
|
||||
"/a/b/index.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Dir(), qt.Equals, "/a/b")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsFalse)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsTrue)
|
||||
c.Assert(p.Lang(), qt.Equals, "no")
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "index.no")
|
||||
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
|
||||
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index branch content file",
|
||||
"/a/b/_index.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b")
|
||||
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
|
||||
c.Assert(p.Container(), qt.Equals, "b")
|
||||
c.Assert(p.ContainerDir(), qt.Equals, "/a")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"})
|
||||
c.Assert(p.IsBranchBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsBundle(), qt.IsTrue)
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.NameNoExt(), qt.Equals, "_index.no")
|
||||
c.Assert(p.NameNoLang(), qt.Equals, "_index.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index root no slash",
|
||||
"_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Name(), qt.Equals, "_index.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index root",
|
||||
"/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "md")
|
||||
c.Assert(p.Name(), qt.Equals, "_index.md")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index first",
|
||||
"/a/_index.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Section(), qt.Equals, "a")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Index text file",
|
||||
"/a/b/index.no.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/index.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
|
||||
c.Assert(p.IsLeafBundle(), qt.IsFalse)
|
||||
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Empty",
|
||||
"",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
c.Assert(p.Name(), qt.Equals, "")
|
||||
c.Assert(p.Path(), qt.Equals, "/")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Slash",
|
||||
"/",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/")
|
||||
c.Assert(p.Ext(), qt.Equals, "")
|
||||
c.Assert(p.Name(), qt.Equals, "")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Trim Leading Slash bundle",
|
||||
"foo/bar/index.no.md",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Path(), qt.Equals, "/foo/bar/index.no.md")
|
||||
pp := p.TrimLeadingSlash()
|
||||
c.Assert(pp.Path(), qt.Equals, "foo/bar/index.no.md")
|
||||
c.Assert(pp.PathNoLang(), qt.Equals, "foo/bar/index.md")
|
||||
c.Assert(pp.Base(), qt.Equals, "foo/bar")
|
||||
c.Assert(pp.Dir(), qt.Equals, "foo/bar")
|
||||
c.Assert(pp.ContainerDir(), qt.Equals, "foo")
|
||||
c.Assert(pp.Container(), qt.Equals, "bar")
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "bar")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Trim Leading Slash file",
|
||||
"foo/bar.txt",
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Path(), qt.Equals, "/foo/bar.txt")
|
||||
pp := p.TrimLeadingSlash()
|
||||
c.Assert(pp.Path(), qt.Equals, "foo/bar.txt")
|
||||
c.Assert(pp.PathNoLang(), qt.Equals, "foo/bar.txt")
|
||||
c.Assert(pp.Base(), qt.Equals, "foo/bar.txt")
|
||||
c.Assert(pp.Dir(), qt.Equals, "foo")
|
||||
c.Assert(pp.ContainerDir(), qt.Equals, "foo")
|
||||
c.Assert(pp.Container(), qt.Equals, "foo")
|
||||
c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "bar")
|
||||
},
|
||||
},
|
||||
{
|
||||
"File separator",
|
||||
filepath.FromSlash("/a/b/c.txt"),
|
||||
func(c *qt.C, p *Path) {
|
||||
c.Assert(p.Base(), qt.Equals, "/a/b/c.txt")
|
||||
c.Assert(p.Ext(), qt.Equals, "txt")
|
||||
c.Assert(p.Name(), qt.Equals, "c.txt")
|
||||
c.Assert(p.Path(), qt.Equals, "/a/b/c.txt")
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
c.Run(test.name, func(c *qt.C) {
|
||||
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasExt(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(HasExt("/a/b/c.txt"), qt.IsTrue)
|
||||
c.Assert(HasExt("/a/b.c/d.txt"), qt.IsTrue)
|
||||
c.Assert(HasExt("/a/b/c"), qt.IsFalse)
|
||||
c.Assert(HasExt("/a/b.c/d"), qt.IsFalse)
|
||||
}
|
||||
|
||||
func BenchmarkParseIdentity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package paths_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestRemovePathAccents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
removePathAccents = true
|
||||
-- content/διακριτικός.md --
|
||||
-- content/διακριτικός.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
List
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/διακριτικός/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/διακριτικος/index.html", "fr|Single")
|
||||
}
|
||||
|
||||
func TestDisablePathToLower(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
disablePathToLower = true
|
||||
-- content/MySection/MyPage.md --
|
||||
-- content/MySection/MyPage.fr.md --
|
||||
-- content/MySection/MyBundle/index.md --
|
||||
-- content/MySection/MyBundle/index.fr.md --
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Language.Lang }}|Single.
|
||||
-- layouts/_default/list.html --
|
||||
{{ .Language.Lang }}|List.
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/mysection/index.html", "en|List")
|
||||
b.AssertFileContent("public/en/mysection/mypage/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/MySection/index.html", "fr|List")
|
||||
b.AssertFileContent("public/fr/MySection/MyPage/index.html", "fr|Single")
|
||||
b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single")
|
||||
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
|
||||
|
||||
package paths
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[PathTypeFile-0]
|
||||
_ = x[PathTypeContentResource-1]
|
||||
_ = x[PathTypeContentSingle-2]
|
||||
_ = x[PathTypeLeaf-3]
|
||||
_ = x[PathTypeBranch-4]
|
||||
}
|
||||
|
||||
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
|
||||
|
||||
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
|
||||
|
||||
func (i PathType) String() string {
|
||||
if i < 0 || i >= PathType(len(_PathType_index)-1) {
|
||||
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
|
||||
}
|
||||
+24
-10
@@ -51,9 +51,10 @@ var pb pathBridge
|
||||
|
||||
// MakePermalink combines base URL with content path to create full URL paths.
|
||||
// Example
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
//
|
||||
// base: http://spf13.com/
|
||||
// path: post/how-i-blog
|
||||
// result: http://spf13.com/post/how-i-blog
|
||||
func MakePermalink(host, plink string) *url.URL {
|
||||
base, err := url.Parse(host)
|
||||
if err != nil {
|
||||
@@ -70,6 +71,8 @@ func MakePermalink(host, plink string) *url.URL {
|
||||
}
|
||||
|
||||
base.Path = path.Join(base.Path, p.Path)
|
||||
base.Fragment = p.Fragment
|
||||
base.RawQuery = p.RawQuery
|
||||
|
||||
// path.Join will strip off the last /, so put it back if it was there.
|
||||
hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/")
|
||||
@@ -117,17 +120,19 @@ func PrettifyURL(in string) string {
|
||||
|
||||
// PrettifyURLPath takes a URL path to a content and converts it
|
||||
// to enable pretty URLs.
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
//
|
||||
// /section/name.html becomes /section/name/index.html
|
||||
// /section/name/ becomes /section/name/index.html
|
||||
// /section/name/index.html becomes /section/name/index.html
|
||||
func PrettifyURLPath(in string) string {
|
||||
return prettifyPath(in, pb)
|
||||
}
|
||||
|
||||
// Uglify does the opposite of PrettifyURLPath().
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
//
|
||||
// /section/name/index.html becomes /section/name.html
|
||||
// /section/name/ becomes /section/name.html
|
||||
// /section/name.html becomes /section/name.html
|
||||
func Uglify(in string) string {
|
||||
if path.Ext(in) == "" {
|
||||
if len(in) < 2 {
|
||||
@@ -158,7 +163,6 @@ func Uglify(in string) string {
|
||||
// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned.
|
||||
func UrlToFilename(s string) (string, bool) {
|
||||
u, err := url.ParseRequestURI(s)
|
||||
|
||||
if err != nil {
|
||||
return filepath.FromSlash(s), false
|
||||
}
|
||||
@@ -179,3 +183,13 @@ func UrlToFilename(s string) (string, bool) {
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
||||
// URLEscape escapes unicode letters.
|
||||
func URLEscape(uri string) string {
|
||||
// escape unicode letters
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func TestMakePermalink(t *testing.T) {
|
||||
{"http://abc.com", "bar", "http://abc.com/bar"},
|
||||
{"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"},
|
||||
{"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"},
|
||||
{"http://abc.com/foo", "post/bar?a=b#c", "http://abc.com/foo/post/bar?a=b#c"},
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate
|
||||
|
||||
// P is a predicate function that tests whether a value of type T satisfies some condition.
|
||||
type P[T any] func(T) bool
|
||||
|
||||
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
|
||||
func (p P[T]) And(ps ...P[T]) P[T] {
|
||||
return func(v T) bool {
|
||||
for _, pp := range ps {
|
||||
if !pp(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
|
||||
func (p P[T]) Or(ps ...P[T]) P[T] {
|
||||
return func(v T) bool {
|
||||
for _, pp := range ps {
|
||||
if pp(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Negate returns a predicate that is a logical negation of this predicate.
|
||||
func (p P[T]) Negate() P[T] {
|
||||
return func(v T) bool {
|
||||
return !p(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a new slice holding only the elements of s that satisfy p.
|
||||
// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length.
|
||||
func (p P[T]) Filter(s []T) []T {
|
||||
var n int
|
||||
for _, v := range s {
|
||||
if p(v) {
|
||||
s[n] = v
|
||||
n++
|
||||
}
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
// FilterCopy returns a new slice holding only the elements of s that satisfy p.
|
||||
func (p P[T]) FilterCopy(s []T) []T {
|
||||
var result []T
|
||||
for _, v := range s {
|
||||
if p(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package predicate_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/predicate"
|
||||
)
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
|
||||
c.Assert(p(1), qt.IsTrue)
|
||||
c.Assert(p(2), qt.IsFalse)
|
||||
|
||||
neg := p.Negate()
|
||||
c.Assert(neg(1), qt.IsFalse)
|
||||
c.Assert(neg(2), qt.IsTrue)
|
||||
|
||||
and := p.And(intP2)
|
||||
c.Assert(and(1), qt.IsFalse)
|
||||
c.Assert(and(2), qt.IsFalse)
|
||||
c.Assert(and(10), qt.IsTrue)
|
||||
|
||||
or := p.Or(intP2)
|
||||
c.Assert(or(1), qt.IsTrue)
|
||||
c.Assert(or(2), qt.IsTrue)
|
||||
c.Assert(or(10), qt.IsTrue)
|
||||
c.Assert(or(11), qt.IsFalse)
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
p = p.Or(intP2)
|
||||
|
||||
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
|
||||
|
||||
c.Assert(p.Filter(ints), qt.DeepEquals, []int{1, 2, 1, 2})
|
||||
c.Assert(ints, qt.DeepEquals, []int{1, 2, 1, 2, 1, 6, 7, 8, 2})
|
||||
}
|
||||
|
||||
func TestFilterCopy(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var p predicate.P[int] = intP1
|
||||
p = p.Or(intP2)
|
||||
|
||||
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
|
||||
|
||||
c.Assert(p.FilterCopy(ints), qt.DeepEquals, []int{1, 2, 1, 2})
|
||||
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2})
|
||||
}
|
||||
|
||||
var intP1 = func(i int) bool {
|
||||
if i == 10 {
|
||||
return true
|
||||
}
|
||||
return i == 1
|
||||
}
|
||||
|
||||
var intP2 = func(i int) bool {
|
||||
if i == 10 {
|
||||
return true
|
||||
}
|
||||
return i == 2
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rungroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Group is a group of workers that can be used to enqueue work and wait for
|
||||
// them to finish.
|
||||
type Group[T any] interface {
|
||||
Enqueue(T) error
|
||||
Wait() error
|
||||
}
|
||||
|
||||
type runGroup[T any] struct {
|
||||
ctx context.Context
|
||||
g *errgroup.Group
|
||||
ch chan T
|
||||
}
|
||||
|
||||
// Config is the configuration for a new Group.
|
||||
type Config[T any] struct {
|
||||
NumWorkers int
|
||||
Handle func(context.Context, T) error
|
||||
}
|
||||
|
||||
// Run creates a new Group with the given configuration.
|
||||
func Run[T any](ctx context.Context, cfg Config[T]) Group[T] {
|
||||
if cfg.NumWorkers <= 0 {
|
||||
cfg.NumWorkers = 1
|
||||
}
|
||||
if cfg.Handle == nil {
|
||||
panic("Handle must be set")
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
// Buffered for performance.
|
||||
ch := make(chan T, cfg.NumWorkers)
|
||||
|
||||
for i := 0; i < cfg.NumWorkers; i++ {
|
||||
g.Go(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := cfg.Handle(ctx, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return &runGroup[T]{
|
||||
ctx: ctx,
|
||||
g: g,
|
||||
ch: ch,
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue enqueues a new item to be handled by the workers.
|
||||
func (r *runGroup[T]) Enqueue(t T) error {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return nil
|
||||
case r.ch <- t:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait waits for all workers to finish and returns the first error.
|
||||
func (r *runGroup[T]) Wait() error {
|
||||
close(r.ch)
|
||||
return r.g.Wait()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rungroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
var result int
|
||||
adder := func(ctx context.Context, i int) error {
|
||||
result += i
|
||||
return nil
|
||||
}
|
||||
|
||||
g := Run[int](
|
||||
context.Background(),
|
||||
Config[int]{
|
||||
Handle: adder,
|
||||
},
|
||||
)
|
||||
|
||||
c.Assert(g, qt.IsNotNil)
|
||||
g.Enqueue(32)
|
||||
g.Enqueue(33)
|
||||
c.Assert(g.Wait(), qt.IsNil)
|
||||
c.Assert(result, qt.Equals, 65)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -57,7 +57,6 @@ line 3`
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(collected, qt.DeepEquals, []string{"line 1\n", "line 2\n", "\n", "line 3"})
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkVisitLinesAfter(b *testing.B) {
|
||||
@@ -68,9 +67,6 @@ func BenchmarkVisitLinesAfter(b *testing.B) {
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
VisitLinesAfter(lines, func(s string) {
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ func ToStringSlicePreserveStringE(v any) ([]string, error) {
|
||||
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.
|
||||
|
||||
@@ -45,5 +45,4 @@ func TestToDuration(t *testing.T) {
|
||||
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))
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -35,11 +35,11 @@ func NewEvictingStringQueue(size int) *EvictingStringQueue {
|
||||
}
|
||||
|
||||
// Add adds a new string to the tail of the queue if it's not already there.
|
||||
func (q *EvictingStringQueue) Add(v string) {
|
||||
func (q *EvictingStringQueue) Add(v string) *EvictingStringQueue {
|
||||
q.mu.Lock()
|
||||
if q.set[v] {
|
||||
q.mu.Unlock()
|
||||
return
|
||||
return q
|
||||
}
|
||||
|
||||
if len(q.set) == q.size {
|
||||
@@ -50,6 +50,17 @@ func (q *EvictingStringQueue) Add(v string) {
|
||||
q.set[v] = true
|
||||
q.vals = append(q.vals, v)
|
||||
q.mu.Unlock()
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *EvictingStringQueue) Len() int {
|
||||
if q == nil {
|
||||
return 0
|
||||
}
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return len(q.vals)
|
||||
}
|
||||
|
||||
// Contains returns whether the queue contains v.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -92,5 +92,31 @@ type DevMarker interface {
|
||||
DevOnly()
|
||||
}
|
||||
|
||||
// Unwrapper is implemented by types that can unwrap themselves.
|
||||
type Unwrapper interface {
|
||||
// Unwrapv is for internal use only.
|
||||
// It got its slightly odd name to prevent collisions with user types.
|
||||
Unwrapv() any
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying value of v if it implements Unwrapper, otherwise v is returned.
|
||||
func Unwrapv(v any) any {
|
||||
if u, ok := v.(Unwrapper); ok {
|
||||
return u.Unwrapv()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// LowHigh is typically used to represent a slice boundary.
|
||||
type LowHigh struct {
|
||||
Low int
|
||||
High int
|
||||
}
|
||||
|
||||
// This is only used for debugging purposes.
|
||||
var InvocationCounter atomic.Int64
|
||||
|
||||
// NewTrue returns a pointer to b.
|
||||
func NewBool(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
+15
-13
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -23,10 +23,12 @@ import (
|
||||
// A BaseURL in Hugo is normally on the form scheme://path, but the
|
||||
// form scheme: is also valid (mailto:hugo@rules.com).
|
||||
type BaseURL struct {
|
||||
url *url.URL
|
||||
WithPath string
|
||||
WithoutPath string
|
||||
BasePath string
|
||||
url *url.URL
|
||||
WithPath string
|
||||
WithPathNoTrailingSlash string
|
||||
WithoutPath string
|
||||
BasePath string
|
||||
BasePathNoTrailingSlash string
|
||||
}
|
||||
|
||||
func (b BaseURL) String() string {
|
||||
@@ -92,19 +94,19 @@ func NewBaseURLFromString(b string) (BaseURL, error) {
|
||||
return BaseURL{}, err
|
||||
}
|
||||
return newBaseURLFromURL(u)
|
||||
|
||||
}
|
||||
|
||||
func newBaseURLFromURL(u *url.URL) (BaseURL, error) {
|
||||
baseURL := BaseURL{url: u, WithPath: u.String()}
|
||||
var baseURLNoPath = baseURL.URL()
|
||||
// A baseURL should always have a trailing slash, see #11669.
|
||||
if !strings.HasSuffix(u.Path, "/") {
|
||||
u.Path += "/"
|
||||
}
|
||||
baseURL := BaseURL{url: u, WithPath: u.String(), WithPathNoTrailingSlash: strings.TrimSuffix(u.String(), "/")}
|
||||
baseURLNoPath := baseURL.URL()
|
||||
baseURLNoPath.Path = ""
|
||||
baseURL.WithoutPath = baseURLNoPath.String()
|
||||
|
||||
basePath := u.Path
|
||||
if basePath != "" && basePath != "/" {
|
||||
baseURL.BasePath = basePath
|
||||
}
|
||||
baseURL.BasePath = u.Path
|
||||
baseURL.BasePathNoTrailingSlash = strings.TrimSuffix(u.Path, "/")
|
||||
|
||||
return baseURL, nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -21,17 +21,24 @@ import (
|
||||
|
||||
func TestBaseURL(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
b, err := NewBaseURLFromString("http://example.com")
|
||||
|
||||
b, err := NewBaseURLFromString("http://example.com/")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com")
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/")
|
||||
|
||||
b, err = NewBaseURLFromString("http://example.com")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/")
|
||||
c.Assert(b.WithPathNoTrailingSlash, qt.Equals, "http://example.com")
|
||||
c.Assert(b.BasePath, qt.Equals, "/")
|
||||
|
||||
p, err := b.WithProtocol("webcal://")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com")
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com/")
|
||||
|
||||
p, err = b.WithProtocol("webcal")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com")
|
||||
c.Assert(p.String(), qt.Equals, "webcal://example.com/")
|
||||
|
||||
_, err = b.WithProtocol("mailto:")
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
@@ -57,11 +64,18 @@ func TestBaseURL(t *testing.T) {
|
||||
|
||||
b, err = NewBaseURLFromString("")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "")
|
||||
c.Assert(b.String(), qt.Equals, "/")
|
||||
|
||||
// BaseURL with sub path
|
||||
b, err = NewBaseURLFromString("http://example.com/sub")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/sub")
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/sub/")
|
||||
c.Assert(b.WithPathNoTrailingSlash, qt.Equals, "http://example.com/sub")
|
||||
c.Assert(b.BasePath, qt.Equals, "/sub/")
|
||||
c.Assert(b.BasePathNoTrailingSlash, qt.Equals, "/sub")
|
||||
|
||||
b, err = NewBaseURLFromString("http://example.com/sub/")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(b.String(), qt.Equals, "http://example.com/sub/")
|
||||
c.Assert(b.HostURL(), qt.Equals, "http://example.com")
|
||||
}
|
||||
|
||||
@@ -52,3 +52,16 @@ func Eq(v1, v2 any) bool {
|
||||
|
||||
return v1 == v2
|
||||
}
|
||||
|
||||
// ProbablyEq returns whether v1 is probably equal to v2.
|
||||
func ProbablyEq(v1, v2 any) bool {
|
||||
if Eq(v1, v2) {
|
||||
return true
|
||||
}
|
||||
|
||||
if peqer, ok := v1.(ProbablyEqer); ok {
|
||||
return peqer.ProbablyEq(v2)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -79,5 +79,4 @@ func BenchmarkStringSort(b *testing.B) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+132
-74
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
// Copyright 2024 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -27,15 +27,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/privacy"
|
||||
"github.com/gohugoio/hugo/config/security"
|
||||
"github.com/gohugoio/hugo/config/services"
|
||||
"github.com/gohugoio/hugo/deploy"
|
||||
"github.com/gohugoio/hugo/deploy/deployconfig"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib/segments"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/markup/markup_config"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
@@ -62,6 +66,7 @@ type InternalConfig struct {
|
||||
Verbose bool
|
||||
Clock string
|
||||
Watch bool
|
||||
FastRenderMode bool
|
||||
LiveReloadPort int
|
||||
}
|
||||
|
||||
@@ -99,9 +104,11 @@ type Config struct {
|
||||
RootConfig
|
||||
|
||||
// Author information.
|
||||
// Deprecated: Use taxonomies instead.
|
||||
Author map[string]any
|
||||
|
||||
// Social links.
|
||||
// Deprecated: Use .Site.Params instead.
|
||||
Social map[string]string
|
||||
|
||||
// The build configuration section contains build-related configuration options.
|
||||
@@ -133,12 +140,15 @@ type Config struct {
|
||||
// a slice of page matcher and params to apply to those pages.
|
||||
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"`
|
||||
|
||||
// The segments defines segments for the site. Used for partial/segmented builds.
|
||||
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
|
||||
|
||||
// Menu configuration.
|
||||
// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
|
||||
Menus *config.ConfigNamespace[map[string]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"`
|
||||
|
||||
// The deployment configuration section contains for hugo deploy.
|
||||
Deployment deploy.DeployConfig `mapstructure:"-"`
|
||||
// The deployment configuration section contains for hugo deployconfig.
|
||||
Deployment deployconfig.DeployConfig `mapstructure:"-"`
|
||||
|
||||
// Module configuration.
|
||||
Module modules.Config `mapstructure:"-"`
|
||||
@@ -204,7 +214,7 @@ func (c Config) cloneForLang() *Config {
|
||||
x.DisableKinds = copyStringSlice(x.DisableKinds)
|
||||
x.DisableLanguages = copyStringSlice(x.DisableLanguages)
|
||||
x.MainSections = copyStringSlice(x.MainSections)
|
||||
x.IgnoreErrors = copyStringSlice(x.IgnoreErrors)
|
||||
x.IgnoreLogs = copyStringSlice(x.IgnoreLogs)
|
||||
x.IgnoreFiles = copyStringSlice(x.IgnoreFiles)
|
||||
x.Theme = copyStringSlice(x.Theme)
|
||||
|
||||
@@ -282,12 +292,13 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
|
||||
disabledLangs := make(map[string]bool)
|
||||
for _, lang := range c.DisableLanguages {
|
||||
if lang == c.DefaultContentLanguage {
|
||||
return fmt.Errorf("cannot disable default content language %q", lang)
|
||||
}
|
||||
disabledLangs[lang] = true
|
||||
}
|
||||
for lang, language := range c.Languages {
|
||||
if !language.Disabled && disabledLangs[lang] {
|
||||
language.Disabled = true
|
||||
c.Languages[lang] = language
|
||||
}
|
||||
if language.Disabled {
|
||||
disabledLangs[lang] = true
|
||||
if lang == c.DefaultContentLanguage {
|
||||
@@ -296,9 +307,9 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
}
|
||||
}
|
||||
|
||||
ignoredErrors := make(map[string]bool)
|
||||
for _, err := range c.IgnoreErrors {
|
||||
ignoredErrors[strings.ToLower(err)] = true
|
||||
ignoredLogIDs := make(map[string]bool)
|
||||
for _, err := range c.IgnoreLogs {
|
||||
ignoredLogIDs[strings.ToLower(err)] = true
|
||||
}
|
||||
|
||||
baseURL, err := urls.NewBaseURLFromString(c.BaseURL)
|
||||
@@ -354,11 +365,12 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
BaseURLLiveReload: baseURL,
|
||||
DisabledKinds: disabledKinds,
|
||||
DisabledLanguages: disabledLangs,
|
||||
IgnoredErrors: ignoredErrors,
|
||||
IgnoredLogs: ignoredLogIDs,
|
||||
KindOutputFormats: kindOutputFormats,
|
||||
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
|
||||
IsUglyURLSection: isUglyURL,
|
||||
IgnoreFile: ignoreFile,
|
||||
SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
|
||||
MainSections: c.MainSections,
|
||||
Clock: clock,
|
||||
transientErr: transientErr,
|
||||
@@ -388,13 +400,15 @@ type ConfigCompiled struct {
|
||||
Timeout time.Duration
|
||||
BaseURL urls.BaseURL
|
||||
BaseURLLiveReload urls.BaseURL
|
||||
ServerInterface string
|
||||
KindOutputFormats map[string]output.Formats
|
||||
DisabledKinds map[string]bool
|
||||
DisabledLanguages map[string]bool
|
||||
IgnoredErrors map[string]bool
|
||||
IgnoredLogs map[string]bool
|
||||
CreateTitle func(s string) string
|
||||
IsUglyURLSection func(section string) bool
|
||||
IgnoreFile func(filename string) bool
|
||||
SegmentFilter segments.SegmentFilter
|
||||
MainSections []string
|
||||
Clock time.Time
|
||||
|
||||
@@ -407,24 +421,28 @@ type ConfigCompiled struct {
|
||||
}
|
||||
|
||||
// This may be set after the config is compiled.
|
||||
func (c *ConfigCompiled) SetMainSectionsIfNotSet(sections []string) {
|
||||
func (c *ConfigCompiled) SetMainSections(sections []string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.MainSections != nil {
|
||||
return
|
||||
}
|
||||
c.MainSections = sections
|
||||
}
|
||||
|
||||
// IsMainSectionsSet returns whether the main sections have been set.
|
||||
func (c *ConfigCompiled) IsMainSectionsSet() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.MainSections != nil
|
||||
}
|
||||
|
||||
// This is set after the config is compiled by the server command.
|
||||
func (c *ConfigCompiled) SetBaseURL(baseURL, baseURLLiveReload urls.BaseURL) {
|
||||
func (c *ConfigCompiled) SetServerInfo(baseURL, baseURLLiveReload urls.BaseURL, serverInterface string) {
|
||||
c.BaseURL = baseURL
|
||||
c.BaseURLLiveReload = baseURLLiveReload
|
||||
c.ServerInterface = serverInterface
|
||||
}
|
||||
|
||||
// RootConfig holds all the top-level configuration options in Hugo
|
||||
type RootConfig struct {
|
||||
|
||||
// The base URL of the site.
|
||||
// Note that the default value is empty, but Hugo requires a valid URL (e.g. "https://example.com/") to work properly.
|
||||
// <docsmeta>{"identifiers": ["URL"] }</docsmeta>
|
||||
@@ -464,6 +482,10 @@ type RootConfig struct {
|
||||
// A list of languages to disable.
|
||||
DisableLanguages []string
|
||||
|
||||
// The named segments to render.
|
||||
// This needs to match the name of the segment in the segments configuration.
|
||||
RenderSegments []string
|
||||
|
||||
// Disable the injection of the Hugo generator tag on the home page.
|
||||
DisableHugoGeneratorInject bool
|
||||
|
||||
@@ -495,8 +517,8 @@ type RootConfig struct {
|
||||
// Enable to disable the build lock file.
|
||||
NoBuildLock bool
|
||||
|
||||
// A list of error IDs to ignore.
|
||||
IgnoreErrors []string
|
||||
// A list of log IDs to ignore.
|
||||
IgnoreLogs []string
|
||||
|
||||
// A list of regexps that match paths to ignore.
|
||||
// Deprecated: Use the settings on module imports.
|
||||
@@ -530,6 +552,9 @@ type RootConfig struct {
|
||||
// Note that this currently only works for English, but you can provide your own title in the content file's front matter.
|
||||
PluralizeListTitles bool
|
||||
|
||||
// Whether to capitalize automatic page titles, applicable to section, taxonomy, and term pages.
|
||||
CapitalizeListTitles bool
|
||||
|
||||
// Make all relative URLs absolute using the baseURL.
|
||||
// <docsmeta>{"identifiers": ["baseURL"] }</docsmeta>
|
||||
CanonifyURLs bool
|
||||
@@ -647,16 +672,26 @@ type Configs struct {
|
||||
LanguageConfigMap map[string]*Config
|
||||
LanguageConfigSlice []*Config
|
||||
|
||||
IsMultihost bool
|
||||
Languages langs.Languages
|
||||
LanguagesDefaultFirst langs.Languages
|
||||
IsMultihost bool
|
||||
|
||||
Modules modules.Modules
|
||||
ModulesClient *modules.Client
|
||||
|
||||
// All below is set in Init.
|
||||
Languages langs.Languages
|
||||
LanguagesDefaultFirst langs.Languages
|
||||
ContentPathParser *paths.PathParser
|
||||
|
||||
configLangs []config.AllProvider
|
||||
}
|
||||
|
||||
func (c *Configs) Validate(logger loggers.Logger) error {
|
||||
for p := range c.Base.Cascade.Config {
|
||||
page.CheckCascadePattern(logger, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// transientErr returns the last transient error found during config compilation.
|
||||
func (c *Configs) transientErr() error {
|
||||
for _, l := range c.LanguageConfigSlice {
|
||||
@@ -673,6 +708,59 @@ func (c *Configs) IsZero() bool {
|
||||
}
|
||||
|
||||
func (c *Configs) Init() error {
|
||||
var languages langs.Languages
|
||||
defaultContentLanguage := c.Base.DefaultContentLanguage
|
||||
for k, v := range c.LanguageConfigMap {
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
languages = append(languages, language)
|
||||
}
|
||||
|
||||
// Sort the sites by language weight (if set) or lang.
|
||||
sort.Slice(languages, func(i, j int) bool {
|
||||
li := languages[i]
|
||||
lj := languages[j]
|
||||
if li.Weight != lj.Weight {
|
||||
return li.Weight < lj.Weight
|
||||
}
|
||||
return li.Lang < lj.Lang
|
||||
})
|
||||
|
||||
for _, l := range languages {
|
||||
c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang])
|
||||
}
|
||||
|
||||
// Filter out disabled languages.
|
||||
var n int
|
||||
for _, l := range languages {
|
||||
if !l.Disabled {
|
||||
languages[n] = l
|
||||
n++
|
||||
}
|
||||
}
|
||||
languages = languages[:n]
|
||||
|
||||
var languagesDefaultFirst langs.Languages
|
||||
for _, l := range languages {
|
||||
if l.Lang == defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
for _, l := range languages {
|
||||
if l.Lang != defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
|
||||
c.Languages = languages
|
||||
c.LanguagesDefaultFirst = languagesDefaultFirst
|
||||
|
||||
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled}
|
||||
|
||||
c.configLangs = make([]config.AllProvider, len(c.Languages))
|
||||
for i, l := range c.LanguagesDefaultFirst {
|
||||
c.configLangs[i] = ConfigLanguage{
|
||||
@@ -750,10 +838,9 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
|
||||
langConfigMap := make(map[string]*Config)
|
||||
var langConfigs []*Config
|
||||
|
||||
languagesConfig := cfg.GetStringMap("languages")
|
||||
var isMultiHost bool
|
||||
var isMultihost bool
|
||||
|
||||
if err := all.CompileConfig(logger); err != nil {
|
||||
return nil, err
|
||||
@@ -784,13 +871,13 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
// We accidentally allowed it in the past, so we need to support it a little longer,
|
||||
// But log a warning.
|
||||
if _, found := params[kk]; !found {
|
||||
helpers.Deprecated(fmt.Sprintf("config: languages.%s.%s: custom params on the language top level", k, kk), fmt.Sprintf("Put the value below [languages.%s.params]. See https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120", k), false)
|
||||
hugo.Deprecate(fmt.Sprintf("config: languages.%s.%s: custom params on the language top level", k, kk), fmt.Sprintf("Put the value below [languages.%s.params]. See https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120", k), "v0.112.0")
|
||||
params[kk] = vv
|
||||
}
|
||||
}
|
||||
if kk == "baseurl" {
|
||||
// baseURL configure don the language level is a multihost setup.
|
||||
isMultiHost = true
|
||||
isMultihost = true
|
||||
}
|
||||
mergedConfig.Set(kk, vv)
|
||||
rootv := cfg.Get(kk)
|
||||
@@ -839,6 +926,18 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Adjust Goldmark config defaults for multilingual, single-host sites.
|
||||
if len(languagesConfig) > 1 && !isMultihost && !clone.Markup.Goldmark.DuplicateResourceFiles {
|
||||
if !clone.Markup.Goldmark.DuplicateResourceFiles {
|
||||
if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil {
|
||||
clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true)
|
||||
}
|
||||
if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil {
|
||||
clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
langConfigMap[k] = clone
|
||||
case maps.ParamsMergeStrategy:
|
||||
default:
|
||||
@@ -847,65 +946,24 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
}
|
||||
}
|
||||
|
||||
var languages langs.Languages
|
||||
defaultContentLanguage := all.DefaultContentLanguage
|
||||
for k, v := range langConfigMap {
|
||||
languageConf := v.Languages[k]
|
||||
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
languages = append(languages, language)
|
||||
}
|
||||
|
||||
// Sort the sites by language weight (if set) or lang.
|
||||
sort.Slice(languages, func(i, j int) bool {
|
||||
li := languages[i]
|
||||
lj := languages[j]
|
||||
if li.Weight != lj.Weight {
|
||||
return li.Weight < lj.Weight
|
||||
}
|
||||
return li.Lang < lj.Lang
|
||||
})
|
||||
|
||||
for _, l := range languages {
|
||||
langConfigs = append(langConfigs, langConfigMap[l.Lang])
|
||||
}
|
||||
|
||||
var languagesDefaultFirst langs.Languages
|
||||
for _, l := range languages {
|
||||
if l.Lang == defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
for _, l := range languages {
|
||||
if l.Lang != defaultContentLanguage {
|
||||
languagesDefaultFirst = append(languagesDefaultFirst, l)
|
||||
}
|
||||
}
|
||||
|
||||
bcfg.PublishDir = all.PublishDir
|
||||
res.BaseConfig = bcfg
|
||||
all.CommonDirs.CacheDir = bcfg.CacheDir
|
||||
for _, l := range langConfigs {
|
||||
for _, l := range langConfigMap {
|
||||
l.CommonDirs.CacheDir = bcfg.CacheDir
|
||||
}
|
||||
|
||||
cm := &Configs{
|
||||
Base: all,
|
||||
LanguageConfigMap: langConfigMap,
|
||||
LanguageConfigSlice: langConfigs,
|
||||
LoadingInfo: res,
|
||||
IsMultihost: isMultiHost,
|
||||
Languages: languages,
|
||||
LanguagesDefaultFirst: languagesDefaultFirst,
|
||||
Base: all,
|
||||
LanguageConfigMap: langConfigMap,
|
||||
LoadingInfo: res,
|
||||
IsMultihost: isMultihost,
|
||||
}
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func decodeConfigFromParams(fs afero.Fs, logger loggers.Logger, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error {
|
||||
|
||||
var decoderSetups []decodeWeight
|
||||
|
||||
if len(keys) == 0 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user