From 21e8c0882789c7e87df21ba4c6caa756cb49833b Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Tue, 16 Sep 2025 13:58:20 -0700 Subject: [PATCH] Add a basic utility to compare different versions of mdbook This is a very simplistic utility to compare the output of different versions of mdbook. This is useful when making changes to compare real-world books to see what changes actually happen. This is a variation of scripts that I have been using for a few years. This could definitely use some improvements, but this seems like it could be useful as-is. --- Cargo.lock | 4 ++ crates/mdbook-compare/Cargo.toml | 12 ++++ crates/mdbook-compare/README.md | 26 +++++++ crates/mdbook-compare/src/main.rs | 113 ++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 crates/mdbook-compare/Cargo.toml create mode 100644 crates/mdbook-compare/README.md create mode 100644 crates/mdbook-compare/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index c57c9cafe2..b32495c33f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1219,6 +1219,10 @@ dependencies = [ "walkdir", ] +[[package]] +name = "mdbook-compare" +version = "0.0.0" + [[package]] name = "mdbook-core" version = "0.5.0-alpha.1" diff --git a/crates/mdbook-compare/Cargo.toml b/crates/mdbook-compare/Cargo.toml new file mode 100644 index 0000000000..66f7248331 --- /dev/null +++ b/crates/mdbook-compare/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mdbook-compare" +publish = false +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/crates/mdbook-compare/README.md b/crates/mdbook-compare/README.md new file mode 100644 index 0000000000..f5239906bc --- /dev/null +++ b/crates/mdbook-compare/README.md @@ -0,0 +1,26 @@ +# mdbook-compare + +This is a simple utility to compare the output of two different versions of mdbook. + +To use this: + +1. Install [`tidy`](https://www.html-tidy.org/). +2. Install or build the initial version of mdbook that you want to compare. +3. Install or build the new version of mdbook that you want to compare. +4. Run `mdbook-compare` with the arguments to the mdbook executables and the books to build. + +```sh +cargo run --manifest-path /path/to/mdBook/Cargo.toml -p mdbook-compare -- \ + /path/to/orig/mdbook /path/to/my-book /path/to/new/mdbook /path/to/my-book +``` + +It takes two separate paths for the book to use for "before" and "after" in case you need to customize the book to run on older versions. If you don't need that, then you can use the same directory for both the before and after. + +`mdbook-compare` will do the following: + +1. Clean up any book directories. +2. Build the book with the first mdbook. +3. Build the book with the second mdbook. +4. The output of those two commands are stored in directories called `compare1` and `compare2`. +5. The HTML in those directories is normalized using `tidy`. +6. Runs `git diff` to compare the output. diff --git a/crates/mdbook-compare/src/main.rs b/crates/mdbook-compare/src/main.rs new file mode 100644 index 0000000000..c5828aec5b --- /dev/null +++ b/crates/mdbook-compare/src/main.rs @@ -0,0 +1,113 @@ +//! Utility to compare the output of two different versions of mdbook. + +use std::path::Path; +use std::process::Command; + +macro_rules! error { + ($msg:literal $($arg:tt)*) => { + eprint!("error: "); + eprintln!($msg $($arg)*); + std::process::exit(1); + }; +} + +fn main() { + let mut args = std::env::args().skip(1); + let (Some(mdbook1), Some(book1), Some(mdbook2), Some(book2)) = + (args.next(), args.next(), args.next(), args.next()) + else { + eprintln!("error: Expected four arguments: "); + std::process::exit(1); + }; + let mdbook1 = Path::new(&mdbook1); + let mdbook2 = Path::new(&mdbook2); + let book1 = Path::new(&book1); + let book2 = Path::new(&book2); + let compare1 = Path::new("compare1"); + let compare2 = Path::new("compare2"); + clean(compare1); + clean(compare2); + clean(&book1.join("book")); + clean(&book2.join("book")); + build(mdbook1, book1); + std::fs::rename(book1.join("book"), compare1).unwrap(); + build(mdbook2, book2); + std::fs::rename(book2.join("book"), compare2).unwrap(); + diff(compare1, compare2); +} + +fn clean(path: &Path) { + if path.exists() { + println!("removing {path:?}"); + std::fs::remove_dir_all(path).unwrap(); + } +} + +fn build(mdbook: &Path, book: &Path) { + println!("running `{mdbook:?} build` in `{book:?}`"); + let status = Command::new(mdbook) + .arg("build") + .current_dir(book) + .status() + .unwrap_or_else(|e| { + error!("expected {mdbook:?} executable to exist: {e}"); + }); + if !status.success() { + error!("process {mdbook:?} failed"); + } + process(&book.join("book")); +} + +fn process(path: &Path) { + for entry in std::fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + process(&path); + } else { + if path.extension().is_some_and(|ext| ext == "html") { + tidy(&path); + process_html(&path); + } else { + std::fs::remove_file(path).unwrap(); + } + } + } +} + +fn process_html(path: &Path) { + let content = std::fs::read_to_string(path).unwrap(); + let Some(start_index) = content.find("
") else { + return; + }; + let end_index = content.rfind("
").unwrap(); + let new_content = &content[start_index..end_index + 8]; + std::fs::write(path, new_content).unwrap(); +} + +fn tidy(path: &Path) { + // quiet, no wrap, modify in place + let args = "-q -w 0 -m --custom-tags yes --drop-empty-elements no"; + println!("running `tidy {args}` in `{path:?}`"); + let status = Command::new("tidy") + .args(args.split(' ')) + .arg(path) + .status() + .expect("tidy should be installed"); + if !status.success() { + // Exit code 1 is a warning. + if status.code() != Some(1) { + error!("tidy failed: {status}"); + } + } +} + +fn diff(a: &Path, b: &Path) { + let args = "diff --no-index"; + println!("running `git diff {args} {a:?} {b:?}`"); + Command::new("git") + .args(args.split(' ')) + .args([a, b]) + .status() + .unwrap(); +}