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