Merge pull request #2766 from ehuss/crate-split

Split mdbook into multiple crates
This commit is contained in:
Eric Huss 2025-07-24 00:54:10 +00:00 committed by GitHub
commit 97d9078a32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
142 changed files with 2773 additions and 2275 deletions

4
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,4 @@
# Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits.
# Rustfmt for 2024
c7b67e363bb9ce3383636ee615e8e761bf185b33

View file

@ -40,7 +40,7 @@ jobs:
- name: msrv
os: ubuntu-22.04
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
rust: 1.82.0
rust: 1.85.0
target: x86_64-unknown-linux-gnu
name: ${{ matrix.name }}
steps:
@ -48,9 +48,9 @@ jobs:
- name: Install Rust
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
- name: Build and run tests
run: cargo test --locked --target ${{ matrix.target }}
run: cargo test --workspace --locked --target ${{ matrix.target }}
- name: Test no default
run: cargo test --no-default-features --target ${{ matrix.target }}
run: cargo test --workspace --no-default-features --target ${{ matrix.target }}
aarch64-cross-builds:
runs-on: ubuntu-22.04

View file

@ -123,6 +123,18 @@ Please consider the following when making a change:
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
## Tests
The main test harness is described in the [testsuite documentation](tests/testsuite/README.md). There are several different commands to run different kinds of tests:
- `cargo test --workspace` — This runs all of the unit and integration tests, except for the GUI tests.
- `cargo test --test gui` — This runs the [GUI test harness](#browser-compatibility-and-testing). This does not get run automatically due to its extra requirements.
- `npm run lint` — [Checks the `.js` files](#checking-changes-in-js-files)
- `cargo test --workspace --no-default-features` — Testing without default features helps check that all feature checks are implemented correctly.
- `cargo clippy --workspace --all-targets --no-deps -- -D warnings` — This makes sure that there are no clippy warnings.
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --document-private-items --no-deps` — This verifies that there aren't any rustdoc warnings.
- `cargo fmt --check` — Verifies that everything is formatted correctly.
## Making a pull-request
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.

121
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1255,54 +1255,147 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "mdbook"
version = "0.4.52"
version = "0.5.0-alpha.1"
dependencies = [
"ammonia",
"anyhow",
"axum",
"chrono",
"clap",
"clap_complete",
"elasticlunr-rs",
"env_logger",
"futures-util",
"handlebars",
"hex",
"ignore",
"log",
"memchr",
"mdbook-core",
"mdbook-driver",
"mdbook-html",
"mdbook-markdown",
"mdbook-preprocessor",
"mdbook-renderer",
"mdbook-summary",
"notify",
"notify-debouncer-mini",
"opener",
"pathdiff",
"pretty_assertions",
"pulldown-cmark 0.10.3",
"regex",
"select",
"semver",
"serde",
"serde_json",
"sha2",
"shlex",
"snapbox",
"tempfile",
"tokio",
"toml",
"topological-sort",
"tower-http",
"walkdir",
]
[[package]]
name = "mdbook-core"
version = "0.5.0-alpha.1"
dependencies = [
"anyhow",
"log",
"regex",
"serde",
"serde_json",
"tempfile",
"toml",
]
[[package]]
name = "mdbook-driver"
version = "0.5.0-alpha.1"
dependencies = [
"anyhow",
"log",
"mdbook-core",
"mdbook-html",
"mdbook-markdown",
"mdbook-preprocessor",
"mdbook-renderer",
"mdbook-summary",
"regex",
"serde_json",
"shlex",
"tempfile",
"toml",
"topological-sort",
]
[[package]]
name = "mdbook-html"
version = "0.5.0-alpha.1"
dependencies = [
"ammonia",
"anyhow",
"elasticlunr-rs",
"handlebars",
"hex",
"log",
"mdbook-core",
"mdbook-markdown",
"mdbook-renderer",
"pretty_assertions",
"pulldown-cmark 0.10.3",
"regex",
"serde",
"serde_json",
"sha2",
"tempfile",
"toml",
]
[[package]]
name = "mdbook-markdown"
version = "0.5.0-alpha.1"
dependencies = [
"log",
"pulldown-cmark 0.10.3",
"regex",
]
[[package]]
name = "mdbook-preprocessor"
version = "0.5.0-alpha.1"
dependencies = [
"anyhow",
"mdbook-core",
"serde",
"serde_json",
]
[[package]]
name = "mdbook-remove-emphasis"
version = "0.1.0"
dependencies = [
"mdbook",
"mdbook-preprocessor",
"pulldown-cmark 0.12.2",
"pulldown-cmark-to-cmark",
"serde_json",
]
[[package]]
name = "mdbook-renderer"
version = "0.5.0-alpha.1"
dependencies = [
"anyhow",
"mdbook-core",
"serde",
"serde_json",
]
[[package]]
name = "mdbook-summary"
version = "0.5.0-alpha.1"
dependencies = [
"anyhow",
"log",
"mdbook-core",
"memchr",
"pulldown-cmark 0.10.3",
"serde",
]
[[package]]
name = "memchr"
version = "2.7.5"

View file

@ -1,79 +1,130 @@
[workspace]
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
members = [
".",
"crates/*",
"examples/remove-emphasis/mdbook-remove-emphasis",
]
[workspace.lints.clippy]
all = { level = "allow", priority = -2 }
correctness = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
[workspace.lints.rust]
missing_docs = "warn"
rust_2018_idioms = "warn"
[workspace.package]
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/rust-lang/mdBook"
rust-version = "1.85.0" # Keep in sync with installation.md and .github/workflows/main.yml
[workspace.dependencies]
ammonia = "4.1.1"
anyhow = "1.0.98"
axum = "0.8.4"
chrono = { version = "0.4.41", default-features = false, features = ["clock"] }
clap = { version = "4.5.41", features = ["cargo", "wrap_help"] }
clap_complete = "4.5.55"
elasticlunr-rs = "3.0.2"
env_logger = "0.11.8"
futures-util = "0.3.31"
handlebars = "6.3.2"
hex = "0.4.3"
ignore = "0.4.23"
log = "0.4.27"
mdbook-core = { path = "crates/mdbook-core" }
mdbook-driver = { path = "crates/mdbook-driver" }
mdbook-html = { path = "crates/mdbook-html" }
mdbook-markdown = { path = "crates/mdbook-markdown" }
mdbook-preprocessor = { path = "crates/mdbook-preprocessor" }
mdbook-renderer = { path = "crates/mdbook-renderer" }
mdbook-summary = { path = "crates/mdbook-summary" }
memchr = "2.7.5"
notify = "8.1.0"
notify-debouncer-mini = "0.6.0"
opener = "0.8.2"
pathdiff = "0.2.3"
pretty_assertions = "1.4.1"
pulldown-cmark = { version = "0.10.3", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.11.1"
select = "0.6.1"
semver = "1.0.26"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
sha2 = "0.10.9"
shlex = "1.3.0"
snapbox = "0.6.21"
tempfile = "3.20.0"
tokio = "1.46.1"
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
topological-sort = "0.2.2"
tower-http = "0.6.6"
walkdir = "2.5.0"
[package]
name = "mdbook"
version = "0.4.52"
version = "0.5.0-alpha.1"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>"
]
documentation = "https://rust-lang.github.io/mdBook/index.html"
edition = "2021"
edition.workspace = true
exclude = ["/guide/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0"
license.workspace = true
readme = "README.md"
repository = "https://github.com/rust-lang/mdBook"
repository.workspace = true
description = "Creates a book from markdown files"
rust-version = "1.82" # Keep in sync with installation.md and .github/workflows/main.yml
rust-version.workspace = true
[dependencies]
anyhow = "1.0.71"
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
clap_complete = "4.3.2"
env_logger = "0.11.1"
handlebars = "6.0"
hex = "0.4.3"
log = "0.4.17"
memchr = "2.5.0"
opener = "0.8.1"
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.8.1"
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
sha2 = "0.10.8"
shlex = "1.3.0"
tempfile = "3.4.0"
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
topological-sort = "0.2.2"
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
clap_complete.workspace = true
env_logger.workspace = true
log.workspace = true
mdbook-core.workspace = true
mdbook-driver.workspace = true
mdbook-html.workspace = true
mdbook-markdown.workspace = true
mdbook-preprocessor.workspace = true
mdbook-renderer.workspace = true
mdbook-summary.workspace = true
opener.workspace = true
toml.workspace = true
# Watch feature
notify = { version = "8.0.0", optional = true }
notify-debouncer-mini = { version = "0.6.0", optional = true }
ignore = { version = "0.4.20", optional = true }
pathdiff = { version = "0.2.1", optional = true }
walkdir = { version = "2.3.3", optional = true }
ignore = { workspace = true, optional = true }
notify = { workspace = true, optional = true }
notify-debouncer-mini = { workspace = true, optional = true }
pathdiff = { workspace = true, optional = true }
walkdir = { workspace = true, optional = true }
# Serve feature
futures-util = { version = "0.3.28", optional = true }
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"], optional = true }
axum = { version = "0.8.0", features = ["ws"], optional = true }
tower-http = { version = "0.6.0", features = ["fs", "trace"], optional = true }
# Search feature
elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "4.0.0", optional = true }
axum = { workspace = true, features = ["ws"], optional = true }
futures-util = { workspace = true, optional = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"], optional = true }
tower-http = { workspace = true, features = ["fs", "trace"], optional = true }
[dev-dependencies]
select = "0.6.0"
semver = "1.0.17"
snapbox = { version = "0.6.21", features = ["diff", "dir", "term-svg", "regex", "json"] }
pretty_assertions = "1.3.0"
walkdir = "2.3.3"
regex.workspace = true
select.workspace = true
semver.workspace = true
serde_json.workspace = true
snapbox = { workspace = true, features = ["diff", "dir", "term-svg", "regex", "json"] }
tempfile.workspace = true
walkdir.workspace = true
[features]
default = ["watch", "serve", "search"]
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
search = ["dep:elasticlunr-rs", "dep:ammonia"]
search = ["mdbook-html/search"]
[[bin]]
doc = false

View file

@ -0,0 +1,22 @@
[package]
name = "mdbook-core"
version = "0.5.0-alpha.1"
description = "The base support library for mdbook, intended for internal use only"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
log.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,13 @@
# mdbook-core
[![Documentation](https://img.shields.io/docsrs/mdbook-core)](https://docs.rs/mdbook-core)
[![crates.io](https://img.shields.io/crates/v/mdbook-core.svg)](https://crates.io/crates/mdbook-core)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the base support library for [mdBook](https://rust-lang.github.io/mdBook/). It is intended for internal use only. Other mdBook crates depend on this for any types that are shared across the crates.
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -0,0 +1,242 @@
//! A tree structure representing a book.
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fmt::{self, Display, Formatter};
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
/// A tree structure representing a book.
///
/// For the moment a book is just a collection of [`BookItems`] which are
/// accessible by either iterating (immutably) over the book with [`iter()`], or
/// recursively applying a closure to each section to mutate the chapters, using
/// [`for_each_mut()`].
///
/// [`iter()`]: #method.iter
/// [`for_each_mut()`]: #method.for_each_mut
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Book {
/// The sections in this book.
pub sections: Vec<BookItem>,
__non_exhaustive: (),
}
impl Book {
/// Create an empty book.
pub fn new() -> Self {
Default::default()
}
/// Creates a new book with the given items.
pub fn new_with_items(items: Vec<BookItem>) -> Book {
Book {
sections: items,
__non_exhaustive: (),
}
}
/// Get a depth-first iterator over the items in the book.
pub fn iter(&self) -> BookItems<'_> {
BookItems {
items: self.sections.iter().collect(),
}
}
/// Recursively apply a closure to each item in the book, allowing you to
/// mutate them.
///
/// # Note
///
/// Unlike the `iter()` method, this requires a closure instead of returning
/// an iterator. This is because using iterators can possibly allow you
/// to have iterator invalidation errors.
pub fn for_each_mut<F>(&mut self, mut func: F)
where
F: FnMut(&mut BookItem),
{
for_each_mut(&mut func, &mut self.sections);
}
/// Append a `BookItem` to the `Book`.
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
self.sections.push(item.into());
self
}
}
fn for_each_mut<'a, F, I>(func: &mut F, items: I)
where
F: FnMut(&mut BookItem),
I: IntoIterator<Item = &'a mut BookItem>,
{
for item in items {
if let BookItem::Chapter(ch) = item {
for_each_mut(func, &mut ch.sub_items);
}
func(item);
}
}
/// Enum representing any type of item which can be added to a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BookItem {
/// A nested chapter.
Chapter(Chapter),
/// A section separator.
Separator,
/// A part title.
PartTitle(String),
}
impl From<Chapter> for BookItem {
fn from(other: Chapter) -> BookItem {
BookItem::Chapter(other)
}
}
/// The representation of a "chapter", usually mapping to a single file on
/// disk however it may contain multiple sub-chapters.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Chapter {
/// The chapter's name.
pub name: String,
/// The chapter's contents.
pub content: String,
/// The chapter's section number, if it has one.
pub number: Option<SectionNumber>,
/// Nested items.
pub sub_items: Vec<BookItem>,
/// The chapter's location, relative to the `SUMMARY.md` file.
///
/// **Note**: After the index preprocessor runs, any README files will be
/// modified to be `index.md`. If you need access to the actual filename
/// on disk, use [`Chapter::source_path`] instead.
///
/// This is `None` for a draft chapter.
pub path: Option<PathBuf>,
/// The chapter's source file, relative to the `SUMMARY.md` file.
///
/// **Note**: Beware that README files will internally be treated as
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
/// exists if you need access to the true file path.
///
/// This is `None` for a draft chapter, or a synthetically generated
/// chapter that has no file on disk.
pub source_path: Option<PathBuf>,
/// An ordered list of the names of each chapter above this one in the hierarchy.
pub parent_names: Vec<String>,
}
impl Chapter {
/// Create a new chapter with the provided content.
pub fn new<P: Into<PathBuf>>(
name: &str,
content: String,
p: P,
parent_names: Vec<String>,
) -> Chapter {
let path: PathBuf = p.into();
Chapter {
name: name.to_string(),
content,
path: Some(path.clone()),
source_path: Some(path),
parent_names,
..Default::default()
}
}
/// Create a new draft chapter that is not attached to a source markdown file (and thus
/// has no content).
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
Chapter {
name: name.to_string(),
content: String::new(),
path: None,
source_path: None,
parent_names,
..Default::default()
}
}
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
pub fn is_draft_chapter(&self) -> bool {
self.path.is_none()
}
}
impl Display for Chapter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(ref section_number) = self.number {
write!(f, "{section_number} ")?;
}
write!(f, "{}", self.name)
}
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
/// a pretty `Display` impl.
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct SectionNumber(pub Vec<u32>);
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
for item in &self.0 {
write!(f, "{item}.")?;
}
Ok(())
}
}
}
impl Deref for SectionNumber {
type Target = Vec<u32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SectionNumber {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<u32> for SectionNumber {
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
SectionNumber(it.into_iter().collect())
}
}
/// A depth-first iterator over the items in a book.
///
/// # Note
///
/// This struct shouldn't be created directly, instead prefer the
/// [`Book::iter()`] method.
pub struct BookItems<'a> {
items: VecDeque<&'a BookItem>,
}
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;
fn next(&mut self) -> Option<Self::Item> {
let item = self.items.pop_front();
if let Some(BookItem::Chapter(ch)) = item {
// if we wanted a breadth-first iterator we'd `extend()` here
for sub_item in ch.sub_items.iter().rev() {
self.items.push_front(sub_item);
}
}
item
}
}

View file

@ -0,0 +1,124 @@
use super::*;
#[test]
fn section_number_has_correct_dotted_representation() {
let inputs = vec![
(vec![0], "0."),
(vec![1, 3], "1.3."),
(vec![1, 2, 3], "1.2.3."),
];
for (input, should_be) in inputs {
let section_number = SectionNumber(input).to_string();
assert_eq!(section_number, should_be);
}
}
#[test]
fn book_iter_iterates_over_sequential_items() {
let sections = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
..Default::default()
}),
BookItem::Separator,
];
let book = Book::new_with_sections(sections);
let should_be: Vec<_> = book.sections.iter().collect();
let got: Vec<_> = book.iter().collect();
assert_eq!(got, should_be);
}
#[test]
fn for_each_mut_visits_all_items() {
let sections = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
source_path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
];
let mut book = Book::new_with_sections(sections);
let num_items = book.iter().count();
let mut visited = 0;
book.for_each_mut(|_| visited += 1);
assert_eq!(visited, num_items);
}
#[test]
fn iterate_over_nested_book_items() {
let sections = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
source_path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
];
let book = Book::new_with_sections(sections);
let got: Vec<_> = book.iter().collect();
assert_eq!(got.len(), 5);
// checking the chapter names are in the order should be sufficient here...
let chapter_names: Vec<String> = got
.into_iter()
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
})
.collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),
String::from("Goodbye World"),
];
assert_eq!(chapter_names, should_be);
}

View file

@ -9,10 +9,10 @@
//! # Examples
//!
//! ```rust
//! # use mdbook::errors::*;
//! # use anyhow::Result;
//! use std::path::PathBuf;
//! use std::str::FromStr;
//! use mdbook::Config;
//! use mdbook_core::config::Config;
//! use toml::Value;
//!
//! # fn run() -> Result<()> {
@ -47,8 +47,9 @@
//! # run().unwrap()
//! ```
#![deny(missing_docs)]
use crate::utils::log_backtrace;
use crate::utils::toml_ext::TomlExt;
use anyhow::{Context, Error, Result, bail};
use log::{debug, trace, warn};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
@ -57,11 +58,8 @@ use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use toml::value::Table;
use toml::Value;
use crate::errors::*;
use crate::utils::{self, toml_ext::TomlExt};
use toml::value::Table;
/// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
@ -185,7 +183,7 @@ impl Config {
Ok(Some(config)) => Some(config),
Ok(None) => None,
Err(e) => {
utils::log_backtrace(&e);
log_backtrace(&e);
None
}
}
@ -1146,7 +1144,8 @@ mod tests {
assert!(cfg.get(key).is_none());
let encoded_key = encode_env_var(key);
env::set_var(encoded_key, value);
// TODO: This is unsafe, and should be rewritten to use a process.
unsafe { env::set_var(encoded_key, value) };
cfg.update_from_env();
@ -1166,7 +1165,8 @@ mod tests {
assert!(cfg.get(key).is_none());
let encoded_key = encode_env_var(key);
env::set_var(encoded_key, value_str);
// TODO: This is unsafe, and should be rewritten to use a process.
unsafe { env::set_var(encoded_key, value_str) };
cfg.update_from_env();
@ -1185,7 +1185,8 @@ mod tests {
assert_ne!(cfg.book.title, Some(should_be.clone()));
env::set_var("MDBOOK_BOOK__TITLE", &should_be);
// TODO: This is unsafe, and should be rewritten to use a process.
unsafe { env::set_var("MDBOOK_BOOK__TITLE", &should_be) };
cfg.update_from_env();
assert_eq!(cfg.book.title, Some(should_be));

View file

@ -0,0 +1,16 @@
//! The base support library for mdbook, intended for internal use only.
/// The current version of `mdbook`.
///
/// This is provided as a way for custom preprocessors and renderers to do
/// compatibility checks.
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub mod book;
pub mod config;
pub mod utils;
/// The error types used in mdbook.
pub mod errors {
pub use anyhow::{Error, Result};
}

View file

@ -1,6 +1,6 @@
//! Filesystem utilities and helpers.
use crate::errors::*;
use anyhow::{Context, Result};
use log::{debug, trace};
use std::fs::{self, File};
use std::io::Write;
@ -29,7 +29,7 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
///
/// ```rust
/// # use std::path::Path;
/// # use mdbook::utils::fs::path_to_root;
/// # use mdbook_core::utils::fs::path_to_root;
/// let path = Path::new("some/relative/path");
/// assert_eq!(path_to_root(path), "../../");
/// ```

View file

@ -0,0 +1,201 @@
//! Various helpers and utilities.
use anyhow::Error;
use log::error;
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::LazyLock;
pub mod fs;
mod string;
pub mod toml_ext;
pub use self::string::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s\s+").unwrap());
RE.replace_all(text, " ")
}
/// Convert the given string to a valid HTML element ID.
/// The only restriction is that the ID must not contain any ASCII whitespace.
pub fn normalize_id(content: &str) -> String {
content
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} else {
None
}
})
.collect::<String>()
}
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
// This function should be made private when the deprecation expires.
#[deprecated(since = "0.4.16", note = "use unique_id_from_content instead")]
pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
static HTML: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(<.*?>)").unwrap());
content = HTML.replace_all(&content, "").into();
const REPL_SUB: &[&str] = &["&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
// Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_start_matches('#').trim();
normalize_id(trimmed)
}
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
///
/// Each ID returned will be unique, if the same `id_counter` is provided on
/// each call.
pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, usize>) -> String {
let id = {
#[allow(deprecated)]
id_from_content(content)
};
// If we have headers with the same normalized id, append an incrementing counter
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 += 1;
unique_id
}
/// Prints a "backtrace" of some `Error`.
pub fn log_backtrace(e: &Error) {
error!("Error: {}", e);
for cause in e.chain().skip(1) {
error!("\tCaused By: {}", cause);
}
}
/// Escape `<` and `>` for HTML.
pub fn bracket_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;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
escaped.push_str(s);
escaped
}
#[cfg(test)]
mod tests {
use super::bracket_escape;
#[allow(deprecated)]
mod id_from_content {
use super::super::id_from_content;
#[test]
fn it_generates_anchors() {
assert_eq!(
id_from_content("## Method-call expressions"),
"method-call-expressions"
);
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
assert_eq!(id_from_content("## `Code` title"), "code-title");
assert_eq!(
id_from_content("## title <span dir=rtl>foo</span>"),
"title-foo"
);
}
#[test]
fn it_generates_anchors_from_non_ascii_initial() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## 中文標題 CJK title"),
"中文標題-cjk-title"
);
assert_eq!(id_from_content("## Über"), "Über");
}
}
mod html_munging {
use super::super::{normalize_id, unique_id_from_content};
#[test]
fn it_normalizes_ids() {
assert_eq!(
normalize_id("`--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(normalize_id("_-_12345"), "_-_12345");
assert_eq!(normalize_id("12345"), "12345");
assert_eq!(normalize_id("中文"), "中文");
assert_eq!(normalize_id("にほんご"), "にほんご");
assert_eq!(normalize_id("한국어"), "한국어");
assert_eq!(normalize_id(""), "");
}
#[test]
fn it_generates_unique_ids_from_content() {
// Same id if not given shared state
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
"中文標題-cjk-title"
);
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
"中文標題-cjk-title"
);
// Different id if given shared state
let mut id_counter = Default::default();
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über");
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut id_counter),
"中文標題-cjk-title"
);
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-1");
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2");
}
}
#[test]
fn escaped_brackets() {
assert_eq!(bracket_escape(""), "");
assert_eq!(bracket_escape("<"), "&lt;");
assert_eq!(bracket_escape(">"), "&gt;");
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("\\"), "\\");
}
}

