From 2c3df28571163d3a3f0d125c5b7af9e68da3f5a6 Mon Sep 17 00:00:00 2001 From: Kara Date: Thu, 28 Sep 2023 10:48:07 -0500 Subject: [PATCH] Integrate https://github.com/rust-lang/mdBook/pull/1306 commit 8664faea083017b1ec7c9d811be28427b8408bef Author: Kara Date: Thu Sep 28 10:47:37 2023 -0500 Update for new mdbook version commit 1b45e7a7a6521b4df6d441788a7fff105eba9240 Merge: e74fdb1 79edc75 Author: Kara Date: Thu Sep 28 10:03:55 2023 -0500 Merge branch 'master' into localization # Conflicts: # Cargo.lock # Cargo.toml # src/book/book.rs # src/book/init.rs # src/book/mod.rs # src/cmd/build.rs # src/cmd/clean.rs # src/cmd/serve.rs # src/cmd/test.rs # src/cmd/watch.rs # src/config.rs # src/preprocess/links.rs # src/renderer/html_handlebars/hbs_renderer.rs # src/renderer/markdown_renderer.rs # src/utils/mod.rs # tests/init.rs commit e74fdb10725d30917ce1b6b39c968730133ba7ee Author: Ruin0x11 Date: Fri Feb 25 14:30:38 2022 -0800 Make `chapter_titles` optional in Book commit 7305e8c60d1c5bf54a05a2289c027905ee13641f Merge: 9d8147c 5921f59 Author: Ruin0x11 Date: Fri Feb 25 14:13:22 2022 -0800 Merge remote-tracking branch 'upstream/master' into localization # Conflicts: # .gitignore # guide/src/en/cli/completions.md # guide/src/en/format/images/rust-logo-blk.svg # guide/src/en/format/markdown.md # guide/src/en/misc/introduction.md # src/renderer/html_handlebars/hbs_renderer.rs # src/utils/mod.rs commit 9d8147c52dd9d50047ba5b29e4af99f92577806e Author: Ruin0x11 Date: Wed Sep 15 21:49:58 2021 -0700 Remove extra `localization.md` commit 56e72a223807a74b7a03c95046edf7b34feca620 Author: Ruin0x11 Date: Wed Sep 15 15:33:28 2021 -0700 [localization] rustfmt commit 92ec3ddc55fe8137613bb9532cf937a2c47ec0d3 Author: Ruin0x11 Date: Wed Sep 15 15:25:31 2021 -0700 [localization] Fixes for latest master commit d6c27abc22a3c7aea349f965ffab6cea5cfa7360 Author: Ruin0x11 Date: Sat Aug 29 16:11:47 2020 -0700 Implement translation fallback of files included with preprocessing commit 5fed5e866dad28322f401617e373ebba68dde606 Author: Ruin0x11 Date: Wed Sep 15 14:29:30 2021 -0700 Update mdBook manual to have information about translations commit 09a8b66e87abf23f547a71b2a2d5bc81f708a189 Author: Ruin0x11 Date: Sat Aug 29 14:41:08 2020 -0700 Improve robustness of link rewriting commit 8d1c0869b7c14fc87a4beedc4a4394fc594a1e84 Author: Ruin0x11 Date: Fri Aug 28 16:33:02 2020 -0700 Fix {{#include}} directives for default language commit 98c3a040228d5e98165bc039d41322d1716c7edf Author: Ruin0x11 Date: Fri Aug 28 16:11:21 2020 -0700 Move example book to multilingual structure commit c72ce182d55f39c1e1a0db96b8abe3cf4ee51fe8 Author: Ruin0x11 Date: Fri Aug 28 14:50:04 2020 -0700 Rewrite links in Markdown to point to fallback if missing in translation It will follow relative links to other pages and embedded images. commit ee740aceff6257964a6f517a815b70ab9c754eea Author: Ruin0x11 Date: Fri Aug 28 12:26:08 2020 -0700 Remove 'default' property on languages, use book.language instead commit a042cfc72b0a100b7f0e411cbce5ccde2afeaa50 Author: Ruin0x11 Date: Fri Aug 28 11:35:42 2020 -0700 Make `mdbook init` output multilingual structure commit 5e223e074e11a8ab6de0376d96829936eeca99a2 Author: Ruin0x11 Date: Fri Aug 28 03:17:26 2020 -0700 Support localizing book title/description commit e17ce64f2bea394dfc834b2476b9f0b3d7383d9e Author: Ruin0x11 Date: Fri Aug 28 02:29:07 2020 -0700 Fix test using create_missing commit 282fdaa3ac5ce4708064d1545e3a6f180dda32ba Author: Ruin0x11 Date: Fri Aug 28 02:05:21 2020 -0700 Redirect to a 404 page when serving translated We can't redirect in warp based on the URL, so redirect to the default language's 404 page instead. See: https://github.com/seanmonstar/warp/issues/171 commit 85ab4d39cd0fd6fc09903edc32621adab068ec31 Author: Ruin0x11 Date: Fri Aug 28 01:36:22 2020 -0700 Redirect to translation index page in serve command commit 8869c2cf065c013500b397b92b06a6bf9b095463 Author: Ruin0x11 Date: Fri Aug 28 00:24:33 2020 -0700 Build multiple books from localizations at once Changes how the `book` module loads books. Now it is possible to load all of the translations of a book and put them into a single output folder. If a book is generated this way, a menu will be created in the handlebars renderer for switching between languages. commit 96d9271d6409e86e3ef6e59b1491fc67c84797ee Author: Ruin0x11 Date: Thu Aug 27 19:44:24 2020 -0700 Specify language for book in command line args - Add a [language] table to book.toml. Each key in the table defines a new language with `name` and `default` properties. - Changes the directory structure of localized books. If the [language] table exists, mdBook will now assume the src/ directory contains subdirectories named after the keys in [language]. The behavior is backwards-compatible if you don't specify [language]. - Specify which language of book to build using the -l/--language argument to `mdbook build` and similar, or omit to use the default language. - Specify the default language by setting the `default` property to `true` in an entry in [language]. Exactly one language must have `default` set to `true` if the [language] table is defined. - Each language has its own SUMMARY.md. It can include links to files not in other translations. If a link in SUMMARY.md refers to a nonexistent file that is specified in the default language, the renderer will gracefully degrade the link to the default language's page. If it still doesn't exist, the config's `create_missing` option will be respected instead. commit 3049d9f10327d33bb3ff1cb5a4d6ad7fddd5bb20 Author: Ruin0x11 Date: Thu Aug 27 16:35:00 2020 -0700 Actually, don't change source root The book paths have to gracefully degrade to the default language if they aren't available. commit 24e6d6b6055fcf31c4ba65746979e304e2408b8b Author: Ruin0x11 Date: Thu Aug 27 16:26:07 2020 -0700 Change book source root depending on language commit e4b443cd4a98e786132ce742c766ccd959e74e56 Author: Ruin0x11 Date: Thu Aug 27 13:27:47 2020 -0700 Add language config section Referencing https://github.com/rust-lang/mdBook/issues/5#issuecomment-323573492. --- .gitignore | 1 + Cargo.lock | 602 ++++++++---------- Cargo.toml | 2 + examples/nop-preprocessor.rs | 12 +- guide/book.toml | 3 + guide/src/{ => en}/404.md | 0 guide/src/{ => en}/README.md | 0 guide/src/{ => en}/SUMMARY.md | 1 + guide/src/{ => en}/cli/README.md | 0 guide/src/{ => en}/cli/build.md | 0 guide/src/{ => en}/cli/clean.md | 0 guide/src/{ => en}/cli/completions.md | 0 guide/src/{ => en}/cli/init.md | 14 +- guide/src/{ => en}/cli/serve.md | 0 guide/src/{ => en}/cli/test.md | 0 guide/src/{ => en}/cli/watch.md | 0 guide/src/{ => en}/continuous-integration.md | 0 guide/src/{ => en}/for_developers/README.md | 0 guide/src/{ => en}/for_developers/backends.md | 6 +- .../mdbook-wordcount/Cargo.toml | 0 .../mdbook-wordcount/src/main.rs | 4 +- .../{ => en}/for_developers/preprocessors.md | 2 +- guide/src/{ => en}/format/README.md | 0 .../{ => en}/format/configuration/README.md | 4 +- .../configuration/environment-variables.md | 0 .../{ => en}/format/configuration/general.md | 0 .../en/format/configuration/localization.md | 86 +++ .../format/configuration/preprocessors.md | 0 .../format/configuration/renderers.md | 0 guide/src/{ => en}/format/example.rs | 0 .../{ => en}/format/images/rust-logo-blk.svg | 0 guide/src/{ => en}/format/markdown.md | 0 guide/src/{ => en}/format/mathjax.md | 0 guide/src/{ => en}/format/mdbook.md | 0 guide/src/{ => en}/format/summary.md | 0 guide/src/{ => en}/format/theme/README.md | 0 guide/src/{ => en}/format/theme/editor.md | 0 guide/src/{ => en}/format/theme/index-hbs.md | 0 .../format/theme/syntax-highlighting.md | 0 guide/src/{ => en}/guide/README.md | 0 guide/src/{ => en}/guide/creating.md | 0 guide/src/{ => en}/guide/installation.md | 0 guide/src/{ => en}/guide/reading.md | 0 guide/src/{ => en}/misc/contributors.md | 1 + src/book/book.rs | 319 ++++++++-- src/book/init.rs | 43 +- src/book/mod.rs | 205 ++++-- src/build_opts.rs | 12 + src/cmd/build.rs | 11 +- src/cmd/clean.rs | 10 +- src/cmd/command_prelude.rs | 15 + src/cmd/init.rs | 8 +- src/cmd/serve.rs | 77 ++- src/cmd/test.rs | 14 +- src/cmd/watch.rs | 22 +- src/config.rs | 261 +++++++- src/lib.rs | 7 +- src/main.rs | 11 +- src/preprocess/cmd.rs | 8 +- src/preprocess/index.rs | 2 +- src/preprocess/links.rs | 90 ++- src/preprocess/mod.rs | 21 +- src/renderer/html_handlebars/hbs_renderer.rs | 414 ++++++++---- .../html_handlebars/helpers/language.rs | 64 ++ src/renderer/html_handlebars/helpers/mod.rs | 1 + src/renderer/markdown_renderer.rs | 41 +- src/renderer/mod.rs | 24 +- src/theme/book.js | 83 +++ src/theme/css/chrome.css | 47 ++ src/theme/index.hbs | 10 + src/utils/fs.rs | 13 +- src/utils/mod.rs | 311 +++++++-- tests/alternative_backends.rs | 6 +- tests/build_process.rs | 19 +- tests/cli/init.rs | 2 +- tests/custom_preprocessors.rs | 4 +- tests/dummy_book/mod.rs | 4 +- tests/init.rs | 26 +- tests/localized_book/book.toml | 35 + tests/localized_book/src/en/README.md | 5 + tests/localized_book/src/en/SUMMARY.md | 9 + tests/localized_book/src/en/chapter/1.md | 2 + tests/localized_book/src/en/chapter/2.md | 2 + tests/localized_book/src/en/chapter/3.md | 1 + tests/localized_book/src/en/chapter/README.md | 1 + tests/localized_book/src/en/example.rs | 6 + .../src/en/inline-link-fallbacks.md | 7 + .../src/en/missing-summary-chapter.md | 3 + tests/localized_book/src/en/rust_logo.png | Bin 0 -> 8496 bytes .../src/en/untranslated-page.md | 3 + tests/localized_book/src/ja/README.md | 3 + tests/localized_book/src/ja/SUMMARY.md | 9 + tests/localized_book/src/ja/chapter/1.md | 1 + tests/localized_book/src/ja/chapter/2.md | 1 + tests/localized_book/src/ja/chapter/README.md | 1 + .../src/ja/inline-link-fallbacks.md | 20 + .../src/ja/translation-local-page.md | 5 + tests/parse_existing_summary_files.rs | 2 +- tests/rendered_output.rs | 22 +- tests/testing.rs | 6 +- 100 files changed, 2278 insertions(+), 809 deletions(-) rename guide/src/{ => en}/404.md (100%) rename guide/src/{ => en}/README.md (100%) rename guide/src/{ => en}/SUMMARY.md (95%) rename guide/src/{ => en}/cli/README.md (100%) rename guide/src/{ => en}/cli/build.md (100%) rename guide/src/{ => en}/cli/clean.md (100%) rename guide/src/{ => en}/cli/completions.md (100%) rename guide/src/{ => en}/cli/init.md (81%) rename guide/src/{ => en}/cli/serve.md (100%) rename guide/src/{ => en}/cli/test.md (100%) rename guide/src/{ => en}/cli/watch.md (100%) rename guide/src/{ => en}/continuous-integration.md (100%) rename guide/src/{ => en}/for_developers/README.md (100%) rename guide/src/{ => en}/for_developers/backends.md (98%) rename guide/src/{ => en}/for_developers/mdbook-wordcount/Cargo.toml (100%) rename guide/src/{ => en}/for_developers/mdbook-wordcount/src/main.rs (92%) rename guide/src/{ => en}/for_developers/preprocessors.md (99%) rename guide/src/{ => en}/format/README.md (100%) rename guide/src/{ => en}/format/configuration/README.md (76%) rename guide/src/{ => en}/format/configuration/environment-variables.md (100%) rename guide/src/{ => en}/format/configuration/general.md (100%) create mode 100644 guide/src/en/format/configuration/localization.md rename guide/src/{ => en}/format/configuration/preprocessors.md (100%) rename guide/src/{ => en}/format/configuration/renderers.md (100%) rename guide/src/{ => en}/format/example.rs (100%) rename guide/src/{ => en}/format/images/rust-logo-blk.svg (100%) rename guide/src/{ => en}/format/markdown.md (100%) rename guide/src/{ => en}/format/mathjax.md (100%) rename guide/src/{ => en}/format/mdbook.md (100%) rename guide/src/{ => en}/format/summary.md (100%) rename guide/src/{ => en}/format/theme/README.md (100%) rename guide/src/{ => en}/format/theme/editor.md (100%) rename guide/src/{ => en}/format/theme/index-hbs.md (100%) rename guide/src/{ => en}/format/theme/syntax-highlighting.md (100%) rename guide/src/{ => en}/guide/README.md (100%) rename guide/src/{ => en}/guide/creating.md (100%) rename guide/src/{ => en}/guide/installation.md (100%) rename guide/src/{ => en}/guide/reading.md (100%) rename guide/src/{ => en}/misc/contributors.md (96%) create mode 100644 src/build_opts.rs create mode 100644 src/renderer/html_handlebars/helpers/language.rs create mode 100644 tests/localized_book/book.toml create mode 100644 tests/localized_book/src/en/README.md create mode 100644 tests/localized_book/src/en/SUMMARY.md create mode 100644 tests/localized_book/src/en/chapter/1.md create mode 100644 tests/localized_book/src/en/chapter/2.md create mode 100644 tests/localized_book/src/en/chapter/3.md create mode 100644 tests/localized_book/src/en/chapter/README.md create mode 100644 tests/localized_book/src/en/example.rs create mode 100644 tests/localized_book/src/en/inline-link-fallbacks.md create mode 100644 tests/localized_book/src/en/missing-summary-chapter.md create mode 100644 tests/localized_book/src/en/rust_logo.png create mode 100644 tests/localized_book/src/en/untranslated-page.md create mode 100644 tests/localized_book/src/ja/README.md create mode 100644 tests/localized_book/src/ja/SUMMARY.md create mode 100644 tests/localized_book/src/ja/chapter/1.md create mode 100644 tests/localized_book/src/ja/chapter/2.md create mode 100644 tests/localized_book/src/ja/chapter/README.md create mode 100644 tests/localized_book/src/ja/inline-link-fallbacks.md create mode 100644 tests/localized_book/src/ja/translation-local-page.md 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 0000000000000000000000000000000000000000..11c6de63c36ddb2f7ffb571aac78207e5a210386 GIT binary patch literal 8496 zcmV-0A*6c7X=%@UQS zBBG$^@&h6&g3^?Z3p9vGkpP0CC`CX6p=yFaAoMOsLJCRVd*2`XUh;Bt@60)8&Y3ea zcmLMfE6TgG?`iwa%sIP&q9~=MBX$FnV5|x3Rr}vJ0?$Tli=rqNd;wSl^wwk%Fga?A z6s4l92~4d0>w`^!#Vj-4x-14Z_w*gU+pH+1qXYN}(1RFQ5AZ8sKt`Q+vZC?UMSmnZ0)gKmf_ZNv_w8`^o6I)81Tv+oat4d)u@{D`GaMJX*8 zV`gZl80y>EP+wn}XsCONp}t=l>Qj_bvl8+cYiel6r|269JZ7PyWY5oo&S%m$2euy&2g!vgMCWbfw7>VDBcn#sP0T=&Q?oV4Ws<_oVq*a+=)YFdoB! zKQz&Gv4bu}S%LAiUa&6#yISZP4J=L2mm~|3cip~R@2IP<{GmbTM}Y;7x|RZCEyiOa z@M40#@fNxir77PgdC~R&r(x!Rk)|t6<^sP5?gF03sH?BcuIczz3)C4_>V#IxzFW!Z~*IVe> z&O%SwWrr4H@b%glnd+I*LXV;t(SaO~*e>A9f%i1%y3j#SS*39UY8Y}iwbe5Z_3%dw2 zIY^@F20REDlHvLdeKkaJCd(WQA>CcH)Kt}gS4Z6P<{@XNw*jBRzP}rJZkJ+@?HqI> zUTc3t=6)U$$2{C~yOcn50RN=<`_$t|ApKxNT_*9hMU)4Ca}cMBpCF5QNdemBB5$V7 zk}w#V+g5=c)6pI6QZRw^Me!PNGReNoqYU&EP5uRZxJ7@)B5}a6+vp-|wCEGa9G(Qc zYM|#s33`-*us`MtVS>m=b$V}`u>n<%on{A{AnUhVC%pW&kAL`f(>|-%TN+do;`~{b{ z*q)V+#69Au%(A=XI2E2M?m{n&-GH2V8TgasHLtK@7-2h5xwWANTz;k_kC?XH?Yki* zpmJ+N4Y>Tg?s(mQy|$tpRBmll(SR4lZa~hg=6KzEy|$tpr~#LN%1u%V6L51-XvZ_r z%B>AG;PNvX*v;~q5x^${w58}&Zf&RmSA(A-`KC=dmG;cx+>UFbbadc$$X^6m3OtB- z7<~!Y61c|j+UqcjygA0AJtrVuYF|K7>&~QH4laGA2la){g^@^>$6Juado1#v*WW_N zeBv({Q+XIT9eJc(xk+CJAa0T-y6&xwRWrsSr9n*>`_V1*tOo3d{K(8H!1$TUf)@R0 zMt@`+zk#g5TaciTkrq1Ik~M&5X+8n-k+8ri$U9$S&Z#<+anHS+hx8!PQpW@DGaQ$} zNJNq8cRT|80!d?Ak4AdO@Y-(Ri-zq2*Z@gZcPGVu@>!bDuVKiVno=99RyTlWNe--N zd(OgcLLNlI<|YAaV^&2khI9kBBX>2|s`f*Hdjhv}3GU@lKq9z~1a76c#(XZaRwf~9 z=K<_~@XyRJK9%PV;!m(oc^!EjSl83|tODF0sI7~TqzArO2lz4Kwi$aKe9G+}$Eb2# zP5cS+A@>5GCrmn*)q#I|ZtnuZIrLPdSNHj!h!k4lBuhWHa?D?PxKNPu~stOY!sq20Ym%Agn?S?dAk z<5mGNwrmq%JSsy!)?EKWX1s9UN9R_@c_Wao5Bd!FyRD_9fm# zolV6HVwIGBNE~yla7N83ZNV7wpP+fbXY=Tsl8J+mxypNejLxHTRpA`+ot%e|usEe0 zY=|Tm$a9UHolDOw37Mgw1K5IPF|P}dnAG{muISsy&fYx4k9aO}TyrFFUch!W@+YL8 z(%U(Hr;?qO5f|1xt1TRb?B;hN@#y`Ko&EmE4)p+}#LYmKr8t_+viJAWFr3yUK8qxm zVwdSdDHi>Z#09a}QJVaNlGP{vJ@E%EO{SnZlB&YjD1L;~wZVGtnUyGZN}DCXmxJ|Q ziN!$_%K@m_x-%^-AC;jLH*?`Tz+U0Hu0&=pisAIpOuDg4{Tr1cW?xv0)DBjv%|v8F zGWL3Fl)FN9+Bf?Uf57_6A}UxLR%yw06wio#<{Jx6CldJ*d(e83cMtWVX+L(QIFHk_ zDB*_b;lv-fB}fu-r9Ih)Vr*<37OI0Ge-%~&oYO zPE3v_t{2toNOYJ@Wr)IXGVy=?RN_fD64`Yn@GT^DP7LyP&Fw2Eh3{TChEvSU3M%nr z4iYZ<0rH~@Npz5RUl&j^53FXKNBl*jQcQUOajF?Wc!rUGOmUt*{lR!2#rDini7(F~ z1=2!$Dy@s;!p@^lOEE8AS9q=@{vuH+zPy0MWQW{b-GzK^C|3sTL+*Tt_=`iOJaP+C z94!<-%%Kmh>NJ;@m2otltHet*7 zObgKF+*uPil=zE4rQC8WuqyufQs8zUihDtKBYbQYbRoZH8zaTCHmd#a4Ux1XisFNR zAsbMydDeRqu!m=zjm!o9gZM;0T9a9bw_0}=#V!Us0r(ipjC4aZ4Xct5A&hu5Vo)(| zHj5C$t{eBCf$5>XtP?2(8T)ZcrM=}L{FgZSdI9=XmkSVE^Uh}r&^Z;AUf^o{o)z~4 z9xOn=>T;-Q!)+{mPm9kgN=WttPVlI2DUvd_$D__HeA4uTHU^yOWm{1^GX?mdM}7Z5 z0xgPwv8~w{@YN0tQIt@0A$u}ip7iYutmjErR;~%5v;(+-`0ocQDt+Y`{BUCd`qGQo znDP6)?MA>V$S&YG%j*;+6#qk!*v$`-$vpk zo3UR_Z(69=^~eu_Dcg~K0+izMW#AIS&rTz~TIKynq|)6@{# z*$xfvMPA-r_)&`9$1y(^Qv(-bG*y%ley{JlT$wUp1?gBX=9cR5^B zXFbHG<{&n95Qb%D(hPW8B3mWM_w&Q0Zzea~4XS2SreoUxgl3#Zx@-180ZkTQo z#o}Kne>eGocg;V&n&Wk?SQ$7R$rLgR$zg5uX#W$!zNah!-s{kz>kajrya(w!usd^z zKSs^uxs-NqZm4IL)2QT=5MYI4*Ty^WD5R2lNcsUsSYER}=E=8R&ZHG{o{8On7Tl8Z z_gxM3Y=r$98iji_lnuwz;Je*u!0&JJ-$HN#=8hEok#{dsrxTx#`^t(8jNO11T%Pjx zF&4UhhMkks&&a2^K^Ia;;awVy^%>+=9hxD?rbwE{&mg4E>W3|KKkwQoC6xgb(}V3% z;LeoKv~t&N#)pA(F}s<42iY~wgI)}dVSh<}?=1_FU$3UmYW{hA^kZk-TpnY~#W>{r>HvOPfX181)7bg1ssWoZ5cnpNvNiU&G?Vij z`g9Qa-#xR)>16bk{$2q((Mo0_PA9fIqiVpd>5s%qp9Z{2{P9Sddn0$lCajC>^%UK| z`+I2}F?o0RA(A!V9P&A0N~0GEM?3>L`|FL{9PjKwzImDkB1dmy5ihyTunWzM!6M{N zYT{nojHiGv0^dcV*p9?59ZzU_fTNL}aBu953|OpRqql*x@l(I1$ai3sWLboNr-=bb z3a9A>=-)!pGle*_Gjg7mXNR6e(wg|Jpsog7PFaeCHil#ezB!z?Kr*i8wc#}v_xxJcTpOqpJ;;0Q?u5-!paxtnSxmgmz0pX3 z%zsnbm{qRLt3$9X=-KB4T|$0tdTPMsk$K1udTfRu8(uEXASQB8eqHe&QuaWejYkc* zEbE0p>m1j{*su-~gs`^DmnGnu1wpAU*C0n)_s4EX13gH}S>N@9 z*G6osmG5;^ObxgnlFi^@%;R!x@R5#0sRrGM-|-~uB8c)PE&rAadmuLU4GhB>O$>M< zlBM7bl9Q`vLGQo{NQs`UfRiYG0=-Z-oneTr&BEZkJIR1o1;*AU66geGT|? zNOI~J+xB@5hEiM(Ms{LdvXRW_$l5e1tCKe|*X$A`W9GXC`~;GkCf2*C8nDue z;Yg%T(Vo&wvY&?F9VDB`Q?UFl^skT@$lLR8lNzv+$asoZSS`kyy&WmuH?{sB8qb0M zgX{wCMZOgd)PR*Vjv}ShX3pe!2Yw#0$(?}5`u?Gn!}N_vS;Nzj^Wo5&TtfrvSISQ( za3kB9db-_4CV1j-DvAIuQBvDAQz!M#Xft)laZKzd^t@G!*hdrzB- zYz+7i!uq6c7D%sR&&9}MDl%K)zl!1j;wBRqgxJ`@rWcqR@Nr0;n2TZKjWS`Y9KTCR=fcfDJtA^Tlz<4{2AkKQ=Sq0l;sO+Pr!5kqFmcQ6Kjv`lE=V!44|r`+IKD_j zey~OZ9!Gp1Q+XBoRIzrPB53MA3$RUU?^=(LGUD5Bned(m3v~Q!ajsjjF zpL$42bB@RV@y#n_tL1YuwLHWnVIeRbISQ*6AsY$2A9+nHT7o=Nx`ENat4SX^oA}{k zD*h!)CX15`(0&yuJGvar?zG1Mha)LHlV182RLP7|yLW(-Y?wpFfWHDeUXZoG8$zsIDCep%jqY=~1 zs%~V*?f@k4{utmP!)ujlppVhvcnEoI9fjD~t%%K~;5G|A^gyit#jk3U#~UJn2!2%OYTA>_$`wFE-=DfZq#D@t{X3KKdB#k00P6eUA?V zK9_R&lPc~^kKKR@F$NC_>a!Z~;lQMQWhJdn0btvxqDRu)Y#vn4-lB)rqj3#1lSBjo5x{Pd!*pxLc2D~SR zIct?lawi(87J}n&C>LXEz^eln5I<{&5|?vhH=u%aAlWBZx4Fp1fIASs=PYrhN;S9$ zm>s(j6=XExA8mYtv@zg)aZ4PgRF5UdBcI}*-GEP-e%Qua2L;l)5hAbLINn$m^=eX`h)_idCDq^-u4&Su_a1DB1t+ZwU za%T*^H`7VQZYI;n@4e&Hocg4ck30+aP0OiI!PyAtA-{p^LDT#wD>9k<)?j@dZj?*U z0&^bu4qPv?X|qKhol{D|)<}Kiyw~A*p*t3O7kW7G43dH;Z(ahn$8G1e6Z!R=fK2{6 zzyjp_`wHNBPi<6|XBbjyWH2xn_%~8;J_{QFcOh#rZx#aMfY%~?w-;g;hJFmQi5ms% zg`8jBO7a=ze56jRCN=QK%D~T&AIZk?eh%53JPs-0GsHsgXygYx<{CW7W*~~hO2{uy z%)#qLs_G5x)2_kDo%tI`INZ~YdQb5%Mhc8XJd;eveF;*{??U9gb`zSPaIAu4(2TwQ zUPEl5bbN*QgIAZ^5I^7<_^WT+18nPUwAzynB*#|Pxp)isEAj-jH}cAJhX=jB_ju)Jiv zr!i|so+JMJG#x!ih?-)@ZKRLwLgc)?v06bF zum|#^8+#3z$URN^;DwOhL>)-b^vK%(UjV#=crq;p-bN1P=K)I**ST&ab3||-Qs4y4 zBA31oOiB67$I+^NHgq9RK1okF&m&Jk52olj1321W(-JunDJ|3mya#!L9fsrq8;U#~ z3;_nR?0xqY;O~ebw+-hI_80MYmhDLkhdj}#m+i~uB#-AJU@HrKCQnLPS~Wk14A#9tlTl;raF8XDYS z!^N~7%N`_x$EQvoQbYgEv`Szri5IR z&J@*vcL=C^uH$uYrTkrwlgt>3S3Z+--FY6p7e2i2)n!RBmPcYhs{)mTG9+UBV(v$5C0~1r&15{1o0_KmVqHKUC5Z+ zj@@`(M{5U)PT(N&Gv2!BQ&s}UlJ8{H?+F|z_T3S?_eY``bLOz%9b@5 z9r$}gvk>z&(I1IN-X3w{8HSuq{|tCFNb4Laq5)^61NpTrqL-NuUO)_a4id)rO6}j* zYyb8jnNJ!GeF)+l(GN*^G#vS{98r^XYO+S~9>k#+InL^h-4LHt(SQdbzqX3f$Vyaf zZB+3tRi)is#V?gx8*0ExJ5jl{p$4q96O~&VRW#ss@ODz%sNCA9q5)U#S61;x<<^E8 zu+mOcZf&RmEA2$()`l9euoRdId=PPFxQO32fFSO9Je-k}Q!I(h$M7{=`OmeE&Q3iTc@{uI>&JoC5 zzG|RHGl!Od4kT$^vk810I0ExWsW)2aQOQrotj2MMSFC-a{S*FK4) zLOm0B%1?7j#ZyR*vxAWY1g<>lr#VVH(t)J)v3+lw%8HfjE^^RQR%v{19fss~w$(Ea z-8oh=G9B{;_g`S27J9a}&{KA4RXlE=+8CMYnchN=q8M{2W{d4kV9gX=j~MDIvpkYA z25VxT|NGJuU2TuP1E-Vr9^4C@j+Fg)F6FaVWNgbkq+Zb7WP7h{WOhyGb+zxvLRuf6 zL&oYfBmrL%S@s|l1(3(VR(FV(fL$$gjln%<^+I5$27T8%>gp>uHt75)cK*eEWhr(^ z>UTxzQnk{zwS_K4X~`w5xVUa6*_XN%e`=yDO_m^~OBxwRwsNfVktzj^?1@`lqg(8U zgAu?@O>|{(;82w1=tL?$T!HMSPQrb6^g!&4q8m6QLErI~`ufUu67(Ki`)(V32O5q+ zBOOTe*HmPTC!?=-Nx?E(b`xFD6 z8f&Yx6-gE5T}oK4iquI;GqhKL)g1Jnk)Tukm6f*RNSYhNRmgj-3zJ*u>?>b#)ZJBU zTQi+UIOn<*G7`zO z=&SzjrZhjpT?}mQ={rUvZ64`KpQ0#zn2aPds2lI1+J93jhCcdKm$eX&DiC+cXCt;n eQ4~dK