mdbook_lint_core/rules/standard/
md047.rs1use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11
12pub struct MD047;
14
15impl MD047 {
16 fn check_file_ending(&self, content: &str) -> Option<String> {
18 if content.is_empty() {
19 return Some("File should end with a single newline character".to_string());
20 }
21
22 let ends_with_newline = content.ends_with('\n');
23 let ends_with_multiple_newlines = content.ends_with("\n\n");
24
25 if !ends_with_newline {
26 Some("File should end with a single newline character".to_string())
27 } else if ends_with_multiple_newlines {
28 let trailing_newlines = content.chars().rev().take_while(|&c| c == '\n').count();
30
31 if trailing_newlines > 1 {
32 Some(format!(
33 "File should end with a single newline character (found {trailing_newlines} trailing newlines)"
34 ))
35 } else {
36 None
37 }
38 } else {
39 None
40 }
41 }
42}
43
44impl Rule for MD047 {
45 fn id(&self) -> &'static str {
46 "MD047"
47 }
48
49 fn name(&self) -> &'static str {
50 "single-trailing-newline"
51 }
52
53 fn description(&self) -> &'static str {
54 "Files should end with a single newline character"
55 }
56
57 fn metadata(&self) -> RuleMetadata {
58 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("mdbook-lint v0.1.0")
59 }
60
61 fn check_with_ast<'a>(
62 &self,
63 document: &Document,
64 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
65 ) -> Result<Vec<Violation>> {
66 let mut violations = Vec::new();
67
68 if let Some(message) = self.check_file_ending(&document.content) {
69 let line_count = document.lines.len();
70 let line_number = if line_count == 0 { 1 } else { line_count };
71
72 violations.push(self.create_violation(message, line_number, 1, Severity::Warning));
73 }
74
75 Ok(violations)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::rule::Rule;
83 use std::path::PathBuf;
84
85 fn create_test_document(content: &str) -> Document {
86 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
87 }
88
89 #[test]
90 fn test_md047_single_newline_valid() {
91 let content = "# Heading\n\nSome content here.\n";
92 let document = create_test_document(content);
93 let rule = MD047;
94 let violations = rule.check(&document).unwrap();
95
96 assert_eq!(violations.len(), 0);
97 }
98
99 #[test]
100 fn test_md047_no_newline_invalid() {
101 let content = "# Heading\n\nSome content here.";
102 let document = create_test_document(content);
103 let rule = MD047;
104 let violations = rule.check(&document).unwrap();
105
106 assert_eq!(violations.len(), 1);
107 assert_eq!(violations[0].rule_id, "MD047");
108 assert!(
109 violations[0]
110 .message
111 .contains("File should end with a single newline character")
112 );
113 }
114
115 #[test]
116 fn test_md047_multiple_newlines_invalid() {
117 let content = "# Heading\n\nSome content here.\n\n";
118 let document = create_test_document(content);
119 let rule = MD047;
120 let violations = rule.check(&document).unwrap();
121
122 assert_eq!(violations.len(), 1);
123 assert_eq!(violations[0].rule_id, "MD047");
124 assert!(violations[0].message.contains("found 2 trailing newlines"));
125 }
126
127 #[test]
128 fn test_md047_three_newlines_invalid() {
129 let content = "# Heading\n\nSome content here.\n\n\n";
130 let document = create_test_document(content);
131 let rule = MD047;
132 let violations = rule.check(&document).unwrap();
133
134 assert_eq!(violations.len(), 1);
135 assert!(violations[0].message.contains("found 3 trailing newlines"));
136 }
137
138 #[test]
139 fn test_md047_empty_file_invalid() {
140 let content = "";
141 let document = create_test_document(content);
142 let rule = MD047;
143 let violations = rule.check(&document).unwrap();
144
145 assert_eq!(violations.len(), 1);
146 assert!(
147 violations[0]
148 .message
149 .contains("File should end with a single newline character")
150 );
151 }
152
153 #[test]
154 fn test_md047_only_newline_valid() {
155 let content = "\n";
156 let document = create_test_document(content);
157 let rule = MD047;
158 let violations = rule.check(&document).unwrap();
159
160 assert_eq!(violations.len(), 0);
161 }
162
163 #[test]
164 fn test_md047_only_multiple_newlines_invalid() {
165 let content = "\n\n";
166 let document = create_test_document(content);
167 let rule = MD047;
168 let violations = rule.check(&document).unwrap();
169
170 assert_eq!(violations.len(), 1);
171 assert!(violations[0].message.contains("found 2 trailing newlines"));
172 }
173
174 #[test]
175 fn test_md047_content_with_final_newline_valid() {
176 let content = "Line 1\nLine 2\nLine 3\n";
177 let document = create_test_document(content);
178 let rule = MD047;
179 let violations = rule.check(&document).unwrap();
180
181 assert_eq!(violations.len(), 0);
182 }
183
184 #[test]
185 fn test_md047_content_without_final_newline_invalid() {
186 let content = "Line 1\nLine 2\nLine 3";
187 let document = create_test_document(content);
188 let rule = MD047;
189 let violations = rule.check(&document).unwrap();
190
191 assert_eq!(violations.len(), 1);
192 assert_eq!(violations[0].line, 3); }
194
195 #[test]
196 fn test_md047_mixed_line_endings_with_newline_valid() {
197 let content = "# Title\r\n\r\nContent here.\n";
198 let document = create_test_document(content);
199 let rule = MD047;
200 let violations = rule.check(&document).unwrap();
201
202 assert_eq!(violations.len(), 0);
203 }
204
205 #[test]
206 fn test_md047_single_line_with_newline_valid() {
207 let content = "Single line\n";
208 let document = create_test_document(content);
209 let rule = MD047;
210 let violations = rule.check(&document).unwrap();
211
212 assert_eq!(violations.len(), 0);
213 }
214
215 #[test]
216 fn test_md047_single_line_without_newline_invalid() {
217 let content = "Single line";
218 let document = create_test_document(content);
219 let rule = MD047;
220 let violations = rule.check(&document).unwrap();
221
222 assert_eq!(violations.len(), 1);
223 assert_eq!(violations[0].line, 1);
224 }
225}