rumdl_lib/utils/blank_lines.rs
1//! What counts as a blank line for the rules that require blank lines around a
2//! construct (MD022, MD031, MD032, MD058).
3//!
4//! Those rules exist so a reader can see where one block ends and the next
5//! begins, and a line holding nothing but an HTML comment separates two blocks
6//! as plainly as an empty one does. CommonMark disagrees - a comment is an HTML
7//! block of type 2 that ends on the line carrying `-->`, so the construct below
8//! it really does start with no blank line between them - but reporting that
9//! costs more than it is worth: the fix inserts a blank line, and for a
10//! directive comment (`<!-- prettier-ignore -->`, `<!-- markdownlint-disable
11//! -->`, a generator's own trigger) adjacency *is* the meaning, so rewriting the
12//! document turns the directive off. rumdl treats such a line as blank instead.
13//! (#866)
14//!
15//! The odd corners are deliberate and match markdownlint's `isBlankLine`, whose
16//! convention this is: an unclosed `<!--` and a bare `-->` each count, which is
17//! what makes a comment spanning several lines work, and `>` characters are
18//! removed so the whole convention holds inside a blockquote.
19
20/// Whether a line contributes nothing but HTML comments, and so separates the
21/// blocks around it the way an empty line does.
22///
23/// A line with any other content is not blank, however little: `text <!-- c -->`
24/// is a paragraph, and a construct written directly below it is missing its
25/// blank line.
26pub fn is_blank_or_comment_only(line: &str) -> bool {
27 if line.trim().is_empty() {
28 return true;
29 }
30 remove_comments(line).replace('>', "").trim().is_empty()
31}
32
33/// Remove every HTML comment from a line, tolerating a comment that opens
34/// without closing and a close marker with no opener.
35///
36/// An unpaired marker takes everything on its side of the line with it: an
37/// opener swallows the rest of the line (the comment continues below), a closer
38/// swallows the start of it (the comment began above). That is what lets a
39/// comment spanning several lines count as blank at both ends.
40fn remove_comments(line: &str) -> String {
41 const OPEN: &str = "<!--";
42 const CLOSE: &str = "-->";
43
44 let mut remaining = line;
45 let mut kept = String::new();
46 loop {
47 let open = remaining.find(OPEN);
48 let close = remaining.find(CLOSE);
49 match (open, close) {
50 (None, None) => {
51 kept.push_str(remaining);
52 return kept;
53 }
54 // An opener with no closer: the comment continues onto the next line,
55 // so it takes the rest of this one.
56 (Some(open), None) => {
57 kept.push_str(&remaining[..open]);
58 return kept;
59 }
60 // A closer with no opener before it: the comment started on an
61 // earlier line, so everything up to and including it is inside it.
62 (None, Some(close)) => {
63 remaining = &remaining[close + CLOSE.len()..];
64 }
65 (Some(open), Some(close)) if close < open => {
66 remaining = &remaining[close + CLOSE.len()..];
67 }
68 // A complete comment: drop it and carry on with what is left.
69 (Some(open), Some(close)) => {
70 kept.push_str(&remaining[..open]);
71 remaining = &remaining[close + CLOSE.len()..];
72 }
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn an_empty_line_is_blank() {
83 assert!(is_blank_or_comment_only(""));
84 assert!(is_blank_or_comment_only(" "));
85 assert!(is_blank_or_comment_only("\t"));
86 }
87
88 #[test]
89 fn a_comment_only_line_is_blank() {
90 assert!(is_blank_or_comment_only("<!-- c -->"));
91 assert!(is_blank_or_comment_only(" <!-- prettier-ignore --> "));
92 assert!(is_blank_or_comment_only("<!-- a --><!-- b -->"));
93 }
94
95 #[test]
96 fn an_unpaired_marker_is_blank_so_a_multi_line_comment_works_at_both_ends() {
97 assert!(is_blank_or_comment_only("<!--"));
98 assert!(is_blank_or_comment_only("-->"));
99 assert!(is_blank_or_comment_only("<!-- opens here"));
100 assert!(is_blank_or_comment_only("closes here -->"));
101 }
102
103 #[test]
104 fn the_convention_holds_inside_a_blockquote() {
105 assert!(is_blank_or_comment_only("> <!-- c -->"));
106 assert!(is_blank_or_comment_only(">> <!-- c -->"));
107 assert!(is_blank_or_comment_only(">"));
108 }
109
110 #[test]
111 fn a_line_carrying_anything_else_is_not_blank() {
112 assert!(!is_blank_or_comment_only("text"));
113 assert!(!is_blank_or_comment_only("text <!-- c -->"));
114 assert!(!is_blank_or_comment_only("<!-- c --> text"));
115 assert!(!is_blank_or_comment_only("<!-- a --> x <!-- b -->"));
116 assert!(!is_blank_or_comment_only("> text"));
117 assert!(!is_blank_or_comment_only("-->text<!--"));
118 }
119}