diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..94c043e3 --- /dev/null +++ b/.git-blame-ignore-revs @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 73f940b4..18b4b82e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29f47f9a..2c230fe3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index dc7e85eb..10ec141e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b15eed77..5a615801 100644 --- a/Cargo.toml +++ b/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 ", "Michael-F-Bryan ", "Matt Ickstadt " ] 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 diff --git a/crates/mdbook-core/Cargo.toml b/crates/mdbook-core/Cargo.toml new file mode 100644 index 00000000..6841704b --- /dev/null +++ b/crates/mdbook-core/Cargo.toml @@ -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 diff --git a/crates/mdbook-core/README.md b/crates/mdbook-core/README.md new file mode 100644 index 00000000..9d541479 --- /dev/null +++ b/crates/mdbook-core/README.md @@ -0,0 +1,13 @@ +# mdbook-core + +[![Documentation](https://img.shields.io/docsrs/mdbook-core)](https://docs.rs/mdbook-core) +[![crates.io](https://img.shields.io/crates/v/mdbook-core.svg)](https://crates.io/crates/mdbook-core) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the base support library for [mdBook](https://rust-lang.github.io/mdBook/). It is intended for internal use only. Other mdBook crates depend on this for any types that are shared across the crates. + +> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning. + +## License + +[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE) diff --git a/crates/mdbook-core/src/book.rs b/crates/mdbook-core/src/book.rs new file mode 100644 index 00000000..0f8048bc --- /dev/null +++ b/crates/mdbook-core/src/book.rs @@ -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, + __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) -> 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(&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>(&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, +{ + 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 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, + /// Nested items. + pub sub_items: Vec, + /// 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, + /// 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, + /// An ordered list of the names of each chapter above this one in the hierarchy. + pub parent_names: Vec, +} + +impl Chapter { + /// Create a new chapter with the provided content. + pub fn new>( + name: &str, + content: String, + p: P, + parent_names: Vec, + ) -> 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) -> 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` with +/// a pretty `Display` impl. +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] +pub struct SectionNumber(pub Vec); + +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; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SectionNumber { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromIterator for SectionNumber { + fn from_iter>(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 { + 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 + } +} diff --git a/crates/mdbook-core/src/book/tests.rs b/crates/mdbook-core/src/book/tests.rs new file mode 100644 index 00000000..b0b33a08 --- /dev/null +++ b/crates/mdbook-core/src/book/tests.rs @@ -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 = 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); + } + diff --git a/src/config.rs b/crates/mdbook-core/src/config.rs similarity index 98% rename from src/config.rs rename to crates/mdbook-core/src/config.rs index 7ef8bcef..9ab564c2 100644 --- a/src/config.rs +++ b/crates/mdbook-core/src/config.rs @@ -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)); diff --git a/crates/mdbook-core/src/lib.rs b/crates/mdbook-core/src/lib.rs new file mode 100644 index 00000000..959ac9b0 --- /dev/null +++ b/crates/mdbook-core/src/lib.rs @@ -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}; +} diff --git a/src/utils/fs.rs b/crates/mdbook-core/src/utils/fs.rs similarity index 99% rename from src/utils/fs.rs rename to crates/mdbook-core/src/utils/fs.rs index 4d07f8fc..c8500a73 100644 --- a/src/utils/fs.rs +++ b/crates/mdbook-core/src/utils/fs.rs @@ -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>(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), "../../"); /// ``` diff --git a/crates/mdbook-core/src/utils/mod.rs b/crates/mdbook-core/src/utils/mod.rs new file mode 100644 index 00000000..caca5dcd --- /dev/null +++ b/crates/mdbook-core/src/utils/mod.rs @@ -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 = 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::() +} + +/// 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 = 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 { + 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 foo"), + "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>"); + assert_eq!(bracket_escape("ab"), "a<test>b"); + assert_eq!(bracket_escape("'"), "'"); + assert_eq!(bracket_escape("\\"), "\\"); + } +} diff --git a/src/utils/string.rs b/crates/mdbook-core/src/utils/string.rs similarity index 100% rename from src/utils/string.rs rename to crates/mdbook-core/src/utils/string.rs diff --git a/src/utils/toml_ext.rs b/crates/mdbook-core/src/utils/toml_ext.rs similarity index 93% rename from src/utils/toml_ext.rs rename to crates/mdbook-core/src/utils/toml_ext.rs index bf25ad11..7975ed57 100644 --- a/src/utils/toml_ext.rs +++ b/crates/mdbook-core/src/utils/toml_ext.rs @@ -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; } diff --git a/crates/mdbook-driver/Cargo.toml b/crates/mdbook-driver/Cargo.toml new file mode 100644 index 00000000..5e9b348e --- /dev/null +++ b/crates/mdbook-driver/Cargo.toml @@ -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 diff --git a/crates/mdbook-driver/README.md b/crates/mdbook-driver/README.md new file mode 100644 index 00000000..21cf8d63 --- /dev/null +++ b/crates/mdbook-driver/README.md @@ -0,0 +1,13 @@ +# mdbook-driver + +[![Documentation](https://img.shields.io/docsrs/mdbook-driver)](https://docs.rs/mdbook-driver) +[![crates.io](https://img.shields.io/crates/v/mdbook-driver.svg)](https://crates.io/crates/mdbook-driver) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the high-level Rust library for running [mdBook](https://rust-lang.github.io/mdBook/). New books can be created using [`BookBuilder`](https://docs.rs/mdbook-driver/latest/mdbook_driver/init/struct.BookBuilder.html). The primary type [`MDBook`](https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html) can be used to manage and render books. + +> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs. + +## License + +[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE) diff --git a/src/preprocess/cmd.rs b/crates/mdbook-driver/src/builtin_preprocessors/cmd.rs similarity index 91% rename from src/preprocess/cmd.rs rename to crates/mdbook-driver/src/builtin_preprocessors/cmd.rs index 149dabda..e97c19e2 100644 --- a/src/preprocess/cmd.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/cmd.rs @@ -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(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); diff --git a/src/preprocess/index.rs b/crates/mdbook-driver/src/builtin_preprocessors/index.rs similarity index 93% rename from src/preprocess/index.rs rename to crates/mdbook-driver/src/builtin_preprocessors/index.rs index 1e58e294..18ac4407 100644 --- a/src/preprocess/index.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/index.rs @@ -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 { diff --git a/src/preprocess/links.rs b/crates/mdbook-driver/src/builtin_preprocessors/links.rs similarity index 98% rename from src/preprocess/links.rs rename to crates/mdbook-driver/src/builtin_preprocessors/links.rs index 951a3436..127ac3fd 100644 --- a/src/preprocess/links.rs +++ b/crates/mdbook-driver/src/builtin_preprocessors/links.rs @@ -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::>(); @@ -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}} ..."; diff --git a/crates/mdbook-driver/src/builtin_preprocessors/mod.rs b/crates/mdbook-driver/src/builtin_preprocessors/mod.rs new file mode 100644 index 00000000..77b48126 --- /dev/null +++ b/crates/mdbook-driver/src/builtin_preprocessors/mod.rs @@ -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; diff --git a/src/renderer/markdown_renderer.rs b/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs similarity index 90% rename from src/renderer/markdown_renderer.rs rename to crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs index 4a5a5c2a..5255bc80 100644 --- a/src/renderer/markdown_renderer.rs +++ b/crates/mdbook-driver/src/builtin_renderers/markdown_renderer.rs @@ -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)] diff --git a/src/renderer/mod.rs b/crates/mdbook-driver/src/builtin_renderers/mod.rs similarity index 66% rename from src/renderer/mod.rs rename to crates/mdbook-driver/src/builtin_renderers/mod.rs index 1c97f8f2..0218a62d 100644 --- a/src/renderer/mod.rs +++ b/crates/mdbook-driver/src/builtin_renderers/mod.rs @@ -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, - #[serde(skip)] - __non_exhaustive: (), -} - -impl RenderContext { - /// Create a new `RenderContext`. - pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext - where - P: Into, - Q: Into, - { - 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(reader: R) -> Result { - 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. /// diff --git a/src/book/init.rs b/crates/mdbook-driver/src/init.rs similarity index 97% rename from src/book/init.rs rename to crates/mdbook-driver/src/init.rs index faca1d09..ec62ce27 100644 --- a/src/book/init.rs +++ b/crates/mdbook-driver/src/init.rs @@ -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)] diff --git a/crates/mdbook-driver/src/lib.rs b/crates/mdbook-driver/src/lib.rs new file mode 100644 index 00000000..470fa039 --- /dev/null +++ b/crates/mdbook-driver/src/lib.rs @@ -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}; diff --git a/crates/mdbook-driver/src/load.rs b/crates/mdbook-driver/src/load.rs new file mode 100644 index 00000000..d2866c47 --- /dev/null +++ b/crates/mdbook-driver/src/load.rs @@ -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>(src_dir: P, cfg: &BuildConfig) -> Result { + 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>(summary: &Summary, src_dir: P) -> Result { + debug!("Loading the book from disk"); + let src_dir = src_dir.as_ref(); + + let prefix = summary.prefix_chapters.iter(); + let numbered = summary.numbered_chapters.iter(); + 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 + Clone>( + item: &SummaryItem, + src_dir: P, + parent_names: Vec, +) -> Result { + 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>( + link: &Link, + src_dir: P, + parent_names: Vec, +) -> Result { + 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::>>()?; + + 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); + } +} diff --git a/src/book/mod.rs b/crates/mdbook-driver/src/mdbook.rs similarity index 71% rename from src/book/mod.rs rename to crates/mdbook-driver/src/mdbook.rs index da88767a..701a0161 100644 --- a/src/book/mod.rs +++ b/crates/mdbook-driver/src/mdbook.rs @@ -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 { - 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); - } -} diff --git a/crates/mdbook-driver/src/mdbook/tests.rs b/crates/mdbook-driver/src/mdbook/tests.rs new file mode 100644 index 00000000..8040217d --- /dev/null +++ b/crates/mdbook-driver/src/mdbook/tests.rs @@ -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 { + 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); +} diff --git a/crates/mdbook-html/Cargo.toml b/crates/mdbook-html/Cargo.toml new file mode 100644 index 00000000..fafdd2e6 --- /dev/null +++ b/crates/mdbook-html/Cargo.toml @@ -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"] diff --git a/crates/mdbook-html/README.md b/crates/mdbook-html/README.md new file mode 100644 index 00000000..ad11a4e4 --- /dev/null +++ b/crates/mdbook-html/README.md @@ -0,0 +1,13 @@ +# mdbook-html + +[![Documentation](https://img.shields.io/docsrs/mdbook-html)](https://docs.rs/mdbook-html) +[![crates.io](https://img.shields.io/crates/v/mdbook-html.svg)](https://crates.io/crates/mdbook-html) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the HTML renderer for [mdBook](https://rust-lang.github.io/mdBook/). This is intended for internal use only. It is automatically included by [`mdbook-driver`](https://crates.io/crates/mdbook-driver) to render books to HTML. + +> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning. + +## License + +[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE) diff --git a/src/front-end/css/ayu-highlight.css b/crates/mdbook-html/front-end/css/ayu-highlight.css similarity index 100% rename from src/front-end/css/ayu-highlight.css rename to crates/mdbook-html/front-end/css/ayu-highlight.css diff --git a/src/front-end/css/chrome.css b/crates/mdbook-html/front-end/css/chrome.css similarity index 100% rename from src/front-end/css/chrome.css rename to crates/mdbook-html/front-end/css/chrome.css diff --git a/src/front-end/css/font-awesome.min.css b/crates/mdbook-html/front-end/css/font-awesome.min.css similarity index 100% rename from src/front-end/css/font-awesome.min.css rename to crates/mdbook-html/front-end/css/font-awesome.min.css diff --git a/src/front-end/css/general.css b/crates/mdbook-html/front-end/css/general.css similarity index 100% rename from src/front-end/css/general.css rename to crates/mdbook-html/front-end/css/general.css diff --git a/src/front-end/css/highlight.css b/crates/mdbook-html/front-end/css/highlight.css similarity index 100% rename from src/front-end/css/highlight.css rename to crates/mdbook-html/front-end/css/highlight.css diff --git a/src/front-end/css/print.css b/crates/mdbook-html/front-end/css/print.css similarity index 100% rename from src/front-end/css/print.css rename to crates/mdbook-html/front-end/css/print.css diff --git a/src/front-end/css/tomorrow-night.css b/crates/mdbook-html/front-end/css/tomorrow-night.css similarity index 100% rename from src/front-end/css/tomorrow-night.css rename to crates/mdbook-html/front-end/css/tomorrow-night.css diff --git a/src/front-end/css/variables.css b/crates/mdbook-html/front-end/css/variables.css similarity index 100% rename from src/front-end/css/variables.css rename to crates/mdbook-html/front-end/css/variables.css diff --git a/src/front-end/fonts/FontAwesome.otf b/crates/mdbook-html/front-end/fonts/FontAwesome.otf similarity index 100% rename from src/front-end/fonts/FontAwesome.otf rename to crates/mdbook-html/front-end/fonts/FontAwesome.otf diff --git a/src/front-end/fonts/OPEN-SANS-LICENSE.txt b/crates/mdbook-html/front-end/fonts/OPEN-SANS-LICENSE.txt similarity index 100% rename from src/front-end/fonts/OPEN-SANS-LICENSE.txt rename to crates/mdbook-html/front-end/fonts/OPEN-SANS-LICENSE.txt diff --git a/src/front-end/fonts/SOURCE-CODE-PRO-LICENSE.txt b/crates/mdbook-html/front-end/fonts/SOURCE-CODE-PRO-LICENSE.txt similarity index 100% rename from src/front-end/fonts/SOURCE-CODE-PRO-LICENSE.txt rename to crates/mdbook-html/front-end/fonts/SOURCE-CODE-PRO-LICENSE.txt diff --git a/src/front-end/fonts/fontawesome-webfont.eot b/crates/mdbook-html/front-end/fonts/fontawesome-webfont.eot similarity index 100% rename from src/front-end/fonts/fontawesome-webfont.eot rename to crates/mdbook-html/front-end/fonts/fontawesome-webfont.eot diff --git a/src/front-end/fonts/fontawesome-webfont.svg b/crates/mdbook-html/front-end/fonts/fontawesome-webfont.svg similarity index 100% rename from src/front-end/fonts/fontawesome-webfont.svg rename to crates/mdbook-html/front-end/fonts/fontawesome-webfont.svg diff --git a/src/front-end/fonts/fontawesome-webfont.ttf b/crates/mdbook-html/front-end/fonts/fontawesome-webfont.ttf similarity index 100% rename from src/front-end/fonts/fontawesome-webfont.ttf rename to crates/mdbook-html/front-end/fonts/fontawesome-webfont.ttf diff --git a/src/front-end/fonts/fontawesome-webfont.woff b/crates/mdbook-html/front-end/fonts/fontawesome-webfont.woff similarity index 100% rename from src/front-end/fonts/fontawesome-webfont.woff rename to crates/mdbook-html/front-end/fonts/fontawesome-webfont.woff diff --git a/src/front-end/fonts/fontawesome-webfont.woff2 b/crates/mdbook-html/front-end/fonts/fontawesome-webfont.woff2 similarity index 100% rename from src/front-end/fonts/fontawesome-webfont.woff2 rename to crates/mdbook-html/front-end/fonts/fontawesome-webfont.woff2 diff --git a/src/front-end/fonts/fonts.css b/crates/mdbook-html/front-end/fonts/fonts.css similarity index 100% rename from src/front-end/fonts/fonts.css rename to crates/mdbook-html/front-end/fonts/fonts.css diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-300.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-300.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-300.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-300.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-300italic.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-300italic.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-300italic.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-300italic.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-600.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-600.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-600.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-600.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-600italic.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-600italic.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-600italic.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-600italic.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-700.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-700.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-700.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-700.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-700italic.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-700italic.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-700italic.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-700italic.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-800.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-800.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-800.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-800.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-800italic.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-800italic.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-800italic.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-800italic.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-italic.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-italic.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-italic.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-italic.woff2 diff --git a/src/front-end/fonts/open-sans-v17-all-charsets-regular.woff2 b/crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-regular.woff2 similarity index 100% rename from src/front-end/fonts/open-sans-v17-all-charsets-regular.woff2 rename to crates/mdbook-html/front-end/fonts/open-sans-v17-all-charsets-regular.woff2 diff --git a/src/front-end/fonts/source-code-pro-v11-all-charsets-500.woff2 b/crates/mdbook-html/front-end/fonts/source-code-pro-v11-all-charsets-500.woff2 similarity index 100% rename from src/front-end/fonts/source-code-pro-v11-all-charsets-500.woff2 rename to crates/mdbook-html/front-end/fonts/source-code-pro-v11-all-charsets-500.woff2 diff --git a/src/front-end/images/favicon.png b/crates/mdbook-html/front-end/images/favicon.png similarity index 100% rename from src/front-end/images/favicon.png rename to crates/mdbook-html/front-end/images/favicon.png diff --git a/src/front-end/images/favicon.svg b/crates/mdbook-html/front-end/images/favicon.svg similarity index 100% rename from src/front-end/images/favicon.svg rename to crates/mdbook-html/front-end/images/favicon.svg diff --git a/src/front-end/js/book.js b/crates/mdbook-html/front-end/js/book.js similarity index 100% rename from src/front-end/js/book.js rename to crates/mdbook-html/front-end/js/book.js diff --git a/src/front-end/js/clipboard.min.js b/crates/mdbook-html/front-end/js/clipboard.min.js similarity index 100% rename from src/front-end/js/clipboard.min.js rename to crates/mdbook-html/front-end/js/clipboard.min.js diff --git a/src/front-end/js/highlight.js b/crates/mdbook-html/front-end/js/highlight.js similarity index 100% rename from src/front-end/js/highlight.js rename to crates/mdbook-html/front-end/js/highlight.js diff --git a/src/front-end/playground_editor/ace.js b/crates/mdbook-html/front-end/playground_editor/ace.js similarity index 100% rename from src/front-end/playground_editor/ace.js rename to crates/mdbook-html/front-end/playground_editor/ace.js diff --git a/src/front-end/playground_editor/editor.js b/crates/mdbook-html/front-end/playground_editor/editor.js similarity index 100% rename from src/front-end/playground_editor/editor.js rename to crates/mdbook-html/front-end/playground_editor/editor.js diff --git a/src/front-end/playground_editor/mode-rust.js b/crates/mdbook-html/front-end/playground_editor/mode-rust.js similarity index 100% rename from src/front-end/playground_editor/mode-rust.js rename to crates/mdbook-html/front-end/playground_editor/mode-rust.js diff --git a/src/front-end/playground_editor/theme-dawn.js b/crates/mdbook-html/front-end/playground_editor/theme-dawn.js similarity index 100% rename from src/front-end/playground_editor/theme-dawn.js rename to crates/mdbook-html/front-end/playground_editor/theme-dawn.js diff --git a/src/front-end/playground_editor/theme-tomorrow_night.js b/crates/mdbook-html/front-end/playground_editor/theme-tomorrow_night.js similarity index 100% rename from src/front-end/playground_editor/theme-tomorrow_night.js rename to crates/mdbook-html/front-end/playground_editor/theme-tomorrow_night.js diff --git a/src/front-end/searcher/elasticlunr.min.js b/crates/mdbook-html/front-end/searcher/elasticlunr.min.js similarity index 100% rename from src/front-end/searcher/elasticlunr.min.js rename to crates/mdbook-html/front-end/searcher/elasticlunr.min.js diff --git a/src/front-end/searcher/mark.min.js b/crates/mdbook-html/front-end/searcher/mark.min.js similarity index 100% rename from src/front-end/searcher/mark.min.js rename to crates/mdbook-html/front-end/searcher/mark.min.js diff --git a/src/front-end/searcher/searcher.js b/crates/mdbook-html/front-end/searcher/searcher.js similarity index 100% rename from src/front-end/searcher/searcher.js rename to crates/mdbook-html/front-end/searcher/searcher.js diff --git a/src/front-end/templates/head.hbs b/crates/mdbook-html/front-end/templates/head.hbs similarity index 100% rename from src/front-end/templates/head.hbs rename to crates/mdbook-html/front-end/templates/head.hbs diff --git a/src/front-end/templates/header.hbs b/crates/mdbook-html/front-end/templates/header.hbs similarity index 100% rename from src/front-end/templates/header.hbs rename to crates/mdbook-html/front-end/templates/header.hbs diff --git a/src/front-end/templates/index.hbs b/crates/mdbook-html/front-end/templates/index.hbs similarity index 100% rename from src/front-end/templates/index.hbs rename to crates/mdbook-html/front-end/templates/index.hbs diff --git a/src/front-end/templates/redirect.hbs b/crates/mdbook-html/front-end/templates/redirect.hbs similarity index 100% rename from src/front-end/templates/redirect.hbs rename to crates/mdbook-html/front-end/templates/redirect.hbs diff --git a/src/front-end/templates/toc.html.hbs b/crates/mdbook-html/front-end/templates/toc.html.hbs similarity index 100% rename from src/front-end/templates/toc.html.hbs rename to crates/mdbook-html/front-end/templates/toc.html.hbs diff --git a/src/front-end/templates/toc.js.hbs b/crates/mdbook-html/front-end/templates/toc.js.hbs similarity index 100% rename from src/front-end/templates/toc.js.hbs rename to crates/mdbook-html/front-end/templates/toc.js.hbs diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs similarity index 82% rename from src/renderer/html_handlebars/hbs_renderer.rs rename to crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index b1ea7520..e3fc55e8 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -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 = [ - ("x()", - "
# #![allow(unused)]\n# fn main() {\nx()\n# }
"), - ("fn main() {}", - "
fn main() {}
"), - ("let s = \"foo\n # bar\n\";", - "
let s = \"foo\n # bar\n\";
"), - ("let s = \"foo\n ## bar\n\";", - "
let s = \"foo\n ## bar\n\";
"), - ("let s = \"foo\n # bar\n#\n\";", - "
let s = \"foo\n # bar\n#\n\";
"), - ("let s = \"foo\n # bar\n\";", - "let s = \"foo\n # bar\n\";"), - ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", - "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
"), + ( + "x()", + "
# #![allow(unused)]\n# fn main() {\nx()\n# }
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "let s = \"foo\n # bar\n\";", + "
let s = \"foo\n # bar\n\";
", + ), + ( + "let s = \"foo\n ## bar\n\";", + "
let s = \"foo\n ## bar\n\";
", + ), + ( + "let s = \"foo\n # bar\n#\n\";", + "
let s = \"foo\n # bar\n#\n\";
", + ), + ( + "let s = \"foo\n # bar\n\";", + "let s = \"foo\n # bar\n\";", + ), + ( + "#![no_std]\nlet s = \"foo\";\n #[some_attr]", + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
", + ), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1096,14 +1104,22 @@ mod tests { #[test] fn add_playground_edition2015() { let inputs = [ - ("x()", - "
# #![allow(unused)]\n# fn main() {\nx()\n# }
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), + ( + "x()", + "
# #![allow(unused)]\n# fn main() {\nx()\n# }
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1120,14 +1136,22 @@ mod tests { #[test] fn add_playground_edition2018() { let inputs = [ - ("x()", - "
# #![allow(unused)]\n# fn main() {\nx()\n# }
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), + ( + "x()", + "
# #![allow(unused)]\n# fn main() {\nx()\n# }
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), ]; for (src, should_be) in &inputs { let got = add_playground_pre( @@ -1144,14 +1168,22 @@ mod tests { #[test] fn add_playground_edition2021() { let inputs = [ - ("x()", - "
# #![allow(unused)]\n# fn main() {\nx()\n# }
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), + ( + "x()", + "
# #![allow(unused)]\n# fn main() {\nx()\n# }
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), + ( + "fn main() {}", + "
fn main() {}
", + ), ]; 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 = [ - ( - "
\n# #![allow(unused)]\n# fn main() {\nx()\n# }
", - "
\n#![allow(unused)]\nfn main() {\nx()\n}
",), - // # must be followed by a space for a line to be hidden - ( - "
\n#fn main() {\nx()\n#}
", - "
\n#fn main() {\nx()\n#}
",), - ( - "
fn main() {}
", - "
fn main() {}
",), - ( - "
let s = \"foo\n # bar\n\";
", - "
let s = \"foo\n bar\n\";
",), - ( - "
let s = \"foo\n ## bar\n\";
", - "
let s = \"foo\n # bar\n\";
",), - ( - "
let s = \"foo\n # bar\n#\n\";
", - "
let s = \"foo\n bar\n\n\";
",), - ( - "let s = \"foo\n # bar\n\";", - "let s = \"foo\n bar\n\";",), - ( - "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
", - "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
",), + ( + "
\n# #![allow(unused)]\n# fn main() {\nx()\n# }
", + "
\n#![allow(unused)]\nfn main() {\nx()\n}
", + ), + // # must be followed by a space for a line to be hidden + ( + "
\n#fn main() {\nx()\n#}
", + "
\n#fn main() {\nx()\n#}
", + ), + ( + "
fn main() {}
", + "
fn main() {}
", + ), + ( + "
let s = \"foo\n # bar\n\";
", + "
let s = \"foo\n bar\n\";
", + ), + ( + "
let s = \"foo\n ## bar\n\";
", + "
let s = \"foo\n # bar\n\";
", + ), + ( + "
let s = \"foo\n # bar\n#\n\";
", + "
let s = \"foo\n bar\n\n\";
", + ), + ( + "let s = \"foo\n # bar\n\";", + "let s = \"foo\n bar\n\";", + ), + ( + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
", + "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
", + ), ]; 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 = [ - ( - "~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()", - "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n",), - ( - "!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()", - "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n",), + ( + "~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()", + "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n", + ), + ( + "!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()", + "hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n", + ), ]; for (src, should_be) in &inputs { let got = hide_lines( diff --git a/src/renderer/html_handlebars/helpers/mod.rs b/crates/mdbook-html/src/html_handlebars/helpers/mod.rs similarity index 100% rename from src/renderer/html_handlebars/helpers/mod.rs rename to crates/mdbook-html/src/html_handlebars/helpers/mod.rs diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/crates/mdbook-html/src/html_handlebars/helpers/navigation.rs similarity index 99% rename from src/renderer/html_handlebars/helpers/navigation.rs rename to crates/mdbook-html/src/html_handlebars/helpers/navigation.rs index 12c69027..1fd43dcf 100644 --- a/src/renderer/html_handlebars/helpers/navigation.rs +++ b/crates/mdbook-html/src/html_handlebars/helpers/navigation.rs @@ -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; diff --git a/src/renderer/html_handlebars/helpers/resources.rs b/crates/mdbook-html/src/html_handlebars/helpers/resources.rs similarity index 98% rename from src/renderer/html_handlebars/helpers/resources.rs rename to crates/mdbook-html/src/html_handlebars/helpers/resources.rs index e8818f05..88adafed 100644 --- a/src/renderer/html_handlebars/helpers/resources.rs +++ b/crates/mdbook-html/src/html_handlebars/helpers/resources.rs @@ -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, diff --git a/src/renderer/html_handlebars/helpers/theme.rs b/crates/mdbook-html/src/html_handlebars/helpers/theme.rs similarity index 100% rename from src/renderer/html_handlebars/helpers/theme.rs rename to crates/mdbook-html/src/html_handlebars/helpers/theme.rs diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/crates/mdbook-html/src/html_handlebars/helpers/toc.rs similarity index 99% rename from src/renderer/html_handlebars/helpers/toc.rs rename to crates/mdbook-html/src/html_handlebars/helpers/toc.rs index a3419ce8..daf2dc39 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/crates/mdbook-html/src/html_handlebars/helpers/toc.rs @@ -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)] diff --git a/src/renderer/html_handlebars/mod.rs b/crates/mdbook-html/src/html_handlebars/mod.rs similarity index 76% rename from src/renderer/html_handlebars/mod.rs rename to crates/mdbook-html/src/html_handlebars/mod.rs index 6cbdf588..6c3ef04f 100644 --- a/src/renderer/html_handlebars/mod.rs +++ b/crates/mdbook-html/src/html_handlebars/mod.rs @@ -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; diff --git a/src/renderer/html_handlebars/search.rs b/crates/mdbook-html/src/html_handlebars/search.rs similarity index 97% rename from src/renderer/html_handlebars/search.rs rename to crates/mdbook-html/src/html_handlebars/search.rs index ea8ae422..849af3d1 100644 --- a/src/renderer/html_handlebars/search.rs +++ b/crates/mdbook-html/src/html_handlebars/search.rs @@ -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 [ diff --git a/src/renderer/html_handlebars/static_files.rs b/crates/mdbook-html/src/html_handlebars/static_files.rs similarity index 96% rename from src/renderer/html_handlebars/static_files.rs rename to crates/mdbook-html/src/html_handlebars/static_files.rs index e1531f42..a9abc923 100644 --- a/src/renderer/html_handlebars/static_files.rs +++ b/crates/mdbook-html/src/html_handlebars/static_files.rs @@ -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 { - 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] diff --git a/crates/mdbook-html/src/lib.rs b/crates/mdbook-html/src/lib.rs new file mode 100644 index 00000000..62b392a5 --- /dev/null +++ b/crates/mdbook-html/src/lib.rs @@ -0,0 +1,6 @@ +//! mdBook HTML renderer. + +mod html_handlebars; +pub mod theme; + +pub use html_handlebars::HtmlHandlebars; diff --git a/crates/mdbook-html/src/theme/fonts.rs b/crates/mdbook-html/src/theme/fonts.rs new file mode 100644 index 00000000..dc8c8c08 --- /dev/null +++ b/crates/mdbook-html/src/theme/fonts.rs @@ -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"), +); diff --git a/src/front-end/mod.rs b/crates/mdbook-html/src/theme/mod.rs similarity index 81% rename from src/front-end/mod.rs rename to crates/mdbook-html/src/theme/mod.rs index 8fd09fc2..2547d3f9 100644 --- a/src/front-end/mod.rs +++ b/crates/mdbook-html/src/theme/mod.rs @@ -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 diff --git a/crates/mdbook-html/src/theme/playground_editor.rs b/crates/mdbook-html/src/theme/playground_editor.rs new file mode 100644 index 00000000..c2462e2f --- /dev/null +++ b/crates/mdbook-html/src/theme/playground_editor.rs @@ -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"); diff --git a/crates/mdbook-html/src/theme/searcher.rs b/crates/mdbook-html/src/theme/searcher.rs new file mode 100644 index 00000000..f1a839b2 --- /dev/null +++ b/crates/mdbook-html/src/theme/searcher.rs @@ -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"); diff --git a/crates/mdbook-markdown/Cargo.toml b/crates/mdbook-markdown/Cargo.toml new file mode 100644 index 00000000..0dc3e723 --- /dev/null +++ b/crates/mdbook-markdown/Cargo.toml @@ -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 diff --git a/crates/mdbook-markdown/README.md b/crates/mdbook-markdown/README.md new file mode 100644 index 00000000..53544f23 --- /dev/null +++ b/crates/mdbook-markdown/README.md @@ -0,0 +1,13 @@ +# mdbook-markdown + +[![Documentation](https://img.shields.io/docsrs/mdbook-markdown)](https://docs.rs/mdbook-markdown) +[![crates.io](https://img.shields.io/crates/v/mdbook-markdown.svg)](https://crates.io/crates/mdbook-markdown) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the Markdown support library for [mdBook](https://rust-lang.github.io/mdBook/). Rust crates (such as preprocessors) can use this library to process Markdown in the same way as mdBook. + +> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs. + +## License + +[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE) diff --git a/src/utils/mod.rs b/crates/mdbook-markdown/src/lib.rs similarity index 53% rename from src/utils/mod.rs rename to crates/mdbook-markdown/src/lib.rs index 597f0ea4..a014ca6b 100644 --- a/src/utils/mod.rs +++ b/crates/mdbook-markdown/src/lib.rs @@ -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 = 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::() -} - -/// 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 = 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 { - 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 = - LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap()); - static MD_LINK: LazyLock = - LazyLock::new(|| Regex::new(r"(?P.*)\.md(?P#.*)?").unwrap()); - - fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> { - if dest.starts_with('#') { - // Fragment-only link. - if let Some(path) = path { - let mut base = path.display().to_string(); - 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 = - 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 = + LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap()); + static MD_LINK: LazyLock = + LazyLock::new(|| Regex::new(r"(?P.*)\.md(?P#.*)?").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 = + 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), - "

