rustdoc_to_markdown/
lib.rs

1//! Transform code blocks from rustdoc into markdown
2//!
3//! Rewrite code block start tags, changing rustdoc into equivalent in markdown:
4//! - "```", "```no_run", "```ignore" and "```should_panic" are converted to "```rust"
5//! - markdown heading are indentend to be one level lower, so the crate name is at the top level
6//!
7//! Code taken almost verbatim from <https://github.com/livioribeiro/cargo-readme/blob/a7fd456433832f873cee890a9e67ece9929fc795/src/readme/process.rs>.
8
9use regex::Regex;
10use std::iter::{IntoIterator, Iterator};
11
12lazy_static::lazy_static! {
13    // Is this code block rust?
14    static ref RE_CODE_RUST: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})(?:rust|(?:(?:rust,)?(?:no_run|ignore|should_panic)))?$").unwrap();
15    // Is this code block just text?
16    static ref RE_CODE_TEXT: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})text$").unwrap();
17    // Is this code block a language other than rust?
18    static ref RE_CODE_OTHER: Regex = Regex::new(r"^(?P<delimiter>`{3,4}|~{3,4})\w[\w,\+]*$").unwrap();
19}
20
21/// Process and concatenate the doc lines into a single String
22///
23/// The processing transforms doc tests into regular rust code blocks and optionally indent the
24/// markdown headings in order to leave the top heading to the crate name
25pub fn process_docs<S: Into<String>, L: Into<Vec<S>>>(
26    lines: L,
27    indent_headings: bool,
28) -> Vec<String> {
29    lines.into().into_iter().process_docs(indent_headings)
30}
31
32pub struct Processor {
33    section: Section,
34    indent_headings: bool,
35    delimiter: Option<String>,
36}
37
38impl Processor {
39    pub fn new(indent_headings: bool) -> Self {
40        Processor {
41            section: Section::None,
42            indent_headings,
43            delimiter: None,
44        }
45    }
46
47    pub fn process_line(&mut self, mut line: String) -> Option<String> {
48        // Skip lines that should be hidden in docs
49        if self.section == Section::CodeRust && line.starts_with("# ") {
50            return None;
51        }
52
53        // indent heading when outside code
54        if self.indent_headings && self.section == Section::None && line.starts_with('#') {
55            line.insert(0, '#');
56        } else if self.section == Section::None {
57            let l = line.clone();
58            if let Some(cap) = RE_CODE_RUST.captures(&l) {
59                self.section = Section::CodeRust;
60                self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
61                line = format!("{}rust", self.delimiter.as_ref().unwrap());
62            } else if let Some(cap) = RE_CODE_TEXT.captures(&l) {
63                self.section = Section::CodeOther;
64                self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
65                line = self.delimiter.clone().unwrap();
66            } else if let Some(cap) = RE_CODE_OTHER.captures(&l) {
67                self.section = Section::CodeOther;
68                self.delimiter = cap.name("delimiter").map(|x| x.as_str().to_owned());
69            }
70        } else if self.section != Section::None && Some(&line) == self.delimiter.as_ref() {
71            self.section = Section::None;
72            line = self.delimiter.take().unwrap_or_else(|| "```".to_owned());
73        }
74
75        Some(line)
76    }
77}
78
79#[derive(PartialEq)]
80enum Section {
81    CodeRust,
82    CodeOther,
83    None,
84}
85
86pub trait DocProcess<S: Into<String>> {
87    fn process_docs(self, indent_headings: bool) -> Vec<String>
88    where
89        Self: Sized + Iterator<Item = S>,
90    {
91        let mut p = Processor::new(indent_headings);
92        self.into_iter()
93            .filter_map(|line| p.process_line(line.into()))
94            .collect()
95    }
96}
97
98impl<S: Into<String>, I: Iterator<Item = S>> DocProcess<S> for I {}
99
100#[cfg(test)]
101mod tests {
102    use super::process_docs;
103
104    const INPUT_HIDDEN_LINE: &[&str] = &[
105        "```",
106        "#[visible]",
107        "let visible = \"visible\";",
108        "# let hidden = \"hidden\";",
109        "```",
110    ];
111
112    const EXPECTED_HIDDEN_LINE: &[&str] =
113        &["```rust", "#[visible]", "let visible = \"visible\";", "```"];
114
115    #[test]
116    fn hide_line_in_rust_code_block() {
117        let result = process_docs(INPUT_HIDDEN_LINE, true);
118        assert_eq!(result, EXPECTED_HIDDEN_LINE);
119    }
120
121    const INPUT_NOT_HIDDEN_LINE: &[&str] = &[
122        "```",
123        "let visible = \"visible\";",
124        "# let hidden = \"hidden\";",
125        "```",
126        "",
127        "```python",
128        "# this line is visible",
129        "visible = True",
130        "```",
131    ];
132
133    const EXPECTED_NOT_HIDDEN_LINE: &[&str] = &[
134        "```rust",
135        "let visible = \"visible\";",
136        "```",
137        "",
138        "```python",
139        "# this line is visible",
140        "visible = True",
141        "```",
142    ];
143
144    #[test]
145    fn do_not_hide_line_in_code_block() {
146        let result = process_docs(INPUT_NOT_HIDDEN_LINE, true);
147        assert_eq!(result, EXPECTED_NOT_HIDDEN_LINE);
148    }
149
150    const INPUT_RUST_CODE_BLOCK: &[&str] = &[
151        "```",
152        "let block = \"simple code block\";",
153        "```",
154        "",
155        "```no_run",
156        "let run = false;",
157        "```",
158        "",
159        "```ignore",
160        "let ignore = true;",
161        "```",
162        "",
163        "```should_panic",
164        "panic!(\"at the disco\");",
165        "```",
166        "",
167        "```C",
168        "int i = 0; // no rust code",
169        "```",
170    ];
171
172    const EXPECTED_RUST_CODE_BLOCK: &[&str] = &[
173        "```rust",
174        "let block = \"simple code block\";",
175        "```",
176        "",
177        "```rust",
178        "let run = false;",
179        "```",
180        "",
181        "```rust",
182        "let ignore = true;",
183        "```",
184        "",
185        "```rust",
186        "panic!(\"at the disco\");",
187        "```",
188        "",
189        "```C",
190        "int i = 0; // no rust code",
191        "```",
192    ];
193
194    #[test]
195    fn transform_rust_code_block() {
196        let result = process_docs(INPUT_RUST_CODE_BLOCK, true);
197        assert_eq!(result, EXPECTED_RUST_CODE_BLOCK);
198    }
199
200    const INPUT_RUST_CODE_BLOCK_RUST_PREFIX: &[&str] = &[
201        "```rust",
202        "let block = \"simple code block\";",
203        "```",
204        "",
205        "```rust,no_run",
206        "let run = false;",
207        "```",
208        "",
209        "```rust,ignore",
210        "let ignore = true;",
211        "```",
212        "",
213        "```rust,should_panic",
214        "panic!(\"at the disco\");",
215        "```",
216        "",
217        "```C",
218        "int i = 0; // no rust code",
219        "```",
220    ];
221
222    #[test]
223    fn transform_rust_code_block_with_prefix() {
224        let result = process_docs(INPUT_RUST_CODE_BLOCK_RUST_PREFIX, true);
225        assert_eq!(result, EXPECTED_RUST_CODE_BLOCK);
226    }
227
228    const INPUT_TEXT_BLOCK: &[&str] = &["```text", "this is text", "```"];
229
230    const EXPECTED_TEXT_BLOCK: &[&str] = &["```", "this is text", "```"];
231
232    #[test]
233    fn transform_text_block() {
234        let result = process_docs(INPUT_TEXT_BLOCK, true);
235        assert_eq!(result, EXPECTED_TEXT_BLOCK);
236    }
237
238    const INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS: &[&str] = &[
239        "```html,django",
240        "{% if True %}True{% endif %}",
241        "```",
242        "",
243        "```html+django",
244        "{% if True %}True{% endif %}",
245        "```",
246    ];
247
248    #[test]
249    fn transform_other_code_block_with_symbols() {
250        let result = process_docs(INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS, true);
251        assert_eq!(result, INPUT_OTHER_CODE_BLOCK_WITH_SYMBOLS);
252    }
253
254    const INPUT_INDENT_HEADINGS: &[&str] = &[
255        "# heading 1",
256        "some text",
257        "## heading 2",
258        "some other text",
259    ];
260
261    const EXPECTED_INDENT_HEADINGS: &[&str] = &[
262        "## heading 1",
263        "some text",
264        "### heading 2",
265        "some other text",
266    ];
267
268    #[test]
269    fn indent_markdown_headings() {
270        let result = process_docs(INPUT_INDENT_HEADINGS, true);
271        assert_eq!(result, EXPECTED_INDENT_HEADINGS);
272    }
273
274    #[test]
275    fn do_not_indent_markdown_headings() {
276        let result = process_docs(INPUT_INDENT_HEADINGS, false);
277        assert_eq!(result, INPUT_INDENT_HEADINGS);
278    }
279
280    const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````", "let i = 1;", "````"];
281
282    const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS: &[&str] = &["````rust", "let i = 1;", "````"];
283
284    #[test]
285    fn alternate_delimiter_4_backticks() {
286        let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS, false);
287        assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS);
288    }
289
290    const INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[
291        "````",
292        "//! ```",
293        "//! let i = 1;",
294        "//! ```",
295        "```python",
296        "i = 1",
297        "```",
298        "````",
299    ];
300
301    const EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED: &[&str] = &[
302        "````rust",
303        "//! ```",
304        "//! let i = 1;",
305        "//! ```",
306        "```python",
307        "i = 1",
308        "```",
309        "````",
310    ];
311
312    #[test]
313    fn alternate_delimiter_4_backticks_nested() {
314        let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED, false);
315        assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_BACKTICKS_NESTED);
316    }
317
318    const INPUT_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~", "let i = 1;", "~~~"];
319
320    const EXPECTED_ALTERNATE_DELIMITER_3_TILDES: &[&str] = &["~~~rust", "let i = 1;", "~~~"];
321
322    #[test]
323    fn alternate_delimiter_3_tildes() {
324        let result = process_docs(INPUT_ALTERNATE_DELIMITER_3_TILDES, false);
325        assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_3_TILDES);
326    }
327
328    const INPUT_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~", "let i = 1;", "~~~~"];
329
330    const EXPECTED_ALTERNATE_DELIMITER_4_TILDES: &[&str] = &["~~~~rust", "let i = 1;", "~~~~"];
331
332    #[test]
333    fn alternate_delimiter_4_tildes() {
334        let result = process_docs(INPUT_ALTERNATE_DELIMITER_4_TILDES, false);
335        assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_4_TILDES);
336    }
337
338    const INPUT_ALTERNATE_DELIMITER_MIXED: &[&str] = &[
339        "```",
340        "let i = 1;",
341        "```",
342        "````",
343        "//! ```",
344        "//! let i = 1;",
345        "//! ```",
346        "```python",
347        "i = 1",
348        "```",
349        "````",
350        "~~~markdown",
351        "```python",
352        "i = 1",
353        "```",
354        "~~~",
355    ];
356
357    const EXPECTED_ALTERNATE_DELIMITER_MIXED: &[&str] = &[
358        "```rust",
359        "let i = 1;",
360        "```",
361        "````rust",
362        "//! ```",
363        "//! let i = 1;",
364        "//! ```",
365        "```python",
366        "i = 1",
367        "```",
368        "````",
369        "~~~markdown",
370        "```python",
371        "i = 1",
372        "```",
373        "~~~",
374    ];
375
376    #[test]
377    fn alternate_delimiter_mixed() {
378        let result = process_docs(INPUT_ALTERNATE_DELIMITER_MIXED, false);
379        assert_eq!(result, EXPECTED_ALTERNATE_DELIMITER_MIXED);
380    }
381}