Skip to main content

hygiene/
blank.rs

1//! The blank check: `hygiene blank PATH...` fails when a Markdown file
2//! has two blank lines in a row.
3//!
4//! # Design
5//!
6//! One blank line ends a paragraph. A second blank line adds nothing,
7//! and a diff that adds it is noise. So no Markdown file of the book
8//! has two blank lines in a row
9//! (01M51K1VMWTNZGD6PT1BYDGWM3). `just wrap` runs `hygiene blank
10//! docs/src`: each `.md` file in the folder and its subfolders.
11//!
12//! The rule `blank` finds each blank line after a blank line. It skips
13//! a code block: the text of a block is an example, as it is. A line
14//! with only spaces is blank.
15//!
16//! ```
17//! let errors = hygiene::blank::check("a.md", "# A\n\n\nText.\n");
18//! assert_eq!(errors.len(), 1);
19//! assert_eq!(errors[0].rule, "blank");
20//! assert_eq!(errors[0].text, "a.md:3: a second blank line. Remove it.");
21//!
22//! let block = "# A\n\n```text\none\n\n\ntwo\n```\n";
23//! assert!(hygiene::blank::check("a.md", block).is_empty());
24//! ```
25
26use crate::Error;
27
28/// The number of each blank line of `text` that follows a blank line,
29/// outside a code block. Each number counts from 1.
30pub fn second_blank_lines(text: &str) -> Vec<usize> {
31    let mut code = false;
32    let mut blank = false;
33    let mut found = Vec::new();
34    for (n, line) in text.lines().enumerate() {
35        let trimmed = line.trim();
36        if trimmed.starts_with("```") {
37            code = !code;
38        }
39        let is_blank = !code && trimmed.is_empty();
40        if is_blank && blank {
41            found.push(n + 1);
42        }
43        blank = is_blank;
44    }
45    found
46}
47
48/// The errors of the Markdown file `name` with the content `text`: one
49/// for each line of [`second_blank_lines`].
50pub fn check(name: &str, text: &str) -> Vec<Error> {
51    second_blank_lines(text)
52        .into_iter()
53        .map(|n| {
54            Error::new(
55                "blank",
56                format!("{name}:{n}: a second blank line. Remove it."),
57            )
58        })
59        .collect()
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn each_blank_line_after_a_blank_line_counts() {
68        assert_eq!(second_blank_lines("a\n\nb\n"), Vec::<usize>::new());
69        assert_eq!(second_blank_lines("a\n\n\nb\n"), [3]);
70        assert_eq!(second_blank_lines("a\n\n\n\nb\n"), [3, 4]);
71        assert_eq!(second_blank_lines("a\n\n  \nb\n"), [3]);
72        assert_eq!(second_blank_lines("\n\na\n"), [2]);
73    }
74
75    #[test]
76    fn a_code_block_keeps_its_blank_lines() {
77        let text = "a\n\n```sh\nx\n\n\ny\n```\n\nb\n";
78        assert_eq!(second_blank_lines(text), Vec::<usize>::new());
79        let indented = "1. a\n\n   ```sh\n   x\n\n\n   ```\n";
80        assert_eq!(second_blank_lines(indented), Vec::<usize>::new());
81    }
82
83    #[test]
84    fn a_blank_line_after_a_code_block_and_a_blank_line_counts() {
85        assert_eq!(second_blank_lines("```\nx\n```\n\n\n# B\n"), [5]);
86    }
87}