Compare commits

..

33 Commits

Author SHA1 Message Date
Eric Huss
27ab7eb2f0 Merge pull request #2470 from ehuss/update-deps
Update dependencies
2024-11-06 17:42:16 +00:00
Eric Huss
6d183be0ec Merge pull request #2471 from ehuss/bump-version
Update to 0.4.41
2024-11-06 17:41:56 +00:00
Eric Huss
c83a34b473 Update to 0.4.41 2024-11-06 09:36:21 -08:00
Eric Huss
d3e0e597d2 Update dependencies 2024-11-06 09:34:07 -08:00
Eric Huss
271bbba7dd Merge pull request #2414 from notriddle/on2
Load the sidebar toc from a shared JS file or iframe
2024-11-02 23:56:19 +00:00
Eric Huss
86ff2e1e6b Merge pull request #2465 from ehuss/footnote-line-height
Set line-height of superscripts to 0
2024-11-02 23:19:27 +00:00
Eric Huss
6ef7cc0ccb Set line-height of superscripts to 0
This changes it so that superscript (and in particular footnote tags)
do not bump the line spacing of previous lines.
2024-11-02 16:12:07 -07:00
Eric Huss
f4cf32e768 Merge pull request #2464 from ehuss/remove-emphasis
Add a real example of remove-emphasis
2024-11-02 22:49:49 +00:00
Eric Huss
47384c1f18 Merge pull request #2463 from Pistonight/bug/theme_popup
fix: themes broken when localStorage has invalid theme id stored
2024-11-02 22:48:17 +00:00
Eric Huss
9e3d533acc Add a real example of remove-emphasis 2024-11-02 15:41:55 -07:00
Eric Huss
5ec4f65ac3 Merge pull request #2454 from GuillaumeGomez/theme-noscript
Improve theme support when JS is disabled
2024-11-02 21:33:57 +00:00
Pistonight
4a330ae36f fix: themes broken when localStorage has invalid theme id stored 2024-10-31 19:02:35 -07:00
Guillaume Gomez
d93fbc0f6b Improve theme support when JS is disabled 2024-10-29 16:20:41 +01:00
Eric Huss
684bb78897 Merge pull request #2448 from jackieh/enhance-syntax-highlighting
Enhance syntax highlighting
2024-10-22 15:49:01 +00:00
Jackie Harris
d0dd16c527 Enhance syntax highlighting
Add syntax highlighting for `hljs-attr` and `hljs-section` CSS classes,
consistent with the Ayu theme.
2024-10-17 12:25:15 -05:00
Eric Huss
f4805343f8 Merge pull request #2442 from hamirmahal/style/simplify-string-formatting-for-readability
style: simplify string formatting for readability
2024-09-25 20:49:56 +00:00
Hamir Mahal
f9add3e936 fix: formatting in src/ and tests/ directories 2024-09-21 15:56:13 -07:00
Hamir Mahal
1fd9656291 style: simplify string formatting for readability 2024-09-21 15:53:59 -07:00
Eric Huss
6f281a6401 Merge pull request #2416 from campeis/update_handlebars_to_v6
chore: update handlebars to v6
2024-09-07 16:11:07 +00:00
Eric Huss
5194d2b3cd Merge pull request #2421 from GuillaumeGomez/copy-code
Unify copy to clipboard icon with docs.rs, rustdoc and crates.io
2024-08-14 15:10:39 +00:00
Guillaume Gomez
b3c23c5f88 Add credits for clipboard image 2024-08-11 16:18:19 +02:00
Eric Huss
a15134cc2f Merge pull request #2423 from radeksvarz/patch-1
added update how to
2024-08-11 12:51:44 +00:00
Eric Huss
b51bb101f2 Tweak heading wording 2024-08-11 05:46:48 -07:00
Eric Huss
59d26dbbe7 Move "upgrade mdbook" description to the build-from-source section 2024-08-11 05:45:26 -07:00
Radek
94baf19e6a added update how to
Updating workflow is not clear for non rust users.
2024-08-07 12:01:19 +02:00
Guillaume Gomez
f1a446fb02 Unify copy to clipboard icon with docs.rs, rustdoc and crates.io 2024-08-02 11:55:17 +02:00
Alessandro Campeis
01d1242753 chore: update handlebars to v6 2024-07-23 10:32:47 +02:00
Michael Howell
203685e91c Make the sidebar work without JS
Uses an iframe instead. The downside of iframes comes from them
not necessarily being same-origin as the main page (particularly
with `file:///` URLs), which can cause themes to fall out of sync,
but that's not a problem here since themes don't work without JS
anyway.
2024-07-16 12:38:00 -07:00
Michael Howell
2cb5b85ab2 Load the sidebar toc from a shared JS file
Before this change, the Rust `unstable-book` is 88MiB.
With this change, it becomes 15MiB. Other pages might not be
as extreme, but it's expected to help any book like this.