View file

@ -1,9 +1,16 @@
//! Helper for working with toml types.
use toml::value::{Table, Value};
pub(crate) trait TomlExt {
/// Helper for working with toml types.
pub trait TomlExt {
/// Read a dotted key.
fn read(&self, key: &str) -> Option<&Value>;
/// Read a dotted key for a mutable value.
fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
/// Insert with a dotted key.
fn insert(&mut self, key: &str, value: Value);
/// Delete a dotted key value.
fn delete(&mut self, key: &str) -> Option<Value>;
}

View file

@ -0,0 +1,27 @@
[package]
name = "mdbook-driver"
version = "0.5.0-alpha.1"
description = "High-level library for running mdBook"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
log.workspace = true
mdbook-core.workspace = true
mdbook-html.workspace = true
mdbook-markdown.workspace = true
mdbook-preprocessor.workspace = true
mdbook-renderer.workspace = true
mdbook-summary.workspace = true
regex.workspace = true
serde_json.workspace = true
shlex.workspace = true
tempfile.workspace = true
toml.workspace = true
topological-sort.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,13 @@
# mdbook-driver
[![Documentation](https://img.shields.io/docsrs/mdbook-driver)](https://docs.rs/mdbook-driver)
[![crates.io](https://img.shields.io/crates/v/mdbook-driver.svg)](https://crates.io/crates/mdbook-driver)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the high-level Rust library for running [mdBook](https://rust-lang.github.io/mdBook/). New books can be created using [`BookBuilder`](https://docs.rs/mdbook-driver/latest/mdbook_driver/init/struct.BookBuilder.html). The primary type [`MDBook`](https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html) can be used to manage and render books.
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,9 +1,9 @@
use super::{Preprocessor, PreprocessorContext};
use crate::book::Book;
use crate::errors::*;
use anyhow::{Context, Result, bail, ensure};
use log::{debug, trace, warn};
use mdbook_core::book::Book;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use shlex::Shlex;
use std::io::{self, Read, Write};
use std::io::{self, Write};
use std::process::{Child, Command, Stdio};
/// A custom preprocessor which will shell out to a 3rd-party program.
@ -41,12 +41,6 @@ impl CmdPreprocessor {
CmdPreprocessor { name, cmd }
}
/// A convenience function custom preprocessors can use to parse the input
/// written to `stdin` by a `CmdRenderer`.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
}
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
let stdin = child.stdin.take().expect("Child has stdin");
@ -183,7 +177,7 @@ mod tests {
use std::path::Path;
fn guide() -> MDBook {
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../guide");
MDBook::load(example).unwrap()
}
@ -200,7 +194,7 @@ mod tests {
let mut buffer = Vec::new();
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
let (got_ctx, got_book) = mdbook_preprocessor::parse_input(buffer.as_slice()).unwrap();
assert_eq!(got_book, md.book);
assert_eq!(got_ctx, ctx);

View file

@ -1,18 +1,18 @@
use anyhow::Result;
use log::warn;
use mdbook_core::book::{Book, BookItem};
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use regex::Regex;
use std::{path::Path, sync::LazyLock};
use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem};
use crate::errors::*;
use log::warn;
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in markdown-based documentation.
#[derive(Default)]
pub struct IndexPreprocessor;
impl IndexPreprocessor {
pub(crate) const NAME: &'static str = "index";
/// Name of this preprocessor.
pub const NAME: &'static str = "index";
/// Create a new `IndexPreprocessor`.
pub fn new() -> Self {

View file

@ -1,18 +1,17 @@
use crate::errors::*;
use crate::utils::{
use anyhow::{Context, Result};
use log::{error, warn};
use mdbook_core::book::{Book, BookItem};
use mdbook_core::utils::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use regex::{CaptureMatches, Captures, Regex};
use std::fs;
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem};
use log::{error, warn};
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
@ -30,7 +29,8 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
pub struct LinkPreprocessor;
impl LinkPreprocessor {
pub(crate) const NAME: &'static str = "links";
/// Name of this preprocessor.
pub const NAME: &'static str = "links";
/// Create a new `LinkPreprocessor`.
pub fn new() -> Self {
@ -684,8 +684,7 @@ mod tests {
#[test]
fn test_find_playgrounds_with_properties() {
let s =
"Some random text with escaped playground {{#playground file.rs editable }} and some \
let s = "Some random text with escaped playground {{#playground file.rs editable }} and some \
more\n text {{#playground my.rs editable no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
@ -714,8 +713,7 @@ mod tests {
#[test]
fn test_find_all_link_types() {
let s =
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
let s = "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
no_run should_panic}} ...";

View file

@ -0,0 +1,9 @@
//! Built-in preprocessors.
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod cmd;
mod index;
mod links;

View file

@ -1,8 +1,8 @@
use crate::book::BookItem;
use crate::errors::*;
use crate::renderer::{RenderContext, Renderer};
use crate::utils;
use anyhow::{Context, Result};
use log::trace;
use mdbook_core::book::BookItem;
use mdbook_core::utils;
use mdbook_renderer::{RenderContext, Renderer};
use std::fs;
#[derive(Default)]

View file

@ -1,106 +1,20 @@
//! `mdbook`'s low level rendering interface.
//! Built-in renderers.
//!
//! # Note
//!
//! You usually don't need to work with this module directly. If you want to
//! implement your own backend, then check out the [For Developers] section of
//! the user guide.
//!
//! The definition for [RenderContext] may be useful though.
//!
//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
//! [RenderContext]: struct.RenderContext.html
pub use self::html_handlebars::HtmlHandlebars;
pub use self::markdown_renderer::MarkdownRenderer;
mod html_handlebars;
mod markdown_renderer;
//! The HTML renderer can be found in the [`mdbook_html`] crate.
use anyhow::{Context, Result, bail};
use log::{error, info, trace, warn};
use mdbook_renderer::{RenderContext, Renderer};
use shlex::Shlex;
use std::collections::HashMap;
use std::fs;
use std::io::{self, ErrorKind, Read};
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
use log::{error, info, trace, warn};
use toml::Value;
use serde::{Deserialize, Serialize};
pub use self::markdown_renderer::MarkdownRenderer;
/// An arbitrary `mdbook` backend.
///
/// Although it's quite possible for you to import `mdbook` as a library and
/// provide your own renderer, there are two main renderer implementations that
/// 99% of users will ever use:
///
/// - [`HtmlHandlebars`] - the built-in HTML renderer
/// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the
/// actual rendering
pub trait Renderer {
/// The `Renderer`'s name.
fn name(&self) -> &str;
/// Invoke the `Renderer`, passing in all the necessary information for
/// describing a book.
fn render(&self, ctx: &RenderContext) -> Result<()>;
}
/// The context provided to all renderers.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RenderContext {
/// Which version of `mdbook` did this come from (as written in `mdbook`'s
/// `Cargo.toml`). Useful if you know the renderer is only compatible with
/// certain versions of `mdbook`.
pub version: String,
/// The book's root directory.
pub root: PathBuf,
/// A loaded representation of the book itself.
pub book: Book,
/// The loaded configuration file.
pub config: Config,
/// Where the renderer *must* put any build artefacts generated. To allow
/// renderers to cache intermediate results, this directory is not
/// guaranteed to be empty or even exist.
pub destination: PathBuf,
#[serde(skip)]
pub(crate) chapter_titles: HashMap<PathBuf, String>,
#[serde(skip)]
__non_exhaustive: (),
}
impl RenderContext {
/// Create a new `RenderContext`.
pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
where
P: Into<PathBuf>,
Q: Into<PathBuf>,
{
RenderContext {
book,
config,
version: crate::MDBOOK_VERSION.to_string(),
root: root.into(),
destination: destination.into(),
chapter_titles: HashMap::new(),
__non_exhaustive: (),
}
}
/// Get the source directory's (absolute) path on disk.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
/// Load a `RenderContext` from its JSON representation.
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
}
}
mod markdown_renderer;
/// A generic renderer which will shell out to an arbitrary executable.
///

View file

@ -1,13 +1,15 @@
//! Support for initializing a new book.
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use super::MDBook;
use crate::config::Config;
use crate::errors::*;
use crate::theme;
use crate::utils::fs::write_file;
use anyhow::{Context, Result};
use log::{debug, error, info, trace};
use mdbook_core::config::Config;
use mdbook_core::utils::fs::write_file;
use mdbook_html::theme;
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]

View file

@ -0,0 +1,68 @@
//! High-level library for running mdBook.
//!
//! This is the high-level library for running
//! [mdBook](https://rust-lang.github.io/mdBook/). There are several
//! reasons for using the programmatic API (over the CLI):
//!
//! - Integrate mdBook in a current project.
//! - Extend the capabilities of mdBook.
//! - Do some processing or test before building your book.
//! - Accessing the public API to help create a new Renderer.
//!
//! ## Additional crates
//!
//! In addition to `mdbook-driver`, there are several other crates available
//! for using and extending mdBook:
//!
//! - [`mdbook_preprocessor`]: Provides support for implementing preprocessors.
//! - [`mdbook_renderer`]: Provides support for implementing renderers.
//! - [`mdbook_markdown`]: The Markdown renderer.
//! - [`mdbook_summary`]: The `SUMMARY.md` parser.
//! - [`mdbook_html`]: The HTML renderer.
//! - [`mdbook_core`]: An internal library that is used by the other crates
//! for shared types. Types from this crate are rexported from the other
//! crates as appropriate.
//!
//! ## Examples
//!
//! If creating a new book from scratch, you'll want to get a [`init::BookBuilder`] via
//! the [`MDBook::init()`] method.
//!
//! ```rust,no_run
//! use mdbook_driver::MDBook;
//! use mdbook_driver::config::Config;
//!
//! let root_dir = "/path/to/book/root";
//!
//! // create a default config and change a couple things
//! let mut cfg = Config::default();
//! cfg.book.title = Some("My Book".to_string());
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
//!
//! MDBook::init(root_dir)
//! .create_gitignore(true)
//! .with_config(cfg)
//! .build()
//! .expect("Book generation failed");
//! ```
//!
//! You can also load an existing book and build it.
//!
//! ```rust,no_run
//! use mdbook_driver::MDBook;
//!
//! let root_dir = "/path/to/book/root";
//!
//! let mut md = MDBook::load(root_dir)
//! .expect("Unable to load the book");
//! md.build().expect("Building failed");
//! ```
pub mod builtin_preprocessors;
pub mod builtin_renderers;
pub mod init;
mod load;
mod mdbook;
pub use mdbook::MDBook;
pub use mdbook_core::{book, config, errors};

View file

@ -0,0 +1,353 @@
use anyhow::{Context, Result};
use log::debug;
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::BuildConfig;
use mdbook_core::utils::bracket_escape;
use mdbook_summary::{Link, Summary, SummaryItem, parse_summary};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
/// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md");
let mut summary_content = String::new();
File::open(&summary_md)
.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:?}"))?;
if cfg.create_missing {
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
}
load_book_from_disk(&summary, src_dir)
}
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut items: Vec<_> = summary
.prefix_chapters
.iter()
.chain(summary.numbered_chapters.iter())
.chain(summary.suffix_chapters.iter())
.collect();
while let Some(next) = items.pop() {
if let SummaryItem::Link(ref link) = *next {
if let Some(ref location) = link.location {
let filename = src_dir.join(location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
debug!("Creating missing file {}", filename.display());
let mut f = File::create(&filename).with_context(|| {
format!("Unable to create missing file: {}", filename.display())
})?;
writeln!(f, "# {}", bracket_escape(&link.name))?;
}
}
items.extend(&link.nested_items);
}
}
Ok(())
}
/// Use the provided `Summary` to load a `Book` from disk.
///
/// You need to pass in the book's source directory because all the links in
/// `SUMMARY.md` give the chapter locations relative to it.
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
debug!("Loading the book from disk");
let src_dir = src_dir.as_ref();
let prefix = summary.prefix_chapters.iter();
let numbered = summary.numbered_chapters.iter();
let suffix = summary.suffix_chapters.iter();
let summary_items = prefix.chain(numbered).chain(suffix);
let mut chapters = Vec::new();
for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
chapters.push(chapter);
}
Ok(Book::new_with_items(chapters))
}
fn load_summary_item<P: AsRef<Path> + Clone>(
item: &SummaryItem,
src_dir: P,
parent_names: Vec<String>,
) -> Result<BookItem> {
match item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(link) => load_chapter(link, src_dir, parent_names).map(BookItem::Chapter),
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
}
}
fn load_chapter<P: AsRef<Path>>(
link: &Link,
src_dir: P,
parent_names: Vec<String>,
) -> Result<Chapter> {
let src_dir = src_dir.as_ref();
let mut ch = if let Some(ref link_location) = link.location {
debug!("Loading {} ({})", link.name, link_location.display());
let location = if link_location.is_absolute() {
link_location.clone()
} else {
src_dir.join(link_location)
};
let mut f = File::open(&location)
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
let mut content = String::new();
f.read_to_string(&mut content).with_context(|| {
format!("Unable to read \"{}\" ({})", link.name, location.display())
})?;
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
content.replace_range(..3, "");
}
let stripped = location
.strip_prefix(src_dir)
.expect("Chapters are always inside a book");
Chapter::new(&link.name, content, stripped, parent_names.clone())
} else {
Chapter::new_draft(&link.name, parent_names.clone())
};
let mut sub_item_parents = parent_names;
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
let sub_items = link
.nested_items
.iter()
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
.collect::<Result<Vec<_>>>()?;
ch.sub_items = sub_items;
Ok(ch)
}
#[cfg(test)]
mod tests {
use super::*;
use mdbook_core::book::SectionNumber;
use std::path::PathBuf;
use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &str = "
# Dummy Chapter
this is some dummy text.
And here is some \
more text.
";
/// Create a dummy `Link` in a temporary directory.
fn dummy_link() -> (Link, TempDir) {
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp.path().join("chapter_1.md");
File::create(&chapter_path)
.unwrap()
.write_all(DUMMY_SRC.as_bytes())
.unwrap();
let link = Link::new("Chapter 1", chapter_path);
(link, temp)
}
/// Create a nested `Link` written to a temporary directory.
fn nested_links() -> (Link, TempDir) {
let (mut root, temp_dir) = dummy_link();
let second_path = temp_dir.path().join("second.md");
File::create(&second_path)
.unwrap()
.write_all(b"Hello World!")
.unwrap();
let mut second = Link::new("Nested Chapter 1", &second_path);
second.number = Some(SectionNumber(vec![1, 2]));
root.nested_items.push(second.clone().into());
root.nested_items.push(SummaryItem::Separator);
root.nested_items.push(second.into());
(root, temp_dir)
}
#[test]
fn load_a_single_chapter_from_disk() {
let (link, temp_dir) = dummy_link();
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_single_chapter_with_utf8_bom_from_disk() {
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp_dir.path().join("chapter_1.md");
File::create(&chapter_path)
.unwrap()
.write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
.unwrap();
let link = Link::new("Chapter 1", chapter_path);
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_a_nonexistent_chapter() {
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
let got = load_chapter(&link, "", Vec::new());
assert!(got.is_err());
}
#[test]
fn load_recursive_link_with_separators() {
let (root, temp) = nested_links();
let nested = Chapter {
name: String::from("Nested Chapter 1"),
content: String::from("Hello World!"),
number: Some(SectionNumber(vec![1, 2])),
path: Some(PathBuf::from("second.md")),
source_path: Some(PathBuf::from("second.md")),
parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(),
};
let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: Some(PathBuf::from("chapter_1.md")),
source_path: Some(PathBuf::from("chapter_1.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested),
],
});
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_book_with_a_single_chapter() {
let (link, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(link)],
..Default::default()
};
let sections = vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: Some(PathBuf::from("chapter_1.md")),
source_path: Some(PathBuf::from("chapter_1.md")),
..Default::default()
})];
let should_be = Book::new_with_items(sections);
let got = load_book_from_disk(&summary, temp.path()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"),
location: Some(PathBuf::from("")),
..Default::default()
})],
..Default::default()
};
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_load_chapters_when_the_link_is_a_directory() {
let (_, temp) = dummy_link();
let dir = temp.path().join("nested");
fs::create_dir(&dir).unwrap();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"),
location: Some(dir),
..Default::default()
})],
..Default::default()
};
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_open_summary_md() {
let cfg = BuildConfig::default();
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let got = load_book(&temp_dir, &cfg);
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
let expected = format!(
r#"Couldn't open SUMMARY.md in {:?} directory"#,
temp_dir.path()
);
assert_eq!(error_message, expected);
}
}

