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_dartdoc(out: &mut String, doc: &str, indent: &str) {
208 if doc.is_empty() {
209 return;
210 }
211 for line in doc.lines() {
212 out.push_str(indent);
213 out.push_str("/// ");
214 out.push_str(line);
215 out.push('\n');
216 }
217}
218
219pub fn emit_gleam_doc(out: &mut String, doc: &str, indent: &str) {
222 if doc.is_empty() {
223 return;
224 }
225 for line in doc.lines() {
226 out.push_str(indent);
227 out.push_str("/// ");
228 out.push_str(line);
229 out.push('\n');
230 }
231}
232
233pub fn emit_zig_doc(out: &mut String, doc: &str, indent: &str) {
236 if doc.is_empty() {
237 return;
238 }
239 for line in doc.lines() {
240 out.push_str(indent);
241 out.push_str("/// ");
242 out.push_str(line);
243 out.push('\n');
244 }
245}
246
247pub fn emit_yard_doc(out: &mut String, doc: &str, indent: &str) {
254 if doc.is_empty() {
255 return;
256 }
257 let sections = parse_rustdoc_sections(doc);
258 let any_section = sections.arguments.is_some()
259 || sections.returns.is_some()
260 || sections.errors.is_some()
261 || sections.example.is_some();
262 let body = if any_section {
263 render_yard_sections(§ions)
264 } else {
265 doc.to_string()
266 };
267 for line in body.lines() {
268 out.push_str(indent);
269 out.push_str("# ");
270 out.push_str(line);
271 out.push('\n');
272 }
273}
274
275pub fn render_yard_sections(sections: &RustdocSections) -> String {
285 let mut out = String::new();
286 if !sections.summary.is_empty() {
287 out.push_str(§ions.summary);
288 }
289 if let Some(args) = sections.arguments.as_deref() {
290 for (name, desc) in parse_arguments_bullets(args) {
291 if !out.is_empty() {
292 out.push('\n');
293 }
294 if desc.is_empty() {
295 out.push_str("@param ");
296 out.push_str(&name);
297 } else {
298 out.push_str("@param ");
299 out.push_str(&name);
300 out.push(' ');
301 out.push_str(&desc);
302 }
303 }
304 }
305 if let Some(ret) = sections.returns.as_deref() {
306 if !out.is_empty() {
307 out.push('\n');
308 }
309 out.push_str("@return ");
310 out.push_str(ret.trim());
311 }
312 if let Some(err) = sections.errors.as_deref() {
313 if !out.is_empty() {
314 out.push('\n');
315 }
316 out.push_str("@raise ");
317 out.push_str(err.trim());
318 }
319 if let Some(example) = sections.example.as_deref() {
320 if !out.is_empty() {
321 out.push('\n');
322 }
323 out.push_str("@example\n");
324 out.push_str(&replace_fence_lang(example.trim(), "ruby"));
325 }
326 out
327}
328
329fn escape_javadoc_line(s: &str) -> String {
339 let mut result = String::with_capacity(s.len());
340 let mut chars = s.chars().peekable();
341 while let Some(ch) = chars.next() {
342 if ch == '`' {
343 let mut code = String::new();
344 for c in chars.by_ref() {
345 if c == '`' {
346 break;
347 }
348 code.push(c);
349 }
350 result.push_str("{@code ");
351 result.push_str(&escape_javadoc_html_entities(&code));
352 result.push('}');
353 } else if ch == '<' {
354 result.push_str("<");
355 } else if ch == '>' {
356 result.push_str(">");
357 } else if ch == '&' {
358 result.push_str("&");
359 } else {
360 result.push(ch);
361 }
362 }
363 result
364}
365
366fn escape_javadoc_html_entities(s: &str) -> String {
369 let mut out = String::with_capacity(s.len());
370 for ch in s.chars() {
371 match ch {
372 '<' => out.push_str("<"),
373 '>' => out.push_str(">"),
374 '&' => out.push_str("&"),
375 other => out.push(other),
376 }
377 }
378 out
379}
380
381#[derive(Debug, Default, Clone, PartialEq, Eq)]
392pub struct RustdocSections {
393 pub summary: String,
395 pub arguments: Option<String>,
397 pub returns: Option<String>,
399 pub errors: Option<String>,
401 pub panics: Option<String>,
403 pub safety: Option<String>,
405 pub example: Option<String>,
407}
408
409pub fn parse_rustdoc_sections(doc: &str) -> RustdocSections {
421 if doc.trim().is_empty() {
422 return RustdocSections::default();
423 }
424 let mut summary = String::new();
425 let mut arguments: Option<String> = None;
426 let mut returns: Option<String> = None;
427 let mut errors: Option<String> = None;
428 let mut panics: Option<String> = None;
429 let mut safety: Option<String> = None;
430 let mut example: Option<String> = None;
431 let mut current: Option<&'static str> = None;
432 let mut buf = String::new();
433 let mut in_fence = false;
434 let flush = |target: Option<&'static str>,
435 buf: &mut String,
436 summary: &mut String,
437 arguments: &mut Option<String>,
438 returns: &mut Option<String>,
439 errors: &mut Option<String>,
440 panics: &mut Option<String>,
441 safety: &mut Option<String>,
442 example: &mut Option<String>| {
443 let body = std::mem::take(buf).trim().to_string();
444 if body.is_empty() {
445 return;
446 }
447 match target {
448 None => {
449 if !summary.is_empty() {
450 summary.push('\n');
451 }
452 summary.push_str(&body);
453 }
454 Some("arguments") => *arguments = Some(body),
455 Some("returns") => *returns = Some(body),
456 Some("errors") => *errors = Some(body),
457 Some("panics") => *panics = Some(body),
458 Some("safety") => *safety = Some(body),
459 Some("example") => *example = Some(body),
460 _ => {}
461 }
462 };
463 for line in doc.lines() {
464 let trimmed = line.trim_start();
465 if trimmed.starts_with("```") {
466 in_fence = !in_fence;
467 buf.push_str(line);
468 buf.push('\n');
469 continue;
470 }
471 if !in_fence {
472 if let Some(rest) = trimmed.strip_prefix("# ") {
473 let head = rest.trim().to_ascii_lowercase();
474 let target = match head.as_str() {
475 "arguments" | "args" => Some("arguments"),
476 "returns" => Some("returns"),
477 "errors" => Some("errors"),
478 "panics" => Some("panics"),
479 "safety" => Some("safety"),
480 "example" | "examples" => Some("example"),
481 _ => None,
482 };
483 if target.is_some() {
484 flush(
485 current,
486 &mut buf,
487 &mut summary,
488 &mut arguments,
489 &mut returns,
490 &mut errors,
491 &mut panics,
492 &mut safety,
493 &mut example,
494 );
495 current = target;
496 continue;
497 }
498 }
499 }
500 buf.push_str(line);
501 buf.push('\n');
502 }
503 flush(
504 current,
505 &mut buf,
506 &mut summary,
507 &mut arguments,
508 &mut returns,
509 &mut errors,
510 &mut panics,
511 &mut safety,
512 &mut example,
513 );
514 RustdocSections {
515 summary,
516 arguments,
517 returns,
518 errors,
519 panics,
520 safety,
521 example,
522 }
523}
524
525pub fn parse_arguments_bullets(body: &str) -> Vec<(String, String)> {
535 let mut out: Vec<(String, String)> = Vec::new();
536 for raw in body.lines() {
537 let line = raw.trim_end();
538 let trimmed = line.trim_start();
539 let is_bullet = trimmed.starts_with("* ") || trimmed.starts_with("- ");
540 if is_bullet {
541 let after = &trimmed[2..];
542 let (name, desc) = if let Some(idx) = after.find(" - ") {
544 (after[..idx].trim(), after[idx + 3..].trim())
545 } else if let Some(idx) = after.find(": ") {
546 (after[..idx].trim(), after[idx + 2..].trim())
547 } else if let Some(idx) = after.find(' ') {
548 (after[..idx].trim(), after[idx + 1..].trim())
549 } else {
550 (after.trim(), "")
551 };
552 let name = name.trim_matches('`').trim_matches('*').to_string();
553 out.push((name, desc.to_string()));
554 } else if !trimmed.is_empty() {
555 if let Some(last) = out.last_mut() {
556 if !last.1.is_empty() {
557 last.1.push(' ');
558 }
559 last.1.push_str(trimmed);
560 }
561 }
562 }
563 out
564}
565
566pub fn replace_fence_lang(body: &str, lang_replacement: &str) -> String {
574 let mut out = String::with_capacity(body.len());
575 for line in body.lines() {
576 let trimmed = line.trim_start();
577 if let Some(rest) = trimmed.strip_prefix("```") {
578 let indent = &line[..line.len() - trimmed.len()];
581 let after_lang = rest.find(',').map(|i| &rest[i..]).unwrap_or("");
582 out.push_str(indent);
583 out.push_str("```");
584 out.push_str(lang_replacement);
585 out.push_str(after_lang);
586 out.push('\n');
587 } else {
588 out.push_str(line);
589 out.push('\n');
590 }
591 }
592 out.trim_end_matches('\n').to_string()
593}
594
595pub fn render_jsdoc_sections(sections: &RustdocSections) -> String {
608 let mut out = String::new();
609 if !sections.summary.is_empty() {
610 out.push_str(§ions.summary);
611 }
612 if let Some(args) = sections.arguments.as_deref() {
613 for (name, desc) in parse_arguments_bullets(args) {
614 if !out.is_empty() {
615 out.push('\n');
616 }
617 if desc.is_empty() {
618 out.push_str(&crate::template_env::render(
619 "doc_jsdoc_param.jinja",
620 minijinja::context! { name => &name },
621 ));
622 } else {
623 out.push_str(&crate::template_env::render(
624 "doc_jsdoc_param_desc.jinja",
625 minijinja::context! { name => &name, desc => &desc },
626 ));
627 }
628 }
629 }
630 if let Some(ret) = sections.returns.as_deref() {
631 if !out.is_empty() {
632 out.push('\n');
633 }
634 out.push_str(&crate::template_env::render(
635 "doc_jsdoc_returns.jinja",
636 minijinja::context! { content => ret.trim() },
637 ));
638 }
639 if let Some(err) = sections.errors.as_deref() {
640 if !out.is_empty() {
641 out.push('\n');
642 }
643 out.push_str(&crate::template_env::render(
644 "doc_jsdoc_throws.jinja",
645 minijinja::context! { content => err.trim() },
646 ));
647 }
648 if let Some(example) = sections.example.as_deref() {
649 if !out.is_empty() {
650 out.push('\n');
651 }
652 out.push_str("@example\n");
653 out.push_str(&replace_fence_lang(example.trim(), "typescript"));
654 }
655 out
656}
657
658pub fn render_javadoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
668 let mut out = String::new();
669 if !sections.summary.is_empty() {
670 out.push_str(§ions.summary);
671 }
672 if let Some(args) = sections.arguments.as_deref() {
673 for (name, desc) in parse_arguments_bullets(args) {
674 if !out.is_empty() {
675 out.push('\n');
676 }
677 if desc.is_empty() {
678 out.push_str(&crate::template_env::render(
679 "doc_javadoc_param.jinja",
680 minijinja::context! { name => &name },
681 ));
682 } else {
683 out.push_str(&crate::template_env::render(
684 "doc_javadoc_param_desc.jinja",
685 minijinja::context! { name => &name, desc => &desc },
686 ));
687 }
688 }
689 }
690 if let Some(ret) = sections.returns.as_deref() {
691 if !out.is_empty() {
692 out.push('\n');
693 }
694 out.push_str(&crate::template_env::render(
695 "doc_javadoc_return.jinja",
696 minijinja::context! { content => ret.trim() },
697 ));
698 }
699 if let Some(err) = sections.errors.as_deref() {
700 if !out.is_empty() {
701 out.push('\n');
702 }
703 out.push_str(&crate::template_env::render(
704 "doc_javadoc_throws.jinja",
705 minijinja::context! { throws_class => throws_class, content => err.trim() },
706 ));
707 }
708 out
709}
710
711pub fn render_csharp_xml_sections(sections: &RustdocSections, exception_class: &str) -> String {
720 let mut out = String::new();
721 out.push_str("<summary>\n");
722 let summary = if sections.summary.is_empty() {
723 ""
724 } else {
725 sections.summary.as_str()
726 };
727 for line in summary.lines() {
728 out.push_str(line);
729 out.push('\n');
730 }
731 out.push_str("</summary>");
732 if let Some(args) = sections.arguments.as_deref() {
733 for (name, desc) in parse_arguments_bullets(args) {
734 out.push('\n');
735 if desc.is_empty() {
736 out.push_str(&crate::template_env::render(
737 "doc_csharp_param.jinja",
738 minijinja::context! { name => &name },
739 ));
740 } else {
741 out.push_str(&crate::template_env::render(
742 "doc_csharp_param_desc.jinja",
743 minijinja::context! { name => &name, desc => &desc },
744 ));
745 }
746 }
747 }
748 if let Some(ret) = sections.returns.as_deref() {
749 out.push('\n');
750 out.push_str(&crate::template_env::render(
751 "doc_csharp_returns.jinja",
752 minijinja::context! { content => ret.trim() },
753 ));
754 }
755 if let Some(err) = sections.errors.as_deref() {
756 out.push('\n');
757 out.push_str(&crate::template_env::render(
758 "doc_csharp_exception.jinja",
759 minijinja::context! {
760 exception_class => exception_class,
761 content => err.trim(),
762 },
763 ));
764 }
765 if let Some(example) = sections.example.as_deref() {
766 out.push('\n');
767 out.push_str("<example><code language=\"csharp\">\n");
768 for line in example.lines() {
770 let t = line.trim_start();
771 if t.starts_with("```") {
772 continue;
773 }
774 out.push_str(line);
775 out.push('\n');
776 }
777 out.push_str("</code></example>");
778 }
779 out
780}
781
782pub fn render_phpdoc_sections(sections: &RustdocSections, throws_class: &str) -> String {
789 let mut out = String::new();
790 if !sections.summary.is_empty() {
791 out.push_str(§ions.summary);
792 }
793 if let Some(args) = sections.arguments.as_deref() {
794 for (name, desc) in parse_arguments_bullets(args) {
795 if !out.is_empty() {
796 out.push('\n');
797 }
798 if desc.is_empty() {
799 out.push_str(&crate::template_env::render(
800 "doc_phpdoc_param.jinja",
801 minijinja::context! { name => &name },
802 ));
803 } else {
804 out.push_str(&crate::template_env::render(
805 "doc_phpdoc_param_desc.jinja",
806 minijinja::context! { name => &name, desc => &desc },
807 ));
808 }
809 }
810 }
811 if let Some(ret) = sections.returns.as_deref() {
812 if !out.is_empty() {
813 out.push('\n');
814 }
815 out.push_str(&crate::template_env::render(
816 "doc_phpdoc_return.jinja",
817 minijinja::context! { content => ret.trim() },
818 ));
819 }
820 if let Some(err) = sections.errors.as_deref() {
821 if !out.is_empty() {
822 out.push('\n');
823 }
824 out.push_str(&crate::template_env::render(
825 "doc_phpdoc_throws.jinja",
826 minijinja::context! { throws_class => throws_class, content => err.trim() },
827 ));
828 }
829 if let Some(example) = sections.example.as_deref() {
830 if !out.is_empty() {
831 out.push('\n');
832 }
833 out.push_str(&replace_fence_lang(example.trim(), "php"));
834 }
835 out
836}
837
838pub fn render_doxygen_sections(sections: &RustdocSections) -> String {
845 let mut out = String::new();
846 if !sections.summary.is_empty() {
847 out.push_str(§ions.summary);
848 }
849 if let Some(args) = sections.arguments.as_deref() {
850 for (name, desc) in parse_arguments_bullets(args) {
851 if !out.is_empty() {
852 out.push('\n');
853 }
854 if desc.is_empty() {
855 out.push_str(&crate::template_env::render(
856 "doc_doxygen_param.jinja",
857 minijinja::context! { name => &name },
858 ));
859 } else {
860 out.push_str(&crate::template_env::render(
861 "doc_doxygen_param_desc.jinja",
862 minijinja::context! { name => &name, desc => &desc },
863 ));
864 }
865 }
866 }
867 if let Some(ret) = sections.returns.as_deref() {
868 if !out.is_empty() {
869 out.push('\n');
870 }
871 out.push_str(&crate::template_env::render(
872 "doc_doxygen_return.jinja",
873 minijinja::context! { content => ret.trim() },
874 ));
875 }
876 if let Some(err) = sections.errors.as_deref() {
877 if !out.is_empty() {
878 out.push('\n');
879 }
880 out.push_str(&crate::template_env::render(
881 "doc_doxygen_errors.jinja",
882 minijinja::context! { content => err.trim() },
883 ));
884 }
885 if let Some(example) = sections.example.as_deref() {
886 if !out.is_empty() {
887 out.push('\n');
888 }
889 out.push_str("\\code\n");
890 for line in example.lines() {
891 let t = line.trim_start();
892 if t.starts_with("```") {
893 continue;
894 }
895 out.push_str(line);
896 out.push('\n');
897 }
898 out.push_str("\\endcode");
899 }
900 out
901}
902
903pub fn doc_first_paragraph_joined(doc: &str) -> String {
916 doc.lines()
917 .take_while(|l| !l.trim().is_empty())
918 .map(str::trim)
919 .collect::<Vec<_>>()
920 .join(" ")
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 #[test]
928 fn test_emit_phpdoc() {
929 let mut out = String::new();
930 emit_phpdoc(&mut out, "Simple documentation", " ", "TestException");
931 assert!(out.contains("/**"));
932 assert!(out.contains("Simple documentation"));
933 assert!(out.contains("*/"));
934 }
935
936 #[test]
937 fn test_phpdoc_escaping() {
938 let mut out = String::new();
939 emit_phpdoc(&mut out, "Handle */ sequences", "", "TestException");
940 assert!(out.contains("Handle * / sequences"));
941 }
942
943 #[test]
944 fn test_emit_csharp_doc() {
945 let mut out = String::new();
946 emit_csharp_doc(&mut out, "C# documentation", " ", "TestException");
947 assert!(out.contains("<summary>"));
948 assert!(out.contains("C# documentation"));
949 assert!(out.contains("</summary>"));
950 }
951
952 #[test]
953 fn test_csharp_xml_escaping() {
954 let mut out = String::new();
955 emit_csharp_doc(&mut out, "foo < bar & baz > qux", "", "TestException");
956 assert!(out.contains("foo < bar & baz > qux"));
957 }
958
959 #[test]
960 fn test_emit_elixir_doc() {
961 let mut out = String::new();
962 emit_elixir_doc(&mut out, "Elixir documentation");
963 assert!(out.contains("@doc \"\"\""));
964 assert!(out.contains("Elixir documentation"));
965 assert!(out.contains("\"\"\""));
966 }
967
968 #[test]
969 fn test_elixir_heredoc_escaping() {
970 let mut out = String::new();
971 emit_elixir_doc(&mut out, "Handle \"\"\" sequences");
972 assert!(out.contains("Handle \"\" \" sequences"));
973 }
974
975 #[test]
976 fn test_emit_roxygen() {
977 let mut out = String::new();
978 emit_roxygen(&mut out, "R documentation");
979 assert!(out.contains("#' R documentation"));
980 }
981
982 #[test]
983 fn test_emit_swift_doc() {
984 let mut out = String::new();
985 emit_swift_doc(&mut out, "Swift documentation", " ");
986 assert!(out.contains("/// Swift documentation"));
987 }
988
989 #[test]
990 fn test_emit_javadoc() {
991 let mut out = String::new();
992 emit_javadoc(&mut out, "Java documentation", " ");
993 assert!(out.contains("/**"));
994 assert!(out.contains("Java documentation"));
995 assert!(out.contains("*/"));
996 }
997
998 #[test]
999 fn test_emit_kdoc() {
1000 let mut out = String::new();
1001 emit_kdoc(&mut out, "Kotlin documentation", " ");
1002 assert!(out.contains("/**"));
1003 assert!(out.contains("Kotlin documentation"));
1004 assert!(out.contains("*/"));
1005 }
1006
1007 #[test]
1008 fn test_emit_dartdoc() {
1009 let mut out = String::new();
1010 emit_dartdoc(&mut out, "Dart documentation", " ");
1011 assert!(out.contains("/// Dart documentation"));
1012 }
1013
1014 #[test]
1015 fn test_emit_gleam_doc() {
1016 let mut out = String::new();
1017 emit_gleam_doc(&mut out, "Gleam documentation", " ");
1018 assert!(out.contains("/// Gleam documentation"));
1019 }
1020
1021 #[test]
1022 fn test_emit_zig_doc() {
1023 let mut out = String::new();
1024 emit_zig_doc(&mut out, "Zig documentation", " ");
1025 assert!(out.contains("/// Zig documentation"));
1026 }
1027
1028 #[test]
1029 fn test_empty_doc_skipped() {
1030 let mut out = String::new();
1031 emit_phpdoc(&mut out, "", "", "TestException");
1032 emit_csharp_doc(&mut out, "", "", "TestException");
1033 emit_elixir_doc(&mut out, "");
1034 emit_roxygen(&mut out, "");
1035 emit_kdoc(&mut out, "", "");
1036 emit_dartdoc(&mut out, "", "");
1037 emit_gleam_doc(&mut out, "", "");
1038 emit_zig_doc(&mut out, "", "");
1039 assert!(out.is_empty());
1040 }
1041
1042 #[test]
1043 fn test_doc_first_paragraph_joined_single_line() {
1044 assert_eq!(doc_first_paragraph_joined("Simple doc."), "Simple doc.");
1045 }
1046
1047 #[test]
1048 fn test_doc_first_paragraph_joined_wrapped_sentence() {
1049 let doc = "Convert HTML to Markdown,\nreturning a result.";
1051 assert_eq!(
1052 doc_first_paragraph_joined(doc),
1053 "Convert HTML to Markdown, returning a result."
1054 );
1055 }
1056
1057 #[test]
1058 fn test_doc_first_paragraph_joined_stops_at_blank_line() {
1059 let doc = "First paragraph.\nStill first.\n\nSecond paragraph.";
1060 assert_eq!(doc_first_paragraph_joined(doc), "First paragraph. Still first.");
1061 }
1062
1063 #[test]
1064 fn test_doc_first_paragraph_joined_empty() {
1065 assert_eq!(doc_first_paragraph_joined(""), "");
1066 }
1067
1068 #[test]
1069 fn test_parse_rustdoc_sections_basic() {
1070 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.";
1071 let sections = parse_rustdoc_sections(doc);
1072 assert_eq!(sections.summary, "Extracts text from a file.");
1073 assert_eq!(sections.arguments.as_deref(), Some("* `path` - The file path."));
1074 assert_eq!(sections.returns.as_deref(), Some("The extracted text."));
1075 assert_eq!(sections.errors.as_deref(), Some("Returns `KreuzbergError` on failure."));
1076 assert!(sections.panics.is_none());
1077 }
1078
1079 #[test]
1080 fn test_parse_rustdoc_sections_example_with_fence() {
1081 let doc = "Run the thing.\n\n# Example\n\n```rust\nlet x = run();\n```";
1082 let sections = parse_rustdoc_sections(doc);
1083 assert_eq!(sections.summary, "Run the thing.");
1084 assert!(sections.example.as_ref().unwrap().contains("```rust"));
1085 assert!(sections.example.as_ref().unwrap().contains("let x = run();"));
1086 }
1087
1088 #[test]
1089 fn test_parse_rustdoc_sections_pound_inside_fence_is_not_a_heading() {
1090 let doc = "Summary.\n\n# Example\n\n```bash\n# install deps\nrun --foo\n```";
1094 let sections = parse_rustdoc_sections(doc);
1095 assert_eq!(sections.summary, "Summary.");
1096 assert!(sections.example.as_ref().unwrap().contains("# install deps"));
1097 }
1098
1099 #[test]
1100 fn test_parse_arguments_bullets_dash_separator() {
1101 let body = "* `path` - The file path.\n* `config` - Optional configuration.";
1102 let pairs = parse_arguments_bullets(body);
1103 assert_eq!(pairs.len(), 2);
1104 assert_eq!(pairs[0], ("path".to_string(), "The file path.".to_string()));
1105 assert_eq!(pairs[1], ("config".to_string(), "Optional configuration.".to_string()));
1106 }
1107
1108 #[test]
1109 fn test_parse_arguments_bullets_continuation_line() {
1110 let body = "* `path` - The file path,\n resolved relative to cwd.\n* `mode` - Open mode.";
1111 let pairs = parse_arguments_bullets(body);
1112 assert_eq!(pairs.len(), 2);
1113 assert_eq!(pairs[0].1, "The file path, resolved relative to cwd.");
1114 }
1115
1116 #[test]
1117 fn test_replace_fence_lang_rust_to_typescript() {
1118 let body = "```rust\nlet x = run();\n```";
1119 let out = replace_fence_lang(body, "typescript");
1120 assert!(out.starts_with("```typescript"));
1121 assert!(out.contains("let x = run();"));
1122 }
1123
1124 #[test]
1125 fn test_replace_fence_lang_preserves_attrs() {
1126 let body = "```rust,no_run\nlet x = run();\n```";
1127 let out = replace_fence_lang(body, "typescript");
1128 assert!(out.starts_with("```typescript,no_run"));
1129 }
1130
1131 #[test]
1132 fn test_replace_fence_lang_no_fence_unchanged() {
1133 let body = "Plain prose with `inline code`.";
1134 let out = replace_fence_lang(body, "typescript");
1135 assert_eq!(out, "Plain prose with `inline code`.");
1136 }
1137
1138 fn fixture_sections() -> RustdocSections {
1139 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```";
1140 parse_rustdoc_sections(doc)
1141 }
1142
1143 #[test]
1144 fn test_render_jsdoc_sections() {
1145 let sections = fixture_sections();
1146 let out = render_jsdoc_sections(§ions);
1147 assert!(out.starts_with("Extracts text from a file."));
1148 assert!(out.contains("@param path - The file path."));
1149 assert!(out.contains("@param config - Optional configuration."));
1150 assert!(out.contains("@returns The extracted text and metadata."));
1151 assert!(out.contains("@throws Returns an error when the file is unreadable."));
1152 assert!(out.contains("@example"));
1153 assert!(out.contains("```typescript"));
1154 assert!(!out.contains("```rust"));
1155 }
1156
1157 #[test]
1158 fn test_render_javadoc_sections() {
1159 let sections = fixture_sections();
1160 let out = render_javadoc_sections(§ions, "KreuzbergRsException");
1161 assert!(out.contains("@param path The file path."));
1162 assert!(out.contains("@return The extracted text and metadata."));
1163 assert!(out.contains("@throws KreuzbergRsException Returns an error when the file is unreadable."));
1164 assert!(out.starts_with("Extracts text from a file."));
1167 }
1168
1169 #[test]
1170 fn test_render_csharp_xml_sections() {
1171 let sections = fixture_sections();
1172 let out = render_csharp_xml_sections(§ions, "KreuzbergException");
1173 assert!(out.contains("<summary>\nExtracts text from a file.\n</summary>"));
1174 assert!(out.contains("<param name=\"path\">The file path.</param>"));
1175 assert!(out.contains("<returns>The extracted text and metadata.</returns>"));
1176 assert!(out.contains("<exception cref=\"KreuzbergException\">"));
1177 assert!(out.contains("<example><code language=\"csharp\">"));
1178 assert!(out.contains("let result = extract"));
1179 }
1180
1181 #[test]
1182 fn test_render_phpdoc_sections() {
1183 let sections = fixture_sections();
1184 let out = render_phpdoc_sections(§ions, "KreuzbergException");
1185 assert!(out.contains("@param mixed $path The file path."));
1186 assert!(out.contains("@return The extracted text and metadata."));
1187 assert!(out.contains("@throws KreuzbergException"));
1188 assert!(out.contains("```php"));
1189 }
1190
1191 #[test]
1192 fn test_render_doxygen_sections() {
1193 let sections = fixture_sections();
1194 let out = render_doxygen_sections(§ions);
1195 assert!(out.contains("\\param path The file path."));
1196 assert!(out.contains("\\return The extracted text and metadata."));
1197 assert!(out.contains("\\code"));
1198 assert!(out.contains("\\endcode"));
1199 }
1200
1201 #[test]
1202 fn test_emit_yard_doc_simple() {
1203 let mut out = String::new();
1204 emit_yard_doc(&mut out, "Simple Ruby documentation", " ");
1205 assert!(out.contains("# Simple Ruby documentation"));
1206 }
1207
1208 #[test]
1209 fn test_emit_yard_doc_empty() {
1210 let mut out = String::new();
1211 emit_yard_doc(&mut out, "", " ");
1212 assert!(out.is_empty());
1213 }
1214
1215 #[test]
1216 fn test_emit_yard_doc_with_sections() {
1217 let mut out = String::new();
1218 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.";
1219 emit_yard_doc(&mut out, doc, " ");
1220 assert!(out.contains("# Extracts text from a file."));
1221 assert!(out.contains("# @param path The file path."));
1222 assert!(out.contains("# @return The extracted text."));
1223 assert!(out.contains("# @raise Returns error on failure."));
1224 }
1225
1226 #[test]
1227 fn test_render_yard_sections() {
1228 let sections = fixture_sections();
1229 let out = render_yard_sections(§ions);
1230 assert!(out.contains("@param path The file path."));
1231 assert!(out.contains("@return The extracted text and metadata."));
1232 assert!(out.contains("@raise Returns an error when the file is unreadable."));
1233 assert!(out.contains("@example"));
1234 assert!(out.contains("```ruby"));
1235 assert!(!out.contains("```rust"));
1236 }
1237}