Skip to main content

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            && 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 = string.strip_prefix(' ').map(ToOwned::to_owned).unwrap_or(string);
43                    lines.push(line);
44                }
45
46                // Multiline comment.
47                _ => {
48                    fn empty_line(str: &str) -> bool {
49                        str.chars().all(char::is_whitespace)
50                    }
51
52                    let comment_lines = string
53                        .lines()
54                        .enumerate()
55                        .filter(|(i, l)| !(*i == 0 && empty_line(l)))
56                        .map(|(_, l)| l.to_owned());
57
58                    lines.extend(comment_lines);
59                }
60            }
61        }
62    }
63
64    match lines.is_empty() {
65        true => Ok(None),
66        false => Ok(Some(Doc { markdown: Markdown::from_lines(&lines) })),
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use indoc::indoc;
74    use pretty_assertions::assert_eq;
75
76    #[test]
77    fn test_doc_from_source_str_no_doc() {
78        let str = indoc! { r#"
79            use std::fs;
80
81            struct Nothing {}
82            "#
83        };
84
85        assert!(extract_doc_from_source_str(str).unwrap().is_none());
86    }
87
88    #[test]
89    fn test_doc_from_source_str_single_line_comment() {
90        let str = indoc! { r#"
91            #![cfg_attr(not(feature = "std"), no_std)]
92            // normal comment
93
94            //! This is the doc for the crate.
95            //!This line doesn't start with space.
96            //!
97            //! And a nice empty line above us.
98            //! Also a line ending in "
99
100            struct Nothing {}
101            "#
102        };
103
104        let doc = extract_doc_from_source_str(str).unwrap().unwrap();
105        let lines: Vec<&str> = doc.lines().collect();
106
107        let expected = vec![
108            "This is the doc for the crate.",
109            "This line doesn't start with space.",
110            "",
111            "And a nice empty line above us.",
112            "Also a line ending in \"",
113        ];
114
115        assert_eq!(lines, expected);
116    }
117
118    #[test]
119    fn test_doc_from_source_str_multi_line_comment() {
120        let str = indoc! { r#"
121            #![cfg_attr(not(feature = "std"), no_std)]
122            /* normal comment */
123
124            /*!
125            This is the doc for the crate.
126             This line start with space.
127
128            And a nice empty line above us.
129            */
130
131            struct Nothing {}
132            "#
133        };
134
135        let doc = extract_doc_from_source_str(str).unwrap().unwrap();
136        let lines: Vec<&str> = doc.lines().collect();
137
138        let expected = vec![
139            "This is the doc for the crate.",
140            " This line start with space.",
141            "",
142            "And a nice empty line above us.",
143        ];
144
145        assert_eq!(lines, expected);
146    }
147
148    #[test]
149    fn test_doc_from_source_str_single_line_keep_indentation() {
150        let str = indoc! { r#"
151            #![cfg_attr(not(feature = "std"), no_std)]
152            // normal comment
153
154            //! This is the doc for the crate.  This crate does:
155            //!
156            //!   1. nothing.
157            //!   2. niente.
158
159            struct Nothing {}
160            "#
161        };
162
163        let doc = extract_doc_from_source_str(str).unwrap().unwrap();
164        let lines: Vec<&str> = doc.lines().collect();
165
166        let expected = vec![
167            "This is the doc for the crate.  This crate does:",
168            "",
169            "  1. nothing.",
170            "  2. niente.",
171        ];
172
173        assert_eq!(lines, expected);
174    }
175}