diff --git a/CHANGELOG.md b/CHANGELOG.md index 9165c90456..041a113c94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ [#2970](https://github.com/rust-lang/mdBook/pull/2970) - Updated cargo dependencies. [#2969](https://github.com/rust-lang/mdBook/pull/2969) +- [#2503](https://github.com/rust-lang/mdBook/pull/2503)) + Added capability to use external crates in code samples which are tested + as doc tests. ### Fixed @@ -255,6 +258,7 @@ The following is a summary of the changes that may require your attention when u [#2926](https://github.com/rust-lang/mdBook/pull/2926) - Hide the sidebar resize indicator when JS isn't available. [#2923](https://github.com/rust-lang/mdBook/pull/2923) +- Allow doctests to use external crates by referencing a `Cargo.toml` ## mdBook 0.5.0-beta.1 [v0.5.0-alpha.1...v0.5.0-beta.1](https://github.com/rust-lang/mdBook/compare/v0.5.0-alpha.1...v0.5.0-beta.1) diff --git a/Cargo.lock b/Cargo.lock index f695aa7f82..2a51b79d1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,9 +173,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -212,6 +212,17 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cargo-manifest" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fae5b96898dec422317cc18037ff6ddd3f7d0766e74a230e9aeddfc21f878267" +dependencies = [ + "serde", + "thiserror", + "toml", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -426,7 +437,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", "crypto-common 0.2.2", ] @@ -738,9 +749,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -845,9 +856,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" dependencies = [ "crossbeam-deque", "globset", @@ -873,9 +884,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1" dependencies = [ "bitflags", "inotify-sys", @@ -958,9 +969,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "mac" @@ -1053,6 +1064,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "mdbook-book-code-samples" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "mdbook-core", + "mdbook-driver", + "mdbook-html", + "mdbook-markdown", + "mdbook-preprocessor", + "mdbook-renderer", + "mdbook-summary", + "pulldown-cmark 0.13.4", + "pulldown-cmark-to-cmark 18.0.0", + "semver", + "serde", + "serde_json", +] + [[package]] name = "mdbook-compare" version = "0.0.0" @@ -1062,6 +1093,7 @@ name = "mdbook-core" version = "0.5.3" dependencies = [ "anyhow", + "cargo-manifest", "regex", "serde", "serde_json", @@ -1107,7 +1139,7 @@ dependencies = [ "mdbook-core", "mdbook-markdown", "mdbook-renderer", - "pulldown-cmark", + "pulldown-cmark 0.13.4", "regex", "serde", "serde_json", @@ -1121,7 +1153,7 @@ dependencies = [ name = "mdbook-markdown" version = "0.5.3" dependencies = [ - "pulldown-cmark", + "pulldown-cmark 0.13.4", "regex", "tracing", ] @@ -1141,8 +1173,8 @@ name = "mdbook-remove-emphasis" version = "0.1.0" dependencies = [ "mdbook-preprocessor", - "pulldown-cmark", - "pulldown-cmark-to-cmark", + "pulldown-cmark 0.13.4", + "pulldown-cmark-to-cmark 22.0.0", "serde_json", ] @@ -1163,16 +1195,16 @@ dependencies = [ "anyhow", "mdbook-core", "memchr", - "pulldown-cmark", + "pulldown-cmark 0.13.4", "serde", "tracing", ] [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -1273,9 +1305,9 @@ dependencies = [ [[package]] name = "num-modular" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" +checksum = "fc41a1374056e9672221567958a66c16be12d0e2c1b408761e14d901c237d5e0" [[package]] name = "num-order" @@ -1300,9 +1332,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opener" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fa337e0cf13357c13ef1dc108df1333eb192f75fc170bea03fcf1fd404c2ee" +checksum = "b2b03ff07a220d0d0ec9a1f0f238951b7967a5a2e96aefcd21a117b1083415e9" dependencies = [ "bstr", "normpath", @@ -1523,6 +1555,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + [[package]] name = "pulldown-cmark" version = "0.13.4" @@ -1541,13 +1584,22 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pulldown-cmark-to-cmark" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e02b63adcb49f2eb675b1694b413b3e9fedbf549dfe2cc98727ad97a0c30650" +dependencies = [ + "pulldown-cmark 0.12.2", +] + [[package]] name = "pulldown-cmark-to-cmark" version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" dependencies = [ - "pulldown-cmark", + "pulldown-cmark 0.13.4", ] [[package]] @@ -1641,9 +1693,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1664,9 +1716,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustix" @@ -1871,9 +1923,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "snapbox" @@ -2384,9 +2436,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] @@ -2661,18 +2713,18 @@ version = "0.0.0" [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1b8fca8dfa..bc69d746e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [ ".", "crates/*", - "examples/remove-emphasis/mdbook-remove-emphasis", "guide/guide-helper", + "examples/remove-emphasis/mdbook-remove-emphasis", "guide/guide-helper", "guide" ] [workspace.lints.clippy] @@ -27,6 +27,7 @@ rust-version = "1.88.0" # Keep in sync with installation.md and .github/workflow [workspace.dependencies] anyhow = "1.0.102" axum = "0.8.9" +cargo-manifest = "0.21.0" clap = { version = "4.6.1", features = ["cargo", "wrap_help"] } clap_complete = "4.6.5" ego-tree = "0.11.0" @@ -75,7 +76,7 @@ version = "0.5.3" authors = [ "Mathieu David ", "Michael-F-Bryan ", - "Matt Ickstadt " + "Matt Ickstadt ", ] documentation = "https://rust-lang.github.io/mdBook/index.html" edition.workspace = true diff --git a/crates/mdbook-core/Cargo.toml b/crates/mdbook-core/Cargo.toml index bce2d09e41..0fe26efc64 100644 --- a/crates/mdbook-core/Cargo.toml +++ b/crates/mdbook-core/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true +cargo-manifest.workspace = true regex.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/mdbook-core/src/config.rs b/crates/mdbook-core/src/config.rs index b2c6862c30..77f9edfb41 100644 --- a/crates/mdbook-core/src/config.rs +++ b/crates/mdbook-core/src/config.rs @@ -420,6 +420,8 @@ impl Default for BuildConfig { #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[non_exhaustive] pub struct RustConfig { + /// Path to a Cargo.toml + pub manifest: Option, /// Rust edition used in playground pub edition: Option, } @@ -745,6 +747,9 @@ mod tests { create-missing = false use-default-preprocessors = true + [rust] + manifest = "./Cargo.toml" + [output.html] theme = "./themedir" default-theme = "rust" @@ -783,7 +788,10 @@ mod tests { use_default_preprocessors: true, extra_watch_dirs: Vec::new(), }; - let rust_should_be = RustConfig { edition: None }; + let rust_should_be = RustConfig { + manifest: Some(PathBuf::from("./Cargo.toml")), + edition: None, + }; let playground_should_be = Playground { editable: true, copyable: true, @@ -861,6 +869,7 @@ mod tests { assert_eq!(got.book, book_should_be); let rust_should_be = RustConfig { + manifest: None, edition: Some(RustEdition::E2015), }; let got = Config::from_str(src).unwrap(); @@ -880,6 +889,7 @@ mod tests { "#; let rust_should_be = RustConfig { + manifest: None, edition: Some(RustEdition::E2018), }; @@ -900,6 +910,7 @@ mod tests { "#; let rust_should_be = RustConfig { + manifest: None, edition: Some(RustEdition::E2021), }; @@ -1187,6 +1198,18 @@ mod tests { ); } + #[test] + #[should_panic(expected = "unknown field `foo`, expected `manifest` or `edition`")] + // invalid key in config file should really generate an error... + fn invalid_rust_setting() { + let src = r#" + [rust] + foo = "bar" + "#; + + Config::from_str(src).unwrap(); + } + #[test] fn contains_key() { let src = r#" diff --git a/crates/mdbook-driver/src/init.rs b/crates/mdbook-driver/src/init.rs index badd24c58b..95df44f1f9 100644 --- a/crates/mdbook-driver/src/init.rs +++ b/crates/mdbook-driver/src/init.rs @@ -8,6 +8,15 @@ use mdbook_html::theme::Theme; use std::path::PathBuf; use tracing::{debug, error, info, trace}; +const INIT_CARGO_TOML: &str = r#" +[package] +name = "book" +version = "0.1.0" +edition = "2024" + +[dependencies] +"#; + /// A helper for setting up a new book and its directory structure. #[derive(Debug, Clone, PartialEq)] pub struct BookBuilder { @@ -82,6 +91,8 @@ impl BookBuilder { self.write_book_toml()?; + self.write_cargo_toml()?; + match MDBook::load(&self.root) { Ok(book) => Ok(book), Err(e) => { @@ -105,6 +116,12 @@ impl BookBuilder { Ok(()) } + fn write_cargo_toml(&self) -> Result<()> { + debug!("Writing Cargo.toml"); + let cargo_toml = self.root.join("Cargo.toml"); + fs::write(&cargo_toml, INIT_CARGO_TOML) + } + fn copy_across_theme(&self) -> Result<()> { debug!("Copying theme"); diff --git a/crates/mdbook-driver/src/mdbook.rs b/crates/mdbook-driver/src/mdbook.rs index 45bae41816..27a9aa6dc5 100644 --- a/crates/mdbook-driver/src/mdbook.rs +++ b/crates/mdbook-driver/src/mdbook.rs @@ -6,19 +6,16 @@ use crate::init::BookBuilder; use crate::load::{load_book, load_book_from_disk}; use anyhow::{Context, Error, Result, bail}; use indexmap::IndexMap; -use mdbook_core::book::{Book, BookItem, BookItems}; -use mdbook_core::config::{Config, RustEdition}; +use mdbook_core::book::{Book, BookItem, BookItems, Chapter}; +use mdbook_core::config::Config; use mdbook_core::utils::fs; use mdbook_html::HtmlHandlebars; use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use mdbook_renderer::{RenderContext, Renderer}; use mdbook_summary::Summary; use serde::Deserialize; -use std::ffi::OsString; -use std::io::IsTerminal; use std::path::{Path, PathBuf}; use std::process::Command; -use tempfile::Builder as TempFileBuilder; use topological_sort::TopologicalSort; use tracing::{debug, info, trace, warn}; @@ -225,32 +222,15 @@ impl MDBook { } /// Run `rustdoc` tests on the book, linking against the provided libraries. - pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { + pub fn test(&mut self) -> Result<()> { // test_chapter with chapter:None will run all tests. - self.test_chapter(library_paths, None) + self.test_chapter(None) } - /// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries. - /// If `chapter` is `None`, all tests will be run. - pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> { - let cwd = std::env::current_dir()?; - let library_args: Vec = library_paths - .into_iter() - .flat_map(|path| { - let path = Path::new(path); - let path = if path.is_relative() { - cwd.join(path).into_os_string() - } else { - path.to_path_buf().into_os_string() - }; - [OsString::from("-L"), path] - }) - .collect(); - - let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; - - let mut chapter_found = false; - + /// Run `cargo test --doc` tests on a specific chapter of the book, + /// linking against the provided libraries. If `chapter` is `None`, + /// all tests will be run. + pub fn test_chapter(&mut self, filter: Option<&str>) -> Result<()> { struct TestRenderer; impl Renderer for TestRenderer { // FIXME: Is "test" the proper renderer name to use here? @@ -265,82 +245,113 @@ impl MDBook { let (book, _) = self.preprocess_book(&TestRenderer)?; - let color_output = std::io::stderr().is_terminal(); - let mut failed = false; - for item in book.iter() { + // let color_output = std::io::stderr().is_terminal(); + + let cargo_toml = if let Some(manifest) = &self.config.rust.manifest { + // extern_args.load(&self.root.join(manifest))?; + self.root.join(manifest) + } else { + let manifest = self.root.join("Cargo.toml"); + fs::write(&manifest, "")?; + manifest + }; + + fn get_chapter(item: &BookItem) -> Option<(&Chapter, &PathBuf)> { if let BookItem::Chapter(ref ch) = *item { - let chapter_path = match ch.path { - Some(ref path) if !path.as_os_str().is_empty() => path, - _ => continue, - }; - - if let Some(chapter) = chapter { - if ch.name != chapter && chapter_path.to_str() != Some(chapter) { - if chapter == "?" { - info!("Skipping chapter '{}'...", ch.name); - } - continue; - } + match ch.path { + Some(ref path) if !path.as_os_str().is_empty() => Some((ch, path)), + _ => None, } - chapter_found = true; - info!("Testing chapter '{}': {:?}", ch.name, chapter_path); - - // write preprocessed file to tempdir - let path = temp_dir.path().join(chapter_path); - fs::write(&path, &ch.content)?; - - let mut cmd = Command::new("rustdoc"); - cmd.current_dir(temp_dir.path()) - .arg(chapter_path) - .arg("--test") - .args(&library_args); - - if let Some(edition) = self.config.rust.edition { - match edition { - RustEdition::E2015 => { - cmd.args(["--edition", "2015"]); - } - RustEdition::E2018 => { - cmd.args(["--edition", "2018"]); - } - RustEdition::E2021 => { - cmd.args(["--edition", "2021"]); - } - RustEdition::E2024 => { - cmd.args(["--edition", "2024"]); - } - _ => panic!("RustEdition {edition:?} not covered"), + } else { + None + } + } + + let mut selected_chapter = None; + + let rustify = |name: String| { + name.chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() { + ch.to_ascii_lowercase() + } else { + '_' } - } + }) + .collect::() + }; - if color_output { - cmd.args(["--color", "always"]); + let chapters: Vec<_> = book + .iter() + .filter_map(get_chapter) + .enumerate() + .map(|(i, (chapter, path))| { + if let Some(needle) = filter { + if chapter.name == needle || path.to_str() == Some(needle) { + info!("Testing chapter '{}': {:?}", chapter.name, path); + selected_chapter = Some(i); + } else { + info!("Skipping chapter '{}'...", chapter.name); + } + } else { + info!("Testing chapter '{}': {:?}", chapter.name, path); } + ( + format!("{}_{}", rustify(chapter.name.clone()), i), + chapter.content.clone(), + ) + }) + .collect(); - debug!("running {:?}", cmd); - let output = cmd - .output() - .with_context(|| "failed to execute `rustdoc`")?; - - if !output.status.success() { - failed = true; - eprintln!( - "ERROR rustdoc returned an error:\n\ - \n--- stdout\n{}\n--- stderr\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - } - } - if failed { - bail!("One or more tests failed"); + let lib = self.root.join(&self.config.book.src).join("lib.rs"); + fs::write( + &lib, + chapters + .iter() + .map(|(name, content)| { + format!( + "{} mod {} {{}}\n", + content + .split("\n") + .map(|line| format!("/// {}\n", line)) + .collect::(), + name + ) + }) + .collect::(), + )?; + + let mut cargo = Command::new("cargo"); + cargo + .current_dir(&self.root) + .arg("test") + .arg("--doc") + .arg("--manifest-path") + .arg(cargo_toml); + if let Some(id) = selected_chapter { + cargo.arg(chapters[id].0.clone()); } - if let Some(chapter) = chapter { - if !chapter_found { - bail!("Chapter not found: {}", chapter); - } + + debug!("running {:?}", cargo); + let output = cargo + .output() + .with_context(|| "failed to execute `cargo`")?; + + if !output.status.success() { + // failed = true; + eprintln!( + "ERROR cargo returned an error:\n\ + \n--- stdout\n{}\n--- stderr\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } else { + print!("{}", String::from_utf8_lossy(&output.stdout)); } + + // Newline is necessary for `rustfmt` related checks not to complain + fs::write(&lib, "\n")?; + Ok(()) } diff --git a/guide/Cargo.toml b/guide/Cargo.toml new file mode 100644 index 0000000000..9094d53938 --- /dev/null +++ b/guide/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mdbook-book-code-samples" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow.workspace = true +clap.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 +pulldown-cmark.workspace = true +pulldown-cmark-to-cmark = "18.0.0" +serde.workspace = true +serde_json.workspace = true +semver.workspace = true diff --git a/guide/book.toml b/guide/book.toml index d8863339a3..138d7b9fb6 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -5,7 +5,8 @@ authors = ["Mathieu David", "Michael-F-Bryan"] language = "en" [rust] -edition = "2018" +## not needed, and will cause an error, if using Cargo.toml: edition = "2021" +manifest = "Cargo.toml" [output.html] smart-punctuation = true diff --git a/guide/src/cli/test.md b/guide/src/cli/test.md index 39a3d4ac7b..2c7989a24d 100644 --- a/guide/src/cli/test.md +++ b/guide/src/cli/test.md @@ -1,32 +1,16 @@ # The test command -When writing a book, you sometimes need to automate some tests. For example, +When writing a book, you may want to provide some code samples, +and it's important that these be kept accurate as your software API evolves. +For example, [The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot -of code examples that could get outdated. Therefore it is very important for -them to be able to automatically test these code examples. +of code samples that could become outdated as the language evolves. -mdBook supports a `test` command that will run all available tests in a book. At -the moment, only Rust tests are supported. +MdBook supports a `test` command which runs code samples in your book as doc tests to verify they +will compile, and, optionally, run correctly. +For details on how to specify the test to be done and outcome to be expected, see [Code Blocks](/format/mdbook.md#code-blocks). -#### Disable tests on a code block - -rustdoc doesn't test code blocks which contain the `ignore` attribute: - - ```rust,ignore - fn main() {} - ``` - -rustdoc also doesn't test code blocks which specify a language other than Rust: - - ```markdown - **Foo**: _bar_ - ``` - -rustdoc *does* test code blocks which have no language specified: - - ``` - This is going to cause an error! - ``` +At the moment, mdBook only supports doc *tests* written in Rust, although code samples can be written and *displayed* in many programming languages. #### Specify a directory @@ -37,7 +21,22 @@ instead of the current working directory. mdbook test path/to/book ``` -#### `--library-path` +#### `--dest-dir` + +The `--dest-dir` (`-d`) option allows you to change the output directory for the +book. Relative paths are interpreted relative to the book's root directory. If +not specified it will default to the value of the `build.build-dir` key in +`book.toml`, or to `./book`. + +#### `--chapter` + +The `--chapter` (`-c`) option allows you to test a specific chapter of the +book using the chapter name or the relative path to the chapter. + +#### `--library-path` `[`deprecated`]` + +***Note*** This argument is deprecated. Since Rust edition 2018, the compiler needs an explicit `--extern` argument for each external crate used in a doc test, it no longer simply scans the library path for likely-looking crates. +New projects should list external crates as dependencies in a **Cargo.toml** file and reference that file in your ***book.toml***, as described in [rust configuration](/format/configuration/general.html#rust-options). The `--library-path` (`-L`) option allows you to add directories to the library search path used by `rustdoc` when it builds and tests the examples. Multiple diff --git a/guide/src/for_developers/backends.md b/guide/src/for_developers/backends.md index 2ab3b3b7f7..ba1d15ab9d 100644 --- a/guide/src/for_developers/backends.md +++ b/guide/src/for_developers/backends.md @@ -31,7 +31,8 @@ a [`RenderContext::from_json()`] constructor which will load a `RenderContext`. This is all the boilerplate necessary for our backend to load the book. -```rust +```rust,should_panic +# // this sample panics because it can't open stdin // src/main.rs use std::io; use mdbook_renderer::RenderContext; @@ -52,14 +53,18 @@ fn main() { ## Inspecting the book -Now our backend has a copy of the book, lets count how many words are in each +Now our backend has a copy of the book, let's count how many words are in each chapter! Because the `RenderContext` contains a [`Book`] field (`book`), and a `Book` has the [`Book::iter()`] method for iterating over all items in a `Book`, this step turns out to be just as easy as the first. -```rust +```rust,should_panic +# // this sample panics because it can't open stdin +use std::io; +use mdbook_renderer::RenderContext; +use mdbook_core::book::{BookItem, Chapter}; fn main() { let mut stdin = io::stdin(); @@ -171,24 +176,25 @@ deserializing to some arbitrary type `T`. To implement this, we'll create our own serializable `WordcountConfig` struct which will encapsulate all configuration for this backend. -First add `serde` and `serde_derive` to your `Cargo.toml`, +First add `serde` to your `Cargo.toml`, -``` -$ cargo add serde serde_derive +```shell +$ cargo add serde ``` And then you can create the config struct, ```rust -use serde_derive::{Serialize, Deserialize}; +use serde::{Serialize, Deserialize}; -... +fn main() { #[derive(Debug, Default, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case")] pub struct WordcountConfig { pub ignores: Vec, } +} ``` Now we just need to deserialize the `WordcountConfig` from our `RenderContext` diff --git a/guide/src/for_developers/preprocessors.md b/guide/src/for_developers/preprocessors.md index 86533ff491..b04ce8d8b5 100644 --- a/guide/src/for_developers/preprocessors.md +++ b/guide/src/for_developers/preprocessors.md @@ -36,9 +36,8 @@ be adapted for other preprocessors.
Example no-op preprocessor -```rust +```rust,no_run // nop-preprocessors.rs - {{#include ../../../examples/nop-preprocessor.rs}} ```
@@ -65,7 +64,12 @@ The [`mdbook-markdown`] crate exposes the [`pulldown-cmark`][pc] crate used by m The following code block shows how to remove all emphasis from markdown, without accidentally breaking the document. -```rust +```rust,compile_fail +# // tagged compile_fail because +# // sample fails to compile here: +# // "trait Borrow not implemented for pulldown_cmark_to_cmark::..." +# // Probably due to version skew on pulldown-cmark +# // between examples/remove-emphasis/Cargo.toml and /Cargo.toml {{#rustdoc_include ../../../examples/remove-emphasis/mdbook-remove-emphasis/src/main.rs:remove_emphasis}} ``` diff --git a/guide/src/format/configuration/general.md b/guide/src/format/configuration/general.md index a01b0b1008..777e2e1a33 100644 --- a/guide/src/format/configuration/general.md +++ b/guide/src/format/configuration/general.md @@ -11,7 +11,7 @@ authors = ["John Doe"] description = "The example book covers examples." [rust] -edition = "2018" +manifest = "./Cargo.toml" [build] build-dir = "my-example-book" @@ -30,7 +30,7 @@ limit-results = 15 ## Supported configuration options -It is important to note that **any** relative path specified in the +> Note: **any** relative path specified in the configuration will always be taken relative from the root of the book where the configuration file is located. @@ -38,6 +38,16 @@ configuration file is located. This is general information about your book. +```toml +[book] +title = "Example book" +authors = ["John Doe", "Jane Doe"] +description = "The example book covers examples." +src = "my-src" # source files in `root/my-src` instead of `root/src` +language = "en" +text-direction = "ltr" +``` + - **title:** The title of the book - **authors:** The author(s) of the book - **description:** A description for the book, which is added as meta @@ -50,17 +60,6 @@ This is general information about your book. - **text-direction**: The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). Possible values: `ltr`, `rtl`. When not specified, the text direction is derived from the book's `language` attribute. -**book.toml** -```toml -[book] -title = "Example book" -authors = ["John Doe", "Jane Doe"] -description = "The example book covers examples." -src = "my-src" # the source files will be found in `root/my-src` instead of `root/src` -language = "en" -text-direction = "ltr" -``` - ### Rust options Options for the Rust language, relevant to running tests and playground @@ -68,19 +67,17 @@ integration. ```toml [rust] -edition = "2015" # the default edition for code blocks +manifest = "path/for/Cargo.toml" +edition = "2015" # [deprecated] the default edition for code blocks ``` +- **manifest**: Path to a ***Cargo.toml*** file which is used to resolve dependencies of your sample code. mdBook also uses the `package.edition` configured in the cargo project as the default for code snippets in your book. +See [Using External Crates and Dependencies](/format/mdbook.html#using-external-crates-and-dependencies) for details. + - **edition**: Rust edition to use by default for the code snippets. Default is `"2015"`. Individual code blocks can be controlled with the `edition2015`, `edition2018`, `edition2021` or `edition2024` annotations, such as: - ~~~text - ```rust,edition2015 - // This only works in 2015. - let try = true; - ``` - ~~~ ### Build options diff --git a/guide/src/format/mathjax.md b/guide/src/format/mathjax.md index d60ed0a5fd..5d73958038 100644 --- a/guide/src/format/mathjax.md +++ b/guide/src/format/mathjax.md @@ -20,24 +20,25 @@ extra backslash to work. Hopefully this limitation will be lifted soon. > to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4 > \end{cases}`). - ### Inline equations + Inline equations are delimited by `\\(` and `\\)`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following: -``` + +```text \\( \int x dx = \frac{x^2}{2} + C \\) ``` ### Block equations + Block equations are delimited by `\\[` and `\\]`. To render the following equation \\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\] - you would write: -```bash +```text \\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\] ``` diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md index 9401107eb2..095a5503fa 100644 --- a/guide/src/format/mdbook.md +++ b/guide/src/format/mdbook.md @@ -1,5 +1,9 @@ # mdBook-specific features +# Code blocks + +These capabilities primarily affect how the user sees or interacts with code samples in your book and are supported directly by mdBook. Some also affect running the sample as a documentation test. (for which mdBook invokes `rustdoc --test`), so : this is detailed in the sections below. + ## Hiding code lines There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix. @@ -9,7 +13,7 @@ This prefix can be escaped with `##` to prevent the hiding of a line that should [rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example -```bash +```text # fn main() { let x = 5; let y = 6; @@ -41,7 +45,7 @@ python = "~" The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this: -```bash +```text ~hidden() nothidden(): ~ hidden() @@ -152,6 +156,7 @@ interpreting them. ```` ## Including portions of a file + Often you only need a specific part of the file, e.g. relevant lines for an example. We support four different modes of partial includes: @@ -176,6 +181,7 @@ the regex `ANCHOR_END:\s*[\w_-]+`. This allows you to put anchors in any kind of commented line. Consider the following file to include: + ```rs /* ANCHOR: all */ @@ -193,6 +199,7 @@ impl System for MySystem { ... } ``` Then in the book, all you have to do is: + ````hbs Here is a component: ```rust,no_run,noplayground @@ -224,7 +231,7 @@ Rustdoc will use the complete example when you run `mdbook test`. For example, consider a file named `file.rs` that contains this Rust program: -```rust +```rust,editable fn main() { let x = add_one(2); assert_eq!(x, 3); @@ -307,6 +314,21 @@ And the `editable` attribute will enable the [editor] as described at [Rust code [Rust Playground]: https://play.rust-lang.org/ +## Using external crates and dependencies + +If your code samples depend on external crates, you will probably want to include `use ` statements in the code and want them to resolve and allow documentation tests to run. +To configure this: + +1. Create a ***Cargo.toml*** file with a `[package.dependencies]` section that defines a dependency for each `` you want to use in any sample. If your book is already embedded in an existing Cargo project, you may be able to use the existing project `Cargo.toml`. +2. In your ***book.toml***: + * configure the path to ***Cargo.toml*** in `rust.manifest`, as described in [rust configuration](/format/configuration/general.html#rust-options). + * remove `rust.edition` if it is configured. The default rust edition will be as specified in the ***Cargo.toml*** (though this can be overridden for a specific code block). + * Refrain from invoking `mdbook test` with `-L` or `--library-path` argument. This, too, will be inferred from cargo project configuration + +# Features for general content + +These can be used in markdown text (outside code blocks). + ## Controlling page \ A chapter can set a \ that is different from its entry in the table of diff --git a/guide/src/lib.rs b/guide/src/lib.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/guide/src/lib.rs @@ -0,0 +1 @@ + diff --git a/src/cmd/test.rs b/src/cmd/test.rs index 2be8e179b3..74859ccc34 100644 --- a/src/cmd/test.rs +++ b/src/cmd/test.rs @@ -25,7 +25,7 @@ pub fn make_subcommand() -> Command { .value_parser(NonEmptyStringValueParser::new()) .action(ArgAction::Append) .help( - "A comma-separated list of directories to add to the crate \ + "[deprecated] A comma-separated list of directories to add to the crate \ search path when building tests", ), ) @@ -33,19 +33,14 @@ pub fn make_subcommand() -> Command { // test command implementation pub fn execute(args: &ArgMatches) -> Result<()> { - let library_paths: Vec<&str> = args - .get_many("library-path") - .map(|it| it.map(String::as_str).collect()) - .unwrap_or_default(); - let chapter: Option<&str> = args.get_one::("chapter").map(|s| s.as_str()); let book_dir = get_book_dir(args); let mut book = MDBook::load(book_dir)?; match chapter { - Some(_) => book.test_chapter(library_paths, chapter), - None => book.test(library_paths), + Some(_) => book.test_chapter(chapter), + None => book.test(), }?; Ok(()) diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index 641243c103..1e2f0ce33f 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -201,7 +201,7 @@ ERROR Invalid configuration file | 2 | title = "bad-config" | ^^^^^ -unknown field `title`, expected `edition` +unknown field `title`, expected `manifest` or `edition` "#]]); diff --git a/tests/testsuite/preprocessor.rs b/tests/testsuite/preprocessor.rs index 33c320785e..d0e3ba43b7 100644 --- a/tests/testsuite/preprocessor.rs +++ b/tests/testsuite/preprocessor.rs @@ -51,7 +51,7 @@ fn test_with_custom_preprocessor() { let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_preprocessor(Spy(Arc::clone(&spy))); - book.test(vec![]).unwrap(); + book.test().unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1);