View file

@ -1,19 +1,18 @@
//! The internal representation of a book and infrastructure for loading it from
//! disk and building it.
//!
//! For examples on using `MDBook`, consult the [top-level documentation][1].
//!
//! [1]: ../index.html
mod book;
mod init;
mod summary;
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
//! The high-level interface for loading and rendering books.
use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
use crate::init::BookBuilder;
use crate::load::{load_book, load_book_from_disk};
use anyhow::{Context, Error, Result, bail};
use log::{debug, error, info, log_enabled, trace, warn};
use mdbook_core::book::{Book, BookItem, BookItems};
use mdbook_core::config::{Config, RustEdition};
use mdbook_core::utils;
use mdbook_html::HtmlHandlebars;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use mdbook_renderer::{RenderContext, Renderer};
use mdbook_summary::Summary;
use std::ffi::OsString;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
@ -22,14 +21,8 @@ use tempfile::Builder as TempFileBuilder;
use toml::Value;
use topological_sort::TopologicalSort;
use crate::errors::*;
use crate::preprocess::{
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
};
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
use crate::utils;
use crate::config::{Config, RustEdition};
#[cfg(test)]
mod tests;
/// The object used to manage and build a book.
pub struct MDBook {
@ -104,7 +97,7 @@ impl MDBook {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = book::load_book(src_dir, &config.build)?;
let book = load_book(src_dir, &config.build)?;
let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?;
@ -127,7 +120,7 @@ impl MDBook {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = book::load_book_from_disk(&summary, src_dir)?;
let book = load_book_from_disk(&summary, src_dir)?;
let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?;
@ -141,13 +134,11 @@ impl MDBook {
})
}
/// Returns a flat depth-first iterator over the elements of the book,
/// it returns a [`BookItem`] enum:
/// `(section: String, bookitem: &BookItem)`
/// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
///
/// ```no_run
/// # use mdbook::MDBook;
/// # use mdbook::book::BookItem;
/// # use mdbook_driver::MDBook;
/// # use mdbook_driver::book::BookItem;
/// # let book = MDBook::load("mybook").unwrap();
/// for item in book.iter() {
/// match *item {
@ -611,7 +602,7 @@ fn preprocessor_should_run(
let key = format!("preprocessor.{}.renderers", preprocessor.name());
let renderer_name = renderer.name();
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
if let Some(Value::Array(explicit_renderers)) = cfg.get(&key) {
return explicit_renderers
.iter()
.filter_map(Value::as_str)
@ -620,278 +611,3 @@ fn preprocessor_should_run(
preprocessor.supports_renderer(renderer_name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use toml::value::Table;
#[test]
fn config_defaults_to_html_renderer_if_empty() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.get("output").is_none());
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "html");
}
#[test]
fn add_a_random_renderer_to_the_config() {
let mut cfg = Config::default();
cfg.set("output.random", Table::new()).unwrap();
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn add_a_random_renderer_with_custom_command_to_the_config() {
let mut cfg = Config::default();
let mut table = Table::new();
table.insert("command".to_string(), Value::String("false".to_string()));
cfg.set("output.random", table).unwrap();
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `preprocessor` table
assert!(cfg.get("preprocessor").is_none());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 2);
assert_eq!(got.as_ref().unwrap()[0].name(), "index");
assert_eq!(got.as_ref().unwrap()[1].name(), "links");
}
#[test]
fn use_default_preprocessors_works() {
let mut cfg = Config::default();
cfg.build.use_default_preprocessors = false;
let got = determine_preprocessors(&cfg).unwrap();
assert_eq!(got.len(), 0);
}
#[test]
fn can_determine_third_party_preprocessors() {
let cfg_str = r#"
[book]
title = "Some Book"
[preprocessor.random]
[build]
build-dir = "outputs"
create-missing = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
assert!(cfg.get_preprocessor("random").is_some());
let got = determine_preprocessors(&cfg).unwrap();
assert!(got.into_iter().any(|p| p.name() == "random"));
}
#[test]
fn preprocessors_can_provide_their_own_commands() {
let cfg_str = r#"
[preprocessor.random]
command = "python random.py"
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
let random = cfg.get_preprocessor("random").unwrap();
let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
assert_eq!(random, "python random.py");
}
#[test]
fn preprocessor_before_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
before = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn preprocessor_after_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
after = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn preprocessor_order_is_honored() {
let cfg_str = r#"
[preprocessor.random]
before = [ "last" ]
after = [ "index" ]
[preprocessor.last]
after = [ "links", "index" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
let index = |name| {
preprocessors
.iter()
.enumerate()
.find(|(_, preprocessor)| preprocessor.name() == name)
.unwrap()
.0
};
let assert_before = |before, after| {
if index(before) >= index(after) {
eprintln!("Preprocessor order:");
for preprocessor in &preprocessors {
eprintln!(" {}", preprocessor.name());
}
panic!("{before} should come before {after}");
}
};
assert_before("index", "random");
assert_before("index", "last");
assert_before("random", "last");
assert_before("links", "last");
}
#[test]
fn cyclic_dependencies_are_detected() {
let cfg_str = r#"
[preprocessor.links]
before = [ "index" ]
[preprocessor.index]
before = [ "links" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn dependencies_dont_register_undefined_preprocessors() {
let cfg_str = r#"
[preprocessor.links]
before = [ "random" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(!preprocessors
.iter()
.any(|preprocessor| preprocessor.name() == "random"));
}
#[test]
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
let cfg_str = r#"
[preprocessor.random]
before = [ "links" ]
[build]
use-default-preprocessors = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(!preprocessors
.iter()
.any(|preprocessor| preprocessor.name() == "links"));
}
#[test]
fn config_respects_preprocessor_selection() {
let cfg_str = r#"
[preprocessor.links]
renderers = ["html"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// double-check that we can access preprocessor.links.renderers[0]
let html = cfg
.get_preprocessor("links")
.and_then(|links| links.get("renderers"))
.and_then(Value::as_array)
.and_then(|renderers| renderers.get(0))
.and_then(Value::as_str)
.unwrap();
assert_eq!(html, "html");
let html_renderer = HtmlHandlebars;
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
assert!(should_run);
}
struct BoolPreprocessor(bool);
impl Preprocessor for BoolPreprocessor {
fn name(&self) -> &str {
"bool-preprocessor"
}
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
unimplemented!()
}
fn supports_renderer(&self, _renderer: &str) -> bool {
self.0
}
}
#[test]
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
let cfg = Config::default();
let html = HtmlHandlebars::new();
let should_be = true;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
assert_eq!(got, should_be);
let should_be = false;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
assert_eq!(got, should_be);
}
}

View file

@ -0,0 +1,275 @@
use super::*;
use std::str::FromStr;
use toml::value::Table;
#[test]
fn config_defaults_to_html_renderer_if_empty() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.get("output").is_none());
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "html");
}
#[test]
fn add_a_random_renderer_to_the_config() {
let mut cfg = Config::default();
cfg.set("output.random", Table::new()).unwrap();
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn add_a_random_renderer_with_custom_command_to_the_config() {
let mut cfg = Config::default();
let mut table = Table::new();
table.insert("command".to_string(), Value::String("false".to_string()));
cfg.set("output.random", table).unwrap();
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `preprocessor` table
assert!(cfg.get("preprocessor").is_none());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 2);
assert_eq!(got.as_ref().unwrap()[0].name(), "index");
assert_eq!(got.as_ref().unwrap()[1].name(), "links");
}
#[test]
fn use_default_preprocessors_works() {
let mut cfg = Config::default();
cfg.build.use_default_preprocessors = false;
let got = determine_preprocessors(&cfg).unwrap();
assert_eq!(got.len(), 0);
}
#[test]
fn can_determine_third_party_preprocessors() {
let cfg_str = r#"
[book]
title = "Some Book"
[preprocessor.random]
[build]
build-dir = "outputs"
create-missing = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
assert!(cfg.get_preprocessor("random").is_some());
let got = determine_preprocessors(&cfg).unwrap();
assert!(got.into_iter().any(|p| p.name() == "random"));
}
#[test]
fn preprocessors_can_provide_their_own_commands() {
let cfg_str = r#"
[preprocessor.random]
command = "python random.py"
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
let random = cfg.get_preprocessor("random").unwrap();
let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
assert_eq!(random, "python random.py");
}
#[test]
fn preprocessor_before_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
before = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn preprocessor_after_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
after = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn preprocessor_order_is_honored() {
let cfg_str = r#"
[preprocessor.random]
before = [ "last" ]
after = [ "index" ]
[preprocessor.last]
after = [ "links", "index" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
let index = |name| {
preprocessors
.iter()
.enumerate()
.find(|(_, preprocessor)| preprocessor.name() == name)
.unwrap()
.0
};
let assert_before = |before, after| {
if index(before) >= index(after) {
eprintln!("Preprocessor order:");
for preprocessor in &preprocessors {
eprintln!(" {}", preprocessor.name());
}
panic!("{before} should come before {after}");
}
};
assert_before("index", "random");
assert_before("index", "last");
assert_before("random", "last");
assert_before("links", "last");
}
#[test]
fn cyclic_dependencies_are_detected() {
let cfg_str = r#"
[preprocessor.links]
before = [ "index" ]
[preprocessor.index]
before = [ "links" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg).is_err());
}
#[test]
fn dependencies_dont_register_undefined_preprocessors() {
let cfg_str = r#"
[preprocessor.links]
before = [ "random" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(
!preprocessors
.iter()
.any(|preprocessor| preprocessor.name() == "random")
);
}
#[test]
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
let cfg_str = r#"
[preprocessor.random]
before = [ "links" ]
[build]
use-default-preprocessors = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg).unwrap();
assert!(
!preprocessors
.iter()
.any(|preprocessor| preprocessor.name() == "links")
);
}
#[test]
fn config_respects_preprocessor_selection() {
let cfg_str = r#"
[preprocessor.links]
renderers = ["html"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// double-check that we can access preprocessor.links.renderers[0]
let html = cfg
.get_preprocessor("links")
.and_then(|links| links.get("renderers"))
.and_then(Value::as_array)
.and_then(|renderers| renderers.get(0))
.and_then(Value::as_str)
.unwrap();
assert_eq!(html, "html");
let html_renderer = HtmlHandlebars;
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
assert!(should_run);
}
struct BoolPreprocessor(bool);
impl Preprocessor for BoolPreprocessor {
fn name(&self) -> &str {
"bool-preprocessor"
}
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
unimplemented!()
}
fn supports_renderer(&self, _renderer: &str) -> bool {
self.0
}
}
#[test]
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
let cfg = Config::default();
let html = HtmlHandlebars::new();
let should_be = true;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
assert_eq!(got, should_be);
let should_be = false;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
assert_eq!(got, should_be);
}

