rumdl_lib/
inline_config.rs1use crate::utils::code_block_utils::CodeBlockUtils;
21use serde_json::Value as JsonValue;
22use std::collections::{HashMap, HashSet};
23
24#[derive(Debug, Clone)]
25pub struct InlineConfig {
26 disabled_at_line: HashMap<usize, HashSet<String>>,
28 enabled_at_line: HashMap<usize, HashSet<String>>,
31 line_disabled_rules: HashMap<usize, HashSet<String>>,
33 file_disabled_rules: HashSet<String>,
35 file_enabled_rules: HashSet<String>,
37 file_rule_config: HashMap<String, JsonValue>,
40}
41
42impl Default for InlineConfig {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl InlineConfig {
49 pub fn new() -> Self {
50 Self {
51 disabled_at_line: HashMap::new(),
52 enabled_at_line: HashMap::new(),
53 line_disabled_rules: HashMap::new(),
54 file_disabled_rules: HashSet::new(),
55 file_enabled_rules: HashSet::new(),
56 file_rule_config: HashMap::new(),
57 }
58 }
59
60 pub fn from_content(content: &str) -> Self {
62 let mut config = Self::new();
63 let lines: Vec<&str> = content.lines().collect();
64
65 let code_blocks = CodeBlockUtils::detect_code_blocks(content);
67
68 let mut line_positions = Vec::with_capacity(lines.len());
70 let mut pos = 0;
71 for line in &lines {
72 line_positions.push(pos);
73 pos += line.len() + 1; }
75
76 let mut currently_disabled = HashSet::new();
78 let mut currently_enabled = HashSet::new(); let mut capture_stack: Vec<(HashSet<String>, HashSet<String>)> = Vec::new();
80
81 for (idx, line) in lines.iter().enumerate() {
82 let line_num = idx + 1; config.disabled_at_line.insert(line_num, currently_disabled.clone());
87 config.enabled_at_line.insert(line_num, currently_enabled.clone());
88
89 let line_start = line_positions[idx];
91 let line_end = line_start + line.len();
92 let in_code_block = code_blocks
93 .iter()
94 .any(|&(block_start, block_end)| line_start >= block_start && line_end <= block_end);
95
96 if in_code_block {
97 continue;
98 }
99
100 if let Some(rules) = parse_disable_file_comment(line) {
103 if rules.is_empty() {
104 config.file_disabled_rules.clear();
106 config.file_disabled_rules.insert("*".to_string());
107 } else {
108 if config.file_disabled_rules.contains("*") {
110 for rule in rules {
112 config.file_enabled_rules.remove(rule);
113 }
114 } else {
115 for rule in rules {
117 config.file_disabled_rules.insert(rule.to_string());
118 }
119 }
120 }
121 }
122
123 if let Some(rules) = parse_enable_file_comment(line) {
125 if rules.is_empty() {
126 config.file_disabled_rules.clear();
128 config.file_enabled_rules.clear();
129 } else {
130 if config.file_disabled_rules.contains("*") {
132 for rule in rules {
134 config.file_enabled_rules.insert(rule.to_string());
135 }
136 } else {
137 for rule in rules {
139 config.file_disabled_rules.remove(rule);
140 }
141 }
142 }
143 }
144
145 if let Some(json_config) = parse_configure_file_comment(line) {
147 if let Some(obj) = json_config.as_object() {
149 for (rule_name, rule_config) in obj {
150 config.file_rule_config.insert(rule_name.clone(), rule_config.clone());
151 }
152 }
153 }
154
155 if let Some(rules) = parse_disable_next_line_comment(line) {
160 let next_line = line_num + 1;
161 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
162 if rules.is_empty() {
163 line_rules.insert("*".to_string());
165 } else {
166 for rule in rules {
167 line_rules.insert(rule.to_string());
168 }
169 }
170 }
171
172 if let Some(rules) = parse_disable_line_comment(line) {
174 let line_rules = config.line_disabled_rules.entry(line_num).or_default();
175 if rules.is_empty() {
176 line_rules.insert("*".to_string());
178 } else {
179 for rule in rules {
180 line_rules.insert(rule.to_string());
181 }
182 }
183 }
184
185 let mut processed_capture = false;
188 let mut processed_restore = false;
189
190 let mut comment_positions = Vec::new();
192
193 if let Some(pos) = line.find("<!-- markdownlint-disable")
194 && !line[pos..].contains("<!-- markdownlint-disable-line")
195 && !line[pos..].contains("<!-- markdownlint-disable-next-line")
196 {
197 comment_positions.push((pos, "disable"));
198 }
199 if let Some(pos) = line.find("<!-- rumdl-disable")
200 && !line[pos..].contains("<!-- rumdl-disable-line")
201 && !line[pos..].contains("<!-- rumdl-disable-next-line")
202 {
203 comment_positions.push((pos, "disable"));
204 }
205
206 if let Some(pos) = line.find("<!-- markdownlint-enable") {
207 comment_positions.push((pos, "enable"));
208 }
209 if let Some(pos) = line.find("<!-- rumdl-enable") {
210 comment_positions.push((pos, "enable"));
211 }
212
213 if let Some(pos) = line.find("<!-- markdownlint-capture") {
214 comment_positions.push((pos, "capture"));
215 }
216 if let Some(pos) = line.find("<!-- rumdl-capture") {
217 comment_positions.push((pos, "capture"));
218 }
219
220 if let Some(pos) = line.find("<!-- markdownlint-restore") {
221 comment_positions.push((pos, "restore"));
222 }
223 if let Some(pos) = line.find("<!-- rumdl-restore") {
224 comment_positions.push((pos, "restore"));
225 }
226
227 comment_positions.sort_by_key(|&(pos, _)| pos);
229
230 for (_, comment_type) in comment_positions {
232 match comment_type {
233 "disable" => {
234 if let Some(rules) = parse_disable_comment(line) {
235 if rules.is_empty() {
236 currently_disabled.clear();
238 currently_disabled.insert("*".to_string());
239 currently_enabled.clear(); } else {
241 if currently_disabled.contains("*") {
243 for rule in rules {
245 currently_enabled.remove(rule);
246 }
247 } else {
248 for rule in rules {
250 currently_disabled.insert(rule.to_string());
251 }
252 }
253 }
254 }
255 }
256 "enable" => {
257 if let Some(rules) = parse_enable_comment(line) {
258 if rules.is_empty() {
259 currently_disabled.clear();
261 currently_enabled.clear();
262 } else {
263 if currently_disabled.contains("*") {
265 for rule in rules {
267 currently_enabled.insert(rule.to_string());
268 }
269 } else {
270 for rule in rules {
272 currently_disabled.remove(rule);
273 }
274 }
275 }
276 }
277 }
278 "capture" => {
279 if !processed_capture && is_capture_comment(line) {
280 capture_stack.push((currently_disabled.clone(), currently_enabled.clone()));
281 processed_capture = true;
282 }
283 }
284 "restore" => {
285 if !processed_restore && is_restore_comment(line) {
286 if let Some((disabled, enabled)) = capture_stack.pop() {
287 currently_disabled = disabled;
288 currently_enabled = enabled;
289 }
290 processed_restore = true;
291 }
292 }
293 _ => {}
294 }
295 }
296 }
297
298 config
299 }
300
301 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
303 if self.file_disabled_rules.contains("*") {
305 return !self.file_enabled_rules.contains(rule_name);
307 } else if self.file_disabled_rules.contains(rule_name) {
308 return true;
309 }
310
311 if let Some(line_rules) = self.line_disabled_rules.get(&line_number)
313 && (line_rules.contains("*") || line_rules.contains(rule_name))
314 {
315 return true;
316 }
317
318 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
320 if disabled_set.contains("*") {
321 if let Some(enabled_set) = self.enabled_at_line.get(&line_number) {
323 return !enabled_set.contains(rule_name);
324 }
325 return true; } else {
327 return disabled_set.contains(rule_name);
328 }
329 }
330
331 false
332 }
333
334 pub fn get_disabled_rules(&self, line_number: usize) -> HashSet<String> {
336 let mut disabled = HashSet::new();
337
338 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
340 if disabled_set.contains("*") {
341 disabled.insert("*".to_string());
343 } else {
346 for rule in disabled_set {
347 disabled.insert(rule.clone());
348 }
349 }
350 }
351
352 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
354 for rule in line_rules {
355 disabled.insert(rule.clone());
356 }
357 }
358
359 disabled
360 }
361
362 pub fn get_rule_config(&self, rule_name: &str) -> Option<&JsonValue> {
364 self.file_rule_config.get(rule_name)
365 }
366
367 pub fn get_all_rule_configs(&self) -> &HashMap<String, JsonValue> {
369 &self.file_rule_config
370 }
371}
372
373pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
375 for prefix in &["<!-- rumdl-disable", "<!-- markdownlint-disable"] {
377 if let Some(start) = line.find(prefix) {
378 let after_prefix = &line[start + prefix.len()..];
379
380 if after_prefix.trim_start().starts_with("-->") {
382 return Some(Vec::new()); }
384
385 if let Some(end) = after_prefix.find("-->") {
387 let rules_str = after_prefix[..end].trim();
388 if !rules_str.is_empty() {
389 let rules: Vec<&str> = rules_str.split_whitespace().collect();
390 return Some(rules);
391 }
392 }
393 }
394 }
395
396 None
397}
398
399pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
401 for prefix in &["<!-- rumdl-enable", "<!-- markdownlint-enable"] {
403 if let Some(start) = line.find(prefix) {
404 let after_prefix = &line[start + prefix.len()..];
405
406 if after_prefix.trim_start().starts_with("-->") {
408 return Some(Vec::new()); }
410
411 if let Some(end) = after_prefix.find("-->") {
413 let rules_str = after_prefix[..end].trim();
414 if !rules_str.is_empty() {
415 let rules: Vec<&str> = rules_str.split_whitespace().collect();
416 return Some(rules);
417 }
418 }
419 }
420 }
421
422 None
423}
424
425pub fn parse_disable_line_comment(line: &str) -> Option<Vec<&str>> {
427 for prefix in &["<!-- rumdl-disable-line", "<!-- markdownlint-disable-line"] {
429 if let Some(start) = line.find(prefix) {
430 let after_prefix = &line[start + prefix.len()..];
431
432 if after_prefix.trim_start().starts_with("-->") {
434 return Some(Vec::new()); }
436
437 if let Some(end) = after_prefix.find("-->") {
439 let rules_str = after_prefix[..end].trim();
440 if !rules_str.is_empty() {
441 let rules: Vec<&str> = rules_str.split_whitespace().collect();
442 return Some(rules);
443 }
444 }
445 }
446 }
447
448 None
449}
450
451pub fn parse_disable_next_line_comment(line: &str) -> Option<Vec<&str>> {
453 for prefix in &["<!-- rumdl-disable-next-line", "<!-- markdownlint-disable-next-line"] {
455 if let Some(start) = line.find(prefix) {
456 let after_prefix = &line[start + prefix.len()..];
457
458 if after_prefix.trim_start().starts_with("-->") {
460 return Some(Vec::new()); }
462
463 if let Some(end) = after_prefix.find("-->") {
465 let rules_str = after_prefix[..end].trim();
466 if !rules_str.is_empty() {
467 let rules: Vec<&str> = rules_str.split_whitespace().collect();
468 return Some(rules);
469 }
470 }
471 }
472 }
473
474 None
475}
476
477pub fn is_capture_comment(line: &str) -> bool {
479 line.contains("<!-- markdownlint-capture -->") || line.contains("<!-- rumdl-capture -->")
480}
481
482pub fn is_restore_comment(line: &str) -> bool {
484 line.contains("<!-- markdownlint-restore -->") || line.contains("<!-- rumdl-restore -->")
485}
486
487pub fn parse_disable_file_comment(line: &str) -> Option<Vec<&str>> {
489 for prefix in &["<!-- rumdl-disable-file", "<!-- markdownlint-disable-file"] {
491 if let Some(start) = line.find(prefix) {
492 let after_prefix = &line[start + prefix.len()..];
493
494 if after_prefix.trim_start().starts_with("-->") {
496 return Some(Vec::new()); }
498
499 if let Some(end) = after_prefix.find("-->") {
501 let rules_str = after_prefix[..end].trim();
502 if !rules_str.is_empty() {
503 let rules: Vec<&str> = rules_str.split_whitespace().collect();
504 return Some(rules);
505 }
506 }
507 }
508 }
509
510 None
511}
512
513pub fn parse_enable_file_comment(line: &str) -> Option<Vec<&str>> {
515 for prefix in &["<!-- rumdl-enable-file", "<!-- markdownlint-enable-file"] {
517 if let Some(start) = line.find(prefix) {
518 let after_prefix = &line[start + prefix.len()..];
519
520 if after_prefix.trim_start().starts_with("-->") {
522 return Some(Vec::new()); }
524
525 if let Some(end) = after_prefix.find("-->") {
527 let rules_str = after_prefix[..end].trim();
528 if !rules_str.is_empty() {
529 let rules: Vec<&str> = rules_str.split_whitespace().collect();
530 return Some(rules);
531 }
532 }
533 }
534 }
535
536 None
537}
538
539pub fn parse_configure_file_comment(line: &str) -> Option<JsonValue> {
541 for prefix in &["<!-- rumdl-configure-file", "<!-- markdownlint-configure-file"] {
543 if let Some(start) = line.find(prefix) {
544 let after_prefix = &line[start + prefix.len()..];
545
546 if let Some(end) = after_prefix.find("-->") {
548 let json_str = after_prefix[..end].trim();
549 if !json_str.is_empty() {
550 if let Ok(value) = serde_json::from_str(json_str) {
552 return Some(value);
553 }
554 }
555 }
556 }
557 }
558
559 None
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn test_parse_disable_comment() {
568 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
570 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
571
572 assert_eq!(
574 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
575 Some(vec!["MD001", "MD002"])
576 );
577
578 assert_eq!(parse_disable_comment("Some regular text"), None);
580 }
581
582 #[test]
583 fn test_parse_disable_line_comment() {
584 assert_eq!(
586 parse_disable_line_comment("<!-- markdownlint-disable-line -->"),
587 Some(vec![])
588 );
589
590 assert_eq!(
592 parse_disable_line_comment("<!-- markdownlint-disable-line MD013 -->"),
593 Some(vec!["MD013"])
594 );
595
596 assert_eq!(parse_disable_line_comment("Some regular text"), None);
598 }
599
600 #[test]
601 fn test_inline_config_from_content() {
602 let content = r#"# Test Document
603
604<!-- markdownlint-disable MD013 -->
605This is a very long line that would normally trigger MD013 but it's disabled
606
607<!-- markdownlint-enable MD013 -->
608This line will be checked again
609
610<!-- markdownlint-disable-next-line MD001 -->
611# This heading will not be checked for MD001
612## But this one will
613
614Some text <!-- markdownlint-disable-line MD013 -->
615
616<!-- markdownlint-capture -->
617<!-- markdownlint-disable MD001 MD002 -->
618# Heading with MD001 disabled
619<!-- markdownlint-restore -->
620# Heading with MD001 enabled again
621"#;
622
623 let config = InlineConfig::from_content(content);
624
625 assert!(config.is_rule_disabled("MD013", 4));
627
628 assert!(!config.is_rule_disabled("MD013", 7));
630
631 assert!(config.is_rule_disabled("MD001", 10));
633
634 assert!(!config.is_rule_disabled("MD001", 11));
636
637 assert!(config.is_rule_disabled("MD013", 13));
639
640 assert!(!config.is_rule_disabled("MD001", 19));
642 }
643
644 #[test]
645 fn test_capture_restore() {
646 let content = r#"<!-- markdownlint-disable MD001 -->
647<!-- markdownlint-capture -->
648<!-- markdownlint-disable MD002 MD003 -->
649<!-- markdownlint-restore -->
650Some content after restore
651"#;
652
653 let config = InlineConfig::from_content(content);
654
655 assert!(config.is_rule_disabled("MD001", 5));
657 assert!(!config.is_rule_disabled("MD002", 5));
658 assert!(!config.is_rule_disabled("MD003", 5));
659 }
660}