From 66d19f03344a9cf0c6128ce41904bb2d0980f69e Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 30 Jul 2025 00:41:21 +0100 Subject: [PATCH 1/2] Refine markdownlint directive logic --- src/wrap.rs | 27 +++++++++++++++++ tests/markdownlint.rs | 67 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/markdownlint.rs diff --git a/src/wrap.rs b/src/wrap.rs index cfd1431b..0ccfa81c 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -18,6 +18,21 @@ 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 supports these forms with optional rule +/// names: +/// - `` +/// - `` +/// - `` +/// - `` +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 +321,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 fn is_markdownlint_directive(line: &str) -> bool { + MARKDOWNLINT_DIRECTIVE_RE.is_match(line.trim()) +} + fn flush_paragraph(out: &mut Vec, buf: &[(String, bool)], indent: &str, width: usize) { if buf.is_empty() { return; @@ -421,6 +440,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..0a818d0e --- /dev/null +++ b/tests/markdownlint.rs @@ -0,0 +1,67 @@ +//! 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); +} + +/// 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. "]); +} From 5d23b5dd88f42977a18abe16d6ec19269a440633 Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 30 Jul 2025 01:28:01 +0100 Subject: [PATCH 2/2] Refine markdownlint directive regex --- src/wrap.rs | 17 ++++++++------- tests/markdownlint.rs | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/wrap.rs b/src/wrap.rs index 0ccfa81c..9cf3e670 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -20,17 +20,18 @@ static BLOCKQUOTE_RE: std::sync::LazyLock = /// Matches `markdownlint` comment directives. /// -/// The regex is case-insensitive and supports these forms with optional rule -/// names: +/// 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") + r"(?i)^\s*\s*$", + ) + .expect("valid markdownlint regex") }); struct PrefixHandler { @@ -321,8 +322,8 @@ fn wrap_preserving_code(text: &str, width: usize) -> Vec { #[doc(hidden)] pub fn is_fence(line: &str) -> bool { FENCE_RE.is_match(line) } -pub fn is_markdownlint_directive(line: &str) -> bool { - MARKDOWNLINT_DIRECTIVE_RE.is_match(line.trim()) +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) { diff --git a/tests/markdownlint.rs b/tests/markdownlint.rs index 0a818d0e..09a0cda9 100644 --- a/tests/markdownlint.rs +++ b/tests/markdownlint.rs @@ -21,6 +21,18 @@ fn test_markdownlint_disable_next_line_preserved() { 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() { @@ -65,3 +77,41 @@ fn test_non_directive_comment_wraps() { 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.", "