cargo_readme/readme/
extract.rs

1//! Extract raw doc comments from rust source code
2
3use std::io::{self, BufRead, BufReader, Read};
4
5/// Read the given `Read`er and return a `Vec` of the rustdoc lines found
6pub 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            // doc ends, code starts
38            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
78/// Strip the "//!" or "/*!" from a line and a single whitespace
79fn normalize_line(mut line: String) -> String {
80    if line.trim() == "//!" || line.trim() == "/*!" {
81        line.clear();
82        line
83    } else {
84        // if the first character after the comment mark is " ", remove it
85        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}