mdwright_lint/stdlib/
duplicate_heading.rs1use std::collections::HashMap;
11
12use crate::diagnostic::Diagnostic;
13use crate::rule::LintRule;
14use mdwright_document::Document;
15
16pub struct DuplicateHeading;
17
18impl LintRule for DuplicateHeading {
19 fn name(&self) -> &str {
20 "duplicate-heading"
21 }
22
23 fn description(&self) -> &str {
24 "Two headings at the same level under the same parent with the same text."
25 }
26
27 fn explain(&self) -> &str {
28 include_str!("explain/duplicate_heading.md")
29 }
30
31 fn is_advisory(&self) -> bool {
32 true
37 }
38
39 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
40 let mut path: [Option<String>; 6] = Default::default();
44 let mut seen: HashMap<(usize, String, String), usize> = HashMap::new();
45 for h in doc.headings() {
46 let level = h.level as usize;
47 let parent_key = (0..level.saturating_sub(1))
49 .rev()
50 .find_map(|i| path.get(i).and_then(Option::as_ref).cloned())
51 .unwrap_or_default();
52 let key = (level, parent_key, h.text.to_ascii_lowercase());
53 if let Some(&first_offset) = seen.get(&key) {
54 let message = format!(
55 "duplicate heading `{}` at level {level}; first defined at byte {first_offset}",
56 h.text
57 );
58 let local = 0..(h.raw_range.end.saturating_sub(h.raw_range.start));
59 if let Some(d) = Diagnostic::at(doc, h.raw_range.start, local, message, None) {
60 out.push(d);
61 }
62 } else {
63 seen.insert(key, h.raw_range.start);
64 }
65 if let Some(slot) = path.get_mut(level.saturating_sub(1)) {
67 *slot = Some(h.text.clone());
68 }
69 for i in level..path.len() {
70 if let Some(slot) = path.get_mut(i) {
71 *slot = None;
72 }
73 }
74 }
75 }
76}