1pub fn emit_phpdoc(out: &mut String, doc: &str, indent: &str, exception_class: &str) {
13 if doc.is_empty() {
14 return;
15 }
16 let sections = parse_rustdoc_sections(doc);
17 let any_section = sections.arguments.is_some()
18 || sections.returns.is_some()
19 || sections.errors.is_some()
20 || sections.example.is_some();
21 let body = if any_section {
22 render_phpdoc_sections(§ions, exception_class)
23 } else {
24 doc.to_string()
25 };
26 out.push_str(indent);
27 out.push_str("/**\n");
28 for line in body.lines() {
29 out.push_str(indent);
30 out.push_str(" * ");
31 out.push_str(&escape_phpdoc_line(line));
32 out.push('\n');
33 }
34 out.push_str(indent);
35 out.push_str(" */\n");
36}
37
38fn escape_phpdoc_line(s: &str) -> String {
40 s.replace("*/", "* /")
41}
42
43pub fn emit_csharp_doc(out: &mut String, doc: &str, indent: &str, exception_class: &str) {
52 if doc.is_empty() {
53 return;
54 }
55 let sections = parse_rustdoc_sections(doc);
56 let any_section = sections.arguments.is_some()
57 || sections.returns.is_some()
58 || sections.errors.is_some()
59 || sections.example.is_some();
60 if !any_section {
61 out.push_str(indent);
63 out.push_str("/// <summary>\n");
64 for line in doc.lines() {
65 out.push_str(indent);
66 out.push_str("/// ");
67 out.push_str(&escape_csharp_doc_line(line));
68 out.push('\n');
69 }
70 out.push_str(indent);
71 out.push_str("/// </summary>\n");
72 return;
73 }
74 let rendered = render_csharp_xml_sections(§ions, exception_class);
75 for line in rendered.lines() {
76 out.push_str(indent);
77 out.push_str("/// ");
78 out.push_str(line);
83 out.push('\n');
84 }
85}
86
87fn escape_csharp_doc_line(s: &str) -> String {
89 s.replace('&', "&").replace('<', "<").replace('>', ">")
90}
91
92pub fn emit_elixir_doc(out: &mut String, doc: &str) {
95 if doc.is_empty() {
96 return;
97 }
98 out.push_str("@doc \"\"\"\n");
99 for line in doc.lines() {
100 out.push_str(&escape_elixir_doc_line(line));
101 out.push('\n');
102 }
103 out.push_str("\"\"\"\n");
104}
105
106pub fn emit_rustdoc(out: &mut String, doc: &str, indent: &str) {
112 if doc.is_empty() {
113 return;
114 }
115 for line in doc.lines() {
116 out.push_str(indent);
117 out.push_str("/// ");
118 out.push_str(line);
119 out.push('\n');
120 }
121}
122
123fn escape_elixir_doc_line(s: &str) -> String {
125 s.replace("\"\"\"", "\"\" \"")
126}
127
128pub fn emit_roxygen(out: &mut String, doc: &str) {
131 if doc.is_empty() {
132 return;
133 }
134 for line in doc.lines() {
135 out.push_str("#' ");
136 out.push_str(line);
137 out.push('\n');
138 }
139}
140
141pub fn emit_swift_doc(out: &mut String, doc: &str, indent: &str) {
144 if doc.is_empty() {
145 return;
146 }
147 for line in doc.lines() {
148 out.push_str(indent);
149 out.push_str("/// ");
150 out.push_str(line);
151 out.push('\n');
152 }
153}
154
155pub fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
159 if doc.is_empty() {
160 return;
161 }
162 out.push_str(indent);
163 out.push_str("/**\n");
164 for line in doc.lines() {
165 let escaped = escape_javadoc_line(line);
166 let trimmed = escaped.trim_end();
167 if trimmed.is_empty() {
168 out.push_str(indent);
169 out.push_str(" *\n");
170 } else {
171 out.push_str(indent);
172 out.push_str(" * ");
173 out.push_str(trimmed);
174 out.push('\n');
175 }
176 }
177 out.push_str(indent);
178 out.push_str(" */\n");
179}
180
181pub fn emit_kdoc(out: &mut String, doc: &str, indent: &str) {
184 if doc.is_empty() {
185 return;
186 }
187 out.push_str(indent);
188 out.push_str("/**\n");
189 for line in doc.lines() {
190 let trimmed = line.trim_end();
191 if trimmed.is_empty() {
192 out.push_str(indent);
193 out.push_str(" *\n");
194 } else {
195 out.push_str(indent);
196 out.push_str(" * ");
197 out.push_str(trimmed);
198 out.push('\n');
199 }
200 }
201 out.push_str(indent);
202 out.push_str(" */\n");
203}
204
205pub fn emit_kdoc_ktfmt_canonical(out: &mut String, doc: &str, indent: &str) {
216 const KTFMT_LINE_WIDTH: usize = 100;
217
218 if doc.is_empty() {
219 return;
220 }
221
222 let lines: Vec<&str> = doc.lines().collect();
223
224 let is_short_single_paragraph = lines.len() == 1 && !lines[0].contains('\n');
226
227 if is_short_single_paragraph {
228 let trimmed = lines[0].trim();
229 let single_line_len = indent.len() + 4 + trimmed.len() + 3; if single_line_len <= KTFMT_LINE_WIDTH {
232 out.push_str(indent);
234 out.push_str("/** ");
235 out.push_str(trimmed);
236 out.push_str(" */\n");
237 return;
238 }
239 }
240
241 out.push_str(indent);
243 out.push_str("/**\n");
244 for line in lines {
245 let trimmed = line.trim_end();
246 if trimmed.is_empty() {
247 out.push_str(indent);
248 out.push_str(" *\n");
249 } else {
250 out.push_str(indent);
251 out.push_str(" * ");
252 out.push_str(trimmed);
253 out.push('\n');
254 }
255 }
256 out.push_str(indent);
257 out.push_str(" */\n");
258}
259
260pub fn emit_dartdoc(out: &mut String, doc: &str, indent: &str) {
263 if doc.is_empty() {
264 return;
265 }
266 for line in doc.lines() {
267 out.push_str(indent);
268 out.push_str("/// ");
269 out.push_str(line);
270 out.push('\n');
271 }
272}
273
274pub fn emit_gleam_doc(out: &mut String, doc: &str, indent: &str) {
277 if doc.is_empty() {
278 return;
279 }
280 for line in doc.lines() {
281 out.push_str(indent);
282 out.push_str("/// ");
283 out.push_str(line);
284 out.push('\n');
285 }
286}
287
288pub fn emit_c_doxygen(out: &mut String, doc: &str, indent: &str) {
309 if doc.trim().is_empty() {
310 return;
311 }
312 let sections = parse_rustdoc_sections(doc);
313 let any_section = sections.arguments.is_some()
314 || sections.returns.is_some()
315 || sections.errors.is_some()
316 || sections.safety.is_some()
317 || sections.example.is_some();
318 let mut body = if any_section {
319 render_doxygen_sections_with_notes(§ions)
320 } else {
321 sections.summary.clone()
322 };
323 body = strip_markdown_links(&body);
324 let wrapped = word_wrap(&body, DOXYGEN_WRAP_WIDTH);
325 for line in wrapped.lines() {
326 out.push_str(indent);
327 out.push_str("/// ");
328 out.push_str(line);
329 out.push('\n');
330 }
331}
332
333const DOXYGEN_WRAP_WIDTH: usize = 100;
334
335fn render_doxygen_sections_with_notes(sections: &RustdocSections) -> String {
340 let mut out = String::new();
341 if !sections.summary.is_empty() {
342 out.push_str(§ions.summary);
343 }
344 if let Some(args) = sections.arguments.as_deref() {
345 for (name, desc) in parse_arguments_bullets(args) {
346 if !out.is_empty() {
347 out.push('\n');
348 }
349 if desc.is_empty() {
350 out.push_str("\\param ");
351 out.push_str(&name);
352 } else {
353 out.push_str("\\param ");
354 out.push_str(&name);
355 out.push(' ');
356 out.push_str(&desc);
357 }
358 }
359 }
360 if let Some(ret) = sections.returns.as_deref() {
361 if !out.is_empty() {
362 out.push('\n');
363 }
364 out.push_str("\\return ");
365 out.push_str(ret.trim());
366 }
367 if let Some(err) = sections.errors.as_deref() {
368 if !out.is_empty() {
369 out.push('\n');
370 }
371 out.push_str("\\note ");
372 out.push_str(err.trim());
373 }
374 if let Some(safety) = sections.safety.as_deref() {
375 if !out.is_empty() {
376 out.push('\n');
377 }
378 out.push_str("\\note SAFETY: ");
379 out.push_str(safety.trim());
380 }
381 if let Some(example) = sections.example.as_deref() {
382 if !out.is_empty() {
383 out.push('\n');
384 }
385 out.push_str("\\code\n");
386 for line in example.lines() {
387 let t = line.trim_start();
388 if t.starts_with("```") {
389 continue;
390 }
391 out.push_str(line);
392 out.push('\n');
393 }
394 out.push_str("\\endcode");
395 }
396 out
397}
398
399fn strip_markdown_links(s: &str) -> String {
402 let mut out = String::with_capacity(s.len());
403 let bytes = s.as_bytes();
404 let mut i = 0;
405 while i < bytes.len() {
406 if bytes[i] == b'[' {
407 if let Some(close) = bytes[i + 1..].iter().position(|&b| b == b']') {
409 let text_end = i + 1 + close;
410 if text_end + 1 < bytes.len() && bytes[text_end + 1] == b'(' {
411 if let Some(paren_close) = bytes[text_end + 2..].iter().position(|&b| b == b')') {
412 let url_start = text_end + 2;
413 let url_end = url_start + paren_close;
414 let text = &s[i + 1..text_end];
415 let url = &s[url_start..url_end];
416 out.push_str(text);
417 out.push_str(" (");
418 out.push_str(url);
419 out.push(')');
420 i = url_end + 1;
421 continue;
422 }
423 }
424 }
425 }
426 out.push(bytes[i] as char);
427 i += 1;
428 }
429 out
430}
431
432fn word_wrap(input: &str, width: usize) -> String {
436 let mut out = String::with_capacity(input.len());
437 let mut in_code = false;
438 for raw in input.lines() {
439 let trimmed = raw.trim_start();
440 if trimmed.starts_with("\\code") {
441 in_code = true;
442 out.push_str(raw);
443 out.push('\n');
444 continue;
445 }
446 if trimmed.starts_with("\\endcode") {
447 in_code = false;
448 out.push_str(raw);
449 out.push('\n');
450 continue;
451 }
452 if in_code || trimmed.starts_with("```") {
453 out.push_str(raw);
454 out.push('\n');
455 continue;
456 }
457 if raw.len() <= width {
458 out.push_str(raw);
459 out.push('\n');
460 continue;
461 }
462 let mut current = String::with_capacity(width);
463 for word in raw.split_whitespace() {
464 if current.is_empty() {
465 current.push_str(word);
466 } else if current.len() + 1 + word.len() > width {
467 out.push_str(¤t);
468 out.push('\n');
469 current.clear();
470 current.push_str(word);
471 } else {
472 current.push(' ');
473 current.push_str(word);
474 }
475 }
476 if !current.is_empty() {
477 out.push_str(¤t);
478 out.push('\n');
479 }
480 }
481 out.trim_end_matches('\n').to_string()
482}
483
484pub fn emit_zig_doc(out: &mut String, doc: &str, indent: &str) {
487 if doc.is_empty() {
488 return;
489 }
490 for line in doc.lines() {
491 out.push_str(indent);
492 out.push_str("/// ");
493 out.push_str(line);
494 out.push('\n');
495 }
496}
497
498pub fn emit_yard_doc(out: &mut String, doc: &str, indent: &str) {
505 if doc.is_empty() {
506 return;
507 }
508 let sections = parse_rustdoc_sections(doc);
509 let any_section = sections.arguments.is_some()
510 || sections.returns.is_some()
511 || sections.errors.is_some()
512 || sections.example.is_some();
513 let body = if any_section {
514 render_yard_sections(§ions)
515 } else {
516 doc.to_string()
517 };
518 for line in body.lines() {
519 out.push_str(indent);
520 out.push_str("# ");
521 out.push_str(line);
522 out.push('\n');
523 }
524}
525
526pub fn render_yard_sections(sections: &RustdocSections) -> String {
536 let mut out = String::new();
537 if !sections.summary.is_empty() {
538 out.push_str(§ions.summary);
539 }
540 if let Some(args) = sections.arguments.as_deref() {
541 for (name, desc) in parse_arguments_bullets(args) {
542 if !out.is_empty() {
543 out.push('\n');
544 }
545 if desc.is_empty() {
546 out.push_str("@param ");
547 out.push_str(&name);
548 } else {
549 out.push_str("@param ");
550 out.push_str(&name);
551 out.push(' ');
552 out.push_str(&desc);
553 }
554 }
555 }
556 if let Some(ret) = sections.returns.as_deref() {
557 if !out.is_empty() {
558 out.push('\n');
559 }
560 out.push_str("@return ");
561 out.push_str(ret.trim());
562 }
563 if let Some(err) = sections.errors.as_deref() {
564 if !out.is_empty() {
565 out.push('\n');
566 }
567 out.push_str("@raise ");
568 out.push_str(err.trim());
569 }
570 if let Some(example) = sections.example.as_deref() {
571 if let Some(body) = example_for_target(example, "ruby") {
572 if !out.is_empty() {
573 out.push('\n');
574 }
575 out.push_str("@example\n");
576 out.push_str(&body);
577 }
578 }
579 out
580}
581
582fn escape_javadoc_line(s: &str) -> String {
592 let mut result = String::with_capacity(s.len());
593 let mut chars = s.chars().peekable();
594 while let Some(ch) = chars.next() {
595 if ch == '`' {
596 let mut code = String::new();
597 for c in chars.by_ref() {
598 if c == '`' {
599 break;
600 }
601 code.push(c);
602 }
603 result.push_str("{@code ");
604 result.push_str(&escape_javadoc_html_entities(&code));
605 result.push('}');
606 } else if ch == '<' {
607 result.push_str("<");
608 } else if ch == '>' {
609 result.push_str(">");
610 } else if ch == '&' {
611 result.push_str("&");
612 } else {
613 result.push(ch);
614 }
615 }
616 result
617}
618
619fn escape_javadoc_html_entities(s: &str) -> String {
622 let mut out = String::with_capacity(s.len());
623 for ch in s.chars() {
624 match ch {
625 '<' => out.push_str("<"),
626 '>' => out.push_str(">"),
627 '&' => out.push_str("&"),
628 other => out.push(other),
629 }
630 }
631 out
632}
633
634#[derive(Debug, Default, Clone, PartialEq, Eq)]
645pub struct RustdocSections {
646 pub summary: String,
648 pub arguments: Option<String>,
650 pub returns: Option<String>,
652 pub errors: Option<String>,
654 pub panics: Option<String>,
656 pub safety: Option<String>,
658 pub example: Option<String>,
660}
661
662pub fn parse_rustdoc_sections(doc: &str) -> RustdocSections {
674 if doc.trim().is_empty() {
675 return RustdocSections::default();
676 }
677 let mut summary = String::new();
678 let mut arguments: Option<String> = None;
679 let mut returns: Option<String> = None;
680 let mut errors: Option<String> = None;
681 let mut panics: Option<String> = None;
682 let mut safety: Option<String> = None;
683 let mut example: Option<String> = None;
684 let mut current: Option<&'static str> = None;
685 let mut buf = String::new();
686 let mut in_fence = false;
687 let flush = |target: Option<&'static str>,
688 buf: &mut String,
689 summary: &mut String,
690 arguments: &mut Option<String>,
691 returns: &mut Option<String>,
692 errors: &mut Option<String>,
693 panics: &mut Option<String>,
694 safety: &mut Option<String>,
695 example: &mut Option<String>| {
696 let body = std::mem::take(buf).trim().to_string();
697 if body.is_empty() {
698 return;
699 }
700 match target {
701 None => {
702 if !summary.is_empty() {
703 summary.push('\n');
704 }
705 summary.push_str(&body);
706 }
707 Some("arguments") => *arguments = Some(body),
708 Some("returns") => *returns = Some(body),
709 Some("errors") => *errors = Some(body),
710 Some("panics") => *panics = Some(body),
711 Some("safety") => *safety = Some(body),
712 Some("example") => *example = Some(body),
713 _ => {}
714 }
715 };
716 for line in doc.lines() {
717 let trimmed = line.trim_start();
718 if trimmed.starts_with("```") {
719 in_fence = !in_fence;
720 buf.push_str(line);
721 buf.push('\n');
722 continue;
723 }
724 if !in_fence {
725 if let Some(rest) = trimmed.strip_prefix("# ") {
726 let head = rest.trim().to_ascii_lowercase();
727 let target = match head.as_str() {
728 "arguments" | "args" => Some("arguments"),
729 "returns" => Some("returns"),
730 "errors" => Some("errors"),
731 "panics" => Some("panics"),
732 "safety" => Some("safety"),
733 "example" | "examples" => Some("example"),
734 _ => None,
735 };
736 if target.is_some() {
737 flush(
738 current,
739 &mut buf,
740 &mut summary,
741 &mut arguments,
742 &mut returns,
743 &mut errors,
744 &mut panics,
745 &mut safety,
746 &mut example,
747 );
748 current = target;
749 continue;
750 }
751 }
752 }
753 buf.push_str(line);
754 buf.push('\n');
755 }
756 flush(
757 current,
758 &mut buf,
759 &mut summary,
760 &mut arguments,
761 &mut returns,
762 &mut errors,
763 &mut panics,
764 &mut safety,
765 &mut example,
766 );
767 RustdocSections {
768 summary,
769 arguments,
770 returns,
771 errors,
772 panics,
773 safety,
774 example,
775 }
776}
777
778pub fn parse_arguments_bullets(body: &str) -> Vec<(String, String)> {
788 let mut out: Vec<(String, String)> = Vec::new();
789 for raw in body.lines() {
790 let line = raw.trim_end();
791 let trimmed = line.trim_start();
792 let is_bullet = trimmed.starts_with("* ") || trimmed.starts_with("- ");
793 if is_bullet {
794 let after = &trimmed[2..];
795 let (name, desc) = if let Some(idx) = after.find(" - ") {
797 (after[..idx].trim(), after[idx + 3..].trim())
798 } else if let Some(idx) = after.find(": ") {
799 (after[..idx].trim(), after[idx + 2..].trim())
800 } else if let Some(idx) = after.find(' ') {
801 (after[..idx].trim(), after[idx + 1..].trim())
802 } else {
803 (after.trim(), "")
804 };
805 let name = name.trim_matches('`').trim_matches('*').to_string();
806 out.push((name, desc.to_string()));
807 } else if !trimmed.is_empty() {
808 if let Some(last) = out.last_mut() {
809 if !last.1.is_empty() {
810 last.1.push(' ');
811 }
812 last.1.push_str(trimmed);
813 }
814 }
815 }
816 out
817}
818
819fn detect_first_fence_lang(body: &str) -> &str {
826 for line in body.lines() {
827 let trimmed = line.trim_start();
828 if let Some(rest) = trimmed.strip_prefix("```") {
829 let tag = rest.split(',').next().unwrap_or("").trim();
830 return if tag.is_empty() { "rust" } else { tag };
831 }
832 }
833 "rust"
834}
835
836pub fn example_for_target(example: &str, target_lang: &str) -> Option<String> {
845 let trimmed = example.trim();
846 let source_lang = detect_first_fence_lang(trimmed);
847 if source_lang == "rust" && target_lang != "rust" {
848 None
849 } else {
850 Some(replace_fence_lang(trimmed, target_lang))
851 }
852}
853
854pub fn replace_fence_lang(body: &str, lang_replacement: &str) -> String {
862 let mut out = String::with_capacity(body.len());
863 for line in body.lines() {
864 let trimmed = line.trim_start();
865 if let Some(rest) = trimmed.strip_prefix("```") {
866 let indent = &line[..line.len() - trimmed.len()];
869 let after_lang = rest.find(',').map(|i| &rest[i..]).unwrap_or("");
870 out.push_str(indent);
871 out.push_str("```");
872 out.push_str(lang_replacement);
873 out.push_str(after_lang);
874 out.push('\n');
875 } else {
876 out.push_str(line);
877 out.push('\n');
878 }
879 }
880 out.trim_end_matches('\n').to_string()
881}
882
883pub fn render_jsdoc_sections(sections: &RustdocSections) -> String {
896 let mut out = String::new();
897 if !sections.summary.is_empty() {
898 out.push_str(§ions.summary);
899 }
900 if let Some(args) = sections.arguments.as_deref() {
901 for (name, desc) in parse_arguments_bullets(args) {
902 if !out.is_empty() {
903 out.push('\n');
904 }
905 if desc.is_empty() {
906 out.push_str(&crate::template_env::render(
907 "doc_jsdoc_param.jinja",
908 minijinja::context! { name => &name },
909 ));
910 } else {
911 out.push_str(&crate::template_env::render(
912 "doc_jsdoc_param_desc.jinja",
913 minijinja::context! { name => &name, desc => &desc },
914 ));
915 }
916 }
917 }
918 if let Some(ret) = sections.returns.as_deref() {
919 if !out.is_empty() {
920 out.push('\n');
921 }
922 out.push_str(&crate::template_env::render(
923 "doc_jsdoc_returns.jinja",
924 minijinja::context! { content => ret.trim() },
925 ));
926 }
927 if let Some(err) = sections.errors.as_deref() {
928 if !out.is_empty() {
929 out.push('\n');
930 }
931 out.push_str(&crate::template_env::render(
932 "doc_jsdoc_throws.jinja",
933 minijinja::context! { content => err.trim() },
934 ));
935 }
936 if let Some(example) = sections.example.as_deref() {
937 if let Some(body) = example_for_target(example, "typescript") {
938 if !out.is_empty() {
939 out.push('\n');
940 }
941 out.push_str("@example\n");
942 out.push_str(&body);
943 }
944 }
945 out
946}
947
948pub fn render_javadoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
958 let mut out = String::new();
959 if !sections.summary.is_empty() {
960 out.push_str(§ions.summary);
961 }
962 if let Some(args) = sections.arguments.as_deref() {
963 for (name, desc) in parse_arguments_bullets(args) {
964 if !out.is_empty() {
965 out.push('\n');
966 }
967 if desc.is_empty() {
968 out.push_str(&crate::template_env::render(
969 "doc_javadoc_param.jinja",
970 minijinja::context! { name => &name },
971 ));
972 } else {
973 out.push_str(&crate::template_env::render(
974 "doc_javadoc_param_desc.jinja",
975 minijinja::context! { name => &name, desc => &desc },
976 ));
977 }
978 }
979 }
980 if let Some(ret) = sections.returns.as_deref() {
981 if !out.is_empty() {
982 out.push('\n');
983 }
984 out.push_str(&crate::template_env::render(
985 "doc_javadoc_return.jinja",
986 minijinja::context! { content => ret.trim() },
987 ));
988 }
989 if let Some(err) = sections.errors.as_deref() {
990 if !out.is_empty() {
991 out.push('\n');
992 }
993 out.push_str(&crate::template_env::render(
994 "doc_javadoc_throws.jinja",
995 minijinja::context! { throws_class => throws_class, content => err.trim() },
996 ));
997 }
998 out
999}
1000
1001pub fn render_csharp_xml_sections(sections: &RustdocSections, exception_class: &str) -> String {
1010 let mut out = String::new();
1011 out.push_str("<summary>\n");
1012 let summary = if sections.summary.is_empty() {
1013 ""
1014 } else {
1015 sections.summary.as_str()
1016 };
1017 for line in summary.lines() {
1018 out.push_str(line);
1019 out.push('\n');
1020 }
1021 out.push_str("</summary>");
1022 if let Some(args) = sections.arguments.as_deref() {
1023 for (name, desc) in parse_arguments_bullets(args) {
1024 out.push('\n');
1025 if desc.is_empty() {
1026 out.push_str(&crate::template_env::render(
1027 "doc_csharp_param.jinja",
1028 minijinja::context! { name => &name },
1029 ));
1030 } else {
1031 out.push_str(&crate::template_env::render(
1032 "doc_csharp_param_desc.jinja",
1033 minijinja::context! { name => &name, desc => &desc },
1034 ));
1035 }
1036 }
1037 }
1038 if let Some(ret) = sections.returns.as_deref() {
1039 out.push('\n');
1040 out.push_str(&crate::template_env::render(
1041 "doc_csharp_returns.jinja",
1042 minijinja::context! { content => ret.trim() },
1043 ));
1044 }
1045 if let Some(err) = sections.errors.as_deref() {
1046 out.push('\n');
1047 out.push_str(&crate::template_env::render(
1048 "doc_csharp_exception.jinja",
1049 minijinja::context! {
1050 exception_class => exception_class,
1051 content => err.trim(),
1052 },
1053 ));
1054 }
1055 if let Some(example) = sections.example.as_deref() {
1056 out.push('\n');
1057 out.push_str("<example><code language=\"csharp\">\n");
1058 for line in example.lines() {
1060 let t = line.trim_start();
1061 if t.starts_with("```") {
1062 continue;
1063 }
1064 out.push_str(line);
1065 out.push('\n');
1066 }
1067 out.push_str("</code></example>");
1068 }
1069 out
1070}
1071
1072pub fn render_phpdoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
1079 let mut out = String::new();
1080 if !sections.summary.is_empty() {
1081 out.push_str(§ions.summary);
1082 }
1083 if let Some(args) = sections.arguments.as_deref() {
1084 for (name, desc) in parse_arguments_bullets(args) {
1085 if !out.is_empty() {
1086 out.push('\n');
1087 }
1088 if desc.is_empty() {
1089 out.push_str(&crate::template_env::render(
1090 "doc_phpdoc_param.jinja",
1091 minijinja::context! { name => &name },
1092 ));
1093 } else {
1094 out.push_str(&crate::template_env::render(
1095 "doc_phpdoc_param_desc.jinja",
1096 minijinja::context! { name => &name, desc => &desc },
1097 ));
1098 }
1099 }
1100 }
1101 if let Some(ret) = sections.returns.as_deref() {
1102 if !out.is_empty() {
1103 out.push('\n');
1104 }
1105 out.push_str(&crate::template_env::render(
1106 "doc_phpdoc_return.jinja",
1107 minijinja::context! { content => ret.trim() },
1108 ));
1109 }
1110 if let Some(err) = sections.errors.as_deref() {
1111 if !out.is_empty() {
1112 out.push('\n');
1113 }
1114 out.push_str(&crate::template_env::render(
1115 "doc_phpdoc_throws.jinja",
1116 minijinja::context! { throws_class => throws_class, content => err.trim() },
1117 ));
1118 }
1119 if let Some(example) = sections.example.as_deref() {
1120 if let Some(body) = example_for_target(example, "php") {
1121 if !out.is_empty() {
1122 out.push('\n');
1123 }
1124 out.push_str(&body);
1125 }
1126 }
1127 out
1128}
1129
1130pub fn render_doxygen_sections(sections: &RustdocSections) -> String {
1137 let mut out = String::new();
1138 if !sections.summary.is_empty() {
1139 out.push_str(§ions.summary);
1140 }
1141 if let Some(args) = sections.arguments.as_deref() {
1142 for (name, desc) in parse_arguments_bullets(args) {
1143 if !out.is_empty() {
1144 out.push('\n');
1145 }
1146 if desc.is_empty() {
1147 out.push_str(&crate::template_env::render(
1148 "doc_doxygen_param.jinja",
1149 minijinja::context! { name => &name },
1150 ));
1151 } else {
1152 out.push_str(&crate::template_env::render(
1153 "doc_doxygen_param_desc.jinja",
1154 minijinja::context! { name => &name, desc => &desc },
1155 ));
1156 }
1157 }
1158 }
1159 if let Some(ret) = sections.returns.as_deref() {
1160 if !out.is_empty() {
1161 out.push('\n');
1162 }
1163 out.push_str(&crate::template_env::render(
1164 "doc_doxygen_return.jinja",
1165 minijinja::context! { content => ret.trim() },
1166 ));
1167 }
1168 if let Some(err) = sections.errors.as_deref() {
1169 if !out.is_empty() {
1170 out.push('\n');
1171 }
1172 out.push_str(&crate::template_env::render(
1173 "doc_doxygen_errors.jinja",
1174 minijinja::context! { content => err.trim() },
1175 ));
1176 }
1177 if let Some(example) = sections.example.as_deref() {
1178 if !out.is_empty() {
1179 out.push('\n');
1180 }
1181 out.push_str("\\code\n");
1182 for line in example.lines() {
1183 let t = line.trim_start();
1184 if t.starts_with("```") {
1185 continue;
1186 }
1187 out.push_str(line);
1188 out.push('\n');
1189 }
1190 out.push_str("\\endcode");
1191 }
1192 out
1193}
1194
1195pub fn doc_first_paragraph_joined(doc: &str) -> String {
1208 doc.lines()
1209 .take_while(|l| !l.trim().is_empty())
1210 .map(str::trim)
1211 .collect::<Vec<_>>()
1212 .join(" ")
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218
1219 #[test]
1220 fn test_emit_phpdoc() {
1221 let mut out = String::new();
1222 emit_phpdoc(&mut out, "Simple documentation", " ", "TestException");
1223 assert!(out.contains("/**"));
1224 assert!(out.contains("Simple documentation"));
1225 assert!(out.contains("*/"));
1226 }
1227
1228 #[test]
1229 fn test_phpdoc_escaping() {
1230 let mut out = String::new();
1231 emit_phpdoc(&mut out, "Handle */ sequences", "", "TestException");
1232 assert!(out.contains("Handle * / sequences"));
1233 }
1234
1235 #[test]
1236 fn test_emit_csharp_doc() {
1237 let mut out = String::new();
1238 emit_csharp_doc(&mut out, "C# documentation", " ", "TestException");
1239 assert!(out.contains("<summary>"));
1240 assert!(out.contains("C# documentation"));
1241 assert!(out.contains("</summary>"));
1242 }
1243
1244 #[test]
1245 fn test_csharp_xml_escaping() {
1246 let mut out = String::new();
1247 emit_csharp_doc(&mut out, "foo < bar & baz > qux", "", "TestException");
1248 assert!(out.contains("foo < bar & baz > qux"));
1249 }
1250
1251 #[test]
1252 fn test_emit_elixir_doc() {
1253 let mut out = String::new();
1254 emit_elixir_doc(&mut out, "Elixir documentation");
1255 assert!(out.contains("@doc \"\"\""));
1256 assert!(out.contains("Elixir documentation"));
1257 assert!(out.contains("\"\"\""));
1258 }
1259
1260 #[test]
1261 fn test_elixir_heredoc_escaping() {
1262 let mut out = String::new();
1263 emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
1264 assert!(out.contains("Handle \"\" \" sequences"));
1265 }
1266
1267 #[test]
1268 fn test_emit_roxygen() {
1269 let mut out = String::new();
1270 emit_roxygen(&mut out, "R documentation");
1271 assert!(out.contains("#' R documentation"));
1272 }
1273
1274 #[test]
1275 fn test_emit_swift_doc() {
1276 let mut out = String::new();
1277 emit_swift_doc(&mut out, "Swift documentation", " ");
1278 assert!(out.contains("/// Swift documentation"));
1279 }
1280
1281 #[test]
1282 fn test_emit_javadoc() {
1283 let mut out = String::new();
1284 emit_javadoc(&mut out, "Java documentation", " ");
1285 assert!(out.contains("/**"));
1286 assert!(out.contains("Java documentation"));
1287 assert!(out.contains("*/"));
1288 }
1289
1290 #[test]
1291 fn test_emit_kdoc() {
1292 let mut out = String::new();
1293 emit_kdoc(&mut out, "Kotlin documentation", " ");
1294 assert!(out.contains("/**"));
1295 assert!(out.contains("Kotlin documentation"));
1296 assert!(out.contains("*/"));
1297 }
1298
1299 #[test]
1300 fn test_emit_dartdoc() {
1301 let mut out = String::new();
1302 emit_dartdoc(&mut out, "Dart documentation", " ");
1303 assert!(out.contains("/// Dart documentation"));
1304 }
1305
1306 #[test]
1307 fn test_emit_gleam_doc() {
1308 let mut out = String::new();
1309 emit_gleam_doc(&mut out, "Gleam documentation", " ");
1310 assert!(out.contains("/// Gleam documentation"));
1311 }
1312
1313 #[test]
1314 fn test_emit_zig_doc() {
1315 let mut out = String::new();
1316 emit_zig_doc(&mut out, "Zig documentation", " ");
1317 assert!(out.contains("/// Zig documentation"));
1318 }
1319
1320 #[test]
1321 fn test_empty_doc_skipped() {
1322 let mut out = String::new();
1323 emit_phpdoc(&mut out, "", "", "TestException");
1324 emit_csharp_doc(&mut out, "", "", "TestException");
1325 emit_elixir_doc(&mut out, "");
1326 emit_roxygen(&mut out, "");
1327 emit_kdoc(&mut out, "", "");
1328 emit_dartdoc(&mut out, "", "");
1329 emit_gleam_doc(&mut out, "", "");
1330 emit_zig_doc(&mut out, "", "");
1331 assert!(out.is_empty());
1332 }
1333
1334 #[test]
1335 fn test_doc_first_paragraph_joined_single_line() {
1336 assert_eq!(doc_first_paragraph_joined("Simple doc."), "Simple doc.");
1337 }
1338
1339 #[test]
1340 fn test_doc_first_paragraph_joined_wrapped_sentence() {
1341 let doc = "Convert HTML to Markdown,\nreturning a result.";
1343 assert_eq!(
1344 doc_first_paragraph_joined(doc),
1345 "Convert HTML to Markdown, returning a result."
1346 );
1347 }
1348
1349 #[test]
1350 fn test_doc_first_paragraph_joined_stops_at_blank_line() {
1351 let doc = "First paragraph.\nStill first.\n\nSecond paragraph.";
1352 assert_eq!(doc_first_paragraph_joined(doc), "First paragraph. Still first.");
1353 }
1354
1355 #[test]
1356 fn test_doc_first_paragraph_joined_empty() {
1357 assert_eq!(doc_first_paragraph_joined(""), "");
1358 }
1359
1360 #[test]
1361 fn test_parse_rustdoc_sections_basic() {
1362 let doc = "Extracts text from a file.\n\n# Arguments\n\n* `path` - The file path.\n\n# Returns\n\nThe extracted text.\n\n# Errors\n\nReturns `KreuzbergError` on failure.";
1363 let sections = parse_rustdoc_sections(doc);
1364 assert_eq!(sections.summary, "Extracts text from a file.");
1365 assert_eq!(sections.arguments.as_deref(), Some("* `path` - The file path."));
1366 assert_eq!(sections.returns.as_deref(), Some("The extracted text."));
1367 assert_eq!(sections.errors.as_deref(), Some("Returns `KreuzbergError` on failure."));
1368 assert!(sections.panics.is_none());
1369 }
1370
1371 #[test]
1372 fn test_parse_rustdoc_sections_example_with_fence() {
1373 let doc = "Run the thing.\n\n# Example\n\n```rust\nlet x = run();\n```";
1374 let sections = parse_rustdoc_sections(doc);
1375 assert_eq!(sections.summary, "Run the thing.");
1376 assert!(sections.example.as_ref().unwrap().contains("```rust"));
1377 assert!(sections.example.as_ref().unwrap().contains("let x = run();"));
1378 }
1379
1380 #[test]
1381 fn test_parse_rustdoc_sections_pound_inside_fence_is_not_a_heading() {
1382 let doc = "Summary.\n\n# Example\n\n```bash\n# install deps\nrun --foo\n```";
1386 let sections = parse_rustdoc_sections(doc);
1387 assert_eq!(sections.summary, "Summary.");
1388 assert!(sections.example.as_ref().unwrap().contains("# install deps"));
1389 }
1390
1391 #[test]
1392 fn test_parse_arguments_bullets_dash_separator() {
1393 let body = "* `path` - The file path.\n* `config` - Optional configuration.";
1394 let pairs = parse_arguments_bullets(body);
1395 assert_eq!(pairs.len(), 2);
1396 assert_eq!(pairs[0], ("path".to_string(), "The file path.".to_string()));
1397 assert_eq!(pairs[1], ("config".to_string(), "Optional configuration.".to_string()));
1398 }
1399
1400 #[test]
1401 fn test_parse_arguments_bullets_continuation_line() {
1402 let body = "* `path` - The file path,\n resolved relative to cwd.\n* `mode` - Open mode.";
1403 let pairs = parse_arguments_bullets(body);
1404 assert_eq!(pairs.len(), 2);
1405 assert_eq!(pairs[0].1, "The file path, resolved relative to cwd.");
1406 }
1407
1408 #[test]
1409 fn test_replace_fence_lang_rust_to_typescript() {
1410 let body = "```rust\nlet x = run();\n```";
1411 let out = replace_fence_lang(body, "typescript");
1412 assert!(out.starts_with("```typescript"));
1413 assert!(out.contains("let x = run();"));
1414 }
1415
1416 #[test]
1417 fn test_replace_fence_lang_preserves_attrs() {
1418 let body = "```rust,no_run\nlet x = run();\n```";
1419 let out = replace_fence_lang(body, "typescript");
1420 assert!(out.starts_with("```typescript,no_run"));
1421 }
1422
1423 #[test]
1424 fn test_replace_fence_lang_no_fence_unchanged() {
1425 let body = "Plain prose with `inline code`.";
1426 let out = replace_fence_lang(body, "typescript");
1427 assert_eq!(out, "Plain prose with `inline code`.");
1428 }
1429
1430 fn fixture_sections() -> RustdocSections {
1431 let doc = "Extracts text from a file.\n\n# Arguments\n\n* `path` - The file path.\n* `config` - Optional configuration.\n\n# Returns\n\nThe extracted text and metadata.\n\n# Errors\n\nReturns an error when the file is unreadable.\n\n# Example\n\n```rust\nlet result = extract(\"file.pdf\")?;\n```";
1432 parse_rustdoc_sections(doc)
1433 }
1434
1435 #[test]
1436 fn test_render_jsdoc_sections() {
1437 let sections = fixture_sections();
1438 let out = render_jsdoc_sections(§ions);
1439 assert!(out.starts_with("Extracts text from a file."));
1440 assert!(out.contains("@param path - The file path."));
1441 assert!(out.contains("@param config - Optional configuration."));
1442 assert!(out.contains("@returns The extracted text and metadata."));
1443 assert!(out.contains("@throws Returns an error when the file is unreadable."));
1444 assert!(!out.contains("@example"), "Rust example must not appear in TSDoc");
1446 assert!(!out.contains("```typescript"));
1447 assert!(!out.contains("```rust"));
1448 }
1449
1450 #[test]
1451 fn test_render_jsdoc_sections_preserves_typescript_example() {
1452 let doc = "Do something.\n\n# Example\n\n```typescript\nconst x = doSomething();\n```";
1453 let sections = parse_rustdoc_sections(doc);
1454 let out = render_jsdoc_sections(§ions);
1455 assert!(out.contains("@example"), "TypeScript example must be preserved");
1456 assert!(out.contains("```typescript"));
1457 }
1458
1459 #[test]
1460 fn test_render_javadoc_sections() {
1461 let sections = fixture_sections();
1462 let out = render_javadoc_sections(§ions, "KreuzbergRsException");
1463 assert!(out.contains("@param path The file path."));
1464 assert!(out.contains("@return The extracted text and metadata."));
1465 assert!(out.contains("@throws KreuzbergRsException Returns an error when the file is unreadable."));
1466 assert!(out.starts_with("Extracts text from a file."));
1469 }
1470
1471 #[test]
1472 fn test_render_csharp_xml_sections() {
1473 let sections = fixture_sections();
1474 let out = render_csharp_xml_sections(§ions, "KreuzbergException");
1475 assert!(out.contains("<summary>\nExtracts text from a file.\n</summary>"));
1476 assert!(out.contains("<param name=\"path\">The file path.</param>"));
1477 assert!(out.contains("<returns>The extracted text and metadata.</returns>"));
1478 assert!(out.contains("<exception cref=\"KreuzbergException\">"));
1479 assert!(out.contains("<example><code language=\"csharp\">"));
1480 assert!(out.contains("let result = extract"));
1481 }
1482
1483 #[test]
1484 fn test_render_phpdoc_sections() {
1485 let sections = fixture_sections();
1486 let out = render_phpdoc_sections(§ions, "KreuzbergException");
1487 assert!(out.contains("@param mixed $path The file path."));
1488 assert!(out.contains("@return The extracted text and metadata."));
1489 assert!(out.contains("@throws KreuzbergException"));
1490 assert!(!out.contains("```php"), "Rust example must not appear in PHPDoc");
1492 assert!(!out.contains("```rust"));
1493 }
1494
1495 #[test]
1496 fn test_render_phpdoc_sections_preserves_php_example() {
1497 let doc = "Do something.\n\n# Example\n\n```php\n$x = doSomething();\n```";
1498 let sections = parse_rustdoc_sections(doc);
1499 let out = render_phpdoc_sections(§ions, "MyException");
1500 assert!(out.contains("```php"), "PHP example must be preserved");
1501 }
1502
1503 #[test]
1504 fn test_render_doxygen_sections() {
1505 let sections = fixture_sections();
1506 let out = render_doxygen_sections(§ions);
1507 assert!(out.contains("\\param path The file path."));
1508 assert!(out.contains("\\return The extracted text and metadata."));
1509 assert!(out.contains("\\code"));
1510 assert!(out.contains("\\endcode"));
1511 }
1512
1513 #[test]
1514 fn test_emit_yard_doc_simple() {
1515 let mut out = String::new();
1516 emit_yard_doc(&mut out, "Simple Ruby documentation", " ");
1517 assert!(out.contains("# Simple Ruby documentation"));
1518 }
1519
1520 #[test]
1521 fn test_emit_yard_doc_empty() {
1522 let mut out = String::new();
1523 emit_yard_doc(&mut out, "", " ");
1524 assert!(out.is_empty());
1525 }
1526
1527 #[test]
1528 fn test_emit_yard_doc_with_sections() {
1529 let mut out = String::new();
1530 let doc = "Extracts text from a file.\n\n# Arguments\n\n* `path` - The file path.\n\n# Returns\n\nThe extracted text.\n\n# Errors\n\nReturns error on failure.";
1531 emit_yard_doc(&mut out, doc, " ");
1532 assert!(out.contains("# Extracts text from a file."));
1533 assert!(out.contains("# @param path The file path."));
1534 assert!(out.contains("# @return The extracted text."));
1535 assert!(out.contains("# @raise Returns error on failure."));
1536 }
1537
1538 #[test]
1539 fn test_emit_c_doxygen_simple_prose() {
1540 let mut out = String::new();
1541 emit_c_doxygen(&mut out, "Free a string.", "");
1542 assert!(out.contains("/// Free a string."), "got: {out}");
1543 }
1544
1545 #[test]
1546 fn test_emit_c_doxygen_with_sections() {
1547 let mut out = String::new();
1548 let doc = "Extract content from a file.\n\n# Arguments\n\n* `path` - Path to the file.\n* `mode` - Read mode.\n\n# Returns\n\nA newly allocated string the caller owns.\n\n# Errors\n\nReturns null when the file is unreadable.";
1549 emit_c_doxygen(&mut out, doc, "");
1550 assert!(out.contains("/// Extract content from a file."));
1551 assert!(out.contains("/// \\param path Path to the file."));
1552 assert!(out.contains("/// \\param mode Read mode."));
1553 assert!(out.contains("/// \\return A newly allocated string the caller owns."));
1554 assert!(out.contains("/// \\note Returns null when the file is unreadable."));
1555 }
1556
1557 #[test]
1558 fn test_emit_c_doxygen_safety_section_maps_to_note() {
1559 let mut out = String::new();
1560 let doc = "Free a buffer.\n\n# Safety\n\nPointer must have been returned by this library.";
1561 emit_c_doxygen(&mut out, doc, "");
1562 assert!(out.contains("/// \\note SAFETY: Pointer must have been returned by this library."));
1563 }
1564
1565 #[test]
1566 fn test_emit_c_doxygen_example_renders_code_fence() {
1567 let mut out = String::new();
1568 let doc = "Demo.\n\n# Example\n\n```rust\nlet x = run();\n```";
1569 emit_c_doxygen(&mut out, doc, "");
1570 assert!(out.contains("/// \\code"));
1571 assert!(out.contains("/// \\endcode"));
1572 assert!(out.contains("let x = run();"));
1573 }
1574
1575 #[test]
1576 fn test_emit_c_doxygen_strips_markdown_links() {
1577 let mut out = String::new();
1578 let doc = "See [the docs](https://example.com/x) for details.";
1579 emit_c_doxygen(&mut out, doc, "");
1580 assert!(
1581 out.contains("the docs (https://example.com/x)"),
1582 "expected flattened link, got: {out}"
1583 );
1584 assert!(!out.contains("](https://"));
1585 }
1586
1587 #[test]
1588 fn test_emit_c_doxygen_word_wraps_long_lines() {
1589 let mut out = String::new();
1590 let long = "a ".repeat(80);
1591 emit_c_doxygen(&mut out, long.trim(), "");
1592 for line in out.lines() {
1593 let body = line.trim_start_matches("/// ");
1596 assert!(body.len() <= 100, "line too long ({}): {line}", body.len());
1597 }
1598 }
1599
1600 #[test]
1601 fn test_emit_c_doxygen_empty_input_is_noop() {
1602 let mut out = String::new();
1603 emit_c_doxygen(&mut out, "", "");
1604 emit_c_doxygen(&mut out, " \n\t ", "");
1605 assert!(out.is_empty());
1606 }
1607
1608 #[test]
1609 fn test_emit_c_doxygen_indent_applied() {
1610 let mut out = String::new();
1611 emit_c_doxygen(&mut out, "Hello.", " ");
1612 assert!(out.starts_with(" /// Hello."));
1613 }
1614
1615 #[test]
1616 fn test_render_yard_sections() {
1617 let sections = fixture_sections();
1618 let out = render_yard_sections(§ions);
1619 assert!(out.contains("@param path The file path."));
1620 assert!(out.contains("@return The extracted text and metadata."));
1621 assert!(out.contains("@raise Returns an error when the file is unreadable."));
1622 assert!(!out.contains("@example"), "Rust example must not appear in YARD");
1624 assert!(!out.contains("```ruby"));
1625 assert!(!out.contains("```rust"));
1626 }
1627
1628 #[test]
1629 fn test_render_yard_sections_preserves_ruby_example() {
1630 let doc = "Do something.\n\n# Example\n\n```ruby\nputs :hi\n```";
1631 let sections = parse_rustdoc_sections(doc);
1632 let out = render_yard_sections(§ions);
1633 assert!(out.contains("@example"), "Ruby example must be preserved");
1634 assert!(out.contains("```ruby"));
1635 }
1636
1637 #[test]
1640 fn example_for_target_rust_fenced_suppressed_for_php() {
1641 let example = "```rust\nlet x = 1;\n```";
1642 assert_eq!(
1643 example_for_target(example, "php"),
1644 None,
1645 "rust-fenced example must be omitted for PHP target"
1646 );
1647 }
1648
1649 #[test]
1650 fn example_for_target_bare_fence_defaults_to_rust_suppressed_for_ruby() {
1651 let example = "```\nlet x = 1;\n```";
1652 assert_eq!(
1653 example_for_target(example, "ruby"),
1654 None,
1655 "bare fence is treated as Rust and must be omitted for Ruby target"
1656 );
1657 }
1658
1659 #[test]
1660 fn example_for_target_php_example_preserved_for_php() {
1661 let example = "```php\n$x = 1;\n```";
1662 let result = example_for_target(example, "php");
1663 assert!(result.is_some(), "PHP example must be preserved for PHP target");
1664 assert!(result.unwrap().contains("```php"));
1665 }
1666
1667 #[test]
1668 fn example_for_target_ruby_example_preserved_for_ruby() {
1669 let example = "```ruby\nputs :hi\n```";
1670 let result = example_for_target(example, "ruby");
1671 assert!(result.is_some(), "Ruby example must be preserved for Ruby target");
1672 assert!(result.unwrap().contains("```ruby"));
1673 }
1674
1675 #[test]
1676 fn render_phpdoc_sections_with_rust_example_emits_no_at_example_block() {
1677 let doc = "Convert HTML.\n\n# Arguments\n\n* `html` - The HTML input.\n\n# Example\n\n```rust\nlet result = convert(html, None)?;\n```";
1678 let sections = parse_rustdoc_sections(doc);
1679 let out = render_phpdoc_sections(§ions, "HtmlToMarkdownException");
1680 assert!(!out.contains("```php"), "no PHP @example block for Rust source");
1681 assert!(!out.contains("```rust"), "raw Rust must not leak into PHPDoc");
1682 assert!(out.contains("@param"), "other sections must still be emitted");
1683 }
1684
1685 #[test]
1688 fn test_emit_kdoc_ktfmt_canonical_short_single_line() {
1689 let mut out = String::new();
1690 emit_kdoc_ktfmt_canonical(&mut out, "Simple doc.", "");
1691 assert_eq!(
1692 out, "/** Simple doc. */\n",
1693 "short single-line comment should collapse to canonical format"
1694 );
1695 }
1696
1697 #[test]
1698 fn test_emit_kdoc_ktfmt_canonical_short_with_indent() {
1699 let mut out = String::new();
1700 emit_kdoc_ktfmt_canonical(&mut out, "Text node (most frequent - 100+ per document)", " ");
1701 assert_eq!(out, " /** Text node (most frequent - 100+ per document) */\n");
1702 }
1703
1704 #[test]
1705 fn test_emit_kdoc_ktfmt_canonical_long_comment_uses_multiline() {
1706 let mut out = String::new();
1707 let long_text = "This is a very long documentation comment that exceeds the 100-character line width limit and should therefore be emitted in multi-line format";
1708 emit_kdoc_ktfmt_canonical(&mut out, long_text, "");
1709 assert!(out.contains("/**\n"), "long comment should start with newline");
1710 assert!(out.contains(" * "), "long comment should use multi-line format");
1711 assert!(out.contains(" */\n"), "long comment should end with newline");
1712 }
1713
1714 #[test]
1715 fn test_emit_kdoc_ktfmt_canonical_multiline_comment() {
1716 let mut out = String::new();
1717 let doc = "First line.\n\nSecond paragraph.";
1718 emit_kdoc_ktfmt_canonical(&mut out, doc, "");
1719 assert!(out.contains("/**\n"), "multi-paragraph should use multi-line format");
1720 assert!(out.contains(" * First line."), "first paragraph preserved");
1721 assert!(out.contains(" *\n"), "blank line preserved");
1722 assert!(out.contains(" * Second paragraph."), "second paragraph preserved");
1723 }
1724
1725 #[test]
1726 fn test_emit_kdoc_ktfmt_canonical_empty_doc() {
1727 let mut out = String::new();
1728 emit_kdoc_ktfmt_canonical(&mut out, "", "");
1729 assert!(out.is_empty(), "empty doc should produce no output");
1730 }
1731
1732 #[test]
1733 fn test_emit_kdoc_ktfmt_canonical_fits_within_100_chars() {
1734 let mut out = String::new();
1735 let content = "a".repeat(93);
1738 emit_kdoc_ktfmt_canonical(&mut out, &content, "");
1739 let line = out.lines().next().unwrap();
1740 assert_eq!(
1741 line.len(),
1742 100,
1743 "should fit exactly at 100 chars and use single-line format"
1744 );
1745 assert!(out.starts_with("/**"), "should use single-line format");
1746 }
1747
1748 #[test]
1749 fn test_emit_kdoc_ktfmt_canonical_exceeds_100_chars() {
1750 let mut out = String::new();
1751 let content = "a".repeat(94);
1753 emit_kdoc_ktfmt_canonical(&mut out, &content, "");
1754 assert!(
1755 out.contains("/**\n"),
1756 "should use multi-line format when exceeding 100 chars"
1757 );
1758 assert!(out.contains(" * "), "multi-line format with ` * ` prefix");
1759 }
1760
1761 #[test]
1762 fn test_emit_kdoc_ktfmt_canonical_respects_indent() {
1763 let mut out = String::new();
1764 let content = "a".repeat(89);
1766 emit_kdoc_ktfmt_canonical(&mut out, &content, " ");
1767 let line = out.lines().next().unwrap();
1768 assert_eq!(line.len(), 100, "should respect indent in 100-char calculation");
1769 assert!(line.starts_with(" /** "), "should include indent");
1770 }
1771
1772 #[test]
1773 fn test_emit_kdoc_ktfmt_canonical_real_world_enum_variant() {
1774 let mut out = String::new();
1775 emit_kdoc_ktfmt_canonical(&mut out, "Text node (most frequent - 100+ per document)", " ");
1776 assert!(out.starts_with(" /** "), "should preserve 4-space indent");
1778 assert!(out.contains(" */\n"), "should end with newline");
1779 let line_count = out.lines().count();
1781 assert_eq!(line_count, 1, "should be single-line format");
1782 }
1783
1784 #[test]
1785 fn test_emit_kdoc_ktfmt_canonical_real_world_data_class_field() {
1786 let mut out = String::new();
1787 let doc = "Heading style to use in Markdown output (ATX `#` or Setext underline).";
1788 emit_kdoc_ktfmt_canonical(&mut out, doc, " ");
1789 let line_count = out.lines().count();
1791 assert_eq!(line_count, 1, "should be single-line format");
1792 assert!(out.starts_with(" /** "), "should have correct indent");
1793 }
1794}