example

\n" - ); - } - - #[test] - fn it_can_adjust_markdown_links() { - assert_eq!( - render_markdown("[example](example.md)", false), - "

example

\n" - ); - assert_eq!( - render_markdown("[example_anchor](example.md#anchor)", false), - "

example_anchor

\n" - ); - - // this anchor contains 'md' inside of it - assert_eq!( - render_markdown("[phantom data](foo.html#phantomdata)", false), - "

phantom data

\n" - ); - } - - #[test] - fn it_can_wrap_tables() { - let src = r#" -| Original | Punycode | Punycode + Encoding | -|-----------------|-----------------|---------------------| -| føø | f-5gaa | f_5gaa | -"#; - let out = r#" -
- -
OriginalPunycodePunycode + Encoding
føøf-5gaaf_5gaa
-
-"#.trim(); - assert_eq!(render_markdown(src, false), out); - } - - #[test] - fn it_can_keep_quotes_straight() { - assert_eq!(render_markdown("'one'", false), "

'one'

\n"); - } - - #[test] - fn it_can_make_quotes_curly_except_when_they_are_in_code() { - let input = r#" -'one' -``` -'two' -``` -`'three'` 'four'"#; - let expected = r#"

‘one’

-
'two'
-
-

'three' ‘four’

-"#; - 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#"

some text with spaces

-
fn main() {
-// code inside is unchanged
-}
-
-

more text with spaces

-"#; - 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#"
-"#; - 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#"
-"#; - 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#"
-"#; - 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 foo"), - "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>"); - assert_eq!(bracket_escape("ab"), "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>"); - assert_eq!(special_escape("ab"), "a<test>b"); - assert_eq!(special_escape("'"), "'"); - assert_eq!(special_escape("\\"), "\"); - assert_eq!(special_escape("&"), "&"); - } -} diff --git a/crates/mdbook-markdown/src/tests.rs b/crates/mdbook-markdown/src/tests.rs new file mode 100644 index 00000000..7f2aea27 --- /dev/null +++ b/crates/mdbook-markdown/src/tests.rs @@ -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>"); + assert_eq!(special_escape("ab"), "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), + "

example

\n" + ); +} + +#[test] +fn it_can_adjust_markdown_links() { + assert_eq!( + render_markdown("[example](example.md)", false), + "

example

\n" + ); + assert_eq!( + render_markdown("[example_anchor](example.md#anchor)", false), + "

example_anchor

\n" + ); + + // this anchor contains 'md' inside of it + assert_eq!( + render_markdown("[phantom data](foo.html#phantomdata)", false), + "

phantom data

\n" + ); +} + +#[test] +fn it_can_wrap_tables() { + let src = r#" +| Original | Punycode | Punycode + Encoding | +|-----------------|-----------------|---------------------| +| føø | f-5gaa | f_5gaa | +"#; + let out = r#" +
+ +
OriginalPunycodePunycode + Encoding
føøf-5gaaf_5gaa
+
+"#.trim(); + assert_eq!(render_markdown(src, false), out); +} + +#[test] +fn it_can_keep_quotes_straight() { + assert_eq!(render_markdown("'one'", false), "

'one'

\n"); +} + +#[test] +fn it_can_make_quotes_curly_except_when_they_are_in_code() { + let input = r#" +'one' +``` +'two' +``` +`'three'` 'four'"#; + let expected = r#"

‘one’

+
'two'
+
+

'three' ‘four’

+"#; + 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#"

some text with spaces

+
fn main() {
+// code inside is unchanged
+}
+
+

more text with spaces

+"#; + 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#"
+"#; + 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#"
+"#; + 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#"
+"#; + 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); +} diff --git a/crates/mdbook-preprocessor/Cargo.toml b/crates/mdbook-preprocessor/Cargo.toml new file mode 100644 index 00000000..c4ad0fc1 --- /dev/null +++ b/crates/mdbook-preprocessor/Cargo.toml @@ -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 diff --git a/crates/mdbook-preprocessor/README.md b/crates/mdbook-preprocessor/README.md new file mode 100644 index 00000000..1063d3fb --- /dev/null +++ b/crates/mdbook-preprocessor/README.md @@ -0,0 +1,13 @@ +# mdbook-preprocessor + +[![Documentation](https://img.shields.io/docsrs/mdbook-preprocessor)](https://docs.rs/mdbook-preprocessor) +[![crates.io](https://img.shields.io/crates/v/mdbook-preprocessor.svg)](https://crates.io/crates/mdbook-preprocessor) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the Rust library to implement a [preprocessor](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html) for [mdBook](https://rust-lang.github.io/mdBook/). + +> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs. + +## License + +[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE) diff --git a/src/preprocess/mod.rs b/crates/mdbook-preprocessor/src/lib.rs similarity index 53% rename from src/preprocess/mod.rs rename to crates/mdbook-preprocessor/src/lib.rs index df01a3db..898d19a3 100644 --- a/src/preprocess/mod.rs +++ b/crates/mdbook-preprocessor/src/lib.rs @@ -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>, - #[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>, + #[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(reader: R) -> Result<(PreprocessorContext, Book)> { + serde_json::from_reader(reader).with_context(|| "Unable to parse the input") +} diff --git a/crates/mdbook-renderer/Cargo.toml b/crates/mdbook-renderer/Cargo.toml new file mode 100644 index 00000000..38869282 --- /dev/null +++ b/crates/mdbook-renderer/Cargo.toml @@ -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 diff --git a/crates/mdbook-renderer/README.md b/crates/mdbook-renderer/README.md new file mode 100644 index 00000000..11e9c821 --- /dev/null +++ b/crates/mdbook-renderer/README.md @@ -0,0 +1,13 @@ +# mdbook-renderer + +[![Documentation](https://img.shields.io/docsrs/mdbook-renderer)](https://docs.rs/mdbook-renderer) +[![crates.io](https://img.shields.io/crates/v/mdbook-renderer.svg)](https://crates.io/crates/mdbook-renderer) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the Rust library to implement a [renderer](https://rust-lang.github.io/mdBook/for_developers/backends.html) for [mdBook](https://rust-lang.github.io/mdBook/). + +> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs. + +## License + +[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE) diff --git a/crates/mdbook-renderer/src/lib.rs b/crates/mdbook-renderer/src/lib.rs new file mode 100644 index 00000000..12b0aab4 --- /dev/null +++ b/crates/mdbook-renderer/src/lib.rs @@ -0,0 +1,91 @@ +//! Library to assist implementing an mdbook renderer. +//! +//! This library is used to implement a +//! [renderer](https://rust-lang.github.io/mdBook/for_developers/backends.html) +//! for [mdBook](https://rust-lang.github.io/mdBook/). See the linked chapter +//! for more information on how to implement a renderer. + +use anyhow::Context; +use mdbook_core::book::Book; +use mdbook_core::config::Config; +use mdbook_core::errors::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::Read; +use std::path::PathBuf; + +pub use mdbook_core::MDBOOK_VERSION; +pub use mdbook_core::book; +pub use mdbook_core::config; +pub use mdbook_core::errors; + +/// An mdbook backend. +/// +/// Types that implement the `Renderer` trait can be used with +/// [`MDBook::with_renderer`] to programmatically add renderers. +/// +/// [`MDBook::with_renderer`]: https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html#method.with_renderer +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, + /// 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: HashMap, + #[serde(skip)] + __non_exhaustive: (), +} + +impl RenderContext { + /// Create a new `RenderContext`. + pub fn new(root: P, book: Book, config: Config, destination: Q) -> RenderContext + where + P: Into, + Q: Into, + { + 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(reader: R) -> Result { + serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`") + } +} diff --git a/crates/mdbook-summary/Cargo.toml b/crates/mdbook-summary/Cargo.toml new file mode 100644 index 00000000..4d52abb2 --- /dev/null +++ b/crates/mdbook-summary/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mdbook-summary" +version = "0.5.0-alpha.1" +description = "Summary parser for 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 +memchr.workspace = true +pulldown-cmark.workspace = true +serde.workspace = true + +[lints] +workspace = true diff --git a/crates/mdbook-summary/README.md b/crates/mdbook-summary/README.md new file mode 100644 index 00000000..543335f3 --- /dev/null +++ b/crates/mdbook-summary/README.md @@ -0,0 +1,13 @@ +# mdbook-summary + +[![Documentation](https://img.shields.io/docsrs/mdbook-summary)](https://docs.rs/mdbook-summary) +[![crates.io](https://img.shields.io/crates/v/mdbook-summary.svg)](https://crates.io/crates/mdbook-summary) +[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md) + +This is the Rust library used to parse the [`SUMMARY.md`](https://rust-lang.github.io/mdBook/format/summary.html) file structure 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) diff --git a/src/book/summary.rs b/crates/mdbook-summary/src/lib.rs similarity index 95% rename from src/book/summary.rs rename to crates/mdbook-summary/src/lib.rs index d25b5667..f0aad211 100644 --- a/src/book/summary.rs +++ b/crates/mdbook-summary/src/lib.rs @@ -1,11 +1,17 @@ -use crate::errors::*; +//! Summary parser for mdBook. +//! +//! This is used to parse the +//! [`SUMMARY.md`](https://rust-lang.github.io/mdBook/format/summary.html) +//! file structure for [mdBook](https://rust-lang.github.io/mdBook/). + +use anyhow::{Context, Error, Result, bail}; use log::{debug, trace, warn}; +pub use mdbook_core::book::SectionNumber; use memchr::Memchr; use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use std::fmt::{self, Display, Formatter}; -use std::ops::{Deref, DerefMut}; +use std::fmt::Display; use std::path::{Path, PathBuf}; /// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be @@ -633,61 +639,10 @@ fn stringify_events(events: Vec>) -> String { .collect() } -/// A section number like "1.2.3", basically just a newtype'd `Vec` with -/// a pretty `Display` impl. -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] -pub struct SectionNumber(pub Vec); - -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; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for SectionNumber { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromIterator for SectionNumber { - fn from_iter>(it: I) -> Self { - SectionNumber(it.into_iter().collect()) - } -} - #[cfg(test)] mod tests { 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 parse_initial_title() { let src = "# Summary"; @@ -776,7 +731,10 @@ mod tests { assert!(got.is_err()); let error_message = got.err().unwrap().to_string(); - assert_eq!(error_message, "failed to parse SUMMARY.md line 2, column 1: Suffix chapters cannot be followed by a list"); + assert_eq!( + error_message, + "failed to parse SUMMARY.md line 2, column 1: Suffix chapters cannot be followed by a list" + ); } #[test] @@ -788,7 +746,10 @@ mod tests { assert!(got.is_err()); let error_message = got.err().unwrap().to_string(); - assert_eq!(error_message, "failed to parse SUMMARY.md line 1, column 0: Suffix chapters cannot be followed by a list"); + assert_eq!( + error_message, + "failed to parse SUMMARY.md line 1, column 0: Suffix chapters cannot be followed by a list" + ); } #[test] diff --git a/examples/nop-preprocessor.rs b/examples/nop-preprocessor.rs index a384dfda..ac30b0f6 100644 --- a/examples/nop-preprocessor.rs +++ b/examples/nop-preprocessor.rs @@ -1,13 +1,15 @@ +//! A basic example of a preprocessor that does nothing. + use crate::nop_lib::Nop; use clap::{Arg, ArgMatches, Command}; -use mdbook::book::Book; -use mdbook::errors::Error; -use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; +use mdbook_preprocessor::book::Book; +use mdbook_preprocessor::errors::Result; +use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use semver::{Version, VersionReq}; use std::io; use std::process; -pub fn make_app() -> Command { +fn make_app() -> Command { Command::new("nop-preprocessor") .about("A mdbook preprocessor which does precisely nothing") .subcommand( @@ -31,18 +33,18 @@ fn main() { } } -fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> { - let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?; +fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<()> { + let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?; let book_version = Version::parse(&ctx.mdbook_version)?; - let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?; + let version_req = VersionReq::parse(mdbook_preprocessor::MDBOOK_VERSION)?; if !version_req.matches(&book_version) { eprintln!( "Warning: The {} plugin was built against version {} of mdbook, \ but we're being called from version {}", pre.name(), - mdbook::MDBOOK_VERSION, + mdbook_preprocessor::MDBOOK_VERSION, ctx.mdbook_version ); } @@ -86,7 +88,7 @@ mod nop_lib { "nop-preprocessor" } - fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { + fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { // In testing we want to tell the preprocessor to blow up by setting a // particular config value if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) { @@ -147,7 +149,7 @@ mod nop_lib { ]"##; let input_json = input_json.as_bytes(); - let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap(); + let (ctx, book) = mdbook_preprocessor::parse_input(input_json).unwrap(); let expected_book = book.clone(); let result = Nop::new().run(&ctx, book); assert!(result.is_ok()); diff --git a/examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml b/examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml index 7571b18d..1b55e291 100644 --- a/examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml +++ b/examples/remove-emphasis/mdbook-remove-emphasis/Cargo.toml @@ -1,10 +1,15 @@ [package] name = "mdbook-remove-emphasis" version = "0.1.0" -edition = "2021" +edition.workspace = true [dependencies] -mdbook = { version = "0.4.40", path = "../../.." } +mdbook-preprocessor.workspace = true pulldown-cmark = { version = "0.12.2", default-features = false } pulldown-cmark-to-cmark = "18.0.0" serde_json = "1.0.132" + +[[bin]] +name = "mdbook-remove-emphasis" +# This is tested through a separate test from the main package. +test = false diff --git a/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs b/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs index e3a5d607..95974d8f 100644 --- a/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs +++ b/examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs @@ -1,10 +1,9 @@ //! This is a demonstration of an mdBook preprocessor which parses markdown //! and removes any instances of emphasis. -use mdbook::book::{Book, Chapter}; -use mdbook::errors::Error; -use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; -use mdbook::BookItem; +use mdbook_preprocessor::book::{Book, BookItem, Chapter}; +use mdbook_preprocessor::errors::Result; +use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use pulldown_cmark::{Event, Parser, Tag, TagEnd}; use std::io; @@ -35,7 +34,7 @@ impl Preprocessor for RemoveEmphasis { "remove-emphasis" } - fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { + fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { let mut total = 0; book.for_each_mut(|item| { let BookItem::Chapter(ch) = item else { @@ -55,7 +54,7 @@ impl Preprocessor for RemoveEmphasis { } // ANCHOR: remove_emphasis -fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result { +fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Result { let mut buf = String::with_capacity(chapter.content.len()); let events = Parser::new(&chapter.content).filter(|e| match e { @@ -71,9 +70,9 @@ fn remove_emphasis(num_removed_items: &mut usize, chapter: &mut Chapter) -> Resu } // ANCHOR_END: remove_emphasis -pub fn handle_preprocessing() -> Result<(), Error> { +pub fn handle_preprocessing() -> Result<()> { let pre = RemoveEmphasis; - let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?; + let (ctx, book) = mdbook_preprocessor::parse_input(io::stdin())?; let processed_book = pre.run(&ctx, book)?; serde_json::to_writer(io::stdout(), &processed_book)?; diff --git a/examples/remove-emphasis/test.rs b/examples/remove-emphasis/test.rs index 1741712b..4349cdb2 100644 --- a/examples/remove-emphasis/test.rs +++ b/examples/remove-emphasis/test.rs @@ -1,10 +1,12 @@ +//! A test to ensure that the remove-emphasis example works. + #[test] fn remove_emphasis_works() { // Tests that the remove-emphasis example works as expected. // Workaround for https://github.com/rust-lang/mdBook/issues/1424 std::env::set_current_dir("examples/remove-emphasis").unwrap(); - let book = mdbook::MDBook::load(".").unwrap(); + let book = mdbook_driver::MDBook::load(".").unwrap(); book.build().unwrap(); let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap(); assert!(ch1.contains("This has light emphasis and bold emphasis.")); diff --git a/guide/src/for_developers/README.md b/guide/src/for_developers/README.md index d8b97709..ea9ed116 100644 --- a/guide/src/for_developers/README.md +++ b/guide/src/for_developers/README.md @@ -1,7 +1,7 @@ # For Developers While `mdbook` is mainly used as a command line tool, you can also import the -underlying library directly and use that to manage a book. It also has a fairly +underlying libraries directly and use those to manage a book. It also has a fairly flexible plugin mechanism, allowing you to create your own custom tooling and consumers (often referred to as *backends*) if you need to do some analysis of the book or render it in a different format. @@ -14,7 +14,6 @@ The two main ways a developer can hook into the book's build process is via, - [Preprocessors](preprocessors.md) - [Alternative Backends](backends.md) - ## The Build Process The process of rendering a book project goes through several steps. @@ -28,20 +27,18 @@ The process of rendering a book project goes through several steps. 1. Run all the preprocessors. 2. Call the backend to render the processed result. - ## Using `mdbook` as a Library -The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its -functionality as a command-line program. As such it is quite easy to create your -own programs which use `mdbook` internally, adding your own functionality (e.g. -a custom preprocessor) or tweaking the build process. +The `mdbook` binary is just a wrapper around the underlying mdBook crates, +exposing their functionality as a command-line program. If you want to +programmatically drive mdBook, you can use the [`mdbook-driver`] crate. +This can be used to add your own functionality or tweak the build process. -The easiest way to find out how to use the `mdbook` crate is by looking at the +The easiest way to find out how to use the `mdbook-driver` crate is by looking at the [API Docs]. The top level documentation explains how one would use the [`MDBook`] type to load and build a book, while the [config] module gives a good explanation on the configuration system. - -[`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html -[API Docs]: https://docs.rs/mdbook/*/mdbook/ -[config]: https://docs.rs/mdbook/*/mdbook/config/index.html +[`MDBook`]: https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html +[API Docs]: https://docs.rs/mdbook-driver/latest/mdbook_driver/ +[config]: https://docs.rs/mdbook-driver/latest/mdbook_driver/config/index.html diff --git a/guide/src/for_developers/backends.md b/guide/src/for_developers/backends.md index 72f8263e..c49381d9 100644 --- a/guide/src/for_developers/backends.md +++ b/guide/src/for_developers/backends.md @@ -16,13 +16,13 @@ This page will step you through creating your own alternative backend in the for of a simple word counting program. Although it will be written in Rust, there's no reason why it couldn't be accomplished using something like Python or Ruby. -First you'll want to create a new binary program and add `mdbook` as a +First you'll want to create a new binary program and add `mdbook-renderer` as a dependency. ```shell $ cargo new --bin mdbook-wordcount $ cd mdbook-wordcount -$ cargo add mdbook +$ cargo add mdbook-renderer ``` When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON @@ -33,10 +33,8 @@ This is all the boilerplate necessary for our backend to load the book. ```rust // src/main.rs -extern crate mdbook; - use std::io; -use mdbook::renderer::RenderContext; +use mdbook_renderer::RenderContext; fn main() { let mut stdin = io::stdin(); @@ -45,13 +43,12 @@ fn main() { ``` > **Note:** The `RenderContext` contains a `version` field. This lets backends - figure out whether they are compatible with the version of `mdbook` it's being - called by. This `version` comes directly from the corresponding field in - `mdbook`'s `Cargo.toml`. - - It is recommended that backends use the [`semver`] crate to inspect this field - and emit a warning if there may be a compatibility issue. - +> figure out whether they are compatible with the version of `mdbook` it's being +> called by. This `version` comes directly from the corresponding field in +> `mdbook`'s `Cargo.toml`. +> +> It is recommended that backends use the [`semver`] crate to inspect this field +> and emit a warning if there may be a compatibility issue. ## Inspecting the Book @@ -183,9 +180,7 @@ $ cargo add serde serde_derive And then you can create the config struct, ```rust -extern crate serde; -#[macro_use] -extern crate serde_derive; +use serde_derive::{Serialize, Deserialize}; ... @@ -337,10 +332,10 @@ the source code or ask questions. [Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins -[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html -[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json +[`RenderContext`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/struct.RenderContext.html +[`RenderContext::from_json()`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/struct.RenderContext.html#method.from_json [`semver`]: https://crates.io/crates/semver -[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html -[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter -[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html +[`Book`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/book/struct.Book.html +[`Book::iter()`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/book/struct.Book.html#method.iter +[`Config`]: https://docs.rs/mdbook-renderer/latest/mdbook_renderer/config/struct.Config.html [issue tracker]: https://github.com/rust-lang/mdBook/issues diff --git a/guide/src/for_developers/preprocessors.md b/guide/src/for_developers/preprocessors.md index 1455aceb..1abd39cc 100644 --- a/guide/src/for_developers/preprocessors.md +++ b/guide/src/for_developers/preprocessors.md @@ -45,7 +45,7 @@ be adapted for other preprocessors. ## Hints For Implementing A Preprocessor -By pulling in `mdbook` as a library, preprocessors can have access to the +By pulling in `mdbook-preprocessor` as a library, preprocessors can have access to the existing infrastructure for dealing with books. For example, a custom preprocessor could use the @@ -60,9 +60,7 @@ chapters) or via the `Book::for_each_mut()` convenience method. The `chapter.content` is just a string which happens to be markdown. While it's entirely possible to use regular expressions or do a manual find & replace, you'll probably want to process the input into something more computer-friendly. -The [`pulldown-cmark`][pc] crate implements a production-quality event-based -Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to -translate events back into markdown text. +The [`mdbook-markdown`] crate exposes the [`pulldown-cmark`][pc] crate used by mdBook to parse Markdown. The [`pulldown-cmark-to-cmark`][pctc] crate can be used to translate events back into markdown text. The following code block shows how to remove all emphasis from markdown, without accidentally breaking the document. @@ -100,11 +98,11 @@ if __name__ == '__main__': [emphasis-example]: https://github.com/rust-lang/mdBook/tree/master/examples/remove-emphasis/ -[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html [pc]: https://crates.io/crates/pulldown-cmark [pctc]: https://crates.io/crates/pulldown-cmark-to-cmark [an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs -[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input -[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut -[`PreprocessorContext`]: https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html -[`Book`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html +[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/trait.Preprocessor.html#method.parse_input +[`Book::for_each_mut()`]: https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/book/struct.Book.html#method.for_each_mut +[`PreprocessorContext`]: https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/struct.PreprocessorContext.html +[`Book`]: https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/book/struct.Book.html +[`mdbook-markdown`]: https://docs.rs/mdbook-markdown/latest/mdbook_markdown/ diff --git a/guide/src/guide/installation.md b/guide/src/guide/installation.md index 61bb2731..72beb50f 100644 --- a/guide/src/guide/installation.md +++ b/guide/src/guide/installation.md @@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`. To build the `mdbook` executable from source, you will first need to install Rust and Cargo. Follow the instructions on the [Rust installation page]. -mdBook currently requires at least Rust version 1.82. +mdBook currently requires at least Rust version 1.85. Once you have installed Rust, the following command can be used to build and install mdBook: diff --git a/package.json b/package.json index dcb25296..48e72718 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "eslint": "^8.57.1" }, "scripts": { - "lint": "eslint src/front-end/*js src/front-end/**/*js", - "lint-fix": "eslint --fix src/front-end/*js src/front-end/**/*js" + "lint": "eslint crates/mdbook-html/front-end/*js crates/mdbook-html/front-end/**/*js", + "lint-fix": "eslint --fix crates/mdbook-html/front-end/*js crates/mdbook-html/front-end/**/*js" } } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..35011368 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +style_edition = "2024" diff --git a/src/book/book.rs b/src/book/book.rs deleted file mode 100644 index fe71fbb2..00000000 --- a/src/book/book.rs +++ /dev/null @@ -1,665 +0,0 @@ -use std::collections::VecDeque; -use std::fmt::{self, Display, Formatter}; -use std::fs::{self, File}; -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; - -use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; -use crate::config::BuildConfig; -use crate::errors::*; -use crate::utils::bracket_escape; -use log::debug; -use serde::{Deserialize, Serialize}; - -/// Load a book into memory from its `src/` directory. -pub fn load_book>(src_dir: P, cfg: &BuildConfig) -> Result { - let src_dir = src_dir.as_ref(); - let summary_md = src_dir.join("SUMMARY.md"); - - 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(()) -} - -/// A dumb 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, - __non_exhaustive: (), -} - -impl Book { - /// Create an empty book. - pub fn new() -> Self { - Default::default() - } - - /// 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(&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>(&mut self, item: I) -> &mut Self { - self.sections.push(item.into()); - self - } -} - -pub fn for_each_mut<'a, F, I>(func: &mut F, items: I) -where - F: FnMut(&mut BookItem), - I: IntoIterator, -{ - 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 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, - /// Nested items. - pub sub_items: Vec, - /// 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, - /// 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, - /// An ordered list of the names of each chapter above this one in the hierarchy. - pub parent_names: Vec, -} - -impl Chapter { - /// Create a new chapter with the provided content. - pub fn new>( - name: &str, - content: String, - p: P, - parent_names: Vec, - ) -> 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) -> 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() - } -} - -/// 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>(summary: &Summary, src_dir: P) -> Result { - debug!("Loading the book from disk"); - let src_dir = src_dir.as_ref(); - - let prefix = summary.prefix_chapters.iter(); - let numbered = summary.numbered_chapters.iter(); - 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 { - sections: chapters, - __non_exhaustive: (), - }) -} - -fn load_summary_item + Clone>( - item: &SummaryItem, - src_dir: P, - parent_names: Vec, -) -> Result { - match item { - SummaryItem::Separator => Ok(BookItem::Separator), - SummaryItem::Link(ref link) => { - load_chapter(link, src_dir, parent_names).map(BookItem::Chapter) - } - SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())), - } -} - -fn load_chapter>( - link: &Link, - src_dir: P, - parent_names: Vec, -) -> Result { - 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::>>()?; - - ch.sub_items = sub_items; - - Ok(ch) -} - -/// 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 { - 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 - } -} - -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) - } -} - -#[cfg(test)] -mod tests { - use super::*; - 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 should_be = Book { - sections: vec![BookItem::Chapter(Chapter { - name: String::from("Chapter 1"), - content: String::from(DUMMY_SRC), - path: Some(PathBuf::from("chapter_1.md")), - source_path: Some(PathBuf::from("chapter_1.md")), - ..Default::default() - })], - ..Default::default() - }; - - let got = load_book_from_disk(&summary, temp.path()).unwrap(); - - assert_eq!(got, should_be); - } - - #[test] - fn book_iter_iterates_over_sequential_items() { - let book = Book { - sections: vec![ - BookItem::Chapter(Chapter { - name: String::from("Chapter 1"), - content: String::from(DUMMY_SRC), - ..Default::default() - }), - BookItem::Separator, - ], - ..Default::default() - }; - - let should_be: Vec<_> = book.sections.iter().collect(); - - let got: Vec<_> = book.iter().collect(); - - assert_eq!(got, should_be); - } - - #[test] - fn iterate_over_nested_book_items() { - let book = Book { - sections: vec![ - BookItem::Chapter(Chapter { - name: String::from("Chapter 1"), - content: String::from(DUMMY_SRC), - 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, - ], - ..Default::default() - }; - - 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 = 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); - } - - #[test] - fn for_each_mut_visits_all_items() { - let mut book = Book { - sections: vec![ - BookItem::Chapter(Chapter { - name: String::from("Chapter 1"), - content: String::from(DUMMY_SRC), - 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, - ], - ..Default::default() - }; - - let num_items = book.iter().count(); - let mut visited = 0; - - book.for_each_mut(|_| visited += 1); - - assert_eq!(visited, num_items); - } - - #[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); - } -} diff --git a/src/cmd/build.rs b/src/cmd/build.rs index e40e5c0c..a04adc5f 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,7 +1,7 @@ use super::command_prelude::*; use crate::{get_book_dir, open}; -use mdbook::errors::Result; -use mdbook::MDBook; +use anyhow::Result; +use mdbook_driver::MDBook; use std::path::PathBuf; // Create clap subcommand arguments diff --git a/src/cmd/clean.rs b/src/cmd/clean.rs index ec77537e..a07bb2ae 100644 --- a/src/cmd/clean.rs +++ b/src/cmd/clean.rs @@ -1,7 +1,8 @@ use super::command_prelude::*; use crate::get_book_dir; use anyhow::Context; -use mdbook::MDBook; +use anyhow::Result; +use mdbook_driver::MDBook; use std::mem::take; use std::path::PathBuf; use std::{fmt, fs}; @@ -15,7 +16,7 @@ pub fn make_subcommand() -> Command { } // Clean command implementation -pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> { +pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let book = MDBook::load(book_dir)?; @@ -47,7 +48,7 @@ pub struct Clean { } impl Clean { - fn new(dir: &PathBuf) -> mdbook::errors::Result { + fn new(dir: &PathBuf) -> Result { let mut files = vec![dir.clone()]; let mut children = Vec::new(); let mut num_files_removed = 0; diff --git a/src/cmd/command_prelude.rs b/src/cmd/command_prelude.rs index 37199425..d5df3af9 100644 --- a/src/cmd/command_prelude.rs +++ b/src/cmd/command_prelude.rs @@ -1,6 +1,6 @@ //! Helpers for building the command-line arguments for commands. -pub use clap::{arg, Arg, ArgMatches, Command}; +pub use clap::{Arg, ArgMatches, Command, arg}; use std::path::PathBuf; pub trait CommandExt: Sized { diff --git a/src/cmd/init.rs b/src/cmd/init.rs index f15fb968..9b0c35bc 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,8 +1,8 @@ use crate::get_book_dir; -use clap::{arg, ArgMatches, Command as ClapCommand}; -use mdbook::config; -use mdbook::errors::Result; -use mdbook::MDBook; +use anyhow::Result; +use clap::{ArgMatches, Command as ClapCommand, arg}; +use mdbook_core::config; +use mdbook_driver::MDBook; use std::io; use std::io::Write; use std::process::Command; diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index beab121f..d1f5d0e4 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -2,15 +2,15 @@ use super::command_prelude::*; #[cfg(feature = "watch")] use super::watch; use crate::{get_book_dir, open}; +use anyhow::Result; +use axum::Router; use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::routing::get; -use axum::Router; use clap::builder::NonEmptyStringValueParser; -use futures_util::sink::SinkExt; use futures_util::StreamExt; -use mdbook::errors::*; -use mdbook::utils::fs::get_404_output_file; -use mdbook::MDBook; +use futures_util::sink::SinkExt; +use mdbook_core::utils::fs::get_404_output_file; +use mdbook_driver::MDBook; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use tokio::sync::broadcast; diff --git a/src/cmd/test.rs b/src/cmd/test.rs index d41e9ef9..36988c47 100644 --- a/src/cmd/test.rs +++ b/src/cmd/test.rs @@ -1,9 +1,9 @@ use super::command_prelude::*; use crate::get_book_dir; -use clap::builder::NonEmptyStringValueParser; +use anyhow::Result; use clap::ArgAction; -use mdbook::errors::Result; -use mdbook::MDBook; +use clap::builder::NonEmptyStringValueParser; +use mdbook_driver::MDBook; use std::path::PathBuf; // Create clap subcommand arguments diff --git a/src/cmd/watch.rs b/src/cmd/watch.rs index 7adb2bbb..4f637505 100644 --- a/src/cmd/watch.rs +++ b/src/cmd/watch.rs @@ -1,7 +1,7 @@ use super::command_prelude::*; use crate::{get_book_dir, open}; -use mdbook::errors::Result; -use mdbook::MDBook; +use anyhow::Result; +use mdbook_driver::MDBook; use std::path::{Path, PathBuf}; mod native; diff --git a/src/cmd/watch/native.rs b/src/cmd/watch/native.rs index fad8d7ce..0eb04e0a 100644 --- a/src/cmd/watch/native.rs +++ b/src/cmd/watch/native.rs @@ -1,7 +1,7 @@ //! A filesystem watcher using native operating system facilities. use ignore::gitignore::Gitignore; -use mdbook::MDBook; +use mdbook_driver::MDBook; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; use std::thread::sleep; diff --git a/src/cmd/watch/poller.rs b/src/cmd/watch/poller.rs index 5e1d1497..65a51188 100644 --- a/src/cmd/watch/poller.rs +++ b/src/cmd/watch/poller.rs @@ -5,7 +5,7 @@ //! had problems correctly reporting changes. use ignore::gitignore::Gitignore; -use mdbook::MDBook; +use mdbook_driver::MDBook; use pathdiff::diff_paths; use std::collections::HashMap; use std::fs::FileType; diff --git a/src/front-end/fonts/mod.rs b/src/front-end/fonts/mod.rs deleted file mode 100644 index 5d2e29cb..00000000 --- a/src/front-end/fonts/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -pub static CSS: &[u8] = include_bytes!("fonts.css"); -// An array of (file_name, file_contents) pairs -pub static LICENSES: [(&str, &[u8]); 2] = [ - ( - "fonts/OPEN-SANS-LICENSE.txt", - include_bytes!("OPEN-SANS-LICENSE.txt"), - ), - ( - "fonts/SOURCE-CODE-PRO-LICENSE.txt", - include_bytes!("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!("open-sans-v17-all-charsets-300.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-300italic.woff2", - include_bytes!("open-sans-v17-all-charsets-300italic.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-regular.woff2", - include_bytes!("open-sans-v17-all-charsets-regular.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-italic.woff2", - include_bytes!("open-sans-v17-all-charsets-italic.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-600.woff2", - include_bytes!("open-sans-v17-all-charsets-600.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-600italic.woff2", - include_bytes!("open-sans-v17-all-charsets-600italic.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-700.woff2", - include_bytes!("open-sans-v17-all-charsets-700.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-700italic.woff2", - include_bytes!("open-sans-v17-all-charsets-700italic.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-800.woff2", - include_bytes!("open-sans-v17-all-charsets-800.woff2"), - ), - ( - "fonts/open-sans-v17-all-charsets-800italic.woff2", - include_bytes!("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!("source-code-pro-v11-all-charsets-500.woff2"), -); diff --git a/src/front-end/playground_editor/mod.rs b/src/front-end/playground_editor/mod.rs deleted file mode 100644 index 19dbf4bc..00000000 --- a/src/front-end/playground_editor/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Theme dependencies for the playground editor. - -pub static JS: &[u8] = include_bytes!("editor.js"); -pub static ACE_JS: &[u8] = include_bytes!("ace.js"); -pub static MODE_RUST_JS: &[u8] = include_bytes!("mode-rust.js"); -pub static THEME_DAWN_JS: &[u8] = include_bytes!("theme-dawn.js"); -pub static THEME_TOMORROW_NIGHT_JS: &[u8] = include_bytes!("theme-tomorrow_night.js"); diff --git a/src/front-end/searcher/mod.rs b/src/front-end/searcher/mod.rs deleted file mode 100644 index d5029db1..00000000 --- a/src/front-end/searcher/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Theme dependencies for in-browser search. Not included in mdbook when -//! the "search" cargo feature is disabled. - -pub static JS: &[u8] = include_bytes!("searcher.js"); -pub static MARK_JS: &[u8] = include_bytes!("mark.min.js"); -pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js"); diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 8a8cb3c9..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! # mdBook -//! -//! **mdBook** is a tool for rendering a collection of markdown documents into -//! a form more suitable for end users like HTML or EPUB. It offers a command -//! line interface, but this crate can be used if more control is required. -//! -//! This is the API doc, the [user guide] is also available if you want -//! information about the command line tool, format, structure etc. It is also -//! rendered with mdBook to showcase the features and default theme. -//! -//! Some reasons why you would want to use the crate (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 -//! - ... -//! -//! > **Note:** While we try to ensure `mdbook`'s command-line interface and -//! > behaviour are backwards compatible, the tool's internals are still -//! > evolving and being iterated on. If you wish to prevent accidental -//! > breakages it is recommended to pin any tools building on top of the -//! > `mdbook` crate to a specific release. -//! -//! # Examples -//! -//! If creating a new book from scratch, you'll want to get a `BookBuilder` via -//! the `MDBook::init()` method. -//! -//! ```rust,no_run -//! use mdbook::MDBook; -//! use mdbook::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::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"); -//! ``` -//! -//! ## Implementing a new Backend -//! -//! `mdbook` has a fairly flexible mechanism for creating additional backends -//! for your book. The general idea is you'll add an extra table in the book's -//! `book.toml` which specifies an executable to be invoked by `mdbook`. This -//! executable will then be called during a build, with an in-memory -//! representation ([`RenderContext`]) of the book being passed to the -//! subprocess via `stdin`. -//! -//! The [`RenderContext`] gives the backend access to the contents of -//! `book.toml` and lets it know which directory all generated artefacts should -//! be placed in. For a much more in-depth explanation, consult the [relevant -//! chapter] in the *For Developers* section of the user guide. -//! -//! To make creating a backend easier, the `mdbook` crate can be imported -//! directly, making deserializing the `RenderContext` easy and giving you -//! access to the various methods for working with the [`Config`]. -//! -//! [user guide]: https://rust-lang.github.io/mdBook/ -//! [`RenderContext`]: renderer::RenderContext -//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html -//! [`Config`]: config::Config - -#![deny(missing_docs)] -#![deny(rust_2018_idioms)] - -pub mod book; -pub mod config; -pub mod preprocess; -pub mod renderer; -#[path = "front-end/mod.rs"] -pub mod theme; -pub mod utils; - -/// 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 use crate::book::BookItem; -pub use crate::book::MDBook; -pub use crate::config::Config; -pub use crate::renderer::Renderer; - -/// The error types used through out this crate. -pub mod errors { - pub(crate) use anyhow::{bail, ensure, Context}; - pub use anyhow::{Error, Result}; -} diff --git a/src/main.rs b/src/main.rs index 3e576c5b..e4e8d55f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +//! The mdbook CLI. + #[macro_use] extern crate clap; #[macro_use] @@ -9,7 +11,7 @@ use clap::{Arg, ArgMatches, Command}; use clap_complete::Shell; use env_logger::Builder; use log::LevelFilter; -use mdbook::utils; +use mdbook_core::utils; use std::env; use std::ffi::OsStr; use std::io::Write; diff --git a/tests/gui/runner.rs b/tests/gui/runner.rs index 3177ff4f..b8d4bacc 100644 --- a/tests/gui/runner.rs +++ b/tests/gui/runner.rs @@ -1,3 +1,9 @@ +//! The GUI test runner. +//! +//! This uses the browser-ui-test npm package to use a headless Chrome to +//! exercise the behavior of rendered books. See `CONTRIBUTING.md` for more +//! information. + use serde_json::Value; use std::collections::HashSet; use std::env::current_dir; diff --git a/tests/testsuite/book_test.rs b/tests/testsuite/book_test.rs index 427c38d1..e40ca63c 100644 --- a/tests/testsuite/book_test.rs +++ b/tests/testsuite/book_test.rs @@ -1,7 +1,7 @@ //! Utility for building and running tests against mdbook. -use mdbook::book::BookBuilder; -use mdbook::MDBook; +use mdbook_driver::MDBook; +use mdbook_driver::init::BookBuilder; use snapbox::IntoData; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -35,7 +35,7 @@ impl BookTest { let dir = Path::new("tests/testsuite").join(dir); assert!(dir.exists(), "{dir:?} should exist"); let tmp = Self::new_tmp(); - mdbook::utils::fs::copy_files_except_ext( + mdbook_core::utils::fs::copy_files_except_ext( &dir, &tmp, true, @@ -424,7 +424,8 @@ fn assert(root: &Path) -> snapbox::Assert { regex!(r"(?m)(?20\d\d-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"), ) .unwrap(); - subs.insert("[VERSION]", mdbook::MDBOOK_VERSION).unwrap(); + subs.insert("[VERSION]", mdbook_core::MDBOOK_VERSION) + .unwrap(); subs.extend(LITERAL_REDACTIONS.into_iter().cloned()) .unwrap(); diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index 1737bdf9..6bbf061d 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -10,9 +10,9 @@ use crate::prelude::*; fn basic_build() { BookTest::from_dir("build/basic_build").run("build", |cmd| { cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }); @@ -24,8 +24,8 @@ fn basic_build() { fn failure_on_missing_file() { BookTest::from_dir("build/missing_file").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Chapter file not found, ./chapter_1.md -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: [NOT_FOUND] +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Chapter file not found, ./chapter_1.md +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [NOT_FOUND] "#]]); }); @@ -46,10 +46,10 @@ fn create_missing() { fn no_reserved_filename() { BookTest::from_dir("build/no_reserved_filename").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: print.md is reserved for internal use +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: print.md is reserved for internal use "#]]); }); diff --git a/tests/testsuite/includes.rs b/tests/testsuite/includes.rs index bc02e513..300df28e 100644 --- a/tests/testsuite/includes.rs +++ b/tests/testsuite/includes.rs @@ -45,10 +45,10 @@ fn recursive_include() { BookTest::from_dir("includes/all_includes") .run("build", |cmd| { cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [ERROR] (mdbook::preprocess::links): Stack depth exceeded in recursive.md. Check for cyclic includes -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [ERROR] (mdbook_driver::builtin_preprocessors::links): Stack depth exceeded in recursive.md. Check for cyclic includes +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }) diff --git a/tests/testsuite/init.rs b/tests/testsuite/init.rs index e5989eb6..946d9f23 100644 --- a/tests/testsuite/init.rs +++ b/tests/testsuite/init.rs @@ -1,7 +1,8 @@ //! Tests for `mdbook init`. use crate::prelude::*; -use mdbook::{Config, MDBook}; +use mdbook_core::config::Config; +use mdbook_driver::MDBook; use std::path::PathBuf; // Tests "init" with no args. @@ -18,7 +19,7 @@ All done, no errors... "#]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book::init): Creating a new book with stub content +[TIMESTAMP] [INFO] (mdbook_driver::init): Creating a new book with stub content "#]]); }) @@ -84,7 +85,7 @@ All done, no errors... "#]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book::init): Creating a new book with stub content +[TIMESTAMP] [INFO] (mdbook_driver::init): Creating a new book with stub content "#]]); }) @@ -116,7 +117,7 @@ All done, no errors... "#]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book::init): Creating a new book with stub content +[TIMESTAMP] [INFO] (mdbook_driver::init): Creating a new book with stub content "#]]) .args(&["--title", "Example title"]); diff --git a/tests/testsuite/markdown.rs b/tests/testsuite/markdown.rs index 72643369..e8366a48 100644 --- a/tests/testsuite/markdown.rs +++ b/tests/testsuite/markdown.rs @@ -20,13 +20,13 @@ fn footnotes() { BookTest::from_dir("markdown/footnotes") .run("build", |cmd| { cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [WARN] (mdbook::utils): footnote `multiple-definitions` in defined multiple times - not updating to new definition -[TIMESTAMP] [WARN] (mdbook::utils): footnote `unused` in `` is defined but not referenced -[TIMESTAMP] [WARN] (mdbook::utils): footnote `multiple-definitions` in footnotes.md defined multiple times - not updating to new definition -[TIMESTAMP] [WARN] (mdbook::utils): footnote `unused` in `footnotes.md` is defined but not referenced -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [WARN] (mdbook_markdown): footnote `multiple-definitions` in defined multiple times - not updating to new definition +[TIMESTAMP] [WARN] (mdbook_markdown): footnote `unused` in `` is defined but not referenced +[TIMESTAMP] [WARN] (mdbook_markdown): footnote `multiple-definitions` in footnotes.md defined multiple times - not updating to new definition +[TIMESTAMP] [WARN] (mdbook_markdown): footnote `unused` in `footnotes.md` is defined but not referenced +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }) diff --git a/tests/testsuite/preprocessor.rs b/tests/testsuite/preprocessor.rs index db8322af..428c9d08 100644 --- a/tests/testsuite/preprocessor.rs +++ b/tests/testsuite/preprocessor.rs @@ -1,9 +1,10 @@ //! Tests for custom preprocessors. use crate::prelude::*; -use mdbook::book::Book; -use mdbook::errors::Result; -use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; +use anyhow::Result; +use mdbook_core::book::Book; +use mdbook_driver::builtin_preprocessors::CmdPreprocessor; +use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use std::sync::{Arc, Mutex}; struct Spy(Arc>); @@ -46,9 +47,9 @@ fn runs_preprocessors() { fn nop_preprocessor() { BookTest::from_dir("preprocessor/nop_preprocessor").run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }); @@ -62,9 +63,9 @@ fn failing_preprocessor() { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started Boom!!1! -[TIMESTAMP] [ERROR] (mdbook::utils): Error: The "nop-preprocessor" preprocessor exited unsuccessfully with [EXIT_STATUS]: 1 status +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: The "nop-preprocessor" preprocessor exited unsuccessfully with [EXIT_STATUS]: 1 status "#]]); }); diff --git a/tests/testsuite/redirects.rs b/tests/testsuite/redirects.rs index d92bf766..c73703ca 100644 --- a/tests/testsuite/redirects.rs +++ b/tests/testsuite/redirects.rs @@ -22,11 +22,11 @@ fn redirects_are_emitted_correctly() { fn redirect_removed_with_fragments_only() { BookTest::from_dir("redirects/redirect_removed_with_fragments_only").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: Unable to emit redirects -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: redirect entry for `old-file.html` only has source paths with `#` fragments +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: Unable to emit redirects +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: redirect entry for `old-file.html` only has source paths with `#` fragments There must be an entry without the `#` fragment to determine the default destination. "#]]); @@ -38,10 +38,10 @@ There must be an entry without the `#` fragment to determine the default destina fn redirect_existing_page() { BookTest::from_dir("redirects/redirect_existing_page").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: redirect found for existing chapter at `/chapter_1.html` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: redirect found for existing chapter at `/chapter_1.html` Either delete the redirect or remove the chapter. "#]]); diff --git a/tests/testsuite/renderer.rs b/tests/testsuite/renderer.rs index 1e162447..9d74f9ba 100644 --- a/tests/testsuite/renderer.rs +++ b/tests/testsuite/renderer.rs @@ -1,8 +1,8 @@ //! Tests for custom renderers. use crate::prelude::*; -use mdbook::errors::Result; -use mdbook::renderer::{RenderContext, Renderer}; +use anyhow::Result; +use mdbook_renderer::{RenderContext, Renderer}; use snapbox::IntoData; use std::fs::File; use std::sync::{Arc, Mutex}; @@ -64,12 +64,12 @@ fn failing_command() { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the failing backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "failing" renderer -[TIMESTAMP] [ERROR] (mdbook::renderer): Renderer exited with non-zero return code. -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: The "failing" renderer failed +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the failing backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "failing" renderer +[TIMESTAMP] [ERROR] (mdbook_driver::builtin_renderers): Renderer exited with non-zero return code. +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: The "failing" renderer failed "#]]); }); @@ -82,13 +82,13 @@ fn missing_renderer() { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the missing backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "missing" renderer -[TIMESTAMP] [ERROR] (mdbook::renderer): The command `trduyvbhijnorgevfuhn` wasn't found, is the "missing" backend installed? If you want to ignore this error when the "missing" backend is not installed, set `optional = true` in the `[output.missing]` section of the book.toml configuration file. -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: Unable to start the backend -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: [NOT_FOUND] +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the missing backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "missing" renderer +[TIMESTAMP] [ERROR] (mdbook_driver::builtin_renderers): The command `trduyvbhijnorgevfuhn` wasn't found, is the "missing" backend installed? If you want to ignore this error when the "missing" backend is not installed, set `optional = true` in the `[output.missing]` section of the book.toml configuration file. +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: Unable to start the backend +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [NOT_FOUND] "#]]); }); @@ -99,10 +99,10 @@ fn missing_renderer() { fn missing_optional_not_fatal() { BookTest::from_dir("renderer/missing_optional_not_fatal").run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the missing backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "missing" renderer -[TIMESTAMP] [WARN] (mdbook::renderer): The command `trduyvbhijnorgevfuhn` for backend `missing` was not found, but was marked as optional. +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the missing backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "missing" renderer +[TIMESTAMP] [WARN] (mdbook_driver::builtin_renderers): The command `trduyvbhijnorgevfuhn` for backend `missing` was not found, but was marked as optional. "#]]); }); @@ -131,9 +131,9 @@ Hello World! "#]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the arguments backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "arguments" renderer +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the arguments backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "arguments" renderer "#]]); }); @@ -156,9 +156,9 @@ fn backends_receive_render_context_via_stdin() { ) .run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the cat-to-file backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "cat-to-file" renderer +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the cat-to-file backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "cat-to-file" renderer "#]]); }) @@ -234,9 +234,9 @@ fn legacy_relative_command_path() { ) .run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the myrenderer backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "myrenderer" renderer +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the myrenderer backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "myrenderer" renderer "#]]); }) @@ -253,10 +253,10 @@ fn legacy_relative_command_path() { ) .run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the myrenderer backend -[TIMESTAMP] [INFO] (mdbook::renderer): Invoking the "myrenderer" renderer -[TIMESTAMP] [WARN] (mdbook::renderer): Renderer command `../renderers/myrenderer[EXE]` uses a path relative to the renderer output directory `[ROOT]/book`. This was previously accepted, but has been deprecated. Relative executable paths should be relative to the book root. +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the myrenderer backend +[TIMESTAMP] [INFO] (mdbook_driver::builtin_renderers): Invoking the "myrenderer" renderer +[TIMESTAMP] [WARN] (mdbook_driver::builtin_renderers): Renderer command `../renderers/myrenderer[EXE]` uses a path relative to the renderer output directory `[ROOT]/book`. This was previously accepted, but has been deprecated. Relative executable paths should be relative to the book root. "#]]); }) diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index 8ab571b2..c3fa0085 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -1,8 +1,7 @@ //! Tests for search support. use crate::prelude::*; -use mdbook::book::Chapter; -use mdbook::BookItem; +use mdbook_core::book::{BookItem, Chapter}; use snapbox::file; use std::path::{Path, PathBuf}; @@ -62,7 +61,10 @@ fn reasonable_search_index() { // See note about InlineHtml in search.rs. Ideally the `alert()` part // should not be in the index, but we don't have a way to scrub inline // html. - assert_eq!(docs[&sneaky]["body"], "I put <HTML> in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed."); + assert_eq!( + docs[&sneaky]["body"], + "I put <HTML> in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed." + ); assert_eq!( docs[&no_headers]["breadcrumbs"], "First Chapter » No Headers" @@ -132,10 +134,10 @@ fn with_no_source_path() { fn chapter_settings_validation_error() { BookTest::from_dir("search/chapter_settings_validation_error").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: [output.html.search.chapter] key `does-not-exist` does not match any chapter paths +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [output.html.search.chapter] key `does-not-exist` does not match any chapter paths "#]]); }); diff --git a/tests/testsuite/test.rs b/tests/testsuite/test.rs index 54be8695..c116bc67 100644 --- a/tests/testsuite/test.rs +++ b/tests/testsuite/test.rs @@ -7,9 +7,9 @@ use crate::prelude::*; fn passing_tests() { BookTest::from_dir("test/passing_tests").run("test", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Intro': "intro.md" -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Passing 1': "passing1.md" -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Passing 2': "passing2.md" +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Intro': "intro.md" +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Passing 1': "passing1.md" +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Passing 2': "passing2.md" "#]]); }); @@ -27,8 +27,8 @@ fn failing_tests() { // still includes a little bit of output, so if that is a problem, // add more redactions. .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Failing Tests': "failing.md" -[TIMESTAMP] [ERROR] (mdbook::book): rustdoc returned an error: +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Failing Tests': "failing.md" +[TIMESTAMP] [ERROR] (mdbook_driver::mdbook): rustdoc returned an error: --- stdout @@ -38,8 +38,8 @@ test failing.md - Failing_Tests (line 3) ... FAILED thread 'main' panicked at failing.md:3:1: fail ... -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Failing Include': "failing_include.md" -[TIMESTAMP] [ERROR] (mdbook::book): rustdoc returned an error: +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Failing Include': "failing_include.md" +[TIMESTAMP] [ERROR] (mdbook_driver::mdbook): rustdoc returned an error: --- stdout ... @@ -48,7 +48,7 @@ test failing_include.md - Failing_Include (line 3) ... FAILED thread 'main' panicked at failing_include.md:3:1: failing! ... -[TIMESTAMP] [ERROR] (mdbook::utils): Error: One or more tests failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: One or more tests failed "#]]); }); @@ -62,14 +62,14 @@ fn test_individual_chapter() { cmd.args(&["Passing 1"]) .expect_stdout(str![[""]]) .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Passing 1': "passing1.md" +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Passing 1': "passing1.md" "#]]); }) // Can also be a source path. .run("test -c passing2.md", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Testing chapter 'Passing 2': "passing2.md" +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Testing chapter 'Passing 2': "passing2.md" "#]]); }); @@ -82,7 +82,7 @@ fn chapter_not_found() { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Chapter not found: bogus +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Chapter not found: bogus "#]]); }); diff --git a/tests/testsuite/theme.rs b/tests/testsuite/theme.rs index b22ae4e2..6ef67c23 100644 --- a/tests/testsuite/theme.rs +++ b/tests/testsuite/theme.rs @@ -9,10 +9,10 @@ fn missing_theme() { .run("build", |cmd| { cmd.expect_failure() .expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed -[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: theme dir [ROOT]/./non-existent-directory does not exist +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Rendering failed +[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: theme dir [ROOT]/./non-existent-directory does not exist "#]]); }); @@ -24,9 +24,9 @@ fn empty_theme() { BookTest::from_dir("theme/empty_theme").run("build", |cmd| { std::fs::create_dir(cmd.dir.join("theme")).unwrap(); cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }); @@ -145,12 +145,12 @@ fn copy_fonts_false_no_theme() { BookTest::from_dir("theme/copy_fonts_false_no_theme") .run("build", |cmd| { cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [WARN] (mdbook::renderer::html_handlebars::static_files): output.html.copy-fonts is deprecated. +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [WARN] (mdbook_html::html_handlebars::static_files): output.html.copy-fonts is deprecated. This book appears to have copy-fonts=false in book.toml without a fonts.css file. Add an empty `theme/fonts/fonts.css` file to squelch this warning. -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }) @@ -164,9 +164,9 @@ fn copy_fonts_false_with_empty_fonts_css() { BookTest::from_dir("theme/copy_fonts_false_with_empty_fonts_css") .run("build", |cmd| { cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }) @@ -180,9 +180,9 @@ fn copy_fonts_false_with_fonts_css() { BookTest::from_dir("theme/copy_fonts_false_with_fonts_css") .run("build", |cmd| { cmd.expect_stderr(str![[r#" -[TIMESTAMP] [INFO] (mdbook::book): Book building has started -[TIMESTAMP] [INFO] (mdbook::book): Running the html backend -[TIMESTAMP] [INFO] (mdbook::renderer::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started +[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend +[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book` "#]]); }) diff --git a/tests/testsuite/toc.rs b/tests/testsuite/toc.rs index eafbdc48..35b5e5ab 100644 --- a/tests/testsuite/toc.rs +++ b/tests/testsuite/toc.rs @@ -2,7 +2,7 @@ use crate::prelude::*; use anyhow::Context; -use mdbook::errors::*; +use anyhow::Result; use select::document::Document; use select::predicate::{Attr, Class, Name, Predicate}; use std::fs;