cargo_readme/readme/
extract.rs1use std::io::{self, BufRead, BufReader, Read};
4
5pub fn extract_docs<R: Read>(reader: R) -> io::Result<Vec<String>> {
7 let mut reader = BufReader::new(reader);
8 let mut line = String::new();
9
10 while reader.read_line(&mut line)? > 0 {
11 if line.starts_with("//!") {
12 return extract_docs_singleline_style(line, reader);
13 }
14
15 if line.starts_with("/*!") {
16 return extract_docs_multiline_style(line, reader);
17 }
18
19 line.clear();
20 }
21
22 Ok(Vec::new())
23}
24
25fn extract_docs_singleline_style<R: Read>(
26 first_line: String,
27 reader: BufReader<R>,
28) -> io::Result<Vec<String>> {
29 let mut result = vec![normalize_line(first_line)];
30
31 for line in reader.lines() {
32 let line = line?;
33
34 if line.starts_with("//!") {
35 result.push(normalize_line(line));
36 } else if line.trim().len() > 0 {
37 break;
39 }
40 }
41
42 Ok(result)
43}
44
45fn extract_docs_multiline_style<R: Read>(
46 first_line: String,
47 reader: BufReader<R>,
48) -> io::Result<Vec<String>> {
49 let mut result = Vec::new();
50 if first_line.starts_with("/*!") && first_line.trim().len() > "/*!".len() {
51 result.push(normalize_line(first_line));
52 }
53
54 let mut nesting: isize = 0;
55
56 for line in reader.lines() {
57 let line = line?;
58 nesting += line.matches("/*").count() as isize;
59
60 if let Some(pos) = line.rfind("*/") {
61 nesting -= line.matches("*/").count() as isize;
62 if nesting < 0 {
63 let mut line = line;
64 let _ = line.split_off(pos);
65 if !line.trim().is_empty() {
66 result.push(line);
67 }
68 break;
69 }
70 }
71
72 result.push(line.trim_end().to_owned());
73 }
74
75 Ok(result)
76}
77
78fn normalize_line(mut line: String) -> String {
80 if line.trim() == "//!" || line.trim() == "/*!" {
81 line.clear();
82 line
83 } else {
84 let split_at = if line.find(" ") == Some(3) { 4 } else { 3 };
86 line.split_at(split_at).1.trim_end().to_owned()
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::io::Cursor;
94
95 const EXPECTED: &[&str] = &[
96 "first line",
97 "",
98 "```",
99 "let rust_code = \"safe\";",
100 "```",
101 "",
102 "```C",
103 "int i = 0; // no rust code",
104 "```",
105 ];
106
107 const INPUT_SINGLELINE: &str = "\
108 //! first line \n\
109 //! \n\
110 //! ``` \n\
111 //! let rust_code = \"safe\"; \n\
112 //! ``` \n\
113 //! \n\
114 //! ```C \n\
115 //! int i = 0; // no rust code \n\
116 //! ``` \n\
117 use std::any::Any; \n\
118 fn main() {}";
119
120 #[test]
121 fn extract_docs_singleline_style() {
122 let reader = Cursor::new(INPUT_SINGLELINE.as_bytes());
123 let result = extract_docs(reader).unwrap();
124 assert_eq!(result, EXPECTED);
125 }
126
127 const INPUT_MULTILINE: &str = "\
128 /*! \n\
129 first line \n\
130 \n\
131 ``` \n\
132 let rust_code = \"safe\"; \n\
133 ``` \n\
134 \n\
135 ```C \n\
136 int i = 0; // no rust code \n\
137 ``` \n\
138 */ \n\
139 use std::any::Any; \n\
140 fn main() {}";
141
142 #[test]
143 fn extract_docs_multiline_style() {
144 let reader = Cursor::new(INPUT_MULTILINE.as_bytes());
145 let result = extract_docs(reader).unwrap();
146 assert_eq!(result, EXPECTED);
147 }
148
149 const INPUT_MIXED_SINGLELINE: &str = "\
150 //! singleline \n\
151 /*! \n\
152 multiline \n\
153 */";
154
155 #[test]
156 fn extract_docs_mix_styles_singleline() {
157 let input = Cursor::new(INPUT_MIXED_SINGLELINE.as_bytes());
158 let expected = "singleline";
159 let result = extract_docs(input).unwrap();
160 assert_eq!(result, &[expected])
161 }
162
163 const INPUT_MIXED_MULTILINE: &str = "\
164 /*! \n\
165 multiline \n\
166 */ \n\
167 //! singleline";
168
169 #[test]
170 fn extract_docs_mix_styles_multiline() {
171 let input = Cursor::new(INPUT_MIXED_MULTILINE.as_bytes());
172 let expected = "multiline";
173 let result = extract_docs(input).unwrap();
174 assert_eq!(result, &[expected]);
175 }
176
177 const INPUT_MULTILINE_NESTED_1: &str = "\
178 /*! \n\
179 level 0 \n\
180 /* \n\
181 level 1 \n\
182 */ \n\
183 level 0 \n\
184 */ \n\
185 fn main() {}";
186
187 const EXPECTED_MULTILINE_NESTED_1: &[&str] = &["level 0", "/*", "level 1", "*/", "level 0"];
188
189 #[test]
190 fn extract_docs_nested_level_1() {
191 let input = Cursor::new(INPUT_MULTILINE_NESTED_1.as_bytes());
192 let result = extract_docs(input).unwrap();
193 assert_eq!(result, EXPECTED_MULTILINE_NESTED_1);
194 }
195
196 const INPUT_MULTILINE_NESTED_2: &str = "\
197 /*! \n\
198 level 0 \n\
199 /* \n\
200 level 1 \n\
201 /* \n\
202 level 2 \n\
203 */ \n\
204 level 1 \n\
205 */ \n\
206 level 0 \n\
207 */ \n\
208 fn main() {}";
209
210 const EXPECTED_MULTILINE_NESTED_2: &[&str] = &[
211 "level 0", "/*", "level 1", "/*", "level 2", "*/", "level 1", "*/", "level 0",
212 ];
213
214 #[test]
215 fn extract_docs_nested_level_2() {
216 let input = Cursor::new(INPUT_MULTILINE_NESTED_2.as_bytes());
217 let result = extract_docs(input).unwrap();
218 assert_eq!(result, EXPECTED_MULTILINE_NESTED_2);
219 }
220}