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