rustdoc_to_markdown.rs

  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&lt;u32&gt;) {}
 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&lt;HashMap&lt;String, String&gt;&gt;) {}
 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&lt;serde_json::Value&gt;) {}</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 = &quot;&lt;latest-version&gt;&quot;
 96            tokio = { version = &quot;&lt;latest-version&gt;&quot;, features = [&quot;full&quot;] }
 97            tower = &quot;&lt;latest-version&gt;&quot;
 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}