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