1use lazy_static::lazy_static;
2use prost_types::source_code_info::Location;
3#[cfg(feature = "cleanup-markdown")]
4use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
5use regex::Regex;
6
7#[derive(Debug, Default, Clone)]
9pub struct Comments {
10 pub leading_detached: Vec<Vec<String>>,
12
13 pub leading: Vec<String>,
15
16 pub trailing: Vec<String>,
18}
19
20impl Comments {
21 pub(crate) fn from_location(location: &Location) -> Comments {
22 let leading_detached = location
23 .leading_detached_comments
24 .iter()
25 .map(get_lines)
26 .collect();
27 let leading = location
28 .leading_comments
29 .as_ref()
30 .map_or(Vec::new(), get_lines);
31 let trailing = location
32 .trailing_comments
33 .as_ref()
34 .map_or(Vec::new(), get_lines);
35 Comments {
36 leading_detached,
37 leading,
38 trailing,
39 }
40 }
41
42 pub fn append_with_indent(&self, indent_level: u8, buf: &mut String) {
46 for detached_block in &self.leading_detached {
48 for line in detached_block {
49 for _ in 0..indent_level {
50 buf.push_str(" ");
51 }
52 buf.push_str("//");
53 buf.push_str(&Self::sanitize_line(line));
54 buf.push('\n');
55 }
56 buf.push('\n');
57 }
58
59 for line in &self.leading {
61 for _ in 0..indent_level {
62 buf.push_str(" ");
63 }
64 buf.push_str("///");
65 buf.push_str(&Self::sanitize_line(line));
66 buf.push('\n');
67 }
68
69 if !self.leading.is_empty() && !self.trailing.is_empty() {
71 for _ in 0..indent_level {
72 buf.push_str(" ");
73 }
74 buf.push_str("///\n");
75 }
76
77 for line in &self.trailing {
79 for _ in 0..indent_level {
80 buf.push_str(" ");
81 }
82 buf.push_str("///");
83 buf.push_str(&Self::sanitize_line(line));
84 buf.push('\n');
85 }
86 }
87
88 fn should_indent(sanitized_line: &str) -> bool {
103 let mut chars = sanitized_line.chars();
104 chars
105 .next()
106 .map_or(false, |c| c != ' ' || chars.next() == Some(' '))
107 }
108
109 fn sanitize_line(line: &str) -> String {
113 lazy_static! {
114 static ref RULE_URL: Regex = Regex::new(r"https?://[^\s)]+").unwrap();
115 static ref RULE_BRACKETS: Regex = Regex::new(r"(\[)(\S+)(])").unwrap();
116 }
117
118 let mut s = RULE_URL.replace_all(line, r"<$0>").to_string();
119 s = RULE_BRACKETS.replace_all(&s, r"\$1$2\$3").to_string();
120 if Self::should_indent(&s) {
121 s.insert(0, ' ');
122 }
123 s
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct Service {
130 pub name: String,
132 pub proto_name: String,
134 pub package: String,
136 pub comments: Comments,
138 pub methods: Vec<Method>,
140 pub options: prost_types::ServiceOptions,
142}
143
144#[derive(Debug, Clone)]
146pub struct Method {
147 pub name: String,
149 pub proto_name: String,
151 pub comments: Comments,
153 pub input_type: String,
155 pub output_type: String,
157 pub input_proto_type: String,
159 pub output_proto_type: String,
161 pub options: prost_types::MethodOptions,
163 pub client_streaming: bool,
165 pub server_streaming: bool,
167}
168
169#[cfg(not(feature = "cleanup-markdown"))]
170fn get_lines<S>(comments: S) -> Vec<String>
171where
172 S: AsRef<str>,
173{
174 comments.as_ref().lines().map(str::to_owned).collect()
175}
176
177#[cfg(feature = "cleanup-markdown")]
178fn get_lines<S>(comments: S) -> Vec<String>
179where
180 S: AsRef<str>,
181{
182 let comments = comments.as_ref();
183 let mut buffer = String::with_capacity(comments.len() + 256);
184 let opts = pulldown_cmark_to_cmark::Options {
185 code_block_token_count: 3,
186 ..Default::default()
187 };
188 match pulldown_cmark_to_cmark::cmark_with_options(
189 Parser::new_ext(comments, Options::all() - Options::ENABLE_SMART_PUNCTUATION).map(
190 |event| {
191 fn map_codeblock(kind: CodeBlockKind) -> CodeBlockKind {
192 match kind {
193 CodeBlockKind::Fenced(s) => {
194 if &*s == "rust" {
195 CodeBlockKind::Fenced("compile_fail".into())
196 } else {
197 CodeBlockKind::Fenced(format!("text,{}", s).into())
198 }
199 }
200 CodeBlockKind::Indented => CodeBlockKind::Fenced("text".into()),
201 }
202 }
203 match event {
204 Event::Start(Tag::CodeBlock(kind)) => {
205 Event::Start(Tag::CodeBlock(map_codeblock(kind)))
206 }
207 Event::End(Tag::CodeBlock(kind)) => {
208 Event::End(Tag::CodeBlock(map_codeblock(kind)))
209 }
210 e => e,
211 }
212 },
213 ),
214 &mut buffer,
215 opts,
216 ) {
217 Ok(_) => buffer.lines().map(str::to_owned).collect(),
218 Err(_) => comments.lines().map(str::to_owned).collect(),
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_comment_append_with_indent_leaves_prespaced_lines() {
228 struct TestCases {
229 name: &'static str,
230 input: String,
231 expected: String,
232 }
233
234 let tests = vec![
235 TestCases {
236 name: "existing_space",
237 input: " A line with a single leading space.".to_string(),
238 expected: "/// A line with a single leading space.\n".to_string(),
239 },
240 TestCases {
241 name: "non_existing_space",
242 input: "A line without a single leading space.".to_string(),
243 expected: "/// A line without a single leading space.\n".to_string(),
244 },
245 TestCases {
246 name: "empty",
247 input: "".to_string(),
248 expected: "///\n".to_string(),
249 },
250 TestCases {
251 name: "multiple_leading_spaces",
252 input: " a line with several leading spaces, such as in a markdown list"
253 .to_string(),
254 expected: "/// a line with several leading spaces, such as in a markdown list\n"
255 .to_string(),
256 },
257 ];
258 for t in tests {
259 let input = Comments {
260 leading_detached: vec![],
261 leading: vec![],
262 trailing: vec![t.input],
263 };
264
265 let mut actual = "".to_string();
266 input.append_with_indent(0, &mut actual);
267
268 assert_eq!(t.expected, actual, "failed {}", t.name);
269 }
270 }
271
272 #[test]
273 fn test_comment_append_with_indent_sanitizes_comment_doc_url() {
274 struct TestCases {
275 name: &'static str,
276 input: String,
277 expected: String,
278 }
279
280 let tests = vec![
281 TestCases {
282 name: "valid_http",
283 input: "See https://www.rust-lang.org/".to_string(),
284 expected: "/// See <https://www.rust-lang.org/>\n".to_string(),
285 },
286 TestCases {
287 name: "valid_https",
288 input: "See https://www.rust-lang.org/".to_string(),
289 expected: "/// See <https://www.rust-lang.org/>\n".to_string(),
290 },
291 TestCases {
292 name: "valid_https_parenthesis",
293 input: "See (https://www.rust-lang.org/)".to_string(),
294 expected: "/// See (<https://www.rust-lang.org/>)\n".to_string(),
295 },
296 TestCases {
297 name: "invalid",
298 input: "See note://abc".to_string(),
299 expected: "/// See note://abc\n".to_string(),
300 },
301 ];
302 for t in tests {
303 let input = Comments {
304 leading_detached: vec![],
305 leading: vec![],
306 trailing: vec![t.input],
307 };
308
309 let mut actual = "".to_string();
310 input.append_with_indent(0, &mut actual);
311
312 assert_eq!(t.expected, actual, "failed {}", t.name);
313 }
314 }
315
316 #[test]
317 fn test_comment_append_with_indent_sanitizes_square_brackets() {
318 struct TestCases {
319 name: &'static str,
320 input: String,
321 expected: String,
322 }
323
324 let tests = vec![
325 TestCases {
326 name: "valid_brackets",
327 input: "foo [bar] baz".to_string(),
328 expected: "/// foo \\[bar\\] baz\n".to_string(),
329 },
330 TestCases {
331 name: "invalid_start_bracket",
332 input: "foo [= baz".to_string(),
333 expected: "/// foo [= baz\n".to_string(),
334 },
335 TestCases {
336 name: "invalid_end_bracket",
337 input: "foo =] baz".to_string(),
338 expected: "/// foo =] baz\n".to_string(),
339 },
340 TestCases {
341 name: "invalid_bracket_combination",
342 input: "[0, 9)".to_string(),
343 expected: "/// [0, 9)\n".to_string(),
344 },
345 ];
346 for t in tests {
347 let input = Comments {
348 leading_detached: vec![],
349 leading: vec![],
350 trailing: vec![t.input],
351 };
352
353 let mut actual = "".to_string();
354 input.append_with_indent(0, &mut actual);
355
356 assert_eq!(t.expected, actual, "failed {}", t.name);
357 }
358 }
359
360 #[test]
361 fn test_codeblocks() {
362 struct TestCase {
363 name: &'static str,
364 input: &'static str,
365 #[allow(unused)]
366 cleanedup_expected: Vec<&'static str>,
367 }
368
369 let tests = vec![
370 TestCase {
371 name: "unlabelled_block",
372 input: " thingy\n",
373 cleanedup_expected: vec!["", "```text", "thingy", "```"],
374 },
375 TestCase {
376 name: "rust_block",
377 input: "```rust\nfoo.bar()\n```\n",
378 cleanedup_expected: vec!["", "```compile_fail", "foo.bar()", "```"],
379 },
380 TestCase {
381 name: "js_block",
382 input: "```javascript\nfoo.bar()\n```\n",
383 cleanedup_expected: vec!["", "```text,javascript", "foo.bar()", "```"],
384 },
385 ];
386
387 for t in tests {
388 let loc = Location {
389 path: vec![],
390 span: vec![],
391 leading_comments: Some(t.input.into()),
392 trailing_comments: None,
393 leading_detached_comments: vec![],
394 };
395 let comments = Comments::from_location(&loc);
396 #[cfg(feature = "cleanup-markdown")]
397 let expected = t.cleanedup_expected;
398 #[cfg(not(feature = "cleanup-markdown"))]
399 let expected: Vec<&str> = t.input.lines().collect();
400 assert_eq!(expected, comments.leading, "failed {}", t.name);
401 }
402 }
403}