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 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 _ => {
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}