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(&format!("@param {name}"));
533 } else {
534 out.push_str(&format!("@param {name} - {desc}"));
535 }
536 }
537 }
538 if let Some(ret) = sections.returns.as_deref() {
539 if !out.is_empty() {
540 out.push('\n');
541 }
542 out.push_str(&format!("@returns {}", ret.trim()));
543 }
544 if let Some(err) = sections.errors.as_deref() {
545 if !out.is_empty() {
546 out.push('\n');
547 }
548 out.push_str(&format!("@throws {}", err.trim()));
549 }
550 if let Some(example) = sections.example.as_deref() {
551 if !out.is_empty() {
552 out.push('\n');
553 }
554 out.push_str("@example\n");
555 out.push_str(&replace_fence_lang(example.trim(), "typescript"));
556 }
557 out
558}
559
560pub fn render_javadoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
570 let mut out = String::new();
571 if !sections.summary.is_empty() {
572 out.push_str(§ions.summary);
573 }
574 if let Some(args) = sections.arguments.as_deref() {
575 for (name, desc) in parse_arguments_bullets(args) {
576 if !out.is_empty() {
577 out.push('\n');
578 }
579 if desc.is_empty() {
580 out.push_str(&format!("@param {name}"));
581 } else {
582 out.push_str(&format!("@param {name} {desc}"));
583 }
584 }
585 }
586 if let Some(ret) = sections.returns.as_deref() {
587 if !out.is_empty() {
588 out.push('\n');
589 }
590 out.push_str(&format!("@return {}", ret.trim()));
591 }
592 if let Some(err) = sections.errors.as_deref() {
593 if !out.is_empty() {
594 out.push('\n');
595 }
596 out.push_str(&format!("@throws {throws_class} {}", err.trim()));
597 }
598 out
599}
600
601pub fn render_csharp_xml_sections(sections: &RustdocSections, exception_class: &str) -> String {
610 let mut out = String::new();
611 out.push_str("<summary>\n");
612 let summary = if sections.summary.is_empty() {
613 ""
614 } else {
615 sections.summary.as_str()
616 };
617 for line in summary.lines() {
618 out.push_str(line);
619 out.push('\n');
620 }
621 out.push_str("</summary>");
622 if let Some(args) = sections.arguments.as_deref() {
623 for (name, desc) in parse_arguments_bullets(args) {
624 out.push('\n');
625 if desc.is_empty() {
626 out.push_str(&format!("<param name=\"{name}\"></param>"));
627 } else {
628 out.push_str(&format!("<param name=\"{name}\">{desc}</param>"));
629 }
630 }
631 }
632 if let Some(ret) = sections.returns.as_deref() {
633 out.push('\n');
634 out.push_str(&format!("<returns>{}</returns>", ret.trim()));
635 }
636 if let Some(err) = sections.errors.as_deref() {
637 out.push('\n');
638 out.push_str(&format!(
639 "<exception cref=\"{exception_class}\">{}</exception>",
640 err.trim()
641 ));
642 }
643 if let Some(example) = sections.example.as_deref() {
644 out.push('\n');
645 out.push_str("<example><code language=\"csharp\">\n");
646 for line in example.lines() {
648 let t = line.trim_start();
649 if t.starts_with("```") {
650 continue;
651 }
652 out.push_str(line);
653 out.push('\n');
654 }
655 out.push_str("</code></example>");
656 }
657 out
658}
659
660pub fn render_phpdoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
667 let mut out = String::new();
668 if !sections.summary.is_empty() {
669 out.push_str(§ions.summary);
670 }
671 if let Some(args) = sections.arguments.as_deref() {
672 for (name, desc) in parse_arguments_bullets(args) {
673 if !out.is_empty() {
674 out.push('\n');
675 }
676 if desc.is_empty() {
677 out.push_str(&format!("@param mixed ${name}"));
678 } else {
679 out.push_str(&format!("@param mixed ${name} {desc}"));
680 }
681 }
682 }
683 if let Some(ret) = sections.returns.as_deref() {
684 if !out.is_empty() {
685 out.push('\n');
686 }
687 out.push_str(&format!("@return {}", ret.trim()));
688 }
689 if let Some(err) = sections.errors.as_deref() {
690 if !out.is_empty() {
691 out.push('\n');
692 }
693 out.push_str(&format!("@throws {throws_class} {}", err.trim()));
694 }
695 if let Some(example) = sections.example.as_deref() {
696 if !out.is_empty() {
697 out.push('\n');
698 }
699 out.push_str(&replace_fence_lang(example.trim(), "php"));
700 }
701 out
702}
703
704pub fn render_doxygen_sections(sections: &RustdocSections) -> String {
711 let mut out = String::new();
712 if !sections.summary.is_empty() {
713 out.push_str(§ions.summary);
714 }
715 if let Some(args) = sections.arguments.as_deref() {
716 for (name, desc) in parse_arguments_bullets(args) {
717 if !out.is_empty() {
718 out.push('\n');
719 }
720 if desc.is_empty() {
721 out.push_str(&format!("\\param {name}"));
722 } else {
723 out.push_str(&format!("\\param {name} {desc}"));
724 }
725 }
726 }
727 if let Some(ret) = sections.returns.as_deref() {
728 if !out.is_empty() {
729 out.push('\n');
730 }
731 out.push_str(&format!("\\return {}", ret.trim()));
732 }
733 if let Some(err) = sections.errors.as_deref() {
734 if !out.is_empty() {
735 out.push('\n');
736 }
737 out.push_str(&format!("Errors: {}", err.trim()));
738 }
739 if let Some(example) = sections.example.as_deref() {
740 if !out.is_empty() {
741 out.push('\n');
742 }
743 out.push_str("\\code\n");
744 for line in example.lines() {
745 let t = line.trim_start();
746 if t.starts_with("```") {
747 continue;
748 }
749 out.push_str(line);
750 out.push('\n');
751 }
752 out.push_str("\\endcode");
753 }
754 out
755}
756
757pub fn doc_first_paragraph_joined(doc: &str) -> String {
770 doc.lines()
771 .take_while(|l| !l.trim().is_empty())
772 .map(str::trim)
773 .collect::<Vec<_>>()
774 .join(" ")
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[test]
782 fn test_emit_phpdoc() {
783 let mut out = String::new();
784 emit_phpdoc(&mut out, "Simple documentation", " ");
785 assert!(out.contains("/**"));
786 assert!(out.contains("Simple documentation"));
787 assert!(out.contains("*/"));
788 }
789
790 #[test]
791 fn test_phpdoc_escaping() {
792 let mut out = String::new();
793 emit_phpdoc(&mut out, "Handle */ sequences", "");
794 assert!(out.contains("Handle * / sequences"));
795 }
796
797 #[test]
798 fn test_emit_csharp_doc() {
799 let mut out = String::new();
800 emit_csharp_doc(&mut out, "C# documentation", " ");
801 assert!(out.contains("<summary>"));
802 assert!(out.contains("C# documentation"));
803 assert!(out.contains("</summary>"));
804 }
805
806 #[test]
807 fn test_csharp_xml_escaping() {
808 let mut out = String::new();
809 emit_csharp_doc(&mut out, "foo < bar & baz > qux", "");
810 assert!(out.contains("foo < bar & baz > qux"));
811 }
812
813 #[test]
814 fn test_emit_elixir_doc() {
815 let mut out = String::new();
816 emit_elixir_doc(&mut out, "Elixir documentation");
817 assert!(out.contains("@doc \"\"\""));
818 assert!(out.contains("Elixir documentation"));
819 assert!(out.contains("\"\"\""));
820 }
821
822 #[test]
823 fn test_elixir_heredoc_escaping() {
824 let mut out = String::new();
825 emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
826 assert!(out.contains("Handle \"\" \" sequences"));
827 }
828
829 #[test]
830 fn test_emit_roxygen() {
831 let mut out = String::new();
832 emit_roxygen(&mut out, "R documentation");
833 assert!(out.contains("#' R documentation"));
834 }
835
836 #[test]
837 fn test_emit_swift_doc() {
838 let mut out = String::new();
839 emit_swift_doc(&mut out, "Swift documentation", " ");
840 assert!(out.contains("/// Swift documentation"));
841 }
842
843 #[test]
844 fn test_emit_javadoc() {
845 let mut out = String::new();
846 emit_javadoc(&mut out, "Java documentation", " ");
847 assert!(out.contains("/**"));
848 assert!(out.contains("Java documentation"));
849 assert!(out.contains("*/"));
850 }
851
852 #[test]
853 fn test_emit_kdoc() {
854 let mut out = String::new();
855 emit_kdoc(&mut out, "Kotlin documentation", " ");
856 assert!(out.contains("/**"));
857 assert!(out.contains("Kotlin documentation"));
858 assert!(out.contains("*/"));
859 }
860
861 #[test]
862 fn test_emit_dartdoc() {
863 let mut out = String::new();
864 emit_dartdoc(&mut out, "Dart documentation", " ");
865 assert!(out.contains("/// Dart documentation"));
866 }
867
868 #[test]
869 fn test_emit_gleam_doc() {
870 let mut out = String::new();
871 emit_gleam_doc(&mut out, "Gleam documentation", " ");
872 assert!(out.contains("/// Gleam documentation"));
873 }
874
875 #[test]
876 fn test_emit_zig_doc() {
877 let mut out = String::new();
878 emit_zig_doc(&mut out, "Zig documentation", " ");
879 assert!(out.contains("/// Zig documentation"));
880 }
881
882 #[test]
883 fn test_empty_doc_skipped() {
884 let mut out = String::new();
885 emit_phpdoc(&mut out, "", "");
886 emit_csharp_doc(&mut out, "", "");
887 emit_elixir_doc(&mut out, "");
888 emit_roxygen(&mut out, "");
889 emit_kdoc(&mut out, "", "");
890 emit_dartdoc(&mut out, "", "");
891 emit_gleam_doc(&mut out, "", "");
892 emit_zig_doc(&mut out, "", "");
893 assert!(out.is_empty());
894 }
895
896 #[test]
897 fn test_doc_first_paragraph_joined_single_line() {
898 assert_eq!(doc_first_paragraph_joined("Simple doc."), "Simple doc.");
899 }
900
901 #[test]
902 fn test_doc_first_paragraph_joined_wrapped_sentence() {
903 let doc = "Convert HTML to Markdown,\nreturning a result.";
905 assert_eq!(
906 doc_first_paragraph_joined(doc),
907 "Convert HTML to Markdown, returning a result."
908 );
909 }
910
911 #[test]
912 fn test_doc_first_paragraph_joined_stops_at_blank_line() {
913 let doc = "First paragraph.\nStill first.\n\nSecond paragraph.";
914 assert_eq!(doc_first_paragraph_joined(doc), "First paragraph. Still first.");
915 }
916
917 #[test]
918 fn test_doc_first_paragraph_joined_empty() {
919 assert_eq!(doc_first_paragraph_joined(""), "");
920 }
921
922 #[test]
923 fn test_parse_rustdoc_sections_basic() {
924 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.";
925 let sections = parse_rustdoc_sections(doc);
926 assert_eq!(sections.summary, "Extracts text from a file.");
927 assert_eq!(sections.arguments.as_deref(), Some("* `path` - The file path."));
928 assert_eq!(sections.returns.as_deref(), Some("The extracted text."));
929 assert_eq!(sections.errors.as_deref(), Some("Returns `KreuzbergError` on failure."));
930 assert!(sections.panics.is_none());
931 }
932
933 #[test]
934 fn test_parse_rustdoc_sections_example_with_fence() {
935 let doc = "Run the thing.\n\n# Example\n\n```rust\nlet x = run();\n```";
936 let sections = parse_rustdoc_sections(doc);
937 assert_eq!(sections.summary, "Run the thing.");
938 assert!(sections.example.as_ref().unwrap().contains("```rust"));
939 assert!(sections.example.as_ref().unwrap().contains("let x = run();"));
940 }
941
942 #[test]
943 fn test_parse_rustdoc_sections_pound_inside_fence_is_not_a_heading() {
944 let doc = "Summary.\n\n# Example\n\n```bash\n# install deps\nrun --foo\n```";
948 let sections = parse_rustdoc_sections(doc);
949 assert_eq!(sections.summary, "Summary.");
950 assert!(sections.example.as_ref().unwrap().contains("# install deps"));
951 }
952
953 #[test]
954 fn test_parse_arguments_bullets_dash_separator() {
955 let body = "* `path` - The file path.\n* `config` - Optional configuration.";
956 let pairs = parse_arguments_bullets(body);
957 assert_eq!(pairs.len(), 2);
958 assert_eq!(pairs[0], ("path".to_string(), "The file path.".to_string()));
959 assert_eq!(pairs[1], ("config".to_string(), "Optional configuration.".to_string()));
960 }
961
962 #[test]
963 fn test_parse_arguments_bullets_continuation_line() {
964 let body = "* `path` - The file path,\n resolved relative to cwd.\n* `mode` - Open mode.";
965 let pairs = parse_arguments_bullets(body);
966 assert_eq!(pairs.len(), 2);
967 assert_eq!(pairs[0].1, "The file path, resolved relative to cwd.");
968 }
969
970 #[test]
971 fn test_replace_fence_lang_rust_to_typescript() {
972 let body = "```rust\nlet x = run();\n```";
973 let out = replace_fence_lang(body, "typescript");
974 assert!(out.starts_with("```typescript"));
975 assert!(out.contains("let x = run();"));
976 }
977
978 #[test]
979 fn test_replace_fence_lang_preserves_attrs() {
980 let body = "```rust,no_run\nlet x = run();\n```";
981 let out = replace_fence_lang(body, "typescript");
982 assert!(out.starts_with("```typescript,no_run"));
983 }
984
985 #[test]
986 fn test_replace_fence_lang_no_fence_unchanged() {
987 let body = "Plain prose with `inline code`.";
988 let out = replace_fence_lang(body, "typescript");
989 assert_eq!(out, "Plain prose with `inline code`.");
990 }
991
992 fn fixture_sections() -> RustdocSections {
993 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```";
994 parse_rustdoc_sections(doc)
995 }
996
997 #[test]
998 fn test_render_jsdoc_sections() {
999 let sections = fixture_sections();
1000 let out = render_jsdoc_sections(§ions);
1001 assert!(out.starts_with("Extracts text from a file."));
1002 assert!(out.contains("@param path - The file path."));
1003 assert!(out.contains("@param config - Optional configuration."));
1004 assert!(out.contains("@returns The extracted text and metadata."));
1005 assert!(out.contains("@throws Returns an error when the file is unreadable."));
1006 assert!(out.contains("@example"));
1007 assert!(out.contains("```typescript"));
1008 assert!(!out.contains("```rust"));
1009 }
1010
1011 #[test]
1012 fn test_render_javadoc_sections() {
1013 let sections = fixture_sections();
1014 let out = render_javadoc_sections(§ions, "KreuzbergRsException");
1015 assert!(out.contains("@param path The file path."));
1016 assert!(out.contains("@return The extracted text and metadata."));
1017 assert!(out.contains("@throws KreuzbergRsException Returns an error when the file is unreadable."));
1018 assert!(out.starts_with("Extracts text from a file."));
1021 }
1022
1023 #[test]
1024 fn test_render_csharp_xml_sections() {
1025 let sections = fixture_sections();
1026 let out = render_csharp_xml_sections(§ions, "KreuzbergException");
1027 assert!(out.contains("<summary>\nExtracts text from a file.\n</summary>"));
1028 assert!(out.contains("<param name=\"path\">The file path.</param>"));
1029 assert!(out.contains("<returns>The extracted text and metadata.</returns>"));
1030 assert!(out.contains("<exception cref=\"KreuzbergException\">"));
1031 assert!(out.contains("<example><code language=\"csharp\">"));
1032 assert!(out.contains("let result = extract"));
1033 }
1034
1035 #[test]
1036 fn test_render_phpdoc_sections() {
1037 let sections = fixture_sections();
1038 let out = render_phpdoc_sections(§ions, "KreuzbergException");
1039 assert!(out.contains("@param mixed $path The file path."));
1040 assert!(out.contains("@return The extracted text and metadata."));
1041 assert!(out.contains("@throws KreuzbergException"));
1042 assert!(out.contains("```php"));
1043 }
1044
1045 #[test]
1046 fn test_render_doxygen_sections() {
1047 let sections = fixture_sections();
1048 let out = render_doxygen_sections(§ions);
1049 assert!(out.contains("\\param path The file path."));
1050 assert!(out.contains("\\return The extracted text and metadata."));
1051 assert!(out.contains("\\code"));
1052 assert!(out.contains("\\endcode"));
1053 }
1054}