mdlint/lint/rules/
md033.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::Event;
5use serde_json::Value;
6
7pub struct MD033;
8
9impl Rule for MD033 {
10 fn name(&self) -> &'static str {
11 "MD033"
12 }
13
14 fn description(&self) -> &'static str {
15 "Inline HTML"
16 }
17
18 fn tags(&self) -> &[&str] {
19 &["html"]
20 }
21
22 fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
23 let allowed_elements: Vec<String> = config
24 .and_then(|c| c.get("allowed_elements"))
25 .and_then(|v| v.as_array())
26 .map(|arr| {
27 arr.iter()
28 .filter_map(|v| v.as_str().map(str::to_lowercase))
29 .collect()
30 })
31 .unwrap_or_default();
32
33 let mut violations = Vec::new();
34
35 for (event, range) in parser.parse_with_offsets() {
36 let html_str = match event {
38 Event::Html(html) | Event::InlineHtml(html) => html.to_string(),
39 _ => continue,
40 };
41
42 let line = parser.offset_to_line(range.start);
43
44 if html_str.trim().starts_with("</") {
46 continue;
47 }
48
49 if let Some(tag_name) = extract_tag_name(&html_str) {
51 let is_disallowed = allowed_elements.is_empty()
52 || !allowed_elements.contains(&tag_name.to_lowercase());
53
54 if is_disallowed {
55 violations.push(Violation {
56 line,
57 column: Some(1),
58 rule: self.name().to_owned(),
59 message: format!("Inline HTML element: <{tag_name}>"),
60 fix: None,
61 });
62 }
63 }
64 }
65
66 violations
67 }
68
69 fn fixable(&self) -> bool {
70 false
71 }
72}
73
74fn extract_tag_name(html: &str) -> Option<String> {
75 let trimmed = html.trim();
76 if trimmed.starts_with('<') {
77 let inner = trimmed.trim_start_matches('<').trim_start_matches('/');
79 if inner.starts_with('!') {
81 return None;
82 }
83 inner
84 .find(|c: char| c.is_whitespace() || c == '>' || c == '/')
85 .map(|end_pos| inner[..end_pos].to_string())
86 } else {
87 None
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_no_html() {
97 let content = "# Heading\n\nNormal **markdown** text.";
98 let parser = MarkdownParser::new(content);
99 let rule = MD033;
100 let violations = rule.check(&parser, None);
101
102 assert_eq!(violations.len(), 0);
103 }
104
105 #[test]
106 fn test_inline_html() {
107 let content = "Text with <br> tag";
108 let parser = MarkdownParser::new(content);
109 let rule = MD033;
110 let violations = rule.check(&parser, None);
111
112 assert_eq!(violations.len(), 1);
113 assert!(violations[0].message.contains("<br>"));
114 }
115
116 #[test]
117 fn test_allowed_elements() {
118 let content = "Text with <br> tag and <div>content</div>";
119 let parser = MarkdownParser::new(content);
120 let rule = MD033;
121 let config = serde_json::json!({ "allowed_elements": ["br"] });
122 let violations = rule.check(&parser, Some(&config));
123
124 assert!(!violations.is_empty());
126 assert!(violations.iter().any(|v| v.message.contains("<div>")));
127 }
128
129 #[test]
130 fn test_block_html() {
131 let content = "<div>\nContent\n</div>";
132 let parser = MarkdownParser::new(content);
133 let rule = MD033;
134 let violations = rule.check(&parser, None);
135
136 assert!(!violations.is_empty());
137 }
138}