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_zig_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
229fn escape_javadoc_line(s: &str) -> String {
239 let mut result = String::with_capacity(s.len());
240 let mut chars = s.chars().peekable();
241 while let Some(ch) = chars.next() {
242 if ch == '`' {
243 let mut code = String::new();
244 for c in chars.by_ref() {
245 if c == '`' {
246 break;
247 }
248 code.push(c);
249 }
250 result.push_str("{@code ");
251 result.push_str(&escape_javadoc_html_entities(&code));
252 result.push('}');
253 } else if ch == '<' {
254 result.push_str("<");
255 } else if ch == '>' {
256 result.push_str(">");
257 } else if ch == '&' {
258 result.push_str("&");
259 } else {
260 result.push(ch);
261 }
262 }
263 result
264}
265
266fn escape_javadoc_html_entities(s: &str) -> String {
269 let mut out = String::with_capacity(s.len());
270 for ch in s.chars() {
271 match ch {
272 '<' => out.push_str("<"),
273 '>' => out.push_str(">"),
274 '&' => out.push_str("&"),
275 other => out.push(other),
276 }
277 }
278 out
279}
280
281#[derive(Debug, Default, Clone, PartialEq, Eq)]
292pub struct RustdocSections {
293 pub summary: String,
295 pub arguments: Option<String>,
297 pub returns: Option<String>,
299 pub errors: Option<String>,
301 pub panics: Option<String>,
303 pub safety: Option<String>,
305 pub example: Option<String>,
307}
308
309pub fn parse_rustdoc_sections(doc: &str) -> RustdocSections {
321 if doc.trim().is_empty() {
322 return RustdocSections::default();
323 }
324 let mut summary = String::new();
325 let mut arguments: Option<String> = None;
326 let mut returns: Option<String> = None;
327 let mut errors: Option<String> = None;
328 let mut panics: Option<String> = None;
329 let mut safety: Option<String> = None;
330 let mut example: Option<String> = None;
331 let mut current: Option<&'static str> = None;
332 let mut buf = String::new();
333 let mut in_fence = false;
334 let flush = |target: Option<&'static str>,
335 buf: &mut String,
336 summary: &mut String,
337 arguments: &mut Option<String>,
338 returns: &mut Option<String>,
339 errors: &mut Option<String>,
340 panics: &mut Option<String>,
341 safety: &mut Option<String>,
342 example: &mut Option<String>| {
343 let body = std::mem::take(buf).trim().to_string();
344 if body.is_empty() {
345 return;
346 }
347 match target {
348 None => {
349 if !summary.is_empty() {
350 summary.push('\n');
351 }
352 summary.push_str(&body);
353 }
354 Some("arguments") => *arguments = Some(body),
355 Some("returns") => *returns = Some(body),
356 Some("errors") => *errors = Some(body),
357 Some("panics") => *panics = Some(body),
358 Some("safety") => *safety = Some(body),
359 Some("example") => *example = Some(body),
360 _ => {}
361 }
362 };
363 for line in doc.lines() {
364 let trimmed = line.trim_start();
365 if trimmed.starts_with("```") {
366 in_fence = !in_fence;
367 buf.push_str(line);
368 buf.push('\n');
369 continue;
370 }
371 if !in_fence {
372 if let Some(rest) = trimmed.strip_prefix("# ") {
373 let head = rest.trim().to_ascii_lowercase();
374 let target = match head.as_str() {
375 "arguments" | "args" => Some("arguments"),
376 "returns" => Some("returns"),
377 "errors" => Some("errors"),
378 "panics" => Some("panics"),
379 "safety" => Some("safety"),
380 "example" | "examples" => Some("example"),
381 _ => None,
382 };
383 if target.is_some() {
384 flush(
385 current,
386 &mut buf,
387 &mut summary,
388 &mut arguments,
389 &mut returns,
390 &mut errors,
391 &mut panics,
392 &mut safety,
393 &mut example,
394 );
395 current = target;
396 continue;
397 }
398 }
399 }
400 buf.push_str(line);
401 buf.push('\n');
402 }
403 flush(
404 current,
405 &mut buf,
406 &mut summary,
407 &mut arguments,
408 &mut returns,
409 &mut errors,
410 &mut panics,
411 &mut safety,
412 &mut example,
413 );
414 RustdocSections {
415 summary,
416 arguments,
417 returns,
418 errors,
419 panics,
420 safety,
421 example,
422 }
423}
424
425pub fn parse_arguments_bullets(body: &str) -> Vec<(String, String)> {
435 let mut out: Vec<(String, String)> = Vec::new();
436 for raw in body.lines() {
437 let line = raw.trim_end();
438 let trimmed = line.trim_start();
439 let is_bullet = trimmed.starts_with("* ") || trimmed.starts_with("- ");
440 if is_bullet {
441 let after = &trimmed[2..];
442 let (name, desc) = if let Some(idx) = after.find(" - ") {
444 (after[..idx].trim(), after[idx + 3..].trim())
445 } else if let Some(idx) = after.find(": ") {
446 (after[..idx].trim(), after[idx + 2..].trim())
447 } else if let Some(idx) = after.find(' ') {
448 (after[..idx].trim(), after[idx + 1..].trim())
449 } else {
450 (after.trim(), "")
451 };
452 let name = name.trim_matches('`').trim_matches('*').to_string();
453 out.push((name, desc.to_string()));
454 } else if !trimmed.is_empty() {
455 if let Some(last) = out.last_mut() {
456 if !last.1.is_empty() {
457 last.1.push(' ');
458 }
459 last.1.push_str(trimmed);
460 }
461 }
462 }
463 out
464}
465
466pub fn replace_fence_lang(body: &str, lang_replacement: &str) -> String {
474 let mut out = String::with_capacity(body.len());
475 for line in body.lines() {
476 let trimmed = line.trim_start();
477 if let Some(rest) = trimmed.strip_prefix("```") {
478 let indent = &line[..line.len() - trimmed.len()];
481 let after_lang = rest.find(',').map(|i| &rest[i..]).unwrap_or("");
482 out.push_str(indent);
483 out.push_str("```");
484 out.push_str(lang_replacement);
485 out.push_str(after_lang);
486 out.push('\n');
487 } else {
488 out.push_str(line);
489 out.push('\n');
490 }
491 }
492 out.trim_end_matches('\n').to_string()
493}
494
495pub fn render_jsdoc_sections(sections: &RustdocSections) -> String {
508 let mut out = String::new();
509 if !sections.summary.is_empty() {
510 out.push_str(§ions.summary);
511 }
512 if let Some(args) = sections.arguments.as_deref() {
513 for (name, desc) in parse_arguments_bullets(args) {
514 if !out.is_empty() {
515 out.push('\n');
516 }
517 if desc.is_empty() {
518 out.push_str(&crate::template_env::render(
519 "doc_jsdoc_param.jinja",
520 minijinja::context! { name => &name },
521 ));
522 } else {
523 out.push_str(&crate::template_env::render(
524 "doc_jsdoc_param_desc.jinja",
525 minijinja::context! { name => &name, desc => &desc },
526 ));
527 }
528 }
529 }
530 if let Some(ret) = sections.returns.as_deref() {
531 if !out.is_empty() {
532 out.push('\n');
533 }
534 out.push_str(&crate::template_env::render(
535 "doc_jsdoc_returns.jinja",
536 minijinja::context! { content => ret.trim() },
537 ));
538 }
539 if let Some(err) = sections.errors.as_deref() {
540 if !out.is_empty() {
541 out.push('\n');
542 }
543 out.push_str(&crate::template_env::render(
544 "doc_jsdoc_throws.jinja",
545 minijinja::context! { content => err.trim() },
546 ));
547 }
548 if let Some(example) = sections.example.as_deref() {
549 if !out.is_empty() {
550 out.push('\n');
551 }
552 out.push_str("@example\n");
553 out.push_str(&replace_fence_lang(example.trim(), "typescript"));
554 }
555 out
556}
557
558pub fn render_javadoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
568 let mut out = String::new();
569 if !sections.summary.is_empty() {
570 out.push_str(§ions.summary);
571 }
572 if let Some(args) = sections.arguments.as_deref() {
573 for (name, desc) in parse_arguments_bullets(args) {
574 if !out.is_empty() {
575 out.push('\n');
576 }
577 if desc.is_empty() {
578 out.push_str(&crate::template_env::render(
579 "doc_javadoc_param.jinja",
580 minijinja::context! { name => &name },
581 ));
582 } else {
583 out.push_str(&crate::template_env::render(
584 "doc_javadoc_param_desc.jinja",
585 minijinja::context! { name => &name, desc => &desc },
586 ));
587 }
588 }
589 }
590 if let Some(ret) = sections.returns.as_deref() {
591 if !out.is_empty() {
592 out.push('\n');
593 }
594 out.push_str(&crate::template_env::render(
595 "doc_javadoc_return.jinja",
596 minijinja::context! { content => ret.trim() },
597 ));
598 }
599 if let Some(err) = sections.errors.as_deref() {
600 if !out.is_empty() {
601 out.push('\n');
602 }
603 out.push_str(&crate::template_env::render(
604 "doc_javadoc_throws.jinja",
605 minijinja::context! { throws_class => throws_class, content => err.trim() },
606 ));
607 }
608 out
609}
610
611pub fn render_csharp_xml_sections(sections: &RustdocSections, exception_class: &str) -> String {
620 let mut out = String::new();
621 out.push_str("<summary>\n");
622 let summary = if sections.summary.is_empty() {
623 ""
624 } else {
625 sections.summary.as_str()
626 };
627 for line in summary.lines() {
628 out.push_str(line);
629 out.push('\n');
630 }
631 out.push_str("</summary>");
632 if let Some(args) = sections.arguments.as_deref() {
633 for (name, desc) in parse_arguments_bullets(args) {
634 out.push('\n');
635 if desc.is_empty() {
636 out.push_str(&crate::template_env::render(
637 "doc_csharp_param.jinja",
638 minijinja::context! { name => &name },
639 ));
640 } else {
641 out.push_str(&crate::template_env::render(
642 "doc_csharp_param_desc.jinja",
643 minijinja::context! { name => &name, desc => &desc },
644 ));
645 }
646 }
647 }
648 if let Some(ret) = sections.returns.as_deref() {
649 out.push('\n');
650 out.push_str(&crate::template_env::render(
651 "doc_csharp_returns.jinja",
652 minijinja::context! { content => ret.trim() },
653 ));
654 }
655 if let Some(err) = sections.errors.as_deref() {
656 out.push('\n');
657 out.push_str(&crate::template_env::render(
658 "doc_csharp_exception.jinja",
659 minijinja::context! {
660 exception_class => exception_class,
661 content => err.trim(),
662 },
663 ));
664 }
665 if let Some(example) = sections.example.as_deref() {
666 out.push('\n');
667 out.push_str("<example><code language=\"csharp\">\n");
668 for line in example.lines() {
670 let t = line.trim_start();
671 if t.starts_with("```") {
672 continue;
673 }
674 out.push_str(line);
675 out.push('\n');
676 }
677 out.push_str("</code></example>");
678 }
679 out
680}
681
682pub fn render_phpdoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
689 let mut out = String::new();
690 if !sections.summary.is_empty() {
691 out.push_str(§ions.summary);
692 }
693 if let Some(args) = sections.arguments.as_deref() {
694 for (name, desc) in parse_arguments_bullets(args) {
695 if !out.is_empty() {
696 out.push('\n');
697 }
698 if desc.is_empty() {
699 out.push_str(&crate::template_env::render(
700 "doc_phpdoc_param.jinja",
701 minijinja::context! { name => &name },
702 ));
703 } else {
704 out.push_str(&crate::template_env::render(
705 "doc_phpdoc_param_desc.jinja",
706 minijinja::context! { name => &name, desc => &desc },
707 ));
708 }
709 }
710 }
711 if let Some(ret) = sections.returns.as_deref() {
712 if !out.is_empty() {
713 out.push('\n');
714 }
715 out.push_str(&crate::template_env::render(
716 "doc_phpdoc_return.jinja",
717 minijinja::context! { content => ret.trim() },
718 ));
719 }
720 if let Some(err) = sections.errors.as_deref() {
721 if !out.is_empty() {
722 out.push('\n');
723 }
724 out.push_str(&crate::template_env::render(
725 "doc_phpdoc_throws.jinja",
726 minijinja::context! { throws_class => throws_class, content => err.trim() },
727 ));
728 }
729 if let Some(example) = sections.example.as_deref() {
730 if !out.is_empty() {
731 out.push('\n');
732 }
733 out.push_str(&replace_fence_lang(example.trim(), "php"));
734 }
735 out
736}
737
738pub fn render_doxygen_sections(sections: &RustdocSections) -> String {
745 let mut out = String::new();
746 if !sections.summary.is_empty() {
747 out.push_str(§ions.summary);
748 }
749 if let Some(args) = sections.arguments.as_deref() {
750 for (name, desc) in parse_arguments_bullets(args) {
751 if !out.is_empty() {
752 out.push('\n');
753 }
754 if desc.is_empty() {
755 out.push_str(&crate::template_env::render(
756 "doc_doxygen_param.jinja",
757 minijinja::context! { name => &name },
758 ));
759 } else {
760 out.push_str(&crate::template_env::render(
761 "doc_doxygen_param_desc.jinja",
762 minijinja::context! { name => &name, desc => &desc },
763 ));
764 }
765 }
766 }
767 if let Some(ret) = sections.returns.as_deref() {
768 if !out.is_empty() {
769 out.push('\n');
770 }
771 out.push_str(&crate::template_env::render(
772 "doc_doxygen_return.jinja",
773 minijinja::context! { content => ret.trim() },
774 ));
775 }
776 if let Some(err) = sections.errors.as_deref() {
777 if !out.is_empty() {
778 out.push('\n');
779 }
780 out.push_str(&crate::template_env::render(
781 "doc_doxygen_errors.jinja",
782 minijinja::context! { content => err.trim() },
783 ));
784 }
785 if let Some(example) = sections.example.as_deref() {
786 if !out.is_empty() {
787 out.push('\n');
788 }
789 out.push_str("\\code\n");
790 for line in example.lines() {
791 let t = line.trim_start();
792 if t.starts_with("```") {
793 continue;
794 }
795 out.push_str(line);
796 out.push('\n');
797 }
798 out.push_str("\\endcode");
799 }
800 out
801}
802
803pub fn doc_first_paragraph_joined(doc: &str) -> String {
816 doc.lines()
817 .take_while(|l| !l.trim().is_empty())
818 .map(str::trim)
819 .collect::<Vec<_>>()
820 .join(" ")
821}
822
823#[cfg(test)]
824mod tests {
825 use super::*;
826
827 #[test]
828 fn test_emit_phpdoc() {
829 let mut out = String::new();
830 emit_phpdoc(&mut out, "Simple documentation", " ");
831 assert!(out.contains("/**"));
832 assert!(out.contains("Simple documentation"));
833 assert!(out.contains("*/"));
834 }
835
836 #[test]
837 fn test_phpdoc_escaping() {
838 let mut out = String::new();
839 emit_phpdoc(&mut out, "Handle */ sequences", "");
840 assert!(out.contains("Handle * / sequences"));
841 }
842
843 #[test]
844 fn test_emit_csharp_doc() {
845 let mut out = String::new();
846 emit_csharp_doc(&mut out, "C# documentation", " ");
847 assert!(out.contains("<summary>"));
848 assert!(out.contains("C# documentation"));
849 assert!(out.contains("</summary>"));
850 }
851
852 #[test]
853 fn test_csharp_xml_escaping() {
854 let mut out = String::new();
855 emit_csharp_doc(&mut out, "foo < bar & baz > qux", "");
856 assert!(out.contains("foo < bar & baz > qux"));
857 }
858
859 #[test]
860 fn test_emit_elixir_doc() {
861 let mut out = String::new();
862 emit_elixir_doc(&mut out, "Elixir documentation");
863 assert!(out.contains("@doc \"\"\""));
864 assert!(out.contains("Elixir documentation"));
865 assert!(out.contains("\"\"\""));
866 }
867
868 #[test]
869 fn test_elixir_heredoc_escaping() {
870 let mut out = String::new();
871 emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
872 assert!(out.contains("Handle \"\" \" sequences"));
873 }
874
875 #[test]
876 fn test_emit_roxygen() {
877 let mut out = String::new();
878 emit_roxygen(&mut out, "R documentation");
879 assert!(out.contains("#' R documentation"));
880 }
881
882 #[test]
883 fn test_emit_swift_doc() {
884 let mut out = String::new();
885 emit_swift_doc(&mut out, "Swift documentation", " ");
886 assert!(out.contains("/// Swift documentation"));
887 }
888
889 #[test]
890 fn test_emit_javadoc() {
891 let mut out = String::new();
892 emit_javadoc(&mut out, "Java documentation", " ");
893 assert!(out.contains("/**"));
894 assert!(out.contains("Java documentation"));
895 assert!(out.contains("*/"));
896 }
897
898 #[test]
899 fn test_emit_kdoc() {
900 let mut out = String::new();
901 emit_kdoc(&mut out, "Kotlin documentation", " ");
902 assert!(out.contains("/**"));
903 assert!(out.contains("Kotlin documentation"));
904 assert!(out.contains("*/"));
905 }
906
907 #[test]
908 fn test_emit_dartdoc() {
909 let mut out = String::new();
910 emit_dartdoc(&mut out, "Dart documentation", " ");
911 assert!(out.contains("/// Dart documentation"));
912 }
913
914 #[test]
915 fn test_emit_zig_doc() {
916 let mut out = String::new();
917 emit_zig_doc(&mut out, "Zig documentation", " ");
918 assert!(out.contains("/// Zig documentation"));
919 }
920
921 #[test]
922 fn test_empty_doc_skipped() {
923 let mut out = String::new();
924 emit_phpdoc(&mut out, "", "");
925 emit_csharp_doc(&mut out, "", "");
926 emit_elixir_doc(&mut out, "");
927 emit_roxygen(&mut out, "");
928 emit_kdoc(&mut out, "", "");
929 emit_dartdoc(&mut out, "", "");
930 emit_zig_doc(&mut out, "", "");
931 assert!(out.is_empty());
932 }
933
934 #[test]
935 fn test_doc_first_paragraph_joined_single_line() {
936 assert_eq!(doc_first_paragraph_joined("Simple doc."), "Simple doc.");
937 }
938
939 #[test]
940 fn test_doc_first_paragraph_joined_wrapped_sentence() {
941 let doc = "Convert HTML to Markdown,\nreturning a result.";
943 assert_eq!(
944 doc_first_paragraph_joined(doc),
945 "Convert HTML to Markdown, returning a result."
946 );
947 }
948
949 #[test]
950 fn test_doc_first_paragraph_joined_stops_at_blank_line() {
951 let doc = "First paragraph.\nStill first.\n\nSecond paragraph.";
952 assert_eq!(doc_first_paragraph_joined(doc), "First paragraph. Still first.");
953 }
954
955 #[test]
956 fn test_doc_first_paragraph_joined_empty() {
957 assert_eq!(doc_first_paragraph_joined(""), "");
958 }
959
960 #[test]
961 fn test_parse_rustdoc_sections_basic() {
962 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.";
963 let sections = parse_rustdoc_sections(doc);
964 assert_eq!(sections.summary, "Extracts text from a file.");
965 assert_eq!(sections.arguments.as_deref(), Some("* `path` - The file path."));
966 assert_eq!(sections.returns.as_deref(), Some("The extracted text."));
967 assert_eq!(sections.errors.as_deref(), Some("Returns `KreuzbergError` on failure."));
968 assert!(sections.panics.is_none());
969 }
970
971 #[test]
972 fn test_parse_rustdoc_sections_example_with_fence() {
973 let doc = "Run the thing.\n\n# Example\n\n```rust\nlet x = run();\n```";
974 let sections = parse_rustdoc_sections(doc);
975 assert_eq!(sections.summary, "Run the thing.");
976 assert!(sections.example.as_ref().unwrap().contains("```rust"));
977 assert!(sections.example.as_ref().unwrap().contains("let x = run();"));
978 }
979
980 #[test]
981 fn test_parse_rustdoc_sections_pound_inside_fence_is_not_a_heading() {
982 let doc = "Summary.\n\n# Example\n\n```bash\n# install deps\nrun --foo\n```";
986 let sections = parse_rustdoc_sections(doc);
987 assert_eq!(sections.summary, "Summary.");
988 assert!(sections.example.as_ref().unwrap().contains("# install deps"));
989 }
990
991 #[test]
992 fn test_parse_arguments_bullets_dash_separator() {
993 let body = "* `path` - The file path.\n* `config` - Optional configuration.";
994 let pairs = parse_arguments_bullets(body);
995 assert_eq!(pairs.len(), 2);
996 assert_eq!(pairs[0], ("path".to_string(), "The file path.".to_string()));
997 assert_eq!(pairs[1], ("config".to_string(), "Optional configuration.".to_string()));
998 }
999
1000 #[test]
1001 fn test_parse_arguments_bullets_continuation_line() {
1002 let body = "* `path` - The file path,\n resolved relative to cwd.\n* `mode` - Open mode.";
1003 let pairs = parse_arguments_bullets(body);
1004 assert_eq!(pairs.len(), 2);
1005 assert_eq!(pairs[0].1, "The file path, resolved relative to cwd.");
1006 }
1007
1008 #[test]
1009 fn test_replace_fence_lang_rust_to_typescript() {
1010 let body = "```rust\nlet x = run();\n```";
1011 let out = replace_fence_lang(body, "typescript");
1012 assert!(out.starts_with("```typescript"));
1013 assert!(out.contains("let x = run();"));
1014 }
1015
1016 #[test]
1017 fn test_replace_fence_lang_preserves_attrs() {
1018 let body = "```rust,no_run\nlet x = run();\n```";
1019 let out = replace_fence_lang(body, "typescript");
1020 assert!(out.starts_with("```typescript,no_run"));
1021 }
1022
1023 #[test]
1024 fn test_replace_fence_lang_no_fence_unchanged() {
1025 let body = "Plain prose with `inline code`.";
1026 let out = replace_fence_lang(body, "typescript");
1027 assert_eq!(out, "Plain prose with `inline code`.");
1028 }
1029
1030 fn fixture_sections() -> RustdocSections {
1031 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```";
1032 parse_rustdoc_sections(doc)
1033 }
1034
1035 #[test]
1036 fn test_render_jsdoc_sections() {
1037 let sections = fixture_sections();
1038 let out = render_jsdoc_sections(§ions);
1039 assert!(out.starts_with("Extracts text from a file."));
1040 assert!(out.contains("@param path - The file path."));
1041 assert!(out.contains("@param config - Optional configuration."));
1042 assert!(out.contains("@returns The extracted text and metadata."));
1043 assert!(out.contains("@throws Returns an error when the file is unreadable."));
1044 assert!(out.contains("@example"));
1045 assert!(out.contains("```typescript"));
1046 assert!(!out.contains("```rust"));
1047 }
1048
1049 #[test]
1050 fn test_render_javadoc_sections() {
1051 let sections = fixture_sections();
1052 let out = render_javadoc_sections(§ions, "KreuzbergRsException");
1053 assert!(out.contains("@param path The file path."));
1054 assert!(out.contains("@return The extracted text and metadata."));
1055 assert!(out.contains("@throws KreuzbergRsException Returns an error when the file is unreadable."));
1056 assert!(out.starts_with("Extracts text from a file."));
1059 }
1060
1061 #[test]
1062 fn test_render_csharp_xml_sections() {
1063 let sections = fixture_sections();
1064 let out = render_csharp_xml_sections(§ions, "KreuzbergException");
1065 assert!(out.contains("<summary>\nExtracts text from a file.\n</summary>"));
1066 assert!(out.contains("<param name=\"path\">The file path.</param>"));
1067 assert!(out.contains("<returns>The extracted text and metadata.</returns>"));
1068 assert!(out.contains("<exception cref=\"KreuzbergException\">"));
1069 assert!(out.contains("<example><code language=\"csharp\">"));
1070 assert!(out.contains("let result = extract"));
1071 }
1072
1073 #[test]
1074 fn test_render_phpdoc_sections() {
1075 let sections = fixture_sections();
1076 let out = render_phpdoc_sections(§ions, "KreuzbergException");
1077 assert!(out.contains("@param mixed $path The file path."));
1078 assert!(out.contains("@return The extracted text and metadata."));
1079 assert!(out.contains("@throws KreuzbergException"));
1080 assert!(out.contains("```php"));
1081 }
1082
1083 #[test]
1084 fn test_render_doxygen_sections() {
1085 let sections = fixture_sections();
1086 let out = render_doxygen_sections(§ions);
1087 assert!(out.contains("\\param path The file path."));
1088 assert!(out.contains("\\return The extracted text and metadata."));
1089 assert!(out.contains("\\code"));
1090 assert!(out.contains("\\endcode"));
1091 }
1092}