diff --git a/.gitignore b/.gitignore index a23c771e89..db9aaa931b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ guide/book .vscode tests/dummy_book/book/ +tests/localized_book/book/ test_book/book/ # Ignore Jetbrains specific files. diff --git a/Cargo.lock b/Cargo.lock index f8ccafc38b..679966cc1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -56,24 +56,23 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" [[package]] name = "anstyle-parse" @@ -90,24 +89,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "assert_cmd" @@ -132,9 +131,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -147,15 +146,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bit-set" @@ -180,9 +173,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "block-buffer" @@ -195,9 +188,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", "regex-automata", @@ -206,9 +199,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" @@ -218,15 +211,18 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -236,53 +232,52 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "winapi", + "windows-targets", ] [[package]] name = "clap" -version = "4.3.12" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab9e8ceb9afdade1ab3f0fd8dbce5b1b2f468ad653baf10e771781b2b67b73" +checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.3.12" +version = "4.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2763db829349bf00cfc06251268865ed4363b93a943174f638daf3ecdba2cd" +checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" dependencies = [ "anstream", "anstyle", "clap_lex", - "once_cell", "strsim", "terminal_size", ] [[package]] name = "clap_complete" -version = "4.3.2" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +checksum = "8baeccdb91cd69189985f87f3c7e453a3a451ab5746cf3be6acc92120bd16d24" dependencies = [ "clap", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "colorchoice" @@ -334,6 +329,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + [[package]] name = "diff" version = "0.1.13" @@ -364,9 +365,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elasticlunr-rs" @@ -395,13 +396,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -416,23 +417,20 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.48.0", + "redox_syscall", + "windows-sys", ] [[package]] @@ -502,7 +500,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", ] [[package]] @@ -555,15 +553,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "globset" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ "aho-corasick", "bstr", @@ -574,9 +572,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -593,9 +591,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "4.3.7" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c3372087601b532857d332f5957cbae686da52bb7810bf038c3e3c3cc2fa0d" +checksum = "c39b3bc2a8f715298032cf5087e58573809374b08160aa7d750582bdb82d2683" dependencies = [ "log", "pest", @@ -613,12 +611,11 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64", "bytes", "headers-core", "http", @@ -638,9 +635,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "html5ever" @@ -686,9 +683,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -713,7 +710,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -800,26 +797,6 @@ dependencies = [ "libc", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "is-terminal" version = "0.4.9" @@ -827,15 +804,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.4", - "windows-sys 0.48.0", + "rustix", + "windows-sys", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -857,9 +834,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -867,9 +844,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -883,21 +860,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" @@ -911,9 +882,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "mac" @@ -954,8 +925,8 @@ dependencies = [ ] [[package]] -name = "mdbook" -version = "0.4.34" +name = "mdbook-spacewizards" +version = "0.4.35" dependencies = [ "ammonia", "anyhow", @@ -967,7 +938,9 @@ dependencies = [ "env_logger", "futures-util", "handlebars", + "http", "ignore", + "lazy_static", "log", "memchr", "notify", @@ -993,9 +966,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" @@ -1031,7 +1004,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1052,25 +1025,26 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "notify" -version = "6.0.1" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "crossbeam-channel", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", "walkdir", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -1085,9 +1059,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -1104,9 +1078,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -1146,9 +1120,9 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", - "windows-targets 0.48.1", + "windows-targets", ] [[package]] @@ -1159,19 +1133,20 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" dependencies = [ + "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" +checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" dependencies = [ "pest", "pest_generator", @@ -1179,22 +1154,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" +checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", ] [[package]] name = "pest_meta" -version = "2.7.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" +checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" dependencies = [ "once_cell", "pest", @@ -1241,29 +1216,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", ] [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1285,9 +1260,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", @@ -1326,9 +1301,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1346,9 +1321,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1383,15 +1358,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -1403,9 +1369,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", @@ -1415,9 +1381,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -1426,9 +1392,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rustc-demangle" @@ -1438,29 +1404,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.37.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.4" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.3", - "windows-sys 0.48.0", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -1469,7 +1421,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64", ] [[package]] @@ -1495,9 +1447,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "select" @@ -1512,35 +1464,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -1561,9 +1513,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -1572,9 +1524,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -1583,30 +1535,30 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -1618,6 +1570,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "string_cache" version = "0.8.7" @@ -1663,9 +1625,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.26" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -1674,16 +1636,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "autocfg", "cfg-if", "fastrand", - "redox_syscall 0.3.5", - "rustix 0.37.23", - "windows-sys 0.48.0", + "redox_syscall", + "rustix", + "windows-sys", ] [[package]] @@ -1699,21 +1660,21 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.37.23", - "windows-sys 0.48.0", + "rustix", + "windows-sys", ] [[package]] @@ -1724,22 +1685,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", ] [[package]] @@ -1769,20 +1730,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.4", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1793,7 +1753,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", ] [[package]] @@ -1809,9 +1769,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -1821,9 +1781,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -1883,13 +1843,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ - "base64 0.13.1", "byteorder", "bytes", + "data-encoding", "http", "httparse", "log", @@ -1902,9 +1862,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" @@ -1914,9 +1874,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -1929,9 +1889,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -1944,9 +1904,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -1982,9 +1942,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -2001,9 +1961,9 @@ dependencies = [ [[package]] name = "warp" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69" +checksum = "c1e92e22e03ff1230c03a1a8ee37d2f89cd489e2e541b7550d6afad96faed169" dependencies = [ "bytes", "futures-channel", @@ -2056,7 +2016,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -2078,7 +2038,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2107,9 +2067,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -2126,16 +2086,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets", ] [[package]] @@ -2144,122 +2095,65 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "xml5ever" diff --git a/Cargo.toml b/Cargo.toml index 37160ff623..7886109d97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ clap_complete = "4.3.2" once_cell = "1.17.1" env_logger = "0.10.0" handlebars = "4.3.7" +lazy_static = "1.0" +http = "0.2.4" log = "0.4.17" memchr = "2.5.0" opener = "0.6.1" diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index 398d7fc784..71f9bd3612 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -1,8 +1,8 @@ use crate::nop_lib::Nop; use clap::{Arg, ArgMatches, Command}; -use mdbook::book::Book; -use mdbook::errors::Error; -use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; +use mdbook_spacewizards::book::Book; +use mdbook_spacewizards::errors::Error; +use mdbook_spacewizards::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; use semver::{Version, VersionReq}; use std::io; use std::process; @@ -35,14 +35,14 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> { let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?; let book_version = Version::parse(&ctx.mdbook_version)?; - let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?; + let version_req = VersionReq::parse(mdbook_spacewizards::MDBOOK_VERSION)?; if !version_req.matches(&book_version) { eprintln!( "Warning: The {} plugin was built against version {} of mdbook, \ but we're being called from version {}", pre.name(), - mdbook::MDBOOK_VERSION, + mdbook_spacewizards::MDBOOK_VERSION, ctx.mdbook_version ); } @@ -147,7 +147,7 @@ mod nop_lib { ]"##; let input_json = input_json.as_bytes(); - let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap(); + let (ctx, book) = mdbook_spacewizards::preprocess::CmdPreprocessor::parse_input(input_json).unwrap(); let expected_book = book.clone(); let result = Nop::new().run(&ctx, book); assert!(result.is_ok()); diff --git a/guide/book.toml b/guide/book.toml index 7ef29f13bb..bf80262584 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -31,3 +31,6 @@ heading-split-level = 2 [output.html.redirect] "/format/config.html" = "configuration/index.html" + +[language.en] +name = "English" diff --git a/guide/src/404.md b/guide/src/en/404.md similarity index 100% rename from guide/src/404.md rename to guide/src/en/404.md diff --git a/guide/src/README.md b/guide/src/en/README.md similarity index 100% rename from guide/src/README.md rename to guide/src/en/README.md diff --git a/guide/src/SUMMARY.md b/guide/src/en/SUMMARY.md similarity index 95% rename from guide/src/SUMMARY.md rename to guide/src/en/SUMMARY.md index 974d65fae7..f061f30e0c 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/en/SUMMARY.md @@ -25,6 +25,7 @@ - [General](format/configuration/general.md) - [Preprocessors](format/configuration/preprocessors.md) - [Renderers](format/configuration/renderers.md) + - [Localization](format/configuration/localization.md) - [Environment Variables](format/configuration/environment-variables.md) - [Theme](format/theme/README.md) - [index.hbs](format/theme/index-hbs.md) diff --git a/guide/src/cli/README.md b/guide/src/en/cli/README.md similarity index 100% rename from guide/src/cli/README.md rename to guide/src/en/cli/README.md diff --git a/guide/src/cli/build.md b/guide/src/en/cli/build.md similarity index 100% rename from guide/src/cli/build.md rename to guide/src/en/cli/build.md diff --git a/guide/src/cli/clean.md b/guide/src/en/cli/clean.md similarity index 100% rename from guide/src/cli/clean.md rename to guide/src/en/cli/clean.md diff --git a/guide/src/cli/completions.md b/guide/src/en/cli/completions.md similarity index 100% rename from guide/src/cli/completions.md rename to guide/src/en/cli/completions.md diff --git a/guide/src/cli/init.md b/guide/src/en/cli/init.md similarity index 81% rename from guide/src/cli/init.md rename to guide/src/en/cli/init.md index 962f564c18..fd3b40fc3e 100644 --- a/guide/src/cli/init.md +++ b/guide/src/en/cli/init.md @@ -14,13 +14,19 @@ up for you: ```bash book-test/ ├── book +├── book.toml └── src - ├── chapter_1.md - └── SUMMARY.md + └── en + ├── chapter_1.md + └── SUMMARY.md ``` -- The `src` directory is where you write your book in markdown. It contains all - the source files, configuration files, etc. +- The `src` directory is where you write your book in Markdown. It contains all + the source files for each translation of your book. By default, a directory + for the English translation is created, `src/en`. + +- The `book.toml` file holds configuration about how your book gets rendered. + See the [configuration](../format/config.md) section for more details. - The `book` directory is where your book is rendered. All the output is ready to be uploaded to a server to be seen by your audience. diff --git a/guide/src/cli/serve.md b/guide/src/en/cli/serve.md similarity index 100% rename from guide/src/cli/serve.md rename to guide/src/en/cli/serve.md diff --git a/guide/src/cli/test.md b/guide/src/en/cli/test.md similarity index 100% rename from guide/src/cli/test.md rename to guide/src/en/cli/test.md diff --git a/guide/src/cli/watch.md b/guide/src/en/cli/watch.md similarity index 100% rename from guide/src/cli/watch.md rename to guide/src/en/cli/watch.md diff --git a/guide/src/continuous-integration.md b/guide/src/en/continuous-integration.md similarity index 100% rename from guide/src/continuous-integration.md rename to guide/src/en/continuous-integration.md diff --git a/guide/src/for_developers/README.md b/guide/src/en/for_developers/README.md similarity index 100% rename from guide/src/for_developers/README.md rename to guide/src/en/for_developers/README.md diff --git a/guide/src/for_developers/backends.md b/guide/src/en/for_developers/backends.md similarity index 98% rename from guide/src/for_developers/backends.md rename to guide/src/en/for_developers/backends.md index 78326a3614..b0388c1812 100644 --- a/guide/src/for_developers/backends.md +++ b/guide/src/en/for_developers/backends.md @@ -36,7 +36,7 @@ This is all the boilerplate necessary for our backend to load the book. extern crate mdbook; use std::io; -use mdbook::renderer::RenderContext; +use mdbook_spacewizards::renderer::RenderContext; fn main() { let mut stdin = io::stdin(); @@ -232,8 +232,8 @@ in [`RenderContext`]. + use std::fs::{self, File}; + use std::io::{self, Write}; - use std::io; - use mdbook::renderer::RenderContext; - use mdbook::book::{BookItem, Chapter}; + use mdbook_spacewizards::renderer::RenderContext; + use mdbook_spacewizards::book::{BookItem, Chapter}; fn main() { ... diff --git a/guide/src/for_developers/mdbook-wordcount/Cargo.toml b/guide/src/en/for_developers/mdbook-wordcount/Cargo.toml similarity index 100% rename from guide/src/for_developers/mdbook-wordcount/Cargo.toml rename to guide/src/en/for_developers/mdbook-wordcount/Cargo.toml diff --git a/guide/src/for_developers/mdbook-wordcount/src/main.rs b/guide/src/en/for_developers/mdbook-wordcount/src/main.rs similarity index 92% rename from guide/src/for_developers/mdbook-wordcount/src/main.rs rename to guide/src/en/for_developers/mdbook-wordcount/src/main.rs index 607338dd88..e55e0b8085 100644 --- a/guide/src/for_developers/mdbook-wordcount/src/main.rs +++ b/guide/src/en/for_developers/mdbook-wordcount/src/main.rs @@ -6,8 +6,8 @@ extern crate serde_derive; use std::process; use std::fs::{self, File}; use std::io::{self, Write}; -use mdbook::renderer::RenderContext; -use mdbook::book::{BookItem, Chapter}; +use mdbook_spacewizards::renderer::RenderContext; +use mdbook_spacewizards::book::{BookItem, Chapter}; fn main() { let mut stdin = io::stdin(); diff --git a/guide/src/for_developers/preprocessors.md b/guide/src/en/for_developers/preprocessors.md similarity index 99% rename from guide/src/for_developers/preprocessors.md rename to guide/src/en/for_developers/preprocessors.md index 1ac462561a..d3b16a3859 100644 --- a/guide/src/for_developers/preprocessors.md +++ b/guide/src/en/for_developers/preprocessors.md @@ -39,7 +39,7 @@ be adapted for other preprocessors. ```rust // nop-preprocessors.rs -{{#include ../../../examples/nop-preprocessor.rs}} +{{#include ../../../../examples/nop-preprocessor.rs}} ``` diff --git a/guide/src/format/README.md b/guide/src/en/format/README.md similarity index 100% rename from guide/src/format/README.md rename to guide/src/en/format/README.md diff --git a/guide/src/format/configuration/README.md b/guide/src/en/format/configuration/README.md similarity index 76% rename from guide/src/format/configuration/README.md rename to guide/src/en/format/configuration/README.md index 4dcb5852dd..c352a75803 100644 --- a/guide/src/format/configuration/README.md +++ b/guide/src/en/format/configuration/README.md @@ -4,9 +4,11 @@ This section details the configuration options available in the ***book.toml***: - **[General]** configuration including the `book`, `rust`, `build` sections - **[Preprocessor]** configuration for default and custom book preprocessors - **[Renderer]** configuration for the HTML, Markdown and custom renderers +- **[Localization]** configuration for books written in more than one language - **[Environment Variable]** configuration for overriding configuration options in your environment [General]: general.md [Preprocessor]: preprocessors.md [Renderer]: renderers.md -[Environment Variable]: environment-variables.md \ No newline at end of file +[Localization]: localization.md +[Environment Variable]: environment-variables.md diff --git a/guide/src/format/configuration/environment-variables.md b/guide/src/en/format/configuration/environment-variables.md similarity index 100% rename from guide/src/format/configuration/environment-variables.md rename to guide/src/en/format/configuration/environment-variables.md diff --git a/guide/src/format/configuration/general.md b/guide/src/en/format/configuration/general.md similarity index 100% rename from guide/src/format/configuration/general.md rename to guide/src/en/format/configuration/general.md diff --git a/guide/src/en/format/configuration/localization.md b/guide/src/en/format/configuration/localization.md new file mode 100644 index 0000000000..62bd730ba1 --- /dev/null +++ b/guide/src/en/format/configuration/localization.md @@ -0,0 +1,86 @@ +# Localization + +It's possible to write your book in more than one language and bundle all of its +translations into a single output folder, with the ability for readers to switch +between each one in the rendered output. The available languages for your book +are defined in the `[language]` table: + +```toml +[language.en] +name = "English" + +[language.ja] +name = "日本語" +title = "本のサンプル" +description = "この本は実例です。" +authors = ["Ruin0x11"] +``` + +Each language must have a human-readable `name` defined. Also, if the +`[language]` table is defined, you must define `book.language` to be a key of +this table, which will indicate the language whose files will be used for +fallbacks if a page is missing in a translation. + +The `title` and `description` fields, if defined, will override the ones set in +the `[book]` section. This way you can translate the book's title and +description. `authors` provides a list of this translation's authors. + +After defining a new language like `[language.ja]`, add a new subdirectory +`src/ja` and create your `SUMMARY.md` and other files there. + +> **Note:** Whether or not the `[language]` table is defined changes the format +> of the `src` directory that mdBook expects to see. If there is no `[language]` +> table, mdBook will treat the `src` directory as a single translation of the +> book, with `SUMMARY.md` at the root: +> +> ``` +> ├── book.toml +> └── src +> ├── chapter +> │ ├── 1.md +> │ ├── 2.md +> │ └── README.md +> ├── README.md +> └── SUMMARY.md +> ``` +> +> If the `[language]` table is defined, mdBook will instead expect to find +> subdirectories under `src` named after the keys in the table: +> +> ``` +> ├── book.toml +> └── src +> ├── en +> │ ├── chapter +> │ │ ├── 1.md +> │ │ ├── 2.md +> │ │ └── README.md +> │ ├── README.md +> │ └── SUMMARY.md +> └── ja +> ├── chapter +> │ ├── 1.md +> │ ├── 2.md +> │ └── README.md +> ├── README.md +> └── SUMMARY.md +> ``` + +If the `[language]` table is used, you can pass the `-l ` argument +to commands like `mdbook build` to build the book for only a single language. In +this example, `` can be `en` or `ja`. + +Some extra notes on translations: + +- In a translation's `SUMMARY.md` or inside Markdown files, you can link to + pages, images or other files that don't exist in the current translation, but + do exist in the default translation. This is so you can have a fallback in + case new pages get added in the default language that haven't been translated + yet. +- Each translation can have its own `SUMMARY.md` with differing content from + other translations. Even if the translation's summary goes out of sync with + the default language, the links will continue to work so long as the pages + exist in either translation. +- Each translation can have its own pages listed in `SUMMARY.md` that don't + exist in the default translation at all, in case extra information specific to + that language is needed. diff --git a/guide/src/format/configuration/preprocessors.md b/guide/src/en/format/configuration/preprocessors.md similarity index 100% rename from guide/src/format/configuration/preprocessors.md rename to guide/src/en/format/configuration/preprocessors.md diff --git a/guide/src/format/configuration/renderers.md b/guide/src/en/format/configuration/renderers.md similarity index 100% rename from guide/src/format/configuration/renderers.md rename to guide/src/en/format/configuration/renderers.md diff --git a/guide/src/format/example.rs b/guide/src/en/format/example.rs similarity index 100% rename from guide/src/format/example.rs rename to guide/src/en/format/example.rs diff --git a/guide/src/format/images/rust-logo-blk.svg b/guide/src/en/format/images/rust-logo-blk.svg similarity index 100% rename from guide/src/format/images/rust-logo-blk.svg rename to guide/src/en/format/images/rust-logo-blk.svg diff --git a/guide/src/format/markdown.md b/guide/src/en/format/markdown.md similarity index 100% rename from guide/src/format/markdown.md rename to guide/src/en/format/markdown.md diff --git a/guide/src/format/mathjax.md b/guide/src/en/format/mathjax.md similarity index 100% rename from guide/src/format/mathjax.md rename to guide/src/en/format/mathjax.md diff --git a/guide/src/format/mdbook.md b/guide/src/en/format/mdbook.md similarity index 100% rename from guide/src/format/mdbook.md rename to guide/src/en/format/mdbook.md diff --git a/guide/src/format/summary.md b/guide/src/en/format/summary.md similarity index 100% rename from guide/src/format/summary.md rename to guide/src/en/format/summary.md diff --git a/guide/src/format/theme/README.md b/guide/src/en/format/theme/README.md similarity index 100% rename from guide/src/format/theme/README.md rename to guide/src/en/format/theme/README.md diff --git a/guide/src/format/theme/editor.md b/guide/src/en/format/theme/editor.md similarity index 100% rename from guide/src/format/theme/editor.md rename to guide/src/en/format/theme/editor.md diff --git a/guide/src/format/theme/index-hbs.md b/guide/src/en/format/theme/index-hbs.md similarity index 100% rename from guide/src/format/theme/index-hbs.md rename to guide/src/en/format/theme/index-hbs.md diff --git a/guide/src/format/theme/syntax-highlighting.md b/guide/src/en/format/theme/syntax-highlighting.md similarity index 100% rename from guide/src/format/theme/syntax-highlighting.md rename to guide/src/en/format/theme/syntax-highlighting.md diff --git a/guide/src/guide/README.md b/guide/src/en/guide/README.md similarity index 100% rename from guide/src/guide/README.md rename to guide/src/en/guide/README.md diff --git a/guide/src/guide/creating.md b/guide/src/en/guide/creating.md similarity index 100% rename from guide/src/guide/creating.md rename to guide/src/en/guide/creating.md diff --git a/guide/src/guide/installation.md b/guide/src/en/guide/installation.md similarity index 100% rename from guide/src/guide/installation.md rename to guide/src/en/guide/installation.md diff --git a/guide/src/guide/reading.md b/guide/src/en/guide/reading.md similarity index 100% rename from guide/src/guide/reading.md rename to guide/src/en/guide/reading.md diff --git a/guide/src/misc/contributors.md b/guide/src/en/misc/contributors.md similarity index 96% rename from guide/src/misc/contributors.md rename to guide/src/en/misc/contributors.md index 362a21fe4f..a1acd279f4 100644 --- a/guide/src/misc/contributors.md +++ b/guide/src/en/misc/contributors.md @@ -20,5 +20,6 @@ shout-out to them! - Vivek Akupatni ([apatniv](https://github.com/apatniv)) - Eric Huss ([ehuss](https://github.com/ehuss)) - Josh Rotenberg ([joshrotenberg](https://github.com/joshrotenberg)) +- [Ruin0x11](https://github.com/Ruin0x11) If you feel you're missing from this list, feel free to add yourself in a PR. diff --git a/src/book/book.rs b/src/book/book.rs index 96c70abc4b..dec567d092 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -1,34 +1,79 @@ -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::fmt::{self, Display, Formatter}; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; -use crate::config::BuildConfig; +use crate::build_opts::BuildOpts; +use crate::config::Config; use crate::errors::*; -use crate::utils::bracket_escape; use log::debug; use serde::{Deserialize, Serialize}; /// Load a book into memory from its `src/` directory. -pub fn load_book>(src_dir: P, cfg: &BuildConfig) -> Result { - let src_dir = src_dir.as_ref(); - let summary_md = src_dir.join("SUMMARY.md"); +pub fn load_book>( + root_dir: P, + cfg: &Config, + build_opts: &BuildOpts, +) -> Result { + if cfg.has_localized_dir_structure() { + match build_opts.language_ident { + // Build a single book's translation. + Some(_) => Ok(LoadedBook::Single(load_single_book_translation( + &root_dir, + cfg, + &build_opts.language_ident, + )?)), + // Build all available translations at once. + None => { + let mut translations = HashMap::new(); + for (lang_ident, _) in cfg.language.0.iter() { + let book = + load_single_book_translation(&root_dir, cfg, &Some(lang_ident.clone()))?; + translations.insert(lang_ident.clone(), book); + } + Ok(LoadedBook::Localized(LocalizedBooks(translations))) + } + } + } else { + Ok(LoadedBook::Single(load_single_book_translation( + &root_dir, cfg, &None, + )?)) + } +} + +fn load_single_book_translation>( + root_dir: P, + cfg: &Config, + language_ident: &Option, +) -> Result { + let localized_src_dir = root_dir + .as_ref() + .join(cfg.get_localized_src_path(language_ident.as_ref()).unwrap()); + let fallback_src_dir = root_dir.as_ref().join(cfg.get_fallback_src_path()); + + let summary_md = localized_src_dir.join("SUMMARY.md"); let mut summary_content = String::new(); File::open(&summary_md) - .with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))? + .with_context(|| { + format!( + "Couldn't open SUMMARY.md in {:?} directory", + localized_src_dir + ) + })? .read_to_string(&mut summary_content)?; 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")?; + if cfg.build.create_missing { + create_missing(&localized_src_dir, &summary) + .with_context(|| "Unable to create missing chapters")?; } - load_book_from_disk(&summary, src_dir) + load_book_from_disk(&summary, localized_src_dir, fallback_src_dir, cfg) } fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { @@ -44,17 +89,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { 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 mut f = File::create(&filename).with_context(|| { - format!("Unable to create missing file: {}", filename.display()) - })?; - writeln!(f, "# {}", bracket_escape(&link.name))?; + create_missing_link(&filename, link)?; } } @@ -65,6 +100,20 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { Ok(()) } +fn create_missing_link(filename: &Path, link: &Link) -> Result<()> { + if let Some(parent) = filename.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + debug!("Creating missing file {}", filename.display()); + + let mut f = File::create(&filename)?; + writeln!(f, "# {}", link.name)?; + + Ok(()) +} + /// A dumb tree structure representing a book. /// /// For the moment a book is just a collection of [`BookItems`] which are @@ -78,6 +127,9 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { pub struct Book { /// The sections in this book. pub sections: Vec, + /// Chapter title overrides for this book. + #[serde(default)] + pub chapter_titles: HashMap, __non_exhaustive: (), } @@ -130,6 +182,89 @@ where } } +/// A collection of `Books`, each one a single localization. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct LocalizedBooks(pub HashMap); + +impl LocalizedBooks { + /// Get a depth-first iterator over the items in the book. + pub fn iter(&self) -> BookItems<'_> { + let mut items = VecDeque::new(); + + for (_, book) in self.0.iter() { + items.extend(book.iter().items); + } + + BookItems { items: items } + } + + /// 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(&mut self, mut func: F) + where + F: FnMut(&mut BookItem), + { + for (_, book) in self.0.iter_mut() { + book.for_each_mut(&mut func); + } + } +} + +/// A book which has been loaded and is ready for rendering. +/// +/// This exists because the result of loading a book directory can be multiple +/// books, each one representing a separate translation, or a single book with +/// no translations. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum LoadedBook { + /// The book was loaded with all translations. + Localized(LocalizedBooks), + /// The book was loaded without any additional translations. + Single(Book), +} + +impl LoadedBook { + /// Get a depth-first iterator over the items in the book. + pub fn iter(&self) -> BookItems<'_> { + match self { + LoadedBook::Localized(books) => books.iter(), + LoadedBook::Single(book) => book.iter(), + } + } + + /// 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(&mut self, mut func: F) + where + F: FnMut(&mut BookItem), + { + match self { + LoadedBook::Localized(books) => books.for_each_mut(&mut func), + LoadedBook::Single(book) => book.for_each_mut(&mut func), + } + } + + /// Returns one of the books loaded. Used for compatibility. + pub fn first(&self) -> &Book { + match self { + LoadedBook::Localized(books) => books.0.iter().next().unwrap().1, + LoadedBook::Single(book) => &book, + } + } +} + /// Enum representing any type of item which can be added to a book. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum BookItem { @@ -209,9 +344,13 @@ impl Chapter { /// /// 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>(summary: &Summary, src_dir: P) -> Result { +pub(crate) fn load_book_from_disk>( + summary: &Summary, + localized_src_dir: P, + fallback_src_dir: P, + cfg: &Config, +) -> Result { 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(); @@ -222,25 +361,35 @@ pub(crate) fn load_book_from_disk>(summary: &Summary, src_dir: P) let mut chapters = Vec::new(); for summary_item in summary_items { - let chapter = load_summary_item(summary_item, src_dir, Vec::new())?; + let chapter = load_summary_item( + summary_item, + localized_src_dir.as_ref(), + fallback_src_dir.as_ref(), + Vec::new(), + cfg, + )?; chapters.push(chapter); } Ok(Book { sections: chapters, + chapter_titles: HashMap::new(), __non_exhaustive: (), }) } fn load_summary_item + Clone>( item: &SummaryItem, - src_dir: P, + localized_src_dir: P, + fallback_src_dir: P, parent_names: Vec, + cfg: &Config, ) -> Result { match item { SummaryItem::Separator => Ok(BookItem::Separator), SummaryItem::Link(ref link) => { - load_chapter(link, src_dir, parent_names).map(BookItem::Chapter) + load_chapter(link, localized_src_dir, fallback_src_dir, parent_names, cfg) + .map(BookItem::Chapter) } SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())), } @@ -248,20 +397,37 @@ fn load_summary_item + Clone>( fn load_chapter>( link: &Link, - src_dir: P, + localized_src_dir: P, + fallback_src_dir: P, parent_names: Vec, + cfg: &Config, ) -> Result { - let src_dir = src_dir.as_ref(); + let src_dir_localized = localized_src_dir.as_ref(); + let src_dir_fallback = fallback_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() { + let mut src_dir = src_dir_localized; + let mut location = if link_location.is_absolute() { link_location.clone() } else { src_dir.join(link_location) }; + if !location.exists() && !link_location.is_absolute() { + src_dir = src_dir_fallback; + location = src_dir.join(link_location); + debug!( + "Falling back to default translation in path \"{}\"", + location.display() + ); + } + if !location.exists() && cfg.build.create_missing { + create_missing_link(&location, &link) + .with_context(|| "Unable to create missing link reference")?; + } + let mut f = File::open(&location) .with_context(|| format!("Chapter file not found, {}", link_location.display()))?; @@ -291,7 +457,15 @@ fn load_chapter>( let sub_items = link .nested_items .iter() - .map(|i| load_summary_item(i, src_dir, sub_item_parents.clone())) + .map(|i| { + load_summary_item( + i, + src_dir_localized, + src_dir_fallback, + sub_item_parents.clone(), + cfg, + ) + }) .collect::>>()?; ch.sub_items = sub_items; @@ -348,7 +522,7 @@ mod tests { this is some dummy text. And here is some \ - more text. +more text. "; /// Create a dummy `Link` in a temporary directory. @@ -390,6 +564,7 @@ And here is some \ #[test] fn load_a_single_chapter_from_disk() { let (link, temp_dir) = dummy_link(); + let cfg = Config::default(); let should_be = Chapter::new( "Chapter 1", DUMMY_SRC.to_string(), @@ -397,13 +572,14 @@ And here is some \ Vec::new(), ); - let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap(); + let got = load_chapter(&link, temp_dir.path(), temp_dir.path(), Vec::new(), &cfg).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 cfg = Config::default(); let chapter_path = temp_dir.path().join("chapter_1.md"); File::create(&chapter_path) @@ -420,7 +596,7 @@ And here is some \ Vec::new(), ); - let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap(); + let got = load_chapter(&link, temp_dir.path(), temp_dir.path(), Vec::new(), &cfg).unwrap(); assert_eq!(got, should_be); } @@ -428,7 +604,10 @@ And here is some \ fn cant_load_a_nonexistent_chapter() { let link = Link::new("Chapter 1", "/foo/bar/baz.md"); - let got = load_chapter(&link, "", Vec::new()); + let mut cfg = Config::default(); + cfg.build.create_missing = false; + + let got = load_chapter(&link, "", "", Vec::new(), &cfg); assert!(got.is_err()); } @@ -445,6 +624,7 @@ And here is some \ parent_names: vec![String::from("Chapter 1")], sub_items: Vec::new(), }; + let cfg = Config::default(); let should_be = BookItem::Chapter(Chapter { name: String::from("Chapter 1"), content: String::from(DUMMY_SRC), @@ -459,7 +639,14 @@ And here is some \ ], }); - let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap(); + let got = load_summary_item( + &SummaryItem::Link(root), + temp.path(), + temp.path(), + Vec::new(), + &cfg, + ) + .unwrap(); assert_eq!(got, should_be); } @@ -470,6 +657,7 @@ And here is some \ numbered_chapters: vec![SummaryItem::Link(link)], ..Default::default() }; + let cfg = Config::default(); let should_be = Book { sections: vec![BookItem::Chapter(Chapter { name: String::from("Chapter 1"), @@ -481,7 +669,7 @@ And here is some \ ..Default::default() }; - let got = load_book_from_disk(&summary, temp.path()).unwrap(); + let got = load_book_from_disk(&summary, temp.path(), temp.path(), &cfg).unwrap(); assert_eq!(got, should_be); } @@ -612,8 +800,9 @@ And here is some \ ..Default::default() }; + let cfg = Config::default(); - let got = load_book_from_disk(&summary, temp.path()); + let got = load_book_from_disk(&summary, temp.path(), temp.path(), &cfg); assert!(got.is_err()); } @@ -631,8 +820,62 @@ And here is some \ })], ..Default::default() }; + let cfg = Config::default(); + + let got = load_book_from_disk(&summary, temp.path(), temp.path(), &cfg); + assert!(got.is_err()); + } + + #[test] + fn can_load_a_nonexistent_chapter_with_fallback() { + let (_, temp_localized) = dummy_link(); + let chapter_path = temp_localized.path().join("chapter_1.md"); + fs::remove_file(&chapter_path).unwrap(); + + let (_, temp_fallback) = dummy_link(); + + let link_relative = Link::new("Chapter 1", "chapter_1.md"); + + let summary = Summary { + numbered_chapters: vec![SummaryItem::Link(link_relative)], + ..Default::default() + }; + let mut cfg = Config::default(); + cfg.build.create_missing = false; + let should_be = Book { + sections: vec![BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + path: Some(PathBuf::from("chapter_1.md")), + source_path: Some(PathBuf::from("chapter_1.md")), + ..Default::default() + })], + ..Default::default() + }; + + let got = load_book_from_disk(&summary, temp_localized.path(), temp_fallback.path(), &cfg) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn cannot_load_a_nonexistent_absolute_link_with_fallback() { + let (link_absolute, temp_localized) = dummy_link(); + let chapter_path = temp_localized.path().join("chapter_1.md"); + fs::remove_file(&chapter_path).unwrap(); + + let (_, temp_fallback) = dummy_link(); + + let summary = Summary { + numbered_chapters: vec![SummaryItem::Link(link_absolute)], + ..Default::default() + }; + let mut cfg = Config::default(); + cfg.build.create_missing = false; + + let got = load_book_from_disk(&summary, temp_localized.path(), temp_fallback.path(), &cfg); - let got = load_book_from_disk(&summary, temp.path()); assert!(got.is_err()); } } diff --git a/src/book/init.rs b/src/book/init.rs index faca1d09aa..ef63cedf35 100644 --- a/src/book/init.rs +++ b/src/book/init.rs @@ -3,7 +3,7 @@ use std::io::Write; use std::path::PathBuf; use super::MDBook; -use crate::config::Config; +use crate::config::{Config, Language}; use crate::errors::*; use crate::theme; use crate::utils::fs::write_file; @@ -16,22 +16,49 @@ pub struct BookBuilder { create_gitignore: bool, config: Config, copy_theme: bool, + language_ident: String, +} + +fn add_default_language(cfg: &mut Config, language_ident: String) { + let language = Language { + name: String::from("English"), + title: None, + authors: None, + description: None, + }; + cfg.language.0.insert(language_ident.clone(), language); + cfg.book.language = Some(language_ident); } impl BookBuilder { /// Create a new `BookBuilder` which will generate a book in the provided /// root directory. pub fn new>(root: P) -> BookBuilder { + let language_ident = String::from("en"); + let mut cfg = Config::default(); + add_default_language(&mut cfg, language_ident.clone()); + BookBuilder { root: root.into(), create_gitignore: false, - config: Config::default(), + config: cfg, copy_theme: false, + language_ident: language_ident, } } - /// Set the [`Config`] to be used. - pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder { + /// Get the output source directory of the builder. + pub fn source_dir(&self) -> PathBuf { + let src = self + .config + .get_localized_src_path(Some(&self.language_ident)) + .unwrap(); + self.root.join(src) + } + + /// Set the `Config` to be used. + pub fn with_config(&mut self, mut cfg: Config) -> &mut BookBuilder { + add_default_language(&mut cfg, self.language_ident.clone()); self.config = cfg; self } @@ -187,7 +214,7 @@ impl BookBuilder { fn create_stub_files(&self) -> Result<()> { debug!("Creating example book contents"); - let src_dir = self.root.join(&self.config.book.src); + let src_dir = self.source_dir(); let summary = src_dir.join("SUMMARY.md"); if !summary.exists() { @@ -207,11 +234,11 @@ impl BookBuilder { } fn create_directory_structure(&self) -> Result<()> { - debug!("Creating directory tree"); + debug!("Creating directory tree at {}", self.root.display()); fs::create_dir_all(&self.root)?; - let src = self.root.join(&self.config.book.src); - fs::create_dir_all(src)?; + let src = self.source_dir(); + fs::create_dir_all(&src)?; let build = self.root.join(&self.config.build.build_dir); fs::create_dir_all(build)?; diff --git a/src/book/mod.rs b/src/book/mod.rs index a5e3e78c6d..85dfb2d8ae 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -10,16 +10,18 @@ mod book; mod init; mod summary; -pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; +pub use self::book::{load_book, Book, BookItem, BookItems, Chapter, LoadedBook, LocalizedBooks}; pub use self::init::BookBuilder; pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; use log::{debug, error, info, log_enabled, trace, warn}; +use std::collections::HashMap; use std::io::Write; use std::path::PathBuf; use std::process::Command; use std::string::ToString; use tempfile::Builder as TempFileBuilder; +use tempfile::TempDir; use toml::Value; use topological_sort::TopologicalSort; @@ -30,6 +32,7 @@ use crate::preprocess::{ use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; use crate::utils; +use crate::build_opts::BuildOpts; use crate::config::{Config, RustEdition}; /// The object used to manage and build a book. @@ -38,8 +41,13 @@ pub struct MDBook { 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, + /// A representation of the book's contents in memory. Can be a single book, + /// or multiple books in different languages. + pub book: LoadedBook, + /// Build options passed from frontend. + pub build_opts: BuildOpts, + + /// List of renderers to be run on the book. renderers: Vec>, /// List of pre-processors to be run on the book. @@ -49,6 +57,15 @@ pub struct MDBook { impl MDBook { /// Load a book from its root directory on disk. pub fn load>(book_root: P) -> Result { + MDBook::load_with_build_opts(book_root, BuildOpts::default()) + } + + /// Load a book from its root directory on disk, passing in options from the + /// frontend. + pub fn load_with_build_opts>( + book_root: P, + build_opts: BuildOpts, + ) -> Result { let book_root = book_root.into(); let config_location = book_root.join("book.toml"); @@ -91,15 +108,18 @@ impl MDBook { } } - MDBook::load_with_config(book_root, config) + MDBook::load_with_config(book_root, config, build_opts) } - /// Load a book from its root directory using a custom `Config`. - pub fn load_with_config>(book_root: P, config: Config) -> Result { + /// Load a book from its root directory using a custom config. + pub fn load_with_config>( + book_root: P, + config: Config, + build_opts: BuildOpts, + ) -> Result { let root = book_root.into(); - let src_dir = root.join(&config.book.src); - let book = book::load_book(src_dir, &config.build)?; + let book = book::load_book(&root, &config, &build_opts)?; let renderers = determine_renderers(&config); let preprocessors = determine_preprocessors(&config)?; @@ -108,6 +128,7 @@ impl MDBook { root, config, book, + build_opts, renderers, preprocessors, }) @@ -118,11 +139,22 @@ impl MDBook { book_root: P, config: Config, summary: Summary, + build_opts: BuildOpts, ) -> Result { let root = book_root.into(); - let src_dir = root.join(&config.book.src); - let book = book::load_book_from_disk(&summary, src_dir)?; + let localized_src_dir = root.join( + config + .get_localized_src_path(build_opts.language_ident.as_ref()) + .unwrap(), + ); + let fallback_src_dir = root.join(config.get_fallback_src_path()); + let book = LoadedBook::Single(book::load_book_from_disk( + &summary, + localized_src_dir, + fallback_src_dir, + &config, + )?); let renderers = determine_renderers(&config); let preprocessors = determine_preprocessors(&config)?; @@ -131,6 +163,7 @@ impl MDBook { root, config, book, + build_opts, renderers, preprocessors, }) @@ -141,8 +174,8 @@ impl MDBook { /// `(section: String, bookitem: &BookItem)` /// /// ```no_run - /// # use mdbook::MDBook; - /// # use mdbook::book::BookItem; + /// # use mdbook_spacewizards::MDBook; + /// # use mdbook_spacewizards::book::BookItem; /// # let book = MDBook::load("mybook").unwrap(); /// for item in book.iter() { /// match *item { @@ -196,39 +229,74 @@ impl MDBook { 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(); + fn preprocess( + &self, + preprocess_ctx: &PreprocessorContext, + renderer: &dyn Renderer, + book: Book, + ) -> Result { + let mut preprocessed_book = book; for preprocessor in &self.preprocessors { 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)) + preprocessed_book + .chapter_titles + .extend(preprocess_ctx.chapter_titles.borrow_mut().drain()); + Ok(preprocessed_book) } /// 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 preprocessed_books = match &self.book { + LoadedBook::Localized(ref books) => { + let mut new_books = HashMap::new(); + + for (language_ident, book) in books.0.iter() { + let preprocess_ctx = PreprocessorContext::new( + self.root.clone(), + Some(language_ident.clone()), + self.build_opts.clone(), + self.config.clone(), + renderer.name().to_string(), + ); + let preprocessed_book = + self.preprocess(&preprocess_ctx, renderer, book.clone())?; + new_books.insert(language_ident.clone(), preprocessed_book); + } + + LoadedBook::Localized(LocalizedBooks(new_books)) + } + LoadedBook::Single(ref book) => { + let preprocess_ctx = PreprocessorContext::new( + self.root.clone(), + None, + self.build_opts.clone(), + self.config.clone(), + renderer.name().to_string(), + ); + + LoadedBook::Single(self.preprocess(&preprocess_ctx, renderer, book.clone())?) + } + }; + + self.render(&preprocessed_books, renderer) + } + + fn render(&self, preprocessed_books: &LoadedBook, renderer: &dyn Renderer) -> Result<()> { let name = renderer.name(); let build_dir = self.build_dir_for(name); - let mut render_context = RenderContext::new( + let render_context = RenderContext::new( self.root.clone(), - preprocessed_book, + preprocessed_books.clone(), + self.build_opts.clone(), self.config.clone(), build_dir, ); - render_context - .chapter_titles - .extend(preprocess_ctx.chapter_titles.borrow_mut().drain()); info!("Running the {} backend", renderer.name()); renderer @@ -250,46 +318,27 @@ impl MDBook { 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 library_args: Vec<&str> = (0..library_paths.len()) - .map(|_| "-L") - .zip(library_paths.into_iter()) - .flat_map(|x| vec![x.0, x.1]) - .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(()) - } - } + fn test_book( + &self, + book: &Book, + temp_dir: &TempDir, + library_args: &Vec<&str>, + language_ident: Option, + ) -> Result<()> { + // FIXME: Is "test" the proper renderer name to use here? + let preprocess_context = PreprocessorContext::new( + self.root.clone(), + language_ident, + self.build_opts.clone(), + self.config.clone(), + "test".to_string(), + ); - // Index Preprocessor is disabled so that chapter paths - // continue to point to the actual markdown files. - self.preprocessors = determine_preprocessors(&self.config)? - .into_iter() - .filter(|pre| pre.name() != IndexPreprocessor::NAME) - .collect(); - let (book, _) = self.preprocess_book(&TestRenderer)?; + let book = LinkPreprocessor::new().run(&preprocess_context, book.clone())?; + // Index Preprocessor is disabled so that chapter paths continue to point to the + // actual markdown files. - let mut failed = false; + /*let mut failed = false; for item in book.iter() { if let BookItem::Chapter(ref ch) = *item { let chapter_path = match ch.path { @@ -314,7 +363,7 @@ impl MDBook { tmpf.write_all(ch.content.as_bytes())?; let mut cmd = Command::new("rustdoc"); - cmd.arg(&path).arg("--test").args(&library_args); + cmd.arg(&path).arg("--test").args(library_args); if let Some(edition) = self.config.rust.edition { match edition { @@ -351,7 +400,31 @@ impl MDBook { if !chapter_found { bail!("Chapter not found: {}", chapter); } + }*/ + Ok(()) + } + + /// Run `rustdoc` tests on the book, linking against the provided libraries. + pub fn test(&self, library_paths: Vec<&str>) -> Result<()> { + let library_args: Vec<&str> = (0..library_paths.len()) + .map(|_| "-L") + .zip(library_paths.into_iter()) + .flat_map(|x| vec![x.0, x.1]) + .collect(); + + let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; + + match self.book { + LoadedBook::Localized(ref books) => { + for (language_ident, book) in books.0.iter() { + self.test_book(book, &temp_dir, &library_args, Some(language_ident.clone()))?; + } + } + LoadedBook::Single(ref book) => { + self.test_book(&book, &temp_dir, &library_args, None)? + } } + Ok(()) } diff --git a/src/build_opts.rs b/src/build_opts.rs new file mode 100644 index 0000000000..0fea05ac62 --- /dev/null +++ b/src/build_opts.rs @@ -0,0 +1,12 @@ +//! Build options. + +use serde::{Deserialize, Serialize}; + +/// Build options passed from the frontend to control how the book is built. +/// Separate from `Config`, which is global to all book languages. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "kebab-case")] +pub struct BuildOpts { + /// Language of the book to render. + pub language_ident: Option, +} diff --git a/src/cmd/build.rs b/src/cmd/build.rs index e40e5c0c72..c05d523dcc 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,7 +1,8 @@ +use crate::{get_book_dir, get_build_opts, open}; +use clap::{Arg, ArgMatches}; use super::command_prelude::*; -use crate::{get_book_dir, open}; -use mdbook::errors::Result; -use mdbook::MDBook; +use mdbook_spacewizards::errors::Result; +use mdbook_spacewizards::MDBook; use std::path::PathBuf; // Create clap subcommand arguments @@ -11,12 +12,14 @@ pub fn make_subcommand() -> Command { .arg_dest_dir() .arg_root_dir() .arg_open() + .arg_language() } // Build command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::load(book_dir)?; + let opts = get_build_opts(args); + let mut book = MDBook::load_with_build_opts(&book_dir, opts)?; if let Some(dest_dir) = args.get_one::("dest-dir") { book.config.build.build_dir = dest_dir.into(); diff --git a/src/cmd/clean.rs b/src/cmd/clean.rs index 48b4147ca9..f58e2728c7 100644 --- a/src/cmd/clean.rs +++ b/src/cmd/clean.rs @@ -1,7 +1,7 @@ +use crate::{get_book_dir, get_build_opts}; use super::command_prelude::*; -use crate::get_book_dir; use anyhow::Context; -use mdbook::MDBook; +use mdbook_spacewizards::MDBook; use std::fs; use std::path::PathBuf; @@ -11,12 +11,14 @@ pub fn make_subcommand() -> Command { .about("Deletes a built book") .arg_dest_dir() .arg_root_dir() + .arg_language() } // Clean command implementation -pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> { +pub fn execute(args: &ArgMatches) -> mdbook_spacewizards::errors::Result<()> { let book_dir = get_book_dir(args); - let book = MDBook::load(book_dir)?; + let build_opts = get_build_opts(args); + let book = MDBook::load_with_build_opts(&book_dir, build_opts)?; let dir_to_remove = match args.get_one::("dest-dir") { Some(dest_dir) => dest_dir.into(), diff --git a/src/cmd/command_prelude.rs b/src/cmd/command_prelude.rs index b6362e6033..be06194c79 100644 --- a/src/cmd/command_prelude.rs +++ b/src/cmd/command_prelude.rs @@ -1,6 +1,7 @@ //! Helpers for building the command-line arguments for commands. pub use clap::{arg, Arg, ArgMatches, Command}; +use clap::builder::NonEmptyStringValueParser; use std::path::PathBuf; pub trait CommandExt: Sized { @@ -22,6 +23,20 @@ pub trait CommandExt: Sized { ) } + fn arg_language(self) -> Self { + self._arg( + Arg::new("language") + .short('l') + .long("language") + .value_name("language") + .value_parser(NonEmptyStringValueParser::new()) + .help( + "Only valid if the [language] table in the config is not empty.\n\ + If omitted, builds all translations and provides a menu in the generated output for switching between them." + ) + ) + } + fn arg_root_dir(self) -> Self { self._arg( Arg::new("dir") diff --git a/src/cmd/init.rs b/src/cmd/init.rs index 2c6415b6d9..7fba4e0ff9 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,8 +1,8 @@ use crate::get_book_dir; use clap::{arg, ArgMatches, Command as ClapCommand}; -use mdbook::config; -use mdbook::errors::Result; -use mdbook::MDBook; +use mdbook_spacewizards::config; +use mdbook_spacewizards::errors::Result; +use mdbook_spacewizards::MDBook; use std::io; use std::io::Write; use std::process::Command; @@ -78,7 +78,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { } builder.build()?; - println!("\nAll done, no errors..."); + println!("\nCreated new book at {}", builder.source_dir().display()); Ok(()) } diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index eeb19cb371..590127ab95 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -1,14 +1,16 @@ use super::command_prelude::*; #[cfg(feature = "watch")] use super::watch; -use crate::{get_book_dir, open}; +use crate::{get_book_dir, get_build_opts, open}; +use clap::{Arg, ArgMatches}; use clap::builder::NonEmptyStringValueParser; use futures_util::sink::SinkExt; use futures_util::StreamExt; -use mdbook::errors::*; -use mdbook::utils; -use mdbook::utils::fs::get_404_output_file; -use mdbook::MDBook; +use http::Uri; +use mdbook_spacewizards::errors::*; +use mdbook_spacewizards::utils; +use mdbook_spacewizards::utils::fs::get_404_output_file; +use mdbook_spacewizards::MDBook; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use tokio::sync::broadcast; @@ -43,12 +45,14 @@ pub fn make_subcommand() -> Command { .help("Port to use for HTTP connections"), ) .arg_open() + .arg_language() } // Serve command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::load(book_dir)?; + let build_opts = get_build_opts(args); + let mut book = MDBook::load_with_build_opts(&book_dir, build_opts.clone())?; let port = args.get_one::("port").unwrap(); let hostname = args.get_one::("hostname").unwrap(); @@ -69,6 +73,18 @@ pub fn execute(args: &ArgMatches) -> Result<()> { update_config(&mut book); book.build()?; + let language: Option = match build_opts.language_ident { + // index.html will be at the root directory. + Some(_) => None, + None => match book.config.default_language() { + // If book has translations, index.html will be under src/en/ or + // similar. + Some(lang_ident) => Some(lang_ident.clone()), + // If not, it will be at the root. + None => None, + }, + }; + let sockaddr: SocketAddr = address .to_socket_addrs()? .next() @@ -86,7 +102,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let reload_tx = tx.clone(); let thread_handle = std::thread::spawn(move || { - serve(build_dir, sockaddr, reload_tx, &file_404); + serve(build_dir, sockaddr, reload_tx, &file_404, language); }); let serving_url = format!("http://{}", address); @@ -102,10 +118,11 @@ pub fn execute(args: &ArgMatches) -> Result<()> { info!("Building book..."); // FIXME: This area is really ugly because we need to re-set livereload :( - let result = MDBook::load(book_dir).and_then(|mut b| { - update_config(&mut b); - b.build() - }); + let result = + MDBook::load_with_build_opts(&book_dir, build_opts.clone()).and_then(|mut b| { + update_config(&mut b); + b.build() + }); if let Err(e) = result { error!("Unable to load the book"); @@ -126,6 +143,7 @@ async fn serve( address: SocketAddr, reload_tx: broadcast::Sender, file_404: &str, + language: Option, ) { // A warp Filter which captures `reload_tx` and provides an `rx` copy to // receive reload messages. @@ -149,10 +167,6 @@ async fn serve( }); // A warp Filter that serves from the filesystem. let book_route = warp::fs::dir(build_dir.clone()); - // The fallback route for 404 errors - let fallback_route = warp::fs::file(build_dir.join(file_404)) - .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND)); - let routes = livereload.or(book_route).or(fallback_route); std::panic::set_hook(Box::new(move |panic_info| { // exit if serve panics @@ -160,5 +174,36 @@ async fn serve( std::process::exit(1); })); - warp::serve(routes).run(address).await; + if let Some(lang_ident) = language { + // Redirect root to the default translation directory, if serving a localized book. + // NOTE: This can't be `/{lang_ident}`, or the static assets won't get loaded. + // BUG: Redirects get cached if you change the --language parameter, + // meaning you'll get a 404 unless you disable the cache in Developer + // Tools. + let index_for_language = format!("/{}/index.html", lang_ident) + .parse::() + .unwrap(); + let redirect_to_index = + warp::path::end().map(move || warp::redirect(index_for_language.clone())); + + // BUG: It is not possible to conditionally redirect to the correct 404 + // page depending on the URL using warp, so just redirect to the one in + // the default language. + // See: https://github.com/seanmonstar/warp/issues/171 + let fallback_route = warp::fs::file(build_dir.join(lang_ident).join(file_404)) + .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND)); + + let routes = livereload + .or(redirect_to_index) + .or(book_route) + .or(fallback_route); + warp::serve(routes).run(address).await; + } else { + // The fallback route for 404 errors + let fallback_route = warp::fs::file(build_dir.join(file_404)) + .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND)); + + let routes = livereload.or(book_route).or(fallback_route); + warp::serve(routes).run(address).await; + }; } diff --git a/src/cmd/test.rs b/src/cmd/test.rs index 69f99f4095..35d685afa5 100644 --- a/src/cmd/test.rs +++ b/src/cmd/test.rs @@ -1,9 +1,9 @@ use super::command_prelude::*; -use crate::get_book_dir; use clap::builder::NonEmptyStringValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; -use mdbook::errors::Result; -use mdbook::MDBook; +use crate::{get_book_dir, get_build_opts}; +use mdbook_spacewizards::errors::Result; +use mdbook_spacewizards::MDBook; use std::path::PathBuf; // Create clap subcommand arguments @@ -32,11 +32,12 @@ pub fn make_subcommand() -> Command { search path when building tests", ), ) + .arg_language() } // test command implementation pub fn execute(args: &ArgMatches) -> Result<()> { - let library_paths: Vec<&str> = args + /*let library_paths: Vec<&str> = args .get_many("library-path") .map(|it| it.map(String::as_str).collect()) .unwrap_or_default(); @@ -44,7 +45,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let chapter: Option<&str> = args.get_one::("chapter").map(|s| s.as_str()); let book_dir = get_book_dir(args); - let mut book = MDBook::load(book_dir)?; + let build_opts = get_build_opts(args); + let mut book = MDBook::load_with_build_opts(&book_dir, build_opts)?; if let Some(dest_dir) = args.get_one::("dest-dir") { book.config.build.build_dir = dest_dir.to_path_buf(); @@ -52,7 +54,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { match chapter { Some(_) => book.test_chapter(library_paths, chapter), None => book.test(library_paths), - }?; + }?;*/ Ok(()) } diff --git a/src/cmd/watch.rs b/src/cmd/watch.rs index 9fd5085d8d..3f85bc5700 100644 --- a/src/cmd/watch.rs +++ b/src/cmd/watch.rs @@ -1,9 +1,10 @@ use super::command_prelude::*; -use crate::{get_book_dir, open}; use ignore::gitignore::Gitignore; -use mdbook::errors::Result; -use mdbook::utils; -use mdbook::MDBook; +use crate::{get_book_dir, get_build_opts, open}; +use clap::{Arg, ArgMatches}; +use mdbook_spacewizards::errors::Result; +use mdbook_spacewizards::utils; +use mdbook_spacewizards::MDBook; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; use std::thread::sleep; @@ -16,12 +17,14 @@ pub fn make_subcommand() -> Command { .arg_dest_dir() .arg_root_dir() .arg_open() + .arg_language() } // Watch command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); - let mut book = MDBook::load(book_dir)?; + let build_opts = get_build_opts(args); + let mut book = MDBook::load_with_build_opts(&book_dir, build_opts.clone())?; let update_config = |book: &mut MDBook| { if let Some(dest_dir) = args.get_one::("dest-dir") { @@ -42,10 +45,11 @@ pub fn execute(args: &ArgMatches) -> Result<()> { trigger_on_change(&book, |paths, book_dir| { info!("Files changed: {:?}\nBuilding book...\n", paths); - let result = MDBook::load(book_dir).and_then(|mut b| { - update_config(&mut b); - b.build() - }); + let result = + MDBook::load_with_build_opts(&book_dir, build_opts.clone()).and_then(|mut b| { + update_config(&mut b); + b.build() + }); if let Err(e) = result { error!("Unable to build the book"); diff --git a/src/config.rs b/src/config.rs index 7f56e797ab..b9470e6779 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,10 +9,10 @@ //! # Examples //! //! ```rust -//! # use mdbook::errors::*; +//! # use mdbook_spacewizards::errors::*; //! use std::path::PathBuf; //! use std::str::FromStr; -//! use mdbook::Config; +//! use mdbook_spacewizards::Config; //! use toml::Value; //! //! # fn run() -> Result<()> { @@ -50,6 +50,7 @@ #![deny(missing_docs)] use log::{debug, trace, warn}; +use anyhow::anyhow; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; use std::env; @@ -73,6 +74,8 @@ pub struct Config { pub build: BuildConfig, /// Information about Rust language support. pub rust: RustConfig, + /// Information about localizations of this book. + pub language: LanguageConfig, rest: Value, } @@ -251,6 +254,136 @@ impl Config { self.get(&key).and_then(Value::as_table) } + /// Gets the language configured for a book. + pub fn get_language>(&self, index: Option) -> Result> { + match self.default_language() { + // Languages have been specified, assume directory structure with + // language subfolders. + Some(ref default) => match index { + // Make sure that the language we passed was actually declared + // in the config, and return an `Err` if not. + Some(lang_ident) => match self.language.0.get(lang_ident.as_ref()) { + Some(_) => Ok(Some(lang_ident.as_ref().into())), + None => Err(anyhow!( + "Expected [language.{}] to be declared in book.toml", + lang_ident.as_ref() + )), + }, + // Use the default specified in book.toml. + None => Ok(Some(default.to_string())), + }, + + // No [language] table was declared in book.toml. + None => match index { + // We passed in a language from the frontend, but the config + // offers no languages. + Some(lang_ident) => Err(anyhow!( + "No [language] table in book.toml, expected [language.{}] to be declared", + lang_ident.as_ref() + )), + // Default to previous non-localized behavior. + None => Ok(None), + }, + } + } + + /// Get the source directory of a localized book corresponding to language ident `index`. + pub fn get_localized_src_path>(&self, index: Option) -> Result { + let language = self.get_language(index)?; + + match language { + Some(lang_ident) => { + let mut buf = PathBuf::new(); + buf.push(self.book.src.clone()); + buf.push(lang_ident); + Ok(buf) + } + + // No [language] table was declared in book.toml. Preserve backwards + // compatibility by just returning `src`. + None => Ok(self.book.src.clone()), + } + } + + /// Gets the localized title of the book. + pub fn get_localized_title>(&self, index: Option) -> Option { + let language = self.get_language(index).unwrap(); + + match language { + Some(lang_ident) => self + .language + .0 + .get(&lang_ident) + .unwrap() + .title + .clone() + .or(self.book.title.clone()), + None => self.book.title.clone(), + } + } + + /// Gets the localized description of the book. + pub fn get_localized_description>(&self, index: Option) -> Option { + let language = self.get_language(index).unwrap(); + + match language { + Some(lang_ident) => self + .language + .0 + .get(&lang_ident) + .unwrap() + .description + .clone() + .or(self.book.description.clone()), + None => self.book.description.clone(), + } + } + + /// Get the fallback source directory of a book. If chapters/sections are + /// missing in a localization, any links to them will gracefully degrade to + /// the files that exist in this directory. + pub fn get_fallback_src_path(&self) -> PathBuf { + match self.default_language() { + // Languages have been specified, assume directory structure with + // language subfolders. + Some(default) => { + let mut buf = PathBuf::new(); + buf.push(self.book.src.clone()); + buf.push(default); + buf + } + + // No default language was configured in book.toml. Preserve + // backwards compatibility by just returning `src`. + None => self.book.src.clone(), + } + } + + /// If true, mdBook should assume there are subdirectories under src/ + /// corresponding to the localizations in the config. If false, src/ is a + /// single directory containing the summary file and the rest. + pub fn has_localized_dir_structure(&self) -> bool { + !self.language.0.is_empty() + } + + /// Obtains the default language for this config. + pub fn default_language(&self) -> Option { + if self.has_localized_dir_structure() { + let language_ident = self + .book + .language + .clone() + .expect("Config has [language] table, but `book.language` not was declared"); + self.language.0.get(&language_ident).expect(&format!( + "Expected [language.{}] to be declared in book.toml", + language_ident + )); + Some(language_ident) + } else { + None + } + } + fn from_legacy(mut table: Value) -> Config { let mut cfg = Config::default(); @@ -291,6 +424,7 @@ impl Default for Config { book: BookConfig::default(), build: BuildConfig::default(), rust: RustConfig::default(), + language: LanguageConfig::default(), rest: Value::Table(Table::default()), } } @@ -340,9 +474,38 @@ impl<'de> serde::Deserialize<'de> for Config { .transpose()? .unwrap_or_default(); + let language: LanguageConfig = table + .remove("language") + .and_then(|value| value.try_into().ok()) + .unwrap_or_default(); + + if !language.0.is_empty() { + if book.language.is_none() { + return Err(D::Error::custom( + "If the [language] table is specified, then `book.language` must be declared", + )); + } + let language_ident = book.language.clone().unwrap(); + if language.0.get(&language_ident).is_none() { + return Err(D::Error::custom(format!( + "Expected [language.{}] to be declared in book.toml", + language_ident + ))); + } + for (ident, language) in language.0.iter() { + if language.name.is_empty() { + return Err(D::Error::custom(format!( + "`name` property for [language.{}] must be non-empty", + ident + ))); + } + } + } + Ok(Config { book, build, + language, rust, rest: Value::Table(table), }) @@ -367,6 +530,12 @@ impl Serialize for Config { table.insert("rust", rust_config); } + if !self.language.0.is_empty() { + let language_config = + Value::try_from(&self.language).expect("should always be serializable"); + table.insert("language", language_config); + } + table.serialize(s) } } @@ -407,8 +576,6 @@ pub struct BookConfig { pub description: Option, /// Location of the book source relative to the book's root directory. pub src: PathBuf, - /// Does this book support more than one language? - pub multilingual: bool, /// The main language of the book. pub language: Option, /// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). @@ -423,7 +590,6 @@ impl Default for BookConfig { authors: Vec::new(), description: None, src: PathBuf::from("src"), - multilingual: false, language: Some(String::from("en")), text_direction: None, } @@ -752,6 +918,25 @@ impl Default for Search { } } +/// Configuration for localizations of this book +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct LanguageConfig(pub HashMap); + +/// Configuration for a single localization +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct Language { + /// Human-readable name of the language. + pub name: String, + /// Localized title of the book. + pub title: Option, + /// The authors of the translation. + pub authors: Option>, + /// Localized description of the book. + pub description: Option, +} + /// Allows you to "update" any arbitrary field in a struct by round-tripping via /// a `toml::Value`. /// @@ -786,7 +971,6 @@ mod tests { title = "Some Book" authors = ["Michael-F-Bryan "] description = "A completely useless book" - multilingual = true src = "source" language = "ja" @@ -815,6 +999,15 @@ mod tests { [preprocessor.first] [preprocessor.second] + + [language.en] + name = "English" + + [language.ja] + name = "日本語" + title = "なんかの本" + description = "何の役にも立たない本" + authors = ["Ruin0x11"] "#; #[test] @@ -825,7 +1018,6 @@ mod tests { title: Some(String::from("Some Book")), authors: vec![String::from("Michael-F-Bryan ")], description: Some(String::from("A completely useless book")), - multilingual: true, src: PathBuf::from("source"), language: Some(String::from("ja")), text_direction: None, @@ -864,6 +1056,25 @@ mod tests { .collect(), ..Default::default() }; + let mut language_should_be = LanguageConfig::default(); + language_should_be.0.insert( + String::from("en"), + Language { + name: String::from("English"), + title: None, + description: None, + authors: None, + }, + ); + language_should_be.0.insert( + String::from("ja"), + Language { + name: String::from("日本語"), + title: Some(String::from("なんかの本")), + description: Some(String::from("何の役にも立たない本")), + authors: Some(vec![String::from("Ruin0x11")]), + }, + ); let got = Config::from_str(src).unwrap(); @@ -871,6 +1082,8 @@ mod tests { assert_eq!(got.build, build_should_be); assert_eq!(got.rust, rust_should_be); assert_eq!(got.html_config().unwrap(), html_should_be); + assert_eq!(got.language, language_should_be); + assert_eq!(got.default_language(), Some(String::from("ja"))); } #[test] @@ -1302,6 +1515,40 @@ mod tests { } #[test] + fn book_language_without_languages_table() { + let src = r#" + [book] + language = "en" + "#; + + let got = Config::from_str(src).unwrap(); + assert_eq!(got.default_language(), None); + } + + #[test] + #[should_panic(expected = "Invalid configuration file")] + fn default_language_must_exist_in_languages_table() { + let src = r#" + [language.ja] + name = "日本語" + "#; + + Config::from_str(src).unwrap(); + } + + #[test] + #[should_panic(expected = "Invalid configuration file")] + fn validate_language_config_must_have_name() { + let src = r#" + [book] + language = "en" + + [language.en] + "#; + + Config::from_str(src).unwrap(); + } + fn print_config() { let src = r#" [output.html.print] diff --git a/src/lib.rs b/src/lib.rs index 14cd94d9d3..76df83213d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,8 +28,8 @@ //! the `MDBook::init()` method. //! //! ```rust,no_run -//! use mdbook::MDBook; -//! use mdbook::config::Config; +//! use mdbook_spacewizards::MDBook; +//! use mdbook_spacewizards::config::Config; //! //! let root_dir = "/path/to/book/root"; //! @@ -48,7 +48,7 @@ //! You can also load an existing book and build it. //! //! ```rust,no_run -//! use mdbook::MDBook; +//! use mdbook_spacewizards::MDBook; //! //! let root_dir = "/path/to/book/root"; //! @@ -84,6 +84,7 @@ #![deny(rust_2018_idioms)] pub mod book; +pub mod build_opts; pub mod config; pub mod preprocess; pub mod renderer; diff --git a/src/main.rs b/src/main.rs index 3e576c5b53..e3549250e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,8 @@ use clap::{Arg, ArgMatches, Command}; use clap_complete::Shell; use env_logger::Builder; use log::LevelFilter; -use mdbook::utils; +use mdbook_spacewizards::build_opts::BuildOpts; +use mdbook_spacewizards::utils; use std::env; use std::ffi::OsStr; use std::io::Write; @@ -133,6 +134,14 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf { } } +fn get_build_opts(args: &ArgMatches) -> BuildOpts { + let language = args.get_one::("language"); + + BuildOpts { + language_ident: language.cloned(), + } +} + fn open>(path: P) { info!("Opening web browser"); if let Err(e) = opener::open(path) { diff --git a/src/preprocess/cmd.rs b/src/preprocess/cmd.rs index 149dabda56..fb39f08c36 100644 --- a/src/preprocess/cmd.rs +++ b/src/preprocess/cmd.rs @@ -179,6 +179,7 @@ impl Preprocessor for CmdPreprocessor { #[cfg(test)] mod tests { use super::*; + use crate::build_opts::BuildOpts; use crate::MDBook; use std::path::Path; @@ -193,16 +194,19 @@ mod tests { let md = guide(); let ctx = PreprocessorContext::new( md.root.clone(), + None, + BuildOpts::default(), md.config.clone(), "some-renderer".to_string(), ); let mut buffer = Vec::new(); - cmd.write_input(&mut buffer, &md.book, &ctx).unwrap(); + cmd.write_input(&mut buffer, &md.book.first(), &ctx) + .unwrap(); let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap(); - assert_eq!(got_book, md.book); + assert_eq!(got_book, *md.book.first()); assert_eq!(got_ctx, ctx); } } diff --git a/src/preprocess/index.rs b/src/preprocess/index.rs index 004b7eda6e..3b54f4c65e 100644 --- a/src/preprocess/index.rs +++ b/src/preprocess/index.rs @@ -27,7 +27,7 @@ impl Preprocessor for IndexPreprocessor { } fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result { - let source_dir = ctx.root.join(&ctx.config.book.src); + let source_dir = ctx.source_dir(); book.for_each_mut(|section: &mut BookItem| { if let BookItem::Chapter(ref mut ch) = *section { if let Some(ref mut path) = ch.path { diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs index 0af211960a..622307332e 100644 --- a/src/preprocess/links.rs +++ b/src/preprocess/links.rs @@ -10,7 +10,7 @@ use std::path::{Path, PathBuf}; use super::{Preprocessor, PreprocessorContext}; use crate::book::{Book, BookItem}; -use log::{error, warn}; +use log::{error, warn, debug}; use once_cell::sync::Lazy; const ESCAPE_CHAR: char = '\\'; @@ -44,19 +44,34 @@ impl Preprocessor for LinkPreprocessor { } fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result { - let src_dir = ctx.root.join(&ctx.config.book.src); + let src_dir = ctx + .config + .get_localized_src_path(ctx.language_ident.as_ref()) + .unwrap(); + let src_dir = ctx.root.join(src_dir); + + let fallback_src_dir = ctx + .config + .get_localized_src_path(ctx.config.default_language().as_ref()) + .unwrap(); + let fallback_src_dir = ctx.root.join(fallback_src_dir); book.for_each_mut(|section: &mut BookItem| { if let BookItem::Chapter(ref mut ch) = *section { if let Some(ref chapter_path) = ch.path { - let base = chapter_path - .parent() - .map(|dir| src_dir.join(dir)) - .expect("All book items have a parent"); + let parent = chapter_path.parent().expect("All book items have a parent"); + let base = src_dir.join(parent); + let fallback = fallback_src_dir.join(parent); let mut chapter_title = ch.name.clone(); - let content = - replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title); + let content = replace_all( + &ch.content, + base, + Some(fallback), + chapter_path, + 0, + &mut chapter_title, + ); ch.content = content; if chapter_title != ch.name { ctx.chapter_titles @@ -74,6 +89,7 @@ impl Preprocessor for LinkPreprocessor { fn replace_all( s: &str, path: P1, + fallback: Option, source: P2, depth: usize, chapter_title: &mut String, @@ -86,6 +102,7 @@ where // the indices after that will not correspond, // we therefore have to store the difference to correct this let path = path.as_ref(); + let fallback = fallback.as_ref(); let source = source.as_ref(); let mut previous_end_index = 0; let mut replaced = String::new(); @@ -93,13 +110,14 @@ 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, fallback, chapter_title) { Ok(new_content) => { if depth < MAX_LINK_NESTED_DEPTH { if let Some(rel_path) = link.link_type.relative_path(path) { replaced.push_str(&replace_all( &new_content, rel_path, + None, source, depth + 1, chapter_title, @@ -319,9 +337,10 @@ impl<'a> Link<'a> { }) } - fn render_with_path>( + fn render_with_path, P2: AsRef>( &self, - base: P, + base: P1, + fallback: Option, chapter_title: &mut String, ) -> Result { let base = base.as_ref(); @@ -329,7 +348,20 @@ impl<'a> Link<'a> { // omit the escape char LinkType::Escaped => Ok(self.link_text[1..].to_owned()), LinkType::Include(ref pat, ref range_or_anchor) => { - let target = base.join(pat); + let mut target = base.join(pat); + + if !target.exists() { + if let Some(fallback) = fallback { + let fallback_target = fallback.as_ref().join(pat); + if fallback_target.exists() { + debug!( + "Included file fallback: {:?} => {:?}", + target, fallback_target + ); + target = fallback_target; + } + } + } fs::read_to_string(&target) .map(|s| match range_or_anchor { @@ -345,7 +377,20 @@ impl<'a> Link<'a> { }) } LinkType::RustdocInclude(ref pat, ref range_or_anchor) => { - let target = base.join(pat); + let mut target = base.join(pat); + + if !target.exists() { + if let Some(fallback) = fallback { + let fallback_target = fallback.as_ref().join(pat); + if fallback_target.exists() { + debug!( + "Included file fallback: {:?} => {:?}", + target, fallback_target + ); + target = fallback_target; + } + } + } fs::read_to_string(&target) .map(|s| match range_or_anchor { @@ -365,7 +410,20 @@ impl<'a> Link<'a> { }) } LinkType::Playground(ref pat, ref attrs) => { - let target = base.join(pat); + let mut target = base.join(pat); + + if !target.exists() { + if let Some(fallback) = fallback { + let fallback_target = fallback.as_ref().join(pat); + if fallback_target.exists() { + debug!( + "Included file fallback: {:?} => {:?}", + target, fallback_target + ); + target = fallback_target; + } + } + } let mut contents = fs::read_to_string(&target).with_context(|| { format!( @@ -444,7 +502,7 @@ mod tests { {{#include file.rs}} << an escaped link! ```"; let mut chapter_title = "test_replace_all_escaped".to_owned(); - assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end); + assert_eq!(replace_all(start, "", None, "", 0, &mut chapter_title), end); } #[test] @@ -456,7 +514,7 @@ mod tests { # My Chapter "; let mut chapter_title = "test_set_chapter_title".to_owned(); - assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end); + assert_eq!(replace_all(start, "", None, "", 0, &mut chapter_title), end); assert_eq!(chapter_title, "My Title"); } diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index df01a3dbfb..35cb466d5f 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -9,6 +9,7 @@ mod index; mod links; use crate::book::Book; +use crate::build_opts::BuildOpts; use crate::config::Config; use crate::errors::*; @@ -23,6 +24,11 @@ use std::path::PathBuf; pub struct PreprocessorContext { /// The location of the book directory on disk. pub root: PathBuf, + /// The language of the book being built. Is only `Some` if the book is part + /// of a multilingual build output. + pub language_ident: Option, + /// The build options passed from the frontend. + pub build_opts: BuildOpts, /// The book configuration (`book.toml`). pub config: Config, /// The `Renderer` this preprocessor is being used with. @@ -37,9 +43,17 @@ pub struct PreprocessorContext { impl PreprocessorContext { /// Create a new `PreprocessorContext`. - pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self { + pub(crate) fn new( + root: PathBuf, + language_ident: Option, + build_opts: BuildOpts, + config: Config, + renderer: String, + ) -> Self { PreprocessorContext { root, + language_ident, + build_opts, config, renderer, mdbook_version: crate::MDBOOK_VERSION.to_string(), @@ -47,6 +61,11 @@ impl PreprocessorContext { __non_exhaustive: (), } } + + /// Get the directory containing this book's source files. + pub fn source_dir(&self) -> PathBuf { + self.root.join(&self.config.book.src) + } } /// An operation which is run immediately after loading a book into memory and diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 8ea2f49efc..766bd7fed2 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,10 +1,10 @@ -use crate::book::{Book, BookItem}; -use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition}; +use crate::book::{Book, BookItem, LoadedBook}; +use crate::config::{Code, BookConfig, Config, HtmlConfig, Playground, RustEdition}; use crate::errors::*; use crate::renderer::html_handlebars::helpers; use crate::renderer::{RenderContext, Renderer}; use crate::theme::{self, playground_editor, Theme}; -use crate::utils; +use crate::utils::{self, RenderMarkdownContext}; use std::borrow::Cow; use std::collections::BTreeMap; @@ -27,10 +27,193 @@ impl HtmlHandlebars { HtmlHandlebars } + fn render_books<'a>( + &self, + ctx: &RenderContext, + src_dir: &PathBuf, + html_config: &HtmlConfig, + handlebars: &mut Handlebars<'a>, + theme: &Theme, + ) -> Result<()> { + match ctx.book { + LoadedBook::Localized(ref books) => { + for (lang_ident, book) in books.0.iter() { + let localized_destination = ctx.destination.join(lang_ident); + let localized_build_dir = ctx.config.build.build_dir.join(lang_ident); + self.render_book( + ctx, + &book, + src_dir, + &localized_destination, + &localized_build_dir, + &Some(lang_ident.to_string()), + html_config, + handlebars, + theme, + )?; + } + } + LoadedBook::Single(ref book) => { + self.render_book( + ctx, + &book, + src_dir, + &ctx.destination, + &ctx.config.build.build_dir, + &ctx.build_opts.language_ident, + html_config, + handlebars, + theme, + )?; + } + } + + Ok(()) + } + + fn render_book<'a>( + &self, + ctx: &RenderContext, + book: &Book, + src_dir: &PathBuf, + destination: &PathBuf, + build_dir: &PathBuf, + language: &Option, + html_config: &HtmlConfig, + handlebars: &mut Handlebars<'a>, + theme: &Theme, + ) -> Result<()> { + let book_config = &ctx.config.book; + let build_dir = ctx.root.join(build_dir); + let mut data = make_data( + &ctx.root, + &book, + &ctx.book, + &ctx.config, + language, + &html_config, + &theme, + )?; + + // Print version + let mut print_content = String::new(); + + fs::create_dir_all(&destination) + .with_context(|| "Unexpected error when constructing destination path")?; + + let mut is_index = true; + for item in book.iter() { + let item_ctx = RenderItemContext { + handlebars: &handlebars, + destination: destination.to_path_buf(), + data: data.clone(), + is_index, + book_config: book_config.clone(), + html_config: html_config.clone(), + edition: ctx.config.rust.edition, + chapter_titles: &book.chapter_titles, + }; + self.render_item( + item, + item_ctx, + &src_dir, + language, + &ctx.config, + &mut print_content, + )?; + is_index = false; + } + + // Render 404 page + if html_config.input_404 != Some("".to_string()) { + self.render_404( + ctx, + &html_config, + &src_dir, + destination, + language, + handlebars, + &mut data, + )?; + } + + // Print version + self.configure_print_version(&mut data, &print_content); + if let Some(ref title) = ctx.config.book.title { + data.insert("title".to_owned(), json!(title)); + } + + // Render the handlebars template with the data + if html_config.print.enable { + debug!("Render template"); + let rendered = handlebars.render("index", &data)?; + + let rendered = + self.post_process(rendered, &html_config.playground, &html_config.code, ctx.config.rust.edition); + + utils::fs::write_file(destination, "print.html", rendered.as_bytes())?; + debug!("Creating print.html ✓"); + } + + debug!("Copy static files"); + self.copy_static_files(destination, &theme, &html_config) + .with_context(|| "Unable to copy across static files")?; + self.copy_additional_css_and_js(&html_config, &ctx.root, destination) + .with_context(|| "Unable to copy across additional CSS and JS")?; + + // Render search index + #[cfg(feature = "search")] + { + let search = html_config.search.clone().unwrap_or_default(); + if search.enable { + super::search::create_files(&search, destination, book)?; + } + } + + self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect) + .context("Unable to emit redirects")?; + + // `src_dir` points to the root source directory. If this book + // is actually multilingual and we specified a single language + // to build on the command line, then `src_dir` will not be + // pointing at the subdirectory with the specified translation's + // index/summary files. We have to append the language + // identifier to prevent the files from the other translations + // from being copied in the final step. + let extra_file_dir = match language { + Some(lang_ident) => { + // my_book/src/ja/ + let mut path = src_dir.clone(); + path.push(lang_ident); + path + } + // my_book/src/ + None => src_dir.clone(), + }; + debug!( + "extra file dir {:?} {:?} {:?}", + extra_file_dir, language, ctx.config + ); + + // Copy all remaining files, avoid a recursive copy from/to the book build dir + utils::fs::copy_files_except_ext( + &extra_file_dir, + &destination, + true, + Some(&build_dir), + &["md"], + )?; + + Ok(()) + } + fn render_item( &self, item: &BookItem, mut ctx: RenderItemContext<'_>, + src_dir: &PathBuf, + language: &Option, + cfg: &Config, print_content: &mut String, ) -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state @@ -54,11 +237,33 @@ impl HtmlHandlebars { .insert("git_repository_edit_url".to_owned(), json!(edit_url)); } - let content = ch.content.clone(); - let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); + let mut md_ctx = match language { + Some(lang_ident) => RenderMarkdownContext { + path: path.clone(), + src_dir: src_dir.clone(), + language: Some(lang_ident.clone()), + fallback_language: cfg.default_language(), + prepend_parent: false, + }, + None => RenderMarkdownContext { + path: path.clone(), + src_dir: src_dir.clone(), + language: None, + fallback_language: None, + prepend_parent: false, + }, + }; - let fixed_content = - utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path)); + let content = ch.content.clone(); + let content = + utils::render_markdown_with_path(&content, ctx.html_config.curly_quotes, Some(&md_ctx)); + + md_ctx.prepend_parent = true; + let fixed_content = utils::render_markdown_with_path( + &ch.content, + ctx.html_config.curly_quotes, + Some(&md_ctx), + ); if !ctx.is_index && ctx.html_config.print.page_break { // 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 @@ -143,11 +348,12 @@ impl HtmlHandlebars { &self, ctx: &RenderContext, html_config: &HtmlConfig, - src_dir: &Path, + src_dir: &PathBuf, + destination: &PathBuf, + language_ident: &Option, handlebars: &mut Handlebars<'_>, data: &mut serde_json::Map, ) -> Result<()> { - let destination = &ctx.destination; let content_404 = if let Some(ref filename) = html_config.input_404 { let path = src_dir.join(filename); std::fs::read_to_string(&path) @@ -161,26 +367,37 @@ impl HtmlHandlebars { })? } else { "# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \ - navigation bar or search to continue." + navigation bar or search to continue." .to_string() } }; let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes); let mut data_404 = data.clone(); - let base_url = if let Some(site_url) = &html_config.site_url { - site_url + let mut base_url = if let Some(site_url) = &html_config.site_url { + site_url.clone() } 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." + this to ensure the 404 page work correctly, especially if your site is hosted in a \ + subdirectory on the HTTP server." ); - "/" + String::from("/") }; + + // Set the subdirectory to the currently localized version if using a + // multilingual output format. + if let LoadedBook::Localized(_) = ctx.book { + if let Some(lang_ident) = language_ident { + base_url.push_str(lang_ident); + base_url.push_str("/"); + } + } + 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("path_to_root".to_owned(), json!("")); data_404.insert("content".to_owned(), json!(html_content_404)); let mut title = String::from("Page not found"); @@ -376,6 +593,10 @@ impl HtmlHandlebars { handlebars.register_helper("next", Box::new(helpers::navigation::next)); // TODO: remove theme_option in 0.5, it is not needed. handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option)); + handlebars.register_helper( + "language_option", + Box::new(helpers::language::language_option), + ); } /// Copy across any additional CSS and JavaScript files which the book @@ -503,12 +724,9 @@ impl Renderer for HtmlHandlebars { } 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 src_dir = ctx.source_dir(); let destination = &ctx.destination; - let book = &ctx.book; - let build_dir = ctx.root.join(&ctx.config.build.build_dir); if destination.exists() { utils::fs::remove_dir_content(destination) @@ -557,93 +775,23 @@ impl Renderer for HtmlHandlebars { 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)?; - - // Print version - let mut print_content = String::new(); - - fs::create_dir_all(destination) - .with_context(|| "Unexpected error when constructing destination path")?; - - let mut is_index = true; - for item in book.iter() { - let ctx = RenderItemContext { - handlebars: &handlebars, - destination: destination.to_path_buf(), - data: data.clone(), - is_index, - book_config: book_config.clone(), - html_config: html_config.clone(), - edition: ctx.config.rust.edition, - chapter_titles: &ctx.chapter_titles, - }; - self.render_item(item, ctx, &mut print_content)?; - // Only the first non-draft chapter item should be treated as the "index" - is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter()); - } - - // Render 404 page - if html_config.input_404 != Some("".to_string()) { - self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?; - } - - // Print version - self.configure_print_version(&mut data, &print_content); - if let Some(ref title) = ctx.config.book.title { - data.insert("title".to_owned(), json!(title)); - } - - // Render the handlebars template with the data - if html_config.print.enable { - debug!("Render template"); - let rendered = handlebars.render("index", &data)?; - - let rendered = self.post_process( - rendered, - &html_config.playground, - &html_config.code, - ctx.config.rust.edition, - ); - - utils::fs::write_file(destination, "print.html", rendered.as_bytes())?; - debug!("Creating print.html ✓"); - } - - debug!("Copy static files"); - self.copy_static_files(destination, &theme, &html_config) - .with_context(|| "Unable to copy across static files")?; - self.copy_additional_css_and_js(&html_config, &ctx.root, destination) - .with_context(|| "Unable to copy across additional CSS and JS")?; - - // Render search index - #[cfg(feature = "search")] - { - let search = html_config.search.unwrap_or_default(); - if search.enable { - super::search::create_files(&search, destination, book)?; - } - } - - 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 - utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?; - - Ok(()) + self.render_books(ctx, &src_dir, &html_config, &mut handlebars, &theme) } } fn make_data( root: &Path, book: &Book, + loaded_book: &LoadedBook, config: &Config, + language_ident: &Option, html_config: &HtmlConfig, theme: &Theme, ) -> Result> { trace!("make_data"); let mut data = serde_json::Map::new(); + data.insert( "language".to_owned(), json!(config.book.language.clone().unwrap_or_default()), @@ -654,11 +802,11 @@ fn make_data( ); data.insert( "book_title".to_owned(), - json!(config.book.title.clone().unwrap_or_default()), + json!(config.get_localized_title(language_ident.as_ref())), ); data.insert( "description".to_owned(), - json!(config.book.description.clone().unwrap_or_default()), + json!(config.get_localized_description(language_ident.as_ref())), ); if theme.favicon_png.is_some() { data.insert("favicon_png".to_owned(), json!("favicon.png")); @@ -767,6 +915,22 @@ fn make_data( }; data.insert("git_repository_icon".to_owned(), json!(git_repository_icon)); + match loaded_book { + LoadedBook::Localized(books) => { + data.insert("languages_enabled".to_owned(), json!(true)); + let mut languages = Vec::new(); + for (lang_ident, _) in books.0.iter() { + languages.push(lang_ident.clone()); + } + languages.sort(); + data.insert("languages".to_owned(), json!(languages)); + data.insert("language_config".to_owned(), json!(config.language.clone())); + } + LoadedBook::Single(_) => { + data.insert("languages_enabled".to_owned(), json!(false)); + } + } + let mut chapters = vec![]; for item in book.iter() { @@ -1150,20 +1314,20 @@ mod tests { #[test] fn add_playground() { let inputs = [ - ("x()", - "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), - ("fn main() {}", - "
fn main() {}
"), - ("let s = \"foo\n # bar\n\";", - "
let s = \"foo\n # bar\n\";
"), - ("let s = \"foo\n ## bar\n\";", - "
let s = \"foo\n ## bar\n\";
"), - ("let s = \"foo\n # bar\n#\n\";", - "
let s = \"foo\n # bar\n#\n\";
"), - ("let s = \"foo\n # bar\n\";", - "let s = \"foo\n # bar\n\";"), - ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", - "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
"), + ("x()", + "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("let s = \"foo\n # bar\n\";", + "
let s = \"foo\n bar\n\";\n
"), + ("let s = \"foo\n ## bar\n\";", + "
let s = \"foo\n # bar\n\";\n
"), + ("let s = \"foo\n # bar\n#\n\";", + "
let s = \"foo\n bar\n\n\";\n
"), + ("let s = \"foo\n # bar\n\";", + "let s = \"foo\n bar\n\";\n"), + ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]\n
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1180,14 +1344,14 @@ mod tests { #[test] fn add_playground_edition2015() { let inputs = [ - ("x()", - "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), + ("x()", + "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1204,14 +1368,14 @@ mod tests { #[test] fn add_playground_edition2018() { let inputs = [ - ("x()", - "
# #![allow(unused)]\n#fn main() {\nx()\n#}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), + ("x()", + "
\n#![allow(unused)]\nfn main() {\nx()\n}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), + ("fn main() {}", + "
fn main() {}\n
"), ]; for (src, should_be) in &inputs { let got = add_playground_pre( diff --git a/src/renderer/html_handlebars/helpers/language.rs b/src/renderer/html_handlebars/helpers/language.rs new file mode 100644 index 0000000000..241c25a31d --- /dev/null +++ b/src/renderer/html_handlebars/helpers/language.rs @@ -0,0 +1,64 @@ +use crate::config::LanguageConfig; +use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError}; +use std::path::Path; +use log::trace; + +pub fn language_option( + h: &Helper<'_, '_>, + _r: &Handlebars<'_>, + ctx: &Context, + rc: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> Result<(), RenderError> { + trace!("language_option (handlebars helper)"); + + let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| { + RenderError::new("Param 0 with String type is required for language_option helper.") + })?; + + let languages = rc.evaluate(ctx, "@root/language_config").and_then(|c| { + serde_json::value::from_value::(c.as_json().clone()) + .map_err(|_| RenderError::new("Could not decode the JSON data")) + })?; + + let current_path = rc + .evaluate(ctx, "@root/path")? + .as_json() + .as_str() + .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? + .replace("\"", ""); + + let rendered_path = Path::new(¤t_path) + .with_extension("html") + .to_str() + .ok_or_else(|| RenderError::new("Path could not be converted to str"))? + .to_string(); + + let path_to_root = rc + .evaluate(ctx, "@root/path_to_root")? + .as_json() + .as_str() + .ok_or_else(|| RenderError::new("Type error for `path_to_root`, string expected"))? + .to_string(); + + let language = languages + .0 + .get(param) + .ok_or_else(|| RenderError::new(format!("Unknown language identifier '{}'", param)))?; + + let mut href = String::new(); + href.push_str(&path_to_root); + href.push_str("../"); + href.push_str(param); + href.push_str("/"); + href.push_str(&rendered_path); + + out.write(&format!( + "")?; + + Ok(()) +} diff --git a/src/renderer/html_handlebars/helpers/mod.rs b/src/renderer/html_handlebars/helpers/mod.rs index 52be6d204b..14256f8d0c 100644 --- a/src/renderer/html_handlebars/helpers/mod.rs +++ b/src/renderer/html_handlebars/helpers/mod.rs @@ -1,3 +1,4 @@ +pub mod language; pub mod navigation; pub mod theme; pub mod toc; diff --git a/src/renderer/markdown_renderer.rs b/src/renderer/markdown_renderer.rs index 4a5a5c2afe..c454814bb9 100644 --- a/src/renderer/markdown_renderer.rs +++ b/src/renderer/markdown_renderer.rs @@ -1,9 +1,10 @@ -use crate::book::BookItem; +use crate::book::{Book, BookItem, LoadedBook}; use crate::errors::*; use crate::renderer::{RenderContext, Renderer}; use crate::utils; use log::trace; use std::fs; +use std::path::Path; #[derive(Default)] /// A renderer to output the Markdown after the preprocessors have run. Mostly useful @@ -31,22 +32,36 @@ impl Renderer for MarkdownRenderer { .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(), - )?; + match book { + LoadedBook::Localized(books) => { + for (lang_ident, book) in books.0.iter() { + let localized_destination = destination.join(lang_ident); + render_book(&localized_destination, book)?; } } + LoadedBook::Single(book) => render_book(destination, &book)?, } - fs::create_dir_all(destination) - .with_context(|| "Unexpected error when constructing destination path")?; - Ok(()) } } + +fn render_book(destination: &Path, book: &Book) -> Result<()> { + fs::create_dir_all(destination) + .with_context(|| "Unexpected error when constructing destination path")?; + + trace!("markdown render"); + for item in book.iter() { + if let BookItem::Chapter(ref ch) = *item { + if !ch.is_draft_chapter() { + utils::fs::write_file( + destination, + &ch.path.as_ref().expect("Checked path exists before"), + ch.content.as_bytes(), + )?; + } + } + } + + Ok(()) +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 1c97f8f221..7d056c088c 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -18,13 +18,13 @@ mod html_handlebars; mod markdown_renderer; use shlex::Shlex; -use std::collections::HashMap; use std::fs; use std::io::{self, ErrorKind, Read}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use crate::book::Book; +use crate::book::LoadedBook; +use crate::build_opts::BuildOpts; use crate::config::Config; use crate::errors::*; use log::{error, info, trace, warn}; @@ -59,8 +59,12 @@ pub struct RenderContext { pub version: String, /// The book's root directory. pub root: PathBuf, - /// A loaded representation of the book itself. - pub book: Book, + /// A loaded representation of the book itself. This can either be a single + /// book or a set of localized books, to allow for the renderer to insert + /// its own logic for handling switching between the localizations. + pub book: LoadedBook, + /// The build options passed from the frontend. + pub build_opts: BuildOpts, /// The loaded configuration file. pub config: Config, /// Where the renderer *must* put any build artefacts generated. To allow @@ -68,25 +72,29 @@ pub struct RenderContext { /// guaranteed to be empty or even exist. pub destination: PathBuf, #[serde(skip)] - pub(crate) chapter_titles: HashMap, - #[serde(skip)] __non_exhaustive: (), } impl RenderContext { /// Create a new `RenderContext`. - pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext + pub fn new( + root: P, + book: LoadedBook, + build_opts: BuildOpts, + config: Config, + destination: Q, + ) -> RenderContext where P: Into, Q: Into, { RenderContext { book, + build_opts, config, version: crate::MDBOOK_VERSION.to_string(), root: root.into(), destination: destination.into(), - chapter_titles: HashMap::new(), __non_exhaustive: (), } } diff --git a/src/theme/book.js b/src/theme/book.js index aa12e7eccf..3a552b0922 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -440,6 +440,89 @@ function playground_text(playground, hidden = true) { }); })(); +(function languages() { + var languageToggleButton = document.getElementById('language-toggle'); + var languagePopup = document.getElementById('language-list'); + + if (languageToggleButton !== null) { + function showLanguages() { + languagePopup.style.display = 'block'; + languageToggleButton.setAttribute('aria-expanded', true); + } + + function hideLanguages() { + languagePopup.style.display = 'none'; + languageToggleButton.setAttribute('aria-expanded', false); + languageToggleButton.focus(); + } + + function set_language(language) { + console.log("Set language " + language) + } + + languageToggleButton.addEventListener('click', function () { + if (languagePopup.style.display === 'block') { + hideLanguages(); + } else { + showLanguages(); + } + }); + + languagePopup.addEventListener('click', function (e) { + var language = e.target.id || e.target.parentElement.id; + set_language(language); + }); + + languagePopup.addEventListener('focusout', function(e) { + // e.relatedTarget is null in Safari and Firefox on macOS (see workaround below) + if (!!e.relatedTarget && !languageToggleButton.contains(e.relatedTarget) && !languagePopup.contains(e.relatedTarget)) { + hideLanguages(); + } + }); + + // 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 (languagePopup.style.display === 'block' && !languageToggleButton.contains(e.target) && !languagePopup.contains(e.target)) { + hideLanguages(); + } + }); + + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (!languagePopup.contains(e.target)) { return; } + + switch (e.key) { + case 'Escape': + e.preventDefault(); + hideLanguages(); + break; + case 'ArrowUp': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.previousElementSibling) { + li.previousElementSibling.querySelector('button').focus(); + } + break; + case 'ArrowDown': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.nextElementSibling) { + li.nextElementSibling.querySelector('button').focus(); + } + break; + case 'Home': + e.preventDefault(); + languagePopup.querySelector('li:first-child button').focus(); + break; + case 'End': + e.preventDefault(); + languagePopup.querySelector('li:last-child button').focus(); + break; + } + }); + } +})(); + (function sidebar() { var body = document.querySelector("body"); var sidebar = document.getElementById("sidebar"); diff --git a/src/theme/css/chrome.css b/src/theme/css/chrome.css index 2314f7a161..eae57d3f44 100644 --- a/src/theme/css/chrome.css +++ b/src/theme/css/chrome.css @@ -585,3 +585,50 @@ ul#searchresults span.teaser em { margin-inline-start: -14px; width: 14px; } + +/* Language Menu Popup */ + +.language-popup { + position: absolute; + left: 150px; + 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; +} +.language-popup .default { + color: var(--icons); +} +.language-popup .language { + width: 100%; + border: 0; + margin: 0; + padding: 2px 10px; + line-height: 25px; + white-space: nowrap; + text-align: left; + cursor: pointer; + color: inherit; + background: inherit; + font-size: inherit; +} +.language-popup .language:hover { + background-color: var(--theme-hover); +} +.language-popup .language:hover:first-child, +.language-popup .language:hover:last-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} + +.language-popup a { + color: var(--fg); + text-decoration: none; +} diff --git a/src/theme/index.hbs b/src/theme/index.hbs index 2ee58c62ee..b6c937ed54 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -162,6 +162,16 @@ {{/if}} + {{#if languages_enabled}} + + + {{/if}}

