Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ static FOOTNOTE_RE: std::sync::LazyLock<Regex> =
static BLOCKQUOTE_RE: std::sync::LazyLock<Regex> =
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`):
/// - `<!-- markdownlint-disable -->`
/// - `<!-- markdownlint-enable -->`
/// - `<!-- markdownlint-disable-line MD001 MD005 -->`
/// - `<!-- markdownlint-disable-next-line MD001 MD005 -->`
static MARKDOWNLINT_DIRECTIVE_RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Comment thread
leynos marked this conversation as resolved.
Regex::new(
r"(?i)^\s*<!--\s*markdownlint-(?:disable|enable|disable-line|disable-next-line)(?:\s+[A-Za-z0-9_\-/]+)*\s*-->\s*$",
)
.expect("valid markdownlint regex")
});

struct PrefixHandler {
re: &'static std::sync::LazyLock<Regex>,
is_bq: bool,
Expand Down Expand Up @@ -306,6 +322,10 @@ fn wrap_preserving_code(text: &str, width: usize) -> Vec<String> {
#[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<String>, buf: &[(String, bool)], indent: &str, width: usize) {
if buf.is_empty() {
return;
Expand Down Expand Up @@ -421,6 +441,14 @@ pub fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
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();
Expand Down
117 changes: 117 additions & 0 deletions tests/markdownlint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Tests for markdownlint directive handling during wrapping.
//!
//! These tests ensure that comment directives such as
//! `<!-- markdownlint-disable-next-line -->` remain on their own line
//! after processing. Regular comments should still be wrapped normally.

use mdtablefix::process_stream;

#[macro_use]
mod prelude;
use prelude::*;
Comment on lines +7 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider consolidating imports.

The separate import of mdtablefix::process_stream and the macro use could be organised more cleanly.

Apply this diff to consolidate the imports:

-use mdtablefix::process_stream;
-
-#[macro_use]
-mod prelude;
-use prelude::*;
+#[macro_use]
+mod prelude;
+
+use mdtablefix::process_stream;
+use prelude::*;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use mdtablefix::process_stream;
#[macro_use]
mod prelude;
use prelude::*;
#[macro_use]
mod prelude;
use mdtablefix::process_stream;
use prelude::*;
🤖 Prompt for AI Agents
In tests/markdownlint.rs around lines 7 to 11, the imports are split between a
macro use declaration and a separate import of mdtablefix::process_stream.
Consolidate these imports by combining the macro use and the process_stream
import into a single organized block at the top of the file, improving clarity
and reducing redundancy.


/// 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",
"<!-- markdownlint-disable-next-line MD013 -->",
];
let output = process_stream(&input);
assert_eq!(output, input);
}
Comment thread
leynos marked this conversation as resolved.

/// 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.",
"<!-- markdownlint-disable-next-line MD013 -->",
"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!(
"<!-- This comment contains many words and should be wrapped across ",
"multiple lines to ensure that regular comments are formatted ",
"correctly. -->"
),
];
let output = process_stream(&input);
assert_eq!(
output,
lines_vec![
"Intro text that preludes a lengthy comment. <!-- This comment contains many",
"words and should be wrapped across multiple lines to ensure that regular",
"comments are formatted correctly. -->",
]
);
}

/// Other markdownlint directives should also remain on their own lines, even
/// when indented or combined with multiple rule names.
#[rstest]
#[case("<!-- markdownlint-disable-line MD001 MD005 -->")]
#[case("<!-- markdownlint-enable MD001 -->")]
#[case(" <!-- markdownlint-disable -->")]
#[case("<!-- markdownlint-disable MD001 MD002 -->")]
#[case("<!-- MarkDownLint-disable-line MD003 -->")]
#[case("<!-- MARKDOWNLINT-disable-next-line MD004 MD005 -->")]
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.", "<!-- markdowndisable -->"];
let output = process_stream(&input);
assert_eq!(output, lines_vec!["Intro line. <!-- markdowndisable -->"]);
}
Comment thread
leynos marked this conversation as resolved.

/// Malformed or partially correct directive comments should wrap normally.
#[test]
fn test_malformed_directive_missing_closing() {
let input = lines_vec!["Text before.", "<!-- markdownlint-disable"];
let output = process_stream(&input);
assert_eq!(output, lines_vec!["Text before. <!-- markdownlint-disable"]);
}

#[test]
fn test_malformed_directive_extra_text() {
let input = lines_vec!["Text before.", "<!-- markdownlint-disable --> extra"];
let output = process_stream(&input);
assert_eq!(
output,
lines_vec!["Text before. <!-- markdownlint-disable --> extra"]
);
}

#[test]
fn test_malformed_directive_typo() {
let input = lines_vec!["Text before.", "<!-- markdownlnt-disable-line MD001 -->"];
let output = process_stream(&input);
assert_eq!(
output,
lines_vec!["Text before. <!-- markdownlnt-disable-line MD001 -->"]
);
}

#[test]
fn test_malformed_directive_incomplete_tag() {
let input = lines_vec!["Text before.", "<!-- markdownlint-disable-line MD001 "];
let output = process_stream(&input);
assert_eq!(
output,
lines_vec!["Text before. <!-- markdownlint-disable-line MD001"]
);
}
Loading