Skip to main content

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}