From f6f4a90e1220054b82f6c0a532efa51ed36bd487 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 4 Mar 2025 10:08:24 +0200 Subject: [PATCH] xtask: also generate EXAMPLES section Include "Examples" section from READMEs if they exist into the generated manual page. Signed-off-by: Manos Pitsidianakis --- Cargo.lock | 16 +++++++++++ xtask/Cargo.toml | 3 +- xtask/README.md | 2 ++ xtask/src/main.rs | 70 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a438c17..0661a8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,6 +857,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "markdown" +version = "1.0.0-alpha.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9047e0a37a596d4e15411a1ffbdabe71c328908cb90a721cb9bf8dcf3434e6d2" +dependencies = [ + "unicode-id", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1741,6 +1750,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -2350,6 +2365,7 @@ version = "0.1.0" dependencies = [ "clap", "clap_mangen", + "markdown", "toml 0.8.19", ] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index a2f68f5..ddd24b3 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -13,6 +13,7 @@ publish = false clap = { version = "4.5", features = ["derive"], optional = true } clap_mangen = { version = "0.2.24", optional = true } toml = { version = "0.8.19", optional = true } +markdown = { version = "=1.0.0-alpha.23", optional = true } [build-dependencies] @@ -22,7 +23,7 @@ vhost-device-scmi = [] vhost-device-sound = ["vhost-device-sound-alsa", "vhost-device-sound-pipewire"] vhost-device-sound-alsa = ["mangen"] vhost-device-sound-pipewire = ["mangen"] -mangen = ["dep:clap_mangen", "dep:clap", "dep:toml"] +mangen = ["dep:clap_mangen", "dep:clap", "dep:toml", "dep:markdown"] [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("alsa-backend", "pw-backend"))'] } diff --git a/xtask/README.md b/xtask/README.md index c54d67f..e4a30f0 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -6,6 +6,8 @@ This binary crate provides support for running useful tasks with `cargo xtask <. The `mangen` task which is enabled by the `mangen` cargo feature, builds ROFF manual pages for binary crates in this repository. It uses the [`clap_mangen`](https://crates.io/crates/clap_mangen) crate to generate ROFF from the crate's argument types which implement the `clap::CommandFactory` trait, through the `clap::Parser` derive macro. +Furthmore, if the `README.md` of a crate contains an `Examples` heading, it includes it in the manual page. + ```session $ cargo xtask mangen Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 01ad6b7..a31f228 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -15,6 +15,8 @@ use clap::CommandFactory; #[cfg(feature = "mangen")] use clap_mangen::Man; #[cfg(feature = "mangen")] +use markdown::{to_mdast, ParseOptions}; +#[cfg(feature = "mangen")] use toml::value::Table; // Use vhost-device-sound's args module as our own using the #[path] attribute @@ -63,7 +65,19 @@ fn print_help() { } #[cfg(feature = "mangen")] -fn mangen_for_crate(manifest: Table) -> Result, Box> { +fn mangen_for_crate( + crate_dir: std::path::PathBuf, +) -> Result, Box> { + let readme_md = std::fs::read_to_string(crate_dir.join("README.md"))?; + let example_text = parse_examples_from_readme(readme_md).unwrap_or_default(); + let examples = if example_text.is_empty() { + None + } else { + Some(example_text.trim()) + }; + let manifest = std::fs::read_to_string(crate_dir.join("Cargo.toml"))?; + let manifest = manifest.as_str().parse::()?; + let name: &'static str = manifest["package"]["name"] .as_str() .unwrap() @@ -94,6 +108,14 @@ fn mangen_for_crate(manifest: Table) -> Result, Box = Default::default(); man.render(&mut buffer)?; + if let Some(examples) = examples { + let mut examples_section = clap_mangen::roff::Roff::new(); + examples_section.control("SH", ["EXAMPLES"]); + for line in examples.lines() { + examples_section.text(vec![line.into()]); + } + examples_section.to_writer(&mut buffer)?; + } clap_mangen::roff::Roff::new() .control("SH", ["REPORTING BUGS"]) .text(vec![format!( @@ -105,6 +127,41 @@ fn mangen_for_crate(manifest: Table) -> Result, Box Result> { + use markdown::mdast; + + let mdast = to_mdast(&readme_md, &ParseOptions::gfm()).map_err(|err| err.to_string())?; + let mut example_text = String::new(); + if let mdast::Node::Root(root) = mdast { + if let Some(examples_index) = root.children.iter().position(|r| matches!(r, mdast::Node::Heading(mdast::Heading { ref children, .. }) if matches!(children.first(), Some(mdast::Node::Text(mdast::Text { ref value, .. })) if value.trim() == "Examples"))){ + let mdast::Node::Heading(examples_heading) = + &root.children[examples_index] + else { + // SAFETY: Unreachable because we found the exact position earlier. + unreachable!(); + }; + let depth = examples_heading.depth; + let mut i = examples_index + 1; + while i < root.children.len() && !matches!(root.children[i], mdast::Node::Heading(ref h) if h.depth >= depth) { + match &root.children[i] { + mdast::Node::Paragraph(p) => { + example_text.push_str(&p.children.iter().map(|t| t.to_string()).collect::>().join(" ")); + example_text.push_str("\n\n"); + }, + mdast::Node::Code(c) => { + example_text.push_str(&c.value); + example_text.push_str("\n\n"); + }, + _ => {}, + } + i += 1; + } + } + } + Ok(example_text) +} + #[cfg(feature = "mangen")] fn mangen() -> Result<(), Box> { let workspace_dir = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")) @@ -124,11 +181,7 @@ fn mangen() -> Result<(), Box> { { use vhost_device_sound::SoundArgs; - let manifest = - std::fs::read_to_string(workspace_dir.join("vhost-device-sound/Cargo.toml"))?; - let manifest = manifest.as_str().parse::
()?; - - let buffer = mangen_for_crate::(manifest)?; + let buffer = mangen_for_crate::(workspace_dir.join("vhost-device-sound"))?; let man_path = dist_dir.join("vhost-device-sound.1"); buffers.push((man_path, buffer)); } @@ -136,10 +189,7 @@ fn mangen() -> Result<(), Box> { { use vhost_device_scmi::ScmiArgs; - let manifest = std::fs::read_to_string(workspace_dir.join("vhost-device-scmi/Cargo.toml"))?; - let manifest = manifest.as_str().parse::
()?; - - let buffer = mangen_for_crate::(manifest)?; + let buffer = mangen_for_crate::(workspace_dir.join("vhost-device-scmi"))?; let man_path = dist_dir.join("vhost-device-scmi.1"); buffers.push((man_path, buffer)); }