rumdl_lib/
inline_config.rs1use crate::utils::code_block_utils::CodeBlockUtils;
22use serde_json::Value as JsonValue;
23use std::collections::{HashMap, HashSet};
24
25#[derive(Debug, Clone)]
26pub struct InlineConfig {
27 disabled_at_line: HashMap<usize, HashSet<String>>,
29 enabled_at_line: HashMap<usize, HashSet<String>>,
32 line_disabled_rules: HashMap<usize, HashSet<String>>,
34 file_disabled_rules: HashSet<String>,
36 file_enabled_rules: HashSet<String>,
38 file_rule_config: HashMap<String, JsonValue>,
41}
42
43impl Default for InlineConfig {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl InlineConfig {
50 pub fn new() -> Self {
51 Self {
52 disabled_at_line: HashMap::new(),
53 enabled_at_line: HashMap::new(),
54 line_disabled_rules: HashMap::new(),
55 file_disabled_rules: HashSet::new(),
56 file_enabled_rules: HashSet::new(),
57 file_rule_config: HashMap::new(),
58 }
59 }
60
61 pub fn from_content(content: &str) -> Self {
63 let mut config = Self::new();
64 let lines: Vec<&str> = content.lines().collect();
65
66 let code_blocks = CodeBlockUtils::detect_code_blocks(content);
68
69 let mut line_positions = Vec::with_capacity(lines.len());
71 let mut pos = 0;
72 for line in &lines {
73 line_positions.push(pos);
74 pos += line.len() + 1; }
76
77 let mut currently_disabled = HashSet::new();
79 let mut currently_enabled = HashSet::new(); let mut capture_stack: Vec<(HashSet<String>, HashSet<String>)> = Vec::new();
81
82 for (idx, line) in lines.iter().enumerate() {
83 let line_num = idx + 1; config.disabled_at_line.insert(line_num, currently_disabled.clone());
88 config.enabled_at_line.insert(line_num, currently_enabled.clone());
89
90 let line_start = line_positions[idx];
92 let line_end = line_start + line.len();
93 let in_code_block = code_blocks
94 .iter()
95 .any(|&(block_start, block_end)| line_start >= block_start && line_end <= block_end);
96
97 if in_code_block {
98 continue;
99 }
100
101 if let Some(rules) = parse_disable_file_comment(line) {
104 if rules.is_empty() {
105 config.file_disabled_rules.clear();
107 config.file_disabled_rules.insert("*".to_string());
108 } else {
109 if config.file_disabled_rules.contains("*") {
111 for rule in rules {
113 config.file_enabled_rules.remove(rule);
114 }
115 } else {
116 for rule in rules {
118 config.file_disabled_rules.insert(rule.to_string());
119 }
120 }
121 }
122 }
123
124 if let Some(rules) = parse_enable_file_comment(line) {
126 if rules.is_empty() {
127 config.file_disabled_rules.clear();
129 config.file_enabled_rules.clear();
130 } else {
131 if config.file_disabled_rules.contains("*") {
133 for rule in rules {
135 config.file_enabled_rules.insert(rule.to_string());
136 }
137 } else {
138 for rule in rules {
140 config.file_disabled_rules.remove(rule);
141 }
142 }
143 }
144 }
145
146 if let Some(json_config) = parse_configure_file_comment(line) {
148 if let Some(obj) = json_config.as_object() {
150 for (rule_name, rule_config) in obj {
151 config.file_rule_config.insert(rule_name.clone(), rule_config.clone());
152 }
153 }
154 }
155
156 if let Some(rules) = parse_disable_next_line_comment(line) {
161 let next_line = line_num + 1;
162 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
163 if rules.is_empty() {
164 line_rules.insert("*".to_string());
166 } else {
167 for rule in rules {
168 line_rules.insert(rule.to_string());
169 }
170 }
171 }
172
173 if line.contains("<!-- prettier-ignore -->") {
175 let next_line = line_num + 1;
176 let line_rules = config.line_disabled_rules.entry(next_line).or_default();
177 line_rules.insert("*".to_string());
178 }
179
180 if let Some(rules) = parse_disable_line_comment(line) {
182 let line_rules = config.line_disabled_rules.entry(line_num).or_default();
183 if rules.is_empty() {
184 line_rules.insert("*".to_string());
186 } else {
187 for rule in rules {
188 line_rules.insert(rule.to_string());
189 }
190 }
191 }
192
193 let mut processed_capture = false;
196 let mut processed_restore = false;
197
198 let mut comment_positions = Vec::new();
200
201 if let Some(pos) = line.find("<!-- markdownlint-disable")
202 && !line[pos..].contains("<!-- markdownlint-disable-line")
203 && !line[pos..].contains("<!-- markdownlint-disable-next-line")
204 {
205 comment_positions.push((pos, "disable"));
206 }
207 if let Some(pos) = line.find("<!-- rumdl-disable")
208 && !line[pos..].contains("<!-- rumdl-disable-line")
209 && !line[pos..].contains("<!-- rumdl-disable-next-line")
210 {
211 comment_positions.push((pos, "disable"));
212 }
213
214 if let Some(pos) = line.find("<!-- markdownlint-enable") {
215 comment_positions.push((pos, "enable"));
216 }
217 if let Some(pos) = line.find("<!-- rumdl-enable") {
218 comment_positions.push((pos, "enable"));
219 }
220
221 if let Some(pos) = line.find("<!-- markdownlint-capture") {
222 comment_positions.push((pos, "capture"));
223 }
224 if let Some(pos) = line.find("<!-- rumdl-capture") {
225 comment_positions.push((pos, "capture"));
226 }
227
228 if let Some(pos) = line.find("<!-- markdownlint-restore") {
229 comment_positions.push((pos, "restore"));
230 }
231 if let Some(pos) = line.find("<!-- rumdl-restore") {
232 comment_positions.push((pos, "restore"));
233 }
234
235 comment_positions.sort_by_key(|&(pos, _)| pos);
237
238 for (_, comment_type) in comment_positions {
240 match comment_type {
241 "disable" => {
242 if let Some(rules) = parse_disable_comment(line) {
243 if rules.is_empty() {
244 currently_disabled.clear();
246 currently_disabled.insert("*".to_string());
247 currently_enabled.clear(); } else {
249 if currently_disabled.contains("*") {
251 for rule in rules {
253 currently_enabled.remove(rule);
254 }
255 } else {
256 for rule in rules {
258 currently_disabled.insert(rule.to_string());
259 }
260 }
261 }
262 }
263 }
264 "enable" => {
265 if let Some(rules) = parse_enable_comment(line) {
266 if rules.is_empty() {
267 currently_disabled.clear();
269 currently_enabled.clear();
270 } else {
271 if currently_disabled.contains("*") {
273 for rule in rules {
275 currently_enabled.insert(rule.to_string());
276 }
277 } else {
278 for rule in rules {
280 currently_disabled.remove(rule);
281 }
282 }
283 }
284 }
285 }
286 "capture" => {
287 if !processed_capture && is_capture_comment(line) {
288 capture_stack.push((currently_disabled.clone(), currently_enabled.clone()));
289 processed_capture = true;
290 }
291 }
292 "restore" => {
293 if !processed_restore && is_restore_comment(line) {
294 if let Some((disabled, enabled)) = capture_stack.pop() {
295 currently_disabled = disabled;
296 currently_enabled = enabled;
297 }
298 processed_restore = true;
299 }
300 }
301 _ => {}
302 }
303 }
304 }
305
306 config
307 }
308
309 pub fn is_rule_disabled(&self, rule_name: &str, line_number: usize) -> bool {
311 if self.file_disabled_rules.contains("*") {
313 return !self.file_enabled_rules.contains(rule_name);
315 } else if self.file_disabled_rules.contains(rule_name) {
316 return true;
317 }
318
319 if let Some(line_rules) = self.line_disabled_rules.get(&line_number)
321 && (line_rules.contains("*") || line_rules.contains(rule_name))
322 {
323 return true;
324 }
325
326 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
328 if disabled_set.contains("*") {
329 if let Some(enabled_set) = self.enabled_at_line.get(&line_number) {
331 return !enabled_set.contains(rule_name);
332 }
333 return true; } else {
335 return disabled_set.contains(rule_name);
336 }
337 }
338
339 false
340 }
341
342 pub fn get_disabled_rules(&self, line_number: usize) -> HashSet<String> {
344 let mut disabled = HashSet::new();
345
346 if let Some(disabled_set) = self.disabled_at_line.get(&line_number) {
348 if disabled_set.contains("*") {
349 disabled.insert("*".to_string());
351 } else {
354 for rule in disabled_set {
355 disabled.insert(rule.clone());
356 }
357 }
358 }
359
360 if let Some(line_rules) = self.line_disabled_rules.get(&line_number) {
362 for rule in line_rules {
363 disabled.insert(rule.clone());
364 }
365 }
366
367 disabled
368 }
369
370 pub fn get_rule_config(&self, rule_name: &str) -> Option<&JsonValue> {
372 self.file_rule_config.get(rule_name)
373 }
374
375 pub fn get_all_rule_configs(&self) -> &HashMap<String, JsonValue> {
377 &self.file_rule_config
378 }
379}
380
381pub fn parse_disable_comment(line: &str) -> Option<Vec<&str>> {
383 for prefix in &["<!-- rumdl-disable", "<!-- markdownlint-disable"] {
385 if let Some(start) = line.find(prefix) {
386 let after_prefix = &line[start + prefix.len()..];
387
388 if after_prefix.trim_start().starts_with("-->") {
390 return Some(Vec::new()); }
392
393 if let Some(end) = after_prefix.find("-->") {
395 let rules_str = after_prefix[..end].trim();
396 if !rules_str.is_empty() {
397 let rules: Vec<&str> = rules_str.split_whitespace().collect();
398 return Some(rules);
399 }
400 }
401 }
402 }
403
404 None
405}
406
407pub fn parse_enable_comment(line: &str) -> Option<Vec<&str>> {
409 for prefix in &["<!-- rumdl-enable", "<!-- markdownlint-enable"] {
411 if let Some(start) = line.find(prefix) {
412 let after_prefix = &line[start + prefix.len()..];
413
414 if after_prefix.trim_start().starts_with("-->") {
416 return Some(Vec::new()); }
418
419 if let Some(end) = after_prefix.find("-->") {
421 let rules_str = after_prefix[..end].trim();
422 if !rules_str.is_empty() {
423 let rules: Vec<&str> = rules_str.split_whitespace().collect();
424 return Some(rules);
425 }
426 }
427 }
428 }
429
430 None
431}
432
433pub fn parse_disable_line_comment(line: &str) -> Option<Vec<&str>> {
435 for prefix in &["<!-- rumdl-disable-line", "<!-- markdownlint-disable-line"] {
437 if let Some(start) = line.find(prefix) {
438 let after_prefix = &line[start + prefix.len()..];
439
440 if after_prefix.trim_start().starts_with("-->") {
442 return Some(Vec::new()); }
444
445 if let Some(end) = after_prefix.find("-->") {
447 let rules_str = after_prefix[..end].trim();
448 if !rules_str.is_empty() {
449 let rules: Vec<&str> = rules_str.split_whitespace().collect();
450 return Some(rules);
451 }
452 }
453 }
454 }
455
456 None
457}
458
459pub fn parse_disable_next_line_comment(line: &str) -> Option<Vec<&str>> {
461 for prefix in &["<!-- rumdl-disable-next-line", "<!-- markdownlint-disable-next-line"] {
463 if let Some(start) = line.find(prefix) {
464 let after_prefix = &line[start + prefix.len()..];
465
466 if after_prefix.trim_start().starts_with("-->") {
468 return Some(Vec::new()); }
470
471 if let Some(end) = after_prefix.find("-->") {
473 let rules_str = after_prefix[..end].trim();
474 if !rules_str.is_empty() {
475 let rules: Vec<&str> = rules_str.split_whitespace().collect();
476 return Some(rules);
477 }
478 }
479 }
480 }
481
482 None
483}
484
485pub fn is_capture_comment(line: &str) -> bool {
487 line.contains("<!-- markdownlint-capture -->") || line.contains("<!-- rumdl-capture -->")
488}
489
490pub fn is_restore_comment(line: &str) -> bool {
492 line.contains("<!-- markdownlint-restore -->") || line.contains("<!-- rumdl-restore -->")
493}
494
495pub fn parse_disable_file_comment(line: &str) -> Option<Vec<&str>> {
497 for prefix in &["<!-- rumdl-disable-file", "<!-- markdownlint-disable-file"] {
499 if let Some(start) = line.find(prefix) {
500 let after_prefix = &line[start + prefix.len()..];
501
502 if after_prefix.trim_start().starts_with("-->") {
504 return Some(Vec::new()); }
506
507 if let Some(end) = after_prefix.find("-->") {
509 let rules_str = after_prefix[..end].trim();
510 if !rules_str.is_empty() {
511 let rules: Vec<&str> = rules_str.split_whitespace().collect();
512 return Some(rules);
513 }
514 }
515 }
516 }
517
518 None
519}
520
521pub fn parse_enable_file_comment(line: &str) -> Option<Vec<&str>> {
523 for prefix in &["<!-- rumdl-enable-file", "<!-- markdownlint-enable-file"] {
525 if let Some(start) = line.find(prefix) {
526 let after_prefix = &line[start + prefix.len()..];
527
528 if after_prefix.trim_start().starts_with("-->") {
530 return Some(Vec::new()); }
532
533 if let Some(end) = after_prefix.find("-->") {
535 let rules_str = after_prefix[..end].trim();
536 if !rules_str.is_empty() {
537 let rules: Vec<&str> = rules_str.split_whitespace().collect();
538 return Some(rules);
539 }
540 }
541 }
542 }
543
544 None
545}
546
547pub fn parse_configure_file_comment(line: &str) -> Option<JsonValue> {
549 for prefix in &["<!-- rumdl-configure-file", "<!-- markdownlint-configure-file"] {
551 if let Some(start) = line.find(prefix) {
552 let after_prefix = &line[start + prefix.len()..];
553
554 if let Some(end) = after_prefix.find("-->") {
556 let json_str = after_prefix[..end].trim();
557 if !json_str.is_empty() {
558 if let Ok(value) = serde_json::from_str(json_str) {
560 return Some(value);
561 }
562 }
563 }
564 }
565 }
566
567 None
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn test_parse_disable_comment() {
576 assert_eq!(parse_disable_comment("<!-- markdownlint-disable -->"), Some(vec![]));
578 assert_eq!(parse_disable_comment("<!-- rumdl-disable -->"), Some(vec![]));
579
580 assert_eq!(
582 parse_disable_comment("<!-- markdownlint-disable MD001 MD002 -->"),
583 Some(vec!["MD001", "MD002"])
584 );
585
586 assert_eq!(parse_disable_comment("Some regular text"), None);
588 }
589
590 #[test]
591 fn test_parse_disable_line_comment() {
592 assert_eq!(
594 parse_disable_line_comment("<!-- markdownlint-disable-line -->"),
595 Some(vec![])
596 );
597
598 assert_eq!(
600 parse_disable_line_comment("<!-- markdownlint-disable-line MD013 -->"),
601 Some(vec!["MD013"])
602 );
603
604 assert_eq!(parse_disable_line_comment("Some regular text"), None);
606 }
607
608 #[test]
609 fn test_inline_config_from_content() {
610 let content = r#"# Test Document
611
612<!-- markdownlint-disable MD013 -->
613This is a very long line that would normally trigger MD013 but it's disabled
614
615<!-- markdownlint-enable MD013 -->
616This line will be checked again
617
618<!-- markdownlint-disable-next-line MD001 -->
619# This heading will not be checked for MD001
620## But this one will
621
622Some text <!-- markdownlint-disable-line MD013 -->
623
624<!-- markdownlint-capture -->
625<!-- markdownlint-disable MD001 MD002 -->
626# Heading with MD001 disabled
627<!-- markdownlint-restore -->
628# Heading with MD001 enabled again
629"#;
630
631 let config = InlineConfig::from_content(content);
632
633 assert!(config.is_rule_disabled("MD013", 4));
635
636 assert!(!config.is_rule_disabled("MD013", 7));
638
639 assert!(config.is_rule_disabled("MD001", 10));
641
642 assert!(!config.is_rule_disabled("MD001", 11));
644
645 assert!(config.is_rule_disabled("MD013", 13));
647
648 assert!(!config.is_rule_disabled("MD001", 19));
650 }
651
652 #[test]
653 fn test_capture_restore() {
654 let content = r#"<!-- markdownlint-disable MD001 -->
655<!-- markdownlint-capture -->
656<!-- markdownlint-disable MD002 MD003 -->
657<!-- markdownlint-restore -->
658Some content after restore
659"#;
660
661 let config = InlineConfig::from_content(content);
662
663 assert!(config.is_rule_disabled("MD001", 5));
665 assert!(!config.is_rule_disabled("MD002", 5));
666 assert!(!config.is_rule_disabled("MD003", 5));
667 }
668}