View file

@ -0,0 +1,35 @@
[package]
name = "mdbook-html"
version = "0.5.0-alpha.1"
description = "mdBook HTML renderer"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
ammonia = { workspace = true, optional = true }
anyhow.workspace = true
elasticlunr-rs = { workspace = true, optional = true }
handlebars.workspace = true
hex.workspace = true
log.workspace = true
mdbook-core.workspace = true
mdbook-markdown.workspace = true
mdbook-renderer.workspace = true
pulldown-cmark.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
tempfile.workspace = true
toml.workspace = true
[lints]
workspace = true
[features]
search = ["dep:ammonia", "dep:elasticlunr-rs"]

View file

@ -0,0 +1,13 @@
# mdbook-html
[![Documentation](https://img.shields.io/docsrs/mdbook-html)](https://docs.rs/mdbook-html)
[![crates.io](https://img.shields.io/crates/v/mdbook-html.svg)](https://crates.io/crates/mdbook-html)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the HTML renderer for [mdBook](https://rust-lang.github.io/mdBook/). This is intended for internal use only. It is automatically included by [`mdbook-driver`](https://crates.io/crates/mdbook-driver) to render books to HTML.
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,12 +1,17 @@
use crate::book::{Book, BookItem};
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::html_handlebars::StaticFiles;
use crate::renderer::{RenderContext, Renderer};
use crate::theme::{self, Theme};
use crate::utils;
use super::helpers;
use super::static_files::StaticFiles;
use crate::theme::Theme;
use anyhow::{Context, Result, bail};
use handlebars::Handlebars;
use log::{debug, info, trace, warn};
use mdbook_core::book::{Book, BookItem};
use mdbook_core::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
use mdbook_core::utils;
use mdbook_core::utils::fs::get_404_output_file;
use mdbook_markdown::{render_markdown, render_markdown_with_path};
use mdbook_renderer::{RenderContext, Renderer};
use regex::{Captures, Regex};
use serde_json::json;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;
@ -14,12 +19,6 @@ use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::utils::fs::get_404_output_file;
use handlebars::Handlebars;
use log::{debug, info, trace, warn};
use regex::{Captures, Regex};
use serde_json::json;
/// The HTML renderer for mdBook.
#[derive(Default)]
pub struct HtmlHandlebars;
@ -57,13 +56,10 @@ impl HtmlHandlebars {
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
}
let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation());
let content = render_markdown(&ch.content, ctx.html_config.smart_punctuation());
let fixed_content = utils::render_markdown_with_path(
&ch.content,
ctx.html_config.smart_punctuation(),
Some(path),
);
let fixed_content =
render_markdown_with_path(&ch.content, ctx.html_config.smart_punctuation(), Some(path));
if !ctx.is_index && ctx.html_config.print.page_break {
// Add page break between chapters
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
@ -178,8 +174,7 @@ impl HtmlHandlebars {
.to_string()
}
};
let html_content_404 =
utils::render_markdown(&content_404, html_config.smart_punctuation());
let html_content_404 = render_markdown(&content_404, html_config.smart_punctuation());
let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url {
@ -364,7 +359,7 @@ impl Renderer for HtmlHandlebars {
None => ctx.root.join("theme"),
};
let theme = theme::Theme::new(theme_dir);
let theme = Theme::new(theme_dir);
debug!("Register the index handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
@ -400,7 +395,7 @@ impl Renderer for HtmlHandlebars {
// Render search index
#[cfg(feature = "search")]
{
let default = crate::config::Search::default();
let default = mdbook_core::config::Search::default();
let search = html_config.search.as_ref().unwrap_or(&default);
if search.enable {
super::search::create_files(&search, &mut static_files, &book)?;
@ -1008,9 +1003,8 @@ fn collect_redirects_for_path(
#[cfg(test)]
mod tests {
use crate::config::TextDirection;
use super::*;
use mdbook_core::config::TextDirection;
use pretty_assertions::assert_eq;
#[test]
@ -1066,20 +1060,34 @@ mod tests {
#[test]
fn add_playground() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>"),
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>"),
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
(
"<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
),
(
"<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
),
(
"<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
),
(
"<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>",
),
(
"<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
),
(
"<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
@ -1096,14 +1104,22 @@ mod tests {
#[test]
fn add_playground_edition2015() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
(
"<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
),
(
"<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>",
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
@ -1120,14 +1136,22 @@ mod tests {
#[test]
fn add_playground_edition2018() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
(
"<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
),
(
"<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>",
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
@ -1144,14 +1168,22 @@ mod tests {
#[test]
fn add_playground_edition2021() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
(
"<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
),
(
"<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>",
),
(
"<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>",
),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
@ -1169,31 +1201,39 @@ mod tests {
#[test]
fn hide_lines_language_rust() {
let inputs = [
(
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>",),
// # must be followed by a space for a line to be hidden
(
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>",),
(
"<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n# fn main() {\nx()\n# }</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>",
),
// # must be followed by a space for a line to be hidden
(
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">\n#fn main() {\nx()\n#}</code></pre>",
),
(
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>",
),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>",
),
(
"<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>",
),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
),
];
for (src, should_be) in &inputs {
let got = hide_lines(src, &Code::default());
@ -1204,12 +1244,14 @@ mod tests {
#[test]
fn hide_lines_language_other() {
let inputs = [
(
"<code class=\"language-python\">~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()</code>",
"<code class=\"language-python\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
(
"<code class=\"language-python hidelines=!!!\">!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()</code>",
"<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
(
"<code class=\"language-python\">~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()</code>",
"<code class=\"language-python\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",
),
(
"<code class=\"language-python hidelines=!!!\">!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()</code>",
"<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",
),
];
for (src, should_be) in &inputs {
let got = hide_lines(

View file

@ -5,8 +5,8 @@ use handlebars::{
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, Renderable,
};
use crate::utils;
use log::{debug, trace};
use mdbook_core::utils;
use serde_json::json;
type StringMap = BTreeMap<String, String>;

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::utils;
use mdbook_core::utils;
use handlebars::{
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,

View file

@ -1,11 +1,10 @@
use std::path::Path;
use std::{cmp::Ordering, collections::BTreeMap};
use crate::utils::special_escape;
use handlebars::{
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
};
use mdbook_markdown::special_escape;
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]

View file

@ -1,9 +1,7 @@
pub use self::hbs_renderer::HtmlHandlebars;
pub use self::static_files::StaticFiles;
mod hbs_renderer;
mod helpers;
mod static_files;
#[cfg(feature = "search")]
mod search;
mod static_files;
pub use self::hbs_renderer::HtmlHandlebars;

View file

@ -1,20 +1,19 @@
use super::static_files::StaticFiles;
use crate::theme::searcher;
use anyhow::{Context, Result, bail};
use elasticlunr::{Index, IndexBuilder};
use log::{debug, warn};
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::{Search, SearchChapterSettings};
use mdbook_core::utils;
use mdbook_markdown::new_cmark_parser;
use pulldown_cmark::*;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use elasticlunr::{Index, IndexBuilder};
use pulldown_cmark::*;
use crate::book::{Book, BookItem, Chapter};
use crate::config::{Search, SearchChapterSettings};
use crate::errors::*;
use crate::renderer::html_handlebars::StaticFiles;
use crate::theme::searcher;
use crate::utils;
use log::{debug, warn};
use serde::Serialize;
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
@ -134,7 +133,7 @@ fn render_item(
.with_context(|| "Could not convert HTML path to str")?;
let anchor_base = utils::fs::normalize_path(filepath);
let mut p = utils::new_cmark_parser(&chapter.content, false).peekable();
let mut p = new_cmark_parser(&chapter.content, false).peekable();
let mut in_heading = false;
let max_section_depth = u32::from(search_config.heading_split_level);
@ -393,7 +392,7 @@ fn chapter_settings_priority() {
"cli/inner" = { enable = true }
"foo" = {} # Just to make sure empty table is allowed.
"#;
let cfg: crate::Config = toml::from_str(cfg).unwrap();
let cfg: mdbook_core::config::Config = toml::from_str(cfg).unwrap();
let html = cfg.html_config().unwrap();
let chapter_configs = sort_search_config(&html.search.unwrap().chapter);
for (path, enable) in [

View file

@ -1,13 +1,11 @@
//! Support for writing static files.
use super::helpers::resources::ResourceHelper;
use crate::theme::{self, Theme, playground_editor};
use anyhow::{Context, Result};
use log::{debug, warn};
use crate::config::HtmlConfig;
use crate::errors::*;
use crate::renderer::html_handlebars::helpers::resources::ResourceHelper;
use crate::theme::{self, playground_editor, Theme};
use crate::utils;
use mdbook_core::config::HtmlConfig;
use mdbook_core::utils;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::{self, File};
@ -172,7 +170,7 @@ impl StaticFiles {
use std::io::Read;
for static_file in &mut self.static_files {
match static_file {
StaticFile::Builtin {
&mut StaticFile::Builtin {
ref mut filename,
ref data,
} => {
@ -193,7 +191,7 @@ impl StaticFiles {
}
}
}
StaticFile::Additional {
&mut StaticFile::Additional {
ref mut filename,
ref input_location,
} => {
@ -227,7 +225,7 @@ impl StaticFiles {
}
pub fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
use crate::utils::fs::write_file;
use mdbook_core::utils::fs::write_file;
use regex::bytes::{Captures, Regex};
// The `{{ resource "name" }}` directive in static resources look like
// handlebars syntax, even if they technically aren't.
@ -263,8 +261,8 @@ impl StaticFiles {
write_file(destination, filename, &data)?;
}
StaticFile::Additional {
ref input_location,
ref filename,
input_location,
filename,
} => {
let output_location = destination.join(filename);
debug!(
@ -300,9 +298,9 @@ impl StaticFiles {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::HtmlConfig;
use crate::theme::Theme;
use crate::utils::fs::write_file;
use mdbook_core::config::HtmlConfig;
use mdbook_core::utils::fs::write_file;
use tempfile::TempDir;
#[test]

View file

@ -0,0 +1,6 @@
//! mdBook HTML renderer.
mod html_handlebars;
pub mod theme;
pub use html_handlebars::HtmlHandlebars;

View file

@ -0,0 +1,61 @@
pub static CSS: &[u8] = include_bytes!("../../front-end/fonts/fonts.css");
// An array of (file_name, file_contents) pairs
pub static LICENSES: [(&str, &[u8]); 2] = [
(
"fonts/OPEN-SANS-LICENSE.txt",
include_bytes!("../../front-end/fonts/OPEN-SANS-LICENSE.txt"),
),
(
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
include_bytes!("../../front-end/fonts/SOURCE-CODE-PRO-LICENSE.txt"),
),
];
// An array of (file_name, file_contents) pairs
pub static OPEN_SANS: [(&str, &[u8]); 10] = [
(
"fonts/open-sans-v17-all-charsets-300.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-300.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-300italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-300italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-regular.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-regular.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-600.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-600.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-600italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-600italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-700.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-700.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-700italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-700italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-800.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-800.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-800italic.woff2",
include_bytes!("../../front-end/fonts/open-sans-v17-all-charsets-800italic.woff2"),
),
];
// A (file_name, file_contents) pair
pub static SOURCE_CODE_PRO: (&str, &[u8]) = (
"fonts/source-code-pro-v11-all-charsets-500.woff2",
include_bytes!("../../front-end/fonts/source-code-pro-v11-all-charsets-500.woff2"),
);

View file

@ -1,43 +1,46 @@
#![allow(missing_docs)]
pub mod playground_editor;
pub mod fonts;
#[cfg(feature = "search")]
pub mod searcher;
use anyhow::Result;
use log::warn;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use crate::errors::*;
use log::warn;
pub static INDEX: &[u8] = include_bytes!("templates/index.hbs");
pub static HEAD: &[u8] = include_bytes!("templates/head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("templates/redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("templates/header.hbs");
pub static TOC_JS: &[u8] = include_bytes!("templates/toc.js.hbs");
pub static TOC_HTML: &[u8] = include_bytes!("templates/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");
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
pub static FAVICON_PNG: &[u8] = include_bytes!("images/favicon.png");
pub static FAVICON_SVG: &[u8] = include_bytes!("images/favicon.svg");
pub static JS: &[u8] = include_bytes!("js/book.js");
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("js/highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("css/tomorrow-night.css");
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("css/highlight.css");
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("css/ayu-highlight.css");
pub static CLIPBOARD_JS: &[u8] = include_bytes!("js/clipboard.min.js");
pub static FONT_AWESOME: &[u8] = include_bytes!("css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("fonts/FontAwesome.otf");
pub mod fonts;
pub mod playground_editor;
#[cfg(feature = "search")]
pub mod searcher;
pub static INDEX: &[u8] = include_bytes!("../../front-end/templates/index.hbs");
pub static HEAD: &[u8] = include_bytes!("../../front-end/templates/head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("../../front-end/templates/redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("../../front-end/templates/header.hbs");
pub static TOC_JS: &[u8] = include_bytes!("../../front-end/templates/toc.js.hbs");
pub static TOC_HTML: &[u8] = include_bytes!("../../front-end/templates/toc.html.hbs");
pub static CHROME_CSS: &[u8] = include_bytes!("../../front-end/css/chrome.css");
pub static GENERAL_CSS: &[u8] = include_bytes!("../../front-end/css/general.css");
pub static PRINT_CSS: &[u8] = include_bytes!("../../front-end/css/print.css");
pub static VARIABLES_CSS: &[u8] = include_bytes!("../../front-end/css/variables.css");
pub static FAVICON_PNG: &[u8] = include_bytes!("../../front-end/images/favicon.png");
pub static FAVICON_SVG: &[u8] = include_bytes!("../../front-end/images/favicon.svg");
pub static JS: &[u8] = include_bytes!("../../front-end/js/book.js");
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("../../front-end/js/highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("../../front-end/css/tomorrow-night.css");
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("../../front-end/css/highlight.css");
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("../../front-end/css/ayu-highlight.css");
pub static CLIPBOARD_JS: &[u8] = include_bytes!("../../front-end/js/clipboard.min.js");
pub static FONT_AWESOME: &[u8] = include_bytes!("../../front-end/css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &[u8] =
include_bytes!("../../front-end/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &[u8] =
include_bytes!("../../front-end/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &[u8] =
include_bytes!("../../front-end/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &[u8] =
include_bytes!("../../front-end/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &[u8] =
include_bytes!("../../front-end/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("../../front-end/fonts/FontAwesome.otf");
/// The `Theme` struct should be used instead of the static variables because
/// the `new()` method will look if the user has a theme directory in their

View file

@ -0,0 +1,8 @@
//! Theme dependencies for the playground editor.
pub static JS: &[u8] = include_bytes!("../../front-end/playground_editor/editor.js");
pub static ACE_JS: &[u8] = include_bytes!("../../front-end/playground_editor/ace.js");
pub static MODE_RUST_JS: &[u8] = include_bytes!("../../front-end/playground_editor/mode-rust.js");
pub static THEME_DAWN_JS: &[u8] = include_bytes!("../../front-end/playground_editor/theme-dawn.js");
pub static THEME_TOMORROW_NIGHT_JS: &[u8] =
include_bytes!("../../front-end/playground_editor/theme-tomorrow_night.js");

View file

@ -0,0 +1,6 @@
//! Theme dependencies for in-browser search. Not included in mdbook when
//! the "search" cargo feature is disabled.
pub static JS: &[u8] = include_bytes!("../../front-end/searcher/searcher.js");
pub static MARK_JS: &[u8] = include_bytes!("../../front-end/searcher/mark.min.js");
pub static ELASTICLUNR_JS: &[u8] = include_bytes!("../../front-end/searcher/elasticlunr.min.js");

View file

@ -0,0 +1,16 @@
[package]
name = "mdbook-markdown"
version = "0.5.0-alpha.1"
description = "Markdown processing used in mdBook"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
log.workspace = true
pulldown-cmark.workspace = true
regex.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,13 @@
# mdbook-markdown
[![Documentation](https://img.shields.io/docsrs/mdbook-markdown)](https://docs.rs/mdbook-markdown)
[![crates.io](https://img.shields.io/crates/v/mdbook-markdown.svg)](https://crates.io/crates/mdbook-markdown)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Markdown support library for [mdBook](https://rust-lang.github.io/mdBook/). Rust crates (such as preprocessors) can use this library to process Markdown in the same way as mdBook.
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,194 +1,27 @@
//! Various helpers and utilities.
//! Markdown processing used in mdBook.
//!
//! This crate provides functions for processing Markdown in the same way as
//! [mdBook](https://rust-lang.github.io/mdBook/). The [`pulldown_cmark`]
//! crate is used as the underlying parser. This crate re-exports
//! [`pulldown_cmark`] so that you can access its types.
//!
//! The parser in this library adds several modifications to the
//! [`pulldown_cmark`] event stream. For example, it adjusts some links,
//! modifies the behavior of footnotes, and adds various HTML wrappers.
pub mod fs;
mod string;
pub(crate) mod toml_ext;
use crate::errors::Error;
use log::error;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd, html};
use regex::Regex;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write;
use std::path::Path;
use std::sync::LazyLock;
pub use self::string::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
#[doc(inline)]
pub use pulldown_cmark;
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s\s+").unwrap());
RE.replace_all(text, " ")
}
/// Convert the given string to a valid HTML element ID.
/// The only restriction is that the ID must not contain any ASCII whitespace.
pub fn normalize_id(content: &str) -> String {
content
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} else {
None
}
})
.collect::<String>()
}
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
// This function should be made private when the deprecation expires.
#[deprecated(since = "0.4.16", note = "use unique_id_from_content instead")]
pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
static HTML: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(<.*?>)").unwrap());
content = HTML.replace_all(&content, "").into();
const REPL_SUB: &[&str] = &["&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
// Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_start_matches('#').trim();
normalize_id(trimmed)
}
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
///
/// Each ID returned will be unique, if the same `id_counter` is provided on
/// each call.
pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, usize>) -> String {
let id = {
#[allow(deprecated)]
id_from_content(content)
};
// If we have headers with the same normalized id, append an incrementing counter
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 += 1;
unique_id
}
/// Fix links to the correct location.
///
/// This adjusts links, such as turning `.md` extensions to `.html`.
///
/// `path` is the path to the page being rendered relative to the root of the
/// book. This is used for the `print.html` page so that links on the print
/// page go to the original location. Normal page rendering sets `path` to
/// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult.
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
static SCHEME_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
static MD_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
if dest.starts_with('#') {
// Fragment-only link.
if let Some(path) = path {
let mut base = path.display().to_string();
if base.ends_with(".md") {
base.replace_range(base.len() - 3.., ".html");
}
return format!("{base}{dest}").into();
} else {
return dest;
}
}
// Don't modify links with schemes like `https`.
if !SCHEME_LINK.is_match(&dest) {
// This is a relative link, adjust it as necessary.
let mut fixed_link = String::new();
if let Some(path) = path {
let base = path
.parent()
.expect("path can't be empty")
.to_str()
.expect("utf-8 paths only");
if !base.is_empty() {
write!(fixed_link, "{base}/").unwrap();
}
}
if let Some(caps) = MD_LINK.captures(&dest) {
fixed_link.push_str(&caps["link"]);
fixed_link.push_str(".html");
if let Some(anchor) = caps.name("anchor") {
fixed_link.push_str(anchor.as_str());
}
} else {
fixed_link.push_str(&dest);
};
return CowStr::from(fixed_link);
}
dest
}
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
// This is a terrible hack, but should be reasonably reliable. Nobody
// should ever parse a tag with a regex. However, there isn't anything
// in Rust that I know of that is suitable for handling partial html
// fragments like those generated by pulldown_cmark.
//
// There are dozens of HTML tags/attributes that contain paths, so
// feel free to add more tags if desired; these are the only ones I
// care about right now.
static HTML_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
HTML_LINK
.replace_all(&html, |caps: &regex::Captures<'_>| {
let fixed = fix(caps[2].into(), path);
format!("{}{}\"", &caps[1], fixed)
})
.into_owned()
.into()
}
match event {
Event::Start(Tag::Link {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Link {
link_type,
dest_url: fix(dest_url, path),
title,
id,
}),
Event::Start(Tag::Image {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Image {
link_type,
dest_url: fix(dest_url, path),
title,
id,
}),
Event::Html(html) => Event::Html(fix_html(html, path)),
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
_ => event,
}
}
#[cfg(test)]
mod tests;
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
@ -415,16 +248,115 @@ fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
}
}
/// Prints a "backtrace" of some `Error`.
pub fn log_backtrace(e: &Error) {
error!("Error: {}", e);
/// Fix links to the correct location.
///
/// This adjusts links, such as turning `.md` extensions to `.html`.
///
/// `path` is the path to the page being rendered relative to the root of the
/// book. This is used for the `print.html` page so that links on the print
/// page go to the original location. Normal page rendering sets `path` to
/// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult.
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
static SCHEME_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
static MD_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
for cause in e.chain().skip(1) {
error!("\tCaused By: {}", cause);
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
if dest.starts_with('#') {
// Fragment-only link.
if let Some(path) = path {
let mut base = path.display().to_string();
if base.ends_with(".md") {
base.replace_range(base.len() - 3.., ".html");
}
return format!("{base}{dest}").into();
} else {
return dest;
}
}
// Don't modify links with schemes like `https`.
if !SCHEME_LINK.is_match(&dest) {
// This is a relative link, adjust it as necessary.
let mut fixed_link = String::new();
if let Some(path) = path {
let base = path
.parent()
.expect("path can't be empty")
.to_str()
.expect("utf-8 paths only");
if !base.is_empty() {
write!(fixed_link, "{base}/").unwrap();
}
}
if let Some(caps) = MD_LINK.captures(&dest) {
fixed_link.push_str(&caps["link"]);
fixed_link.push_str(".html");
if let Some(anchor) = caps.name("anchor") {
fixed_link.push_str(anchor.as_str());
}
} else {
fixed_link.push_str(&dest);
};
return CowStr::from(fixed_link);
}
dest
}
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
// This is a terrible hack, but should be reasonably reliable. Nobody
// should ever parse a tag with a regex. However, there isn't anything
// in Rust that I know of that is suitable for handling partial html
// fragments like those generated by pulldown_cmark.
//
// There are dozens of HTML tags/attributes that contain paths, so
// feel free to add more tags if desired; these are the only ones I
// care about right now.
static HTML_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
HTML_LINK
.replace_all(&html, |caps: &regex::Captures<'_>| {
let fixed = fix(caps[2].into(), path);
format!("{}{}\"", &caps[1], fixed)
})
.into_owned()
.into()
}
match event {
Event::Start(Tag::Link {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Link {
link_type,
dest_url: fix(dest_url, path),
title,
id,
}),
Event::Start(Tag::Image {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Image {
link_type,
dest_url: fix(dest_url, path),
title,
id,
}),
Event::Html(html) => Event::Html(fix_html(html, path)),
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
_ => event,
}
}
pub(crate) fn special_escape(mut s: &str) -> String {
/// Escape characters to make it safe for an HTML string.
pub 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) {
@ -443,262 +375,3 @@ pub(crate) fn special_escape(mut s: &str) -> String {
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] = &['<', '>'];
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;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
escaped.push_str(s);
escaped
}
#[cfg(test)]
mod tests {
use super::{bracket_escape, special_escape};
mod render_markdown {
use super::super::render_markdown;
#[test]
fn preserves_external_links() {
assert_eq!(
render_markdown("[example](https://www.rust-lang.org/)", false),
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
);
}
#[test]
fn it_can_adjust_markdown_links() {
assert_eq!(
render_markdown("[example](example.md)", false),
"<p><a href=\"example.html\">example</a></p>\n"
);
assert_eq!(
render_markdown("[example_anchor](example.md#anchor)", false),
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
);
// this anchor contains 'md' inside of it
assert_eq!(
render_markdown("[phantom data](foo.html#phantomdata)", false),
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
);
}
#[test]
fn it_can_wrap_tables() {
let src = r#"
| Original | Punycode | Punycode + Encoding |
|-----------------|-----------------|---------------------|
| føø | f-5gaa | f_5gaa |
"#;
let out = r#"
<div class="table-wrapper"><table><thead><tr><th>Original</th><th>Punycode</th><th>Punycode + Encoding</th></tr></thead><tbody>
<tr><td>føø</td><td>f-5gaa</td><td>f_5gaa</td></tr>
</tbody></table>
</div>
"#.trim();
assert_eq!(render_markdown(src, false), out);
}
#[test]
fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
}
#[test]
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
let input = r#"
'one'
```
'two'
```
`'three'` 'four'"#;
let expected = r#"<p>one</p>
<pre><code>'two'
</code></pre>
<p><code>'three'</code> four</p>
"#;
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn whitespace_outside_of_codeblock_header_is_preserved() {
let input = r#"
some text with spaces
```rust
fn main() {
// code inside is unchanged
}
```
more text with spaces
"#;
let expected = r#"<p>some text with spaces</p>
<pre><code class="language-rust">fn main() {
// code inside is unchanged
}
</code></pre>
<p>more text with spaces</p>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_properties_are_passed_as_space_delimited_class() {
let input = r#"
```rust,no_run,should_panic,property_3
```
"#;
let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
let input = r#"
```rust, no_run,,,should_panic , ,property_3
```
"#;
let expected = r#"<pre><code class="language-rust,,,,,no_run,,,should_panic,,,,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_without_properties_has_proper_html_class() {
let input = r#"
```rust
```
"#;
let expected = r#"<pre><code class="language-rust"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
let input = r#"
```rust
```
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
}
#[allow(deprecated)]
mod id_from_content {
use super::super::id_from_content;
#[test]
fn it_generates_anchors() {
assert_eq!(
id_from_content("## Method-call expressions"),
"method-call-expressions"
);
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
assert_eq!(id_from_content("## `Code` title"), "code-title");
assert_eq!(
id_from_content("## title <span dir=rtl>foo</span>"),
"title-foo"
);
}
#[test]
fn it_generates_anchors_from_non_ascii_initial() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## 中文標題 CJK title"),
"中文標題-cjk-title"
);
assert_eq!(id_from_content("## Über"), "Über");
}
}
mod html_munging {
use super::super::{normalize_id, unique_id_from_content};
#[test]
fn it_normalizes_ids() {
assert_eq!(
normalize_id("`--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(normalize_id("_-_12345"), "_-_12345");
assert_eq!(normalize_id("12345"), "12345");
assert_eq!(normalize_id("中文"), "中文");
assert_eq!(normalize_id("にほんご"), "にほんご");
assert_eq!(normalize_id("한국어"), "한국어");
assert_eq!(normalize_id(""), "");
}
#[test]
fn it_generates_unique_ids_from_content() {
// Same id if not given shared state
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
"中文標題-cjk-title"
);
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
"中文標題-cjk-title"
);
// Different id if given shared state
let mut id_counter = Default::default();
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über");
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut id_counter),
"中文標題-cjk-title"
);
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-1");
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2");
}
}
#[test]
fn escaped_brackets() {
assert_eq!(bracket_escape(""), "");
assert_eq!(bracket_escape("<"), "&lt;");
assert_eq!(bracket_escape(">"), "&gt;");
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

@ -0,0 +1,147 @@
use super::render_markdown;
use super::*;
#[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;");
}
#[test]
fn preserves_external_links() {
assert_eq!(
render_markdown("[example](https://www.rust-lang.org/)", false),
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
);
}
#[test]
fn it_can_adjust_markdown_links() {
assert_eq!(
render_markdown("[example](example.md)", false),
"<p><a href=\"example.html\">example</a></p>\n"
);
assert_eq!(
render_markdown("[example_anchor](example.md#anchor)", false),
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
);
// this anchor contains 'md' inside of it
assert_eq!(
render_markdown("[phantom data](foo.html#phantomdata)", false),
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
);
}
#[test]
fn it_can_wrap_tables() {
let src = r#"
| Original | Punycode | Punycode + Encoding |
|-----------------|-----------------|---------------------|
| føø | f-5gaa | f_5gaa |
"#;
let out = r#"
<div class="table-wrapper"><table><thead><tr><th>Original</th><th>Punycode</th><th>Punycode + Encoding</th></tr></thead><tbody>
<tr><td>føø</td><td>f-5gaa</td><td>f_5gaa</td></tr>
</tbody></table>
</div>
"#.trim();
assert_eq!(render_markdown(src, false), out);
}
#[test]
fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
}
#[test]
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
let input = r#"
'one'
```
'two'
```
`'three'` 'four'"#;
let expected = r#"<p>one</p>
<pre><code>'two'
</code></pre>
<p><code>'three'</code> four</p>
"#;
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn whitespace_outside_of_codeblock_header_is_preserved() {
let input = r#"
some text with spaces
```rust
fn main() {
// code inside is unchanged
}
```
more text with spaces
"#;
let expected = r#"<p>some text with spaces</p>
<pre><code class="language-rust">fn main() {
// code inside is unchanged
}
</code></pre>
<p>more text with spaces</p>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_properties_are_passed_as_space_delimited_class() {
let input = r#"
```rust,no_run,should_panic,property_3
```
"#;
let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
let input = r#"
```rust, no_run,,,should_panic , ,property_3
```
"#;
let expected = r#"<pre><code class="language-rust,,,,,no_run,,,should_panic,,,,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_without_properties_has_proper_html_class() {
let input = r#"
```rust
```
"#;
let expected = r#"<pre><code class="language-rust"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
let input = r#"
```rust
```
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}

View file

@ -0,0 +1,17 @@
[package]
name = "mdbook-preprocessor"
version = "0.5.0-alpha.1"
description = "Library to assist implementing an mdBook preprocessor"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
mdbook-core.workspace = true
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,13 @@
# mdbook-preprocessor
[![Documentation](https://img.shields.io/docsrs/mdbook-preprocessor)](https://docs.rs/mdbook-preprocessor)
[![crates.io](https://img.shields.io/crates/v/mdbook-preprocessor.svg)](https://crates.io/crates/mdbook-preprocessor)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Rust library to implement a [preprocessor](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html) for [mdBook](https://rust-lang.github.io/mdBook/).
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View file

@ -1,56 +1,32 @@
//! Book preprocessing.
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod cmd;
mod index;
mod links;
use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
//! Library to assist implementing an mdbook preprocessor.
//!
//! This library is used to implement a
//! [preprocessor](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html)
//! for [mdBook](https://rust-lang.github.io/mdBook/). See the linked chapter
//! for more information on how to implement a preprocessor.
use anyhow::Context;
use mdbook_core::book::Book;
use mdbook_core::config::Config;
use mdbook_core::errors::Result;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Read;
use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
/// The book configuration (`book.toml`).
pub config: Config,
/// The `Renderer` this preprocessor is being used with.
pub renderer: String,
/// The calling `mdbook` version.
pub mdbook_version: String,
#[serde(skip)]
pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>,
#[serde(skip)]
__non_exhaustive: (),
}
impl PreprocessorContext {
/// Create a new `PreprocessorContext`.
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
PreprocessorContext {
root,
config,
renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(),
chapter_titles: RefCell::new(HashMap::new()),
__non_exhaustive: (),
}
}
}
pub use mdbook_core::MDBOOK_VERSION;
pub use mdbook_core::book;
pub use mdbook_core::config;
pub use mdbook_core::errors;
/// An operation which is run immediately after loading a book into memory and
/// before it gets rendered.
///
/// Types that implement the `Preprocessor` trait can be used with
/// [`MDBook::with_preprocessor`] to programmatically add preprocessors.
///
/// [`MDBook::with_preprocessor`]: https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html#method.with_preprocessor
pub trait Preprocessor {
/// Get the `Preprocessor`'s name.
fn name(&self) -> &str;
@ -67,3 +43,44 @@ pub trait Preprocessor {
true
}
}
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
/// The book configuration (`book.toml`).
pub config: Config,
/// The `Renderer` this preprocessor is being used with.
pub renderer: String,
/// The calling `mdbook` version.
pub mdbook_version: String,
/// Internal mapping of chapter titles.
///
/// This is used internally by mdbook to compute custom chapter titles.
/// This should not be used outside of mdbook's internals.
#[serde(skip)]
pub chapter_titles: RefCell<HashMap<PathBuf, String>>,
#[serde(skip)]
__non_exhaustive: (),
}
impl PreprocessorContext {
/// Create a new `PreprocessorContext`.
pub fn new(root: PathBuf, config: Config, renderer: String) -> Self {
PreprocessorContext {
root,
config,
renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(),
chapter_titles: RefCell::new(HashMap::new()),
__non_exhaustive: (),
}
}
}
/// Parses the input given to a preprocessor.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
}

View file

@ -0,0 +1,17 @@
[package]
name = "mdbook-renderer"
version = "0.5.0-alpha.1"
description = "Library to assist implementing an mdBook renderer"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
mdbook-core.workspace = true
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,13 @@
# mdbook-renderer
[![Documentation](https://img.shields.io/docsrs/mdbook-renderer)](https://docs.rs/mdbook-renderer)
[![crates.io](https://img.shields.io/crates/v/mdbook-renderer.svg)](https://crates.io/crates/mdbook-renderer)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the Rust library to implement a [renderer](https://rust-lang.github.io/mdBook/for_developers/backends.html) for [mdBook](https://rust-lang.github.io/mdBook/).
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

Some files were not shown because too many files have changed in this diff Show more