markdown_includes/rustdoc_parse/
extract_doc.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 */
5
6use super::Doc;
7use anyhow::Context;
8use fs_err as fs;
9use std::path::Path;
10
11pub fn extract_doc_from_source_file(file_path: impl AsRef<Path>) -> anyhow::Result<Option<Doc>> {
12    let source: String = fs::read_to_string(file_path.as_ref())
13        .context(format!("cannot open source file {:?}", file_path.as_ref()))?;
14
15    extract_doc_from_source_str(&source)
16}
17
18pub fn extract_doc_from_source_str(source: &str) -> anyhow::Result<Option<Doc>> {
19    use syn::{parse_str, Lit, Meta, MetaNameValue};
20
21    let ast: syn::File = parse_str(source).context("cannot parse source file")?;
22    let mut lines: Vec<String> = Vec::with_capacity(1024);
23
24    for attr in &ast.attrs {
25        if Doc::is_toplevel_doc(attr) {
26            if let Ok(Meta::NameValue(MetaNameValue {
27                lit: Lit::Str(lstr),
28                ..
29            })) = attr.parse_meta()
30            {
31                let string = &lstr.value();
32
33                match string.lines().count() {
34                    0 => lines.push(String::new()),
35                    1 => {
36                        let line = string.strip_prefix(' ').unwrap_or(string);
37                        lines.push(line.to_owned());
38                    }
39
40                    // Multiline comment.
41                    _ => {
42                        fn empty_line(str: &str) -> bool {
43                            str.chars().all(char::is_whitespace)
44                        }
45
46                        let x = string
47                            .lines()
48                            .enumerate()
49                            .filter(|(i, l)| !(*i == 0 && empty_line(l)))
50                            .map(|(_, l)| l);
51
52                        lines.extend(x.map(ToOwned::to_owned));
53                    }
54                }
55            }
56        }
57    }
58
59    match lines.is_empty() {
60        true => Ok(None),
61        false => Ok(Some(Doc {
62            content: lines.join("\n"),
63        })),
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use indoc::indoc;
71    use pretty_assertions::assert_eq;
72
73    #[test]
74    fn test_doc_from_source_str_no_doc() {
75        let str = indoc! { r#"
76            use std::fs;
77
78            struct Nothing {}
79            "#
80        };
81
82        assert!(extract_doc_from_source_str(str).unwrap().is_none());
83    }
84
85    #[test]
86    fn test_doc_from_source_str_single_line_comment() {
87        let str = indoc! { r#"
88            #![cfg_attr(not(feature = "std"), no_std)]
89            // normal comment
90
91            //! This is the doc for the crate.
92            //!This line doesn't start with space.
93            //!
94            //! And a nice empty line above us.
95            //! Also a line ending in "
96
97            struct Nothing {}
98            "#
99        };
100
101        let doc = extract_doc_from_source_str(str).unwrap().unwrap();
102        let lines: Vec<&str> = doc.content.lines().collect();
103
104        let expected = vec![
105            "This is the doc for the crate.",
106            "This line doesn't start with space.",
107            "",
108            "And a nice empty line above us.",
109            "Also a line ending in \"",
110        ];
111
112        assert_eq!(lines, expected);
113    }
114
115    #[test]
116    fn test_doc_from_source_str_multi_line_comment() {
117        let str = indoc! { r#"
118            #![cfg_attr(not(feature = "std"), no_std)]
119            /* normal comment */
120
121            /*!
122            This is the doc for the crate.
123             This line start with space.
124
125            And a nice empty line above us.
126            */
127
128            struct Nothing {}
129            "#
130        };
131
132        let doc = extract_doc_from_source_str(str).unwrap().unwrap();
133        let lines: Vec<&str> = doc.content.lines().collect();
134
135        let expected = vec![
136            "This is the doc for the crate.",
137            " This line start with space.",
138            "",
139            "And a nice empty line above us.",
140        ];
141
142        assert_eq!(lines, expected);
143    }
144
145    #[test]
146    fn test_doc_from_source_str_single_line_keep_indentation() {
147        let str = indoc! { r#"
148            #![cfg_attr(not(feature = "std"), no_std)]
149            // normal comment
150
151            //! This is the doc for the crate.  This crate does:
152            //!
153            //!   1. nothing.
154            //!   2. niente.
155
156            struct Nothing {}
157            "#
158        };
159
160        let doc = extract_doc_from_source_str(str).unwrap().unwrap();
161        let lines: Vec<&str> = doc.content.lines().collect();
162
163        let expected = vec![
164            "This is the doc for the crate.  This crate does:",
165            "",
166            "  1. nothing.",
167            "  2. niente.",
168        ];
169
170        assert_eq!(lines, expected);
171    }
172}