1 //! Utilities for manipulating C/C++ comments.
2 
3 use std::iter;
4 
5 /// The type of a comment.
6 #[derive(Debug, PartialEq, Eq)]
7 enum Kind {
8     /// A `///` comment, or something of the like.
9     /// All lines in a comment should start with the same symbol.
10     SingleLines,
11     /// A `/**` comment, where each other line can start with `*` and the
12     /// entire block ends with `*/`.
13     MultiLine,
14 }
15 
16 /// Preprocesses a C/C++ comment so that it is a valid Rust comment.
preprocess(comment: &str, indent: usize) -> String17 pub fn preprocess(comment: &str, indent: usize) -> String {
18     match self::kind(&comment) {
19         Some(Kind::SingleLines) => preprocess_single_lines(comment, indent),
20         Some(Kind::MultiLine) => preprocess_multi_line(comment, indent),
21         None => comment.to_owned(),
22     }
23 }
24 
25 /// Gets the kind of the doc comment, if it is one.
kind(comment: &str) -> Option<Kind>26 fn kind(comment: &str) -> Option<Kind> {
27     if comment.starts_with("/*") {
28         Some(Kind::MultiLine)
29     } else if comment.starts_with("//") {
30         Some(Kind::SingleLines)
31     } else {
32         None
33     }
34 }
35 
make_indent(indent: usize) -> String36 fn make_indent(indent: usize) -> String {
37     const RUST_INDENTATION: usize = 4;
38     iter::repeat(' ').take(indent * RUST_INDENTATION).collect()
39 }
40 
41 /// Preprocesses multiple single line comments.
42 ///
43 /// Handles lines starting with both `//` and `///`.
preprocess_single_lines(comment: &str, indent: usize) -> String44 fn preprocess_single_lines(comment: &str, indent: usize) -> String {
45     debug_assert!(comment.starts_with("//"), "comment is not single line");
46 
47     let indent = make_indent(indent);
48     let mut is_first = true;
49     let lines: Vec<_> = comment
50         .lines()
51         .map(|l| l.trim().trim_start_matches('/'))
52         .map(|l| {
53             let indent = if is_first { "" } else { &*indent };
54             is_first = false;
55             format!("{}///{}", indent, l)
56         })
57         .collect();
58     lines.join("\n")
59 }
60 
preprocess_multi_line(comment: &str, indent: usize) -> String61 fn preprocess_multi_line(comment: &str, indent: usize) -> String {
62     let comment = comment
63         .trim_start_matches('/')
64         .trim_end_matches('/')
65         .trim_end_matches('*');
66 
67     let indent = make_indent(indent);
68     // Strip any potential `*` characters preceding each line.
69     let mut is_first = true;
70     let mut lines: Vec<_> = comment
71         .lines()
72         .map(|line| line.trim().trim_start_matches('*').trim_start_matches('!'))
73         .skip_while(|line| line.trim().is_empty()) // Skip the first empty lines.
74         .map(|line| {
75             let indent = if is_first { "" } else { &*indent };
76             is_first = false;
77             format!("{}///{}", indent, line)
78         })
79         .collect();
80 
81     // Remove the trailing line corresponding to the `*/`.
82     if lines
83         .last()
84         .map_or(false, |l| l.trim().is_empty() || l.trim() == "///")
85     {
86         lines.pop();
87     }
88 
89     lines.join("\n")
90 }
91 
92 #[cfg(test)]
93 mod test {
94     use super::*;
95 
96     #[test]
picks_up_single_and_multi_line_doc_comments()97     fn picks_up_single_and_multi_line_doc_comments() {
98         assert_eq!(kind("/// hello"), Some(Kind::SingleLines));
99         assert_eq!(kind("/** world */"), Some(Kind::MultiLine));
100     }
101 
102     #[test]
processes_single_lines_correctly()103     fn processes_single_lines_correctly() {
104         assert_eq!(preprocess("/// hello", 0), "/// hello");
105         assert_eq!(preprocess("// hello", 0), "/// hello");
106         assert_eq!(preprocess("//    hello", 0), "///    hello");
107     }
108 
109     #[test]
processes_multi_lines_correctly()110     fn processes_multi_lines_correctly() {
111         assert_eq!(
112             preprocess("/** hello \n * world \n * foo \n */", 0),
113             "/// hello\n/// world\n/// foo"
114         );
115 
116         assert_eq!(
117             preprocess("/**\nhello\n*world\n*foo\n*/", 0),
118             "///hello\n///world\n///foo"
119         );
120     }
121 }
122