Skip to main content

brief/
validate.rs

1use crate::ast::{Block, Document};
2use crate::diag::{Code, Diagnostic};
3use crate::span::{SourceMap, Span};
4use std::collections::BTreeMap;
5
6pub struct ValidateOpts {
7    pub strict_heading_levels: bool,
8}
9
10impl Default for ValidateOpts {
11    fn default() -> Self {
12        ValidateOpts {
13            strict_heading_levels: false,
14        }
15    }
16}
17
18pub fn validate(doc: &Document, opts: &ValidateOpts, src: &SourceMap) -> Vec<Diagnostic> {
19    let mut diags = Vec::new();
20    if opts.strict_heading_levels {
21        check_heading_monotonic(doc, &mut diags);
22    }
23    check_duplicate_anchors(doc, src, &mut diags);
24    diags
25}
26
27fn check_heading_monotonic(doc: &Document, diags: &mut Vec<Diagnostic>) {
28    let mut last: u8 = 0;
29    for b in &doc.blocks {
30        if let Block::Heading { level, span, .. } = b {
31            if last > 0 && *level > last + 1 {
32                diags.push(
33                    Diagnostic::new(Code::HeadingMonotonic, *span)
34                        .label(format!("heading level jumps from {} to {}", last, level))
35                        .help("increment heading levels by at most one"),
36                );
37            }
38            last = *level;
39        }
40    }
41}
42
43fn check_duplicate_anchors(doc: &Document, src: &SourceMap, diags: &mut Vec<Diagnostic>) {
44    let mut seen: BTreeMap<String, Span> = BTreeMap::new();
45    for b in &doc.blocks {
46        collect_anchor(b, src, &mut seen, diags);
47    }
48}
49
50fn collect_anchor(
51    block: &Block,
52    src: &SourceMap,
53    seen: &mut BTreeMap<String, Span>,
54    diags: &mut Vec<Diagnostic>,
55) {
56    match block {
57        Block::Heading { anchor, span, .. } => {
58            if let Some(name) = anchor {
59                if let Some(first_span) = seen.get(name) {
60                    let (first_line, _) = src.line_col(first_span.start);
61                    diags.push(Diagnostic::new(Code::DuplicateHeadingAnchor, *span).label(
62                        format!("anchor `{}` already used at line {}", name, first_line),
63                    ));
64                } else {
65                    seen.insert(name.clone(), *span);
66                }
67            }
68        }
69        Block::Blockquote { children, .. } | Block::BlockShortcode { children, .. } => {
70            for c in children {
71                collect_anchor(c, src, seen, diags);
72            }
73        }
74        Block::List { items, .. } => {
75            for it in items {
76                for c in &it.children {
77                    collect_anchor(c, src, seen, diags);
78                }
79            }
80        }
81        Block::DefinitionList { .. } => {}
82        Block::Paragraph { .. }
83        | Block::CodeBlock { .. }
84        | Block::Table { .. }
85        | Block::HorizontalRule { .. } => {}
86    }
87}