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 {
if rumdl_lib::merge_conflict::detect_for_rules(content, rules, config, None).is_some() {
return 0;
}
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 content_changed =
super::processing::apply_fixes_coordinated(&block_rules, &mut formatted, true, true, config, None);
if !content_changed {
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 source: Vec<&str> = block.markdown.split('\n').collect();
let md_lines: Vec<&str> = markdown.split('\n').collect();
let sources = source_lines(&source, &md_lines);
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 (md_line, source_index) in md_lines.iter().zip(sources) {
if !result.is_empty() {
result.push('\n');
}
let meta = source_index.and_then(|index| block.line_metadata.get(index));
let indent = meta.map_or(dominant_indent, |m| m.leading_whitespace.as_str());
result.push_str(indent);
if md_line.is_empty() {
result.push_str(bare_prefix);
} else if let Some(meta) = meta.filter(|_| source_index.is_some_and(|index| !source[index].is_empty())) {
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
}
const EDITED_LINE_COST: usize = 3;
const GAP_LINE_COST: usize = 2;
#[derive(Clone, Copy)]
enum Step {
Paired,
Removed,
Inserted,
}
fn source_lines(source: &[&str], formatted: &[&str]) -> Vec<Option<usize>> {
let mut sources = vec![None; formatted.len()];
let shorter = source.len().min(formatted.len());
let prefix = (0..shorter).take_while(|&i| source[i] == formatted[i]).count();
let suffix = (0..shorter - prefix)
.take_while(|&k| source[source.len() - 1 - k] == formatted[formatted.len() - 1 - k])
.count();
for (index, slot) in sources.iter_mut().enumerate().take(prefix) {
*slot = Some(index);
}
for k in 0..suffix {
sources[formatted.len() - 1 - k] = Some(source.len() - 1 - k);
}
let old = &source[prefix..source.len() - suffix];
let new = &formatted[prefix..formatted.len() - suffix];
let width = new.len() + 1;
let mut steps = vec![Step::Inserted; (old.len() + 1) * width];
let mut previous: Vec<usize> = (0..width).map(|j| j * GAP_LINE_COST).collect();
let mut current = vec![0; width];
for i in 1..=old.len() {
current[0] = i * GAP_LINE_COST;
steps[i * width] = Step::Removed;
for j in 1..width {
let unchanged = old[i - 1] == new[j - 1];
let paired = (
previous[j - 1] + if unchanged { 0 } else { EDITED_LINE_COST },
Step::Paired,
);
let removed = (previous[j] + GAP_LINE_COST, Step::Removed);
let inserted = (current[j - 1] + GAP_LINE_COST, Step::Inserted);
let (cost, step) = [removed, inserted, paired]
.into_iter()
.reduce(|best, candidate| if candidate.0 < best.0 { candidate } else { best })
.expect("three candidates");
current[j] = cost;
steps[i * width + j] = step;
}
std::mem::swap(&mut previous, &mut current);
}
let (mut i, mut j) = (old.len(), new.len());
while i > 0 || j > 0 {
match steps[i * width + j] {
Step::Paired => {
i -= 1;
j -= 1;
sources[prefix + j] = Some(prefix + i);
}
Step::Removed => i -= 1,
Step::Inserted => j -= 1,
}
}
sources
}
#[cfg(test)]
mod tests {
use super::*;
fn block_of(source: &str) -> DocCommentBlock {
let mut blocks = extract_doc_comment_blocks(source);
assert_eq!(blocks.len(), 1, "expected one doc comment block in {source:?}");
blocks.remove(0)
}
#[test]
fn test_restore_prefixes_basic() {
let block = block_of("/// Hello\n/// World\n");
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 = block_of("/// First\n///\n/// Third\n");
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 = block_of("/// # Heading\n/// Text\n");
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 = block_of("//! Module\n");
let restored = restore_doc_comment_prefixes("Module", &block, true);
assert_eq!(restored, "//! Module\n");
}
#[test]
fn test_restore_indented() {
let block = block_of(" /// Indented\n");
let restored = restore_doc_comment_prefixes("Indented", &block, true);
assert_eq!(restored, " /// Indented\n");
}
#[test]
fn test_restore_no_trailing_newline() {
let block = block_of("/// Hello");
let restored = restore_doc_comment_prefixes("Hello", &block, false);
assert_eq!(restored, "/// Hello");
}
#[test]
fn test_restore_preserves_tab_prefix() {
let block = block_of("///\tcontent\n");
let restored = restore_doc_comment_prefixes("content", &block, true);
assert_eq!(restored, "///\tcontent\n");
}
#[test]
fn test_restore_preserves_no_space_prefix() {
let block = block_of("///content\n");
let restored = restore_doc_comment_prefixes("content", &block, true);
assert_eq!(restored, "///content\n");
}
#[test]
fn test_restore_after_removed_lines_keeps_each_lines_prefix() {
let block = block_of("//! text\n//!\n//!\n//!\n//! more\n//!\tend\n");
let restored = restore_doc_comment_prefixes("text\n\nmore\nend", &block, true);
assert_eq!(restored, "//! text\n//!\n//! more\n//!\tend\n");
}
#[test]
fn test_restore_after_removed_lines_keeps_each_lines_indentation() {
let block = block_of("/// text\n///\n///\n /// more\n");
let restored = restore_doc_comment_prefixes("text\n\nmore", &block, true);
assert_eq!(restored, "/// text\n///\n /// more\n");
}
#[test]
fn test_restore_after_inserted_line_keeps_each_later_lines_separator() {
let block = block_of("/// # Heading\n///\tTabbed\n///no-space\n");
let restored = restore_doc_comment_prefixes("# Heading\n\nTabbed\nno-space", &block, true);
assert_eq!(restored, "/// # Heading\n///\n///\tTabbed\n///no-space\n");
}
#[test]
fn test_restore_line_edited_in_place_keeps_its_prefix() {
let block = block_of("///\tfirst \n///second\n");
let restored = restore_doc_comment_prefixes("first\nsecond", &block, true);
assert_eq!(restored, "///\tfirst\n///second\n");
}
#[test]
fn test_restore_text_written_over_an_empty_line_gets_the_standard_prefix() {
let block = block_of("/// a\n///\n");
let restored = restore_doc_comment_prefixes("a\nb", &block, true);
assert_eq!(restored, "/// a\n/// b\n");
}
#[test]
fn test_source_lines_pairs_equal_replaced_and_inserted_lines() {
assert_eq!(
source_lines(&["a", "", "", "b"], &["a", "", "b"]),
[Some(0), Some(1), Some(3)]
);
assert_eq!(source_lines(&["# h", "t"], &["# h", "", "t"]), [Some(0), None, Some(1)]);
assert_eq!(source_lines(&["x ", "y"], &["x", "y"]), [Some(0), Some(1)]);
assert_eq!(source_lines(&["x "], &["x", "z"]), [Some(0), None]);
}
#[test]
fn test_source_lines_pairs_an_edited_line_that_now_equals_its_neighbor_with_itself() {
assert_eq!(source_lines(&["foo ", "foo"], &["foo", "foo"]), [Some(0), Some(1)]);
assert_eq!(source_lines(&["foo", "foo "], &["foo", "foo"]), [Some(0), Some(1)]);
assert_eq!(
source_lines(&["a", "foo ", "foo", "b"], &["a", "foo", "foo", "b"]),
[Some(0), Some(1), Some(2), Some(3)]
);
}
#[test]
fn test_source_lines_prefers_moving_an_unchanged_line_over_editing_two() {
assert_eq!(
source_lines(&["# H", "x", ""], &["# H", "", "x"]),
[Some(0), None, Some(1)]
);
}
#[test]
fn test_source_lines_breaks_ties_the_same_way_every_time() {
assert_eq!(source_lines(&["a"], &["b", "a", "a", "b"]), [None, Some(0), None, None]);
assert_eq!(source_lines(&["a", "b"], &["b", "a"]), [None, Some(0)]);
}
#[test]
fn test_source_lines_with_no_lines_on_one_side() {
assert_eq!(source_lines(&["a", "b"], &[]), Vec::<Option<usize>>::new());
assert_eq!(source_lines(&[], &["a", "b"]), [None, None]);
}
#[test]
fn test_restore_line_trimmed_to_equal_its_neighbor_keeps_its_prefix_and_indentation() {
let block = block_of("///foo \n/// foo\n");
let restored = restore_doc_comment_prefixes("foo\nfoo", &block, true);
assert_eq!(restored, "///foo\n/// foo\n");
let block = block_of("///foo \n /// foo\n");
let restored = restore_doc_comment_prefixes("foo\nfoo", &block, true);
assert_eq!(restored, "///foo\n /// foo\n");
}
}