1use std::collections::{HashMap, HashSet};
16
17#[derive(Debug, Clone)]
18pub struct InlineConfig {
19 disabled_at_line: HashMap<usize, HashSet<String>>,
21 line_disabled_rules: HashMap<usize, HashSet<String>>,
23}
24
25impl Default for InlineConfig {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl InlineConfig {
32 pub fn new() -> Self {
33 Self {
34 disabled_at_line: HashMap::new(),
35 line_disabled_rules: HashMap::new(),
36 }
37 }
38
39 pub fn from_content(content: &str) -> Self {
41 let mut config = Self::new();
42 let lines: Vec<&str> = content.lines().collect();
43
44 let mut currently_disabled = HashSet::new();
46 let mut capture_stack: Vec<HashSet<String>> = Vec::new();
47
48 for (idx, line) in lines.iter().enumerate() {
49 let line_num = idx + 1; config.disabled_at_line.insert(line_num, currently_disabled.clone());
54
55 if let Some(rules) = parse_disable_next_line_comment(line) {
59 let next_line = line_num + 1;
60 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
61 if rules.is_empty() {
62 line_rules.insert("*".to_string());
64 } else {
65 for rule in rules {
66 line_rules.insert(rule.to_string());
67 }
68 }
69 }
70 else if let Some(rules) = parse_disable_line_comment(line) {
72 let line_rules = config.line_disabled_rules.entry(line_num).or_default();
73 if rules.is_empty() {
74 line_rules.insert("*".to_string());
76 } else {
77 for rule in rules {
78 line_rules.insert(rule.to_string());
79 }
80 }
81 }
82 else if is_capture_comment(line) {
84 capture_stack.push(currently_disabled.clone());
85 }
86 else if is_restore_comment(line) {
88 if let Some(captured) = capture_stack.pop() {
89 currently_disabled = captured;
90 }
91 }
92 else if let Some(rules) = parse_disable_comment(line) {
94 if rules.is_empty() {
95 currently_disabled.clear();
97 currently_disabled.insert("*".to_string());
98 } else {
99 for rule in rules {
100 currently_disabled.insert(rule.to_string());
101 }
102 }
103 }
104 else if let Some(rules) = parse_enable_comment(line) {
106 if rules.is_empty() {
107 currently_disabled.clear();
109 } else {
110 for rule in rules {
112 currently_disabled.remove(rule);
113 }
114 }
115 }
116 }
117
118 config
119 }
120
121 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
123 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
125 if line_rules.contains("*") || line_rules.contains(rule_name) {
126 return true;
127 }
128 }
129
130 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
132 disabled_set.contains("*") || disabled_set.contains(rule_name)
133 } else {
134 false
135 }
136 }
137
138 pub fn get_disabled_rules(&self, line_number: usize) -> HashSet<String> {
140 let mut disabled = HashSet::new();
141
142 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
144 for rule in disabled_set {
145 disabled.insert(rule.clone());
146 }
147 }
148
149 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
151 for rule in line_rules {
152 disabled.insert(rule.clone());
153 }
154 }
155
156 disabled
157 }
158}
159
160pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
162 for prefix in &["<!-- rumdl-disable", "<!-- markdownlint-disable"] {
164 if let Some(start) = line.find(prefix) {
165 let after_prefix = &line[start + prefix.len()..];
166
167 if after_prefix.trim_start().starts_with("-->") {
169 return Some(Vec::new()); }
171
172 if let Some(end) = after_prefix.find("-->") {
174 let rules_str = after_prefix[..end].trim();
175 if !rules_str.is_empty() {
176 let rules: Vec<&str> = rules_str.split_whitespace().collect();
177 return Some(rules);
178 }
179 }
180 }
181 }
182
183 None
184}
185
186pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
188 for prefix in &["<!-- rumdl-enable", "<!-- markdownlint-enable"] {
190 if let Some(start) = line.find(prefix) {
191 let after_prefix = &line[start + prefix.len()..];
192
193 if after_prefix.trim_start().starts_with("-->") {
195 return Some(Vec::new()); }
197
198 if let Some(end) = after_prefix.find("-->") {
200 let rules_str = after_prefix[..end].trim();
201 if !rules_str.is_empty() {
202 let rules: Vec<&str> = rules_str.split_whitespace().collect();
203 return Some(rules);
204 }
205 }
206 }
207 }
208
209 None
210}
211
212pub fn parse_disable_line_comment(line: &str) -> Option<Vec<&str>> {
214 for prefix in &["<!-- rumdl-disable-line", "<!-- markdownlint-disable-line"] {
216 if let Some(start) = line.find(prefix) {
217 let after_prefix = &line[start + prefix.len()..];
218
219 if after_prefix.trim_start().starts_with("-->") {
221 return Some(Vec::new()); }
223
224 if let Some(end) = after_prefix.find("-->") {
226 let rules_str = after_prefix[..end].trim();
227 if !rules_str.is_empty() {
228 let rules: Vec<&str> = rules_str.split_whitespace().collect();
229 return Some(rules);
230 }
231 }
232 }
233 }
234
235 None
236}
237
238pub fn parse_disable_next_line_comment(line: &str) -> Option<Vec<&str>> {
240 for prefix in &["<!-- rumdl-disable-next-line", "<!-- markdownlint-disable-next-line"] {
242 if let Some(start) = line.find(prefix) {
243 let after_prefix = &line[start + prefix.len()..];
244
245 if after_prefix.trim_start().starts_with("-->") {
247 return Some(Vec::new()); }
249
250 if let Some(end) = after_prefix.find("-->") {
252 let rules_str = after_prefix[..end].trim();
253 if !rules_str.is_empty() {
254 let rules: Vec<&str> = rules_str.split_whitespace().collect();
255 return Some(rules);
256 }
257 }
258 }
259 }
260
261 None
262}
263
264pub fn is_capture_comment(line: &str) -> bool {
266 line.contains("<!-- markdownlint-capture -->") || line.contains("<!-- rumdl-capture -->")
267}
268
269pub fn is_restore_comment(line: &str) -> bool {
271 line.contains("<!-- markdownlint-restore -->") || line.contains("<!-- rumdl-restore -->")
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_parse_disable_comment() {
280 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
282 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
283
284 assert_eq!(
286 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
287 Some(vec!["MD001", "MD002"])
288 );
289
290 assert_eq!(parse_disable_comment("Some regular text"), None);
292 }
293
294 #[test]
295 fn test_parse_disable_line_comment() {
296 assert_eq!(
298 parse_disable_line_comment("<!-- markdownlint-disable-line -->"),
299 Some(vec![])
300 );
301
302 assert_eq!(
304 parse_disable_line_comment("<!-- markdownlint-disable-line MD013 -->"),
305 Some(vec!["MD013"])
306 );
307
308 assert_eq!(parse_disable_line_comment("Some regular text"), None);
310 }
311
312 #[test]
313 fn test_inline_config_from_content() {
314 let content = r#"# Test Document
315
316<!-- markdownlint-disable MD013 -->
317This is a very long line that would normally trigger MD013 but it's disabled
318
319<!-- markdownlint-enable MD013 -->
320This line will be checked again
321
322<!-- markdownlint-disable-next-line MD001 -->
323# This heading will not be checked for MD001
324## But this one will
325
326Some text <!-- markdownlint-disable-line MD013 -->
327
328<!-- markdownlint-capture -->
329<!-- markdownlint-disable MD001 MD002 -->
330# Heading with MD001 disabled
331<!-- markdownlint-restore -->
332# Heading with MD001 enabled again
333"#;
334
335 let config = InlineConfig::from_content(content);
336
337 assert!(config.is_rule_disabled("MD013", 4));
339
340 assert!(!config.is_rule_disabled("MD013", 7));
342
343 assert!(config.is_rule_disabled("MD001", 10));
345
346 assert!(!config.is_rule_disabled("MD001", 11));
348
349 assert!(config.is_rule_disabled("MD013", 13));
351
352 assert!(!config.is_rule_disabled("MD001", 19));
354 }
355
356 #[test]
357 fn test_capture_restore() {
358 let content = r#"<!-- markdownlint-disable MD001 -->
359<!-- markdownlint-capture -->
360<!-- markdownlint-disable MD002 MD003 -->
361<!-- markdownlint-restore -->
362Some content after restore
363"#;
364
365 let config = InlineConfig::from_content(content);
366
367 assert!(config.is_rule_disabled("MD001", 5));
369 assert!(!config.is_rule_disabled("MD002", 5));
370 assert!(!config.is_rule_disabled("MD003", 5));
371 }
372}