use super::sanitize::{
DocTarget, sanitize_rust_idioms, sanitize_rust_idioms_keep_sections, wrap_bare_bracket_references,
};
use super::sections::{
RustdocSections, example_for_target, parse_arguments_bullets, parse_rustdoc_sections, render_csharp_xml_sections,
render_phpdoc_sections,
};
pub fn emit_phpdoc(out: &mut String, doc: &str, indent: &str, exception_class: &str) {
if doc.is_empty() {
return;
}
let sanitized = sanitize_rust_idioms(doc, DocTarget::PhpDoc);
let sections = parse_rustdoc_sections(&sanitized);
let any_section = sections.arguments.is_some()
|| sections.returns.is_some()
|| sections.errors.is_some()
|| sections.example.is_some();
let body = if any_section {
render_phpdoc_sections(§ions, exception_class)
} else {
sanitized
};
out.push_str(indent);
out.push_str("/**\n");
for line in body.lines() {
out.push_str(indent);
out.push_str(" * ");
out.push_str(&escape_phpdoc_line(line));
out.push('\n');
}
out.push_str(indent);
out.push_str(" */\n");
}
fn escape_phpdoc_line(s: &str) -> String {
s.replace("*/", "* /")
}
pub fn emit_csharp_doc(out: &mut String, doc: &str, indent: &str, exception_class: &str) {
if doc.is_empty() {
return;
}
let raw_sections = parse_rustdoc_sections(doc);
let sections = RustdocSections {
summary: sanitize_rust_idioms_keep_sections(&raw_sections.summary, DocTarget::CSharpDoc),
arguments: raw_sections
.arguments
.as_deref()
.map(|s| sanitize_rust_idioms_keep_sections(s, DocTarget::CSharpDoc)),
returns: raw_sections
.returns
.as_deref()
.map(|s| sanitize_rust_idioms_keep_sections(s, DocTarget::CSharpDoc)),
errors: raw_sections
.errors
.as_deref()
.map(|s| sanitize_rust_idioms_keep_sections(s, DocTarget::CSharpDoc)),
panics: raw_sections
.panics
.as_deref()
.map(|s| sanitize_rust_idioms_keep_sections(s, DocTarget::CSharpDoc)),
safety: raw_sections
.safety
.as_deref()
.map(|s| sanitize_rust_idioms_keep_sections(s, DocTarget::CSharpDoc)),
example: None,
};
let any_section = sections.arguments.is_some()
|| sections.returns.is_some()
|| sections.errors.is_some()
|| sections.example.is_some();
if !any_section {
out.push_str(indent);
out.push_str("/// <summary>\n");
for line in sections.summary.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
out.push_str(indent);
out.push_str("/// </summary>\n");
return;
}
let rendered = render_csharp_xml_sections(§ions, exception_class);
for line in rendered.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
pub fn emit_elixir_doc(out: &mut String, doc: &str) {
if doc.is_empty() {
return;
}
out.push_str("@doc \"\"\"\n");
for line in doc.lines() {
out.push_str(&escape_elixir_doc_line(line));
out.push('\n');
}
out.push_str("\"\"\"\n");
}
pub fn emit_rustdoc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
for line in doc.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
fn escape_elixir_doc_line(s: &str) -> String {
s.replace("\"\"\"", "\"\" \"")
}
pub fn emit_roxygen(out: &mut String, doc: &str) {
if doc.is_empty() {
return;
}
for line in doc.lines() {
out.push_str("#' ");
out.push_str(line);
out.push('\n');
}
}
pub fn emit_swift_doc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
for line in doc.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
pub fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
out.push_str(indent);
out.push_str("/**\n");
for line in doc.lines() {
let escaped = escape_javadoc_line(line);
let trimmed = escaped.trim_end();
if trimmed.is_empty() {
out.push_str(indent);
out.push_str(" *\n");
} else {
out.push_str(indent);
out.push_str(" * ");
out.push_str(trimmed);
out.push('\n');
}
}
out.push_str(indent);
out.push_str(" */\n");
}
pub fn emit_kdoc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
out.push_str(indent);
out.push_str("/**\n");
for line in doc.lines() {
let trimmed = line.trim_end();
if trimmed.is_empty() {
out.push_str(indent);
out.push_str(" *\n");
} else {
out.push_str(indent);
out.push_str(" * ");
out.push_str(&escape_kdoc_line(trimmed));
out.push('\n');
}
}
out.push_str(indent);
out.push_str(" */\n");
}
pub(crate) fn escape_kdoc_line(s: &str) -> String {
s.replace("*/", "* /").replace("/*", "/ *")
}
pub fn emit_kdoc_ktfmt_canonical(out: &mut String, doc: &str, indent: &str) {
const KTFMT_LINE_WIDTH: usize = 100;
if doc.is_empty() {
return;
}
let lines: Vec<&str> = doc.lines().collect();
let is_short_single_paragraph = lines.len() == 1 && !lines[0].contains('\n');
if is_short_single_paragraph {
let trimmed = lines[0].trim();
let escaped = escape_kdoc_line(trimmed);
let single_line_len = indent.len() + 4 + escaped.len() + 3; if single_line_len <= KTFMT_LINE_WIDTH {
out.push_str(indent);
out.push_str("/** ");
out.push_str(&escaped);
out.push_str(" */\n");
return;
}
}
out.push_str(indent);
out.push_str("/**\n");
for line in lines {
let trimmed = line.trim_end();
if trimmed.is_empty() {
out.push_str(indent);
out.push_str(" *\n");
} else {
out.push_str(indent);
out.push_str(" * ");
out.push_str(&escape_kdoc_line(trimmed));
out.push('\n');
}
}
out.push_str(indent);
out.push_str(" */\n");
}
pub fn emit_dartdoc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
for line in doc.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
pub fn emit_gleam_doc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
for line in doc.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
pub fn emit_c_doxygen(out: &mut String, doc: &str, indent: &str) {
if doc.trim().is_empty() {
return;
}
let sections = parse_rustdoc_sections(doc);
let any_section = sections.arguments.is_some()
|| sections.returns.is_some()
|| sections.errors.is_some()
|| sections.safety.is_some()
|| sections.example.is_some();
let mut body = if any_section {
render_doxygen_sections_with_notes(§ions)
} else {
sections.summary.clone()
};
body = strip_markdown_links(&body);
body = wrap_bare_bracket_references(&body);
let wrapped = word_wrap(&body, DOXYGEN_WRAP_WIDTH);
for line in wrapped.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
const DOXYGEN_WRAP_WIDTH: usize = 100;
fn render_doxygen_sections_with_notes(sections: &RustdocSections) -> String {
let mut out = String::new();
if !sections.summary.is_empty() {
out.push_str(§ions.summary);
}
if let Some(args) = sections.arguments.as_deref() {
for (name, desc) in parse_arguments_bullets(args) {
if !out.is_empty() {
out.push('\n');
}
if desc.is_empty() {
out.push_str("\\param ");
out.push_str(&name);
} else {
out.push_str("\\param ");
out.push_str(&name);
out.push(' ');
out.push_str(&desc);
}
}
}
if let Some(ret) = sections.returns.as_deref() {
if !out.is_empty() {
out.push('\n');
}
out.push_str("\\return ");
out.push_str(ret.trim());
}
if let Some(err) = sections.errors.as_deref() {
if !out.is_empty() {
out.push('\n');
}
out.push_str("\\note ");
out.push_str(err.trim());
}
if let Some(safety) = sections.safety.as_deref() {
if !out.is_empty() {
out.push('\n');
}
out.push_str("\\note SAFETY: ");
out.push_str(safety.trim());
}
if let Some(example) = sections.example.as_deref() {
if !out.is_empty() {
out.push('\n');
}
out.push_str("\\code\n");
for line in example.lines() {
let t = line.trim_start();
if t.starts_with("```") {
continue;
}
out.push_str(line);
out.push('\n');
}
out.push_str("\\endcode");
}
out
}
fn strip_markdown_links(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' {
if let Some(close) = bytes[i + 1..].iter().position(|&b| b == b']') {
let text_end = i + 1 + close;
if text_end + 1 < bytes.len() && bytes[text_end + 1] == b'(' {
if let Some(paren_close) = bytes[text_end + 2..].iter().position(|&b| b == b')') {
let url_start = text_end + 2;
let url_end = url_start + paren_close;
let text = &s[i + 1..text_end];
let url = &s[url_start..url_end];
out.push_str(text);
out.push_str(" (");
out.push_str(url);
out.push(')');
i = url_end + 1;
continue;
}
}
}
}
out.push(bytes[i] as char);
i += 1;
}
out
}
fn word_wrap(input: &str, width: usize) -> String {
let mut out = String::with_capacity(input.len());
let mut in_code = false;
for raw in input.lines() {
let trimmed = raw.trim_start();
if trimmed.starts_with("\\code") {
in_code = true;
out.push_str(raw);
out.push('\n');
continue;
}
if trimmed.starts_with("\\endcode") {
in_code = false;
out.push_str(raw);
out.push('\n');
continue;
}
if in_code || trimmed.starts_with("```") {
out.push_str(raw);
out.push('\n');
continue;
}
if raw.len() <= width {
out.push_str(raw);
out.push('\n');
continue;
}
let mut current = String::with_capacity(width);
for word in raw.split_whitespace() {
if current.is_empty() {
current.push_str(word);
} else if current.len() + 1 + word.len() > width {
out.push_str(¤t);
out.push('\n');
current.clear();
current.push_str(word);
} else {
current.push(' ');
current.push_str(word);
}
}
if !current.is_empty() {
out.push_str(¤t);
out.push('\n');
}
}
out.trim_end_matches('\n').to_string()
}
pub fn emit_zig_doc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
for line in doc.lines() {
out.push_str(indent);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
pub fn emit_yard_doc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
let sections = parse_rustdoc_sections(doc);
let any_section = sections.arguments.is_some()
|| sections.returns.is_some()
|| sections.errors.is_some()
|| sections.example.is_some();
let body = if any_section {
render_yard_sections(§ions)
} else {
doc.to_string()
};
for line in body.lines() {
out.push_str(indent);
out.push_str("# ");
out.push_str(line);
out.push('\n');
}
}
pub fn render_yard_sections(sections: &RustdocSections) -> String {
let mut out = String::new();
if !sections.summary.is_empty() {
out.push_str(§ions.summary);
}
if let Some(args) = sections.arguments.as_deref() {
for (name, desc) in parse_arguments_bullets(args) {
if !out.is_empty() {
out.push('\n');
}
if desc.is_empty() {
out.push_str("@param ");
out.push_str(&name);
} else {
out.push_str("@param ");
out.push_str(&name);
out.push(' ');
out.push_str(&desc);
}
}
}
if let Some(ret) = sections.returns.as_deref() {
if !out.is_empty() {
out.push('\n');
}
out.push_str("@return ");
out.push_str(ret.trim());
}
if let Some(err) = sections.errors.as_deref() {
if !out.is_empty() {
out.push('\n');
}
out.push_str("@raise ");
out.push_str(err.trim());
}
if let Some(example) = sections.example.as_deref() {
if let Some(body) = example_for_target(example, "ruby") {
if !out.is_empty() {
out.push('\n');
}
out.push_str("@example\n");
out.push_str(&body);
}
}
out
}
fn escape_javadoc_line(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '`' {
let mut code = String::new();
for c in chars.by_ref() {
if c == '`' {
break;
}
code.push(c);
}
result.push_str("{@code ");
result.push_str(&escape_javadoc_html_entities(&code));
result.push('}');
} else if ch == '<' {
result.push_str("<");
} else if ch == '>' {
result.push_str(">");
} else if ch == '&' {
result.push_str("&");
} else {
result.push(ch);
}
}
result
}
fn escape_javadoc_html_entities(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
other => out.push(other),
}
}
out
}