{{ book_title }}

diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 1c3132162b..50a8917d5e 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -28,7 +28,7 @@ pub fn write_file>(build_dir: &Path, filename: P, content: &[u8]) /// /// ```rust /// # use std::path::Path; -/// # use mdbook::utils::fs::path_to_root; +/// # use mdbook_spacewizards::utils::fs::path_to_root; /// let path = Path::new("some/relative/path"); /// assert_eq!(path_to_root(path), "../../"); /// ``` @@ -47,6 +47,9 @@ pub fn path_to_root>(path: P) -> String { .fold(String::new(), |mut s, c| { match c { Component::Normal(_) => s.push_str("../"), + Component::ParentDir => { + s.truncate(s.len() - 3); + } _ => { debug!("Other path component... {:?}", c); } @@ -245,7 +248,7 @@ pub fn get_404_output_file(input_404: &Option) -> String { #[cfg(test)] mod tests { - use super::copy_files_except_ext; + use super::{copy_files_except_ext, path_to_root}; use std::{fs, io::Result, path::Path}; #[cfg(target_os = "windows")] @@ -325,4 +328,10 @@ mod tests { panic!("output/symlink.png should exist") } } + + #[test] + fn test_path_to_root() { + assert_eq!(path_to_root("some/relative/path"), "../../"); + assert_eq!(path_to_root("some/relative/other/../path"), "../../"); + } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 9156916ea6..415b205b5f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,7 +4,8 @@ pub mod fs; mod string; pub(crate) mod toml_ext; use crate::errors::Error; -use log::error; +use log::{error, debug}; +use lazy_static::lazy_static; use once_cell::sync::Lazy; use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use regex::Regex; @@ -12,13 +13,41 @@ use regex::Regex; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; pub use self::string::{ take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines, take_rustdoc_include_lines, }; +/// Context for rendering markdown. This is used for fixing up links in the +/// output if one is missing in a translation. +#[derive(Clone, Debug)] +pub struct RenderMarkdownContext { + /// Directory of the file being rendered, relative to the language's directory. + /// If the file is "src/en/chapter/README.md", it is "chapter". + pub path: PathBuf, + /// Absolute path to the source directory of the book being rendered, across + /// all languages. + /// If the file is "src/en/chapter/README.md", it is "src/". + pub src_dir: PathBuf, + /// Language of the book being rendered. + /// If the file is "src/en/chapter/README.md", it is "en". + /// If the book is not multilingual, it is `None`. + pub language: Option, + /// Fallback language to use if a link is missing. This is configured in + /// `book.language` in the config. + /// If the book is not multilingual, it is `None`. + pub fallback_language: Option, + /// If true, prepend the parent path to the link. + pub prepend_parent: bool, +} + +lazy_static! { + static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap(); + static ref MD_LINK: Regex = Regex::new(r"(?P.*)\.md(?P#.*)?").unwrap(); +} + /// Replaces multiple consecutive whitespace characters with a single space character. pub fn collapse_whitespace(text: &str) -> Cow<'_, str> { static RE: Lazy = Lazy::new(|| Regex::new(r"\s\s+").unwrap()); @@ -81,27 +110,64 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap>( + fixed_link: &mut String, + path_to_dest: P, + dest: &str, + src_dir: &PathBuf, + language: &str, + fallback_language: &str, +) { + // We are inside a multilingual book. + // + // `fixed_link` is a string relative to the current language directory, like + // "cli/README.md". Prepend the language's source directory (like "src/ja") and see + // if the file exists. + let mut path_on_disk = src_dir.clone(); + path_on_disk.push(language); + path_on_disk.push(path_to_dest.as_ref()); + path_on_disk.push(dest); + + debug!("Checking if {} exists", path_on_disk.display()); + if !path_on_disk.exists() { + // Now see if the file exists in the fallback language directory (like "src/en"). + let mut fallback_path = src_dir.clone(); + fallback_path.push(fallback_language); + fallback_path.push(path_to_dest.as_ref()); + fallback_path.push(dest); + + debug!( + "Not found, checking if fallback {} exists", + fallback_path.display() + ); + if fallback_path.exists() { + // We can fall back to this link. Get enough parent directories to + // reach the root source directory, append the fallback language + // directory to it, the prepend the whole thing to the link. + let mut relative_path = PathBuf::from(path_to_dest.as_ref()); + relative_path.push(dest); + + let mut path_to_fallback_src = fs::path_to_root(&relative_path); + // One more parent directory out of language folder ("en") + write!(path_to_fallback_src, "../{}/", fallback_language).unwrap(); + + debug!( + "Rewriting link to be under fallback: {}", + path_to_fallback_src + ); + fixed_link.insert_str(0, &path_to_fallback_src); + } + } } -/// Fix links to the correct location. -/// -/// This adjusts links, such as turning `.md` extensions to `.html`. -/// -/// `path` is the path to the page being rendered relative to the root of the -/// book. This is used for the `print.html` page so that links on the print -/// page go to the original location. Normal page rendering sets `path` to -/// None. Ideally, print page links would link to anchors on the print page, -/// but that is very difficult. -fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { - static SCHEME_LINK: Lazy = Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap()); - static MD_LINK: Lazy = - Lazy::new(|| Regex::new(r"(?P.*)\.md(?P#.*)?").unwrap()); - - fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> { - if dest.starts_with('#') { - // Fragment-only link. - if let Some(path) = path { - let mut base = path.display().to_string(); +fn fix<'a>(dest: CowStr<'a>, ctx: Option<&RenderMarkdownContext>) -> CowStr<'a> { + if dest.starts_with('#') { + // Fragment-only link. + if let Some(ctx) = ctx { + if ctx.prepend_parent { + let mut base = ctx.path.display().to_string(); if base.ends_with(".md") { base.replace_range(base.len() - 3.., ".html"); } @@ -109,65 +175,99 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { } else { return dest; } + } else { + return dest; } - // Don't modify links with schemes like `https`. - if !SCHEME_LINK.is_match(&dest) { - // This is a relative link, adjust it as necessary. - let mut fixed_link = String::new(); - if let Some(path) = path { - let base = path - .parent() - .expect("path can't be empty") - .to_str() - .expect("utf-8 paths only"); - if !base.is_empty() { - write!(fixed_link, "{}/", base).unwrap(); + } + // Don't modify links with schemes like `https`. + if !SCHEME_LINK.is_match(&dest) { + // This is a relative link, adjust it as necessary. + let mut fixed_link = String::new(); + + if let Some(ctx) = ctx { + let base = ctx.path.parent().expect("path can't be empty"); + + // If the book is multilingual, check if the file actually + // exists, and if not rewrite the link to the fallback + // language's page. + if let Some(language) = &ctx.language { + if let Some(fallback_language) = &ctx.fallback_language { + rewrite_if_missing( + &mut fixed_link, + &base, + &dest, + &ctx.src_dir, + &language, + &fallback_language, + ); } } - if let Some(caps) = MD_LINK.captures(&dest) { - fixed_link.push_str(&caps["link"]); - fixed_link.push_str(".html"); - if let Some(anchor) = caps.name("anchor") { - fixed_link.push_str(anchor.as_str()); + if ctx.prepend_parent { + let base = base.to_str().expect("utf-8 paths only"); + if !base.is_empty() { + write!(fixed_link, "{}/", base).unwrap(); } - } else { - fixed_link.push_str(&dest); - }; - return CowStr::from(fixed_link); + } } - dest + + if let Some(caps) = MD_LINK.captures(&dest) { + fixed_link.push_str(&caps["link"]); + fixed_link.push_str(".html"); + if let Some(anchor) = caps.name("anchor") { + fixed_link.push_str(anchor.as_str()); + } + } else { + fixed_link.push_str(&dest); + }; + + debug!("Fixed link: {:?}, {:?} => {:?}", dest, ctx, fixed_link); + return CowStr::from(fixed_link); } + dest +} - fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> { - // This is a terrible hack, but should be reasonably reliable. Nobody - // should ever parse a tag with a regex. However, there isn't anything - // in Rust that I know of that is suitable for handling partial html - // fragments like those generated by pulldown_cmark. - // - // There are dozens of HTML tags/attributes that contain paths, so - // feel free to add more tags if desired; these are the only ones I - // care about right now. - static HTML_LINK: Lazy = - Lazy::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap()); - - HTML_LINK - .replace_all(&html, |caps: ®ex::Captures<'_>| { - let fixed = fix(caps[2].into(), path); - format!("{}{}\"", &caps[1], fixed) - }) - .into_owned() - .into() +fn fix_html<'a>(html: CowStr<'a>, ctx: Option<&RenderMarkdownContext>) -> CowStr<'a> { + // This is a terrible hack, but should be reasonably reliable. Nobody + // should ever parse a tag with a regex. However, there isn't anything + // in Rust that I know of that is suitable for handling partial html + // fragments like those generated by pulldown_cmark. + // + // There are dozens of HTML tags/attributes that contain paths, so + // feel free to add more tags if desired; these are the only ones I + // care about right now. + lazy_static! { + static ref HTML_LINK: Regex = + Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap(); } + HTML_LINK + .replace_all(&html, move |caps: ®ex::Captures<'_>| { + let fixed = fix(caps[2].into(), ctx); + format!("{}{}\"", &caps[1], fixed) + }) + .into_owned() + .into() +} + +/// Fix links to the correct location. +/// +/// This adjusts links, such as turning `.md` extensions to `.html`. +/// +/// `path` is the path to the page being rendered relative to the root of the +/// book. This is used for the `print.html` page so that links on the print +/// page go to the original location. Normal page rendering sets `path` to +/// None. Ideally, print page links would link to anchors on the print page, +/// but that is very difficult. +fn adjust_links<'a>(event: Event<'a>, ctx: Option<&RenderMarkdownContext>) -> Event<'a> { match event { Event::Start(Tag::Link(link_type, dest, title)) => { - Event::Start(Tag::Link(link_type, fix(dest, path), title)) + Event::Start(Tag::Link(link_type, fix(dest, ctx), title)) } Event::Start(Tag::Image(link_type, dest, title)) => { - Event::Start(Tag::Image(link_type, fix(dest, path), title)) + Event::Start(Tag::Image(link_type, fix(dest, ctx), title)) } - Event::Html(html) => Event::Html(fix_html(html, path)), + Event::Html(html) => Event::Html(fix_html(html, ctx)), _ => event, } } @@ -190,12 +290,16 @@ pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> { Parser::new_ext(text, opts) } -pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String { +pub fn render_markdown_with_path( + text: &str, + curly_quotes: bool, + ctx: Option<&RenderMarkdownContext>, +) -> String { let mut s = String::with_capacity(text.len() * 3 / 2); let p = new_cmark_parser(text, curly_quotes); let events = p .map(clean_codeblock_headers) - .map(|event| adjust_links(event, path)) + .map(|event| adjust_links(event, ctx)) .flat_map(|event| { let (a, b) = wrap_tables(event); a.into_iter().chain(b) @@ -265,7 +369,7 @@ mod tests { use super::bracket_escape; mod render_markdown { - use super::super::render_markdown; + use super::super::{fix, render_markdown, RenderMarkdownContext}; #[test] fn preserves_external_links() { @@ -398,6 +502,77 @@ more text with spaces assert_eq!(render_markdown(input, false), expected); assert_eq!(render_markdown(input, true), expected); } + + use std::fs; + use std::fs::File; + use std::io::Write; + use std::path::PathBuf; + use tempfile; + + #[test] + fn test_link_rewriting() { + use pulldown_cmark::CowStr; + + let _ = env_logger::builder().is_test(true).try_init(); + let test = |dest, path, exists, expected| { + let src_dir = tempfile::tempdir().unwrap(); + let path = PathBuf::from(path); + + let ctx = if exists { + Some(RenderMarkdownContext { + path: path, + src_dir: PathBuf::new(), + language: None, + fallback_language: None, + prepend_parent: false, + }) + } else { + let localized_dir = src_dir.path().join("ja"); + fs::create_dir_all(&localized_dir).unwrap(); + + let fallback_dir = src_dir.path().join("en"); + fs::create_dir_all(&fallback_dir).unwrap(); + + let chapter_path = fallback_dir.join(path.parent().unwrap()).join(dest); + fs::create_dir_all(chapter_path.parent().unwrap()).unwrap(); + debug!("Create: {}", chapter_path.display()); + File::create(&chapter_path) + .unwrap() + .write_all(b"# Chapter") + .unwrap(); + + Some(RenderMarkdownContext { + path: path, + src_dir: PathBuf::from(src_dir.path()), + language: Some(String::from("ja")), + fallback_language: Some(String::from("en")), + prepend_parent: false, + }) + }; + + assert_eq!( + fix(CowStr::from(dest), ctx.as_ref()), + CowStr::from(expected) + ); + }; + + test("../b/summary.md", "a/index.md", true, "../b/summary.html"); + test( + "../b/summary.md", + "a/index.md", + false, + "../../en/../b/summary.html", + ); + test("../c/summary.md", "a/b/index.md", true, "../c/summary.html"); + test( + "../c/summary.md", + "a/b/index.md", + false, + "../../../en/../c/summary.html", + ); + test("#translations", "config.md", true, "#translations"); + test("#translations", "config.md", false, "#translations"); + } } #[allow(deprecated)] diff --git a/tests/alternative_backends.rs b/tests/alternative_backends.rs index 72594e5762..29471a8da2 100644 --- a/tests/alternative_backends.rs +++ b/tests/alternative_backends.rs @@ -1,7 +1,7 @@ //! Integration tests to make sure alternative backends work. -use mdbook::config::Config; -use mdbook::MDBook; +use mdbook_spacewizards::config::Config; +use mdbook_spacewizards::MDBook; use std::fs; use std::path::Path; use tempfile::{Builder as TempFileBuilder, TempDir}; @@ -54,7 +54,7 @@ fn tee_command>(out_file: P) -> String { #[test] #[cfg(not(windows))] fn backends_receive_render_context_via_stdin() { - use mdbook::renderer::RenderContext; + use mdbook_spacewizards::renderer::RenderContext; use std::fs::File; let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap(); diff --git a/tests/build_process.rs b/tests/build_process.rs index 10d0b4a9a8..bfde45b91b 100644 --- a/tests/build_process.rs +++ b/tests/build_process.rs @@ -1,12 +1,13 @@ mod dummy_book; use crate::dummy_book::DummyBook; -use mdbook::book::Book; -use mdbook::config::Config; -use mdbook::errors::*; -use mdbook::preprocess::{Preprocessor, PreprocessorContext}; -use mdbook::renderer::{RenderContext, Renderer}; -use mdbook::MDBook; +use mdbook_spacewizards::book::Book; +use mdbook_spacewizards::build_opts::BuildOpts; +use mdbook_spacewizards::config::Config; +use mdbook_spacewizards::errors::*; +use mdbook_spacewizards::preprocess::{Preprocessor, PreprocessorContext}; +use mdbook_spacewizards::renderer::{RenderContext, Renderer}; +use mdbook_spacewizards::MDBook; use std::sync::{Arc, Mutex}; struct Spy(Arc>); @@ -48,8 +49,9 @@ fn mdbook_runs_preprocessors() { let temp = DummyBook::new().build().unwrap(); let cfg = Config::default(); + let build_opts = BuildOpts::default(); - let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let mut book = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); book.with_preprocessor(Spy(Arc::clone(&spy))); book.build().unwrap(); @@ -68,8 +70,9 @@ fn mdbook_runs_renderers() { let temp = DummyBook::new().build().unwrap(); let cfg = Config::default(); + let build_opts = BuildOpts::default(); - let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let mut book = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); book.with_renderer(Spy(Arc::clone(&spy))); book.build().unwrap(); diff --git a/tests/cli/init.rs b/tests/cli/init.rs index 6bd1227437..8b19a1498b 100644 --- a/tests/cli/init.rs +++ b/tests/cli/init.rs @@ -1,7 +1,7 @@ use crate::cli::cmd::mdbook_cmd; use crate::dummy_book::DummyBook; -use mdbook::config::Config; +use mdbook_spacewizards::config::Config; /// Run `mdbook init` with `--force` to skip the confirmation prompts #[test] diff --git a/tests/custom_preprocessors.rs b/tests/custom_preprocessors.rs index 8237602de0..7b82cc98cb 100644 --- a/tests/custom_preprocessors.rs +++ b/tests/custom_preprocessors.rs @@ -1,8 +1,8 @@ mod dummy_book; use crate::dummy_book::DummyBook; -use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; -use mdbook::MDBook; +use mdbook_spacewizards::preprocess::{CmdPreprocessor, Preprocessor}; +use mdbook_spacewizards::MDBook; fn example() -> CmdPreprocessor { CmdPreprocessor::new( diff --git a/tests/dummy_book/mod.rs b/tests/dummy_book/mod.rs index f91ed9f075..977f4a2c57 100644 --- a/tests/dummy_book/mod.rs +++ b/tests/dummy_book/mod.rs @@ -5,8 +5,8 @@ #![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)] use anyhow::Context; -use mdbook::errors::*; -use mdbook::MDBook; +use mdbook_spacewizards::errors::*; +use mdbook_spacewizards::MDBook; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::Path; diff --git a/tests/init.rs b/tests/init.rs index 2b6ad507ce..d88f5ca8a2 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -1,5 +1,5 @@ -use mdbook::config::Config; -use mdbook::MDBook; +use mdbook_spacewizards::config::Config; +use mdbook_spacewizards::MDBook; use pretty_assertions::assert_eq; use std::fs; use std::fs::File; @@ -11,7 +11,13 @@ use tempfile::Builder as TempFileBuilder; /// are created. #[test] fn base_mdbook_init_should_create_default_content() { - let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"]; + let created_files = vec![ + "book", + "src", + "src/en", + "src/en/SUMMARY.md", + "src/en/chapter_1.md", + ]; let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); for file in &created_files { @@ -29,7 +35,7 @@ fn base_mdbook_init_should_create_default_content() { let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap(); assert_eq!( contents, - "[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\n" + "[book]\nauthors = []\nlanguage = \"en\"\nsrc = \"src\"\n[language.en]\nname = \"English\"\n" ); } @@ -40,7 +46,7 @@ fn run_mdbook_init_should_create_content_from_summary() { let created_files = vec!["intro.md", "first.md", "outro.md"]; let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); - let src_dir = temp.path().join("src"); + let src_dir = temp.path().join("src").join("en"); fs::create_dir_all(src_dir.clone()).unwrap(); static SUMMARY: &str = r#"# Summary @@ -67,7 +73,13 @@ fn run_mdbook_init_should_create_content_from_summary() { /// files, then call `mdbook init`. #[test] fn run_mdbook_init_with_custom_book_and_src_locations() { - let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"]; + let created_files = vec![ + "out", + "in", + "in/en", + "in/en/SUMMARY.md", + "in/en/chapter_1.md", + ]; let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); for file in &created_files { @@ -96,7 +108,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() { let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap(); assert_eq!( contents, - "[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nextra-watch-dirs = []\nuse-default-preprocessors = true\n" + "[book]\nauthors = []\nlanguage = \"en\"\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nuse-default-preprocessors = true\n[language.en]\nname = \"English\"\n" ); } diff --git a/tests/localized_book/book.toml b/tests/localized_book/book.toml new file mode 100644 index 0000000000..03199a0c1d --- /dev/null +++ b/tests/localized_book/book.toml @@ -0,0 +1,35 @@ +[book] +title = "Localized Book" +description = "Testing mdBook localization features" +authors = ["Ruin0x11"] +language = "en" + +[rust] +edition = "2018" + +[output.html] +mathjax-support = true +site-url = "/mdBook/" + +[output.html.playground] +editable = true +line-numbers = true + +[output.html.search] +limit-results = 20 +use-boolean-and = true +boost-title = 2 +boost-hierarchy = 2 +boost-paragraph = 1 +expand = true +heading-split-level = 2 + +[language.en] +name = "English" +default = true + +[language.ja] +name = "日本語" +title = "翻訳された本" +description = "mdBookの翻訳機能のテスト用の本" +authors = ["Ruin0x11"] diff --git a/tests/localized_book/src/en/README.md b/tests/localized_book/src/en/README.md new file mode 100644 index 0000000000..b3f713ba9c --- /dev/null +++ b/tests/localized_book/src/en/README.md @@ -0,0 +1,5 @@ +# Localized Book + +This is a test of the book localization features. + +Select a language from the dropdown to see a translation of the current page. diff --git a/tests/localized_book/src/en/SUMMARY.md b/tests/localized_book/src/en/SUMMARY.md new file mode 100644 index 0000000000..e94726f6ce --- /dev/null +++ b/tests/localized_book/src/en/SUMMARY.md @@ -0,0 +1,9 @@ +# Summary + +- [README](README.md) +- [Chapter 1](chapter/README.md) + - [Section 1](chapter/1.md) + - [Section 2](chapter/2.md) +- [Untranslated Page](untranslated-page.md) +- [Inline Link Fallbacks](inline-link-fallbacks.md) +- [Missing Summary Chapter](missing-summary-chapter.md) diff --git a/tests/localized_book/src/en/chapter/1.md b/tests/localized_book/src/en/chapter/1.md new file mode 100644 index 0000000000..a0e9a83157 --- /dev/null +++ b/tests/localized_book/src/en/chapter/1.md @@ -0,0 +1,2 @@ +# First section. + diff --git a/tests/localized_book/src/en/chapter/2.md b/tests/localized_book/src/en/chapter/2.md new file mode 100644 index 0000000000..17378866a7 --- /dev/null +++ b/tests/localized_book/src/en/chapter/2.md @@ -0,0 +1,2 @@ +# Second section. + diff --git a/tests/localized_book/src/en/chapter/3.md b/tests/localized_book/src/en/chapter/3.md new file mode 100644 index 0000000000..6e68b92df8 --- /dev/null +++ b/tests/localized_book/src/en/chapter/3.md @@ -0,0 +1 @@ +# 第三節 diff --git a/tests/localized_book/src/en/chapter/README.md b/tests/localized_book/src/en/chapter/README.md new file mode 100644 index 0000000000..0809d65017 --- /dev/null +++ b/tests/localized_book/src/en/chapter/README.md @@ -0,0 +1 @@ +# First chapter page. diff --git a/tests/localized_book/src/en/example.rs b/tests/localized_book/src/en/example.rs new file mode 100644 index 0000000000..6b49705c15 --- /dev/null +++ b/tests/localized_book/src/en/example.rs @@ -0,0 +1,6 @@ +fn main() { + println!("Hello World!"); +# +# // You can even hide lines! :D +# println!("I am hidden! Expand the code snippet to see me"); +} diff --git a/tests/localized_book/src/en/inline-link-fallbacks.md b/tests/localized_book/src/en/inline-link-fallbacks.md new file mode 100644 index 0000000000..b789edaadf --- /dev/null +++ b/tests/localized_book/src/en/inline-link-fallbacks.md @@ -0,0 +1,7 @@ +# Inline Link Fallbacks + +This page tests localization fallbacks of inline links. + +Select another language from the dropdown to see a demonstation. + +![Rust logo](rust_logo.png) diff --git a/tests/localized_book/src/en/missing-summary-chapter.md b/tests/localized_book/src/en/missing-summary-chapter.md new file mode 100644 index 0000000000..86a3329be5 --- /dev/null +++ b/tests/localized_book/src/en/missing-summary-chapter.md @@ -0,0 +1,3 @@ +# Missing Summary Chapter + +This page is to test that inline links to a page missing in a translation's SUMMARY.md redirect to the page in the fallback translation. diff --git a/tests/localized_book/src/en/rust_logo.png b/tests/localized_book/src/en/rust_logo.png new file mode 100644 index 0000000000..11c6de63c3 Binary files /dev/null and b/tests/localized_book/src/en/rust_logo.png differ diff --git a/tests/localized_book/src/en/untranslated-page.md b/tests/localized_book/src/en/untranslated-page.md new file mode 100644 index 0000000000..ad7f3d454d --- /dev/null +++ b/tests/localized_book/src/en/untranslated-page.md @@ -0,0 +1,3 @@ +# Untranslated page. + +This page is not available in any translation. If things work correctly, you should see this page written in the fallback language (English) if the other translations list it on their summary page. diff --git a/tests/localized_book/src/ja/README.md b/tests/localized_book/src/ja/README.md new file mode 100644 index 0000000000..77229d775d --- /dev/null +++ b/tests/localized_book/src/ja/README.md @@ -0,0 +1,3 @@ +# 本の翻訳 + +これは本の翻訳のテストです。 diff --git a/tests/localized_book/src/ja/SUMMARY.md b/tests/localized_book/src/ja/SUMMARY.md new file mode 100644 index 0000000000..0aacc6e538 --- /dev/null +++ b/tests/localized_book/src/ja/SUMMARY.md @@ -0,0 +1,9 @@ +# 目次 + +- [README](README.md) +- [第一章](chapter/README.md) + - [第一節](chapter/1.md) + - [第二節](chapter/2.md) +- [Untranslated Page](untranslated-page.md) +- [内部リンクの入れ替え](inline-link-fallbacks.md) +- [日本語専用のページ](translation-local-page.md) diff --git a/tests/localized_book/src/ja/chapter/1.md b/tests/localized_book/src/ja/chapter/1.md new file mode 100644 index 0000000000..eae183f142 --- /dev/null +++ b/tests/localized_book/src/ja/chapter/1.md @@ -0,0 +1 @@ +# 第一節。 diff --git a/tests/localized_book/src/ja/chapter/2.md b/tests/localized_book/src/ja/chapter/2.md new file mode 100644 index 0000000000..f23ac37be9 --- /dev/null +++ b/tests/localized_book/src/ja/chapter/2.md @@ -0,0 +1 @@ +# 第二節。 diff --git a/tests/localized_book/src/ja/chapter/README.md b/tests/localized_book/src/ja/chapter/README.md new file mode 100644 index 0000000000..00e762ff0d --- /dev/null +++ b/tests/localized_book/src/ja/chapter/README.md @@ -0,0 +1 @@ +# 第一章のページ。 diff --git a/tests/localized_book/src/ja/inline-link-fallbacks.md b/tests/localized_book/src/ja/inline-link-fallbacks.md new file mode 100644 index 0000000000..a295353c9c --- /dev/null +++ b/tests/localized_book/src/ja/inline-link-fallbacks.md @@ -0,0 +1,20 @@ +# 内部リンクの入れ替え + +以下のイメージは英語バージョンから移植されたでしょうか。 + +If inline link substitution works, then an image should appear below, sourced from the English translation. + +![Rust logo](rust_logo.png) + +Here is an [inline link](translation-local-page.md) to an existing page in this translation. + +Here is an [inline link](missing-summary-chapter.md) to a page missing from this translation's `SUMMARY.md`. It should have been modified to point to the page in the English version of the book. + +Also, here is an [inline link](blah.md) to a page missing from both translations. It should point to this language's 404 page. + +Here is a file included from the default language. +```rust +{{ #include example.rs }} +``` + +The substitution won't work if you specify the `-l`/`--language` option, since it only builds a single translation in that case. diff --git a/tests/localized_book/src/ja/translation-local-page.md b/tests/localized_book/src/ja/translation-local-page.md new file mode 100644 index 0000000000..92a93a687a --- /dev/null +++ b/tests/localized_book/src/ja/translation-local-page.md @@ -0,0 +1,5 @@ +# 日本語専用のページ + +実は、このページは英語バージョンに存在しません。 + +This page doesn't exist in the English translation. It is unique to this translation only. diff --git a/tests/parse_existing_summary_files.rs b/tests/parse_existing_summary_files.rs index 418ec31fee..6d7c0c66ec 100644 --- a/tests/parse_existing_summary_files.rs +++ b/tests/parse_existing_summary_files.rs @@ -1,7 +1,7 @@ //! Some integration tests to make sure the `SUMMARY.md` parser can deal with //! some real-life examples. -use mdbook::book; +use mdbook_spacewizards::book; use std::fs::File; use std::io::Read; use std::path::Path; diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 7626b9e8ac..c12a3430f0 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -3,10 +3,11 @@ mod dummy_book; use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook}; use anyhow::Context; -use mdbook::config::Config; -use mdbook::errors::*; -use mdbook::utils::fs::write_file; -use mdbook::MDBook; +use mdbook_spacewizards::build_opts::BuildOpts; +use mdbook_spacewizards::config::Config; +use mdbook_spacewizards::errors::*; +use mdbook_spacewizards::utils::fs::write_file; +use mdbook_spacewizards::MDBook; use pretty_assertions::assert_eq; use select::document::Document; use select::predicate::{Class, Name, Predicate}; @@ -346,8 +347,9 @@ fn failure_on_missing_file() { let mut cfg = Config::default(); cfg.build.create_missing = false; + let build_opts = BuildOpts::default(); - let got = MDBook::load_with_config(temp.path(), cfg); + let got = MDBook::load_with_config(temp.path(), cfg, build_opts); assert!(got.is_err()); } @@ -359,9 +361,10 @@ fn create_missing_file_with_config() { let mut cfg = Config::default(); cfg.build.create_missing = true; + let build_opts = BuildOpts::default(); assert!(!temp.path().join("src").join("intro.md").exists()); - let _md = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let _md = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); assert!(temp.path().join("src").join("intro.md").exists()); } @@ -449,7 +452,8 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() { let mut cfg = Config::default(); cfg.set("book.src", "src2") .expect("Couldn't set config.book.src to \"src2\"."); - let md = MDBook::load_with_config(temp.path(), cfg).unwrap(); + let build_opts = BuildOpts::default(); + let md = MDBook::load_with_config(temp.path(), cfg, build_opts).unwrap(); md.build().unwrap(); let first_index = temp.path().join("book").join("first").join("index.html"); @@ -495,7 +499,7 @@ fn theme_dir_overrides_work_correctly() { let md = MDBook::load(book_dir).unwrap(); md.build().unwrap(); - let built_index = book_dir.join("book").join("index.html"); + let built_index = book_dir.join("book").join("en").join("index.html"); dummy_book::assert_contains_strings(built_index, &["This is a modified index.hbs!"]); } @@ -723,7 +727,7 @@ fn failure_on_missing_theme_directory() { #[cfg(feature = "search")] mod search { use crate::dummy_book::DummyBook; - use mdbook::MDBook; + use mdbook_spacewizards::MDBook; use std::fs::{self, File}; use std::path::Path; diff --git a/tests/testing.rs b/tests/testing.rs index 3030c5cb66..6c5271697f 100644 --- a/tests/testing.rs +++ b/tests/testing.rs @@ -2,12 +2,12 @@ mod dummy_book; use crate::dummy_book::DummyBook; -use mdbook::MDBook; +use mdbook_spacewizards::MDBook; #[test] fn mdbook_can_correctly_test_a_passing_book() { let temp = DummyBook::new().with_passing_test(true).build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); let result = md.test(vec![]); assert!( @@ -20,7 +20,7 @@ fn mdbook_can_correctly_test_a_passing_book() { #[test] fn mdbook_detects_book_with_failing_tests() { let temp = DummyBook::new().with_passing_test(false).build().unwrap(); - let mut md = MDBook::load(temp.path()).unwrap(); + let md = MDBook::load(temp.path()).unwrap(); assert!(md.test(vec![]).is_err()); }