mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-27 16:13:52 -05:00
Compare commits
1262 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b29f8a717 | ||
|
|
94569a42da | ||
|
|
97747621aa | ||
|
|
723d9df6c7 | ||
|
|
45e2158e84 | ||
|
|
59017ea918 | ||
|
|
f857ab294c | ||
|
|
938a9126b0 | ||
|
|
23724b0a6b | ||
|
|
7bdea7c085 | ||
|
|
2bcae6b0a9 | ||
|
|
6457b381d8 | ||
|
|
9eb6fe0483 | ||
|
|
674733864f | ||
|
|
3d39bf4adb | ||
|
|
55830b9fca | ||
|
|
8bd3c52abf | ||
|
|
de8202a67f | ||
|
|
8f277a0d39 | ||
|
|
d63aeb6526 | ||
|
|
eb83d080f6 | ||
|
|
710ec2755d | ||
|
|
5d64d0e5f2 | ||
|
|
718ceecfa2 | ||
|
|
09616e31af | ||
|
|
7e7a3b495e | ||
|
|
d39deca76a | ||
|
|
8385e750ec | ||
|
|
8571d70b52 | ||
|
|
4b5ea14ee1 | ||
|
|
c53379e3ac | ||
|
|
6bf7fadc29 | ||
|
|
d193775a3b | ||
|
|
8e4bc4aecd | ||
|
|
2afad43bdd | ||
|
|
5445458d1a | ||
|
|
ef476a7329 | ||
|
|
262afdc2f8 | ||
|
|
ef10e720a5 | ||
|
|
fff6087f36 | ||
|
|
922f0d8ad4 | ||
|
|
ab1325b213 | ||
|
|
63b159741b | ||
|
|
4a9a517f27 | ||
|
|
700839f77f | ||
|
|
152132458e | ||
|
|
054da77b6a | ||
|
|
9a5e8dbb0a | ||
|
|
63c45cd879 | ||
|
|
bc7ca458b6 | ||
|
|
07fb33f5da | ||
|
|
7d1566860c | ||
|
|
f0117ec3df | ||
|
|
22065ebc79 | ||
|
|
5905bf1d85 | ||
|
|
1646e4923a | ||
|
|
1e190137c3 | ||
|
|
4417f8cb0a | ||
|
|
cc7f8be496 | ||
|
|
051fc9f01d | ||
|
|
d0bde467e0 | ||
|
|
475951c9ee | ||
|
|
5b2cc1735b | ||
|
|
1c00395230 | ||
|
|
9799326590 | ||
|
|
2546c8cc60 | ||
|
|
33c9b4063e | ||
|
|
f66fd92f32 | ||
|
|
541e16335b | ||
|
|
6cc40cb5f7 | ||
|
|
37f8a79d4d | ||
|
|
385246a9ef | ||
|
|
7619f9a91c | ||
|
|
f27c3aea4c | ||
|
|
b3bd103742 | ||
|
|
7e5fa3565b | ||
|
|
6d9f49cbc5 | ||
|
|
8670bcc540 | ||
|
|
005f4d648a | ||
|
|
59343b525d | ||
|
|
e8d7dd6f57 | ||
|
|
54175698d5 | ||
|
|
07ed00e8f7 | ||
|
|
ab8a4dfa5a | ||
|
|
e2c954f693 | ||
|
|
b2111a3f91 | ||
|
|
8ba833feb2 | ||
|
|
7124f4c7de | ||
|
|
1cc4cbb202 | ||
|
|
405f407260 | ||
|
|
eaa778bebd | ||
|
|
a17c1d1b95 | ||
|
|
8a27d1b7ac | ||
|
|
d6cd50b601 | ||
|
|
68d9bcfec4 | ||
|
|
3fa49214ad | ||
|
|
ddf02e0c0c | ||
|
|
3992bc18f5 | ||
|
|
49f9c9741e | ||
|
|
f84b1a15b6 | ||
|
|
ac11e00aa2 | ||
|
|
860e8d109e | ||
|
|
adcbd117da | ||
|
|
08d9fddfc9 | ||
|
|
118c1096ea | ||
|
|
18813516e1 | ||
|
|
a6944683e6 | ||
|
|
f27ae21825 | ||
|
|
58af25384d | ||
|
|
51a80febb3 | ||
|
|
7945ac8e8c | ||
|
|
a097fe6232 | ||
|
|
4b5004b621 | ||
|
|
7b7dee4a4e | ||
|
|
5282083dec | ||
|
|
816913bd72 | ||
|
|
1620858032 | ||
|
|
4a07076896 | ||
|
|
3a2705d742 | ||
|
|
b0229e76a5 | ||
|
|
a43ef4ca4b | ||
|
|
780fd83cac | ||
|
|
d5406c8dff | ||
|
|
5e7b6d7d9d | ||
|
|
07d2989486 | ||
|
|
5fe7e9531d | ||
|
|
f958a0e8cf | ||
|
|
dbad189b26 | ||
|
|
4a06e067c5 | ||
|
|
98a093a0ff | ||
|
|
9b5c57bf48 | ||
|
|
87e9cc0ac3 | ||
|
|
e37e5314f8 | ||
|
|
4913bf82f1 | ||
|
|
4e41c844c3 | ||
|
|
0ae202ea85 | ||
|
|
71083144e8 | ||
|
|
7eb63bf0e3 | ||
|
|
283b2798e4 | ||
|
|
91842c3db6 | ||
|
|
fd88719b68 | ||
|
|
c438e13027 | ||
|
|
8e9697bd8b | ||
|
|
dcf7ae45c2 | ||
|
|
38fa5b5ebc | ||
|
|
41262665e8 | ||
|
|
21dc3b0eb1 | ||
|
|
44cd371066 | ||
|
|
1c766160c3 | ||
|
|
2cb4093cc8 | ||
|
|
dad941454e | ||
|
|
2a93606727 | ||
|
|
78819f4525 | ||
|
|
2c7d192b50 | ||
|
|
e15f80407d | ||
|
|
4fc72e8d9f | ||
|
|
b4c53b9e9c | ||
|
|
2011ddb479 | ||
|
|
4a28995641 | ||
|
|
4c397a9be0 | ||
|
|
f1b413444b | ||
|
|
cd1b54f41f | ||
|
|
83c307be3c | ||
|
|
9ec49d978f | ||
|
|
7dee816838 | ||
|
|
5b79ed4144 | ||
|
|
aa96e1174e | ||
|
|
7fcacf3386 | ||
|
|
2aa2b95f0f | ||
|
|
797112ef36 | ||
|
|
f24221a1d7 | ||
|
|
6223189b95 | ||
|
|
73aeed48d7 | ||
|
|
09c3e542da | ||
|
|
9fae3c88eb | ||
|
|
8159dea9ea | ||
|
|
e5386f9230 | ||
|
|
5fafc3f1f6 | ||
|
|
39a148fc8d | ||
|
|
873e4fe40f | ||
|
|
604d4dd78a | ||
|
|
ddeb3ce54f | ||
|
|
15958773d5 | ||
|
|
ba4c3ed873 | ||
|
|
53d39a8654 | ||
|
|
f1731329e1 | ||
|
|
51d7998ba4 | ||
|
|
d27a2bdd1d | ||
|
|
cd3e26fb90 | ||
|
|
1c034bdd9a | ||
|
|
2b242494b0 | ||
|
|
d4763d2c90 | ||
|
|
03443f723c | ||
|
|
c3f4b8114a | ||
|
|
21e8c08827 | ||
|
|
de155f859b | ||
|
|
737090abf1 | ||
|
|
fb4fa867d1 | ||
|
|
c606a010c7 | ||
|
|
09f05e8a86 | ||
|
|
1daa650d61 | ||
|
|
2474ae799b | ||
|
|
f393e22896 | ||
|
|
3629e2c051 | ||
|
|
e7b15274b5 | ||
|
|
d15a40123c | ||
|
|
166a972e9a | ||
|
|
f2db034587 | ||
|
|
a4140ba535 | ||
|
|
e3bb655663 | ||
|
|
8bb9a7ff42 | ||
|
|
3e673ce424 | ||
|
|
787882069e | ||
|
|
e6fffcf9ec | ||
|
|
c78d463864 | ||
|
|
14f249071c | ||
|
|
0dc65a1ac4 | ||
|
|
8f46ad8575 | ||
|
|
6dd8be2ab2 | ||
|
|
29b71be0a5 | ||
|
|
f3bb6ce7ff | ||
|
|
0ce670d124 | ||
|
|
ebcd293fee | ||
|
|
5eaae38bf4 | ||
|
|
be63b44038 | ||
|
|
30d3aeb691 | ||
|
|
06af133838 | ||
|
|
327417373f | ||
|
|
1b55d4a389 | ||
|
|
ac1674845f | ||
|
|
148d282065 | ||
|
|
c0dc4b7367 | ||
|
|
6f3fac763c | ||
|
|
73a1652b64 | ||
|
|
9b5e5e7c0f | ||
|
|
d071d127ef | ||
|
|
321a76bd27 | ||
|
|
8c5b72ca3f | ||
|
|
3e421d353d | ||
|
|
3cd055c508 | ||
|
|
189eea5beb | ||
|
|
3f45b024f2 | ||
|
|
a40f1f281b | ||
|
|
6c847275c5 | ||
|
|
aced9f609c | ||
|
|
82a7df41a6 | ||
|
|
f6c11d12e0 | ||
|
|
313be7162f | ||
|
|
800fb54aeb | ||
|
|
45e700db00 | ||
|
|
24c7ffcd62 | ||
|
|
ec436adca2 | ||
|
|
b8ad85c16f | ||
|
|
6be8e526d6 | ||
|
|
f4012757a7 | ||
|
|
0722d81295 | ||
|
|
402d11414c | ||
|
|
988ed9b5bc | ||
|
|
4a47b3d18a | ||
|
|
82a457b548 | ||
|
|
54a5d749fc | ||
|
|
c177081104 | ||
|
|
1b00525574 | ||
|
|
dbb51d32db | ||
|
|
b4641d2830 | ||
|
|
03f2806cae | ||
|
|
4498739095 | ||
|
|
29ae40c3ec | ||
|
|
6746df7ce9 | ||
|
|
a0a01ecd60 | ||
|
|
21f2435182 | ||
|
|
338a9b424e | ||
|
|
25c47ed0bc | ||
|
|
0f0d1f3377 | ||
|
|
ae1a8c362c | ||
|
|
0043043bb3 | ||
|
|
e9e3bb6ddd | ||
|
|
03ba7d9089 | ||
|
|
d7892f5601 | ||
|
|
0a29ba6eb6 | ||
|
|
4d9095b603 | ||
|
|
235c1f87f0 | ||
|
|
5d44ef91dc | ||
|
|
e7084e5548 | ||
|
|
4637d5f5d1 | ||
|
|
4bac54883b | ||
|
|
2dc8c5e686 | ||
|
|
7b3e6973be | ||
|
|
9fce4ad74c | ||
|
|
76c5b6967a | ||
|
|
04ff12a206 | ||
|
|
3236ec0aea | ||
|
|
ff5e85af51 | ||
|
|
c1b631d086 | ||
|
|
39ce6123b6 | ||
|
|
01da420f76 | ||
|
|
df037d132d | ||
|
|
534725cbb8 | ||
|
|
4c948b4547 | ||
|
|
bba1216df1 | ||
|
|
3e5ec749ba | ||
|
|
7e0949175a | ||
|
|
b51d3e5106 | ||
|
|
f47066f68f | ||
|
|
9ac0eb288a | ||
|
|
79e9ae48a1 | ||
|
|
d29072783f | ||
|
|
e284eb1c30 | ||
|
|
b09c588ca9 | ||
|
|
a6a60eef31 | ||
|
|
95128b6662 | ||
|
|
824e2a7681 | ||
|
|
43e690dd13 | ||
|
|
4a2d3a7e85 | ||
|
|
eda77b81db | ||
|
|
8befcc74d1 | ||
|
|
5956092b4b | ||
|
|
c25e866796 | ||
|
|
1d1274e53a | ||
|
|
841c68d05e | ||
|
|
37273ba8e0 | ||
|
|
91f04bc7ba | ||
|
|
7cce45818a | ||
|
|
84fbd679e7 | ||
|
|
aef12bbb20 | ||
|
|
00eba964ce | ||
|
|
cf7762f57f | ||
|
|
26319f4124 | ||
|
|
f8e66db41c | ||
|
|
529cfc34ec | ||
|
|
8053774ba3 | ||
|
|
fe76ee626f | ||
|
|
f6c062fc98 | ||
|
|
97d9078a32 | ||
|
|
a397f64356 | ||
|
|
fad53f720f | ||
|
|
dcfb527342 | ||
|
|
e0a4fb1ea9 | ||
|
|
6e6518a7ae | ||
|
|
12fc0ff5c3 | ||
|
|
b8a7b6e846 | ||
|
|
9229e80499 | ||
|
|
ae6c4522bb | ||
|
|
fdebbfdce2 | ||
|
|
40745600a3 | ||
|
|
5a31947eb7 | ||
|
|
d758753551 | ||
|
|
9b27b14985 | ||
|
|
f5fc54461a | ||
|
|
6aac696ee1 | ||
|
|
c8571f592c | ||
|
|
7eccd1d556 | ||
|
|
06324d8b24 | ||
|
|
753780f653 | ||
|
|
3087686559 | ||
|
|
6805740e20 | ||
|
|
8f3b6b4776 | ||
|
|
3278f84373 | ||
|
|
12285f505d | ||
|
|
e123879c8c | ||
|
|
7bcdfe6f0f | ||
|
|
29f936b1eb | ||
|
|
bd3e555962 | ||
|
|
02b6628048 | ||
|
|
4ae5a53791 | ||
|
|
fc76a47d6e | ||
|
|
a224bfd7d7 | ||
|
|
f51d89ba02 | ||
|
|
461884f109 | ||
|
|
bc3399cc22 | ||
|
|
4a655ff2a3 | ||
|
|
877a3af671 | ||
|
|
1499e850ac | ||
|
|
c7b67e363b | ||
|
|
d5a505e0c6 | ||
|
|
0de13cf5a9 | ||
|
|
d6d5d6e674 | ||
|
|
5264074c1b | ||
|
|
702c676107 | ||
|
|
a387846b20 | ||
|
|
0eabdb0169 | ||
|
|
92836f3988 | ||
|
|
05c6a99446 | ||
|
|
68893f785f | ||
|
|
f6dd0a4a13 | ||
|
|
432b4296ab | ||
|
|
6b1dc01a3f | ||
|
|
9d8e99f8d7 | ||
|
|
e152e197c1 | ||
|
|
7e68d01e7d | ||
|
|
bd97611eb0 | ||
|
|
8e579072b8 | ||
|
|
a918910a52 | ||
|
|
1eeb0d23e6 | ||
|
|
c842b5d06e | ||
|
|
0ac89dd826 | ||
|
|
7ec083e426 | ||
|
|
15c93b56ed | ||
|
|
ec6f26e652 | ||
|
|
cdce9a7666 | ||
|
|
63432355e6 | ||
|
|
4bf2b472bb | ||
|
|
1476ec72c3 | ||
|
|
d1c09791ab | ||
|
|
16b99be17f | ||
|
|
bcd4552bdf | ||
|
|
f942f3835e | ||
|
|
e96c608c11 | ||
|
|
e6315bf2b1 | ||
|
|
6d63343b46 | ||
|
|
e1e4518499 | ||
|
|
34d68403da | ||
|
|
e395341210 | ||
|
|
f4c54178c8 | ||
|
|
63e1dac122 | ||
|
|
e8dff6f97e | ||
|
|
534666f551 | ||
|
|
f3ee794283 | ||
|
|
94f9a9c5e0 | ||
|
|
dc6b0a6e58 | ||
|
|
2fa13cf4e0 | ||
|
|
d64a863223 | ||
|
|
1fb91d67f6 | ||
|
|
1b046e5a90 | ||
|
|
e9f5e3d7c0 | ||
|
|
c6a5d05c64 | ||
|
|
f93b2675ff | ||
|
|
365918bf89 | ||
|
|
76be253f76 | ||
|
|
0210e69abc | ||
|
|
cdbf6d2806 | ||
|
|
902ded9f89 | ||
|
|
851932bd4b | ||
|
|
f38dc687e3 | ||
|
|
391ee3bae2 | ||
|
|
ad3096e871 | ||
|
|
179bd8dcd5 | ||
|
|
426e7bee17 | ||
|
|
564c80bf6d | ||
|
|
363c12e9c3 | ||
|
|
1ffa9fe830 | ||
|
|
91d13c390a | ||
|
|
ce8d8120f8 | ||
|
|
038b5fb232 | ||
|
|
fd768efba2 | ||
|
|
481346812c | ||
|
|
3d07798832 | ||
|
|
33da0c26c4 | ||
|
|
91e94024cd | ||
|
|
c9ad6dbf10 | ||
|
|
2b455fcd34 | ||
|
|
4573f4d882 | ||
|
|
63ae0d5c18 | ||
|
|
52e406bf95 | ||
|
|
3e871d1971 | ||
|
|
84a5ba9707 | ||
|
|
44d9f4e95b | ||
|
|
d24c0ca0d7 | ||
|
|
623fc606a4 | ||
|
|
a8aee21cd0 | ||
|
|
649a021647 | ||
|
|
785ee564c5 | ||
|
|
006f99ee99 | ||
|
|
2a4e5140c9 | ||
|
|
3c10b00096 | ||
|
|
c9ddb4dd98 | ||
|
|
9822c2a178 | ||
|
|
199efd0f2c | ||
|
|
17b197620b | ||
|
|
23abd20589 | ||
|
|
7e9be8dee3 | ||
|
|
09d22e926f | ||
|
|
1696f5680e | ||
|
|
a54ee0215e | ||
|
|
9b12c5130f | ||
|
|
084771bcd5 | ||
|
|
7215d60c67 | ||
|
|
0224190ec0 | ||
|
|
ae2fc9a9d1 | ||
|
|
d65d2b2a8e | ||
|
|
69972080f0 | ||
|
|
5f2453e446 | ||
|
|
20f71af4cb | ||
|
|
efc5ee4449 | ||
|
|
14d412b279 | ||
|
|
707319e004 | ||
|
|
bdd16e25fa | ||
|
|
9a1f983e65 | ||
|
|
c2c37705e7 | ||
|
|
5f227613aa | ||
|
|
0274ad6e87 | ||
|
|
dd27c4f8ba | ||
|
|
25b9acc321 | ||
|
|
10fae8596c | ||
|
|
909bd1c54e | ||
|
|
f324aebdec | ||
|
|
5a84d641cd | ||
|
|
0b577ebd76 | ||
|
|
2056c87e28 | ||
|
|
8bfa6462f8 | ||
|
|
a8660048ca | ||
|
|
cad8988f8d | ||
|
|
3fce1151dd | ||
|
|
d23bdaa527 | ||
|
|
2f10831a80 | ||
|
|
a38a30da1e | ||
|
|
82000d917f | ||
|
|
f482aeaca3 | ||
|
|
86638abea9 | ||
|
|
a2cf838baf | ||
|
|
5bc25e32eb | ||
|
|
15c6f3f318 | ||
|
|
cb2a63ea0a | ||
|
|
50dfa365c7 | ||
|
|
3e22a5cdad | ||
|
|
5034707a73 | ||
|
|
d815b0cc52 | ||
|
|
fca149a52c | ||
|
|
b4221680e4 | ||
|
|
ba448a9dd5 | ||
|
|
aa29ef04a2 | ||
|
|
20d42a53d3 | ||
|
|
8c8f0a4dbf | ||
|
|
6904653a82 | ||
|
|
74e01ea6e3 | ||
|
|
0732cb47b9 | ||
|
|
29338b5ade | ||
|
|
4019060ef4 | ||
|
|
3e1d750efa | ||
|
|
41bfbc69e6 | ||
|
|
6fdd7b4a17 | ||
|
|
c6d9f15cba | ||
|
|
0f397ebdb5 | ||
|
|
342b6ee7b5 | ||
|
|
9952ac15a5 | ||
|
|
7add0dbf10 | ||
|
|
03470a7531 | ||
|
|
dd778d50f9 | ||
|
|
ac3e4b6c1e | ||
|
|
3706ddc5cc | ||
|
|
adcea9b3b9 | ||
|
|
ba8107120c | ||
|
|
b9e433710d | ||
|
|
f10d23e893 | ||
|
|
12b4a9631a | ||
|
|
36fa0064de | ||
|
|
566a42c4f7 | ||
|
|
6e143ce2a1 | ||
|
|
46963ebf65 | ||
|
|
c948fe4d6a | ||
|
|
b0ef5a54cc | ||
|
|
fbc875dd9f | ||
|
|
e6b1413d22 | ||
|
|
1d3b99c0df | ||
|
|
8181445d99 | ||
|
|
14aeb0cb83 | ||
|
|
3e6b42cfba | ||
|
|
c57a8fcfc4 | ||
|
|
9d6fcc9afe | ||
|
|
ea8f0f6161 | ||
|
|
06e8f6f849 | ||
|
|
36e5525ea5 | ||
|
|
e5d5f5d02b | ||
|
|
98088c91dd | ||
|
|
a4f7d11e92 | ||
|
|
4c7e85ba82 | ||
|
|
9693c4af05 | ||
|
|
3052fe3827 | ||
|
|
c85c3eb292 | ||
|
|
0d734bbb03 | ||
|
|
b9b34f97d9 | ||
|
|
ee59e22603 | ||
|
|
22a6dca69b | ||
|
|
4f698f813c | ||
|
|
97f1948681 | ||
|
|
7acc7a03a8 | ||
|
|
0ed1cbe486 | ||
|
|
2c382a58d3 | ||
|
|
b7a27d2759 | ||
|
|
d67dbc74fd | ||
|
|
d9d27f38c3 | ||
|
|
4886c92fa4 | ||
|
|
195d97a514 | ||
|
|
e954e872f0 | ||
|
|
e74b4b0507 | ||
|
|
4946c78e8c | ||
|
|
54d8d37b77 | ||
|
|
20eea0b41e | ||
|
|
8835bdc47e | ||
|
|
7a1977a78c | ||
|
|
629c09df4d | ||
|
|
7247e5f9a1 | ||
|
|
a3c0ecdb45 | ||
|
|
b20b1757a9 | ||
|
|
1bc2ebd775 | ||
|
|
a56cffeb4e | ||
|
|
c908ac8cc5 | ||
|
|
b47d1cff33 | ||
|
|
780daa73ae | ||
|
|
9114905a93 | ||
|
|
a7aaef1e85 | ||
|
|
9823246ecd | ||
|
|
861940ba4b | ||
|
|
3ed302467e | ||
|
|
f54356da10 | ||
|
|
418d677584 | ||
|
|
a0eb8c0a0e | ||
|
|
67b4260021 | ||
|
|
7c6d47e8b6 | ||
|
|
43281c85c5 | ||
|
|
e73d3b7cfa | ||
|
|
1de8cf8ba6 | ||
|
|
85afbe466e | ||
|
|
63b312948a | ||
|
|
3a8faba645 | ||
|
|
6d6bee0dc9 | ||
|
|
4b266f1ebc | ||
|
|
fc7ef59dee | ||
|
|
5fa9f12427 | ||
|
|
07b25cdb64 | ||
|
|
105d836fbc | ||
|
|
74fcaf5273 | ||
|
|
74200f7395 | ||
|
|
a7ca2e169f | ||
|
|
1a5286b25c | ||
|
|
c493d3b5e3 | ||
|
|
a68091a84c | ||
|
|
32daca669a | ||
|
|
cf5a78c0e1 | ||
|
|
b0cf568ba4 | ||
|
|
bf544be282 | ||
|
|
4f0dba8fdb | ||
|
|
5390e44dec | ||
|
|
0fb814c6d6 | ||
|
|
e7e3317ff0 | ||
|
|
d68a596455 | ||
|
|
ace2abff34 | ||
|
|
0c6439faad | ||
|
|
e7418f21f9 | ||
|
|
19146c403e | ||
|
|
66ded2302f | ||
|
|
98abb22be1 | ||
|
|
ab304e7d38 | ||
|
|
fbc21592af | ||
|
|
e7b69114ed | ||
|
|
8a9ecd212d | ||
|
|
ec157cd1cd | ||
|
|
d3bcb359fa | ||
|
|
2a4e5583ab | ||
|
|
3978612611 | ||
|
|
4941acdb87 | ||
|
|
7e3d2f96ab | ||
|
|
ddba36b24c | ||
|
|
35cf96a064 | ||
|
|
5777a0edc4 | ||
|
|
53c3a92285 | ||
|
|
82db7f5b93 | ||
|
|
879449447f | ||
|
|
132ca0dca3 | ||
|
|
56c2b9ba3a | ||
|
|
542b6feed1 | ||
|
|
2af44a396f | ||
|
|
40d91fff29 | ||
|
|
59eab7cfc2 | ||
|
|
1b524ff356 | ||
|
|
9b873e9d97 | ||
|
|
b6d6cb2711 | ||
|
|
c8095160d0 | ||
|
|
ae6db3a87e | ||
|
|
18f57f5bd9 | ||
|
|
09a37284b0 | ||
|
|
dff5ac64e5 | ||
|
|
0ee565a5ff | ||
|
|
9e4854f349 | ||
|
|
74d48f5ad2 | ||
|
|
0b51a74c16 | ||
|
|
ce63cc31f4 | ||
|
|
d6720fc671 | ||
|
|
64cca1399b | ||
|
|
629c2ad2fd | ||
|
|
d325e821cd | ||
|
|
ac3a7faa54 | ||
|
|
35ed24cd18 | ||
|
|
81d42f1c6e | ||
|
|
618a2fa78b | ||
|
|
0bf6751eed | ||
|
|
f92eac4acd | ||
|
|
69ef52fd13 | ||
|
|
cc8ce35b4d | ||
|
|
2a13ca2fbf | ||
|
|
59e6afcaad | ||
|
|
4d9a455a27 | ||
|
|
74b2c79d46 | ||
|
|
ed407b091c | ||
|
|
6c8020a3b9 | ||
|
|
42f18d1e51 | ||
|
|
abf3e4ab50 | ||
|
|
d1078434af | ||
|
|
8f024dabc3 | ||
|
|
0c580c32c4 | ||
|
|
90960126e8 | ||
|
|
aa37f24fc1 | ||
|
|
3f4f287e6e | ||
|
|
55fe75c716 | ||
|
|
c6236ead67 | ||
|
|
68e3572278 | ||
|
|
27ab7eb2f0 | ||
|
|
6d183be0ec | ||
|
|
c83a34b473 | ||
|
|
d3e0e597d2 | ||
|
|
271bbba7dd | ||
|
|
86ff2e1e6b | ||
|
|
6ef7cc0ccb | ||
|
|
f4cf32e768 | ||
|
|
47384c1f18 | ||
|
|
9e3d533acc | ||
|
|
5ec4f65ac3 | ||
|
|
4a330ae36f | ||
|
|
d93fbc0f6b | ||
|
|
684bb78897 | ||
|
|
d0dd16c527 | ||
|
|
f4805343f8 | ||
|
|
f9add3e936 | ||
|
|
1fd9656291 | ||
|
|
6f281a6401 | ||
|
|
5194d2b3cd | ||
|
|
b3c23c5f88 | ||
|
|
a15134cc2f | ||
|
|
b51bb101f2 | ||
|
|
59d26dbbe7 | ||
|
|
94baf19e6a | ||
|
|
f1a446fb02 | ||
|
|
01d1242753 | ||
|
|
203685e91c | ||
|
|
2cb5b85ab2 | ||
|
|
ec996d3509 | ||
|
|
5ed3223185 | ||
|
|
3bdcc0a5a6 | ||
|
|
1e4d4887e1 | ||
|
|
94b922d27a | ||
|
|
0a8f9a3f6b | ||
|
|
a5d4d1e0ad | ||
|
|
2bdb5866a7 | ||
|
|
65932289f7 | ||
|
|
e34f9c6408 | ||
|
|
e0e13e375e | ||
|
|
4c333bee95 | ||
|
|
965f7bde0d | ||
|
|
cfcba01d03 | ||
|
|
3dc40f1742 | ||
|
|
5a366f5707 | ||
|
|
b960c697dc | ||
|
|
0383b26f46 | ||
|
|
8884008b4d | ||
|
|
af3012b0f2 | ||
|
|
3d6caa504f | ||
|
|
87213edf39 | ||
|
|
4f081bb5ce | ||
|
|
78d356d2ed | ||
|
|
64dc7f41d8 | ||
|
|
cb0a992d8d | ||
|
|
c2eb375f69 | ||
|
|
a555c6b6b2 | ||
|
|
0752fa4e43 | ||
|
|
f14fc61b4b | ||
|
|
89878519b4 | ||
|
|
46d57bcf3c | ||
|
|
f3e85da9a7 | ||
|
|
83444650a3 | ||
|
|
dae7490739 | ||
|
|
5bc87d5c17 | ||
|
|
7a58c415de | ||
|
|
09576d7d57 | ||
|
|
0bfcd3c9ce | ||
|
|
bf3de7a80d | ||
|
|
7269c372d6 | ||
|
|
8308f15e93 | ||
|
|
2420919ca8 | ||
|
|
c671c2e904 | ||
|
|
c9df8dd1f3 | ||
|
|
8ae86d4310 | ||
|
|
c144c26dcf | ||
|
|
481f6b1531 | ||
|
|
b267d56ba7 | ||
|
|
dd139f8228 | ||
|
|
be4756e4bf | ||
|
|
bd323fb930 | ||
|
|
aff1070f43 | ||
|
|
b6742e90b1 | ||
|
|
95b6ed7965 | ||
|
|
5a35144d4f | ||
|
|
5f5f9d6fd5 | ||
|
|
c602a2fcd6 | ||
|
|
821d3c423c | ||
|
|
6b89f5dad8 | ||
|
|
d28cf53009 | ||
|
|
504900d7bd | ||
|
|
0cc439eee3 | ||
|
|
e8b8f34f2b | ||
|
|
58a23e06a1 | ||
|
|
5a4ac03c0d | ||
|
|
c5a506e240 | ||
|
|
bc5cd13c16 | ||
|
|
d406c7c09b | ||
|
|
9cf3117636 | ||
|
|
61786ddcdf | ||
|
|
f33281fae2 | ||
|
|
93bd457a54 | ||
|
|
600824bed2 | ||
|
|
42e635bb9e | ||
|
|
d48810f045 | ||
|
|
3387cf373d | ||
|
|
7825bd6c5a | ||
|
|
ba14f4ad53 | ||
|
|
02bbc3f777 | ||
|
|
45a2d0b40e | ||
|
|
53eccf7047 | ||
|
|
63000bc122 | ||
|
|
220cb4f0c8 | ||
|
|
7ce3a41184 | ||
|
|
51efaf2e81 | ||
|
|
f0d6d428dc | ||
|
|
01778fc90a | ||
|
|
d9928ad3f9 | ||
|
|
77b7876986 | ||
|
|
745f7c7313 | ||
|
|
0a96d0e3fa | ||
|
|
e3ad9d097e | ||
|
|
573b6522f9 | ||
|
|
59d3717159 | ||
|
|
a42eafc316 | ||
|
|
11f839b9e5 | ||
|
|
721274239a | ||
|
|
090eba0db5 | ||
|
|
88be4ac417 | ||
|
|
c1d622e56e | ||
|
|
91af1c3b54 | ||
|
|
4b6813ecee | ||
|
|
32687e64fe | ||
|
|
b7f46213c7 | ||
|
|
aa8982bdb4 | ||
|
|
14826db606 | ||
|
|
847a582022 | ||
|
|
97cd00faeb | ||
|
|
8d4193fb46 | ||
|
|
8d4ae388fa | ||
|
|
7082689866 | ||
|
|
40c034ed3f | ||
|
|
208d5ea7ab | ||
|
|
ed51438c8b | ||
|
|
49fce6673a | ||
|
|
a016ac0d2b | ||
|
|
ad55f5367e | ||
|
|
660cbfa6ce | ||
|
|
982608246e | ||
|
|
6f6de2cf05 | ||
|
|
ae3e3f8269 | ||
|
|
dc21f1497b | ||
|
|
5c8941ba16 | ||
|
|
b0a001c6a4 | ||
|
|
722c55f85f | ||
|
|
3ab19f3295 | ||
|
|
621ffc46c0 | ||
|
|
fbb629c02e | ||
|
|
80d3a86468 | ||
|
|
8e8fd2717e | ||
|
|
f92d24e89c | ||
|
|
94e0a44e15 | ||
|
|
f25181f68d | ||
|
|
cf19eb1386 | ||
|
|
0583119698 | ||
|
|
3389f3db7f | ||
|
|
c642f5f8a3 | ||
|
|
ceb8b509e2 | ||
|
|
65dae11e47 | ||
|
|
d5b1676216 | ||
|
|
09f222baf7 | ||
|
|
802e7bffc3 | ||
|
|
fb272d1afa | ||
|
|
b871676def | ||
|
|
869fe2f50d | ||
|
|
db877b1c9b | ||
|
|
4749f9d97a | ||
|
|
8564a7fb51 | ||
|
|
6be98e0bbd | ||
|
|
5e0c68c45e | ||
|
|
7717b9dcf2 | ||
|
|
819a108f07 | ||
|
|
3a99899114 | ||
|
|
1088066c69 | ||
|
|
73d44503fd | ||
|
|
25aaff0bd6 | ||
|
|
29691461c5 | ||
|
|
a74e4dcec8 | ||
|
|
0b0b548d7a | ||
|
|
02f3823e4c | ||
|
|
36327efe9d | ||
|
|
079f52a191 | ||
|
|
c9f1d01346 | ||
|
|
9bc68bdd93 | ||
|
|
56c225bd34 | ||
|
|
55c017cad1 | ||
|
|
7849d55b99 | ||
|
|
c903cc8827 | ||
|
|
4a797b9565 | ||
|
|
57b487eaa3 | ||
|
|
891b7c06f2 | ||
|
|
f7e212ec9c | ||
|
|
228538ea62 | ||
|
|
347e7886e1 | ||
|
|
bfa5fb8844 | ||
|
|
a8fd6038f1 | ||
|
|
fbfe887084 | ||
|
|
aed991f75f | ||
|
|
ab2cb71c00 | ||
|
|
fcfde083e7 | ||
|
|
4614a3637a | ||
|
|
d450544d6b | ||
|
|
9340e6a78d | ||
|
|
e00b8835cc | ||
|
|
429ca06289 | ||
|
|
0fbfc90bea | ||
|
|
581e5025a2 | ||
|
|
e57fce290b | ||
|
|
d5a3682de9 | ||
|
|
75f5862218 | ||
|
|
aed518f945 | ||
|
|
e942d41c1d | ||
|
|
38fcfd8732 | ||
|
|
82ec68128d | ||
|
|
9497354cfd | ||
|
|
baa936439d | ||
|
|
394061d28d | ||
|
|
0f25db67dc | ||
|
|
49ba91961f | ||
|
|
28ce772ae9 | ||
|
|
424c2d9f6b | ||
|
|
89797064b8 | ||
|
|
7824aed878 | ||
|
|
8236c43c90 | ||
|
|
6df89fbe94 | ||
|
|
b423bf7ddd | ||
|
|
cdbdb8248c | ||
|
|
db45052d7e | ||
|
|
804bbf6564 | ||
|
|
bd3b9bacf6 | ||
|
|
5505d57066 | ||
|
|
cf88c4e720 | ||
|
|
9911e86039 | ||
|
|
9eba0f6ab2 | ||
|
|
6d265c1cce | ||
|
|
904aa530b5 | ||
|
|
fa316f3edc | ||
|
|
41d19e7338 | ||
|
|
4f15a3f85c | ||
|
|
222166ca5a | ||
|
|
ab3eb81e52 | ||
|
|
f37486a74f | ||
|
|
a38b854338 | ||
|
|
e18113a746 | ||
|
|
d4edbd1acf | ||
|
|
056e45a003 | ||
|
|
72b3227824 | ||
|
|
a51f8a6b8e | ||
|
|
1ef8d70ac4 | ||
|
|
a204946d39 | ||
|
|
3c7795cf44 | ||
|
|
9349204636 | ||
|
|
d2bcd04133 | ||
|
|
61708ad0bd | ||
|
|
c9cfe22fd6 | ||
|
|
5572d3d4de | ||
|
|
1441fe0b91 | ||
|
|
7df1d8c838 | ||
|
|
3a51abfcad | ||
|
|
870e9086dc | ||
|
|
1db52ff531 | ||
|
|
e3be293420 | ||
|
|
bbc32dff82 | ||
|
|
861197e61c | ||
|
|
34e5ef22a0 | ||
|
|
b141297651 | ||
|
|
0cb977e603 | ||
|
|
c8a5adcee9 | ||
|
|
ecdb411711 | ||
|
|
a4e206168d | ||
|
|
4f1b5eae54 | ||
|
|
54f14e89cf | ||
|
|
1b3922d466 | ||
|
|
00a30a9984 | ||
|
|
db6699dae2 | ||
|
|
4d229d7b94 | ||
|
|
d94c5f8380 | ||
|
|
099217390e | ||
|
|
4c4ab8a57d | ||
|
|
d746b23749 | ||
|
|
f77c597e01 | ||
|
|
3c54a4d33b | ||
|
|
cf9de82c2a | ||
|
|
c3155e2642 | ||
|
|
d8f171a996 | ||
|
|
0ef3bb1cc6 | ||
|
|
54df8234ed | ||
|
|
dc08e37320 | ||
|
|
45a8575b95 | ||
|
|
be966cfe1f | ||
|
|
f4507aeb9b | ||
|
|
0985691fbd | ||
|
|
01047846a9 | ||
|
|
75a6d65e5a | ||
|
|
71ea92bbec | ||
|
|
aac6de01de | ||
|
|
af036d9f45 | ||
|
|
04016f3be6 | ||
|
|
41567b0456 | ||
|
|
9db3a601ca | ||
|
|
35fdd00203 | ||
|
|
7a435be018 | ||
|
|
dec0e24275 | ||
|
|
c624fc078b | ||
|
|
b9c6b326b7 | ||
|
|
0003072623 | ||
|
|
bffdb0b03d | ||
|
|
b5ffc734a2 | ||
|
|
a2c88ae0f1 | ||
|
|
efb671aaf2 | ||
|
|
a4b4b8f649 | ||
|
|
4c59405e5c | ||
|
|
703a215ef8 | ||
|
|
f5f96bc4f4 | ||
|
|
1668ab7877 | ||
|
|
26fc0da9a9 | ||
|
|
c15220d1a1 | ||
|
|
7c4562a8b3 | ||
|
|
6e3176f726 | ||
|
|
958b456873 | ||
|
|
a43b5b69ab | ||
|
|
1517435441 | ||
|
|
7abb28cb2e | ||
|
|
112fd4aac3 | ||
|
|
90fbe112af | ||
|
|
c150529c7c | ||
|
|
fa6aa2ced8 | ||
|
|
39664985ba | ||
|
|
ab1e9694bc | ||
|
|
2c710d3b7d | ||
|
|
581ab2c945 | ||
|
|
274b48c82f | ||
|
|
e352e4f59c | ||
|
|
734936d819 | ||
|
|
0e1384b4d2 | ||
|
|
2160613c6a | ||
|
|
69bb5c7fba | ||
|
|
f32e1a7773 | ||
|
|
703c2f214b | ||
|
|
6de831778a | ||
|
|
ca46086e79 | ||
|
|
0079184c16 | ||
|
|
dcc9efea0a | ||
|
|
a3b508fab9 | ||
|
|
5359b487f2 | ||
|
|
c2d973997a | ||
|
|
b09aa0e65c | ||
|
|
41a6f0d43e | ||
|
|
9764f8886b | ||
|
|
1ba2c063e0 | ||
|
|
c640294dbf | ||
|
|
dec487c62b | ||
|
|
1ba74a30fc | ||
|
|
fcf0cebf6c | ||
|
|
e14d38194f | ||
|
|
294aad092e | ||
|
|
8767ebf835 | ||
|
|
cd907f2edf | ||
|
|
eb77083d23 | ||
|
|
219362318c | ||
|
|
68a75dae48 | ||
|
|
87a381e0a7 | ||
|
|
0b2520f84a | ||
|
|
21ab85cd03 | ||
|
|
486bf32ac7 | ||
|
|
4f6610716a | ||
|
|
6db4ca71da | ||
|
|
d5319e2b4f | ||
|
|
cda44480b7 | ||
|
|
fb0af12433 | ||
|
|
b5f858da4e | ||
|
|
59bd5db556 | ||
|
|
cf1557e454 | ||
|
|
36e1f01091 | ||
|
|
e3c484af01 | ||
|
|
4deb5c7cee | ||
|
|
21fb329d56 | ||
|
|
678b469835 | ||
|
|
ded48ddac7 | ||
|
|
8a02fc755f | ||
|
|
4844f72b96 | ||
|
|
f32bd6f945 | ||
|
|
f64fcbc07d | ||
|
|
c34c3bf730 | ||
|
|
de4c551363 | ||
|
|
d45f02d38c | ||
|
|
666975a1ef | ||
|
|
144a1e4009 | ||
|
|
8b486dfc71 | ||
|
|
db092a404e | ||
|
|
edda3d1b51 | ||
|
|
27a11e7b35 | ||
|
|
cfd4c93d88 | ||
|
|
b1ca805d2a | ||
|
|
852a882fab | ||
|
|
fb0cbc90e3 | ||
|
|
3a24f10d7c | ||
|
|
3fc036e01a | ||
|
|
056a46cc97 | ||
|
|
f8df8ed72d | ||
|
|
79c159d123 | ||
|
|
a8c37ceace | ||
|
|
cb01f11ad1 | ||
|
|
7aaa84853d | ||
|
|
75857fbf73 | ||
|
|
c8db0c8ec6 | ||
|
|
3958260353 | ||
|
|
8cdb8d0367 | ||
|
|
66bf85b14f | ||
|
|
1a0892745e | ||
|
|
76b0493fb0 | ||
|
|
74eb4059d6 | ||
|
|
13f53eb64f | ||
|
|
b3941526cb | ||
|
|
fff067b2a8 | ||
|
|
217546c2a0 | ||
|
|
40c06f5e77 | ||
|
|
bb09caa9a3 | ||
|
|
4ebefeb43a | ||
|
|
8f01d0234f | ||
|
|
13035baeae | ||
|
|
92afe9bd3c | ||
|
|
4c1aca0abb | ||
|
|
da166e051d | ||
|
|
6a4ba95926 | ||
|
|
6688bd8d7b | ||
|
|
01313a39cc | ||
|
|
f92911b8aa | ||
|
|
42d6fd5804 | ||
|
|
a8a45a5fbe | ||
|
|
11781f0c1b | ||
|
|
53055e0345 | ||
|
|
1af6d4b0ec | ||
|
|
3e311dc975 | ||
|
|
04e31eb07b | ||
|
|
eb82ddca0b | ||
|
|
1d2b720ebe | ||
|
|
4c303c3b1d | ||
|
|
42129c6181 | ||
|
|
a10a57e67d | ||
|
|
fa5f32c7fd | ||
|
|
a91e888575 | ||
|
|
8571883923 | ||
|
|
4cf005d4bd | ||
|
|
b38792c166 | ||
|
|
248863addf | ||
|
|
7e2752e71f | ||
|
|
cbf0ca027d | ||
|
|
2c2ba636a9 | ||
|
|
494e6722b2 | ||
|
|
ddf71222c5 | ||
|
|
1d89127d8f | ||
|
|
5f00625c14 | ||
|
|
000a93dc77 | ||
|
|
1f8c090a5f | ||
|
|
0547868d4d | ||
|
|
1056b8361c | ||
|
|
a5f861bf2b | ||
|
|
93aee6419e | ||
|
|
4b1a7e9ae7 | ||
|
|
2f5e89f3ec | ||
|
|
2b903ad057 | ||
|
|
fb397e6fa0 | ||
|
|
00a55b35a8 | ||
|
|
d65ce55453 | ||
|
|
37d756ae75 | ||
|
|
f8782666ba | ||
|
|
c74c682939 | ||
|
|
8b49600673 | ||
|
|
29c729fd23 | ||
|
|
5d65967448 | ||
|
|
bf258eeb9b | ||
|
|
af6237015a | ||
|
|
a462fb63c3 | ||
|
|
f3332fb0da | ||
|
|
5bea83114b | ||
|
|
fe8bb38ec1 | ||
|
|
a60571321a | ||
|
|
e1c2e1a753 | ||
|
|
800dbf2929 | ||
|
|
1880447dce | ||
|
|
268dbb099f | ||
|
|
ae275ad1b1 | ||
|
|
ceff050bb4 | ||
|
|
8357811d96 | ||
|
|
860a17d85a | ||
|
|
ba324cddb6 | ||
|
|
a5dcd78393 | ||
|
|
8e1195322a | ||
|
|
2a2b51c8ab | ||
|
|
eb5ec2a314 | ||
|
|
445529a68f | ||
|
|
981b79b3b3 | ||
|
|
78bcda02cb | ||
|
|
cdfa5ad990 | ||
|
|
720f502e9d | ||
|
|
adf6129769 | ||
|
|
a5fddfa468 | ||
|
|
7c37dd5e85 | ||
|
|
857ca19fe4 | ||
|
|
a19d91ef37 | ||
|
|
ac8526688a | ||
|
|
1e1c99bbdb | ||
|
|
0c89293029 | ||
|
|
39eb78c88b | ||
|
|
372842aac6 | ||
|
|
7934e06668 | ||
|
|
44f982f8e5 | ||
|
|
1f04a62648 | ||
|
|
675c8c3f4e | ||
|
|
97cb77bbdd | ||
|
|
46345b8e49 | ||
|
|
566451e9a7 | ||
|
|
15626294b0 | ||
|
|
6cab04554e | ||
|
|
2ae7f007cc | ||
|
|
89e37a7751 | ||
|
|
0dca4d9b9f | ||
|
|
b85c3035fe | ||
|
|
6899d94027 | ||
|
|
fa0f9df497 | ||
|
|
917df6e97d | ||
|
|
5921f59817 | ||
|
|
50bad7f983 | ||
|
|
972c61fa76 | ||
|
|
b73d02fb8c | ||
|
|
6c4974b5c6 | ||
|
|
81d661c4f1 | ||
|
|
2213312938 | ||
|
|
4ae7ab5e87 | ||
|
|
59569984e2 | ||
|
|
89b580ab52 | ||
|
|
85df785cd3 | ||
|
|
fde88c22a8 | ||
|
|
0ec4b692f4 | ||
|
|
c5c8f1a6d3 | ||
|
|
336640633b | ||
|
|
4206739492 | ||
|
|
9e6217871e | ||
|
|
7b1241d0f2 | ||
|
|
9acc0debec | ||
|
|
a226de38b6 | ||
|
|
f5b0b1934a | ||
|
|
64838ce07d | ||
|
|
526a0394b0 | ||
|
|
1cacef025d |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --manifest-path=crates/xtask/Cargo.toml --"
|
||||
26
.git-blame-ignore-revs
Normal file
26
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,26 @@
|
||||
# Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits.
|
||||
|
||||
# rustfmt
|
||||
ad0794a0bd692e4f2ff23b85e361889620e93f51
|
||||
# rustfmt and use_try_shorthand
|
||||
75bbd55128083897d40c3f5265cc5b1f10314ddb
|
||||
# rustfmt
|
||||
382fc4139b96bde3c4b8875b499c720eabc89c6a
|
||||
# rustfmt
|
||||
154e0fb3080c6ffc225b0d47b5d835e589789892
|
||||
# rustfmt
|
||||
5835da243244bfc5c95c6c6db96f453da4bb5740
|
||||
# rustfmt
|
||||
fd9d27e082f5e9eea50e4fa9fa3a22060d02c66b
|
||||
# rustfmt
|
||||
1d69ccae4854f13552d452d0bffef95cbff70364
|
||||
# rustfmt
|
||||
3688f73052454bf510a5acc85cf55aae450c6e46
|
||||
# rustfmt
|
||||
742dbbc91700dce1b7d910bca6b3e10a5ae46b86
|
||||
# rustfmt 1.38
|
||||
b88839cc25a6fd1c782101e94318959e8079bb20
|
||||
# rustfmt 1.40
|
||||
2f59943c04f0aa204a9238d6a699ba9cc06c88d9
|
||||
# Rustfmt for 2024
|
||||
c7b67e363bb9ce3383636ee615e8e761bf185b33
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -6,3 +6,5 @@
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.eot binary
|
||||
*.woff2 binary
|
||||
|
||||
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ["C-bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for filing a 🐛 bug report 😄!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: >
|
||||
Please provide a clear and concise description of what the bug is,
|
||||
including what currently happens and what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps
|
||||
description: Please list the steps to reproduce the bug.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
- type: textarea
|
||||
id: possible-solutions
|
||||
attributes:
|
||||
label: Possible Solution(s)
|
||||
description: >
|
||||
Not obligatory, but suggest a fix/reason for the bug,
|
||||
or ideas how to implement the addition or change.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Provide any additional notes that might be helpful.
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: >
|
||||
Please paste the output of running `mdbook --version` or which version
|
||||
of the library you are using.
|
||||
render: text
|
||||
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Enhancement
|
||||
description: Suggest an idea for enhancing mdBook
|
||||
labels: ["C-enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing a 🙋 feature request 😄!
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: >
|
||||
Please provide a clear description of your use case and the problem
|
||||
this feature request is trying to solve.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: >
|
||||
Please provide a clear and concise description of what you want to happen.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: Provide any additional context or information that might be helpful.
|
||||
24
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Question
|
||||
description: Have a question on how to use mdBook?
|
||||
labels: ["C-question"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Got a question on how to do something with mdBook?
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: >
|
||||
Enter your question here. Please try to provide as much detail as possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: >
|
||||
Please paste the output of running `mdbook --version` or which version
|
||||
of the library you are using.
|
||||
render: text
|
||||
71
.github/renovate.json5
vendored
Normal file
71
.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
schedule: ['before 5am on the first day of the month'],
|
||||
// Raise from default of 2 to reduce trickle.
|
||||
prHourlyLimit: 6,
|
||||
dependencyDashboard: true,
|
||||
// Creates PRs if this renovate config file needs updating.
|
||||
configMigration: true,
|
||||
ignorePaths: [
|
||||
'guide/src/for_developers/mdbook-wordcount/',
|
||||
],
|
||||
customManagers: [
|
||||
// Custom manager to extract the version of cargo-semver-checks from the workflow.
|
||||
{
|
||||
customType: 'regex',
|
||||
managerFilePatterns: [
|
||||
'/^.github.workflows.main.yml$/',
|
||||
],
|
||||
matchStrings: [
|
||||
'cargo-semver-checks.releases.download.v(?<currentValue>\\d+\\.\\d+(\\.\\d+)?)',
|
||||
],
|
||||
depNameTemplate: 'cargo-semver-checks',
|
||||
packageNameTemplate: 'obi1kenobi/cargo-semver-checks',
|
||||
datasourceTemplate: 'github-releases',
|
||||
},
|
||||
],
|
||||
packageRules: [
|
||||
// The next two rules disable compatible dependency updates. I wasn't
|
||||
// able to get Renovate to be able to update Cargo.toml for compatible
|
||||
// updates only, update all transitive dependencies, and do that all
|
||||
// in a single PR. Instead, the `update-dependencies.sh` will handle
|
||||
// that.
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchUpdateTypes: ['patch'],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchCurrentVersion: '>=1.0.0',
|
||||
matchUpdateTypes: ['minor'],
|
||||
enabled: false,
|
||||
},
|
||||
// Allow minor updates for pre-1.0 dependencies (semver-breaking)
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchCurrentVersion: '<1.0.0',
|
||||
matchUpdateTypes: ['minor'],
|
||||
},
|
||||
// Allow major updates for stable dependencies (semver-breaking)
|
||||
{
|
||||
matchManagers: ['cargo'],
|
||||
matchCurrentVersion: '>=1.0.0',
|
||||
matchUpdateTypes: ['major'],
|
||||
},
|
||||
// Update cargo-semver-checks when a new version is available.
|
||||
{
|
||||
commitMessageTopic: 'cargo-semver-checks',
|
||||
matchManagers: [
|
||||
'custom.regex',
|
||||
],
|
||||
matchDepNames: [
|
||||
'cargo-semver-checks',
|
||||
],
|
||||
extractVersion: '^v(?<version>\\d+\\.\\d+\\.\\d+)',
|
||||
schedule: [
|
||||
'* * * * *',
|
||||
],
|
||||
internalChecksFilter: 'strict',
|
||||
},
|
||||
]
|
||||
}
|
||||
70
.github/workflows/deploy.yml
vendored
70
.github/workflows/deploy.yml
vendored
@@ -3,40 +3,66 @@ on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Deploy Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-22.04
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
name: Deploy ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install hub
|
||||
run: ci/install-hub.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable
|
||||
shell: bash
|
||||
- name: Build and deploy artifacts
|
||||
run: ci/install-rust.sh stable ${{ matrix.target }}
|
||||
- name: Build asset
|
||||
run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }}
|
||||
- name: Update release with new asset
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ci/make-release.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build guide
|
||||
- name: Deploy to GitHub
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch guide/book/.nojekyll
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
- name: Deploy the User Guide to GitHub Pages using the gh-pages branch
|
||||
run: ci/publish-guide.sh
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Required for OIDC token exchange
|
||||
id-token: write
|
||||
environment: publish
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Authenticate with crates.io
|
||||
id: auth
|
||||
uses: rust-lang/crates-io-auth-action@v1
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
|
||||
run: cargo publish --workspace --no-verify
|
||||
|
||||
136
.github/workflows/main.yml
vendored
136
.github/workflows/main.yml
vendored
@@ -1,51 +1,147 @@
|
||||
name: CI
|
||||
on:
|
||||
# Only run when merging to master, or open/synchronize/reopen a PR.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build: [stable, beta, nightly, macos, windows, msrv]
|
||||
include:
|
||||
- build: stable
|
||||
- name: stable linux
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
- build: beta
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: beta linux
|
||||
os: ubuntu-latest
|
||||
rust: beta
|
||||
- build: nightly
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: nightly linux
|
||||
os: ubuntu-latest
|
||||
rust: nightly
|
||||
- build: macos
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- name: stable x86_64-unknown-linux-musl
|
||||
os: ubuntu-22.04
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- name: stable x86_64 macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
- build: windows
|
||||
target: x86_64-apple-darwin
|
||||
- name: stable aarch64 macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: aarch64-apple-darwin
|
||||
- name: stable windows-msvc
|
||||
os: windows-latest
|
||||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
rust: 1.46.0
|
||||
target: x86_64-pc-windows-msvc
|
||||
- name: msrv
|
||||
os: ubuntu-22.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.88.0
|
||||
target: x86_64-unknown-linux-gnu
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
|
||||
- name: Build and run tests
|
||||
run: cargo test
|
||||
run: cargo test --workspace --locked --target ${{ matrix.target }}
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features
|
||||
run: cargo test --workspace --no-default-features --target ${{ matrix.target }}
|
||||
|
||||
aarch64-cross-builds:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl
|
||||
- name: Build
|
||||
run: cargo build --locked --target aarch64-unknown-linux-musl
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
- run: cargo fmt --check
|
||||
|
||||
gui:
|
||||
name: GUI tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Install npm
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Install browser-ui-test
|
||||
run: npm install
|
||||
- name: Run eslint
|
||||
run: npm run lint
|
||||
- name: Build and run tests (+ GUI)
|
||||
run: cargo test --locked --target x86_64-unknown-linux-gnu --test gui
|
||||
|
||||
# Ensure there are no clippy warnings
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy --workspace --all-targets --no-deps -- -D warnings
|
||||
|
||||
docs:
|
||||
name: Check API docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Ensure intradoc links are valid
|
||||
run: cargo doc --workspace --document-private-items --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
|
||||
check-version-bump:
|
||||
name: Check version bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup update stable && rustup default stable
|
||||
- name: Install cargo-semver-checks
|
||||
run: |
|
||||
mkdir installed-bins
|
||||
curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.45.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \
|
||||
| tar -xz --directory=./installed-bins
|
||||
echo `pwd`/installed-bins >> $GITHUB_PATH
|
||||
- run: cargo semver-checks --workspace
|
||||
|
||||
# The success job is here to consolidate the total success/failure state of
|
||||
# all other jobs. This job is then included in the GitHub branch protection
|
||||
# rule which prevents merges unless all other jobs are passing. This makes
|
||||
# it easier to manage the list of jobs via this yml file and to prevent
|
||||
# accidentally adding new jobs without also updating the branch protections.
|
||||
success:
|
||||
name: Success gate
|
||||
if: always()
|
||||
needs:
|
||||
- test
|
||||
- rustfmt
|
||||
- aarch64-cross-builds
|
||||
- gui
|
||||
- clippy
|
||||
- docs
|
||||
- check-version-bump
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||
- name: Done
|
||||
run: exit 0
|
||||
|
||||
21
.github/workflows/update-dependencies.yml
vendored
Normal file
21
.github/workflows/update-dependencies.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Update dependencies
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update:
|
||||
name: Update dependencies
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'rust-lang/mdBook'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
|
||||
- name: Install cargo-edit
|
||||
run: cargo install cargo-edit --locked
|
||||
- name: Update dependencies
|
||||
run: ci/update-dependencies.sh
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -8,7 +8,8 @@ guide/book
|
||||
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
test_book/book/
|
||||
tests/gui/books/*/book/
|
||||
tests/testsuite/*/*/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
@@ -16,3 +17,8 @@ test_book/book/
|
||||
# Ignore Vim temporary and swap files.
|
||||
*.sw?
|
||||
*~
|
||||
|
||||
# GUI tests
|
||||
node_modules
|
||||
package-lock.json
|
||||
package.json
|
||||
|
||||
969
CHANGELOG.md
969
CHANGELOG.md
@@ -1,5 +1,974 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.5.2
|
||||
[v0.5.1...v0.5.2](https://github.com/rust-lang/mdBook/compare/v0.5.1...v0.5.2)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Rust crate html5ever to 0.36.0.
|
||||
[#2970](https://github.com/rust-lang/mdBook/pull/2970)
|
||||
- Updated cargo dependencies.
|
||||
[#2969](https://github.com/rust-lang/mdBook/pull/2969)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed repeated error message when HTML config is invalid in `mdbook serve`.
|
||||
[#2983](https://github.com/rust-lang/mdBook/pull/2983)
|
||||
- Fixed sidebar scroll position when heading nav is involved.
|
||||
[#2982](https://github.com/rust-lang/mdBook/pull/2982)
|
||||
- Fixed color for rustdoc error messages.
|
||||
[#2981](https://github.com/rust-lang/mdBook/pull/2981)
|
||||
- Fixed usage of custom preprocessors with `MDBook::test`.
|
||||
[#2980](https://github.com/rust-lang/mdBook/pull/2980)
|
||||
|
||||
## mdBook 0.5.1
|
||||
[v0.5.0...v0.5.1](https://github.com/rust-lang/mdBook/compare/v0.5.0...v0.5.1)
|
||||
|
||||
### Changed
|
||||
- Changed the scrollbar background to be transparent.
|
||||
[#2932](https://github.com/rust-lang/mdBook/pull/2932)
|
||||
- Ignore invalid top-level environment variable config keys. This allows setting things like `MDBOOK_VERSION` to not cause an error.
|
||||
[#2952](https://github.com/rust-lang/mdBook/pull/2952)
|
||||
|
||||
### Fixed
|
||||
- Fixed the sidebar heading nav to have the correct nesting levels.
|
||||
[#2953](https://github.com/rust-lang/mdBook/pull/2953)
|
||||
- Various Font Awesome fixes and improvements.
|
||||
[#2951](https://github.com/rust-lang/mdBook/pull/2951)
|
||||
|
||||
## mdBook 0.5.0
|
||||
[v0.4.52...v0.5.0](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0)
|
||||
|
||||
The 0.5.0 release is the next major release of mdBook, containing over 130 PRs since 0.4.52! The primary focus for this release has been an evolution of the Rust APIs to make it easier to maintain, to evolve in a backwards-compatible fashion, to clean up some things that have accumulated over time, and to significantly improve the performance and compile-times.
|
||||
|
||||
This release also includes many new features described below.
|
||||
|
||||
We have prepared a [0.5 Migration Guide](#05-migration-guide) to help existing authors switch from 0.4.
|
||||
|
||||
The final 0.5.0 release only contains the following changes since [0.5.0-beta.2](#mdbook-050-beta2):
|
||||
|
||||
- Added error handling to environment config handling. This checks that environment variables starting with `MDBOOK_` are correctly specified instead of silently ignoring. This also fixed being able to replace entire top-level tables like `MDBOOK_OUTPUT`.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
|
||||
## 0.5 Migration Guide
|
||||
|
||||
The 0.5 release contains several breaking changes from the 0.4 release. Preprocessors and renderers will need to be migrated to continue to work with this release. After updating your configuration, it is recommended to carefully compare and review how your book renders to ensure everything is working correctly.
|
||||
|
||||
If you have overridden any of the theme files, you will likely need to update them to match the current version.
|
||||
|
||||
See the entries below for [mdBook 0.5.0-alpha.1](#mdbook-050-alpha1), [mdBook 0.5.0-beta.1](#mdbook-050-beta1), and [mdBook 0.5.0-beta.2](#mdbook-050-beta2) for a more complete list of changes and fixes.
|
||||
|
||||
The following is a summary of the changes that may require your attention when updating to 0.5:
|
||||
|
||||
### Major additions
|
||||
|
||||
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
|
||||
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
|
||||
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#definition-lists) for more.
|
||||
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
|
||||
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#admonitions) for more.
|
||||
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
|
||||
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
|
||||
### Config changes
|
||||
|
||||
- Unknown fields in config are now an error.
|
||||
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
|
||||
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
|
||||
- Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
|
||||
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
|
||||
- Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
|
||||
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
|
||||
- If the `command` path for a renderer or preprocessor is relative, it is now always relative to the book root.
|
||||
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
|
||||
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
|
||||
- Added the `optional` field for preprocessors. The default is `false`, so this also means it is an error by default if the preprocessor is missing.
|
||||
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
|
||||
- `output.html.smart-punctuation` is now `true` by default.
|
||||
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
|
||||
- `output.html.hash-files` is now `true` by default.
|
||||
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
|
||||
- Removed support for google-analytics. Use a theme extension (like `head.hbs`) if you need to continue to support this.
|
||||
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
|
||||
- Removed the `book.multilingual` field. This was never used.
|
||||
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
|
||||
- Removed the very old legacy config support. Warnings have been displayed in previous versions on how to migrate.
|
||||
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
|
||||
- Top-level config values set from the environment like `MDBOOK_BOOK` now *replace* the contents of the top-level table instead of merging into it.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
- Invalid environment variables are now rejected. Previously unknown keys like `MDBOOK_FOO` would be ignored, or keys or invalid values inside objects like the `[book]` table would be ignored.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
|
||||
### Theme changes
|
||||
|
||||
- Replaced the `{{#previous}}` and `{{#next}}` handlebars helpers with simple objects that contain the previous and next values.
|
||||
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
|
||||
- Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
|
||||
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
|
||||
|
||||
### Rendering changes
|
||||
|
||||
- Updated to a newer version of `pulldown-cmark`. This brings a large number of fixes to markdown processing.
|
||||
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
|
||||
- The font-awesome font is no longer loaded as a font. Instead, the corresponding SVG is embedded in the output for the corresponding `<i>` tags. Additionally, a handlebars helper has been added for the `hbs` files. This also updates the version from 4.7.0 to 6.2.0, which means some of the icon names and styles have changed. Most of the free icons are in the "solid" set. See the [free icon set](https://fontawesome.com/v6/search) for the available icons.
|
||||
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
|
||||
- Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
|
||||
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
|
||||
- There is a new internal HTML rendering pipeline. This is primarily intended to give mdBook more flexibility in generating its HTML output. This resulted in some small changes to the HTML structure. HTML parsing may now be more strict than before.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
|
||||
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
|
||||
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
|
||||
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
|
||||
- Header ID generation has some minor changes to bring the ID generation closer to other tools and sites:
|
||||
- IDs now use Unicode lowercase instead of ASCII lowercase.
|
||||
[#2922](https://github.com/rust-lang/mdBook/pull/2922)
|
||||
- Headers that start or end with HTML characters like `<`, `&`, or `>` now replace those characters in the link ID with `-` instead of being stripped.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- Headers are no longer modified if the tag is manually written HTML.
|
||||
[#2913](https://github.com/rust-lang/mdBook/pull/2913)
|
||||
|
||||
### CLI changes
|
||||
|
||||
- Removed the `--dest-dir` option to `mdbook test`. It was unused since `mdbook test` does not generate output.
|
||||
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
|
||||
- Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
|
||||
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
|
||||
|
||||
### Rust API
|
||||
|
||||
- The Rust API has been split into several crates ([#2766](https://github.com/rust-lang/mdBook/pull/2766)). In summary, the different crates are:
|
||||
- `mdbook` — The CLI binary.
|
||||
- [`mdbook-driver`](https://docs.rs/mdbook-driver/latest/mdbook_driver/) — The high-level library for running mdBook, primarily through the `MDBook` type. If you are driving mdBook programmatically, this is the crate you want.
|
||||
- [`mdbook-preprocessor`](https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/) — Support for implementing preprocessors. If you have a preprocessor, then this is the crate you should depend on.
|
||||
- [`mdbook-renderer`](https://docs.rs/mdbook-renderer/latest/mdbook_renderer/) — Support for implementing renderers. If you have a custom renderer, this is the crate you should depend on.
|
||||
- [`mdbook-markdown`](https://docs.rs/mdbook-markdown/latest/mdbook_markdown/) — The Markdown renderer. If you are processing markdown, this is the crate you should depend on. This is essentially a thin wrapper around `pulldown-cmark`, and re-exports that crate so that you can ensure the version stays in sync with mdBook.
|
||||
- [`mdbook-summary`](https://docs.rs/mdbook-summary/latest/mdbook_summary/) — The `SUMMARY.md` parser.
|
||||
- [`mdbook-html`](https://docs.rs/mdbook-html/latest/mdbook_html/) — The HTML renderer.
|
||||
- [`mdbook-core`](https://docs.rs/mdbook-core/latest/mdbook_core/) — An internal library that is used by the other crates for shared types. You should not depend on this crate directly since types from this crate are re-exported from the other crates as appropriate.
|
||||
- Changes to `Config`:
|
||||
- [`Config::get`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.get) is now generic over the return value, using `serde` to deserialize the value. It also returns a `Result` to handle deserialization errors. [#2773](https://github.com/rust-lang/mdBook/pull/2773)
|
||||
- [`Config::set`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.set) now validates that the config keys and values are valid.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
- [`Config::update_from_env`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.update_from_env) now returns a `Result` to indicate any errors.
|
||||
[#2942](https://github.com/rust-lang/mdBook/pull/2942)
|
||||
- Removed `Config::get_deserialized`. Use `Config::get` instead.
|
||||
- Removed `Config::get_deserialized_opt`. Use `Config::get` instead.
|
||||
- Removed `Config::get_mut`. Use `Config::set` instead.
|
||||
- Removed deprecated `Config::get_deserialized_opt`. Use `Config::get` instead.
|
||||
- Removed `Config::get_renderer`. Use `Config::get` instead.
|
||||
- Removed `Config::get_preprocessor`. Use `Config::get` instead.
|
||||
- Public types have been switch to use the `#[non_exhaustive]` attribute to help allow them to change in a backwards-compatible way.
|
||||
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
|
||||
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
|
||||
- Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded. This allows the caller to replace an entry.
|
||||
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
|
||||
- Added `MarkdownOptions` struct to specify settings for markdown rendering for `mdbook_markdown::new_cmark_parser`.
|
||||
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
|
||||
- Renamed `Book::sections` to `Book::items`.
|
||||
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
|
||||
- `mdbook::book::load_book` is now private. Instead, use one of the `MDBook` load functions like `MDBook::load_with_config`.
|
||||
- Removed `HtmlConfig::smart_punctuation` method, use the field of the same name.
|
||||
- `CmdPreprocessor::parse_input` moved to `mdbook_preprocessor::parse_input`.
|
||||
- `Preprocessor::supports_renderer` now returns a `Result<bool>` instead of `bool` to be able to handle errors.
|
||||
- Most of the types from the `theme` module are now private. The `Theme` struct is still exposed for working with themes.
|
||||
- Various functions in the `utils::fs` module have been removed, renamed, or reworked.
|
||||
- Most of the functions in the `utils` module have been moved, removed, or made private.
|
||||
|
||||
## mdBook 0.5.0-beta.2
|
||||
[v0.5.0-beta.1...v0.5.0-beta.2](https://github.com/rust-lang/mdBook/compare/v0.5.0-beta.1...v0.5.0-beta.2)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a warning when a Font Awesome icon is missing.
|
||||
[#2915](https://github.com/rust-lang/mdBook/pull/2915)
|
||||
- Added some trace logging for event processing.
|
||||
[#2911](https://github.com/rust-lang/mdBook/pull/2911)
|
||||
- Added `Config::contains_key`.
|
||||
[#2910](https://github.com/rust-lang/mdBook/pull/2910)
|
||||
|
||||
### Changed
|
||||
|
||||
- Heading IDs are now lowercase.
|
||||
[#2922](https://github.com/rust-lang/mdBook/pull/2922)
|
||||
- Updated cargo dependencies.
|
||||
[#2916](https://github.com/rust-lang/mdBook/pull/2916)
|
||||
- Removed italics for in quotes/comments in code blocks with the `ayu` theme.
|
||||
[#2904](https://github.com/rust-lang/mdBook/pull/2904)
|
||||
- Exposed "search" feature from mdbook-driver.
|
||||
[#2907](https://github.com/rust-lang/mdBook/pull/2907)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed rust fenced code blocks with an indent.
|
||||
[#2905](https://github.com/rust-lang/mdBook/pull/2905)
|
||||
- Headers and `dt` tags are no longer modified if the tag is manually written HTML.
|
||||
[#2913](https://github.com/rust-lang/mdBook/pull/2913)
|
||||
- Fixed print page links for internal links to non-chapters.
|
||||
[#2914](https://github.com/rust-lang/mdBook/pull/2914)
|
||||
- Better handling for unbalanced HTML tags.
|
||||
[#2924](https://github.com/rust-lang/mdBook/pull/2924)
|
||||
- Handle unclosed HTML tags inside a markdown element.
|
||||
[#2927](https://github.com/rust-lang/mdBook/pull/2927)
|
||||
- Fixed missing font-awesome icons in the guide.
|
||||
[#2926](https://github.com/rust-lang/mdBook/pull/2926)
|
||||
- Hide the sidebar resize indicator when JS isn't available.
|
||||
[#2923](https://github.com/rust-lang/mdBook/pull/2923)
|
||||
|
||||
## mdBook 0.5.0-beta.1
|
||||
[v0.5.0-alpha.1...v0.5.0-beta.1](https://github.com/rust-lang/mdBook/compare/v0.5.0-alpha.1...v0.5.0-beta.1)
|
||||
|
||||
### Changed
|
||||
|
||||
- Reworked the look of the header navigation.
|
||||
[#2898](https://github.com/rust-lang/mdBook/pull/2898)
|
||||
- Update cargo dependencies.
|
||||
[#2896](https://github.com/rust-lang/mdBook/pull/2896)
|
||||
- Improved the heading nav debug.
|
||||
[#2892](https://github.com/rust-lang/mdBook/pull/2892)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error message for config.get deserialization error.
|
||||
[#2902](https://github.com/rust-lang/mdBook/pull/2902)
|
||||
- Filter `<mark>` tags from sidebar heading nav.
|
||||
[#2899](https://github.com/rust-lang/mdBook/pull/2899)
|
||||
- Avoid divide-by-zero in heading nav computation
|
||||
[#2891](https://github.com/rust-lang/mdBook/pull/2891)
|
||||
- Fixed heading nav with folded chapters.
|
||||
[#2893](https://github.com/rust-lang/mdBook/pull/2893)
|
||||
|
||||
## mdBook 0.5.0-alpha.1
|
||||
[v0.4.52...v0.5.0-alpha.1](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0-alpha.1)
|
||||
|
||||
### Added
|
||||
|
||||
- The location of the generated HTML book is now displayed on the console.
|
||||
[#2729](https://github.com/rust-lang/mdBook/pull/2729)
|
||||
- ❗ Added the `optional` field for preprocessors. The default is `false`, so this also changes it so that it is an error if the preprocessor is missing.
|
||||
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
|
||||
- ❗ Added `MarkdownOptions` struct to specify settings for markdown rendering.
|
||||
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
|
||||
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
|
||||
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
|
||||
- Added the mdbook version to the guide.
|
||||
[#2826](https://github.com/rust-lang/mdBook/pull/2826)
|
||||
- Added `Book::chapters` and `Book::for_each_chapter_mut` to more conveniently iterate over chapters (instead of all items).
|
||||
[#2838](https://github.com/rust-lang/mdBook/pull/2838)
|
||||
- ❗ Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
|
||||
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
|
||||
- ❗ Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
|
||||
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
|
||||
|
||||
### Changed
|
||||
|
||||
- ❗ The `mdbook` crate has been split into multiple crates.
|
||||
[#2766](https://github.com/rust-lang/mdBook/pull/2766)
|
||||
- The minimum Rust version has been updated to 1.88.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- ❗ `pulldown-cmark` has been upgraded to 0.13.0, bringing a large number of fixes to markdown processing.
|
||||
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
|
||||
- ❗ Switched public types to `non_exhaustive` to help allow them to change in a backwards-compatible way.
|
||||
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
|
||||
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
|
||||
- ❗ Unknown fields in config are now an error.
|
||||
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
|
||||
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
|
||||
- ❗ Changed `id_from_content` to be private.
|
||||
[#2791](https://github.com/rust-lang/mdBook/pull/2791)
|
||||
- ❗ Changed preprocessor `command` to use paths relative to the book root.
|
||||
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
|
||||
- ❗ Replaced the `{{#previous}}` and `{{#next}}` handelbars navigation helpers with objects.
|
||||
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
|
||||
- ❗ Use embedded SVG instead of fonts for icons, font-awesome 6.2.
|
||||
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
|
||||
- The `book.src` field is no longer serialized if it is the default of "src".
|
||||
[#2800](https://github.com/rust-lang/mdBook/pull/2800)
|
||||
- ❗ Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded.
|
||||
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
|
||||
- ❗ Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
|
||||
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
|
||||
- ❗ Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
|
||||
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
|
||||
- ❗ `output.html.smart-punctuation` is now `true` by default.
|
||||
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
|
||||
- ❗ Renamed `Book::sections` to `Book::items`.
|
||||
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
|
||||
- ❗ `output.html.hash-files` is now `true` by default.
|
||||
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
|
||||
- Switched from `log` to `tracing`.
|
||||
[#2829](https://github.com/rust-lang/mdBook/pull/2829)
|
||||
- ❗ Rewrote the HTML rendering pipeline.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- ❗ Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
|
||||
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
|
||||
- ❗ Moved theme copy to the Theme type and reduced visibility.
|
||||
[#2857](https://github.com/rust-lang/mdBook/pull/2857)
|
||||
- ❗ Cleaned up some fs-related utilities.
|
||||
[#2856](https://github.com/rust-lang/mdBook/pull/2856)
|
||||
- ❗ Moved `get_404_output_file` to `HtmlConfig`.
|
||||
[#2855](https://github.com/rust-lang/mdBook/pull/2855)
|
||||
- ❗ Moved `take_lines` functions to `mdbook-driver` and made private.
|
||||
[#2854](https://github.com/rust-lang/mdBook/pull/2854)
|
||||
- Updated dependencies.
|
||||
[#2793](https://github.com/rust-lang/mdBook/pull/2793)
|
||||
[#2869](https://github.com/rust-lang/mdBook/pull/2869)
|
||||
|
||||
### Removed
|
||||
|
||||
- ❗ Removed `toml` as a public dependency.
|
||||
[#2773](https://github.com/rust-lang/mdBook/pull/2773)
|
||||
- ❗ Removed the `book.multilingual` field. This was never used.
|
||||
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
|
||||
- ❗ Removed support for google-analytics.
|
||||
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
|
||||
- ❗ Removed the very old legacy config support.
|
||||
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
|
||||
- ❗ Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
|
||||
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
|
||||
- Removed old warning about `book.json`.
|
||||
[#2789](https://github.com/rust-lang/mdBook/pull/2789)
|
||||
- ❗ Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
|
||||
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
|
||||
- ❗ Removed legacy relative renderer command paths. Relative renderer command paths now must always be relative to the book root.
|
||||
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
|
||||
- ❗ Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
|
||||
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
|
||||
- ❗ Removed the `--dest-dir` option to `mdbook test`.
|
||||
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed handling of multiple footnotes in a row.
|
||||
[#2807](https://github.com/rust-lang/mdBook/pull/2807)
|
||||
- Fixed ID collisions when the numeric suffix gets used.
|
||||
[#2846](https://github.com/rust-lang/mdBook/pull/2846)
|
||||
- Fixed missing css vars for no-js dark mode.
|
||||
[#2850](https://github.com/rust-lang/mdBook/pull/2850)
|
||||
|
||||
## mdBook 0.4.52
|
||||
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
|
||||
|
||||
**Note:** If you have a custom `index.hbs` theme file, it is recommended that you update it to the latest version to pick up the fixes in this release.
|
||||
|
||||
### Added
|
||||
- Added the ability to redirect `#` HTML fragments using the existing `output.html.redirect` table.
|
||||
[#2747](https://github.com/rust-lang/mdBook/pull/2747)
|
||||
- Added the `rel="edit"` attribute to the edit page button.
|
||||
[#2702](https://github.com/rust-lang/mdBook/pull/2702)
|
||||
|
||||
### Changed
|
||||
- The search index is now only loaded when the search input is opened instead of always being loaded.
|
||||
[#2553](https://github.com/rust-lang/mdBook/pull/2553)
|
||||
[#2735](https://github.com/rust-lang/mdBook/pull/2735)
|
||||
- The `mdbook serve` command has switched its underlying server library from warp to axum.
|
||||
[#2748](https://github.com/rust-lang/mdBook/pull/2748)
|
||||
- Updated dependencies.
|
||||
[#2752](https://github.com/rust-lang/mdBook/pull/2752)
|
||||
|
||||
### Fixed
|
||||
- The sidebar is now set to `display:none` when it is hidden in order to prevent the browser's search from thinking the sidebar's text is visible.
|
||||
[#2725](https://github.com/rust-lang/mdBook/pull/2725)
|
||||
- Fixed search index URL not updating correctly when `hash-files` is enabled.
|
||||
[#2742](https://github.com/rust-lang/mdBook/pull/2742)
|
||||
[#2746](https://github.com/rust-lang/mdBook/pull/2746)
|
||||
- Fixed several sidebar animation bugs, particularly when manually resizing.
|
||||
[#2750](https://github.com/rust-lang/mdBook/pull/2750)
|
||||
|
||||
## mdBook 0.4.51
|
||||
[v0.4.50...v0.4.51](https://github.com/rust-lang/mdBook/compare/v0.4.50...v0.4.51)
|
||||
|
||||
### Fixed
|
||||
- Fixed regression that broke the `S` search hotkey.
|
||||
[#2713](https://github.com/rust-lang/mdBook/pull/2713)
|
||||
|
||||
## mdBook 0.4.50
|
||||
[v0.4.49...v0.4.50](https://github.com/rust-lang/mdBook/compare/v0.4.49...v0.4.50)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a keyboard shortcut help popup when pressing `?`.
|
||||
[#2608](https://github.com/rust-lang/mdBook/pull/2608)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the look of the sidebar resize handle to match the new rustdoc format.
|
||||
[#2691](https://github.com/rust-lang/mdBook/pull/2691)
|
||||
- `/` can now be used to open the search bar.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Pressing enter from the search bar will navigate to the first entry.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Updated `opener` to drop some dependencies.
|
||||
[#2709](https://github.com/rust-lang/mdBook/pull/2709)
|
||||
- Updated dependencies, MSRV raised to 1.82.
|
||||
[#2711](https://github.com/rust-lang/mdBook/pull/2711)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed uncaught exception when pressing down when there are no search results.
|
||||
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
|
||||
- Fixed syntax highlighting of Rust code in the ACE editor.
|
||||
[#2710](https://github.com/rust-lang/mdBook/pull/2710)
|
||||
|
||||
## mdBook 0.4.49
|
||||
[v0.4.48...v0.4.49](https://github.com/rust-lang/mdBook/compare/v0.4.48...v0.4.49)
|
||||
|
||||
### Added
|
||||
|
||||
- Added a warning on unused fields in the root of `book.toml`.
|
||||
[#2622](https://github.com/rust-lang/mdBook/pull/2622)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated dependencies.
|
||||
[#2650](https://github.com/rust-lang/mdBook/pull/2650)
|
||||
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
|
||||
- Updated minimum Rust version to 1.81.
|
||||
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
|
||||
- The unused `book.multilingual` field is no longer serialized, or shown in `mdbook init`.
|
||||
[#2689](https://github.com/rust-lang/mdBook/pull/2689)
|
||||
- Speed up search index loading by using `JSON.parse` instead of parsing JavaScript.
|
||||
[#2633](https://github.com/rust-lang/mdBook/pull/2633)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Search highlighting will not try to highlight in SVG `<text>` elements because it breaks the element.
|
||||
[#2668](https://github.com/rust-lang/mdBook/pull/2668)
|
||||
- Fixed scrolling of the sidebar when a search highlight term is in the URL.
|
||||
[#2675](https://github.com/rust-lang/mdBook/pull/2675)
|
||||
- Fixed issues when multiple footnote definitions use the same ID. Now, only one definition is used, and a warning is displayed.
|
||||
[#2681](https://github.com/rust-lang/mdBook/pull/2681)
|
||||
- The sidebar is now restricted to 80% of the viewport width to make it possible to collapse it when the viewport is very narrow.
|
||||
[#2679](https://github.com/rust-lang/mdBook/pull/2679)
|
||||
|
||||
## mdBook 0.4.48
|
||||
[v0.4.47...v0.4.48](https://github.com/rust-lang/mdBook/compare/v0.4.47...v0.4.48)
|
||||
|
||||
### Added
|
||||
|
||||
- Footnotes now have back-reference links. These links bring the reader back to the original location. As part of this change, footnotes are now only rendered at the bottom of the page. This also includes some styling updates and fixes for footnote rendering.
|
||||
[#2626](https://github.com/rust-lang/mdBook/pull/2626)
|
||||
- Added an "Auto" theme selection option which will default to the system-preferred mode. This will also automatically switch when the system changes the preferred mode.
|
||||
[#2576](https://github.com/rust-lang/mdBook/pull/2576)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `searchindex.json` file has been removed; only the `searchindex.js` file will be generated.
|
||||
[#2552](https://github.com/rust-lang/mdBook/pull/2552)
|
||||
- Updated Javascript code to use eslint.
|
||||
[#2554](https://github.com/rust-lang/mdBook/pull/2554)
|
||||
- An error is generated if there are duplicate files in `SUMMARY.md`.
|
||||
[#2613](https://github.com/rust-lang/mdBook/pull/2613)
|
||||
|
||||
## mdBook 0.4.47
|
||||
[v0.4.46...v0.4.47](https://github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed search not showing up in sub-directories.
|
||||
[#2586](https://github.com/rust-lang/mdBook/pull/2586)
|
||||
|
||||
## mdBook 0.4.46
|
||||
[v0.4.45...v0.4.46](https://github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `output.html.hash-files` config option has been added to add hashes to static filenames to bust any caches when a book is updated. `{{resource}}` template tags have been added so that links can be properly generated to those files.
|
||||
[#1368](https://github.com/rust-lang/mdBook/pull/1368)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Playground links for Rust 2024 now set the edition correctly.
|
||||
[#2557](https://github.com/rust-lang/mdBook/pull/2557)
|
||||
|
||||
## mdBook 0.4.45
|
||||
[v0.4.44...v0.4.45](https://github.com/rust-lang/mdBook/compare/v0.4.44...v0.4.45)
|
||||
|
||||
### Changed
|
||||
|
||||
- Added context to error message when rustdoc is not found.
|
||||
[#2545](https://github.com/rust-lang/mdBook/pull/2545)
|
||||
- Slightly changed the styling rules around margins of footnotes.
|
||||
[#2524](https://github.com/rust-lang/mdBook/pull/2524)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue where it would panic if a source_path is not set.
|
||||
[#2550](https://github.com/rust-lang/mdBook/pull/2550)
|
||||
|
||||
## mdBook 0.4.44
|
||||
[v0.4.43...v0.4.44](https://github.com/rust-lang/mdBook/compare/v0.4.43...v0.4.44)
|
||||
|
||||
### Added
|
||||
|
||||
- Added pre-built aarch64-apple-darwin binaries to the releases.
|
||||
[#2500](https://github.com/rust-lang/mdBook/pull/2500)
|
||||
- `mdbook clean` now shows a summary of what it did.
|
||||
[#2458](https://github.com/rust-lang/mdBook/pull/2458)
|
||||
- Added the `output.html.search.chapter` config setting to disable search indexing of individual chapters.
|
||||
[#2533](https://github.com/rust-lang/mdBook/pull/2533)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed auto-scrolling the side-bar when loading a page with a `#` fragment URL.
|
||||
[#2517](https://github.com/rust-lang/mdBook/pull/2517)
|
||||
- Fixed display of sidebar when javascript is disabled.
|
||||
[#2529](https://github.com/rust-lang/mdBook/pull/2529)
|
||||
- Fixed the sidebar visibility getting out of sync with the button.
|
||||
[#2532](https://github.com/rust-lang/mdBook/pull/2532)
|
||||
|
||||
### Changed
|
||||
|
||||
- ❗ Rust code block hidden lines now follow the same logic as rustdoc. This requires a space after the `#` symbol.
|
||||
[#2530](https://github.com/rust-lang/mdBook/pull/2530)
|
||||
- ❗ Updated the Linux pre-built binaries which requires a newer version of glibc (2.34).
|
||||
[#2523](https://github.com/rust-lang/mdBook/pull/2523)
|
||||
- Updated dependencies
|
||||
[#2538](https://github.com/rust-lang/mdBook/pull/2538)
|
||||
[#2539](https://github.com/rust-lang/mdBook/pull/2539)
|
||||
|
||||
## mdBook 0.4.43
|
||||
[v0.4.42...v0.4.43](https://github.com/rust-lang/mdBook/compare/v0.4.42...v0.4.43)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed setting the title in `mdbook init` when no git user is configured.
|
||||
[#2486](https://github.com/rust-lang/mdBook/pull/2486)
|
||||
|
||||
### Changed
|
||||
|
||||
- The Rust 2024 edition no longer needs `-Zunstable-options`.
|
||||
[#2495](https://github.com/rust-lang/mdBook/pull/2495)
|
||||
|
||||
## mdBook 0.4.42
|
||||
[v0.4.41...v0.4.42](https://github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed chapter list folding.
|
||||
[#2473](https://github.com/rust-lang/mdBook/pull/2473)
|
||||
|
||||
## mdBook 0.4.41
|
||||
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
|
||||
|
||||
**Note:** If you have a custom `index.hbs` theme file, you will need to update it to the latest version.
|
||||
|
||||
### Added
|
||||
|
||||
- Added preliminary support for Rust 2024 edition.
|
||||
[#2398](https://github.com/rust-lang/mdBook/pull/2398)
|
||||
- Added a full example of the remove-emphasis preprocessor.
|
||||
[#2464](https://github.com/rust-lang/mdBook/pull/2464)
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted styling of clipboard/play icons.
|
||||
[#2421](https://github.com/rust-lang/mdBook/pull/2421)
|
||||
- Updated to handlebars v6.
|
||||
[#2416](https://github.com/rust-lang/mdBook/pull/2416)
|
||||
- Attr and section rules now have specific code highlighting.
|
||||
[#2448](https://github.com/rust-lang/mdBook/pull/2448)
|
||||
- The sidebar is now loaded from a common file, significantly reducing the book size when there are many chapters.
|
||||
[#2414](https://github.com/rust-lang/mdBook/pull/2414)
|
||||
- Updated dependencies.
|
||||
[#2470](https://github.com/rust-lang/mdBook/pull/2470)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved theme support when JavaScript is disabled.
|
||||
[#2454](https://github.com/rust-lang/mdBook/pull/2454)
|
||||
- Fixed broken themes when localStorage has an invalid theme id.
|
||||
[#2463](https://github.com/rust-lang/mdBook/pull/2463)
|
||||
- Adjusted the line-height of superscripts (and footnotes) to avoid adding extra space between lines.
|
||||
[#2465](https://github.com/rust-lang/mdBook/pull/2465)
|
||||
|
||||
## mdBook 0.4.40
|
||||
[v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reverted the update to pulldown-cmark which broke the semver API.
|
||||
[#2388](https://github.com/rust-lang/mdBook/pull/2388)
|
||||
|
||||
## mdBook 0.4.39
|
||||
[v0.4.38...v0.4.39](https://github.com/rust-lang/mdBook/compare/v0.4.38...v0.4.39)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the automatic deploy broken in the previous release.
|
||||
[#2383](https://github.com/rust-lang/mdBook/pull/2383)
|
||||
|
||||
## mdBook 0.4.38
|
||||
[v0.4.37...v0.4.38](https://github.com/rust-lang/mdBook/compare/v0.4.37...v0.4.38)
|
||||
|
||||
### Added
|
||||
|
||||
- Added `nix` to the default set of languages supported for syntax highlighting.
|
||||
[#2262](https://github.com/rust-lang/mdBook/pull/2262)
|
||||
|
||||
### Changed
|
||||
|
||||
- The `output.html.curly-quotes` option has been renamed to `output.html.smart-punctuation` to better reflect what it does. The old option `curly-quotes` is kept for compatibility, but may be removed in the future.
|
||||
[#2327](https://github.com/rust-lang/mdBook/pull/2327)
|
||||
- The file-watcher used in `mdbook serve` and `mdbook watch` now uses a poll-based watcher instead of the native operating system notifications. This should fix issues on various systems and environments, and more accurately detect when files change. The native watcher can still be used with the `--watcher native` CLI option.
|
||||
[#2325](https://github.com/rust-lang/mdBook/pull/2325)
|
||||
- `mdbook test` output now includes color, and shows relative paths to the source.
|
||||
[#2259](https://github.com/rust-lang/mdBook/pull/2259)
|
||||
- Updated dependencies, MSRV raised to 1.74
|
||||
[#2350](https://github.com/rust-lang/mdBook/pull/2350)
|
||||
[#2351](https://github.com/rust-lang/mdBook/pull/2351)
|
||||
[#2378](https://github.com/rust-lang/mdBook/pull/2378)
|
||||
[#2381](https://github.com/rust-lang/mdBook/pull/2381)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduced memory allocation when copying files.
|
||||
[#2355](https://github.com/rust-lang/mdBook/pull/2355)
|
||||
- Fixed the horizontal divider in `SUMMARY.md` from being indented into the previous nested section.
|
||||
[#2364](https://github.com/rust-lang/mdBook/pull/2364)
|
||||
- Removed unnecessary `@import` in the CSS.
|
||||
[#2260](https://github.com/rust-lang/mdBook/pull/2260)
|
||||
|
||||
## mdBook 0.4.37
|
||||
[v0.4.36...v0.4.37](https://github.com/rust-lang/mdBook/compare/v0.4.36...v0.4.37)
|
||||
|
||||
### Changed
|
||||
- ❗️ Updated the markdown parser. This brings in many changes to more closely follow the CommonMark spec. This may cause some small rendering changes. It is recommended to compare the output of the old and new version to check for changes. See <https://github.com/raphlinus/pulldown-cmark/releases/tag/v0.10.0> for more information.
|
||||
[#2308](https://github.com/rust-lang/mdBook/pull/2308)
|
||||
- The warning about the legacy `src/theme` directory has been removed.
|
||||
[#2263](https://github.com/rust-lang/mdBook/pull/2263)
|
||||
- Updated dependencies. MSRV raised to 1.71.0.
|
||||
[#2283](https://github.com/rust-lang/mdBook/pull/2283)
|
||||
[#2293](https://github.com/rust-lang/mdBook/pull/2293)
|
||||
[#2297](https://github.com/rust-lang/mdBook/pull/2297)
|
||||
[#2310](https://github.com/rust-lang/mdBook/pull/2310)
|
||||
[#2309](https://github.com/rust-lang/mdBook/pull/2309)
|
||||
- Some internal performance/memory improvements.
|
||||
[#2273](https://github.com/rust-lang/mdBook/pull/2273)
|
||||
[#2290](https://github.com/rust-lang/mdBook/pull/2290)
|
||||
- Made the `pathdiff` dependency optional based on the `watch` feature.
|
||||
[#2291](https://github.com/rust-lang/mdBook/pull/2291)
|
||||
|
||||
### Fixed
|
||||
- The `s` shortcut key handler should not trigger when focus is in an HTML form.
|
||||
[#2311](https://github.com/rust-lang/mdBook/pull/2311)
|
||||
|
||||
## mdBook 0.4.36
|
||||
[v0.4.35...v0.4.36](https://github.com/rust-lang/mdBook/compare/v0.4.35...v0.4.36)
|
||||
|
||||
### Added
|
||||
- Added Nim to the default highlighted languages.
|
||||
[#2232](https://github.com/rust-lang/mdBook/pull/2232)
|
||||
- Added a small indicator for the sidebar resize handle.
|
||||
[#2209](https://github.com/rust-lang/mdBook/pull/2209)
|
||||
|
||||
### Changed
|
||||
- Updated dependencies. MSRV raised to 1.70.0.
|
||||
[#2173](https://github.com/rust-lang/mdBook/pull/2173)
|
||||
[#2250](https://github.com/rust-lang/mdBook/pull/2250)
|
||||
[#2252](https://github.com/rust-lang/mdBook/pull/2252)
|
||||
|
||||
### Fixed
|
||||
- Fixed blank column in print page when the sidebar was visible.
|
||||
[#2235](https://github.com/rust-lang/mdBook/pull/2235)
|
||||
- Fixed indentation of code blocks when Javascript is disabled.
|
||||
[#2162](https://github.com/rust-lang/mdBook/pull/2162)
|
||||
- Fixed a panic when `mdbook serve` or `mdbook watch` were given certain kinds of paths.
|
||||
[#2229](https://github.com/rust-lang/mdBook/pull/2229)
|
||||
|
||||
## mdBook 0.4.35
|
||||
[v0.4.34...v0.4.35](https://github.com/rust-lang/mdBook/compare/v0.4.34...v0.4.35)
|
||||
|
||||
### Added
|
||||
- Added the `book.text-direction` setting for explicit support for right-to-left languages.
|
||||
[#1641](https://github.com/rust-lang/mdBook/pull/1641)
|
||||
- Added `rel=prefetch` to the "next" links to potentially improve browser performance.
|
||||
[#2168](https://github.com/rust-lang/mdBook/pull/2168)
|
||||
- Added a `.warning` CSS class which is styled for displaying warning blocks.
|
||||
[#2187](https://github.com/rust-lang/mdBook/pull/2187)
|
||||
|
||||
### Changed
|
||||
- Better support of the sidebar when JavaScript is disabled.
|
||||
[#2175](https://github.com/rust-lang/mdBook/pull/2175)
|
||||
|
||||
## mdBook 0.4.34
|
||||
[v0.4.33...v0.4.34](https://github.com/rust-lang/mdBook/compare/v0.4.33...v0.4.34)
|
||||
|
||||
### Fixed
|
||||
- Fixed file change watcher failing on macOS with a large number of files.
|
||||
[#2157](https://github.com/rust-lang/mdBook/pull/2157)
|
||||
|
||||
## mdBook 0.4.33
|
||||
[v0.4.32...v0.4.33](https://github.com/rust-lang/mdBook/compare/v0.4.32...v0.4.33)
|
||||
|
||||
### Added
|
||||
- The `color-scheme` CSS property is now set based on the light/dark theme, which applies some slight color differences in browser elements like scroll bars on some browsers.
|
||||
[#2134](https://github.com/rust-lang/mdBook/pull/2134)
|
||||
|
||||
### Fixed
|
||||
- Fixed watching of extra-watch-dirs when not running in the book root directory.
|
||||
[#2146](https://github.com/rust-lang/mdBook/pull/2146)
|
||||
- Reverted the dependency update to the `toml` crate (again!). This was an unintentional breaking change in 0.4.32.
|
||||
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
|
||||
- Changed macOS change notifications to use the kqueue implementation which should fix some issues with repeated rebuilds when a file changed.
|
||||
[#2152](https://github.com/rust-lang/mdBook/pull/2152)
|
||||
- Don't set a background color in the print page for code blocks in a header.
|
||||
[#2150](https://github.com/rust-lang/mdBook/pull/2150)
|
||||
|
||||
## mdBook 0.4.32
|
||||
[v0.4.31...v0.4.32](https://github.com/rust-lang/mdBook/compare/v0.4.31...v0.4.32)
|
||||
|
||||
### Fixed
|
||||
- Fixed theme-color meta tag not syncing with the theme.
|
||||
[#2118](https://github.com/rust-lang/mdBook/pull/2118)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies.
|
||||
[#2121](https://github.com/rust-lang/mdBook/pull/2121)
|
||||
[#2122](https://github.com/rust-lang/mdBook/pull/2122)
|
||||
[#2123](https://github.com/rust-lang/mdBook/pull/2123)
|
||||
[#2124](https://github.com/rust-lang/mdBook/pull/2124)
|
||||
[#2125](https://github.com/rust-lang/mdBook/pull/2125)
|
||||
[#2126](https://github.com/rust-lang/mdBook/pull/2126)
|
||||
|
||||
## mdBook 0.4.31
|
||||
[v0.4.30...v0.4.31](https://github.com/rust-lang/mdBook/compare/v0.4.30...v0.4.31)
|
||||
|
||||
### Fixed
|
||||
- Fixed menu border render flash during page navigation.
|
||||
[#2101](https://github.com/rust-lang/mdBook/pull/2101)
|
||||
- Fixed flicker setting sidebar scroll position.
|
||||
[#2104](https://github.com/rust-lang/mdBook/pull/2104)
|
||||
- Fixed compile error with proc-macro2 on latest Rust nightly.
|
||||
[#2109](https://github.com/rust-lang/mdBook/pull/2109)
|
||||
|
||||
## mdBook 0.4.30
|
||||
[v0.4.29...v0.4.30](https://github.com/rust-lang/mdBook/compare/v0.4.29...v0.4.30)
|
||||
|
||||
### Added
|
||||
- Added support for heading attributes.
|
||||
Attributes are specified in curly braces just after the heading text.
|
||||
An HTML ID can be specified with `#` and classes with `.`.
|
||||
For example: `## My heading {#custom-id .class1 .class2}`
|
||||
[#2013](https://github.com/rust-lang/mdBook/pull/2013)
|
||||
- Added support for hidden code lines for languages other than Rust.
|
||||
The `output.html.code.hidelines` table allows you to define the prefix character that will be used to hide code lines based on the language.
|
||||
[#2093](https://github.com/rust-lang/mdBook/pull/2093)
|
||||
|
||||
### Fixed
|
||||
- Fixed a few minor markdown rendering issues.
|
||||
[#2092](https://github.com/rust-lang/mdBook/pull/2092)
|
||||
|
||||
## mdBook 0.4.29
|
||||
[v0.4.28...v0.4.29](https://github.com/rust-lang/mdBook/compare/v0.4.28...v0.4.29)
|
||||
|
||||
### Changed
|
||||
- Built-in fonts are no longer copied when `fonts/fonts.css` is overridden in the theme directory.
|
||||
Additionally, the warning about `copy-fonts` has been removed if `fonts/fonts.css` is specified.
|
||||
[#2080](https://github.com/rust-lang/mdBook/pull/2080)
|
||||
- `mdbook init --force` now skips all interactive prompts as intended.
|
||||
[#2057](https://github.com/rust-lang/mdBook/pull/2057)
|
||||
- Updated dependencies
|
||||
[#2063](https://github.com/rust-lang/mdBook/pull/2063)
|
||||
[#2086](https://github.com/rust-lang/mdBook/pull/2086)
|
||||
[#2082](https://github.com/rust-lang/mdBook/pull/2082)
|
||||
[#2084](https://github.com/rust-lang/mdBook/pull/2084)
|
||||
[#2085](https://github.com/rust-lang/mdBook/pull/2085)
|
||||
|
||||
### Fixed
|
||||
- Switched from the `gitignore` library to `ignore`. This should bring some improvements with gitignore handling.
|
||||
[#2076](https://github.com/rust-lang/mdBook/pull/2076)
|
||||
|
||||
## mdBook 0.4.28
|
||||
[v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28)
|
||||
|
||||
### Changed
|
||||
- The sidebar is now shown on wide screens when localstorage is disabled.
|
||||
[#2017](https://github.com/rust-lang/mdBook/pull/2017)
|
||||
- Preprocessors are now run with `mdbook test`.
|
||||
[#1986](https://github.com/rust-lang/mdBook/pull/1986)
|
||||
|
||||
### Fixed
|
||||
- Fixed regression in 0.4.26 that prevented the title bar from scrolling properly on smaller screens.
|
||||
[#2039](https://github.com/rust-lang/mdBook/pull/2039)
|
||||
|
||||
## mdBook 0.4.27
|
||||
[v0.4.26...v0.4.27](https://github.com/rust-lang/mdBook/compare/v0.4.26...v0.4.27)
|
||||
|
||||
### Changed
|
||||
- Reverted the dependency update to the `toml` crate. This was an unintentional breaking change in 0.4.26.
|
||||
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
|
||||
|
||||
## mdBook 0.4.26
|
||||
[v0.4.25...v0.4.26](https://github.com/rust-lang/mdBook/compare/v0.4.25...v0.4.26)
|
||||
|
||||
**The 0.4.26 release has been yanked due to an unintentional breaking change.**
|
||||
|
||||
### Changed
|
||||
- Removed custom scrollbars for webkit browsers
|
||||
[#1961](https://github.com/rust-lang/mdBook/pull/1961)
|
||||
- Updated some dependencies
|
||||
[#1998](https://github.com/rust-lang/mdBook/pull/1998)
|
||||
[#2009](https://github.com/rust-lang/mdBook/pull/2009)
|
||||
[#2011](https://github.com/rust-lang/mdBook/pull/2011)
|
||||
- Fonts are now part of the theme.
|
||||
The `output.html.copy-fonts` option has been deprecated.
|
||||
To define custom fonts, be sure to define `theme/fonts.css`.
|
||||
[#1987](https://github.com/rust-lang/mdBook/pull/1987)
|
||||
|
||||
### Fixed
|
||||
- Fixed overflow viewport issue with mobile Safari
|
||||
[#1994](https://github.com/rust-lang/mdBook/pull/1994)
|
||||
|
||||
## mdBook 0.4.25
|
||||
[e14d381...1ba74a3](https://github.com/rust-lang/mdBook/compare/e14d381...1ba74a3)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression where `mdbook test -L deps path-to-book` would not work.
|
||||
[#1959](https://github.com/rust-lang/mdBook/pull/1959)
|
||||
|
||||
## mdBook 0.4.24
|
||||
[eb77083...8767ebf](https://github.com/rust-lang/mdBook/compare/eb77083...8767ebf)
|
||||
|
||||
### Fixed
|
||||
- The precompiled linux-gnu mdbook binary available on [GitHub Releases](https://github.com/rust-lang/mdBook/releases) inadvertently switched to a newer version of glibc. This release goes back to an older version that should be more compatible on older versions of Linux.
|
||||
[#1955](https://github.com/rust-lang/mdBook/pull/1955)
|
||||
|
||||
## mdBook 0.4.23
|
||||
[678b469...68a75da](https://github.com/rust-lang/mdBook/compare/678b469...68a75da)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies
|
||||
[#1951](https://github.com/rust-lang/mdBook/pull/1951)
|
||||
[#1952](https://github.com/rust-lang/mdBook/pull/1952)
|
||||
[#1844](https://github.com/rust-lang/mdBook/pull/1844)
|
||||
- Updated minimum Rust version to 1.60.
|
||||
[#1951](https://github.com/rust-lang/mdBook/pull/1951)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression where playground code was missing hidden lines, preventing it from compiling correctly.
|
||||
[#1950](https://github.com/rust-lang/mdBook/pull/1950)
|
||||
|
||||
## mdBook 0.4.22
|
||||
[40c06f5...4844f72](https://github.com/rust-lang/mdBook/compare/40c06f5...4844f72)
|
||||
|
||||
### Added
|
||||
- Added a `--chapter` option to `mdbook test` to specify a specific chapter to test.
|
||||
[#1741](https://github.com/rust-lang/mdBook/pull/1741)
|
||||
- Added CSS styling for `<kbd>` tags.
|
||||
[#1906](https://github.com/rust-lang/mdBook/pull/1906)
|
||||
- Added pre-compiled binaries for `x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl` (see [Releases](https://github.com/rust-lang/mdBook/releases)).
|
||||
[#1862](https://github.com/rust-lang/mdBook/pull/1862)
|
||||
- Added `build.extra-watch-dirs` which is an array of additional directories to watch for changes when running `mdbook serve`.
|
||||
[#1884](https://github.com/rust-lang/mdBook/pull/1884)
|
||||
|
||||
### Changed
|
||||
- Removed the `type="text/javascript"` attribute from `<script>` tags.
|
||||
[#1881](https://github.com/rust-lang/mdBook/pull/1881)
|
||||
- Switched to building with Rust Edition 2021.
|
||||
This raises the minimum supported Rust version to 1.56.
|
||||
[#1887](https://github.com/rust-lang/mdBook/pull/1887)
|
||||
- When hidden code is hidden, the hidden parts are no longer copied to the clipboard via the copy button.
|
||||
[#1911](https://github.com/rust-lang/mdBook/pull/1911)
|
||||
- Various HTML changes and fixes to be more compliant with HTML5.
|
||||
[#1924](https://github.com/rust-lang/mdBook/pull/1924)
|
||||
- The theme picker now shows which theme is currently selected.
|
||||
[#1935](https://github.com/rust-lang/mdBook/pull/1935)
|
||||
|
||||
### Fixed
|
||||
- Avoid blank line at the end of an ACE code block
|
||||
[#1836](https://github.com/rust-lang/mdBook/pull/1836)
|
||||
|
||||
|
||||
## mdBook 0.4.21
|
||||
[92afe9b...8f01d02](https://github.com/rust-lang/mdBook/compare/92afe9b...8f01d02)
|
||||
|
||||
### Fixed
|
||||
- Fixed an issue where mdBook would fail to compile with Rust nightly-2022-07-22.
|
||||
[#1861](https://github.com/rust-lang/mdBook/pull/1861)
|
||||
|
||||
## mdBook 0.4.20
|
||||
[53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0)
|
||||
|
||||
### Fixed
|
||||
- Fixed a regression in 0.4.19 where inline code would have excessive padding
|
||||
in some situations such as headings.
|
||||
[#1855](https://github.com/rust-lang/mdBook/pull/1855)
|
||||
|
||||
## mdBook 0.4.19
|
||||
[ae275ad...53055e0](https://github.com/rust-lang/mdBook/compare/ae275ad...53055e0)
|
||||
|
||||
### Added
|
||||
- The `serve` command now supports HEAD requests.
|
||||
[#1825](https://github.com/rust-lang/mdBook/pull/1825)
|
||||
|
||||
### Changed
|
||||
- An error is now generated when a custom theme directory does not exist.
|
||||
[#1791](https://github.com/rust-lang/mdBook/pull/1791)
|
||||
- Very wide tables now have independent horizontal scrolling so that scrolling
|
||||
to see the rest of the table will not scroll the entire page.
|
||||
[#1617](https://github.com/rust-lang/mdBook/pull/1617)
|
||||
- The buttons on code blocks are now only shown when the mouse cursor hovers
|
||||
over them (or tapped on mobile). There is also some extra spacing to reduce
|
||||
the overlap with the code.
|
||||
[#1806](https://github.com/rust-lang/mdBook/pull/1806)
|
||||
- The first chapter always generates an `index.html` file. Previously it would
|
||||
only generate the index file for prefix chapters.
|
||||
[#1829](https://github.com/rust-lang/mdBook/pull/1829)
|
||||
|
||||
### Fixed
|
||||
- `mdbook serve --open` now properly handles the case if the first chapter is a draft.
|
||||
[#1714](https://github.com/rust-lang/mdBook/pull/1714)
|
||||
[#1830](https://github.com/rust-lang/mdBook/pull/1830)
|
||||
- Very long words (over 80 characters) are no longer indexed to avoid a stack overflow.
|
||||
[#1833](https://github.com/rust-lang/mdBook/pull/1833)
|
||||
|
||||
## mdBook 0.4.18
|
||||
[981b79b...ae275ad](https://github.com/rust-lang/mdBook/compare/981b79b...ae275ad)
|
||||
|
||||
### Fixed
|
||||
- Fixed rendering of SUMMARY links that contain markdown escapes or other
|
||||
markdown elements.
|
||||
[#1785](https://github.com/rust-lang/mdBook/pull/1785)
|
||||
|
||||
## mdBook 0.4.17
|
||||
[a5fddfa...981b79b](https://github.com/rust-lang/mdBook/compare/a5fddfa...981b79b)
|
||||
|
||||
### Fixed
|
||||
- Fixed parsing of `output.html.print` configuration table.
|
||||
[#1775](https://github.com/rust-lang/mdBook/pull/1775)
|
||||
|
||||
## mdBook 0.4.16
|
||||
[68a5c09...a5fddfa](https://github.com/rust-lang/mdBook/compare/68a5c09...a5fddfa)
|
||||
|
||||
### Added
|
||||
- Added `output.html.print.page-break` config option to control whether or not
|
||||
there is a page break between chapters in the print output.
|
||||
[#1728](https://github.com/rust-lang/mdBook/pull/1728)
|
||||
- Added `output.html.playground.runnable` config option to globally disable
|
||||
the run button in code blocks.
|
||||
[#1546](https://github.com/rust-lang/mdBook/pull/1546)
|
||||
|
||||
### Changed
|
||||
- The `mdbook serve` live reload websocket now uses the protocol, host, and
|
||||
port of the current page, allowing access through a proxy.
|
||||
[#1771](https://github.com/rust-lang/mdBook/pull/1771)
|
||||
- The 404 not-found page now includes the books title in the HTML title tag.
|
||||
[#1693](https://github.com/rust-lang/mdBook/pull/1693)
|
||||
- Migrated to clap 3.0 which handles CLI option parsing.
|
||||
[#1731](https://github.com/rust-lang/mdBook/pull/1731)
|
||||
|
||||
### Fixed
|
||||
- Minor fixes to the markdown parser.
|
||||
[#1729](https://github.com/rust-lang/mdBook/pull/1729)
|
||||
- Fixed incorrect parsing in `SUMMARY.md` when it didn't start with a title.
|
||||
[#1744](https://github.com/rust-lang/mdBook/pull/1744)
|
||||
- Fixed duplicate anchor IDs for links in search results.
|
||||
[#1749](https://github.com/rust-lang/mdBook/pull/1749)
|
||||
|
||||
## mdBook 0.4.15
|
||||
[5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09)
|
||||
|
||||
|
||||
170
CONTRIBUTING.md
170
CONTRIBUTING.md
@@ -7,13 +7,22 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
|
||||
|
||||
### Issues to work on
|
||||
## Issue assignment
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
**:warning: Important :warning:**
|
||||
|
||||
Before working on pull request, please ping us on the corresponding issue.
|
||||
The current PR backlog is beyond what we can process at this time.
|
||||
Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews.
|
||||
If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review.
|
||||
|
||||
## Issues to work on
|
||||
|
||||
If you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
These issues can be a good launching pad for more involved issues.
|
||||
Easy tasks for a first time contribution include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
|
||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||
[A-JavaScript](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
@@ -21,18 +30,18 @@ If you come from a web development background, you might be interested in issues
|
||||
[A-HTML](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
|
||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||
When you decide you want to work on a specific issue, and it isn't already assigned to someone else, assign the issue to yourself by leaving a comment with the text `@rustbot claim`.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
|
||||
Issues on the issue tracker are categorized with the following labels:
|
||||
|
||||
- **A**-prefixed labels state which area of the project an issue relates to.
|
||||
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
||||
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
|
||||
- **M**-prefixed labels are meta-issues regarding the management of the mdBook project itself
|
||||
- **S**-prefixed labels show the status of the issue
|
||||
- **T**-prefixed labels show the type of issue
|
||||
- **C**-prefixed labels show the category of issue
|
||||
|
||||
### Building mdBook
|
||||
## Building mdBook
|
||||
|
||||
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
|
||||
|
||||
@@ -47,11 +56,11 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
|
||||
|
||||
### Code Quality
|
||||
## Code quality
|
||||
|
||||
We love code quality and Rust has some excellent tools to assist you with contributions.
|
||||
|
||||
#### Formatting Code with rustfmt
|
||||
### Formatting code with rustfmt
|
||||
|
||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||
This will ensure we have good quality source code that is better for us all to maintain.
|
||||
@@ -59,7 +68,7 @@ This will ensure we have good quality source code that is better for us all to m
|
||||
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
|
||||
The quick guide is
|
||||
|
||||
1. Install it
|
||||
1. Install it (`rustfmt` is usually installed by default via [rustup](https://rustup.rs/)):
|
||||
```
|
||||
rustup component add rustfmt
|
||||
```
|
||||
@@ -71,18 +80,14 @@ The quick guide is
|
||||
```
|
||||
cargo fmt
|
||||
```
|
||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
||||
When run through `cargo` it will format all bin and lib files in the current package.
|
||||
|
||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
|
||||
|
||||
### Finding issues with clippy
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang/rust-clippy)
|
||||
[Clippy](https://doc.rust-lang.org/clippy/) is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will help us maintain awesome code.
|
||||
|
||||
1. To install
|
||||
```
|
||||
@@ -93,17 +98,63 @@ The best documentation can be found over at [rust-clippy](https://github.com/rus
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang.github.io/rust-clippy/master/index.html).
|
||||
## Change requirements
|
||||
|
||||
### Making a pull-request
|
||||
Please consider the following when making a change:
|
||||
|
||||
* Almost all changes that modify the Rust code must be accompanied with a test.
|
||||
|
||||
* Almost all features and changes must update the documentation.
|
||||
mdBook has the [mdBook Guide](https://rust-lang.github.io/mdBook/) whose source is at <https://github.com/rust-lang/mdBook/tree/master/guide>.
|
||||
|
||||
* Almost all Rust items should be documented with doc comments.
|
||||
See the [Rustdoc Book](https://doc.rust-lang.org/rustdoc/) for more information on writing doc comments.
|
||||
|
||||
* Breaking the API can only be done in major SemVer releases.
|
||||
These are done very infrequently, so it is preferred to avoid these when possible.
|
||||
See [SemVer Compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for more information on what a SemVer breaking change is.
|
||||
|
||||
(Note: At this time, some SemVer breaking changes are inevitable due to the current code structure.
|
||||
An example is adding new fields to the config structures.
|
||||
These are intended to be fixed in the next major release.)
|
||||
|
||||
* Similarly, the CLI interface is considered to be stable.
|
||||
Care should be taken to avoid breaking existing workflows.
|
||||
|
||||
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
|
||||
|
||||
## Tests
|
||||
|
||||
The main test harness is described in the [testsuite documentation](tests/testsuite/README.md). There are several different commands to run different kinds of tests:
|
||||
|
||||
- `cargo test --workspace` — This runs all of the unit and integration tests, except for the GUI tests.
|
||||
- `cargo test --test gui` — This runs the [GUI test harness](#browser-compatibility-and-testing). This does not get run automatically due to its extra requirements.
|
||||
- `npm run lint` — [Checks the `.js` files](#checking-changes-in-js-files)
|
||||
- `cargo test --workspace --no-default-features` — Testing without default features helps check that all feature checks are implemented correctly.
|
||||
- `cargo clippy --workspace --all-targets --no-deps -- -D warnings` — This makes sure that there are no clippy warnings.
|
||||
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --document-private-items --no-deps` — This verifies that there aren't any rustdoc warnings.
|
||||
- `cargo fmt --check` — Verifies that everything is formatted correctly.
|
||||
- `cargo +stable semver-checks` — Verifies that no SemVer breaking changes have been made. You must install [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks) first.
|
||||
|
||||
To help simplify running all these commands, you can run the following cargo command:
|
||||
|
||||
```sh
|
||||
cargo xtask test-all
|
||||
```
|
||||
|
||||
It is useful to run all tests before submitting a PR. While developing I recommend to run some subset of that command based on what you are working on. There are individual arguments for each one. For example:
|
||||
|
||||
```sh
|
||||
cargo xtask test-workspace clippy doc eslint fmt gui semver-checks
|
||||
```
|
||||
|
||||
While developing, remove any of those arguments that are not relevant to what you are changing, or are really slow.
|
||||
|
||||
## Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||
|
||||
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
||||
and [rustfmt](https://github.com/rust-lang/rustfmt) on the code first.
|
||||
This is not a requirement though and will never block a pull-request from being merged.
|
||||
|
||||
That's it, happy contributions! :tada: :tada: :tada:
|
||||
|
||||
## Browser compatibility and testing
|
||||
@@ -113,18 +164,77 @@ We generally strive to keep mdBook compatible with a relatively recent browser o
|
||||
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
|
||||
If possible, do your best to avoid breaking older browser releases.
|
||||
|
||||
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||
GUI tests are checked with the GUI testsuite. To run it, you need to install `npm` first. Then run:
|
||||
|
||||
## Updating higlight.js
|
||||
```
|
||||
cargo test --test gui
|
||||
```
|
||||
|
||||
If you want to only run some tests, you can filter them by passing (part of) their name:
|
||||
|
||||
```
|
||||
cargo test --test gui -- search
|
||||
```
|
||||
|
||||
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it with the provided
|
||||
command then re-run the tests.
|
||||
|
||||
If you want to disable the headless mode, use the `--disable-headless-test` option:
|
||||
|
||||
```
|
||||
cargo test --test gui -- --disable-headless-test
|
||||
```
|
||||
|
||||
The GUI tests are in the directory `tests/gui` in text files with the `.goml` extension. The books that the tests use are located in the `tests/gui/books` directory. These tests are run using a `node.js` framework called `browser-ui-test`. You can find documentation for this language on its [repository](https://github.com/GuillaumeGomez/browser-UI-test/blob/master/goml-script.md).
|
||||
|
||||
### Checking changes in `.js` files
|
||||
|
||||
The `.js` files source code is checked using [`eslint`](https://eslint.org/). This is a linter (just like `clippy` in Rust)
|
||||
for the Javascript language. You can install it with `npm` by running the following command:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Then you can run it using:
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Updating highlight.js
|
||||
|
||||
The following are instructions for updating [highlight.js](https://highlightjs.org/).
|
||||
|
||||
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||
1. Check out a tagged release (like `10.1.1`).
|
||||
1. Run `npm install`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim nix properties r scala x86asm yaml`
|
||||
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
|
||||
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [syntax GUI test](https://github.com/rust-lang/mdBook/tree/master/tests/gui/books/highlighting) contains a chapter with many languages to examine. Update the test (`highlighting.goml`) to add any new languages.
|
||||
|
||||
## Publishing new releases
|
||||
|
||||
Instructions for mdBook maintainers to publish a new release:
|
||||
|
||||
1. Create a PR that bumps the version and updates the changelog:
|
||||
1. `git fetch upstream`
|
||||
2. `git checkout -B bump-version upstream/master && git branch --set-upstream-to=origin/bump-version`
|
||||
3. `cargo xtask bump <BUMP>`
|
||||
- This will update the version of all the crates.
|
||||
- `cargo set-version` must first be installed with `cargo install cargo-edit`.
|
||||
- Replace `<BUMP>` with the kind of bump (patch, alpha, etc.)
|
||||
4. `cargo xtask changelog`
|
||||
- This will update `CHANGELOG.md` to add a list of all changes at the top. You will need to move those into the appropriate categories. Most changes that are generally not relevant to a user should be removed. Rewrite the descriptions so that a user can reasonably figure out what it means.
|
||||
5. `git add --update .`
|
||||
6. `git commit`
|
||||
7. `git push`
|
||||
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
|
||||
```bash
|
||||
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \
|
||||
gh release create -R rust-lang/mdbook v$MDBOOK_VERS \
|
||||
--title v$MDBOOK_VERS \
|
||||
--notes "See https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#mdbook-${MDBOOK_VERS//.} for a complete list of changes."
|
||||
```
|
||||
|
||||
2536
Cargo.lock
generated
2536
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
172
Cargo.toml
172
Cargo.toml
@@ -1,67 +1,157 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"crates/*",
|
||||
"examples/remove-emphasis/mdbook-remove-emphasis", "guide/guide-helper",
|
||||
]
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "allow", priority = -2 }
|
||||
correctness = { level = "warn", priority = -1 }
|
||||
complexity = { level = "warn", priority = -1 }
|
||||
exhaustive_enums = "warn"
|
||||
exhaustive_structs = "warn"
|
||||
manual_non_exhaustive = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
rust_2018_idioms = "warn"
|
||||
unreachable_pub = "warn"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
rust-version = "1.88.0" # Keep in sync with installation.md and .github/workflows/main.yml
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
axum = "0.8.7"
|
||||
clap = { version = "4.5.53", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.5.61"
|
||||
ego-tree = "0.10.0"
|
||||
elasticlunr-rs = "3.0.2"
|
||||
font-awesome-as-a-crate = "0.3.0"
|
||||
futures-util = "0.3.31"
|
||||
glob = "0.3.3"
|
||||
handlebars = "6.3.2"
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.36.0"
|
||||
indexmap = "2.12.1"
|
||||
ignore = "0.4.25"
|
||||
mdbook-core = { path = "crates/mdbook-core", version = "0.5.2" }
|
||||
mdbook-driver = { path = "crates/mdbook-driver", version = "0.5.2" }
|
||||
mdbook-html = { path = "crates/mdbook-html", version = "0.5.2" }
|
||||
mdbook-markdown = { path = "crates/mdbook-markdown", version = "0.5.2" }
|
||||
mdbook-preprocessor = { path = "crates/mdbook-preprocessor", version = "0.5.2" }
|
||||
mdbook-renderer = { path = "crates/mdbook-renderer", version = "0.5.2" }
|
||||
mdbook-summary = { path = "crates/mdbook-summary", version = "0.5.2" }
|
||||
memchr = "2.7.6"
|
||||
notify = "8.2.0"
|
||||
notify-debouncer-mini = "0.7.0"
|
||||
opener = "0.8.3"
|
||||
pathdiff = "0.2.3"
|
||||
pulldown-cmark = { version = "0.13.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
|
||||
regex = "1.12.2"
|
||||
select = "0.6.1"
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
sha2 = "0.10.9"
|
||||
shlex = "1.3.0"
|
||||
snapbox = "0.6.23"
|
||||
tempfile = "3.23.0"
|
||||
tokio = "1.48.0"
|
||||
toml = "0.9.8"
|
||||
topological-sort = "0.2.2"
|
||||
tower-http = "0.6.7"
|
||||
tracing = "0.1.43"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.15"
|
||||
version = "0.5.2"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
documentation = "https://rust-lang.github.io/mdBook/index.html"
|
||||
edition.workspace = true
|
||||
exclude = ["/guide/*"]
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
repository.workspace = true
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
clap = "2.24"
|
||||
env_logger = "0.7.1"
|
||||
handlebars = "4.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
memchr = "2.0"
|
||||
opener = "0.5"
|
||||
pulldown-cmark = "0.9.0"
|
||||
regex = "1.0.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
shlex = "1"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
topological-sort = "0.1.0"
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
mdbook-core.workspace = true
|
||||
mdbook-driver.workspace = true
|
||||
mdbook-html.workspace = true
|
||||
mdbook-markdown.workspace = true
|
||||
mdbook-preprocessor.workspace = true
|
||||
mdbook-renderer.workspace = true
|
||||
mdbook-summary.workspace = true
|
||||
opener.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
gitignore = { version = "1.0", optional = true }
|
||||
ignore = { workspace = true, optional = true }
|
||||
notify = { workspace = true, optional = true }
|
||||
notify-debouncer-mini = { workspace = true, optional = true }
|
||||
pathdiff = { workspace = true, optional = true }
|
||||
walkdir = { workspace = true, optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.1", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "3", optional = true }
|
||||
axum = { workspace = true, features = ["ws"], optional = true }
|
||||
futures-util = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"], optional = true }
|
||||
tower-http = { workspace = true, features = ["fs", "trace"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "1"
|
||||
predicates = "2"
|
||||
select = "0.5"
|
||||
semver = "0.11.0"
|
||||
pretty_assertions = "0.6"
|
||||
walkdir = "2.0"
|
||||
glob.workspace = true
|
||||
regex.workspace = true
|
||||
select.workspace = true
|
||||
semver.workspace = true
|
||||
serde_json.workspace = true
|
||||
snapbox = { workspace = true, features = ["diff", "dir", "term-svg", "regex", "json"] }
|
||||
tempfile.workspace = true
|
||||
walkdir.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["notify", "gitignore"]
|
||||
serve = ["futures-util", "tokio", "warp"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
|
||||
search = ["mdbook-html/search"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
name = "mdbook"
|
||||
|
||||
[[example]]
|
||||
name = "nop-preprocessor"
|
||||
test = true
|
||||
|
||||
[[example]]
|
||||
name = "remove-emphasis"
|
||||
path = "examples/remove-emphasis/test.rs"
|
||||
crate-type = ["lib"]
|
||||
test = true
|
||||
|
||||
[[test]]
|
||||
harness = false
|
||||
test = false
|
||||
name = "gui"
|
||||
path = "tests/gui/runner.rs"
|
||||
crate-type = ["bin"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mdBook
|
||||
|
||||
[](https://github.com/rust-lang/mdBook/actions?workflow=CI)
|
||||
[](https://github.com/rust-lang/mdBook/actions/workflows/main.yml)
|
||||
[](https://crates.io/crates/mdbook)
|
||||
[](LICENSE)
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the `hub` executable into hub/bin
|
||||
set -ex
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-linux-amd64-2.12.8.tgz -o hub.tgz
|
||||
mkdir hub
|
||||
tar -xzvf hub.tgz --strip=1 -C hub
|
||||
;;
|
||||
macos*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-darwin-amd64-2.12.8.tgz -o hub.tgz
|
||||
mkdir hub
|
||||
tar -xzvf hub.tgz --strip=1 -C hub
|
||||
;;
|
||||
windows*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-windows-amd64-2.12.8.zip -o hub.zip
|
||||
7z x hub.zip -ohub
|
||||
;;
|
||||
*)
|
||||
echo "OS should be first parameter, was: $1"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$PWD/hub/bin" >> $GITHUB_PATH
|
||||
@@ -13,6 +13,30 @@ TOOLCHAIN="$1"
|
||||
rustup set profile minimal
|
||||
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
|
||||
rustup update --no-self-update $TOOLCHAIN
|
||||
if [ -n "$2" ]
|
||||
then
|
||||
TARGET="$2"
|
||||
HOST=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
if [ "$HOST" != "$TARGET" ]
|
||||
then
|
||||
rustup component add llvm-tools-preview --toolchain=$TOOLCHAIN
|
||||
rustup component add rust-std-$TARGET --toolchain=$TOOLCHAIN
|
||||
fi
|
||||
if [[ $TARGET == *"musl" ]]
|
||||
then
|
||||
# This is needed by libdbus-sys.
|
||||
sudo apt update -y && sudo apt install musl-dev musl-tools -y
|
||||
fi
|
||||
if [[ $TARGET == "aarch64-unknown-linux-musl" ]]
|
||||
then
|
||||
echo CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=rust-lld >> $GITHUB_ENV
|
||||
# This `CC` is some nonsense needed for libdbus-sys (via opener).
|
||||
# I don't know if this is really the right thing to do, but it seems to work.
|
||||
sudo apt install gcc-aarch64-linux-gnu -y
|
||||
echo CC=aarch64-linux-gnu-gcc >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
rustup default $TOOLCHAIN
|
||||
rustup -V
|
||||
rustc -Vv
|
||||
|
||||
@@ -11,16 +11,17 @@ fi
|
||||
TAG=${GITHUB_REF#*/tags/}
|
||||
|
||||
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
target=$2
|
||||
export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --bin mdbook --release
|
||||
cd target/release
|
||||
cargo build --locked --bin mdbook --release --target $target
|
||||
cd target/$target/release
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
asset="mdbook-$TAG-$target.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
asset="mdbook-$TAG-$target.tar.gz"
|
||||
# There is a bug with BSD tar on macOS where the first 8MB of the file are
|
||||
# sometimes all NUL bytes. See https://github.com/actions/cache/issues/403
|
||||
# and https://github.com/rust-lang/cargo/issues/8603 for some more
|
||||
@@ -30,7 +31,7 @@ case $1 in
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
windows*)
|
||||
asset="mdbook-$TAG-$host.zip"
|
||||
asset="mdbook-$TAG-$target.zip"
|
||||
7z a ../../$asset mdbook.exe
|
||||
;;
|
||||
*)
|
||||
@@ -39,9 +40,10 @@ case $1 in
|
||||
esac
|
||||
cd ../..
|
||||
|
||||
if [[ -z "$GITHUB_TOKEN" ]]
|
||||
if [[ -z "$GITHUB_ENV" ]]
|
||||
then
|
||||
echo "$GITHUB_TOKEN not set, skipping deploy."
|
||||
echo "GITHUB_ENV not set, run: gh release upload $TAG target/$asset"
|
||||
else
|
||||
hub release edit -m "" --attach $asset $TAG
|
||||
echo "MDBOOK_TAG=$TAG" >> $GITHUB_ENV
|
||||
echo "MDBOOK_ASSET=target/$asset" >> $GITHUB_ENV
|
||||
fi
|
||||
38
ci/publish-guide.sh
Executable file
38
ci/publish-guide.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# This publishes the user guide to GitHub Pages.
|
||||
#
|
||||
# If this is a pre-release, then it goes in a separate directory called "pre-release".
|
||||
# Commits are amended to avoid keeping history which can balloon the repo size.
|
||||
set -ex
|
||||
|
||||
cargo run --no-default-features -F search -- build guide
|
||||
|
||||
VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[] | select(.name == "mdbook") | .version')
|
||||
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
PRERELEASE=true
|
||||
else
|
||||
PRERELEASE=false
|
||||
fi
|
||||
|
||||
git fetch origin gh-pages
|
||||
git worktree add gh-pages gh-pages
|
||||
git config user.name "Deploy from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
if [[ "$PRERELEASE" == "true" ]]
|
||||
then
|
||||
rm -rf pre-release
|
||||
mv ../guide/book pre-release
|
||||
git add pre-release
|
||||
git commit --amend -m "Deploy $GITHUB_SHA pre-release to gh-pages"
|
||||
else
|
||||
# Delete everything except pre-release and .git.
|
||||
find . -mindepth 1 -maxdepth 1 -not -name "pre-release" -not -name ".git" -exec rm -rf {} +
|
||||
# Copy the guide here.
|
||||
find ../guide/book/ -mindepth 1 -maxdepth 1 -exec mv {} . \;
|
||||
git add .
|
||||
git commit --amend -m "Deploy $GITHUB_SHA to gh-pages"
|
||||
fi
|
||||
|
||||
git push --force origin +gh-pages
|
||||
44
ci/update-dependencies.sh
Executable file
44
ci/update-dependencies.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Updates all compatible Cargo dependencies.
|
||||
#
|
||||
# I wasn't able to get Renovate to update compatible dependencies in a way
|
||||
# that I like, so this script takes care of it. This uses `cargo upgrade` to
|
||||
# ensure that `Cargo.toml` also gets updated. This also makes sure that all
|
||||
# transitive dependencies are updated.
|
||||
|
||||
set -ex
|
||||
|
||||
git fetch origin update-dependencies
|
||||
if git checkout update-dependencies
|
||||
then
|
||||
git reset --hard origin/master
|
||||
else
|
||||
git checkout -b update-dependencies
|
||||
fi
|
||||
|
||||
cat > commit-message << 'EOF'
|
||||
Update cargo dependencies
|
||||
|
||||
```
|
||||
EOF
|
||||
cargo upgrade >> commit-message
|
||||
echo '```' >> commit-message
|
||||
if git diff --quiet
|
||||
then
|
||||
echo "No changes detected, exiting."
|
||||
exit 0
|
||||
fi
|
||||
# Also update any transitive dependencies.
|
||||
cargo update
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add Cargo.toml Cargo.lock
|
||||
git commit -F commit-message
|
||||
|
||||
git push --force origin update-dependencies
|
||||
|
||||
gh pr create --fill \
|
||||
--head update-dependencies \
|
||||
--base master
|
||||
12
crates/mdbook-compare/Cargo.toml
Normal file
12
crates/mdbook-compare/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "mdbook-compare"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
26
crates/mdbook-compare/README.md
Normal file
26
crates/mdbook-compare/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# mdbook-compare
|
||||
|
||||
This is a simple utility to compare the output of two different versions of mdbook.
|
||||
|
||||
To use this:
|
||||
|
||||
1. Install [`tidy`](https://www.html-tidy.org/).
|
||||
2. Install or build the initial version of mdbook that you want to compare.
|
||||
3. Install or build the new version of mdbook that you want to compare.
|
||||
4. Run `mdbook-compare` with the arguments to the mdbook executables and the books to build.
|
||||
|
||||
```sh
|
||||
cargo run --manifest-path /path/to/mdBook/Cargo.toml -p mdbook-compare -- \
|
||||
/path/to/orig/mdbook /path/to/my-book /path/to/new/mdbook /path/to/my-book
|
||||
```
|
||||
|
||||
It takes two separate paths for the book to use for "before" and "after" in case you need to customize the book to run on older versions. If you don't need that, then you can use the same directory for both the before and after.
|
||||
|
||||
`mdbook-compare` will do the following:
|
||||
|
||||
1. Clean up any book directories.
|
||||
2. Build the book with the first mdbook.
|
||||
3. Build the book with the second mdbook.
|
||||
4. The output of those two commands are stored in directories called `compare1` and `compare2`.
|
||||
5. The HTML in those directories is normalized using `tidy`.
|
||||
6. Runs `git diff` to compare the output.
|
||||
113
crates/mdbook-compare/src/main.rs
Normal file
113
crates/mdbook-compare/src/main.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Utility to compare the output of two different versions of mdbook.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
macro_rules! error {
|
||||
($msg:literal $($arg:tt)*) => {
|
||||
eprint!("error: ");
|
||||
eprintln!($msg $($arg)*);
|
||||
std::process::exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let (Some(mdbook1), Some(book1), Some(mdbook2), Some(book2)) =
|
||||
(args.next(), args.next(), args.next(), args.next())
|
||||
else {
|
||||
eprintln!("error: Expected four arguments: <exe1> <dir1> <exe2> <dir2>");
|
||||
std::process::exit(1);
|
||||
};
|
||||
let mdbook1 = Path::new(&mdbook1);
|
||||
let mdbook2 = Path::new(&mdbook2);
|
||||
let book1 = Path::new(&book1);
|
||||
let book2 = Path::new(&book2);
|
||||
let compare1 = Path::new("compare1");
|
||||
let compare2 = Path::new("compare2");
|
||||
clean(compare1);
|
||||
clean(compare2);
|
||||
clean(&book1.join("book"));
|
||||
clean(&book2.join("book"));
|
||||
build(mdbook1, book1);
|
||||
std::fs::rename(book1.join("book"), compare1).unwrap();
|
||||
build(mdbook2, book2);
|
||||
std::fs::rename(book2.join("book"), compare2).unwrap();
|
||||
diff(compare1, compare2);
|
||||
}
|
||||
|
||||
fn clean(path: &Path) {
|
||||
if path.exists() {
|
||||
println!("removing {path:?}");
|
||||
std::fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn build(mdbook: &Path, book: &Path) {
|
||||
println!("running `{mdbook:?} build` in `{book:?}`");
|
||||
let status = Command::new(mdbook)
|
||||
.arg("build")
|
||||
.current_dir(book)
|
||||
.status()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("expected {mdbook:?} executable to exist: {e}");
|
||||
});
|
||||
if !status.success() {
|
||||
error!("process {mdbook:?} failed");
|
||||
}
|
||||
process(&book.join("book"));
|
||||
}
|
||||
|
||||
fn process(path: &Path) {
|
||||
for entry in std::fs::read_dir(path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
process(&path);
|
||||
} else {
|
||||
if path.extension().is_some_and(|ext| ext == "html") {
|
||||
tidy(&path);
|
||||
process_html(&path);
|
||||
} else {
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_html(path: &Path) {
|
||||
let content = std::fs::read_to_string(path).unwrap();
|
||||
let Some(start_index) = content.find("<main>") else {
|
||||
return;
|
||||
};
|
||||
let end_index = content.rfind("</main>").unwrap();
|
||||
let new_content = &content[start_index..end_index + 8];
|
||||
std::fs::write(path, new_content).unwrap();
|
||||
}
|
||||
|
||||
fn tidy(path: &Path) {
|
||||
// quiet, no wrap, modify in place
|
||||
let args = "-q -w 0 -m --custom-tags yes --drop-empty-elements no";
|
||||
println!("running `tidy {args}` in `{path:?}`");
|
||||
let status = Command::new("tidy")
|
||||
.args(args.split(' '))
|
||||
.arg(path)
|
||||
.status()
|
||||
.expect("tidy should be installed");
|
||||
if !status.success() {
|
||||
// Exit code 1 is a warning.
|
||||
if status.code() != Some(1) {
|
||||
error!("tidy failed: {status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diff(a: &Path, b: &Path) {
|
||||
let args = "diff --no-index";
|
||||
println!("running `git {args} {a:?} {b:?}`");
|
||||
Command::new("git")
|
||||
.args(args.split(' '))
|
||||
.args([a, b])
|
||||
.status()
|
||||
.unwrap();
|
||||
}
|
||||
22
crates/mdbook-core/Cargo.toml
Normal file
22
crates/mdbook-core/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "mdbook-core"
|
||||
version = "0.5.2"
|
||||
description = "The base support library for mdbook, intended for internal use only"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
13
crates/mdbook-core/README.md
Normal file
13
crates/mdbook-core/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# mdbook-core
|
||||
|
||||
[](https://docs.rs/mdbook-core)
|
||||
[](https://crates.io/crates/mdbook-core)
|
||||
[](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
|
||||
|
||||
This is the base support library for [mdBook](https://rust-lang.github.io/mdBook/). It is intended for internal use only. Other mdBook crates depend on this for any types that are shared across the crates.
|
||||
|
||||
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)
|
||||
288
crates/mdbook-core/src/book.rs
Normal file
288
crates/mdbook-core/src/book.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
//! A tree structure representing a book.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// A tree structure representing a book.
|
||||
///
|
||||
/// A book is just a collection of [`BookItems`] which are accessible by
|
||||
/// either iterating (immutably) over the book with [`iter()`], or recursively
|
||||
/// applying a closure to each item to mutate the chapters, using
|
||||
/// [`for_each_mut()`].
|
||||
///
|
||||
/// [`iter()`]: #method.iter
|
||||
/// [`for_each_mut()`]: #method.for_each_mut
|
||||
#[allow(
|
||||
clippy::exhaustive_structs,
|
||||
reason = "This cannot be extended without breaking preprocessors."
|
||||
)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Book {
|
||||
/// The items in this book.
|
||||
pub items: Vec<BookItem>,
|
||||
}
|
||||
|
||||
impl Book {
|
||||
/// Create an empty book.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Creates a new book with the given items.
|
||||
pub fn new_with_items(items: Vec<BookItem>) -> Book {
|
||||
Book { items }
|
||||
}
|
||||
|
||||
/// Get a depth-first iterator over the items in the book.
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
BookItems {
|
||||
items: self.items.iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A depth-first iterator over each [`Chapter`], skipping draft chapters.
|
||||
pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
|
||||
self.iter().filter_map(|item| match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recursively apply a closure to each item in the book, allowing you to
|
||||
/// mutate them.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Unlike the `iter()` method, this requires a closure instead of returning
|
||||
/// an iterator. This is because using iterators can possibly allow you
|
||||
/// to have iterator invalidation errors.
|
||||
pub fn for_each_mut<F>(&mut self, mut func: F)
|
||||
where
|
||||
F: FnMut(&mut BookItem),
|
||||
{
|
||||
for_each_mut(&mut func, &mut self.items);
|
||||
}
|
||||
|
||||
/// Recursively apply a closure to each non-draft chapter in the book,
|
||||
/// allowing you to mutate them.
|
||||
pub fn for_each_chapter_mut<F>(&mut self, mut func: F)
|
||||
where
|
||||
F: FnMut(&mut Chapter),
|
||||
{
|
||||
for_each_mut(
|
||||
&mut |item| {
|
||||
let BookItem::Chapter(ch) = item else {
|
||||
return;
|
||||
};
|
||||
if ch.is_draft_chapter() {
|
||||
return;
|
||||
}
|
||||
func(ch)
|
||||
},
|
||||
&mut self.items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Append a `BookItem` to the `Book`.
|
||||
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
|
||||
self.items.push(item.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_mut<'a, F, I>(func: &mut F, items: I)
|
||||
where
|
||||
F: FnMut(&mut BookItem),
|
||||
I: IntoIterator<Item = &'a mut BookItem>,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ch) = item {
|
||||
for_each_mut(func, &mut ch.sub_items);
|
||||
}
|
||||
|
||||
func(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing any type of item which can be added to a book.
|
||||
#[allow(
|
||||
clippy::exhaustive_enums,
|
||||
reason = "This cannot be extended without breaking preprocessors."
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BookItem {
|
||||
/// A nested chapter.
|
||||
Chapter(Chapter),
|
||||
/// A section separator.
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl From<Chapter> for BookItem {
|
||||
fn from(other: Chapter) -> BookItem {
|
||||
BookItem::Chapter(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// The representation of a "chapter", usually mapping to a single file on
|
||||
/// disk however it may contain multiple sub-chapters.
|
||||
#[allow(
|
||||
clippy::exhaustive_structs,
|
||||
reason = "This cannot be extended without breaking preprocessors."
|
||||
)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Chapter {
|
||||
/// The chapter's name.
|
||||
pub name: String,
|
||||
/// The chapter's contents.
|
||||
pub content: String,
|
||||
/// The chapter's section number, if it has one.
|
||||
pub number: Option<SectionNumber>,
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: After the index preprocessor runs, any README files will be
|
||||
/// modified to be `index.md`. If you need access to the actual filename
|
||||
/// on disk, use [`Chapter::source_path`] instead.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
pub path: Option<PathBuf>,
|
||||
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: Beware that README files will internally be treated as
|
||||
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
|
||||
/// exists if you need access to the true file path.
|
||||
///
|
||||
/// This is `None` for a draft chapter, or a synthetically generated
|
||||
/// chapter that has no file on disk.
|
||||
pub source_path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Chapter {
|
||||
/// Create a new chapter with the provided content.
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
name: &str,
|
||||
content: String,
|
||||
p: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Chapter {
|
||||
let path: PathBuf = p.into();
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content,
|
||||
path: Some(path.clone()),
|
||||
source_path: Some(path),
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft chapter that is not attached to a source markdown file (and thus
|
||||
/// has no content).
|
||||
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: String::new(),
|
||||
path: None,
|
||||
source_path: None,
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
|
||||
pub fn is_draft_chapter(&self) -> bool {
|
||||
self.path.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{section_number} ")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
/// a pretty `Display` impl.
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SectionNumber(Vec<u32>);
|
||||
|
||||
impl SectionNumber {
|
||||
/// Creates a new [`SectionNumber`].
|
||||
pub fn new(numbers: impl Into<Vec<u32>>) -> SectionNumber {
|
||||
SectionNumber(numbers.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if self.0.is_empty() {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
for item in &self.0 {
|
||||
write!(f, "{item}.")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SectionNumber {
|
||||
type Target = Vec<u32>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SectionNumber {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<u32> for SectionNumber {
|
||||
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
|
||||
SectionNumber(it.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A depth-first iterator over the items in a book.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This struct shouldn't be created directly, instead prefer the
|
||||
/// [`Book::iter()`] method.
|
||||
pub struct BookItems<'a> {
|
||||
items: VecDeque<&'a BookItem>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BookItems<'a> {
|
||||
type Item = &'a BookItem;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.items.pop_front();
|
||||
|
||||
if let Some(BookItem::Chapter(ch)) = item {
|
||||
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||
for sub_item in ch.sub_items.iter().rev() {
|
||||
self.items.push_front(sub_item);
|
||||
}
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
}
|
||||
123
crates/mdbook-core/src/book/tests.rs
Normal file
123
crates/mdbook-core/src/book/tests.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn section_number_has_correct_dotted_representation() {
|
||||
let inputs = vec![
|
||||
(vec![0], "0."),
|
||||
(vec![1, 3], "1.3."),
|
||||
(vec![1, 2, 3], "1.2.3."),
|
||||
];
|
||||
|
||||
for (input, should_be) in inputs {
|
||||
let section_number = SectionNumber(input).to_string();
|
||||
assert_eq!(section_number, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_iter_iterates_over_sequential_items() {
|
||||
let items = vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from("# Chapter 1"),
|
||||
..Default::default()
|
||||
}),
|
||||
BookItem::Separator,
|
||||
];
|
||||
let book = Book::new_with_items(items);
|
||||
|
||||
let should_be: Vec<_> = book.items.iter().collect();
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_each_mut_visits_all_items() {
|
||||
let items = vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from("# Chapter 1"),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
];
|
||||
let mut book = Book::new_with_items(items);
|
||||
|
||||
let num_items = book.iter().count();
|
||||
let mut visited = 0;
|
||||
|
||||
book.for_each_mut(|_| visited += 1);
|
||||
|
||||
assert_eq!(visited, num_items);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterate_over_nested_book_items() {
|
||||
let items = vec![
|
||||
BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from("# Chapter 1"),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Hello World",
|
||||
String::new(),
|
||||
"Chapter_1/hello.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(Chapter::new(
|
||||
"Goodbye World",
|
||||
String::new(),
|
||||
"Chapter_1/goodbye.md",
|
||||
Vec::new(),
|
||||
)),
|
||||
],
|
||||
}),
|
||||
BookItem::Separator,
|
||||
];
|
||||
let book = Book::new_with_items(items);
|
||||
|
||||
let got: Vec<_> = book.iter().collect();
|
||||
|
||||
assert_eq!(got.len(), 5);
|
||||
|
||||
// checking the chapter names are in the order should be sufficient here...
|
||||
let chapter_names: Vec<String> = got
|
||||
.into_iter()
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
String::from("Goodbye World"),
|
||||
];
|
||||
|
||||
assert_eq!(chapter_names, should_be);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
16
crates/mdbook-core/src/lib.rs
Normal file
16
crates/mdbook-core/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! The base support library for mdbook, intended for internal use only.
|
||||
|
||||
/// The current version of `mdbook`.
|
||||
///
|
||||
/// This is provided as a way for custom preprocessors and renderers to do
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
pub mod utils;
|
||||
|
||||
/// The error types used in mdbook.
|
||||
pub mod errors {
|
||||
pub use anyhow::{Error, Result};
|
||||
}
|
||||
273
crates/mdbook-core/src/utils/fs.rs
Normal file
273
crates/mdbook-core/src/utils/fs.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
//! Filesystem utilities and helpers.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use tracing::debug;
|
||||
|
||||
/// Reads a file into a string.
|
||||
///
|
||||
/// Equivalent to [`std::fs::read_to_string`] with better error messages.
|
||||
pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let path = path.as_ref();
|
||||
fs::read_to_string(path).with_context(|| format!("failed to read `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Writes a file to disk.
|
||||
///
|
||||
/// Equivalent to [`std::fs::write`] with better error messages. This will
|
||||
/// also create the parent directory if it doesn't exist.
|
||||
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
debug!("Writing `{}`", path.display());
|
||||
if let Some(parent) = path.parent() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, contents.as_ref())
|
||||
.with_context(|| format!("failed to write `{}`", path.display()))
|
||||
}
|
||||
|
||||
/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
|
||||
pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
|
||||
let p = p.as_ref();
|
||||
fs::create_dir_all(p)
|
||||
.with_context(|| format!("failed to create directory `{}`", p.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to
|
||||
/// the root of the given path.
|
||||
///
|
||||
/// This is mostly interesting for a relative path to point back to the
|
||||
/// directory from where the path starts.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::path::Path;
|
||||
/// # use mdbook_core::utils::fs::path_to_root;
|
||||
/// let path = Path::new("some/relative/path");
|
||||
/// assert_eq!(path_to_root(path), "../../");
|
||||
/// ```
|
||||
///
|
||||
/// **note:** it's not very fool-proof, if you find a situation where
|
||||
/// it doesn't return the correct path.
|
||||
/// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it.
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.into()
|
||||
.parent()
|
||||
.expect("")
|
||||
.components()
|
||||
.fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("Other path component... {:?}", c);
|
||||
}
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself.
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)
|
||||
.with_context(|| format!("failed to read directory `{}`", dir.display()))?
|
||||
.flatten()
|
||||
{
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(&item)
|
||||
.with_context(|| format!("failed to remove `{}`", item.display()))?;
|
||||
} else {
|
||||
fs::remove_file(&item)
|
||||
.with_context(|| format!("failed to remove `{}`", item.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies all files of a directory to another one except the files
|
||||
/// with the extensions given in the `ext_blacklist` array
|
||||
pub fn copy_files_except_ext(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
recursive: bool,
|
||||
avoid_dir: Option<&PathBuf>,
|
||||
ext_blacklist: &[&str],
|
||||
) -> Result<()> {
|
||||
debug!(
|
||||
"Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}",
|
||||
from.display(),
|
||||
to.display(),
|
||||
ext_blacklist,
|
||||
avoid_dir
|
||||
);
|
||||
|
||||
// Check that from and to are different
|
||||
if from == to {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?.path();
|
||||
let metadata = entry
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read {entry:?}"))?;
|
||||
|
||||
let entry_file_name = entry.file_name().unwrap();
|
||||
let target_file_path = to.join(entry_file_name);
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry == to.as_os_str() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(avoid) = avoid_dir {
|
||||
if entry == *avoid {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// check if output dir already exists
|
||||
if !target_file_path.exists() {
|
||||
fs::create_dir(&target_file_path)?;
|
||||
}
|
||||
|
||||
copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?;
|
||||
} else if metadata.is_file() {
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug!("Copying {entry:?} to {target_file_path:?}");
|
||||
copy(&entry, &target_file_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies a file.
|
||||
fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
return copy_inner(from, to)
|
||||
.with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()));
|
||||
|
||||
// This is a workaround for an issue with the macOS file watcher.
|
||||
// Rust's `std::fs::copy` function uses `fclonefileat`, which creates
|
||||
// clones on APFS. Unfortunately fs events seem to trigger on both
|
||||
// sides of the clone, and there doesn't seem to be a way to differentiate
|
||||
// which side it is.
|
||||
// https://github.com/notify-rs/notify/issues/465#issuecomment-1657261035
|
||||
// contains more information.
|
||||
//
|
||||
// This is essentially a copy of the simple copy code path in Rust's
|
||||
// standard library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
let mut reader = std::fs::File::open(from)?;
|
||||
let metadata = reader.metadata()?;
|
||||
if !metadata.is_file() {
|
||||
anyhow::bail!(
|
||||
"expected a file, `{}` appears to be {:?}",
|
||||
from.display(),
|
||||
metadata.file_type()
|
||||
);
|
||||
}
|
||||
let perm = metadata.permissions();
|
||||
let mut writer = OpenOptions::new()
|
||||
.mode(perm.mode())
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(to)?;
|
||||
let writer_metadata = writer.metadata()?;
|
||||
if writer_metadata.is_file() {
|
||||
// Set the correct file permissions, in case the file already existed.
|
||||
// Don't set the permissions on already existing non-files like
|
||||
// pipes/FIFOs or device nodes.
|
||||
writer.set_permissions(perm)?;
|
||||
}
|
||||
std::io::copy(&mut reader, &mut writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
|
||||
fs::copy(from, to)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Result;
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
|
||||
std::os::windows::fs::symlink_file(src, dst)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
|
||||
std::os::unix::fs::symlink(src, dst)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempfile::TempDir::new() {
|
||||
Ok(t) => t,
|
||||
Err(e) => panic!("Could not create a temp dir: {e}"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
write(tmp.path().join("file.txt"), "").unwrap();
|
||||
write(tmp.path().join("file.md"), "").unwrap();
|
||||
write(tmp.path().join("file.png"), "").unwrap();
|
||||
write(tmp.path().join("sub_dir/file.png"), "").unwrap();
|
||||
write(tmp.path().join("sub_dir_exists/file.txt"), "").unwrap();
|
||||
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
|
||||
panic!("Could not symlink file.png: {err}");
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
create_dir_all(tmp.path().join("output")).unwrap();
|
||||
create_dir_all(tmp.path().join("output/sub_dir_exists")).unwrap();
|
||||
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
{
|
||||
panic!("Error while executing the function:\n{e:?}");
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !tmp.path().join("output/file.txt").exists() {
|
||||
panic!("output/file.txt should exist")
|
||||
}
|
||||
if tmp.path().join("output/file.md").exists() {
|
||||
panic!("output/file.md should not exist")
|
||||
}
|
||||
if !tmp.path().join("output/file.png").exists() {
|
||||
panic!("output/file.png should exist")
|
||||
}
|
||||
if !tmp.path().join("output/sub_dir/file.png").exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !tmp.path().join("output/sub_dir_exists/file.txt").exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !tmp.path().join("output/symlink.png").exists() {
|
||||
panic!("output/symlink.png should exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
78
crates/mdbook-core/src/utils/html.rs
Normal file
78
crates/mdbook-core/src/utils/html.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! Utilities for dealing with HTML.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Escape characters to make it safe for an HTML string.
|
||||
pub fn escape_html_attribute(text: &str) -> Cow<'_, str> {
|
||||
let needs_escape: &[char] = &['<', '>', '\'', '"', '\\', '&'];
|
||||
let mut s = text;
|
||||
let mut output = String::new();
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
output.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => output.push_str("<"),
|
||||
b'>' => output.push_str(">"),
|
||||
b'\'' => output.push_str("'"),
|
||||
b'"' => output.push_str("""),
|
||||
b'\\' => output.push_str("\"),
|
||||
b'&' => output.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
if output.is_empty() {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
output.push_str(s);
|
||||
Cow::Owned(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape `<`, `>`, and '&' for HTML.
|
||||
pub fn escape_html(text: &str) -> Cow<'_, str> {
|
||||
let needs_escape: &[char] = &['<', '>', '&'];
|
||||
let mut s = text;
|
||||
let mut output = String::new();
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
output.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => output.push_str("<"),
|
||||
b'>' => output.push_str(">"),
|
||||
b'&' => output.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
if output.is_empty() {
|
||||
Cow::Borrowed(text)
|
||||
} else {
|
||||
output.push_str(s);
|
||||
Cow::Owned(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_are_escaped() {
|
||||
assert_eq!(escape_html_attribute(""), "");
|
||||
assert_eq!(escape_html_attribute("<"), "<");
|
||||
assert_eq!(escape_html_attribute(">"), ">");
|
||||
assert_eq!(escape_html_attribute("<>"), "<>");
|
||||
assert_eq!(escape_html_attribute("<test>"), "<test>");
|
||||
assert_eq!(escape_html_attribute("a<test>b"), "a<test>b");
|
||||
assert_eq!(escape_html_attribute("'"), "'");
|
||||
assert_eq!(escape_html_attribute("\\"), "\");
|
||||
assert_eq!(escape_html_attribute("&"), "&");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_is_escaped() {
|
||||
assert_eq!(escape_html(""), "");
|
||||
assert_eq!(escape_html("<"), "<");
|
||||
assert_eq!(escape_html(">"), ">");
|
||||
assert_eq!(escape_html("&"), "&");
|
||||
assert_eq!(escape_html("<>"), "<>");
|
||||
assert_eq!(escape_html("<test>"), "<test>");
|
||||
assert_eq!(escape_html("a<test>b"), "a<test>b");
|
||||
assert_eq!(escape_html("'"), "'");
|
||||
assert_eq!(escape_html("\\"), "\\");
|
||||
}
|
||||
37
crates/mdbook-core/src/utils/mod.rs
Normal file
37
crates/mdbook-core/src/utils/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Various helpers and utilities.
|
||||
|
||||
use anyhow::Error;
|
||||
use std::fmt::Write;
|
||||
use tracing::error;
|
||||
|
||||
pub mod fs;
|
||||
mod html;
|
||||
mod toml_ext;
|
||||
|
||||
pub(crate) use self::toml_ext::TomlExt;
|
||||
|
||||
pub use self::html::{escape_html, escape_html_attribute};
|
||||
|
||||
/// Defines a `static` with a [`regex::Regex`].
|
||||
#[macro_export]
|
||||
macro_rules! static_regex {
|
||||
($name:ident, $regex:literal) => {
|
||||
static $name: std::sync::LazyLock<regex::Regex> =
|
||||
std::sync::LazyLock::new(|| regex::Regex::new($regex).unwrap());
|
||||
};
|
||||
($name:ident, bytes, $regex:literal) => {
|
||||
static $name: std::sync::LazyLock<regex::bytes::Regex> =
|
||||
std::sync::LazyLock::new(|| regex::bytes::Regex::new($regex).unwrap());
|
||||
};
|
||||
}
|
||||
|
||||
/// Prints a "backtrace" of some `Error`.
|
||||
pub fn log_backtrace(e: &Error) {
|
||||
let mut message = format!("{e}");
|
||||
|
||||
for cause in e.chain().skip(1) {
|
||||
write!(message, "\n\tCaused by: {cause}").unwrap();
|
||||
}
|
||||
|
||||
error!("{message}");
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
//! Helper for working with toml types.
|
||||
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
/// Helper for working with toml types.
|
||||
pub(crate) trait TomlExt {
|
||||
/// Read a dotted key.
|
||||
fn read(&self, key: &str) -> Option<&Value>;
|
||||
fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
|
||||
/// Insert with a dotted key.
|
||||
fn insert(&mut self, key: &str, value: Value);
|
||||
fn delete(&mut self, key: &str) -> Option<Value>;
|
||||
}
|
||||
|
||||
impl TomlExt for Value {
|
||||
@@ -16,14 +19,6 @@ impl TomlExt for Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
if let Some((head, tail)) = split(key) {
|
||||
self.get_mut(head)?.read_mut(tail)
|
||||
} else {
|
||||
self.get_mut(key)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&mut self, key: &str, value: Value) {
|
||||
if !self.is_table() {
|
||||
*self = Value::Table(Table::new());
|
||||
@@ -40,16 +35,6 @@ impl TomlExt for Value {
|
||||
table.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&mut self, key: &str) -> Option<Value> {
|
||||
if let Some((head, tail)) = split(key) {
|
||||
self.get_mut(head)?.delete(tail)
|
||||
} else if let Some(table) = self.as_table_mut() {
|
||||
table.remove(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split(key: &str) -> Option<(&str, &str)> {
|
||||
@@ -65,12 +50,11 @@ fn split(key: &str) -> Option<(&str, &str)> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn read_simple_table() {
|
||||
let src = "[table]";
|
||||
let value = Value::from_str(src).unwrap();
|
||||
let value: Value = toml::from_str(src).unwrap();
|
||||
|
||||
let got = value.read("table").unwrap();
|
||||
|
||||
@@ -80,7 +64,7 @@ mod tests {
|
||||
#[test]
|
||||
fn read_nested_item() {
|
||||
let src = "[table]\nnested=true";
|
||||
let value = Value::from_str(src).unwrap();
|
||||
let value: Value = toml::from_str(src).unwrap();
|
||||
|
||||
let got = value.read("table.nested").unwrap();
|
||||
|
||||
@@ -107,24 +91,4 @@ mod tests {
|
||||
let inserted = value.read("first.second").unwrap();
|
||||
assert_eq!(inserted, &item);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_top_level_item() {
|
||||
let src = "top = true";
|
||||
let mut value = Value::from_str(src).unwrap();
|
||||
|
||||
let got = value.delete("top").unwrap();
|
||||
|
||||
assert_eq!(got, Value::Boolean(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_nested_item() {
|
||||
let src = "[table]\n nested = true";
|
||||
let mut value = Value::from_str(src).unwrap();
|
||||
|
||||
let got = value.delete("table.nested").unwrap();
|
||||
|
||||
assert_eq!(got, Value::Boolean(true));
|
||||
}
|
||||
}
|
||||
32
crates/mdbook-driver/Cargo.toml
Normal file
32
crates/mdbook-driver/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "mdbook-driver"
|
||||
version = "0.5.2"
|
||||
description = "High-level library for running mdBook"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
indexmap.workspace = true
|
||||
mdbook-core.workspace = true
|
||||
mdbook-html.workspace = true
|
||||
mdbook-markdown.workspace = true
|
||||
mdbook-preprocessor.workspace = true
|
||||
mdbook-renderer.workspace = true
|
||||
mdbook-summary.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
shlex.workspace = true
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
topological-sort.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
search = ["mdbook-html/search"]
|
||||
13
crates/mdbook-driver/README.md
Normal file
13
crates/mdbook-driver/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# mdbook-driver
|
||||
|
||||
[](https://docs.rs/mdbook-driver)
|
||||
[](https://crates.io/crates/mdbook-driver)
|
||||
[](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
|
||||
|
||||
This is the high-level Rust library for running [mdBook](https://rust-lang.github.io/mdBook/). New books can be created using [`BookBuilder`](https://docs.rs/mdbook-driver/latest/mdbook_driver/init/struct.BookBuilder.html). The primary type [`MDBook`](https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html) can be used to manage and render books.
|
||||
|
||||
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)
|
||||
@@ -1,49 +1,32 @@
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::Book;
|
||||
use crate::errors::*;
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use mdbook_core::book::Book;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Stdio};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
/// A custom preprocessor which will shell out to a 3rd-party program.
|
||||
///
|
||||
/// # Preprocessing Protocol
|
||||
///
|
||||
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
|
||||
/// execute the shell command `$cmd supports $renderer`. If the renderer is
|
||||
/// supported, custom preprocessors should exit with a exit code of `0`,
|
||||
/// any other exit code be considered as unsupported.
|
||||
///
|
||||
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
|
||||
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
|
||||
/// should then "return" a processed book by printing it to `stdout` as JSON.
|
||||
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
|
||||
/// to parse the input provided by `mdbook`.
|
||||
///
|
||||
/// Exiting with a non-zero exit code while preprocessing is considered an
|
||||
/// error. `stderr` is passed directly through to the user, so it can be used
|
||||
/// for logging or emitting warnings if desired.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// An example preprocessor is available in this project's `examples/`
|
||||
/// directory.
|
||||
/// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html>
|
||||
/// for a description of the preprocessor protocol.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdPreprocessor {
|
||||
name: String,
|
||||
cmd: String,
|
||||
root: PathBuf,
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
impl CmdPreprocessor {
|
||||
/// Create a new `CmdPreprocessor`.
|
||||
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
|
||||
CmdPreprocessor { name, cmd }
|
||||
}
|
||||
|
||||
/// A convenience function custom preprocessors can use to parse the input
|
||||
/// written to `stdin` by a `CmdRenderer`.
|
||||
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
|
||||
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
|
||||
CmdPreprocessor {
|
||||
name,
|
||||
cmd,
|
||||
root,
|
||||
optional,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||
@@ -69,22 +52,6 @@ impl CmdPreprocessor {
|
||||
pub fn cmd(&self) -> &str {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
fn command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for CmdPreprocessor {
|
||||
@@ -93,19 +60,31 @@ impl Preprocessor for CmdPreprocessor {
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut cmd = self.command()?;
|
||||
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
|
||||
|
||||
let mut child = cmd
|
||||
let mut child = match cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&self.root)
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
)
|
||||
})?;
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
crate::handle_command_error(
|
||||
e,
|
||||
self.optional,
|
||||
"preprocessor",
|
||||
"preprocessor",
|
||||
&self.name,
|
||||
&self.cmd,
|
||||
)?;
|
||||
// This should normally not be reached, since the validation
|
||||
// for NotFound should have already happened when running the
|
||||
// "supports" command.
|
||||
return Ok(book);
|
||||
}
|
||||
};
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
@@ -133,45 +112,37 @@ impl Preprocessor for CmdPreprocessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
fn supports_renderer(&self, renderer: &str) -> Result<bool> {
|
||||
debug!(
|
||||
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||
self.name(),
|
||||
renderer
|
||||
);
|
||||
|
||||
let mut cmd = match self.command() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||
self.name(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
|
||||
|
||||
let outcome = cmd
|
||||
match cmd
|
||||
.arg("supports")
|
||||
.arg(renderer)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&self.root)
|
||||
.status()
|
||||
.map(|status| status.code() == Some(0));
|
||||
|
||||
if let Err(ref e) = outcome {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
{
|
||||
Ok(status) => Ok(status.code() == Some(0)),
|
||||
Err(e) => {
|
||||
crate::handle_command_error(
|
||||
e,
|
||||
self.optional,
|
||||
"preprocessor",
|
||||
"preprocessor",
|
||||
&self.name,
|
||||
&self.cmd,
|
||||
)?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
outcome.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,14 +153,19 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
fn guide() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../guide");
|
||||
MDBook::load(example).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_parse_input() {
|
||||
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
|
||||
let md = guide();
|
||||
let cmd = CmdPreprocessor::new(
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
md.root.clone(),
|
||||
false,
|
||||
);
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
@@ -199,7 +175,7 @@ mod tests {
|
||||
let mut buffer = Vec::new();
|
||||
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
||||
|
||||
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
let (got_ctx, got_book) = mdbook_preprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(got_book, md.book);
|
||||
assert_eq!(got_ctx, ctx);
|
||||
@@ -1,18 +1,19 @@
|
||||
use regex::Regex;
|
||||
use anyhow::Result;
|
||||
use mdbook_core::book::{Book, BookItem};
|
||||
use mdbook_core::static_regex;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use tracing::warn;
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "index";
|
||||
/// Name of this preprocessor.
|
||||
pub const NAME: &'static str = "index";
|
||||
|
||||
/// Create a new `IndexPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
@@ -67,10 +68,9 @@ fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
}
|
||||
|
||||
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
|
||||
}
|
||||
RE.is_match(
|
||||
static_regex!(README, r"(?i)^readme$");
|
||||
|
||||
README.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
@@ -1,15 +1,18 @@
|
||||
use crate::errors::*;
|
||||
use crate::utils::{
|
||||
use self::take_lines::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::fs;
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::book::{Book, BookItem};
|
||||
use mdbook_core::static_regex;
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use regex::{CaptureMatches, Captures};
|
||||
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
mod take_lines;
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
@@ -17,18 +20,20 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
||||
///
|
||||
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
||||
///. lines, or only between the specified anchors.
|
||||
/// lines, or only between the specified anchors.
|
||||
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
||||
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||
/// block and provides them to Rustdoc for testing.
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
/// - `{{# title}}` - Override \<title\> of a webpage.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "links";
|
||||
/// Name of this preprocessor.
|
||||
pub const NAME: &'static str = "links";
|
||||
|
||||
/// Create a new `LinkPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
@@ -91,7 +96,7 @@ where
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match link.render_with_path(&path, chapter_title) {
|
||||
match link.render_with_path(path, chapter_title) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
@@ -324,7 +329,7 @@ impl<'a> Link<'a> {
|
||||
let base = base.as_ref();
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
|
||||
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
@@ -405,22 +410,19 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
}
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
static_regex!(
|
||||
LINK,
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([^}]+) # link target path and space separated properties
|
||||
\}\} # link closing parens"
|
||||
);
|
||||
|
||||
LinkIter(LINK.captures_iter(contents))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -489,7 +491,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -515,7 +517,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -532,7 +534,7 @@ mod tests {
|
||||
fn test_find_links_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -551,7 +553,7 @@ mod tests {
|
||||
fn test_find_links_with_line_number() {
|
||||
let s = "Some random text with {{#include file.rs:10}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -570,7 +572,7 @@ mod tests {
|
||||
fn test_find_links_with_from_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -589,7 +591,7 @@ mod tests {
|
||||
fn test_find_links_with_to_range() {
|
||||
let s = "Some random text with {{#include file.rs::20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -608,7 +610,7 @@ mod tests {
|
||||
fn test_find_links_with_full_range() {
|
||||
let s = "Some random text with {{#include file.rs::}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -627,7 +629,7 @@ mod tests {
|
||||
fn test_find_links_with_no_range_specified() {
|
||||
let s = "Some random text with {{#include file.rs}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -646,7 +648,7 @@ mod tests {
|
||||
fn test_find_links_with_anchor() {
|
||||
let s = "Some random text with {{#include file.rs:anchor}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -666,7 +668,7 @@ mod tests {
|
||||
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -681,12 +683,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_playgrounds_with_properties() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||
let s = "Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
@@ -711,13 +712,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_all_link_types() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||
let s = "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(
|
||||
res[0],
|
||||
@@ -1,9 +1,9 @@
|
||||
use regex::Regex;
|
||||
use mdbook_core::static_regex;
|
||||
use std::ops::Bound::{Excluded, Included, Unbounded};
|
||||
use std::ops::RangeBounds;
|
||||
|
||||
/// Take a range of lines from a string.
|
||||
pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
pub(super) fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
let start = match range.start_bound() {
|
||||
Excluded(&n) => n + 1,
|
||||
Included(&n) => n,
|
||||
@@ -23,14 +23,12 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ANCHOR_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
static ref ANCHOR_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
}
|
||||
static_regex!(ANCHOR_START, r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)");
|
||||
static_regex!(ANCHOR_END, r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)");
|
||||
|
||||
/// Take anchored lines from a string.
|
||||
/// Lines containing anchor are ignored.
|
||||
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
pub(super) fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
let mut retained = Vec::<&str>::new();
|
||||
let mut anchor_found = false;
|
||||
|
||||
@@ -62,7 +60,7 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
/// For any lines not in the range, include them but use `#` at the beginning. This will hide the
|
||||
/// lines from initial display but include them when expanding the code snippet or testing with
|
||||
/// rustdoc.
|
||||
pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
pub(super) fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
let mut output = String::with_capacity(s.len());
|
||||
|
||||
for (index, line) in s.lines().enumerate() {
|
||||
@@ -80,7 +78,7 @@ pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> S
|
||||
/// For any lines not between the anchors, include them but use `#` at the beginning. This will
|
||||
/// hide the lines from initial display but include them when expanding the code snippet or testing
|
||||
/// with rustdoc.
|
||||
pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
pub(super) fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
let mut output = String::with_capacity(s.len());
|
||||
let mut within_anchored_section = false;
|
||||
|
||||
@@ -122,6 +120,7 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
|
||||
fn take_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(take_lines(s, 1..3), "ipsum\ndolor");
|
||||
@@ -163,6 +162,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::reversed_empty_ranges)] // Intentionally checking that those are correctly handled
|
||||
fn take_rustdoc_include_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(
|
||||
9
crates/mdbook-driver/src/builtin_preprocessors/mod.rs
Normal file
9
crates/mdbook-driver/src/builtin_preprocessors/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Built-in preprocessors.
|
||||
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
|
||||
mod cmd;
|
||||
mod index;
|
||||
mod links;
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::book::BookItem;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use tracing::trace;
|
||||
|
||||
use std::fs;
|
||||
|
||||
#[derive(Default)]
|
||||
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
||||
/// when debugging preprocessors.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct MarkdownRenderer;
|
||||
|
||||
impl MarkdownRenderer {
|
||||
@@ -27,24 +26,19 @@ impl Renderer for MarkdownRenderer {
|
||||
let book = &ctx.book;
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||
}
|
||||
|
||||
trace!("markdown render");
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.is_draft_chapter() {
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
&ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.content.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for ch in book.chapters() {
|
||||
let path = ctx
|
||||
.destination
|
||||
.join(ch.path.as_ref().expect("Checked path exists before"));
|
||||
fs::write(path, &ch.content)?;
|
||||
}
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
88
crates/mdbook-driver/src/builtin_renderers/mod.rs
Normal file
88
crates/mdbook-driver/src/builtin_renderers/mod.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Built-in renderers.
|
||||
//!
|
||||
//! The HTML renderer can be found in the [`mdbook_html`] crate.
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use std::process::Stdio;
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
pub use self::markdown_renderer::MarkdownRenderer;
|
||||
|
||||
mod markdown_renderer;
|
||||
|
||||
/// A generic renderer which will shell out to an arbitrary executable.
|
||||
///
|
||||
/// See <https://rust-lang.github.io/mdBook/for_developers/backends.html>
|
||||
/// for a description of the renderer protocol.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdRenderer {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdRenderer {
|
||||
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
|
||||
pub fn new(name: String, cmd: String) -> CmdRenderer {
|
||||
CmdRenderer { name, cmd }
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
info!("Invoking the \"{}\" renderer", self.name);
|
||||
|
||||
let optional_key = format!("output.{}.optional", self.name);
|
||||
let optional = match ctx.config.get(&optional_key) {
|
||||
Ok(Some(value)) => value,
|
||||
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
|
||||
Ok(None) => false,
|
||||
};
|
||||
|
||||
let _ = fs::create_dir_all(&ctx.destination);
|
||||
|
||||
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
|
||||
let mut child = match cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&ctx.destination)
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return crate::handle_command_error(
|
||||
e, optional, "output", "backend", &self.name, &self.cmd,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
|
||||
// explicitly close the `stdin` file handle
|
||||
drop(stdin);
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.with_context(|| "Error waiting for the backend to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||
|
||||
if !status.success() {
|
||||
error!("Renderer exited with non-zero return code.");
|
||||
bail!("The \"{}\" renderer failed", self.name);
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
//! Support for initializing a new book.
|
||||
|
||||
use super::MDBook;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::theme;
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::config::Config;
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_html::theme::Theme;
|
||||
use std::path::PathBuf;
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -97,12 +98,10 @@ impl BookBuilder {
|
||||
fn write_book_toml(&self) -> Result<()> {
|
||||
debug!("Writing book.toml");
|
||||
let book_toml = self.root.join("book.toml");
|
||||
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||
let cfg =
|
||||
toml::to_string(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||
|
||||
File::create(book_toml)
|
||||
.with_context(|| "Couldn't create book.toml")?
|
||||
.write_all(&cfg)
|
||||
.with_context(|| "Unable to write config to book.toml")?;
|
||||
fs::write(&book_toml, cfg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -110,63 +109,15 @@ impl BookBuilder {
|
||||
debug!("Copying theme");
|
||||
|
||||
let html_config = self.config.html_config().unwrap_or_default();
|
||||
let themedir = html_config.theme_dir(&self.root);
|
||||
|
||||
if !themedir.exists() {
|
||||
debug!(
|
||||
"{} does not exist, creating the directory",
|
||||
themedir.display()
|
||||
);
|
||||
fs::create_dir(&themedir)?;
|
||||
}
|
||||
|
||||
let mut index = File::create(themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
let cssdir = themedir.join("css");
|
||||
if !cssdir.exists() {
|
||||
fs::create_dir(&cssdir)?;
|
||||
}
|
||||
|
||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||
general_css.write_all(theme::GENERAL_CSS)?;
|
||||
|
||||
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
||||
chrome_css.write_all(theme::CHROME_CSS)?;
|
||||
|
||||
if html_config.print.enable {
|
||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||
print_css.write_all(theme::PRINT_CSS)?;
|
||||
}
|
||||
|
||||
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON_PNG)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.svg"))?;
|
||||
favicon.write_all(theme::FAVICON_SVG)?;
|
||||
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
|
||||
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
|
||||
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
|
||||
|
||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||
|
||||
Theme::copy_theme(&html_config, &self.root)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_gitignore(&self) -> Result<()> {
|
||||
debug!("Creating .gitignore");
|
||||
|
||||
let mut f = File::create(self.root.join(".gitignore"))?;
|
||||
|
||||
writeln!(f, "{}", self.config.build.build_dir.display())?;
|
||||
|
||||
fs::write(
|
||||
self.root.join(".gitignore"),
|
||||
format!("{}", self.config.build.build_dir.display()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -177,15 +128,14 @@ impl BookBuilder {
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
if !summary.exists() {
|
||||
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
fs::write(
|
||||
summary,
|
||||
"# Summary\n\
|
||||
\n\
|
||||
- [Chapter 1](./chapter_1.md)\n",
|
||||
)?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f =
|
||||
File::create(&chapter_1).with_context(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
fs::write(src_dir.join("chapter_1.md"), "# Chapter 1\n")?;
|
||||
} else {
|
||||
trace!("Existing summary found, no need to create stub files.");
|
||||
}
|
||||
@@ -197,10 +147,10 @@ impl BookBuilder {
|
||||
fs::create_dir_all(&self.root)?;
|
||||
|
||||
let src = self.root.join(&self.config.book.src);
|
||||
fs::create_dir_all(&src)?;
|
||||
fs::create_dir_all(src)?;
|
||||
|
||||
let build = self.root.join(&self.config.build.build_dir);
|
||||
fs::create_dir_all(&build)?;
|
||||
fs::create_dir_all(build)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
131
crates/mdbook-driver/src/lib.rs
Normal file
131
crates/mdbook-driver/src/lib.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! High-level library for running mdBook.
|
||||
//!
|
||||
//! This is the high-level library for running
|
||||
//! [mdBook](https://rust-lang.github.io/mdBook/). There are several
|
||||
//! reasons for using the programmatic API (over the CLI):
|
||||
//!
|
||||
//! - Integrate mdBook in a current project.
|
||||
//! - Extend the capabilities of mdBook.
|
||||
//! - Do some processing or test before building your book.
|
||||
//! - Accessing the public API to help create a new Renderer.
|
||||
//!
|
||||
//! ## Additional crates
|
||||
//!
|
||||
//! In addition to `mdbook-driver`, there are several other crates available
|
||||
//! for using and extending mdBook:
|
||||
//!
|
||||
//! - [`mdbook_preprocessor`]: Provides support for implementing preprocessors.
|
||||
//! - [`mdbook_renderer`]: Provides support for implementing renderers.
|
||||
//! - [`mdbook_markdown`]: The Markdown renderer.
|
||||
//! - [`mdbook_summary`]: The `SUMMARY.md` parser.
|
||||
//! - [`mdbook_html`]: The HTML renderer.
|
||||
//! - [`mdbook_core`]: An internal library that is used by the other crates
|
||||
//! for shared types. Types from this crate are rexported from the other
|
||||
//! crates as appropriate.
|
||||
//!
|
||||
//! ## Cargo features
|
||||
//!
|
||||
//! The following cargo features are available:
|
||||
//!
|
||||
//! - `search`: Enables the search index in the HTML renderer.
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! If creating a new book from scratch, you'll want to get a [`init::BookBuilder`] via
|
||||
//! the [`MDBook::init()`] method.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use mdbook_driver::MDBook;
|
||||
//! use mdbook_driver::config::Config;
|
||||
//!
|
||||
//! let root_dir = "/path/to/book/root";
|
||||
//!
|
||||
//! // create a default config and change a couple things
|
||||
//! let mut cfg = Config::default();
|
||||
//! cfg.book.title = Some("My Book".to_string());
|
||||
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
|
||||
//!
|
||||
//! MDBook::init(root_dir)
|
||||
//! .create_gitignore(true)
|
||||
//! .with_config(cfg)
|
||||
//! .build()
|
||||
//! .expect("Book generation failed");
|
||||
//! ```
|
||||
//!
|
||||
//! You can also load an existing book and build it.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use mdbook_driver::MDBook;
|
||||
//!
|
||||
//! let root_dir = "/path/to/book/root";
|
||||
//!
|
||||
//! let mut md = MDBook::load(root_dir)
|
||||
//! .expect("Unable to load the book");
|
||||
//! md.build().expect("Building failed");
|
||||
//! ```
|
||||
|
||||
pub mod builtin_preprocessors;
|
||||
pub mod builtin_renderers;
|
||||
pub mod init;
|
||||
mod load;
|
||||
mod mdbook;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
pub use mdbook::MDBook;
|
||||
pub use mdbook_core::{book, config, errors};
|
||||
use shlex::Shlex;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tracing::{error, warn};
|
||||
|
||||
/// Creates a [`Command`] for command renderers and preprocessors.
|
||||
fn compose_command(cmd: &str, root: &Path) -> Result<Command> {
|
||||
let mut words = Shlex::new(cmd);
|
||||
let exe = match words.next() {
|
||||
Some(e) => PathBuf::from(e),
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let exe = if exe.components().count() == 1 {
|
||||
// Search PATH for the executable.
|
||||
exe
|
||||
} else {
|
||||
// Relative path is relative to book root.
|
||||
root.join(&exe)
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(exe);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Handles a failure for a preprocessor or renderer.
|
||||
fn handle_command_error(
|
||||
error: std::io::Error,
|
||||
optional: bool,
|
||||
key: &str,
|
||||
what: &str,
|
||||
name: &str,
|
||||
cmd: &str,
|
||||
) -> Result<()> {
|
||||
if let std::io::ErrorKind::NotFound = error.kind() {
|
||||
if optional {
|
||||
warn!(
|
||||
"The command `{cmd}` for {what} `{name}` was not found, \
|
||||
but is marked as optional.",
|
||||
);
|
||||
return Ok(());
|
||||
} else {
|
||||
error!(
|
||||
"The command `{cmd}` wasn't found, is the `{name}` {what} installed? \
|
||||
If you want to ignore this error when the `{name}` {what} is not installed, \
|
||||
set `optional = true` in the `[{key}.{name}]` section of the book.toml configuration file.",
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error).with_context(|| format!("Unable to run the {what} `{name}`"))?
|
||||
}
|
||||
309
crates/mdbook-driver/src/load.rs
Normal file
309
crates/mdbook-driver/src/load.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook_core::book::{Book, BookItem, Chapter};
|
||||
use mdbook_core::config::BuildConfig;
|
||||
use mdbook_core::utils::{escape_html, fs};
|
||||
use mdbook_summary::{Link, Summary, SummaryItem, parse_summary};
|
||||
use std::path::Path;
|
||||
use tracing::debug;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub(crate) fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let summary_md = src_dir.join("SUMMARY.md");
|
||||
|
||||
let summary_content = fs::read_to_string(&summary_md)?;
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
}
|
||||
|
||||
load_book_from_disk(&summary, src_dir)
|
||||
}
|
||||
|
||||
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
let mut items: Vec<_> = summary
|
||||
.prefix_chapters
|
||||
.iter()
|
||||
.chain(summary.numbered_chapters.iter())
|
||||
.chain(summary.suffix_chapters.iter())
|
||||
.collect();
|
||||
|
||||
while let Some(next) = items.pop() {
|
||||
if let SummaryItem::Link(ref link) = *next {
|
||||
if let Some(ref location) = link.location {
|
||||
let filename = src_dir.join(location);
|
||||
if !filename.exists() {
|
||||
if let Some(parent) = filename.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
let title = escape_html(&link.name);
|
||||
fs::write(&filename, format!("# {title}\n"))?;
|
||||
}
|
||||
}
|
||||
|
||||
items.extend(&link.nested_items);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the provided `Summary` to load a `Book` from disk.
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
debug!("Loading the book from disk");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let prefix = summary.prefix_chapters.iter();
|
||||
let numbered = summary.numbered_chapters.iter();
|
||||
let suffix = summary.suffix_chapters.iter();
|
||||
|
||||
let summary_items = prefix.chain(numbered).chain(suffix);
|
||||
|
||||
let mut chapters = Vec::new();
|
||||
|
||||
for summary_item in summary_items {
|
||||
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
Ok(Book::new_with_items(chapters))
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path> + Clone>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<BookItem> {
|
||||
match item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(link) => load_chapter(link, src_dir, parent_names).map(BookItem::Chapter),
|
||||
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
|
||||
_ => panic!("SummaryItem {item:?} not covered"),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_chapter<P: AsRef<Path>>(
|
||||
link: &Link,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<Chapter> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let mut ch = if let Some(ref link_location) = link.location {
|
||||
debug!("Loading {} ({})", link.name, link_location.display());
|
||||
|
||||
let location = if link_location.is_absolute() {
|
||||
link_location.clone()
|
||||
} else {
|
||||
src_dir.join(link_location)
|
||||
};
|
||||
|
||||
let mut content = std::fs::read_to_string(&location)
|
||||
.with_context(|| format!("failed to read chapter `{}`", link_location.display()))?;
|
||||
|
||||
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
|
||||
content.replace_range(..3, "");
|
||||
}
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||
} else {
|
||||
Chapter::new_draft(&link.name, parent_names.clone())
|
||||
};
|
||||
|
||||
let mut sub_item_parents = parent_names;
|
||||
|
||||
ch.number = link.number.clone();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
let sub_items = link
|
||||
.nested_items
|
||||
.iter()
|
||||
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
ch.sub_items = sub_items;
|
||||
|
||||
Ok(ch)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mdbook_core::book::SectionNumber;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &str = "
|
||||
# Dummy Chapter
|
||||
|
||||
this is some dummy text.
|
||||
|
||||
And here is some \
|
||||
more text.
|
||||
";
|
||||
|
||||
/// Create a dummy `Link` in a temporary directory.
|
||||
fn dummy_link() -> (Link, TempDir) {
|
||||
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
fs::write(&chapter_path, DUMMY_SRC).unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
(link, temp)
|
||||
}
|
||||
|
||||
/// Create a nested `Link` written to a temporary directory.
|
||||
fn nested_links() -> (Link, TempDir) {
|
||||
let (mut root, temp_dir) = dummy_link();
|
||||
|
||||
let second_path = temp_dir.path().join("second.md");
|
||||
fs::write(&second_path, "Hello World!").unwrap();
|
||||
|
||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||
second.number = Some(SectionNumber::new([1, 2]));
|
||||
|
||||
root.nested_items.push(second.clone().into());
|
||||
root.nested_items.push(SummaryItem::Separator);
|
||||
root.nested_items.push(second.into());
|
||||
|
||||
(root, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_from_disk() {
|
||||
let (link, temp_dir) = dummy_link();
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
"chapter_1.md",
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_single_chapter_with_utf8_bom_from_disk() {
|
||||
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let chapter_path = temp_dir.path().join("chapter_1.md");
|
||||
fs::write(&chapter_path, format!("\u{feff}{DUMMY_SRC}")).unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
|
||||
let should_be = Chapter::new(
|
||||
"Chapter 1",
|
||||
DUMMY_SRC.to_string(),
|
||||
"chapter_1.md",
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_a_nonexistent_chapter() {
|
||||
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||
|
||||
let got = load_chapter(&link, "", Vec::new());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_recursive_link_with_separators() {
|
||||
let (root, temp) = nested_links();
|
||||
|
||||
let mut nested = Chapter::new(
|
||||
"Nested Chapter 1",
|
||||
String::from("Hello World!"),
|
||||
"second.md",
|
||||
vec![String::from("Chapter 1")],
|
||||
);
|
||||
nested.number = Some(SectionNumber::new([1, 2]));
|
||||
let mut chapter =
|
||||
Chapter::new("Chapter 1", String::from(DUMMY_SRC), "chapter_1.md", vec![]);
|
||||
chapter.sub_items = vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
BookItem::Separator,
|
||||
BookItem::Chapter(nested),
|
||||
];
|
||||
let should_be = BookItem::Chapter(chapter);
|
||||
|
||||
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_a_book_with_a_single_chapter() {
|
||||
let (link, temp) = dummy_link();
|
||||
let mut summary = Summary::default();
|
||||
summary.numbered_chapters = vec![SummaryItem::Link(link)];
|
||||
let chapter = Chapter::new(
|
||||
"Chapter 1",
|
||||
String::from(DUMMY_SRC),
|
||||
PathBuf::from("chapter_1.md"),
|
||||
vec![],
|
||||
);
|
||||
let items = vec![BookItem::Chapter(chapter)];
|
||||
let should_be = Book::new_with_items(items);
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path()).unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_chapters_with_an_empty_path() {
|
||||
let (_, temp) = dummy_link();
|
||||
let mut summary = Summary::default();
|
||||
let link = Link::new("Empty", "");
|
||||
summary.numbered_chapters = vec![SummaryItem::Link(link)];
|
||||
let got = load_book_from_disk(&summary, temp.path());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_load_chapters_when_the_link_is_a_directory() {
|
||||
let (_, temp) = dummy_link();
|
||||
let dir = temp.path().join("nested");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let mut summary = Summary::default();
|
||||
let link = Link::new("nested", dir);
|
||||
summary.numbered_chapters = vec![SummaryItem::Link(link)];
|
||||
|
||||
let got = load_book_from_disk(&summary, temp.path());
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cant_open_summary_md() {
|
||||
let cfg = BuildConfig::default();
|
||||
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||
|
||||
let got = load_book(&temp_dir, &cfg);
|
||||
assert!(got.is_err());
|
||||
let error_message = got.err().unwrap().to_string();
|
||||
let expected = format!(
|
||||
r#"failed to read `{}`"#,
|
||||
temp_dir.path().join("SUMMARY.md").display()
|
||||
);
|
||||
assert_eq!(error_message, expected);
|
||||
}
|
||||
}
|
||||
569
crates/mdbook-driver/src/mdbook.rs
Normal file
569
crates/mdbook-driver/src/mdbook.rs
Normal file
@@ -0,0 +1,569 @@
|
||||
//! The high-level interface for loading and rendering books.
|
||||
|
||||
use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
|
||||
use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
|
||||
use crate::init::BookBuilder;
|
||||
use crate::load::{load_book, load_book_from_disk};
|
||||
use anyhow::{Context, Error, Result, bail};
|
||||
use indexmap::IndexMap;
|
||||
use mdbook_core::book::{Book, BookItem, BookItems};
|
||||
use mdbook_core::config::{Config, RustEdition};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_html::HtmlHandlebars;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use mdbook_summary::Summary;
|
||||
use serde::Deserialize;
|
||||
use std::ffi::OsString;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
/// The book's root directory.
|
||||
pub root: PathBuf,
|
||||
|
||||
/// The configuration used to tweak now a book is built.
|
||||
pub config: Config,
|
||||
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
|
||||
/// Renderers to execute.
|
||||
renderers: IndexMap<String, Box<dyn Renderer>>,
|
||||
|
||||
/// Pre-processors to be run on the book.
|
||||
preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
/// Load a book from its root directory on disk.
|
||||
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
|
||||
let book_root = book_root.into();
|
||||
let config_location = book_root.join("book.toml");
|
||||
|
||||
let mut config = if config_location.exists() {
|
||||
debug!("Loading config from {}", config_location.display());
|
||||
Config::from_disk(&config_location)?
|
||||
} else {
|
||||
Config::default()
|
||||
};
|
||||
|
||||
config.update_from_env()?;
|
||||
|
||||
if tracing::enabled!(tracing::Level::TRACE) {
|
||||
for line in format!("Config: {config:#?}").lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
MDBook::load_with_config(book_root, config)
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom `Config`.
|
||||
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = load_book(src_dir, &config.build)?;
|
||||
|
||||
let renderers = determine_renderers(&config)?;
|
||||
let preprocessors = determine_preprocessors(&config, &root)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom `Config` and a custom summary.
|
||||
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
|
||||
book_root: P,
|
||||
config: Config,
|
||||
summary: Summary,
|
||||
) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = load_book_from_disk(&summary, src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config)?;
|
||||
let preprocessors = determine_preprocessors(&config, &root)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use mdbook_driver::MDBook;
|
||||
/// # use mdbook_driver::book::BookItem;
|
||||
/// # let book = MDBook::load("mybook").unwrap();
|
||||
/// for item in book.iter() {
|
||||
/// match *item {
|
||||
/// BookItem::Chapter(ref chapter) => {},
|
||||
/// BookItem::Separator => {},
|
||||
/// BookItem::PartTitle(ref title) => {}
|
||||
/// _ => {}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // would print something like this:
|
||||
/// // 1. Chapter 1
|
||||
/// // 1.1 Sub Chapter
|
||||
/// // 1.2 Sub Chapter
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// ```
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
self.book.iter()
|
||||
}
|
||||
|
||||
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
|
||||
/// and its accompanying directory structure.
|
||||
///
|
||||
/// The `BookBuilder` creates some boilerplate files and directories to get
|
||||
/// you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
/// ├── book
|
||||
/// └── src
|
||||
/// ├── chapter_1.md
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// It uses the path provided as the root directory for your book, then adds
|
||||
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
|
||||
/// to get you started.
|
||||
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
|
||||
BookBuilder::new(book_root)
|
||||
}
|
||||
|
||||
/// Tells the renderer to build our book and put it in the build directory.
|
||||
pub fn build(&self) -> Result<()> {
|
||||
info!("Book building has started");
|
||||
|
||||
for renderer in self.renderers.values() {
|
||||
self.execute_build_process(&**renderer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run preprocessors and return the final book.
|
||||
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
for preprocessor in self.preprocessors.values() {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config)? {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
Ok((preprocessed_book, preprocess_ctx))
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
|
||||
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
|
||||
let mut render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
preprocessed_book,
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
render_context
|
||||
.chapter_titles
|
||||
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.with_context(|| "Rendering failed")
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method.
|
||||
/// The only requirement is that your renderer implement the [`Renderer`]
|
||||
/// trait.
|
||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||
self.renderers
|
||||
.insert(renderer.name().to_string(), Box::new(renderer));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`] to be used when rendering the book.
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors
|
||||
.insert(preprocessor.name().to_string(), Box::new(preprocessor));
|
||||
self
|
||||
}
|
||||
|
||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
// test_chapter with chapter:None will run all tests.
|
||||
self.test_chapter(library_paths, None)
|
||||
}
|
||||
|
||||
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
|
||||
/// If `chapter` is `None`, all tests will be run.
|
||||
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
|
||||
let cwd = std::env::current_dir()?;
|
||||
let library_args: Vec<OsString> = library_paths
|
||||
.into_iter()
|
||||
.flat_map(|path| {
|
||||
let path = Path::new(path);
|
||||
let path = if path.is_relative() {
|
||||
cwd.join(path).into_os_string()
|
||||
} else {
|
||||
path.to_path_buf().into_os_string()
|
||||
};
|
||||
[OsString::from("-L"), path]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
let mut chapter_found = false;
|
||||
|
||||
struct TestRenderer;
|
||||
impl Renderer for TestRenderer {
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
fn name(&self) -> &str {
|
||||
"test"
|
||||
}
|
||||
|
||||
fn render(&self, _: &RenderContext) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let (book, _) = self.preprocess_book(&TestRenderer)?;
|
||||
|
||||
let color_output = std::io::stderr().is_terminal();
|
||||
let mut failed = false;
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
let chapter_path = match ch.path {
|
||||
Some(ref path) if !path.as_os_str().is_empty() => path,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if let Some(chapter) = chapter {
|
||||
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
|
||||
if chapter == "?" {
|
||||
info!("Skipping chapter '{}'...", ch.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
chapter_found = true;
|
||||
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(chapter_path);
|
||||
fs::write(&path, &ch.content)?;
|
||||
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
cmd.current_dir(temp_dir.path())
|
||||
.arg(chapter_path)
|
||||
.arg("--test")
|
||||
.args(&library_args);
|
||||
|
||||
if let Some(edition) = self.config.rust.edition {
|
||||
match edition {
|
||||
RustEdition::E2015 => {
|
||||
cmd.args(["--edition", "2015"]);
|
||||
}
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(["--edition", "2018"]);
|
||||
}
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024"]);
|
||||
}
|
||||
_ => panic!("RustEdition {edition:?} not covered"),
|
||||
}
|
||||
}
|
||||
|
||||
if color_output {
|
||||
cmd.args(["--color", "always"]);
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd
|
||||
.output()
|
||||
.with_context(|| "failed to execute `rustdoc`")?;
|
||||
|
||||
if !output.status.success() {
|
||||
failed = true;
|
||||
eprintln!(
|
||||
"ERROR rustdoc returned an error:\n\
|
||||
\n--- stdout\n{}\n--- stderr\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
if let Some(chapter) = chapter {
|
||||
if !chapter_found {
|
||||
bail!("Chapter not found: {}", chapter);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The logic for determining where a backend should put its build
|
||||
/// artefacts.
|
||||
///
|
||||
/// If there is only 1 renderer, put it in the directory pointed to by the
|
||||
/// `build.build_dir` key in [`Config`]. If there is more than one then the
|
||||
/// renderer gets its own directory within the main build dir.
|
||||
///
|
||||
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
||||
///
|
||||
/// - build/
|
||||
/// - index.html
|
||||
/// - ...
|
||||
///
|
||||
/// Otherwise if there are multiple:
|
||||
///
|
||||
/// - build/
|
||||
/// - epub/
|
||||
/// - my_awesome_book.epub
|
||||
/// - html/
|
||||
/// - index.html
|
||||
/// - ...
|
||||
/// - latex/
|
||||
/// - my_awesome_book.tex
|
||||
///
|
||||
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
|
||||
let build_dir = self.root.join(&self.config.build.build_dir);
|
||||
|
||||
if self.renderers.len() <= 1 {
|
||||
build_dir
|
||||
} else {
|
||||
build_dir.join(backend_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the directory containing this book's source files.
|
||||
pub fn source_dir(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
/// Get the directory containing the theme resources for the book.
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
self.config
|
||||
.html_config()
|
||||
.unwrap_or_default()
|
||||
.theme_dir(&self.root)
|
||||
}
|
||||
}
|
||||
|
||||
/// An `output` table.
|
||||
#[derive(Deserialize)]
|
||||
struct OutputConfig {
|
||||
command: Option<String>,
|
||||
}
|
||||
|
||||
/// Look at the `Config` and try to figure out what renderers to use.
|
||||
fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
|
||||
let mut renderers = IndexMap::new();
|
||||
|
||||
let outputs = config.outputs::<OutputConfig>()?;
|
||||
renderers.extend(outputs.into_iter().map(|(key, table)| {
|
||||
let renderer = if key == "html" {
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||
Box::new(CmdRenderer::new(key.clone(), command))
|
||||
};
|
||||
(key, renderer)
|
||||
}));
|
||||
|
||||
// if we couldn't find anything, add the HTML renderer as a default
|
||||
if renderers.is_empty() {
|
||||
renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
|
||||
}
|
||||
|
||||
Ok(renderers)
|
||||
}
|
||||
|
||||
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
|
||||
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// A `preprocessor` table.
|
||||
#[derive(Deserialize)]
|
||||
struct PreprocessorConfig {
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
before: Vec<String>,
|
||||
#[serde(default)]
|
||||
after: Vec<String>,
|
||||
#[serde(default)]
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(
|
||||
config: &Config,
|
||||
root: &Path,
|
||||
) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
|
||||
// Collect the names of all preprocessors intended to be run, and the order
|
||||
// in which they should be run.
|
||||
let mut preprocessor_names = TopologicalSort::<String>::new();
|
||||
|
||||
if config.build.use_default_preprocessors {
|
||||
for name in DEFAULT_PREPROCESSORS {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
|
||||
|
||||
for (name, table) in preprocessor_table.iter() {
|
||||
preprocessor_names.insert(name.to_string());
|
||||
|
||||
let exists = |name| {
|
||||
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|
||||
|| preprocessor_table.contains_key(name)
|
||||
};
|
||||
|
||||
for after in &table.before {
|
||||
if !exists(&after) {
|
||||
// Only warn so that preprocessors can be toggled on and off (e.g. for
|
||||
// troubleshooting) without having to worry about order too much.
|
||||
warn!(
|
||||
"preprocessor.{}.after contains \"{}\", which was not found",
|
||||
name, after
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(name, after);
|
||||
}
|
||||
}
|
||||
|
||||
for before in &table.after {
|
||||
if !exists(&before) {
|
||||
// See equivalent warning above for rationale
|
||||
warn!(
|
||||
"preprocessor.{}.before contains \"{}\", which was not found",
|
||||
name, before
|
||||
);
|
||||
} else {
|
||||
preprocessor_names.add_dependency(before, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that all links have been established, queue preprocessors in a suitable order
|
||||
let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
|
||||
// `pop_all()` returns an empty vector when no more items are not being depended upon
|
||||
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
|
||||
.take_while(|names| !names.is_empty())
|
||||
{
|
||||
// The `topological_sort` crate does not guarantee a stable order for ties, even across
|
||||
// runs of the same program. Thus, we break ties manually by sorting.
|
||||
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
|
||||
// values ([1]), which may not be an alphabetical sort.
|
||||
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
|
||||
// preprocessor execution order.
|
||||
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
|
||||
names.sort();
|
||||
for name in names {
|
||||
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
|
||||
"links" => Box::new(LinkPreprocessor::new()),
|
||||
"index" => Box::new(IndexPreprocessor::new()),
|
||||
_ => {
|
||||
// The only way to request a custom preprocessor is through the `preprocessor`
|
||||
// table, so it must exist, be a table, and contain the key.
|
||||
let table = &preprocessor_table[&name];
|
||||
let command = table
|
||||
.command
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("mdbook-{name}"));
|
||||
Box::new(CmdPreprocessor::new(
|
||||
name.clone(),
|
||||
command,
|
||||
root.to_owned(),
|
||||
table.optional,
|
||||
))
|
||||
}
|
||||
};
|
||||
preprocessors.insert(name, preprocessor);
|
||||
}
|
||||
}
|
||||
|
||||
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
|
||||
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
|
||||
if preprocessor_names.is_empty() {
|
||||
Ok(preprocessors)
|
||||
} else {
|
||||
Err(Error::msg("Cyclic dependency detected in preprocessors"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether we should run a particular `Preprocessor` in combination
|
||||
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
|
||||
/// method if the user doesn't say anything.
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(
|
||||
preprocessor: &dyn Preprocessor,
|
||||
renderer: &dyn Renderer,
|
||||
cfg: &Config,
|
||||
) -> Result<bool> {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
}
|
||||
|
||||
let key = format!("preprocessor.{}.renderers", preprocessor.name());
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
match cfg.get::<Vec<String>>(&key) {
|
||||
Ok(Some(explicit_renderers)) => {
|
||||
Ok(explicit_renderers.iter().any(|name| name == renderer_name))
|
||||
}
|
||||
Ok(None) => preprocessor.supports_renderer(renderer_name),
|
||||
Err(e) => bail!("failed to get `{key}`: {e}"),
|
||||
}
|
||||
}
|
||||
284
crates/mdbook-driver/src/mdbook/tests.rs
Normal file
284
crates/mdbook-driver/src/mdbook/tests.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_html_renderer_if_empty() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `output` table
|
||||
assert!(cfg.outputs::<toml::Value>().unwrap().is_empty());
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_a_random_renderer_to_the_config() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.set("output.random", Table::new()).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "random");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_a_random_renderer_with_custom_command_to_the_config() {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
let mut table = Table::new();
|
||||
table.insert("command".to_string(), Value::String("false".to_string()));
|
||||
cfg.set("output.random", table).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].name(), "random");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
|
||||
let cfg = Config::default();
|
||||
|
||||
// make sure we haven't got anything in the `preprocessor` table
|
||||
assert!(cfg.preprocessors::<toml::Value>().unwrap().is_empty());
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
let names: Vec<_> = got.values().map(|p| p.name()).collect();
|
||||
assert_eq!(names, ["index", "links"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_default_preprocessors_works() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.build.use_default_preprocessors = false;
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_determine_third_party_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
[preprocessor.random]
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
assert!(cfg.get::<Value>("preprocessor.random").unwrap().is_some());
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
assert!(got.contains_key("random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessors_can_provide_their_own_commands() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg
|
||||
.get::<OutputConfig>("preprocessor.random")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(random.command, Some("python random.py".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_before_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_after_must_be_array() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
after = 0
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_order_is_honored() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "last" ]
|
||||
after = [ "index" ]
|
||||
|
||||
[preprocessor.last]
|
||||
after = [ "links", "index" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
let index = |name| preprocessors.get_index_of(name).unwrap();
|
||||
let assert_before = |before, after| {
|
||||
if index(before) >= index(after) {
|
||||
eprintln!("Preprocessor order:");
|
||||
for preprocessor in preprocessors.keys() {
|
||||
eprintln!(" {}", preprocessor);
|
||||
}
|
||||
panic!("{before} should come before {after}");
|
||||
}
|
||||
};
|
||||
|
||||
assert_before("index", "random");
|
||||
assert_before("index", "last");
|
||||
assert_before("random", "last");
|
||||
assert_before("links", "last");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_dependencies_are_detected() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "index" ]
|
||||
|
||||
[preprocessor.index]
|
||||
before = [ "links" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_undefined_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
before = [ "random" ]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
// Does not contain "random"
|
||||
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["index", "links"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
before = [ "links" ]
|
||||
|
||||
[build]
|
||||
use-default-preprocessors = false
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
// Does not contain "links"
|
||||
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["random"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
let pre = LinkPreprocessor::new();
|
||||
|
||||
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg).unwrap();
|
||||
assert!(should_run);
|
||||
}
|
||||
|
||||
struct BoolPreprocessor(bool);
|
||||
impl Preprocessor for BoolPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"bool-preprocessor"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
|
||||
Ok(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
|
||||
let cfg = Config::default();
|
||||
let html = HtmlHandlebars::new();
|
||||
|
||||
let should_be = true;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let should_be = false;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
// Default is to sort preprocessors alphabetically.
|
||||
#[test]
|
||||
fn preprocessor_sorted_by_name() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.xyz]
|
||||
[preprocessor.abc]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
|
||||
|
||||
let names: Vec<_> = got.values().map(|p| p.name()).collect();
|
||||
assert_eq!(names, ["abc", "index", "links", "xyz"]);
|
||||
}
|
||||
|
||||
// Default is to sort renderers alphabetically.
|
||||
#[test]
|
||||
fn renderers_sorted_by_name() {
|
||||
let cfg_str = r#"
|
||||
[output.xyz]
|
||||
[output.abc]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
let got = determine_renderers(&cfg).unwrap();
|
||||
|
||||
let names: Vec<_> = got.values().map(|p| p.name()).collect();
|
||||
assert_eq!(names, ["abc", "xyz"]);
|
||||
}
|
||||
37
crates/mdbook-html/Cargo.toml
Normal file
37
crates/mdbook-html/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "mdbook-html"
|
||||
version = "0.5.2"
|
||||
description = "mdBook HTML renderer"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
ego-tree.workspace = true
|
||||
elasticlunr-rs = { workspace = true, optional = true }
|
||||
font-awesome-as-a-crate.workspace = true
|
||||
handlebars.workspace = true
|
||||
hex.workspace = true
|
||||
html5ever.workspace = true
|
||||
indexmap.workspace = true
|
||||
mdbook-core.workspace = true
|
||||
mdbook-markdown.workspace = true
|
||||
mdbook-renderer.workspace = true
|
||||
pulldown-cmark.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
search = ["dep:elasticlunr-rs"]
|
||||
13
crates/mdbook-html/README.md
Normal file
13
crates/mdbook-html/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# mdbook-html
|
||||
|
||||
[](https://docs.rs/mdbook-html)
|
||||
[](https://crates.io/crates/mdbook-html)
|
||||
[](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
|
||||
|
||||
This is the HTML renderer for [mdBook](https://rust-lang.github.io/mdBook/). This is intended for internal use only. It is automatically included by [`mdbook-driver`](https://crates.io/crates/mdbook-driver) to render books to HTML.
|
||||
|
||||
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
|
||||
|
||||
## License
|
||||
|
||||
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)
|
||||
@@ -8,13 +8,11 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
overflow-x: auto;
|
||||
background: #191f26;
|
||||
color: #e6e1cf;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6773;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
756
crates/mdbook-html/front-end/css/chrome.css
Normal file
756
crates/mdbook-html/front-end/css/chrome.css
Normal file
@@ -0,0 +1,756 @@
|
||||
/* CSS for UI elements (a.k.a. chrome) */
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) transparent;
|
||||
}
|
||||
#mdbook-searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
a > .hljs {
|
||||
color: var(--links);
|
||||
}
|
||||
|
||||
/*
|
||||
mdbook-body-container is necessary because mobile browsers don't seem to like
|
||||
overflow-x on the body tag when there is a <meta name="viewport"> tag.
|
||||
*/
|
||||
#mdbook-body-container {
|
||||
/*
|
||||
This is used when the sidebar pushes the body content off the side of
|
||||
the screen on small screens. Without it, dragging on mobile Safari
|
||||
will want to reposition the viewport in a weird way.
|
||||
*/
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#mdbook-menu-bar,
|
||||
#mdbook-menu-bar-hover-placeholder {
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#mdbook-menu-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
border-block-end-color: var(--bg);
|
||||
border-block-end-width: 1px;
|
||||
border-block-end-style: solid;
|
||||
}
|
||||
#mdbook-menu-bar.sticky,
|
||||
#mdbook-menu-bar-hover-placeholder:hover + #mdbook-menu-bar,
|
||||
#mdbook-menu-bar:hover,
|
||||
html.sidebar-visible #mdbook-menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
}
|
||||
#mdbook-menu-bar-hover-placeholder {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
height: var(--menu-bar-height);
|
||||
}
|
||||
#mdbook-menu-bar.bordered {
|
||||
border-block-end-color: var(--table-border-color);
|
||||
}
|
||||
#mdbook-menu-bar .fa-svg, #mdbook-menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
line-height: var(--menu-bar-height);
|
||||
cursor: pointer;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@media only screen and (max-width: 420px) {
|
||||
#mdbook-menu-bar .fa-svg, #mdbook-menu-bar .icon-button {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.icon-button .fa-svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-buttons {
|
||||
margin: 0 15px;
|
||||
}
|
||||
.right-buttons a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.left-buttons {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
html:not(.js) .left-buttons button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 2.4rem;
|
||||
line-height: var(--menu-bar-height);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.menu-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-bar,
|
||||
.menu-bar:visited,
|
||||
.nav-chapters,
|
||||
.nav-chapters:visited,
|
||||
.mobile-nav-chapters,
|
||||
.mobile-nav-chapters:visited,
|
||||
.menu-bar .icon-button,
|
||||
.menu-bar a .fa-svg {
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
.menu-bar .fa-svg:hover,
|
||||
.menu-bar .icon-button:hover,
|
||||
.nav-chapters:hover,
|
||||
.mobile-nav-chapters .fa-svg:hover {
|
||||
color: var(--icons-hover);
|
||||
}
|
||||
|
||||
/* Nav Icons */
|
||||
|
||||
.nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
min-width: 90px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-hover);
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
margin-block-start: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
width: 90px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.previous { float: left; }
|
||||
[dir=rtl] .previous { float: right; }
|
||||
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.next {
|
||||
float: right;
|
||||
right: var(--page-padding);
|
||||
}
|
||||
[dir=rtl] .next {
|
||||
float: left;
|
||||
right: unset;
|
||||
left: var(--page-padding);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1080px) {
|
||||
.nav-wide-wrapper { display: none; }
|
||||
.nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
@media only screen and (max-width: 1380px) {
|
||||
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
||||
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:not(pre):not(a) > .hljs {
|
||||
color: var(--inline-code-color);
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
pre > .buttons {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 0px;
|
||||
top: 2px;
|
||||
margin: 0px;
|
||||
padding: 2px 0px;
|
||||
|
||||
color: var(--sidebar-fg);
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0.1s linear, opacity 0.1s linear;
|
||||
}
|
||||
pre:hover > .buttons {
|
||||
visibility: visible;
|
||||
opacity: 1
|
||||
}
|
||||
pre > .buttons :hover {
|
||||
color: var(--sidebar-active);
|
||||
border-color: var(--icons-hover);
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 2px 3px 0px 4px;
|
||||
font-size: 23px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
border-color: var(--icons);
|
||||
background-color: var(--theme-popup-bg);
|
||||
transition: 100ms;
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
pre > .buttons button.clip-button {
|
||||
padding: 2px 4px 0px 6px;
|
||||
}
|
||||
pre > .buttons button.clip-button::before {
|
||||
/* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license
|
||||
*/
|
||||
content: url('data:image/svg+xml,<svg width="21" height="20" viewBox="0 0 24 25" \
|
||||
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
||||
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
||||
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
||||
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
||||
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
||||
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
||||
</svg>');
|
||||
filter: var(--copy-button-filter);
|
||||
}
|
||||
pre > .buttons button.clip-button:hover::before {
|
||||
filter: var(--copy-button-filter-hover);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar-resize-indicator {
|
||||
/* Hide resize indicator on devices with limited accuracy */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* FIXME: ACE editors overlap their buttons because ACE does absolute
|
||||
positioning within the code block which breaks padding. The only solution I
|
||||
can think of is to move the padding to the outer pre tag (or insert a div
|
||||
wrapper), but that would require fixing a whole bunch of CSS rules.
|
||||
*/
|
||||
.hljs.ace_editor {
|
||||
padding: 0rem 0rem;
|
||||
}
|
||||
|
||||
pre > .result {
|
||||
margin-block-start: 10px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
||||
#mdbook-searchresults a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding-block-start: 0;
|
||||
padding-block-end: 1px;
|
||||
padding-inline-start: 3px;
|
||||
padding-inline-end: 3px;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: -1px;
|
||||
margin-inline-start: -3px;
|
||||
margin-inline-end: -3px;
|
||||
background-color: var(--search-mark-bg);
|
||||
transition: background-color 300ms linear;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mark.fade-out {
|
||||
background-color: rgba(0,0,0,0) !important;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.searchbar-outer {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#mdbook-searchbar-outer.searching #mdbook-searchbar {
|
||||
padding-right: 30px;
|
||||
}
|
||||
#mdbook-searchbar-outer .spinner-wrapper {
|
||||
display: none;
|
||||
}
|
||||
#mdbook-searchbar-outer.searching .spinner-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
--spinner-margin: 2px;
|
||||
position: absolute;
|
||||
margin-block-start: calc(var(--searchbar-margin-block-start) + var(--spinner-margin));
|
||||
right: var(--spinner-margin);
|
||||
top: 0;
|
||||
bottom: var(--spinner-margin);
|
||||
padding: 6px;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
#fa-spin {
|
||||
animation: rotating 2s linear infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#mdbook-searchbar {
|
||||
width: 100%;
|
||||
margin-block-start: var(--searchbar-margin-block-start);
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
border: 1px solid var(--searchbar-border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--searchbar-bg);
|
||||
color: var(--searchbar-fg);
|
||||
}
|
||||
#mdbook-searchbar:focus,
|
||||
#mdbook-searchbar.active {
|
||||
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
||||
}
|
||||
|
||||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding-block-start: 18px;
|
||||
padding-block-end: 0;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 0;
|
||||
color: var(--searchresults-header-fg);
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
border-block-end: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#mdbook-searchresults {
|
||||
list-style: none;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
ul#mdbook-searchresults li {
|
||||
margin: 10px 0px;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
ul#mdbook-searchresults li.focus {
|
||||
background-color: var(--searchresults-li-bg);
|
||||
}
|
||||
ul#mdbook-searchresults span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 20px;
|
||||
margin-inline-end: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
ul#mdbook-searchresults span.teaser em {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-y: contain;
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.sidebar-iframe-inner {
|
||||
--padding: 10px;
|
||||
|
||||
background-color: var(--sidebar-bg);
|
||||
padding: var(--padding);
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: var(--sidebar-fg);
|
||||
min-height: calc(100vh - var(--padding) * 2);
|
||||
}
|
||||
.sidebar-iframe-outer {
|
||||
border: none;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
[dir=rtl] .sidebar { left: unset; right: 0; }
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
html:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar .sidebar-scrollbox {
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.sidebar .sidebar-resize-handle {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::before {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
border-left: dotted 2px currentColor;
|
||||
}
|
||||
.sidebar-resize-handle .sidebar-resize-indicator::after {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
border-left: dotted 2px currentColor;
|
||||
}
|
||||
|
||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
||||
left: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
right: unset;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
|
||||
}
|
||||
|
||||
html:not(.js) .sidebar-resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* sidebar-hidden */
|
||||
#mdbook-sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
[dir=rtl] #mdbook-sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
background: var(--sidebar-bg);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
[dir=rtl] #mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
@media only screen and (min-width: 620px) {
|
||||
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
|
||||
}
|
||||
[dir=rtl] #mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-inline-start: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.chapter li {
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
|
||||
/* This is a span wrapping the chapter link and the fold chevron. */
|
||||
.chapter-link-wrapper {
|
||||
/* Used to position the chevron to the right, allowing the text to wrap before it. */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chapter li a {
|
||||
/* Remove underlines. */
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
|
||||
.chapter li a:hover {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li a.active {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
/* This is the toggle chevron. */
|
||||
.chapter-fold-toggle {
|
||||
cursor: pointer;
|
||||
/* Positions the chevron to the side. */
|
||||
margin-inline-start: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.chapter-fold-toggle div {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
/* collapse the section */
|
||||
.chapter li:not(.expanded) > ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chapter li.chapter-item {
|
||||
line-height: 1.5em;
|
||||
margin-block-start: 0.6em;
|
||||
}
|
||||
|
||||
/* When expanded, rotate the chevron to point down. */
|
||||
.chapter li.expanded > span > .chapter-fold-toggle div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.chapter a.current-header {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.on-this-page {
|
||||
margin-left: 22px;
|
||||
border-inline-start: 4px solid var(--sidebar-header-border-color);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.on-this-page > ol {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Horizontal line in chapter list. */
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
.chapter .spacer {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
/* On touch devices, add more vertical spacing to make it easier to tap links. */
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-inline-start: 20px;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
/* Theme Menu Popup */
|
||||
|
||||
.theme-popup {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: var(--menu-bar-height);
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
background: var(--theme-popup-bg);
|
||||
border: 1px solid var(--theme-popup-border);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
/* Don't let the children's background extend past the rounded corners. */
|
||||
overflow: hidden;
|
||||
}
|
||||
[dir=rtl] .theme-popup { left: unset; right: 10px; }
|
||||
.theme-popup .default {
|
||||
color: var(--icons);
|
||||
}
|
||||
.theme-popup .theme {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 2px 20px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
.theme-popup .theme:hover {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
|
||||
.theme-selected::before {
|
||||
display: inline-block;
|
||||
content: "✓";
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
/* The container for the help popup that covers the whole window. */
|
||||
#mdbook-help-container {
|
||||
/* Position and size for the whole window. */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* This uses flex layout (which is set in book.js), and centers the popup
|
||||
in the window.*/
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
/* Dim out the book while the popup is visible. */
|
||||
background: var(--overlay-bg);
|
||||
}
|
||||
|
||||
/* The popup help box. */
|
||||
#mdbook-help-popup {
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
border-width: 1px;
|
||||
border-color: var(--theme-popup-border);
|
||||
border-style: solid;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mdbook-help-title {
|
||||
text-align: center;
|
||||
/* mdbook's margin for h2 is way too large. */
|
||||
margin: 10px;
|
||||
}
|
||||
408
crates/mdbook-html/front-end/css/general.css
Normal file
408
crates/mdbook-html/front-end/css/general.css
Normal file
@@ -0,0 +1,408 @@
|
||||
/* Base styles and content styles */
|
||||
|
||||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
color-scheme: var(--color-scheme);
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: var(--fg);
|
||||
background-color: var(--bg);
|
||||
text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--mono-font) !important;
|
||||
font-size: var(--code-font-size);
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
/* make long words/inline code not x overflow */
|
||||
main {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* make wide tables scroll if they overflow */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Don't change font size in headers. */
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.boring { opacity: 0.6; }
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h2, h3 { margin-block-start: 2.5em; }
|
||||
h4, h5 { margin-block-start: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
margin-block-start: 1em;
|
||||
}
|
||||
|
||||
h1:target::before,
|
||||
h2:target::before,
|
||||
h3:target::before,
|
||||
h4:target::before,
|
||||
h5:target::before,
|
||||
h6:target::before,
|
||||
dt:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-inline-start: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
/* This is broken on Safari as of version 14, but is fixed
|
||||
in Safari Technology Preview 117 which I think will be Safari 14.2.
|
||||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||
*/
|
||||
:target {
|
||||
/* Safari does not support logical properties */
|
||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #mdbook-menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
html:not(.js) .page-wrapper,
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
[dir=rtl]:not(.js) .page-wrapper,
|
||||
[dir=rtl].js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 5px 50px 5px;
|
||||
}
|
||||
.content main {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content p { line-height: 1.45em; }
|
||||
.content ol { line-height: 1.45em; }
|
||||
.content ul { line-height: 1.45em; }
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img, .content video { max-width: 100%; }
|
||||
.content .header:link,
|
||||
.content .header:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
.content .header:link,
|
||||
.content .header:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table td {
|
||||
padding: 3px 20px;
|
||||
border: 1px var(--table-border-color) solid;
|
||||
}
|
||||
table thead {
|
||||
background: var(--table-header-bg);
|
||||
}
|
||||
table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead th {
|
||||
padding: 3px 20px;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
/* Alternate background colors for rows */
|
||||
table tbody tr:nth-child(2n) {
|
||||
background: var(--table-alternate-bg);
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
margin: 20px 0;
|
||||
padding: 0 20px;
|
||||
color: var(--fg);
|
||||
background-color: var(--quote-bg);
|
||||
border-block-start: .1em solid var(--quote-border);
|
||||
border-block-end: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
/* TODO: Remove .warning in a future version of mdbook, it is replaced by
|
||||
blockquote tags. */
|
||||
.warning {
|
||||
margin: 20px;
|
||||
padding: 0 20px;
|
||||
border-inline-start: 2px solid var(--warning-border);
|
||||
}
|
||||
|
||||
.warning:before {
|
||||
position: absolute;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-inline-start: calc(-1.5rem - 21px);
|
||||
content: "ⓘ";
|
||||
text-align: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--warning-border);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
blockquote .warning:before {
|
||||
background-color: var(--quote-bg);
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: var(--table-border-color);
|
||||
border-radius: 4px;
|
||||
border: solid 1px var(--theme-popup-border);
|
||||
box-shadow: inset 0 -1px 0 var(--theme-hover);
|
||||
display: inline-block;
|
||||
font-size: var(--code-font-size);
|
||||
font-family: var(--mono-font);
|
||||
line-height: 10px;
|
||||
padding: 4px 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
sup {
|
||||
/* Set the line-height for superscript and footnote references so that there
|
||||
isn't an awkward space appearing above lines that contain the footnote.
|
||||
|
||||
See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583
|
||||
for an explanation.
|
||||
*/
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
/* The default spacing for a list is a little too large. */
|
||||
.footnote-definition ul,
|
||||
.footnote-definition ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.footnote-definition > li {
|
||||
/* Required to position the ::before target */
|
||||
position: relative;
|
||||
}
|
||||
.footnote-definition > li:target {
|
||||
scroll-margin-top: 50vh;
|
||||
}
|
||||
.footnote-reference:target {
|
||||
scroll-margin-top: 50vh;
|
||||
}
|
||||
/* Draws a border around the footnote (including the marker) when it is selected.
|
||||
TODO: If there are multiple linkbacks, highlight which one you just came
|
||||
from so you know which one to click.
|
||||
*/
|
||||
.footnote-definition > li:target::before {
|
||||
border: 2px solid var(--footnote-highlight);
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
bottom: -8px;
|
||||
left: -32px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
}
|
||||
/* Pulses the footnote reference so you can quickly see where you left off reading.
|
||||
This could use some improvement.
|
||||
*/
|
||||
@media not (prefers-reduced-motion) {
|
||||
.footnote-reference:target {
|
||||
animation: fn-highlight 0.8s;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@keyframes fn-highlight {
|
||||
from {
|
||||
background-color: var(--footnote-highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||
left: -8px; /* Half of the width of the icon */
|
||||
top: -35px;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
margin: 5px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.chapter li.part-title {
|
||||
color: var(--sidebar-fg);
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result-no-output {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.fa-svg svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
margin-bottom: -0.1em;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.1em;
|
||||
}
|
||||
|
||||
/* This uses a CSS counter to add numbers to definitions, but only if there is
|
||||
more than one definition. */
|
||||
dl, dt {
|
||||
counter-reset: dd-counter;
|
||||
}
|
||||
|
||||
/* When there is more than one definition, increment the counter. The first
|
||||
selector selects the first definition, and the second one selects definitions
|
||||
2 and beyond.*/
|
||||
dd:has(+ dd), dd + dd {
|
||||
counter-increment: dd-counter;
|
||||
/* Use flex display to help with positioning the numbers when there is a p
|
||||
tag inside the definition. */
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* Shows the counter for definitions. The first selector selects the first
|
||||
definition, and the second one selections definitions 2 and beyond.*/
|
||||
dd:has(+ dd)::before, dd + dd::before {
|
||||
content: counter(dd-counter) ". ";
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
dd > p {
|
||||
/* For loose definitions that have a p tag inside, don't add a bunch of
|
||||
space before the definition. */
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Remove some excess space from the bottom. */
|
||||
.blockquote-tag p:last-child {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.blockquote-tag {
|
||||
/* Add some padding to make the vertical bar a little taller than the text.*/
|
||||
padding: 2px 0px 2px 20px;
|
||||
/* Add a solid color bar on the left side. */
|
||||
border-inline-start-style: solid;
|
||||
border-inline-start-width: 4px;
|
||||
/* Disable the background color from normal blockquotes . */
|
||||
background-color: inherit;
|
||||
/* Disable border blocks from blockquotes. */
|
||||
border-block-start: none;
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.blockquote-tag-title svg {
|
||||
fill: currentColor;
|
||||
/* Add space between the icon and the title. */
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.blockquote-tag-note {
|
||||
border-inline-start-color: var(--blockquote-note-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-tip {
|
||||
border-inline-start-color: var(--blockquote-tip-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-important {
|
||||
border-inline-start-color: var(--blockquote-important-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-warning {
|
||||
border-inline-start-color: var(--blockquote-warning-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-caution {
|
||||
border-inline-start-color: var(--blockquote-caution-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-note .blockquote-tag-title {
|
||||
color: var(--blockquote-note-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-tip .blockquote-tag-title {
|
||||
color: var(--blockquote-tip-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-important .blockquote-tag-title {
|
||||
color: var(--blockquote-important-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-warning .blockquote-tag-title {
|
||||
color: var(--blockquote-warning-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-caution .blockquote-tag-title {
|
||||
color: var(--blockquote-caution-color);
|
||||
}
|
||||
|
||||
.blockquote-tag-title {
|
||||
/* Slightly increase the weight for more emphasis. */
|
||||
font-weight: 600;
|
||||
/* Vertically center the icon with the text. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Remove default large margins for a more compact display. */
|
||||
margin: 2px 0 8px 0;
|
||||
}
|
||||
|
||||
.blockquote-tag-title .fa-svg {
|
||||
fill: currentColor;
|
||||
/* Add some space between the icon and the text. */
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
@@ -61,7 +62,6 @@
|
||||
overflow-x: auto;
|
||||
background: #f6f7f6;
|
||||
color: #000;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
#mdbook-sidebar,
|
||||
#mdbook-menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
transform: none;
|
||||
margin-left: 0px;
|
||||
#mdbook-page-wrapper.page-wrapper {
|
||||
transform: none !important;
|
||||
margin-inline-start: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
#content {
|
||||
#mdbook-content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -23,11 +23,7 @@
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666;
|
||||
border-radius: 5px;
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Tomorrow Night Theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
/* https://github.com/jmblog/color-themes-for-highlightjs */
|
||||
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
/* https://github.com/jmblog/color-themes-for-highlightjs */
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.hljs-comment {
|
||||
@@ -11,6 +11,7 @@
|
||||
/* Tomorrow Red */
|
||||
.hljs-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-tag,
|
||||
.hljs-regexp,
|
||||
.ruby .hljs-constant,
|
||||
@@ -54,6 +55,7 @@
|
||||
|
||||
/* Tomorrow Aqua */
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.css .hljs-hexcolor {
|
||||
color: #8abeb7;
|
||||
}
|
||||
@@ -81,8 +83,6 @@
|
||||
overflow-x: auto;
|
||||
background: #1d1f21;
|
||||
color: #c5c8c6;
|
||||
padding: 0.5em;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.coffeescript .javascript,
|
||||
@@ -2,10 +2,16 @@
|
||||
/* Globals */
|
||||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--sidebar-target-width: 300px;
|
||||
--sidebar-width: min(var(--sidebar-target-width), 80vw);
|
||||
--sidebar-resize-indicator-width: 8px;
|
||||
--sidebar-resize-indicator-space: 2px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
--code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
--searchbar-margin-block-start: 5px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
@@ -36,6 +42,8 @@
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(210, 25%, 13%);
|
||||
--table-header-bg: hsl(210, 25%, 28%);
|
||||
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||
@@ -48,6 +56,25 @@
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #252932;
|
||||
--search-mark-bg: #e3b171;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%);
|
||||
|
||||
--footnote-highlight: #2668a6;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #74b9ff;
|
||||
--blockquote-tip-color: #09ca09;
|
||||
--blockquote-important-color: #d3abff;
|
||||
--blockquote-warning-color: #f0b72f;
|
||||
--blockquote-caution-color: #f21424;
|
||||
|
||||
--sidebar-header-border-color: #c18639;
|
||||
}
|
||||
|
||||
.coal {
|
||||
@@ -76,6 +103,8 @@
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
@@ -88,9 +117,28 @@
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #4493f8;
|
||||
--blockquote-tip-color: #08ae08;
|
||||
--blockquote-important-color: #ab7df8;
|
||||
--blockquote-warning-color: #d29922;
|
||||
--blockquote-caution-color: #d91b29;
|
||||
|
||||
--sidebar-header-border-color: #3473ad;
|
||||
}
|
||||
|
||||
.light {
|
||||
.light, html:not(.js) {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
|
||||
@@ -116,6 +164,8 @@
|
||||
--quote-bg: hsl(197, 37%, 96%);
|
||||
--quote-border: hsl(197, 37%, 91%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(0, 0%, 95%);
|
||||
--table-header-bg: hsl(0, 0%, 80%);
|
||||
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||
@@ -128,6 +178,25 @@
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #e4f2fe;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: light;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45.49%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%);
|
||||
|
||||
--footnote-highlight: #7e7eff;
|
||||
|
||||
--overlay-bg: rgba(200, 200, 205, 0.4);
|
||||
|
||||
--blockquote-note-color: #0969da;
|
||||
--blockquote-tip-color: #008000;
|
||||
--blockquote-important-color: #8250df;
|
||||
--blockquote-warning-color: #9a6700;
|
||||
--blockquote-caution-color: #b52731;
|
||||
|
||||
--sidebar-header-border-color: #6e6edb;
|
||||
}
|
||||
|
||||
.navy {
|
||||
@@ -156,6 +225,8 @@
|
||||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(226, 23%, 16%);
|
||||
--table-header-bg: hsl(226, 23%, 31%);
|
||||
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||
@@ -168,6 +239,25 @@
|
||||
--searchresults-border-color: #5c5c68;
|
||||
--searchresults-li-bg: #242430;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%);
|
||||
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #4493f8;
|
||||
--blockquote-tip-color: #09ca09;
|
||||
--blockquote-important-color: #ab7df8;
|
||||
--blockquote-warning-color: #d29922;
|
||||
--blockquote-caution-color: #f21424;
|
||||
|
||||
--sidebar-header-border-color: #2f6ab5;
|
||||
}
|
||||
|
||||
.rust {
|
||||
@@ -196,6 +286,8 @@
|
||||
--quote-bg: hsl(60, 5%, 75%);
|
||||
--quote-border: hsl(60, 5%, 70%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(60, 9%, 82%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||
@@ -208,10 +300,27 @@
|
||||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%);
|
||||
|
||||
--footnote-highlight: #d3a17a;
|
||||
|
||||
--overlay-bg: rgba(150, 150, 150, 0.25);
|
||||
|
||||
--blockquote-note-color: #023b95;
|
||||
--blockquote-tip-color: #007700;
|
||||
--blockquote-important-color: #8250df;
|
||||
--blockquote-warning-color: #603700;
|
||||
--blockquote-caution-color: #aa1721;
|
||||
|
||||
--sidebar-header-border-color: #8c391f;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.light.no-js {
|
||||
html:not(.js) {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
@@ -237,6 +346,8 @@
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
@@ -249,5 +360,24 @@
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
|
||||
--footnote-highlight: #4079ae;
|
||||
|
||||
--overlay-bg: rgba(33, 40, 48, 0.4);
|
||||
|
||||
--blockquote-note-color: #4493f8;
|
||||
--blockquote-tip-color: #08ae08;
|
||||
--blockquote-important-color: #ab7df8;
|
||||
--blockquote-warning-color: #d29922;
|
||||
--blockquote-caution-color: #d91b29;
|
||||
|
||||
--sidebar-header-border-color: #3473ad;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -16,7 +16,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-300italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -25,7 +25,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-regular.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -34,7 +34,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -43,7 +43,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -52,7 +52,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-600italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -61,7 +61,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -70,7 +70,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-700italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -79,7 +79,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -88,7 +88,7 @@
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
url('{{ resource "fonts/open-sans-v17-all-charsets-800italic.woff2" }}') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@@ -96,5 +96,5 @@
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
src: url('{{ resource "fonts/source-code-pro-v11-all-charsets-500.woff2" }}') format('woff2');
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
843
crates/mdbook-html/front-end/js/book.js
Normal file
843
crates/mdbook-html/front-end/js/book.js
Normal file
@@ -0,0 +1,843 @@
|
||||
'use strict';
|
||||
|
||||
/* global default_theme, default_dark_theme, default_light_theme, hljs, ClipboardJS */
|
||||
|
||||
// Fix back button cache problem
|
||||
window.onunload = function() { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playground_text(playground, hidden = true) {
|
||||
const code_block = playground.querySelector('code');
|
||||
|
||||
if (window.ace && code_block.classList.contains('editable')) {
|
||||
const editor = window.ace.edit(code_block);
|
||||
return editor.getValue();
|
||||
} else if (hidden) {
|
||||
return code_block.textContent;
|
||||
} else {
|
||||
return code_block.innerText;
|
||||
}
|
||||
}
|
||||
|
||||
(function codeSnippets() {
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
|
||||
]);
|
||||
}
|
||||
|
||||
const playgrounds = Array.from(document.querySelectorAll('.playground'));
|
||||
if (playgrounds.length > 0) {
|
||||
fetch_with_timeout('https://play.rust-lang.org/meta/crates', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
const playground_crates = response.crates.map(item => item['id']);
|
||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playground_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playground_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
const code_block = playground_block.querySelector('code');
|
||||
if (code_block.classList.contains('editable')) {
|
||||
const editor = window.ace.edit(code_block);
|
||||
editor.addEventListener('change', () => {
|
||||
update_play_button(playground_block, playground_crates);
|
||||
});
|
||||
// add Ctrl-Enter command to execute rust code
|
||||
editor.commands.addCommand({
|
||||
name: 'run',
|
||||
bindKey: {
|
||||
win: 'Ctrl-Enter',
|
||||
mac: 'Ctrl-Enter',
|
||||
},
|
||||
exec: _editor => run_rust_code(playground_block),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on https://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
const play_button = pre_block.querySelector('.play-button');
|
||||
|
||||
// skip if code is `no_run`
|
||||
if (pre_block.querySelector('code').classList.contains('no_run')) {
|
||||
play_button.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
const txt = playground_text(pre_block);
|
||||
const re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
const snippet_crates = [];
|
||||
let item;
|
||||
while (item = re.exec(txt)) {
|
||||
snippet_crates.push(item[1]);
|
||||
}
|
||||
|
||||
// check if all used crates are available on play.rust-lang.org
|
||||
const all_available = snippet_crates.every(function(elem) {
|
||||
return playground_crates.indexOf(elem) > -1;
|
||||
});
|
||||
|
||||
if (all_available) {
|
||||
play_button.classList.remove('hidden');
|
||||
play_button.hidden = false;
|
||||
} else {
|
||||
play_button.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function run_rust_code(code_block) {
|
||||
let result_block = code_block.querySelector('.result');
|
||||
if (!result_block) {
|
||||
result_block = document.createElement('code');
|
||||
result_block.className = 'result hljs language-bash';
|
||||
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
const text = playground_text(code_block);
|
||||
const classes = code_block.querySelector('code').classList;
|
||||
let edition = '2015';
|
||||
classes.forEach(className => {
|
||||
if (className.startsWith('edition')) {
|
||||
edition = className.slice(7);
|
||||
}
|
||||
});
|
||||
const params = {
|
||||
version: 'stable',
|
||||
optimize: '0',
|
||||
code: text,
|
||||
edition: edition,
|
||||
};
|
||||
|
||||
if (text.indexOf('#![feature') !== -1) {
|
||||
params.version = 'nightly';
|
||||
}
|
||||
|
||||
result_block.innerText = 'Running...';
|
||||
|
||||
fetch_with_timeout('https://play.rust-lang.org/evaluate.json', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.result.trim() === '') {
|
||||
result_block.innerText = 'No output';
|
||||
result_block.classList.add('result-no-output');
|
||||
} else {
|
||||
result_block.innerText = response.result;
|
||||
result_block.classList.remove('result-no-output');
|
||||
}
|
||||
})
|
||||
.catch(error => result_block.innerText = 'Playground Communication: ' + error.message);
|
||||
}
|
||||
|
||||
// Syntax highlighting Configuration
|
||||
hljs.configure({
|
||||
tabReplace: ' ', // 4 spaces
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
const code_nodes = Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
// Don't highlight `inline code` blocks in headers.
|
||||
.filter(function(node) {
|
||||
return !node.parentElement.classList.contains('header');
|
||||
});
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
code_nodes
|
||||
.filter(function(node) {
|
||||
return node.classList.contains('editable');
|
||||
})
|
||||
.forEach(function(block) {
|
||||
block.classList.remove('language-rust');
|
||||
});
|
||||
|
||||
code_nodes
|
||||
.filter(function(node) {
|
||||
return !node.classList.contains('editable');
|
||||
})
|
||||
.forEach(function(block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
code_nodes.forEach(function(block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function(block) {
|
||||
block.classList.add('hljs');
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('code.hljs')).forEach(function(block) {
|
||||
|
||||
const lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
if (!lines.length) {
|
||||
return;
|
||||
}
|
||||
block.classList.add('hide-boring');
|
||||
|
||||
const buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = '<button title="Show hidden lines" \
|
||||
aria-label="Show hidden lines"></button>';
|
||||
buttons.firstChild.innerHTML = document.getElementById('fa-eye').innerHTML;
|
||||
|
||||
// add expand button
|
||||
const pre_block = block.parentNode;
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
buttons.firstChild.addEventListener('click', function(e) {
|
||||
if (this.title === 'Show hidden lines') {
|
||||
this.innerHTML = document.getElementById('fa-eye-slash').innerHTML;
|
||||
this.title = 'Hide lines';
|
||||
this.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (this.title === 'Hide lines') {
|
||||
this.innerHTML = document.getElementById('fa-eye').innerHTML;
|
||||
this.title = 'Show hidden lines';
|
||||
this.setAttribute('aria-label', e.target.title);
|
||||
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function(block) {
|
||||
const pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
let buttons = pre_block.querySelector('.buttons');
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
const clipButton = document.createElement('button');
|
||||
clipButton.className = 'clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process playground code blocks
|
||||
Array.from(document.querySelectorAll('.playground')).forEach(function(pre_block) {
|
||||
// Add play button
|
||||
let buttons = pre_block.querySelector('.buttons');
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
const runCodeButton = document.createElement('button');
|
||||
runCodeButton.className = 'play-button';
|
||||
runCodeButton.hidden = true;
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
runCodeButton.innerHTML = document.getElementById('fa-play').innerHTML;
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
runCodeButton.addEventListener('click', () => {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
const copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
const code_block = pre_block.querySelector('code');
|
||||
if (window.ace && code_block.classList.contains('editable')) {
|
||||
const undoChangesButton = document.createElement('button');
|
||||
undoChangesButton.className = 'reset-button';
|
||||
undoChangesButton.title = 'Undo changes';
|
||||
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||
undoChangesButton.innerHTML +=
|
||||
document.getElementById('fa-clock-rotate-left').innerHTML;
|
||||
|
||||
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||
|
||||
undoChangesButton.addEventListener('click', function() {
|
||||
const editor = window.ace.edit(code_block);
|
||||
editor.setValue(editor.originalCode);
|
||||
editor.clearSelection();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function themes() {
|
||||
const html = document.querySelector('html');
|
||||
const themeToggleButton = document.getElementById('mdbook-theme-toggle');
|
||||
const themePopup = document.getElementById('mdbook-theme-list');
|
||||
const themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
const themeIds = [];
|
||||
themePopup.querySelectorAll('button.theme').forEach(function(el) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
const stylesheets = {
|
||||
ayuHighlight: document.querySelector('#mdbook-ayu-highlight-css'),
|
||||
tomorrowNight: document.querySelector('#mdbook-tomorrow-night-css'),
|
||||
highlight: document.querySelector('#mdbook-highlight-css'),
|
||||
};
|
||||
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector('button#mdbook-theme-' + get_theme()).focus();
|
||||
}
|
||||
|
||||
function updateThemeSelected() {
|
||||
themePopup.querySelectorAll('.theme-selected').forEach(function(el) {
|
||||
el.classList.remove('theme-selected');
|
||||
});
|
||||
const selected = get_saved_theme() ?? 'default_theme';
|
||||
let element = themePopup.querySelector('button#mdbook-theme-' + selected);
|
||||
if (element === null) {
|
||||
// Fall back in case there is no "Default" item.
|
||||
element = themePopup.querySelector('button#mdbook-theme-' + get_theme());
|
||||
}
|
||||
element.classList.add('theme-selected');
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
themePopup.style.display = 'none';
|
||||
themeToggleButton.setAttribute('aria-expanded', false);
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function get_saved_theme() {
|
||||
let theme = null;
|
||||
try {
|
||||
theme = localStorage.getItem('mdbook-theme');
|
||||
} catch {
|
||||
// ignore error.
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
|
||||
function delete_saved_theme() {
|
||||
localStorage.removeItem('mdbook-theme');
|
||||
}
|
||||
|
||||
function get_theme() {
|
||||
const theme = get_saved_theme();
|
||||
if (theme === null || theme === undefined || !themeIds.includes('mdbook-theme-' + theme)) {
|
||||
if (typeof default_dark_theme === 'undefined') {
|
||||
// A customized index.hbs might not define this, so fall back to
|
||||
// old behavior of determining the default on page load.
|
||||
return default_theme;
|
||||
}
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? default_dark_theme
|
||||
: default_light_theme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
let previousTheme = default_theme;
|
||||
function set_theme(theme, store = true) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme === 'coal' || theme === 'navy') {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = false;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = 'ace/theme/tomorrow_night';
|
||||
} else if (theme === 'ayu') {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
ace_theme = 'ace/theme/tomorrow_night';
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
ace_theme = 'ace/theme/dawn';
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
window.editors.forEach(function(editor) {
|
||||
editor.setTheme(ace_theme);
|
||||
});
|
||||
}
|
||||
|
||||
if (store) {
|
||||
try {
|
||||
localStorage.setItem('mdbook-theme', theme);
|
||||
} catch {
|
||||
// ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
previousTheme = theme;
|
||||
updateThemeSelected();
|
||||
}
|
||||
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
query.onchange = function() {
|
||||
set_theme(get_theme(), false);
|
||||
};
|
||||
|
||||
// Set theme.
|
||||
set_theme(get_theme(), false);
|
||||
|
||||
themeToggleButton.addEventListener('click', function() {
|
||||
if (themePopup.style.display === 'block') {
|
||||
hideThemes();
|
||||
} else {
|
||||
showThemes();
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('click', function(e) {
|
||||
let theme;
|
||||
if (e.target.className === 'theme') {
|
||||
theme = e.target.id;
|
||||
} else if (e.target.parentElement.className === 'theme') {
|
||||
theme = e.target.parentElement.id;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
theme = theme.replace(/^mdbook-theme-/, '');
|
||||
|
||||
if (theme === 'default_theme' || theme === null) {
|
||||
delete_saved_theme();
|
||||
set_theme(get_theme(), false);
|
||||
} else {
|
||||
set_theme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
themePopup.addEventListener('focusout', function(e) {
|
||||
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||
if (!!e.relatedTarget &&
|
||||
!themeToggleButton.contains(e.relatedTarget) &&
|
||||
!themePopup.contains(e.relatedTarget)
|
||||
) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS:
|
||||
// https://github.com/rust-lang/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' &&
|
||||
!themeToggleButton.contains(e.target) &&
|
||||
!themePopup.contains(e.target)
|
||||
) {
|
||||
hideThemes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
if (!themePopup.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let li;
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
hideThemes();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
li = document.activeElement.parentElement;
|
||||
if (li && li.previousElementSibling) {
|
||||
li.previousElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
li = document.activeElement.parentElement;
|
||||
if (li && li.nextElementSibling) {
|
||||
li.nextElementSibling.querySelector('button').focus();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:first-child button').focus();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
themePopup.querySelector('li:last-child button').focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function sidebar() {
|
||||
const sidebar = document.getElementById('mdbook-sidebar');
|
||||
const sidebarLinks = document.querySelectorAll('#mdbook-sidebar a');
|
||||
const sidebarToggleButton = document.getElementById('mdbook-sidebar-toggle');
|
||||
const sidebarResizeHandle = document.getElementById('mdbook-sidebar-resize-handle');
|
||||
const sidebarCheckbox = document.getElementById('mdbook-sidebar-toggle-anchor');
|
||||
let firstContact = null;
|
||||
|
||||
|
||||
/* Because we cannot change the `display` using only CSS after/before the transition, we
|
||||
need JS to do it. We change the display to prevent the browsers search to find text inside
|
||||
the collapsed sidebar. */
|
||||
if (!document.documentElement.classList.contains('sidebar-visible')) {
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
sidebar.addEventListener('transitionend', () => {
|
||||
/* We only change the display to "none" if we're collapsing the sidebar. */
|
||||
if (!sidebarCheckbox.checked) {
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
});
|
||||
sidebarToggleButton.addEventListener('click', () => {
|
||||
/* To allow the sidebar expansion animation, we first need to put back the display. */
|
||||
if (!sidebarCheckbox.checked) {
|
||||
sidebar.style.display = '';
|
||||
// Workaround for Safari skipping the animation when changing
|
||||
// `display` and a transform in the same event loop. This forces a
|
||||
// reflow after updating the display.
|
||||
sidebar.offsetHeight;
|
||||
}
|
||||
});
|
||||
|
||||
function showSidebar() {
|
||||
document.documentElement.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', true);
|
||||
sidebar.setAttribute('aria-hidden', false);
|
||||
try {
|
||||
localStorage.setItem('mdbook-sidebar', 'visible');
|
||||
} catch {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
function hideSidebar() {
|
||||
document.documentElement.classList.remove('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
sidebarToggleButton.setAttribute('aria-expanded', false);
|
||||
sidebar.setAttribute('aria-hidden', true);
|
||||
try {
|
||||
localStorage.setItem('mdbook-sidebar', 'hidden');
|
||||
} catch {
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle sidebar
|
||||
sidebarCheckbox.addEventListener('change', function sidebarToggle() {
|
||||
if (sidebarCheckbox.checked) {
|
||||
const current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else {
|
||||
hideSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize() {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
document.documentElement.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
let pos = e.clientX - sidebar.offsetLeft;
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (!document.documentElement.classList.contains('sidebar-visible')) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize() {
|
||||
document.documentElement.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
time: Date.now(),
|
||||
};
|
||||
}, { passive: true });
|
||||
|
||||
document.addEventListener('touchmove', function(e) {
|
||||
if (!firstContact) {
|
||||
return;
|
||||
}
|
||||
|
||||
const curX = e.touches[0].clientX;
|
||||
const xDiff = curX - firstContact.x,
|
||||
tDiff = Date.now() - firstContact.time;
|
||||
|
||||
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
||||
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) {
|
||||
showSidebar();
|
||||
} else if (xDiff < 0 && curX < 300) {
|
||||
hideSidebar();
|
||||
}
|
||||
|
||||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if (window.search && window.search.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
const html = document.querySelector('html');
|
||||
|
||||
function next() {
|
||||
const nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
const previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
}
|
||||
function showHelp() {
|
||||
const container = document.getElementById('mdbook-help-container');
|
||||
const overlay = document.getElementById('mdbook-help-popup');
|
||||
container.style.display = 'flex';
|
||||
|
||||
// Clicking outside the popup will dismiss it.
|
||||
const mouseHandler = event => {
|
||||
if (overlay.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
document.removeEventListener('mousedown', mouseHandler);
|
||||
hideHelp();
|
||||
};
|
||||
|
||||
// Pressing esc will dismiss the popup.
|
||||
const escapeKeyHandler = event => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
document.removeEventListener('keydown', escapeKeyHandler, true);
|
||||
hideHelp();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escapeKeyHandler, true);
|
||||
document.getElementById('mdbook-help-container')
|
||||
.addEventListener('mousedown', mouseHandler);
|
||||
}
|
||||
function hideHelp() {
|
||||
document.getElementById('mdbook-help-container').style.display = 'none';
|
||||
}
|
||||
|
||||
// Usually needs the Shift key to be pressed
|
||||
switch (e.key) {
|
||||
case '?':
|
||||
e.preventDefault();
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
|
||||
// Rest of the keys are only active when the Shift key is not pressed
|
||||
if (e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
if (html.dir === 'rtl') {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
if (html.dir === 'rtl') {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
(function clipboard() {
|
||||
const clipButtons = document.querySelectorAll('.clip-button');
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = '';
|
||||
elem.className = 'clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'clip-button tooltipped';
|
||||
}
|
||||
|
||||
const clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
text: function(trigger) {
|
||||
hideTooltip(trigger);
|
||||
const playground = trigger.closest('pre');
|
||||
return playground_text(playground, false);
|
||||
},
|
||||
});
|
||||
|
||||
Array.from(clipButtons).forEach(function(clipButton) {
|
||||
clipButton.addEventListener('mouseout', function(e) {
|
||||
hideTooltip(e.currentTarget);
|
||||
});
|
||||
});
|
||||
|
||||
clipboardSnippets.on('success', function(e) {
|
||||
e.clearSelection();
|
||||
showTooltip(e.trigger, 'Copied!');
|
||||
});
|
||||
|
||||
clipboardSnippets.on('error', function(e) {
|
||||
showTooltip(e.trigger, 'Clipboard error!');
|
||||
});
|
||||
})();
|
||||
|
||||
(function scrollToTop() {
|
||||
const menuTitle = document.querySelector('.menu-title');
|
||||
|
||||
menuTitle.addEventListener('click', function() {
|
||||
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
|
||||
(function controllMenu() {
|
||||
const menu = document.getElementById('mdbook-menu-bar');
|
||||
|
||||
(function controllPosition() {
|
||||
let scrollTop = document.scrollingElement.scrollTop;
|
||||
let prevScrollTop = scrollTop;
|
||||
const minMenuY = -menu.clientHeight - 50;
|
||||
// When the script loads, the page can be at any scroll (e.g. if you refresh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
let topCache = menu.style.top.slice(0, -2);
|
||||
menu.classList.remove('sticky');
|
||||
let stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||
document.addEventListener('scroll', function() {
|
||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
// `null` means that it doesn't need to be updated
|
||||
let nextSticky = null;
|
||||
let nextTop = null;
|
||||
const scrollDown = scrollTop > prevScrollTop;
|
||||
const menuPosAbsoluteY = topCache - scrollTop;
|
||||
if (scrollDown) {
|
||||
nextSticky = false;
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextTop = prevScrollTop;
|
||||
}
|
||||
} else {
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextSticky = true;
|
||||
} else if (menuPosAbsoluteY < minMenuY) {
|
||||
nextTop = prevScrollTop + minMenuY;
|
||||
}
|
||||
}
|
||||
if (nextSticky === true && stickyCache === false) {
|
||||
menu.classList.add('sticky');
|
||||
stickyCache = true;
|
||||
} else if (nextSticky === false && stickyCache === true) {
|
||||
menu.classList.remove('sticky');
|
||||
stickyCache = false;
|
||||
}
|
||||
if (nextTop !== null) {
|
||||
menu.style.top = nextTop + 'px';
|
||||
topCache = nextTop;
|
||||
}
|
||||
prevScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
function updateBorder() {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}
|
||||
updateBorder();
|
||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
||||
})();
|
||||
})();
|
||||
54
crates/mdbook-html/front-end/js/highlight.js
Normal file
54
crates/mdbook-html/front-end/js/highlight.js
Normal file
File diff suppressed because one or more lines are too long
555
crates/mdbook-html/front-end/searcher/searcher.js
Normal file
555
crates/mdbook-html/front-end/searcher/searcher.js
Normal file
@@ -0,0 +1,555 @@
|
||||
'use strict';
|
||||
|
||||
/* global Mark, elasticlunr, path_to_root */
|
||||
|
||||
window.search = window.search || {};
|
||||
(function search() {
|
||||
// Search functionality
|
||||
//
|
||||
// You can use !hasFocus() to prevent keyhandling in your key
|
||||
// event handlers while the user is typing their search.
|
||||
|
||||
if (!Mark || !elasticlunr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
// IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(search, pos) {
|
||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
||||
};
|
||||
}
|
||||
|
||||
const search_wrap = document.getElementById('mdbook-search-wrapper'),
|
||||
searchbar_outer = document.getElementById('mdbook-searchbar-outer'),
|
||||
searchbar = document.getElementById('mdbook-searchbar'),
|
||||
searchresults = document.getElementById('mdbook-searchresults'),
|
||||
searchresults_outer = document.getElementById('mdbook-searchresults-outer'),
|
||||
searchresults_header = document.getElementById('mdbook-searchresults-header'),
|
||||
searchicon = document.getElementById('mdbook-search-toggle'),
|
||||
content = document.getElementById('mdbook-content'),
|
||||
|
||||
// SVG text elements don't render if inside a <mark> tag.
|
||||
mark_exclude = ['text'],
|
||||
marker = new Mark(content),
|
||||
URL_SEARCH_PARAM = 'search',
|
||||
URL_MARK_PARAM = 'highlight';
|
||||
|
||||
let current_searchterm = '',
|
||||
doc_urls = [],
|
||||
search_options = {
|
||||
bool: 'AND',
|
||||
expand: true,
|
||||
fields: {
|
||||
title: {boost: 1},
|
||||
body: {boost: 1},
|
||||
breadcrumbs: {boost: 0},
|
||||
},
|
||||
},
|
||||
searchindex = null,
|
||||
results_options = {
|
||||
teaser_word_count: 30,
|
||||
limit_results: 30,
|
||||
},
|
||||
teaser_count = 0;
|
||||
|
||||
function hasFocus() {
|
||||
return searchbar === document.activeElement;
|
||||
}
|
||||
|
||||
function removeChildren(elem) {
|
||||
while (elem.firstChild) {
|
||||
elem.removeChild(elem.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to parse a url into its building blocks.
|
||||
function parseURL(url) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
return {
|
||||
source: url,
|
||||
protocol: a.protocol.replace(':', ''),
|
||||
host: a.hostname,
|
||||
port: a.port,
|
||||
params: (function() {
|
||||
const ret = {};
|
||||
const seg = a.search.replace(/^\?/, '').split('&');
|
||||
for (const part of seg) {
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
const s = part.split('=');
|
||||
ret[s[0]] = s[1];
|
||||
}
|
||||
return ret;
|
||||
})(),
|
||||
file: (a.pathname.match(/\/([^/?#]+)$/i) || ['', ''])[1],
|
||||
hash: a.hash.replace('#', ''),
|
||||
path: a.pathname.replace(/^([^/])/, '/$1'),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to recreate a url string from its building blocks.
|
||||
function renderURL(urlobject) {
|
||||
let url = urlobject.protocol + '://' + urlobject.host;
|
||||
if (urlobject.port !== '') {
|
||||
url += ':' + urlobject.port;
|
||||
}
|
||||
url += urlobject.path;
|
||||
let joiner = '?';
|
||||
for (const prop in urlobject.params) {
|
||||
if (Object.prototype.hasOwnProperty.call(urlobject.params, prop)) {
|
||||
url += joiner + prop + '=' + urlobject.params[prop];
|
||||
joiner = '&';
|
||||
}
|
||||
}
|
||||
if (urlobject.hash !== '') {
|
||||
url += '#' + urlobject.hash;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// Helper to escape html special chars for displaying the teasers
|
||||
const escapeHTML = (function() {
|
||||
const MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
'\'': ''',
|
||||
};
|
||||
const repl = function(c) {
|
||||
return MAP[c];
|
||||
};
|
||||
return function(s) {
|
||||
return s.replace(/[&<>'"]/g, repl);
|
||||
};
|
||||
})();
|
||||
|
||||
function formatSearchMetric(count, searchterm) {
|
||||
if (count === 1) {
|
||||
return count + ' search result for \'' + searchterm + '\':';
|
||||
} else if (count === 0) {
|
||||
return 'No search results for \'' + searchterm + '\'.';
|
||||
} else {
|
||||
return count + ' search results for \'' + searchterm + '\':';
|
||||
}
|
||||
}
|
||||
|
||||
function formatSearchResult(result, searchterms) {
|
||||
const teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||
teaser_count++;
|
||||
|
||||
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||
const url = doc_urls[result.ref].split('#');
|
||||
if (url.length === 1) { // no anchor found
|
||||
url.push('');
|
||||
}
|
||||
|
||||
// encodeURIComponent escapes all chars that could allow an XSS except
|
||||
// for '. Due to that we also manually replace ' with its url-encoded
|
||||
// representation (%27).
|
||||
const encoded_search = encodeURIComponent(searchterms.join(' ')).replace(/'/g, '%27');
|
||||
|
||||
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + encoded_search
|
||||
+ '#' + url[1] + '" aria-details="mdbook-teaser_' + teaser_count + '">'
|
||||
+ result.doc.breadcrumbs + '</a>'
|
||||
+ '<span class="teaser" id="mdbook-teaser_' + teaser_count
|
||||
+ '" aria-label="Search Result Teaser">' + teaser + '</span>';
|
||||
}
|
||||
|
||||
function makeTeaser(body, searchterms) {
|
||||
// The strategy is as follows:
|
||||
// First, assign a value to each word in the document:
|
||||
// Words that correspond to search terms (stemmer aware): 40
|
||||
// Normal words: 2
|
||||
// First word in a sentence: 8
|
||||
// Then use a sliding window with a constant number of words and count the
|
||||
// sum of the values of the words within the window. Then use the window that got the
|
||||
// maximum sum. If there are multiple maximas, then get the last one.
|
||||
// Enclose the terms in <em>.
|
||||
const stemmed_searchterms = searchterms.map(function(w) {
|
||||
return elasticlunr.stemmer(w.toLowerCase());
|
||||
});
|
||||
const searchterm_weight = 40;
|
||||
const weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||
// split in sentences, then words
|
||||
const sentences = body.toLowerCase().split('. ');
|
||||
let index = 0;
|
||||
let value = 0;
|
||||
let searchterm_found = false;
|
||||
for (const sentenceindex in sentences) {
|
||||
const words = sentences[sentenceindex].split(' ');
|
||||
value = 8;
|
||||
for (const wordindex in words) {
|
||||
const word = words[wordindex];
|
||||
if (word.length > 0) {
|
||||
for (const searchtermindex in stemmed_searchterms) {
|
||||
if (elasticlunr.stemmer(word).startsWith(
|
||||
stemmed_searchterms[searchtermindex])
|
||||
) {
|
||||
value = searchterm_weight;
|
||||
searchterm_found = true;
|
||||
}
|
||||
}
|
||||
weighted.push([word, value, index]);
|
||||
value = 2;
|
||||
}
|
||||
index += word.length;
|
||||
index += 1; // ' ' or '.' if last word in sentence
|
||||
}
|
||||
index += 1; // because we split at a two-char boundary '. '
|
||||
}
|
||||
|
||||
if (weighted.length === 0) {
|
||||
return body;
|
||||
}
|
||||
|
||||
const window_weight = [];
|
||||
const window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||
|
||||
let cur_sum = 0;
|
||||
for (let wordindex = 0; wordindex < window_size; wordindex++) {
|
||||
cur_sum += weighted[wordindex][1];
|
||||
}
|
||||
window_weight.push(cur_sum);
|
||||
for (let wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
||||
cur_sum -= weighted[wordindex][1];
|
||||
cur_sum += weighted[wordindex + window_size][1];
|
||||
window_weight.push(cur_sum);
|
||||
}
|
||||
|
||||
let max_sum_window_index = 0;
|
||||
if (searchterm_found) {
|
||||
let max_sum = 0;
|
||||
// backwards
|
||||
for (let i = window_weight.length - 1; i >= 0; i--) {
|
||||
if (window_weight[i] > max_sum) {
|
||||
max_sum = window_weight[i];
|
||||
max_sum_window_index = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
max_sum_window_index = 0;
|
||||
}
|
||||
|
||||
// add <em/> around searchterms
|
||||
const teaser_split = [];
|
||||
index = weighted[max_sum_window_index][2];
|
||||
for (let i = max_sum_window_index; i < max_sum_window_index + window_size; i++) {
|
||||
const word = weighted[i];
|
||||
if (index < word[2]) {
|
||||
// missing text from index to start of `word`
|
||||
teaser_split.push(body.substring(index, word[2]));
|
||||
index = word[2];
|
||||
}
|
||||
if (word[1] === searchterm_weight) {
|
||||
teaser_split.push('<em>');
|
||||
}
|
||||
index = word[2] + word[0].length;
|
||||
teaser_split.push(body.substring(word[2], index));
|
||||
if (word[1] === searchterm_weight) {
|
||||
teaser_split.push('</em>');
|
||||
}
|
||||
}
|
||||
|
||||
return teaser_split.join('');
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
results_options = config.results_options;
|
||||
search_options = config.search_options;
|
||||
doc_urls = config.doc_urls;
|
||||
searchindex = elasticlunr.Index.load(config.index);
|
||||
|
||||
searchbar_outer.classList.remove('searching');
|
||||
|
||||
searchbar.focus();
|
||||
|
||||
const searchterm = searchbar.value.trim();
|
||||
if (searchterm !== '') {
|
||||
searchbar.classList.add('active');
|
||||
doSearch(searchterm);
|
||||
}
|
||||
}
|
||||
|
||||
function initSearchInteractions(config) {
|
||||
// Set up events
|
||||
searchicon.addEventListener('click', () => {
|
||||
searchIconClickHandler();
|
||||
}, false);
|
||||
searchbar.addEventListener('keyup', () => {
|
||||
searchbarKeyUpHandler();
|
||||
}, false);
|
||||
document.addEventListener('keydown', e => {
|
||||
globalKeyHandler(e);
|
||||
}, false);
|
||||
// If the user uses the browser buttons, do the same as if a reload happened
|
||||
window.onpopstate = () => {
|
||||
doSearchOrMarkFromUrl();
|
||||
};
|
||||
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
||||
document.addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
|
||||
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||
doSearchOrMarkFromUrl();
|
||||
|
||||
// Exported functions
|
||||
config.hasFocus = hasFocus;
|
||||
}
|
||||
|
||||
initSearchInteractions(window.search);
|
||||
|
||||
function unfocusSearchbar() {
|
||||
// hacky, but just focusing a div only works once
|
||||
const tmp = document.createElement('input');
|
||||
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
|
||||
searchicon.appendChild(tmp);
|
||||
tmp.focus();
|
||||
tmp.remove();
|
||||
}
|
||||
|
||||
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
||||
function doSearchOrMarkFromUrl() {
|
||||
// Check current URL for search request
|
||||
const url = parseURL(window.location.href);
|
||||
if (Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM)
|
||||
&& url.params[URL_SEARCH_PARAM] !== '') {
|
||||
showSearch(true);
|
||||
searchbar.value = decodeURIComponent(
|
||||
(url.params[URL_SEARCH_PARAM] + '').replace(/\+/g, '%20'));
|
||||
searchbarKeyUpHandler(); // -> doSearch()
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(url.params, URL_MARK_PARAM)) {
|
||||
const words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
|
||||
marker.mark(words, {
|
||||
exclude: mark_exclude,
|
||||
});
|
||||
|
||||
const markers = document.querySelectorAll('mark');
|
||||
const hide = () => {
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
markers[i].classList.add('fade-out');
|
||||
window.setTimeout(() => {
|
||||
marker.unmark();
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
markers[i].addEventListener('click', hide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey ||
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
e.shiftKey ||
|
||||
e.target.type === 'textarea' ||
|
||||
e.target.type === 'text' ||
|
||||
!hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
searchbar.classList.remove('active');
|
||||
setSearchUrlParameters('',
|
||||
searchbar.value.trim() !== '' ? 'push' : 'replace');
|
||||
if (hasFocus()) {
|
||||
unfocusSearchbar();
|
||||
}
|
||||
showSearch(false);
|
||||
marker.unmark();
|
||||
} else if (!hasFocus() && (e.key === 's' || e.key === '/')) {
|
||||
e.preventDefault();
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else if (hasFocus() && (e.key === 'ArrowDown'
|
||||
|| e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
const first = searchresults.firstElementChild;
|
||||
if (first !== null) {
|
||||
unfocusSearchbar();
|
||||
first.classList.add('focus');
|
||||
if (e.key === 'Enter') {
|
||||
window.location.assign(first.querySelector('a'));
|
||||
}
|
||||
}
|
||||
} else if (!hasFocus() && (e.key === 'ArrowDown'
|
||||
|| e.key === 'ArrowUp'
|
||||
|| e.key === 'Enter')) {
|
||||
// not `:focus` because browser does annoying scrolling
|
||||
const focused = searchresults.querySelector('li.focus');
|
||||
if (!focused) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowDown') {
|
||||
const next = focused.nextElementSibling;
|
||||
if (next) {
|
||||
focused.classList.remove('focus');
|
||||
next.classList.add('focus');
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
focused.classList.remove('focus');
|
||||
const prev = focused.previousElementSibling;
|
||||
if (prev) {
|
||||
prev.classList.add('focus');
|
||||
} else {
|
||||
searchbar.select();
|
||||
}
|
||||
} else { // Enter
|
||||
window.location.assign(focused.querySelector('a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadSearchScript(url, id) {
|
||||
if (document.getElementById(id)) {
|
||||
return;
|
||||
}
|
||||
searchbar_outer.classList.add('searching');
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.id = id;
|
||||
script.onload = () => init(window.search);
|
||||
script.onerror = error => {
|
||||
console.error(`Failed to load \`${url}\`: ${error}`);
|
||||
};
|
||||
document.head.append(script);
|
||||
}
|
||||
|
||||
function showSearch(yes) {
|
||||
if (yes) {
|
||||
loadSearchScript(
|
||||
window.path_to_searchindex_js ||
|
||||
path_to_root + '{{ resource "searchindex.js" }}',
|
||||
'mdbook-search-index');
|
||||
search_wrap.classList.remove('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
search_wrap.classList.add('hidden');
|
||||
searchicon.setAttribute('aria-expanded', 'false');
|
||||
const results = searchresults.children;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
results[i].classList.remove('focus');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showResults(yes) {
|
||||
if (yes) {
|
||||
searchresults_outer.classList.remove('hidden');
|
||||
} else {
|
||||
searchresults_outer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for search icon
|
||||
function searchIconClickHandler() {
|
||||
if (search_wrap.classList.contains('hidden')) {
|
||||
showSearch(true);
|
||||
window.scrollTo(0, 0);
|
||||
searchbar.select();
|
||||
} else {
|
||||
showSearch(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Eventhandler for keyevents while the searchbar is focused
|
||||
function searchbarKeyUpHandler() {
|
||||
const searchterm = searchbar.value.trim();
|
||||
if (searchterm !== '') {
|
||||
searchbar.classList.add('active');
|
||||
doSearch(searchterm);
|
||||
} else {
|
||||
searchbar.classList.remove('active');
|
||||
showResults(false);
|
||||
removeChildren(searchresults);
|
||||
}
|
||||
|
||||
setSearchUrlParameters(searchterm, 'push_if_new_search_else_replace');
|
||||
|
||||
// Remove marks
|
||||
marker.unmark();
|
||||
}
|
||||
|
||||
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and
|
||||
// `#heading-anchor`. `action` can be one of "push", "replace",
|
||||
// "push_if_new_search_else_replace" and replaces or pushes a new browser history item.
|
||||
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
|
||||
function setSearchUrlParameters(searchterm, action) {
|
||||
const url = parseURL(window.location.href);
|
||||
const first_search = !Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM);
|
||||
|
||||
if (searchterm !== '' || action === 'push_if_new_search_else_replace') {
|
||||
url.params[URL_SEARCH_PARAM] = searchterm;
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
url.hash = '';
|
||||
} else {
|
||||
delete url.params[URL_MARK_PARAM];
|
||||
delete url.params[URL_SEARCH_PARAM];
|
||||
}
|
||||
// A new search will also add a new history item, so the user can go back
|
||||
// to the page prior to searching. A updated search term will only replace
|
||||
// the url.
|
||||
if (action === 'push' || action === 'push_if_new_search_else_replace' && first_search ) {
|
||||
history.pushState({}, document.title, renderURL(url));
|
||||
} else if (action === 'replace' ||
|
||||
action === 'push_if_new_search_else_replace' &&
|
||||
!first_search
|
||||
) {
|
||||
history.replaceState({}, document.title, renderURL(url));
|
||||
}
|
||||
}
|
||||
|
||||
function doSearch(searchterm) {
|
||||
// Don't search the same twice
|
||||
if (current_searchterm === searchterm) {
|
||||
return;
|
||||
}
|
||||
searchbar_outer.classList.add('searching');
|
||||
if (searchindex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_searchterm = searchterm;
|
||||
|
||||
// Do the actual search
|
||||
const results = searchindex.search(searchterm, search_options);
|
||||
const resultcount = Math.min(results.length, results_options.limit_results);
|
||||
|
||||
// Display search metrics
|
||||
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
||||
|
||||
// Clear and insert results
|
||||
const searchterms = searchterm.split(' ');
|
||||
removeChildren(searchresults);
|
||||
for (let i = 0; i < resultcount ; i++) {
|
||||
const resultElem = document.createElement('li');
|
||||
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||
searchresults.appendChild(resultElem);
|
||||
}
|
||||
|
||||
// Display results
|
||||
showResults(true);
|
||||
searchbar_outer.classList.remove('searching');
|
||||
}
|
||||
|
||||
// Exported functions
|
||||
search.hasFocus = hasFocus;
|
||||
})(window.search);
|
||||
367
crates/mdbook-html/front-end/templates/index.hbs
Normal file
367
crates/mdbook-html/front-end/templates/index.hbs
Normal file
@@ -0,0 +1,367 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="{{ default_theme }} sidebar-visible" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
{{#if is_print }}
|
||||
<meta name="robots" content="noindex">
|
||||
{{/if}}
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
{{/if}}
|
||||
|
||||
|
||||
<!-- Custom HTML head -->
|
||||
{{> head}}
|
||||
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ resource "favicon.svg" }}">
|
||||
{{/if}}
|
||||
{{#if favicon_png}}
|
||||
<link rel="shortcut icon" href="{{ resource "favicon.png" }}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" id="mdbook-highlight-css" href="{{ resource "highlight.css" }}">
|
||||
<link rel="stylesheet" id="mdbook-tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
|
||||
<link rel="stylesheet" id="mdbook-ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
<!-- MathJax -->
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
|
||||
<!-- Provide site root and default themes to javascript -->
|
||||
<script>
|
||||
const path_to_root = "{{ path_to_root }}";
|
||||
const default_light_theme = "{{ default_theme }}";
|
||||
const default_dark_theme = "{{ preferred_dark_theme }}";
|
||||
{{#if search_js}}
|
||||
window.path_to_searchindex_js = "{{ resource "searchindex.js" }}";
|
||||
{{/if}}
|
||||
</script>
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="{{ resource "toc.js" }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mdbook-help-container">
|
||||
<div id="mdbook-help-popup">
|
||||
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
|
||||
<div>
|
||||
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
|
||||
{{#if search_enabled}}
|
||||
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
|
||||
{{/if}}
|
||||
<p>Press <kbd>?</kbd> to show this help</p>
|
||||
<p>Press <kbd>Esc</kbd> to hide this help</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mdbook-body-container">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
try {
|
||||
let theme = localStorage.getItem('mdbook-theme');
|
||||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
|
||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||
}
|
||||
|
||||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||
}
|
||||
} catch (e) { }
|
||||
</script>
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script>
|
||||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
||||
let theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
const html = document.documentElement;
|
||||
html.classList.remove('{{ default_theme }}')
|
||||
html.classList.add(theme);
|
||||
html.classList.add("js");
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="mdbook-sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
let sidebar = null;
|
||||
const sidebar_toggle = document.getElementById("mdbook-sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
sidebar_toggle.checked = false;
|
||||
}
|
||||
if (sidebar === 'visible') {
|
||||
sidebar_toggle.checked = true;
|
||||
} else {
|
||||
html.classList.remove('sidebar-visible');
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav id="mdbook-sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<!-- populated by js -->
|
||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
|
||||
</noscript>
|
||||
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="mdbook-page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
{{> header}}
|
||||
<div id="mdbook-menu-bar-hover-placeholder"></div>
|
||||
<div id="mdbook-menu-bar" class="menu-bar sticky">
|
||||
<div class="left-buttons">
|
||||
<label id="mdbook-sidebar-toggle" class="icon-button" for="mdbook-sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="mdbook-sidebar">
|
||||
{{fa "solid" "bars"}}
|
||||
</label>
|
||||
<button id="mdbook-theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="mdbook-theme-list">
|
||||
{{fa "solid" "paintbrush"}}
|
||||
</button>
|
||||
<ul id="mdbook-theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-default_theme">Auto</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-light">Light</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="mdbook-search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="mdbook-searchbar">
|
||||
{{fa "solid" "magnifying-glass"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
{{#if print_enable}}
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
{{fa "solid" "print" "print-button"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if git_repository_url}}
|
||||
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
|
||||
{{fa git_repository_icon_class git_repository_icon}}
|
||||
</a>
|
||||
{{/if}}
|
||||
{{#if git_repository_edit_url}}
|
||||
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit" rel="edit">
|
||||
{{fa "solid" "pencil" "git-edit-button"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if search_enabled}}
|
||||
<div id="mdbook-search-wrapper" class="hidden">
|
||||
<form id="mdbook-searchbar-outer" class="searchbar-outer">
|
||||
<div class="search-wrapper">
|
||||
<input type="search" id="mdbook-searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="mdbook-searchresults-outer" aria-describedby="searchresults-header">
|
||||
<div class="spinner-wrapper">
|
||||
{{fa "solid" "spinner" "fa-spin"}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="mdbook-searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="mdbook-searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="mdbook-searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script>
|
||||
document.getElementById('mdbook-sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('mdbook-sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#mdbook-sidebar a')).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="mdbook-content" class="content">
|
||||
<main>
|
||||
{{{ content }}}
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#if previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
{{#if (eq ../text_direction "rtl")}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if next}}
|
||||
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
{{#if (eq ../text_direction "rtl")}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
{{#if previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
{{#if (eq ../text_direction "rtl")}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if next}}
|
||||
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
{{#if (eq text_direction "rtl")}}
|
||||
{{fa "solid" "angle-left"}}
|
||||
{{else}}
|
||||
{{fa "solid" "angle-right"}}
|
||||
{{/if}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
<template id=fa-eye>{{fa "solid" "eye"}}</template>
|
||||
<template id=fa-eye-slash>{{fa "solid" "eye-slash"}}</template>
|
||||
<template id=fa-copy>{{fa "regular" "copy"}}</template>
|
||||
<template id=fa-play>{{fa "solid" "play"}}</template>
|
||||
<template id=fa-clock-rotate-left>{{fa "solid" "clock-rotate-left"}}</template>
|
||||
|
||||
{{#if live_reload_endpoint}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script>
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
|
||||
const socket = new WebSocket(wsAddress);
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
socket.close();
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_line_numbers}}
|
||||
<script>
|
||||
window.playground_line_numbers = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_copyable}}
|
||||
<script>
|
||||
window.playground_copyable = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
<script src="{{ resource "ace.js" }}"></script>
|
||||
<script src="{{ resource "mode-rust.js" }}"></script>
|
||||
<script src="{{ resource "editor.js" }}"></script>
|
||||
<script src="{{ resource "theme-dawn.js" }}"></script>
|
||||
<script src="{{ resource "theme-tomorrow_night.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
{{#if search_js}}
|
||||
<script src="{{ resource "elasticlunr.min.js" }}"></script>
|
||||
<script src="{{ resource "mark.min.js" }}"></script>
|
||||
<script src="{{ resource "searcher.js" }}"></script>
|
||||
{{/if}}
|
||||
|
||||
<script src="{{ resource "clipboard.min.js" }}"></script>
|
||||
<script src="{{ resource "highlight.js" }}"></script>
|
||||
<script src="{{ resource "book.js" }}"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script src="{{ resource this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
{{#if mathjax_support}}
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
MathJax.Hub.Register.StartupHook('End', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{else}}
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
window.setTimeout(window.print, 100);
|
||||
});
|
||||
</script>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if fragment_map}}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fragmentMap =
|
||||
{{{fragment_map}}}
|
||||
;
|
||||
const target = fragmentMap[window.location.hash];
|
||||
if (target) {
|
||||
let url = new URL(target, window.location.href);
|
||||
window.location.replace(url.href);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
36
crates/mdbook-html/front-end/templates/redirect.hbs
Normal file
36
crates/mdbook-html/front-end/templates/redirect.hbs
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; URL={{url}}">
|
||||
<link rel="canonical" href="{{url}}">
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||
|
||||
<script>
|
||||
// This handles redirects that involve fragments.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fragmentMap =
|
||||
{{{fragment_map}}}
|
||||
;
|
||||
const fragment = window.location.hash;
|
||||
if (fragment) {
|
||||
let redirectUrl = "{{url}}";
|
||||
const target = fragmentMap[fragment];
|
||||
if (target) {
|
||||
let url = new URL(target, window.location.href);
|
||||
redirectUrl = url.href;
|
||||
} else {
|
||||
let url = new URL(redirectUrl, window.location.href);
|
||||
url.hash = window.location.hash;
|
||||
redirectUrl = url.href;
|
||||
}
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
// else redirect handled by http-equiv
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
40
crates/mdbook-html/front-end/templates/toc.html.hbs
Normal file
40
crates/mdbook-html/front-end/templates/toc.html.hbs
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- sidebar iframe generated using mdBook
|
||||
|
||||
This is a frame, and not included directly in the page, to control the total size of the
|
||||
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
the total size of the page becomes O(n**2).
|
||||
|
||||
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
|
||||
instead added to the main page by `toc.js` instead. The JavaScript mode is better
|
||||
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
|
||||
the rest of the page, so the sidebar and the main page theme would fall out of sync.
|
||||
-->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex">
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
{{/if}}
|
||||
<!-- Custom HTML head -->
|
||||
{{> head}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
|
||||
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ resource this }}">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body class="sidebar-iframe-inner">
|
||||
{{#toc}}{{/toc}}
|
||||
</body>
|
||||
</html>
|
||||
456
crates/mdbook-html/front-end/templates/toc.js.hbs
Normal file
456
crates/mdbook-html/front-end/templates/toc.js.hbs
Normal file
@@ -0,0 +1,456 @@
|
||||
// Populate the sidebar
|
||||
//
|
||||
// This is a script, and not included directly in the page, to control the total size of the book.
|
||||
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
// the total size of the page becomes O(n**2).
|
||||
class MDBookSidebarScrollbox extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
connectedCallback() {
|
||||
this.innerHTML = '{{#toc}}{{/toc}}';
|
||||
// Set the current, active page, and reveal it if it's hidden
|
||||
let current_page = document.location.href.toString().split('#')[0].split('?')[0];
|
||||
if (current_page.endsWith('/')) {
|
||||
current_page += 'index.html';
|
||||
}
|
||||
const links = Array.prototype.slice.call(this.querySelectorAll('a'));
|
||||
const l = links.length;
|
||||
for (let i = 0; i < l; ++i) {
|
||||
const link = links[i];
|
||||
const href = link.getAttribute('href');
|
||||
if (href && !href.startsWith('#') && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
}
|
||||
// The 'index' page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page
|
||||
|| i === 0
|
||||
&& path_to_root === ''
|
||||
&& current_page.endsWith('/index.html')) {
|
||||
link.classList.add('active');
|
||||
let parent = link.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) {
|
||||
parent.classList.add('expanded');
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Track and set sidebar scroll position
|
||||
this.addEventListener('click', e => {
|
||||
if (e.target.tagName === 'A') {
|
||||
const clientRect = e.target.getBoundingClientRect();
|
||||
const sidebarRect = this.getBoundingClientRect();
|
||||
sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top);
|
||||
}
|
||||
}, { passive: true });
|
||||
const sidebarScrollOffset = sessionStorage.getItem('sidebar-scroll-offset');
|
||||
sessionStorage.removeItem('sidebar-scroll-offset');
|
||||
if (sidebarScrollOffset !== null) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
const activeSection = this.querySelector('.active');
|
||||
if (activeSection) {
|
||||
const clientRect = activeSection.getBoundingClientRect();
|
||||
const sidebarRect = this.getBoundingClientRect();
|
||||
const currentOffset = clientRect.top - sidebarRect.top;
|
||||
this.scrollTop += currentOffset - parseFloat(sidebarScrollOffset);
|
||||
}
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via
|
||||
// 'next/previous chapter' buttons
|
||||
const activeSection = document.querySelector('#mdbook-sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
// Toggle buttons
|
||||
const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle');
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
Array.from(sidebarAnchorToggles).forEach(el => {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
}
|
||||
}
|
||||
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
|
||||
|
||||
{{#if sidebar_header_nav}}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Support for dynamically adding headers to the sidebar.
|
||||
|
||||
(function() {
|
||||
// This is used to detect which direction the page has scrolled since the
|
||||
// last scroll event.
|
||||
let lastKnownScrollPosition = 0;
|
||||
// This is the threshold in px from the top of the screen where it will
|
||||
// consider a header the "current" header when scrolling down.
|
||||
const defaultDownThreshold = 150;
|
||||
// Same as defaultDownThreshold, except when scrolling up.
|
||||
const defaultUpThreshold = 300;
|
||||
// The threshold is a virtual horizontal line on the screen where it
|
||||
// considers the "current" header to be above the line. The threshold is
|
||||
// modified dynamically to handle headers that are near the bottom of the
|
||||
// screen, and to slightly offset the behavior when scrolling up vs down.
|
||||
let threshold = defaultDownThreshold;
|
||||
// This is used to disable updates while scrolling. This is needed when
|
||||
// clicking the header in the sidebar, which triggers a scroll event. It
|
||||
// is somewhat finicky to detect when the scroll has finished, so this
|
||||
// uses a relatively dumb system of disabling scroll updates for a short
|
||||
// time after the click.
|
||||
let disableScroll = false;
|
||||
// Array of header elements on the page.
|
||||
let headers;
|
||||
// Array of li elements that are initially collapsed headers in the sidebar.
|
||||
// I'm not sure why eslint seems to have a false positive here.
|
||||
// eslint-disable-next-line prefer-const
|
||||
let headerToggles = [];
|
||||
// This is a debugging tool for the threshold which you can enable in the console.
|
||||
let thresholdDebug = false;
|
||||
|
||||
// Updates the threshold based on the scroll position.
|
||||
function updateThreshold() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
|
||||
// The number of pixels below the viewport, at most documentHeight.
|
||||
// This is used to push the threshold down to the bottom of the page
|
||||
// as the user scrolls towards the bottom.
|
||||
const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight));
|
||||
// The number of pixels above the viewport, at least defaultDownThreshold.
|
||||
// Similar to pixelsBelow, this is used to push the threshold back towards
|
||||
// the top when reaching the top of the page.
|
||||
const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop);
|
||||
// How much the threshold should be offset once it gets close to the
|
||||
// bottom of the page.
|
||||
const bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold);
|
||||
let adjustedBottomAdd = bottomAdd;
|
||||
|
||||
// Adjusts bottomAdd for a small document. The calculation above
|
||||
// assumes the document is at least twice the windowheight in size. If
|
||||
// it is less than that, then bottomAdd needs to be shrunk
|
||||
// proportional to the difference in size.
|
||||
if (documentHeight < windowHeight * 2) {
|
||||
const maxPixelsBelow = documentHeight - windowHeight;
|
||||
const t = 1 - pixelsBelow / Math.max(1, maxPixelsBelow);
|
||||
const clamp = Math.max(0, Math.min(1, t));
|
||||
adjustedBottomAdd *= clamp;
|
||||
}
|
||||
|
||||
let scrollingDown = true;
|
||||
if (scrollTop < lastKnownScrollPosition) {
|
||||
scrollingDown = false;
|
||||
}
|
||||
|
||||
if (scrollingDown) {
|
||||
// When scrolling down, move the threshold up towards the default
|
||||
// downwards threshold position. If near the bottom of the page,
|
||||
// adjustedBottomAdd will offset the threshold towards the bottom
|
||||
// of the page.
|
||||
const amountScrolledDown = scrollTop - lastKnownScrollPosition;
|
||||
const adjustedDefault = defaultDownThreshold + adjustedBottomAdd;
|
||||
threshold = Math.max(adjustedDefault, threshold - amountScrolledDown);
|
||||
} else {
|
||||
// When scrolling up, move the threshold down towards the default
|
||||
// upwards threshold position. If near the bottom of the page,
|
||||
// quickly transition the threshold back up where it normally
|
||||
// belongs.
|
||||
const amountScrolledUp = lastKnownScrollPosition - scrollTop;
|
||||
const adjustedDefault = defaultUpThreshold - pixelsAbove
|
||||
+ Math.max(0, adjustedBottomAdd - defaultDownThreshold);
|
||||
threshold = Math.min(adjustedDefault, threshold + amountScrolledUp);
|
||||
}
|
||||
|
||||
if (documentHeight <= windowHeight) {
|
||||
threshold = 0;
|
||||
}
|
||||
|
||||
if (thresholdDebug) {
|
||||
const id = 'mdbook-threshold-debug-data';
|
||||
let data = document.getElementById(id);
|
||||
if (data === null) {
|
||||
data = document.createElement('div');
|
||||
data.id = id;
|
||||
data.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
background-color: 0xeeeeee;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
`;
|
||||
document.body.appendChild(data);
|
||||
}
|
||||
data.innerHTML = `
|
||||
<table>
|
||||
<tr><td>documentHeight</td><td>${documentHeight.toFixed(1)}</td></tr>
|
||||
<tr><td>windowHeight</td><td>${windowHeight.toFixed(1)}</td></tr>
|
||||
<tr><td>scrollTop</td><td>${scrollTop.toFixed(1)}</td></tr>
|
||||
<tr><td>pixelsAbove</td><td>${pixelsAbove.toFixed(1)}</td></tr>
|
||||
<tr><td>pixelsBelow</td><td>${pixelsBelow.toFixed(1)}</td></tr>
|
||||
<tr><td>bottomAdd</td><td>${bottomAdd.toFixed(1)}</td></tr>
|
||||
<tr><td>adjustedBottomAdd</td><td>${adjustedBottomAdd.toFixed(1)}</td></tr>
|
||||
<tr><td>scrollingDown</td><td>${scrollingDown}</td></tr>
|
||||
<tr><td>threshold</td><td>${threshold.toFixed(1)}</td></tr>
|
||||
</table>
|
||||
`;
|
||||
drawDebugLine();
|
||||
}
|
||||
|
||||
lastKnownScrollPosition = scrollTop;
|
||||
}
|
||||
|
||||
function drawDebugLine() {
|
||||
if (!document.body) {
|
||||
return;
|
||||
}
|
||||
const id = 'mdbook-threshold-debug-line';
|
||||
const existingLine = document.getElementById(id);
|
||||
if (existingLine) {
|
||||
existingLine.remove();
|
||||
}
|
||||
const line = document.createElement('div');
|
||||
line.id = id;
|
||||
line.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${threshold}px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 2px;
|
||||
background-color: red;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
`;
|
||||
document.body.appendChild(line);
|
||||
}
|
||||
|
||||
function mdbookEnableThresholdDebug() {
|
||||
thresholdDebug = true;
|
||||
updateThreshold();
|
||||
drawDebugLine();
|
||||
}
|
||||
|
||||
window.mdbookEnableThresholdDebug = mdbookEnableThresholdDebug;
|
||||
|
||||
// Updates which headers in the sidebar should be expanded. If the current
|
||||
// header is inside a collapsed group, then it, and all its parents should
|
||||
// be expanded.
|
||||
function updateHeaderExpanded(currentA) {
|
||||
// Add expanded to all header-item li ancestors.
|
||||
let current = currentA.parentElement;
|
||||
while (current) {
|
||||
if (current.tagName === 'LI' && current.classList.contains('header-item')) {
|
||||
current.classList.add('expanded');
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Updates which header is marked as the "current" header in the sidebar.
|
||||
// This is done with a virtual Y threshold, where headers at or below
|
||||
// that line will be considered the current one.
|
||||
function updateCurrentHeader() {
|
||||
if (!headers || !headers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the classes, which will be rebuilt below.
|
||||
const els = document.getElementsByClassName('current-header');
|
||||
for (const el of els) {
|
||||
el.classList.remove('current-header');
|
||||
}
|
||||
for (const toggle of headerToggles) {
|
||||
toggle.classList.remove('expanded');
|
||||
}
|
||||
|
||||
// Find the last header that is above the threshold.
|
||||
let lastHeader = null;
|
||||
for (const header of headers) {
|
||||
const rect = header.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
lastHeader = header;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastHeader === null) {
|
||||
lastHeader = headers[0];
|
||||
const rect = lastHeader.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight;
|
||||
if (rect.top >= windowHeight) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the anchor in the summary.
|
||||
const href = '#' + lastHeader.id;
|
||||
const a = [...document.querySelectorAll('.header-in-summary')]
|
||||
.find(element => element.getAttribute('href') === href);
|
||||
if (!a) {
|
||||
return;
|
||||
}
|
||||
|
||||
a.classList.add('current-header');
|
||||
|
||||
updateHeaderExpanded(a);
|
||||
}
|
||||
|
||||
// Updates which header is "current" based on the threshold line.
|
||||
function reloadCurrentHeader() {
|
||||
if (disableScroll) {
|
||||
return;
|
||||
}
|
||||
updateThreshold();
|
||||
updateCurrentHeader();
|
||||
}
|
||||
|
||||
|
||||
// When clicking on a header in the sidebar, this adjusts the threshold so
|
||||
// that it is located next to the header. This is so that header becomes
|
||||
// "current".
|
||||
function headerThresholdClick(event) {
|
||||
// See disableScroll description why this is done.
|
||||
disableScroll = true;
|
||||
setTimeout(() => {
|
||||
disableScroll = false;
|
||||
}, 100);
|
||||
// requestAnimationFrame is used to delay the update of the "current"
|
||||
// header until after the scroll is done, and the header is in the new
|
||||
// position.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Closest is needed because if it has child elements like <code>.
|
||||
const a = event.target.closest('a');
|
||||
const href = a.getAttribute('href');
|
||||
const targetId = href.substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
if (targetElement) {
|
||||
threshold = targetElement.getBoundingClientRect().bottom;
|
||||
updateCurrentHeader();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Takes the nodes from the given head and copies them over to the
|
||||
// destination, along with some filtering.
|
||||
function filterHeader(source, dest) {
|
||||
const clone = source.cloneNode(true);
|
||||
clone.querySelectorAll('mark').forEach(mark => {
|
||||
mark.replaceWith(...mark.childNodes);
|
||||
});
|
||||
dest.append(...clone.childNodes);
|
||||
}
|
||||
|
||||
// Scans page for headers and adds them to the sidebar.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const activeSection = document.querySelector('#mdbook-sidebar .active');
|
||||
if (activeSection === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const main = document.getElementsByTagName('main')[0];
|
||||
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
|
||||
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
|
||||
|
||||
if (headers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a tree of headers in the sidebar.
|
||||
|
||||
const stack = [];
|
||||
|
||||
const firstLevel = parseInt(headers[0].tagName.charAt(1));
|
||||
for (let i = 1; i < firstLevel; i++) {
|
||||
const ol = document.createElement('ol');
|
||||
ol.classList.add('section');
|
||||
if (stack.length > 0) {
|
||||
stack[stack.length - 1].ol.appendChild(ol);
|
||||
}
|
||||
stack.push({level: i + 1, ol: ol});
|
||||
}
|
||||
|
||||
// The level where it will start folding deeply nested headers.
|
||||
const foldLevel = 3;
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i];
|
||||
const level = parseInt(header.tagName.charAt(1));
|
||||
|
||||
const currentLevel = stack[stack.length - 1].level;
|
||||
if (level > currentLevel) {
|
||||
// Begin nesting to this level.
|
||||
for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) {
|
||||
const ol = document.createElement('ol');
|
||||
ol.classList.add('section');
|
||||
const last = stack[stack.length - 1];
|
||||
const lastChild = last.ol.lastChild;
|
||||
// Handle the case where jumping more than one nesting
|
||||
// level, which doesn't have a list item to place this new
|
||||
// list inside of.
|
||||
if (lastChild) {
|
||||
lastChild.appendChild(ol);
|
||||
} else {
|
||||
last.ol.appendChild(ol);
|
||||
}
|
||||
stack.push({level: nextLevel, ol: ol});
|
||||
}
|
||||
} else if (level < currentLevel) {
|
||||
while (stack.length > 1 && stack[stack.length - 1].level > level) {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('header-item');
|
||||
li.classList.add('expanded');
|
||||
if (level < foldLevel) {
|
||||
li.classList.add('expanded');
|
||||
}
|
||||
const span = document.createElement('span');
|
||||
span.classList.add('chapter-link-wrapper');
|
||||
const a = document.createElement('a');
|
||||
span.appendChild(a);
|
||||
a.href = '#' + header.id;
|
||||
a.classList.add('header-in-summary');
|
||||
filterHeader(header.children[0], a);
|
||||
a.addEventListener('click', headerThresholdClick);
|
||||
const nextHeader = headers[i + 1];
|
||||
if (nextHeader !== undefined) {
|
||||
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
|
||||
if (nextLevel > level && level >= foldLevel) {
|
||||
const toggle = document.createElement('a');
|
||||
toggle.classList.add('chapter-fold-toggle');
|
||||
toggle.classList.add('header-toggle');
|
||||
toggle.addEventListener('click', () => {
|
||||
li.classList.toggle('expanded');
|
||||
});
|
||||
const toggleDiv = document.createElement('div');
|
||||
toggleDiv.textContent = '❱';
|
||||
toggle.appendChild(toggleDiv);
|
||||
span.appendChild(toggle);
|
||||
headerToggles.push(li);
|
||||
}
|
||||
}
|
||||
li.appendChild(span);
|
||||
|
||||
const currentParent = stack[stack.length - 1];
|
||||
currentParent.ol.appendChild(li);
|
||||
}
|
||||
|
||||
const onThisPage = document.createElement('div');
|
||||
onThisPage.classList.add('on-this-page');
|
||||
onThisPage.append(stack[0].ol);
|
||||
const activeItemSpan = activeSection.parentElement;
|
||||
activeItemSpan.after(onThisPage);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
|
||||
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
|
||||
})();
|
||||
|
||||
{{/if}}
|
||||
26
crates/mdbook-html/src/html/admonitions.rs
Normal file
26
crates/mdbook-html/src/html/admonitions.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use pulldown_cmark::BlockQuoteKind;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_NOTE: &str = r#"<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_TIP: &str = r#"<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_IMPORTANT: &str = r#"<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_WARNING: &str = r#"<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
|
||||
|
||||
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
|
||||
const ICON_CAUTION: &str = r#"<path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
|
||||
|
||||
pub(crate) fn select_tag(kind: BlockQuoteKind) -> (&'static str, &'static str, &'static str) {
|
||||
match kind {
|
||||
BlockQuoteKind::Note => ("note", ICON_NOTE, "Note"),
|
||||
BlockQuoteKind::Tip => ("tip", ICON_TIP, "Tip"),
|
||||
BlockQuoteKind::Important => ("important", ICON_IMPORTANT, "Important"),
|
||||
BlockQuoteKind::Warning => ("warning", ICON_WARNING, "Warning"),
|
||||
BlockQuoteKind::Caution => ("caution", ICON_CAUTION, "Caution"),
|
||||
}
|
||||
}
|
||||
193
crates/mdbook-html/src/html/hide_lines.rs
Normal file
193
crates/mdbook-html/src/html/hide_lines.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! Support for hiding code lines.
|
||||
|
||||
use crate::html::{Element, Node};
|
||||
use ego_tree::{NodeId, Tree};
|
||||
use html5ever::tendril::StrTendril;
|
||||
use mdbook_core::static_regex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Wraps hidden lines in a `<span>` for the given code block.
|
||||
pub(crate) fn hide_lines(
|
||||
tree: &mut Tree<Node>,
|
||||
code_id: NodeId,
|
||||
hidelines: &HashMap<String, String>,
|
||||
) {
|
||||
let mut node = tree.get_mut(code_id).unwrap();
|
||||
let el = node.value().as_element().unwrap();
|
||||
|
||||
let classes: Vec<_> = el.attr("class").unwrap_or_default().split(' ').collect();
|
||||
let language = classes
|
||||
.iter()
|
||||
.filter_map(|cls| cls.strip_prefix("language-"))
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let hideline_info = classes
|
||||
.iter()
|
||||
.filter_map(|cls| cls.strip_prefix("hidelines="))
|
||||
.map(|prefix| prefix.to_string())
|
||||
.next();
|
||||
|
||||
if let Some(mut child) = node.first_child()
|
||||
&& let Node::Text(text) = child.value()
|
||||
{
|
||||
if language == "rust" {
|
||||
let new_nodes = hide_lines_rust(text);
|
||||
child.detach();
|
||||
let root = tree.extend_tree(new_nodes);
|
||||
let root_id = root.id();
|
||||
let mut node = tree.get_mut(code_id).unwrap();
|
||||
node.reparent_from_id_append(root_id);
|
||||
} else {
|
||||
// Use the prefix from the code block, else the prefix from config.
|
||||
let hidelines_prefix = hideline_info
|
||||
.as_deref()
|
||||
.or_else(|| hidelines.get(&language).map(|p| p.as_str()));
|
||||
if let Some(prefix) = hidelines_prefix {
|
||||
let new_nodes = hide_lines_with_prefix(text, prefix);
|
||||
child.detach();
|
||||
let root = tree.extend_tree(new_nodes);
|
||||
let root_id = root.id();
|
||||
let mut node = tree.get_mut(code_id).unwrap();
|
||||
node.reparent_from_id_append(root_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps hidden lines in a `<span>` specifically for Rust code blocks.
|
||||
fn hide_lines_rust(text: &StrTendril) -> Tree<Node> {
|
||||
static_regex!(BORING_LINES_REGEX, r"^(\s*)#(.?)(.*)$");
|
||||
|
||||
let mut tree = Tree::new(Node::Fragment);
|
||||
let mut root = tree.root_mut();
|
||||
let mut lines = text.lines().peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
// Don't include newline on the last line.
|
||||
let newline = if lines.peek().is_none() { "" } else { "\n" };
|
||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||
if &caps[2] == "#" {
|
||||
root.append(Node::Text(
|
||||
format!("{}{}{}{newline}", &caps[1], &caps[2], &caps[3]).into(),
|
||||
));
|
||||
continue;
|
||||
} else if matches!(&caps[2], "" | " ") {
|
||||
let mut span = Element::new("span");
|
||||
span.insert_attr("class", "boring".into());
|
||||
let mut span = root.append(Node::Element(span));
|
||||
span.append(Node::Text(
|
||||
format!("{}{}{newline}", &caps[1], &caps[3]).into(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
root.append(Node::Text(format!("{line}{newline}").into()));
|
||||
}
|
||||
tree
|
||||
}
|
||||
|
||||
/// Wraps hidden lines in a `<span>` tag for lines starting with the given prefix.
|
||||
fn hide_lines_with_prefix(content: &str, prefix: &str) -> Tree<Node> {
|
||||
let mut tree = Tree::new(Node::Fragment);
|
||||
let mut root = tree.root_mut();
|
||||
for line in content.lines() {
|
||||
if line.trim_start().starts_with(prefix) {
|
||||
let pos = line.find(prefix).unwrap();
|
||||
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
|
||||
let mut span = Element::new("span");
|
||||
span.insert_attr("class", "boring".into());
|
||||
let mut span = root.append(Node::Element(span));
|
||||
span.append(Node::Text(format!("{ws}{rest}\n").into()));
|
||||
} else {
|
||||
root.append(Node::Text(format!("{line}\n").into()));
|
||||
}
|
||||
}
|
||||
tree
|
||||
}
|
||||
|
||||
/// If this code text is missing an `fn main`, the wrap it with `fn main` in a
|
||||
/// fashion similar to rustdoc, with the wrapper hidden.
|
||||
pub(crate) fn wrap_rust_main(text: &str) -> Option<String> {
|
||||
if !text.contains("fn main") && !text.contains("quick_main!") {
|
||||
let (attrs, code) = partition_rust_source(text);
|
||||
let newline = if code.is_empty() || code.ends_with('\n') {
|
||||
""
|
||||
} else {
|
||||
"\n"
|
||||
};
|
||||
Some(format!(
|
||||
"# #![allow(unused)]\n{attrs}# fn main() {{\n{code}{newline}# }}"
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits Rust inner attributes from the given source string.
|
||||
///
|
||||
/// Returns `(inner_attrs, rest_of_code)`.
|
||||
fn partition_rust_source(s: &str) -> (&str, &str) {
|
||||
static_regex!(
|
||||
HEADER_RE,
|
||||
r"^(?mx)
|
||||
(
|
||||
(?:
|
||||
^[ \t]*\#!\[.* (?:\r?\n)?
|
||||
|
|
||||
^\s* (?:\r?\n)?
|
||||
)*
|
||||
)"
|
||||
);
|
||||
let split_idx = match HEADER_RE.captures(s) {
|
||||
Some(caps) => {
|
||||
let attributes = &caps[1];
|
||||
if attributes.trim().is_empty() {
|
||||
// Don't include pure whitespace as an attribute. The
|
||||
// whitespace in the regex is intended to handle multiple
|
||||
// attributes *separated* by potential whitespace.
|
||||
0
|
||||
} else {
|
||||
attributes.len()
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
s.split_at(split_idx)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_partitions_rust_source() {
|
||||
assert_eq!(partition_rust_source(""), ("", ""));
|
||||
assert_eq!(partition_rust_source("let x = 1;"), ("", "let x = 1;"));
|
||||
assert_eq!(
|
||||
partition_rust_source("fn main()\n{ let x = 1; }\n"),
|
||||
("", "fn main()\n{ let x = 1; }\n")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source("#![allow(foo)]"),
|
||||
("#![allow(foo)]", "")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source("#![allow(foo)]\n"),
|
||||
("#![allow(foo)]\n", "")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source("#![allow(foo)]\nlet x = 1;"),
|
||||
("#![allow(foo)]\n", "let x = 1;")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source(
|
||||
"\n\
|
||||
#![allow(foo)]\n\
|
||||
\n\
|
||||
#![allow(bar)]\n\
|
||||
\n\
|
||||
let x = 1;"
|
||||
),
|
||||
("\n#![allow(foo)]\n\n#![allow(bar)]\n\n", "let x = 1;")
|
||||
);
|
||||
assert_eq!(
|
||||
partition_rust_source(" // Example"),
|
||||
("", " // Example")
|
||||
);
|
||||
}
|
||||
108
crates/mdbook-html/src/html/mod.rs
Normal file
108
crates/mdbook-html/src/html/mod.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
//! HTML rendering support.
|
||||
//!
|
||||
//! This module's primary entry point is [`render_markdown`] which will take
|
||||
//! markdown text and render it to HTML. In summary, the general procedure of
|
||||
//! that function is:
|
||||
//!
|
||||
//! 1. Use [`pulldown_cmark`] to parse the markdown and generate events.
|
||||
//! 2. [`tree`] converts those events to a tree data structure.
|
||||
//! 1. Parse HTML inside the markdown using [`tokenizer`].
|
||||
//! 2. Apply various transformations to the tree data structure, such as adding header links.
|
||||
//! 3. Serialize the tree to HTML in [`serialize()`].
|
||||
|
||||
use ego_tree::Tree;
|
||||
use mdbook_core::book::{Book, Chapter};
|
||||
use mdbook_core::config::{HtmlConfig, RustEdition};
|
||||
use mdbook_markdown::{MarkdownOptions, new_cmark_parser};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod admonitions;
|
||||
mod hide_lines;
|
||||
mod print;
|
||||
mod serialize;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tokenizer;
|
||||
mod tree;
|
||||
|
||||
pub(crate) use hide_lines::{hide_lines, wrap_rust_main};
|
||||
pub(crate) use print::render_print_page;
|
||||
pub(crate) use serialize::serialize;
|
||||
pub(crate) use tree::{Element, Node};
|
||||
|
||||
/// Options for converting a single chapter's markdown to HTML.
|
||||
pub(crate) struct HtmlRenderOptions<'a> {
|
||||
/// Options for parsing markdown.
|
||||
pub markdown_options: MarkdownOptions,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: &'a Path,
|
||||
/// The default Rust edition, used to set the proper class on the code blocks.
|
||||
pub edition: Option<RustEdition>,
|
||||
/// The [`HtmlConfig`], whose options affect how the HTML is generated.
|
||||
pub config: &'a HtmlConfig,
|
||||
}
|
||||
|
||||
impl<'a> HtmlRenderOptions<'a> {
|
||||
/// Creates a new [`HtmlRenderOptions`].
|
||||
pub(crate) fn new(
|
||||
path: &'a Path,
|
||||
config: &'a HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
) -> HtmlRenderOptions<'a> {
|
||||
let mut markdown_options = MarkdownOptions::default();
|
||||
markdown_options.smart_punctuation = config.smart_punctuation;
|
||||
markdown_options.definition_lists = config.definition_lists;
|
||||
markdown_options.admonitions = config.admonitions;
|
||||
HtmlRenderOptions {
|
||||
markdown_options,
|
||||
path,
|
||||
edition,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders markdown to HTML.
|
||||
pub(crate) fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String {
|
||||
let tree = build_tree(text, options);
|
||||
let mut output = String::new();
|
||||
serialize::serialize(&tree, &mut output);
|
||||
output
|
||||
}
|
||||
|
||||
/// Renders markdown to a [`Tree`].
|
||||
fn build_tree(text: &str, options: &HtmlRenderOptions<'_>) -> Tree<Node> {
|
||||
let events = new_cmark_parser(text, &options.markdown_options);
|
||||
tree::MarkdownTreeBuilder::build(options, events)
|
||||
}
|
||||
|
||||
/// The parsed chapter, and some information about the chapter.
|
||||
pub(crate) struct ChapterTree<'book> {
|
||||
pub(crate) chapter: &'book Chapter,
|
||||
/// The path to the chapter relative to the root with the `.html` extension.
|
||||
pub(crate) html_path: PathBuf,
|
||||
/// The chapter tree.
|
||||
pub(crate) tree: Tree<Node>,
|
||||
}
|
||||
|
||||
/// Creates all of the [`ChapterTree`]s for the book.
|
||||
pub(crate) fn build_trees<'book>(
|
||||
book: &'book Book,
|
||||
html_config: &HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
) -> Vec<ChapterTree<'book>> {
|
||||
book.chapters()
|
||||
.map(|ch| {
|
||||
let path = ch.path.as_ref().unwrap();
|
||||
let html_path = ch.path.as_ref().unwrap().with_extension("html");
|
||||
let options = HtmlRenderOptions::new(path, html_config, edition);
|
||||
let tree = build_tree(&ch.content, &options);
|
||||
|
||||
ChapterTree {
|
||||
chapter: ch,
|
||||
html_path,
|
||||
tree,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
215
crates/mdbook-html/src/html/print.rs
Normal file
215
crates/mdbook-html/src/html/print.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
//! Support for generating the print page.
|
||||
//!
|
||||
//! The print page takes all the individual chapters (as `Tree<Node>`
|
||||
//! elements) and modifies the chapters so that they work on a consolidated
|
||||
//! print page, and then serializes it all as one HTML page.
|
||||
|
||||
use super::Node;
|
||||
use crate::html::{ChapterTree, Element, serialize};
|
||||
use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id};
|
||||
use mdbook_core::static_regex;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Takes all the chapter trees, modifies them to be suitable to render for
|
||||
/// the print page, and returns an string of all the chapters rendered to a
|
||||
/// single HTML page.
|
||||
pub(crate) fn render_print_page(mut chapter_trees: Vec<ChapterTree<'_>>) -> String {
|
||||
let (id_remap, mut id_counter) = make_ids_unique(&mut chapter_trees);
|
||||
let path_to_root_id = make_root_id_map(&mut chapter_trees, &mut id_counter);
|
||||
rewrite_links(&mut chapter_trees, &id_remap, &path_to_root_id);
|
||||
|
||||
let mut print_content = String::new();
|
||||
for ChapterTree { tree, .. } in chapter_trees {
|
||||
if !print_content.is_empty() {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
// Add both two CSS properties because of the compatibility issue
|
||||
print_content
|
||||
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
|
||||
}
|
||||
serialize(&tree, &mut print_content);
|
||||
}
|
||||
print_content
|
||||
}
|
||||
|
||||
/// Make all IDs unique, and create a map from old to new IDs.
|
||||
///
|
||||
/// The first map is a map of the chapter path to the IDs that were rewritten
|
||||
/// in that chapter (old ID to new ID).
|
||||
///
|
||||
/// The second map is a map of every ID seen to the number of times it has
|
||||
/// been seen. This is used to generate unique IDs.
|
||||
fn make_ids_unique(
|
||||
chapter_trees: &mut [ChapterTree<'_>],
|
||||
) -> (HashMap<PathBuf, HashMap<String, String>>, HashSet<String>) {
|
||||
let mut id_remap = HashMap::new();
|
||||
let mut id_counter = HashSet::new();
|
||||
for ChapterTree {
|
||||
html_path, tree, ..
|
||||
} in chapter_trees
|
||||
{
|
||||
for value in tree.values_mut() {
|
||||
if let Node::Element(el) = value
|
||||
&& let Some(id) = el.attr("id")
|
||||
{
|
||||
let new_id = unique_id(id, &mut id_counter);
|
||||
if new_id != id {
|
||||
let id = id.to_string();
|
||||
el.insert_attr("id", new_id.clone().into());
|
||||
|
||||
let map: &mut HashMap<_, _> = id_remap.entry(html_path.clone()).or_default();
|
||||
map.insert(id, new_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(id_remap, id_counter)
|
||||
}
|
||||
|
||||
/// Generates a map of a chapter path to the ID of the top of the chapter.
|
||||
///
|
||||
/// If a chapter is missing an `h1` tag, then one is synthesized so that the
|
||||
/// print output has something to link to.
|
||||
fn make_root_id_map(
|
||||
chapter_trees: &mut [ChapterTree<'_>],
|
||||
id_counter: &mut HashSet<String>,
|
||||
) -> HashMap<PathBuf, String> {
|
||||
let mut path_to_root_id = HashMap::new();
|
||||
for ChapterTree {
|
||||
chapter,
|
||||
html_path,
|
||||
tree,
|
||||
..
|
||||
} in chapter_trees
|
||||
{
|
||||
let mut h1_found = false;
|
||||
for value in tree.values_mut() {
|
||||
if let Node::Element(el) = value {
|
||||
if el.name() == "h1" {
|
||||
if let Some(id) = el.attr("id") {
|
||||
h1_found = true;
|
||||
path_to_root_id.insert(html_path.clone(), id.to_string());
|
||||
}
|
||||
break;
|
||||
} else if matches!(el.name(), "h2" | "h3" | "h4" | "h5" | "h6") {
|
||||
// h1 not found.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !h1_found {
|
||||
// Synthesize a root id to be able to link to the start of the page.
|
||||
// TODO: This might want to be a warning? Chapters generally
|
||||
// should start with an h1.
|
||||
let mut h1 = Element::new("h1");
|
||||
let id = id_from_content(&chapter.name);
|
||||
let id = unique_id(&id, id_counter);
|
||||
h1.insert_attr("id", id.clone().into());
|
||||
let mut root = tree.root_mut();
|
||||
let mut h1 = root.prepend(Node::Element(h1));
|
||||
let mut a = Element::new("a");
|
||||
a.insert_attr("href", format!("#{id}").into());
|
||||
a.insert_attr("class", "header".into());
|
||||
let mut a = h1.append(Node::Element(a));
|
||||
a.append(Node::Text(chapter.name.clone().into()));
|
||||
path_to_root_id.insert(html_path.clone(), id);
|
||||
}
|
||||
}
|
||||
|
||||
path_to_root_id
|
||||
}
|
||||
|
||||
/// Rewrite links so that they point to IDs on the print page.
|
||||
fn rewrite_links(
|
||||
chapter_trees: &mut [ChapterTree<'_>],
|
||||
id_remap: &HashMap<PathBuf, HashMap<String, String>>,
|
||||
path_to_root_id: &HashMap<PathBuf, String>,
|
||||
) {
|
||||
static_regex!(
|
||||
LINK,
|
||||
r"(?x)
|
||||
(?P<scheme>^[a-z][a-z0-9+.-]*:)?
|
||||
(?P<path>[^\#]+)?
|
||||
(?:\#(?P<anchor>.*))?"
|
||||
);
|
||||
|
||||
// Rewrite path links to go to the appropriate place.
|
||||
for ChapterTree {
|
||||
html_path, tree, ..
|
||||
} in chapter_trees
|
||||
{
|
||||
let base = html_path.parent().expect("path can't be empty");
|
||||
|
||||
for value in tree.values_mut() {
|
||||
let Node::Element(el) = value else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(el.name(), "a" | "img") {
|
||||
continue;
|
||||
}
|
||||
for attr in ["href", "src", "xlink:href"] {
|
||||
let Some(dest) = el.attr(attr) else {
|
||||
continue;
|
||||
};
|
||||
let Some(caps) = LINK.captures(&dest) else {
|
||||
continue;
|
||||
};
|
||||
if caps.name("scheme").is_some() {
|
||||
continue;
|
||||
}
|
||||
// The lookup_key is the key to look up in the remap table.
|
||||
let mut lookup_key = html_path.clone();
|
||||
if let Some(href_path) = caps.name("path")
|
||||
&& let href_path = href_path.as_str()
|
||||
&& !href_path.is_empty()
|
||||
{
|
||||
lookup_key.pop();
|
||||
lookup_key.push(href_path);
|
||||
lookup_key = normalize_path(&lookup_key);
|
||||
let is_a_chapter = path_to_root_id.contains_key(&lookup_key);
|
||||
if !is_a_chapter {
|
||||
// Make the link relative to the print page location.
|
||||
let mut rel_path = normalize_path(&base.join(href_path)).to_url_path();
|
||||
if let Some(anchor) = caps.name("anchor") {
|
||||
rel_path.push('#');
|
||||
rel_path.push_str(anchor.as_str());
|
||||
}
|
||||
el.insert_attr(attr, rel_path.into());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let id = match caps.name("anchor") {
|
||||
Some(anchor_id) => {
|
||||
let anchor_id = anchor_id.as_str().to_string();
|
||||
match id_remap.get(&lookup_key) {
|
||||
Some(id_map) => match id_map.get(&anchor_id) {
|
||||
Some(new_id) => new_id.clone(),
|
||||
None => anchor_id,
|
||||
},
|
||||
None => {
|
||||
// Assume the anchor goes to some non-remapped
|
||||
// ID that already exists.
|
||||
anchor_id
|
||||
}
|
||||
}
|
||||
}
|
||||
None => match path_to_root_id.get(&lookup_key) {
|
||||
Some(id) => id.to_string(),
|
||||
None => {
|
||||
// This should be guaranteed that either the
|
||||
// chapter itself is in the map (for anchor-only
|
||||
// links), or the is_a_chapter check above.
|
||||
panic!(
|
||||
"internal error: expected `{lookup_key:?}` to be in \
|
||||
root map (chapter path is `{html_path:?}`)"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
el.insert_attr(attr, format!("#{id}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
crates/mdbook-html/src/html/serialize.rs
Normal file
112
crates/mdbook-html/src/html/serialize.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Serializes the [`Node`] tree to an HTML string.
|
||||
|
||||
use super::tree::is_void_element;
|
||||
use super::tree::{Element, Node};
|
||||
use ego_tree::{Tree, iter::Edge};
|
||||
use html5ever::{local_name, ns};
|
||||
use mdbook_core::utils::{escape_html, escape_html_attribute};
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Serializes the given tree of [`Node`] elements to an HTML string.
|
||||
pub(crate) fn serialize(tree: &Tree<Node>, output: &mut String) {
|
||||
for edge in tree.root().traverse() {
|
||||
match edge {
|
||||
Edge::Open(node) => match node.value() {
|
||||
Node::Element(el) => serialize_start(el, output),
|
||||
Node::Text(text) => {
|
||||
output.push_str(&escape_html(text));
|
||||
}
|
||||
Node::Comment(comment) => {
|
||||
output.push_str("<!--");
|
||||
output.push_str(comment);
|
||||
output.push_str("-->");
|
||||
}
|
||||
Node::Fragment => {}
|
||||
Node::RawData(html) => {
|
||||
output.push_str(html);
|
||||
}
|
||||
},
|
||||
Edge::Close(node) => {
|
||||
if let Node::Element(el) = node.value() {
|
||||
serialize_end(el, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this HTML element wants a newline to keep the emitted
|
||||
/// output more readable.
|
||||
fn wants_pretty_html_newline(name: &str) -> bool {
|
||||
matches!(name, |"blockquote"| "dd"
|
||||
| "div"
|
||||
| "dl"
|
||||
| "dt"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6"
|
||||
| "hr"
|
||||
| "li"
|
||||
| "ol"
|
||||
| "p"
|
||||
| "pre"
|
||||
| "table"
|
||||
| "tbody"
|
||||
| "thead"
|
||||
| "tr"
|
||||
| "ul")
|
||||
}
|
||||
|
||||
/// Emit the start tag of an element.
|
||||
fn serialize_start(el: &Element, output: &mut String) {
|
||||
let el_name = el.name();
|
||||
if wants_pretty_html_newline(el_name) {
|
||||
if !output.is_empty() {
|
||||
if !output.ends_with('\n') {
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push('<');
|
||||
output.push_str(el_name);
|
||||
for (attr_name, value) in &el.attrs {
|
||||
output.push(' ');
|
||||
match attr_name.ns {
|
||||
ns!() => (),
|
||||
ns!(xml) => output.push_str("xml:"),
|
||||
ns!(xmlns) => {
|
||||
if el.name.local != local_name!("xmlns") {
|
||||
output.push_str("xmlns:");
|
||||
}
|
||||
}
|
||||
ns!(xlink) => output.push_str("xlink:"),
|
||||
_ => (), // TODO what should it do here?
|
||||
}
|
||||
output.push_str(attr_name.local.deref());
|
||||
output.push_str("=\"");
|
||||
output.push_str(&escape_html_attribute(&value));
|
||||
output.push('"');
|
||||
}
|
||||
if el.self_closing {
|
||||
output.push_str(" /");
|
||||
}
|
||||
output.push('>');
|
||||
}
|
||||
|
||||
/// Emit the end tag of an element.
|
||||
fn serialize_end(el: &Element, output: &mut String) {
|
||||
// Void elements do not have an end tag.
|
||||
if el.self_closing || is_void_element(el.name()) {
|
||||
return;
|
||||
}
|
||||
let name = el.name();
|
||||
output.push_str("</");
|
||||
output.push_str(name);
|
||||
output.push('>');
|
||||
if wants_pretty_html_newline(name) {
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
53
crates/mdbook-html/src/html/tests.rs
Normal file
53
crates/mdbook-html/src/html/tests.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::html::tokenizer::parse_html;
|
||||
use html5ever::tokenizer::{Tag, TagKind, Token};
|
||||
|
||||
// Basic tokenizer behavior of a script.
|
||||
#[test]
|
||||
fn parse_html_script() {
|
||||
let script = r#"
|
||||
if (3 < 5 > 10)
|
||||
{
|
||||
alert("The sky is falling!");
|
||||
}
|
||||
"#;
|
||||
let t = format!("<script>{script}</script>");
|
||||
let ts = parse_html(&t);
|
||||
eprintln!("{ts:#?}",);
|
||||
let mut output = String::new();
|
||||
let mut in_script = false;
|
||||
for t in ts {
|
||||
match t {
|
||||
Token::ParseError(e) => panic!("{e:?}"),
|
||||
Token::CharacterTokens(s) => {
|
||||
if in_script {
|
||||
output.push_str(&s)
|
||||
}
|
||||
}
|
||||
Token::TagToken(Tag {
|
||||
kind: TagKind::StartTag,
|
||||
..
|
||||
}) => in_script = true,
|
||||
Token::TagToken(Tag {
|
||||
kind: TagKind::EndTag,
|
||||
..
|
||||
}) => in_script = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(output, script);
|
||||
}
|
||||
|
||||
// What happens if a script doesn't end.
|
||||
#[test]
|
||||
fn parse_html_script_unclosed() {
|
||||
let t = r#"<script>
|
||||
// Test
|
||||
"#;
|
||||
let ts = parse_html(t);
|
||||
eprintln!("{ts:#?}",);
|
||||
for t in ts {
|
||||
if let Token::ParseError(e) = t {
|
||||
panic!("{e:?}",);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
crates/mdbook-html/src/html/tokenizer.rs
Normal file
83
crates/mdbook-html/src/html/tokenizer.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Support for parsing HTML.
|
||||
//!
|
||||
//! The primary entry point is [`parse_html`] which uses [`html5ever`] to
|
||||
//! tokenize the input.
|
||||
|
||||
use html5ever::TokenizerResult;
|
||||
use html5ever::tendril::ByteTendril;
|
||||
use html5ever::tokenizer::states::RawKind;
|
||||
use html5ever::tokenizer::{
|
||||
BufferQueue, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// Collector for HTML tokens.
|
||||
#[derive(Default)]
|
||||
struct TokenCollector {
|
||||
/// Parsed HTML tokens.
|
||||
tokens: RefCell<Vec<Token>>,
|
||||
}
|
||||
|
||||
impl TokenSink for TokenCollector {
|
||||
type Handle = ();
|
||||
|
||||
fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
|
||||
match &token {
|
||||
Token::DoctypeToken(_) => {}
|
||||
Token::TagToken(tag) => {
|
||||
let tag_name = tag.name.as_bytes();
|
||||
// TODO: This could probably use special support for SVG and MathML.
|
||||
if tag_name == b"script" {
|
||||
match tag.kind {
|
||||
TagKind::StartTag => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
return TokenSinkResult::RawData(RawKind::ScriptData);
|
||||
}
|
||||
TagKind::EndTag => {}
|
||||
}
|
||||
}
|
||||
if tag_name == b"style" {
|
||||
match tag.kind {
|
||||
TagKind::StartTag => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
return TokenSinkResult::RawData(RawKind::Rawtext);
|
||||
}
|
||||
TagKind::EndTag => {}
|
||||
}
|
||||
}
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
Token::CommentToken(_) => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
Token::CharacterTokens(_) => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
Token::NullCharacterToken => {}
|
||||
Token::EOFToken => {}
|
||||
Token::ParseError(_) => {
|
||||
self.tokens.borrow_mut().push(token);
|
||||
}
|
||||
}
|
||||
TokenSinkResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse HTML into tokens.
|
||||
pub(crate) fn parse_html(html: &str) -> Vec<Token> {
|
||||
let tendril: ByteTendril = html.as_bytes().into();
|
||||
let mut queue = BufferQueue::default();
|
||||
queue.push_back(tendril.try_reinterpret().unwrap());
|
||||
|
||||
let collector = TokenCollector::default();
|
||||
let tok = Tokenizer::new(collector, TokenizerOpts::default());
|
||||
let result = tok.feed(&mut queue);
|
||||
assert_eq!(result, TokenizerResult::Done);
|
||||
assert!(
|
||||
queue.is_empty(),
|
||||
"queue wasn't empty: {:?}",
|
||||
queue.pop_front()
|
||||
);
|
||||
tok.end();
|
||||
tok.sink.tokens.take()
|
||||
}
|
||||
1155
crates/mdbook-html/src/html/tree.rs
Normal file
1155
crates/mdbook-html/src/html/tree.rs
Normal file
File diff suppressed because it is too large
Load Diff
696
crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
Normal file
696
crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
Normal file
@@ -0,0 +1,696 @@
|
||||
use super::helpers;
|
||||
use super::static_files::StaticFiles;
|
||||
use crate::html::ChapterTree;
|
||||
use crate::html::{build_trees, render_markdown, serialize};
|
||||
use crate::theme::Theme;
|
||||
use crate::utils::ToUrlPath;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use handlebars::Handlebars;
|
||||
use mdbook_core::book::{Book, BookItem, Chapter};
|
||||
use mdbook_core::config::{BookConfig, Config, HtmlConfig};
|
||||
use mdbook_core::utils::fs;
|
||||
use mdbook_renderer::{RenderContext, Renderer};
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::error;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
/// The HTML renderer for mdBook.
|
||||
#[derive(Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
impl HtmlHandlebars {
|
||||
/// Returns a new instance of [`HtmlHandlebars`].
|
||||
pub fn new() -> Self {
|
||||
HtmlHandlebars
|
||||
}
|
||||
|
||||
fn render_chapter(
|
||||
&self,
|
||||
chapter_tree: &ChapterTree<'_>,
|
||||
prev_ch: Option<&Chapter>,
|
||||
next_ch: Option<&Chapter>,
|
||||
mut ctx: RenderChapterContext<'_>,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
let ch = chapter_tree.chapter;
|
||||
|
||||
let path = ch.path.as_ref().unwrap();
|
||||
// "print.html" is used for the print page.
|
||||
if path == Path::new("print.md") {
|
||||
bail!("{} is reserved for internal use", path.display());
|
||||
};
|
||||
|
||||
if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
|
||||
let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
|
||||
+ "/"
|
||||
+ ch.source_path
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
let edit_url = edit_url_template.replace("{path}", &full_path);
|
||||
ctx.data
|
||||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
serialize(&chapter_tree.tree, &mut content);
|
||||
|
||||
let ctx_path = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ctx_path).with_extension("html");
|
||||
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
|
||||
let title = if let Some(title) = ctx.chapter_titles.get(path) {
|
||||
title.clone()
|
||||
} else if book_title.is_empty() {
|
||||
ch.name.clone()
|
||||
} else {
|
||||
ch.name.clone() + " - " + book_title
|
||||
};
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data
|
||||
.insert("path_to_root".to_owned(), json!(fs::path_to_root(path)));
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
let redirects = collect_redirects_for_path(&filepath, &ctx.html_config.redirect)?;
|
||||
if !redirects.is_empty() {
|
||||
ctx.data.insert(
|
||||
"fragment_map".to_owned(),
|
||||
json!(serde_json::to_string(&redirects)?),
|
||||
);
|
||||
}
|
||||
|
||||
let mut nav = |name: &str, ch: Option<&Chapter>| {
|
||||
let Some(ch) = ch else { return };
|
||||
let path = ch
|
||||
.path
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.with_extension("html")
|
||||
.to_url_path();
|
||||
let obj = json!( {
|
||||
"title": ch.name,
|
||||
"link": path,
|
||||
});
|
||||
ctx.data.insert(name.to_string(), obj);
|
||||
};
|
||||
nav("previous", prev_ch);
|
||||
nav("next", next_ch);
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
// Write to file
|
||||
let out_path = ctx.destination.join(filepath);
|
||||
fs::write(&out_path, rendered)?;
|
||||
|
||||
if prev_ch.is_none() {
|
||||
ctx.data.insert("path".to_owned(), json!("index.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
ctx.data.insert("is_index".to_owned(), json!(true));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
debug!("Creating index.html from {}", ctx_path);
|
||||
fs::write(ctx.destination.join("index.html"), rendered_index)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_404(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
html_config: &HtmlConfig,
|
||||
src_dir: &Path,
|
||||
handlebars: &mut Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||
let path = src_dir.join(filename);
|
||||
fs::read_to_string(&path).with_context(|| "failed to read the 404 input file")?
|
||||
} else {
|
||||
// 404 input not explicitly configured try the default file 404.md
|
||||
let default_404_location = src_dir.join("404.md");
|
||||
if default_404_location.exists() {
|
||||
fs::read_to_string(&default_404_location)
|
||||
.with_context(|| "failed to read the 404 input file")?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
navigation bar or search to continue."
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
let options = crate::html::HtmlRenderOptions::new(
|
||||
Path::new("404.md"),
|
||||
html_config,
|
||||
ctx.config.rust.edition,
|
||||
);
|
||||
let html_content_404 = render_markdown(&content_404, &options);
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
site_url
|
||||
} else {
|
||||
debug!(
|
||||
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
|
||||
this to ensure the 404 page work correctly, especially if your site is hosted in a \
|
||||
subdirectory on the HTTP server."
|
||||
);
|
||||
"/"
|
||||
};
|
||||
data_404.insert("base_url".to_owned(), json!(base_url));
|
||||
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
||||
data_404.insert("path".to_owned(), json!("404.md"));
|
||||
data_404.insert("content".to_owned(), json!(html_content_404));
|
||||
|
||||
let mut title = String::from("Page not found");
|
||||
if let Some(book_title) = &ctx.config.book.title {
|
||||
title.push_str(" - ");
|
||||
title.push_str(book_title);
|
||||
}
|
||||
data_404.insert("title".to_owned(), json!(title));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let output_file = ctx.destination.join(html_config.get_404_output_file());
|
||||
fs::write(output_file, rendered)?;
|
||||
debug!("Creating 404.html ✓");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_print_page(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
handlebars: &Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
chapter_trees: Vec<ChapterTree<'_>>,
|
||||
) -> Result<String> {
|
||||
let print_content = crate::html::render_print_page(chapter_trees);
|
||||
|
||||
if let Some(ref title) = ctx.config.book.title {
|
||||
data.insert("title".to_owned(), json!(title));
|
||||
} else {
|
||||
// Make sure that the Print chapter does not display the title from
|
||||
// the last rendered chapter by removing it from its context
|
||||
data.remove("title");
|
||||
}
|
||||
data.insert("is_print".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("print.md"));
|
||||
data.insert("content".to_owned(), json!(print_content));
|
||||
data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(fs::path_to_root(Path::new("print.md"))),
|
||||
);
|
||||
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
|
||||
handlebars.register_helper(
|
||||
"toc",
|
||||
Box::new(helpers::toc::RenderToc {
|
||||
no_section_label: html_config.no_section_label,
|
||||
}),
|
||||
);
|
||||
handlebars.register_helper("fa", Box::new(helpers::fontawesome::fa_helper));
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
handlebars: &Handlebars<'_>,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if redirects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Emitting redirects");
|
||||
let redirects = combine_fragment_redirects(redirects);
|
||||
|
||||
for (original, (dest, fragment_map)) in redirects {
|
||||
// Note: all paths are relative to the build directory, so the
|
||||
// leading slash in an absolute path means nothing (and would mess
|
||||
// up `root.join(original)`).
|
||||
let original = original.trim_start_matches('/');
|
||||
let filename = root.join(original);
|
||||
if filename.exists() {
|
||||
// This redirect is handled by the in-page fragment mapper.
|
||||
continue;
|
||||
}
|
||||
if dest.is_empty() {
|
||||
bail!(
|
||||
"redirect entry for `{original}` only has source paths with `#` fragments\n\
|
||||
There must be an entry without the `#` fragment to determine the default \
|
||||
destination."
|
||||
);
|
||||
}
|
||||
debug!("Redirecting \"{}\" → \"{}\"", original, dest);
|
||||
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirect(
|
||||
&self,
|
||||
handlebars: &Handlebars<'_>,
|
||||
original: &Path,
|
||||
destination: &str,
|
||||
fragment_map: &BTreeMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if let Some(parent) = original.parent() {
|
||||
fs::create_dir_all(parent)?
|
||||
}
|
||||
|
||||
let js_map = serde_json::to_string(fragment_map)?;
|
||||
|
||||
let ctx = json!({
|
||||
"fragment_map": js_map,
|
||||
"url": destination,
|
||||
});
|
||||
let rendered = handlebars.render("redirect", &ctx).with_context(|| {
|
||||
format!(
|
||||
"Unable to create a redirect file at `{}`",
|
||||
original.display()
|
||||
)
|
||||
})?;
|
||||
fs::write(original, rendered)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let book_config = &ctx.config.book;
|
||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
|
||||
|
||||
if destination.exists() {
|
||||
fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale HTML output")?;
|
||||
}
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => {
|
||||
let dir = ctx.root.join(theme);
|
||||
if !dir.is_dir() {
|
||||
bail!("theme dir {} does not exist", dir.display());
|
||||
}
|
||||
dir
|
||||
}
|
||||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
let theme = Theme::new(theme_dir);
|
||||
|
||||
debug!("Register the index handlebars template");
|
||||
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
|
||||
|
||||
debug!("Register the head handlebars template");
|
||||
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
|
||||
|
||||
debug!("Register the redirect handlebars template");
|
||||
handlebars
|
||||
.register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
|
||||
|
||||
debug!("Register the header handlebars template");
|
||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||
|
||||
debug!("Register the toc handlebars template");
|
||||
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
|
||||
handlebars
|
||||
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;
|
||||
|
||||
debug!("Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||
|
||||
let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
|
||||
|
||||
let chapter_trees = build_trees(book, &html_config, ctx.config.rust.edition);
|
||||
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
{
|
||||
let default = mdbook_core::config::Search::default();
|
||||
let search = html_config.search.as_ref().unwrap_or(&default);
|
||||
if search.enable {
|
||||
super::search::create_files(&search, &mut static_files, &chapter_trees)?;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Render toc js");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
|
||||
debug!("Creating toc.js ✓");
|
||||
}
|
||||
|
||||
if html_config.hash_files {
|
||||
static_files.hash_files()?;
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
let resource_helper = static_files
|
||||
.write_files(&destination)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
|
||||
handlebars.register_helper("resource", Box::new(resource_helper));
|
||||
|
||||
debug!("Render toc html");
|
||||
{
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("toc.html"));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
fs::write(destination.join("toc.html"), rendered_toc)?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("path");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
fs::write(
|
||||
destination.join(".nojekyll"),
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
fs::write(destination.join("CNAME"), format!("{cname}\n"))?;
|
||||
}
|
||||
|
||||
for (i, chapter_tree) in chapter_trees.iter().enumerate() {
|
||||
let previous = (i != 0).then(|| chapter_trees[i - 1].chapter);
|
||||
let next = (i != chapter_trees.len() - 1).then(|| chapter_trees[i + 1].chapter);
|
||||
let ctx = RenderChapterContext {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
book_config: book_config.clone(),
|
||||
html_config: html_config.clone(),
|
||||
chapter_titles: &ctx.chapter_titles,
|
||||
};
|
||||
self.render_chapter(chapter_tree, previous, next, ctx)?;
|
||||
}
|
||||
|
||||
// Render 404 page
|
||||
if html_config.input_404 != Some("".to_string()) {
|
||||
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
|
||||
}
|
||||
|
||||
// Render the print version.
|
||||
if html_config.print.enable {
|
||||
let print_rendered =
|
||||
self.render_print_page(ctx, &handlebars, &mut data, chapter_trees)?;
|
||||
|
||||
fs::write(destination.join("print.html"), print_rendered)?;
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
// Copy all remaining files, avoid a recursive copy from/to the book build dir
|
||||
fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
|
||||
|
||||
info!("HTML book written to `{}`", destination.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(
|
||||
root: &Path,
|
||||
book: &Book,
|
||||
config: &Config,
|
||||
html_config: &HtmlConfig,
|
||||
theme: &Theme,
|
||||
) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
trace!("make_data");
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert(
|
||||
"language".to_owned(),
|
||||
json!(config.book.language.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"text_direction".to_owned(),
|
||||
json!(config.book.realized_text_direction()),
|
||||
);
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"description".to_owned(),
|
||||
json!(config.book.description.clone().unwrap_or_default()),
|
||||
);
|
||||
if theme.favicon_png.is_some() {
|
||||
data.insert("favicon_png".to_owned(), json!("favicon.png"));
|
||||
}
|
||||
if theme.favicon_svg.is_some() {
|
||||
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
|
||||
}
|
||||
if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
|
||||
data.insert(
|
||||
"live_reload_endpoint".to_owned(),
|
||||
json!(live_reload_endpoint),
|
||||
);
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "light".to_string(),
|
||||
};
|
||||
data.insert("default_theme".to_owned(), json!(default_theme));
|
||||
|
||||
let preferred_dark_theme = match html_config.preferred_dark_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "navy".to_string(),
|
||||
};
|
||||
data.insert(
|
||||
"preferred_dark_theme".to_owned(),
|
||||
json!(preferred_dark_theme),
|
||||
);
|
||||
|
||||
if html_config.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html_config.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html_config.additional_css {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_css".to_owned(), json!(css));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html_config.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html_config.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html_config.playground.editable && html_config.playground.copy_js {
|
||||
data.insert("playground_js".to_owned(), json!(true));
|
||||
if html_config.playground.line_numbers {
|
||||
data.insert("playground_line_numbers".to_owned(), json!(true));
|
||||
}
|
||||
}
|
||||
if html_config.playground.copyable {
|
||||
data.insert("playground_copyable".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
||||
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
||||
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
||||
data.insert(
|
||||
"sidebar_header_nav".to_owned(),
|
||||
json!(html_config.sidebar_header_nav),
|
||||
);
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
let search = search.unwrap_or_default();
|
||||
data.insert("search_enabled".to_owned(), json!(search.enable));
|
||||
data.insert(
|
||||
"search_js".to_owned(),
|
||||
json!(search.enable && search.copy_js),
|
||||
);
|
||||
} else if search.is_some() {
|
||||
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
|
||||
warn!(
|
||||
"please reinstall with `cargo install mdbook --force --features search`to use the \
|
||||
search feature"
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(ref git_repository_url) = html_config.git_repository_url {
|
||||
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
|
||||
}
|
||||
|
||||
let git_repository_icon = match html_config.git_repository_icon {
|
||||
Some(ref git_repository_icon) => git_repository_icon,
|
||||
None => "fab-github",
|
||||
};
|
||||
let git_repository_icon_class = match git_repository_icon.split('-').next() {
|
||||
Some("fa") => "regular",
|
||||
Some("fas") => "solid",
|
||||
Some("fab") => "brands",
|
||||
_ => "regular",
|
||||
};
|
||||
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
||||
data.insert(
|
||||
"git_repository_icon_class".to_owned(),
|
||||
json!(git_repository_icon_class),
|
||||
);
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
// Create the data to inject in the template
|
||||
let mut chapter = BTreeMap::new();
|
||||
|
||||
match *item {
|
||||
BookItem::PartTitle(ref title) => {
|
||||
chapter.insert("part".to_owned(), json!(title));
|
||||
}
|
||||
BookItem::Chapter(ref ch) => {
|
||||
if let Some(ref section) = ch.number {
|
||||
chapter.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
chapter.insert(
|
||||
"has_sub_items".to_owned(),
|
||||
json!((!ch.sub_items.is_empty()).to_string()),
|
||||
);
|
||||
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
if let Some(ref path) = ch.path {
|
||||
let p = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(p));
|
||||
}
|
||||
}
|
||||
BookItem::Separator => {
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
}
|
||||
}
|
||||
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
data.insert("chapters".to_owned(), json!(chapters));
|
||||
|
||||
debug!("[*]: JSON constructed");
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
struct RenderChapterContext<'a> {
|
||||
handlebars: &'a Handlebars<'a>,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
book_config: BookConfig,
|
||||
html_config: HtmlConfig,
|
||||
chapter_titles: &'a HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
/// Redirect mapping.
|
||||
///
|
||||
/// The key is the source path (like `foo/bar.html`). The value is a tuple
|
||||
/// `(destination_path, fragment_map)`. The `destination_path` is the page to
|
||||
/// redirect to. `fragment_map` is the map of fragments that override the
|
||||
/// destination. For example, a fragment `#foo` could redirect to any other
|
||||
/// page or site.
|
||||
type CombinedRedirects = BTreeMap<String, (String, BTreeMap<String, String>)>;
|
||||
fn combine_fragment_redirects(redirects: &HashMap<String, String>) -> CombinedRedirects {
|
||||
let mut combined: CombinedRedirects = BTreeMap::new();
|
||||
// This needs to extract the fragments to generate the fragment map.
|
||||
for (original, new) in redirects {
|
||||
if let Some((source_path, source_fragment)) = original.rsplit_once('#') {
|
||||
let e = combined.entry(source_path.to_string()).or_default();
|
||||
if let Some(old) = e.1.insert(format!("#{source_fragment}"), new.clone()) {
|
||||
error!(
|
||||
"internal error: found duplicate fragment redirect \
|
||||
{old} for {source_path}#{source_fragment}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let e = combined.entry(original.to_string()).or_default();
|
||||
e.0 = new.clone();
|
||||
}
|
||||
}
|
||||
combined
|
||||
}
|
||||
|
||||
/// Collects fragment redirects for an existing page.
|
||||
///
|
||||
/// The returned map has keys like `#foo` and the value is the new destination
|
||||
/// path or URL.
|
||||
fn collect_redirects_for_path(
|
||||
path: &Path,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<BTreeMap<String, String>> {
|
||||
let path = format!("/{}", path.to_url_path());
|
||||
if redirects.contains_key(&path) {
|
||||
bail!(
|
||||
"redirect found for existing chapter at `{path}`\n\
|
||||
Either delete the redirect or remove the chapter."
|
||||
);
|
||||
}
|
||||
|
||||
let key_prefix = format!("{path}#");
|
||||
let map = redirects
|
||||
.iter()
|
||||
.filter_map(|(source, dest)| {
|
||||
source
|
||||
.strip_prefix(&key_prefix)
|
||||
.map(|fragment| (format!("#{fragment}"), dest.to_string()))
|
||||
})
|
||||
.collect();
|
||||
Ok(map)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use font_awesome_as_a_crate as fa;
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use tracing::trace;
|
||||
|
||||
pub(crate) fn fa_helper(
|
||||
h: &Helper<'_>,
|
||||
_r: &Handlebars<'_>,
|
||||
_ctx: &Context,
|
||||
_rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("fa_helper (handlebars helper)");
|
||||
|
||||
let type_ = h
|
||||
.param(0)
|
||||
.and_then(|v| v.value().as_str())
|
||||
.and_then(|v| fa::Type::from_str(v).ok())
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 0 with String type is required for fontawesome helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let name = h.param(1).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderErrorReason::Other(
|
||||
"Param 1 with String type is required for fontawesome helper.".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
trace!("fa_helper: {} {}", type_, name);
|
||||
|
||||
let name = name
|
||||
.strip_prefix("fa-")
|
||||
.or_else(|| name.strip_prefix("fab-"))
|
||||
.or_else(|| name.strip_prefix("fas-"))
|
||||
.unwrap_or(name);
|
||||
|
||||
if let Some(id) = h.param(2).and_then(|v| v.value().as_str()) {
|
||||
out.write(&format!("<span class=fa-svg id=\"{}\">", id))?;
|
||||
} else {
|
||||
out.write("<span class=fa-svg>")?;
|
||||
}
|
||||
out.write(
|
||||
fa::svg(type_, name)
|
||||
.map_err(|_| RenderErrorReason::Other(format!("Missing font {}", name)))?,
|
||||
)?;
|
||||
out.write("</span>")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user