Skip to content

Commit f6f4a90

Browse files
epilysvireshk
authored andcommitted
xtask: also generate EXAMPLES section
Include "Examples" section from READMEs if they exist into the generated manual page. Signed-off-by: Manos Pitsidianakis <[email protected]>
1 parent 72f811c commit f6f4a90

File tree

4 files changed

+80
-11
lines changed

4 files changed

+80
-11
lines changed

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xtask/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ publish = false
1313
clap = { version = "4.5", features = ["derive"], optional = true }
1414
clap_mangen = { version = "0.2.24", optional = true }
1515
toml = { version = "0.8.19", optional = true }
16+
markdown = { version = "=1.0.0-alpha.23", optional = true }
1617

1718
[build-dependencies]
1819

@@ -22,7 +23,7 @@ vhost-device-scmi = []
2223
vhost-device-sound = ["vhost-device-sound-alsa", "vhost-device-sound-pipewire"]
2324
vhost-device-sound-alsa = ["mangen"]
2425
vhost-device-sound-pipewire = ["mangen"]
25-
mangen = ["dep:clap_mangen", "dep:clap", "dep:toml"]
26+
mangen = ["dep:clap_mangen", "dep:clap", "dep:toml", "dep:markdown"]
2627

2728
[lints.rust]
2829
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(feature, values("alsa-backend", "pw-backend"))'] }

xtask/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This binary crate provides support for running useful tasks with `cargo xtask <.
66

77
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.
88

9+
Furthmore, if the `README.md` of a crate contains an `Examples` heading, it includes it in the manual page.
10+
911
```session
1012
$ cargo xtask mangen
1113
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s

xtask/src/main.rs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use clap::CommandFactory;
1515
#[cfg(feature = "mangen")]
1616
use clap_mangen::Man;
1717
#[cfg(feature = "mangen")]
18+
use markdown::{to_mdast, ParseOptions};
19+
#[cfg(feature = "mangen")]
1820
use toml::value::Table;
1921

2022
// Use vhost-device-sound's args module as our own using the #[path] attribute
@@ -63,7 +65,19 @@ fn print_help() {
6365
}
6466

6567
#[cfg(feature = "mangen")]
66-
fn mangen_for_crate<T: CommandFactory>(manifest: Table) -> Result<Vec<u8>, Box<dyn Error>> {
68+
fn mangen_for_crate<T: CommandFactory>(
69+
crate_dir: std::path::PathBuf,
70+
) -> Result<Vec<u8>, Box<dyn Error>> {
71+
let readme_md = std::fs::read_to_string(crate_dir.join("README.md"))?;
72+
let example_text = parse_examples_from_readme(readme_md).unwrap_or_default();
73+
let examples = if example_text.is_empty() {
74+
None
75+
} else {
76+
Some(example_text.trim())
77+
};
78+
let manifest = std::fs::read_to_string(crate_dir.join("Cargo.toml"))?;
79+
let manifest = manifest.as_str().parse::<Table>()?;
80+
6781
let name: &'static str = manifest["package"]["name"]
6882
.as_str()
6983
.unwrap()
@@ -94,6 +108,14 @@ fn mangen_for_crate<T: CommandFactory>(manifest: Table) -> Result<Vec<u8>, Box<d
94108
let man = Man::new(cmd);
95109
let mut buffer: Vec<u8> = Default::default();
96110
man.render(&mut buffer)?;
111+
if let Some(examples) = examples {
112+
let mut examples_section = clap_mangen::roff::Roff::new();
113+
examples_section.control("SH", ["EXAMPLES"]);
114+
for line in examples.lines() {
115+
examples_section.text(vec![line.into()]);
116+
}
117+
examples_section.to_writer(&mut buffer)?;
118+
}
97119
clap_mangen::roff::Roff::new()
98120
.control("SH", ["REPORTING BUGS"])
99121
.text(vec![format!(
@@ -105,6 +127,41 @@ fn mangen_for_crate<T: CommandFactory>(manifest: Table) -> Result<Vec<u8>, Box<d
105127
Ok(buffer)
106128
}
107129

130+
#[cfg(feature = "mangen")]
131+
fn parse_examples_from_readme(readme_md: String) -> Result<String, Box<dyn Error>> {
132+
use markdown::mdast;
133+
134+
let mdast = to_mdast(&readme_md, &ParseOptions::gfm()).map_err(|err| err.to_string())?;
135+
let mut example_text = String::new();
136+
if let mdast::Node::Root(root) = mdast {
137+
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"))){
138+
let mdast::Node::Heading(examples_heading) =
139+
&root.children[examples_index]
140+
else {
141+
// SAFETY: Unreachable because we found the exact position earlier.
142+
unreachable!();
143+
};
144+
let depth = examples_heading.depth;
145+
let mut i = examples_index + 1;
146+
while i < root.children.len() && !matches!(root.children[i], mdast::Node::Heading(ref h) if h.depth >= depth) {
147+
match &root.children[i] {
148+
mdast::Node::Paragraph(p) => {
149+
example_text.push_str(&p.children.iter().map(|t| t.to_string()).collect::<Vec<String>>().join(" "));
150+
example_text.push_str("\n\n");
151+
},
152+
mdast::Node::Code(c) => {
153+
example_text.push_str(&c.value);
154+
example_text.push_str("\n\n");
155+
},
156+
_ => {},
157+
}
158+
i += 1;
159+
}
160+
}
161+
}
162+
Ok(example_text)
163+
}
164+
108165
#[cfg(feature = "mangen")]
109166
fn mangen() -> Result<(), Box<dyn Error>> {
110167
let workspace_dir = std::path::Path::new(&env!("CARGO_MANIFEST_DIR"))
@@ -124,22 +181,15 @@ fn mangen() -> Result<(), Box<dyn Error>> {
124181
{
125182
use vhost_device_sound::SoundArgs;
126183

127-
let manifest =
128-
std::fs::read_to_string(workspace_dir.join("vhost-device-sound/Cargo.toml"))?;
129-
let manifest = manifest.as_str().parse::<Table>()?;
130-
131-
let buffer = mangen_for_crate::<SoundArgs>(manifest)?;
184+
let buffer = mangen_for_crate::<SoundArgs>(workspace_dir.join("vhost-device-sound"))?;
132185
let man_path = dist_dir.join("vhost-device-sound.1");
133186
buffers.push((man_path, buffer));
134187
}
135188
#[cfg(feature = "vhost-device-scmi")]
136189
{
137190
use vhost_device_scmi::ScmiArgs;
138191

139-
let manifest = std::fs::read_to_string(workspace_dir.join("vhost-device-scmi/Cargo.toml"))?;
140-
let manifest = manifest.as_str().parse::<Table>()?;
141-
142-
let buffer = mangen_for_crate::<ScmiArgs>(manifest)?;
192+
let buffer = mangen_for_crate::<ScmiArgs>(workspace_dir.join("vhost-device-scmi"))?;
143193
let man_path = dist_dir.join("vhost-device-scmi.1");
144194
buffers.push((man_path, buffer));
145195
}

0 commit comments

Comments
 (0)