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}