mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-31 18:52:38 +00:00
Compare commits
573 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fc339dc25 | |||
| 5236cb8e9a | |||
| a28865cfc3 | |||
| cb6c77b813 | |||
| be58c7b9c8 | |||
| 62d031aedf | |||
| ce06bdb16a | |||
| 2efc1a64c3 | |||
| 25ddbb09fe | |||
| e691c48a5a | |||
| 094709e105 | |||
| a574469797 | |||
| 822dc627a1 | |||
| 43f9df0194 | |||
| 1021714449 | |||
| 6f069e549b | |||
| 5a83bf314f | |||
| 0483299bc0 | |||
| 27b62a546c | |||
| 35bfca3b14 | |||
| 9cd54cab20 | |||
| a8853f1c5a | |||
| 4d93aca27d | |||
| f2167de834 | |||
| 64b6b29075 | |||
| bb9c2988f8 | |||
| c04030b14c | |||
| f7691fe965 | |||
| efe0b4e5c0 | |||
| d5a0b6bbbc | |||
| 5178cd13a7 | |||
| 9ce0a1fb70 | |||
| ab9214768d | |||
| 5068681707 | |||
| b17a61a605 | |||
| eb16165694 | |||
| 7829474088 | |||
| 256418917c | |||
| c84f506f8e | |||
| 4b5f743959 | |||
| 931a132450 | |||
| 8378358857 | |||
| 831d23cb4d | |||
| 514e18dc27 | |||
| b804a70881 | |||
| 30a7c9ea37 | |||
| 8346848109 | |||
| 54ecade02f | |||
| 9433cf5a92 | |||
| 3c909eb3f3 | |||
| 724b3e673b | |||
| 056c754771 | |||
| a9a93d082d | |||
| b09a40333f | |||
| 7540a62834 | |||
| 7e75aeca80 | |||
| e14e0b192f | |||
| ed698e94c1 | |||
| 89e2716d29 | |||
| aba2647c15 | |||
| f37c5a2567 | |||
| aded0f25fd | |||
| bc337e6ab5 | |||
| 112461fded | |||
| 94ab125b27 | |||
| dcfeed35c6 | |||
| f9b4eb4f39 | |||
| e82b2dc8c1 | |||
| 5df2b79dd2 | |||
| 12742bac71 | |||
| fabf026f49 | |||
| 94f0f7e597 | |||
| 3c29c5af8e | |||
| 33502667fb | |||
| d3489eba5d | |||
| 17d7ecde2b | |||
| e4b2572880 | |||
| 782dd15858 | |||
| aff9c09166 | |||
| fdd4a768f0 | |||
| abeeff1325 | |||
| f7aeaa6129 | |||
| 7d78a2afd3 | |||
| 5570a6e478 | |||
| c0b3a1af03 | |||
| a8cb1b07b4 | |||
| 2900801acf | |||
| f3d5190793 | |||
| 1ccc0b79a2 | |||
| cbed50e22b | |||
| 05ecd8111c | |||
| e456e34bdb | |||
| 5d14d04ac6 | |||
| ddcb402859 | |||
| 3a44920e79 | |||
| 79a06aa4b6 | |||
| d212f60949 | |||
| 488776b649 | |||
| b8b8436fcc | |||
| 42d8dfc8c8 | |||
| 47506d1644 | |||
| 2998fa0cd5 | |||
| d16a7a33ff | |||
| 5b1edd281a | |||
| 2bd9d9099d | |||
| 4b7d3e57a4 | |||
| 4f3c095963 | |||
| 486bc46a52 | |||
| 61f210dd7a | |||
| f8446188db | |||
| d523aa4bb0 | |||
| b8725f5181 | |||
| 0bc4b0246d | |||
| faeb55c1d8 | |||
| 6c6a6c87ec | |||
| 1d18eb0574 | |||
| 33a7b36fd4 | |||
| 6180c85fb8 | |||
| d3a98325c3 | |||
| b2a676f5f0 | |||
| 3a786a248d | |||
| 729593c842 | |||
| e65268f2c2 | |||
| f863b6a07e | |||
| f5be599204 | |||
| b14c11b0e1 | |||
| 0addb2add6 | |||
| 8bba85601f | |||
| 7082a5d143 | |||
| aa281b5135 | |||
| 3250198724 | |||
| d6a4af7018 | |||
| 27569275b1 | |||
| 4b2738d871 | |||
| 74309fe569 | |||
| 9c88a8a55a | |||
| 95e72f5e8e | |||
| 1c7b7b4ef2 | |||
| 1ad117cbe2 | |||
| dac7092a9c | |||
| b27ccf34bf | |||
| 286d0b7dc3 | |||
| 83c873ff37 | |||
| a2440dc0e2 | |||
| 1e9ac3dcc2 | |||
| 40e99672b6 | |||
| 3a3badfd1d | |||
| df021317a9 | |||
| 78578632f5 | |||
| 6b78b3810a | |||
| 78a4c2e32e | |||
| d4ebfea1ff | |||
| 93aa6261b4 | |||
| acc14b4646 | |||
| 2d7709d155 | |||
| deff9e154b | |||
| 889aca054a | |||
| e3ed4a83b8 | |||
| 6636cf1bea | |||
| f669ef6bec | |||
| ed7b3e2619 | |||
| 2bf686ee21 | |||
| 9f74dc2a52 | |||
| d1661b823a | |||
| 7930d2132a | |||
| eb038cfa0a | |||
| 129c27ee6e | |||
| 44da60d869 | |||
| 1e3e34002d | |||
| 1b7ecfc2e1 | |||
| 27f5a906a2 | |||
| 2fdc4a24d5 | |||
| f6863e1ef7 | |||
| d6c16afde0 | |||
| 6f3716dc22 | |||
| 5a52cd5f92 | |||
| 4a366fcfee | |||
| 083311d033 | |||
| 0fe4ff1875 | |||
| 1f42e47e47 | |||
| 35fbfb19a1 | |||
| 3a3089121b | |||
| c21e5179ce | |||
| a205f24ba5 | |||
| 92979d9288 | |||
| 0a3340e952 | |||
| 6b21ac3e67 | |||
| d14420539a | |||
| 3d4a9882bf | |||
| bdca972794 | |||
| 604ddb90c5 | |||
| b09e495a71 | |||
| 42cde66663 | |||
| ad60948c46 | |||
| c397f2c080 | |||
| b9e18e20c8 | |||
| 2159d77f36 | |||
| 535755e4f8 | |||
| 3583dd6d71 | |||
| e5b4cb2419 | |||
| 821adf3ae8 | |||
| 8ba9a27366 | |||
| 235acf2232 | |||
| ac0fac9347 | |||
| 23f48c300c | |||
| e2201ef15f | |||
| b5e17f7c83 | |||
| 31a8bb8c07 | |||
| 8e825ddf5b | |||
| 646a52a5c5 | |||
| e421696d02 | |||
| 4b4af2c52e | |||
| fae48d7457 | |||
| 06d28a464d | |||
| 6818170308 | |||
| c5279064df | |||
| 152cffb13a | |||
| 0d5110d033 | |||
| d3b81ee58e | |||
| 1fd30d462e | |||
| 498d629958 | |||
| 10ac2ec446 | |||
| ce264b936c | |||
| 91f49c0700 | |||
| a475bf125c | |||
| f1dede3708 | |||
| fa873a6cb3 | |||
| fb732d5322 | |||
| ad705aac06 | |||
| 52ac85fbc4 | |||
| 048a64be1c | |||
| 3033a9a37e | |||
| 27d4211187 | |||
| 4730a78a76 | |||
| b7706d6aec | |||
| 48413d76f4 | |||
| 66b9e2bff2 | |||
| 398996e8b0 | |||
| 7f005f5e98 | |||
| 011e3aaa9e | |||
| 3697448450 | |||
| 2638b04c62 | |||
| cf47f43ff1 | |||
| 3b6bd1210a | |||
| 8388cd90e8 | |||
| 807c551922 | |||
| bc57f5c36e | |||
| fe6a6f2737 | |||
| 2650fa772b | |||
| ef525b15d4 | |||
| f916814697 | |||
| cae07ce84b | |||
| 4f9c109dc5 | |||
| f1a00b2069 | |||
| df4cbbd3bd | |||
| 2e2e34a935 | |||
| 2eed35c826 | |||
| dd692c710a | |||
| ed4f1edbd7 | |||
| 058cc6c2c3 | |||
| 4b82f74848 | |||
| 06976ebb87 | |||
| 555a5612b2 | |||
| 75e54345f9 | |||
| df50c108ba | |||
| e27fd4c1b8 | |||
| b7ca3e1b3a | |||
| 13e64d7276 | |||
| 9b26b5487b | |||
| 3dafe206e3 | |||
| 37d6463479 | |||
| 43d446522a | |||
| 4f72e79120 | |||
| fe6676c775 | |||
| 7a97d3e6bc | |||
| 6667c6d743 | |||
| cfda13b363 | |||
| bb2fe814c2 | |||
| e5d66074ce | |||
| d970327d7b | |||
| 0013bea901 | |||
| 30bc4ed0a0 | |||
| f6f22ad944 | |||
| 400fe96aee | |||
| 3f45e729f4 | |||
| ffaa73dc8a | |||
| f8d8c85428 | |||
| 10dc87bf86 | |||
| 7231869ba8 | |||
| ccd3285484 | |||
| 600047ff1c | |||
| 5f2e1cb896 | |||
| c8ce65046d | |||
| a53f962312 | |||
| daffeec30d | |||
| b8b91f5506 | |||
| f0effac804 | |||
| be3ae3ec92 | |||
| 2cf8fe2ea2 | |||
| 3b103cb7b7 | |||
| 0cc936583b | |||
| c15c7da42a | |||
| bcbe57c6e9 | |||
| e38e881248 | |||
| 0665a3951b | |||
| 1ed8c36336 | |||
| e03eb90a36 | |||
| c915d0d325 | |||
| 47d4edce60 | |||
| ea8ef573c6 | |||
| 293e12355d | |||
| fdf3c3b823 | |||
| 312d2252be | |||
| f627903efa | |||
| a2f0f9d475 | |||
| 1ff54ab3d9 | |||
| 456f5476cf | |||
| 09811d0bee | |||
| d351ac2e69 | |||
| 59f57900a4 | |||
| 59cdad325a | |||
| 6022f219f1 | |||
| 0c8a415483 | |||
| f4675fa0f0 | |||
| ebb56e8bdb | |||
| 6b9934a266 | |||
| c7f057797c | |||
| 08d14113b6 | |||
| 94d6d6780f | |||
| 45c9c45d1d | |||
| fce32c07fb | |||
| d32ff16fd6 | |||
| e6eda2a370 | |||
| 66f688f712 | |||
| 2c934be8ab | |||
| 94d0e79d33 | |||
| b9a503febb | |||
| fff132537b | |||
| 8999de193c | |||
| a81c64e43e | |||
| 7ef5096257 | |||
| 16b691a875 | |||
| aefd9a80a6 | |||
| 57ace857aa | |||
| fdff0d3af4 | |||
| 834617f9f8 | |||
| ef20ec1fba | |||
| 5a0ee2b934 | |||
| 6746826689 | |||
| caf608d566 | |||
| 6711dacded | |||
| de23293486 | |||
| 918177ac25 | |||
| 83a980d290 | |||
| 4a16b5f4b0 | |||
| a95896878f | |||
| 873f8805cb | |||
| 3743875778 | |||
| baa62d0abb | |||
| 78c99463fd | |||
| abc54080ec | |||
| a655e00d70 | |||
| 9d973004f5 | |||
| 2247546017 | |||
| d139a037d9 | |||
| 0a88741fe8 | |||
| d999b4e82b | |||
| 20148345aa | |||
| e5052f4e09 | |||
| c81fbf4625 | |||
| a6f199f7a6 | |||
| 0dd06bdac0 | |||
| 5c5384916e | |||
| 56c61559b2 | |||
| 78f8475a05 | |||
| c09ee78fd2 | |||
| f6ae436c58 | |||
| 2182ecfd34 | |||
| e85833d868 | |||
| 43a5aaa778 | |||
| 1639fd20d8 | |||
| 3a44bf182f | |||
| c2a6741394 | |||
| 06bd013641 | |||
| 755d1ffe7a | |||
| fb3cb05cc3 | |||
| d07882dfb7 | |||
| 7f5356717d | |||
| 789ef8c639 | |||
| 71931b30b1 | |||
| d40116e5f9 | |||
| c362634b7d | |||
| d71120852a | |||
| a6b1eb1e91 | |||
| 27110133ff | |||
| 31faf98ce3 | |||
| f14d773841 | |||
| 3aa838cf2b | |||
| a3a26597cf | |||
| 92d776b0df | |||
| e6dd54943f | |||
| 766085c2dc | |||
| b5d13ca16b | |||
| 6b02f5c0f4 | |||
| 88e447c449 | |||
| b718d743b7 | |||
| 0ba19c57f1 | |||
| 0afa2897a0 | |||
| f219ac09f6 | |||
| 786f72302f | |||
| 0cae1cf828 | |||
| 3b18042ee6 | |||
| e38843f4bf | |||
| 87f0dc14ab | |||
| b7d9ab136d | |||
| 04d4c08dbc | |||
| 016dd4a69a | |||
| c6b599a06d | |||
| 3a6784b162 | |||
| 48d7a641d0 | |||
| 2c5a036def | |||
| 592c5b3072 | |||
| a0c4a3660e | |||
| 062510cf1f | |||
| 4e1d0cd9f1 | |||
| a451c49fde | |||
| ff16c42ed0 | |||
| 166483fe12 | |||
| f01505c910 | |||
| 8278384b96 | |||
| 65e610e161 | |||
| b56d9a1294 | |||
| d6fde8fa13 | |||
| 2bac371544 | |||
| 9b4b97a722 | |||
| 501543d4b6 | |||
| 6ffa882349 | |||
| 1eb8b36b38 | |||
| 00c74ee7ff | |||
| e25aa655f4 | |||
| 12679b4083 | |||
| c39ac5738d | |||
| a3535c8486 | |||
| 9da617912b | |||
| 1ab4658c0d | |||
| 75acff5f20 | |||
| 5c9d5413a4 | |||
| 59ebc83d72 | |||
| b6b37a1f00 | |||
| ebe4d39f17 | |||
| 1b0aeeaaf0 | |||
| a4c513caeb | |||
| d741064beb | |||
| 3eb313fef4 | |||
| b93417aa1d | |||
| fd1f4a7860 | |||
| 4c240800a4 | |||
| 38204c4ab6 | |||
| 2f2bc7ff70 | |||
| 242b297f57 | |||
| f44c9ab9cd | |||
| 9f9695cf7b | |||
| 0831d8ccaf | |||
| 1d4ff024ca | |||
| 521e63ac7c | |||
| 47d38628ec | |||
| 0024dcfe3e | |||
| beec1fc98e | |||
| 306573def0 | |||
| 80c8f3b81a | |||
| 6b6dcb44a0 | |||
| d96f2a460f | |||
| ac53035030 | |||
| b874a1ba7a | |||
| 4108705934 | |||
| 9c1e82085e | |||
| 2b73e89d6d | |||
| febf0aec8b | |||
| a4fad5be6b | |||
| 6084f0433c | |||
| cbb7214b6c | |||
| e6136b36f3 | |||
| 659917a002 | |||
| 9d194ab904 | |||
| a305609e18 | |||
| 179de5f5bc | |||
| 5dc1f95b63 | |||
| 6bc892fc24 | |||
| 5f06dbf779 | |||
| 0850e97984 | |||
| a3b4b10f65 | |||
| e3df6478f0 | |||
| 626afc9825 | |||
| e1027c5846 | |||
| e1a052ecb8 | |||
| bfc3488b8e | |||
| ce84b524f4 | |||
| 3cea2932e1 | |||
| 0efd374805 | |||
| 98293eaa15 | |||
| 2b8d907ab7 | |||
| 43338c3a99 | |||
| dea71670c0 | |||
| a5d0a57e6b | |||
| f465571b33 | |||
| f8212d2000 | |||
| 78e8a744b3 | |||
| c790029e1d | |||
| 554553c09c | |||
| de37455ec7 | |||
| 282f6035e7 | |||
| 360fa12213 | |||
| 02aa320030 | |||
| d2640fbc19 | |||
| 1637d12e37 | |||
| 3a7706b069 | |||
| 2955f93fc6 | |||
| 1f0c4e1fb3 | |||
| 91ab455d84 | |||
| ca1e46efb9 | |||
| fd71fa89bd | |||
| b5a3aa7082 | |||
| 3d5928889a | |||
| dc7bc7b4d2 | |||
| 42ed602580 | |||
| 6a2968fd5c | |||
| 23d5fc82ee | |||
| 8531ec7ca3 | |||
| 9f27091e10 | |||
| 187621ae24 | |||
| 4172a835e5 | |||
| fc97388968 | |||
| d67e843c12 | |||
| 6e33c557b0 | |||
| 128f14efad | |||
| 34ee27a78b | |||
| 0f1fc01ef2 | |||
| f32ccd018f | |||
| ffb609f4ab | |||
| 19d91abc92 | |||
| f5c0a2b4b2 | |||
| 31828026a1 | |||
| c74b0f8f9b | |||
| 80949dc73f | |||
| e02629f81a | |||
| b239595af5 | |||
| ed4a345efe | |||
| 9717ac7dce | |||
| 692ec00872 | |||
| d3dd74fd65 | |||
| 80230f26a3 | |||
| 6464981adb | |||
| 2e6712e281 | |||
| 65deb72dc4 | |||
| dc4226a8b2 | |||
| bf5f10faa9 | |||
| 019bd5576b | |||
| c3115292a7 | |||
| 0c6c98e401 | |||
| 90c7749085 | |||
| 13435a6f60 | |||
| 07b96d16e8 | |||
| ceaff7cafc | |||
| b84389c5e0 | |||
| 8de5324479 | |||
| 20cbc2c785 | |||
| 2174525cec | |||
| 7a6192647a | |||
| d68367cbe7 | |||
| 5241f0baee | |||
| b5b36e3200 | |||
| 4f0665f476 | |||
| fc8c5ad824 |
@@ -1,8 +1,9 @@
|
||||
defaults: &defaults
|
||||
working_directory: /go/src/github.com/gohugoio
|
||||
docker:
|
||||
- image: bepsays/ci-goreleaser:0.34.2-11
|
||||
|
||||
- image: bepsays/ci-goreleaser:1.11-3
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
@@ -12,11 +13,11 @@ jobs:
|
||||
path: hugo
|
||||
- run:
|
||||
command: |
|
||||
go get -d github.com/magefile/mage/...
|
||||
git clone git@github.com:gohugoio/hugoDocs.git
|
||||
cd hugo
|
||||
mage vendor
|
||||
mage check
|
||||
go mod download
|
||||
sleep 5
|
||||
go test -p 1 ./...
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
@@ -24,7 +25,7 @@ jobs:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /go/src/github.com/gohugoio
|
||||
at: /root/project
|
||||
- run:
|
||||
command: |
|
||||
cd hugo
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
*.md
|
||||
*.log
|
||||
*.txt
|
||||
.git
|
||||
.github
|
||||
.circleci
|
||||
docs
|
||||
examples
|
||||
@@ -0,0 +1,8 @@
|
||||
# Text files have auto line endings
|
||||
* text=auto
|
||||
|
||||
# Go source files always have LF line endings
|
||||
*.go text eol=lf
|
||||
|
||||
# SVG files should not be modified
|
||||
*.svg -text
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
hugo
|
||||
/hugo
|
||||
docs/public*
|
||||
/.idea
|
||||
hugo.exe
|
||||
@@ -15,5 +15,10 @@ vendor/*/
|
||||
*.debug
|
||||
coverage*.out
|
||||
|
||||
dock.sh
|
||||
|
||||
GoBuilds
|
||||
dist
|
||||
|
||||
|
||||
vendor
|
||||
+21
-4
@@ -1,27 +1,44 @@
|
||||
language: go
|
||||
sudo: false
|
||||
dist: trusty
|
||||
env:
|
||||
global:
|
||||
- HUGO_BUILD_TAGS="extended"
|
||||
git:
|
||||
depth: false
|
||||
go:
|
||||
- 1.9.5
|
||||
- "1.10.1"
|
||||
- "1.11.4"
|
||||
- tip
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
- windows
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
fast_finish: true
|
||||
exclude:
|
||||
- os: windows
|
||||
go: tip
|
||||
|
||||
install:
|
||||
- mkdir -p $HOME/src
|
||||
- mv $HOME/gopath/src/github.com/gohugoio/hugo $HOME/src
|
||||
- export TRAVIS_BUILD_DIR=$HOME/src/hugo
|
||||
- cd $HOME/src/hugo
|
||||
- go get github.com/magefile/mage
|
||||
- mage -v vendor
|
||||
script:
|
||||
- mage -v hugoRace
|
||||
- go mod download
|
||||
- mage -v test
|
||||
- mage -v check
|
||||
- mage -v hugo
|
||||
- ./hugo -s docs/
|
||||
- ./hugo --renderToMemory -s docs/
|
||||
- df -h
|
||||
|
||||
before_install:
|
||||
- df -h
|
||||
# https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
|
||||
- if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then choco install mingw -y; export PATH=/c/tools/mingw64/bin:"$PATH"; fi
|
||||
- gem install asciidoctor
|
||||
- type asciidoctor
|
||||
|
||||
+20
-34
@@ -18,11 +18,8 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
|
||||
* [Submitting Patches](#submitting-patches)
|
||||
* [Code Contribution Guidelines](#code-contribution-guidelines)
|
||||
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
|
||||
* [Vendored Dependencies](#vendored-dependencies)
|
||||
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
|
||||
* [Using Git Remotes](#using-git-remotes)
|
||||
* [Build Hugo with Your Changes](#build-hugo-with-your-changes)
|
||||
* [Updating the Hugo Sources](#updating-the-hugo-sources)
|
||||
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
|
||||
|
||||
## Asking Support Questions
|
||||
|
||||
@@ -73,7 +70,7 @@ To make the contribution process as seamless as possible, we ask for the followi
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Linux and macOS) and [AppVeyor](https://ci.appveyor.com/project/gohugoio/hugo/branch/master) (Windows) will fail the build if `mage check` fails.
|
||||
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
@@ -106,30 +103,25 @@ Fixes #1949
|
||||
|
||||
### Fetching the Sources From GitHub
|
||||
|
||||
Due to the way Go handles package imports, the best approach for working on a
|
||||
Hugo fork is to use Git Remotes. Here's a simple walk-through for getting
|
||||
started:
|
||||
Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
|
||||
|
||||
1. Get the Hugo source:
|
||||
```bash
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
```bash
|
||||
go get -u -v -d github.com/gohugoio/hugo
|
||||
```
|
||||
>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`.
|
||||
|
||||
1. Install Mage:
|
||||
For some convenient build and test targets, you also will want to install Mage:
|
||||
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
```
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
```
|
||||
|
||||
1. Change to the Hugo source directory and fetch the dependencies:
|
||||
|
||||
```bash
|
||||
cd $HOME/go/src/github.com/gohugoio/hugo
|
||||
mage vendor
|
||||
```
|
||||
|
||||
Note that Hugo uses [Go Dep](https://github.com/golang/dep) to vendor dependencies, rather than a simple `go get`. We don't commit the vendored packages themselves to the Hugo git repository. The call to `mage vendor` takes care of all this for you.
|
||||
Now, to make a change to Hugo's source:
|
||||
|
||||
1. Create a new branch for your changes (the branch name is arbitrary):
|
||||
|
||||
@@ -167,7 +159,7 @@ Hugo uses [mage](https://github.com/magefile/mage) to sync vendor dependencies,
|
||||
cd $HOME/go/src/github.com/gohugoio/hugo
|
||||
```
|
||||
|
||||
To build Hugo:
|
||||
To build Hugo:
|
||||
|
||||
```bash
|
||||
mage hugo
|
||||
@@ -192,14 +184,8 @@ To list all available commands along with descriptions:
|
||||
mage -l
|
||||
```
|
||||
|
||||
### Updating the Hugo Sources
|
||||
|
||||
If you want to stay in sync with the Hugo repository, you can easily pull down
|
||||
the source changes, but you'll need to keep the vendored packages up-to-date as
|
||||
well.
|
||||
**Note:** From Hugo 0.43 we have added a build tag, `extended` that adds **SCSS support**. This needs a C compiler installed to build. You can enable this when building by:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
mage vendor
|
||||
```
|
||||
|
||||
HUGO_BUILD_TAGS=extended mage install
|
||||
````
|
||||
|
||||
Regular → Executable
+17
-14
@@ -1,23 +1,26 @@
|
||||
FROM golang:1.9.0-alpine3.6 AS build
|
||||
# GitHub: https://github.com/gohugoio
|
||||
# Twitter: https://twitter.com/gohugoio
|
||||
# Website: https://gohugo.io/
|
||||
|
||||
RUN apk add --no-cache --virtual git musl-dev
|
||||
RUN go get github.com/golang/dep/cmd/dep
|
||||
FROM golang:1.11-alpine3.7 AS build
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GO111MODULE=on
|
||||
|
||||
WORKDIR /go/src/github.com/gohugoio/hugo
|
||||
ADD . /go/src/github.com/gohugoio/hugo/
|
||||
RUN dep ensure
|
||||
RUN apk add --no-cache \
|
||||
git \
|
||||
musl-dev
|
||||
COPY . /go/src/github.com/gohugoio/hugo/
|
||||
RUN go install -ldflags '-s -w'
|
||||
|
||||
FROM alpine:3.6
|
||||
RUN \
|
||||
adduser -h /site -s /sbin/nologin -u 1000 -D hugo && \
|
||||
apk add --no-cache \
|
||||
dumb-init
|
||||
COPY --from=build /go/bin/hugo /bin/hugo
|
||||
USER hugo
|
||||
# ---
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /go/bin/hugo /hugo
|
||||
WORKDIR /site
|
||||
VOLUME /site
|
||||
EXPOSE 1313
|
||||
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "hugo"]
|
||||
ENTRYPOINT [ "/hugo" ]
|
||||
CMD [ "--help" ]
|
||||
|
||||
Generated
-437
@@ -1,437 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "a368813c5e648fee92e5f6c30e3944ff9d5e8895"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/PuerkitoBio/purell"
|
||||
packages = ["."]
|
||||
revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/PuerkitoBio/urlesc"
|
||||
packages = ["."]
|
||||
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/chroma"
|
||||
packages = [
|
||||
".",
|
||||
"formatters",
|
||||
"formatters/html",
|
||||
"lexers",
|
||||
"lexers/a",
|
||||
"lexers/b",
|
||||
"lexers/c",
|
||||
"lexers/circular",
|
||||
"lexers/d",
|
||||
"lexers/e",
|
||||
"lexers/f",
|
||||
"lexers/g",
|
||||
"lexers/h",
|
||||
"lexers/i",
|
||||
"lexers/internal",
|
||||
"lexers/j",
|
||||
"lexers/k",
|
||||
"lexers/l",
|
||||
"lexers/m",
|
||||
"lexers/n",
|
||||
"lexers/o",
|
||||
"lexers/p",
|
||||
"lexers/q",
|
||||
"lexers/r",
|
||||
"lexers/s",
|
||||
"lexers/t",
|
||||
"lexers/v",
|
||||
"lexers/w",
|
||||
"lexers/x",
|
||||
"lexers/y",
|
||||
"styles"
|
||||
]
|
||||
revision = "6b1131c0069211a7729895c26f8c51765a4baf25"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/bep/debounce"
|
||||
packages = ["."]
|
||||
revision = "844797fa1dd9ba969d71b62797ff19d1e49d4eac"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/bep/gitmap"
|
||||
packages = ["."]
|
||||
revision = "012701e8669671499fc43e9792335a1dcbfe2afb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/chaseadamsio/goorgeous"
|
||||
packages = ["."]
|
||||
revision = "dcf1ef873b8987bf12596fe6951c48347986eb2f"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cpuguy83/go-md2man"
|
||||
packages = ["md2man"]
|
||||
revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1"
|
||||
version = "v1.0.8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/danwakefield/fnmatch"
|
||||
packages = ["."]
|
||||
revision = "cbb64ac3d964b81592e64f957ad53df015803288"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/disintegration/imaging"
|
||||
packages = ["."]
|
||||
revision = "dd50a3ee9985ccd313a2f03c398fcaedc96dc707"
|
||||
version = "v1.2.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dlclark/regexp2"
|
||||
packages = [
|
||||
".",
|
||||
"syntax"
|
||||
]
|
||||
revision = "487489b64fb796de2e55f4e8a4ad1e145f80e957"
|
||||
version = "v1.1.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/eknkc/amber"
|
||||
packages = [
|
||||
".",
|
||||
"parser"
|
||||
]
|
||||
revision = "cdade1c073850f4ffc70a829e31235ea6892853b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fortytw2/leaktest"
|
||||
packages = ["."]
|
||||
revision = "a5ef70473c97b71626b9abeda80ee92ba2a7de9e"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
|
||||
version = "v1.4.7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobwas/glob"
|
||||
packages = [
|
||||
".",
|
||||
"compiler",
|
||||
"match",
|
||||
"syntax",
|
||||
"syntax/ast",
|
||||
"syntax/lexer",
|
||||
"util/runes",
|
||||
"util/strings"
|
||||
]
|
||||
revision = "5ccd90ef52e1e632236f7326478d4faa74f99438"
|
||||
version = "v0.2.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-immutable-radix"
|
||||
packages = ["."]
|
||||
revision = "7f3cd4390caab3250a57f30efdb2a65dd7649ecf"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/golang-lru"
|
||||
packages = ["simplelru"]
|
||||
revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl/ast",
|
||||
"hcl/parser",
|
||||
"hcl/printer",
|
||||
"hcl/scanner",
|
||||
"hcl/strconv",
|
||||
"hcl/token",
|
||||
"json/parser",
|
||||
"json/scanner",
|
||||
"json/token"
|
||||
]
|
||||
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jdkato/prose"
|
||||
packages = [
|
||||
"internal/util",
|
||||
"transform"
|
||||
]
|
||||
revision = "20d3663d4bc9dd10d75abcde9d92e04b4861c674"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/kyokomi/emoji"
|
||||
packages = ["."]
|
||||
revision = "7e06b236c489543f53868841f188a294e3383eab"
|
||||
version = "v1.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/magefile/mage"
|
||||
packages = [
|
||||
"mg",
|
||||
"sh"
|
||||
]
|
||||
revision = "2f974307b636f59c13b88704cf350a4772fef271"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6"
|
||||
version = "v1.7.6"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/markbates/inflect"
|
||||
packages = ["."]
|
||||
revision = "a12c3aec81a6a938bf584a4bac567afed9256586"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/miekg/mmark"
|
||||
packages = ["."]
|
||||
revision = "fd2f6c1403b37925bd7fe13af05853b8ae58ee5f"
|
||||
version = "v1.3.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
revision = "00c29f56e2386353d58c599509e8dc3801b0d716"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/muesli/smartcrop"
|
||||
packages = [
|
||||
".",
|
||||
"options"
|
||||
]
|
||||
revision = "f6ebaa786a12a0fdb2d7c6dee72808e68c296464"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
packages = [
|
||||
"i18n/bundle",
|
||||
"i18n/language",
|
||||
"i18n/translation"
|
||||
]
|
||||
revision = "0dc1626d56435e9d605a29875701721c54bc9bbd"
|
||||
version = "v1.10.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
packages = ["."]
|
||||
revision = "b8a9be070da40449e501c3c4730a889e42d87a9e"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
packages = ["."]
|
||||
revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5"
|
||||
version = "v1.5.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sanity-io/litter"
|
||||
packages = ["."]
|
||||
revision = "ae543b7ba8fd6af63e4976198f146e1348ae53c1"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||
packages = ["."]
|
||||
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem"
|
||||
]
|
||||
revision = "63644898a8da0bc22138abf860edaf5277b6102e"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
revision = "8965335b8c7107321228e3e3702cab9832751bac"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = [
|
||||
".",
|
||||
"doc"
|
||||
]
|
||||
revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/fsync"
|
||||
packages = ["."]
|
||||
revision = "12a01e648f05a938100a26858d2d59a120307a18"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/nitro"
|
||||
packages = ["."]
|
||||
revision = "24d7ef30a12da0bdc5e2eb370a79c659ddccf0e8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"require"
|
||||
]
|
||||
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/yosssi/ace"
|
||||
packages = ["."]
|
||||
revision = "ea038f4770b6746c3f8f84f14fa60d9fe1205b56"
|
||||
version = "v0.0.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/image"
|
||||
packages = [
|
||||
"bmp",
|
||||
"draw",
|
||||
"math/f64",
|
||||
"riff",
|
||||
"tiff",
|
||||
"tiff/lzw",
|
||||
"vp8",
|
||||
"vp8l",
|
||||
"webp"
|
||||
]
|
||||
revision = "f315e440302883054d0c2bd85486878cb4f8572c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"idna"
|
||||
]
|
||||
revision = "61147c48b25b599e5b561d2e9c4f3e1ef489ca41"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "3b87a42e500a6dc65dae1a55d0b641295971163e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/language",
|
||||
"internal/language/compact",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
"width"
|
||||
]
|
||||
revision = "2cb43934f0eece38629746959acc633cba083fe4"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "b9b38ece73fd6c0d0b20eb2ff85dfcc15a82841223b4403566712fd7b7ae7d59"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-151
@@ -1,151 +0,0 @@
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/PuerkitoBio/purell"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/alecthomas/chroma"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/bep/gitmap"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/chaseadamsio/goorgeous"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/disintegration/imaging"
|
||||
version = "~v1.2.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/magefile/mage"
|
||||
version = "v1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/eknkc/amber"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fortytw2/leaktest"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
version = "^1.4.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/websocket"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-immutable-radix"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jdkato/prose"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/kyokomi/emoji"
|
||||
version = "1.5.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/markbates/inflect"
|
||||
revision = "a12c3aec81a6a938bf584a4bac567afed9256586"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/miekg/mmark"
|
||||
version = "^1.3.6"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
version = "^1.10.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/afero"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cast"
|
||||
version = "^1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
version = "^0.0.1"
|
||||
name = "github.com/spf13/cobra"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/fsync"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/nitro"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/pflag"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/viper"
|
||||
version = "1.0.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/yosssi/ace"
|
||||
version = "^0.0.5"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/image"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/text"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobwas/glob"
|
||||
version = "0.2.2"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/muesli/smartcrop"
|
||||
branch = "master"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sanity-io/litter"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/bep/debounce"
|
||||
version = "^1.1.0"
|
||||
@@ -4,16 +4,15 @@ A Fast and Flexible Static Site Generator built with love by [bep](https://githu
|
||||
|
||||
[Website](https://gohugo.io) |
|
||||
[Forum](https://discourse.gohugo.io) |
|
||||
[Developer Chat (no support)](https://gitter.im/gohugoio/hugo) |
|
||||
[Documentation](https://gohugo.io/overview/introduction/) |
|
||||
[Installation Guide](https://gohugo.io/overview/installing/) |
|
||||
[Developer Chat (no support)](https://gitter.im/spf13/hugo) |
|
||||
[Documentation](https://gohugo.io/getting-started/) |
|
||||
[Installation Guide](https://gohugo.io/getting-started/installing/) |
|
||||
[Contribution Guide](CONTRIBUTING.md) |
|
||||
[Twitter](http://twitter.com/gohugoio)
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://travis-ci.org/gohugoio/hugo)
|
||||
[](https://ci.appveyor.com/project/bep/hugo/branch/master)
|
||||
[](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://travis-ci.org/gohugoio/hugo)
|
||||
[](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
## Overview
|
||||
@@ -36,7 +35,7 @@ Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBS
|
||||
|
||||
Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan 9, and Solaris.
|
||||
|
||||
**Complete documentation is available at [Hugo Documentation][].**
|
||||
**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
|
||||
|
||||
## Choose How to Install
|
||||
|
||||
@@ -50,29 +49,25 @@ Building the binaries is an easy task for an experienced `go` getter.
|
||||
|
||||
### Install Hugo as Your Site Generator (Binary Install)
|
||||
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/overview/installing/).
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/).
|
||||
|
||||
### Build and Install the Binaries from Source (Advanced Install)
|
||||
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Git](http://git-scm.com/)
|
||||
* [Go (latest or previous version)](https://golang.org/dl/)
|
||||
|
||||
#### Vendored Dependencies
|
||||
|
||||
Hugo uses [dep](https://github.com/golang/dep) to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. Therefore, a simple `go get` is _not_ supported because the command is not vendor aware.
|
||||
|
||||
The simplest way is to use [mage](https://github.com/magefile/mage) (a Make alternative for Go projects.)
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Go (at least Go 1.11)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example:
|
||||
|
||||
```bash
|
||||
go get github.com/magefile/mage
|
||||
go get -d github.com/gohugoio/hugo
|
||||
cd ${GOPATH:-$HOME/go}/src/github.com/gohugoio/hugo
|
||||
mage vendor
|
||||
mage install
|
||||
mkdir $HOME/src
|
||||
cd $HOME/src
|
||||
git clone https://github.com/gohugoio/hugo.git
|
||||
cd hugo
|
||||
go install
|
||||
```
|
||||
|
||||
**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.**
|
||||
@@ -124,3 +119,71 @@ For a complete guide to contributing code to Hugo, see the [Contribution Guide](
|
||||
|
||||
[Go]: https://golang.org/
|
||||
[Hugo Documentation]: https://gohugo.io/overview/introduction/
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries, in lexical order:
|
||||
|
||||
| Dependency | License |
|
||||
| :------------- | :------------- |
|
||||
| [github.com/BurntSushi/locker](https://github.com/BurntSushi/locker) | The Unlicense |
|
||||
| [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | MIT License |
|
||||
| [github.com/PuerkitoBio/purell](https://github.com/PuerkitoBio/purell) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/PuerkitoBio/urlesc](https://github.com/PuerkitoBio/urlesc) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/alecthomas/chroma](https://github.com/alecthomas/chroma) | MIT License |
|
||||
| [github.com/bep/debounce](https://github.com/bep/debounce) | MIT License |
|
||||
| [github.com/bep/gitmap](https://github.com/bep/gitmap) | MIT License |
|
||||
| [github.com/bep/go-tocss](https://github.com/bep/go-tocss) | MIT License |
|
||||
| [github.com/chaseadamsio/goorgeous](https://github.com/chaseadamsio/goorgeous) | MIT License |
|
||||
| [github.com/cpuguy83/go-md2man](https://github.com/cpuguy83/go-md2man) | MIT License |
|
||||
| [github.com/danwakefield/fnmatch](https://github.com/danwakefield/fnmatch) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/disintegration/imaging](https://github.com/disintegration/imaging) | MIT License |
|
||||
| [github.com/dlclark/regexp2](https://github.com/dlclark/regexp2) | MIT License |
|
||||
| [github.com/eknkc/amber](https://github.com/eknkc/amber) | MIT License |
|
||||
| [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/gobwas/glob](https://github.com/gobwas/glob) | MIT License |
|
||||
| [github.com/gorilla/websocket](https://github.com/gorilla/websocket) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/hashicorp/go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) | Mozilla Public License 2.0 |
|
||||
| [github.com/hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) | Mozilla Public License 2.0 |
|
||||
| [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | Mozilla Public License 2.0 |
|
||||
| [github.com/jdkato/prose](https://github.com/jdkato/prose) | MIT License |
|
||||
| [github.com/kyokomi/emoji](https://github.com/kyokomi/emoji) | MIT License |
|
||||
| [github.com/magiconair/properties](https://github.com/magiconair/properties) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/markbates/inflect](https://github.com/markbates/inflect) | MIT License |
|
||||
| [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | MIT License |
|
||||
| [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | MIT License |
|
||||
| [github.com/miekg/mmark](https://github.com/miekg/mmark) | Simplified BSD License |
|
||||
| [github.com/mitchellh/hashstructure](https://github.com/mitchellh/hashstructure) | MIT License |
|
||||
| [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | MIT License |
|
||||
| [github.com/muesli/smartcrop](https://github.com/muesli/smartcrop) | MIT License |
|
||||
| [github.com/nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) | MIT License |
|
||||
| [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) | MIT License |
|
||||
| [github.com/pelletier/go-toml](https://github.com/pelletier/go-toml) | MIT License |
|
||||
| [github.com/pkg/errors](https://github.com/pkg/errors) | BSD 2-Clause "Simplified" License |
|
||||
| [github.com/russross/blackfriday](https://github.com/russross/blackfriday) | Simplified BSD License |
|
||||
| [github.com/shurcooL/sanitized_anchor_name](https://github.com/shurcooL/sanitized_anchor_name) | MIT License |
|
||||
| [github.com/spf13/afero](https://github.com/spf13/afero) | Apache License 2.0 |
|
||||
| [github.com/spf13/cast](https://github.com/spf13/cast) | MIT License |
|
||||
| [github.com/spf13/cobra](https://github.com/spf13/cobra) | Apache License 2.0 |
|
||||
| [github.com/spf13/fsync](https://github.com/spf13/fsync) | MIT License |
|
||||
| [github.com/spf13/jwalterweatherman](https://github.com/spf13/jwalterweatherman) | MIT License |
|
||||
| [github.com/spf13/nitro](https://github.com/spf13/nitro) | Apache License 2.0 |
|
||||
| [github.com/spf13/pflag](https://github.com/spf13/pflag) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [github.com/spf13/viper](https://github.com/spf13/viper) | MIT License |
|
||||
| [github.com/tdewolff/minify](https://github.com/tdewolff/minify) | MIT License |
|
||||
| [github.com/tdewolff/parse](https://github.com/tdewolff/parse) | MIT License |
|
||||
| [github.com/wellington/go-libsass](https://github.com/wellington/go-libsass) | Apache License 2.0 |
|
||||
| [github.com/yosssi/ace](https://github.com/yosssi/ace) | MIT License |
|
||||
| [golang.org/x/image](https://golang.org/x/image) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/net](https://golang.org/x/net) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/sync](https://golang.org/x/sync) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/sys](https://golang.org/x/sys) | BSD 3-Clause "New" or "Revised" License |
|
||||
| [golang.org/x/text](https://golang.org/x/text) | BSD 3-Clause "New" or "Revised" License
|
||||
| [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) | Apache License 2.0 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
init:
|
||||
- set PATH=%PATH%;C:\MinGW\bin;%GOPATH%\bin
|
||||
- go version
|
||||
- go env
|
||||
|
||||
# clones and cd's to path
|
||||
clone_folder: C:\GOPATH\src\github.com\gohugoio\hugo
|
||||
|
||||
install:
|
||||
- gem install asciidoctor
|
||||
- pip install docutils
|
||||
- go get github.com/magefile/mage
|
||||
|
||||
build_script:
|
||||
- mage vendor hugoRace
|
||||
- mage -v check
|
||||
- hugo -s docs/
|
||||
- hugo --renderToMemory -s docs/
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash
|
||||
Vendored
+353
@@ -0,0 +1,353 @@
|
||||
// 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 filecache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/BurntSushi/locker"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
filecacheRootDirname = "filecache"
|
||||
)
|
||||
|
||||
// Cache caches a set of files in a directory. This is usually a file on
|
||||
// disk, but since this is backed by an Afero file system, it can be anything.
|
||||
type Cache struct {
|
||||
Fs afero.Fs
|
||||
|
||||
// Max age for items in this cache. Negative duration means forever,
|
||||
// 0 is effectively turning this cache off.
|
||||
maxAge time.Duration
|
||||
|
||||
nlocker *lockTracker
|
||||
}
|
||||
|
||||
type lockTracker struct {
|
||||
seenMu sync.RWMutex
|
||||
seen map[string]struct{}
|
||||
|
||||
*locker.Locker
|
||||
}
|
||||
|
||||
// Lock tracks the ids in use. We use this information to do garbage collection
|
||||
// after a Hugo build.
|
||||
func (l *lockTracker) Lock(id string) {
|
||||
l.seenMu.RLock()
|
||||
if _, seen := l.seen[id]; !seen {
|
||||
l.seenMu.RUnlock()
|
||||
l.seenMu.Lock()
|
||||
l.seen[id] = struct{}{}
|
||||
l.seenMu.Unlock()
|
||||
} else {
|
||||
l.seenMu.RUnlock()
|
||||
}
|
||||
|
||||
l.Locker.Lock(id)
|
||||
}
|
||||
|
||||
// ItemInfo contains info about a cached file.
|
||||
type ItemInfo struct {
|
||||
// This is the file's name relative to the cache's filesystem.
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewCache creates a new file cache with the given filesystem and max age.
|
||||
func NewCache(fs afero.Fs, maxAge time.Duration) *Cache {
|
||||
return &Cache{
|
||||
Fs: fs,
|
||||
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
|
||||
maxAge: maxAge,
|
||||
}
|
||||
}
|
||||
|
||||
// lockedFile is a file with a lock that is released on Close.
|
||||
type lockedFile struct {
|
||||
afero.File
|
||||
unlock func()
|
||||
}
|
||||
|
||||
func (l *lockedFile) Close() error {
|
||||
defer l.unlock()
|
||||
return l.File.Close()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
id = cleanID(id)
|
||||
c.nlocker.Lock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
c.nlocker.Unlock(id)
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
return info, &lockedFile{
|
||||
File: f,
|
||||
unlock: func() { c.nlocker.Unlock(id) },
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadOrCreate tries to lookup the file in cache.
|
||||
// If found, it is passed to read and then closed.
|
||||
// If not found a new file is created and passed to create, which should close
|
||||
// it when done.
|
||||
func (c *Cache) ReadOrCreate(id string,
|
||||
read func(info ItemInfo, r io.Reader) error,
|
||||
create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info = ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
err = read(info, r)
|
||||
defer r.Close()
|
||||
return
|
||||
}
|
||||
|
||||
f, err := helpers.OpenFileForWriting(c.Fs, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = create(info, f)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will
|
||||
// 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) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
r, err := create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return info, hugio.ToReadCloser(r), nil
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
return info,
|
||||
hugio.ToReadCloser(&buff),
|
||||
afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff))
|
||||
}
|
||||
|
||||
// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice.
|
||||
func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
b, err := create()
|
||||
if err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
|
||||
if c.maxAge == 0 {
|
||||
return info, b, nil
|
||||
}
|
||||
|
||||
if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil {
|
||||
return info, nil, err
|
||||
}
|
||||
return info, b, nil
|
||||
|
||||
}
|
||||
|
||||
// GetBytes gets the file content with the given id from the cahce, nil if none found.
|
||||
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
if r := c.getOrRemove(id); r != nil {
|
||||
defer r.Close()
|
||||
b, err := ioutil.ReadAll(r)
|
||||
return info, b, err
|
||||
}
|
||||
|
||||
return info, nil, nil
|
||||
}
|
||||
|
||||
// Get gets the file with the given id from the cahce, nil if none found.
|
||||
func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
info := ItemInfo{Name: id}
|
||||
|
||||
r := c.getOrRemove(id)
|
||||
|
||||
return info, r, nil
|
||||
}
|
||||
|
||||
// getOrRemove gets the file with the given id. If it's expired, it will
|
||||
// be removed.
|
||||
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
|
||||
if c.maxAge == 0 {
|
||||
// No caching.
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.maxAge > 0 {
|
||||
fi, err := c.Fs.Stat(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.isExpired(fi.ModTime()) {
|
||||
c.Fs.Remove(id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (c *Cache) isExpired(modTime time.Time) bool {
|
||||
if c.maxAge < 0 {
|
||||
return false
|
||||
}
|
||||
return c.maxAge == 0 || time.Now().Sub(modTime) > c.maxAge
|
||||
}
|
||||
|
||||
// For testing
|
||||
func (c *Cache) getString(id string) string {
|
||||
id = cleanID(id)
|
||||
|
||||
c.nlocker.Lock(id)
|
||||
defer c.nlocker.Unlock(id)
|
||||
|
||||
f, err := c.Fs.Open(id)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b, _ := ioutil.ReadAll(f)
|
||||
return string(b)
|
||||
|
||||
}
|
||||
|
||||
// Caches is a named set of caches.
|
||||
type Caches map[string]*Cache
|
||||
|
||||
// Get gets a named cache, nil if none found.
|
||||
func (f Caches) Get(name string) *Cache {
|
||||
return f[strings.ToLower(name)]
|
||||
}
|
||||
|
||||
// NewCaches creates a new set of file caches from the given
|
||||
// configuration.
|
||||
func NewCaches(p *helpers.PathSpec) (Caches, error) {
|
||||
dcfg, err := decodeConfig(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs := p.Fs.Source
|
||||
|
||||
m := make(Caches)
|
||||
for k, v := range dcfg {
|
||||
var cfs afero.Fs
|
||||
|
||||
if v.isResourceDir {
|
||||
cfs = p.BaseFs.Resources.Fs
|
||||
} else {
|
||||
cfs = fs
|
||||
}
|
||||
|
||||
var baseDir string
|
||||
if !strings.HasPrefix(v.Dir, "_gen") {
|
||||
// We do cache eviction (file removes) and since the user can set
|
||||
// his/hers own cache directory, we really want to make sure
|
||||
// we do not delete any files that do not belong to this cache.
|
||||
// We do add the cache name as the root, but this is an extra safe
|
||||
// guard. We skip the files inside /resources/_gen/ because
|
||||
// that would be breaking.
|
||||
baseDir = filepath.Join(v.Dir, filecacheRootDirname, k)
|
||||
} else {
|
||||
baseDir = filepath.Join(v.Dir, k)
|
||||
}
|
||||
if err = cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bfs := afero.NewBasePathFs(cfs, baseDir)
|
||||
|
||||
m[k] = NewCache(bfs, v.MaxAge)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func cleanID(name string) string {
|
||||
return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator)
|
||||
}
|
||||
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
// 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 filecache
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const (
|
||||
cachesConfigKey = "caches"
|
||||
|
||||
resourcesGenDir = ":resourceDir/_gen"
|
||||
)
|
||||
|
||||
var defaultCacheConfig = cacheConfig{
|
||||
MaxAge: -1, // Never expire
|
||||
Dir: ":cacheDir/:project",
|
||||
}
|
||||
|
||||
const (
|
||||
cacheKeyGetJSON = "getjson"
|
||||
cacheKeyGetCSV = "getcsv"
|
||||
cacheKeyImages = "images"
|
||||
cacheKeyAssets = "assets"
|
||||
)
|
||||
|
||||
var defaultCacheConfigs = map[string]cacheConfig{
|
||||
cacheKeyGetJSON: defaultCacheConfig,
|
||||
cacheKeyGetCSV: defaultCacheConfig,
|
||||
cacheKeyImages: cacheConfig{
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
cacheKeyAssets: cacheConfig{
|
||||
MaxAge: -1,
|
||||
Dir: resourcesGenDir,
|
||||
},
|
||||
}
|
||||
|
||||
type cachesConfig map[string]cacheConfig
|
||||
|
||||
type cacheConfig struct {
|
||||
// Max age of cache entries in this cache. Any items older than this will
|
||||
// be removed and not returned from the cache.
|
||||
// a negative value means forever, 0 means cache is disabled.
|
||||
MaxAge time.Duration
|
||||
|
||||
// The directory where files are stored.
|
||||
Dir string
|
||||
|
||||
// Will resources/_gen will get its own composite filesystem that
|
||||
// also checks any theme.
|
||||
isResourceDir bool
|
||||
}
|
||||
|
||||
// GetJSONCache gets the file cache for getJSON.
|
||||
func (f Caches) GetJSONCache() *Cache {
|
||||
return f[cacheKeyGetJSON]
|
||||
}
|
||||
|
||||
// GetCSVCache gets the file cache for getCSV.
|
||||
func (f Caches) GetCSVCache() *Cache {
|
||||
return f[cacheKeyGetCSV]
|
||||
}
|
||||
|
||||
// ImageCache gets the file cache for processed images.
|
||||
func (f Caches) ImageCache() *Cache {
|
||||
return f[cacheKeyImages]
|
||||
}
|
||||
|
||||
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
|
||||
func (f Caches) AssetsCache() *Cache {
|
||||
return f[cacheKeyAssets]
|
||||
}
|
||||
|
||||
func decodeConfig(p *helpers.PathSpec) (cachesConfig, error) {
|
||||
c := make(cachesConfig)
|
||||
valid := make(map[string]bool)
|
||||
// Add defaults
|
||||
for k, v := range defaultCacheConfigs {
|
||||
c[k] = v
|
||||
valid[k] = true
|
||||
}
|
||||
|
||||
cfg := p.Cfg
|
||||
|
||||
m := cfg.GetStringMap(cachesConfigKey)
|
||||
|
||||
_, isOsFs := p.Fs.Source.(*afero.OsFs)
|
||||
|
||||
for k, v := range m {
|
||||
cc := defaultCacheConfig
|
||||
|
||||
dc := &mapstructure.DecoderConfig{
|
||||
Result: &cc,
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(dc)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
if err := decoder.Decode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.Dir == "" {
|
||||
return c, errors.New("must provide cache Dir")
|
||||
}
|
||||
|
||||
name := strings.ToLower(k)
|
||||
if !valid[name] {
|
||||
return nil, errors.Errorf("%q is not a valid cache name", name)
|
||||
}
|
||||
|
||||
c[name] = cc
|
||||
}
|
||||
|
||||
// This is a very old flag in Hugo, but we need to respect it.
|
||||
disabled := cfg.GetBool("ignoreCache")
|
||||
|
||||
for k, v := range c {
|
||||
dir := filepath.ToSlash(filepath.Clean(v.Dir))
|
||||
hadSlash := strings.HasPrefix(dir, "/")
|
||||
parts := strings.Split(dir, "/")
|
||||
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
resolved, isResource, err := resolveDirPlaceholder(p, part)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
if isResource {
|
||||
v.isResourceDir = true
|
||||
}
|
||||
parts[i] = resolved
|
||||
}
|
||||
}
|
||||
|
||||
dir = path.Join(parts...)
|
||||
if hadSlash {
|
||||
dir = "/" + dir
|
||||
}
|
||||
v.Dir = filepath.Clean(filepath.FromSlash(dir))
|
||||
|
||||
if !v.isResourceDir {
|
||||
if isOsFs && !filepath.IsAbs(v.Dir) {
|
||||
return c, errors.Errorf("%q must resolve to an absolute directory", v.Dir)
|
||||
}
|
||||
|
||||
// Avoid cache in root, e.g. / (Unix) or c:\ (Windows)
|
||||
if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 {
|
||||
return c, errors.Errorf("%q is a root folder and not allowed as cache dir", v.Dir)
|
||||
}
|
||||
}
|
||||
|
||||
if disabled {
|
||||
v.MaxAge = 0
|
||||
}
|
||||
|
||||
c[k] = v
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
|
||||
func resolveDirPlaceholder(p *helpers.PathSpec, placeholder string) (cacheDir string, isResource bool, err error) {
|
||||
switch strings.ToLower(placeholder) {
|
||||
case ":resourcedir":
|
||||
return "", true, nil
|
||||
case ":cachedir":
|
||||
d, err := helpers.GetCacheDir(p.Fs.Source, p.Cfg)
|
||||
return d, false, err
|
||||
case ":project":
|
||||
return filepath.Base(p.WorkingDir), false, nil
|
||||
}
|
||||
|
||||
return "", false, errors.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// 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 filecache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecodeConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archetypeDir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10m"
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = "11h"
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := hugofs.NewMem(cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
decoded, err := decodeConfig(p)
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(4, len(decoded))
|
||||
|
||||
c2 := decoded["getcsv"]
|
||||
assert.Equal("11h0m0s", c2.MaxAge.String())
|
||||
assert.Equal(filepath.FromSlash("/path/to/c2"), c2.Dir)
|
||||
|
||||
c3 := decoded["images"]
|
||||
assert.Equal(time.Duration(-1), c3.MaxAge)
|
||||
assert.Equal(filepath.FromSlash("/path/to/c3"), c3.Dir)
|
||||
|
||||
}
|
||||
|
||||
func TestDecodeConfigIgnoreCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
ignoreCache = true
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = 1234
|
||||
dir = "/path/to/c1"
|
||||
[caches.getCSV]
|
||||
maxAge = 3456
|
||||
dir = "/path/to/c2"
|
||||
[caches.images]
|
||||
dir = "/path/to/c3"
|
||||
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := hugofs.NewMem(cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
decoded, err := decodeConfig(p)
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(4, len(decoded))
|
||||
|
||||
for _, v := range decoded {
|
||||
assert.Equal(time.Duration(0), v.MaxAge)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDecodeConfigDefault(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
cfg := newTestConfig()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cfg.Set("resourceDir", "c:\\cache\\resources")
|
||||
cfg.Set("cacheDir", "c:\\cache\\thecache")
|
||||
|
||||
} else {
|
||||
cfg.Set("resourceDir", "/cache/resources")
|
||||
cfg.Set("cacheDir", "/cache/thecache")
|
||||
}
|
||||
|
||||
fs := hugofs.NewMem(cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
decoded, err := decodeConfig(p)
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
assert.Equal(4, len(decoded))
|
||||
|
||||
imgConfig := decoded[cacheKeyImages]
|
||||
jsonConfig := decoded[cacheKeyGetJSON]
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
assert.Equal("_gen", imgConfig.Dir)
|
||||
} else {
|
||||
assert.Equal("_gen", imgConfig.Dir)
|
||||
assert.Equal("/cache/thecache/hugoproject", jsonConfig.Dir)
|
||||
}
|
||||
|
||||
assert.True(imgConfig.isResourceDir)
|
||||
assert.False(jsonConfig.isResourceDir)
|
||||
}
|
||||
|
||||
func TestDecodeConfigInvalidDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10m"
|
||||
dir = "/"
|
||||
|
||||
`
|
||||
if runtime.GOOS == "windows" {
|
||||
configStr = strings.Replace(configStr, "/", "c:\\\\", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := hugofs.NewMem(cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
_, err = decodeConfig(p)
|
||||
assert.Error(err)
|
||||
|
||||
}
|
||||
|
||||
func newTestConfig() *viper.Viper {
|
||||
cfg := viper.New()
|
||||
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
|
||||
cfg.Set("contentDir", "content")
|
||||
cfg.Set("dataDir", "data")
|
||||
cfg.Set("resourceDir", "resources")
|
||||
cfg.Set("i18nDir", "i18n")
|
||||
cfg.Set("layoutDir", "layouts")
|
||||
cfg.Set("archetypeDir", "archetypes")
|
||||
cfg.Set("assetDir", "assets")
|
||||
|
||||
return cfg
|
||||
}
|
||||
Vendored
+80
@@ -0,0 +1,80 @@
|
||||
// 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 filecache
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Prune removes expired and unused items from this cache.
|
||||
// The last one requires a full build so the cache usage can be tracked.
|
||||
// Note that we operate directly on the filesystem here, so this is not
|
||||
// thread safe.
|
||||
func (c Caches) Prune() (int, error) {
|
||||
counter := 0
|
||||
for k, cache := range c {
|
||||
err := afero.Walk(cache.Fs, "", func(name string, info os.FileInfo, err error) error {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
name = cleanID(name)
|
||||
|
||||
if info.IsDir() {
|
||||
f, err := cache.Fs.Open(name)
|
||||
if err != nil {
|
||||
// This cache dir may not exist.
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Readdirnames(1)
|
||||
if err == io.EOF {
|
||||
// Empty dir.
|
||||
return cache.Fs.Remove(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
shouldRemove := cache.isExpired(info.ModTime())
|
||||
|
||||
if !shouldRemove && len(cache.nlocker.seen) > 0 {
|
||||
// Remove it if it's not been touched/used in the last build.
|
||||
_, seen := cache.nlocker.seen[name]
|
||||
shouldRemove = !seen
|
||||
}
|
||||
|
||||
if shouldRemove {
|
||||
err := cache.Fs.Remove(name)
|
||||
if err == nil {
|
||||
counter++
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return counter, errors.Wrapf(err, "failed to prune cache %q", k)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return counter, nil
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// 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 filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPrune(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/c"
|
||||
[caches.getcsv]
|
||||
maxAge = "200ms"
|
||||
dir = "/cache/d"
|
||||
[caches.assets]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
[caches.images]
|
||||
maxAge = "200ms"
|
||||
dir = ":resourceDir/_gen"
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
|
||||
for _, name := range []string{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} {
|
||||
msg := fmt.Sprintf("cache: %s", name)
|
||||
fs := hugofs.NewMem(cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
caches, err := NewCaches(p)
|
||||
assert.NoError(err)
|
||||
cache := caches[name]
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
cache.GetOrCreateBytes(id, func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
if i == 4 {
|
||||
// This will expire the first 5
|
||||
time.Sleep(201 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := caches.Prune()
|
||||
assert.NoError(err)
|
||||
assert.Equal(5, count, msg)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.getString(id)
|
||||
if i < 5 {
|
||||
assert.Equal("", v, id)
|
||||
} else {
|
||||
assert.Equal("abc", v, id)
|
||||
}
|
||||
}
|
||||
|
||||
caches, err = NewCaches(p)
|
||||
assert.NoError(err)
|
||||
cache = caches[name]
|
||||
// Touch one and then prune.
|
||||
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
|
||||
return []byte("abc"), nil
|
||||
})
|
||||
|
||||
count, err = caches.Prune()
|
||||
assert.NoError(err)
|
||||
assert.Equal(4, count)
|
||||
|
||||
// Now only the i5 should be left.
|
||||
for i := 0; i < 10; i++ {
|
||||
id := fmt.Sprintf("i%d", i)
|
||||
v := cache.getString(id)
|
||||
if i != 5 {
|
||||
assert.Equal("", v, id)
|
||||
} else {
|
||||
assert.Equal("abc", v, id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+257
@@ -0,0 +1,257 @@
|
||||
// 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 filecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFileCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
tempWorkingDir, err := ioutil.TempDir("", "hugo_filecache_test_work")
|
||||
assert.NoError(err)
|
||||
defer os.Remove(tempWorkingDir)
|
||||
|
||||
tempCacheDir, err := ioutil.TempDir("", "hugo_filecache_test_cache")
|
||||
assert.NoError(err)
|
||||
defer os.Remove(tempCacheDir)
|
||||
|
||||
osfs := afero.NewOsFs()
|
||||
|
||||
for _, test := range []struct {
|
||||
cacheDir string
|
||||
workingDir string
|
||||
}{
|
||||
// Run with same dirs twice to make sure that works.
|
||||
{tempCacheDir, tempWorkingDir},
|
||||
{tempCacheDir, tempWorkingDir},
|
||||
} {
|
||||
|
||||
configStr := `
|
||||
workingDir = "WORKING_DIR"
|
||||
resourceDir = "resources"
|
||||
cacheDir = "CACHEDIR"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getJSON]
|
||||
maxAge = "10h"
|
||||
dir = ":cacheDir/c"
|
||||
|
||||
`
|
||||
|
||||
winPathSep := "\\\\"
|
||||
|
||||
replacer := strings.NewReplacer("CACHEDIR", test.cacheDir, "WORKING_DIR", test.workingDir)
|
||||
|
||||
configStr = replacer.Replace(configStr)
|
||||
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
|
||||
fs := hugofs.NewFrom(osfs, cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
caches, err := NewCaches(p)
|
||||
assert.NoError(err)
|
||||
|
||||
cache := caches.Get("GetJSON")
|
||||
assert.NotNil(cache)
|
||||
assert.Equal("10h0m0s", cache.maxAge.String())
|
||||
|
||||
bfs, ok := cache.Fs.(*afero.BasePathFs)
|
||||
assert.True(ok)
|
||||
filename, err := bfs.RealPath("key")
|
||||
assert.NoError(err)
|
||||
if test.cacheDir != "" {
|
||||
assert.Equal(filepath.Join(test.cacheDir, "c/"+filecacheRootDirname+"/getjson/key"), filename)
|
||||
} else {
|
||||
// Temp dir.
|
||||
assert.Regexp(regexp.MustCompile(".*hugo_cache.*"+filecacheRootDirname+".*key"), filename)
|
||||
}
|
||||
|
||||
cache = caches.Get("Images")
|
||||
assert.NotNil(cache)
|
||||
assert.Equal(time.Duration(-1), cache.maxAge)
|
||||
bfs, ok = cache.Fs.(*afero.BasePathFs)
|
||||
assert.True(ok)
|
||||
filename, _ = bfs.RealPath("key")
|
||||
assert.Equal(filepath.FromSlash("_gen/images/key"), filename)
|
||||
|
||||
rf := func(s string) func() (io.ReadCloser, error) {
|
||||
return func() (io.ReadCloser, error) {
|
||||
return struct {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}{
|
||||
strings.NewReader(s),
|
||||
ioutil.NopCloser(nil),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
bf := func() ([]byte, error) {
|
||||
return []byte("bcd"), nil
|
||||
}
|
||||
|
||||
for _, c := range []*Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
|
||||
for i := 0; i < 2; i++ {
|
||||
info, r, err := c.GetOrCreate("a", rf("abc"))
|
||||
assert.NoError(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("a", info.Name)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal("abc", string(b))
|
||||
|
||||
info, b, err = c.GetOrCreateBytes("b", bf)
|
||||
assert.NoError(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("b", info.Name)
|
||||
assert.Equal("bcd", string(b))
|
||||
|
||||
_, b, err = c.GetOrCreateBytes("a", bf)
|
||||
assert.NoError(err)
|
||||
assert.Equal("abc", string(b))
|
||||
|
||||
_, r, err = c.GetOrCreate("a", rf("bcd"))
|
||||
assert.NoError(err)
|
||||
b, _ = ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal("abc", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
assert.NotNil(caches.Get("getJSON"))
|
||||
|
||||
info, w, err := caches.ImageCache().WriteCloser("mykey")
|
||||
assert.NoError(err)
|
||||
assert.Equal("mykey", info.Name)
|
||||
io.WriteString(w, "Hugo is great!")
|
||||
w.Close()
|
||||
assert.Equal("Hugo is great!", caches.ImageCache().getString("mykey"))
|
||||
|
||||
info, r, err := caches.ImageCache().Get("mykey")
|
||||
assert.NoError(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("mykey", info.Name)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal("Hugo is great!", string(b))
|
||||
|
||||
info, b, err = caches.ImageCache().GetBytes("mykey")
|
||||
assert.NoError(err)
|
||||
assert.Equal("mykey", info.Name)
|
||||
assert.Equal("Hugo is great!", string(b))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFileCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
configStr := `
|
||||
resourceDir = "myresources"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
i18nDir = "i18n"
|
||||
layoutDir = "layouts"
|
||||
assetDir = "assets"
|
||||
archeTypedir = "archetypes"
|
||||
|
||||
[caches]
|
||||
[caches.getjson]
|
||||
maxAge = "1s"
|
||||
dir = "/cache/c"
|
||||
|
||||
`
|
||||
|
||||
cfg, err := config.FromConfigString(configStr, "toml")
|
||||
assert.NoError(err)
|
||||
fs := hugofs.NewMem(cfg)
|
||||
p, err := helpers.NewPathSpec(fs, cfg)
|
||||
assert.NoError(err)
|
||||
|
||||
caches, err := NewCaches(p)
|
||||
assert.NoError(err)
|
||||
|
||||
const cacheName = "getjson"
|
||||
|
||||
filenameData := func(i int) (string, string) {
|
||||
data := fmt.Sprintf("data: %d", i)
|
||||
filename := fmt.Sprintf("file%d", i)
|
||||
return filename, data
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 20; j++ {
|
||||
c := caches.Get(cacheName)
|
||||
assert.NotNil(c)
|
||||
filename, data := filenameData(i)
|
||||
_, r, err := c.GetOrCreate(filename, func() (io.ReadCloser, error) {
|
||||
return hugio.ToReadCloser(strings.NewReader(data)), nil
|
||||
})
|
||||
assert.NoError(err)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
r.Close()
|
||||
assert.Equal(data, string(b))
|
||||
// Trigger some expiration.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}(i)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCleanID(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
assert.Equal(filepath.FromSlash("a/b/c.txt"), cleanID(filepath.FromSlash("/a/b//c.txt")))
|
||||
assert.Equal(filepath.FromSlash("a/b/c.txt"), cleanID(filepath.FromSlash("a/b//c.txt")))
|
||||
}
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
// 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 interface{}
|
||||
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 invoced only once for this cache.
|
||||
func (c *Cache) GetOrCreate(key string, create func() (interface{}, error)) (interface{}, 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)
|
||||
|
||||
// Double check
|
||||
if entry, found := c.cache[key]; found {
|
||||
return entry.value, entry.err
|
||||
}
|
||||
|
||||
// Create it.
|
||||
value, err := create()
|
||||
|
||||
c.mu.Lock()
|
||||
c.cache[key] = cacheEntry{value: value, err: err}
|
||||
c.mu.Unlock()
|
||||
|
||||
return value, err
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// 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"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNamedCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
cache := New()
|
||||
|
||||
counter := 0
|
||||
create := func() (interface{}, error) {
|
||||
counter++
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
v1, err := cache.GetOrCreate("a1", create)
|
||||
assert.NoError(err)
|
||||
assert.Equal(1, v1)
|
||||
v2, err := cache.GetOrCreate("a2", create)
|
||||
assert.NoError(err)
|
||||
assert.Equal(2, v2)
|
||||
}
|
||||
|
||||
cache.Clear()
|
||||
|
||||
v3, err := cache.GetOrCreate("a2", create)
|
||||
assert.NoError(err)
|
||||
assert.Equal(3, v3)
|
||||
}
|
||||
|
||||
func TestNamedCacheConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cache := New()
|
||||
|
||||
create := func(i int) func() (interface{}, error) {
|
||||
return func() (interface{}, 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))
|
||||
assert.NoError(err)
|
||||
assert.Equal(j, v)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
Vendored
+30
-11
@@ -24,34 +24,52 @@ type Partition struct {
|
||||
Load func() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
type lazyPartition struct {
|
||||
// Lazy represents a lazily loaded cache.
|
||||
type Lazy struct {
|
||||
initSync sync.Once
|
||||
initErr error
|
||||
cache map[string]interface{}
|
||||
load func() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
func (l *lazyPartition) init() error {
|
||||
var err error
|
||||
// NewLazy creates a lazy cache with the given load func.
|
||||
func NewLazy(load func() (map[string]interface{}, error)) *Lazy {
|
||||
return &Lazy{load: load}
|
||||
}
|
||||
|
||||
func (l *Lazy) init() error {
|
||||
l.initSync.Do(func() {
|
||||
var c map[string]interface{}
|
||||
c, err = l.load()
|
||||
c, err := l.load()
|
||||
l.cache = c
|
||||
l.initErr = err
|
||||
|
||||
})
|
||||
|
||||
return err
|
||||
return l.initErr
|
||||
}
|
||||
|
||||
// Get initializes the cache if not already initialized, then looks up the
|
||||
// given key.
|
||||
func (l *Lazy) Get(key string) (interface{}, bool, error) {
|
||||
l.init()
|
||||
if l.initErr != nil {
|
||||
return nil, false, l.initErr
|
||||
}
|
||||
v, found := l.cache[key]
|
||||
return v, found, nil
|
||||
}
|
||||
|
||||
// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key.
|
||||
type PartitionedLazyCache struct {
|
||||
partitions map[string]*lazyPartition
|
||||
partitions map[string]*Lazy
|
||||
}
|
||||
|
||||
// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied
|
||||
// partitions.
|
||||
func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache {
|
||||
lazyPartitions := make(map[string]*lazyPartition, len(partitions))
|
||||
lazyPartitions := make(map[string]*Lazy, len(partitions))
|
||||
for _, partition := range partitions {
|
||||
lazyPartitions[partition.Key] = &lazyPartition{load: partition.Load}
|
||||
lazyPartitions[partition.Key] = NewLazy(partition.Load)
|
||||
}
|
||||
cache := &PartitionedLazyCache{partitions: lazyPartitions}
|
||||
|
||||
@@ -67,11 +85,12 @@ func (c *PartitionedLazyCache) Get(partition, key string) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := p.init(); err != nil {
|
||||
v, found, err := p.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, found := p.cache[key]; found {
|
||||
if found {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
type benchmarkCmd struct {
|
||||
benchmarkTimes int
|
||||
cpuProfileFile string
|
||||
memProfileFile string
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func (b *commandsBuilder) newBenchmarkCmd() *benchmarkCmd {
|
||||
cmd := &cobra.Command{
|
||||
Use: "benchmark",
|
||||
Short: "Benchmark Hugo by building a site a number of times.",
|
||||
Long: `Hugo can build a site many times over and analyze the running process
|
||||
creating a benchmark.`,
|
||||
}
|
||||
|
||||
c := &benchmarkCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
|
||||
|
||||
cmd.Flags().StringVar(&c.cpuProfileFile, "cpuprofile", "", "path/filename for the CPU profile file")
|
||||
cmd.Flags().StringVar(&c.memProfileFile, "memprofile", "", "path/filename for the memory profile file")
|
||||
cmd.Flags().IntVarP(&c.benchmarkTimes, "count", "n", 13, "number of times to build the site")
|
||||
cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
|
||||
|
||||
cmd.RunE = c.benchmark
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *benchmarkCmd) benchmark(cmd *cobra.Command, args []string) error {
|
||||
cfgInit := func(c *commandeer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
comm, err := initializeConfig(false, &c.hugoBuilderCommon, c, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var memProf *os.File
|
||||
if c.memProfileFile != "" {
|
||||
memProf, err = os.Create(c.memProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var cpuProf *os.File
|
||||
if c.cpuProfileFile != "" {
|
||||
cpuProf, err = os.Create(c.cpuProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
memAllocated := memStats.TotalAlloc
|
||||
mallocs := memStats.Mallocs
|
||||
if cpuProf != nil {
|
||||
pprof.StartCPUProfile(cpuProf)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
for i := 0; i < c.benchmarkTimes; i++ {
|
||||
if err = comm.resetAndBuildSites(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
totalTime := time.Since(t)
|
||||
|
||||
if memProf != nil {
|
||||
pprof.WriteHeapProfile(memProf)
|
||||
memProf.Close()
|
||||
}
|
||||
if cpuProf != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProf.Close()
|
||||
}
|
||||
|
||||
runtime.ReadMemStats(&memStats)
|
||||
totalMemAllocated := memStats.TotalAlloc - memAllocated
|
||||
totalMallocs := memStats.Mallocs - mallocs
|
||||
|
||||
jww.FEEDBACK.Println()
|
||||
jww.FEEDBACK.Printf("Average time per operation: %vms\n", int(1000*totalTime.Seconds()/float64(c.benchmarkTimes)))
|
||||
jww.FEEDBACK.Printf("Average memory allocated per operation: %vkB\n", totalMemAllocated/uint64(c.benchmarkTimes)/1024)
|
||||
jww.FEEDBACK.Printf("Average allocations per operation: %v\n", totalMallocs/uint64(c.benchmarkTimes))
|
||||
|
||||
return nil
|
||||
}
|
||||
+215
-66
@@ -14,58 +14,112 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gohugoio/hugo/utils"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
src "github.com/gohugoio/hugo/source"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
)
|
||||
|
||||
type commandeer struct {
|
||||
type commandeerHugoState struct {
|
||||
*deps.DepsCfg
|
||||
hugo *hugolib.HugoSites
|
||||
fsCreate sync.Once
|
||||
}
|
||||
|
||||
hugo *hugolib.HugoSites
|
||||
type commandeer struct {
|
||||
*commandeerHugoState
|
||||
|
||||
logger *loggers.Logger
|
||||
|
||||
// Currently only set when in "fast render mode". But it seems to
|
||||
// be fast enough that we could maybe just add it for all server modes.
|
||||
changeDetector *fileChangeDetector
|
||||
|
||||
// We need to reuse this on server rebuilds.
|
||||
destinationFs afero.Fs
|
||||
|
||||
h *hugoBuilderCommon
|
||||
ftch flagsToConfigHandler
|
||||
|
||||
pathSpec *helpers.PathSpec
|
||||
visitedURLs *types.EvictingStringQueue
|
||||
|
||||
staticDirsConfig []*src.Dirs
|
||||
|
||||
// We watch these for changes.
|
||||
configFiles []string
|
||||
|
||||
doWithCommandeer func(c *commandeer) error
|
||||
|
||||
// We can do this only once.
|
||||
fsCreate sync.Once
|
||||
// We watch these for changes.
|
||||
configFiles []string
|
||||
|
||||
// Used in cases where we get flooded with events in server mode.
|
||||
debounce func(f func())
|
||||
|
||||
serverPorts []int
|
||||
languagesConfigured bool
|
||||
languages helpers.Languages
|
||||
languages langs.Languages
|
||||
doLiveReload bool
|
||||
fastRenderMode bool
|
||||
showErrorInBrowser bool
|
||||
|
||||
configured bool
|
||||
paused bool
|
||||
|
||||
// Any error from the last build.
|
||||
buildErr error
|
||||
}
|
||||
|
||||
func (c *commandeer) errCount() int {
|
||||
return int(c.logger.ErrorCounter.Count())
|
||||
}
|
||||
|
||||
func (c *commandeer) getErrorWithContext() interface{} {
|
||||
errCount := c.errCount()
|
||||
|
||||
if errCount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
|
||||
m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors()))
|
||||
m["Version"] = hugo.BuildVersionString()
|
||||
|
||||
fe := herrors.UnwrapErrorWithFileContext(c.buildErr)
|
||||
if fe != nil {
|
||||
m["File"] = fe
|
||||
}
|
||||
|
||||
if c.h.verbose {
|
||||
var b bytes.Buffer
|
||||
herrors.FprintStackTrace(&b, c.buildErr)
|
||||
m["StackTrace"] = b.String()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *commandeer) Set(key string, value interface{}) {
|
||||
@@ -75,31 +129,14 @@ func (c *commandeer) Set(key string, value interface{}) {
|
||||
c.Cfg.Set(key, value)
|
||||
}
|
||||
|
||||
// PathSpec lazily creates a new PathSpec, as all the paths must
|
||||
// be configured before it is created.
|
||||
func (c *commandeer) PathSpec() *helpers.PathSpec {
|
||||
c.configured = true
|
||||
return c.pathSpec
|
||||
}
|
||||
|
||||
func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||
c.destinationFs = fs.Destination
|
||||
c.DepsCfg.Fs = fs
|
||||
ps, err := helpers.NewPathSpec(fs, c.Cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.pathSpec = ps
|
||||
|
||||
dirsConfig, err := c.createStaticDirsConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.staticDirsConfig = dirsConfig
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCommandeer(running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
|
||||
var rebuildDebouncer func(f func())
|
||||
if running {
|
||||
@@ -110,22 +147,92 @@ func newCommandeer(running bool, h *hugoBuilderCommon, f flagsToConfigHandler, d
|
||||
}
|
||||
|
||||
c := &commandeer{
|
||||
h: h,
|
||||
ftch: f,
|
||||
doWithCommandeer: doWithCommandeer,
|
||||
visitedURLs: types.NewEvictingStringQueue(10),
|
||||
debounce: rebuildDebouncer,
|
||||
h: h,
|
||||
ftch: f,
|
||||
commandeerHugoState: &commandeerHugoState{},
|
||||
doWithCommandeer: doWithCommandeer,
|
||||
visitedURLs: types.NewEvictingStringQueue(10),
|
||||
debounce: rebuildDebouncer,
|
||||
// This will be replaced later, but we need something to log to before the configuration is read.
|
||||
logger: loggers.NewLogger(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, running),
|
||||
}
|
||||
|
||||
return c, c.loadConfig(running)
|
||||
return c, c.loadConfig(mustHaveConfigFile, running)
|
||||
}
|
||||
|
||||
func (c *commandeer) loadConfig(running bool) error {
|
||||
type fileChangeDetector struct {
|
||||
sync.Mutex
|
||||
current map[string]string
|
||||
prev map[string]string
|
||||
|
||||
irrelevantRe *regexp.Regexp
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
f.current[name] = md5sum
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) changed() []string {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
var c []string
|
||||
for k, v := range f.current {
|
||||
vv, found := f.prev[k]
|
||||
if !found || v != vv {
|
||||
c = append(c, k)
|
||||
}
|
||||
}
|
||||
|
||||
return f.filterIrrelevant(c)
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
|
||||
var filtered []string
|
||||
for _, v := range in {
|
||||
if !f.irrelevantRe.MatchString(v) {
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (f *fileChangeDetector) PrepareNew() {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
f.Lock()
|
||||
defer f.Unlock()
|
||||
|
||||
if f.current == nil {
|
||||
f.current = make(map[string]string)
|
||||
f.prev = make(map[string]string)
|
||||
return
|
||||
}
|
||||
|
||||
f.prev = make(map[string]string)
|
||||
for k, v := range f.current {
|
||||
f.prev[k] = v
|
||||
}
|
||||
f.current = make(map[string]string)
|
||||
}
|
||||
|
||||
func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
|
||||
|
||||
if c.DepsCfg == nil {
|
||||
c.DepsCfg = &deps.DepsCfg{}
|
||||
}
|
||||
|
||||
if c.logger != nil {
|
||||
// Truncate the error log if this is a reload.
|
||||
c.logger.Reset()
|
||||
}
|
||||
|
||||
cfg := c.DepsCfg
|
||||
c.configured = false
|
||||
cfg.Running = running
|
||||
@@ -142,6 +249,8 @@ func (c *commandeer) loadConfig(running bool) error {
|
||||
sourceFs = c.DepsCfg.Fs.Source
|
||||
}
|
||||
|
||||
environment := c.h.getEnvironment(running)
|
||||
|
||||
doWithConfig := func(cfg config.Provider) error {
|
||||
|
||||
if c.ftch != nil {
|
||||
@@ -149,7 +258,7 @@ func (c *commandeer) loadConfig(running bool) error {
|
||||
}
|
||||
|
||||
cfg.Set("workingDir", dir)
|
||||
|
||||
cfg.Set("environment", environment)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -162,22 +271,43 @@ func (c *commandeer) loadConfig(running bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := c.h.source
|
||||
if configPath == "" {
|
||||
configPath = dir
|
||||
}
|
||||
config, configFiles, err := hugolib.LoadConfig(
|
||||
hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: c.h.source, WorkingDir: dir, Filename: c.h.cfgFile},
|
||||
hugolib.ConfigSourceDescriptor{
|
||||
Fs: sourceFs,
|
||||
Path: configPath,
|
||||
WorkingDir: dir,
|
||||
Filename: c.h.cfgFile,
|
||||
AbsConfigDir: c.h.getConfigDir(dir),
|
||||
Environment: environment},
|
||||
doWithCommandeer,
|
||||
doWithConfig)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
if mustHaveConfigFile {
|
||||
return err
|
||||
}
|
||||
if err != hugolib.ErrNoConfigFile {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
c.configFiles = configFiles
|
||||
|
||||
if l, ok := c.Cfg.Get("languagesSorted").(helpers.Languages); ok {
|
||||
if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
|
||||
c.languagesConfigured = true
|
||||
c.languages = l
|
||||
}
|
||||
|
||||
// Set some commonly used flags
|
||||
c.doLiveReload = !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
|
||||
c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
|
||||
c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
|
||||
|
||||
// This is potentially double work, but we need to do this one more time now
|
||||
// that all the languages have been configured.
|
||||
if c.doWithCommandeer != nil {
|
||||
@@ -186,12 +316,13 @@ func (c *commandeer) loadConfig(running bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := c.createLogger(config)
|
||||
logger, err := c.createLogger(config, running)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Logger = logger
|
||||
c.logger = logger
|
||||
|
||||
createMemFs := config.GetBool("renderToMemory")
|
||||
|
||||
@@ -203,47 +334,65 @@ func (c *commandeer) loadConfig(running bool) error {
|
||||
c.fsCreate.Do(func() {
|
||||
fs := hugofs.NewFrom(sourceFs, config)
|
||||
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
if createMemFs {
|
||||
if c.destinationFs != nil {
|
||||
// Need to reuse the destination on server rebuilds.
|
||||
fs.Destination = c.destinationFs
|
||||
} else if createMemFs {
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
fs.Destination = new(afero.MemMapFs)
|
||||
}
|
||||
|
||||
if c.fastRenderMode {
|
||||
// For now, fast render mode only. It should, however, be fast enough
|
||||
// for the full variant, too.
|
||||
changeDetector := &fileChangeDetector{
|
||||
// We use this detector to decide to do a Hot reload of a single path or not.
|
||||
// We need to filter out source maps and possibly some other to be able
|
||||
// to make that decision.
|
||||
irrelevantRe: regexp.MustCompile(`\.map$`),
|
||||
}
|
||||
changeDetector.PrepareNew()
|
||||
fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
|
||||
c.changeDetector = changeDetector
|
||||
}
|
||||
|
||||
err = c.initFs(fs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var h *hugolib.HugoSites
|
||||
|
||||
h, err = hugolib.NewHugoSites(*c.DepsCfg)
|
||||
c.hugo = h
|
||||
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheDir := config.GetString("cacheDir")
|
||||
if cacheDir != "" {
|
||||
if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
|
||||
cacheDir = cacheDir + helpers.FilePathSeparator
|
||||
}
|
||||
isDir, err := helpers.DirExists(cacheDir, sourceFs)
|
||||
utils.CheckErr(cfg.Logger, err)
|
||||
if !isDir {
|
||||
mkdir(cacheDir)
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
} else {
|
||||
config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs))
|
||||
cacheDir, err := helpers.GetCacheDir(sourceFs, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
|
||||
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
||||
|
||||
themeDir := c.PathSpec().GetThemeDir()
|
||||
themeDir := c.hugo.PathSpec.GetFirstThemeDir()
|
||||
if themeDir != "" {
|
||||
if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
|
||||
return newSystemError("Unable to find theme Directory:", themeDir)
|
||||
}
|
||||
}
|
||||
|
||||
themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
|
||||
dir, themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
|
||||
|
||||
if themeVersionMismatch {
|
||||
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
||||
name := filepath.Base(dir)
|
||||
cfg.Logger.ERROR.Printf("%s theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||
strings.ToUpper(name), hugo.CurrentVersion.ReleaseVersion(), minVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+60
-12
@@ -14,10 +14,15 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib/paths"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/spf13/nitro"
|
||||
)
|
||||
|
||||
@@ -43,9 +48,8 @@ func (b *commandsBuilder) addAll() *commandsBuilder {
|
||||
newEnvCmd(),
|
||||
newConfigCmd(),
|
||||
newCheckCmd(),
|
||||
b.newBenchmarkCmd(),
|
||||
newConvertCmd(),
|
||||
newNewCmd(),
|
||||
b.newNewCmd(),
|
||||
newListCmd(),
|
||||
newImportCmd(),
|
||||
newGenCmd(),
|
||||
@@ -79,14 +83,14 @@ var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
|
||||
|
||||
// Used in tests.
|
||||
type commandsBuilderGetter interface {
|
||||
getCmmandsBuilder() *commandsBuilder
|
||||
getCommandsBuilder() *commandsBuilder
|
||||
}
|
||||
type baseBuilderCmd struct {
|
||||
*baseCmd
|
||||
*commandsBuilder
|
||||
}
|
||||
|
||||
func (b *baseBuilderCmd) getCmmandsBuilder() *commandsBuilder {
|
||||
func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder {
|
||||
return b.commandsBuilder
|
||||
}
|
||||
|
||||
@@ -148,7 +152,7 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
c, err := initializeConfig(true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,6 +163,7 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
})
|
||||
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)")
|
||||
cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir")
|
||||
cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
|
||||
|
||||
// Set bash-completion
|
||||
@@ -185,8 +190,9 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
}
|
||||
|
||||
type hugoBuilderCommon struct {
|
||||
source string
|
||||
baseURL string
|
||||
source string
|
||||
baseURL string
|
||||
environment string
|
||||
|
||||
buildWatch bool
|
||||
|
||||
@@ -200,15 +206,45 @@ type hugoBuilderCommon struct {
|
||||
quiet bool
|
||||
|
||||
cfgFile string
|
||||
cfgDir string
|
||||
logFile string
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
|
||||
if cc.cfgDir != "" {
|
||||
return paths.AbsPathify(baseDir, cc.cfgDir)
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
|
||||
return paths.AbsPathify(baseDir, v)
|
||||
}
|
||||
|
||||
return paths.AbsPathify(baseDir, "config")
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
|
||||
if cc.environment != "" {
|
||||
return cc.environment
|
||||
}
|
||||
|
||||
if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found {
|
||||
return v
|
||||
}
|
||||
|
||||
if isServer {
|
||||
return hugo.EnvironmentDevelopment
|
||||
}
|
||||
|
||||
return hugo.EnvironmentProduction
|
||||
}
|
||||
|
||||
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories")
|
||||
cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft")
|
||||
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
|
||||
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
|
||||
cmd.Flags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.Flags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
|
||||
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
|
||||
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
|
||||
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
|
||||
@@ -216,8 +252,6 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
|
||||
cmd.Flags().StringP("theme", "t", "", "theme to use (located in /themes/THEMENAME/)")
|
||||
cmd.Flags().StringP("themesDir", "", "", "filesystem path to themes directory")
|
||||
cmd.Flags().Bool("uglyURLs", false, "(deprecated) if true, use /filename.html instead of /filename/")
|
||||
cmd.Flags().Bool("canonifyURLs", false, "(deprecated) if true, all relative URLs will be canonicalized using baseURL")
|
||||
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/")
|
||||
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages")
|
||||
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
|
||||
@@ -225,8 +259,6 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program")
|
||||
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
|
||||
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
|
||||
cmd.Flags().Bool("pluralizeListTitles", true, "(deprecated) pluralize titles in lists using inflect")
|
||||
cmd.Flags().Bool("preserveTaxonomyNames", false, `(deprecated) preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu")`)
|
||||
cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.")
|
||||
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
|
||||
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
|
||||
@@ -234,6 +266,8 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
|
||||
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
|
||||
|
||||
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
|
||||
|
||||
// Set bash-completion.
|
||||
// Each flag must first be defined before using the SetAnnotation() call.
|
||||
_ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
@@ -241,3 +275,17 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
|
||||
_ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
|
||||
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
|
||||
}
|
||||
|
||||
func checkErr(logger *loggers.Logger, err error, s ...string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if len(s) == 0 {
|
||||
logger.CRITICAL.Println(err)
|
||||
return
|
||||
}
|
||||
for _, message := range s {
|
||||
logger.ERROR.Println(message)
|
||||
}
|
||||
logger.ERROR.Println(err)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,11 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
check func(command []cmder)
|
||||
}{{[]string{"server",
|
||||
"--config=myconfig.toml",
|
||||
"--configDir=myconfigdir",
|
||||
"--contentDir=mycontent",
|
||||
"--disableKinds=page,home",
|
||||
"--environment=testing",
|
||||
"--configDir=myconfigdir",
|
||||
"--layoutDir=mylayouts",
|
||||
"--theme=mytheme",
|
||||
"--gc",
|
||||
@@ -72,12 +75,13 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
"--port=1366",
|
||||
"--renderToDisk",
|
||||
"--source=mysource",
|
||||
"--uglyURLs"}, func(commands []cmder) {
|
||||
}, func(commands []cmder) {
|
||||
var sc *serverCmd
|
||||
for _, command := range commands {
|
||||
if b, ok := command.(commandsBuilderGetter); ok {
|
||||
v := b.getCmmandsBuilder().hugoBuilderCommon
|
||||
v := b.getCommandsBuilder().hugoBuilderCommon
|
||||
assert.Equal("myconfig.toml", v.cfgFile)
|
||||
assert.Equal("myconfigdir", v.cfgDir)
|
||||
assert.Equal("mysource", v.source)
|
||||
assert.Equal("https://example.com/b/", v.baseURL)
|
||||
}
|
||||
@@ -93,6 +97,7 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
assert.True(sc.noHTTPCache)
|
||||
assert.True(sc.renderToDisk)
|
||||
assert.Equal(1366, sc.serverPort)
|
||||
assert.Equal("testing", sc.environment)
|
||||
|
||||
cfg := viper.New()
|
||||
sc.flagsToConfig(cfg)
|
||||
@@ -105,7 +110,6 @@ func TestCommandsPersistentFlags(t *testing.T) {
|
||||
|
||||
assert.Equal([]string{"page", "home"}, cfg.Get("disableKinds"))
|
||||
|
||||
assert.True(cfg.GetBool("uglyURLs"))
|
||||
assert.True(cfg.GetBool("gc"))
|
||||
|
||||
// The flag is named i18n-warnings
|
||||
@@ -161,7 +165,6 @@ func TestCommandsExecute(t *testing.T) {
|
||||
{nil, []string{sourceFlag}, ""},
|
||||
{nil, []string{sourceFlag, "--renderToMemory"}, ""},
|
||||
{[]string{"config"}, []string{sourceFlag}, ""},
|
||||
{[]string{"benchmark"}, []string{sourceFlag, "-n=1"}, ""},
|
||||
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
|
||||
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
|
||||
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
|
||||
@@ -234,6 +237,7 @@ Single: {{ .Title }}
|
||||
writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
|
||||
|
||||
List: {{ .Title }}
|
||||
Environment: {{ hugo.Environment }}
|
||||
|
||||
`)
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ func newConfigCmd() *configCmd {
|
||||
}
|
||||
|
||||
func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := initializeConfig(false, &c.hugoBuilderCommon, c, nil)
|
||||
cfg, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, nil)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+94
-39
@@ -14,17 +14,27 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
|
||||
src "github.com/gohugoio/hugo/source"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -60,7 +70,7 @@ See convert's subcommands toJSON, toTOML and toYAML for more information.`,
|
||||
Long: `toJSON converts all front matter in the content directory
|
||||
to use JSON for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(rune([]byte(parser.JSONLead)[0]))
|
||||
return cc.convertContents(metadecoders.JSON)
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
@@ -69,7 +79,7 @@ to use JSON for the front matter.`,
|
||||
Long: `toTOML converts all front matter in the content directory
|
||||
to use TOML for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(rune([]byte(parser.TOMLLead)[0]))
|
||||
return cc.convertContents(metadecoders.TOML)
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
@@ -78,7 +88,7 @@ to use TOML for the front matter.`,
|
||||
Long: `toYAML converts all front matter in the content directory
|
||||
to use YAML for the front matter.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cc.convertContents(rune([]byte(parser.YAMLLead)[0]))
|
||||
return cc.convertContents(metadecoders.YAML)
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -91,16 +101,18 @@ to use YAML for the front matter.`,
|
||||
return cc
|
||||
}
|
||||
|
||||
func (cc *convertCmd) convertContents(mark rune) error {
|
||||
func (cc *convertCmd) convertContents(format metadecoders.Format) error {
|
||||
if cc.outputDir == "" && !cc.unsafe {
|
||||
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
|
||||
}
|
||||
|
||||
c, err := initializeConfig(false, &cc.hugoBuilderCommon, cc, nil)
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Cfg.Set("buildDrafts", true)
|
||||
|
||||
h, err := hugolib.NewHugoSites(*c.DepsCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -114,17 +126,17 @@ func (cc *convertCmd) convertContents(mark rune) error {
|
||||
|
||||
site.Log.FEEDBACK.Println("processing", len(site.AllPages), "content files")
|
||||
for _, p := range site.AllPages {
|
||||
if err := cc.convertAndSavePage(p, site, mark); err != nil {
|
||||
if err := cc.convertAndSavePage(p, site, format); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, mark rune) error {
|
||||
func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
|
||||
// The resources are not in .Site.AllPages.
|
||||
for _, r := range p.Resources.ByType("page") {
|
||||
if err := cc.convertAndSavePage(r.(*hugolib.Page), site, mark); err != nil {
|
||||
if err := cc.convertAndSavePage(r.(*hugolib.Page), site, targetFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -134,61 +146,104 @@ func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, ma
|
||||
return nil
|
||||
}
|
||||
|
||||
errMsg := fmt.Errorf("Error processing file %q", p.Path())
|
||||
|
||||
site.Log.INFO.Println("Attempting to convert", p.LogicalName())
|
||||
newPage, err := site.NewPage(p.LogicalName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, _ := p.File.(src.ReadableFile)
|
||||
file, err := f.Open()
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println("Error reading file:", p.Path())
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
psr, err := parser.ReadFrom(file)
|
||||
pf, err := parseContentFile(file)
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println("Error processing file:", p.Path())
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
metadata, err := psr.Metadata()
|
||||
// better handling of dates in formats that don't have support for them
|
||||
if pf.frontMatterFormat == metadecoders.JSON || pf.frontMatterFormat == metadecoders.YAML || pf.frontMatterFormat == metadecoders.TOML {
|
||||
for k, v := range pf.frontMatter {
|
||||
switch vv := v.(type) {
|
||||
case time.Time:
|
||||
pf.frontMatter[k] = vv.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newContent bytes.Buffer
|
||||
err = parser.InterfaceToFrontMatter(pf.frontMatter, targetFormat, &newContent)
|
||||
if err != nil {
|
||||
site.Log.ERROR.Println("Error processing file:", p.Path())
|
||||
site.Log.ERROR.Println(errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
// better handling of dates in formats that don't have support for them
|
||||
if mark == parser.FormatToLeadRune("json") || mark == parser.FormatToLeadRune("yaml") || mark == parser.FormatToLeadRune("toml") {
|
||||
newMetadata := cast.ToStringMap(metadata)
|
||||
for k, v := range newMetadata {
|
||||
switch vv := v.(type) {
|
||||
case time.Time:
|
||||
newMetadata[k] = vv.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
metadata = newMetadata
|
||||
}
|
||||
|
||||
newPage.SetSourceContent(psr.Content())
|
||||
if err = newPage.SetSourceMetaData(metadata, mark); err != nil {
|
||||
site.Log.ERROR.Printf("Failed to set source metadata for file %q: %s. For more info see For more info see https://github.com/gohugoio/hugo/issues/2458", newPage.FullFilePath(), err)
|
||||
return nil
|
||||
}
|
||||
newContent.Write(pf.content)
|
||||
|
||||
newFilename := p.Filename()
|
||||
|
||||
if cc.outputDir != "" {
|
||||
newFilename = filepath.Join(cc.outputDir, p.Dir(), newPage.LogicalName())
|
||||
contentDir := strings.TrimSuffix(newFilename, p.Path())
|
||||
contentDir = filepath.Base(contentDir)
|
||||
|
||||
newFilename = filepath.Join(cc.outputDir, contentDir, p.Path())
|
||||
}
|
||||
|
||||
if err = newPage.SaveSourceAs(newFilename); err != nil {
|
||||
return fmt.Errorf("Failed to save file %q: %s", newFilename, err)
|
||||
fs := hugofs.Os
|
||||
if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil {
|
||||
return errors.Wrapf(err, "Failed to save file %q:", newFilename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type parsedFile struct {
|
||||
frontMatterFormat metadecoders.Format
|
||||
frontMatterSource []byte
|
||||
frontMatter map[string]interface{}
|
||||
|
||||
// Everything after Front Matter
|
||||
content []byte
|
||||
}
|
||||
|
||||
func parseContentFile(r io.Reader) (parsedFile, error) {
|
||||
var pf parsedFile
|
||||
|
||||
psr, err := pageparser.Parse(r, pageparser.Config{})
|
||||
if err != nil {
|
||||
return pf, err
|
||||
}
|
||||
|
||||
iter := psr.Iterator()
|
||||
|
||||
walkFn := func(item pageparser.Item) bool {
|
||||
if pf.frontMatterSource != nil {
|
||||
// The rest is content.
|
||||
pf.content = psr.Input()[item.Pos:]
|
||||
// Done
|
||||
return false
|
||||
} else if item.IsFrontMatter() {
|
||||
pf.frontMatterFormat = metadecoders.FormatFromFrontMatterType(item.Type)
|
||||
pf.frontMatterSource = item.Val
|
||||
}
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
iter.PeekWalk(walkFn)
|
||||
|
||||
metadata, err := metadecoders.Default.UnmarshalToMap(pf.frontMatterSource, pf.frontMatterFormat)
|
||||
if err != nil {
|
||||
return pf, err
|
||||
}
|
||||
pf.frontMatter = metadata
|
||||
|
||||
return pf, nil
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -45,7 +46,7 @@ in the "man" directory under the current directory.`,
|
||||
header := &doc.GenManHeader{
|
||||
Section: "1",
|
||||
Manual: "Hugo Manual",
|
||||
Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion),
|
||||
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
|
||||
}
|
||||
if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) {
|
||||
cc.genmandir += helpers.FilePathSeparator
|
||||
|
||||
@@ -23,6 +23,13 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
ansiEsc = "\u001B"
|
||||
clearLine = "\r\033[K"
|
||||
hideCursor = ansiEsc + "[?25l"
|
||||
showCursor = ansiEsc + "[?25h"
|
||||
)
|
||||
|
||||
type flagsToConfigHandler interface {
|
||||
flagsToConfig(cfg config.Provider)
|
||||
}
|
||||
|
||||
+446
-391
File diff suppressed because it is too large
Load Diff
@@ -23,5 +23,5 @@ func init() {
|
||||
|
||||
You need to open cmd.exe and run Hugo from there.
|
||||
|
||||
Visit http://gohugo.io/ for more information.`
|
||||
Visit https://gohugo.io/ for more information.`
|
||||
}
|
||||
|
||||
+67
-34
@@ -16,6 +16,7 @@ package commands
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -24,6 +25,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
@@ -73,23 +77,23 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root
|
||||
func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if len(args) < 2 {
|
||||
return newUserError(`Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
|
||||
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
|
||||
}
|
||||
|
||||
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
return newUserError("Path error:", args[0])
|
||||
return newUserError("path error:", args[0])
|
||||
}
|
||||
|
||||
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
|
||||
if err != nil {
|
||||
return newUserError("Path error:", args[1])
|
||||
return newUserError("path error:", args[1])
|
||||
}
|
||||
|
||||
jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
|
||||
|
||||
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
|
||||
return newUserError("Target path should not be inside the Jekyll root, aborting.")
|
||||
return newUserError("abort: target path should not be inside the Jekyll root")
|
||||
}
|
||||
|
||||
forceImport, _ := cmd.Flags().GetBool("force")
|
||||
@@ -97,7 +101,7 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
fs := afero.NewOsFs()
|
||||
jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot)
|
||||
if !hasAnyPost {
|
||||
return errors.New("Your Jekyll root contains neither posts nor drafts, aborting.")
|
||||
return errors.New("abort: jekyll root contains neither posts nor drafts")
|
||||
}
|
||||
|
||||
site, err := i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
|
||||
@@ -120,7 +124,7 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
|
||||
|
||||
relPath, err := filepath.Rel(jekyllRoot, path)
|
||||
if err != nil {
|
||||
return newUserError("Get rel path error:", path)
|
||||
return newUserError("get rel path error:", path)
|
||||
}
|
||||
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
@@ -204,13 +208,13 @@ func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPos
|
||||
fs := s.Fs.Source
|
||||
if exists, _ := helpers.Exists(targetDir, fs); exists {
|
||||
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
|
||||
return nil, errors.New("Target path \"" + targetDir + "\" already exists but not a directory")
|
||||
return nil, errors.New("target path \"" + targetDir + "\" exists but is not a directory")
|
||||
}
|
||||
|
||||
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
|
||||
|
||||
if !isEmpty && !force {
|
||||
return nil, errors.New("Target path \"" + targetDir + "\" already exists and is not empty")
|
||||
return nil, errors.New("target path \"" + targetDir + "\" exists and is not empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +257,7 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := parser.HandleYAMLMetaData(b)
|
||||
c, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -262,7 +266,7 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
|
||||
return c
|
||||
}
|
||||
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) {
|
||||
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]interface{}) (err error) {
|
||||
title := "My New Hugo Site"
|
||||
baseURL := "http://example.org/"
|
||||
|
||||
@@ -288,15 +292,14 @@ func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind stri
|
||||
"languageCode": "en-us",
|
||||
"disablePathToLower": true,
|
||||
}
|
||||
kind = parser.FormatSanitize(kind)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
|
||||
err = parser.InterfaceToConfig(in, kind, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs)
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(kind)), &buf, fs)
|
||||
}
|
||||
|
||||
func copyFile(source string, dest string) error {
|
||||
@@ -445,39 +448,25 @@ func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft b
|
||||
return err
|
||||
}
|
||||
|
||||
psr, err := parser.ReadFrom(bytes.NewReader(contentBytes))
|
||||
pf, err := parseContentFile(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Parse file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
metadata, err := psr.Metadata()
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Processing file error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
newmetadata, err := convertJekyllMetaData(metadata, postName, postDate, draft)
|
||||
newmetadata, err := convertJekyllMetaData(pf.frontMatter, postName, postDate, draft)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("Convert metadata error:", path)
|
||||
return err
|
||||
}
|
||||
|
||||
jww.TRACE.Println(newmetadata)
|
||||
content := convertJekyllContent(newmetadata, string(psr.Content()))
|
||||
content := convertJekyllContent(newmetadata, string(pf.content))
|
||||
|
||||
page, err := s.NewPage(filename)
|
||||
if err != nil {
|
||||
jww.ERROR.Println("New page error", filename)
|
||||
return err
|
||||
fs := hugofs.Os
|
||||
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
|
||||
return fmt.Errorf("Failed to save file %q:", filename)
|
||||
}
|
||||
|
||||
page.SetSourceContent([]byte(content))
|
||||
page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune("yaml"))
|
||||
page.SaveSourceAs(targetFile)
|
||||
|
||||
jww.TRACE.Println("Target file:", targetFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -557,7 +546,6 @@ func convertJekyllContent(m interface{}, content string) string {
|
||||
}{
|
||||
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
|
||||
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), "{{< highlight $1 >}}"},
|
||||
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
|
||||
}
|
||||
|
||||
@@ -571,6 +559,7 @@ func convertJekyllContent(m interface{}, content string) string {
|
||||
}{
|
||||
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
|
||||
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag},
|
||||
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag},
|
||||
}
|
||||
|
||||
for _, replace := range replaceListFunc {
|
||||
@@ -580,6 +569,50 @@ func convertJekyllContent(m interface{}, content string) string {
|
||||
return content
|
||||
}
|
||||
|
||||
func replaceHighlightTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
|
||||
parts := r.FindStringSubmatch(match)
|
||||
lastQuote := rune(0)
|
||||
f := func(c rune) bool {
|
||||
switch {
|
||||
case c == lastQuote:
|
||||
lastQuote = rune(0)
|
||||
return false
|
||||
case lastQuote != rune(0):
|
||||
return false
|
||||
case unicode.In(c, unicode.Quotation_Mark):
|
||||
lastQuote = c
|
||||
return false
|
||||
default:
|
||||
return unicode.IsSpace(c)
|
||||
}
|
||||
}
|
||||
// splitting string by space but considering quoted section
|
||||
items := strings.FieldsFunc(parts[1], f)
|
||||
|
||||
result := bytes.NewBufferString("{{< highlight ")
|
||||
result.WriteString(items[0]) // language
|
||||
options := items[1:]
|
||||
for i, opt := range options {
|
||||
opt = strings.Replace(opt, "\"", "", -1)
|
||||
if opt == "linenos" {
|
||||
opt = "linenos=table"
|
||||
}
|
||||
if i == 0 {
|
||||
opt = " \"" + opt
|
||||
}
|
||||
if i < len(options)-1 {
|
||||
opt += ","
|
||||
} else if i == len(options)-1 {
|
||||
opt += "\""
|
||||
}
|
||||
result.WriteString(opt)
|
||||
}
|
||||
|
||||
result.WriteString(" >}}")
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func replaceImageTag(match string) string {
|
||||
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
|
||||
result := bytes.NewBufferString("{{< figure ")
|
||||
|
||||
@@ -97,6 +97,9 @@ func TestConvertJekyllContent(t *testing.T) {
|
||||
{map[interface{}]interface{}{},
|
||||
"{% highlight go %}\nvar s int\n{% endhighlight %}",
|
||||
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}"},
|
||||
{map[interface{}]interface{}{},
|
||||
"{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
|
||||
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}"},
|
||||
|
||||
// Octopress image tag
|
||||
{map[interface{}]interface{}{},
|
||||
|
||||
+3
-3
@@ -50,7 +50,7 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
|
||||
c.Set("buildDrafts", true)
|
||||
return nil
|
||||
}
|
||||
c, err := initializeConfig(false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,7 +86,7 @@ posted in the future.`,
|
||||
c.Set("buildFuture", true)
|
||||
return nil
|
||||
}
|
||||
c, err := initializeConfig(false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -122,7 +122,7 @@ expired.`,
|
||||
c.Set("buildExpired", true)
|
||||
return nil
|
||||
}
|
||||
c, err := initializeConfig(false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+27
-74
@@ -30,16 +30,14 @@ import (
|
||||
var _ cmder = (*newCmd)(nil)
|
||||
|
||||
type newCmd struct {
|
||||
hugoBuilderCommon
|
||||
contentEditor string
|
||||
contentType string
|
||||
|
||||
*baseCmd
|
||||
*baseBuilderCmd
|
||||
}
|
||||
|
||||
func newNewCmd() *newCmd {
|
||||
cc := &newCmd{}
|
||||
cc.baseCmd = newBaseCmd(&cobra.Command{
|
||||
func (b *commandsBuilder) newNewCmd() *newCmd {
|
||||
cmd := &cobra.Command{
|
||||
Use: "new [path]",
|
||||
Short: "Create new content for your site",
|
||||
Long: `Create a new content file and automatically set the date and title.
|
||||
@@ -47,18 +45,22 @@ It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.`,
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
RunE: cc.newContent,
|
||||
})
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
}
|
||||
|
||||
cc.cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
|
||||
cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
cc.cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
|
||||
cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
|
||||
|
||||
cc.cmd.AddCommand(newNewSiteCmd().getCommand())
|
||||
cc.cmd.AddCommand(newNewThemeCmd().getCommand())
|
||||
cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
|
||||
cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
|
||||
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
|
||||
cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
|
||||
|
||||
cmd.AddCommand(newNewSiteCmd().getCommand())
|
||||
cmd.AddCommand(newNewThemeCmd().getCommand())
|
||||
|
||||
cmd.RunE = cc.newContent
|
||||
|
||||
return cc
|
||||
}
|
||||
@@ -71,7 +73,7 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := initializeConfig(false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
c, err := initializeConfig(true, false, &n.hugoBuilderCommon, n, cfgInit)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -85,69 +87,13 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
|
||||
|
||||
var kind string
|
||||
|
||||
createPath, kind = newContentPathSection(createPath)
|
||||
createPath, kind = newContentPathSection(c.hugo, createPath)
|
||||
|
||||
if n.contentType != "" {
|
||||
kind = n.contentType
|
||||
}
|
||||
|
||||
cfg := c.DepsCfg
|
||||
|
||||
ps, err := helpers.NewPathSpec(cfg.Fs, cfg.Cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If a site isn't in use in the archetype template, we can skip the build.
|
||||
siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) {
|
||||
if !siteUsed {
|
||||
return hugolib.NewSite(*cfg)
|
||||
}
|
||||
var s *hugolib.Site
|
||||
if err := c.initSites(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.hugo.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s = c.hugo.Sites[0]
|
||||
|
||||
if len(c.hugo.Sites) > 1 {
|
||||
// Find the best match.
|
||||
for _, ss := range c.hugo.Sites {
|
||||
if strings.Contains(createPath, "."+ss.Language.Lang) {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return create.NewContent(ps, siteFactory, kind, createPath)
|
||||
}
|
||||
|
||||
func nextStepsText() string {
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
|
||||
|
||||
1. Download a theme into the same-named folder.
|
||||
Choose a theme from https://themes.gohugo.io/, or
|
||||
create your own with the "hugo new theme <THEMENAME>" command.
|
||||
2. Perhaps you want to add some content. You can add single files
|
||||
with "hugo new `)
|
||||
|
||||
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
|
||||
|
||||
nextStepsText.WriteString(`".
|
||||
3. Start the built-in live server via "hugo server".
|
||||
|
||||
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
|
||||
|
||||
return nextStepsText.String()
|
||||
return create.NewContent(c.hugo, kind, createPath)
|
||||
}
|
||||
|
||||
func mkdir(x ...string) {
|
||||
@@ -168,10 +114,17 @@ func touchFile(fs afero.Fs, x ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newContentPathSection(path string) (string, string) {
|
||||
func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
|
||||
// Forward slashes is used in all examples. Convert if needed.
|
||||
// Issue #1133
|
||||
createpath := filepath.FromSlash(path)
|
||||
|
||||
if h != nil {
|
||||
for _, s := range h.Sites {
|
||||
createpath = strings.TrimPrefix(createpath, s.PathSpec.ContentDir)
|
||||
}
|
||||
}
|
||||
|
||||
var section string
|
||||
// assume the first directory is the section (kind)
|
||||
if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
// Issue #1133
|
||||
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
|
||||
p, s := newContentPathSection("/post/new.md")
|
||||
p, s := newContentPathSection(nil, "/post/new.md")
|
||||
assert.Equal(t, filepath.FromSlash("/post/new.md"), p)
|
||||
assert.Equal(t, "post", s)
|
||||
}
|
||||
|
||||
+31
-14
@@ -15,24 +15,21 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/create"
|
||||
|
||||
"fmt"
|
||||
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/create"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var _ cmder = (*newSiteCmd)(nil)
|
||||
@@ -98,13 +95,13 @@ func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := fs.Source.MkdirAll(dir, 0777); err != nil {
|
||||
return fmt.Errorf("Failed to create dir: %s", err)
|
||||
return _errors.Wrap(err, "Failed to create dir")
|
||||
}
|
||||
}
|
||||
|
||||
createConfig(fs, basepath, n.configFormat)
|
||||
|
||||
// Create a defaul archetype file.
|
||||
// Create a default archetype file.
|
||||
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
|
||||
strings.NewReader(create.ArchetypeTemplateTemplate), fs.Source)
|
||||
|
||||
@@ -136,13 +133,33 @@ func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
|
||||
"title": "My New Hugo Site",
|
||||
"languageCode": "en-us",
|
||||
}
|
||||
kind = parser.FormatSanitize(kind)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf)
|
||||
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
|
||||
}
|
||||
|
||||
func nextStepsText() string {
|
||||
var nextStepsText bytes.Buffer
|
||||
|
||||
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
|
||||
|
||||
1. Download a theme into the same-named folder.
|
||||
Choose a theme from https://themes.gohugo.io/, or
|
||||
create your own with the "hugo new theme <THEMENAME>" command.
|
||||
2. Perhaps you want to add some content. You can add single files
|
||||
with "hugo new `)
|
||||
|
||||
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
|
||||
|
||||
nextStepsText.WriteString(`".
|
||||
3. Start the built-in live server via "hugo server".
|
||||
|
||||
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
|
||||
|
||||
return nextStepsText.String()
|
||||
}
|
||||
|
||||
+29
-12
@@ -14,16 +14,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"bytes"
|
||||
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -53,8 +51,9 @@ as you see fit.`,
|
||||
return ccmd
|
||||
}
|
||||
|
||||
// newTheme creates a new Hugo theme template
|
||||
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
|
||||
c, err := initializeConfig(false, &n.hugoBuilderCommon, n, nil)
|
||||
c, err := initializeConfig(false, false, &n.hugoBuilderCommon, n, nil)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,13 +63,13 @@ func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
|
||||
createpath := c.PathSpec().AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
|
||||
jww.INFO.Println("creating theme at", createpath)
|
||||
createpath := c.hugo.PathSpec.AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
|
||||
jww.FEEDBACK.Println("Creating theme at", createpath)
|
||||
|
||||
cfg := c.DepsCfg
|
||||
|
||||
if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x {
|
||||
return newUserError(createpath, "already exists")
|
||||
return errors.New(createpath + " already exists")
|
||||
}
|
||||
|
||||
mkdir(createpath, "layouts", "_default")
|
||||
@@ -81,6 +80,24 @@ func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
|
||||
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html")
|
||||
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html")
|
||||
|
||||
baseofDefault := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
{{- partial "head.html" . -}}
|
||||
<body>
|
||||
{{- partial "header.html" . -}}
|
||||
<div id="content">
|
||||
{{- block "main" . }}{{- end }}
|
||||
</div>
|
||||
{{- partial "footer.html" . -}}
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), cfg.Fs.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "head.html")
|
||||
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html")
|
||||
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html")
|
||||
|
||||
@@ -118,7 +135,7 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
`)
|
||||
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE.md"), bytes.NewReader(by), cfg.Fs.Source)
|
||||
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), cfg.Fs.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,12 +152,12 @@ func (n *newThemeCmd) createThemeMD(fs *hugofs.Fs, inpath string) (err error) {
|
||||
|
||||
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
|
||||
license = "MIT"
|
||||
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE.md"
|
||||
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
|
||||
description = ""
|
||||
homepage = "http://example.com/"
|
||||
tags = []
|
||||
features = []
|
||||
min_version = "0.38"
|
||||
min_version = "0.41"
|
||||
|
||||
[author]
|
||||
name = ""
|
||||
|
||||
+92
-40
@@ -14,6 +14,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -28,10 +30,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -52,7 +56,8 @@ type serverCmd struct {
|
||||
serverWatch bool
|
||||
noHTTPCache bool
|
||||
|
||||
disableFastRender bool
|
||||
disableFastRender bool
|
||||
disableBrowserError bool
|
||||
|
||||
*baseBuilderCmd
|
||||
}
|
||||
@@ -93,6 +98,7 @@ of a second, you will be able to save and see your changes nearly instantly.`,
|
||||
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
|
||||
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
|
||||
cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
|
||||
|
||||
cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
|
||||
cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
|
||||
@@ -122,27 +128,30 @@ func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
|
||||
var serverPorts []int
|
||||
|
||||
func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
// If a Destination is provided via flag write to disk
|
||||
destination, _ := cmd.Flags().GetString("destination")
|
||||
if destination != "" {
|
||||
s.renderToDisk = true
|
||||
sc.renderToDisk = true
|
||||
}
|
||||
|
||||
var serverCfgInit sync.Once
|
||||
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("renderToMemory", !s.renderToDisk)
|
||||
c.Set("renderToMemory", !sc.renderToDisk)
|
||||
if cmd.Flags().Changed("navigateToChanged") {
|
||||
c.Set("navigateToChanged", s.navigateToChanged)
|
||||
c.Set("navigateToChanged", sc.navigateToChanged)
|
||||
}
|
||||
if cmd.Flags().Changed("disableLiveReload") {
|
||||
c.Set("disableLiveReload", s.disableLiveReload)
|
||||
c.Set("disableLiveReload", sc.disableLiveReload)
|
||||
}
|
||||
if cmd.Flags().Changed("disableFastRender") {
|
||||
c.Set("disableFastRender", s.disableFastRender)
|
||||
c.Set("disableFastRender", sc.disableFastRender)
|
||||
}
|
||||
if s.serverWatch {
|
||||
if cmd.Flags().Changed("disableBrowserError") {
|
||||
c.Set("disableBrowserError", sc.disableBrowserError)
|
||||
}
|
||||
if sc.serverWatch {
|
||||
c.Set("watch", true)
|
||||
}
|
||||
|
||||
@@ -158,25 +167,25 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
serverPorts = make([]int, 1)
|
||||
|
||||
if c.languages.IsMultihost() {
|
||||
if !s.serverAppend {
|
||||
if !sc.serverAppend {
|
||||
err = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
}
|
||||
serverPorts = make([]int, len(c.languages))
|
||||
}
|
||||
|
||||
currentServerPort := s.serverPort
|
||||
currentServerPort := sc.serverPort
|
||||
|
||||
for i := 0; i < len(serverPorts); i++ {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(s.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
l.Close()
|
||||
serverPorts[i] = currentServerPort
|
||||
} else {
|
||||
if i == 0 && s.cmd.Flags().Changed("port") {
|
||||
if i == 0 && sc.cmd.Flags().Changed("port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
err = newSystemErrorF("Server startup failed: %s", err)
|
||||
}
|
||||
jww.ERROR.Println("port", s.serverPort, "already in use, attempting to use an available port")
|
||||
c.logger.FEEDBACK.Println("port", sc.serverPort, "already in use, attempting to use an available port")
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
if err != nil {
|
||||
err = newSystemError("Unable to find alternative port to use:", err)
|
||||
@@ -190,9 +199,9 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
|
||||
c.serverPorts = serverPorts
|
||||
|
||||
c.Set("port", s.serverPort)
|
||||
if s.liveReloadPort != -1 {
|
||||
c.Set("liveReloadPort", s.liveReloadPort)
|
||||
c.Set("port", sc.serverPort)
|
||||
if sc.liveReloadPort != -1 {
|
||||
c.Set("liveReloadPort", sc.liveReloadPort)
|
||||
} else {
|
||||
c.Set("liveReloadPort", serverPorts[0])
|
||||
}
|
||||
@@ -206,7 +215,7 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
serverPort = serverPorts[0]
|
||||
}
|
||||
|
||||
baseURL, err := s.fixURL(language, s.baseURL, serverPort)
|
||||
baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -223,10 +232,10 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
if err := memStats(); err != nil {
|
||||
jww.ERROR.Println("memstats error:", err)
|
||||
jww.WARN.Println("memstats error:", err)
|
||||
}
|
||||
|
||||
c, err := initializeConfig(true, &s.hugoBuilderCommon, s, cfgInit)
|
||||
c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -240,7 +249,7 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Watch runs its own server as part of the routine
|
||||
if s.serverWatch {
|
||||
if sc.serverWatch {
|
||||
|
||||
watchDirs, err := c.getDirList()
|
||||
if err != nil {
|
||||
@@ -266,15 +275,16 @@ func (s *serverCmd) server(cmd *cobra.Command, args []string) error {
|
||||
|
||||
}
|
||||
|
||||
return c.serve(s)
|
||||
return c.serve(sc)
|
||||
|
||||
}
|
||||
|
||||
type fileServer struct {
|
||||
baseURLs []string
|
||||
roots []string
|
||||
c *commandeer
|
||||
s *serverCmd
|
||||
baseURLs []string
|
||||
roots []string
|
||||
errorTemplate tpl.Template
|
||||
c *commandeer
|
||||
s *serverCmd
|
||||
}
|
||||
|
||||
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
|
||||
@@ -288,7 +298,9 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
publishDir = filepath.Join(publishDir, root)
|
||||
}
|
||||
|
||||
absPublishDir := f.c.PathSpec().AbsPathify(publishDir)
|
||||
absPublishDir := f.c.hugo.PathSpec.AbsPathify(publishDir)
|
||||
|
||||
jww.FEEDBACK.Printf("Environment: %q", f.c.hugo.Deps.Site.Hugo().Environment)
|
||||
|
||||
if i == 0 {
|
||||
if f.s.renderToDisk {
|
||||
@@ -298,33 +310,62 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
}
|
||||
}
|
||||
|
||||
httpFs := afero.NewHttpFs(f.c.Fs.Destination)
|
||||
httpFs := afero.NewHttpFs(f.c.destinationFs)
|
||||
fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
|
||||
|
||||
doLiveReload := !f.s.buildWatch && !f.c.Cfg.GetBool("disableLiveReload")
|
||||
fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender")
|
||||
|
||||
if i == 0 && fastRenderMode {
|
||||
if i == 0 && f.c.fastRenderMode {
|
||||
jww.FEEDBACK.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, "", "", fmt.Errorf("Invalid baseURL: %s", err)
|
||||
return nil, "", "", errors.Wrap(err, "Invalid baseURL")
|
||||
}
|
||||
|
||||
decorate := func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if f.c.showErrorInBrowser {
|
||||
// First check the error state
|
||||
err := f.c.getErrorWithContext()
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
var b bytes.Buffer
|
||||
err := f.errorTemplate.Execute(&b, err)
|
||||
if err != nil {
|
||||
f.c.logger.ERROR.Println(err)
|
||||
}
|
||||
port = 1313
|
||||
if !f.c.paused {
|
||||
port = f.c.Cfg.GetInt("liveReloadPort")
|
||||
}
|
||||
fmt.Fprint(w, injectLiveReloadScript(&b, port))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if f.s.noHTTPCache {
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
}
|
||||
|
||||
if fastRenderMode {
|
||||
if f.c.fastRenderMode && f.c.buildErr == nil {
|
||||
p := r.RequestURI
|
||||
if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
|
||||
if !f.c.visitedURLs.Contains(p) {
|
||||
// If not already on stack, re-render that single page.
|
||||
if err := f.c.partialReRender(p); err != nil {
|
||||
f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", p))
|
||||
if f.c.showErrorInBrowser {
|
||||
http.Redirect(w, r, p, 301)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.c.visitedURLs.Add(p)
|
||||
|
||||
}
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
@@ -345,6 +386,11 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
|
||||
return mu, u.String(), endpoint, nil
|
||||
}
|
||||
|
||||
var logErrorRe = regexp.MustCompile("(?s)ERROR \\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} ")
|
||||
|
||||
func removeErrorPrefixFromLog(content string) string {
|
||||
return logErrorRe.ReplaceAllLiteralString(content, "")
|
||||
}
|
||||
func (c *commandeer) serve(s *serverCmd) error {
|
||||
|
||||
isMultiHost := c.hugo.IsMultihost()
|
||||
@@ -365,11 +411,17 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
roots = []string{""}
|
||||
}
|
||||
|
||||
templ, err := c.hugo.TextTmpl.Parse("__default_server_error", buildErrorTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv := &fileServer{
|
||||
baseURLs: baseURLs,
|
||||
roots: roots,
|
||||
c: c,
|
||||
s: s,
|
||||
baseURLs: baseURLs,
|
||||
roots: roots,
|
||||
c: c,
|
||||
s: s,
|
||||
errorTemplate: templ,
|
||||
}
|
||||
|
||||
doLiveReload := !c.Cfg.GetBool("disableLiveReload")
|
||||
@@ -392,7 +444,7 @@ func (c *commandeer) serve(s *serverCmd) error {
|
||||
go func() {
|
||||
err = http.ListenAndServe(endpoint, mu)
|
||||
if err != nil {
|
||||
jww.ERROR.Printf("Error: %s\n", err.Error())
|
||||
c.logger.ERROR.Printf("Error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
@@ -453,7 +505,7 @@ func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, er
|
||||
if strings.Contains(u.Host, ":") {
|
||||
u.Host, _, err = net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err)
|
||||
return "", errors.Wrap(err, "Failed to split baseURL hostpost")
|
||||
}
|
||||
}
|
||||
u.Host += fmt.Sprintf(":%d", port)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/gohugoio/hugo/transform"
|
||||
"github.com/gohugoio/hugo/transform/livereloadinject"
|
||||
)
|
||||
|
||||
var buildErrorTemplate = `<!doctype html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hugo Server: Error</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: "Muli",avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 16px;
|
||||
background-color: black;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
main {
|
||||
margin: auto;
|
||||
width: 95%;
|
||||
padding: 1rem;
|
||||
}
|
||||
.version {
|
||||
color: #ccc;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.stack {
|
||||
margin-top: 6rem;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.highlight {
|
||||
overflow-x: auto;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: #272822;
|
||||
border: 1px solid black;
|
||||
}
|
||||
a {
|
||||
color: #0594cb;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{{ highlight .Error "apl" "noclasses=true,style=monokai" }}
|
||||
{{ with .File }}
|
||||
{{ $params := printf "noclasses=true,style=monokai,linenos=table,hl_lines=%d,linenostart=%d" (add .LinesPos 1) (sub .Position.LineNumber .LinesPos) }}
|
||||
{{ $lexer := .ChromaLexer | default "go-html-template" }}
|
||||
{{ highlight (delimit .Lines "\n") $lexer $params }}
|
||||
{{ end }}
|
||||
{{ with .StackTrace }}
|
||||
{{ highlight . "apl" "noclasses=true,style=monokai" }}
|
||||
{{ end }}
|
||||
<p class="version">{{ .Version }}</p>
|
||||
<a href="">Reload Page</a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func injectLiveReloadScript(src io.Reader, port int) string {
|
||||
var b bytes.Buffer
|
||||
chain := transform.Chain{livereloadinject.New(port)}
|
||||
chain.Apply(&b, src)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +29,10 @@ import (
|
||||
)
|
||||
|
||||
func TestServer(t *testing.T) {
|
||||
if isWindowsCI() {
|
||||
// TODO(bep) not sure why server tests have started to fail on the Windows CI server.
|
||||
t.Skip("Skip server test on appveyor")
|
||||
}
|
||||
assert := require.New(t)
|
||||
dir, err := createSimpleTestSite(t)
|
||||
assert.NoError(err)
|
||||
@@ -62,6 +68,7 @@ func TestServer(t *testing.T) {
|
||||
homeContent := helpers.ReaderToString(resp.Body)
|
||||
|
||||
assert.Contains(homeContent, "List: Hugo Commands")
|
||||
assert.Contains(homeContent, "Environment: development")
|
||||
|
||||
// Stop the server.
|
||||
stop <- true
|
||||
@@ -107,3 +114,19 @@ func TestFixURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveErrorPrefixFromLog(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at <partial "logo" .>: error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image
|
||||
ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
|
||||
`
|
||||
|
||||
withoutError := removeErrorPrefixFromLog(content)
|
||||
|
||||
assert.False(strings.Contains(withoutError, "ERROR"), withoutError)
|
||||
|
||||
}
|
||||
|
||||
func isWindowsCI() bool {
|
||||
return runtime.GOOS == "windows" && os.Getenv("CI") != ""
|
||||
}
|
||||
|
||||
+18
-29
@@ -17,53 +17,43 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
src "github.com/gohugoio/hugo/source"
|
||||
"github.com/spf13/fsync"
|
||||
)
|
||||
|
||||
type staticSyncer struct {
|
||||
c *commandeer
|
||||
d *src.Dirs
|
||||
}
|
||||
|
||||
func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
|
||||
dirs, err := src.NewDirs(c.Fs, c.Cfg, c.DepsCfg.Logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &staticSyncer{c: c, d: dirs}, nil
|
||||
return &staticSyncer{c: c}, nil
|
||||
}
|
||||
|
||||
func (s *staticSyncer) isStatic(path string) bool {
|
||||
return s.d.IsStatic(path)
|
||||
func (s *staticSyncer) isStatic(filename string) bool {
|
||||
return s.c.hugo.BaseFs.SourceFilesystems.IsStatic(filename)
|
||||
}
|
||||
|
||||
func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
c := s.c
|
||||
|
||||
syncFn := func(dirs *src.Dirs, publishDir string) (uint64, error) {
|
||||
staticSourceFs, err := dirs.CreateStaticFs()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
|
||||
publishDir := c.hugo.PathSpec.PublishDir
|
||||
// If root, remove the second '/'
|
||||
if publishDir == "//" {
|
||||
publishDir = helpers.FilePathSeparator
|
||||
}
|
||||
|
||||
if dirs.Language != nil {
|
||||
// Multihost setup
|
||||
publishDir = filepath.Join(publishDir, dirs.Language.Lang)
|
||||
}
|
||||
|
||||
if staticSourceFs == nil {
|
||||
c.Logger.WARN.Println("No static directories found to sync")
|
||||
return 0, nil
|
||||
if sourceFs.PublishFolder != "" {
|
||||
publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
|
||||
}
|
||||
|
||||
syncer := fsync.NewSyncer()
|
||||
syncer.NoTimes = c.Cfg.GetBool("noTimes")
|
||||
syncer.NoChmod = c.Cfg.GetBool("noChmod")
|
||||
syncer.SrcFs = staticSourceFs
|
||||
syncer.SrcFs = sourceFs.Fs
|
||||
syncer.DestFs = c.Fs.Destination
|
||||
|
||||
// prevent spamming the log on changes
|
||||
@@ -88,8 +78,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
|
||||
fromPath := ev.Name
|
||||
|
||||
// If we are here we already know the event took place in a static dir
|
||||
relPath := dirs.MakeStaticPathRelative(fromPath)
|
||||
relPath := sourceFs.MakePathRelative(fromPath)
|
||||
if relPath == "" {
|
||||
// Not member of this virtual host.
|
||||
continue
|
||||
@@ -105,7 +94,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
// the source of that static file. In this case Hugo will incorrectly remove that file
|
||||
// from the published directory.
|
||||
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) {
|
||||
if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) {
|
||||
// If file doesn't exist in any static dir, remove it
|
||||
toRemove := filepath.Join(publishDir, relPath)
|
||||
|
||||
@@ -116,10 +105,10 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
|
||||
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
|
||||
c.Logger.ERROR.Println(err)
|
||||
c.logger.ERROR.Println(err)
|
||||
}
|
||||
} else {
|
||||
c.Logger.ERROR.Println(err)
|
||||
c.logger.ERROR.Println(err)
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -128,7 +117,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
|
||||
// For all other event operations Hugo will sync static.
|
||||
logger.Println("Syncing", relPath, "to", publishDir)
|
||||
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
|
||||
c.Logger.ERROR.Println(err)
|
||||
c.logger.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-14
@@ -14,11 +14,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/spf13/cobra"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
@@ -44,13 +40,5 @@ func newVersionCmd() *versionCmd {
|
||||
}
|
||||
|
||||
func printHugoVersion() {
|
||||
if hugolib.CommitHash == "" {
|
||||
if hugolib.BuildDate == "" {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH)
|
||||
} else {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
|
||||
}
|
||||
} else {
|
||||
jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate)
|
||||
}
|
||||
jww.FEEDBACK.Println(hugo.BuildVersionString())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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 collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Append appends from to a slice to and returns the resulting slice.
|
||||
// If length of from is one and the only element is a slice of same type as to,
|
||||
// it will be appended.
|
||||
func Append(to interface{}, from ...interface{}) (interface{}, error) {
|
||||
tov, toIsNil := indirect(reflect.ValueOf(to))
|
||||
|
||||
toIsNil = toIsNil || to == nil
|
||||
var tot reflect.Type
|
||||
|
||||
if !toIsNil {
|
||||
if tov.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("expected a slice, got %T", to)
|
||||
}
|
||||
|
||||
tot = tov.Type().Elem()
|
||||
toIsNil = tov.Len() == 0
|
||||
|
||||
if len(from) == 1 {
|
||||
fromv := reflect.ValueOf(from[0])
|
||||
if fromv.Kind() == reflect.Slice {
|
||||
if toIsNil {
|
||||
// If we get nil []string, we just return the []string
|
||||
return from[0], nil
|
||||
}
|
||||
|
||||
fromt := reflect.TypeOf(from[0]).Elem()
|
||||
|
||||
// If we get []string []string, we append the from slice to to
|
||||
if tot == fromt {
|
||||
return reflect.AppendSlice(tov, fromv).Interface(), nil
|
||||
} else if !fromt.AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toIsNil {
|
||||
return Slice(from...), nil
|
||||
}
|
||||
|
||||
for _, f := range from {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
}
|
||||
tov = reflect.Append(tov, fv)
|
||||
}
|
||||
|
||||
return tov.Interface(), nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]interface{}, error) {
|
||||
var tos []interface{}
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface{}, error) {
|
||||
var tos []interface{}
|
||||
|
||||
for i := 0; i < tov.Len(); i++ {
|
||||
tos = append(tos, tov.Index(i).Interface())
|
||||
}
|
||||
|
||||
for _, v := range from {
|
||||
tos = append(tos, v)
|
||||
}
|
||||
|
||||
return tos, nil
|
||||
}
|
||||
|
||||
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
|
||||
// TODO(bep) consolidate
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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 collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAppend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for i, test := range []struct {
|
||||
start interface{}
|
||||
addend []interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}},
|
||||
{[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}},
|
||||
{[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}},
|
||||
{nil, []interface{}{"a", "b"}, []string{"a", "b"}},
|
||||
{nil, []interface{}{nil}, []interface{}{nil}},
|
||||
{[]interface{}{}, []interface{}{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}},
|
||||
{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}},
|
||||
{&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}},
|
||||
[]interface{}{&tstSlicer{"c"}},
|
||||
tstSlicers{&tstSlicer{"a"},
|
||||
&tstSlicer{"b"},
|
||||
&tstSlicer{"c"}}},
|
||||
{testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}},
|
||||
[]interface{}{&tstSlicerIn1{"c"}},
|
||||
testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}},
|
||||
//https://github.com/gohugoio/hugo/issues/5361
|
||||
{[]string{"a", "b"}, []interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]string{"a", "b"}, []interface{}{&tstSlicer{"a"}},
|
||||
[]interface{}{"a", "b", &tstSlicer{"a"}}},
|
||||
// Errors
|
||||
{"", []interface{}{[]string{"a", "b"}}, false},
|
||||
// No string concatenation.
|
||||
{"ab",
|
||||
[]interface{}{"c"},
|
||||
false},
|
||||
} {
|
||||
|
||||
errMsg := fmt.Sprintf("[%d]", i)
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
|
||||
if b, ok := test.expected.(bool); ok && !b {
|
||||
require.Error(t, err, errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
require.NoError(t, err, errMsg)
|
||||
|
||||
if !reflect.DeepEqual(test.expected, result) {
|
||||
t.Fatalf("%s got\n%T: %v\nexpected\n%T: %v", errMsg, result, result, test.expected, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 collections contains common Hugo functionality related to collection
|
||||
// handling.
|
||||
package collections
|
||||
|
||||
// Grouper defines a very generic way to group items by a given key.
|
||||
type Grouper interface {
|
||||
Group(key interface{}, items interface{}) (interface{}, error)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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 collections
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Slicer defines a very generic way to create a typed slice. This is used
|
||||
// in collections.Slice template func to get types such as Pages, PageGroups etc.
|
||||
// instead of the less useful []interface{}.
|
||||
type Slicer interface {
|
||||
Slice(items interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
// Slice returns a slice of all passed arguments.
|
||||
func Slice(args ...interface{}) interface{} {
|
||||
if len(args) == 0 {
|
||||
return args
|
||||
}
|
||||
|
||||
first := args[0]
|
||||
firstType := reflect.TypeOf(first)
|
||||
|
||||
if firstType == nil {
|
||||
return args
|
||||
}
|
||||
|
||||
if g, ok := first.(Slicer); ok {
|
||||
v, err := g.Slice(args)
|
||||
if err == nil {
|
||||
return v
|
||||
}
|
||||
|
||||
// If Slice fails, the items are not of the same type and
|
||||
// []interface{} is the best we can do.
|
||||
return args
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
// This can be a mix of types.
|
||||
for i := 1; i < len(args); i++ {
|
||||
if firstType != reflect.TypeOf(args[i]) {
|
||||
// []interface{} is the best we can do
|
||||
return args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args))
|
||||
for i, arg := range args {
|
||||
slice.Index(i).Set(reflect.ValueOf(arg))
|
||||
}
|
||||
return slice.Interface()
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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 collections
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/assert"
|
||||
)
|
||||
|
||||
var _ Slicer = (*tstSlicer)(nil)
|
||||
var _ Slicer = (*tstSlicerIn1)(nil)
|
||||
var _ Slicer = (*tstSlicerIn2)(nil)
|
||||
var _ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
var _ testSlicerInterface = (*tstSlicerIn1)(nil)
|
||||
|
||||
type testSlicerInterface interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
type testSlicerInterfaces []testSlicerInterface
|
||||
|
||||
type tstSlicerIn1 struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type tstSlicerIn2 struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type tstSlicer struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case testSlicerInterface:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
result := make(testSlicerInterfaces, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case testSlicerInterface:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn1) Name() string {
|
||||
return p.Name()
|
||||
}
|
||||
|
||||
func (p *tstSlicerIn2) Name() string {
|
||||
return p.Name()
|
||||
}
|
||||
|
||||
func (p *tstSlicer) Slice(in interface{}) (interface{}, error) {
|
||||
items := in.([]interface{})
|
||||
result := make(tstSlicers, len(items))
|
||||
for i, v := range items {
|
||||
switch vv := v.(type) {
|
||||
case *tstSlicer:
|
||||
result[i] = vv
|
||||
default:
|
||||
return nil, errors.New("invalid type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type tstSlicers []*tstSlicer
|
||||
|
||||
func TestSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for i, test := range []struct {
|
||||
args []interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{[]interface{}{"a", "b"}, []string{"a", "b"}},
|
||||
{[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}},
|
||||
{[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}},
|
||||
{[]interface{}{}, []interface{}{}},
|
||||
{[]interface{}{nil}, []interface{}{nil}},
|
||||
{[]interface{}{5, "b"}, []interface{}{5, "b"}},
|
||||
{[]interface{}{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}},
|
||||
{[]interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}},
|
||||
} {
|
||||
errMsg := fmt.Sprintf("[%d] %v", i, test.args)
|
||||
|
||||
result := Slice(test.args...)
|
||||
|
||||
assert.Equal(t, test.expected, result, errMsg)
|
||||
}
|
||||
|
||||
assert.Len(t, Slice(), 0)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// 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 errors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// LineMatcher contains the elements used to match an error to a line
|
||||
type LineMatcher struct {
|
||||
Position text.Position
|
||||
Error error
|
||||
|
||||
LineNumber int
|
||||
Offset int
|
||||
Line string
|
||||
}
|
||||
|
||||
// LineMatcherFn is used to match a line with an error.
|
||||
type LineMatcherFn func(m LineMatcher) bool
|
||||
|
||||
// SimpleLineMatcher simply matches by line number.
|
||||
var SimpleLineMatcher = func(m LineMatcher) bool {
|
||||
return m.Position.LineNumber == m.LineNumber
|
||||
}
|
||||
|
||||
var _ text.Positioner = ErrorContext{}
|
||||
|
||||
// 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
|
||||
|
||||
// The position of the error in the Lines above. 0 based.
|
||||
LinesPos int
|
||||
|
||||
position text.Position
|
||||
|
||||
// The lexer to use for syntax highlighting.
|
||||
// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
|
||||
ChromaLexer string
|
||||
}
|
||||
|
||||
// Position returns the text position of this error.
|
||||
func (e ErrorContext) Position() text.Position {
|
||||
return e.position
|
||||
}
|
||||
|
||||
var _ causer = (*ErrorWithFileContext)(nil)
|
||||
|
||||
// ErrorWithFileContext is an error with some additional file context related
|
||||
// to that error.
|
||||
type ErrorWithFileContext struct {
|
||||
cause error
|
||||
ErrorContext
|
||||
}
|
||||
|
||||
func (e *ErrorWithFileContext) Error() string {
|
||||
pos := e.Position()
|
||||
if pos.IsValid() {
|
||||
return pos.String() + ": " + e.cause.Error()
|
||||
}
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e *ErrorWithFileContext) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
|
||||
// If no match could be found, the original error is returned with false as the second return value.
|
||||
func WithFileContextForFile(e error, realFilename, filename string, fs afero.Fs, matcher LineMatcherFn) (error, bool) {
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return e, false
|
||||
}
|
||||
defer f.Close()
|
||||
return WithFileContext(e, realFilename, f, matcher)
|
||||
}
|
||||
|
||||
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
|
||||
// If no match could be found, the original error is returned with false as the second return value.
|
||||
func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatcherFn) (error, bool) {
|
||||
if e == nil {
|
||||
panic("error missing")
|
||||
}
|
||||
le := UnwrapFileError(e)
|
||||
|
||||
if le == nil {
|
||||
var ok bool
|
||||
if le, ok = ToFileError("", e).(FileError); !ok {
|
||||
return e, false
|
||||
}
|
||||
}
|
||||
|
||||
var errCtx ErrorContext
|
||||
|
||||
posle := le.Position()
|
||||
|
||||
if posle.Offset != -1 {
|
||||
errCtx = locateError(r, le, func(m LineMatcher) bool {
|
||||
if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) {
|
||||
lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber
|
||||
m.Position = text.Position{LineNumber: lno}
|
||||
}
|
||||
return matcher(m)
|
||||
})
|
||||
} else {
|
||||
errCtx = locateError(r, le, matcher)
|
||||
}
|
||||
|
||||
pos := &errCtx.position
|
||||
|
||||
if pos.LineNumber == -1 {
|
||||
return e, false
|
||||
}
|
||||
|
||||
pos.Filename = realFilename
|
||||
|
||||
if le.Type() != "" {
|
||||
errCtx.ChromaLexer = chromaLexerFromType(le.Type())
|
||||
} else {
|
||||
errCtx.ChromaLexer = chromaLexerFromFilename(realFilename)
|
||||
}
|
||||
|
||||
return &ErrorWithFileContext{cause: e, ErrorContext: errCtx}, true
|
||||
}
|
||||
|
||||
// UnwrapErrorWithFileContext tries to unwrap an ErrorWithFileContext from err.
|
||||
// It returns nil if this is not possible.
|
||||
func UnwrapErrorWithFileContext(err error) *ErrorWithFileContext {
|
||||
for err != nil {
|
||||
switch v := err.(type) {
|
||||
case *ErrorWithFileContext:
|
||||
return v
|
||||
case causer:
|
||||
err = v.Cause()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func chromaLexerFromType(fileType string) string {
|
||||
switch fileType {
|
||||
case "html", "htm":
|
||||
return "go-html-template"
|
||||
}
|
||||
return fileType
|
||||
}
|
||||
|
||||
func extNoDelimiter(filename string) string {
|
||||
return strings.TrimPrefix(".", filepath.Ext(filename))
|
||||
}
|
||||
|
||||
func chromaLexerFromFilename(filename string) string {
|
||||
if strings.Contains(filename, "layouts") {
|
||||
return "go-html-template"
|
||||
}
|
||||
|
||||
ext := extNoDelimiter(filename)
|
||||
return chromaLexerFromType(ext)
|
||||
}
|
||||
|
||||
func locateErrorInString(src string, matcher LineMatcherFn) ErrorContext {
|
||||
return locateError(strings.NewReader(src), &fileError{}, matcher)
|
||||
}
|
||||
|
||||
func locateError(r io.Reader, le FileError, matches LineMatcherFn) ErrorContext {
|
||||
if le == nil {
|
||||
panic("must provide an error")
|
||||
}
|
||||
|
||||
errCtx := ErrorContext{position: text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1}, LinesPos: -1}
|
||||
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errCtx
|
||||
}
|
||||
|
||||
pos := &errCtx.position
|
||||
lepos := le.Position()
|
||||
|
||||
lines := strings.Split(string(b), "\n")
|
||||
|
||||
if le != nil && lepos.ColumnNumber >= 0 {
|
||||
pos.ColumnNumber = lepos.ColumnNumber
|
||||
}
|
||||
|
||||
lineNo := 0
|
||||
posBytes := 0
|
||||
|
||||
for li, line := range lines {
|
||||
lineNo = li + 1
|
||||
m := LineMatcher{
|
||||
Position: le.Position(),
|
||||
Error: le,
|
||||
LineNumber: lineNo,
|
||||
Offset: posBytes,
|
||||
Line: line,
|
||||
}
|
||||
if errCtx.LinesPos == -1 && matches(m) {
|
||||
pos.LineNumber = lineNo
|
||||
break
|
||||
}
|
||||
|
||||
posBytes += len(line)
|
||||
}
|
||||
|
||||
if pos.LineNumber != -1 {
|
||||
low := pos.LineNumber - 3
|
||||
if low < 0 {
|
||||
low = 0
|
||||
}
|
||||
|
||||
if pos.LineNumber > 2 {
|
||||
errCtx.LinesPos = 2
|
||||
} else {
|
||||
errCtx.LinesPos = pos.LineNumber - 1
|
||||
}
|
||||
|
||||
high := pos.LineNumber + 2
|
||||
if high > len(lines) {
|
||||
high = len(lines)
|
||||
}
|
||||
|
||||
errCtx.Lines = lines[low:high]
|
||||
|
||||
}
|
||||
|
||||
return errCtx
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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 errors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestErrorLocator(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
lineMatcher := func(m LineMatcher) bool {
|
||||
return strings.Contains(m.Line, "THEONE")
|
||||
}
|
||||
|
||||
lines := `LINE 1
|
||||
LINE 2
|
||||
LINE 3
|
||||
LINE 4
|
||||
This is THEONE
|
||||
LINE 6
|
||||
LINE 7
|
||||
LINE 8
|
||||
`
|
||||
|
||||
location := locateErrorInString(lines, lineMatcher)
|
||||
assert.Equal([]string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}, location.Lines)
|
||||
|
||||
pos := location.Position()
|
||||
assert.Equal(5, pos.LineNumber)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
assert.Equal([]string{"This is THEONE"}, locateErrorInString(`This is THEONE`, lineMatcher).Lines)
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
This is THEONE
|
||||
L2
|
||||
`, lineMatcher)
|
||||
assert.Equal(2, location.Position().LineNumber)
|
||||
assert.Equal(1, location.LinesPos)
|
||||
assert.Equal([]string{"L1", "This is THEONE", "L2", ""}, location.Lines)
|
||||
|
||||
location = locateErrorInString(`This is THEONE
|
||||
L2
|
||||
`, lineMatcher)
|
||||
assert.Equal(0, location.LinesPos)
|
||||
assert.Equal([]string{"This is THEONE", "L2", ""}, location.Lines)
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
This THEONE
|
||||
`, lineMatcher)
|
||||
assert.Equal([]string{"L1", "This THEONE", ""}, location.Lines)
|
||||
assert.Equal(1, location.LinesPos)
|
||||
|
||||
location = locateErrorInString(`L1
|
||||
L2
|
||||
This THEONE
|
||||
`, lineMatcher)
|
||||
assert.Equal([]string{"L1", "L2", "This THEONE", ""}, location.Lines)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
location = locateErrorInString("NO MATCH", lineMatcher)
|
||||
assert.Equal(-1, location.Position().LineNumber)
|
||||
assert.Equal(-1, location.LinesPos)
|
||||
assert.Equal(0, len(location.Lines))
|
||||
|
||||
lineMatcher = func(m LineMatcher) bool {
|
||||
return m.LineNumber == 6
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
H
|
||||
I
|
||||
J`, lineMatcher)
|
||||
|
||||
assert.Equal([]string{"D", "E", "F", "G", "H"}, location.Lines)
|
||||
assert.Equal(6, location.Position().LineNumber)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
// Test match EOF
|
||||
lineMatcher = func(m LineMatcher) bool {
|
||||
return m.LineNumber == 4
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
`, lineMatcher)
|
||||
|
||||
assert.Equal([]string{"B", "C", ""}, location.Lines)
|
||||
assert.Equal(4, location.Position().LineNumber)
|
||||
assert.Equal(2, location.LinesPos)
|
||||
|
||||
offsetMatcher := func(m LineMatcher) bool {
|
||||
return m.Offset == 1
|
||||
}
|
||||
|
||||
location = locateErrorInString(`A
|
||||
B
|
||||
C
|
||||
D
|
||||
E`, offsetMatcher)
|
||||
|
||||
assert.Equal([]string{"A", "B", "C", "D"}, location.Lines)
|
||||
assert.Equal(2, location.Position().LineNumber)
|
||||
assert.Equal(1, location.LinesPos)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 herrors contains common Hugo errors and error related utilities.
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// As defined in https://godoc.org/github.com/pkg/errors
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
type stackTracer interface {
|
||||
StackTrace() _errors.StackTrace
|
||||
}
|
||||
|
||||
// PrintStackTrace prints the error's stack trace to stdoud.
|
||||
func PrintStackTrace(err error) {
|
||||
FprintStackTrace(os.Stdout, err)
|
||||
}
|
||||
|
||||
// FprintStackTrace prints the error's stack trace to w.
|
||||
func FprintStackTrace(w io.Writer, err error) {
|
||||
if err, ok := err.(stackTracer); ok {
|
||||
for _, f := range err.StackTrace() {
|
||||
fmt.Fprintf(w, "%+s:%d\n", f, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrFeatureNotAvailable denotes that a feature is unavailable.
|
||||
//
|
||||
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
|
||||
// and this error is used to signal those situations.
|
||||
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version")
|
||||
@@ -0,0 +1,133 @@
|
||||
// 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
|
||||
// limitatio ns under the License.
|
||||
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
_ causer = (*fileError)(nil)
|
||||
)
|
||||
|
||||
// FileError represents an error when handling a file: Parsing a config file,
|
||||
// execute a template etc.
|
||||
type FileError interface {
|
||||
error
|
||||
|
||||
text.Positioner
|
||||
|
||||
// A string identifying the type of file, e.g. JSON, TOML, markdown etc.
|
||||
Type() string
|
||||
}
|
||||
|
||||
var _ FileError = (*fileError)(nil)
|
||||
|
||||
type fileError struct {
|
||||
position text.Position
|
||||
|
||||
fileType string
|
||||
|
||||
cause error
|
||||
}
|
||||
|
||||
// Position returns the text position of this error.
|
||||
func (e fileError) Position() text.Position {
|
||||
return e.position
|
||||
}
|
||||
|
||||
func (e *fileError) Type() string {
|
||||
return e.fileType
|
||||
}
|
||||
|
||||
func (e *fileError) Error() string {
|
||||
if e.cause == nil {
|
||||
return ""
|
||||
}
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (f *fileError) Cause() error {
|
||||
return f.cause
|
||||
}
|
||||
|
||||
// NewFileError creates a new FileError.
|
||||
func NewFileError(fileType string, offset, lineNumber, columnNumber int, err error) FileError {
|
||||
pos := text.Position{Offset: offset, LineNumber: lineNumber, ColumnNumber: columnNumber}
|
||||
return &fileError{cause: err, fileType: fileType, position: pos}
|
||||
}
|
||||
|
||||
// UnwrapFileError tries to unwrap a FileError from err.
|
||||
// It returns nil if this is not possible.
|
||||
func UnwrapFileError(err error) FileError {
|
||||
for err != nil {
|
||||
switch v := err.(type) {
|
||||
case FileError:
|
||||
return v
|
||||
case causer:
|
||||
err = v.Cause()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToFileErrorWithOffset will return a new FileError with a line number
|
||||
// with the given offset from the original.
|
||||
func ToFileErrorWithOffset(fe FileError, offset int) FileError {
|
||||
pos := fe.Position()
|
||||
return ToFileErrorWithLineNumber(fe, pos.LineNumber+offset)
|
||||
}
|
||||
|
||||
// ToFileErrorWithOffset will return a new FileError with the given line number.
|
||||
func ToFileErrorWithLineNumber(fe FileError, lineNumber int) FileError {
|
||||
pos := fe.Position()
|
||||
pos.LineNumber = lineNumber
|
||||
return &fileError{cause: fe, fileType: fe.Type(), position: pos}
|
||||
}
|
||||
|
||||
// ToFileError will convert the given error to an error supporting
|
||||
// the FileError interface.
|
||||
func ToFileError(fileType string, err error) FileError {
|
||||
for _, handle := range lineNumberExtractors {
|
||||
lno, col := handle(err)
|
||||
offset, typ := extractOffsetAndType(err)
|
||||
if fileType == "" {
|
||||
fileType = typ
|
||||
}
|
||||
|
||||
if lno > 0 || offset != -1 {
|
||||
return NewFileError(fileType, offset, lno, col, err)
|
||||
}
|
||||
}
|
||||
// Fall back to the pointing to line number 1.
|
||||
return NewFileError(fileType, -1, 1, 1, err)
|
||||
}
|
||||
|
||||
func extractOffsetAndType(e error) (int, string) {
|
||||
e = errors.Cause(e)
|
||||
switch v := e.(type) {
|
||||
case *json.UnmarshalTypeError:
|
||||
return int(v.Offset), "json"
|
||||
case *json.SyntaxError:
|
||||
return int(v.Offset), "json"
|
||||
default:
|
||||
return -1, ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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 herrors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToLineNumberError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := require.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
in error
|
||||
offset int
|
||||
lineNumber int
|
||||
columnNumber int
|
||||
}{
|
||||
{errors.New("no line number for you"), 0, 1, 1},
|
||||
{errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15},
|
||||
{errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1},
|
||||
{errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7},
|
||||
{errors.New("error in front matter: Near line 32 (last key parsed 'title')"), 0, 32, 1},
|
||||
{errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7},
|
||||
} {
|
||||
|
||||
got := ToFileError("template", test.in)
|
||||
|
||||
errMsg := fmt.Sprintf("[%d][%T]", i, got)
|
||||
le, ok := got.(FileError)
|
||||
assert.True(ok)
|
||||
|
||||
assert.True(ok, errMsg)
|
||||
pos := le.Position()
|
||||
assert.Equal(test.lineNumber, pos.LineNumber, errMsg)
|
||||
assert.Equal(test.columnNumber, pos.ColumnNumber, errMsg)
|
||||
assert.Error(errors.Cause(got))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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
|
||||
// limitatio ns under the License.
|
||||
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var lineNumberExtractors = []lineNumberExtractor{
|
||||
// Template/shortcode parse errors
|
||||
newLineNumberErrHandlerFromRegexp(".*:(\\d+):(\\d*):"),
|
||||
newLineNumberErrHandlerFromRegexp(".*:(\\d+):"),
|
||||
|
||||
// TOML parse errors
|
||||
newLineNumberErrHandlerFromRegexp(".*Near line (\\d+)(\\s.*)"),
|
||||
|
||||
// YAML parse errors
|
||||
newLineNumberErrHandlerFromRegexp("line (\\d+):"),
|
||||
|
||||
// i18n bundle errors
|
||||
newLineNumberErrHandlerFromRegexp("\\((\\d+),\\s(\\d*)"),
|
||||
}
|
||||
|
||||
type lineNumberExtractor func(e error) (int, int)
|
||||
|
||||
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
|
||||
re := regexp.MustCompile(expression)
|
||||
return extractLineNo(re)
|
||||
}
|
||||
|
||||
func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
|
||||
return func(e error) (int, int) {
|
||||
if e == nil {
|
||||
panic("no error")
|
||||
}
|
||||
col := 1
|
||||
s := e.Error()
|
||||
m := re.FindStringSubmatch(s)
|
||||
if len(m) >= 2 {
|
||||
lno, _ := strconv.Atoi(m[1])
|
||||
if len(m) > 2 {
|
||||
col, _ = strconv.Atoi(m[2])
|
||||
}
|
||||
|
||||
if col <= 0 {
|
||||
col = 1
|
||||
}
|
||||
|
||||
return lno, col
|
||||
}
|
||||
|
||||
return -1, col
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 hugio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadSeeker wraps io.Reader and io.Seeker.
|
||||
type ReadSeeker interface {
|
||||
io.Reader
|
||||
io.Seeker
|
||||
}
|
||||
|
||||
// ReadSeekCloser is implemented by afero.File. We use this as the common type for
|
||||
// content in Resource objects, even for strings.
|
||||
type ReadSeekCloser interface {
|
||||
ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close.
|
||||
type ReadSeekerNoOpCloser struct {
|
||||
ReadSeeker
|
||||
}
|
||||
|
||||
// Close does nothing.
|
||||
func (r ReadSeekerNoOpCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
|
||||
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekerNoOpCloser {
|
||||
return ReadSeekerNoOpCloser{r}
|
||||
}
|
||||
|
||||
// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser
|
||||
// from the given string.
|
||||
func NewReadSeekerNoOpCloserFromString(content string) ReadSeekerNoOpCloser {
|
||||
return ReadSeekerNoOpCloser{strings.NewReader(content)}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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 hugio
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type multiWriteCloser struct {
|
||||
io.Writer
|
||||
closers []io.WriteCloser
|
||||
}
|
||||
|
||||
func (m multiWriteCloser) Close() error {
|
||||
var err error
|
||||
for _, c := range m.closers {
|
||||
if closeErr := c.Close(); err != nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NewMultiWriteCloser creates a new io.WriteCloser that duplicates its writes to all the
|
||||
// provided writers.
|
||||
func NewMultiWriteCloser(writeClosers ...io.WriteCloser) io.WriteCloser {
|
||||
writers := make([]io.Writer, len(writeClosers))
|
||||
for i, w := range writeClosers {
|
||||
writers[i] = w
|
||||
}
|
||||
return multiWriteCloser{Writer: io.MultiWriter(writers...), closers: writeClosers}
|
||||
}
|
||||
|
||||
// ToWriteCloser creates an io.WriteCloser from the given io.Writer.
|
||||
// If it's not already, one will be created with a Close method that does nothing.
|
||||
func ToWriteCloser(w io.Writer) io.WriteCloser {
|
||||
if rw, ok := w.(io.WriteCloser); ok {
|
||||
return rw
|
||||
}
|
||||
|
||||
return struct {
|
||||
io.Writer
|
||||
io.Closer
|
||||
}{
|
||||
w,
|
||||
ioutil.NopCloser(nil),
|
||||
}
|
||||
}
|
||||
|
||||
// ToReadCloser creates an io.ReadCloser from the given io.Reader.
|
||||
// If it's not already, one will be created with a Close method that does nothing.
|
||||
func ToReadCloser(r io.Reader) io.ReadCloser {
|
||||
if rc, ok := r.(io.ReadCloser); ok {
|
||||
return rc
|
||||
}
|
||||
|
||||
return struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
r,
|
||||
ioutil.NopCloser(nil),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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 hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvironmentDevelopment = "development"
|
||||
EnvironmentProduction = "production"
|
||||
)
|
||||
|
||||
var (
|
||||
// commitHash contains the current Git revision. Use make to build to make
|
||||
// sure this gets set.
|
||||
commitHash string
|
||||
|
||||
// buildDate contains the date of the current build.
|
||||
buildDate string
|
||||
)
|
||||
|
||||
// Info contains information about the current Hugo environment
|
||||
type Info struct {
|
||||
CommitHash string
|
||||
BuildDate string
|
||||
|
||||
// The build environment.
|
||||
// Defaults are "production" (hugo) and "development" (hugo server).
|
||||
// This can also be set by the user.
|
||||
// It can be any string, but it will be all lower case.
|
||||
Environment string
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
func (i Info) Version() VersionString {
|
||||
return CurrentVersion.Version()
|
||||
}
|
||||
|
||||
// Generator a Hugo meta generator HTML tag.
|
||||
func (i Info) Generator() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String()))
|
||||
}
|
||||
|
||||
// NewInfo creates a new Hugo Info object.
|
||||
func NewInfo(environment string) Info {
|
||||
if environment == "" {
|
||||
environment = EnvironmentProduction
|
||||
}
|
||||
return Info{
|
||||
CommitHash: commitHash,
|
||||
BuildDate: buildDate,
|
||||
Environment: environment,
|
||||
}
|
||||
}
|
||||
@@ -11,23 +11,25 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHugoInfo(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
assert.Equal(helpers.CurrentHugoVersion.Version(), hugoInfo.Version)
|
||||
assert.IsType(helpers.HugoVersionString(""), hugoInfo.Version)
|
||||
assert.Equal(CommitHash, hugoInfo.CommitHash)
|
||||
assert.Equal(BuildDate, hugoInfo.BuildDate)
|
||||
assert.Contains(hugoInfo.Generator, fmt.Sprintf("Hugo %s", hugoInfo.Version))
|
||||
hugoInfo := NewInfo("")
|
||||
|
||||
assert.Equal(CurrentVersion.Version(), hugoInfo.Version())
|
||||
assert.IsType(VersionString(""), hugoInfo.Version())
|
||||
assert.Equal(commitHash, hugoInfo.CommitHash)
|
||||
assert.Equal(buildDate, hugoInfo.BuildDate)
|
||||
assert.Equal("production", hugoInfo.Environment)
|
||||
assert.Contains(hugoInfo.Generator(), fmt.Sprintf("Hugo %s", hugoInfo.Version()))
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 hugo
|
||||
|
||||
import "github.com/gohugoio/hugo/langs"
|
||||
|
||||
// Site represents a site in the build. This is currently a very narrow interface,
|
||||
// but the actual implementation will be richer, see hugolib.SiteInfo.
|
||||
type Site interface {
|
||||
Language() *langs.Language
|
||||
IsServer() bool
|
||||
Hugo() Info
|
||||
}
|
||||
@@ -11,13 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build windows
|
||||
// +build extended
|
||||
|
||||
package commands
|
||||
package hugo
|
||||
|
||||
const (
|
||||
ansiEsc = ""
|
||||
clearLine = ""
|
||||
hideCursor = ""
|
||||
showCursor = ""
|
||||
)
|
||||
var isExtended = true
|
||||
@@ -11,13 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !windows
|
||||
// +build !extended
|
||||
|
||||
package commands
|
||||
package hugo
|
||||
|
||||
const (
|
||||
ansiEsc = "\u001B"
|
||||
clearLine = "\r\033[K"
|
||||
hideCursor = ansiEsc + "[?25l"
|
||||
showCursor = ansiEsc + "[?25h"
|
||||
)
|
||||
var isExtended = false
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,18 +11,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/compare"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// HugoVersion represents the Hugo build version.
|
||||
type HugoVersion struct {
|
||||
// Version represents the Hugo build version.
|
||||
type Version struct {
|
||||
// Major and minor version.
|
||||
Number float32
|
||||
|
||||
@@ -35,32 +37,34 @@ type HugoVersion struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ compare.Eqer = (*HugoVersionString)(nil)
|
||||
_ compare.Comparer = (*HugoVersionString)(nil)
|
||||
_ compare.Eqer = (*VersionString)(nil)
|
||||
_ compare.Comparer = (*VersionString)(nil)
|
||||
)
|
||||
|
||||
type HugoVersionString string
|
||||
|
||||
func (v HugoVersion) String() string {
|
||||
return hugoVersion(v.Number, v.PatchLevel, v.Suffix)
|
||||
func (v Version) String() string {
|
||||
return version(v.Number, v.PatchLevel, v.Suffix)
|
||||
}
|
||||
|
||||
func (v HugoVersion) Version() HugoVersionString {
|
||||
return HugoVersionString(v.String())
|
||||
// Version returns the Hugo version.
|
||||
func (v Version) Version() VersionString {
|
||||
return VersionString(v.String())
|
||||
}
|
||||
|
||||
func (h HugoVersionString) String() string {
|
||||
// VersionString represents a Hugo version string.
|
||||
type VersionString string
|
||||
|
||||
func (h VersionString) String() string {
|
||||
return string(h)
|
||||
}
|
||||
|
||||
// Implements compare.Comparer
|
||||
func (h HugoVersionString) Compare(other interface{}) int {
|
||||
v := MustParseHugoVersion(h.String())
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other interface{}) int {
|
||||
v := MustParseVersion(h.String())
|
||||
return compareVersionsWithSuffix(v.Number, v.PatchLevel, v.Suffix, other)
|
||||
}
|
||||
|
||||
// Implements compare.Eqer
|
||||
func (h HugoVersionString) Eq(other interface{}) bool {
|
||||
// Eq implements the compare.Eqer interface.
|
||||
func (h VersionString) Eq(other interface{}) bool {
|
||||
s, err := cast.ToStringE(other)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -70,9 +74,9 @@ func (h HugoVersionString) Eq(other interface{}) bool {
|
||||
|
||||
var versionSuffixes = []string{"-test", "-DEV"}
|
||||
|
||||
// ParseHugoVersion parses a version string.
|
||||
func ParseHugoVersion(s string) (HugoVersion, error) {
|
||||
var vv HugoVersion
|
||||
// ParseVersion parses a version string.
|
||||
func ParseVersion(s string) (Version, error) {
|
||||
var vv Version
|
||||
for _, suffix := range versionSuffixes {
|
||||
if strings.HasSuffix(s, suffix) {
|
||||
vv.Suffix = suffix
|
||||
@@ -88,10 +92,10 @@ func ParseHugoVersion(s string) (HugoVersion, error) {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
// MustParseHugoVersion parses a version string
|
||||
// MustParseVersion parses a version string
|
||||
// and panics if any error occurs.
|
||||
func MustParseHugoVersion(s string) HugoVersion {
|
||||
vv, err := ParseHugoVersion(s)
|
||||
func MustParseVersion(s string) Version {
|
||||
vv, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -99,36 +103,52 @@ func MustParseHugoVersion(s string) HugoVersion {
|
||||
}
|
||||
|
||||
// ReleaseVersion represents the release version.
|
||||
func (v HugoVersion) ReleaseVersion() HugoVersion {
|
||||
func (v Version) ReleaseVersion() Version {
|
||||
v.Suffix = ""
|
||||
return v
|
||||
}
|
||||
|
||||
// Next returns the next Hugo release version.
|
||||
func (v HugoVersion) Next() HugoVersion {
|
||||
return HugoVersion{Number: v.Number + 0.01}
|
||||
func (v Version) Next() Version {
|
||||
return Version{Number: v.Number + 0.01}
|
||||
}
|
||||
|
||||
// Prev returns the previous Hugo release version.
|
||||
func (v HugoVersion) Prev() HugoVersion {
|
||||
return HugoVersion{Number: v.Number - 0.01}
|
||||
func (v Version) Prev() Version {
|
||||
return Version{Number: v.Number - 0.01}
|
||||
}
|
||||
|
||||
// NextPatchLevel returns the next patch/bugfix Hugo version.
|
||||
// This will be a patch increment on the previous Hugo version.
|
||||
func (v HugoVersion) NextPatchLevel(level int) HugoVersion {
|
||||
return HugoVersion{Number: v.Number - 0.01, PatchLevel: level}
|
||||
func (v Version) NextPatchLevel(level int) Version {
|
||||
return Version{Number: v.Number - 0.01, PatchLevel: level}
|
||||
}
|
||||
|
||||
// CurrentHugoVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentHugoVersion = HugoVersion{
|
||||
Number: 0.41,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
// BuildVersionString creates a version string. This is what you see when
|
||||
// running "hugo version".
|
||||
func BuildVersionString() string {
|
||||
program := "Hugo Static Site Generator"
|
||||
|
||||
version := "v" + CurrentVersion.String()
|
||||
if commitHash != "" {
|
||||
version += "-" + strings.ToUpper(commitHash)
|
||||
}
|
||||
if isExtended {
|
||||
version += "/extended"
|
||||
}
|
||||
|
||||
osArch := runtime.GOOS + "/" + runtime.GOARCH
|
||||
|
||||
date := buildDate
|
||||
if date == "" {
|
||||
date = "unknown"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date)
|
||||
|
||||
}
|
||||
|
||||
func hugoVersion(version float32, patchVersion int, suffix string) string {
|
||||
func version(version float32, patchVersion int, suffix string) string {
|
||||
if patchVersion > 0 {
|
||||
return fmt.Sprintf("%.2f.%d%s", version, patchVersion, suffix)
|
||||
}
|
||||
@@ -140,7 +160,7 @@ func hugoVersion(version float32, patchVersion int, suffix string) string {
|
||||
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
|
||||
// the running version.
|
||||
func CompareVersion(version interface{}) int {
|
||||
return compareVersionsWithSuffix(CurrentHugoVersion.Number, CurrentHugoVersion.PatchLevel, CurrentHugoVersion.Suffix, version)
|
||||
return compareVersionsWithSuffix(CurrentVersion.Number, CurrentVersion.PatchLevel, CurrentVersion.Suffix, version)
|
||||
}
|
||||
|
||||
func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int {
|
||||
@@ -166,7 +186,7 @@ func compareVersionsWithSuffix(inVersion float32, inPatchVersion int, suffix str
|
||||
return -1
|
||||
}
|
||||
|
||||
v, err := ParseHugoVersion(s)
|
||||
v, err := ParseVersion(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 hugo
|
||||
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Number: 0.53,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
)
|
||||
|
||||
func TestHugoVersion(t *testing.T) {
|
||||
assert.Equal(t, "0.15-DEV", hugoVersion(0.15, 0, "-DEV"))
|
||||
assert.Equal(t, "0.15.2-DEV", hugoVersion(0.15, 2, "-DEV"))
|
||||
assert.Equal(t, "0.15-DEV", version(0.15, 0, "-DEV"))
|
||||
assert.Equal(t, "0.15.2-DEV", version(0.15, 2, "-DEV"))
|
||||
|
||||
v := HugoVersion{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"}
|
||||
v := Version{Number: 0.21, PatchLevel: 0, Suffix: "-DEV"}
|
||||
|
||||
require.Equal(t, v.ReleaseVersion().String(), "0.21")
|
||||
require.Equal(t, "0.21-DEV", v.String())
|
||||
@@ -62,9 +62,9 @@ func TestCompareVersions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseHugoVersion(t *testing.T) {
|
||||
require.Equal(t, "0.25", MustParseHugoVersion("0.25").String())
|
||||
require.Equal(t, "0.25.2", MustParseHugoVersion("0.25.2").String())
|
||||
require.Equal(t, "0.25-test", MustParseHugoVersion("0.25-test").String())
|
||||
require.Equal(t, "0.25-DEV", MustParseHugoVersion("0.25-DEV").String())
|
||||
require.Equal(t, "0.25", MustParseVersion("0.25").String())
|
||||
require.Equal(t, "0.25.2", MustParseVersion("0.25.2").String())
|
||||
require.Equal(t, "0.25-test", MustParseVersion("0.25-test").String())
|
||||
require.Equal(t, "0.25-DEV", MustParseVersion("0.25-DEV").String())
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
var (
|
||||
// Counts ERROR logs to the global jww logger.
|
||||
GlobalErrorCounter *jww.Counter
|
||||
)
|
||||
|
||||
func init() {
|
||||
GlobalErrorCounter = &jww.Counter{}
|
||||
jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError))
|
||||
}
|
||||
|
||||
// Logger wraps a *loggers.Logger and some other related logging state.
|
||||
type Logger struct {
|
||||
*jww.Notepad
|
||||
ErrorCounter *jww.Counter
|
||||
|
||||
// This is only set in server mode.
|
||||
errors *bytes.Buffer
|
||||
}
|
||||
|
||||
func (l *Logger) Errors() string {
|
||||
if l.errors == nil {
|
||||
return ""
|
||||
}
|
||||
return ansiColorRe.ReplaceAllString(l.errors.String(), "")
|
||||
}
|
||||
|
||||
// Reset resets the logger's internal state.
|
||||
func (l *Logger) Reset() {
|
||||
l.ErrorCounter.Reset()
|
||||
if l.errors != nil {
|
||||
l.errors.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogger creates a new Logger for the given thresholds
|
||||
func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
|
||||
return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors)
|
||||
}
|
||||
|
||||
// NewDebugLogger is a convenience function to create a debug logger.
|
||||
func NewDebugLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelDebug)
|
||||
}
|
||||
|
||||
// NewWarningLogger is a convenience function to create a warning logger.
|
||||
func NewWarningLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelWarn)
|
||||
}
|
||||
|
||||
// NewErrorLogger is a convenience function to create an error logger.
|
||||
func NewErrorLogger() *Logger {
|
||||
return newBasicLogger(jww.LevelError)
|
||||
}
|
||||
|
||||
var (
|
||||
ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m")
|
||||
errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)")
|
||||
)
|
||||
|
||||
type ansiCleaner struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a ansiCleaner) Write(p []byte) (n int, err error) {
|
||||
return a.w.Write(ansiColorRe.ReplaceAll(p, []byte("")))
|
||||
}
|
||||
|
||||
type labelColorizer struct {
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (a labelColorizer) Write(p []byte) (n int, err error) {
|
||||
replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string {
|
||||
switch m {
|
||||
case "ERROR", "FATAL":
|
||||
return terminal.Error(m)
|
||||
case "WARN":
|
||||
return terminal.Warning(m)
|
||||
default:
|
||||
return m
|
||||
}
|
||||
})
|
||||
// io.MultiWriter will abort if we return a bigger write count than input
|
||||
// bytes, so we lie a little.
|
||||
_, err = a.w.Write([]byte(replaced))
|
||||
return len(p), err
|
||||
|
||||
}
|
||||
|
||||
// InitGlobalLogger initializes the global logger, used in some rare cases.
|
||||
func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) {
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
jww.SetStdoutOutput(outHandle)
|
||||
jww.SetLogOutput(logHandle)
|
||||
jww.SetLogThreshold(logThreshold)
|
||||
jww.SetStdoutThreshold(stdoutThreshold)
|
||||
|
||||
}
|
||||
|
||||
func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
|
||||
isTerm := terminal.IsTerminal(os.Stdout)
|
||||
if logHandle != ioutil.Discard && isTerm {
|
||||
// Remove any Ansi coloring from log output
|
||||
logHandle = ansiCleaner{w: logHandle}
|
||||
}
|
||||
|
||||
if isTerm {
|
||||
outHandle = labelColorizer{w: outHandle}
|
||||
}
|
||||
|
||||
return outHandle, logHandle
|
||||
|
||||
}
|
||||
|
||||
func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger {
|
||||
errorCounter := &jww.Counter{}
|
||||
outHandle, logHandle = getLogWriters(outHandle, logHandle)
|
||||
|
||||
listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError)}
|
||||
var errorBuff *bytes.Buffer
|
||||
if saveErrors {
|
||||
errorBuff = new(bytes.Buffer)
|
||||
errorCapture := func(t jww.Threshold) io.Writer {
|
||||
if t != jww.LevelError {
|
||||
// Only interested in ERROR
|
||||
return nil
|
||||
}
|
||||
|
||||
return errorBuff
|
||||
}
|
||||
|
||||
listeners = append(listeners, errorCapture)
|
||||
}
|
||||
|
||||
return &Logger{
|
||||
Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...),
|
||||
ErrorCounter: errorCounter,
|
||||
errors: errorBuff,
|
||||
}
|
||||
}
|
||||
|
||||
func newBasicLogger(t jww.Threshold) *Logger {
|
||||
return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2018 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loggers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
l := NewWarningLogger()
|
||||
|
||||
l.ERROR.Println("One error")
|
||||
l.ERROR.Println("Two error")
|
||||
l.WARN.Println("A warning")
|
||||
|
||||
assert.Equal(uint64(2), l.ErrorCounter.Count())
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 maps
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ToLower makes all the keys in the given map lower cased and will do so
|
||||
// recursively.
|
||||
// Notes:
|
||||
// * This will modify the map given.
|
||||
// * Any nested map[interface{}]interface{} will be converted to map[string]interface{}.
|
||||
func ToLower(m map[string]interface{}) {
|
||||
for k, v := range m {
|
||||
switch v.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
v = cast.ToStringMap(v)
|
||||
ToLower(v.(map[string]interface{}))
|
||||
case map[string]interface{}:
|
||||
ToLower(v.(map[string]interface{}))
|
||||
}
|
||||
|
||||
lKey := strings.ToLower(k)
|
||||
if k != lKey {
|
||||
delete(m, k)
|
||||
m[lKey] = v
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type keyRename struct {
|
||||
pattern glob.Glob
|
||||
newKey string
|
||||
}
|
||||
|
||||
// KeyRenamer supports renaming of keys in a map.
|
||||
type KeyRenamer struct {
|
||||
renames []keyRename
|
||||
}
|
||||
|
||||
// NewKeyRenamer creates a new KeyRenamer given a list of pattern and new key
|
||||
// value pairs.
|
||||
func NewKeyRenamer(patternKeys ...string) (KeyRenamer, error) {
|
||||
var renames []keyRename
|
||||
for i := 0; i < len(patternKeys); i += 2 {
|
||||
g, err := glob.Compile(strings.ToLower(patternKeys[i]), '/')
|
||||
if err != nil {
|
||||
return KeyRenamer{}, err
|
||||
}
|
||||
renames = append(renames, keyRename{pattern: g, newKey: patternKeys[i+1]})
|
||||
}
|
||||
|
||||
return KeyRenamer{renames: renames}, nil
|
||||
}
|
||||
|
||||
func (r KeyRenamer) getNewKey(keyPath string) string {
|
||||
for _, matcher := range r.renames {
|
||||
if matcher.pattern.Match(keyPath) {
|
||||
return matcher.newKey
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Rename renames the keys in the given map according
|
||||
// to the patterns in the current KeyRenamer.
|
||||
func (r KeyRenamer) Rename(m map[string]interface{}) {
|
||||
r.renamePath("", m)
|
||||
}
|
||||
|
||||
func (KeyRenamer) keyPath(k1, k2 string) string {
|
||||
k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
|
||||
if k1 == "" {
|
||||
return k2
|
||||
} else {
|
||||
return k1 + "/" + k2
|
||||
}
|
||||
}
|
||||
|
||||
func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
|
||||
for key, val := range m {
|
||||
keyPath := r.keyPath(parentKeyPath, key)
|
||||
switch val.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
val = cast.ToStringMap(val)
|
||||
r.renamePath(keyPath, val.(map[string]interface{}))
|
||||
case map[string]interface{}:
|
||||
r.renamePath(keyPath, val.(map[string]interface{}))
|
||||
}
|
||||
|
||||
newKey := r.getNewKey(keyPath)
|
||||
|
||||
if newKey != "" {
|
||||
delete(m, key)
|
||||
m[newKey] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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 maps
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToLower(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
map[string]interface{}{
|
||||
"abC": 32,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"abc": 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
map[string]interface{}{
|
||||
"abC": 32,
|
||||
"deF": map[interface{}]interface{}{
|
||||
23: "A value",
|
||||
24: map[string]interface{}{
|
||||
"AbCDe": "A value",
|
||||
"eFgHi": "Another value",
|
||||
},
|
||||
},
|
||||
"gHi": map[string]interface{}{
|
||||
"J": 25,
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"abc": 32,
|
||||
"def": map[string]interface{}{
|
||||
"23": "A value",
|
||||
"24": map[string]interface{}{
|
||||
"abcde": "A value",
|
||||
"efghi": "Another value",
|
||||
},
|
||||
},
|
||||
"ghi": map[string]interface{}{
|
||||
"j": 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
// ToLower modifies input.
|
||||
ToLower(test.input)
|
||||
if !reflect.DeepEqual(test.expected, test.input) {
|
||||
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameKeys(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
m := map[string]interface{}{
|
||||
"a": 32,
|
||||
"ren1": "m1",
|
||||
"ren2": "m1_2",
|
||||
"sub": map[string]interface{}{
|
||||
"subsub": map[string]interface{}{
|
||||
"REN1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
},
|
||||
"no": map[string]interface{}{
|
||||
"ren1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"a": 32,
|
||||
"new1": "m1",
|
||||
"new2": "m1_2",
|
||||
"sub": map[string]interface{}{
|
||||
"subsub": map[string]interface{}{
|
||||
"new1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
},
|
||||
"no": map[string]interface{}{
|
||||
"ren1": "m2",
|
||||
"ren2": "m2_2",
|
||||
},
|
||||
}
|
||||
|
||||
renamer, err := NewKeyRenamer(
|
||||
"{ren1,sub/*/ren1}", "new1",
|
||||
"{Ren2,sub/ren2}", "new2",
|
||||
)
|
||||
assert.NoError(err)
|
||||
|
||||
renamer.Rename(m)
|
||||
|
||||
if !reflect.DeepEqual(expected, m) {
|
||||
t.Errorf("Expected\n%#v, got\n%#v\n", expected, m)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,14 +11,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
package maps
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/math"
|
||||
"github.com/gohugoio/hugo/common/collections"
|
||||
"github.com/gohugoio/hugo/common/math"
|
||||
)
|
||||
|
||||
// Scratch is a writable context used for stateful operations in Page/Node rendering.
|
||||
@@ -40,14 +41,12 @@ func (c *Scratch) Add(key string, newAddend interface{}) (string, error) {
|
||||
if found {
|
||||
var err error
|
||||
|
||||
addendV := reflect.ValueOf(existingAddend)
|
||||
addendV := reflect.TypeOf(existingAddend)
|
||||
|
||||
if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array {
|
||||
nav := reflect.ValueOf(newAddend)
|
||||
if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array {
|
||||
newVal = reflect.AppendSlice(addendV, nav).Interface()
|
||||
} else {
|
||||
newVal = reflect.Append(addendV, nav).Interface()
|
||||
newVal, err = collections.Append(existingAddend, newAddend)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
newVal, err = math.DoArithmetic(existingAddend, newAddend, '+')
|
||||
@@ -73,7 +72,7 @@ func (c *Scratch) Set(key string, value interface{}) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Reset deletes the given key
|
||||
// Delete deletes the given key.
|
||||
func (c *Scratch) Delete(key string) string {
|
||||
c.mu.Lock()
|
||||
delete(c.values, key)
|
||||
@@ -81,7 +80,7 @@ func (c *Scratch) Delete(key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get returns a value previously set by Add or Set
|
||||
// Get returns a value previously set by Add or Set.
|
||||
func (c *Scratch) Get(key string) interface{} {
|
||||
c.mu.RLock()
|
||||
val := c.values[key]
|
||||
@@ -104,7 +103,7 @@ func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap
|
||||
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
|
||||
func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
c.mu.RLock()
|
||||
|
||||
@@ -130,6 +129,7 @@ func (c *Scratch) GetSortedMapValues(key string) interface{} {
|
||||
return sortedArray
|
||||
}
|
||||
|
||||
func newScratch() *Scratch {
|
||||
// NewScratch returns a new instance Scratch.
|
||||
func NewScratch() *Scratch {
|
||||
return &Scratch{values: make(map[string]interface{})}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2015 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -11,36 +11,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
package maps
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScratchAdd(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Add("int1", 10)
|
||||
scratch.Add("int1", 20)
|
||||
scratch.Add("int2", 20)
|
||||
|
||||
assert.Equal(t, int64(30), scratch.Get("int1"))
|
||||
assert.Equal(t, 20, scratch.Get("int2"))
|
||||
assert.Equal(int64(30), scratch.Get("int1"))
|
||||
assert.Equal(20, scratch.Get("int2"))
|
||||
|
||||
scratch.Add("float1", float64(10.5))
|
||||
scratch.Add("float1", float64(20.1))
|
||||
|
||||
assert.Equal(t, float64(30.6), scratch.Get("float1"))
|
||||
assert.Equal(float64(30.6), scratch.Get("float1"))
|
||||
|
||||
scratch.Add("string1", "Hello ")
|
||||
scratch.Add("string1", "big ")
|
||||
scratch.Add("string1", "World!")
|
||||
|
||||
assert.Equal(t, "Hello big World!", scratch.Get("string1"))
|
||||
assert.Equal("Hello big World!", scratch.Get("string1"))
|
||||
|
||||
scratch.Add("scratch", scratch)
|
||||
_, err := scratch.Add("scratch", scratch)
|
||||
@@ -53,12 +55,14 @@ func TestScratchAdd(t *testing.T) {
|
||||
|
||||
func TestScratchAddSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
|
||||
_, err := scratch.Add("intSlice", []int{1, 2})
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(err)
|
||||
_, err = scratch.Add("intSlice", 3)
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(err)
|
||||
|
||||
sl := scratch.Get("intSlice")
|
||||
expected := []int{1, 2, 3}
|
||||
@@ -66,10 +70,9 @@ func TestScratchAddSlice(t *testing.T) {
|
||||
if !reflect.DeepEqual(expected, sl) {
|
||||
t.Errorf("Slice difference, go %q expected %q", sl, expected)
|
||||
}
|
||||
|
||||
_, err = scratch.Add("intSlice", []int{4, 5})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(err)
|
||||
|
||||
sl = scratch.Get("intSlice")
|
||||
expected = []int{1, 2, 3, 4, 5}
|
||||
@@ -77,29 +80,61 @@ func TestScratchAddSlice(t *testing.T) {
|
||||
if !reflect.DeepEqual(expected, sl) {
|
||||
t.Errorf("Slice difference, go %q expected %q", sl, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5275
|
||||
func TestScratchAddTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("slice", []interface{}{})
|
||||
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
assert.NoError(err)
|
||||
assert.Equal([]int{1, 2}, scratch.Get("slice"))
|
||||
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5361
|
||||
func TestScratchAddDifferentTypedSliceToInterfaceSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("slice", []string{"foo"})
|
||||
|
||||
_, err := scratch.Add("slice", []int{1, 2})
|
||||
assert.NoError(err)
|
||||
assert.Equal([]interface{}{"foo", 1, 2}, scratch.Get("slice"))
|
||||
|
||||
}
|
||||
|
||||
func TestScratchSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("key", "val")
|
||||
assert.Equal(t, "val", scratch.Get("key"))
|
||||
assert.Equal("val", scratch.Get("key"))
|
||||
}
|
||||
|
||||
func TestScratchDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.Set("key", "val")
|
||||
scratch.Delete("key")
|
||||
scratch.Add("key", "Lucy Parsons")
|
||||
assert.Equal(t, "Lucy Parsons", scratch.Get("key"))
|
||||
assert.Equal("Lucy Parsons", scratch.Get("key"))
|
||||
}
|
||||
|
||||
// Issue #2005
|
||||
func TestScratchInParallel(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
scratch := newScratch()
|
||||
scratch := NewScratch()
|
||||
|
||||
key := "counter"
|
||||
scratch.Set(key, int64(1))
|
||||
for i := 1; i <= 10; i++ {
|
||||
@@ -133,7 +168,7 @@ func TestScratchInParallel(t *testing.T) {
|
||||
|
||||
func TestScratchGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
scratch := NewScratch()
|
||||
nothing := scratch.Get("nothing")
|
||||
if nothing != nil {
|
||||
t.Errorf("Should not return anything, but got %v", nothing)
|
||||
@@ -142,18 +177,20 @@ func TestScratchGet(t *testing.T) {
|
||||
|
||||
func TestScratchSetInMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
assert := require.New(t)
|
||||
|
||||
scratch := NewScratch()
|
||||
scratch.SetInMap("key", "lux", "Lux")
|
||||
scratch.SetInMap("key", "abc", "Abc")
|
||||
scratch.SetInMap("key", "zyx", "Zyx")
|
||||
scratch.SetInMap("key", "abc", "Abc (updated)")
|
||||
scratch.SetInMap("key", "def", "Def")
|
||||
assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key"))
|
||||
assert.Equal([]interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key"))
|
||||
}
|
||||
|
||||
func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
scratch := newScratch()
|
||||
scratch := NewScratch()
|
||||
nothing := scratch.GetSortedMapValues("nothing")
|
||||
if nothing != nil {
|
||||
t.Errorf("Should not return anything, but got %v", nothing)
|
||||
@@ -161,7 +198,7 @@ func TestScratchGetSortedMapValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkScratchGet(b *testing.B) {
|
||||
scratch := newScratch()
|
||||
scratch := NewScratch()
|
||||
scratch.Add("A", 1)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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 math
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to
|
||||
// determine the type of the two terms.
|
||||
func DoArithmetic(a, b interface{}, op rune) (interface{}, error) {
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
var ai, bi int64
|
||||
var af, bf float64
|
||||
var au, bu uint64
|
||||
switch av.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
ai = av.Int()
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
bi = bv.Int()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
af = float64(ai) // may overflow
|
||||
ai = 0
|
||||
bf = bv.Float()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bu = bv.Uint()
|
||||
if ai >= 0 {
|
||||
au = uint64(ai)
|
||||
ai = 0
|
||||
} else {
|
||||
bi = int64(bu) // may overflow
|
||||
bu = 0
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
af = av.Float()
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
bf = float64(bv.Int()) // may overflow
|
||||
case reflect.Float32, reflect.Float64:
|
||||
bf = bv.Float()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bf = float64(bv.Uint()) // may overflow
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
au = av.Uint()
|
||||
switch bv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
bi = bv.Int()
|
||||
if bi >= 0 {
|
||||
bu = uint64(bi)
|
||||
bi = 0
|
||||
} else {
|
||||
ai = int64(au) // may overflow
|
||||
au = 0
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
af = float64(au) // may overflow
|
||||
au = 0
|
||||
bf = bv.Float()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
bu = bv.Uint()
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
}
|
||||
case reflect.String:
|
||||
as := av.String()
|
||||
if bv.Kind() == reflect.String && op == '+' {
|
||||
bs := bv.String()
|
||||
return as + bs, nil
|
||||
}
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
default:
|
||||
return nil, errors.New("Can't apply the operator to the values")
|
||||
}
|
||||
|
||||
switch op {
|
||||
case '+':
|
||||
if ai != 0 || bi != 0 {
|
||||
return ai + bi, nil
|
||||
} else if af != 0 || bf != 0 {
|
||||
return af + bf, nil
|
||||
} else if au != 0 || bu != 0 {
|
||||
return au + bu, nil
|
||||
}
|
||||
return 0, nil
|
||||
case '-':
|
||||
if ai != 0 || bi != 0 {
|
||||
return ai - bi, nil
|
||||
} else if af != 0 || bf != 0 {
|
||||
return af - bf, nil
|
||||
} else if au != 0 || bu != 0 {
|
||||
return au - bu, nil
|
||||
}
|
||||
return 0, nil
|
||||
case '*':
|
||||
if ai != 0 || bi != 0 {
|
||||
return ai * bi, nil
|
||||
} else if af != 0 || bf != 0 {
|
||||
return af * bf, nil
|
||||
} else if au != 0 || bu != 0 {
|
||||
return au * bu, nil
|
||||
}
|
||||
return 0, nil
|
||||
case '/':
|
||||
if bi != 0 {
|
||||
return ai / bi, nil
|
||||
} else if bf != 0 {
|
||||
return af / bf, nil
|
||||
} else if bu != 0 {
|
||||
return au / bu, nil
|
||||
}
|
||||
return nil, errors.New("Can't divide the value by 0")
|
||||
default:
|
||||
return nil, errors.New("There is no such an operation")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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 math
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDoArithmetic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for i, test := range []struct {
|
||||
a interface{}
|
||||
b interface{}
|
||||
op rune
|
||||
expect interface{}
|
||||
}{
|
||||
{3, 2, '+', int64(5)},
|
||||
{3, 2, '-', int64(1)},
|
||||
{3, 2, '*', int64(6)},
|
||||
{3, 2, '/', int64(1)},
|
||||
{3.0, 2, '+', float64(5)},
|
||||
{3.0, 2, '-', float64(1)},
|
||||
{3.0, 2, '*', float64(6)},
|
||||
{3.0, 2, '/', float64(1.5)},
|
||||
{3, 2.0, '+', float64(5)},
|
||||
{3, 2.0, '-', float64(1)},
|
||||
{3, 2.0, '*', float64(6)},
|
||||
{3, 2.0, '/', float64(1.5)},
|
||||
{3.0, 2.0, '+', float64(5)},
|
||||
{3.0, 2.0, '-', float64(1)},
|
||||
{3.0, 2.0, '*', float64(6)},
|
||||
{3.0, 2.0, '/', float64(1.5)},
|
||||
{uint(3), uint(2), '+', uint64(5)},
|
||||
{uint(3), uint(2), '-', uint64(1)},
|
||||
{uint(3), uint(2), '*', uint64(6)},
|
||||
{uint(3), uint(2), '/', uint64(1)},
|
||||
{uint(3), 2, '+', uint64(5)},
|
||||
{uint(3), 2, '-', uint64(1)},
|
||||
{uint(3), 2, '*', uint64(6)},
|
||||
{uint(3), 2, '/', uint64(1)},
|
||||
{3, uint(2), '+', uint64(5)},
|
||||
{3, uint(2), '-', uint64(1)},
|
||||
{3, uint(2), '*', uint64(6)},
|
||||
{3, uint(2), '/', uint64(1)},
|
||||
{uint(3), -2, '+', int64(1)},
|
||||
{uint(3), -2, '-', int64(5)},
|
||||
{uint(3), -2, '*', int64(-6)},
|
||||
{uint(3), -2, '/', int64(-1)},
|
||||
{-3, uint(2), '+', int64(-1)},
|
||||
{-3, uint(2), '-', int64(-5)},
|
||||
{-3, uint(2), '*', int64(-6)},
|
||||
{-3, uint(2), '/', int64(-1)},
|
||||
{uint(3), 2.0, '+', float64(5)},
|
||||
{uint(3), 2.0, '-', float64(1)},
|
||||
{uint(3), 2.0, '*', float64(6)},
|
||||
{uint(3), 2.0, '/', float64(1.5)},
|
||||
{3.0, uint(2), '+', float64(5)},
|
||||
{3.0, uint(2), '-', float64(1)},
|
||||
{3.0, uint(2), '*', float64(6)},
|
||||
{3.0, uint(2), '/', float64(1.5)},
|
||||
{0, 0, '+', 0},
|
||||
{0, 0, '-', 0},
|
||||
{0, 0, '*', 0},
|
||||
{"foo", "bar", '+', "foobar"},
|
||||
{3, 0, '/', false},
|
||||
{3.0, 0, '/', false},
|
||||
{3, 0.0, '/', false},
|
||||
{uint(3), uint(0), '/', false},
|
||||
{3, uint(0), '/', false},
|
||||
{-3, uint(0), '/', false},
|
||||
{uint(3), 0, '/', false},
|
||||
{3.0, uint(0), '/', false},
|
||||
{uint(3), 0.0, '/', false},
|
||||
{3, "foo", '+', false},
|
||||
{3.0, "foo", '+', false},
|
||||
{uint(3), "foo", '+', false},
|
||||
{"foo", 3, '+', false},
|
||||
{"foo", "bar", '-', false},
|
||||
{3, 2, '%', false},
|
||||
} {
|
||||
errMsg := fmt.Sprintf("[%d] %v", i, test)
|
||||
|
||||
result, err := DoArithmetic(test.a, test.b, test.op)
|
||||
|
||||
if b, ok := test.expect.(bool); ok && !b {
|
||||
require.Error(t, err, errMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
require.NoError(t, err, errMsg)
|
||||
assert.Equal(t, test.expect, result, errMsg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 terminal contains helper for the terminal, such as coloring output.
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const (
|
||||
errorColor = "\033[1;31m%s\033[0m"
|
||||
warningColor = "\033[0;33m%s\033[0m"
|
||||
noticeColor = "\033[1;36m%s\033[0m"
|
||||
)
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal and the TERM
|
||||
// environment variable isn't a dumb one.
|
||||
func IsTerminal(f *os.File) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
return false
|
||||
}
|
||||
|
||||
fd := f.Fd()
|
||||
return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
|
||||
}
|
||||
|
||||
// Notice colorizes the string in a noticeable color.
|
||||
func Notice(s string) string {
|
||||
return colorize(s, noticeColor)
|
||||
}
|
||||
|
||||
// Error colorizes the string in a colour that grabs attention.
|
||||
func Error(s string) string {
|
||||
return colorize(s, errorColor)
|
||||
}
|
||||
|
||||
// Warning colorizes the string in a colour that warns.
|
||||
func Warning(s string) string {
|
||||
return colorize(s, warningColor)
|
||||
}
|
||||
|
||||
// colorize s in color.
|
||||
func colorize(s, color string) string {
|
||||
s = fmt.Sprintf(color, doublePercent(s))
|
||||
return singlePercent(s)
|
||||
}
|
||||
|
||||
func doublePercent(str string) string {
|
||||
return strings.Replace(str, "%", "%%", -1)
|
||||
}
|
||||
|
||||
func singlePercent(str string) string {
|
||||
return strings.Replace(str, "%%", "%", -1)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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 text
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/terminal"
|
||||
)
|
||||
|
||||
// Positioner represents a thing that knows its position in a text file or stream,
|
||||
// typically an error.
|
||||
type Positioner interface {
|
||||
Position() Position
|
||||
}
|
||||
|
||||
// Position holds a source position in a text file or stream.
|
||||
type Position struct {
|
||||
Filename string // filename, if any
|
||||
Offset int // byte offset, starting at 0. It's set to -1 if not provided.
|
||||
LineNumber int // line number, starting at 1
|
||||
ColumnNumber int // column number, starting at 1 (character count per line)
|
||||
}
|
||||
|
||||
func (pos Position) String() string {
|
||||
if pos.Filename == "" {
|
||||
pos.Filename = "<stream>"
|
||||
}
|
||||
return positionStringFormatfunc(pos)
|
||||
}
|
||||
|
||||
// IsValid returns true if line number is > 0.
|
||||
func (pos Position) IsValid() bool {
|
||||
return pos.LineNumber > 0
|
||||
}
|
||||
|
||||
var positionStringFormatfunc func(p Position) string
|
||||
|
||||
func createPositionStringFormatter(formatStr string) func(p Position) string {
|
||||
|
||||
if formatStr == "" {
|
||||
formatStr = "\":file::line::col\""
|
||||
}
|
||||
|
||||
var identifiers = []string{":file", ":line", ":col"}
|
||||
var identifiersFound []string
|
||||
|
||||
for i := range formatStr {
|
||||
for _, id := range identifiers {
|
||||
if strings.HasPrefix(formatStr[i:], id) {
|
||||
identifiersFound = append(identifiersFound, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d")
|
||||
format := replacer.Replace(formatStr)
|
||||
|
||||
f := func(pos Position) string {
|
||||
args := make([]interface{}, len(identifiersFound))
|
||||
for i, id := range identifiersFound {
|
||||
switch id {
|
||||
case ":file":
|
||||
args[i] = pos.Filename
|
||||
case ":line":
|
||||
args[i] = pos.LineNumber
|
||||
case ":col":
|
||||
args[i] = pos.ColumnNumber
|
||||
}
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
|
||||
if terminal.IsTerminal(os.Stdout) {
|
||||
return terminal.Notice(msg)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func init() {
|
||||
positionStringFormatfunc = createPositionStringFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT"))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 text
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPositionStringFormatter(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
pos := Position{Filename: "/my/file.txt", LineNumber: 12, ColumnNumber: 13, Offset: 14}
|
||||
|
||||
assert.Equal("/my/file.txt|13|12", createPositionStringFormatter(":file|:col|:line")(pos))
|
||||
assert.Equal("13|/my/file.txt|12", createPositionStringFormatter(":col|:file|:line")(pos))
|
||||
assert.Equal("好:13", createPositionStringFormatter("好::col")(pos))
|
||||
assert.Equal("\"/my/file.txt:12:13\"", createPositionStringFormatter("")(pos))
|
||||
assert.Equal("\"/my/file.txt:12:13\"", pos.String())
|
||||
|
||||
}
|
||||
@@ -52,6 +52,13 @@ func (q *EvictingStringQueue) Add(v string) {
|
||||
q.mu.Unlock()
|
||||
}
|
||||
|
||||
// Contains returns whether the queue contains v.
|
||||
func (q *EvictingStringQueue) Contains(v string) bool {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.set[v]
|
||||
}
|
||||
|
||||
// Peek looks at the last element added to the queue.
|
||||
func (q *EvictingStringQueue) Peek() string {
|
||||
q.mu.Lock()
|
||||
|
||||
@@ -36,6 +36,9 @@ func TestEvictingStringQueue(t *testing.T) {
|
||||
queue.Add("a")
|
||||
queue.Add("b")
|
||||
|
||||
assert.True(queue.Contains("a"))
|
||||
assert.False(queue.Contains("foo"))
|
||||
|
||||
assert.Equal([]string{"b", "a"}, queue.PeekAll())
|
||||
assert.Equal("b", queue.Peek())
|
||||
queue.Add("c")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
@@ -41,6 +41,8 @@ func (k KeyValues) String() string {
|
||||
return fmt.Sprintf("%v: %v", k.Key, k.Values)
|
||||
}
|
||||
|
||||
// NewKeyValuesStrings takes a given key and slice of values and returns a new
|
||||
// KeyValues struct.
|
||||
func NewKeyValuesStrings(key string, values ...string) KeyValues {
|
||||
iv := make([]interface{}, len(values))
|
||||
for i := 0; i < len(values); i++ {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 urls
|
||||
|
||||
// RefLinker is implemented by those who support reference linking.
|
||||
// args must contain a path, but can also point to the target
|
||||
// language or output format.
|
||||
type RefLinker interface {
|
||||
Ref(args map[string]interface{}) (string, error)
|
||||
RelRef(args map[string]interface{}) (string, error)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
|
||||
func FromConfigString(config, configType string) (Provider, error) {
|
||||
v := newViper()
|
||||
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.MergeConfigMap(m)
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// FromFile loads the configuration from the given filename.
|
||||
func FromFile(fs afero.Fs, filename string) (Provider, error) {
|
||||
m, err := loadConfigFromFile(fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := newViper()
|
||||
|
||||
err = v.MergeConfigMap(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// FromFileToMap is the same as FromFile, but it returns the config values
|
||||
// as a simple map.
|
||||
func FromFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
return loadConfigFromFile(fs, filename)
|
||||
}
|
||||
|
||||
func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}, error) {
|
||||
m, err := metadecoders.Default.UnmarshalToMap(data, format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
RenameKeys(m)
|
||||
|
||||
return m, nil
|
||||
|
||||
}
|
||||
|
||||
func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, error) {
|
||||
m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
RenameKeys(m)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var keyAliases maps.KeyRenamer
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
keyAliases, err = maps.NewKeyRenamer(
|
||||
// Before 0.53 we used singular for "menu".
|
||||
"{menu,languages/*/menu}", "menus",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// RenameKeys renames config keys in m recursively according to a global Hugo
|
||||
// alias definition.
|
||||
func RenameKeys(m map[string]interface{}) {
|
||||
keyAliases.Rename(m)
|
||||
}
|
||||
|
||||
func newViper() *viper.Viper {
|
||||
v := viper.New()
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvPrefix("hugo")
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -14,9 +14,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// Provider provides the configuration settings for Hugo.
|
||||
@@ -32,13 +30,13 @@ type Provider interface {
|
||||
IsSet(key string) bool
|
||||
}
|
||||
|
||||
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
|
||||
func FromConfigString(config, configType string) (Provider, error) {
|
||||
v := viper.New()
|
||||
v.SetConfigType(configType)
|
||||
if err := v.ReadConfig(strings.NewReader(config)); err != nil {
|
||||
return nil, err
|
||||
// GetStringSlicePreserveString returns a string slice from the given config and key.
|
||||
// It differs from the GetStringSlice method in that if the config value is a string,
|
||||
// we do not attempt to split it into fields.
|
||||
func GetStringSlicePreserveString(cfg Provider, key string) []string {
|
||||
sd := cfg.Get(key)
|
||||
if sds, ok := sd.(string); ok {
|
||||
return []string{sds}
|
||||
}
|
||||
return v, nil
|
||||
|
||||
return cast.ToStringSlice(sd)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetStringSlicePreserveString(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
cfg := viper.New()
|
||||
|
||||
s := "This is a string"
|
||||
sSlice := []string{"This", "is", "a", "slice"}
|
||||
|
||||
cfg.Set("s1", s)
|
||||
cfg.Set("s2", sSlice)
|
||||
|
||||
assert.Equal([]string{s}, GetStringSlicePreserveString(cfg, "s1"))
|
||||
assert.Equal(sSlice, GetStringSlicePreserveString(cfg, "s2"))
|
||||
assert.Nil(GetStringSlicePreserveString(cfg, "s3"))
|
||||
}
|
||||
@@ -30,7 +30,6 @@ type Config struct {
|
||||
Disqus Disqus
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram
|
||||
SpeakerDeck SpeakerDeck
|
||||
Twitter Twitter
|
||||
Vimeo Vimeo
|
||||
YouTube YouTube
|
||||
@@ -65,11 +64,6 @@ type Instagram struct {
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// SpeakerDeck holds the privacy configuration settings related to the SpeakerDeck shortcode.
|
||||
type SpeakerDeck struct {
|
||||
Service `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
// Twitter holds the privacy configuration settingsrelated to the Twitter shortcode.
|
||||
type Twitter struct {
|
||||
Service `mapstructure:",squash"`
|
||||
@@ -77,11 +71,19 @@ type Twitter struct {
|
||||
// When set to true, the Tweet and its embedded page on your site are not used
|
||||
// for purposes that include personalized suggestions and personalized ads.
|
||||
EnableDNT bool
|
||||
|
||||
// If simple mode is enabled, a static and no-JS version of the Tweet will be built.
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// Vimeo holds the privacy configuration settingsrelated to the Vimeo shortcode.
|
||||
type Vimeo struct {
|
||||
Service `mapstructure:",squash"`
|
||||
|
||||
// If simple mode is enabled, only a thumbnail is fetched from i.vimeocdn.com and
|
||||
// shown with a play button overlaid. If a user clicks the button, he/she will
|
||||
// be taken to the video page on vimeo.com in a new browser tab.
|
||||
Simple bool
|
||||
}
|
||||
|
||||
// YouTube holds the privacy configuration settingsrelated to the YouTube shortcode.
|
||||
@@ -94,6 +96,7 @@ type YouTube struct {
|
||||
PrivacyEnhanced bool
|
||||
}
|
||||
|
||||
// DecodeConfig creates a privacy Config from a given Hugo configuration.
|
||||
func DecodeConfig(cfg config.Provider) (pc Config, err error) {
|
||||
if !cfg.IsSet(privacyConfigKey) {
|
||||
return
|
||||
|
||||
@@ -39,13 +39,13 @@ useSessionStorage = true
|
||||
[privacy.instagram]
|
||||
disable = true
|
||||
simple = true
|
||||
[privacy.speakerDeck]
|
||||
disable = true
|
||||
[privacy.twitter]
|
||||
disable = true
|
||||
enableDNT = true
|
||||
simple = true
|
||||
[privacy.vimeo]
|
||||
disable = true
|
||||
simple = true
|
||||
[privacy.youtube]
|
||||
disable = true
|
||||
privacyEnhanced = true
|
||||
@@ -65,11 +65,11 @@ simple = true
|
||||
assert.True(pc.GoogleAnalytics.UseSessionStorage)
|
||||
assert.True(pc.Instagram.Disable)
|
||||
assert.True(pc.Instagram.Simple)
|
||||
assert.True(pc.SpeakerDeck.Disable)
|
||||
assert.True(pc.Twitter.Disable)
|
||||
assert.True(pc.Twitter.EnableDNT)
|
||||
assert.True(pc.Twitter.Simple)
|
||||
assert.True(pc.Vimeo.Disable)
|
||||
|
||||
assert.True(pc.Vimeo.Simple)
|
||||
assert.True(pc.YouTube.PrivacyEnhanced)
|
||||
assert.True(pc.YouTube.Disable)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type Config struct {
|
||||
Disqus Disqus
|
||||
GoogleAnalytics GoogleAnalytics
|
||||
Instagram Instagram
|
||||
Twitter Twitter
|
||||
}
|
||||
|
||||
// Disqus holds the functional configuration settings related to the Disqus template.
|
||||
@@ -52,12 +53,21 @@ type Instagram struct {
|
||||
DisableInlineCSS bool
|
||||
}
|
||||
|
||||
// Twitter holds the functional configuration settings related to the Twitter shortcodes.
|
||||
type Twitter struct {
|
||||
// The Simple variant of Twitter is decorated with a basic set of inline styles.
|
||||
// This means that if you want to provide your own CSS, you want
|
||||
// to disable the inline CSS provided by Hugo.
|
||||
DisableInlineCSS bool
|
||||
}
|
||||
|
||||
// DecodeConfig creates a services Config from a given Hugo configuration.
|
||||
func DecodeConfig(cfg config.Provider) (c Config, err error) {
|
||||
m := cfg.GetStringMap(servicesConfigKey)
|
||||
|
||||
err = mapstructure.WeakDecode(m, &c)
|
||||
|
||||
// Keep backwards compability.
|
||||
// Keep backwards compatibility.
|
||||
if c.GoogleAnalytics.ID == "" {
|
||||
// Try the global config
|
||||
c.GoogleAnalytics.ID = cfg.GetString(googleAnalyticsKey)
|
||||
|
||||
@@ -35,6 +35,8 @@ shortname = "DS"
|
||||
id = "ga_id"
|
||||
[services.instagram]
|
||||
disableInlineCSS = true
|
||||
[services.twitter]
|
||||
disableInlineCSS = true
|
||||
`
|
||||
cfg, err := config.FromConfigString(tomlConfig, "toml")
|
||||
assert.NoError(err)
|
||||
|
||||
+236
-70
@@ -16,70 +16,80 @@ package create
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/spf13/afero"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
)
|
||||
|
||||
// NewContent creates a new content file in the content directory based upon the
|
||||
// given kind, which is used to lookup an archetype.
|
||||
func NewContent(
|
||||
ps *helpers.PathSpec,
|
||||
siteFactory func(filename string, siteUsed bool) (*hugolib.Site, error), kind, targetPath string) error {
|
||||
sites *hugolib.HugoSites, kind, targetPath string) error {
|
||||
targetPath = filepath.Clean(targetPath)
|
||||
ext := helpers.Ext(targetPath)
|
||||
ps := sites.PathSpec
|
||||
archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs
|
||||
sourceFs := ps.Fs.Source
|
||||
|
||||
jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext)
|
||||
|
||||
archetypeFilename := findArchetype(ps, kind, ext)
|
||||
archetypeFilename, isDir := findArchetype(ps, kind, ext)
|
||||
contentPath, s := resolveContentPath(sites, sourceFs, targetPath)
|
||||
|
||||
if isDir {
|
||||
|
||||
langFs := hugofs.NewLanguageFs(s.Language.Lang, sites.LanguageSet(), archetypeFs)
|
||||
|
||||
cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cm.siteUsed {
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
name := filepath.Base(targetPath)
|
||||
return newContentFromDir(archetypeFilename, sites, archetypeFs, sourceFs, cm, name, contentPath)
|
||||
}
|
||||
|
||||
// Building the sites can be expensive, so only do it if really needed.
|
||||
siteUsed := false
|
||||
|
||||
if archetypeFilename != "" {
|
||||
f, err := ps.Fs.Source.Open(archetypeFilename)
|
||||
|
||||
var err error
|
||||
siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
if helpers.ReaderContains(f, []byte(".Site")) {
|
||||
siteUsed = true
|
||||
if siteUsed {
|
||||
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s, err := siteFactory(targetPath, siteUsed)
|
||||
content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var content []byte
|
||||
|
||||
content, err = executeArcheTypeAsTemplate(s, kind, targetPath, archetypeFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The site may have multiple content dirs, and we currently do not know which contentDir the
|
||||
// user wants to create this content in. We should improve on this, but we start by testing if the
|
||||
// provided path points to an existing dir. If so, use it as is.
|
||||
var contentPath string
|
||||
var exists bool
|
||||
targetDir := filepath.Dir(targetPath)
|
||||
|
||||
if targetDir != "" && targetDir != "." {
|
||||
exists, _ = helpers.Exists(targetDir, ps.Fs.Source)
|
||||
}
|
||||
|
||||
if exists {
|
||||
contentPath = targetPath
|
||||
} else {
|
||||
contentPath = s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath))
|
||||
}
|
||||
|
||||
if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,44 +111,200 @@ func NewContent(
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindArchetype takes a given kind/archetype of content and returns an output
|
||||
// path for that archetype. If no archetype is found, an empty string is
|
||||
// returned.
|
||||
func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string) {
|
||||
search := []string{ps.AbsPathify(ps.Cfg.GetString("archetypeDir"))}
|
||||
|
||||
if ps.Cfg.GetString("theme") != "" {
|
||||
themeDir := filepath.Join(ps.AbsPathify(ps.Cfg.GetString("themesDir")+"/"+ps.Cfg.GetString("theme")), "/archetypes/")
|
||||
if _, err := ps.Fs.Source.Stat(themeDir); os.IsNotExist(err) {
|
||||
jww.ERROR.Printf("Unable to find archetypes directory for theme %q at %q", ps.Cfg.GetString("theme"), themeDir)
|
||||
} else {
|
||||
search = append(search, themeDir)
|
||||
func targetSite(sites *hugolib.HugoSites, fi *hugofs.LanguageFileInfo) *hugolib.Site {
|
||||
for _, s := range sites.Sites {
|
||||
if fi.Lang() == s.Language.Lang {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
for _, x := range search {
|
||||
// If the new content isn't in a subdirectory, kind == "".
|
||||
// Therefore it should be excluded otherwise `is a directory`
|
||||
// error will occur. github.com/gohugoio/hugo/issues/411
|
||||
var pathsToCheck = []string{"default"}
|
||||
|
||||
if ext != "" {
|
||||
if kind != "" {
|
||||
pathsToCheck = append([]string{kind + ext, "default" + ext}, pathsToCheck...)
|
||||
} else {
|
||||
pathsToCheck = append([]string{"default" + ext}, pathsToCheck...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range pathsToCheck {
|
||||
curpath := filepath.Join(x, p)
|
||||
jww.DEBUG.Println("checking", curpath, "for archetypes")
|
||||
if exists, _ := helpers.Exists(curpath, ps.Fs.Source); exists {
|
||||
jww.INFO.Println("curpath: " + curpath)
|
||||
return curpath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return sites.Sites[0]
|
||||
}
|
||||
|
||||
func newContentFromDir(
|
||||
archetypeDir string,
|
||||
sites *hugolib.HugoSites,
|
||||
sourceFs, targetFs afero.Fs,
|
||||
cm archetypeMap, name, targetPath string) error {
|
||||
|
||||
for _, f := range cm.otherFiles {
|
||||
filename := f.Filename()
|
||||
// Just copy the file to destination.
|
||||
in, err := sourceFs.Open(filename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open non-content file")
|
||||
}
|
||||
|
||||
targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
|
||||
|
||||
targetDir := filepath.Dir(targetFilename)
|
||||
if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "failed to create target directory for %s:", targetDir)
|
||||
}
|
||||
|
||||
out, err := targetFs.Create(targetFilename)
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in.Close()
|
||||
out.Close()
|
||||
}
|
||||
|
||||
for _, f := range cm.contentFiles {
|
||||
filename := f.Filename()
|
||||
s := targetSite(sites, f)
|
||||
targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir))
|
||||
|
||||
content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to execute archetype template")
|
||||
}
|
||||
|
||||
if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil {
|
||||
return errors.Wrap(err, "failed to save results")
|
||||
}
|
||||
}
|
||||
|
||||
jww.FEEDBACK.Println(targetPath, "created")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type archetypeMap struct {
|
||||
// These needs to be parsed and executed as Go templates.
|
||||
contentFiles []*hugofs.LanguageFileInfo
|
||||
// These are just copied to destination.
|
||||
otherFiles []*hugofs.LanguageFileInfo
|
||||
// If the templates needs a fully built site. This can potentially be
|
||||
// expensive, so only do when needed.
|
||||
siteUsed bool
|
||||
}
|
||||
|
||||
func mapArcheTypeDir(
|
||||
ps *helpers.PathSpec,
|
||||
fs afero.Fs,
|
||||
archetypeDir string) (archetypeMap, error) {
|
||||
|
||||
var m archetypeMap
|
||||
|
||||
walkFn := func(filename string, fi os.FileInfo, err error) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fil := fi.(*hugofs.LanguageFileInfo)
|
||||
|
||||
if hugolib.IsContentFile(filename) {
|
||||
m.contentFiles = append(m.contentFiles, fil)
|
||||
if !m.siteUsed {
|
||||
m.siteUsed, err = usesSiteVar(fs, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
m.otherFiles = append(m.otherFiles, fil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := helpers.SymbolicWalk(fs, archetypeDir, walkFn); err != nil {
|
||||
return m, errors.Wrapf(err, "failed to walk archetype dir %q", archetypeDir)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func usesSiteVar(fs afero.Fs, filename string) (bool, error) {
|
||||
f, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to open archetype file")
|
||||
}
|
||||
defer f.Close()
|
||||
return helpers.ReaderContains(f, []byte(".Site")), nil
|
||||
}
|
||||
|
||||
// Resolve the target content path.
|
||||
func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) {
|
||||
targetDir := filepath.Dir(targetPath)
|
||||
first := sites.Sites[0]
|
||||
|
||||
var (
|
||||
s *hugolib.Site
|
||||
siteContentDir string
|
||||
)
|
||||
|
||||
// Try the filename: my-post.en.md
|
||||
for _, ss := range sites.Sites {
|
||||
if strings.Contains(targetPath, "."+ss.Language.Lang+".") {
|
||||
s = ss
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, ss := range sites.Sites {
|
||||
contentDir := ss.PathSpec.ContentDir
|
||||
if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) {
|
||||
contentDir += helpers.FilePathSeparator
|
||||
}
|
||||
if strings.HasPrefix(targetPath, contentDir) {
|
||||
siteContentDir = ss.PathSpec.ContentDir
|
||||
if s == nil {
|
||||
s = ss
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
s = first
|
||||
}
|
||||
|
||||
if targetDir != "" && targetDir != "." {
|
||||
exists, _ := helpers.Exists(targetDir, fs)
|
||||
|
||||
if exists {
|
||||
return targetPath, s
|
||||
}
|
||||
}
|
||||
|
||||
if siteContentDir != "" {
|
||||
pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir))
|
||||
return s.PathSpec.AbsPathify(pp), s
|
||||
|
||||
} else {
|
||||
return s.PathSpec.AbsPathify(filepath.Join(first.PathSpec.ContentDir, targetPath)), s
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// FindArchetype takes a given kind/archetype of content and returns the path
|
||||
// to the archetype in the archetype filesystem, blank if none found.
|
||||
func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) {
|
||||
fs := ps.BaseFs.Archetypes.Fs
|
||||
|
||||
var pathsToCheck []string
|
||||
|
||||
if kind != "" {
|
||||
pathsToCheck = append(pathsToCheck, kind+ext)
|
||||
}
|
||||
pathsToCheck = append(pathsToCheck, "default"+ext, "default")
|
||||
|
||||
for _, p := range pathsToCheck {
|
||||
fi, err := fs.Stat(p)
|
||||
if err == nil {
|
||||
return p, fi.IsDir()
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
|
||||
@@ -80,7 +82,7 @@ var (
|
||||
"%}x}", "%}}")
|
||||
)
|
||||
|
||||
func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFilename string) ([]byte, error) {
|
||||
func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archetypeFilename string) ([]byte, error) {
|
||||
|
||||
var (
|
||||
archetypeContent []byte
|
||||
@@ -88,19 +90,16 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFile
|
||||
err error
|
||||
)
|
||||
|
||||
ps, err := helpers.NewPathSpec(s.Deps.Fs, s.Deps.Cfg)
|
||||
sp := source.NewSourceSpec(ps, ps.Fs.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := sp.NewFileInfo("", targetPath, false, nil)
|
||||
f := s.SourceSpec.NewFileInfo("", targetPath, false, nil)
|
||||
|
||||
name := f.TranslationBaseName()
|
||||
if name == "" {
|
||||
name = f.TranslationBaseName()
|
||||
|
||||
if name == "index" || name == "_index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
if name == "index" || name == "_index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
}
|
||||
}
|
||||
|
||||
data := ArchetypeFileData{
|
||||
@@ -115,9 +114,9 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFile
|
||||
// TODO(bep) archetype revive the issue about wrong tpl funcs arg order
|
||||
archetypeTemplate = []byte(ArchetypeTemplateTemplate)
|
||||
} else {
|
||||
archetypeTemplate, err = afero.ReadFile(s.Fs.Source, archetypeFilename)
|
||||
archetypeTemplate, err = afero.ReadFile(s.BaseFs.Archetypes.Fs, archetypeFilename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read archetype file %q: %s", archetypeFilename, err)
|
||||
return nil, fmt.Errorf("failed to read archetype file %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -130,14 +129,14 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFile
|
||||
templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler)
|
||||
templateName := "_text/" + helpers.Filename(archetypeFilename)
|
||||
if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse archetype file %q: %s", archetypeFilename, err)
|
||||
return nil, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename)
|
||||
}
|
||||
|
||||
templ := templateHandler.Lookup(templateName)
|
||||
templ, _ := templateHandler.Lookup(templateName)
|
||||
|
||||
var buff bytes.Buffer
|
||||
if err := templ.Execute(&buff, data); err != nil {
|
||||
return nil, fmt.Errorf("Failed to process archetype file %q: %s", archetypeFilename, err)
|
||||
return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename)
|
||||
}
|
||||
|
||||
archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String()))
|
||||
|
||||
+103
-29
@@ -35,8 +35,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNewContent(t *testing.T) {
|
||||
v := viper.New()
|
||||
initViper(v)
|
||||
assert := require.New(t)
|
||||
|
||||
cases := []struct {
|
||||
kind string
|
||||
@@ -49,6 +48,14 @@ func TestNewContent(t *testing.T) {
|
||||
{"stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file
|
||||
{"", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype
|
||||
{"product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter
|
||||
{"lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}},
|
||||
{"lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}},
|
||||
{"lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}},
|
||||
{"lang", "post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}},
|
||||
{"shortcodes", "shortcodes/go.md", []string{
|
||||
`title = "GO"`,
|
||||
"{{< myshortcode >}}",
|
||||
@@ -56,23 +63,20 @@ func TestNewContent(t *testing.T) {
|
||||
"{{</* comment */>}}\n{{%/* comment */%}}"}}, // shortcodes
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
cfg, fs := newTestCfg()
|
||||
ps, err := helpers.NewPathSpec(fs, cfg)
|
||||
require.NoError(t, err)
|
||||
for i, c := range cases {
|
||||
cfg, fs := newTestCfg(assert)
|
||||
assert.NoError(initFs(fs))
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, initFs(fs))
|
||||
assert.NoError(err)
|
||||
|
||||
siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) {
|
||||
return h.Sites[0], nil
|
||||
assert.NoError(create.NewContent(h, c.kind, c.path))
|
||||
|
||||
fname := filepath.FromSlash(c.path)
|
||||
if !strings.HasPrefix(fname, "content") {
|
||||
fname = filepath.Join("content", fname)
|
||||
}
|
||||
|
||||
require.NoError(t, create.NewContent(ps, siteFactory, c.kind, c.path))
|
||||
|
||||
fname := filepath.Join("content", filepath.FromSlash(c.path))
|
||||
content := readFileFromFs(t, fs.Source, fname)
|
||||
for i, v := range c.expected {
|
||||
for _, v := range c.expected {
|
||||
found := strings.Contains(content, v)
|
||||
if !found {
|
||||
t.Fatalf("[%d] %q missing from output:\n%q", i, v, content)
|
||||
@@ -81,14 +85,54 @@ func TestNewContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func initViper(v *viper.Viper) {
|
||||
v.Set("metaDataFormat", "toml")
|
||||
v.Set("archetypeDir", "archetypes")
|
||||
v.Set("contentDir", "content")
|
||||
v.Set("themesDir", "themes")
|
||||
v.Set("layoutDir", "layouts")
|
||||
v.Set("i18nDir", "i18n")
|
||||
v.Set("theme", "sample")
|
||||
func TestNewContentFromDir(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
cfg, fs := newTestCfg(assert)
|
||||
assert.NoError(initFs(fs))
|
||||
|
||||
archetypeDir := filepath.Join("archetypes", "my-bundle")
|
||||
assert.NoError(fs.Source.Mkdir(archetypeDir, 0755))
|
||||
|
||||
archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle")
|
||||
assert.NoError(fs.Source.Mkdir(archetypeThemeDir, 0755))
|
||||
|
||||
contentFile := `
|
||||
File: %s
|
||||
Site Lang: {{ .Site.Language.Lang }}
|
||||
Name: {{ replace .Name "-" " " | title }}
|
||||
i18n: {{ T "hugo" }}
|
||||
`
|
||||
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755))
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0755))
|
||||
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0755))
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755))
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0755))
|
||||
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755))
|
||||
assert.NoError(afero.WriteFile(fs.Source, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755))
|
||||
|
||||
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
|
||||
assert.NoError(err)
|
||||
assert.Equal(2, len(h.Sites))
|
||||
|
||||
assert.NoError(create.NewContent(h, "my-bundle", "post/my-post"))
|
||||
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`)
|
||||
|
||||
// Content files should get the correct site context.
|
||||
// TODO(bep) archetype check i18n
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Post`, `i18n: Hugo Rocks!`)
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Site Lang: nn`, `Name: My Post`, `i18n: Hugo Rokkar!`)
|
||||
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: My Post`)
|
||||
|
||||
assert.NoError(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"))
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
|
||||
assertContains(assert, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
|
||||
|
||||
}
|
||||
|
||||
func initFs(fs *hugofs.Fs) error {
|
||||
@@ -131,6 +175,10 @@ title = "{{ .BaseFileName | upper }}"
|
||||
path: filepath.Join("archetypes", "emptydate.md"),
|
||||
content: "+++\ndate =\"\"\ntitle = \"Empty Date Arch title\"\ntest = \"test1\"\n+++\n",
|
||||
},
|
||||
{
|
||||
path: filepath.Join("archetypes", "lang.md"),
|
||||
content: `Site Lang: {{ .Site.Language.Lang }}|Name: {{ replace .Name "-" " " | title }}|i18n: {{ T "hugo" }}`,
|
||||
},
|
||||
// #3623x
|
||||
{
|
||||
path: filepath.Join("archetypes", "shortcodes.md"),
|
||||
@@ -165,6 +213,12 @@ Some text.
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertContains(assert *require.Assertions, v interface{}, matches ...string) {
|
||||
for _, m := range matches {
|
||||
assert.Contains(v, m)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(bep) extract common testing package with this and some others
|
||||
func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
|
||||
filename = filepath.FromSlash(filename)
|
||||
@@ -184,15 +238,35 @@ func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func newTestCfg() (*viper.Viper, *hugofs.Fs) {
|
||||
func newTestCfg(assert *require.Assertions) (*viper.Viper, *hugofs.Fs) {
|
||||
|
||||
v := viper.New()
|
||||
fs := hugofs.NewMem(v)
|
||||
cfg := `
|
||||
|
||||
v.SetFs(fs.Source)
|
||||
theme = "mytheme"
|
||||
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
languageName = "English"
|
||||
[languages.nn]
|
||||
weight = 2
|
||||
languageName = "Nynorsk"
|
||||
contentDir = "content_nn"
|
||||
|
||||
initViper(v)
|
||||
`
|
||||
|
||||
return v, fs
|
||||
mm := afero.NewMemMapFs()
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join("i18n", "en.toml"), []byte(`[hugo]
|
||||
other = "Hugo Rocks!"`), 0755))
|
||||
assert.NoError(afero.WriteFile(mm, filepath.Join("i18n", "nn.toml"), []byte(`[hugo]
|
||||
other = "Hugo Rokkar!"`), 0755))
|
||||
|
||||
assert.NoError(afero.WriteFile(mm, "config.toml", []byte(cfg), 0755))
|
||||
|
||||
v, _, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
|
||||
assert.NoError(err)
|
||||
|
||||
return v, hugofs.NewFrom(mm, v)
|
||||
|
||||
}
|
||||
|
||||
Vendored
+139
-14
@@ -1,16 +1,22 @@
|
||||
package deps
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/metrics"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/resource"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
@@ -20,8 +26,9 @@ import (
|
||||
// There will be normally only one instance of deps in play
|
||||
// at a given time, i.e. one per Site built.
|
||||
type Deps struct {
|
||||
|
||||
// The logger to use.
|
||||
Log *jww.Notepad `json:"-"`
|
||||
Log *loggers.Logger `json:"-"`
|
||||
|
||||
// Used to log errors that may repeat itself many times.
|
||||
DistinctErrorLog *helpers.DistinctLogger
|
||||
@@ -29,6 +36,9 @@ type Deps struct {
|
||||
// The templates to use. This will usually implement the full tpl.TemplateHandler.
|
||||
Tmpl tpl.TemplateFinder `json:"-"`
|
||||
|
||||
// We use this to parse and execute ad-hoc text templates.
|
||||
TextTmpl tpl.TemplateParseFinder `json:"-"`
|
||||
|
||||
// The file systems to use.
|
||||
Fs *hugofs.Fs `json:"-"`
|
||||
|
||||
@@ -41,13 +51,23 @@ type Deps struct {
|
||||
// The SourceSpec to use
|
||||
SourceSpec *source.SourceSpec `json:"-"`
|
||||
|
||||
// The Resource Spec to use
|
||||
ResourceSpec *resource.Spec
|
||||
|
||||
// The configuration to use
|
||||
Cfg config.Provider `json:"-"`
|
||||
|
||||
// The file cache to use.
|
||||
FileCaches filecache.Caches
|
||||
|
||||
// The translation func to use
|
||||
Translate func(translationID string, args ...interface{}) string `json:"-"`
|
||||
|
||||
Language *helpers.Language
|
||||
// The language in use. TODO(bep) consolidate with site
|
||||
Language *langs.Language
|
||||
|
||||
// The site building.
|
||||
Site hugo.Site
|
||||
|
||||
// All the output formats available for the current site.
|
||||
OutputFormatsConfig output.Formats
|
||||
@@ -61,6 +81,63 @@ type Deps struct {
|
||||
|
||||
// Timeout is configurable in site config.
|
||||
Timeout time.Duration
|
||||
|
||||
// BuildStartListeners will be notified before a build starts.
|
||||
BuildStartListeners *Listeners
|
||||
|
||||
*globalErrHandler
|
||||
}
|
||||
|
||||
type globalErrHandler struct {
|
||||
// Channel for some "hard to get to" build errors
|
||||
buildErrors chan error
|
||||
}
|
||||
|
||||
// SendErr sends the error on a channel to be handled later.
|
||||
// This can be used in situations where returning and aborting the current
|
||||
// operation isn't practical.
|
||||
func (e *globalErrHandler) SendError(err error) {
|
||||
if e.buildErrors != nil {
|
||||
select {
|
||||
case e.buildErrors <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
jww.ERROR.Println(err)
|
||||
}
|
||||
|
||||
func (e *globalErrHandler) StartErrorCollector() chan error {
|
||||
e.buildErrors = make(chan error, 10)
|
||||
return e.buildErrors
|
||||
}
|
||||
|
||||
// Listeners represents an event listener.
|
||||
type Listeners struct {
|
||||
sync.Mutex
|
||||
|
||||
// A list of funcs to be notified about an event.
|
||||
listeners []func()
|
||||
}
|
||||
|
||||
// Add adds a function to a Listeners instance.
|
||||
func (b *Listeners) Add(f func()) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
b.listeners = append(b.listeners, f)
|
||||
}
|
||||
|
||||
// Notify executes all listener functions.
|
||||
func (b *Listeners) Notify() {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
for _, notify := range b.listeners {
|
||||
notify()
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceProvider is used to create and refresh, and clone resources needed.
|
||||
@@ -85,10 +162,6 @@ func (d *Deps) LoadResources() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if th, ok := d.Tmpl.(tpl.TemplateHandler); ok {
|
||||
th.PrintErrors()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -114,7 +187,7 @@ func New(cfg DepsCfg) (*Deps, error) {
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
|
||||
logger = loggers.NewErrorLogger()
|
||||
}
|
||||
|
||||
if fs == nil {
|
||||
@@ -122,12 +195,30 @@ func New(cfg DepsCfg) (*Deps, error) {
|
||||
fs = hugofs.NewDefault(cfg.Language)
|
||||
}
|
||||
|
||||
if cfg.MediaTypes == nil {
|
||||
cfg.MediaTypes = media.DefaultTypes
|
||||
}
|
||||
|
||||
if cfg.OutputFormats == nil {
|
||||
cfg.OutputFormats = output.DefaultFormats
|
||||
}
|
||||
|
||||
ps, err := helpers.NewPathSpec(fs, cfg.Language)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileCaches, err := filecache.NewCaches(ps)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to create file caches from configuration")
|
||||
}
|
||||
|
||||
resourceSpec, err := resource.NewSpec(ps, fileCaches, logger, cfg.OutputFormats, cfg.MediaTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentSpec, err := helpers.NewContentSpec(cfg.Language)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,9 +243,14 @@ func New(cfg DepsCfg) (*Deps, error) {
|
||||
PathSpec: ps,
|
||||
ContentSpec: contentSpec,
|
||||
SourceSpec: sp,
|
||||
ResourceSpec: resourceSpec,
|
||||
Cfg: cfg.Language,
|
||||
Language: cfg.Language,
|
||||
Site: cfg.Site,
|
||||
FileCaches: fileCaches,
|
||||
BuildStartListeners: &Listeners{},
|
||||
Timeout: time.Duration(timeoutms) * time.Millisecond,
|
||||
globalErrHandler: &globalErrHandler{},
|
||||
}
|
||||
|
||||
if cfg.Cfg.GetBool("templateMetrics") {
|
||||
@@ -166,10 +262,11 @@ func New(cfg DepsCfg) (*Deps, error) {
|
||||
|
||||
// ForLanguage creates a copy of the Deps with the language dependent
|
||||
// parts switched out.
|
||||
func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) {
|
||||
func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, error) {
|
||||
l := cfg.Language
|
||||
var err error
|
||||
|
||||
d.PathSpec, err = helpers.NewPathSpec(d.Fs, l)
|
||||
d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, l, d.BaseFs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -179,9 +276,26 @@ func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.Site = cfg.Site
|
||||
|
||||
// The resource cache is global so reuse.
|
||||
// TODO(bep) clean up these inits.
|
||||
resourceCache := d.ResourceSpec.ResourceCache
|
||||
d.ResourceSpec, err = resource.NewSpec(d.PathSpec, d.ResourceSpec.FileCaches, d.Log, cfg.OutputFormats, cfg.MediaTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.ResourceSpec.ResourceCache = resourceCache
|
||||
|
||||
d.Cfg = l
|
||||
d.Language = l
|
||||
|
||||
if onCreated != nil {
|
||||
if err = onCreated(&d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := d.translationProvider.Clone(&d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -190,6 +304,8 @@ func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.BuildStartListeners = &Listeners{}
|
||||
|
||||
return &d, nil
|
||||
|
||||
}
|
||||
@@ -200,17 +316,26 @@ func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) {
|
||||
type DepsCfg struct {
|
||||
|
||||
// The Logger to use.
|
||||
Logger *jww.Notepad
|
||||
Logger *loggers.Logger
|
||||
|
||||
// The file systems to use
|
||||
Fs *hugofs.Fs
|
||||
|
||||
// The language to use.
|
||||
Language *helpers.Language
|
||||
Language *langs.Language
|
||||
|
||||
// The Site in use
|
||||
Site hugo.Site
|
||||
|
||||
// The configuration to use.
|
||||
Cfg config.Provider
|
||||
|
||||
// The media types configured.
|
||||
MediaTypes media.Types
|
||||
|
||||
// The output formats configured.
|
||||
OutputFormats output.Formats
|
||||
|
||||
// Template handling.
|
||||
TemplateProvider ResourceProvider
|
||||
WithTemplate func(templ tpl.TemplateHandler) error
|
||||
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 120
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 30
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- Keep
|
||||
- Security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: Stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
|
||||
|
||||
If you still think this is important, please tell us why.
|
||||
|
||||
This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Hugo Docs
|
||||
|
||||
Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in GoLang.
|
||||
Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in Go.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
Add some **general info** about {{ replace .Name "-" " " | title }} here.
|
||||
|
||||
The site is built by:
|
||||
|
||||
* [Person 1](https://example.com)
|
||||
* [Person 1](https://example.com)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,37 @@
|
||||
---
|
||||
|
||||
title: {{ replace .Name "-" " " | title }}
|
||||
date: {{ now.Format "2006-01-02" }}
|
||||
|
||||
description: "A short description of this page."
|
||||
|
||||
# The URL to the site on the internet.
|
||||
siteURL: https://gohugo.io/
|
||||
|
||||
# Link to the site's Hugo source code if public and you can/want to share.
|
||||
# Remove or leave blank if not needed/wanted.
|
||||
siteSource: https://github.com/gohugoio/hugoDocs
|
||||
|
||||
# Add credit to the article author. Leave blank or remove if not needed/wanted.
|
||||
byline: "[bep](https://github.com/bep), Hugo Lead"
|
||||
|
||||
---
|
||||
|
||||
To complete this showcase:
|
||||
|
||||
1. Write the story about your site in this file.
|
||||
2. Add a summary to the `bio.md` file in this folder.
|
||||
3. Replace the `featured-template.png` with a screenshot of your site. You can rename it, but it must contain the word `featured`.
|
||||
4. Create a new pull request in https://github.com/gohugoio/hugoDocs/pulls
|
||||
|
||||
The content of this bundle explained:
|
||||
|
||||
index.md
|
||||
: The main content file. Fill in required front matter metadata and write your story. I does not have to be a novel. It can even be self-promotional, but it should include Hugo in some form.
|
||||
|
||||
bio.md
|
||||
: A short summary of the website. Site credits (who built it) fits nicely here.
|
||||
|
||||
featured.png
|
||||
: A reasonably sized screenshot of your website. It can be named anything, but the name must start with "featured". The sample image is `1500x750` (2:1 aspect ratio).
|
||||
|
||||
+7
-4
@@ -34,7 +34,6 @@ section = [ "HTML", "RSS"]
|
||||
|
||||
[mediaTypes]
|
||||
[mediaTypes."text/netlify"]
|
||||
suffix = ""
|
||||
delimiter = ""
|
||||
|
||||
[outputFormats]
|
||||
@@ -70,7 +69,7 @@ twitter = "GoHugoIO"
|
||||
[params]
|
||||
description = "The world’s fastest framework for building websites"
|
||||
## Used for views in rendered HTML (i.e., rather than using the .Hugo variable)
|
||||
release = "0.41"
|
||||
release = "0.53"
|
||||
## Setting this to true will add a "noindex" to *EVERY* page on the site
|
||||
removefromexternalsearch = false
|
||||
## Gh repo for site footer (include trailing slash)
|
||||
@@ -108,7 +107,7 @@ twitter = "GoHugoIO"
|
||||
# Note that you can also set this per image processing.
|
||||
resampleFilter = "CatmullRom"
|
||||
|
||||
# Defatult JPEG quality setting. Default is 75.
|
||||
# Default JPEG quality setting. Default is 75.
|
||||
quality = 75
|
||||
|
||||
anchor = "smart"
|
||||
@@ -167,7 +166,11 @@ anchor = "smart"
|
||||
weight = 35
|
||||
identifier = "variables"
|
||||
url = "/variables/"
|
||||
|
||||
[[menu.docs]]
|
||||
name = "Hugo Pipes"
|
||||
weight = 36
|
||||
identifier = "pipes"
|
||||
url = "/hugo-pipes/"
|
||||
[[menu.docs]]
|
||||
name = "CLI"
|
||||
weight = 40
|
||||
|
||||
@@ -12,7 +12,6 @@ menu:
|
||||
weight: 20
|
||||
sections_weight: 20
|
||||
draft: false
|
||||
aliases: [/about/features]
|
||||
toc: true
|
||||
---
|
||||
|
||||
@@ -53,12 +52,13 @@ toc: true
|
||||
* Integrated [Google Analytics][] support
|
||||
* Automatic [RSS][] creation
|
||||
* Support for [Go][], [Amber], and [Ace][] HTML templates
|
||||
* [Syntax highlighting][] powered by [Pygments][]
|
||||
* [Syntax highlighting][] powered by [Chroma][] (partly compatible with Pygments)
|
||||
|
||||
|
||||
[Ace]: /templates/alternatives/
|
||||
[aliases]: /content-management/urls/#aliases
|
||||
[Amber]: https://github.com/eknkc/amber
|
||||
[Chroma]: https://github.com/alecthomas/chroma
|
||||
[content summaries]: /content-management/summaries/
|
||||
[content types]: /content-management/types/
|
||||
[Disqus]: https://disqus.com/
|
||||
@@ -77,7 +77,6 @@ toc: true
|
||||
[Permalink]: /content-management/urls/#permalinks
|
||||
[Powerful theming]: /themes/
|
||||
[Pretty URLs]: /content-management/urls/
|
||||
[Pygments]: http://pygments.org/
|
||||
[RSS]: /templates/rss/
|
||||
[Shortcodes]: /content-management/shortcodes/
|
||||
[sort content]: /templates/
|
||||
|
||||
@@ -23,11 +23,12 @@ toc: true
|
||||
|
||||
**Hugo is a static site generator. By using Hugo you are already standing on very solid ground. Static HTML files on disk are much easier to reason about compared to server and database driven web sites.**
|
||||
|
||||
But even static websites can integrate with external services, so from version `0.41`, Hugo provides a `Privacy Config` that covers the relevant built-in templates.
|
||||
But even static websites can integrate with external services, so from version `0.41`, Hugo provides a **Privacy Config** that covers the relevant built-in templates.
|
||||
|
||||
Note that:
|
||||
|
||||
* These settings have their defaults setting set to _off_, i.e. how it worked before Hugo `0.41`. You must do your own evaluation of your site and apply the appropriate settings.
|
||||
* These settings work with the [internal templates](/templates/internal/). Some theme may contain custom templates for embedding services like Google Analytics. In that case these options have no effect.
|
||||
* We will continue this work and improve this further in future Hugo versions.
|
||||
|
||||
## All Privacy Settings
|
||||
@@ -46,13 +47,13 @@ useSessionStorage = false
|
||||
[privacy.instagram]
|
||||
disable = false
|
||||
simple = false
|
||||
[privacy.speakerDeck]
|
||||
disable = false
|
||||
[privacy.twitter]
|
||||
disable = false
|
||||
enableDNT = false
|
||||
simple = false
|
||||
[privacy.vimeo]
|
||||
disable = false
|
||||
simple = false
|
||||
[privacy.youtube]
|
||||
disable = false
|
||||
privacyEnhanced = false
|
||||
@@ -71,8 +72,6 @@ disable = true
|
||||
disable = true
|
||||
[privacy.instagram]
|
||||
disable = true
|
||||
[privacy.speakerDeck]
|
||||
disable = true
|
||||
[privacy.twitter]
|
||||
disable = true
|
||||
[privacy.vimeo]
|
||||
@@ -112,8 +111,25 @@ disableInlineCSS = true
|
||||
enableDNT
|
||||
: Enabling this for the twitter/tweet shortcode, the tweet and its embedded page on your site are not used for purposes that include personalized suggestions and personalized ads.
|
||||
|
||||
simple
|
||||
: If simple mode is enabled, a static and no-JS version of a tweet will be built.
|
||||
|
||||
|
||||
**Note:** If you use the _simple mode_ for Twitter, you may want to disable the inlines styles provided by Hugo:
|
||||
|
||||
{{< code-toggle file="config">}}
|
||||
[services]
|
||||
[services.twitter]
|
||||
disableInlineCSS = true
|
||||
{{< /code-toggle >}}
|
||||
|
||||
### YouTube
|
||||
|
||||
privacyEnhanced
|
||||
: When you turn on privacy-enhanced mode, YouTube won’t store information about visitors on your website unless the user plays the embedded video.
|
||||
|
||||
### Vimeo
|
||||
|
||||
simple
|
||||
: If simple mode is enabled, the video thumbnail is fetched from Vimeo's servers and it is overlayed with a play button. If the user clicks to play the video, it will open in a new tab directly on Vimeo's website.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ This documentation belongs in other places in this documentation site, but is pu
|
||||
|
||||
Also see this demo project from [bep](https://github.com/bep/), the clever Norwegian behind these new features:
|
||||
|
||||
* http://hugotest.bep.is/
|
||||
* https://temp.bep.is/hugotest/
|
||||
* https://github.com/bep/hugotest (source)
|
||||
|
||||
## Page Resources
|
||||
@@ -199,7 +199,7 @@ You can configure an `imaging` section in `config.toml` with default image proce
|
||||
# See https://github.com/disintegration/imaging
|
||||
resampleFilter = "box"
|
||||
|
||||
# Defatult JPEG quality setting. Default is 75.
|
||||
# Default JPEG quality setting. Default is 75.
|
||||
quality = 68
|
||||
```
|
||||
|
||||
|
||||
@@ -43,27 +43,23 @@ Hugo is for people building a blog, a company site, a portfolio site, documentat
|
||||
|
||||
[@spf13]: https://twitter.com/@spf13
|
||||
[Aerobatic]: https://www.aerobatic.com/
|
||||
[Amazon S3]: http://aws.amazon.com/s3/
|
||||
[Azure]: https://blogs.msdn.microsoft.com/acoat/2016/01/28/publish-a-static-web-site-using-azure-web-apps/
|
||||
[CloudFront]: http://aws.amazon.com/cloudfront/ "Amazon CloudFront"
|
||||
[contributing to it]: https://github.com/gohugoio/hugo
|
||||
[DreamHost]: http://www.dreamhost.com/
|
||||
[Amazon S3]: https://aws.amazon.com/s3/
|
||||
[Azure]: https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website
|
||||
[CloudFront]: https://aws.amazon.com/cloudfront/ "Amazon CloudFront"
|
||||
[DreamHost]: https://www.dreamhost.com/
|
||||
[Firebase]: https://firebase.google.com/docs/hosting/ "Firebase static hosting"
|
||||
[GitHub Pages]: https://pages.github.com/
|
||||
[GitLab Pages]: https://about.gitlab.com/features/pages/
|
||||
[Go language]: https://golang.org/
|
||||
[GoDaddy]: https://www.godaddy.com/ "Godaddy.com Hosting"
|
||||
[Google Cloud Storage]: http://cloud.google.com/storage/
|
||||
[Google Cloud Storage]: https://cloud.google.com/storage/
|
||||
[Heroku]: https://www.heroku.com/
|
||||
[Jekyll]: http://jekyllrb.com/
|
||||
[Jekyll]: https://jekyllrb.com/
|
||||
[Middleman]: https://middlemanapp.com/
|
||||
[Middleman]: https://middlemanapp.com/
|
||||
[Nanoc]: http://nanoc.ws/
|
||||
[Nanoc]: https://nanoc.ws/
|
||||
[Netlify]: https://netlify.com
|
||||
[Rackspace]: https://www.rackspace.com/cloud/files
|
||||
[Surge]: https://surge.sh
|
||||
[contributing to it]: https://github.com/gohugoio/hugo
|
||||
[rackspace]: https://www.rackspace.com/cloud/files
|
||||
[static site generator]: /about/benefits/
|
||||
[Rackspace]: https://www.rackspace.com/cloud/files
|
||||
[static site generator]: /about/benefits/
|
||||
[Surge]: https://surge.sh
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user