1//! Provides conversion from rustdoc's HTML output to Markdown.
2
3#![deny(missing_docs)]
4
5mod markdown_writer;
6
7use std::io::Read;
8
9use anyhow::{Context, Result};
10use html5ever::driver::ParseOpts;
11use html5ever::parse_document;
12use html5ever::tendril::TendrilSink;
13use html5ever::tree_builder::TreeBuilderOpts;
14use markup5ever_rcdom::RcDom;
15
16use crate::markdown_writer::MarkdownWriter;
17
18/// Converts the provided rustdoc HTML to Markdown.
19pub fn convert_rustdoc_to_markdown(mut html: impl Read) -> Result<String> {
20 let parse_options = ParseOpts {
21 tree_builder: TreeBuilderOpts {
22 drop_doctype: true,
23 ..Default::default()
24 },
25 ..Default::default()
26 };
27 let dom = parse_document(RcDom::default(), parse_options)
28 .from_utf8()
29 .read_from(&mut html)
30 .context("failed to parse rustdoc HTML")?;
31
32 let markdown_writer = MarkdownWriter::new();
33 let markdown = markdown_writer
34 .run(&dom.document)
35 .context("failed to convert rustdoc to HTML")?;
36
37 Ok(markdown)
38}
39
40#[cfg(test)]
41mod tests {
42 use indoc::indoc;
43 use pretty_assertions::assert_eq;
44
45 use super::*;
46
47 #[test]
48 fn test_rust_code_block() {
49 let html = indoc! {r#"
50 <pre class="rust rust-example-rendered"><code><span class="kw">use </span>axum::extract::{Path, Query, Json};
51 <span class="kw">use </span>std::collections::HashMap;
52
53 <span class="comment">// `Path` gives you the path parameters and deserializes them.
54 </span><span class="kw">async fn </span>path(Path(user_id): Path<u32>) {}
55
56 <span class="comment">// `Query` gives you the query parameters and deserializes them.
57 </span><span class="kw">async fn </span>query(Query(params): Query<HashMap<String, String>>) {}
58
59 <span class="comment">// Buffer the request body and deserialize it as JSON into a
60 // `serde_json::Value`. `Json` supports any type that implements
61 // `serde::Deserialize`.
62 </span><span class="kw">async fn </span>json(Json(payload): Json<serde_json::Value>) {}</code></pre>
63 "#};
64 let expected = indoc! {"
65 ```rs
66 use axum::extract::{Path, Query, Json};
67 use std::collections::HashMap;
68
69 // `Path` gives you the path parameters and deserializes them.
70 async fn path(Path(user_id): Path<u32>) {}
71
72 // `Query` gives you the query parameters and deserializes them.
73 async fn query(Query(params): Query<HashMap<String, String>>) {}
74
75 // Buffer the request body and deserialize it as JSON into a
76 // `serde_json::Value`. `Json` supports any type that implements
77 // `serde::Deserialize`.
78 async fn json(Json(payload): Json<serde_json::Value>) {}
79 ```
80 "}
81 .trim();
82
83 assert_eq!(
84 convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
85 expected
86 )
87 }
88
89 #[test]
90 fn test_toml_code_block() {
91 let html = indoc! {r##"
92 <h2 id="required-dependencies"><a class="doc-anchor" href="#required-dependencies">ยง</a>Required dependencies</h2>
93 <p>To use axum there are a few dependencies you have to pull in as well:</p>
94 <div class="example-wrap"><pre class="language-toml"><code>[dependencies]
95 axum = "<latest-version>"
96 tokio = { version = "<latest-version>", features = ["full"] }
97 tower = "<latest-version>"
98 </code></pre></div>
99 "##};
100 let expected = indoc! {r#"
101 ## Required dependencies
102
103 To use axum there are a few dependencies you have to pull in as well:
104
105 ```toml
106 [dependencies]
107 axum = "<latest-version>"
108 tokio = { version = "<latest-version>", features = ["full"] }
109 tower = "<latest-version>"
110
111 ```
112 "#}
113 .trim();
114
115 assert_eq!(
116 convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
117 expected
118 )
119 }
120}