This change is so drastic because, if every chapter has a link to
every other chapter, the result is *O*(n<sup>2</sup>) text output.
2024-07-15 18:51:32 -07:00
Eric Huss
ec996d3509 Merge pull request #2406 from ehuss/fix-smart-link
Fix broken link to "Smart Punctuation"
2024-06-24 21:40:46 +00:00
Eric Huss
5ed3223185 Fix broken link to "Smart Punctuation" 2024-06-24 14:32:55 -07:00
Eric Huss
3bdcc0a5a6 Merge pull request #2398 from ehuss/edition2024
Add support for Rust Edition 2024
2024-06-12 22:59:25 +00:00
Eric Huss
1e4d4887e1 Add support for Rust Edition 2024 2024-06-12 15:53:56 -07:00
39 changed files with 1239 additions and 579 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1 @@
book

View File

@@ -0,0 +1,5 @@
[book]
title = "remove-emphasis"
[preprocessor.remove-emphasis]
command = "cargo run --manifest-path=mdbook-remove-emphasis/Cargo.toml --locked"

View 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"

View 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(())
}

View File

@@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View File

@@ -0,0 +1,3 @@
# Chapter 1
This has *light emphasis* and **bold emphasis**.

View 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."));
}

View File

@@ -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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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}");
}
};

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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],

View File

@@ -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
}

View File

@@ -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(&current_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(&current_path))?;
out.write(&tmp)?;
out.write("\"")?;
if path == &current_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 {

View File

@@ -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) => {}
}

View File

@@ -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', {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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%);
}
}

View File

@@ -16,6 +16,7 @@
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-attr,
.hljs-tag,
.hljs-name,
.hljs-regexp,

View File

@@ -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">

View File

@@ -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
View 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
View 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' });
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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("&lt;"),
b'>' => escaped.push_str("&gt;"),
b'\'' => escaped.push_str("&#39;"),
b'\\' => escaped.push_str("&#92;"),
b'&' => escaped.push_str("&amp;"),
_ => 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("<>"), "&lt;&gt;");
assert_eq!(bracket_escape("<test>"), "&lt;test&gt;");
assert_eq!(bracket_escape("a<test>b"), "a&lt;test&gt;b");
assert_eq!(bracket_escape("'"), "'");
assert_eq!(bracket_escape("\\"), "\\");
}
#[test]
fn escaped_special() {
assert_eq!(special_escape(""), "");
assert_eq!(special_escape("<"), "&lt;");
assert_eq!(special_escape(">"), "&gt;");
assert_eq!(special_escape("<>"), "&lt;&gt;");
assert_eq!(special_escape("<test>"), "&lt;test&gt;");
assert_eq!(special_escape("a<test>b"), "a&lt;test&gt;b");
assert_eq!(special_escape("'"), "&#39;");
assert_eq!(special_escape("\\"), "&#92;");
assert_eq!(special_escape("&"), "&amp;");
}
}

View File

@@ -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())

View File

@@ -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`"
);
}

View File

@@ -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> &lt;escaped tag&gt;</a>"#,
],