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}