mdbook_lint_core/rules/standard/
md018.rs1use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11
12pub struct MD018;
14
15impl Rule for MD018 {
16 fn id(&self) -> &'static str {
17 "MD018"
18 }
19
20 fn name(&self) -> &'static str {
21 "no-missing-space-atx"
22 }
23
24 fn description(&self) -> &'static str {
25 "No space after hash on atx style heading"
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("#!") {
46 let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
48
49 if trimmed.len() > hash_count {
51 let after_hashes = &trimmed[hash_count..];
52
53 if !after_hashes.is_empty() && !after_hashes.starts_with(' ') {
55 let column = line.len() - line.trim_start().len() + hash_count + 1;
56
57 violations.push(self.create_violation(
58 "No space after hash on atx style heading".to_string(),
59 line_num,
60 column,
61 Severity::Warning,
62 ));
63 }
64 }
65 }
66 }
67
68 Ok(violations)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::rule::Rule;
76 use std::path::PathBuf;
77
78 fn create_test_document(content: &str) -> Document {
79 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
80 }
81
82 #[test]
83 fn test_md018_valid_headings() {
84 let content = "# Heading 1\n## Heading 2\n### Heading 3";
85 let document = create_test_document(content);
86 let rule = MD018;
87 let violations = rule.check(&document).unwrap();
88
89 assert_eq!(violations.len(), 0);
90 }
91
92 #[test]
93 fn test_md018_no_space_after_hash() {
94 let content = "#Heading without space";
95 let document = create_test_document(content);
96 let rule = MD018;
97 let violations = rule.check(&document).unwrap();
98
99 assert_eq!(violations.len(), 1);
100 assert_eq!(violations[0].rule_id, "MD018");
101 assert_eq!(violations[0].line, 1);
102 assert_eq!(violations[0].column, 2);
103 assert!(violations[0].message.contains("No space after hash"));
104 }
105
106 #[test]
107 fn test_md018_multiple_violations() {
108 let content = "#Heading 1\n##Heading 2\n### Valid heading\n####Another violation";
109 let document = create_test_document(content);
110 let rule = MD018;
111 let violations = rule.check(&document).unwrap();
112
113 assert_eq!(violations.len(), 3);
114 assert_eq!(violations[0].line, 1);
115 assert_eq!(violations[1].line, 2);
116 assert_eq!(violations[2].line, 4);
117 }
118
119 #[test]
120 fn test_md018_indented_heading() {
121 let content = " #Indented heading without space";
122 let document = create_test_document(content);
123 let rule = MD018;
124 let violations = rule.check(&document).unwrap();
125
126 assert_eq!(violations.len(), 1);
127 assert_eq!(violations[0].column, 4); }
129
130 #[test]
131 fn test_md018_empty_heading() {
132 let content = "#\n##\n###";
133 let document = create_test_document(content);
134 let rule = MD018;
135 let violations = rule.check(&document).unwrap();
136
137 assert_eq!(violations.len(), 0);
139 }
140
141 #[test]
142 fn test_md018_closed_atx_style() {
143 let content = "#Heading#\n##Another#Heading##";
144 let document = create_test_document(content);
145 let rule = MD018;
146 let violations = rule.check(&document).unwrap();
147
148 assert_eq!(violations.len(), 2);
149 assert_eq!(violations[0].line, 1);
150 assert_eq!(violations[1].line, 2);
151 }
152
153 #[test]
154 fn test_md018_setext_headings_ignored() {
155 let content = "Setext Heading\n==============\n\nAnother Setext\n--------------";
156 let document = create_test_document(content);
157 let rule = MD018;
158 let violations = rule.check(&document).unwrap();
159
160 assert_eq!(violations.len(), 0);
162 }
163
164 #[test]
165 fn test_md018_mixed_valid_invalid() {
166 let content = "# Valid heading\n#Invalid heading\n## Another valid\n###Invalid again";
167 let document = create_test_document(content);
168 let rule = MD018;
169 let violations = rule.check(&document).unwrap();
170
171 assert_eq!(violations.len(), 2);
172 assert_eq!(violations[0].line, 2);
173 assert_eq!(violations[1].line, 4);
174 }
175
176 #[test]
177 fn test_md018_shebang_lines_ignored() {
178 let content = "#!/bin/bash\n#This should trigger\n#!/usr/bin/env python3\n# This is valid";
179 let document = create_test_document(content);
180 let rule = MD018;
181 let violations = rule.check(&document).unwrap();
182
183 assert_eq!(violations.len(), 1);
185 assert_eq!(violations[0].line, 2);
186 assert!(violations[0].message.contains("No space after hash"));
187 }
188}