Compare commits
564 commits
new-plugin
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ef76a42e4a | ||
![]() |
61f054b602 | ||
![]() |
73d7c6b753 | ||
![]() |
f3afb9ec15 | ||
![]() |
a9de710b0f | ||
![]() |
55525f5c15 | ||
![]() |
47e643a9ee | ||
![]() |
58cf3eebcc | ||
![]() |
b70b6d8c11 | ||
![]() |
bcc81664e5 | ||
![]() |
96ae19b146 | ||
![]() |
cc0271b319 | ||
![]() |
7ba1e0d16a | ||
![]() |
e7e68ce602 | ||
![]() |
c5b368aa8a | ||
![]() |
b5d5076aa8 | ||
![]() |
105d3a5df0 | ||
![]() |
891bfd52de | ||
![]() |
878edba3df | ||
![]() |
bea699ebeb | ||
![]() |
b4ed7bdb49 | ||
![]() |
7aa2c1f904 | ||
![]() |
647bb1ddf1 | ||
![]() |
0b9f027a36 | ||
![]() |
c2a107cb3e | ||
![]() |
49288bc135 | ||
![]() |
9a3cd70d4c | ||
![]() |
efc0bfb738 | ||
![]() |
656401df71 | ||
![]() |
d1e30f150d | ||
![]() |
daab8d63fd | ||
![]() |
559171a34f | ||
![]() |
7acd0eb951 | ||
![]() |
4978458966 | ||
![]() |
beb627c482 | ||
![]() |
21411d37d9 | ||
![]() |
48b1122419 | ||
![]() |
4ab837508b | ||
![]() |
5fd748be8b | ||
![]() |
35c09f45bd | ||
![]() |
0de6478caf | ||
![]() |
390af72d44 | ||
![]() |
a91dcc8075 | ||
![]() |
dd53a77595 | ||
![]() |
eacc598930 | ||
![]() |
774a29a372 | ||
![]() |
82702d25d3 | ||
![]() |
8b399b964a | ||
![]() |
b6c35f0331 | ||
![]() |
ea423d72e1 | ||
![]() |
8a3b05d2af | ||
![]() |
38a6c2d475 | ||
![]() |
5f32be57f7 | ||
![]() |
fbf7bb3c5c | ||
![]() |
e7f0d111fc | ||
![]() |
0172253002 | ||
![]() |
0e35b72fc2 | ||
![]() |
0e4b8d81b0 | ||
![]() |
6e0684b128 | ||
![]() |
6fdeb58f63 | ||
![]() |
6902af29ef | ||
![]() |
d7a4433523 | ||
![]() |
d9fa597d79 | ||
![]() |
0e325ee56e | ||
![]() |
919d047349 | ||
![]() |
17e526317c | ||
![]() |
89db40a552 | ||
![]() |
86fefef6fe | ||
![]() |
5304158c0a | ||
![]() |
5dd66dcf62 | ||
![]() |
be933edf28 | ||
![]() |
ca4aae8f66 | ||
![]() |
d56429a7f8 | ||
![]() |
a63c168e86 | ||
![]() |
a0457666a5 | ||
![]() |
cc54e8031e | ||
![]() |
bcc2a5ecec | ||
![]() |
d61a3a2adc | ||
![]() |
3e803e7f03 | ||
![]() |
95e24bbc2d | ||
![]() |
82f7a7f309 | ||
![]() |
1dea803651 | ||
![]() |
992f3aba7b | ||
![]() |
48099d55ce | ||
![]() |
03415834c6 | ||
![]() |
ce1c4f3054 | ||
![]() |
fbb9c034eb | ||
![]() |
1aefd3e520 | ||
![]() |
7b8c5bb2ae | ||
![]() |
a447df807b | ||
![]() |
a6164f0bc0 | ||
![]() |
d0178a7b0b | ||
![]() |
11e234f9e9 | ||
![]() |
806c003f98 | ||
![]() |
12f174533f | ||
![]() |
f4a66dc523 | ||
![]() |
ca65274456 | ||
![]() |
c2cf64a027 | ||
![]() |
f9c62cfc53 | ||
![]() |
d397b32328 | ||
![]() |
7d87d483a0 | ||
![]() |
ee74b1a323 | ||
![]() |
3695fd875b | ||
![]() |
9ff2aa0944 | ||
![]() |
5681f5724c | ||
![]() |
dee99e91b5 | ||
![]() |
9a87ae81f3 | ||
![]() |
67ef0dc71e | ||
![]() |
ec2eb2464c | ||
![]() |
ca4ba22ef9 | ||
![]() |
9cc9eaf241 | ||
![]() |
b90c0cc6fe | ||
![]() |
694c5a9290 | ||
![]() |
7b84f5b670 | ||
![]() |
e60ec1abfb | ||
![]() |
08a66b2f8b | ||
![]() |
2ee2609020 | ||
![]() |
c2ff1badf8 | ||
![]() |
7f69c93b6a | ||
![]() |
2d3d966b94 | ||
![]() |
808bdb50c3 | ||
![]() |
a856f0284d | ||
![]() |
678ea7fe4a | ||
![]() |
9e8bf5e4c9 | ||
![]() |
d654c17b2c | ||
![]() |
08f32f27c8 | ||
![]() |
5404e88c0b | ||
![]() |
a5c6e365d9 | ||
![]() |
fc71373709 | ||
![]() |
9311c2244f | ||
![]() |
444698f987 | ||
![]() |
d1ff684c89 | ||
![]() |
90be8952cd | ||
![]() |
3d2bb56a3f | ||
![]() |
9877aecf5e | ||
![]() |
078e5ff316 | ||
![]() |
843bbb652c | ||
![]() |
1c585326ab | ||
![]() |
9819c279bb | ||
![]() |
414c8dd742 | ||
![]() |
075477b9ce | ||
![]() |
44eb77da2c | ||
![]() |
8b4aac2686 | ||
![]() |
4d24884863 | ||
![]() |
bd17b6c039 | ||
![]() |
96512011a0 | ||
![]() |
fe84645a0f | ||
![]() |
b94267a6f6 | ||
![]() |
0a1be15179 | ||
![]() |
7bde4c1781 | ||
![]() |
b925ab5583 | ||
![]() |
64c917b53f | ||
![]() |
a45254b1fc | ||
![]() |
71b535648f | ||
![]() |
c45d7be575 | ||
![]() |
c625aa1de6 | ||
![]() |
5f3100604c | ||
![]() |
5727578b26 | ||
![]() |
132b235aee | ||
![]() |
100fa30578 | ||
![]() |
4b0819c288 | ||
![]() |
7d26e2bb5a | ||
![]() |
7facd55afb | ||
![]() |
144cb03294 | ||
![]() |
66928295e3 | ||
![]() |
1e1c64b245 | ||
![]() |
d7ffd1b7d7 | ||
![]() |
deb2e556a4 | ||
![]() |
0fdac81d5e | ||
![]() |
1251db17fd | ||
![]() |
86133fef8f | ||
![]() |
e52a9765a5 | ||
![]() |
78ec9c37eb | ||
![]() |
515f93cbd3 | ||
![]() |
d5e5aed0d9 | ||
![]() |
3671178a35 | ||
![]() |
b272c7880b | ||
![]() |
5aa89f4497 | ||
![]() |
fbacd3b81f | ||
![]() |
1d5b9117e2 | ||
![]() |
68ee3c942d | ||
![]() |
9c4dd4162b | ||
![]() |
a8326f8870 | ||
![]() |
b6f1834239 | ||
![]() |
8d44e8ac3a | ||
![]() |
45523dcc35 | ||
![]() |
3490af0287 | ||
![]() |
4fd195db19 | ||
![]() |
1ff10b9b63 | ||
![]() |
8273efe00e | ||
![]() |
558e2d1e0d | ||
![]() |
29e0fe0f58 | ||
![]() |
762ad5e44d | ||
![]() |
a6a5b0b86b | ||
![]() |
913023e377 | ||
![]() |
52f9cfa63c | ||
![]() |
6073122961 | ||
![]() |
8a50b6c4e4 | ||
![]() |
3a37b8339b | ||
![]() |
81fcf3a47a | ||
![]() |
8e464ed762 | ||
![]() |
054621ab13 | ||
![]() |
7cd51a1334 | ||
![]() |
b38b07e83a | ||
![]() |
bcd9051b14 | ||
![]() |
c6cf6ee943 | ||
![]() |
1c587f8616 | ||
![]() |
62cc5e91f4 | ||
![]() |
1b3acd107e | ||
![]() |
1289706b46 | ||
![]() |
f27172a1f7 | ||
![]() |
c7fc38b73c | ||
![]() |
c286fc1e33 | ||
![]() |
a9d73e3b26 | ||
![]() |
47a17508e8 | ||
![]() |
0640ebc5cb | ||
![]() |
8bea5f1927 | ||
![]() |
e44d0bde9e | ||
![]() |
f474db77b2 | ||
![]() |
33e81104b8 | ||
![]() |
3ae3c208f7 | ||
![]() |
81cfcc469e | ||
![]() |
b2082175e3 | ||
![]() |
60302d5f47 | ||
![]() |
bb2b6e49a4 | ||
![]() |
41186fb41c | ||
![]() |
439f3f35f7 | ||
![]() |
46e6915ba2 | ||
![]() |
78e0f9ecea | ||
![]() |
2ed12c5bce | ||
![]() |
2bd647207a | ||
![]() |
7b516ccc9c | ||
![]() |
148c1a0c52 | ||
![]() |
b23a398dd5 | ||
![]() |
b3acf878e1 | ||
![]() |
78f1fc9fa0 | ||
![]() |
702aaf5b4b | ||
![]() |
ef378b9b56 | ||
![]() |
ab34400307 | ||
![]() |
ed886b57e7 | ||
![]() |
f512ab7a48 | ||
![]() |
ab03544600 | ||
![]() |
ae96c069c4 | ||
![]() |
04c1ed9aee | ||
![]() |
ab12f8ed6d | ||
![]() |
c2920901fa | ||
![]() |
99e87c061f | ||
![]() |
c82fe20382 | ||
![]() |
567c4473bb | ||
![]() |
4630a7a224 | ||
![]() |
a176f38537 | ||
![]() |
0dae5d249a | ||
![]() |
2286814509 | ||
![]() |
caaac152cc | ||
![]() |
375c25d5f5 | ||
![]() |
b29d45e3a4 | ||
![]() |
c7d418b896 | ||
![]() |
8304e6b74b | ||
![]() |
221a9cbe86 | ||
![]() |
c2e83dd729 | ||
![]() |
81fe5a72d3 | ||
![]() |
a4b92a05ef | ||
![]() |
def471c917 | ||
![]() |
138c27c5d6 | ||
![]() |
e4f3ee06ec | ||
![]() |
98f6d0f8c8 | ||
![]() |
6a4b26c9bb | ||
![]() |
137d465ba8 | ||
![]() |
6a0d01494e | ||
![]() |
1e7f927aa4 | ||
![]() |
2ac0d43877 | ||
![]() |
99ddaa0ca2 | ||
![]() |
fc980eacf0 | ||
![]() |
1bafadc279 | ||
![]() |
4896f31596 | ||
![]() |
733a8a82ad | ||
![]() |
70cad7c7d1 | ||
![]() |
37a1d7ad6a | ||
![]() |
06a1f73ad2 | ||
![]() |
6daa930366 | ||
![]() |
1d18a32454 | ||
![]() |
3109a988c4 | ||
![]() |
bed63d2156 | ||
![]() |
3467157805 | ||
![]() |
62c940bde9 | ||
![]() |
6ea3166a8b | ||
![]() |
8e8e3231ec | ||
![]() |
331cc94b2c | ||
![]() |
fd0e1007db | ||
![]() |
669655930b | ||
![]() |
7d9b0aa011 | ||
![]() |
75c30c5844 | ||
![]() |
7c12a9670b | ||
![]() |
70427b81d0 | ||
![]() |
235b727017 | ||
![]() |
2397b70abb | ||
![]() |
2268516412 | ||
![]() |
6283d63331 | ||
![]() |
f58c265fb7 | ||
![]() |
414b221e6b | ||
![]() |
c0447fd7d0 | ||
![]() |
947ad9023b | ||
![]() |
a4d7b8505c | ||
![]() |
7326e31479 | ||
![]() |
3264848cde | ||
![]() |
57f9aeb696 | ||
![]() |
b2ffed2b17 | ||
![]() |
7912037f2e | ||
![]() |
03b1cb9a0b | ||
![]() |
3b59ca1321 | ||
![]() |
643e56cd6b | ||
![]() |
d4ffbe0c5c | ||
![]() |
0192fb648d | ||
![]() |
72f0000a82 | ||
![]() |
ed60a94be8 | ||
![]() |
f95a3c27a2 | ||
![]() |
7cb6461cb2 | ||
![]() |
d7ef24664d | ||
![]() |
0be9a17cfa | ||
![]() |
5e236e6ef1 | ||
![]() |
52b32f459b | ||
![]() |
0760459e8f | ||
![]() |
795f568aea | ||
![]() |
a67a08d54a | ||
![]() |
975be4236e | ||
![]() |
1ea73feab7 | ||
![]() |
0912413f11 | ||
![]() |
a10089856f | ||
![]() |
4599305acf | ||
![]() |
e17d2a27df | ||
![]() |
35afff4ed9 | ||
![]() |
16272cc6a6 | ||
![]() |
16d7656421 | ||
![]() |
162df3593d | ||
![]() |
9ad47016fb | ||
![]() |
43bad78452 | ||
![]() |
96d85c8e69 | ||
![]() |
4ae9634c52 | ||
![]() |
6573fd54dd | ||
![]() |
3399a30b19 | ||
![]() |
a9740474eb | ||
![]() |
a03962f7f8 | ||
![]() |
58b37dc630 | ||
![]() |
e96f7f58b9 | ||
![]() |
f602c371ea | ||
![]() |
b703477ed8 | ||
![]() |
3465441b58 | ||
![]() |
4e6efa9d9e | ||
![]() |
0446b5f919 | ||
![]() |
f1abdd5410 | ||
![]() |
3bf0bcf337 | ||
![]() |
0e70d8661e | ||
![]() |
a8a2a391ca | ||
![]() |
cba6776afd | ||
![]() |
4f6006cfcf | ||
![]() |
85ed427969 | ||
![]() |
40d6e5bb2c | ||
![]() |
4433acfce4 | ||
![]() |
e26ed4398c | ||
![]() |
514761e1cd | ||
![]() |
f56270430f | ||
![]() |
e5a40b67a7 | ||
![]() |
98be18c1f8 | ||
![]() |
6a2e35b125 | ||
![]() |
40c00b9708 | ||
![]() |
04619d57ad | ||
![]() |
5f75afde21 | ||
![]() |
0b7795270f | ||
![]() |
44e82382f3 | ||
![]() |
7e028d354e | ||
![]() |
dc2a2cc92e | ||
![]() |
e0fc5be8e3 | ||
![]() |
2011da83ad | ||
![]() |
7fc472bb7f | ||
![]() |
cb868f0852 | ||
![]() |
069188b2b4 | ||
![]() |
7620b7a87d | ||
![]() |
a8cad00da7 | ||
![]() |
7489a6f769 | ||
![]() |
13648020c4 | ||
![]() |
3029c7e774 | ||
![]() |
0ea460529a | ||
![]() |
5e9a256a91 | ||
![]() |
af23e1bfbb | ||
![]() |
8a507e33ec | ||
![]() |
5705e3a7dc | ||
![]() |
6c2145d0d9 | ||
![]() |
eb0f1e23ab | ||
![]() |
c17bcdd385 | ||
![]() |
862d7ad066 | ||
![]() |
fb224744e7 | ||
![]() |
c66703ed19 | ||
![]() |
f2bd0ff0ef | ||
![]() |
e44dcbf05d | ||
![]() |
d0059ed69e | ||
![]() |
7af3462d99 | ||
![]() |
3693e10cbf | ||
![]() |
05ea52813a | ||
![]() |
7b4a795142 | ||
![]() |
dcf2db3ff4 | ||
![]() |
9a86d48314 | ||
![]() |
39687805f5 | ||
![]() |
74ac87d4e5 | ||
![]() |
3ee8f43325 | ||
![]() |
e429d94f3f | ||
![]() |
adfcadf36b | ||
![]() |
bb66adb1d4 | ||
![]() |
c75e62e724 | ||
![]() |
b11b6fbf94 | ||
![]() |
3ccc0fb5d0 | ||
![]() |
e4ec9bd1ae | ||
![]() |
0b645902a7 | ||
![]() |
e2e2704a26 | ||
![]() |
e1cb39618d | ||
![]() |
3692c57123 | ||
![]() |
9a3fc1ee32 | ||
![]() |
5ce88c48f5 | ||
![]() |
387e47bde6 | ||
![]() |
5a32bb19cf | ||
![]() |
795ddf5f1b | ||
![]() |
f9348807f3 | ||
![]() |
25891fb7ca | ||
![]() |
1b72440a61 | ||
![]() |
ea81fddd96 | ||
![]() |
ad581d38e8 | ||
![]() |
6f2eb86f6a | ||
![]() |
d93a529e41 | ||
![]() |
d30cbc7d7b | ||
![]() |
38795ad77b | ||
![]() |
210c0aff76 | ||
![]() |
4d64cc1708 | ||
![]() |
bd3bdad9b1 | ||
![]() |
f753debbba | ||
![]() |
51ce961bae | ||
![]() |
1db94e8dd3 | ||
![]() |
cb1a82fc67 | ||
![]() |
c2c45356d2 | ||
![]() |
2fb3ed1157 | ||
![]() |
58fe27aa54 | ||
![]() |
5f76d94e42 | ||
![]() |
77d1881927 | ||
![]() |
361316f7b3 | ||
![]() |
4e7a0d4c90 | ||
![]() |
dd027db409 | ||
![]() |
e6c6bcbe26 | ||
![]() |
dfcb669d68 | ||
![]() |
5dac19c696 | ||
![]() |
e28729b0de | ||
![]() |
488191b237 | ||
![]() |
6eab23f8b7 | ||
![]() |
edcd1022ab | ||
![]() |
a3ed5b7719 | ||
![]() |
25c8bc11ea | ||
![]() |
99f57f3513 | ||
![]() |
67594d4bca | ||
![]() |
273d4ad6a7 | ||
![]() |
a30405de7c | ||
![]() |
6e4082c7cb | ||
![]() |
e399b6063a | ||
![]() |
e12f5edc79 | ||
![]() |
c7b36f65c3 | ||
![]() |
9a3a7d7f50 | ||
![]() |
a7754c5246 | ||
![]() |
0e6edb7211 | ||
![]() |
96be3254ab | ||
![]() |
135295b073 | ||
![]() |
b97bd2b99d | ||
![]() |
55613d717f | ||
![]() |
bf6ba24434 | ||
![]() |
6396e136d3 | ||
![]() |
0aef123193 | ||
![]() |
08ac715884 | ||
![]() |
dbdb7e29af | ||
![]() |
3a5d2d1c68 | ||
![]() |
f6e78dd47c | ||
![]() |
ad2a05fa3a | ||
![]() |
eb1c5d8f83 | ||
![]() |
e7caa267a3 | ||
![]() |
2c1c398581 | ||
![]() |
8dc43ae256 | ||
![]() |
930e8bb04d | ||
![]() |
da219abe4f | ||
![]() |
553030428d | ||
![]() |
6082cb46f5 | ||
![]() |
dfc5be16aa | ||
![]() |
ffc8fd3c60 | ||
![]() |
94420c9d4e | ||
![]() |
09d54e4863 | ||
![]() |
cd8b9f8d2c | ||
![]() |
aedaf625ff | ||
![]() |
2e5144996b | ||
![]() |
3afb6e29e4 | ||
![]() |
23f2cb16e3 | ||
![]() |
6569342f2a | ||
![]() |
3a21bd10e1 | ||
![]() |
67b39fff12 | ||
![]() |
9bc2e55d58 | ||
![]() |
b72eb5b03a | ||
![]() |
6be80e8d5c | ||
![]() |
13a7a7dc65 | ||
![]() |
149c384dce | ||
![]() |
818def3629 | ||
![]() |
60ad551523 | ||
![]() |
40a61d0615 | ||
![]() |
a2cd3c1cda | ||
![]() |
c706964880 | ||
![]() |
9f72bff710 | ||
![]() |
c169581485 | ||
![]() |
e1a1ca75cf | ||
![]() |
7fd7954c5d | ||
![]() |
9a46023d76 | ||
![]() |
d256db6bbc | ||
![]() |
87b85b1d29 | ||
![]() |
93ba4e45fc | ||
![]() |
e893ac5447 | ||
![]() |
a703bcd984 | ||
![]() |
26749ee1d3 | ||
![]() |
a3feed45fd | ||
![]() |
499453f622 | ||
![]() |
9be3a7f769 | ||
![]() |
24a7a11ccd | ||
![]() |
ce12c56644 | ||
![]() |
70fbaafe40 | ||
![]() |
a1b39bfa33 | ||
![]() |
4930387924 | ||
![]() |
f2fa130527 | ||
![]() |
7024377cad | ||
![]() |
a117529516 | ||
![]() |
ba4e4d9496 | ||
![]() |
88c0adc0af | ||
![]() |
1ee5402179 | ||
![]() |
8592cad324 | ||
![]() |
1d4312f01f | ||
![]() |
686aaa2941 | ||
![]() |
5a7ac0a3c9 | ||
![]() |
f2ccd8829f | ||
![]() |
a1c894c49d | ||
![]() |
bc79620b3a | ||
![]() |
cc2700b817 | ||
![]() |
04937e88bf | ||
![]() |
3d1301dc6d | ||
![]() |
6d47244eac | ||
![]() |
497935af91 | ||
![]() |
19a63f2cdd | ||
![]() |
794f08a7be | ||
![]() |
ef77026c0c | ||
![]() |
04d9d523d8 | ||
![]() |
8463a33b62 | ||
![]() |
d7747ea0b2 | ||
![]() |
6dc400c91c | ||
![]() |
d8e79c7a0a | ||
![]() |
067f523293 | ||
![]() |
3bdcf123a0 | ||
![]() |
7d6324aaf7 | ||
![]() |
65933f731f | ||
![]() |
47e2ca02c1 | ||
![]() |
4b8145a7ca | ||
![]() |
3ddd50bf7f | ||
![]() |
2a248f96e9 | ||
![]() |
5e46e6a771 | ||
![]() |
f52ef504fd | ||
![]() |
1978c6a62f | ||
![]() |
2240dea8ba | ||
![]() |
ed98a5bd69 |
91 changed files with 3791 additions and 653 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,6 +1,11 @@
|
||||||
*~
|
*~
|
||||||
*.fasl
|
*.fasl
|
||||||
|
*.dx32fsl
|
||||||
|
*.dx64fsl
|
||||||
|
*.lx32fsl
|
||||||
|
*.lx64fsl
|
||||||
ignore/
|
ignore/
|
||||||
generated/
|
generated/
|
||||||
.curr
|
.curr
|
||||||
.prev
|
.prev
|
||||||
|
build/
|
||||||
|
|
24
.travis.yml
Normal file
24
.travis.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
language: common-lisp
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- PATH=~/.roswell/bin:$PATH
|
||||||
|
- ROSWELL_INSTALL_DIR=$HOME/.roswell
|
||||||
|
matrix:
|
||||||
|
- LISP=sbcl-bin
|
||||||
|
- LISP=ccl-bin
|
||||||
|
|
||||||
|
install:
|
||||||
|
- curl -L https://raw.githubusercontent.com/snmsts/roswell/release/scripts/install-for-ci.sh | sh
|
||||||
|
- ros install coleslaw
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.roswell
|
||||||
|
- $HOME/.config/common-lisp
|
||||||
|
|
||||||
|
script:
|
||||||
|
- ros -s prove -e "(ql:quickload '(coleslaw coleslaw-test))"
|
||||||
|
-e '(or (prove:run :coleslaw-test) (uiop:quit -1))'
|
||||||
|
- cli-tests/basic.sh false
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Copyright (c) 2013, Brit Butler
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
163
NEWS.md
Normal file
163
NEWS.md
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
Legend:
|
||||||
|
* Site-Breaking Change:
|
||||||
|
A change that will break most config files or coleslaw installations.
|
||||||
|
It is expected to effect all users but should require only minor
|
||||||
|
user effort to resolve.
|
||||||
|
* Incompatible Change:
|
||||||
|
A change to Coleslaw's exported interface. Plugins or Themes that have
|
||||||
|
not been upstreamed are effected and may require minor effort to fix.
|
||||||
|
|
||||||
|
## Changes for 0.9.7 (2014-11-25):
|
||||||
|
|
||||||
|
* **New Plugin**: Support for [embedded gfycats](http://gfycat.com/)
|
||||||
|
has been added. See the [plugin use][plg-use] docs for further details.
|
||||||
|
* **Enhancement**: UTF-8 support has been made more portable and
|
||||||
|
added to the Wordpress import plugin. (Thanks @cmstrickland!)
|
||||||
|
* **Enhancement**: Filenames are now included in errors from the
|
||||||
|
content loader. (via @PuercoPop)
|
||||||
|
* **Enhancement**: Coleslaw now handles **deploy-dir**, **repo**,
|
||||||
|
and **staging-dir** config options more gracefully. Previously,
|
||||||
|
various errors could be encountered if directory options lacked
|
||||||
|
a trailing slash.
|
||||||
|
* Several portability fixes were made to CCL's encoding handling
|
||||||
|
and usage in the post-receive script.
|
||||||
|
* An initarg bug was fixed in the directory-does-not-exist condition.
|
||||||
|
* Some namespacing bugs in the Static Pages plugin have been fixed.
|
||||||
|
|
||||||
|
## Changes for 0.9.6 (2014-09-27):
|
||||||
|
|
||||||
|
* **SITE-BREAKING CHANGE**: Coleslaw now defaults to a "basic" deploy
|
||||||
|
instead of the previous symlinked, timestamped deploy strategy.
|
||||||
|
To retain the previous behavior, add `(versioned)` to your config's
|
||||||
|
`:plugins` list.
|
||||||
|
* **Incompatible Change**: Custom themes will be broken by a change
|
||||||
|
to URL handling. Previously, we were hand-constructing URLs in the
|
||||||
|
templates. All site objects now store their URL in an instance slot.
|
||||||
|
In general, hrefs should be of the form `<a href="{$config.domain}/{$obj.url}"> ...</a>`.
|
||||||
|
* **Incompatible Change**: The interface of the `add-injection` function
|
||||||
|
has changed. If you have written a plugin which uses `add-injection`
|
||||||
|
you should update it to conform to the [new interface][plg-api].
|
||||||
|
* **New Plugin**: Support for [twitter summary cards][ts-cards] on blog
|
||||||
|
posts has been added thanks to @PuercoPop.
|
||||||
|
* **Docs**: Improved README and Theming docs. New Config File docs.
|
||||||
|
* Changes to `:routing` would previously break links in the templates
|
||||||
|
but now work seamlessly due to the updated URL handling.
|
||||||
|
* Loading content is more robust when empty lines or metadata are passed.
|
||||||
|
Thanks to @PuercoPop for the bug report and preliminary fix.
|
||||||
|
* The config `:repo` option is now deprecated as its value has become
|
||||||
|
a required argument to `coleslaw:main`. The value passed to `main`
|
||||||
|
will override the config value going forward.
|
||||||
|
* Improved handling of directories and error-reporting when they
|
||||||
|
don't exist is available thanks to @PuercoPop.
|
||||||
|
* The templates are now HTML5 valid thanks to @Ferada.
|
||||||
|
* Fixed a bug where RSS/Atom tag feeds were being published multiple times.
|
||||||
|
|
||||||
|
## Changes for 0.9.5 (2014-06-13):
|
||||||
|
|
||||||
|
* **New Plugin**: Incremental builds, cutting runtime for generating
|
||||||
|
medium to large sites roughly in half!
|
||||||
|
* **New Plugin**: A Twitter plugin to tweet about your new posts. Thanks to @PuercoPop!
|
||||||
|
* Config options for the HTML lang and charset attributes. Thanks to @ryumei!
|
||||||
|
* Coleslaw now exports a `get-updated-files` function which can be
|
||||||
|
used to get a list of file-status/file-name pairs that were changed
|
||||||
|
in the last git push. There is also an exported `find-content-by-path`
|
||||||
|
function to retrieve content objects from the above file-name. These
|
||||||
|
were used by both the Twitter and Incremental plugins.
|
||||||
|
* The usual bugfixes, performance improvements, and documentation tweaks.
|
||||||
|
|
||||||
|
## Changes for 0.9.4 (2014-05-05):
|
||||||
|
|
||||||
|
* **SITE-BREAKING CHANGE**: Coleslaw now supports user-defined routing.
|
||||||
|
Instead of hard-coding the paths various content types are stored at,
|
||||||
|
they **must** be specified in the configuration file (.coleslawrc).
|
||||||
|
Just copy the `:routing` key from the [example][example.rc] to
|
||||||
|
get the old behavior.
|
||||||
|
* **SITE-BREAKING CHANGE**: Coleslaw's multi-site support has changed.
|
||||||
|
Instead of having a single .coleslawrc in the user's home directory
|
||||||
|
that has sections for multiple repos, a .coleslawrc may be included
|
||||||
|
*in* the blog repo itself. If no .coleslawrc is found in the repo,
|
||||||
|
it is loaded from the user's home directory instead.
|
||||||
|
* Coleslaw no longer expects a particular repo layout. Use whatever
|
||||||
|
directory hierarchy you like.
|
||||||
|
* New Content Type Plugin: Static Pages, accepting a title, url, and
|
||||||
|
optionally tags and a date. All files with a `.page` extension are
|
||||||
|
compiled as static pages and reuse the POST template.
|
||||||
|
To enable Static Pages, add `(static-pages)` to the `:plugins`
|
||||||
|
section of your config.
|
||||||
|
* Coleslaw now allows content without a date or tags. Note that POSTs
|
||||||
|
without a date will still show up in the reverse chronological
|
||||||
|
indexes at the very end.
|
||||||
|
* Fixed an embarrassing escaping bug in our last quicklisp release.
|
||||||
|
|
||||||
|
## Changes for 0.9.3 (2014-04-16):
|
||||||
|
|
||||||
|
* **INCOMPATIBLE CHANGE**: `page-path` and the `blog` config class are no longer exported.
|
||||||
|
* **INCOMPATIBLE CHANGE**: `render-content` has been renamed `render-text` for clarity.
|
||||||
|
* New Docs: [A Hacker's Guide to Coleslaw][hacking_guide] and [Themes][theming_guide]!
|
||||||
|
* A new theme *readable* based on bootswatch readable, courtesy of @rmoritz!
|
||||||
|
* Posts may have an author to support multi-user blogs courtesy of @tychoish.
|
||||||
|
* Fixes to the ReStructuredText plugin courtesy of @tychoish.
|
||||||
|
* UTF-8 fixes for config files and site content courtesy of @cl-ment.
|
||||||
|
* Fix timestamps in the sitemap plugin courtesy of @woudshoo.
|
||||||
|
|
||||||
|
## Changes for 0.9.2 (2013-05-11):
|
||||||
|
|
||||||
|
* **INCOMPATIBLE CHANGE**: Renamed staging, deploy config options staging-dir, deploy-dir.
|
||||||
|
* A plugin for Github Pages support. (thanks @mrordinaire!)
|
||||||
|
* A new and improved implementation of tags. (thanks @woudshoo!)
|
||||||
|
* A THEME-DOES-NOT-EXIST error is raised when the theme can't be found.
|
||||||
|
|
||||||
|
## Changes for 0.9.1 (2013-04-10):
|
||||||
|
|
||||||
|
* Added a PREVIEW function for REPL use.
|
||||||
|
* Make ATOM and RSS templates a separate "generic" theme. (thanks @woudshoo!)
|
||||||
|
* Fixed bug where repeatedly loading plugins caused them to appear in the page more than once. (thanks @woudshoo!)
|
||||||
|
* Fixes to spacing in navigation and tagsoup. (thanks @woudshoo!)
|
||||||
|
|
||||||
|
## Changes for 0.9 (2013-02-20):
|
||||||
|
|
||||||
|
* **INCOMPATIBLE CHANGE**: All :plugins in .coleslawrc must be lists. (i.e. (mathjax) not mathjax)
|
||||||
|
* Add support for analytics via Google.
|
||||||
|
* Add support for Restructured Text via cl-docutils.
|
||||||
|
* Add support for deploying to Amazon S3.
|
||||||
|
* Add a heroku plugin to ease hunchentoot deployments. (thanks @jsmpereira!)
|
||||||
|
* Ensure coleslaw exits after MAIN. Fixes issue #13.
|
||||||
|
* Greatly improved docs for the various plugins and plugin API.
|
||||||
|
|
||||||
|
## Changes for 0.8 (2013-01-06):
|
||||||
|
|
||||||
|
* Add support for new [content types](http://blog.redlinernotes.com/posts/Lessons-from-Coleslaw.html).
|
||||||
|
* Support for [Multi-site Publishing](http://blub.co.za/posts/Adding-multi-site-support-to-Coleslaw.html).
|
||||||
|
* CCL and Atom feed bugfixes.
|
||||||
|
* Major code refactor and docs update.
|
||||||
|
|
||||||
|
## Changes for 0.7 (2012-09-20):
|
||||||
|
|
||||||
|
* Add commenting support via Disqus plugin.
|
||||||
|
* Add formal plugin API with per-page predicate support. (aka "injections")
|
||||||
|
* Note jsmpereira's [coleslaw heroku package](https://github.com/jsmpereira/coleslaw-heroku) in README.
|
||||||
|
* Support for RSS feeds of arbitrary tags, e.g. "lisp" posts.
|
||||||
|
|
||||||
|
## Changes for 0.6.5 (2012-09-12):
|
||||||
|
|
||||||
|
* Add support for ATOM feeds.
|
||||||
|
* Add support for a sitenav in coleslawrc configs.
|
||||||
|
* Template and rendering cleanup.
|
||||||
|
* Miscellaneous deployment improvements.
|
||||||
|
|
||||||
|
## Changes for 0.6 (2012-08-29):
|
||||||
|
|
||||||
|
* Support Markdown in core rather than as a plugin.
|
||||||
|
* Improve documentation + README.
|
||||||
|
* Copious bugfixes and code cleanups.
|
||||||
|
|
||||||
|
## Changes for 0.5 (2012-08-22):
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
|
|
||||||
|
[hacking_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
|
||||||
|
[theming_guide]: https://github.com/redline6561/coleslaw/blob/master/docs/themes.md
|
||||||
|
[example.rc]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
||||||
|
[plg-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md
|
||||||
|
[plg-api]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md#extension-points
|
||||||
|
[ts-cards]: https://dev.twitter.com/cards/types/summary
|
149
README.md
149
README.md
|
@ -1,5 +1,8 @@
|
||||||
# coleslaw
|
# coleslaw
|
||||||
|
|
||||||
|
[](https://travis-ci.org/kingcons/coleslaw)
|
||||||
|
[](http://quickdocs.org/coleslaw/)
|
||||||
|
|
||||||
<img src="https://raw.github.com/redline6561/coleslaw/master/themes/hyde/css/logo_medium.jpg" alt="coleslaw logo" align="right"/>
|
<img src="https://raw.github.com/redline6561/coleslaw/master/themes/hyde/css/logo_medium.jpg" alt="coleslaw logo" align="right"/>
|
||||||
|
|
||||||
> [Czeslaw Milosz](http://blog.redlinernotes.com/tag/milosz.html) was the writer-in-residence at UNC c. 1992.
|
> [Czeslaw Milosz](http://blog.redlinernotes.com/tag/milosz.html) was the writer-in-residence at UNC c. 1992.
|
||||||
|
@ -7,59 +10,121 @@
|
||||||
> drinking coffee, reading, writing, eating chips and salsa. I remember a gentleness
|
> drinking coffee, reading, writing, eating chips and salsa. I remember a gentleness
|
||||||
> behind the enormous bushy eyebrows and that we called him Coleslaw. - anon
|
> behind the enormous bushy eyebrows and that we called him Coleslaw. - anon
|
||||||
|
|
||||||
Coleslaw aims to be flexible blog software suitable for replacing a single-user static site compiler such as Jekyll.
|
Coleslaw is Flexible Lisp Blogware similar to [Frog](https://github.com/greghendershott/frog), [Jekyll](http://jekyllrb.com/), or [Hakyll](http://jaspervdj.be/hakyll/).
|
||||||
|
|
||||||
|
Have questions?
|
||||||
|
- IRC in **#coleslaw** on Freenode!
|
||||||
|
- Subscribe to the mailing list [**coleslaw@common-lisp.net**](https://mailman.common-lisp.net/listinfo/coleslaw).
|
||||||
|
|
||||||
|
See the [wiki](https://github.com/redline6561/coleslaw/wiki/Example-sites) for a list of coleslaw-powered blogs.
|
||||||
|
|
||||||
|
Coleslaw should run on any conforming Common Lisp implementations but
|
||||||
|
testing is primarily done on [SBCL](http://www.sbcl.org/) and [CCL](http://ccl.clozure.com/).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Git for storage
|
* Git for storage
|
||||||
* RSS feeds!
|
* RSS/Atom feeds
|
||||||
* Markdown Support with Code Highlighting provided by [colorize](http://www.cliki.net/colorize).
|
* Themes
|
||||||
* Currently supports: Common Lisp, Emacs Lisp, Scheme, Clojure, C, C++, Java, Python, Erlang, Haskell, Obj-C, Diff.
|
* A [Plugin API](docs/plugin-api.md) and [**plugins**](docs/plugin-use.md) for...
|
||||||
* Plugins to...
|
|
||||||
* Use LaTeX (inside pairs of $$) via Mathjax
|
|
||||||
* Import from wordpress
|
|
||||||
* There is also a [Heroku buildpack](https://github.com/jsmpereira/coleslaw-heroku) maintained by Jose Pereira.
|
|
||||||
|
|
||||||
## Installation
|
| plugins | plugins | plugins |
|
||||||
This software should be portable to any conforming Common Lisp implementation but this guide will assume SBCL is installed. Testing has also been done on CCL.
|
|--------------------------------------------------------|----------------------------------------------|----------------------------------------------------------|
|
||||||
Server side setup:
|
| Sitemap generation | Incremental builds | Analytics via Google or [Matomo](https://www.matomo.org) |
|
||||||
|
| Comments via [Disqus](http://disqus.com/) | Comments via [isso](http://posativ.org/isso) | Hosting via [Amazon S3](http://aws.amazon.com/s3/) |
|
||||||
|
| Hosting via [Github Pages](https://pages.github.com/) | Embedding [gfycats](http://gfycat.com/) | [Tweeting](http://twitter.com/) about new posts |
|
||||||
|
| [Mathjax](http://mathjax.org/) | Posts in ReStructured Text | [Wordpress](http://wordpress.org/) import |
|
||||||
|
| [Pygments](http://pygments.org/) | [colorize](http://www.cliki.net/colorize) | |
|
||||||
|
|
||||||
1. Setup git and create a bare repo as shown [here](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server).
|
|
||||||
2. Install Lisp and [Quicklisp](http://quicklisp.org/).
|
|
||||||
3. ```wget -c https://raw.github.com/redline6561/coleslaw/master/example.coleslawrc -O ~/.coleslawrc``` # and edit as necessary
|
|
||||||
4. ```wget -c https://raw.github.com/redline6561/coleslaw/master/example.post-receieve -O your-blog.git/hooks/post-receive``` # and edit as necessary
|
|
||||||
5. ```chmod +x your-blog/.git/hooks/post-receive```
|
|
||||||
6. Create or clone your blog repo locally. Add your server as a remote with ```git remote add prod git@my-host.com:path/to/repo.git```
|
|
||||||
7. Point the web server of your choice at the symlink /path/to/deploy-dir/.curr/
|
|
||||||
|
|
||||||
Now whenever you push a new commit to the server, coleslaw will update your blog automatically! You may need to git push -u prod master the first time.
|
## Installation/Tutorial
|
||||||
|
|
||||||
|
<!-- Don't let the first user select from multiple choises -->
|
||||||
|
|
||||||
|
Step 1: Install this library.
|
||||||
|
|
||||||
## The Post Format
|
|
||||||
Coleslaw expects post files to be formatted as follows:
|
|
||||||
```
|
```
|
||||||
;;;;;
|
$ ros install coleslaw-org/coleslaw # With [Roswell](https://roswell.github.io/)
|
||||||
title: foo
|
$ export PATH="$HOME/.roswell/bin:$PATH" # If you haven't done this before for Roswell
|
||||||
tags: bar, baz
|
or
|
||||||
date: yyyy-mm-dd hh:mm:ss
|
CL-USER> (ql:quickload :coleslaw-cli)
|
||||||
format: html (for raw html) or md (for markdown)
|
|
||||||
;;;;;
|
|
||||||
your post
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Importing from Wordpress
|
Step 2: Initialize your blog repository.
|
||||||
There is a "plugin" to import from wordpress. At some point, it should be turned into a standalone script. Until then...
|
|
||||||
|
|
||||||
1. Export your posts from wordpress.
|
```
|
||||||
2. In your lisp of choice, do the following:
|
$ mkdir yourblog ; cd yourblog
|
||||||
1. ```(ql:quickload 'coleslaw)```
|
$ git init
|
||||||
2. ```(in-package :coleslaw)```
|
$ coleslaw setup # or
|
||||||
3. ```(load-plugins '(import))```
|
CL-USER> (coleslaw-cli:setup)
|
||||||
4. ```(coleslaw-import::import-posts "/path/to/export.xml")```
|
```
|
||||||
|
|
||||||
The XML will be read and placed into .post files in the :repo location specified in your [.coleslawrc](http://github.com/redline6561/coleslaw/blob/master/example.coleslawrc).
|
`coleslaw setup` / `(coleslaw-cli:setup)` will generate a `.coleslawrc` file in
|
||||||
|
the current directory, which contains the configuration of the static website.
|
||||||
|
|
||||||
## Writing your own plugins
|
Step 3: Write a post file in the current directory.
|
||||||
For now, see the [API](http://redlinernotes.com/docs/coleslaw.html) and the [mathjax plugin](https://github.com/redline6561/coleslaw/blob/master/plugins/mathjax.lisp) for an example.
|
The file should contain a certain metadata, so use the `coleslaw new` command,
|
||||||
A proper guide about this will be written later.
|
which instantiates a correct file for you.
|
||||||
|
|
||||||
## Theming
|
```
|
||||||
A default theme, hyde, is provided. Themes are made using Google's closure-template and the source for [hyde](https://github.com/redline6561/coleslaw/tree/master/themes/hyde) should be simple and instructive until I can provide better docs.
|
$ coleslaw new
|
||||||
|
Created a post 2017-11-06.post .
|
||||||
|
# or
|
||||||
|
CL-USER> (coleslaw-cli:new "post")
|
||||||
|
Created a post 2017-11-06.post .
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 4: Generate the site from those post files.
|
||||||
|
The result goes to the *staging directory* specified in the `.coleslawrc` file.
|
||||||
|
The staging directory is `/tmp/coleslaw/` by default.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coleslaw # or
|
||||||
|
$ coleslaw generate # or
|
||||||
|
$ coleslaw stage # or
|
||||||
|
CL-USER> (coleslaw-cli:generate) ; or
|
||||||
|
CL-USER> (coleslaw-cli:stage) ; --- these are all aliases
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 5: You can launch a web server to check the result on a browser.
|
||||||
|
(Running a webserver sometimes has a benefit over just opening an html file,
|
||||||
|
e.g. the relative links behaves differently on a file:/// protocol)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coleslaw preview # or
|
||||||
|
CL-USER> (coleslaw-cli:preview)
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 6: and watch the file system to automatically regenerate the site!
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coleslaw watch # or even better,
|
||||||
|
$ coleslaw watch-preview # or, on REPL,
|
||||||
|
CL-USER> (coleslaw-cli:watch) ;; watch-preview does not work on REPL right now
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 7: When you think your article is publishable, run
|
||||||
|
|
||||||
|
```
|
||||||
|
$ coleslaw deploy # or
|
||||||
|
CL-USER> (coleslaw-cli:deploy)
|
||||||
|
```
|
||||||
|
|
||||||
|
To move the contents in the staging dir to the deploy dir.
|
||||||
|
By default, this deploy command uses `rsync` to sync the directories,
|
||||||
|
where the deploy dir could be a remote directory on the server which is running your website.
|
||||||
|
By using a plugin, you can customize this behavior e.g. running the deploy on gh-pages.
|
||||||
|
|
||||||
|
For further customization, e.g. adding a new plugin, developing a new plugin, changing the deploy option, or creating a new theme,
|
||||||
|
see the [config docs](docs).
|
||||||
|
|
||||||
|
We provide three default themes: hyde, the default, and readable (based on
|
||||||
|
[bootswatch readable](http://bootswatch.com/readable/)).
|
||||||
|
|
||||||
|
A core goal of *coleslaw* is to be both pleasant to read and easy to
|
||||||
|
hack on and extend. If you want to understand the internals and bend
|
||||||
|
*coleslaw* to do new and interesting things, I strongly encourage you
|
||||||
|
to read the [Hacker's Guide to Coleslaw][hackers]. You'll find some
|
||||||
|
current **TODO** items towards the bottom.
|
||||||
|
|
||||||
|
[hackers]: docs/hacking.md
|
||||||
|
|
10
TODO
10
TODO
|
@ -1,10 +0,0 @@
|
||||||
What about themes? Templates are themes. DUH.
|
|
||||||
|
|
||||||
BUGS:
|
|
||||||
; Slugs aren't unicode safe. See [reddit discussion](http://www.reddit.com/r/lisp/comments/yvh6g/coleslaw_jekylllike_static_blogware_in_500_lines/) and [mozilla code](https://github.com/mozilla/unicode-slugify/blob/master/slugify/__init__.py).
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
; doc themes and plugins, s3+hunchentoot. -> 0.8
|
|
||||||
; unit tests -> 0.9
|
|
||||||
; Incremental compilation: only "touched" posts+tags+months and by-n. -> 1.0
|
|
||||||
;; possible plugins: analytics, logging/monitoring, crossposting
|
|
45
cli-tests/basic.sh
Executable file
45
cli-tests/basic.sh
Executable file
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash -x
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
dir=$(mktemp -d)
|
||||||
|
|
||||||
|
cd $dir
|
||||||
|
|
||||||
|
coleslaw setup
|
||||||
|
|
||||||
|
cat .coleslawrc
|
||||||
|
|
||||||
|
post=$(coleslaw new post "my first blog")
|
||||||
|
|
||||||
|
echo "my firrrrrrst text!!!!" >> "$post"
|
||||||
|
|
||||||
|
cat "$post"
|
||||||
|
|
||||||
|
coleslaw generate
|
||||||
|
|
||||||
|
coleslaw preview &
|
||||||
|
pid=$!
|
||||||
|
|
||||||
|
trap "kill $pid; rm -rf $dir" EXIT
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
try_query=${1:-true}
|
||||||
|
|
||||||
|
if $try_query
|
||||||
|
then
|
||||||
|
# Doesn't run on Travis!
|
||||||
|
curl --fail 127.0.0.1:5000
|
||||||
|
! curl --fail 127.0.0.1:5000/nosuchurl
|
||||||
|
echo success!
|
||||||
|
fi
|
||||||
|
|
||||||
|
# (
|
||||||
|
# wget 127.0.0.1:5000/nosuchurl -O-
|
||||||
|
# echo $?
|
||||||
|
# true
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
|
321
cli/cli.lisp
Normal file
321
cli/cli.lisp
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
(defpackage :coleslaw-cli
|
||||||
|
(:use :cl :trivia)
|
||||||
|
(:export
|
||||||
|
#:copy-theme
|
||||||
|
#:setup
|
||||||
|
#:new
|
||||||
|
#:generate
|
||||||
|
#:preview
|
||||||
|
#:watch
|
||||||
|
#:watch-preview
|
||||||
|
#:help
|
||||||
|
#:stage
|
||||||
|
#:deploy))
|
||||||
|
|
||||||
|
(in-package :coleslaw-cli)
|
||||||
|
|
||||||
|
(defun setup-coleslawrc (user &aux (path (merge-pathnames ".coleslawrc")))
|
||||||
|
"Set up the default .coleslawrc file in the current directory."
|
||||||
|
(with-open-file (s path :direction :output :if-exists :supersede :if-does-not-exist :create)
|
||||||
|
(format t "~&Generating ~a ...~%" path)
|
||||||
|
;; odd formatting in this source code because emacs has problem detecting the parenthesis inside a string
|
||||||
|
(format s ";;; -*- mode : lisp -*-~%(~
|
||||||
|
;; Required information
|
||||||
|
:author \"~a\" ;; to be placed on post pages and in the copyright/CC-BY-SA notice
|
||||||
|
:deploy-dir \"deploy/\" ;; for Coleslaw's generated HTML to go in
|
||||||
|
:domain \"\" ;; to generate absolute links to the site content. Note: with :cname option of gh-pages, this requires a url scheme, e.g. https://fake.org
|
||||||
|
:routing ((:post \"posts/~~a\") ;; to determine the URL scheme of content on the site
|
||||||
|
(:tag-index \"tag/~~a\")
|
||||||
|
(:month-index \"date/~~a\")
|
||||||
|
(:numeric-index \"~~d\")
|
||||||
|
(:feed \"~~a.xml\")
|
||||||
|
(:tag-feed \"tag/~~a.xml\")
|
||||||
|
(:sitemap \"~~a.xml\"))
|
||||||
|
:title \"Improved Means for Achieving Deteriorated Ends\" ;; a site title
|
||||||
|
:theme \"hyde\" ;; to select one of the themes in \"coleslaw/themes/\"
|
||||||
|
|
||||||
|
;; Optional information
|
||||||
|
:excerpt-sep \"<!--more-->\" ;; to set the separator for excerpt in content
|
||||||
|
:feeds (\"lisp\")
|
||||||
|
:plugins ((analytics :tracking-code \"foo\")
|
||||||
|
(disqus :shortname \"my-site-name\")
|
||||||
|
; (incremental) ;; *Remove comment to enable incremental builds.
|
||||||
|
(mathjax)
|
||||||
|
(sitemap)
|
||||||
|
(static-pages)
|
||||||
|
;; deployment plugins
|
||||||
|
;; deployment to github pages
|
||||||
|
; (gh-pages :url \"git@github.com:myaccount/myrepo.git\"
|
||||||
|
; ; :cname t ;; if you want to use the custom domain --- see http://pages.github.com/
|
||||||
|
; )
|
||||||
|
;; versioned deployment. Remove comment to enable symlinked, timestamped deploys.
|
||||||
|
; (versioned)
|
||||||
|
;; default deploy method is rsync
|
||||||
|
(rsync \"-avz\" \"--delete\" \"--exclude\" \".git/\" \"--exclude\" \".gitignore\" \"--copy-links\")
|
||||||
|
)
|
||||||
|
:sitenav ((:url \"http://~a.github.com/\" :name \"Home\")
|
||||||
|
(:url \"http://twitter.com/~a\" :name \"Twitter\")
|
||||||
|
(:url \"http://github.com/~a\" :name \"Code\")
|
||||||
|
(:url \"http://soundcloud.com/~a\" :name \"Music\")
|
||||||
|
(:url \"http://redlinernotes.com/docs/talks/\" :name \"Talks\"))
|
||||||
|
:staging-dir \"/tmp/coleslaw/\" ;; for Coleslaw to do intermediate work, default: \"/tmp/coleslaw\"
|
||||||
|
)
|
||||||
|
|
||||||
|
;; * Prerequisites described in plugin docs."
|
||||||
|
user
|
||||||
|
user
|
||||||
|
user
|
||||||
|
user
|
||||||
|
user)))
|
||||||
|
|
||||||
|
(defun copy-theme (which &optional (target which))
|
||||||
|
"Copy the theme named WHICH into the blog directory and rename it into TARGET"
|
||||||
|
(format t "~&Copying themes/~a ...~%" which)
|
||||||
|
(if (probe-file (format nil "themes/~a" which))
|
||||||
|
(format t "~& themes/~a already exists.~%" which)
|
||||||
|
(progn
|
||||||
|
(ensure-directories-exist "themes/" :verbose t)
|
||||||
|
(uiop:run-program `("cp" "-v" "-r"
|
||||||
|
,(namestring (coleslaw::app-path "themes/~a/" which))
|
||||||
|
,(namestring (merge-pathnames (format nil "themes/~a" target))))))))
|
||||||
|
|
||||||
|
(defun setup (&optional (user (uiop:getenv "USER")))
|
||||||
|
(setup-coleslawrc user)
|
||||||
|
(copy-theme "hyde" "default"))
|
||||||
|
|
||||||
|
(defun read-rc (&aux (path (merge-pathnames ".coleslawrc")))
|
||||||
|
(with-open-file (s (if (probe-file path)
|
||||||
|
path
|
||||||
|
(merge-pathnames #p".coleslawrc" (user-homedir-pathname))))
|
||||||
|
(read s)))
|
||||||
|
|
||||||
|
(defun new (&optional (type "post") name)
|
||||||
|
(let ((sep (getf (read-rc) :separator ";;;;;")))
|
||||||
|
(multiple-value-match (get-decoded-time)
|
||||||
|
((second minute hour date month year _ _ _)
|
||||||
|
(let* ((name (or name
|
||||||
|
(format nil "~a-~2,,,'0@a-~2,,,'0@a" year month date)))
|
||||||
|
(path (merge-pathnames (make-pathname :name name :type type))))
|
||||||
|
(with-open-file (s path
|
||||||
|
:direction :output :if-exists :error :if-does-not-exist :create)
|
||||||
|
(format s "~
|
||||||
|
~a
|
||||||
|
title: ~a
|
||||||
|
tags: bar, baz
|
||||||
|
date: ~a-~2,,,'0@a-~2,,,'0@a ~2,,,'0@a:~2,,,'0@a:~2,,,'0@a
|
||||||
|
format: md
|
||||||
|
~:[~*~;URL: pages/~a.html~%~]~
|
||||||
|
~a
|
||||||
|
|
||||||
|
<!-- **** your post here (remove this line) **** -->
|
||||||
|
<!-- format: could be 'html' (for raw html) or 'md' (for markdown). -->
|
||||||
|
|
||||||
|
Here is my content.
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
Excerpt separator can also be extracted from content.
|
||||||
|
Add `excerpt: <string>` to the above metadata.
|
||||||
|
Excerpt separator is `<!--more-->` by default.
|
||||||
|
"
|
||||||
|
sep
|
||||||
|
name
|
||||||
|
year month date hour minute second
|
||||||
|
(string= type "page") name
|
||||||
|
sep)
|
||||||
|
(format *error-output* "~&Created a ~a \"~a\".~%" type name)
|
||||||
|
(format t "~&~a~%" path)
|
||||||
|
path))))))
|
||||||
|
|
||||||
|
(defun generate ()
|
||||||
|
(stage))
|
||||||
|
|
||||||
|
(defun stage ()
|
||||||
|
(prog1 (coleslaw:main *default-pathname-defaults* :deploy nil)
|
||||||
|
(format t "~&Page generated at the staging dir ~a~%" (getf (read-rc) :staging-dir))))
|
||||||
|
|
||||||
|
(defun deploy ()
|
||||||
|
(prog1 (coleslaw:main *default-pathname-defaults* :deploy t)
|
||||||
|
(format t "~&Page deployed at the deploy dir ~a~%" (getf (read-rc) :deploy-dir))))
|
||||||
|
|
||||||
|
(defun preview (&optional (path (getf (read-rc) :staging-dir)))
|
||||||
|
;; clack depends on the global binding of *default-pathname-defaults*.
|
||||||
|
(let ((oldpath *default-pathname-defaults*))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(when path
|
||||||
|
(setf *default-pathname-defaults* (truename path)))
|
||||||
|
(format t "~%Starting a Clack server at ~a. Press C-c to stop it~%" path)
|
||||||
|
(clack:clackup
|
||||||
|
(lack:builder
|
||||||
|
:accesslog
|
||||||
|
(:static :path (lambda (p)
|
||||||
|
(if (char= #\/ (alexandria:last-elt p))
|
||||||
|
(concatenate 'string p "index.html")
|
||||||
|
p)))
|
||||||
|
#'identity)
|
||||||
|
:use-thread nil))
|
||||||
|
(setf *default-pathname-defaults* oldpath))))
|
||||||
|
|
||||||
|
;; code from fs-watcher
|
||||||
|
|
||||||
|
(defun mtime (pathname)
|
||||||
|
"Returns the mtime of a pathname"
|
||||||
|
(when (ignore-errors (probe-file pathname))
|
||||||
|
(file-write-date pathname)))
|
||||||
|
|
||||||
|
(defun dir-contents (pathnames test)
|
||||||
|
(remove-if-not test
|
||||||
|
;; uiop:slurp-input-stream
|
||||||
|
(uiop:run-program `("find" ,@(mapcar #'namestring pathnames))
|
||||||
|
:output :lines)))
|
||||||
|
|
||||||
|
(defun run-loop (pathnames mtimes callback delay)
|
||||||
|
"The main loop constantly polling the filesystem"
|
||||||
|
(loop
|
||||||
|
(sleep delay)
|
||||||
|
(map nil
|
||||||
|
#'(lambda (pathname)
|
||||||
|
(let ((mtime (mtime pathname)))
|
||||||
|
(unless (eql mtime (gethash pathname mtimes))
|
||||||
|
(funcall callback pathname)
|
||||||
|
(if mtime
|
||||||
|
(setf (gethash pathname mtimes) mtime)
|
||||||
|
(remhash pathname mtimes)))))
|
||||||
|
pathnames)))
|
||||||
|
|
||||||
|
(defun watch (&optional (source-path *default-pathname-defaults*))
|
||||||
|
(format t "~&Start watching! : ~a~%" source-path)
|
||||||
|
(let ((pathnames
|
||||||
|
(dir-contents (list source-path)
|
||||||
|
(lambda (p) (not (equal "fasl" (pathname-type p))))))
|
||||||
|
(mtimes (make-hash-table)))
|
||||||
|
(dolist (pathname pathnames)
|
||||||
|
(setf (gethash pathname mtimes) (mtime pathname)))
|
||||||
|
(ignore-errors
|
||||||
|
(run-loop pathnames
|
||||||
|
mtimes
|
||||||
|
(lambda (pathname)
|
||||||
|
(format t "~&Changes detected! : ~a~%" pathname)
|
||||||
|
(finish-output)
|
||||||
|
(handler-case
|
||||||
|
(coleslaw:main source-path)
|
||||||
|
(error (c)
|
||||||
|
(format *error-output* "something happened... ~a" c))))
|
||||||
|
1))))
|
||||||
|
|
||||||
|
(defun watch-preview (&optional (source-path *default-pathname-defaults*))
|
||||||
|
(when (member :swank *features*)
|
||||||
|
(warn "FIXME: This command does not do what you intend from a SLIME session."))
|
||||||
|
(ignore-errors
|
||||||
|
(uiop:run-program
|
||||||
|
;; The hackiness here is because clack fails? to handle? SIGINT correctly when run in a threaded mode
|
||||||
|
`("sh" "-c" ,(format nil "coleslaw watch ~a &~
|
||||||
|
coleslaw preview &~
|
||||||
|
jobs -p;~
|
||||||
|
trap \"kill $(jobs -p)\" EXIT;~
|
||||||
|
wait" source-path))
|
||||||
|
:output :interactive
|
||||||
|
:error-output :interactive)))
|
||||||
|
|
||||||
|
(defun help ()
|
||||||
|
(format *error-output* "
|
||||||
|
|
||||||
|
|
||||||
|
Coleslaw, a Flexible Lisp Blogware.
|
||||||
|
Written by: Brit Butler <redline6561@gmail.com>.
|
||||||
|
Distributed by BSD license.
|
||||||
|
|
||||||
|
Command Line Syntax:
|
||||||
|
|
||||||
|
coleslaw setup [NAME] --- Sets up a new .coleslawrc file in the current directory.
|
||||||
|
coleslaw copy-theme THEME [TARGET] --- Copies the installed THEME in coleslaw to the current directory with a different name TARGET.
|
||||||
|
coleslaw new [TYPE] [NAME] --- Creates a new content file with the correct format. TYPE defaults to 'post', NAME defaults to the current date.
|
||||||
|
coleslaw stage --- Generates the static html in the staging dir.
|
||||||
|
coleslaw generate --- Alias to `coleslaw stage`.
|
||||||
|
coleslaw deploy --- Generates the static html in the staging dir, then publish it to the deploy dir.
|
||||||
|
coleslaw preview [DIRECTORY] --- Runs a preview server at port 5000. DIRECTORY defaults to the staging directory.
|
||||||
|
coleslaw watch [DIRECTORY] --- Watches the given directory and generates the site when changes are detected. Defaults to the current directory.
|
||||||
|
coleslaw --- Alias to `coleslaw stage`.
|
||||||
|
coleslaw -h --- Show this help
|
||||||
|
|
||||||
|
Corresponding REPL commands are available in coleslaw-cli package.
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(ql:quickload :coleslaw-cli)
|
||||||
|
(coleslaw-cli:setup &optional name)
|
||||||
|
(coleslaw-cli:copy-theme theme &optional target)
|
||||||
|
(coleslaw-cli:new &optional type name)
|
||||||
|
(coleslaw-cli:stage)
|
||||||
|
(coleslaw-cli:generate)
|
||||||
|
(coleslaw-cli:deploy)
|
||||||
|
(coleslaw-cli:preview &optional directory)
|
||||||
|
(coleslaw-cli:watch &optional directory)
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* set up a blog
|
||||||
|
|
||||||
|
mkdir yourblog ; cd yourblog
|
||||||
|
git init
|
||||||
|
coleslaw setup
|
||||||
|
git commit -a -m 'initial repo'
|
||||||
|
|
||||||
|
* Copy the base theme to the current directory for modification
|
||||||
|
|
||||||
|
coleslaw copy-theme hyde mytheme
|
||||||
|
|
||||||
|
* Create a post
|
||||||
|
|
||||||
|
coleslaw new
|
||||||
|
|
||||||
|
* Create a page (static page)
|
||||||
|
|
||||||
|
coleslaw new page
|
||||||
|
|
||||||
|
* Generate a site
|
||||||
|
|
||||||
|
coleslaw generate
|
||||||
|
# or just:
|
||||||
|
coleslaw
|
||||||
|
|
||||||
|
* Preview a site
|
||||||
|
|
||||||
|
coleslaw preview
|
||||||
|
# or
|
||||||
|
coleslaw preview .
|
||||||
|
|
||||||
|
"
|
||||||
|
))
|
||||||
|
|
||||||
|
(defun main (&rest argv)
|
||||||
|
(declare (ignorable argv))
|
||||||
|
(match argv
|
||||||
|
((list* "setup" rest)
|
||||||
|
(apply #'setup rest))
|
||||||
|
((list* "preview" rest)
|
||||||
|
(apply #'preview rest))
|
||||||
|
((list* "watch" rest)
|
||||||
|
(apply #'watch rest))
|
||||||
|
((list* "watch-preview" rest)
|
||||||
|
(apply #'watch-preview rest))
|
||||||
|
((list* "new" rest)
|
||||||
|
(apply #'new rest))
|
||||||
|
((list* "generate" rest)
|
||||||
|
(apply #'generate rest))
|
||||||
|
((list* "stage" rest)
|
||||||
|
(apply #'stage rest))
|
||||||
|
((list* "deploy" rest)
|
||||||
|
(apply #'deploy rest))
|
||||||
|
(nil
|
||||||
|
(generate))
|
||||||
|
((list* "copy-theme" rest)
|
||||||
|
(apply #'copy-theme rest))
|
||||||
|
((list* (or "-v" "--version") _)
|
||||||
|
)
|
||||||
|
((list* (or "-h" "--help") _)
|
||||||
|
(help))))
|
||||||
|
|
||||||
|
(when (member :swank *features*)
|
||||||
|
(help))
|
14
coleslaw-cli.asd
Normal file
14
coleslaw-cli.asd
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
(defsystem #:coleslaw-cli
|
||||||
|
:name "coleslaw"
|
||||||
|
:description "Flexible Lisp Blogware"
|
||||||
|
:version "0.9.7"
|
||||||
|
:license "BSD"
|
||||||
|
:author "Brit Butler <redline6561@gmail.com>"
|
||||||
|
:pathname "cli/"
|
||||||
|
:depends-on (:coleslaw
|
||||||
|
:clack
|
||||||
|
:trivia
|
||||||
|
:uiop)
|
||||||
|
:serial t
|
||||||
|
:components ((:file "cli")))
|
||||||
|
|
14
coleslaw-test.asd
Normal file
14
coleslaw-test.asd
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
(in-package #:asdf-user)
|
||||||
|
|
||||||
|
(defsystem #:coleslaw-test
|
||||||
|
:description "A test suite for coleslaw."
|
||||||
|
:license "BSD"
|
||||||
|
:author "Brit Butler <redline6561@gmail.com>"
|
||||||
|
:depends-on (:coleslaw :coleslaw-cli :prove)
|
||||||
|
:defsystem-depends-on (:prove-asdf)
|
||||||
|
:components ((:module "tests"
|
||||||
|
:components
|
||||||
|
((:test-file "tests")
|
||||||
|
(:test-file "cli"))))
|
||||||
|
:perform (test-op :after (op c)
|
||||||
|
(uiop:symbol-call :prove 'run c)))
|
46
coleslaw.asd
46
coleslaw.asd
|
@ -1,37 +1,33 @@
|
||||||
|
(in-package #:asdf-user)
|
||||||
|
|
||||||
(defsystem #:coleslaw
|
(defsystem #:coleslaw
|
||||||
:name "coleslaw-core"
|
:name "coleslaw"
|
||||||
:description "Flexible Lisp Blogware"
|
:description "Flexible Lisp Blogware"
|
||||||
:version "0.7"
|
:version "0.9.7"
|
||||||
:license "BSD"
|
:license "BSD"
|
||||||
:author "Brit Butler <redline6561@gmail.com>"
|
:author "Brit Butler <redline6561@gmail.com>"
|
||||||
:pathname "src/"
|
:pathname "src/"
|
||||||
:depends-on (:closure-template :3bmd :3bmd-ext-code-blocks
|
:depends-on (:closure-template
|
||||||
:alexandria :local-time :trivial-shell :cl-fad)
|
:3bmd
|
||||||
|
:3bmd-ext-code-blocks
|
||||||
|
:alexandria
|
||||||
|
:local-time
|
||||||
|
:inferior-shell
|
||||||
|
:cl-fad
|
||||||
|
:cl-ppcre
|
||||||
|
:closer-mop
|
||||||
|
:cl-unicode
|
||||||
|
:uiop)
|
||||||
:serial t
|
:serial t
|
||||||
:components ((:file "packages")
|
:components ((:file "coleslaw-conf")
|
||||||
|
(:file "packages")
|
||||||
(:file "util")
|
(:file "util")
|
||||||
(:file "config")
|
(:file "config")
|
||||||
(:file "themes")
|
(:file "themes")
|
||||||
|
(:file "documents")
|
||||||
|
(:file "content")
|
||||||
(:file "posts")
|
(:file "posts")
|
||||||
(:file "indices")
|
(:file "indexes")
|
||||||
(:file "feeds")
|
(:file "feeds")
|
||||||
(:file "coleslaw"))
|
(:file "coleslaw"))
|
||||||
:in-order-to ((test-op (load-op coleslaw-tests)))
|
:in-order-to ((test-op (test-op coleslaw-test))))
|
||||||
:perform (test-op :after (op c)
|
|
||||||
(funcall (intern "RUN!" :coleslaw-tests)
|
|
||||||
(intern "COLESLAW-TESTS" :coleslaw-tests))))
|
|
||||||
|
|
||||||
(defsystem #:coleslaw-tests
|
|
||||||
:depends-on (coleslaw fiveam)
|
|
||||||
:pathname "tests/"
|
|
||||||
:serial t
|
|
||||||
:components ((:file "packages")
|
|
||||||
(:file "tests")))
|
|
||||||
|
|
||||||
(defmethod operation-done-p ((op test-op)
|
|
||||||
(c (eql (find-system :coleslaw))))
|
|
||||||
(values nil))
|
|
||||||
|
|
||||||
(defpackage #:coleslaw-conf (:export #:*basedir*))
|
|
||||||
(defparameter coleslaw-conf:*basedir*
|
|
||||||
(make-pathname :name nil :type nil :defaults *load-truename*))
|
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<title>API for COLESLAW</title>
|
|
||||||
<style type="text/css" media="all">
|
|
||||||
body {margin: 0 2em .5em 2em;font-family: Verdana,Arial,sans-serif;}
|
|
||||||
.package {background: #efefef;
|
|
||||||
padding: 1.5em 0 1em 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: x-large;}
|
|
||||||
.definition {background: #efefef; padding: .3em 1em;}
|
|
||||||
a.symbolname, a:visited.symbolname {font-weight: bold;}
|
|
||||||
.initargs {font-size: small;}
|
|
||||||
.slots {font-size: small;}
|
|
||||||
div.label {border-bottom: 1px solid #efefef; margin-bottom: .5em}
|
|
||||||
.symboldecl, .footer {margin: 0 2em 2em 2em;}
|
|
||||||
.symbolname {font-weight: bold; color: gray;}
|
|
||||||
.symboltype {font-style: italic;margin-left: 1.5em; font-size: smaller;}
|
|
||||||
.documentation {color: gray; font-family: Fixed,monospace;margin: 0 0 1.5em 0.5em;}
|
|
||||||
.packagedocumentation {color: gray;
|
|
||||||
font-family: Fixed,monospace;
|
|
||||||
margin: 0 0 1.5em 0;
|
|
||||||
border: 1px solid #efefef;
|
|
||||||
padding-left: 1.5em;}
|
|
||||||
.symbolcomments span {font-weight: bold;}
|
|
||||||
.footer {font-size: x-small; text-align: right; margin-top: 2em; padding-top: 1em; border-top: 1px solid gray;}
|
|
||||||
.undocumented {color: red; font-weight: bold;}
|
|
||||||
a, a:visited {font-weight: bold; color: gray; text-decoration: none; font-weight: normal;}
|
|
||||||
a:hover {border-bottom: 1px solid gray; }
|
|
||||||
.label {font-weight: bold; font-style: italic;color: gray;}
|
|
||||||
.labeltitle {font-weight: bold; font-style: italic;color: gray; border: 1px solid #efefef; padding: .25em;margin-bottom: .5em}
|
|
||||||
.frame {marin-top: 1.5em}
|
|
||||||
.expander {border: 2px solid #efefef; color: gray;
|
|
||||||
font-weight: bold;
|
|
||||||
font-family: Fixed,monospace;
|
|
||||||
margin-right: .25em; padding: 0 .25em;cursor: pointer;}
|
|
||||||
</style>
|
|
||||||
<script type="text/javascript">
|
|
||||||
function expand (expander, id) {
|
|
||||||
var text = expander.innerHTML;
|
|
||||||
if (text == '-')
|
|
||||||
{
|
|
||||||
expander.innerHTML = '+';
|
|
||||||
document.getElementById(id).style.display = 'none';
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
expander.innerHTML = '-';
|
|
||||||
document.getElementById(id).style.display = '';
|
|
||||||
}
|
|
||||||
}</script></head>
|
|
||||||
<body>
|
|
||||||
<div class="package">
|
|
||||||
<div class="definition">API for package:
|
|
||||||
<a class="symbolname" name="coleslaw_package" href="#coleslaw_package">coleslaw</a></div></div>
|
|
||||||
<div class="packagedocumentation">
|
|
||||||
<pre>Homepage: <a href="http://github.com/redline6561/coleslaw">Github</a></pre></div>
|
|
||||||
<div class="frame">
|
|
||||||
<div class="labeltitle">
|
|
||||||
<span class="expander" onclick="expand(this, 'functions');">-</span>Functions</div>
|
|
||||||
<div id="functions">
|
|
||||||
<div class="symboldecl">
|
|
||||||
<div class="definition">
|
|
||||||
<a class="symbolname" name="add-injection_func" href="#add-injection_func">add-injection</a>
|
|
||||||
<span class="lambdalist">str location</span>
|
|
||||||
<span class="symboltype">standard-generic-function</span></div>
|
|
||||||
<div class="documentation">
|
|
||||||
<pre>Add STR to the list of elements injected in LOCATION.</pre></div></div>
|
|
||||||
<div class="symboldecl">
|
|
||||||
<div class="definition">
|
|
||||||
<a class="symbolname" name="deploy_func" href="#deploy_func">deploy</a>
|
|
||||||
<span class="lambdalist">staging</span>
|
|
||||||
<span class="symboltype">standard-generic-function</span></div>
|
|
||||||
<div class="documentation">
|
|
||||||
<pre>Deploy the STAGING dir, updating the .prev and .curr symlinks.</pre></div></div>
|
|
||||||
<div class="symboldecl">
|
|
||||||
<div class="definition">
|
|
||||||
<a class="symbolname" name="(setf deploy)_func" href="#(setf deploy)_func">(setf deploy)</a>
|
|
||||||
<span class="lambdalist">new-value object</span>
|
|
||||||
<span class="symboltype">standard-generic-function</span></div>
|
|
||||||
<div class="documentation">
|
|
||||||
<pre>:undocumented</pre></div></div>
|
|
||||||
<div class="symboldecl">
|
|
||||||
<div class="definition">
|
|
||||||
<a class="symbolname" name="main_func" href="#main_func">main</a>
|
|
||||||
<span class="lambdalist"></span>
|
|
||||||
<span class="symboltype">function</span></div>
|
|
||||||
<div class="documentation">
|
|
||||||
<pre>Load the user's config, then compile and deploy the blog.</pre></div></div>
|
|
||||||
<div class="symboldecl">
|
|
||||||
<div class="definition">
|
|
||||||
<a class="symbolname" name="render_func" href="#render_func">render</a>
|
|
||||||
<span class="lambdalist">content &key next prev &allow-other-keys</span>
|
|
||||||
<span class="symboltype">standard-generic-function</span></div>
|
|
||||||
<div class="documentation">
|
|
||||||
<pre>Render the given CONTENT to HTML.</pre></div></div>
|
|
||||||
<div class="symboldecl">
|
|
||||||
<div class="definition">
|
|
||||||
<a class="symbolname" name="render-content_func" href="#render-content_func">render-content</a>
|
|
||||||
<span class="lambdalist">text format</span>
|
|
||||||
<span class="symboltype">standard-generic-function</span></div>
|
|
||||||
<div class="documentation">
|
|
||||||
<pre>Compile TEXT from the given FORMAT to HTML for display.</pre></div></div></div></div>
|
|
||||||
<div class="footer">Generated by:
|
|
||||||
<a href="http://common-lisp.net/project/cl-api">CL-API</a></div></body></html>
|
|
37
docs/config.md
Normal file
37
docs/config.md
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
## Where
|
||||||
|
|
||||||
|
Coleslaw needs a `.coleslawrc` file to operate properly. That file is usually located at
|
||||||
|
$HOME/.coleslawrc but may also be placed in the blog repo itself.
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
The only *required* information in the config is:
|
||||||
|
* `:author` => to be placed on post pages and in the copyright/CC-BY-SA notice
|
||||||
|
* `:deploy-dir` => for Coleslaw's generated HTML to go in
|
||||||
|
* `:domain` => to generate absolute links to the site content
|
||||||
|
* `:routing` => to determine the URL scheme of content on the site
|
||||||
|
* `:title` => to provide a site title
|
||||||
|
* `:theme` => to select one of the themes in "coleslaw/themes/"
|
||||||
|
|
||||||
|
It is usually recommend to start from the [example config][ex_config] and pare down from there.
|
||||||
|
|
||||||
|
[ex_config]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
||||||
|
|
||||||
|
## Extras
|
||||||
|
|
||||||
|
There are also many *optional* config parameters such as:
|
||||||
|
* `:charset` => to set HTML attributes for international characters, default: "UTF-8"
|
||||||
|
* `:feeds` => to generate RSS and Atom feeds for certain tagged content
|
||||||
|
* `:excerpt-sep` => to set the separator for excerpt in content, default: `<!--more-->`
|
||||||
|
* `:lang` => to set HTML attributes indicating the site language, default: "en"
|
||||||
|
* `:license` => to override the displayed content license, the default is CC-BY-SA
|
||||||
|
* `:page-ext` => to set the suffix of generated files, default: "html". "" for no extension
|
||||||
|
* `:plugins` => to configure and enable coleslaw's [various plugins][plugin-use]
|
||||||
|
* `:separator` => to set the separator for content metadata, default: ";;;;;"
|
||||||
|
* `:sitenav` => to provide relevant links and ease navigation
|
||||||
|
* `:staging-dir` => for Coleslaw to do intermediate work, default: "/tmp/coleslaw"
|
||||||
|
* `:title-fn` => to modify document slugs after they are generated, default: `'identity`
|
||||||
|
|
||||||
|
[plugin-use]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-use.md
|
26
docs/content-format.md
Normal file
26
docs/content-format.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
# The Content Format
|
||||||
|
|
||||||
|
Coleslaw expects content to have a file extension matching the class
|
||||||
|
of the content. (I.e. `.post` for blog posts, `.page` for static pages, etc.)
|
||||||
|
|
||||||
|
There should also be a metadata header on all files
|
||||||
|
starting and ending with the config-specified `:separator`, ";;;;;" by
|
||||||
|
default. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
;;;;;
|
||||||
|
title: foo
|
||||||
|
tags: bar, baz
|
||||||
|
date: yyyy-mm-dd hh:mm:ss
|
||||||
|
format: html (for raw html) or md (for markdown)
|
||||||
|
excerpt: Can also be extracted from content (see :excerpt-sep config param)
|
||||||
|
;;;;;
|
||||||
|
your post
|
||||||
|
```
|
||||||
|
|
||||||
|
Posts require the `title:` and `format:` fields.
|
||||||
|
Pages require the `title:` and `url:` fields.
|
||||||
|
|
||||||
|
To omit a field, simply do not have the line present, empty lines and
|
||||||
|
fields (e.g. "tags:" followed by whitespace) will be ignored.
|
13
docs/deploy.md
Normal file
13
docs/deploy.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Deploying on a standalone server
|
||||||
|
|
||||||
|
Coleslaw can deploy to a standalone server.
|
||||||
|
If you want this server installation, initialize a bare git repo and
|
||||||
|
set up the post-receive hook on that repo.
|
||||||
|
|
||||||
|
* First initialize a [git bare repo](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server) on the server.
|
||||||
|
* Copy [example post-receive hook][post_hook] to your blog's bare repo and set the executable bit (`chmod +x`).
|
||||||
|
|
||||||
|
* Point the web server at `:deploy-dir` attribute on the config file.
|
||||||
|
Or "deploy-dir/.curr" if the `versioned` plugin is enabled.
|
||||||
|
|
||||||
|
[post_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive
|
404
docs/hacking.md
Normal file
404
docs/hacking.md
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
## Coleslaw: A Hacker's Guide
|
||||||
|
|
||||||
|
Here we'll provide an overview of key concepts and technical decisions
|
||||||
|
in *coleslaw* and a few suggestions about future directions. Please
|
||||||
|
keep in mind that *coleslaw* was written on a lark when 3 friends had
|
||||||
|
the idea to each complete their half-dreamed wordpress replacement in
|
||||||
|
a week. Though it has evolved considerably since it's inception, like
|
||||||
|
any software some mess remains.
|
||||||
|
|
||||||
|
## Overall Structure
|
||||||
|
|
||||||
|
Conceptually, coleslaw processes a blog as follows:
|
||||||
|
|
||||||
|
1. Coleslaw loads the user's config, then reads the blog repo loading
|
||||||
|
any `.post` files or other content. CONTENT and INDEX objects are
|
||||||
|
created from those files.
|
||||||
|
|
||||||
|
2. The CONTENT and INDEX objects are then fed to the templating engine
|
||||||
|
to produce HTML files in a config-specified staging directory,
|
||||||
|
usually under `/tmp`.
|
||||||
|
|
||||||
|
3. A deploy method (possibly customized via plugins) is called with the
|
||||||
|
staging directory. It does whatever work is needed to make the
|
||||||
|
generated HTML files (and any static content) visible to the web.
|
||||||
|
|
||||||
|
## A Note on Performance
|
||||||
|
|
||||||
|
A recent test on my Core i5 touting Thinkpad generated my 430-post blog
|
||||||
|
in 2.7 seconds. This averages out to about 6-7 milliseconds per piece of
|
||||||
|
content. However, about 400 of those 430 items were HTML posts from a
|
||||||
|
wordpress export, not markdown posts that require parsing with 3bmd.
|
||||||
|
I expect that 3bmd would be the main bottleneck on a larger site. It
|
||||||
|
would be worthwhile to see how well [cl-markdown][clmd] performs as
|
||||||
|
a replacement if this becomes an issue for users though we would lose
|
||||||
|
source highlighting from [colorize][clrz] and should also investigate
|
||||||
|
[pygments][pyg] as a replacement. Using the new [incremental][incf] plugin
|
||||||
|
reduced runtime to 1.36 seconds, almost cutting it in half.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Plugins
|
||||||
|
|
||||||
|
**Coleslaw** strongly encourages extending functionality via plugins.
|
||||||
|
The Plugin API is well-documented and flexible enough for many use
|
||||||
|
cases. Do check the [API docs][api_docs] when contemplating a new
|
||||||
|
feature and see if a plugin would be appropriate.
|
||||||
|
|
||||||
|
### Templates and Theming
|
||||||
|
|
||||||
|
User configs are allowed to specify a theme. A theme consists of a
|
||||||
|
directory under "themes/" containing css, images, and at least
|
||||||
|
3 templates: Base, Index, and Post.
|
||||||
|
|
||||||
|
**Coleslaw** uses [cl-closure-template][closure_template]
|
||||||
|
exclusively for templating. **cl-closure-template** is a well
|
||||||
|
documented CL implementation of Google's Closure Templates. Each
|
||||||
|
template file should contain a namespace like
|
||||||
|
`coleslaw.theme.theme-name`.
|
||||||
|
|
||||||
|
Each template creates a lisp function in the theme's package when
|
||||||
|
loaded. These functions take a property list (or plist) as an argument
|
||||||
|
and return rendered HTML. **Coleslaw** defines a helper called
|
||||||
|
`theme-fn` for easy access to the template functions. Additionally,
|
||||||
|
there are RSS, ATOM, and sitemap templates *coleslaw* uses automatically.
|
||||||
|
No need for individual themes to reimplement a standard, after all!
|
||||||
|
|
||||||
|
Unfortunately, it is not very pleasant to debug broken templates.
|
||||||
|
Efforts to remedy this are being pursued for the next release.
|
||||||
|
Two particular issues to note are transposed Closure commands,
|
||||||
|
e.g. "${foo}" instead of "{$foo}", and trying to use nonexistent
|
||||||
|
keys or slots which fails silently instead of producing an error.
|
||||||
|
|
||||||
|
### The Lifecycle of a Page
|
||||||
|
|
||||||
|
- `(progn
|
||||||
|
(load-config "/my/blog/repo/path")
|
||||||
|
(compile-theme (theme *config*)))`
|
||||||
|
|
||||||
|
Coleslaw first needs the config loaded and theme compiled,
|
||||||
|
as neither the blog location, the theme to use, and other
|
||||||
|
crucial information are not yet known.
|
||||||
|
|
||||||
|
- `(load-content)`
|
||||||
|
|
||||||
|
A page starts, obviously, with a file. When *coleslaw* loads your
|
||||||
|
content, it iterates over a list of content types (i.e. subclasses of
|
||||||
|
CONTENT). For each content type, it iterates over all files in the
|
||||||
|
repo with a matching extension, e.g. ".post" for POSTs. Objects of the
|
||||||
|
appropriate class are created from each matching file and inserted
|
||||||
|
into the an in-memory data store. Then the INDEXes are created from
|
||||||
|
the loaded content and added to the data store.
|
||||||
|
|
||||||
|
- `(compile-blog dir)`
|
||||||
|
|
||||||
|
Compilation starts by ensuring the staging directory (`/tmp/coleslaw/`
|
||||||
|
by default) exists, cd'ing there, and copying over any necessary theme
|
||||||
|
assets. Then *coleslaw* iterates over all the content types and index
|
||||||
|
classes, rendering all of their instances and writing the HTML to disk.
|
||||||
|
After this, an 'index.html' symlink is created pointing to the first
|
||||||
|
reverse-chronological index.
|
||||||
|
|
||||||
|
- `(deploy dir)`
|
||||||
|
|
||||||
|
Finally, we move the staging directory to a path under the config's
|
||||||
|
`:deploy-dir`. If the versioned plugin is enabled, it is a timestamped
|
||||||
|
path and we delete the directory pointed to by the old '.prev' symlink,
|
||||||
|
point '.curr' at '.prev', and point '.curr' at our freshly built site.
|
||||||
|
|
||||||
|
### Blogs vs Sites
|
||||||
|
|
||||||
|
**Coleslaw** is blogware. When I designed it, I only cared that it
|
||||||
|
could replace my server's wordpress install. As a result, the code
|
||||||
|
until very recently was structured in terms of POSTs and
|
||||||
|
INDEXes. Roughly speaking, a POST is a blog entry and an INDEX is a
|
||||||
|
collection of POSTs or other content. An INDEX really only serves to
|
||||||
|
group a set of content objects on a page, it isn't content itself.
|
||||||
|
|
||||||
|
Content Types were added in 0.8 as a step towards making *coleslaw*
|
||||||
|
suitable for more use cases. Any subclass of CONTENT that implements
|
||||||
|
the *document protocol* counts as a content type. However, only POSTs
|
||||||
|
are currently included in the bundled INDEXes since there isn't yet a
|
||||||
|
formal relationship to determine which content types should be
|
||||||
|
included on which indexes. It is straightforward for users to implement
|
||||||
|
their own dedicated INDEX for new Content Types.
|
||||||
|
|
||||||
|
### The Document Protocol
|
||||||
|
|
||||||
|
The *document protocol* was born during a giant refactoring in 0.9.3.
|
||||||
|
Any object that will be rendered to HTML should adhere to the protocol.
|
||||||
|
Subclasses of CONTENT (content types) that implement the protocol will
|
||||||
|
be seamlessly picked up by *coleslaw* and included on the rendered site.
|
||||||
|
|
||||||
|
All current Content Types and Indexes implement the protocol faithfully.
|
||||||
|
It consists of 2 "class" methods, 2 instance methods, and an invariant.
|
||||||
|
|
||||||
|
There are also 5 helper functions provided that should prove useful in
|
||||||
|
implementing new content types.
|
||||||
|
|
||||||
|
|
||||||
|
**Class Methods**:
|
||||||
|
|
||||||
|
Class Methods don't *really* exist in Common Lisp, as methods are
|
||||||
|
defined on generic functions and not on the class itself. But since
|
||||||
|
it's useful to think about a Class as being responsible for its
|
||||||
|
instances in the case of a blog, we implement class methods by
|
||||||
|
eql-specializing on the class, e.g.
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(defmethod foo ((doc-type (eql (find-class 'bar))))
|
||||||
|
... )
|
||||||
|
```
|
||||||
|
|
||||||
|
- `discover`: Create instances for documents of the class and put them
|
||||||
|
in the in-memory database with `add-document`.
|
||||||
|
|
||||||
|
For CONTENT, this means checking the blog repo for any files with a
|
||||||
|
matching extension and loading them from disk. If your class is a
|
||||||
|
subclass of CONTENT, it inherits a pleasant default method for this.
|
||||||
|
|
||||||
|
For INDEXes, this means iterating over any relevant CONTENT in the
|
||||||
|
database, and creating INDEXes in the database that include that
|
||||||
|
content.
|
||||||
|
|
||||||
|
- `publish`: Iterate over all instances of the class, rendering each
|
||||||
|
one to HTML and writing it out to the staging directory on disk.
|
||||||
|
|
||||||
|
|
||||||
|
**Instance Methods**:
|
||||||
|
|
||||||
|
- `page-url`: Retrieve the relative path for the object on the site.
|
||||||
|
The implementation of `page-url` is not fully specified. For most
|
||||||
|
content types, we compute and store the path on the instance at
|
||||||
|
initialization time making `page-url` just a reader method.
|
||||||
|
|
||||||
|
- `render`: A method that calls the appropriate template with `theme-fn`,
|
||||||
|
passing it any needed arguments and returning rendered HTML.
|
||||||
|
|
||||||
|
**Invariants**:
|
||||||
|
|
||||||
|
- Any Content Types (subclasses of CONTENT) are expected to be stored in
|
||||||
|
the site's git repo with the lowercased class-name as a file extension,
|
||||||
|
i.e. (".post" for POST files).
|
||||||
|
|
||||||
|
**Protocol Helpers**:
|
||||||
|
|
||||||
|
- `add-document`: Add the document to *coleslaw*'s in-memory
|
||||||
|
database. It will error if the `page-url` of the document is not
|
||||||
|
unique. Such a hash collision represents content on the site being
|
||||||
|
shadowed/overwritten. This should be used in your `discover` method.
|
||||||
|
|
||||||
|
- `delete-document`: Remove a document from *coleslaw*'s in-memory
|
||||||
|
database. This is currently only used by the incremental compilation
|
||||||
|
plugin.
|
||||||
|
|
||||||
|
- `write-document`: Write the document out to disk as HTML. It takes
|
||||||
|
an optional template name and render-args to pass to the template.
|
||||||
|
This should be used in your `publish` method.
|
||||||
|
|
||||||
|
- `find-all`: Return a list of all documents of the requested class.
|
||||||
|
This is often used in the `publish` method to iterate over documents
|
||||||
|
of a given type.
|
||||||
|
|
||||||
|
- `purge-all`: Remove all instances of the requested class from the DB.
|
||||||
|
This is primarily used at the REPL or for debugging but it is also
|
||||||
|
used in a `:before` method on `discover` to keep it idempotent.
|
||||||
|
|
||||||
|
### Current Content Types & Indexes
|
||||||
|
|
||||||
|
There are 5 INDEX subclasses at present: TAG-INDEX, MONTH-INDEX,
|
||||||
|
NUMERIC-INDEX, FEED, and TAG-FEED. Respectively, they support
|
||||||
|
grouping content by tags, publishing date, and reverse chronological
|
||||||
|
order. Feeds exist to special case RSS and ATOM generation.
|
||||||
|
Currently, there is only 1 content type: POST, for blog entries.
|
||||||
|
PAGE, a content type for static page support, is available as a plugin.
|
||||||
|
|
||||||
|
## Areas for Improvement (i.e. The Roadmap)
|
||||||
|
|
||||||
|
### TODO for 0.9.7
|
||||||
|
|
||||||
|
* Test suite improvements:
|
||||||
|
* `load-content`/`read-content`/parsing
|
||||||
|
* Content Discovery
|
||||||
|
* Theme Compilation
|
||||||
|
* Content Publishing
|
||||||
|
* Common Plugins including Injections
|
||||||
|
* Add proper errors to read-content/load-content? Not just ignoring bad data. Line info, etc.
|
||||||
|
* Improved template debugging? "${" instead of "{$", static checks for valid slots, etc.
|
||||||
|
At least a serious investigation into how such things might be provided.
|
||||||
|
* Some minor scripting conveniences with cl-launch? (Scaffold a post/page, Enable incremental, Build, etc).
|
||||||
|
|
||||||
|
### Assorted Cleanups
|
||||||
|
|
||||||
|
* Try to get tag-index urls out of the tags. Post templates use them.
|
||||||
|
* Profile/memoize find-all calls in **INDEX** `render` method.
|
||||||
|
|
||||||
|
### Real Error Handling
|
||||||
|
|
||||||
|
One reason Coleslaw's code base is so small is probably the
|
||||||
|
omission of any serious error handling. Trying to debug
|
||||||
|
coleslaw if there's a problem during a build is unpleasant
|
||||||
|
at best, especially for anyone not coming from the lisp world.
|
||||||
|
|
||||||
|
We need to start handling errors and reporting errors in ways
|
||||||
|
that are useful to the user. Example errors users have encountered:
|
||||||
|
|
||||||
|
1. Loading of Content. If `read-content` fails to parse a file, we
|
||||||
|
should tell the user what file failed and why. We also should
|
||||||
|
probably enforce more constraints about metadata. E.g. Empty
|
||||||
|
metadata is not allowed/meaningful. Trailing space after separator, etc.
|
||||||
|
2. Trying to load content from the bare repo instead of the clone.
|
||||||
|
i.e. Specifying the `:repo` in .coleslawrc as the bare repo.
|
||||||
|
The README should clarify this point and the need for posts to be
|
||||||
|
".post" files.
|
||||||
|
3. Custom themes that try to access non-existent properties of content
|
||||||
|
do not currently error. They just wind up returning whitespace.
|
||||||
|
When the theme compiles, we should alert the user to any obvious
|
||||||
|
issues with it.
|
||||||
|
4. Dear Lord it was miserable even debugging a transposed character error
|
||||||
|
in one of the templates. "${foo}" instead of "{$foo}". But fuck supporting
|
||||||
|
multiple templating backends I have enough problems. What can we do?
|
||||||
|
|
||||||
|
### Scripting Conveniences
|
||||||
|
|
||||||
|
It would be convenient to add command-line tools/scripts to run coleslaw,
|
||||||
|
set up the db for incremental builds, scaffold a new post, etc. for new users.
|
||||||
|
Fukamachi's Shelly, Xach's buildapp or Fare's cl-launch would be useful here. frog and hakyll are
|
||||||
|
reasonable points of inspiration for commands to offer.
|
||||||
|
|
||||||
|
#### Commands
|
||||||
|
|
||||||
|
This is a initial set of commands which will be used to implement the first coleslaw cli, feel free to contribute!
|
||||||
|
Imagine a executable `coleslaw`, the commands would be invoked like this: `coleslaw <commandname> <args>`
|
||||||
|
|
||||||
|
* `build` generates the site. Takes:
|
||||||
|
* `--repo-dir`: first defaults to `~/.coleslawrc`'s `repo-dir` then to `.` and otherwise fails
|
||||||
|
* `clean` removes the files from `output-dir` and `staging-dir`. Takes:
|
||||||
|
* `--repo-dir`: See above
|
||||||
|
* `rebuild` is a shortcut for `clean` and then `build`. Takes:
|
||||||
|
* `--repo-dir`: See above
|
||||||
|
* `post` creates a new empty `.post` file. Takes:
|
||||||
|
* `--repo-dir`: See above
|
||||||
|
* `--title`: title (also used for generating file name)
|
||||||
|
* `--date`: same as the header-key. If not given, current time is used.
|
||||||
|
* `--format`: same as the header-key (optional, defaults to `md`)
|
||||||
|
* …
|
||||||
|
* `serve` starts a hunchentoot serving the blog locally
|
||||||
|
* maybe `page` which is `post` for static sites.
|
||||||
|
|
||||||
|
Ideas for later:
|
||||||
|
|
||||||
|
* Deployment. It would be nice to have but user needs vary greatly
|
||||||
|
and there are multiple deployment methods to support (heroku, s3,
|
||||||
|
gh-pages, git, etc). This will require some careful thought.
|
||||||
|
|
||||||
|
### Plugin Constraints
|
||||||
|
|
||||||
|
There is no system for determining what plugins work together or
|
||||||
|
enforcing the requirements or constraints of any particular
|
||||||
|
plugin. That is to say, the plugins are not actually modular. They are
|
||||||
|
closer to controlled monkey-patching.
|
||||||
|
|
||||||
|
While adding a [real module system to common lisp][asdf3] is probably
|
||||||
|
out of scope, we might be able to add some kind of [contract library][qpq]
|
||||||
|
for implementing this functionality. At the very least, a way to check
|
||||||
|
some assertions and error out at plugin load time if they fail should be
|
||||||
|
doable. I might not be able to [make illegal states unrepresentable][misu],
|
||||||
|
but I can sure as hell make them harder to construct than they are now.
|
||||||
|
|
||||||
|
@PuercoPop has suggested looking into how [wookie does plugins][wookie].
|
||||||
|
It's much more heavyweight but might be worth looking into. If we go that
|
||||||
|
route, the plugin support code will be almost half the coleslaw core.
|
||||||
|
Weigh the tradeoffs carefully.
|
||||||
|
|
||||||
|
### New Content Type: Shouts!
|
||||||
|
|
||||||
|
I've also toyed with the idea of a content type called a SHOUT, which
|
||||||
|
would be used primarily to reference or embed other content, sort of a
|
||||||
|
mix between a retweet and a del.icio.us bookmark. We encounter plenty
|
||||||
|
of great things on the web. Most of mine winds up forgotten in browser
|
||||||
|
tabs or stored on twitter's servers. It would be cool to see SHOUTs as
|
||||||
|
a plugin, probably with a dedicated SHOUT-INDEX, and some sort of
|
||||||
|
oEmbed/embed.ly/noembed support.
|
||||||
|
|
||||||
|
### Better Content Types
|
||||||
|
|
||||||
|
Creating a new content type is both straightforward and doable as a
|
||||||
|
plugin. All that is really required is a subclass of CONTENT with
|
||||||
|
any needed slots, a template, a `render` method to call the template
|
||||||
|
with any needed options, a `page-url` method for layout, and a
|
||||||
|
`publish` method.
|
||||||
|
|
||||||
|
Unfortunately, this does not solve:
|
||||||
|
|
||||||
|
1. The issue of compiling the template at load-time and making sure it
|
||||||
|
was installed in the theme package. The plugin would need to do
|
||||||
|
this itself or the template would need to be included in 'core'.
|
||||||
|
Thankfully, this should be easy with *cl-closure-template*.
|
||||||
|
2. More seriously, there is no formal relationship between content
|
||||||
|
types and indexes. Consequentially, INDEXes include only POST
|
||||||
|
objects at the moment. Whether the INDEX should specify what
|
||||||
|
Content Types it includes or the CONTENT which indexes it appears
|
||||||
|
on is not yet clear.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
The preferred workflow is more or less:
|
||||||
|
|
||||||
|
- fork and clone
|
||||||
|
- make a branch
|
||||||
|
- commit your changes
|
||||||
|
- push your branch to your github fork
|
||||||
|
- open a Pull Request
|
||||||
|
|
||||||
|
#### Fork and clone
|
||||||
|
|
||||||
|
You may clone the main github repository or your fork, whichever you cloned
|
||||||
|
will be known as origin in your git repository. You have to add the other git
|
||||||
|
repository to your remotes, so if you cloned from your fork execute:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add upstream git@github.com:redline6561/coleslaw.git
|
||||||
|
```
|
||||||
|
|
||||||
|
If you cloned from the main github repository execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add fork git@github.com:<YourUsername>/coleslaw.git
|
||||||
|
```
|
||||||
|
|
||||||
|
For the rest of the steps we will assume you cloned from your fork and that the main github repository has the remote name of upstream.
|
||||||
|
|
||||||
|
#### Make a branch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b <branch_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
It is important to work always on branch so one can track changes in upstream by simply executing ```git pull upstream master:master``` from the master branch. If one can't come up with a suitable branch name just name it patch-n.
|
||||||
|
2
|
||||||
|
#### Commit your changes
|
||||||
|
|
||||||
|
Make the changes you want to coleslaw, add the files with that changes (```git add <path/to/file>```) and commit them (```git commit```). Your commit message should strive to sum up what has changes and why.
|
||||||
|
|
||||||
|
#### Push your branch to your github fork
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin branch
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Open a Pull Request
|
||||||
|
|
||||||
|
After pushing the branch to your fork, on github you should see a button to open a pull request. In the PR message give the rationale for your changes.
|
||||||
|
|
||||||
|
[closure_template]: https://github.com/archimag/cl-closure-template
|
||||||
|
[api_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/plugin-api.md
|
||||||
|
[clmd]: https://github.com/gwkkwg/cl-markdown
|
||||||
|
[clrz]: https://github.com/redline6561/colorize
|
||||||
|
[pyg]: http://pygments.org/
|
||||||
|
[incf]: https://github.com/redline6561/coleslaw/blob/master/plugins/incremental.lisp
|
||||||
|
[asdf3]: https://github.com/fare/asdf3-2013
|
||||||
|
[qpq]: https://github.com/sellout/quid-pro-quo
|
||||||
|
[misu]: https://blogs.janestreet.com/effective-ml-revisited/
|
||||||
|
[wookie]: https://github.com/orthecreedence/wookie/blob/master/plugin.lisp#L181
|
55
docs/plugin-api.md
Normal file
55
docs/plugin-api.md
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# General Use
|
||||||
|
|
||||||
|
1. A lisp file should be created in coleslaw's ```plugins``` directory or the local `plugins` directory of the blog.
|
||||||
|
2. Any necessary lisp libraries not loaded by coleslaw should be included like so:
|
||||||
|
|
||||||
|
```(eval-when (:compile-toplevel :load-toplevel) (ql:quickload '(foo bar)))```
|
||||||
|
|
||||||
|
3. A package should be created for the plugin code like so:
|
||||||
|
|
||||||
|
```(defpackage :coleslaw-$NAME (:use :cl) (:export #:enable) ...)```
|
||||||
|
|
||||||
|
where $NAME is the pathname-name of the lisp file. (eg. `:coleslaw-disqus` for `disqus.lisp`)
|
||||||
|
4. An enable function should be present even if it's a no-op. Any work to enable the plugin is done there.
|
||||||
|
|
||||||
|
|
||||||
|
# Extension Points
|
||||||
|
|
||||||
|
* **New functionality via JS**, for example the Disqus and Mathjax plugins.
|
||||||
|
In this case, the plugin's `enable` function should call
|
||||||
|
[`add-injection`](http://redlinernotes.com/docs/coleslaw.html#add-injection_func)
|
||||||
|
with an injection and a keyword. The injection is a function that takes a
|
||||||
|
*Document* and returns a string to insert in the page or nil.
|
||||||
|
The keyword specifies whether the injected text goes in the HEAD or BODY element. The
|
||||||
|
[Disqus plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/disqus.lisp)
|
||||||
|
is a good example of this.
|
||||||
|
|
||||||
|
* **New markup formats**, for example the
|
||||||
|
[ReStructuredText plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/rst.lisp),
|
||||||
|
can be created by definining an appropriate `render-text`
|
||||||
|
method. The method takes `text` and `format` arguments and is
|
||||||
|
[EQL-specialized](http://www.gigamonkeys.com/book/object-reorientation-generic-functions.html#defmethod)
|
||||||
|
on the format. Format should be a keyword matching the file
|
||||||
|
extension (or `pathname-type`) of the markup format.
|
||||||
|
(eg. `:rst` for ReStructuredText)
|
||||||
|
|
||||||
|
* **New hosting options**, for example the
|
||||||
|
[Amazon S3 plugin](http://github.com/redline6561/coleslaw/blob/master/plugins/s3.lisp),
|
||||||
|
can be created by definining a `deploy :after` method. The method
|
||||||
|
takes a staging directory, likely uninteresting in the `:after`
|
||||||
|
stage. But by importing `*config*` from the coleslaw package and
|
||||||
|
getting its deploy location with `(deploy-dir *config*)` a number of
|
||||||
|
interesting options become possible.
|
||||||
|
|
||||||
|
* **New content types**, for example the
|
||||||
|
[static page content type](http://github.com/redline6561/coleslaw/blob/master/plugins/static-pages.lisp),
|
||||||
|
can be created by definining a subclass of CONTENT along with a
|
||||||
|
template, and `render`, `page-url`, and `publish` methods.
|
||||||
|
The PAGE content type cheats a bit by reusing the existing POST template.
|
||||||
|
|
||||||
|
* **New service integrations**, for example crossposting to
|
||||||
|
twitter/facebook/tumblr/livejournal/etc, is also possible by
|
||||||
|
adding an :after hook to the deploy method. The hook can iterate
|
||||||
|
over the results of the `get-updated-files` to crosspost any new content.
|
||||||
|
The [Twitter plugin](https://github.com/redline6561/coleslaw/blob/master/plugins/twitter.lisp)
|
||||||
|
is a good example of this.
|
283
docs/plugin-use.md
Normal file
283
docs/plugin-use.md
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
# General Use
|
||||||
|
|
||||||
|
* To enable a plugin, add its name and settings to your
|
||||||
|
[.coleslawrc][config_file]. Plugin settings are described
|
||||||
|
below. Note that some plugins require additional setup.
|
||||||
|
|
||||||
|
* Available plugins are listed below with usage descriptions and
|
||||||
|
config examples.
|
||||||
|
|
||||||
|
## Direct deployment via rsync
|
||||||
|
|
||||||
|
**Description**: This directly sends the contents of the staging dir to the deployed directory.
|
||||||
|
The former default deployment method.
|
||||||
|
|
||||||
|
**Example**: `(rsync "--exclude" ".git/" "--exclude" ".gitignore" "--copy-links")`
|
||||||
|
|
||||||
|
## Analytics via Google
|
||||||
|
|
||||||
|
**Description**: Provides traffic analysis through
|
||||||
|
[Google Analytics](http://www.google.com/analytics/).
|
||||||
|
|
||||||
|
**Example**: `(gtag :tracking-code "google-provided-unique-id")`
|
||||||
|
|
||||||
|
**Note**: You can use `(analytics :tracking-code "google-provided-unique-id")` for the legacy integration with Google Analytics.
|
||||||
|
|
||||||
|
## Analytics via Piwik
|
||||||
|
|
||||||
|
**Description**: Provides traffic analysis through
|
||||||
|
[Matomo](https://www.matomo.org).
|
||||||
|
|
||||||
|
**Example**: `(matomo :matomo-url "matomo.example.com" :matomo-site "example-site")`
|
||||||
|
|
||||||
|
## CL-WHO
|
||||||
|
|
||||||
|
**Description**: Allows the user to write posts cl-who markup. Just create a
|
||||||
|
post with `format: cl-who` and the plugin will do the rest.
|
||||||
|
|
||||||
|
**Example**: (cl-who)
|
||||||
|
|
||||||
|
## Comments via Disqus
|
||||||
|
|
||||||
|
**Description**: Provides comment support through
|
||||||
|
[Disqus](http://www.disqus.com/).
|
||||||
|
|
||||||
|
**Example**: `(disqus :shortname "disqus-provided-unique-id")`
|
||||||
|
|
||||||
|
## Comments via isso
|
||||||
|
|
||||||
|
**Description**: Provides comment support through
|
||||||
|
[isso](https://posativ.org/isso/).
|
||||||
|
|
||||||
|
**Example**: `(isso :isso-url "your-isso-url")`
|
||||||
|
|
||||||
|
## HTML5 Gifs via Gfycat
|
||||||
|
|
||||||
|
**Description**: Provides support for embedding [gfycat](http://gfycat.com/) gifs.
|
||||||
|
Any content tagged 'gfycat' containing an IMG element of the form
|
||||||
|
`<img class="gfyitem" data-id="your-gfy-slug" />` will embed the
|
||||||
|
corresponding gfy.
|
||||||
|
|
||||||
|
**Example**: `(gfycat)`
|
||||||
|
|
||||||
|
## Deploying / Hosting via Github Pages
|
||||||
|
|
||||||
|
**Description**:
|
||||||
|
|
||||||
|
Coleslaw deploys the blog to the specified branch of the given url.
|
||||||
|
* `url` -- a string, git repository url that you already have a push access.
|
||||||
|
* `branch` -- a string, the branch to publish, either `"gh-pages"` or `"master"` can be used.
|
||||||
|
* `remote` -- a string, the remote name that we use in the deploy directory. defaulted to `"origin"`.
|
||||||
|
* `cname` -- a string denoting the custom domain name, or `t`. If `cname` is `t`, the value is inferred
|
||||||
|
from the domain name specified in the `.coleslawrc`.
|
||||||
|
The value is written into `CNAME` file in the repository root.
|
||||||
|
For details, see [github-pages](http://pages.github.com/).
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
|
||||||
|
``` lisp
|
||||||
|
(gh-pages :url "git@github.com:myaccount/myrepo.git"
|
||||||
|
:branch "gh-pages"
|
||||||
|
:remote "origin"
|
||||||
|
:cname t)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Incremental Builds
|
||||||
|
|
||||||
|
**Description**: Primarily a performance enhancement. Caches the
|
||||||
|
content database between builds with
|
||||||
|
[cl-store][http://common-lisp.net/project/cl-store/] to avoid
|
||||||
|
parsing the whole git repo every time. May become default
|
||||||
|
functionality instead of a plugin at some point. Substantially
|
||||||
|
reduces runtime for medium to large sites.
|
||||||
|
|
||||||
|
**Example**: `(incremental)`
|
||||||
|
|
||||||
|
**Setup**: You must run the `examples/dump_db.sh` script to
|
||||||
|
generate a database dump for your site before enabling the
|
||||||
|
incremental plugin.
|
||||||
|
|
||||||
|
## LaTeX via Mathjax
|
||||||
|
|
||||||
|
**Description**: Provides LaTeX support through
|
||||||
|
[Mathjax](http://www.mathjax.org/) for posts tagged with "math" and
|
||||||
|
indexes containing such posts. Any text enclosed in $$ will be
|
||||||
|
rendered, for example, ```$$ \lambda \scriptstyle{f}. (\lambda
|
||||||
|
x. (\scriptstyle{f} (x x)) \lambda x. (\scriptstyle{f} (x x)))
|
||||||
|
$$```.
|
||||||
|
|
||||||
|
**Example**: ```(mathjax)```
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
|
||||||
|
- `:force`, when non-nil, will force the inclusion of MathJax on all
|
||||||
|
posts. Default value is `nil`.
|
||||||
|
|
||||||
|
- `:location` specifies the location of the `MathJax.js` file. The
|
||||||
|
default value is `"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js"`.
|
||||||
|
This is useful if you have a local copy of MathJax and want to use that
|
||||||
|
version.
|
||||||
|
|
||||||
|
- `:preset` allows the specification of the config parameter of
|
||||||
|
`MathJax.js`. The default value is `"TeX-AMS-MML_HTMLorMML"`.
|
||||||
|
|
||||||
|
- `:config` is used as supplementary inline configuration to the
|
||||||
|
`MathJax.Hub.Config ({ ... });`. It is unused by default.
|
||||||
|
|
||||||
|
## Markless
|
||||||
|
|
||||||
|
**Description**: [Markless](https://shirakumo.github.io/markless) is a
|
||||||
|
new document markup standard. To use it in your posts, create the
|
||||||
|
posts with `format: markless`. The output is generated using
|
||||||
|
[cl-markless-plump](https://shirakumo.github.io/cl-markless/cl-markless-plump/),
|
||||||
|
meaning any syntax extensions that work with it should also be
|
||||||
|
available in Coleslaw.
|
||||||
|
|
||||||
|
**Example**: `(mess)`
|
||||||
|
|
||||||
|
## ReStructuredText
|
||||||
|
|
||||||
|
**Description**: Some people really like
|
||||||
|
[ReStructuredText](http://docutils.sourceforge.net/rst.html). Who
|
||||||
|
knows why? But it only took one method to add, so yeah! Just create
|
||||||
|
a post with `format: rst` and the plugin will do the rest.
|
||||||
|
|
||||||
|
**Example**: `(rst)`
|
||||||
|
|
||||||
|
## S3 Hosting
|
||||||
|
|
||||||
|
**Description**: Allows hosting your blog entirely via
|
||||||
|
[Amazon S3](http://aws.amazon.com/s3/). It is suggested you closely
|
||||||
|
follow the relevant
|
||||||
|
[AWS guide](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html)
|
||||||
|
to get the DNS setup correctly. Your `:auth-file` should match that
|
||||||
|
described in the
|
||||||
|
[ZS3 docs](http://www.xach.com/lisp/zs3/#file-credentials).
|
||||||
|
|
||||||
|
**Example**: `(s3 :auth-file "/home/redline/.aws_creds" :bucket
|
||||||
|
"blog.redlinernotes.com")`
|
||||||
|
|
||||||
|
## Sitemap generator
|
||||||
|
|
||||||
|
**Description**: This plugin generates a sitemap.xml under the page
|
||||||
|
root, which is useful if you want google to crawl your site.
|
||||||
|
|
||||||
|
**Example**: `(sitemap)`
|
||||||
|
|
||||||
|
## Static Pages
|
||||||
|
|
||||||
|
**Description**: This plugin allows you to add `.page` files to your
|
||||||
|
repo, that will be rendered to static pages at a designated URL.
|
||||||
|
|
||||||
|
**Example**: `(static-pages)`
|
||||||
|
|
||||||
|
## Twitter
|
||||||
|
|
||||||
|
**Description**: This plugin tweets every time a new post is added to
|
||||||
|
your repo. See Setup for an example of how to get your access token
|
||||||
|
& secret.
|
||||||
|
|
||||||
|
**Example**: `(twitter :api-key "<api-key>"
|
||||||
|
:api-secret "<api-secret>"
|
||||||
|
:access-token "<access-token>"
|
||||||
|
:access-secret "<access-secret>")`
|
||||||
|
|
||||||
|
**Setup**:
|
||||||
|
- Create a new [twitter app](https://apps.twitter.com/). Take note of the api key & secret.
|
||||||
|
|
||||||
|
- In the repl do the following:
|
||||||
|
```lisp
|
||||||
|
;; Load Chirp
|
||||||
|
(ql:quickload :chirp)
|
||||||
|
|
||||||
|
;; Use the api key & secret to get a URL where a pin code will be handled to you.
|
||||||
|
(chirp:initiate-authentication
|
||||||
|
:api-key "D1pMCK17gI10bQ6orBPS0w"
|
||||||
|
:api-secret "BfkvKNRRMoBPkEtDYAAOPW4s2G9U8Z7u3KAf0dBUA")
|
||||||
|
;; => "https://api.twitter.com/oauth/authorize?oauth_token=cJIw9MJM5HEtQqZKahkj1cPn3m3kMb0BYEp6qhaRxfk"
|
||||||
|
|
||||||
|
;; Exchange the pin code for an access token and and access secret. Take note
|
||||||
|
;; of them.
|
||||||
|
CL-USER> (chirp:complete-authentication "4173325")
|
||||||
|
;; => "18403733-bXtuum6qbab1O23ltUcwIk2w9NS3RusUFiuum4D3w"
|
||||||
|
;; "zDFsFSaLerRz9PEXqhfB0h0FNfUIDgbEe59NIHpRWQbWk"
|
||||||
|
|
||||||
|
;; Finally verify the credentials
|
||||||
|
(chirp:account/verify-credentials)
|
||||||
|
#<CHIRP-OBJECTS:USER PuercoPop #18405433>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Twitter Summary Cards
|
||||||
|
|
||||||
|
**Description**: Add Summary Card metadata to blog posts
|
||||||
|
to enhance twitter links to that content.
|
||||||
|
|
||||||
|
**Example**: `(twitter-summary-card :twitter-handle "@redline6561")
|
||||||
|
|
||||||
|
## Versioning Deploys
|
||||||
|
|
||||||
|
Either [automatic git interaction](#git-versioned) or [double
|
||||||
|
versioning](#double-versioning)
|
||||||
|
|
||||||
|
### Git Versioned
|
||||||
|
|
||||||
|
**Description**: Automatically stages, commits, and/or pushes the server's
|
||||||
|
sources. Assumes that a git repository exists in the server's directory. Pushing
|
||||||
|
is optional.
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
`(git-versioned "~/src/dir/" 'stage 'commit 'push)`
|
||||||
|
|
||||||
|
|
||||||
|
`(git-versioned "~/src/dir/" 'stage 'commit)`
|
||||||
|
|
||||||
|
### Double Versioning
|
||||||
|
|
||||||
|
**Description**: Originally, this was Coleslaw's only deploy behavior.
|
||||||
|
Instead of deploying directly to `:deploy-dir`, creates `.curr` and
|
||||||
|
`.prev` symlinks in the *deploy-dir*, which point to timestamped
|
||||||
|
directories of the last two deploys of the site. Deploys prior to the
|
||||||
|
last two are automatically cleaned up.
|
||||||
|
|
||||||
|
**Example**: `(versioned)`
|
||||||
|
|
||||||
|
## Wordpress Importer
|
||||||
|
|
||||||
|
**NOTE**: This plugin really should be rewritten to act as a
|
||||||
|
standalone script. It is designed for one time use and using it
|
||||||
|
through a site config is pretty silly.
|
||||||
|
|
||||||
|
**Description**: Import blog posts from Wordpress using their export
|
||||||
|
tool. Blog entries will be read from the XML and converted into
|
||||||
|
.post files. Afterwards the XML file will be deleted to prevent
|
||||||
|
reimporting. Optionally an `:output` argument may be supplied to the
|
||||||
|
plugin. If provided, it should be a directory in which to store the
|
||||||
|
.post files. Otherwise, the value of `:repo` in your .coleslawrc
|
||||||
|
will be used.
|
||||||
|
|
||||||
|
**Example**: `(import :filepath "/home/redline/redlinernotes-export.timestamp.xml"
|
||||||
|
:output "/home/redlinernotes/blog/")`
|
||||||
|
|
||||||
|
[config_file]: http://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc
|
||||||
|
|
||||||
|
|
||||||
|
## Markdown Embeding youtube Youtube
|
||||||
|
|
||||||
|
**Description**: Embed youtube videos in markdown using the shorthand syntax
|
||||||
|
`!yt[<video-id>(|options*)*]`. Options can be *width*, *height* or any of the
|
||||||
|
[player parameters](https://developers.google.com/youtube/player_parameters).
|
||||||
|
|
||||||
|
For example `!yt[oeul8fTG9dM|width=480,allowfullscreen]`.
|
||||||
|
|
||||||
|
**Example**: `(3bmd-youtube)`
|
||||||
|
|
||||||
|
## Code Highlighting via Pygments
|
||||||
|
|
||||||
|
**Description**: Provides code highlighting with [Pygments](http://pygments.org/)
|
||||||
|
instead of [colorize](http://www.cliki.net/colorize). Pygments supports over
|
||||||
|
300 languages and text formats. Look at
|
||||||
|
[3bmd](https://github.com/3b/3bmd/blob/master/README.md) for more info.
|
||||||
|
|
||||||
|
**Example**: `(pygments)`
|
||||||
|
|
||||||
|
**Setup**: Install `Pygments` and verify that the `pygmentize` command works (`pygmentize -V` should print the version number). You also need to verify that your theme includes an appropriate css file.
|
219
docs/themes.md
Normal file
219
docs/themes.md
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
# Themes
|
||||||
|
|
||||||
|
The theming support in coleslaw is very flexible and relatively easy
|
||||||
|
to use. However it does require some knowledge of HTML, CSS, and how
|
||||||
|
coleslaw processes content.
|
||||||
|
|
||||||
|
To understand how coleslaw works, a look at the [hacking][hck]
|
||||||
|
documentation will prove useful. This document focuses mainly on the
|
||||||
|
template engine and how you can influence the resulting HTML.
|
||||||
|
|
||||||
|
## High-Level Overview
|
||||||
|
|
||||||
|
Themes are written using [Closure Templates][clt]. Those templates are
|
||||||
|
then compiled into functions that Lisp calls with the blog data to get
|
||||||
|
HTML. Since the Lisp code to use theme functions is already written,
|
||||||
|
your theme must follow a few rules.
|
||||||
|
|
||||||
|
Every theme **must** be in a folder under "themes/" named after the
|
||||||
|
theme. The theme's templates must start with a namespace declaration
|
||||||
|
like so: `{namespace coleslaw.theme.$MY-THEME-NAME}`.
|
||||||
|
|
||||||
|
A theme must have three templates which take *specific arguments*
|
||||||
|
(to be described later).
|
||||||
|
1. Base
|
||||||
|
2. Post
|
||||||
|
3. Index
|
||||||
|
|
||||||
|
## Two types of pages
|
||||||
|
|
||||||
|
Coleslaw generates two types of pages: `index` pages and `post` pages.
|
||||||
|
Every page other than those in the `posts/` directory is an `index`.
|
||||||
|
|
||||||
|
**Every** page uses the `base.tmpl` and fills in the content using
|
||||||
|
either the `post` or `index` templates. No important logic should be
|
||||||
|
in *any* template, they are only used to provide a consistent layout.
|
||||||
|
|
||||||
|
* `base.tmpl` This template generates the outer shell of the HTML.
|
||||||
|
It keeps a consistent look and feel for all pages in the blog. The
|
||||||
|
actual content (i.e., not header/footer/css) comes from other templates.
|
||||||
|
|
||||||
|
* `index.tmpl` This template generates the content of the `index` pages.
|
||||||
|
That is, any page with more than one content object, e.g. the homepage.
|
||||||
|
|
||||||
|
* `post.tmpl` This templates generates content for the individual posts.
|
||||||
|
|
||||||
|
Here's a visual example to make things clearer:
|
||||||
|
```
|
||||||
|
INDEX HTML FILES INDIVIDUAL POST HTML FILES
|
||||||
|
|-------------------------| |-------------------------|
|
||||||
|
| base.tmpl | | base.tmpl |
|
||||||
|
| | | |
|
||||||
|
| |-------------------| | | |------------------| |
|
||||||
|
| | index.tmpl | | | | post.tmpl | |
|
||||||
|
| | | | | | | |
|
||||||
|
| |-------------------| | | |------------------| |
|
||||||
|
| | | |
|
||||||
|
|-------------------------| |-------------------------|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note on Style Sheets (css)
|
||||||
|
|
||||||
|
If you only want to change the way the blog is styled, it is probably
|
||||||
|
simplest to either modify the existing default theme, `hyde`, or copy
|
||||||
|
it in entirety and then tweak only the CSS of your new theme. A large
|
||||||
|
amount of visual difference can be had with a minimum of (or no)
|
||||||
|
template hacking. There is plenty of advice on CSS styling on the web.
|
||||||
|
I'm no expert but feel free to send pull requests modifying a theme's
|
||||||
|
CSS or improving this section, perhaps by recommending a CSS resource.
|
||||||
|
|
||||||
|
## Creating a Theme from Scratch (with code)
|
||||||
|
|
||||||
|
### Step 1. Create the directory.
|
||||||
|
|
||||||
|
A theme name must be a valid lisp symbol. For this example, we'll use
|
||||||
|
`trivial`, so create a `themes/trivial` directory in the *coleslaw* repo.
|
||||||
|
|
||||||
|
### Step 2. Create the templates.
|
||||||
|
|
||||||
|
As described above, we need 3 template files `base.tmpl`, `post.tmpl`
|
||||||
|
and `index.tmpl`. Initially, let's just create the simplest theme that
|
||||||
|
compiles correctly.
|
||||||
|
|
||||||
|
base.tmpl:
|
||||||
|
```
|
||||||
|
{namespace coleslaw.theme.trivial}
|
||||||
|
{template base}
|
||||||
|
{/template}
|
||||||
|
```
|
||||||
|
post.tmpl:
|
||||||
|
```
|
||||||
|
{namespace coleslaw.theme.trivial}
|
||||||
|
{template post}
|
||||||
|
{/template}
|
||||||
|
```
|
||||||
|
index.tmpl:
|
||||||
|
```
|
||||||
|
{namespace coleslaw.theme.trivial}
|
||||||
|
{template index}
|
||||||
|
{/template}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create three template functions that coleslaw can find, named
|
||||||
|
`base`, `post`, and `index`.
|
||||||
|
|
||||||
|
### Step 3. Use it in your config.
|
||||||
|
|
||||||
|
At this point, you can change the `:theme` in your `.coleslawrc` to
|
||||||
|
`trivial` and then generate your blog with `(coleslaw:main)`. However,
|
||||||
|
all the HTML files will be empty because our templates are empty!
|
||||||
|
|
||||||
|
### Intermezzo I, The Templating Language
|
||||||
|
|
||||||
|
The templating language is documented [elsewhere][clt].
|
||||||
|
However as a short primer:
|
||||||
|
|
||||||
|
* Everything is output literally, except template commands.
|
||||||
|
* Template commands are enclosed in `{` and `}`.
|
||||||
|
* Variables, which are provided by coleslaw, can be referenced
|
||||||
|
inside a template command. So to use a variable you have to say
|
||||||
|
`{$variable}` or `{$variable.key}`.
|
||||||
|
**WARNING**: At present, cl-closure-template does not have great debugging.
|
||||||
|
If you typo this, e.g. `${variable}`, you will receive an *uninformative*
|
||||||
|
and apparently unrelated error. Also, attempted access of non-existent keys
|
||||||
|
fails silently. We are exploring options for making debugging easier in a
|
||||||
|
future release.
|
||||||
|
* If statements are written as `{if ...} ... {else} ... {/if}`.
|
||||||
|
Typical examples are: `{if $injections.body} ... {/if}` or
|
||||||
|
`{if not isLast($link)} ... {/if}`.
|
||||||
|
* Loops can be written as `{foreach $var in $sequence} ... {/foreach}`.
|
||||||
|
|
||||||
|
### Intermezzo II, Variables provided by Coleslaw
|
||||||
|
|
||||||
|
The variable that should be available to all templates is:
|
||||||
|
- **config** This contains the `.coleslawrc` content.
|
||||||
|
|
||||||
|
#### Base Template Variables
|
||||||
|
|
||||||
|
- **raw** HTML generated by a sub template, `index` or `post`.
|
||||||
|
- **content** The object which was used to generate **raw**.
|
||||||
|
- **pubdate** A string containing the publication date.
|
||||||
|
- **injections** A list containing the injections. Injections are used
|
||||||
|
by plugins mostly to add Javascript to the page.
|
||||||
|
|
||||||
|
#### Index Template Variables
|
||||||
|
|
||||||
|
- **tags** A list containing all the tags, each with keys
|
||||||
|
`name` and `url`.
|
||||||
|
- **months** A list of all the content months, each with keys
|
||||||
|
`name` and `url`.
|
||||||
|
- **index** This is the meat of the content. This variable has
|
||||||
|
the following keys:
|
||||||
|
- `content`, a list of content (see below)
|
||||||
|
- `name`, a name to use in links or href tags
|
||||||
|
- `title`, a title to use in H1 or header tags
|
||||||
|
- **prev** Nil or the previous index with keys: `url` and `title`.
|
||||||
|
- **next** Nil or the next index with keys: `url` and `title`.
|
||||||
|
|
||||||
|
#### Post Template Variable
|
||||||
|
|
||||||
|
- **prev**
|
||||||
|
- **next**
|
||||||
|
- **post** All these variables are post objects. **prev** and
|
||||||
|
**next** are the adjacent posts when put in
|
||||||
|
chronological order. Each post has the following keys:
|
||||||
|
- `url`, the relative url of the post
|
||||||
|
- `tags`, a list of tags (each with keys `name` and `url`)
|
||||||
|
- `date`, the date of posting
|
||||||
|
- `text`, the HTML of the post's body
|
||||||
|
- `title`, the title of the post
|
||||||
|
- `excerpt`, the excerpt of the post, same as `text` by default
|
||||||
|
|
||||||
|
### Step 4. Include the content
|
||||||
|
|
||||||
|
*NOTE*: We can keep the template engine from escaping raw HTML by
|
||||||
|
adding a `|noAutoescape` clause to commands, like so: `{$raw |noAutoescape}`.
|
||||||
|
|
||||||
|
Let's now rewrite `base.tmpl` like this:
|
||||||
|
```
|
||||||
|
{namespace coleslaw.theme.trivial}
|
||||||
|
{template base}
|
||||||
|
<html>
|
||||||
|
<head><title>Trivial Theme For Coleslaw</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>All my pages have this title</h1>
|
||||||
|
{$raw |noAutoescape}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{/template}
|
||||||
|
```
|
||||||
|
|
||||||
|
A simple `index.tmpl` looks like this:
|
||||||
|
```
|
||||||
|
{namespace coleslaw.theme.trivial}
|
||||||
|
{template index}
|
||||||
|
{foreach $obj in $index.content}
|
||||||
|
<h1>{$object.title}</h1>
|
||||||
|
{$object.excerpt |noAutoescape}
|
||||||
|
{/foreach}
|
||||||
|
{/template}
|
||||||
|
```
|
||||||
|
|
||||||
|
And a simple `post.tmpl` is similarly:
|
||||||
|
```
|
||||||
|
{namespace coleslaw.theme.trivial}
|
||||||
|
{template post}
|
||||||
|
<h1>{$post.title}</h1>
|
||||||
|
{$post.text |noAutoescape}
|
||||||
|
{/template}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
All of the files are now populated with content. There are still no links
|
||||||
|
between the pages so navigation is cumbersome but adding links is simple.
|
||||||
|
Just do: `<a href="{$config.domain}/{$object.url}">{$object.name}</a>`.
|
||||||
|
|
||||||
|
[clt]: https://developers.google.com/closure/templates/
|
||||||
|
[ovr]: https://github.com/redline6561/coleslaw/blob/master/docs/overview.md
|
||||||
|
[hck]: https://github.com/redline6561/coleslaw/blob/master/docs/hacking.md
|
|
@ -1,13 +0,0 @@
|
||||||
(:author "Brit Butler"
|
|
||||||
:deploy "/home/git/blog/"
|
|
||||||
:domain "http://blog.redlinernotes.com"
|
|
||||||
:feeds ("lisp")
|
|
||||||
:plugins (mathjax)
|
|
||||||
:repo "/home/git/tmp/improvedmeans/"
|
|
||||||
:sitenav ((:url "http://redlinernotes.com/" :name "Home")
|
|
||||||
(:url "http://twitter.com/redline6561" :name "Twitter")
|
|
||||||
(:url "http://github.com/redline6561" :name "Code")
|
|
||||||
(:url "http://soundcloud.com/redlinernotes" :name "Music")
|
|
||||||
(:url "http://redlinernotes.com/docs/talks/" :name "Talks"))
|
|
||||||
:title "Improved Means for Achieving Deteriorated Ends"
|
|
||||||
:theme "hyde")
|
|
|
@ -1,10 +0,0 @@
|
||||||
GIT_REPO=$HOME/improvedmeans.git
|
|
||||||
# TMP_GIT_CLONE _must_ match the :repo arg in coleslawrc excluding trailing slash
|
|
||||||
TMP_GIT_CLONE=$HOME/tmp/improvedmeans
|
|
||||||
LISP=sbcl
|
|
||||||
|
|
||||||
git clone $GIT_REPO $TMP_GIT_CLONE
|
|
||||||
# Only ccl and sbcl support the eval switch, other lisps require a patch here
|
|
||||||
$LISP --eval "(ql:quickload 'coleslaw)" --eval "(coleslaw:main)" --eval "(trivial-shell:exit)"
|
|
||||||
rm -Rf $TMP_GIT_CLONE
|
|
||||||
exit
|
|
17
examples/dump-db.lisp
Normal file
17
examples/dump-db.lisp
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
|
(ql:quickload '(coleslaw cl-store)))
|
||||||
|
|
||||||
|
(in-package :coleslaw)
|
||||||
|
|
||||||
|
(defun main ()
|
||||||
|
(let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db")))
|
||||||
|
(format t "~%~%Coleslaw loaded. Attempting to load config file.~%")
|
||||||
|
(load-config "")
|
||||||
|
(format t "~%Config loaded. Attempting to load blog content.~%")
|
||||||
|
(load-content)
|
||||||
|
(format t "~%Content loaded. Attempting to dump content database.~%")
|
||||||
|
(cl-store:store *site* db-file)
|
||||||
|
(format t "~%Content database saved to ~s!~%~%" (namestring db-file))))
|
||||||
|
|
||||||
|
(main)
|
||||||
|
(exit)
|
18
examples/dump_db.sh
Executable file
18
examples/dump_db.sh
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
LISP=sbcl
|
||||||
|
|
||||||
|
### DON'T EDIT BELOW THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING. ###
|
||||||
|
DUMP="dump-db.lisp"
|
||||||
|
if [ "$LISP"="cmucl" ] || [ "$LISP"="lispworks" ] || [ "$LISP"="gcl" ] || [ "$LISP"="abcl" ];
|
||||||
|
then $LISP -load $DUMP
|
||||||
|
else
|
||||||
|
if [ "$LISP"="clisp" ];
|
||||||
|
then $LISP -i $DUMP
|
||||||
|
else
|
||||||
|
if [ "$LISP"="allegro" ];
|
||||||
|
then $LISP -l $DUMP
|
||||||
|
else $LISP --load $DUMP #SBCL CCL ECL
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
29
examples/example.coleslawrc
Normal file
29
examples/example.coleslawrc
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
(:author "Brit Butler"
|
||||||
|
:deploy-dir "/home/git/blog/"
|
||||||
|
:domain "http://blog.redlinernotes.com"
|
||||||
|
:excerpt-sep "<!--more-->"
|
||||||
|
:feeds ("lisp")
|
||||||
|
:plugins ((analytics :tracking-code "foo")
|
||||||
|
(disqus :shortname "my-site-name")
|
||||||
|
; (incremental) ;; *Remove comment to enable incremental builds.
|
||||||
|
(mathjax)
|
||||||
|
(sitemap)
|
||||||
|
(static-pages)
|
||||||
|
; (versioned) ;; *Remove comment to enable symlinked, timestamped deploys.
|
||||||
|
)
|
||||||
|
:routing ((:post "posts/~a")
|
||||||
|
(:tag-index "tag/~a")
|
||||||
|
(:month-index "date/~a")
|
||||||
|
(:numeric-index "~d")
|
||||||
|
(:feed "~a.xml")
|
||||||
|
(:tag-feed "tag/~a.xml"))
|
||||||
|
:sitenav ((:url "http://redlinernotes.com/" :name "Home")
|
||||||
|
(:url "http://twitter.com/redline6561" :name "Twitter")
|
||||||
|
(:url "http://github.com/redline6561" :name "Code")
|
||||||
|
(:url "http://soundcloud.com/redlinernotes" :name "Music")
|
||||||
|
(:url "http://redlinernotes.com/docs/talks/" :name "Talks"))
|
||||||
|
:staging-dir "/tmp/coleslaw/"
|
||||||
|
:title "Improved Means for Achieving Deteriorated Ends"
|
||||||
|
:theme "hyde")
|
||||||
|
|
||||||
|
;; * Prerequisites described in plugin docs.
|
41
examples/example.post-receive
Normal file
41
examples/example.post-receive
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
########## CONFIGURATION VALUES ##########
|
||||||
|
|
||||||
|
TMP_GIT_CLONE=$HOME/tmp/improvedmeans/
|
||||||
|
|
||||||
|
# Set LISP to your preferred implementation. The following
|
||||||
|
# implementations are currently supported:
|
||||||
|
# * sbcl
|
||||||
|
# * ccl
|
||||||
|
LISP=sbcl
|
||||||
|
|
||||||
|
########## DON'T EDIT ANYTHING BELOW THIS LINE ##########
|
||||||
|
|
||||||
|
if cd `dirname "$0"`/..; then
|
||||||
|
GIT_REPO=`pwd`
|
||||||
|
cd $OLDPWD || exit 1
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone $GIT_REPO $TMP_GIT_CLONE || exit 1
|
||||||
|
|
||||||
|
while read oldrev newrev refname; do
|
||||||
|
if [ $refname = "refs/heads/master" ]; then
|
||||||
|
echo -e "\n Master updated. Running coleslaw...\n"
|
||||||
|
if [ $LISP = sbcl ]; then
|
||||||
|
sbcl --eval "(ql:quickload 'coleslaw)" \
|
||||||
|
--eval "(coleslaw:main \"$TMP_GIT_CLONE\" :oldrev \"$oldrev\")" \
|
||||||
|
--eval "(uiop:quit)"
|
||||||
|
elif [ $LISP = ccl ]; then
|
||||||
|
ccl -e "(ql:quickload 'coleslaw)" \
|
||||||
|
-e "(coleslaw:main \"$TMP_GIT_CLONE\" :oldrev \"$oldrev\")" \
|
||||||
|
-e "(uiop:quit)"
|
||||||
|
else
|
||||||
|
echo -e "$LISP is not a supported lisp dialect at this time. Exiting.\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf $TMP_GIT_CLONE
|
||||||
|
exit
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
sbcl --eval "(ql:quickload '(coleslaw sb-introspect cl-api))" \
|
|
||||||
--eval "(cl-api:api-gen :coleslaw \"docs/coleslaw.html\")" \
|
|
||||||
--eval "(progn (terpri) (sb-ext:quit))"
|
|
12
plugins/3bmd-youtube.lisp
Normal file
12
plugins/3bmd-youtube.lisp
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload :3bmd-youtube))
|
||||||
|
|
||||||
|
(defpackage #:coleslaw-3bmd-youtube
|
||||||
|
(:use #:cl)
|
||||||
|
(:export
|
||||||
|
#:enable))
|
||||||
|
|
||||||
|
(in-package #:coleslaw-3bmd-youtube)
|
||||||
|
|
||||||
|
(defun enable ()
|
||||||
|
(setf 3bmd-youtube:*youtube-embeds* t))
|
|
@ -1,2 +1,25 @@
|
||||||
(in-package :coleslaw)
|
(defpackage :coleslaw-analytics
|
||||||
|
(:use :cl)
|
||||||
|
(:export #:enable)
|
||||||
|
(:import-from :coleslaw #:add-injection))
|
||||||
|
|
||||||
|
(in-package :coleslaw-analytics)
|
||||||
|
|
||||||
|
(defvar *analytics-js*
|
||||||
|
"<script type=\"text/javascript\">
|
||||||
|
|
||||||
|
var _gaq = _gaq || [];
|
||||||
|
_gaq.push(['_setAccount', '~a']);
|
||||||
|
_gaq.push(['_trackPageview']);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||||
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||||
|
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||||
|
})();
|
||||||
|
|
||||||
|
</script>")
|
||||||
|
|
||||||
|
(defun enable (&key tracking-code)
|
||||||
|
(let ((snippet (format nil *analytics-js* tracking-code)))
|
||||||
|
(add-injection (constantly snippet) :head)))
|
||||||
|
|
23
plugins/cl-who.lisp
Normal file
23
plugins/cl-who.lisp
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload 'cl-who))
|
||||||
|
|
||||||
|
(defpackage :coleslaw-cl-who
|
||||||
|
(:use #:cl #:cl-who)
|
||||||
|
(:import-from #:coleslaw :render-text)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-cl-who)
|
||||||
|
|
||||||
|
(defmethod render-text (text (format (eql :cl-who)))
|
||||||
|
(let* ((*package* (find-package "COLESLAW-CL-WHO"))
|
||||||
|
(sexps (with-input-from-string (v text)
|
||||||
|
(do* ((line (read v)
|
||||||
|
(read v nil 'done))
|
||||||
|
(acc (list line)
|
||||||
|
(cons line acc)))
|
||||||
|
((eql line 'done)
|
||||||
|
(nreverse (cdr acc)))))))
|
||||||
|
(eval `(with-html-output-to-string (v) ,@sexps))))
|
||||||
|
|
||||||
|
(defun enable ()
|
||||||
|
)
|
|
@ -1,7 +1,8 @@
|
||||||
(defpackage :coleslaw-disqus
|
(defpackage :coleslaw-disqus
|
||||||
(:use :cl)
|
(:use :cl)
|
||||||
(:export #:enable)
|
(:export #:enable)
|
||||||
(:import-from :coleslaw #:add-injection))
|
(:import-from :coleslaw #:add-injection
|
||||||
|
#:post))
|
||||||
|
|
||||||
(in-package :coleslaw-disqus)
|
(in-package :coleslaw-disqus)
|
||||||
|
|
||||||
|
@ -23,5 +24,7 @@
|
||||||
<a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>")
|
<a href=\"http://disqus.com\" class=\"dsq-brlink\">comments powered by <span class=\"logo-disqus\">Disqus</span></a>")
|
||||||
|
|
||||||
(defun enable (&key shortname)
|
(defun enable (&key shortname)
|
||||||
(add-injection (list (format nil *disqus-header* shortname)
|
(flet ((inject-p (x)
|
||||||
(lambda (x) (typep x post))) :head))
|
(when (typep x 'post)
|
||||||
|
(format nil *disqus-header* shortname))))
|
||||||
|
(add-injection #'inject-p :body)))
|
||||||
|
|
25
plugins/gfycat.lisp
Normal file
25
plugins/gfycat.lisp
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
(defpackage :coleslaw-gfycat
|
||||||
|
(:use :cl)
|
||||||
|
(:export #:enable)
|
||||||
|
(:import-from :coleslaw #:add-injection
|
||||||
|
#:content
|
||||||
|
#:tag-p))
|
||||||
|
|
||||||
|
(in-package :coleslaw-gfycat)
|
||||||
|
|
||||||
|
(defvar *gfycat-header*
|
||||||
|
"<script>
|
||||||
|
(function(d, t) {
|
||||||
|
var g = d.createElement(t),
|
||||||
|
s = d.getElementsByTagName(t)[0];
|
||||||
|
g.src = 'http://assets.gfycat.com/js/gfyajax-0.517d.js';
|
||||||
|
s.parentNode.insertBefore(g, s);
|
||||||
|
}(document, 'script'));
|
||||||
|
</script>")
|
||||||
|
|
||||||
|
(defun enable ()
|
||||||
|
(flet ((inject-p (x)
|
||||||
|
(when (and (typep x 'content)
|
||||||
|
(tag-p "gfycat" x))
|
||||||
|
*gfycat-header*)))
|
||||||
|
(add-injection #'inject-p :head)))
|
38
plugins/gh-pages.lisp
Normal file
38
plugins/gh-pages.lisp
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload 'puri :silent t))
|
||||||
|
(defpackage :coleslaw-gh-pages
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw
|
||||||
|
#:*config*
|
||||||
|
#:domain
|
||||||
|
#:deploy
|
||||||
|
#:staging-dir
|
||||||
|
#:deploy-dir)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-gh-pages)
|
||||||
|
|
||||||
|
(defvar *options* nil)
|
||||||
|
|
||||||
|
(defmethod deploy (staging)
|
||||||
|
(uiop:run-program (list* (namestring
|
||||||
|
(merge-pathnames "plugins/publish-gh-pages.sh"
|
||||||
|
coleslaw-conf:*basedir*))
|
||||||
|
(namestring
|
||||||
|
(merge-pathnames (staging-dir *config*)))
|
||||||
|
(namestring
|
||||||
|
(merge-pathnames (deploy-dir *config*)))
|
||||||
|
*options*)
|
||||||
|
:output t
|
||||||
|
:error-output t))
|
||||||
|
|
||||||
|
(defun enable (&key url (branch "gh-pages") (remote "origin") cname)
|
||||||
|
(check-type url string)
|
||||||
|
(check-type remote string)
|
||||||
|
(check-type branch string)
|
||||||
|
(if (eq t cname)
|
||||||
|
(progn
|
||||||
|
(setf cname (puri:uri-host (puri:parse-uri (domain *config*))))
|
||||||
|
(check-type cname string)
|
||||||
|
(setf *options* (list url branch remote cname)))
|
||||||
|
(setf *options* (list url branch remote))))
|
46
plugins/git-versioned.lisp
Normal file
46
plugins/git-versioned.lisp
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
(defpackage :coleslaw-git-versioned
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw
|
||||||
|
#:*config*
|
||||||
|
#:run-lines)
|
||||||
|
(:import-from :uiop #:ensure-directory-pathname))
|
||||||
|
|
||||||
|
(in-package :coleslaw-git-versioned)
|
||||||
|
|
||||||
|
(defconstant +nothing-to-commit+ 1
|
||||||
|
"Error code when git-commit has nothing staged to commit.")
|
||||||
|
|
||||||
|
;; These have their symbol-functions set in order to close over the src-dir
|
||||||
|
;; variable.
|
||||||
|
(defun git-versioned ()
|
||||||
|
"Run all git commands as specified in the .coleslawrc.")
|
||||||
|
(defun command (args)
|
||||||
|
"Automatically git commit and push the blog to remote."
|
||||||
|
(declare (ignore args)))
|
||||||
|
|
||||||
|
(defun enable (src-dir &rest commands)
|
||||||
|
"Define git-versioned functions at runtime."
|
||||||
|
(setf (symbol-function 'git-versioned)
|
||||||
|
(lambda ()
|
||||||
|
(loop for fsym in commands
|
||||||
|
do (funcall (symbol-function (intern (symbol-name fsym)
|
||||||
|
:coleslaw-git-versioned))))))
|
||||||
|
(setf (symbol-function 'command)
|
||||||
|
(lambda (args)
|
||||||
|
(run-lines src-dir
|
||||||
|
(format nil "git ~A" args)))))
|
||||||
|
(defmethod coleslaw:deploy :before (staging)
|
||||||
|
(declare (ignore staging))
|
||||||
|
(git-versioned))
|
||||||
|
|
||||||
|
(defun stage () (command "stage -A"))
|
||||||
|
(defun commit (&optional (commit-message "Automatic commit."))
|
||||||
|
(handler-case (command (format nil "commit -m '~A'" commit-message))
|
||||||
|
(uiop/run-program:subprocess-error (error)
|
||||||
|
(case (uiop/run-program:subprocess-error-code error)
|
||||||
|
(+nothing-to-commit+ (format t "Nothing to commit. Error ~d"
|
||||||
|
+nothing-to-commit+))
|
||||||
|
(otherwise (error error))))))
|
||||||
|
|
||||||
|
(defun upload ()
|
||||||
|
(command "push"))
|
21
plugins/gtag.lisp
Normal file
21
plugins/gtag.lisp
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
(defpackage :coleslaw-gtag
|
||||||
|
(:use :cl)
|
||||||
|
(:export #:enable)
|
||||||
|
(:import-from :coleslaw #:add-injection))
|
||||||
|
|
||||||
|
(in-package :coleslaw-gtag)
|
||||||
|
|
||||||
|
(defvar *analytics-js*
|
||||||
|
"<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
|
<script async src='https://www.googletagmanager.com/gtag/js?id=~a'></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', '~a');
|
||||||
|
</script>")
|
||||||
|
|
||||||
|
(defun enable (&key tracking-code)
|
||||||
|
(let ((snippet (format nil *analytics-js* tracking-code tracking-code)))
|
||||||
|
(add-injection (constantly snippet) :head)))
|
20
plugins/heroku.lisp
Normal file
20
plugins/heroku.lisp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload 'hunchentoot))
|
||||||
|
|
||||||
|
(defpackage :coleslaw-heroku
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from #:hunchentoot :create-folder-dispatcher-and-handler
|
||||||
|
:create-static-file-dispatcher-and-handler
|
||||||
|
:*dispatch-table*)
|
||||||
|
(:import-from #:coleslaw :deploy)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-heroku)
|
||||||
|
|
||||||
|
(defmethod deploy :after (staging)
|
||||||
|
(push (create-folder-dispatcher-and-handler "/" "/app/.curr/")
|
||||||
|
*dispatch-table*)
|
||||||
|
(push (create-static-file-dispatcher-and-handler "/" "/app/.curr/index.html")
|
||||||
|
*dispatch-table*))
|
||||||
|
|
||||||
|
(defun enable ())
|
|
@ -1,4 +0,0 @@
|
||||||
(ql:quickload '(hunchentoot))
|
|
||||||
|
|
||||||
(in-package :coleslaw)
|
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
(:import-from :coleslaw #:slugify
|
(:import-from :coleslaw #:slugify
|
||||||
#:load-config
|
#:load-config
|
||||||
#:*config*
|
#:*config*
|
||||||
#:repo)
|
#:repo
|
||||||
|
#:repo-dir
|
||||||
|
#:separator)
|
||||||
(:import-from :local-time #:+short-month-names+)
|
(:import-from :local-time #:+short-month-names+)
|
||||||
(:import-from :cl-ppcre #:regex-replace-all))
|
(:import-from :cl-ppcre #:regex-replace-all))
|
||||||
|
|
||||||
|
@ -28,10 +30,10 @@
|
||||||
(format nil "~a-~2,'0d-~2,'0d ~a" year (position month +short-month-names+
|
(format nil "~a-~2,'0d-~2,'0d ~a" year (position month +short-month-names+
|
||||||
:test #'string=) date time)))
|
:test #'string=) date time)))
|
||||||
|
|
||||||
(defun import-post (post output &optional (since nil since-supplied-p))
|
(defun import-post (post output &optional since)
|
||||||
(when (and (string= "publish" (node-val "wp:status" post)) ; is it public?
|
(when (and (string= "publish" (node-val "wp:status" post)) ; is it public?
|
||||||
(string= "post" (node-val "wp:post_type" post)) ; is it a post?
|
(string= "post" (node-val "wp:post_type" post)) ; is it a post?
|
||||||
(or (not since-supplied-p) (string>= (get-timestamp post) since)))
|
(or (null since) (string>= (get-timestamp post) since)))
|
||||||
(let ((slug (slugify (node-val "title" post))))
|
(let ((slug (slugify (node-val "title" post))))
|
||||||
(when (string= "" slug)
|
(when (string= "" slug)
|
||||||
(error "No valid slug-title for post ~a." (get-timestamp post)))
|
(error "No valid slug-title for post ~a." (get-timestamp post)))
|
||||||
|
@ -40,22 +42,23 @@
|
||||||
(format nil "~a.post" slug) output))))
|
(format nil "~a.post" slug) output))))
|
||||||
|
|
||||||
(defun export-post (title tags date content path output)
|
(defun export-post (title tags date content path output)
|
||||||
(with-open-file (out (merge-pathnames path (or output (repo *config*)))
|
(with-open-file (out (merge-pathnames path (or output (repo-dir *config*)))
|
||||||
:direction :output
|
:direction :output
|
||||||
:if-exists :supersede
|
:if-exists :supersede
|
||||||
:if-does-not-exist :create)
|
:if-does-not-exist :create
|
||||||
|
:external-format :utf-8)
|
||||||
;; TODO: What other data/metadata should we write out?
|
;; TODO: What other data/metadata should we write out?
|
||||||
(format out ";;;;;~%")
|
(format out "~A~%" (separator *config*))
|
||||||
(format out "title: ~A~%" title)
|
(format out "title: ~A~%" title)
|
||||||
(format out "tags: ~A~%" (format nil "~{~A~^, ~}" tags))
|
(format out "tags: ~A~%" (format nil "~{~A~^, ~}" tags))
|
||||||
(format out "date: ~A~%" date)
|
(format out "date: ~A~%" date)
|
||||||
(format out "format: html~%") ; post format: html, md, rst, etc
|
(format out "format: html~%") ; post format: html, md, rst, etc
|
||||||
(format out ";;;;;~%")
|
(format out "~A~%" (separator *config*))
|
||||||
(format out "~A~%" (regex-replace-all (string #\Newline) content "<br>"))))
|
(format out "~A~%" (regex-replace-all (string #\Newline) content "<br>"))))
|
||||||
|
|
||||||
(defun import-posts (filepath output &optional since)
|
(defun import-posts (filepath output &optional since)
|
||||||
(when (probe-file filepath)
|
(when (probe-file filepath)
|
||||||
(ensure-directories-exist (repo *config*))
|
(ensure-directories-exist (or output (repo-dir *config*)))
|
||||||
(let* ((xml (cxml:parse-file filepath (cxml-dom:make-dom-builder)))
|
(let* ((xml (cxml:parse-file filepath (cxml-dom:make-dom-builder)))
|
||||||
(posts (dom:get-elements-by-tag-name xml "item")))
|
(posts (dom:get-elements-by-tag-name xml "item")))
|
||||||
(loop for post across posts do (import-post post output since))
|
(loop for post across posts do (import-post post output since))
|
||||||
|
|
82
plugins/incremental.lisp
Normal file
82
plugins/incremental.lisp
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload 'cl-store))
|
||||||
|
|
||||||
|
(defpackage :coleslaw-incremental
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :alexandria #:when-let)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:content
|
||||||
|
#:index
|
||||||
|
#:discover
|
||||||
|
#:get-updated-files
|
||||||
|
#:find-content-by-path
|
||||||
|
#:add-document
|
||||||
|
#:delete-document
|
||||||
|
;; Private
|
||||||
|
#:all-subclasses
|
||||||
|
#:do-subclasses
|
||||||
|
#:read-content
|
||||||
|
#:construct
|
||||||
|
#:rel-path
|
||||||
|
#:repo
|
||||||
|
#:update-content-metadata)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-incremental)
|
||||||
|
|
||||||
|
;; In contrast to the original incremental plans, full of shoving state into
|
||||||
|
;; the right place by hand and avoiding writing pages to disk that hadn't
|
||||||
|
;; changed, the new plan is to only avoid redundant parsing of content in
|
||||||
|
;; the git repo. The rest of coleslaw's operation is "fast enough".
|
||||||
|
;;
|
||||||
|
;; Prior to enabling the plugin a user must have a cl-store dump of the
|
||||||
|
;; database at ~/.coleslaw.db. There is a dump_db shell script in
|
||||||
|
;; examples to generate the database dump.
|
||||||
|
;;
|
||||||
|
;; We're gonna be a bit dirty here and monkey patch. The compilation model
|
||||||
|
;; still isn't an "exposed" part of Coleslaw. After some experimentation maybe
|
||||||
|
;; we'll settle on an interface.
|
||||||
|
|
||||||
|
(defun coleslaw::load-content ()
|
||||||
|
(let ((db-file (rel-path (user-homedir-pathname) ".coleslaw.db")))
|
||||||
|
(setf coleslaw::*site* (cl-store:restore db-file))
|
||||||
|
(loop for (status path) in (get-updated-files)
|
||||||
|
for file-path = (rel-path (repo-dir *config*) path)
|
||||||
|
do (update-content status file-path))
|
||||||
|
(update-content-metadata)
|
||||||
|
;; Discover's :before method will delete any possibly outdated indexes.
|
||||||
|
(do-subclasses (itype index)
|
||||||
|
(discover itype))
|
||||||
|
(cl-store:store coleslaw::*site* db-file)))
|
||||||
|
|
||||||
|
(defun update-content (status path)
|
||||||
|
(cond ((string= "D" status) (process-change :deleted path))
|
||||||
|
((string= "M" status) (process-change :modified path))
|
||||||
|
((string= "A" status) (process-change :added path))))
|
||||||
|
|
||||||
|
(defgeneric process-change (status path &key &allow-other-keys)
|
||||||
|
(:documentation "Updates the database as needed for the STATUS change to PATH.")
|
||||||
|
(:method :around (status path &key)
|
||||||
|
(let ((extension (pathname-type path))
|
||||||
|
(ctypes (all-subclasses (find-class 'content))))
|
||||||
|
;; If the updated file's extension doesn't match one of our content types,
|
||||||
|
;; we don't need to mess with it at all. Otherwise, since the class is
|
||||||
|
;; annoyingly tricky to determine, pass it along.
|
||||||
|
(when-let (ctype (find extension ctypes :test #'class-name-p))
|
||||||
|
(call-next-method status path :ctype ctype)))))
|
||||||
|
|
||||||
|
(defmethod process-change ((status (eql :deleted)) path &key)
|
||||||
|
(let ((old (find-content-by-path path)))
|
||||||
|
(delete-document old)))
|
||||||
|
|
||||||
|
(defmethod process-change ((status (eql :modified)) path &key ctype)
|
||||||
|
(let ((old (find-content-by-path path))
|
||||||
|
(new (construct ctype (read-content path))))
|
||||||
|
(delete-document old)
|
||||||
|
(add-document new)))
|
||||||
|
|
||||||
|
(defmethod process-change ((status (eql :added)) path &key ctype)
|
||||||
|
(let ((new (construct ctype (read-content path))))
|
||||||
|
(add-document new)))
|
||||||
|
|
||||||
|
(defun enable ())
|
20
plugins/isso.lisp
Normal file
20
plugins/isso.lisp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
(defpackage :coleslaw-isso
|
||||||
|
(:use :cl)
|
||||||
|
(:export #:enable)
|
||||||
|
(:import-from :coleslaw #:add-injection
|
||||||
|
#:post))
|
||||||
|
|
||||||
|
(in-package :coleslaw-isso)
|
||||||
|
|
||||||
|
(defvar *isso-header*
|
||||||
|
"<div class=\"comments\">
|
||||||
|
<section id=\"isso-thread\"></section>
|
||||||
|
<script data-isso=\"~a/\"
|
||||||
|
src=\"~a/js/embed.min.js\"></script>
|
||||||
|
</div>")
|
||||||
|
|
||||||
|
(defun enable (&key isso-url)
|
||||||
|
(flet ((inject-p (x)
|
||||||
|
(when (typep x 'post)
|
||||||
|
(format nil *isso-header* isso-url isso-url))))
|
||||||
|
(add-injection #'inject-p :body)))
|
12
plugins/markless.lisp
Normal file
12
plugins/markless.lisp
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload 'cl-markless-plump))
|
||||||
|
|
||||||
|
(defpackage #:coleslaw-markless
|
||||||
|
(:use #:cl)
|
||||||
|
(:export #:enable))
|
||||||
|
(in-package :coleslaw-markless)
|
||||||
|
|
||||||
|
(defmethod coleslaw:render-text (text (format (eql :markless)))
|
||||||
|
(cl-markless:output text :target NIL :format 'cl-markless-plump:plump))
|
||||||
|
|
||||||
|
(defun enable ())
|
|
@ -2,29 +2,29 @@
|
||||||
(:use :cl)
|
(:use :cl)
|
||||||
(:export #:enable)
|
(:export #:enable)
|
||||||
(:import-from :coleslaw #:add-injection
|
(:import-from :coleslaw #:add-injection
|
||||||
#:post
|
#:content
|
||||||
#:index
|
#:index
|
||||||
#:post-tags
|
#:tag-p
|
||||||
#:index-posts))
|
#:index-content))
|
||||||
|
|
||||||
(in-package :coleslaw-mathjax)
|
(in-package :coleslaw-mathjax)
|
||||||
|
|
||||||
(defvar *mathjax-header* "<script type=\"text/x-mathjax-config\">
|
(defvar *mathjax-header* "~@[<script type=\"text/x-mathjax-config\">
|
||||||
MathJax.Hub.Config({
|
MathJax.Hub.Config({~A});
|
||||||
tex2jax: {
|
</script>~]
|
||||||
inlineMath: [['$$','$$']]
|
<script type=\"text/javascript\" src=\"~A~@[?config=~A~]\"></script>")
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script type=\"text/javascript\"
|
|
||||||
src=\"http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML\">
|
|
||||||
</script>")
|
|
||||||
|
|
||||||
(defun enable ()
|
(defgeneric mathjax-p (document)
|
||||||
(labels ((math-post-p (post)
|
(:documentation "Test if DOCUMENT requires contains any math-tagged content.")
|
||||||
(member "math" (post-tags post) :test #'string=))
|
(:method ((content content))
|
||||||
(mathjax-p (content)
|
(tag-p "math" content))
|
||||||
(etypecase content
|
(:method ((index index))
|
||||||
(post (math-post-p content))
|
(and (slot-boundp index 'content)
|
||||||
(index (some #'math-post-p (index-posts content))))))
|
(some #'mathjax-p (index-content index)))))
|
||||||
(add-injection (list *mathjax-header* #'mathjax-p) :head)))
|
|
||||||
|
(defun enable (&key force config (preset "TeX-AMS-MML_HTMLorMML")
|
||||||
|
(location "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js"))
|
||||||
|
(flet ((inject-p (x)
|
||||||
|
(when (or force (mathjax-p x))
|
||||||
|
(format nil *mathjax-header* config location preset))))
|
||||||
|
(add-injection #'inject-p :head)))
|
||||||
|
|
25
plugins/matomo.lisp
Normal file
25
plugins/matomo.lisp
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
(defpackage :coleslaw-matomo
|
||||||
|
(:use :cl)
|
||||||
|
(:export #:enable)
|
||||||
|
(:import-from :coleslaw #:add-injection))
|
||||||
|
|
||||||
|
(in-package :coleslaw-matomo)
|
||||||
|
|
||||||
|
(defvar *matomo-js*
|
||||||
|
"<script type="text/javascript">
|
||||||
|
var _paq = _paq || [];
|
||||||
|
_paq.push(['trackPageView']);
|
||||||
|
_paq.push(['enableLinkTracking']);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var u='//~a/';
|
||||||
|
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||||
|
_paq.push(['setSiteId', '~a']);
|
||||||
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
|
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
|
</script>")
|
||||||
|
|
||||||
|
(defun enable (&key matomo-url matomo-site)
|
||||||
|
(let ((snippet (format nil *matomo-js* matomo-url matomo-site)))
|
||||||
|
(add-injection (constantly snippet) :head)))
|
55
plugins/publish-gh-pages.sh
Executable file
55
plugins/publish-gh-pages.sh
Executable file
|
@ -0,0 +1,55 @@
|
||||||
|
#!/bin/bash -x
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
staging=$1
|
||||||
|
deploy=$2
|
||||||
|
url=$3
|
||||||
|
branch=$4
|
||||||
|
remote=$5
|
||||||
|
cname=$6
|
||||||
|
|
||||||
|
if [[ -d $deploy && ! -d $deploy/.git ]]
|
||||||
|
then
|
||||||
|
echo "Target directory $deploy exists and is not a git repository. Aborting" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d $deploy ]]
|
||||||
|
then
|
||||||
|
git clone --no-checkout --origin $remote $url $deploy
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd $deploy
|
||||||
|
|
||||||
|
# safe and most reliable way to check if the branch exist
|
||||||
|
if git show-ref --verify --quiet refs/heads/$branch
|
||||||
|
then
|
||||||
|
# if the branch exists locally
|
||||||
|
git checkout $branch
|
||||||
|
elif git show-ref --verify --quiet refs/remotes/$remote/$branch
|
||||||
|
then
|
||||||
|
# if the branch does not exist locally but exist in the specified remote ---
|
||||||
|
# Note, git checkout $branch will search the branch with the same name with
|
||||||
|
# ALL remotes, and set it as the tracking branch if there is a single such
|
||||||
|
# remote, but does not allow the user to necessarily specify which.
|
||||||
|
git checkout -b $branch --track $remote/$branch
|
||||||
|
else
|
||||||
|
# if there is no matching branch, make an orphan branch
|
||||||
|
git checkout --orphan $branch
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -avz --delete --exclude .git/ --copy-links $staging $deploy
|
||||||
|
|
||||||
|
if [[ ! -z "$cname" ]]
|
||||||
|
then
|
||||||
|
echo $cname > CNAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add -A # add all changes in the worktree
|
||||||
|
git add $(git ls-files -o ) # add all untracked files in the worktree
|
||||||
|
|
||||||
|
git commit -m "Deployed on $(date)"
|
||||||
|
|
||||||
|
git push $remote $branch
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
(in-package :coleslaw)
|
(defpackage #:coleslaw-pygments
|
||||||
|
(:use #:cl)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package #:coleslaw-pygments)
|
||||||
|
|
||||||
|
(defun enable ()
|
||||||
|
(setf 3bmd-code-blocks:*renderer* :pygments))
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
(eval-when (:compile-toplevel :load-toplevel)
|
|
||||||
(ql:quickload 'docutils))
|
|
||||||
|
|
||||||
(defpackage :coleslaw-rst
|
|
||||||
(:use :cl :coleslaw))
|
|
||||||
|
|
||||||
(in-package :coleslaw-rst)
|
|
21
plugins/robocopy.lisp
Normal file
21
plugins/robocopy.lisp
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
(defpackage :coleslaw-robocopy
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:deploy
|
||||||
|
#:deploy-dir)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-robocopy)
|
||||||
|
|
||||||
|
(defvar *args* nil)
|
||||||
|
|
||||||
|
(defmethod deploy (staging)
|
||||||
|
(coleslaw::run-program
|
||||||
|
"(robocopy ~A ~A ~{~A~^ ~}) ^& IF %ERRORLEVEL% LEQ 1 exit 0"
|
||||||
|
(merge-pathnames staging)
|
||||||
|
(merge-pathnames (deploy-dir *config*))
|
||||||
|
*args*))
|
||||||
|
|
||||||
|
(defun enable (&rest args)
|
||||||
|
(setf *args* args))
|
32
plugins/rst.lisp
Normal file
32
plugins/rst.lisp
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload 'docutils))
|
||||||
|
|
||||||
|
(defpackage :coleslaw-rst
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:render-text)
|
||||||
|
(:import-from :docutils #:read-rst #:write-part #:register-settings-spec
|
||||||
|
#:visit-node #:write-document #:document)
|
||||||
|
(:import-from :docutils.writer.html #:html-writer
|
||||||
|
#:body-pre-docinfo
|
||||||
|
#:body
|
||||||
|
#:parts)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-rst)
|
||||||
|
|
||||||
|
;; XXX: This is not an ideal solution as it affects other uses of docutils in
|
||||||
|
;; the same lisp image.
|
||||||
|
(defmethod visit-node :after ((writer html-writer) (document document))
|
||||||
|
"This method removes unnecessary HTML elements, such as html, head, body
|
||||||
|
and make docutils output only html fragment with document itself."
|
||||||
|
(setf (slot-value writer 'parts) '(body-pre-docinfo
|
||||||
|
body)))
|
||||||
|
|
||||||
|
(defmethod render-text (text (format (eql :rst)))
|
||||||
|
(register-settings-spec '((:generator nil)
|
||||||
|
(:datestamp nil)))
|
||||||
|
(let ((writer (make-instance 'html-writer))
|
||||||
|
(document (read-rst text)))
|
||||||
|
(write-document writer document 'string)))
|
||||||
|
|
||||||
|
(defun enable ())
|
19
plugins/rsync.lisp
Normal file
19
plugins/rsync.lisp
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
(defpackage :coleslaw-rsync
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:deploy
|
||||||
|
#:deploy-dir)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-rsync)
|
||||||
|
|
||||||
|
(defvar *args* nil)
|
||||||
|
|
||||||
|
(defmethod deploy (staging)
|
||||||
|
(coleslaw::run-program "rsync ~{~A~^ ~} ~A ~A" *args*
|
||||||
|
(merge-pathnames staging)
|
||||||
|
(merge-pathnames (deploy-dir *config*))))
|
||||||
|
|
||||||
|
(defun enable (&rest args)
|
||||||
|
(setf *args* args))
|
|
@ -1,16 +1,15 @@
|
||||||
(eval-when (:compile-toplevel)
|
(eval-when (:compile-toplevel :load-toplevel)
|
||||||
(ql:quickload '(zs3)))
|
(ql:quickload 'zs3))
|
||||||
|
|
||||||
(defpackage :coleslaw-s3
|
(defpackage :coleslaw-s3
|
||||||
(:use :cl :coleslaw :zs3))
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:deploy
|
||||||
|
#:deploy-dir
|
||||||
|
#:*config*)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
(in-package :coleslaw-s3)
|
(in-package :coleslaw-s3)
|
||||||
|
|
||||||
(defparameter *credentials* (get-credentials :s3)
|
|
||||||
"The credentials to authenticate with Amazon Web Services.
|
|
||||||
Stored in a file with the access key on the first line
|
|
||||||
and the secret key on the second.")
|
|
||||||
|
|
||||||
(defparameter *content-type-map* '(("html" . "text/html")
|
(defparameter *content-type-map* '(("html" . "text/html")
|
||||||
("css" . "text/css")
|
("css" . "text/css")
|
||||||
("png" . "image/png")
|
("png" . "image/png")
|
||||||
|
@ -26,34 +25,30 @@ and the secret key on the second.")
|
||||||
(defun content-type (extension)
|
(defun content-type (extension)
|
||||||
(cdr (assoc extension *content-type-map* :test #'equal)))
|
(cdr (assoc extension *content-type-map* :test #'equal)))
|
||||||
|
|
||||||
(defun init ()
|
(defun stale-keys ()
|
||||||
(unless *credentials*
|
(loop for key being the hash-values in *cache* collecting key))
|
||||||
(set-credentials :s3 (file-credentials "~/.aws"))
|
|
||||||
(setf *credentials* (get-credentials :s3))))
|
|
||||||
|
|
||||||
(defun stale-keys (&key cache)
|
(defun s3-sync (filepath dir)
|
||||||
(loop for key being the hash-values in cache collecting key))
|
(let ((etag (zs3:file-etag filepath))
|
||||||
|
(key (enough-namestring filepath dir)))
|
||||||
|
(if (gethash etag *cache*)
|
||||||
|
(remhash etag *cache*)
|
||||||
|
(zs3:put-file filepath *bucket* key :public t
|
||||||
|
:content-type (content-type (pathname-type filepath))))))
|
||||||
|
|
||||||
(defun s3-sync (filepath &key bucket dir public-p cache)
|
(defun dir->s3 (dir)
|
||||||
(flet ((compute-key (namestring)
|
(flet ((upload (file) (s3-sync file dir)))
|
||||||
(subseq namestring (length (namestring (truename dir))))))
|
(cl-fad:walk-directory dir #'upload)))
|
||||||
(let* ((etag (file-etag filepath))
|
|
||||||
(namestring (namestring filepath))
|
|
||||||
(key (compute-key namestring)))
|
|
||||||
(if (gethash etag cache)
|
|
||||||
(remhash etag cache)
|
|
||||||
(put-file filepath bucket key :public public-p
|
|
||||||
:content-type (content-type (pathname-type filepath)))))))
|
|
||||||
|
|
||||||
(defun dir->s3 (dir &key bucket cache public-p)
|
(defmethod deploy :after (staging)
|
||||||
(cl-fad:walk-directory dir (lambda (file)
|
(let ((blog (deploy-dir *config*)))
|
||||||
(s3-sync file :cache cache :dir dir
|
(loop for key across (zs3:all-keys *bucket*)
|
||||||
:bucket bucket :public-p public-p))))
|
do (setf (gethash (zs3:etag key) *cache*) key))
|
||||||
|
(dir->s3 blog)
|
||||||
|
(zs3:delete-objects (stale-keys) *bucket*)))
|
||||||
|
|
||||||
(defmethod coleslaw::render-site :after ()
|
(defun enable (&key auth-file bucket)
|
||||||
(init)
|
"AUTH-FILE: Path to file with the access key on the first line and the secret
|
||||||
(let* ((keys (all-keys *bucket*)))
|
key on the second."
|
||||||
(loop for key across keys do (setf (gethash (etag key) *cache*) key))
|
(setf zs3:*credentials* (zs3:file-credentials auth-file)
|
||||||
(dir->s3 coleslaw::*output-dir* :bucket *bucket* :cache *cache* :public-p t)
|
*bucket* bucket))
|
||||||
(when (stale-keys :cache *cache*)
|
|
||||||
(delete-objects (stale-keys) *bucket*))))
|
|
||||||
|
|
30
plugins/sitemap.lisp
Normal file
30
plugins/sitemap.lisp
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
(defpackage :coleslaw-sitemap
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:index
|
||||||
|
#:page-url
|
||||||
|
#:find-all
|
||||||
|
#:publish
|
||||||
|
#:theme-fn
|
||||||
|
#:add-document
|
||||||
|
#:write-document)
|
||||||
|
(:import-from :alexandria #:hash-table-values)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-sitemap)
|
||||||
|
|
||||||
|
(defclass sitemap (index)
|
||||||
|
((urls :initarg :urls :reader urls)))
|
||||||
|
|
||||||
|
(defmethod page-url ((object sitemap)) "sitemap.xml")
|
||||||
|
|
||||||
|
;; We do 'discovery' in the publish method here because we can't ensure the
|
||||||
|
;; sitemap discover method is called last. Need all other content to be
|
||||||
|
;; discovered/in the DB.
|
||||||
|
(defmethod publish ((doc-type (eql (find-class 'sitemap))))
|
||||||
|
(let* ((base-urls '("" "sitemap.xml"))
|
||||||
|
(urls (mapcar #'page-url (hash-table-values coleslaw::*site*)))
|
||||||
|
(sitemap (make-instance 'sitemap :urls (append base-urls urls))))
|
||||||
|
(write-document sitemap (theme-fn 'sitemap "sitemap"))))
|
||||||
|
|
||||||
|
(defun enable ())
|
40
plugins/static-pages.lisp
Normal file
40
plugins/static-pages.lisp
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
(defpackage :coleslaw-static-pages
|
||||||
|
(:use :cl)
|
||||||
|
(:export #:enable)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:assert-field
|
||||||
|
#:content
|
||||||
|
#:find-all
|
||||||
|
#:render
|
||||||
|
#:publish
|
||||||
|
#:theme-fn
|
||||||
|
#:render-text
|
||||||
|
#:write-document))
|
||||||
|
|
||||||
|
(in-package :coleslaw-static-pages)
|
||||||
|
|
||||||
|
(defclass page (content)
|
||||||
|
((title :initarg :title :reader coleslaw::title-of)
|
||||||
|
(format :initarg :format :reader coleslaw::page-format))
|
||||||
|
;; default format is markdown (for backward compatibility)
|
||||||
|
(:default-initargs :format :md))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((object page) &key)
|
||||||
|
(assert-field 'title object)
|
||||||
|
(assert-field 'coleslaw::url object)
|
||||||
|
(with-slots (coleslaw::url coleslaw::text format title) object
|
||||||
|
(setf coleslaw::url (make-pathname :defaults coleslaw::url)
|
||||||
|
format (alexandria:make-keyword (string-upcase format))
|
||||||
|
coleslaw::text (render-text coleslaw::text format))))
|
||||||
|
|
||||||
|
(defmethod render ((object page) &key next prev)
|
||||||
|
;; For the time being, we'll re-use the normal post theme.
|
||||||
|
(declare (ignore next prev))
|
||||||
|
(funcall (theme-fn 'post) (list :config *config*
|
||||||
|
:post object)))
|
||||||
|
|
||||||
|
(defmethod publish ((doc-type (eql (find-class 'page))))
|
||||||
|
(dolist (page (find-all 'page))
|
||||||
|
(write-document page)))
|
||||||
|
|
||||||
|
(defun enable ())
|
25
plugins/twitter-summary-card.lisp
Normal file
25
plugins/twitter-summary-card.lisp
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
(defpackage :coleslaw-twitter-summary-card
|
||||||
|
(:use :cl :coleslaw)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-twitter-summary-card)
|
||||||
|
|
||||||
|
(defun summary-card (post twitter-handle)
|
||||||
|
"TODO: Figure if and how to include twitter:url meta property."
|
||||||
|
(format nil "<meta property=\"twitter:card\" content=\"summary\" />
|
||||||
|
~@[<meta property=\"twitter:author\" content=\"~A\" />~]
|
||||||
|
<meta property=\"twitter:title\" content=\"~A\" />
|
||||||
|
<meta property=\"twitter:description\" content=\"~A\" />"
|
||||||
|
twitter-handle
|
||||||
|
(title-of post)
|
||||||
|
(let ((text (content-text post)))
|
||||||
|
(if (< 200 (length text))
|
||||||
|
(subseq text 0 199)
|
||||||
|
text))))
|
||||||
|
|
||||||
|
(defun enable (&key twitter-handle)
|
||||||
|
(add-injection
|
||||||
|
(lambda (x)
|
||||||
|
(when (typep x 'post)
|
||||||
|
(summary-card x twitter-handle)))
|
||||||
|
:head))
|
88
plugins/twitter.lisp
Normal file
88
plugins/twitter.lisp
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
(:eval-when (:compile-toplevel :load-toplevel)
|
||||||
|
(ql:quickload :chirp))
|
||||||
|
|
||||||
|
(defpackage :coleslaw-twitter
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:deploy
|
||||||
|
#:get-updated-files
|
||||||
|
#:find-content-by-path
|
||||||
|
#:title-of
|
||||||
|
#:author-of
|
||||||
|
#:page-url
|
||||||
|
#:plugin-conf-error)
|
||||||
|
(:export #:enable))
|
||||||
|
|
||||||
|
(in-package :coleslaw-twitter)
|
||||||
|
|
||||||
|
(defvar *tweet-format* '(:title "by" :author)
|
||||||
|
"Controls what the tweet annoucing the post looks like.")
|
||||||
|
|
||||||
|
(defvar *tweet-format-fn* nil "Function that expects an instance of
|
||||||
|
coleslaw:post and returns the tweet content.")
|
||||||
|
|
||||||
|
(defvar *tweet-format-dsl-mapping*
|
||||||
|
'((:title title-of)
|
||||||
|
(:author author-of)))
|
||||||
|
|
||||||
|
(define-condition malformed-tweet-format (error)
|
||||||
|
((item :initarg :item :reader item))
|
||||||
|
(:report
|
||||||
|
(lambda (condition stream)
|
||||||
|
(format stream "Malformed tweet format. Can't proccess: ~A"
|
||||||
|
(item condition)))))
|
||||||
|
|
||||||
|
(defun compile-tweet-format (tweet-format)
|
||||||
|
(flet ((accessor-for (x)
|
||||||
|
(rest (assoc x *tweet-format-dsl-mapping*))))
|
||||||
|
(lambda (post)
|
||||||
|
(apply #'format nil "~{~A~^ ~}"
|
||||||
|
(loop for item in *tweet-format*
|
||||||
|
unless (or (keywordp item) (stringp item))
|
||||||
|
(error 'malformed-tweet-format :item item)
|
||||||
|
when (keywordp item)
|
||||||
|
collect (funcall (accessor-for item) post)
|
||||||
|
when (stringp item)
|
||||||
|
collect item)))))
|
||||||
|
|
||||||
|
(defun enable (&key api-key api-secret access-token access-secret tweet-format)
|
||||||
|
(if (and api-key api-secret access-token access-secret)
|
||||||
|
(setf chirp:*oauth-api-key* api-key
|
||||||
|
chirp:*oauth-api-secret* api-secret
|
||||||
|
chirp:*oauth-access-token* access-token
|
||||||
|
chirp:*oauth-access-secret* access-secret)
|
||||||
|
(error 'plugin-conf-error :plugin "twitter"
|
||||||
|
:message "Credentials missing."))
|
||||||
|
|
||||||
|
;; fallback to chirp for credential erros
|
||||||
|
(chirp:account/verify-credentials)
|
||||||
|
(when tweet-format
|
||||||
|
(setf *tweet-format* tweet-format))
|
||||||
|
(setf *tweet-format-fn* (compile-tweet-format *tweet-format*)))
|
||||||
|
|
||||||
|
(defmethod deploy :after (staging)
|
||||||
|
(declare (ignore staging))
|
||||||
|
(loop :for (state file) :in (get-updated-files)
|
||||||
|
:when (and (string= "A" state) (string= "post" (pathname-type file)))
|
||||||
|
:do (tweet-new-post file)))
|
||||||
|
|
||||||
|
(defun tweet-new-post (file)
|
||||||
|
"Retrieve content matching FILE from in memory DB and publish it."
|
||||||
|
(let ((post (find-content-by-path file)))
|
||||||
|
(chirp:statuses/update (%format-post 0 post))))
|
||||||
|
|
||||||
|
(defun %format-post (offset post)
|
||||||
|
"Guarantee that the tweet content is 140 chars at most. The 117 comes from
|
||||||
|
the spaxe needed for a space and the url."
|
||||||
|
(let* ((content-prefix (subseq (render-tweet post) 0 (- 117 offset)))
|
||||||
|
(content (format nil "~A ~A/~A" content-prefix
|
||||||
|
(coleslaw::domain *config*)
|
||||||
|
(page-url post)))
|
||||||
|
(content-length (chirp:compute-status-length content)))
|
||||||
|
(cond
|
||||||
|
((>= 140 content-length) content)
|
||||||
|
((< 140 content-length) (%format-post (1- offset) post)))))
|
||||||
|
|
||||||
|
(defun render-tweet (post)
|
||||||
|
"Sans the url, which is a must."
|
||||||
|
(funcall *tweet-format-fn* post))
|
24
plugins/versioned.lisp
Normal file
24
plugins/versioned.lisp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
(defpackage :coleslaw-versioned
|
||||||
|
(:use :cl)
|
||||||
|
(:import-from :coleslaw #:*config*
|
||||||
|
#:deploy-dir
|
||||||
|
#:rel-path
|
||||||
|
#:run-program
|
||||||
|
#:update-symlink))
|
||||||
|
|
||||||
|
(in-package :coleslaw-versioned)
|
||||||
|
|
||||||
|
(defmethod coleslaw:deploy (staging)
|
||||||
|
(let* ((dest (deploy-dir *config*))
|
||||||
|
(new-build (rel-path dest "generated/~a" (get-universal-time)))
|
||||||
|
(prev (rel-path dest ".prev"))
|
||||||
|
(curr (rel-path dest ".curr")))
|
||||||
|
(ensure-directories-exist new-build)
|
||||||
|
(run-program "mv ~a ~a" staging new-build)
|
||||||
|
(when (and (probe-file prev) (truename prev))
|
||||||
|
(run-program "rm -r ~a" (truename prev)))
|
||||||
|
(when (probe-file curr)
|
||||||
|
(update-symlink prev (truename curr)))
|
||||||
|
(update-symlink curr new-build)))
|
||||||
|
|
||||||
|
(defun enable ())
|
10
roswell/coleslaw.ros
Executable file
10
roswell/coleslaw.ros
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#|-*- mode:lisp -*-|#
|
||||||
|
#|
|
||||||
|
exec ros -Q -L sbcl-bin -m coleslaw -- $0 "$@"
|
||||||
|
|#
|
||||||
|
(progn ;;init forms
|
||||||
|
(ros:ensure-asdf)
|
||||||
|
#+quicklisp (ql:quickload '(:coleslaw-cli) :silent t))
|
||||||
|
|
||||||
|
(in-package :coleslaw-cli)
|
10
src/coleslaw-conf.lisp
Normal file
10
src/coleslaw-conf.lisp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
(defpackage #:coleslaw-conf
|
||||||
|
(:use #:cl)
|
||||||
|
(:export #:*basedir*))
|
||||||
|
|
||||||
|
(in-package #:coleslaw-conf)
|
||||||
|
|
||||||
|
(defparameter *basedir*
|
||||||
|
(uiop/pathname:pathname-parent-directory-pathname
|
||||||
|
#.(or *compile-file-truename* *load-truename*))
|
||||||
|
"A pathname pointing to Coleslaw's top level directory.")
|
|
@ -1,64 +1,80 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
(defgeneric render (content &key &allow-other-keys)
|
(defvar *last-revision* nil
|
||||||
(:documentation "Render the given CONTENT to HTML."))
|
"The git revision prior to the last push. For use with GET-UPDATED-FILES.")
|
||||||
|
|
||||||
(defun render-page (content &optional theme-fn &rest render-args)
|
(defun main (repo-dir &key oldrev (deploy t))
|
||||||
"Render the given CONTENT to disk using THEME-FN if supplied.
|
"Load the user's config file, compile the blog in REPO-DIR into STAGING-DIR,
|
||||||
Additional args to render CONTENT can be passed via RENDER-ARGS."
|
and optionally deploy the blog to DEPLOY-DIR.
|
||||||
(let* ((path (etypecase content
|
OLDREV -- the git revision prior to the last push.
|
||||||
(post (format nil "posts/~a.html" (post-slug content)))
|
DEPLOY -- when non-nil, perform the deploy. (default: t)"
|
||||||
(index (index-path content))))
|
(load-config repo-dir)
|
||||||
(filepath (merge-pathnames path (staging *config*)))
|
(setf *last-revision* oldrev)
|
||||||
(page (funcall (theme-fn (or theme-fn 'base))
|
(load-content)
|
||||||
(list :config *config*
|
(compile-theme (theme *config*))
|
||||||
:content content
|
(let ((dir (staging-dir *config*)))
|
||||||
:raw (apply 'render content render-args)
|
(compile-blog dir)
|
||||||
:pubdate (make-pubdate)
|
(when deploy
|
||||||
:injections (find-injections content)))))
|
(deploy dir))))
|
||||||
(ensure-directories-exist filepath)
|
|
||||||
(with-open-file (out filepath
|
(defun load-content ()
|
||||||
:direction :output
|
"Load all content stored in the blog's repo."
|
||||||
:if-does-not-exist :create)
|
(do-subclasses (ctype content)
|
||||||
(write-line page out))))
|
(discover ctype))
|
||||||
|
(update-content-metadata)
|
||||||
|
(do-subclasses (itype index)
|
||||||
|
(discover itype)))
|
||||||
|
|
||||||
(defun compile-blog (staging)
|
(defun compile-blog (staging)
|
||||||
"Compile the blog to a STAGING directory as specified in .coleslawrc."
|
"Compile the blog to a STAGING directory as specified in .coleslawrc."
|
||||||
(when (probe-file staging)
|
|
||||||
(run-program "rm -R ~a" staging))
|
|
||||||
(ensure-directories-exist staging)
|
(ensure-directories-exist staging)
|
||||||
(with-current-directory staging
|
(with-current-directory staging
|
||||||
(dolist (dir (list (app-path "themes/~a/css" (theme *config*))
|
(let ((theme-dir (find-theme (theme *config*))))
|
||||||
(merge-pathnames "static" (repo *config*))))
|
(dolist (dir (list (merge-pathnames "css" theme-dir)
|
||||||
(when (probe-file dir)
|
(merge-pathnames "img" theme-dir)
|
||||||
(run-program "cp -R ~a ." dir)))
|
(merge-pathnames "js" theme-dir)
|
||||||
(render-posts)
|
(repo-path "static")))
|
||||||
(render-indices)
|
(when (probe-file dir)
|
||||||
(render-feeds (feeds *config*))))
|
(if (uiop:os-windows-p)
|
||||||
|
(run-program "(robocopy ~a ~a /MIR /IS) ^& IF %ERRORLEVEL% LEQ 1 exit 0" dir (path:basename dir))
|
||||||
|
(run-program "rsync --delete -raz ~a ." dir)))))
|
||||||
|
(do-subclasses (ctype content)
|
||||||
|
(publish ctype))
|
||||||
|
(do-subclasses (itype index)
|
||||||
|
(publish itype))
|
||||||
|
(let ((recent-posts (reduce #'(lambda (a b)
|
||||||
|
(if (< (index-name a) (index-name b))
|
||||||
|
a b))
|
||||||
|
(find-all 'numeric-index))))
|
||||||
|
(update-symlink "index.html" (page-url recent-posts)))))
|
||||||
|
|
||||||
(defgeneric deploy (staging)
|
(defgeneric deploy (staging)
|
||||||
(:documentation "Deploy the STAGING dir, updating the .prev and .curr symlinks.")
|
(:documentation "Deploy the STAGING build to the directory specified in the config.")
|
||||||
(:method (staging)
|
(:method (staging)
|
||||||
(with-current-directory coleslaw-conf:*basedir*
|
"By default, do nothing"
|
||||||
(let* ((coleslaw-conf:*basedir* (deploy *config*))
|
(declare)))
|
||||||
(new-build (app-path "generated/~a" (get-universal-time)))
|
|
||||||
(prev (app-path ".prev"))
|
|
||||||
(curr (app-path ".curr")))
|
|
||||||
(ensure-directories-exist new-build)
|
|
||||||
(run-program "mv ~a ~a" staging new-build)
|
|
||||||
(when (probe-file prev)
|
|
||||||
(let ((dest (truename prev)))
|
|
||||||
(if (equal prev dest)
|
|
||||||
(delete-file prev)
|
|
||||||
(run-program "rm -R ~a" dest))))
|
|
||||||
(when (probe-file curr)
|
|
||||||
(update-symlink prev (truename curr)))
|
|
||||||
(update-symlink curr new-build)))))
|
|
||||||
|
|
||||||
(defun main ()
|
(defun update-symlink (path target)
|
||||||
"Load the user's config, then compile and deploy the blog."
|
"Update the symlink at PATH to point to TARGET."
|
||||||
(load-config)
|
(run-program "ln -sfn ~a ~a" target path))
|
||||||
(load-posts)
|
|
||||||
(compile-theme (theme *config*))
|
(defun preview (path &optional (content-type 'post))
|
||||||
(compile-blog (staging *config*))
|
"Render the content at PATH under user's configured repo and save it to
|
||||||
(deploy (staging *config*)))
|
~/tmp.html. Load the user's config and theme if necessary."
|
||||||
|
(let ((current-working-directory (cl-fad:pathname-directory-pathname path)))
|
||||||
|
(unless *config*
|
||||||
|
(load-config (namestring current-working-directory))
|
||||||
|
(compile-theme (theme *config*)))
|
||||||
|
(let* ((file (rel-path (repo-dir *config*) path))
|
||||||
|
(content (construct content-type (read-content file))))
|
||||||
|
(write-file "tmp.html" (render-page content)))))
|
||||||
|
|
||||||
|
(defun render-page (content &optional theme-fn &rest render-args)
|
||||||
|
"Render the given CONTENT to HTML using THEME-FN if supplied.
|
||||||
|
Additional args to render CONTENT can be passed via RENDER-ARGS."
|
||||||
|
(funcall (or theme-fn (theme-fn 'base))
|
||||||
|
(list :config *config*
|
||||||
|
:content content
|
||||||
|
:raw (apply 'render content render-args)
|
||||||
|
:pubdate (format-rfc1123-timestring nil (local-time:now))
|
||||||
|
:injections (find-injections content))))
|
||||||
|
|
109
src/config.lisp
109
src/config.lisp
|
@ -1,42 +1,93 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
(defclass blog ()
|
(defclass blog ()
|
||||||
((author :initarg :author :initform "" :accessor author)
|
((author :initarg :author :reader author)
|
||||||
(deploy :initarg :deploy :initform nil :accessor deploy)
|
(charset :initarg :charset :reader charset)
|
||||||
(domain :initarg :domain :initform "" :accessor domain)
|
(deploy-dir :initarg :deploy-dir :reader deploy-dir)
|
||||||
(feeds :initarg :feeds :initform nil :accessor feeds)
|
(domain :initarg :domain :reader domain)
|
||||||
(license :initarg :license :initform nil :accessor license)
|
(excerpt-sep :initarg :excerpt-sep :reader excerpt-sep)
|
||||||
(plugins :initarg :plugins :initform nil :accessor plugins)
|
(feeds :initarg :feeds :reader feeds)
|
||||||
(repo :initarg :repo :initform #p"/" :accessor repo)
|
(name-fn :initarg :name-fn :reader name-fn)
|
||||||
(sitenav :initarg :sitenav :initform nil :accessor sitenav)
|
(lang :initarg :lang :reader lang)
|
||||||
(staging :initarg :staging :initform #p"/tmp/coleslaw/" :accessor staging)
|
(license :initarg :license :reader license)
|
||||||
(title :initarg :title :initform "" :accessor title)
|
(page-ext :initarg :page-ext :reader page-ext)
|
||||||
(theme :initarg :theme :initform "hyde" :accessor theme)))
|
(plugins :initarg :plugins :reader plugins)
|
||||||
|
(repo :initarg :repo :accessor repo-dir)
|
||||||
|
(routing :initarg :routing :reader routing)
|
||||||
|
(separator :initarg :separator :reader separator)
|
||||||
|
(sitenav :initarg :sitenav :reader sitenav)
|
||||||
|
(staging-dir :initarg :staging-dir :reader staging-dir)
|
||||||
|
(theme :initarg :theme :reader theme)
|
||||||
|
(title :initarg :title :reader title))
|
||||||
|
(:default-initargs
|
||||||
|
:feeds nil
|
||||||
|
:license nil
|
||||||
|
:plugins '((rsync "-avz" "--delete" "--exclude" ".git/" "--exclude" ".gitignore" "--copy-links"))
|
||||||
|
:sitenav nil
|
||||||
|
:excerpt-sep "<!--more-->"
|
||||||
|
:name-fn 'identity
|
||||||
|
:charset "UTF-8"
|
||||||
|
:lang "en"
|
||||||
|
:page-ext "html"
|
||||||
|
:separator ";;;;;"
|
||||||
|
:staging-dir "/tmp/coleslaw"))
|
||||||
|
|
||||||
|
(defun dir-slot-reader (config name)
|
||||||
|
"Take CONFIG and NAME, and return a directory pathname for the matching SLOT."
|
||||||
|
(ensure-directory-pathname (slot-value config name)))
|
||||||
|
|
||||||
|
(defmethod deploy-dir ((config blog)) (dir-slot-reader config 'deploy-dir))
|
||||||
|
(defmethod repo-dir ((config blog)) (dir-slot-reader config 'repo))
|
||||||
|
(defmethod staging-dir ((config blog)) (dir-slot-reader config 'staging-dir))
|
||||||
|
|
||||||
(defparameter *config* nil
|
(defparameter *config* nil
|
||||||
"A variable to store the blog configuration and plugin settings.")
|
"A variable to store the blog configuration and plugin settings.")
|
||||||
|
|
||||||
(defun enable-plugin (file &rest args)
|
(define-condition plugin-conf-error ()
|
||||||
"Given a path to a plugin, FILE, compile+load it, then call its ENABLE function."
|
((plugin :initarg :plugin :reader plugin)
|
||||||
(compile-file file)
|
(message :initarg :message :reader message))
|
||||||
(load file)
|
(:report (lambda (condition stream)
|
||||||
(let* ((pkgname (format nil "coleslaw-~a" (pathname-name file)))
|
(format stream "~A: ~A" (plugin condition) (message condition))))
|
||||||
(plugin-pkg (find-package (string-upcase pkgname))))
|
(:documentation "Condition to signal when the plugin is misconfigured."))
|
||||||
(apply (find-symbol "ENABLE" plugin-pkg) args)))
|
|
||||||
|
(defun enable-plugin (name args)
|
||||||
|
"Given a plugin, NAME, compile+load it and call its ENABLE function with ARGS."
|
||||||
|
(flet ((plugin-path (sym)
|
||||||
|
(if (probe-file (repo-path "plugins/~(~A~).lisp" sym))
|
||||||
|
(repo-path "plugins/~(~A~)" sym)
|
||||||
|
(app-path "plugins/~(~A~)" sym)))
|
||||||
|
(plugin-package (sym)
|
||||||
|
(format nil "~:@(coleslaw-~A~)" sym)))
|
||||||
|
(let ((file (plugin-path name)))
|
||||||
|
(multiple-value-bind (output-file error)
|
||||||
|
(ignore-errors (compile-file file :verbose nil :print nil))
|
||||||
|
(when error
|
||||||
|
(warn "Error while compiling plugin ~A: ~A.~%" name error))
|
||||||
|
(load (or output-file file) :verbose t)))
|
||||||
|
(let ((package (find-package (plugin-package name))))
|
||||||
|
(apply (find-symbol "ENABLE" package) args))))
|
||||||
|
|
||||||
(defun load-plugins (plugins)
|
(defun load-plugins (plugins)
|
||||||
"Compile and load the listed PLUGINS. It is expected that matching *.lisp files
|
"Compile and load the listed PLUGINS. It is expected that matching *.lisp files
|
||||||
are in the plugins folder in coleslaw's source directory."
|
are in the plugins folder in coleslaw's source directory."
|
||||||
(flet ((plugin-path (name)
|
(setf *injections* nil)
|
||||||
(app-path "plugins/~a" (string-downcase (symbol-name sym)))))
|
(dolist (plugin plugins)
|
||||||
(dolist (plugin plugins)
|
(destructuring-bind (name &rest args) plugin
|
||||||
(etypecase plugin
|
(enable-plugin name args))))
|
||||||
(list (destructuring-bind (name &rest args) plugin
|
|
||||||
(apply 'enable-plugin (plugin-path name) args)))
|
|
||||||
(symbol (enable-plugin (plugin-path plugin)))))))
|
|
||||||
|
|
||||||
(defun load-config (&optional (dir (user-homedir-pathname)))
|
(defun discover-config-path (repo-path)
|
||||||
"Load the coleslaw configuration from DIR/.coleslawrc. DIR is ~ by default."
|
"Check the supplied REPO-PATH for a .coleslawrc and if one
|
||||||
(with-open-file (in (merge-pathnames ".coleslawrc" dir))
|
doesn't exist, use the .coleslawrc in the home directory."
|
||||||
(setf *config* (apply #'make-instance 'blog (read in))))
|
(let ((repo-config (rel-path repo-path ".coleslawrc")))
|
||||||
|
(if (file-exists-p repo-config)
|
||||||
|
repo-config
|
||||||
|
(rel-path (user-homedir-pathname) ".coleslawrc"))))
|
||||||
|
|
||||||
|
(defun load-config (&optional (repo-dir ""))
|
||||||
|
"Find and load the coleslaw configuration from .coleslawrc. REPO-DIR will be
|
||||||
|
preferred over the home directory if provided."
|
||||||
|
(with-open-file (in (discover-config-path repo-dir) :external-format :utf-8)
|
||||||
|
(let ((config-form (read in)))
|
||||||
|
(setf *config* (construct 'blog config-form)
|
||||||
|
(repo-dir *config*) repo-dir)))
|
||||||
(load-plugins (plugins *config*)))
|
(load-plugins (plugins *config*)))
|
||||||
|
|
119
src/content.lisp
Normal file
119
src/content.lisp
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
(in-package :coleslaw)
|
||||||
|
|
||||||
|
;; Tagging
|
||||||
|
|
||||||
|
(defclass tag ()
|
||||||
|
((name :initarg :name :reader tag-name)
|
||||||
|
(slug :initarg :slug :reader tag-slug)
|
||||||
|
(url :initarg :url)))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((tag tag) &key)
|
||||||
|
(with-slots (url slug) tag
|
||||||
|
(setf url (compute-url nil slug 'tag-index))))
|
||||||
|
|
||||||
|
(defun make-tag (str)
|
||||||
|
"Takes a string and returns a TAG instance with a name and slug."
|
||||||
|
(let ((trimmed (string-trim " " str)))
|
||||||
|
(make-instance 'tag :name trimmed :slug (slugify trimmed))))
|
||||||
|
|
||||||
|
(defun tag-slug= (a b)
|
||||||
|
"Test if the slugs for tag A and B are equal."
|
||||||
|
(string= (tag-slug a) (tag-slug b)))
|
||||||
|
|
||||||
|
;; Slugs
|
||||||
|
|
||||||
|
(defun slug-char-p (char &key (allowed-chars (list #\- #\~)))
|
||||||
|
"Determine if CHAR is a valid slug (i.e. URL) character."
|
||||||
|
;; use the first char of the general unicode category as kind of
|
||||||
|
;; hyper general category
|
||||||
|
(let ((cat (char (cl-unicode:general-category char) 0))
|
||||||
|
(allowed-cats (list #\L #\N))) ; allowed Unicode categories in URLs
|
||||||
|
(cond
|
||||||
|
((member cat allowed-cats) t)
|
||||||
|
((member char allowed-chars) t)
|
||||||
|
(t nil))))
|
||||||
|
|
||||||
|
(defun unicode-space-p (char)
|
||||||
|
"Determine if CHAR is a kind of whitespace by unicode category means."
|
||||||
|
(char= (char (cl-unicode:general-category char) 0) #\Z))
|
||||||
|
|
||||||
|
(defun slugify (string)
|
||||||
|
"Return a version of STRING suitable for use as a URL."
|
||||||
|
(let ((slugified (remove-if-not #'slug-char-p
|
||||||
|
(substitute-if #\- #'unicode-space-p string))))
|
||||||
|
(if (zerop (length slugified))
|
||||||
|
(error "Post title '~a' does not contain characters suitable for a slug!" string )
|
||||||
|
slugified)))
|
||||||
|
|
||||||
|
;; Content Types
|
||||||
|
|
||||||
|
(defclass content ()
|
||||||
|
((url :initarg :url :reader page-url)
|
||||||
|
(date :initarg :date :reader content-date)
|
||||||
|
(file :initarg :file :reader content-file)
|
||||||
|
(tags :initarg :tags :reader content-tags)
|
||||||
|
(text :initarg :text :reader content-text))
|
||||||
|
(:default-initargs :tags nil :date nil))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((object content) &key)
|
||||||
|
(with-slots (tags) object
|
||||||
|
(when (stringp tags)
|
||||||
|
(setf tags (mapcar #'make-tag (cl-ppcre:split "," tags))))))
|
||||||
|
|
||||||
|
(defun parse-initarg (line)
|
||||||
|
"Given a metadata header, LINE, parse an initarg name/value pair from it."
|
||||||
|
(let ((name (string-upcase (subseq line 0 (position #\: line))))
|
||||||
|
(match (nth-value 1 (scan-to-strings "[a-zA-Z]+:\\s+(.*)" line))))
|
||||||
|
(when match
|
||||||
|
(list (make-keyword name) (aref match 0)))))
|
||||||
|
|
||||||
|
(defun parse-metadata (stream)
|
||||||
|
"Given a STREAM, parse metadata from it or signal an appropriate condition."
|
||||||
|
(flet ((get-next-line (input)
|
||||||
|
(string-trim '(#\Space #\Return #\Newline #\Tab) (read-line input nil))))
|
||||||
|
(unless (string= (get-next-line stream) (separator *config*))
|
||||||
|
(error "The file, ~a, lacks the expected header: ~a" (file-namestring stream) (separator *config*)))
|
||||||
|
(loop for line = (get-next-line stream)
|
||||||
|
until (string= line (separator *config*))
|
||||||
|
appending (parse-initarg line))))
|
||||||
|
|
||||||
|
(defun read-content (file)
|
||||||
|
"Returns a plist of metadata from FILE with :text holding the content."
|
||||||
|
(flet ((slurp-remainder (stream)
|
||||||
|
(let ((seq (make-string (- (file-length stream)
|
||||||
|
(file-position stream)))))
|
||||||
|
(read-sequence seq stream)
|
||||||
|
(remove #\Nul seq))))
|
||||||
|
(with-open-file (in file :external-format :utf-8)
|
||||||
|
(let ((metadata (parse-metadata in))
|
||||||
|
(content (slurp-remainder in))
|
||||||
|
(filepath (enough-namestring file (repo-dir *config*))))
|
||||||
|
(append metadata (list :text content :file filepath))))))
|
||||||
|
|
||||||
|
;; Helper Functions
|
||||||
|
|
||||||
|
(defun tag-p (tag obj)
|
||||||
|
"Test if OBJ is tagged with TAG."
|
||||||
|
(let ((tag (if (typep tag 'tag) tag (make-tag tag))))
|
||||||
|
(member tag (content-tags obj) :test #'tag-slug=)))
|
||||||
|
|
||||||
|
(defun month-p (month obj)
|
||||||
|
"Test if OBJ was written in MONTH."
|
||||||
|
(search month (content-date obj)))
|
||||||
|
|
||||||
|
(defun by-date (content)
|
||||||
|
"Sort CONTENT in reverse chronological order."
|
||||||
|
(sort content #'string> :key #'content-date))
|
||||||
|
|
||||||
|
(defun find-content-by-path (path)
|
||||||
|
"Find the CONTENT corresponding to the file at PATH."
|
||||||
|
(find path (find-all 'content) :key #'content-file :test #'string=))
|
||||||
|
|
||||||
|
(defgeneric render-text (text format)
|
||||||
|
(:documentation "Render TEXT of the given FORMAT to HTML for display.")
|
||||||
|
(:method (text (format (eql :html)))
|
||||||
|
text)
|
||||||
|
(:method (text (format (eql :md)))
|
||||||
|
(let ((3bmd-code-blocks:*code-blocks* t))
|
||||||
|
(with-output-to-string (str)
|
||||||
|
(3bmd:parse-string-and-print-to-stream text str)))))
|
84
src/documents.lisp
Normal file
84
src/documents.lisp
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
(in-package :coleslaw)
|
||||||
|
|
||||||
|
;;;; The Document Protocol
|
||||||
|
|
||||||
|
;; Data Storage
|
||||||
|
|
||||||
|
(defvar *site* (make-hash-table :test #'equal)
|
||||||
|
"An in-memory database to hold all site documents, keyed on relative URLs.")
|
||||||
|
|
||||||
|
;; Class Methods
|
||||||
|
|
||||||
|
(defgeneric publish (doc-type)
|
||||||
|
(:documentation "Write pages to disk for all documents of the given DOC-TYPE."))
|
||||||
|
|
||||||
|
(defgeneric discover (doc-type)
|
||||||
|
(:documentation "Load all documents of the given DOC-TYPE into memory.")
|
||||||
|
(:method (doc-type)
|
||||||
|
(let ((file-type (format nil "~(~A~)" (class-name doc-type))))
|
||||||
|
(do-files (file (repo-dir *config*) file-type)
|
||||||
|
(let ((obj (construct (class-name doc-type) (read-content file))))
|
||||||
|
(add-document obj))))))
|
||||||
|
|
||||||
|
(defmethod discover :before (doc-type)
|
||||||
|
(purge-all (class-name doc-type)))
|
||||||
|
|
||||||
|
;; Instance Methods
|
||||||
|
|
||||||
|
(defgeneric page-url (document)
|
||||||
|
(:documentation "The relative URL to the DOCUMENT."))
|
||||||
|
|
||||||
|
(defgeneric render (document &key &allow-other-keys)
|
||||||
|
(:documentation "Render the given DOCUMENT to HTML."))
|
||||||
|
|
||||||
|
;; Helper Functions
|
||||||
|
|
||||||
|
(defun compute-url (document unique-id &optional class)
|
||||||
|
"Compute the relative URL for a DOCUMENT based on its UNIQUE-ID. If CLASS
|
||||||
|
is provided, it overrides the route used."
|
||||||
|
(let* ((class-name (or class (class-name (class-of document))))
|
||||||
|
(route (get-route class-name)))
|
||||||
|
(unless route
|
||||||
|
(error "No routing method found for: ~A" class-name))
|
||||||
|
(let* ((result (format nil route unique-id))
|
||||||
|
(type (or (pathname-type result) (page-ext *config*))))
|
||||||
|
(make-pathname :name (funcall (name-fn *config*) (pathname-name result))
|
||||||
|
:type type
|
||||||
|
:defaults result))))
|
||||||
|
|
||||||
|
(defun get-route (doc-type)
|
||||||
|
"Return the route format string for DOC-TYPE."
|
||||||
|
(second (assoc (make-keyword doc-type) (routing *config*))))
|
||||||
|
|
||||||
|
(defun add-document (document)
|
||||||
|
"Add DOCUMENT to the in-memory database. Error if a matching entry is present."
|
||||||
|
(let ((url (page-url document)))
|
||||||
|
(if (gethash url *site*)
|
||||||
|
(error "There is already an existing document with the url ~a" url)
|
||||||
|
(setf (gethash url *site*) document))))
|
||||||
|
|
||||||
|
(defun delete-document (document)
|
||||||
|
"Given a DOCUMENT, delete it from the in-memory database."
|
||||||
|
(remhash (page-url document) *site*))
|
||||||
|
|
||||||
|
(defun write-document (document &optional theme-fn &rest render-args)
|
||||||
|
"Write the given DOCUMENT to disk as HTML. If THEME-FN is present,
|
||||||
|
use it as the template passing any RENDER-ARGS."
|
||||||
|
(let ((html (if (or theme-fn render-args)
|
||||||
|
(apply #'render-page document theme-fn render-args)
|
||||||
|
(render-page document nil)))
|
||||||
|
(url (namestring (page-url document))))
|
||||||
|
(write-file (rel-path (staging-dir *config*) url) html)))
|
||||||
|
|
||||||
|
(defun find-all (doc-type &optional (matches-p (lambda (x) (typep x doc-type))))
|
||||||
|
"Return a list of all instances of a given DOC-TYPE."
|
||||||
|
(loop for val being the hash-values in *site*
|
||||||
|
when (funcall matches-p val) collect val))
|
||||||
|
|
||||||
|
(defun purge-all (doc-type)
|
||||||
|
"Remove all instances of DOC-TYPE from memory."
|
||||||
|
(flet ((matches-class-name-p (x)
|
||||||
|
(class-name-p (symbol-name doc-type)
|
||||||
|
(class-of x))))
|
||||||
|
(dolist (obj (find-all doc-type #'matches-class-name-p))
|
||||||
|
(remhash (page-url obj) *site*))))
|
|
@ -1,30 +1,37 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
(defun date-to-timestamp (date)
|
;;; Atom and RSS Feeds
|
||||||
"Convert a post DATE to a local-time timestamp."
|
|
||||||
(destructuring-bind (date time) (cl-ppcre:split " " date)
|
|
||||||
(apply 'local-time:encode-timestamp 0
|
|
||||||
(mapcar #'parse-integer
|
|
||||||
(append (reverse (cl-ppcre:split ":" time))
|
|
||||||
(reverse (cl-ppcre:split "-" date)))))))
|
|
||||||
|
|
||||||
(defun make-pubdate (&optional date)
|
(defclass base-feed () ((format :initarg :format :reader feed-format)))
|
||||||
"Make a RFC1123 pubdate representing the current time or DATE, when supplied."
|
|
||||||
(let ((timestamp (if date
|
|
||||||
(date-to-timestamp date)
|
|
||||||
(local-time:now))))
|
|
||||||
(local-time:format-rfc1123-timestring nil timestamp)))
|
|
||||||
|
|
||||||
(defun render-feeds (feeds)
|
(defclass feed (index base-feed) ())
|
||||||
"Render and write the given FEEDS for the site."
|
|
||||||
(flet ((first-10 (list)
|
(defmethod discover ((doc-type (eql (find-class 'feed))))
|
||||||
(subseq list 0 (min (length list) 10))))
|
(let ((content (by-date (find-all 'post))))
|
||||||
(let* ((by-date (by-date (hash-table-values *posts*)))
|
(dolist (format '(rss atom))
|
||||||
(posts (first-10 by-date)))
|
(let ((feed (make-instance 'feed :format format
|
||||||
(render-page (make-instance 'index :path "rss.xml" :posts posts) :rss)
|
:content (take-up-to 10 content)
|
||||||
(render-page (make-instance 'index :path "feed.atom" :posts posts) :atom)
|
:slug (format nil "~(~a~)" format))))
|
||||||
(dolist (feed feeds)
|
(add-document feed)))))
|
||||||
(let ((index (index-by-tag feed by-date)))
|
|
||||||
(setf (index-path index) (format nil "tag/~a-rss.xml" feed)
|
(defmethod publish ((doc-type (eql (find-class 'feed))))
|
||||||
(index-posts index) (first-10 (index-posts index)))
|
(dolist (feed (find-all 'feed))
|
||||||
(render-page index :rss))))))
|
(write-document feed (theme-fn (feed-format feed) "feeds"))))
|
||||||
|
|
||||||
|
;;; Tag Feeds
|
||||||
|
|
||||||
|
(defclass tag-feed (index base-feed) ())
|
||||||
|
|
||||||
|
(defmethod discover ((doc-type (eql (find-class 'tag-feed))))
|
||||||
|
(let ((content (by-date (find-all 'post))))
|
||||||
|
(dolist (tag (feeds *config*))
|
||||||
|
(let ((tagged (remove-if-not (lambda (x) (tag-p tag x)) content)))
|
||||||
|
(dolist (format '(rss atom))
|
||||||
|
(let ((feed (make-instance 'tag-feed :format format
|
||||||
|
:content (take-up-to 10 tagged)
|
||||||
|
:slug (format nil "~a-~(~a~)" tag format))))
|
||||||
|
(add-document feed)))))))
|
||||||
|
|
||||||
|
(defmethod publish ((doc-type (eql (find-class 'tag-feed))))
|
||||||
|
(dolist (feed (find-all 'tag-feed))
|
||||||
|
(write-document feed (theme-fn (feed-format feed) "feeds"))))
|
||||||
|
|
109
src/indexes.lisp
Normal file
109
src/indexes.lisp
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
(in-package :coleslaw)
|
||||||
|
|
||||||
|
(defvar *all-months* nil
|
||||||
|
"The list of months in which content was authored.")
|
||||||
|
(defvar *all-tags* nil
|
||||||
|
"The list of tags which content has been tagged with.")
|
||||||
|
|
||||||
|
(defvar *pages-listings-on-index* 10
|
||||||
|
"Page listings in the index per page.")
|
||||||
|
|
||||||
|
(defclass index ()
|
||||||
|
((url :initarg :url :reader page-url)
|
||||||
|
(name :initarg :name :reader index-name)
|
||||||
|
(title :initarg :title :reader title-of)
|
||||||
|
(content :initarg :content :reader index-content)))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((object index) &key slug)
|
||||||
|
(with-slots (url) object
|
||||||
|
(setf url (compute-url object slug))))
|
||||||
|
|
||||||
|
(defmethod render ((object index) &key prev next)
|
||||||
|
(funcall (theme-fn 'index) (list :tags (find-all 'tag-index)
|
||||||
|
:months (find-all 'month-index)
|
||||||
|
:config *config*
|
||||||
|
:index object
|
||||||
|
:prev prev
|
||||||
|
:next next)))
|
||||||
|
|
||||||
|
;;; Index by Tag
|
||||||
|
|
||||||
|
(defclass tag-index (index) ())
|
||||||
|
|
||||||
|
(defmethod discover ((doc-type (eql (find-class 'tag-index))))
|
||||||
|
(let ((content (by-date (find-all 'post))))
|
||||||
|
(dolist (tag *all-tags*)
|
||||||
|
(add-document (index-by-tag tag content)))))
|
||||||
|
|
||||||
|
(defun index-by-tag (tag content)
|
||||||
|
"Return an index of all CONTENT matching the given TAG."
|
||||||
|
(make-instance 'tag-index :slug (tag-slug tag) :name (tag-name tag)
|
||||||
|
:content (remove-if-not (lambda (x) (tag-p tag x)) content)
|
||||||
|
:title (format nil "Content tagged ~a" (tag-name tag))))
|
||||||
|
|
||||||
|
(defmethod publish ((doc-type (eql (find-class 'tag-index))))
|
||||||
|
(dolist (index (find-all 'tag-index))
|
||||||
|
(write-document index)))
|
||||||
|
|
||||||
|
;;; Index by Month
|
||||||
|
|
||||||
|
(defclass month-index (index) ())
|
||||||
|
|
||||||
|
(defmethod discover ((doc-type (eql (find-class 'month-index))))
|
||||||
|
(let ((content (by-date (find-all 'post))))
|
||||||
|
(dolist (month *all-months*)
|
||||||
|
(add-document (index-by-month month content)))))
|
||||||
|
|
||||||
|
(defun index-by-month (month content)
|
||||||
|
"Return an index of all CONTENT matching the given MONTH."
|
||||||
|
(make-instance 'month-index :slug month :name month
|
||||||
|
:content (remove-if-not (lambda (x) (month-p month x)) content)
|
||||||
|
:title (format nil "Content from ~a" month)))
|
||||||
|
|
||||||
|
(defmethod publish ((doc-type (eql (find-class 'month-index))))
|
||||||
|
(dolist (index (find-all 'month-index))
|
||||||
|
(write-document index)))
|
||||||
|
|
||||||
|
;;; Reverse Chronological Index
|
||||||
|
|
||||||
|
(defclass numeric-index (index) ())
|
||||||
|
|
||||||
|
(defmethod discover ((doc-type (eql (find-class 'numeric-index))))
|
||||||
|
(let ((content (by-date (find-all 'post))))
|
||||||
|
(dotimes (i (ceiling (length content) *pages-listings-on-index*))
|
||||||
|
(add-document (index-by-n i content)))))
|
||||||
|
|
||||||
|
(defun index-by-n (i content)
|
||||||
|
"Return the index for the Ith page of CONTENT in reverse chronological order."
|
||||||
|
(let ((content (subseq content (* *pages-listings-on-index* i))))
|
||||||
|
(make-instance 'numeric-index :slug (1+ i) :name (1+ i)
|
||||||
|
:content (take-up-to *pages-listings-on-index* content)
|
||||||
|
:title "Recent Content")))
|
||||||
|
|
||||||
|
(defmethod publish ((doc-type (eql (find-class 'numeric-index))))
|
||||||
|
(let ((indexes (sort (find-all 'numeric-index) #'< :key #'index-name)))
|
||||||
|
(loop for (next index prev) on (append '(nil) indexes)
|
||||||
|
while index do (write-document index nil :prev prev :next next))))
|
||||||
|
|
||||||
|
;;; Helper Functions
|
||||||
|
|
||||||
|
(defun update-content-metadata ()
|
||||||
|
"Set *ALL-TAGS* and *ALL-MONTHS* to the union of all tags and months
|
||||||
|
of content loaded in the DB."
|
||||||
|
(setf *all-tags* (all-tags))
|
||||||
|
(setf *all-months* (all-months)))
|
||||||
|
|
||||||
|
(defun all-months ()
|
||||||
|
"Retrieve a list of all months with published content."
|
||||||
|
(let ((months (loop :for post :in (find-all 'post)
|
||||||
|
:for content-date := (content-date post)
|
||||||
|
:when content-date
|
||||||
|
:collect (subseq content-date 0
|
||||||
|
(min 7 (length content-date))))))
|
||||||
|
(sort (remove-duplicates months :test #'string=) #'string>)))
|
||||||
|
|
||||||
|
(defun all-tags ()
|
||||||
|
"Retrieve a list of all tags used in content."
|
||||||
|
(let* ((dupes (mappend #'content-tags (find-all 'post)))
|
||||||
|
(tags (remove-duplicates dupes :test #'tag-slug=)))
|
||||||
|
(sort tags #'string< :key #'tag-name)))
|
|
@ -1,70 +0,0 @@
|
||||||
(in-package :coleslaw)
|
|
||||||
|
|
||||||
(defclass index ()
|
|
||||||
((path :initform nil :initarg :path :accessor index-path)
|
|
||||||
(posts :initform nil :initarg :posts :accessor index-posts)
|
|
||||||
(title :initform nil :initarg :title :accessor index-title)))
|
|
||||||
|
|
||||||
(defmethod render ((content index) &key prev next)
|
|
||||||
(funcall (theme-fn 'index) (list :tags (all-tags)
|
|
||||||
:months (all-months)
|
|
||||||
:config *config*
|
|
||||||
:index content
|
|
||||||
:prev prev
|
|
||||||
:next next)))
|
|
||||||
|
|
||||||
(defun all-months ()
|
|
||||||
"Retrieve a list of all months with published posts."
|
|
||||||
(sort (remove-duplicates (mapcar (lambda (x) (get-month (post-date x)))
|
|
||||||
(hash-table-values *posts*)) :test #'string=)
|
|
||||||
#'string>))
|
|
||||||
|
|
||||||
(defun all-tags ()
|
|
||||||
"Retrieve a list of all tags used in posts."
|
|
||||||
(sort (remove-duplicates (mappend 'post-tags (hash-table-values *posts*))
|
|
||||||
:test #'string=) #'string<))
|
|
||||||
|
|
||||||
(defun get-month (timestamp)
|
|
||||||
"Extract the YYYY-MM portion of TIMESTAMP."
|
|
||||||
(subseq timestamp 0 7))
|
|
||||||
|
|
||||||
(defun by-date (posts)
|
|
||||||
"Sort POSTS in reverse chronological order."
|
|
||||||
(sort posts #'string> :key #'post-date))
|
|
||||||
|
|
||||||
(defun index-by-tag (tag posts)
|
|
||||||
"Return an index of all POSTS matching the given TAG."
|
|
||||||
(let ((content (remove-if-not (lambda (post) (member tag (post-tags post)
|
|
||||||
:test #'string=)) posts)))
|
|
||||||
(make-instance 'index :path (format nil "tag/~a.html" tag)
|
|
||||||
:posts content
|
|
||||||
:title (format nil "Posts tagged ~a" tag))))
|
|
||||||
|
|
||||||
(defun index-by-month (month posts)
|
|
||||||
"Return an index of all POSTS matching the given MONTH."
|
|
||||||
(let ((content (remove-if-not (lambda (post) (search month (post-date post)))
|
|
||||||
posts)))
|
|
||||||
(make-instance 'index :path (format nil "date/~a.html" month)
|
|
||||||
:posts content
|
|
||||||
:title (format nil "Posts from ~a" month))))
|
|
||||||
|
|
||||||
(defun index-by-n (i posts &optional (step 10))
|
|
||||||
"Return the index for the Ith page of POSTS in reverse chronological order."
|
|
||||||
(make-instance 'index :path (format nil "~d.html" (1+ i))
|
|
||||||
:posts (let ((index (* step i)))
|
|
||||||
(subseq posts index (min (length posts)
|
|
||||||
(+ index step))))
|
|
||||||
:title "Recent Posts"))
|
|
||||||
|
|
||||||
(defun render-indices ()
|
|
||||||
"Render the indices to view posts in groups of size N, by month, and by tag."
|
|
||||||
(let ((posts (by-date (hash-table-values *posts*))))
|
|
||||||
(dolist (tag (all-tags))
|
|
||||||
(render-page (index-by-tag tag posts)))
|
|
||||||
(dolist (month (all-months))
|
|
||||||
(render-page (index-by-month month posts)))
|
|
||||||
(dotimes (i (ceiling (length posts) 10))
|
|
||||||
(render-page (index-by-n i posts) nil
|
|
||||||
:prev (and (plusp i) i)
|
|
||||||
:next (and (< (* (1+ i) 10) (length posts)) (+ 2 i)))))
|
|
||||||
(update-symlink "index.html" "1.html"))
|
|
|
@ -4,11 +4,49 @@
|
||||||
(:import-from :alexandria #:hash-table-values
|
(:import-from :alexandria #:hash-table-values
|
||||||
#:make-keyword
|
#:make-keyword
|
||||||
#:mappend)
|
#:mappend)
|
||||||
|
(:import-from :cl-fad #:file-exists-p)
|
||||||
|
(:import-from :cl-ppcre #:scan-to-strings #:split)
|
||||||
(:import-from :closure-template #:compile-template)
|
(:import-from :closure-template #:compile-template)
|
||||||
|
(:import-from :local-time #:format-rfc1123-timestring)
|
||||||
|
(:import-from :uiop #:getcwd
|
||||||
|
#:ensure-directory-pathname)
|
||||||
(:export #:main
|
(:export #:main
|
||||||
#:blog
|
#:preview
|
||||||
|
#:*config*
|
||||||
|
;; Config Accessors
|
||||||
|
#:author
|
||||||
|
#:deploy-dir
|
||||||
|
#:domain
|
||||||
|
#:page-ext
|
||||||
|
#:repo-dir
|
||||||
|
#:staging-dir
|
||||||
|
#:title
|
||||||
|
;; Core Classes
|
||||||
|
#:content
|
||||||
#:post
|
#:post
|
||||||
#:index
|
#:index
|
||||||
|
;; Content Helpers
|
||||||
|
#:title-of
|
||||||
|
#:author-of
|
||||||
|
#:find-content-by-path
|
||||||
|
;; Theming + Plugin API
|
||||||
|
#:theme-fn
|
||||||
|
#:plugin-conf-error
|
||||||
|
#:render-text
|
||||||
#:add-injection
|
#:add-injection
|
||||||
#:render-content
|
#:get-updated-files
|
||||||
#:deploy))
|
#:deploy
|
||||||
|
;; The Document Protocol
|
||||||
|
#:discover
|
||||||
|
#:publish
|
||||||
|
#:page-url
|
||||||
|
#:render
|
||||||
|
#:find-all
|
||||||
|
#:purge-all
|
||||||
|
#:add-document
|
||||||
|
#:delete-document
|
||||||
|
#:write-document
|
||||||
|
#:content-text
|
||||||
|
|
||||||
|
;; Error reporting
|
||||||
|
#:assert-field))
|
||||||
|
|
|
@ -1,82 +1,31 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
(defparameter *posts* (make-hash-table :test #'equal)
|
(defclass post (content)
|
||||||
"A hash table to store all the posts and their metadata.")
|
((title :initarg :title :reader title-of)
|
||||||
|
(author :initarg :author :reader author-of)
|
||||||
|
(excerpt :initarg :excerpt :reader excerpt-of)
|
||||||
|
(format :initarg :format :reader post-format))
|
||||||
|
(:default-initargs :author nil :excerpt nil))
|
||||||
|
|
||||||
(defclass post ()
|
(defmethod initialize-instance :after ((object post) &key)
|
||||||
((slug :initform nil :initarg :slug :accessor post-slug)
|
(with-slots (url title author excerpt format text) object
|
||||||
(title :initform nil :initarg :title :accessor post-title)
|
(let (post-content)
|
||||||
(tags :initform nil :initarg :tags :accessor post-tags)
|
(setf url (compute-url object (slugify title))
|
||||||
(date :initform nil :initarg :date :accessor post-date)
|
format (make-keyword (string-upcase format))
|
||||||
(format :initform nil :initarg :format :accessor post-format)
|
post-content (render-text text format)
|
||||||
(content :initform nil :initarg :content :accessor post-content)))
|
excerpt (or excerpt
|
||||||
|
(first (split (excerpt-sep *config*)
|
||||||
|
post-content
|
||||||
|
:limit 2)))
|
||||||
|
text post-content
|
||||||
|
author (or author (author *config*))))))
|
||||||
|
|
||||||
(defmethod render ((content post) &key prev next)
|
(defmethod render ((object post) &key prev next)
|
||||||
(funcall (theme-fn 'post) (list :config *config*
|
(funcall (theme-fn 'post) (list :config *config*
|
||||||
:post content
|
:post object
|
||||||
:prev prev
|
:prev prev
|
||||||
:next next)))
|
:next next)))
|
||||||
|
|
||||||
(defun load-posts ()
|
(defmethod publish ((doc-type (eql (find-class 'post))))
|
||||||
"Read the stored .post files from the repo."
|
(loop for (next post prev) on (append '(nil) (by-date (find-all 'post)))
|
||||||
(clrhash *posts*)
|
while post do (write-document post nil :prev prev :next next)))
|
||||||
(do-files (file (repo *config*) "post")
|
|
||||||
(with-open-file (in file)
|
|
||||||
(let ((post (read-post in)))
|
|
||||||
(if (gethash (post-slug post) *posts*)
|
|
||||||
(error "There is already an existing post with the slug ~a."
|
|
||||||
(post-slug post))
|
|
||||||
(setf (gethash (post-slug post) *posts*) post))))))
|
|
||||||
|
|
||||||
(defun render-posts ()
|
|
||||||
"Iterate through the files in the repo to render+write the posts out to disk."
|
|
||||||
(loop for (prev post next) on (append '(nil) (sort (hash-table-values *posts*)
|
|
||||||
#'string< :key #'post-date))
|
|
||||||
while post do (render-page post nil :prev prev :next next)))
|
|
||||||
|
|
||||||
(defgeneric render-content (text format)
|
|
||||||
(:documentation "Compile TEXT from the given FORMAT to HTML for display.")
|
|
||||||
(:method (text (format (eql :html)))
|
|
||||||
text))
|
|
||||||
|
|
||||||
(defmethod render-content (text (format (eql :md)))
|
|
||||||
(let ((3bmd-code-blocks:*code-blocks* t))
|
|
||||||
(with-output-to-string (str)
|
|
||||||
(3bmd:parse-string-and-print-to-stream text str))))
|
|
||||||
|
|
||||||
(defun read-post (in)
|
|
||||||
"Make a POST instance based on the data from the stream IN."
|
|
||||||
(flet ((check-header ()
|
|
||||||
(unless (string= (read-line in) ";;;;;")
|
|
||||||
(error "The provided file lacks the expected header.")))
|
|
||||||
(parse-field (str)
|
|
||||||
(nth-value 1 (cl-ppcre:scan-to-strings "[a-zA-Z]+: (.*)" str)))
|
|
||||||
(read-tags (str)
|
|
||||||
(mapcar #'string-downcase (cl-ppcre:split ", " str)))
|
|
||||||
(slurp-remainder ()
|
|
||||||
(let ((seq (make-string (- (file-length in) (file-position in)))))
|
|
||||||
(read-sequence seq in)
|
|
||||||
(remove #\Nul seq))))
|
|
||||||
(check-header)
|
|
||||||
(let ((args (loop for field in '("title" "tags" "date" "format")
|
|
||||||
for line = (read-line in nil)
|
|
||||||
appending (list (make-keyword (string-upcase field))
|
|
||||||
(aref (parse-field line) 0)))))
|
|
||||||
(check-header)
|
|
||||||
(setf (getf args :tags) (read-tags (getf args :tags))
|
|
||||||
(getf args :format) (make-keyword (string-upcase (getf args :format))))
|
|
||||||
(apply 'make-instance 'post
|
|
||||||
(append args (list :content (render-content (slurp-remainder)
|
|
||||||
(getf args :format))
|
|
||||||
:slug (slugify (getf args :title))))))))
|
|
||||||
|
|
||||||
(defun slug-char-p (char)
|
|
||||||
"Determine if CHAR is a valid slug (i.e. URL) character."
|
|
||||||
(or (char<= #\0 char #\9)
|
|
||||||
(char<= #\a char #\z)
|
|
||||||
(char<= #\A char #\Z)
|
|
||||||
(member char '(#\_ #\- #\.))))
|
|
||||||
|
|
||||||
(defun slugify (string)
|
|
||||||
"Return a version of STRING suitable for use as a URL."
|
|
||||||
(remove-if-not #'slug-char-p (substitute #\- #\Space string)))
|
|
||||||
|
|
|
@ -1,45 +1,45 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
(defparameter *injections* ()
|
(defparameter *injections* '()
|
||||||
"A list that stores pairs of (string . predicate) to inject in the page.")
|
"A list that stores pairs of (string . predicate) to inject in the page.")
|
||||||
|
|
||||||
(defun add-injection (injection location)
|
(defun add-injection (injection location)
|
||||||
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be
|
"Adds an INJECTION to a given LOCATION for rendering. The INJECTION should be a
|
||||||
a string which will always be added or a (string . lambda). In the latter case,
|
function that takes a DOCUMENT and returns NIL or a STRING for template insertion."
|
||||||
the lambda takes a single argument, a content object, i.e. a POST or INDEX, and
|
(push injection (getf *injections* location)))
|
||||||
any return value other than nil indicates the injection should be added."
|
|
||||||
(let ((result (etypecase injection
|
|
||||||
(string (list injection #'identity))
|
|
||||||
(list injection))))
|
|
||||||
(push result (getf *injections* location))))
|
|
||||||
|
|
||||||
(defun find-injections (content)
|
(defun find-injections (content)
|
||||||
|
"Iterate over *INJECTIONS* collecting any that should be added to CONTENT."
|
||||||
(flet ((injections-for (location)
|
(flet ((injections-for (location)
|
||||||
(loop for (injection . predicate) in (getf *injections* location)
|
(loop for injection in (getf *injections* location)
|
||||||
when (funcall predicate content)
|
collecting (funcall injection content))))
|
||||||
collect injection)))
|
|
||||||
(list :head (injections-for :head)
|
(list :head (injections-for :head)
|
||||||
:body (injections-for :body))))
|
:body (injections-for :body))))
|
||||||
|
|
||||||
(defun theme-package (&key (name (theme *config*)))
|
(define-condition theme-does-not-exist (error)
|
||||||
"Find the package matching the theme NAME."
|
((theme :initarg :theme :reader theme))
|
||||||
(find-package (string-upcase (concatenate 'string "coleslaw.theme." name))))
|
(:report (lambda (c stream)
|
||||||
|
(format stream "Cannot find the theme: '~A'" (theme c)))))
|
||||||
|
|
||||||
(defun theme-fn (name)
|
(defun theme-package (name)
|
||||||
"Find the symbol NAME inside the current theme's package."
|
"Find the package matching the theme NAME or signal THEME-DOES-NOT-EXIST."
|
||||||
(find-symbol (princ-to-string name) (theme-package)))
|
(or (find-package (format nil "~:@(coleslaw.theme.~A~)" name))
|
||||||
|
(error 'theme-does-not-exist :theme name)))
|
||||||
|
|
||||||
|
(defun theme-fn (name &optional (package (theme *config*)))
|
||||||
|
"Find the symbol NAME inside PACKAGE which defaults to the theme package."
|
||||||
|
(find-symbol (princ-to-string name) (theme-package package)))
|
||||||
|
|
||||||
|
(defun find-theme (theme)
|
||||||
|
"Find the theme prefering themes in the local repo."
|
||||||
|
(let ((local-theme (repo-path "themes/~a/" theme)))
|
||||||
|
(if (probe-file local-theme)
|
||||||
|
local-theme
|
||||||
|
(app-path "themes/~a/" theme))))
|
||||||
|
|
||||||
(defun compile-theme (theme)
|
(defun compile-theme (theme)
|
||||||
"Locate and compile the templates for the given THEME."
|
"Locate and compile the templates for the given THEME."
|
||||||
(do-files (file (app-path "themes/~a/" theme) "tmpl")
|
(do-files (file (find-theme theme) "tmpl")
|
||||||
(compile-template :common-lisp-backend file))
|
(compile-template :common-lisp-backend file))
|
||||||
(do-files (file (app-path "themes/") "tmpl")
|
(do-files (file (app-path "themes/") "tmpl")
|
||||||
(compile-template :common-lisp-backend file)))
|
(compile-template :common-lisp-backend file)))
|
||||||
|
|
||||||
;; DOCUMENTATION
|
|
||||||
;; A theme directory should be named after the theme and contain *.tmpl files
|
|
||||||
;; that define the following functions in a coleslaw.theme.$NAME namespace.
|
|
||||||
;; Required templates:
|
|
||||||
;; {template base}
|
|
||||||
;; {template post}
|
|
||||||
;; {template index}
|
|
||||||
|
|
167
src/util.lisp
167
src/util.lisp
|
@ -1,54 +1,135 @@
|
||||||
(in-package :coleslaw)
|
(in-package :coleslaw)
|
||||||
|
|
||||||
|
|
||||||
|
(define-condition coleslaw-condition ()
|
||||||
|
())
|
||||||
|
|
||||||
|
(define-condition field-missing (error coleslaw-condition)
|
||||||
|
((field-name :initarg :field-name :reader missing-field-field-name)
|
||||||
|
(file :initarg :file :reader missing-field-file
|
||||||
|
:documentation "The path of the file where the field is missing."))
|
||||||
|
(:report
|
||||||
|
(lambda (c s)
|
||||||
|
(format s "~A: The required field ~A is missing."
|
||||||
|
(missing-field-file c)
|
||||||
|
(missing-field-field-name c)))))
|
||||||
|
|
||||||
|
(defmacro assert-field (field-name content)
|
||||||
|
`(when (not (slot-boundp ,content ,field-name))
|
||||||
|
(error 'field-missing
|
||||||
|
:field-name ,field-name
|
||||||
|
:file (content-file ,content))))
|
||||||
|
|
||||||
|
(defun construct (class-name args)
|
||||||
|
"Create an instance of CLASS-NAME with the given ARGS."
|
||||||
|
(apply 'make-instance class-name args))
|
||||||
|
|
||||||
|
;; Thanks to bknr-web for this bit of code.
|
||||||
|
(defun all-subclasses (class)
|
||||||
|
"Return a list of all the subclasses of CLASS."
|
||||||
|
(let ((subclasses (closer-mop:class-direct-subclasses class)))
|
||||||
|
(append subclasses (loop for subclass in subclasses
|
||||||
|
nconc (all-subclasses subclass)))))
|
||||||
|
|
||||||
|
(defmacro do-subclasses ((var class) &body body)
|
||||||
|
"Iterate over the subclasses of CLASS performing BODY with VAR
|
||||||
|
lexically bound to the current subclass."
|
||||||
|
(alexandria:with-gensyms (klasses)
|
||||||
|
`(let ((,klasses (all-subclasses (find-class ',class))))
|
||||||
|
(loop for ,var in ,klasses do ,@body))))
|
||||||
|
|
||||||
|
(defmacro do-files ((var path &optional extension) &body body)
|
||||||
|
"For each file under PATH, run BODY. If EXTENSION is provided, only run
|
||||||
|
BODY on files that match the given extension."
|
||||||
|
(alexandria:with-gensyms (extension-p)
|
||||||
|
`(flet ((,extension-p (file)
|
||||||
|
(string= (pathname-type file) ,extension)))
|
||||||
|
(cl-fad:walk-directory ,path (lambda (,var) ,@body)
|
||||||
|
:follow-symlinks nil
|
||||||
|
:test (if ,extension
|
||||||
|
#',extension-p
|
||||||
|
(constantly t))))))
|
||||||
|
|
||||||
|
(define-condition directory-does-not-exist (error)
|
||||||
|
((directory :initarg :dir :reader dir))
|
||||||
|
(:report (lambda (c stream)
|
||||||
|
(format stream "The directory '~A' does not exist" (dir c)))))
|
||||||
|
|
||||||
|
(defun (setf getcwd) (path)
|
||||||
|
"Change the operating system's current directory to PATH."
|
||||||
|
(setf path (ensure-directory-pathname path))
|
||||||
|
(unless (and (uiop:directory-exists-p path)
|
||||||
|
(uiop:chdir path))
|
||||||
|
(error 'directory-does-not-exist :dir path))
|
||||||
|
path)
|
||||||
|
|
||||||
|
(defmacro with-current-directory (path &body body)
|
||||||
|
"Change the current directory to PATH and execute BODY in
|
||||||
|
an UNWIND-PROTECT, then change back to the current directory."
|
||||||
|
(alexandria:with-gensyms (old)
|
||||||
|
`(let ((,old (getcwd)))
|
||||||
|
(unwind-protect (progn
|
||||||
|
(setf (getcwd) ,path)
|
||||||
|
,@body)
|
||||||
|
(setf (getcwd) ,old)))))
|
||||||
|
|
||||||
|
(defun exit ()
|
||||||
|
;; KLUDGE: Just call UIOP for now. Don't want users updating scripts.
|
||||||
|
"Exit the lisp system returning a 0 status code."
|
||||||
|
(uiop:quit))
|
||||||
|
|
||||||
|
(defun fmt (fmt-str args)
|
||||||
|
"A convenient FORMAT interface for string building."
|
||||||
|
(apply 'format nil fmt-str args))
|
||||||
|
|
||||||
|
(defun rel-path (base path &rest args)
|
||||||
|
"Take a relative PATH and return the corresponding pathname beneath BASE.
|
||||||
|
If ARGS is provided, use (fmt path args) as the value of PATH."
|
||||||
|
(merge-pathnames (fmt path args) base))
|
||||||
|
|
||||||
(defun app-path (path &rest args)
|
(defun app-path (path &rest args)
|
||||||
"Take a relative PATH and return the corresponding pathname beneath coleslaw.
|
"Return a relative path beneath coleslaw."
|
||||||
If ARGS is provided, use (apply 'format nil PATH ARGS) as the value of PATH."
|
(apply 'rel-path coleslaw-conf:*basedir* path args))
|
||||||
(merge-pathnames (apply 'format nil path args) coleslaw-conf:*basedir*))
|
|
||||||
|
(defun repo-path (path &rest args)
|
||||||
|
"Return a relative path beneath the repo being processed."
|
||||||
|
(apply 'rel-path (repo-dir *config*) path args))
|
||||||
|
|
||||||
(defun run-program (program &rest args)
|
(defun run-program (program &rest args)
|
||||||
"Take a PROGRAM and execute the corresponding shell command. If ARGS is provided,
|
"Take a PROGRAM and execute the corresponding shell command. If ARGS is provided,
|
||||||
use (apply 'format nil PROGRAM ARGS) as the value of PROGRAM."
|
use (fmt program args) as the value of PROGRAM."
|
||||||
(trivial-shell:shell-command (apply 'format nil program args)))
|
(inferior-shell:run (fmt program args) :show t))
|
||||||
|
|
||||||
(defun update-symlink (path target)
|
(defun run-lines (dir &rest programs)
|
||||||
"Update the symlink at PATH to point to TARGET."
|
"Runs some programs, in a directory."
|
||||||
(run-program "ln -sfn ~a ~a" target path))
|
(mapc (lambda (line)
|
||||||
|
(run-program "cd ~A && ~A" dir line))
|
||||||
|
programs))
|
||||||
|
|
||||||
(defmacro do-files ((var path &optional extension) &body body)
|
(defun take-up-to (n seq)
|
||||||
"For each file on PATH, run BODY. If EXTENSION is provided, only run BODY
|
"Take elements from SEQ until all elements or N have been taken."
|
||||||
on files that match the given extension."
|
(subseq seq 0 (min (length seq) n)))
|
||||||
(alexandria:with-gensyms (ext)
|
|
||||||
`(dolist (,var (cl-fad:list-directory ,path))
|
|
||||||
,@(if extension
|
|
||||||
`((let ((,ext (pathname-type ,var)))
|
|
||||||
(when (and ,ext (string= ,ext ,extension))
|
|
||||||
,@body)))
|
|
||||||
`,body))))
|
|
||||||
|
|
||||||
(defun current-directory ()
|
(defun write-file (path text)
|
||||||
"Return the operating system's current directory."
|
"Write the given TEXT to PATH. PATH is overwritten if it exists and created
|
||||||
#+sbcl (sb-posix:getcwd)
|
along with any missing parent directories otherwise."
|
||||||
#+ccl (current-directory)
|
(ensure-directories-exist path)
|
||||||
#+ecl (si:getcwd)
|
(with-open-file (out path
|
||||||
#+cmucl (unix:unix-current-directory)
|
:direction :output
|
||||||
#+clisp (ext:cd)
|
:if-exists :supersede
|
||||||
#-(or sbcl ccl ecl cmucl clisp) (error "Not implemented yet."))
|
:if-does-not-exist :create
|
||||||
|
:external-format :utf-8)
|
||||||
|
(write text :stream out :escape nil)))
|
||||||
|
|
||||||
(defun (setf current-directory) (path)
|
(defun get-updated-files (&optional (revision *last-revision*))
|
||||||
"Change the operating system's current directory to PATH."
|
"Return a plist of (file-status file-name) for files that were changed
|
||||||
#+sbcl (sb-posix:chdir path)
|
in the git repo since REVISION."
|
||||||
#+ccl (setf (current-directory) path)
|
(flet ((split-on-whitespace (str)
|
||||||
#+ecl (si:chdir path)
|
(cl-ppcre:split "\\s+" str)))
|
||||||
#+cmucl (unix:unix-chdir (namestring path))
|
(let ((cmd (format nil "git diff --name-status ~A HEAD" revision)))
|
||||||
#+clisp (ext:cd path)
|
(mapcar #'split-on-whitespace (inferior-shell:run/lines cmd)))))
|
||||||
#-(or sbcl ccl ecl cmucl clisp) (error "Not implemented yet."))
|
|
||||||
|
|
||||||
(defmacro with-current-directory (path &body body)
|
(defun class-name-p (name class)
|
||||||
"Change the current OS directory to PATH and execute BODY in
|
"True if the specified string is the name of the class provided"
|
||||||
an UNWIND-PROTECT, then change back to the current directory."
|
;; This feels way too clever. I wish I could think of a better option.
|
||||||
(alexandria:with-gensyms (old)
|
(string-equal name (symbol-name (class-name class))))
|
||||||
`(let ((,old (current-directory)))
|
|
||||||
(unwind-protect (progn
|
|
||||||
(setf (current-directory) ,path)
|
|
||||||
,@body)
|
|
||||||
(setf (current-directory) ,old)))))
|
|
||||||
|
|
23
tests/cli.lisp
Normal file
23
tests/cli.lisp
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
(in-package :coleslaw-tests)
|
||||||
|
|
||||||
|
(plan 2)
|
||||||
|
|
||||||
|
(let ((*default-pathname-defaults*
|
||||||
|
(pathname
|
||||||
|
(format nil "~a/"
|
||||||
|
(uiop:run-program `("mktemp" "-d")
|
||||||
|
:output `(:string :stripped t))))))
|
||||||
|
(coleslaw-cli:setup)
|
||||||
|
(let ((file (coleslaw-cli:new)))
|
||||||
|
(ok (probe-file file)))
|
||||||
|
(coleslaw-cli:deploy)
|
||||||
|
(print (format nil "~adeploy/index.html" *default-pathname-defaults*))
|
||||||
|
(ok (probe-file (format nil "~adeploy/index.html" *default-pathname-defaults*))))
|
||||||
|
|
||||||
|
(finalize)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
2
tests/files/.coleslawrc
Normal file
2
tests/files/.coleslawrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
;; -*- mode: lisp -*-
|
||||||
|
()
|
7
tests/files/127.txt
Normal file
7
tests/files/127.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
;;;;;
|
||||||
|
title: We should handle CR-LF
|
||||||
|
tags: fixtures
|
||||||
|
date: 2014-12-16
|
||||||
|
format: md
|
||||||
|
excerpt: An excerpt
|
||||||
|
;;;;;
|
15
tests/for-fixture-generation.lisp
Normal file
15
tests/for-fixture-generation.lisp
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
(in-package #:cl-user)
|
||||||
|
|
||||||
|
;; Code for generating some files in tests/files/
|
||||||
|
|
||||||
|
(defun write-with-cr-lf (line stream)
|
||||||
|
(format stream line)
|
||||||
|
(format stream "~A~A" #\Return #\Linefeed))
|
||||||
|
|
||||||
|
(with-open-file (out (asdf:system-relative-pathname :coleslaw-test "tests/files/127.txt") :direction :output :if-exists :overwrite)
|
||||||
|
(write-with-cr-lf ";;;;;" out)
|
||||||
|
(write-with-cr-lf "title: We should handle CR-LF" out)
|
||||||
|
(write-with-cr-lf "tags: fixtures" out)
|
||||||
|
(write-with-cr-lf "date: 2014-12-16" out)
|
||||||
|
(write-with-cr-lf "format: md" out)
|
||||||
|
(write-with-cr-lf ";;;;;" out))
|
|
@ -1,3 +0,0 @@
|
||||||
(defpackage :coleslaw-tests
|
|
||||||
(:use :cl :fiveam)
|
|
||||||
(:export #:run!))
|
|
61
tests/plugins/twitter-summary-card.lisp
Normal file
61
tests/plugins/twitter-summary-card.lisp
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
(defpackage :summary-card-tests
|
||||||
|
(:use :cl :coleslaw :stefil))
|
||||||
|
|
||||||
|
(in-package :summary-card-tests)
|
||||||
|
|
||||||
|
(defsuite summary-cards)
|
||||||
|
(in-suite summary-cards)
|
||||||
|
|
||||||
|
;; TODO: Create a fixture to either load a mocked config or load set of plugins.
|
||||||
|
;; Then wrap these tests to use that fixture. Then add these to defsystem, setup
|
||||||
|
;; general test run with other packages.
|
||||||
|
|
||||||
|
(coleslaw::enable-plugin :twitter-summary-card)
|
||||||
|
|
||||||
|
(defvar *short-post*
|
||||||
|
(make-instance 'post :title "hai" :text "very greetings" :format "html"))
|
||||||
|
|
||||||
|
(defvar *long-post*
|
||||||
|
(make-instance 'post :title "What a Wonderful World"
|
||||||
|
:text "I see trees of green, red roses too. I see them
|
||||||
|
bloom, for me and you. And I think to myself, what a wonderful world.
|
||||||
|
|
||||||
|
I see skies of blue,
|
||||||
|
And clouds of white.
|
||||||
|
The bright blessed day,
|
||||||
|
The dark sacred night.
|
||||||
|
And I think to myself,
|
||||||
|
What a wonderful world.
|
||||||
|
|
||||||
|
The colors of the rainbow,
|
||||||
|
So pretty in the sky.
|
||||||
|
Are also on the faces,
|
||||||
|
Of people going by,
|
||||||
|
I see friends shaking hands.
|
||||||
|
Saying, \"How do you do?\"
|
||||||
|
They're really saying,
|
||||||
|
\"I love you\".
|
||||||
|
|
||||||
|
I hear babies cry,
|
||||||
|
I watch them grow,
|
||||||
|
They'll learn much more,
|
||||||
|
Than I'll ever know.
|
||||||
|
And I think to myself,
|
||||||
|
What a wonderful world.
|
||||||
|
|
||||||
|
Yes, I think to myself,
|
||||||
|
What a wonderful world. " :format "html"))
|
||||||
|
|
||||||
|
(deftest summary-card-sans-twitter-handle ()
|
||||||
|
(let ((summary-card (summary-card *short-post* nil)))
|
||||||
|
(is (null (cl-ppcre:scan "twitter:author" summary-card)))))
|
||||||
|
|
||||||
|
(deftest summary-card-with-twitter-handle ()
|
||||||
|
(let ((summary-card (summary-card *short-post* "@PuercoPop")))
|
||||||
|
(is (cl-ppcre:scan "twitter:author" summary-card))))
|
||||||
|
|
||||||
|
(deftest summary-card-trims-long-post ()
|
||||||
|
(let ((summary-card (summary-card *long-post* nil)))
|
||||||
|
(multiple-value-bind ())
|
||||||
|
;; (scan "twitter:description\" content=\"(.*)\"" summary-card)
|
||||||
|
summary-card))
|
|
@ -1 +1,24 @@
|
||||||
|
(defpackage :coleslaw-tests
|
||||||
|
(:use :cl :prove))
|
||||||
|
|
||||||
(in-package :coleslaw-tests)
|
(in-package :coleslaw-tests)
|
||||||
|
|
||||||
|
(plan 4)
|
||||||
|
|
||||||
|
(diag "COLESLAW-CONF:*BASEDIR* points to Coleslaw's top level directory")
|
||||||
|
(is (car (last (pathname-directory coleslaw-conf:*basedir*)))
|
||||||
|
"coleslaw" :test #'string=)
|
||||||
|
(ok (probe-file (merge-pathnames #P"plugins" coleslaw-conf:*basedir*))
|
||||||
|
"COLESLAW-CONF:*BASEDIR* has a plugins sub-directory")
|
||||||
|
(ok (probe-file (merge-pathnames #P"themes" coleslaw-conf:*basedir*))
|
||||||
|
"COLESLAW-CONF:*BASEDIR* has a themes sub-directory")
|
||||||
|
|
||||||
|
|
||||||
|
(coleslaw::load-config (asdf:system-relative-pathname :coleslaw-test "tests/files/"))
|
||||||
|
|
||||||
|
(with-open-file (in (asdf:system-relative-pathname :coleslaw-test "tests/files/127.txt"))
|
||||||
|
(diag "PARSE-METADATA should handle files with CR-LF line endings.")
|
||||||
|
(is (coleslaw::parse-metadata in) '(:TITLE "We should handle CR-LF" :TAGS "fixtures" :DATE "2014-12-16" :FORMAT
|
||||||
|
"md" :EXCERPT "An excerpt") :test 'equalp))
|
||||||
|
|
||||||
|
(finalize)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{namespace coleslaw.theme.hyde}
|
{namespace coleslaw.theme.feeds}
|
||||||
|
|
||||||
{template atom}
|
{template atom}
|
||||||
<?xml version="1.0"?>{\n}
|
<?xml version="1.0"?>{\n}
|
||||||
|
@ -12,9 +12,9 @@
|
||||||
<name>{$config.author}</name>
|
<name>{$config.author}</name>
|
||||||
</author>
|
</author>
|
||||||
|
|
||||||
{foreach $post in $content.posts}
|
{foreach $post in $content.content}
|
||||||
<entry>
|
<entry>
|
||||||
<link type="text/html" rel="alternate" href="{$config.domain}/posts/{$post.slug}.html"/>
|
<link type="text/html" rel="alternate" href="{$config.domain}/{$post.url}"/>
|
||||||
<title>{$post.title}</title>
|
<title>{$post.title}</title>
|
||||||
<published>{$post.date}</published>
|
<published>{$post.date}</published>
|
||||||
<updated>{$post.date}</updated>
|
<updated>{$post.date}</updated>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<name>{$config.author}</name>
|
<name>{$config.author}</name>
|
||||||
<uri>{$config.domain}</uri>
|
<uri>{$config.domain}</uri>
|
||||||
</author>
|
</author>
|
||||||
<content type="html">{$post.content |noAutoescape}</content>
|
<content type="html">{$post.text |escapeHtml}</content>
|
||||||
</entry>
|
</entry>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
{template base}
|
{template base}
|
||||||
<!doctype html>{\n}
|
<!doctype html>{\n}
|
||||||
<html>
|
<html lang="{$config.lang}">
|
||||||
<head>
|
<head>
|
||||||
<title>{$config.title}</title>
|
<title>{$config.title}</title>
|
||||||
<meta http-equiv="content-type" content="application/xhtml+xml; charset=UTF-8" />
|
<meta http-equiv="content-type" content="text/html;" charset="{$config.charset}" />
|
||||||
<link href="http://fonts.googleapis.com/css?family=Vollkorn:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="http://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Vollkorn:regular,italic,bold,bolditalic" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="//fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet" type="text/css" />
|
||||||
<link href= "{$config.domain}/css/style.css" rel="stylesheet" type="text/css" />
|
<link href= "{$config.domain}/css/style.css" rel="stylesheet" type="text/css" />
|
||||||
<link rel="alternate" href="{$config.domain}/rss.xml" type="application/rss+xml" />
|
<link rel="alternate" href="{$config.domain}/rss.xml" type="application/rss+xml" />
|
||||||
{if $injections.head}
|
{if $injections.head}
|
||||||
|
@ -20,8 +21,12 @@
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<a href="{$config.domain}">{$config.title}</a> |
|
<a href="{$config.domain}">{$config.title}</a> |
|
||||||
{foreach $link in $config.sitenav}
|
{foreach $link in $config.sitenav}
|
||||||
<a href="{$link.url}">{$link.name}</a>
|
{if $link.relative}
|
||||||
{if not isLast($link)} | {/if}
|
<a href="{$config.domain}/{$link.url}">{$link.name}</a>
|
||||||
|
{else}
|
||||||
|
<a href="{$link.url}">{$link.name}</a>
|
||||||
|
{/if}
|
||||||
|
{if not isLast($link)} {sp}|{sp} {/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</div>
|
</div>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
|
@ -43,7 +48,9 @@
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
by {$config.author}
|
by {$config.author}
|
||||||
<img align="right" src="{$config.domain}/css/logo_small.jpg" />
|
<a id="coleslaw-logo" href="https://github.com/redline6561/coleslaw">
|
||||||
|
<img src="{$config.domain}/css/logo_small.jpg" alt="Coleslaw logo" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#content { background: #fff; padding-top: 1em }
|
#content { background: #fff; padding-top: 1em }
|
||||||
#header { float: right; margin-left: 1em; margin-bottom: 1em }
|
#header { float: right; margin-left: 1em; margin-bottom: 1em }
|
||||||
|
#coleslaw-logo { float: right; }
|
||||||
a { text-decoration: none; color: #992900 }
|
a { text-decoration: none; color: #992900 }
|
||||||
a.anchor { color: black }
|
a.anchor { color: black }
|
||||||
.date { font-style: italic }
|
.date { font-style: italic }
|
||||||
|
@ -46,3 +47,54 @@ span.paren5 { background-color : inherit; -webkit-transition: background-color 0
|
||||||
span.paren5:hover { color : inherit; background-color : #CAFFCA; }
|
span.paren5:hover { color : inherit; background-color : #CAFFCA; }
|
||||||
span.paren6 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
span.paren6 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
span.paren6:hover { color : inherit; background-color : #FFBAFF; }
|
span.paren6:hover { color : inherit; background-color : #FFBAFF; }
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
|
||||||
|
#content {
|
||||||
|
padding-top: 0em
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-left: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta, .article-content {
|
||||||
|
float: left;
|
||||||
|
margin-left: 0em;
|
||||||
|
margin-right: 0em;
|
||||||
|
width: 90%;
|
||||||
|
padding-left: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article, .article-content {
|
||||||
|
margin-left: 0em;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#coleslaw-logo {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#coleslaw-logo img {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fineprint a img {
|
||||||
|
width: auto;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,32 +2,32 @@
|
||||||
|
|
||||||
{template index}
|
{template index}
|
||||||
<h1 class="title">{$index.title}</h1>
|
<h1 class="title">{$index.title}</h1>
|
||||||
{foreach $post in $index.posts}
|
{foreach $obj in $index.content}
|
||||||
<div class="article-meta">
|
<div class="article-meta">
|
||||||
<a class="article-title" href="{$config.domain}/posts/{$post.slug}.html">{$post.title}</a>
|
<a class="article-title" href="{$config.domain}/{$obj.url}">{$obj.title}</a>
|
||||||
<div class="date"> posted on {$post.date}</div>
|
<div class="date"> posted on {$obj.date}</div>
|
||||||
<div class="article">{$post.content |noAutoescape}</div>
|
<div class="article">{$obj.excerpt |noAutoescape}</div>
|
||||||
</div>
|
</div>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
<div id="relative-nav">
|
<div id="relative-nav">
|
||||||
{if $prev} <a href="{$prev}.html">Previous</a> {/if}
|
{if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a> {/if}
|
||||||
{if $next} <a href="{$next}.html">Next</a> {/if}
|
{if $next} <a href="{$config.domain}/{$next.url}">Next</a> {/if}
|
||||||
</div>
|
</div>
|
||||||
{if $tags}
|
{if $tags}
|
||||||
<div id="tagsoup">
|
<div id="tagsoup">
|
||||||
<p>This blog covers
|
<p>This blog covers
|
||||||
{foreach $tag in $tags}
|
{foreach $tag in $tags}
|
||||||
<a href="{$config.domain}/tag/{$tag}.html">{$tag}</a>
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
{if not isLast($tag)}, {/if}
|
{if not isLast($tag)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{if $months}
|
{if $months}
|
||||||
<div id="monthsoup">
|
<div id="monthsoup">
|
||||||
<p>View posts from
|
<p>View content from
|
||||||
{foreach $month in $months}
|
{foreach $month in $months}
|
||||||
<a href="{$config.domain}/date/{$month}.html">{$month}</a>
|
<a href="{$config.domain}/{$month.url}">{$month.name}</a>{nil}
|
||||||
{if not isLast($month)}, {/if}
|
{if not isLast($month)},{sp}{/if}
|
||||||
{/foreach}
|
{/foreach}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -4,20 +4,24 @@
|
||||||
<div class="article-meta">{\n}
|
<div class="article-meta">{\n}
|
||||||
<h1 class="title">{$post.title}</h1>{\n}
|
<h1 class="title">{$post.title}</h1>{\n}
|
||||||
<div class="tags">{\n}
|
<div class="tags">{\n}
|
||||||
Tagged as {foreach $tag in $post.tags}
|
{if $post.tags}
|
||||||
<a href="../tag/{$tag}.html">{$tag}</a>
|
Tagged as {foreach $tag in $post.tags}
|
||||||
{if not isLast($tag)}, {/if}
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
{/foreach}
|
{if not isLast($tag)},{sp}{/if}
|
||||||
|
{/foreach}
|
||||||
|
{/if}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
<div class="date">{\n}
|
<div class="date">{\n}
|
||||||
Written on {$post.date}
|
{if $post.date}
|
||||||
|
Written on {$post.date}
|
||||||
|
{/if}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
<div class="article-content">{\n}
|
<div class="article-content">{\n}
|
||||||
{$post.content |noAutoescape}
|
{$post.text |noAutoescape}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
<div class="relative-nav">{\n}
|
<div class="relative-nav">{\n}
|
||||||
{if $prev} <a href="{$config.domain}/posts/{$prev.slug}.html">Previous</a><br> {/if}{\n}
|
{if $prev} <a href="{$config.domain}/{$prev.url}">Previous</a><br> {/if}{\n}
|
||||||
{if $next} <a href="{$config.domain}/posts/{$next.slug}.html">Next</a><br> {/if}{\n}
|
{if $next} <a href="{$config.domain}/{$next.url}">Next</a><br> {/if}{\n}
|
||||||
</div>{\n}
|
</div>{\n}
|
||||||
{/template}
|
{/template}
|
||||||
|
|
76
themes/readable/base.tmpl
Normal file
76
themes/readable/base.tmpl
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{namespace coleslaw.theme.readable}
|
||||||
|
|
||||||
|
{template base}
|
||||||
|
<!DOCTYPE html>{\n}
|
||||||
|
<html lang="{$config.lang}">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset={$config.charset}">
|
||||||
|
<title>{$config.title}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="{$config.domain}/css/bootstrap.min.css" rel="stylesheet" media="screen">
|
||||||
|
<link href="{$config.domain}/css/custom.css" rel="stylesheet" media="screen">
|
||||||
|
<link rel="alternate" href="{$config.domain}/rss.xml" type="application/rss+xml" />
|
||||||
|
{if $injections.head}
|
||||||
|
{foreach $injection in $injections.head}
|
||||||
|
{$injection |noAutoescape}
|
||||||
|
{/foreach}
|
||||||
|
{/if}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="offset2 span8">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="navbar navbar-inverse">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<a class="brand" href="{$config.domain}">{$config.title}</a>
|
||||||
|
<ul class="nav">
|
||||||
|
{foreach $link in $config.sitenav}
|
||||||
|
<li>
|
||||||
|
{if $link.relative}
|
||||||
|
<a href="{$config.domain}/{$link.url}">{$link.name}</a>
|
||||||
|
{else}
|
||||||
|
<a href="{$link.url}">{$link.name}</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
{$raw |noAutoescape}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{if $injections.body}
|
||||||
|
{foreach $injection in $injections.body}
|
||||||
|
<div class="row-fluid">
|
||||||
|
{$injection |noAutoescape}
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<hr>
|
||||||
|
<p class="fineprint">Unless otherwise credited all material
|
||||||
|
{if $config.license}
|
||||||
|
{$config.license}
|
||||||
|
{else}
|
||||||
|
<a rel="license" href="http://creativecommons.org/licenses/by-sa/3.0/deed.en_US">
|
||||||
|
<img alt="Creative Commons License" style="border-width:0" src="{$config.domain}/img/cc-by-sa.png" />
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
by {$config.author}
|
||||||
|
<a id="coleslaw-logo" href="https://github.com/redline6561/coleslaw">
|
||||||
|
<img src="{$config.domain}/img/logo_small.jpg" alt="Coleslaw logo" /></p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{$config.domain}/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{/template}
|
9
themes/readable/css/bootstrap.min.css
vendored
Normal file
9
themes/readable/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
42
themes/readable/css/custom.css
Normal file
42
themes/readable/css/custom.css
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#coleslaw-logo { float: right; }
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));
|
||||||
|
background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));
|
||||||
|
background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));
|
||||||
|
background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));
|
||||||
|
}
|
||||||
|
pre { overflow: auto; }
|
||||||
|
p.date-posted { font-style: italic; }
|
||||||
|
p.fineprint { font-size: smaller; }
|
||||||
|
|
||||||
|
/* Stolen from lisppaste for the colorize output of 3bmd */
|
||||||
|
.paste { background-color: #F4F4F4; color: black; }
|
||||||
|
.paste:hover { background-color: #F4F4F4; color: black; }
|
||||||
|
.symbol { color : #770055; background-color : transparent; border: 0px; margin: 0px;}
|
||||||
|
.special { color : #FF5000; background-color : inherit; }
|
||||||
|
.keyword { color : #770000; background-color : inherit; }
|
||||||
|
.comment { color : #007777; background-color : inherit; }
|
||||||
|
.string { color : #777777; background-color : inherit; }
|
||||||
|
.atom { color : #314F4F; background-color : inherit; }
|
||||||
|
.macro { color : #FF5000; background-color : inherit; }
|
||||||
|
.variable { color : #36648B; background-color : inherit; }
|
||||||
|
.function { color : #8B4789; background-color : inherit; }
|
||||||
|
.attribute { color : #FF5000; background-color : inherit; }
|
||||||
|
.character { color : #0055AA; background-color : inherit; }
|
||||||
|
.syntaxerror { color : #FF0000; background-color : inherit; }
|
||||||
|
.diff-deleted { color : #5F2121; background-color : inherit; }
|
||||||
|
.diff-added { color : #215F21; background-color : inherit; }
|
||||||
|
span.paren1 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
|
span.paren1:hover { color : inherit; background-color : #BAFFFF; }
|
||||||
|
span.paren2 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
|
span.paren2:hover { color : inherit; background-color : #FFCACA; }
|
||||||
|
span.paren3 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
|
span.paren3:hover { color : inherit; background-color : #FFFFBA; }
|
||||||
|
span.paren4 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
|
span.paren4:hover { color : inherit; background-color : #CACAFF; }
|
||||||
|
span.paren5 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
|
span.paren5:hover { color : inherit; background-color : #CAFFCA; }
|
||||||
|
span.paren6 { background-color : inherit; -webkit-transition: background-color 0.2s linear; }
|
||||||
|
span.paren6:hover { color : inherit; background-color : #FFBAFF; }
|
BIN
themes/readable/img/cc-by-sa.png
Normal file
BIN
themes/readable/img/cc-by-sa.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 672 B |
BIN
themes/readable/img/glyphicons-halflings-white.png
Normal file
BIN
themes/readable/img/glyphicons-halflings-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
themes/readable/img/glyphicons-halflings.png
Normal file
BIN
themes/readable/img/glyphicons-halflings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
themes/readable/img/logo_small.jpg
Normal file
BIN
themes/readable/img/logo_small.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
38
themes/readable/index.tmpl
Normal file
38
themes/readable/index.tmpl
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{namespace coleslaw.theme.readable}
|
||||||
|
|
||||||
|
{template index}
|
||||||
|
<h1 class="page-header">{$index.title}</h1>
|
||||||
|
{foreach $obj in $index.content}
|
||||||
|
<div class="row-fluid">
|
||||||
|
<h1><a href="{$config.domain}/{$obj.url}">{$obj.title}</a></h1>
|
||||||
|
<p class="date-posted">posted on {$obj.date}</p>
|
||||||
|
{$obj.excerpt |noAutoescape}
|
||||||
|
</div>
|
||||||
|
{/foreach}
|
||||||
|
<div id="relative-nav">
|
||||||
|
<ul class="pager">
|
||||||
|
{if $prev} <li class="previous"><a href="{$config.domain}/{$prev.url}">Previous</a></li> {/if}
|
||||||
|
{if $next} <li class="next"><a href="{$config.domain}/{$next.url}">Next</a></li> {/if}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{if $tags}
|
||||||
|
<div class="row-fluid">
|
||||||
|
<p>This blog covers
|
||||||
|
{foreach $tag in $tags}
|
||||||
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
|
{if not isLast($tag)},{sp}{/if}
|
||||||
|
{/foreach}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{if $months}
|
||||||
|
<div class="row-fluid">
|
||||||
|
<p>View content from
|
||||||
|
{foreach $month in $months}
|
||||||
|
<a href="{$config.domain}/{$month.url}">{$month.name}</a>{nil}
|
||||||
|
{if not isLast($month)},{sp}{/if}
|
||||||
|
{/foreach}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/template}
|
6
themes/readable/js/bootstrap.min.js
vendored
Normal file
6
themes/readable/js/bootstrap.min.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/*!
|
||||||
|
* Bootstrap without jQuery v0.3.1
|
||||||
|
* By Daniel Davis under MIT License
|
||||||
|
* https://github.com/tagawa/bootstrap-without-jquery
|
||||||
|
*/
|
||||||
|
;(function(){"use strict";function o(n){for(var f=n.children,r=0,i,t=0,u=f.length;t<u;t++)i=f[t],r+=Math.max(i.clientHeight,i.offsetHeight,i.scrollHeight);return r}function e(n){n=n||window.event;var f=n.currentTarget||n.srcElement,r=f.getAttribute("data-target"),t=document.querySelector(r),u=o(t),i=" "+t.className+" ";return i.indexOf(" in ")>-1?(i=i.replace(" in "," "),t.className=i,t.style.height="0"):(t.className+=" in ",t.style.height=u+"px"),!1}function c(n){n=n||window.event;var r=n.currentTarget||n.srcElement,i=r.parentElement,t=" "+i.className+" ";return t.indexOf(" open ")>-1?(t=t.replace(" open "," "),i.className=t):i.className+=" open ",!1}function h(n){n=n||window.event;var i=n.currentTarget||n.srcElement,t=i.parentElement;return t.className=(" "+t.className+" ").replace(" open "," "),!1}function s(n){n=n||window.event;var i=n.currentTarget||n.srcElement,t=i.parentElement;return t.parentElement.removeChild(t),!1}var f,u,i,r,n,t;for(document.querySelectorAll||(document.querySelectorAll=function(n){var f=document.styleSheets[0]||document.createStyleSheet(),r,i,t,u;for(f.addRule(n,"foo:bar"),r=document.all,i=[],t=0,u=r.length;t<u;t++)r[t].currentStyle.foo==="bar"&&(i[i.length]=r[t]);return f.removeRule(0),i}),f=document.querySelectorAll("[data-toggle=collapse]"),n=0,t=f.length;n<t;n++)f[n].onclick=e;for(u=document.querySelectorAll("[data-toggle=dropdown]"),n=0,t=u.length;n<t;n++)i=u[n],i.setAttribute("tabindex","0"),i.onclick=c,i.onblur=h;for(r=document.querySelectorAll("[data-dismiss=alert]"),n=0,t=r.length;n<t;n++)r[n].onclick=s})();
|
27
themes/readable/post.tmpl
Normal file
27
themes/readable/post.tmpl
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{namespace coleslaw.theme.readable}
|
||||||
|
|
||||||
|
{template post}
|
||||||
|
<div class="row-fluid">{\n}
|
||||||
|
<h1 class="page-header">{$post.title}</h1>{\n}
|
||||||
|
<p>
|
||||||
|
{if $post.tags}
|
||||||
|
Tagged as {foreach $tag in $post.tags}
|
||||||
|
<a href="{$config.domain}/{$tag.url}">{$tag.name}</a>{nil}
|
||||||
|
{if not isLast($tag)},{sp}{/if}
|
||||||
|
{/foreach}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<p class="date-posted">
|
||||||
|
{if $post.date}
|
||||||
|
Written on {$post.date}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{$post.text |noAutoescape}
|
||||||
|
|
||||||
|
<ul class="pager">
|
||||||
|
{if $prev}<li class="previous"><a href="{$config.domain}/{$prev.url}">← Previous</a></li>{/if}{\n}
|
||||||
|
{if $next}<li class="next"><a href="{$config.domain}/{$next.url}">Next →</a></li>{/if}{\n}
|
||||||
|
</ul>
|
||||||
|
</div>{\n}
|
||||||
|
{/template}
|
|
@ -1,4 +1,4 @@
|
||||||
{namespace coleslaw.theme.hyde}
|
{namespace coleslaw.theme.feeds}
|
||||||
|
|
||||||
{template rss}
|
{template rss}
|
||||||
<?xml version="1.0"?>{\n}
|
<?xml version="1.0"?>{\n}
|
||||||
|
@ -10,17 +10,17 @@
|
||||||
<language>en-us</language>
|
<language>en-us</language>
|
||||||
<pubDate>{$pubdate}</pubDate>
|
<pubDate>{$pubdate}</pubDate>
|
||||||
|
|
||||||
{foreach $post in $content.posts}
|
{foreach $post in $content.content}
|
||||||
<item>
|
<item>
|
||||||
<title>{$post.title}</title>
|
<title>{$post.title}</title>
|
||||||
<link>{$config.domain}/posts/{$post.slug}.html</link>
|
<link>{$config.domain}/{$post.url}</link>
|
||||||
<pubDate>{$post.date}</pubDate>
|
<pubDate>{$post.date}</pubDate>
|
||||||
<author>{$config.author}</author>
|
<author>{$config.author}</author>
|
||||||
<guid isPermaLink="true">{$config.domain}/posts/{$post.slug}.html</guid>
|
<guid isPermaLink="true">{$config.domain}/{$post.url}</guid>
|
||||||
{foreach $tag in $post.tags}
|
{foreach $tag in $post.tags}
|
||||||
<category><![CDATA[ {$tag} ]]></category>
|
<category><![CDATA[ {$tag.name |noAutoescape} ]]></category>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
<description><![CDATA[ {$post.content |noAutoescape} ]]></description>
|
<description><![CDATA[ {$post.text |noAutoescape} ]]></description>
|
||||||
</item>
|
</item>
|
||||||
{/foreach}
|
{/foreach}
|
||||||
|
|
||||||
|
|
13
themes/sitemap.tmpl
Normal file
13
themes/sitemap.tmpl
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{namespace coleslaw.theme.sitemap}
|
||||||
|
|
||||||
|
{template sitemap}
|
||||||
|
<?xml version="1.0"?>{\n}
|
||||||
|
<urlset xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>
|
||||||
|
{foreach $url in $content.urls}
|
||||||
|
<url>
|
||||||
|
<loc>{$config.domain}/{$url}</loc>
|
||||||
|
<lastmod>{$pubdate}</lastmod>
|
||||||
|
</url>
|
||||||
|
{/foreach}
|
||||||
|
</urlset>
|
||||||
|
{/template}
|
Loading…
Add table
Reference in a new issue