cargo_rdme/
extract_doc.rs

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