mdbook_lint_core/rules/standard/
md023.rs1use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11
12pub struct MD023;
14
15impl Rule for MD023 {
16 fn id(&self) -> &'static str {
17 "MD023"
18 }
19
20 fn name(&self) -> &'static str {
21 "heading-start-left"
22 }
23
24 fn description(&self) -> &'static str {
25 "Headings must start at the beginning of the line"
26 }
27
28 fn metadata(&self) -> RuleMetadata {
29 RuleMetadata::stable(RuleCategory::Structure).introduced_in("markdownlint v0.1.0")
30 }
31
32 fn check_with_ast<'a>(
33 &self,
34 document: &Document,
35 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
36 ) -> Result<Vec<Violation>> {
37 let mut violations = Vec::new();
38
39 for (line_number, line) in document.lines.iter().enumerate() {
40 let line_num = line_number + 1; let trimmed = line.trim_start();
45 if trimmed.starts_with('#') && !trimmed.starts_with("#!") && line != trimmed {
46 let leading_whitespace = line.len() - trimmed.len();
47
48 violations.push(self.create_violation(
49 format!(
50 "Heading is indented by {} character{}",
51 leading_whitespace,
52 if leading_whitespace == 1 { "" } else { "s" }
53 ),
54 line_num,
55 1,
56 Severity::Warning,
57 ));
58 }
59 }
63
64 Ok(violations)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::rule::Rule;
72 use std::path::PathBuf;
73
74 fn create_test_document(content: &str) -> Document {
75 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
76 }
77
78 #[test]
79 fn test_md023_valid_headings() {
80 let content = "# Heading 1\n## Heading 2\n### Heading 3";
81 let document = create_test_document(content);
82 let rule = MD023;
83 let violations = rule.check(&document).unwrap();
84
85 assert_eq!(violations.len(), 0);
86 }
87
88 #[test]
89 fn test_md023_single_space_indent() {
90 let content = " # Indented heading";
91 let document = create_test_document(content);
92 let rule = MD023;
93 let violations = rule.check(&document).unwrap();
94
95 assert_eq!(violations.len(), 1);
96 assert_eq!(violations[0].rule_id, "MD023");
97 assert_eq!(violations[0].line, 1);
98 assert_eq!(violations[0].column, 1);
99 assert!(violations[0].message.contains("indented by 1 character"));
100 }
101
102 #[test]
103 fn test_md023_multiple_spaces_indent() {
104 let content = " ## Heading with 3 spaces";
105 let document = create_test_document(content);
106 let rule = MD023;
107 let violations = rule.check(&document).unwrap();
108
109 assert_eq!(violations.len(), 1);
110 assert!(violations[0].message.contains("indented by 3 characters"));
111 }
112
113 #[test]
114 fn test_md023_tab_indent() {
115 let content = "\t# Tab indented heading";
116 let document = create_test_document(content);
117 let rule = MD023;
118 let violations = rule.check(&document).unwrap();
119
120 assert_eq!(violations.len(), 1);
121 assert!(violations[0].message.contains("indented by 1 character"));
122 }
123
124 #[test]
125 fn test_md023_mixed_whitespace_indent() {
126 let content = " \t # Mixed whitespace indent";
127 let document = create_test_document(content);
128 let rule = MD023;
129 let violations = rule.check(&document).unwrap();
130
131 assert_eq!(violations.len(), 1);
132 assert!(violations[0].message.contains("indented by 3 characters"));
133 }
134
135 #[test]
136 fn test_md023_multiple_violations() {
137 let content = " # Heading 1\n## Valid heading\n ### Heading 3\n#### Valid heading";
138 let document = create_test_document(content);
139 let rule = MD023;
140 let violations = rule.check(&document).unwrap();
141
142 assert_eq!(violations.len(), 2);
143 assert_eq!(violations[0].line, 1);
144 assert_eq!(violations[1].line, 3);
145 }
146
147 #[test]
148 fn test_md023_setext_headings_ignored() {
149 let content = " Setext Heading\n ==============\n\n Another Setext\n --------------";
150 let document = create_test_document(content);
151 let rule = MD023;
152 let violations = rule.check(&document).unwrap();
153
154 assert_eq!(violations.len(), 0);
156 }
157
158 #[test]
159 fn test_md023_code_blocks_detected() {
160 let content = "```\n # This is in a code block\n ## Should trigger\n```";
161 let document = create_test_document(content);
162 let rule = MD023;
163 let violations = rule.check(&document).unwrap();
164
165 assert_eq!(violations.len(), 2);
168 assert_eq!(violations[0].line, 2);
169 assert_eq!(violations[1].line, 3);
170 }
171
172 #[test]
173 fn test_md023_blockquote_headings() {
174 let content = "> # Heading in blockquote\n> ## Indented heading in blockquote";
175 let document = create_test_document(content);
176 let rule = MD023;
177 let violations = rule.check(&document).unwrap();
178
179 assert_eq!(violations.len(), 0);
182 }
183
184 #[test]
185 fn test_md023_closed_atx_headings() {
186 let content = " # Indented closed heading #\n ## Another indented ##";
187 let document = create_test_document(content);
188 let rule = MD023;
189 let violations = rule.check(&document).unwrap();
190
191 assert_eq!(violations.len(), 2);
192 assert!(violations[0].message.contains("indented by 2 characters"));
193 assert!(violations[1].message.contains("indented by 3 characters"));
194 }
195
196 #[test]
197 fn test_md023_shebang_lines_ignored() {
198 let content =
199 "#!/bin/bash\n #This should trigger\n #!/usr/bin/env python3\n# This is valid";
200 let document = create_test_document(content);
201 let rule = MD023;
202 let violations = rule.check(&document).unwrap();
203
204 assert_eq!(violations.len(), 1);
206 assert_eq!(violations[0].line, 2);
207 assert!(violations[0].message.contains("indented by 2 characters"));
208 }
209}