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