use rumdl_lib::config as rumdl_config;
use rumdl_lib::doc_comment_lint::{DocCommentBlock, DocCommentKind, SKIPPED_RULES, extract_doc_comment_blocks};
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::md013_line_length::MD013LineLength;
pub fn format_doc_comment_blocks(
content: &mut String,
rules: &[Box<dyn Rule>],
config: &rumdl_config::Config,
) -> usize {
let blocks = extract_doc_comment_blocks(content);
if blocks.is_empty() {
return 0;
}
let mut formatted_count = 0;
for block in blocks.into_iter().rev() {
if block.markdown.trim().is_empty() {
continue;
}
let block_rules: Vec<Box<dyn Rule>> = rules
.iter()
.filter(|rule| !SKIPPED_RULES.contains(&rule.name()))
.map(|r| {
if r.name() == "MD013"
&& let Some(md013) = r.as_any().downcast_ref::<MD013LineLength>()
{
return Box::new(md013.with_code_blocks_disabled()) as Box<dyn Rule>;
}
dyn_clone::clone_box(&**r)
})
.collect();
let ctx = LintContext::new(&block.markdown, config.markdown_flavor(), None);
let mut warnings = Vec::new();
for rule in &block_rules {
if let Ok(rule_warnings) = rule.check(&ctx) {
warnings.extend(rule_warnings);
}
}
if warnings.is_empty() {
continue;
}
let mut formatted = block.markdown.clone();
let fixed = super::processing::apply_fixes_coordinated(
&block_rules,
&warnings,
&mut formatted,
true,
true,
config,
None,
);
if fixed == 0 {
continue;
}
let byte_end = block.byte_end.min(content.len());
let original_ends_with_newline = content.as_bytes().get(byte_end.wrapping_sub(1)) == Some(&b'\n');
let restored = restore_doc_comment_prefixes(&formatted, &block, original_ends_with_newline);
content.replace_range(block.byte_start..byte_end, &restored);
formatted_count += 1;
}
formatted_count
}
fn restore_doc_comment_prefixes(markdown: &str, block: &DocCommentBlock, trailing_newline: bool) -> String {
let md_lines: Vec<&str> = markdown.split('\n').collect();
let mut result = String::new();
let dominant_indent = block
.line_metadata
.first()
.map(|m| m.leading_whitespace.as_str())
.unwrap_or("");
let bare_prefix = match block.kind {
DocCommentKind::Outer => "///",
DocCommentKind::Inner => "//!",
};
for (i, md_line) in md_lines.iter().enumerate() {
if i > 0 {
result.push('\n');
}
let indent = block
.line_metadata
.get(i)
.map(|m| m.leading_whitespace.as_str())
.unwrap_or(dominant_indent);
result.push_str(indent);
if md_line.is_empty() {
result.push_str(bare_prefix);
} else if let Some(meta) = block.line_metadata.get(i) {
result.push_str(&meta.prefix);
result.push_str(md_line);
} else {
result.push_str(bare_prefix);
result.push(' ');
result.push_str(md_line);
}
}
if trailing_newline && !result.ends_with('\n') {
result.push('\n');
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use rumdl_lib::doc_comment_lint::{DocCommentBlock, DocCommentKind, DocCommentLineInfo};
#[test]
fn test_restore_prefixes_basic() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 1,
byte_start: 0,
byte_end: 30,
markdown: "Hello\nWorld".to_string(),
line_metadata: vec![
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
},
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
},
],
prefix_byte_lengths: vec![4, 4],
};
let restored = restore_doc_comment_prefixes("Hello\nWorld", &block, true);
assert_eq!(restored, "/// Hello\n/// World\n");
}
#[test]
fn test_restore_prefixes_with_empty_line() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 2,
byte_start: 0,
byte_end: 30,
markdown: "First\n\nThird".to_string(),
line_metadata: vec![
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
},
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "///".to_string(),
},
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
},
],
prefix_byte_lengths: vec![4, 3, 4],
};
let restored = restore_doc_comment_prefixes("First\n\nThird", &block, true);
assert_eq!(restored, "/// First\n///\n/// Third\n");
}
#[test]
fn test_restore_prefixes_new_line_added() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 1,
byte_start: 0,
byte_end: 30,
markdown: "# Heading\nText".to_string(),
line_metadata: vec![
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
},
DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
},
],
prefix_byte_lengths: vec![4, 4],
};
let restored = restore_doc_comment_prefixes("# Heading\n\nText", &block, true);
assert_eq!(restored, "/// # Heading\n///\n/// Text\n");
}
#[test]
fn test_restore_inner_doc_comment() {
let block = DocCommentBlock {
kind: DocCommentKind::Inner,
start_line: 0,
end_line: 0,
byte_start: 0,
byte_end: 15,
markdown: "Module".to_string(),
line_metadata: vec![DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "//! ".to_string(),
}],
prefix_byte_lengths: vec![4],
};
let restored = restore_doc_comment_prefixes("Module", &block, true);
assert_eq!(restored, "//! Module\n");
}
#[test]
fn test_restore_indented() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 0,
byte_start: 0,
byte_end: 20,
markdown: "Indented".to_string(),
line_metadata: vec![DocCommentLineInfo {
leading_whitespace: " ".to_string(),
prefix: "/// ".to_string(),
}],
prefix_byte_lengths: vec![8],
};
let restored = restore_doc_comment_prefixes("Indented", &block, true);
assert_eq!(restored, " /// Indented\n");
}
#[test]
fn test_restore_no_trailing_newline() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 0,
byte_start: 0,
byte_end: 9,
markdown: "Hello".to_string(),
line_metadata: vec![DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "/// ".to_string(),
}],
prefix_byte_lengths: vec![4],
};
let restored = restore_doc_comment_prefixes("Hello", &block, false);
assert_eq!(restored, "/// Hello");
}
#[test]
fn test_restore_preserves_tab_prefix() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 0,
byte_start: 0,
byte_end: 15,
markdown: "content".to_string(),
line_metadata: vec![DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "///\t".to_string(),
}],
prefix_byte_lengths: vec![4],
};
let restored = restore_doc_comment_prefixes("content", &block, true);
assert_eq!(restored, "///\tcontent\n");
}
#[test]
fn test_restore_preserves_no_space_prefix() {
let block = DocCommentBlock {
kind: DocCommentKind::Outer,
start_line: 0,
end_line: 0,
byte_start: 0,
byte_end: 13,
markdown: "content".to_string(),
line_metadata: vec![DocCommentLineInfo {
leading_whitespace: "".to_string(),
prefix: "///".to_string(),
}],
prefix_byte_lengths: vec![3],
};
let restored = restore_doc_comment_prefixes("content", &block, true);
assert_eq!(restored, "///content\n");
}
}