opencv_binding_generator/
comment.rs

1use crate::string_ext::Indent;
2use crate::StrExt;
3
4pub fn strip_doxygen_comment_markers(comment: &str) -> String {
5	// todo, simplify/optimize this function, spec is here https://www.doxygen.nl/manual/docblocks.html
6	const MULTILINE_PREFIX: &str = "/*";
7	const MULTILINE_CONT: &str = "*";
8	const MULTILINE_SUFFIX: &str = "*/";
9	const SINGLELINE: &str = "//";
10	const DETAIL: &str = "!";
11	const SINGLELINE_DETAIL: &str = "/";
12	const SINGLELINE_SIDE: &str = "<";
13
14	fn trim_last_empty_lines(lines: &mut Vec<&str>) {
15		while lines.last().is_some_and(|line| line.is_empty()) {
16			lines.pop();
17		}
18	}
19
20	let mut comment_type = CommentType::SingleLineDelimited;
21	let mut lines = Vec::with_capacity(128);
22	// first pass:
23	// 1. checks whether the comment is single-line or multi-line delimited
24	// 2. checks whether multiline comment is asterisk prefixed
25	// 3. strips comment delimiters for multiline and single line comments
26	// 4. collects resulting stripped lines into `lines` Vec
27	for (i, mut line) in comment.lines_with_nl().enumerate() {
28		let mut line_trimmed = line.trim_start();
29		if i == 0 {
30			if let Some(new_line) = line_trimmed.strip_prefix(MULTILINE_PREFIX) {
31				line = new_line;
32				if let Some(new_line) = line.strip_prefix(MULTILINE_CONT) {
33					line = new_line;
34					comment_type = CommentType::MultilineAsteriskPrefixed;
35				} else {
36					comment_type = CommentType::MultilineWithoutAsteriskPrefix;
37				}
38				line_trimmed = line.trim_start();
39			}
40		} else if let Some(line_clean) = line_trimmed.strip_prefix(MULTILINE_PREFIX) {
41			line = line_clean
42				.trim_start_matches(DETAIL)
43				.trim_start_matches(MULTILINE_CONT)
44				.trim_start();
45		}
46		if comment_type == CommentType::SingleLineDelimited {
47			if let Some(new_line) = line_trimmed.strip_prefix(SINGLELINE) {
48				line = new_line
49					.strip_prefix(SINGLELINE_DETAIL)
50					.or_else(|| new_line.strip_prefix(DETAIL))
51					.unwrap_or(new_line);
52				line = line.strip_prefix(SINGLELINE_SIDE).unwrap_or(line);
53			}
54		} else if i == 1 && comment_type == CommentType::MultilineAsteriskPrefixed && !line_trimmed.starts_with(MULTILINE_CONT) {
55			comment_type = CommentType::MultilineWithoutAsteriskPrefix;
56		}
57		lines.push(line);
58	}
59	trim_last_empty_lines(&mut lines);
60	// trim ending multiline delimiter
61	if let Some(last_line) = lines.last_mut() {
62		if comment_type != CommentType::SingleLineDelimited {
63			*last_line = last_line.trim_end();
64			if let Some(new_line) = last_line.strip_suffix(MULTILINE_SUFFIX) {
65				*last_line = new_line.trim_end();
66			}
67		}
68	}
69	trim_last_empty_lines(&mut lines);
70	// second pass:
71	// 1. calculates common indent
72	// 2. for multiline asterisk prefixed comments, strips this prefix modifying `lines`
73	let mut first_line_indent = None;
74	let mut common_indent: Option<Indent> = None;
75	for line in &mut lines {
76		if comment_type == CommentType::MultilineAsteriskPrefixed {
77			let line_trimmed = line.trim_start();
78			if let Some(line_trimmed) = line_trimmed.strip_prefix(MULTILINE_CONT) {
79				*line = line_trimmed;
80			} else {
81				let trim_start = line.trim_start_idx().min(2);
82				*line = &line[trim_start..];
83			}
84		}
85		if first_line_indent.is_none() {
86			first_line_indent = Some(line.detect_indent());
87		} else {
88			let detected_indent = line.detect_indent();
89			if !line[detected_indent.len..].trim_start().is_empty() {
90				if let Some(common_indent) = common_indent.as_mut() {
91					*common_indent = (*common_indent).min(detected_indent);
92				} else {
93					common_indent = Some(line.detect_indent());
94				}
95			}
96		}
97	}
98	let mut out = String::with_capacity(comment.len());
99	for (i, mut line) in lines.into_iter().enumerate() {
100		if i == 0 {
101			line = &line[first_line_indent.unwrap_or_default().len..];
102			if line.trim().is_empty() {
103				continue;
104			}
105		} else {
106			let indent_len = common_indent.unwrap_or_default().len;
107			if line.len() > indent_len {
108				line = &line[indent_len..];
109			} else {
110				line = "\n";
111			}
112		}
113		let line_clean_end = line.trim_end();
114		if let Some(suffix) = line_clean_end.strip_suffix(MULTILINE_SUFFIX) {
115			out += suffix.trim_end();
116			out.push('\n');
117		} else {
118			out += line;
119		}
120	}
121
122	let out_trim_end = out.trim_end_idx();
123	out.drain(out_trim_end..);
124	out
125}
126
127#[derive(PartialEq)]
128enum CommentType {
129	/// Each line starts with `//`
130	SingleLineDelimited,
131	/// Comment starting with `/*` or `/**` and every remaining line within starts with `*`
132	MultilineAsteriskPrefixed,
133	/// Comment starting with `/*` or `/**`, but without any prefix for the remaining lines
134	MultilineWithoutAsteriskPrefix,
135}
136
137#[cfg(test)]
138mod test {
139	use super::strip_doxygen_comment_markers;
140
141	#[test]
142	fn test_strip_comment_markers() {
143		assert_eq!("test", &strip_doxygen_comment_markers("/** test */"));
144		assert_eq!("test", &strip_doxygen_comment_markers("//   test"));
145		assert_eq!("test", &strip_doxygen_comment_markers("/*test */"));
146
147		{
148			let comment = "/** @overload
149 * @brief It's the same function as #calibrateCameraAruco but without calibration error estimation.
150 */";
151
152			let expected = "@overload
153@brief It's the same function as #calibrateCameraAruco but without calibration error estimation.";
154			assert_eq!(expected, &strip_doxygen_comment_markers(comment));
155		}
156
157		{
158			let comment = "// @overload
159// @brief It's the same function as #calibrateCameraAruco but without calibration error estimation.
160//";
161
162			let expected = "@overload
163@brief It's the same function as #calibrateCameraAruco but without calibration error estimation.";
164			assert_eq!(expected, &strip_doxygen_comment_markers(comment));
165		}
166	}
167}