1pub fn emit_phpdoc(out: &mut String, doc: &str, indent: &str) {
11 if doc.is_empty() {
12 return;
13 }
14 let sections = parse_rustdoc_sections(doc);
15 let any_section = sections.arguments.is_some()
16 || sections.returns.is_some()
17 || sections.errors.is_some()
18 || sections.example.is_some();
19 let body = if any_section {
20 render_phpdoc_sections(§ions, "KreuzbergException")
21 } else {
22 doc.to_string()
23 };
24 out.push_str(indent);
25 out.push_str("/**\n");
26 for line in body.lines() {
27 out.push_str(indent);
28 out.push_str(" * ");
29 out.push_str(&escape_phpdoc_line(line));
30 out.push('\n');
31 }
32 out.push_str(indent);
33 out.push_str(" */\n");
34}
35
36fn escape_phpdoc_line(s: &str) -> String {
38 s.replace("*/", "* /")
39}
40
41pub fn emit_csharp_doc(out: &mut String, doc: &str, indent: &str) {
48 if doc.is_empty() {
49 return;
50 }
51 let sections = parse_rustdoc_sections(doc);
52 let any_section = sections.arguments.is_some()
53 || sections.returns.is_some()
54 || sections.errors.is_some()
55 || sections.example.is_some();
56 if !any_section {
57 out.push_str(indent);
59 out.push_str("/// <summary>\n");
60 for line in doc.lines() {
61 out.push_str(indent);
62 out.push_str("/// ");
63 out.push_str(&escape_csharp_doc_line(line));
64 out.push('\n');
65 }
66 out.push_str(indent);
67 out.push_str("/// </summary>\n");
68 return;
69 }
70 let rendered = render_csharp_xml_sections(§ions, "KreuzbergException");
71 for line in rendered.lines() {
72 out.push_str(indent);
73 out.push_str("/// ");
74 out.push_str(line);
79 out.push('\n');
80 }
81}
82
83fn escape_csharp_doc_line(s: &str) -> String {
85 s.replace('&', "&").replace('<', "<").replace('>', ">")
86}
87
88pub fn emit_elixir_doc(out: &mut String, doc: &str) {
91 if doc.is_empty() {
92 return;
93 }
94 out.push_str("@doc \"\"\"\n");
95 for line in doc.lines() {
96 out.push_str(&escape_elixir_doc_line(line));
97 out.push('\n');
98 }
99 out.push_str("\"\"\"\n");
100}
101
102pub fn emit_rustdoc(out: &mut String, doc: &str, indent: &str) {
108 if doc.is_empty() {
109 return;
110 }
111 for line in doc.lines() {
112 out.push_str(indent);
113 out.push_str("/// ");
114 out.push_str(line);
115 out.push('\n');
116 }
117}
118
119fn escape_elixir_doc_line(s: &str) -> String {
121 s.replace("\"\"\"", "\"\" \"")
122}
123
124pub fn emit_roxygen(out: &mut String, doc: &str) {
127 if doc.is_empty() {
128 return;
129 }
130 for line in doc.lines() {
131 out.push_str("#' ");
132 out.push_str(line);
133 out.push('\n');
134 }
135}
136
137pub fn emit_swift_doc(out: &mut String, doc: &str, indent: &str) {
140 if doc.is_empty() {
141 return;
142 }
143 for line in doc.lines() {
144 out.push_str(indent);
145 out.push_str("/// ");
146 out.push_str(line);
147 out.push('\n');
148 }
149}
150
151pub fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
155 if doc.is_empty() {
156 return;
157 }
158 out.push_str(indent);
159 out.push_str("/**\n");
160 for line in doc.lines() {
161 let escaped = escape_javadoc_line(line);
162 let trimmed = escaped.trim_end();
163 if trimmed.is_empty() {
164 out.push_str(indent);
165 out.push_str(" *\n");
166 } else {
167 out.push_str(indent);
168 out.push_str(" * ");
169 out.push_str(trimmed);
170 out.push('\n');
171 }
172 }
173 out.push_str(indent);
174 out.push_str(" */\n");
175}
176
177pub fn emit_kdoc(out: &mut String, doc: &str, indent: &str) {
180 if doc.is_empty() {
181 return;
182 }
183 out.push_str(indent);
184 out.push_str("/**\n");
185 for line in doc.lines() {
186 let trimmed = line.trim_end();
187 if trimmed.is_empty() {
188 out.push_str(indent);
189 out.push_str(" *\n");
190 } else {
191 out.push_str(indent);
192 out.push_str(" * ");
193 out.push_str(trimmed);
194 out.push('\n');
195 }
196 }
197 out.push_str(indent);
198 out.push_str(" */\n");
199}
200
201pub fn emit_dartdoc(out: &mut String, doc: &str, indent: &str) {
204 if doc.is_empty() {
205 return;
206 }
207 for line in doc.lines() {
208 out.push_str(indent);
209 out.push_str("/// ");
210 out.push_str(line);
211 out.push('\n');
212 }
213}
214
215pub fn emit_gleam_doc(out: &mut String, doc: &str, indent: &str) {
218 if doc.is_empty() {
219 return;
220 }
221 for line in doc.lines() {
222 out.push_str(indent);
223 out.push_str("/// ");
224 out.push_str(line);
225 out.push('\n');
226 }
227}
228
229pub fn emit_zig_doc(out: &mut String, doc: &str, indent: &str) {
232 if doc.is_empty() {
233 return;
234 }
235 for line in doc.lines() {
236 out.push_str(indent);
237 out.push_str("/// ");
238 out.push_str(line);
239 out.push('\n');
240 }
241}
242
243fn escape_javadoc_line(s: &str) -> String {
253 let mut result = String::with_capacity(s.len());
254 let mut chars = s.chars().peekable();
255 while let Some(ch) = chars.next() {
256 if ch == '`' {
257 let mut code = String::new();
258 for c in chars.by_ref() {
259 if c == '`' {
260 break;
261 }
262 code.push(c);
263 }
264 result.push_str("{@code ");
265 result.push_str(&escape_javadoc_html_entities(&code));
266 result.push('}');
267 } else if ch == '<' {
268 result.push_str("<");
269 } else if ch == '>' {
270 result.push_str(">");
271 } else if ch == '&' {
272 result.push_str("&");
273 } else {
274 result.push(ch);
275 }
276 }
277 result
278}
279
280fn escape_javadoc_html_entities(s: &str) -> String {
283 let mut out = String::with_capacity(s.len());
284 for ch in s.chars() {
285 match ch {
286 '<' => out.push_str("<"),
287 '>' => out.push_str(">"),
288 '&' => out.push_str("&"),
289 other => out.push(other),
290 }
291 }
292 out
293}
294
295#[derive(Debug, Default, Clone, PartialEq, Eq)]
306pub struct RustdocSections {
307 pub summary: String,
309 pub arguments: Option<String>,
311 pub returns: Option<String>,
313 pub errors: Option<String>,
315 pub panics: Option<String>,
317 pub safety: Option<String>,
319 pub example: Option<String>,
321}
322
323pub fn parse_rustdoc_sections(doc: &str) -> RustdocSections {
335 if doc.trim().is_empty() {
336 return RustdocSections::default();
337 }
338 let mut summary = String::new();
339 let mut arguments: Option<String> = None;
340 let mut returns: Option<String> = None;
341 let mut errors: Option<String> = None;
342 let mut panics: Option<String> = None;
343 let mut safety: Option<String> = None;
344 let mut example: Option<String> = None;
345 let mut current: Option<&'static str> = None;
346 let mut buf = String::new();
347 let mut in_fence = false;
348 let flush = |target: Option<&'static str>,
349 buf: &mut String,
350 summary: &mut String,
351 arguments: &mut Option<String>,
352 returns: &mut Option<String>,
353 errors: &mut Option<String>,
354 panics: &mut Option<String>,
355 safety: &mut Option<String>,
356 example: &mut Option<String>| {
357 let body = std::mem::take(buf).trim().to_string();
358 if body.is_empty() {
359 return;
360 }
361 match target {
362 None => {
363 if !summary.is_empty() {
364 summary.push('\n');
365 }
366 summary.push_str(&body);
367 }
368 Some("arguments") => *arguments = Some(body),
369 Some("returns") => *returns = Some(body),
370 Some("errors") => *errors = Some(body),
371 Some("panics") => *panics = Some(body),
372 Some("safety") => *safety = Some(body),
373 Some("example") => *example = Some(body),
374 _ => {}
375 }
376 };
377 for line in doc.lines() {
378 let trimmed = line.trim_start();
379 if trimmed.starts_with("```") {
380 in_fence = !in_fence;
381 buf.push_str(line);
382 buf.push('\n');
383 continue;
384 }
385 if !in_fence {
386 if let Some(rest) = trimmed.strip_prefix("# ") {
387 let head = rest.trim().to_ascii_lowercase();
388 let target = match head.as_str() {
389 "arguments" | "args" => Some("arguments"),
390 "returns" => Some("returns"),
391 "errors" => Some("errors"),
392 "panics" => Some("panics"),
393 "safety" => Some("safety"),
394 "example" | "examples" => Some("example"),
395 _ => None,
396 };
397 if target.is_some() {
398 flush(
399 current,
400 &mut buf,
401 &mut summary,
402 &mut arguments,
403 &mut returns,
404 &mut errors,
405 &mut panics,
406 &mut safety,
407 &mut example,
408 );
409 current = target;
410 continue;
411 }
412 }
413 }
414 buf.push_str(line);
415 buf.push('\n');
416 }
417 flush(
418 current,
419 &mut buf,
420 &mut summary,
421 &mut arguments,
422 &mut returns,
423 &mut errors,
424 &mut panics,
425 &mut safety,
426 &mut example,
427 );
428 RustdocSections {
429 summary,
430 arguments,
431 returns,
432 errors,
433 panics,
434 safety,
435 example,
436 }
437}
438
439pub fn parse_arguments_bullets(body: &str) -> Vec<(String, String)> {
449 let mut out: Vec<(String, String)> = Vec::new();
450 for raw in body.lines() {
451 let line = raw.trim_end();
452 let trimmed = line.trim_start();
453 let is_bullet = trimmed.starts_with("* ") || trimmed.starts_with("- ");
454 if is_bullet {
455 let after = &trimmed[2..];
456 let (name, desc) = if let Some(idx) = after.find(" - ") {
458 (after[..idx].trim(), after[idx + 3..].trim())
459 } else if let Some(idx) = after.find(": ") {
460 (after[..idx].trim(), after[idx + 2..].trim())
461 } else if let Some(idx) = after.find(' ') {
462 (after[..idx].trim(), after[idx + 1..].trim())
463 } else {
464 (after.trim(), "")
465 };
466 let name = name.trim_matches('`').trim_matches('*').to_string();
467 out.push((name, desc.to_string()));
468 } else if !trimmed.is_empty() {
469 if let Some(last) = out.last_mut() {
470 if !last.1.is_empty() {
471 last.1.push(' ');
472 }
473 last.1.push_str(trimmed);
474 }
475 }
476 }
477 out
478}
479
480pub fn replace_fence_lang(body: &str, lang_replacement: &str) -> String {
488 let mut out = String::with_capacity(body.len());
489 for line in body.lines() {
490 let trimmed = line.trim_start();
491 if let Some(rest) = trimmed.strip_prefix("```") {
492 let indent = &line[..line.len() - trimmed.len()];
495 let after_lang = rest.find(',').map(|i| &rest[i..]).unwrap_or("");
496 out.push_str(indent);
497 out.push_str("```");
498 out.push_str(lang_replacement);
499 out.push_str(after_lang);
500 out.push('\n');
501 } else {
502 out.push_str(line);
503 out.push('\n');
504 }
505 }
506 out.trim_end_matches('\n').to_string()
507}
508
509pub fn render_jsdoc_sections(sections: &RustdocSections) -> String {
522 let mut out = String::new();
523 if !sections.summary.is_empty() {
524 out.push_str(§ions.summary);
525 }
526 if let Some(args) = sections.arguments.as_deref() {
527 for (name, desc) in parse_arguments_bullets(args) {
528 if !out.is_empty() {
529 out.push('\n');
530 }
531 if desc.is_empty() {
532 out.push_str(&crate::template_env::render(
533 "doc_jsdoc_param.jinja",
534 minijinja::context! { name => &name },
535 ));
536 } else {
537 out.push_str(&crate::template_env::render(
538 "doc_jsdoc_param_desc.jinja",
539 minijinja::context! { name => &name, desc => &desc },
540 ));
541 }
542 }
543 }
544 if let Some(ret) = sections.returns.as_deref() {
545 if !out.is_empty() {
546 out.push('\n');
547 }
548 out.push_str(&crate::template_env::render(
549 "doc_jsdoc_returns.jinja",
550 minijinja::context! { content => ret.trim() },
551 ));
552 }
553 if let Some(err) = sections.errors.as_deref() {
554 if !out.is_empty() {
555 out.push('\n');
556 }
557 out.push_str(&crate::template_env::render(
558 "doc_jsdoc_throws.jinja",
559 minijinja::context! { content => err.trim() },
560 ));
561 }
562 if let Some(example) = sections.example.as_deref() {
563 if !out.is_empty() {
564 out.push('\n');
565 }
566 out.push_str("@example\n");
567 out.push_str(&replace_fence_lang(example.trim(), "typescript"));
568 }
569 out
570}
571
572pub fn render_javadoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
582 let mut out = String::new();
583 if !sections.summary.is_empty() {
584 out.push_str(§ions.summary);
585 }
586 if let Some(args) = sections.arguments.as_deref() {
587 for (name, desc) in parse_arguments_bullets(args) {
588 if !out.is_empty() {
589 out.push('\n');
590 }
591 if desc.is_empty() {
592 out.push_str(&crate::template_env::render(
593 "doc_javadoc_param.jinja",
594 minijinja::context! { name => &name },
595 ));
596 } else {
597 out.push_str(&crate::template_env::render(
598 "doc_javadoc_param_desc.jinja",
599 minijinja::context! { name => &name, desc => &desc },
600 ));
601 }
602 }
603 }
604 if let Some(ret) = sections.returns.as_deref() {
605 if !out.is_empty() {
606 out.push('\n');
607 }
608 out.push_str(&crate::template_env::render(
609 "doc_javadoc_return.jinja",
610 minijinja::context! { content => ret.trim() },
611 ));
612 }
613 if let Some(err) = sections.errors.as_deref() {
614 if !out.is_empty() {
615 out.push('\n');
616 }
617 out.push_str(&crate::template_env::render(
618 "doc_javadoc_throws.jinja",
619 minijinja::context! { throws_class => throws_class, content => err.trim() },
620 ));
621 }
622 out
623}
624
625pub fn render_csharp_xml_sections(sections: &RustdocSections, exception_class: &str) -> String {
634 let mut out = String::new();
635 out.push_str("<summary>\n");
636 let summary = if sections.summary.is_empty() {
637 ""
638 } else {
639 sections.summary.as_str()
640 };
641 for line in summary.lines() {
642 out.push_str(line);
643 out.push('\n');
644 }
645 out.push_str("</summary>");
646 if let Some(args) = sections.arguments.as_deref() {
647 for (name, desc) in parse_arguments_bullets(args) {
648 out.push('\n');
649 if desc.is_empty() {
650 out.push_str(&crate::template_env::render(
651 "doc_csharp_param.jinja",
652 minijinja::context! { name => &name },
653 ));
654 } else {
655 out.push_str(&crate::template_env::render(
656 "doc_csharp_param_desc.jinja",
657 minijinja::context! { name => &name, desc => &desc },
658 ));
659 }
660 }
661 }
662 if let Some(ret) = sections.returns.as_deref() {
663 out.push('\n');
664 out.push_str(&crate::template_env::render(
665 "doc_csharp_returns.jinja",
666 minijinja::context! { content => ret.trim() },
667 ));
668 }
669 if let Some(err) = sections.errors.as_deref() {
670 out.push('\n');
671 out.push_str(&crate::template_env::render(
672 "doc_csharp_exception.jinja",
673 minijinja::context! {
674 exception_class => exception_class,
675 content => err.trim(),
676 },
677 ));
678 }
679 if let Some(example) = sections.example.as_deref() {
680 out.push('\n');
681 out.push_str("<example><code language=\"csharp\">\n");
682 for line in example.lines() {
684 let t = line.trim_start();
685 if t.starts_with("```") {
686 continue;
687 }
688 out.push_str(line);
689 out.push('\n');
690 }
691 out.push_str("</code></example>");
692 }
693 out
694}
695
696pub fn render_phpdoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
703 let mut out = String::new();
704 if !sections.summary.is_empty() {
705 out.push_str(§ions.summary);
706 }
707 if let Some(args) = sections.arguments.as_deref() {
708 for (name, desc) in parse_arguments_bullets(args) {
709 if !out.is_empty() {
710 out.push('\n');
711 }
712 if desc.is_empty() {
713 out.push_str(&crate::template_env::render(
714 "doc_phpdoc_param.jinja",
715 minijinja::context! { name => &name },
716 ));
717 } else {
718 out.push_str(&crate::template_env::render(
719 "doc_phpdoc_param_desc.jinja",
720 minijinja::context! { name => &name, desc => &desc },
721 ));
722 }
723 }
724 }
725 if let Some(ret) = sections.returns.as_deref() {
726 if !out.is_empty() {
727 out.push('\n');
728 }
729 out.push_str(&crate::template_env::render(
730 "doc_phpdoc_return.jinja",
731 minijinja::context! { content => ret.trim() },
732 ));
733 }
734 if let Some(err) = sections.errors.as_deref() {
735 if !out.is_empty() {
736 out.push('\n');
737 }
738 out.push_str(&crate::template_env::render(
739 "doc_phpdoc_throws.jinja",
740 minijinja::context! { throws_class => throws_class, content => err.trim() },
741 ));
742 }
743 if let Some(example) = sections.example.as_deref() {
744 if !out.is_empty() {
745 out.push('\n');
746 }
747 out.push_str(&replace_fence_lang(example.trim(), "php"));
748 }
749 out
750}
751
752pub fn render_doxygen_sections(sections: &RustdocSections) -> String {
759 let mut out = String::new();
760 if !sections.summary.is_empty() {
761 out.push_str(§ions.summary);
762 }
763 if let Some(args) = sections.arguments.as_deref() {
764 for (name, desc) in parse_arguments_bullets(args) {
765 if !out.is_empty() {
766 out.push('\n');
767 }
768 if desc.is_empty() {
769 out.push_str(&crate::template_env::render(
770 "doc_doxygen_param.jinja",
771 minijinja::context! { name => &name },
772 ));
773 } else {
774 out.push_str(&crate::template_env::render(
775 "doc_doxygen_param_desc.jinja",
776 minijinja::context! { name => &name, desc => &desc },
777 ));
778 }
779 }
780 }
781 if let Some(ret) = sections.returns.as_deref() {
782 if !out.is_empty() {
783 out.push('\n');
784 }
785 out.push_str(&crate::template_env::render(
786 "doc_doxygen_return.jinja",
787 minijinja::context! { content => ret.trim() },
788 ));
789 }
790 if let Some(err) = sections.errors.as_deref() {
791 if !out.is_empty() {
792 out.push('\n');
793 }
794 out.push_str(&crate::template_env::render(
795 "doc_doxygen_errors.jinja",
796 minijinja::context! { content => err.trim() },
797 ));
798 }
799 if let Some(example) = sections.example.as_deref() {
800 if !out.is_empty() {
801 out.push('\n');
802 }
803 out.push_str("\\code\n");
804 for line in example.lines() {
805 let t = line.trim_start();
806 if t.starts_with("```") {
807 continue;
808 }
809 out.push_str(line);
810 out.push('\n');
811 }
812 out.push_str("\\endcode");
813 }
814 out
815}
816
817pub fn doc_first_paragraph_joined(doc: &str) -> String {
830 doc.lines()
831 .take_while(|l| !l.trim().is_empty())
832 .map(str::trim)
833 .collect::<Vec<_>>()
834 .join(" ")
835}
836
837#[cfg(test)]
838mod tests {
839 use super::*;
840
841 #[test]
842 fn test_emit_phpdoc() {
843 let mut out = String::new();
844 emit_phpdoc(&mut out, "Simple documentation", " ");
845 assert!(out.contains("/**"));
846 assert!(out.contains("Simple documentation"));
847 assert!(out.contains("*/"));
848 }
849
850 #[test]
851 fn test_phpdoc_escaping() {
852 let mut out = String::new();
853 emit_phpdoc(&mut out, "Handle */ sequences", "");
854 assert!(out.contains("Handle * / sequences"));
855 }
856
857 #[test]
858 fn test_emit_csharp_doc() {
859 let mut out = String::new();
860 emit_csharp_doc(&mut out, "C# documentation", " ");
861 assert!(out.contains("<summary>"));
862 assert!(out.contains("C# documentation"));
863 assert!(out.contains("</summary>"));
864 }
865
866 #[test]
867 fn test_csharp_xml_escaping() {
868 let mut out = String::new();
869 emit_csharp_doc(&mut out, "foo < bar & baz > qux", "");
870 assert!(out.contains("foo < bar & baz > qux"));
871 }
872
873 #[test]
874 fn test_emit_elixir_doc() {
875 let mut out = String::new();
876 emit_elixir_doc(&mut out, "Elixir documentation");
877 assert!(out.contains("@doc \"\"\""));
878 assert!(out.contains("Elixir documentation"));
879 assert!(out.contains("\"\"\""));
880 }
881
882 #[test]
883 fn test_elixir_heredoc_escaping() {
884 let mut out = String::new();
885 emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
886 assert!(out.contains("Handle \"\" \" sequences"));
887 }
888
889 #[test]
890 fn test_emit_roxygen() {
891 let mut out = String::new();
892 emit_roxygen(&mut out, "R documentation");
893 assert!(out.contains("#' R documentation"));
894 }
895
896 #[test]
897 fn test_emit_swift_doc() {
898 let mut out = String::new();
899 emit_swift_doc(&mut out, "Swift documentation", " ");
900 assert!(out.contains("/// Swift documentation"));
901 }
902
903 #[test]
904 fn test_emit_javadoc() {
905 let mut out = String::new();
906 emit_javadoc(&mut out, "Java documentation", " ");
907 assert!(out.contains("/**"));
908 assert!(out.contains("Java documentation"));
909 assert!(out.contains("*/"));
910 }
911
912 #[test]
913 fn test_emit_kdoc() {
914 let mut out = String::new();
915 emit_kdoc(&mut out, "Kotlin documentation", " ");
916 assert!(out.contains("/**"));
917 assert!(out.contains("Kotlin documentation"));
918 assert!(out.contains("*/"));
919 }
920
921 #[test]
922 fn test_emit_dartdoc() {
923 let mut out = String::new();
924 emit_dartdoc(&mut out, "Dart documentation", " ");
925 assert!(out.contains("/// Dart documentation"));
926 }
927
928 #[test]
929 fn test_emit_gleam_doc() {
930 let mut out = String::new();
931 emit_gleam_doc(&mut out, "Gleam documentation", " ");
932 assert!(out.contains("/// Gleam documentation"));
933 }
934
935 #[test]
936 fn test_emit_zig_doc() {
937 let mut out = String::new();
938 emit_zig_doc(&mut out, "Zig documentation", " ");
939 assert!(out.contains("/// Zig documentation"));
940 }
941
942 #[test]
943 fn test_empty_doc_skipped() {
944 let mut out = String::new();
945 emit_phpdoc(&mut out, "", "");
946 emit_csharp_doc(&mut out, "", "");
947 emit_elixir_doc(&mut out, "");
948 emit_roxygen(&mut out, "");
949 emit_kdoc(&mut out, "", "");
950 emit_dartdoc(&mut out, "", "");
951 emit_gleam_doc(&mut out, "", "");
952 emit_zig_doc(&mut out, "", "");
953 assert!(out.is_empty());
954 }
955
956 #[test]
957 fn test_doc_first_paragraph_joined_single_line() {
958 assert_eq!(doc_first_paragraph_joined("Simple doc."), "Simple doc.");
959 }
960
961 #[test]
962 fn test_doc_first_paragraph_joined_wrapped_sentence() {
963 let doc = "Convert HTML to Markdown,\nreturning a result.";
965 assert_eq!(
966 doc_first_paragraph_joined(doc),
967 "Convert HTML to Markdown, returning a result."
968 );
969 }
970
971 #[test]
972 fn test_doc_first_paragraph_joined_stops_at_blank_line() {
973 let doc = "First paragraph.\nStill first.\n\nSecond paragraph.";
974 assert_eq!(doc_first_paragraph_joined(doc), "First paragraph. Still first.");
975 }
976
977 #[test]
978 fn test_doc_first_paragraph_joined_empty() {
979 assert_eq!(doc_first_paragraph_joined(""), "");
980 }
981
982 #[test]
983 fn test_parse_rustdoc_sections_basic() {
984 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.";
985 let sections = parse_rustdoc_sections(doc);
986 assert_eq!(sections.summary, "Extracts text from a file.");
987 assert_eq!(sections.arguments.as_deref(), Some("* `path` - The file path."));
988 assert_eq!(sections.returns.as_deref(), Some("The extracted text."));
989 assert_eq!(sections.errors.as_deref(), Some("Returns `KreuzbergError` on failure."));
990 assert!(sections.panics.is_none());
991 }
992
993 #[test]
994 fn test_parse_rustdoc_sections_example_with_fence() {
995 let doc = "Run the thing.\n\n# Example\n\n```rust\nlet x = run();\n```";
996 let sections = parse_rustdoc_sections(doc);
997 assert_eq!(sections.summary, "Run the thing.");
998 assert!(sections.example.as_ref().unwrap().contains("```rust"));
999 assert!(sections.example.as_ref().unwrap().contains("let x = run();"));
1000 }
1001
1002 #[test]
1003 fn test_parse_rustdoc_sections_pound_inside_fence_is_not_a_heading() {
1004 let doc = "Summary.\n\n# Example\n\n```bash\n# install deps\nrun --foo\n```";
1008 let sections = parse_rustdoc_sections(doc);
1009 assert_eq!(sections.summary, "Summary.");
1010 assert!(sections.example.as_ref().unwrap().contains("# install deps"));
1011 }
1012
1013 #[test]
1014 fn test_parse_arguments_bullets_dash_separator() {
1015 let body = "* `path` - The file path.\n* `config` - Optional configuration.";
1016 let pairs = parse_arguments_bullets(body);
1017 assert_eq!(pairs.len(), 2);
1018 assert_eq!(pairs[0], ("path".to_string(), "The file path.".to_string()));
1019 assert_eq!(pairs[1], ("config".to_string(), "Optional configuration.".to_string()));
1020 }
1021
1022 #[test]
1023 fn test_parse_arguments_bullets_continuation_line() {
1024 let body = "* `path` - The file path,\n resolved relative to cwd.\n* `mode` - Open mode.";
1025 let pairs = parse_arguments_bullets(body);
1026 assert_eq!(pairs.len(), 2);
1027 assert_eq!(pairs[0].1, "The file path, resolved relative to cwd.");
1028 }
1029
1030 #[test]
1031 fn test_replace_fence_lang_rust_to_typescript() {
1032 let body = "```rust\nlet x = run();\n```";
1033 let out = replace_fence_lang(body, "typescript");
1034 assert!(out.starts_with("```typescript"));
1035 assert!(out.contains("let x = run();"));
1036 }
1037
1038 #[test]
1039 fn test_replace_fence_lang_preserves_attrs() {
1040 let body = "```rust,no_run\nlet x = run();\n```";
1041 let out = replace_fence_lang(body, "typescript");
1042 assert!(out.starts_with("```typescript,no_run"));
1043 }
1044
1045 #[test]
1046 fn test_replace_fence_lang_no_fence_unchanged() {
1047 let body = "Plain prose with `inline code`.";
1048 let out = replace_fence_lang(body, "typescript");
1049 assert_eq!(out, "Plain prose with `inline code`.");
1050 }
1051
1052 fn fixture_sections() -> RustdocSections {
1053 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```";
1054 parse_rustdoc_sections(doc)
1055 }
1056
1057 #[test]
1058 fn test_render_jsdoc_sections() {
1059 let sections = fixture_sections();
1060 let out = render_jsdoc_sections(§ions);
1061 assert!(out.starts_with("Extracts text from a file."));
1062 assert!(out.contains("@param path - The file path."));
1063 assert!(out.contains("@param config - Optional configuration."));
1064 assert!(out.contains("@returns The extracted text and metadata."));
1065 assert!(out.contains("@throws Returns an error when the file is unreadable."));
1066 assert!(out.contains("@example"));
1067 assert!(out.contains("```typescript"));
1068 assert!(!out.contains("```rust"));
1069 }
1070
1071 #[test]
1072 fn test_render_javadoc_sections() {
1073 let sections = fixture_sections();
1074 let out = render_javadoc_sections(§ions, "KreuzbergRsException");
1075 assert!(out.contains("@param path The file path."));
1076 assert!(out.contains("@return The extracted text and metadata."));
1077 assert!(out.contains("@throws KreuzbergRsException Returns an error when the file is unreadable."));
1078 assert!(out.starts_with("Extracts text from a file."));
1081 }
1082
1083 #[test]
1084 fn test_render_csharp_xml_sections() {
1085 let sections = fixture_sections();
1086 let out = render_csharp_xml_sections(§ions, "KreuzbergException");
1087 assert!(out.contains("<summary>\nExtracts text from a file.\n</summary>"));
1088 assert!(out.contains("<param name=\"path\">The file path.</param>"));
1089 assert!(out.contains("<returns>The extracted text and metadata.</returns>"));
1090 assert!(out.contains("<exception cref=\"KreuzbergException\">"));
1091 assert!(out.contains("<example><code language=\"csharp\">"));
1092 assert!(out.contains("let result = extract"));
1093 }
1094
1095 #[test]
1096 fn test_render_phpdoc_sections() {
1097 let sections = fixture_sections();
1098 let out = render_phpdoc_sections(§ions, "KreuzbergException");
1099 assert!(out.contains("@param mixed $path The file path."));
1100 assert!(out.contains("@return The extracted text and metadata."));
1101 assert!(out.contains("@throws KreuzbergException"));
1102 assert!(out.contains("```php"));
1103 }
1104
1105 #[test]
1106 fn test_render_doxygen_sections() {
1107 let sections = fixture_sections();
1108 let out = render_doxygen_sections(§ions);
1109 assert!(out.contains("\\param path The file path."));
1110 assert!(out.contains("\\return The extracted text and metadata."));
1111 assert!(out.contains("\\code"));
1112 assert!(out.contains("\\endcode"));
1113 }
1114}