diff --git a/src/wrap.rs b/src/wrap.rs index cfd1431b..9cf3e670 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -18,6 +18,22 @@ static FOOTNOTE_RE: std::sync::LazyLock = static BLOCKQUOTE_RE: std::sync::LazyLock = std::sync::LazyLock::new(|| Regex::new(r"^(\s*(?:>\s*)+)(.*)$").unwrap()); +/// Matches `markdownlint` comment directives. +/// +/// The regex is case-insensitive and recognises these forms with optional rule +/// names (including plugin rules such as `MD013/line-length` or +/// `plugin/rule-name`): +/// - `` +/// - `` +/// - `` +/// - `` +static MARKDOWNLINT_DIRECTIVE_RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { + Regex::new( + r"(?i)^\s*\s*$", + ) + .expect("valid markdownlint regex") +}); + struct PrefixHandler { re: &'static std::sync::LazyLock, is_bq: bool, @@ -306,6 +322,10 @@ fn wrap_preserving_code(text: &str, width: usize) -> Vec { #[doc(hidden)] pub fn is_fence(line: &str) -> bool { FENCE_RE.is_match(line) } +pub(crate) fn is_markdownlint_directive(line: &str) -> bool { + MARKDOWNLINT_DIRECTIVE_RE.is_match(line) +} + fn flush_paragraph(out: &mut Vec, buf: &[(String, bool)], indent: &str, width: usize) { if buf.is_empty() { return; @@ -421,6 +441,14 @@ pub fn wrap_text(lines: &[String], width: usize) -> Vec { continue; } + if is_markdownlint_directive(line) { + flush_paragraph(&mut out, &buf, &indent, width); + buf.clear(); + indent.clear(); + out.push(line.clone()); + continue; + } + if line.trim().is_empty() { flush_paragraph(&mut out, &buf, &indent, width); buf.clear(); diff --git a/tests/markdownlint.rs b/tests/markdownlint.rs new file mode 100644 index 00000000..09a0cda9 --- /dev/null +++ b/tests/markdownlint.rs @@ -0,0 +1,117 @@ +//! Tests for markdownlint directive handling during wrapping. +//! +//! These tests ensure that comment directives such as +//! `` remain on their own line +//! after processing. Regular comments should still be wrapped normally. + +use mdtablefix::process_stream; + +#[macro_use] +mod prelude; +use prelude::*; + +/// The disable-next-line directive must remain intact after wrapping. +#[test] +fn test_markdownlint_disable_next_line_preserved() { + let input = lines_vec![ + "[roadmap](./roadmap.md) and expands on the design ideas described in", + "", + ]; + let output = process_stream(&input); + assert_eq!(output, input); +} + +/// The disable-next-line directive must remain intact when in the middle of the input. +#[test] +fn test_markdownlint_disable_next_line_preserved_middle() { + let input = lines_vec![ + "This is the first line.", + "", + "This is the third line.", + ]; + let output = process_stream(&input); + assert_eq!(output, input); +} + +/// Regular comments should still wrap when necessary. +#[test] +fn test_regular_comment_wraps_normally() { + let input = lines_vec![ + "Intro text that preludes a lengthy comment.", + concat!( + "" + ), + ]; + let output = process_stream(&input); + assert_eq!( + output, + lines_vec![ + "Intro text that preludes a lengthy comment. ", + ] + ); +} + +/// Other markdownlint directives should also remain on their own lines, even +/// when indented or combined with multiple rule names. +#[rstest] +#[case("")] +#[case("")] +#[case(" ")] +#[case("")] +#[case("")] +#[case("")] +fn test_markdownlint_directive_variants_preserved(#[case] directive: &str) { + let input = lines_vec!["A preceding line.", directive]; + let output = process_stream(&input); + assert_eq!(output, input); +} + +/// Comments that resemble directives but are invalid should wrap normally. +#[test] +fn test_non_directive_comment_wraps() { + let input = lines_vec!["Intro line.", ""]; + let output = process_stream(&input); + assert_eq!(output, lines_vec!["Intro line. "]); +} + +/// Malformed or partially correct directive comments should wrap normally. +#[test] +fn test_malformed_directive_missing_closing() { + let input = lines_vec!["Text before.", " extra"]; + let output = process_stream(&input); + assert_eq!( + output, + lines_vec!["Text before. extra"] + ); +} + +#[test] +fn test_malformed_directive_typo() { + let input = lines_vec!["Text before.", ""]; + let output = process_stream(&input); + assert_eq!( + output, + lines_vec!["Text before. "] + ); +} + +#[test] +fn test_malformed_directive_incomplete_tag() { + let input = lines_vec!["Text before.", "