Merge pull request #2766 from ehuss/crate-split
Split mdbook into multiple crates
This commit is contained in:
commit
97d9078a32
142 changed files with 2773 additions and 2275 deletions
4
.git-blame-ignore-revs
Normal file
4
.git-blame-ignore-revs
Normal 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
|
||||
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
121
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
139
Cargo.toml
139
Cargo.toml
|
|
@ -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
|
||||
|
|
|
|||
22
crates/mdbook-core/Cargo.toml
Normal file
22
crates/mdbook-core/Cargo.toml
Normal 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
|
||||
13
crates/mdbook-core/README.md
Normal file
13
crates/mdbook-core/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# mdbook-core
|
||||
|
||||
[](https://docs.rs/mdbook-core)
|
||||
[](https://crates.io/crates/mdbook-core)
|
||||
[](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)
|
||||
242
crates/mdbook-core/src/book.rs
Normal file
242
crates/mdbook-core/src/book.rs
Normal 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
|
||||
}
|
||||
}
|
||||
124
crates/mdbook-core/src/book/tests.rs
Normal file
124
crates/mdbook-core/src/book/tests.rs
Normal 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);
|
||||
}
|
||||
|
||||
|
|
@ -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));
|
||||
16
crates/mdbook-core/src/lib.rs
Normal file
16
crates/mdbook-core/src/lib.rs
Normal 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};
|
||||
}
|
||||
|
|
@ -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), "../../");
|
||||
/// ```
|
||||
201
crates/mdbook-core/src/utils/mod.rs
Normal file
201
crates/mdbook-core/src/utils/mod.rs
Normal 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] = &["<", ">", "&", "'", """];
|
||||
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("<"),
|
||||
b'>' => escaped.push_str(">"),
|
||||
_ => 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("<"), "<");
|
||||
assert_eq!(bracket_escape(">"), ">");
|
||||
assert_eq!(bracket_escape("<>"), "<>");
|
||||
assert_eq!(bracket_escape("<test>"), "<test>");
|
||||
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(bracket_escape("'"), "'");
|
||||
assert_eq!(bracket_escape("\\"), "\\");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
27
crates/mdbook-driver/Cargo.toml
Normal file
27
crates/mdbook-driver/Cargo.toml
Normal 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
|
||||
13
crates/mdbook-driver/README.md
Normal file
13
crates/mdbook-driver/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# mdbook-driver
|
||||
|
||||
[](https://docs.rs/mdbook-driver)
|
||||
[](https://crates.io/crates/mdbook-driver)
|
||||
[](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)
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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}} ...";
|
||||
|
||||
9
crates/mdbook-driver/src/builtin_preprocessors/mod.rs
Normal file
9
crates/mdbook-driver/src/builtin_preprocessors/mod.rs
Normal 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;
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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.
|
||||
///
|
||||
|
|
@ -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)]
|
||||
68
crates/mdbook-driver/src/lib.rs
Normal file
68
crates/mdbook-driver/src/lib.rs
Normal 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};
|
||||
353
crates/mdbook-driver/src/load.rs
Normal file
353
crates/mdbook-driver/src/load.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
275
crates/mdbook-driver/src/mdbook/tests.rs
Normal file
275
crates/mdbook-driver/src/mdbook/tests.rs
Normal 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);
|
||||
}
|
||||
35
crates/mdbook-html/Cargo.toml
Normal file
35
crates/mdbook-html/Cargo.toml
Normal 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"]
|
||||
13
crates/mdbook-html/README.md
Normal file
13
crates/mdbook-html/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# mdbook-html
|
||||
|
||||
[](https://docs.rs/mdbook-html)
|
||||
[](https://crates.io/crates/mdbook-html)
|
||||
[](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)
|
||||
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
|
@ -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(
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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)]
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 [
|
||||
|
|
@ -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]
|
||||
6
crates/mdbook-html/src/lib.rs
Normal file
6
crates/mdbook-html/src/lib.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//! mdBook HTML renderer.
|
||||
|
||||
mod html_handlebars;
|
||||
pub mod theme;
|
||||
|
||||
pub use html_handlebars::HtmlHandlebars;
|
||||
61
crates/mdbook-html/src/theme/fonts.rs
Normal file
61
crates/mdbook-html/src/theme/fonts.rs
Normal 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"),
|
||||
);
|
||||
|
|
@ -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
|
||||
8
crates/mdbook-html/src/theme/playground_editor.rs
Normal file
8
crates/mdbook-html/src/theme/playground_editor.rs
Normal 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");
|
||||
6
crates/mdbook-html/src/theme/searcher.rs
Normal file
6
crates/mdbook-html/src/theme/searcher.rs
Normal 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");
|
||||
16
crates/mdbook-markdown/Cargo.toml
Normal file
16
crates/mdbook-markdown/Cargo.toml
Normal 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
|
||||
13
crates/mdbook-markdown/README.md
Normal file
13
crates/mdbook-markdown/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# mdbook-markdown
|
||||
|
||||
[](https://docs.rs/mdbook-markdown)
|
||||
[](https://crates.io/crates/mdbook-markdown)
|
||||
[](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)
|
||||
|
|
@ -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] = &["<", ">", "&", "'", """];
|
||||
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: ®ex::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: ®ex::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("<"),
|
||||
b'>' => escaped.push_str(">"),
|
||||
_ => 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("<"), "<");
|
||||
assert_eq!(bracket_escape(">"), ">");
|
||||
assert_eq!(bracket_escape("<>"), "<>");
|
||||
assert_eq!(bracket_escape("<test>"), "<test>");
|
||||
assert_eq!(bracket_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(bracket_escape("'"), "'");
|
||||
assert_eq!(bracket_escape("\\"), "\\");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_special() {
|
||||
assert_eq!(special_escape(""), "");
|
||||
assert_eq!(special_escape("<"), "<");
|
||||
assert_eq!(special_escape(">"), ">");
|
||||
assert_eq!(special_escape("<>"), "<>");
|
||||
assert_eq!(special_escape("<test>"), "<test>");
|
||||
assert_eq!(special_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(special_escape("'"), "'");
|
||||
assert_eq!(special_escape("\\"), "\");
|
||||
assert_eq!(special_escape("&"), "&");
|
||||
}
|
||||
}
|
||||
147
crates/mdbook-markdown/src/tests.rs
Normal file
147
crates/mdbook-markdown/src/tests.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
use super::render_markdown;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn escaped_special() {
|
||||
assert_eq!(special_escape(""), "");
|
||||
assert_eq!(special_escape("<"), "<");
|
||||
assert_eq!(special_escape(">"), ">");
|
||||
assert_eq!(special_escape("<>"), "<>");
|
||||
assert_eq!(special_escape("<test>"), "<test>");
|
||||
assert_eq!(special_escape("a<test>b"), "a<test>b");
|
||||
assert_eq!(special_escape("'"), "'");
|
||||
assert_eq!(special_escape("\\"), "\");
|
||||
assert_eq!(special_escape("&"), "&");
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
17
crates/mdbook-preprocessor/Cargo.toml
Normal file
17
crates/mdbook-preprocessor/Cargo.toml
Normal 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
|
||||
13
crates/mdbook-preprocessor/README.md
Normal file
13
crates/mdbook-preprocessor/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# mdbook-preprocessor
|
||||
|
||||
[](https://docs.rs/mdbook-preprocessor)
|
||||
[](https://crates.io/crates/mdbook-preprocessor)
|
||||
[](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)
|
||||
|
|
@ -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")
|
||||
}
|
||||
17
crates/mdbook-renderer/Cargo.toml
Normal file
17
crates/mdbook-renderer/Cargo.toml
Normal 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
|
||||
13
crates/mdbook-renderer/README.md
Normal file
13
crates/mdbook-renderer/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# mdbook-renderer
|
||||
|
||||
[](https://docs.rs/mdbook-renderer)
|
||||
[](https://crates.io/crates/mdbook-renderer)
|
||||
[](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
Loading…
Add table
Reference in a new issue