mirror of
https://github.com/rust-lang/mdBook.git
synced 2026-01-07 17:45:13 -05:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ab7eb2f0 | ||
|
|
6d183be0ec | ||
|
|
c83a34b473 | ||
|
|
d3e0e597d2 | ||
|
|
271bbba7dd | ||
|
|
86ff2e1e6b | ||
|
|
6ef7cc0ccb | ||
|
|
f4cf32e768 | ||
|
|
47384c1f18 | ||
|
|
9e3d533acc | ||
|
|
5ec4f65ac3 | ||
|
|
4a330ae36f | ||
|
|
d93fbc0f6b | ||
|
|
684bb78897 | ||
|
|
d0dd16c527 | ||
|
|
f4805343f8 | ||
|
|
f9add3e936 | ||
|
|
1fd9656291 | ||
|
|
6f281a6401 | ||
|
|
5194d2b3cd | ||
|
|
b3c23c5f88 | ||
|
|
a15134cc2f | ||
|
|
b51bb101f2 | ||
|
|
59d26dbbe7 | ||
|
|
94baf19e6a | ||
|
|
f1a446fb02 | ||
|
|
01d1242753 | ||
|
|
203685e91c | ||
|
|
2cb5b85ab2 | ||
|
|
ec996d3509 | ||
|
|
5ed3223185 | ||
|
|
3bdcc0a5a6 | ||
|
|
1e4d4887e1 |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.41
|
||||
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
|
||||
|
||||
### Added
|
||||
|
||||
- Added preliminary support for Rust 2024 edition.
|
||||
[#2398](https://github.com/rust-lang/mdBook/pull/2398)
|
||||
- Added a full example of the remove-emphasis preprocessor.
|
||||
[#2464](https://github.com/rust-lang/mdBook/pull/2464)
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted styling of clipboard/play icons.
|
||||
[#2421](https://github.com/rust-lang/mdBook/pull/2421)
|
||||
- Updated to handlebars v6.
|
||||
[#2416](https://github.com/rust-lang/mdBook/pull/2416)
|
||||
- Attr and section rules now have specific code highlighting.
|
||||
[#2448](https://github.com/rust-lang/mdBook/pull/2448)
|
||||
- The sidebar is now loaded from a common file, significantly reducing the book size when there are many chapters.
|
||||
[#2414](https://github.com/rust-lang/mdBook/pull/2414)
|
||||
- Updated dependencies.
|
||||
[#2470](https://github.com/rust-lang/mdBook/pull/2470)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved theme support when JavaScript is disabled.
|
||||
[#2454](https://github.com/rust-lang/mdBook/pull/2454)
|
||||
- Fixed broken themes when localStorage has an invalid theme id.
|
||||
[#2463](https://github.com/rust-lang/mdBook/pull/2463)
|
||||
- Adjusted the line-height of superscripts (and footnotes) to avoid adding extra space between lines.
|
||||
[#2465](https://github.com/rust-lang/mdBook/pull/2465)
|
||||
|
||||
## mdBook 0.4.40
|
||||
[v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40)
|
||||
|
||||
|
||||
962
Cargo.lock
generated
962
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -1,6 +1,9 @@
|
||||
[workspace]
|
||||
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
|
||||
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.40"
|
||||
version = "0.4.41"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -23,7 +26,7 @@ clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.3.2"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "5.0"
|
||||
handlebars = "6.0"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.7.0"
|
||||
@@ -73,3 +76,9 @@ name = "mdbook"
|
||||
[[example]]
|
||||
name = "nop-preprocessor"
|
||||
test = true
|
||||
|
||||
[[example]]
|
||||
name = "remove-emphasis"
|
||||
path = "examples/remove-emphasis/test.rs"
|
||||
crate-type = ["lib"]
|
||||
test = true
|
||||
|
||||
@@ -26,7 +26,7 @@ fn main() {
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
1
examples/remove-emphasis/.gitignore
vendored
Normal file
1
examples/remove-emphasis/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
book
|
||||
5
examples/remove-emphasis/book.toml
Normal file
5
examples/remove-emphasis/book.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[book]
|
||||
title = "remove-emphasis"
|
||||
|
||||
[preprocessor.remove-emphasis]
|
||||
command = "cargo run --manifest-path=mdbook-remove-emphasis/Cargo.toml --locked"
|
||||
10
examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml
Normal file
10
examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "mdbook-remove-emphasis"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
mdbook = { version = "0.4.40", path = "../../.." }
|
||||
pulldown-cmark = { version = "0.12.2", default-features = false }
|
||||
pulldown-cmark-to-cmark = "18.0.0"
|
||||
serde_json = "1.0.132"
|
||||
82
examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs
Normal file
82
examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! This is a demonstration of an mdBook preprocessor which parses markdown
|
||||
//! and removes any instances of emphasis.
|
||||
|
||||
use mdbook::book::{Book, Chapter};
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use mdbook::BookItem;
|
||||
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
|
||||
use std::io;
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
match args.next().as_deref() {
|
||||
Some("supports") => {
|
||||
// Supports all renderers.
|
||||
return;
|
||||
}
|
||||
Some(arg) => {
|
||||
eprintln!("unknown argument: {arg}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
if let Err(e) = handle_preprocessing() {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoveEmphasis;
|
||||
|
||||
impl Preprocessor for RemoveEmphasis {
|
||||
fn name(&self) -> &str {
|
||||
"remove-emphasis"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
|
||||
let mut total = 0;
|
||||
book.for_each_mut(|item| {
|
||||
let BookItem::Chapter(ch) = item else {
|
||||
return;
|
||||
};
|
||||
if ch.is_draft_chapter() {
|
||||
return;
|
||||
}
|
||||
match remove_emphasis(&mut total, ch) {
|
||||
Ok(s) => ch.content = s,
|
||||
Err(e) => eprintln!("failed to process chapter: {e:?}"),
|
||||
}
|
||||
});
|
||||
eprintln!("removed {total} emphasis");
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
// ANCHOR: remove_emphasis
|
||||
fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result<String, Error> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| match e {
|
||||
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) => {
|
||||
*num_removed_items += 1;
|
||||
false
|
||||
}
|
||||
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong) => false,
|
||||
_ => true,
|
||||
});
|
||||
|
||||
Ok(pulldown_cmark_to_cmark::cmark(events, &mut buf).map(|_| buf)?)
|
||||
}
|
||||
// ANCHOR_END: remove_emphasis
|
||||
|
||||
pub fn handle_preprocessing() -> Result<(), Error> {
|
||||
let pre = RemoveEmphasis;
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
let processed_book = pre.run(&ctx, book)?;
|
||||
serde_json::to_writer(io::stdout(), &processed_book)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3
examples/remove-emphasis/src/SUMMARY.md
Normal file
3
examples/remove-emphasis/src/SUMMARY.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Summary
|
||||
|
||||
- [Chapter 1](./chapter_1.md)
|
||||
3
examples/remove-emphasis/src/chapter_1.md
Normal file
3
examples/remove-emphasis/src/chapter_1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Chapter 1
|
||||
|
||||
This has *light emphasis* and **bold emphasis**.
|
||||
13
examples/remove-emphasis/test.rs
Normal file
13
examples/remove-emphasis/test.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use mdbook::MDBook;
|
||||
|
||||
#[test]
|
||||
fn remove_emphasis_works() {
|
||||
// Tests that the remove-emphasis example works as expected.
|
||||
|
||||
// Workaround for https://github.com/rust-lang/mdBook/issues/1424
|
||||
std::env::set_current_dir("examples/remove-emphasis").unwrap();
|
||||
let book = MDBook::load(".").unwrap();
|
||||
book.build().unwrap();
|
||||
let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap();
|
||||
assert!(ch1.contains("This has light emphasis and bold emphasis."));
|
||||
}
|
||||
@@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
|
||||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.41/mdbook-v0.4.41-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
|
||||
@@ -68,33 +68,10 @@ The following code block shows how to remove all emphasis from markdown,
|
||||
without accidentally breaking the document.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
{{#rustdoc_include ../../../examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs:remove_emphasis}}
|
||||
```
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
Take a look at the [full example source][emphasis-example] for more details.
|
||||
|
||||
## Implementing a preprocessor with a different language
|
||||
|
||||
@@ -122,11 +99,10 @@ if __name__ == '__main__':
|
||||
```
|
||||
|
||||
|
||||
|
||||
[emphasis-example]: https://github.com/rust-lang/mdBook/tree/master/examples/remove-emphasis/
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
||||
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
||||
|
||||
@@ -123,7 +123,7 @@ The following configuration options are available:
|
||||
[`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||
CSS media query. Defaults to `navy`.
|
||||
- **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
|
||||
See [Smart Punctuation].
|
||||
See [Smart Punctuation](../markdown.md#smart-punctuation).
|
||||
Defaults to `false`.
|
||||
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
|
||||
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
|
||||
|
||||
@@ -30,6 +30,9 @@ cargo install mdbook
|
||||
|
||||
This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
You can run `cargo install mdbook` again whenever you want to update to a new version.
|
||||
That command will check if there is a newer version, and re-install mdBook if a newer version is found.
|
||||
|
||||
To uninstall, run the command `cargo uninstall mdbook`.
|
||||
|
||||
[Rust installation page]: https://www.rust-lang.org/tools/install
|
||||
@@ -47,6 +50,8 @@ cargo install --git https://github.com/rust-lang/mdBook.git mdbook
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
## Modifying and contributing
|
||||
|
||||
If you are interested in making modifications to mdBook itself, check out the [Contributing Guide] for more information.
|
||||
|
||||
[Contributing Guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||
|
||||
@@ -18,11 +18,11 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
|
||||
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 {src_dir:?} directory"))?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content)
|
||||
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||
.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")?;
|
||||
@@ -341,7 +341,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
write!(f, "{section_number} ")?;
|
||||
}
|
||||
|
||||
write!(f, "{}", self.name)
|
||||
|
||||
@@ -92,7 +92,7 @@ impl MDBook {
|
||||
}
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
for line in format!("Config: {config:#?}").lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
}
|
||||
@@ -345,6 +345,9 @@ impl MDBook {
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
RustEdition::E2024 => {
|
||||
cmd.args(["--edition", "2024", "-Zunstable-options"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,15 +483,13 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||
if let Some(before) = table.get("before") {
|
||||
let before = before.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to be an array",
|
||||
name
|
||||
"Expected preprocessor.{name}.before to be an array"
|
||||
))
|
||||
})?;
|
||||
for after in before {
|
||||
let after = after.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.before to contain strings",
|
||||
name
|
||||
"Expected preprocessor.{name}.before to contain strings"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -507,16 +508,12 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
|
||||
|
||||
if let Some(after) = table.get("after") {
|
||||
let after = after.as_array().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to be an array",
|
||||
name
|
||||
))
|
||||
Error::msg(format!("Expected preprocessor.{name}.after to be an array"))
|
||||
})?;
|
||||
for before in after {
|
||||
let before = before.as_str().ok_or_else(|| {
|
||||
Error::msg(format!(
|
||||
"Expected preprocessor.{}.after to contain strings",
|
||||
name
|
||||
"Expected preprocessor.{name}.after to contain strings"
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -578,7 +575,7 @@ fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||
.unwrap_or_else(|| format!("mdbook-{key}"))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
@@ -589,7 +586,7 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{key}"));
|
||||
|
||||
Box::new(CmdRenderer::new(key.to_string(), command))
|
||||
}
|
||||
@@ -783,7 +780,7 @@ mod tests {
|
||||
for preprocessor in &preprocessors {
|
||||
eprintln!(" {}", preprocessor.name());
|
||||
}
|
||||
panic!("{} should come before {}", before, after);
|
||||
panic!("{before} should come before {after}");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -616,7 +616,7 @@ impl Display for SectionNumber {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
for item in &self.0 {
|
||||
write!(f, "{}.", item)?;
|
||||
write!(f, "{item}.")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -763,7 +763,7 @@ mod tests {
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
other => panic!("Unreachable, {other:?}"),
|
||||
};
|
||||
|
||||
let got = parser.parse_link(href);
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let hostname = args.get_one::<String>("hostname").unwrap();
|
||||
let open_browser = args.get_flag("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let address = format!("{hostname}:{port}");
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
@@ -89,7 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
let serving_url = format!("http://{address}");
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
|
||||
@@ -145,7 +145,7 @@ impl Config {
|
||||
if let serde_json::Value::Object(ref map) = parsed_value {
|
||||
// To `set` each `key`, we wrap them as `prefix.key`
|
||||
for (k, v) in map {
|
||||
let full_key = format!("{}.{}", key, k);
|
||||
let full_key = format!("{key}.{k}");
|
||||
self.set(&full_key, v).expect("unreachable");
|
||||
}
|
||||
return;
|
||||
@@ -504,6 +504,9 @@ pub struct RustConfig {
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Rust edition to use for the code.
|
||||
pub enum RustEdition {
|
||||
/// The 2024 edition of Rust
|
||||
#[serde(rename = "2024")]
|
||||
E2024,
|
||||
/// The 2021 edition of Rust
|
||||
#[serde(rename = "2021")]
|
||||
E2021,
|
||||
|
||||
@@ -493,7 +493,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -519,7 +519,7 @@ mod tests {
|
||||
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -536,7 +536,7 @@ mod tests {
|
||||
fn test_find_links_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -555,7 +555,7 @@ mod tests {
|
||||
fn test_find_links_with_line_number() {
|
||||
let s = "Some random text with {{#include file.rs:10}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -574,7 +574,7 @@ mod tests {
|
||||
fn test_find_links_with_from_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -593,7 +593,7 @@ mod tests {
|
||||
fn test_find_links_with_to_range() {
|
||||
let s = "Some random text with {{#include file.rs::20}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -612,7 +612,7 @@ mod tests {
|
||||
fn test_find_links_with_full_range() {
|
||||
let s = "Some random text with {{#include file.rs::}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -631,7 +631,7 @@ mod tests {
|
||||
fn test_find_links_with_no_range_specified() {
|
||||
let s = "Some random text with {{#include file.rs}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -650,7 +650,7 @@ mod tests {
|
||||
fn test_find_links_with_anchor() {
|
||||
let s = "Some random text with {{#include file.rs:anchor}}...";
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
@@ -670,7 +670,7 @@ mod tests {
|
||||
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@@ -690,7 +690,7 @@ mod tests {
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
@@ -721,7 +721,7 @@ mod tests {
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
println!("\nOUTPUT: {res:?}\n");
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(
|
||||
res[0],
|
||||
|
||||
@@ -153,13 +153,13 @@ impl HtmlHandlebars {
|
||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||
let path = src_dir.join(filename);
|
||||
std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("unable to open 404 input file {:?}", path))?
|
||||
.with_context(|| format!("unable to open 404 input file {path:?}"))?
|
||||
} else {
|
||||
// 404 input not explicitly configured try the default file 404.md
|
||||
let default_404_location = src_dir.join("404.md");
|
||||
if default_404_location.exists() {
|
||||
std::fs::read_to_string(&default_404_location).with_context(|| {
|
||||
format!("unable to open 404 input file {:?}", default_404_location)
|
||||
format!("unable to open 404 input file {default_404_location:?}")
|
||||
})?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
@@ -237,7 +237,7 @@ impl HtmlHandlebars {
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
|
||||
write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
@@ -528,6 +528,11 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Register the header handlebars template");
|
||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||
|
||||
debug!("Register the toc handlebars template");
|
||||
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
|
||||
handlebars
|
||||
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;
|
||||
|
||||
debug!("Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||
|
||||
@@ -583,6 +588,18 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Creating print.html ✓");
|
||||
}
|
||||
|
||||
debug!("Render toc");
|
||||
{
|
||||
let rendered_toc = handlebars.render("toc_js", &data)?;
|
||||
utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.js ✓");
|
||||
data.insert("is_toc_html".to_owned(), json!(true));
|
||||
let rendered_toc = handlebars.render("toc_html", &data)?;
|
||||
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?;
|
||||
debug!("Creating toc.html ✓");
|
||||
data.remove("is_toc_html");
|
||||
}
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
@@ -834,11 +851,7 @@ fn insert_link_into_header(
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content,
|
||||
classes = classes
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{content}</a></h{level}>"##
|
||||
)
|
||||
}
|
||||
|
||||
@@ -860,12 +873,7 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
let classes = &caps[2].replace(',', " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
r#"<code{before}class="{classes}"{after}>"#,
|
||||
before = before,
|
||||
classes = classes,
|
||||
after = after
|
||||
)
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
@@ -902,6 +910,7 @@ fn add_playground_pre(
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
Some(RustEdition::E2024) => " edition2024",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
@@ -922,8 +931,7 @@ fn add_playground_pre(
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||
.into()
|
||||
format!("# #![allow(unused)]\n{attrs}#fn main() {{\n{code}#}}").into()
|
||||
};
|
||||
content
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::path::Path;
|
||||
use std::{cmp::Ordering, collections::BTreeMap};
|
||||
|
||||
use crate::utils;
|
||||
use crate::utils::bracket_escape;
|
||||
use crate::utils::special_escape;
|
||||
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
@@ -32,21 +31,6 @@ impl HelperDef for RenderToc {
|
||||
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||
})
|
||||
})?;
|
||||
let current_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace('\"', "");
|
||||
|
||||
let current_section = rc
|
||||
.evaluate(ctx, "@root/section")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fold_enable = rc
|
||||
.evaluate(ctx, "@root/fold_enable")?
|
||||
@@ -64,31 +48,27 @@ impl HelperDef for RenderToc {
|
||||
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
|
||||
})?;
|
||||
|
||||
// If true, then this is the iframe and we need target="_parent"
|
||||
let is_toc_html = rc
|
||||
.evaluate(ctx, "@root/is_toc_html")?
|
||||
.as_json()
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
// The "index" page, which has this attribute set, is supposed to alias the first chapter in
|
||||
// the book, i.e. the first link. There seems to be no easy way to determine which chapter
|
||||
// the "index" is aliasing from within the renderer, so this is used instead to force the
|
||||
// first link to be active. See further below.
|
||||
let mut is_first_chapter = ctx.data().get("is_index").is_some();
|
||||
|
||||
for item in chapters {
|
||||
let (section, level) = if let Some(s) = item.get("section") {
|
||||
let (_section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
} else {
|
||||
("", 1)
|
||||
};
|
||||
|
||||
let is_expanded =
|
||||
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
|
||||
// Expand if folding is disabled, or if the section is an
|
||||
// ancestor or the current section itself.
|
||||
true
|
||||
} else {
|
||||
// Levels that are larger than this would be folded.
|
||||
level - 1 < fold_level as usize
|
||||
};
|
||||
// Expand if folding is disabled, or if levels that are larger than this would not
|
||||
// be folded.
|
||||
let is_expanded = !fold_enable || level - 1 < (fold_level as usize);
|
||||
|
||||
match level.cmp(¤t_level) {
|
||||
Ordering::Greater => {
|
||||
@@ -121,7 +101,7 @@ impl HelperDef for RenderToc {
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
out.write(&bracket_escape(title))?;
|
||||
out.write(&special_escape(title))?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
}
|
||||
@@ -139,16 +119,12 @@ impl HelperDef for RenderToc {
|
||||
.replace('\\', "/");
|
||||
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t_path || is_first_chapter {
|
||||
is_first_chapter = false;
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
out.write(if is_toc_html {
|
||||
"\" target=\"_parent\">"
|
||||
} else {
|
||||
"\">"
|
||||
})?;
|
||||
path_exists = true;
|
||||
}
|
||||
_ => {
|
||||
@@ -167,7 +143,7 @@ impl HelperDef for RenderToc {
|
||||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
out.write(&bracket_escape(name))?
|
||||
out.write(&special_escape(name))?
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
format!("Object.assign(window.search, {index});").as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
@@ -83,7 +83,7 @@ fn add_doc(
|
||||
});
|
||||
|
||||
let url = if let Some(id) = section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
Cow::Owned(format!("{anchor_base}#{id}"))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
};
|
||||
@@ -203,7 +203,7 @@ fn render_item(
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
body.push_str(&format!(" [{number}] "));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ function playground_text(playground, hidden = true) {
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.className = 'clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
@@ -258,7 +258,7 @@ function playground_text(playground, hidden = true) {
|
||||
|
||||
if (window.playground_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.className = 'clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
@@ -289,6 +289,10 @@ function playground_text(playground, hidden = true) {
|
||||
var themeToggleButton = document.getElementById('theme-toggle');
|
||||
var themePopup = document.getElementById('theme-list');
|
||||
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||
var themeIds = [];
|
||||
themePopup.querySelectorAll('button.theme').forEach(function (el) {
|
||||
themeIds.push(el.id);
|
||||
});
|
||||
var stylesheets = {
|
||||
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||
@@ -317,7 +321,7 @@ function playground_text(playground, hidden = true) {
|
||||
function get_theme() {
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (theme === null || theme === undefined) {
|
||||
if (theme === null || theme === undefined || !themeIds.includes(theme)) {
|
||||
return default_theme;
|
||||
} else {
|
||||
return theme;
|
||||
@@ -597,12 +601,12 @@ function playground_text(playground, hidden = true) {
|
||||
|
||||
function hideTooltip(elem) {
|
||||
elem.firstChild.innerText = "";
|
||||
elem.className = 'fa fa-copy clip-button';
|
||||
elem.className = 'clip-button';
|
||||
}
|
||||
|
||||
function showTooltip(elem, msg) {
|
||||
elem.firstChild.innerText = msg;
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
elem.className = 'clip-button tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
|
||||
@@ -40,9 +40,9 @@ a > .hljs {
|
||||
border-block-end-style: solid;
|
||||
}
|
||||
#menu-bar.sticky,
|
||||
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
.js #menu-bar:hover,
|
||||
.js.sidebar-visible #menu-bar {
|
||||
#menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
#menu-bar:hover,
|
||||
html.sidebar-visible #menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
@@ -91,7 +91,7 @@ a > .hljs {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons button {
|
||||
html:not(.js) .left-buttons button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ a > .hljs {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.js .menu-title {
|
||||
.menu-title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -250,8 +250,8 @@ pre > .buttons i {
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
margin: 0px 5px;
|
||||
padding: 3px 5px;
|
||||
font-size: 14px;
|
||||
padding: 4px 4px 3px 5px;
|
||||
font-size: 23px;
|
||||
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
@@ -262,6 +262,27 @@ pre > .buttons button {
|
||||
transition-property: color,border-color,background-color;
|
||||
color: var(--icons);
|
||||
}
|
||||
|
||||
pre > .buttons button.clip-button {
|
||||
padding: 2px 4px 0px 6px;
|
||||
}
|
||||
pre > .buttons button.clip-button::before {
|
||||
/* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license
|
||||
*/
|
||||
content: url('data:image/svg+xml,<svg width="21" height="20" viewBox="0 0 24 25" \
|
||||
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
|
||||
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
|
||||
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
|
||||
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
|
||||
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
|
||||
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
|
||||
</svg>');
|
||||
filter: var(--copy-button-filter);
|
||||
}
|
||||
pre > .buttons button.clip-button:hover::before {
|
||||
filter: var(--copy-button-filter-hover);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
pre > .buttons button {
|
||||
/* On mobile, make it easier to tap buttons. */
|
||||
@@ -399,6 +420,22 @@ ul#searchresults span.teaser em {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.sidebar-iframe-inner {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
padding: 10px 10px;
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.sidebar-iframe-outer {
|
||||
border: none;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
[dir=rtl] .sidebar { left: unset; right: 0; }
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
@@ -406,8 +443,7 @@ ul#searchresults span.teaser em {
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.no-js .sidebar,
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
html:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
|
||||
@@ -190,6 +190,16 @@ kbd {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
sup {
|
||||
/* Set the line-height for superscript and footnote references so that there
|
||||
isn't an awkward space appearing above lines that contain the footnote.
|
||||
|
||||
See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583
|
||||
for an explanation.
|
||||
*/
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-block-start: 2em;
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
--search-mark-bg: #e3b171;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%);
|
||||
}
|
||||
|
||||
.coal {
|
||||
@@ -100,9 +105,14 @@
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
}
|
||||
|
||||
.light {
|
||||
.light, html:not(.js) {
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
|
||||
@@ -144,6 +154,11 @@
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: light;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(45.49%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%);
|
||||
}
|
||||
|
||||
.navy {
|
||||
@@ -188,6 +203,11 @@
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%);
|
||||
}
|
||||
|
||||
.rust {
|
||||
@@ -231,11 +251,14 @@
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
|
||||
--color-scheme: light;
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.light.no-js {
|
||||
html:not(.js) {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
@@ -275,5 +298,12 @@
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
|
||||
/* Same as `--icons` */
|
||||
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
|
||||
/* Same as `--sidebar-active` */
|
||||
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
|
||||
<html lang="{{ language }}" class="{{ default_theme }} sidebar-visible" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
@@ -53,7 +53,7 @@
|
||||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body class="sidebar-visible no-js">
|
||||
<body>
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
@@ -82,19 +82,16 @@
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
var html = document.querySelector('html');
|
||||
const html = document.documentElement;
|
||||
html.classList.remove('{{ default_theme }}')
|
||||
html.classList.add(theme);
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
html.classList.add("js");
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var body = document.querySelector('body');
|
||||
var sidebar = null;
|
||||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
@@ -104,40 +101,22 @@
|
||||
sidebar = 'hidden';
|
||||
}
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
{{#toc}}{{/toc}}
|
||||
</div>
|
||||
<!-- populated by js -->
|
||||
<div class="sidebar-scrollbox"></div>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
|
||||
</noscript>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script async src="{{ path_to_root }}toc.js"></script>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
||||
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
||||
pub static HEADER: &[u8] = include_bytes!("header.hbs");
|
||||
pub static TOC_JS: &[u8] = include_bytes!("toc.js.hbs");
|
||||
pub static TOC_HTML: &[u8] = include_bytes!("toc.html.hbs");
|
||||
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
||||
@@ -50,6 +52,8 @@ pub struct Theme {
|
||||
pub head: Vec<u8>,
|
||||
pub redirect: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub toc_js: Vec<u8>,
|
||||
pub toc_html: Vec<u8>,
|
||||
pub chrome_css: Vec<u8>,
|
||||
pub general_css: Vec<u8>,
|
||||
pub print_css: Vec<u8>,
|
||||
@@ -85,6 +89,8 @@ impl Theme {
|
||||
(theme_dir.join("head.hbs"), &mut theme.head),
|
||||
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
|
||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
||||
(theme_dir.join("toc.js.hbs"), &mut theme.toc_js),
|
||||
(theme_dir.join("toc.html.hbs"), &mut theme.toc_html),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
||||
(theme_dir.join("css/general.css"), &mut theme.general_css),
|
||||
@@ -174,6 +180,8 @@ impl Default for Theme {
|
||||
head: HEAD.to_owned(),
|
||||
redirect: REDIRECT.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
toc_js: TOC_JS.to_owned(),
|
||||
toc_html: TOC_HTML.to_owned(),
|
||||
chrome_css: CHROME_CSS.to_owned(),
|
||||
general_css: GENERAL_CSS.to_owned(),
|
||||
print_css: PRINT_CSS.to_owned(),
|
||||
@@ -232,6 +240,8 @@ mod tests {
|
||||
"head.hbs",
|
||||
"redirect.hbs",
|
||||
"header.hbs",
|
||||
"toc.js.hbs",
|
||||
"toc.html.hbs",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"css/chrome.css",
|
||||
@@ -263,6 +273,8 @@ mod tests {
|
||||
head: Vec::new(),
|
||||
redirect: Vec::new(),
|
||||
header: Vec::new(),
|
||||
toc_js: Vec::new(),
|
||||
toc_html: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
|
||||
43
src/theme/toc.html.hbs
Normal file
43
src/theme/toc.html.hbs
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- sidebar iframe generated using mdBook
|
||||
|
||||
This is a frame, and not included directly in the page, to control the total size of the
|
||||
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
the total size of the page becomes O(n**2).
|
||||
|
||||
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
|
||||
instead added to the main page by `toc.js` instead. The JavaScript mode is better
|
||||
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
|
||||
the rest of the page, so the sidebar and the main page theme would fall out of sync.
|
||||
-->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex">
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
{{/if}}
|
||||
<!-- Custom HTML head -->
|
||||
{{> head}}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
{{#if print_enable}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
|
||||
{{/if}}
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
{{/if}}
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body class="sidebar-iframe-inner">
|
||||
{{#toc}}{{/toc}}
|
||||
</body>
|
||||
</html>
|
||||
54
src/theme/toc.js.hbs
Normal file
54
src/theme/toc.js.hbs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Populate the sidebar
|
||||
//
|
||||
// This is a script, and not included directly in the page, to control the total size of the book.
|
||||
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
|
||||
// the total size of the page becomes O(n**2).
|
||||
var sidebarScrollbox = document.querySelector("#sidebar .sidebar-scrollbox");
|
||||
sidebarScrollbox.innerHTML = '{{#toc}}{{/toc}}';
|
||||
(function() {
|
||||
let current_page = document.location.href.toString();
|
||||
if (current_page.endsWith("/")) {
|
||||
current_page += "index.html";
|
||||
}
|
||||
var links = sidebarScrollbox.querySelectorAll("a");
|
||||
var l = links.length;
|
||||
for (var i = 0; i < l; ++i) {
|
||||
var link = links[i];
|
||||
var href = link.getAttribute("href");
|
||||
if (href && !href.startsWith("#") && !/^(?:[a-z+]+:)?\/\//.test(href)) {
|
||||
link.href = path_to_root + href;
|
||||
}
|
||||
// The "index" page is supposed to alias the first chapter in the book.
|
||||
if (link.href === current_page || (i === 0 && path_to_root === "" && current_page.endsWith("/index.html"))) {
|
||||
link.classList.add("active");
|
||||
var parent = link.parentElement;
|
||||
while (parent) {
|
||||
if (parent.tagName === "LI" && parent.previousElementSibling) {
|
||||
if (parent.previousElementSibling.classList.contains("chapter-item")) {
|
||||
parent.previousElementSibling.classList.add("expanded");
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Track and set sidebar scroll position
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
/* Tomorrow Red */
|
||||
.hljs-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-attr,
|
||||
.hljs-tag,
|
||||
.hljs-regexp,
|
||||
.ruby .hljs-constant,
|
||||
@@ -54,6 +55,7 @@
|
||||
|
||||
/* Tomorrow Aqua */
|
||||
.hljs-title,
|
||||
.hljs-section,
|
||||
.css .hljs-hexcolor {
|
||||
color: #8abeb7;
|
||||
}
|
||||
|
||||
@@ -228,47 +228,47 @@ mod tests {
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempfile::TempDir::new() {
|
||||
Ok(t) => t,
|
||||
Err(e) => panic!("Could not create a temp dir: {}", e),
|
||||
Err(e) => panic!("Could not create a temp dir: {e}"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt: {}", err);
|
||||
panic!("Could not create file.txt: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md: {}", err);
|
||||
panic!("Could not create file.md: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png: {}", err);
|
||||
panic!("Could not create file.png: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir: {}", err);
|
||||
panic!("Could not create sub_dir: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png: {}", err);
|
||||
panic!("Could not create sub_dir/file.png: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists: {}", err);
|
||||
panic!("Could not create sub_dir_exists: {err}");
|
||||
}
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt: {}", err);
|
||||
panic!("Could not create sub_dir_exists/file.txt: {err}");
|
||||
}
|
||||
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
|
||||
panic!("Could not symlink file.png: {}", err);
|
||||
panic!("Could not symlink file.png: {err}");
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output")) {
|
||||
panic!("Could not create output: {}", err);
|
||||
panic!("Could not create output: {err}");
|
||||
}
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {}", err);
|
||||
panic!("Could not create output/sub_dir_exists: {err}");
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
|
||||
{
|
||||
panic!("Error while executing the function:\n{:?}", e);
|
||||
panic!("Error while executing the function:\n{e:?}");
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
|
||||
@@ -77,7 +77,7 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
|
||||
let id_count = id_counter.entry(id.clone()).or_insert(0);
|
||||
let unique_id = match *id_count {
|
||||
0 => id,
|
||||
id_count => format!("{}-{}", id, id_count),
|
||||
id_count => format!("{id}-{id_count}"),
|
||||
};
|
||||
*id_count += 1;
|
||||
unique_id
|
||||
@@ -105,7 +105,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
if base.ends_with(".md") {
|
||||
base.replace_range(base.len() - 3.., ".html");
|
||||
}
|
||||
return format!("{}{}", base, dest).into();
|
||||
return format!("{base}{dest}").into();
|
||||
} else {
|
||||
return dest;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
||||
.to_str()
|
||||
.expect("utf-8 paths only");
|
||||
if !base.is_empty() {
|
||||
write!(fixed_link, "{}/", base).unwrap();
|
||||
write!(fixed_link, "{base}/").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +265,25 @@ pub fn log_backtrace(e: &Error) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn special_escape(mut s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len());
|
||||
let needs_escape: &[char] = &['<', '>', '\'', '\\', '&'];
|
||||
while let Some(next) = s.find(needs_escape) {
|
||||
escaped.push_str(&s[..next]);
|
||||
match s.as_bytes()[next] {
|
||||
b'<' => escaped.push_str("<"),
|
||||
b'>' => escaped.push_str(">"),
|
||||
b'\'' => escaped.push_str("'"),
|
||||
b'\\' => escaped.push_str("\"),
|
||||
b'&' => escaped.push_str("&"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
s = &s[next + 1..];
|
||||
}
|
||||
escaped.push_str(s);
|
||||
escaped
|
||||
}
|
||||
|
||||
pub(crate) fn bracket_escape(mut s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len());
|
||||
let needs_escape: &[char] = &['<', '>'];
|
||||
@@ -283,7 +302,7 @@ pub(crate) fn bracket_escape(mut s: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::bracket_escape;
|
||||
use super::{bracket_escape, special_escape};
|
||||
|
||||
mod render_markdown {
|
||||
use super::super::render_markdown;
|
||||
@@ -506,5 +525,20 @@ more text with spaces
|
||||
assert_eq!(bracket_escape("<>"), "<>");
|
||||
assert_eq!(bracket_escape("<test>"), "<test>");
|
||||
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(bracket_escape("'"), "'");
|
||||
assert_eq!(bracket_escape("\\"), "\\");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_special() {
|
||||
assert_eq!(special_escape(""), "");
|
||||
assert_eq!(special_escape("<"), "<");
|
||||
assert_eq!(special_escape(">"), ">");
|
||||
assert_eq!(special_escape("<>"), "<>");
|
||||
assert_eq!(special_escape("<test>"), "<test>");
|
||||
assert_eq!(special_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(special_escape("'"), "'");
|
||||
assert_eq!(special_escape("\\"), "\");
|
||||
assert_eq!(special_escape("&"), "&");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +117,11 @@ fn dummy_book_with_backend(
|
||||
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set(format!("output.{}.command", name), command)
|
||||
.set(format!("output.{name}.command"), command)
|
||||
.unwrap();
|
||||
|
||||
if backend_is_optional {
|
||||
config
|
||||
.set(format!("output.{}.optional", name), true)
|
||||
.unwrap();
|
||||
config.set(format!("output.{name}.optional"), true).unwrap();
|
||||
}
|
||||
|
||||
let md = MDBook::init(temp.path())
|
||||
|
||||
@@ -23,7 +23,7 @@ fn base_mdbook_init_should_create_default_content() {
|
||||
for file in &created_files {
|
||||
let target = temp.path().join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
assert!(target.exists(), "{file} doesn't exist");
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
|
||||
@@ -59,7 +59,7 @@ fn run_mdbook_init_should_create_content_from_summary() {
|
||||
for file in &created_files {
|
||||
let target = src_dir.join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
assert!(target.exists(), "{file} doesn't exist");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
for file in &created_files {
|
||||
assert!(
|
||||
!temp.path().join(file).exists(),
|
||||
"{} shouldn't exist yet!",
|
||||
file
|
||||
"{file} shouldn't exist yet!"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,8 +87,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
let target = temp.path().join(file);
|
||||
assert!(
|
||||
target.exists(),
|
||||
"{} should have been created by `mdbook init`",
|
||||
file
|
||||
"{file} should have been created by `mdbook init`"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use mdbook::utils::fs::write_file;
|
||||
use mdbook::MDBook;
|
||||
use pretty_assertions::assert_eq;
|
||||
use select::document::Document;
|
||||
use select::predicate::{Class, Name, Predicate};
|
||||
use select::predicate::{Attr, Class, Name, Predicate};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
@@ -61,28 +61,6 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
|
||||
assert!(index_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_sure_bottom_level_files_contain_links_to_chapters() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let dest = temp.path().join("book");
|
||||
let links = vec![
|
||||
r#"href="intro.html""#,
|
||||
r#"href="first/index.html""#,
|
||||
r#"href="first/nested.html""#,
|
||||
r#"href="second.html""#,
|
||||
r#"href="conclusion.html""#,
|
||||
];
|
||||
|
||||
let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];
|
||||
|
||||
for filename in files_in_bottom_dir {
|
||||
assert_contains_strings(dest.join(filename), &links);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_correct_cross_links_in_nested_dir() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
@@ -90,19 +68,6 @@ fn check_correct_cross_links_in_nested_dir() {
|
||||
md.build().unwrap();
|
||||
|
||||
let first = temp.path().join("book").join("first");
|
||||
let links = vec![
|
||||
r#"href="../intro.html""#,
|
||||
r#"href="../first/index.html""#,
|
||||
r#"href="../first/nested.html""#,
|
||||
r#"href="../second.html""#,
|
||||
r#"href="../conclusion.html""#,
|
||||
];
|
||||
|
||||
let files_in_nested_dir = vec!["index.html", "nested.html"];
|
||||
|
||||
for filename in files_in_nested_dir {
|
||||
assert_contains_strings(first.join(filename), &links);
|
||||
}
|
||||
|
||||
assert_contains_strings(
|
||||
first.join("index.html"),
|
||||
@@ -265,9 +230,9 @@ fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
|
||||
entry.file_name().to_string_lossy().ends_with(ending)
|
||||
}
|
||||
|
||||
/// Read the main page (`book/index.html`) and expose it as a DOM which we
|
||||
/// Read the TOC (`book/toc.js`) nested HTML and expose it as a DOM which we
|
||||
/// can search with the `select` crate
|
||||
fn root_index_html() -> Result<Document> {
|
||||
fn toc_js_html() -> Result<Document> {
|
||||
let temp = DummyBook::new()
|
||||
.build()
|
||||
.with_context(|| "Couldn't create the dummy book")?;
|
||||
@@ -275,15 +240,36 @@ fn root_index_html() -> Result<Document> {
|
||||
.build()
|
||||
.with_context(|| "Book building failed")?;
|
||||
|
||||
let index_page = temp.path().join("book").join("index.html");
|
||||
let html = fs::read_to_string(index_page).with_context(|| "Unable to read index.html")?;
|
||||
let toc_path = temp.path().join("book").join("toc.js");
|
||||
let html = fs::read_to_string(toc_path).with_context(|| "Unable to read index.html")?;
|
||||
for line in html.lines() {
|
||||
if let Some(left) = line.strip_prefix("sidebarScrollbox.innerHTML = '") {
|
||||
if let Some(html) = left.strip_suffix("';") {
|
||||
return Ok(Document::from(html));
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("cannot find toc in file")
|
||||
}
|
||||
|
||||
/// Read the TOC fallback (`book/toc.html`) HTML and expose it as a DOM which we
|
||||
/// can search with the `select` crate
|
||||
fn toc_fallback_html() -> Result<Document> {
|
||||
let temp = DummyBook::new()
|
||||
.build()
|
||||
.with_context(|| "Couldn't create the dummy book")?;
|
||||
MDBook::load(temp.path())?
|
||||
.build()
|
||||
.with_context(|| "Book building failed")?;
|
||||
|
||||
let toc_path = temp.path().join("book").join("toc.html");
|
||||
let html = fs::read_to_string(toc_path).with_context(|| "Unable to read index.html")?;
|
||||
Ok(Document::from(html.as_str()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_second_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let doc = toc_js_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
|
||||
should_be.sort_unstable();
|
||||
|
||||
@@ -305,7 +291,7 @@ fn check_second_toc_level() {
|
||||
|
||||
#[test]
|
||||
fn check_first_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let doc = toc_js_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_TOP_LEVEL);
|
||||
|
||||
should_be.extend(TOC_SECOND_LEVEL);
|
||||
@@ -328,7 +314,7 @@ fn check_first_toc_level() {
|
||||
|
||||
#[test]
|
||||
fn check_spacers() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let doc = toc_js_html().unwrap();
|
||||
let should_be = 2;
|
||||
|
||||
let num_spacers = doc
|
||||
@@ -337,6 +323,39 @@ fn check_spacers() {
|
||||
assert_eq!(num_spacers, should_be);
|
||||
}
|
||||
|
||||
// don't use target="_parent" in JS
|
||||
#[test]
|
||||
fn check_link_target_js() {
|
||||
let doc = toc_js_html().unwrap();
|
||||
|
||||
let num_parent_links = doc
|
||||
.find(
|
||||
Class("chapter")
|
||||
.descendant(Name("li"))
|
||||
.descendant(Name("a").and(Attr("target", "_parent"))),
|
||||
)
|
||||
.count();
|
||||
assert_eq!(num_parent_links, 0);
|
||||
}
|
||||
|
||||
// don't use target="_parent" in IFRAME
|
||||
#[test]
|
||||
fn check_link_target_fallback() {
|
||||
let doc = toc_fallback_html().unwrap();
|
||||
|
||||
let num_parent_links = doc
|
||||
.find(
|
||||
Class("chapter")
|
||||
.descendant(Name("li"))
|
||||
.descendant(Name("a").and(Attr("target", "_parent"))),
|
||||
)
|
||||
.count();
|
||||
assert_eq!(
|
||||
num_parent_links,
|
||||
TOC_TOP_LEVEL.len() + TOC_SECOND_LEVEL.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensure building fails if `create-missing` is false and one of the files does
|
||||
/// not exist.
|
||||
#[test]
|
||||
@@ -449,18 +468,15 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
|
||||
let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let first_index = temp.path().join("book").join("first").join("index.html");
|
||||
let first_index = temp.path().join("book").join("toc.js");
|
||||
let expected_strings = vec![
|
||||
r#"href="../first/index.html""#,
|
||||
r#"href="../second/index.html""#,
|
||||
"First README",
|
||||
r#"href="first/index.html""#,
|
||||
r#"href="second/index.html""#,
|
||||
"1st README",
|
||||
"2nd README",
|
||||
];
|
||||
assert_contains_strings(&first_index, &expected_strings);
|
||||
assert_doesnt_contain_strings(&first_index, &["README.html"]);
|
||||
|
||||
let second_index = temp.path().join("book").join("second").join("index.html");
|
||||
let unexpected_strings = vec!["Second README"];
|
||||
assert_doesnt_contain_strings(second_index, &unexpected_strings);
|
||||
assert_doesnt_contain_strings(&first_index, &["README.html", "Second README"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -639,11 +655,11 @@ fn summary_with_markdown_formatting() {
|
||||
let md = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let rendered_path = temp.path().join("book/formatted-summary.html");
|
||||
let rendered_path = temp.path().join("book/toc.js");
|
||||
assert_contains_strings(
|
||||
rendered_path,
|
||||
&[
|
||||
r#"<a href="formatted-summary.html" class="active"><strong aria-hidden="true">1.</strong> Italic code *escape* `escape2`</a>"#,
|
||||
r#"<a href="formatted-summary.html"><strong aria-hidden="true">1.</strong> Italic code *escape* `escape2`</a>"#,
|
||||
r#"<a href="soft.html"><strong aria-hidden="true">2.</strong> Soft line break</a>"#,
|
||||
r#"<a href="escaped-tag.html"><strong aria-hidden="true">3.</strong> <escaped tag></a>"#,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user