cargo_rdme/
extract_doc.rs1use 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 _ => {
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}