brief-core 0.3.0

Compiler library for the Brief markup language: lexer, parser, AST, HTML/LLM emitters, formatter, and Markdown-to-Brief converter.
Documentation
use crate::ast::{Block, Document};
use crate::diag::{Code, Diagnostic};
use crate::span::{SourceMap, Span};
use std::collections::BTreeMap;

pub struct ValidateOpts {
    pub strict_heading_levels: bool,
}

impl Default for ValidateOpts {
    fn default() -> Self {
        ValidateOpts {
            strict_heading_levels: false,
        }
    }
}

pub fn validate(doc: &Document, opts: &ValidateOpts, src: &SourceMap) -> Vec<Diagnostic> {
    let mut diags = Vec::new();
    if opts.strict_heading_levels {
        check_heading_monotonic(doc, &mut diags);
    }
    check_duplicate_anchors(doc, src, &mut diags);
    diags
}

fn check_heading_monotonic(doc: &Document, diags: &mut Vec<Diagnostic>) {
    let mut last: u8 = 0;
    for b in &doc.blocks {
        if let Block::Heading { level, span, .. } = b {
            if last > 0 && *level > last + 1 {
                diags.push(
                    Diagnostic::new(Code::HeadingMonotonic, *span)
                        .label(format!("heading level jumps from {} to {}", last, level))
                        .help("increment heading levels by at most one"),
                );
            }
            last = *level;
        }
    }
}

fn check_duplicate_anchors(doc: &Document, src: &SourceMap, diags: &mut Vec<Diagnostic>) {
    let mut seen: BTreeMap<String, Span> = BTreeMap::new();
    for b in &doc.blocks {
        collect_anchor(b, src, &mut seen, diags);
    }
}

fn collect_anchor(
    block: &Block,
    src: &SourceMap,
    seen: &mut BTreeMap<String, Span>,
    diags: &mut Vec<Diagnostic>,
) {
    match block {
        Block::Heading { anchor, span, .. } => {
            if let Some(name) = anchor {
                if let Some(first_span) = seen.get(name) {
                    let (first_line, _) = src.line_col(first_span.start);
                    diags.push(Diagnostic::new(Code::DuplicateHeadingAnchor, *span).label(
                        format!("anchor `{}` already used at line {}", name, first_line),
                    ));
                } else {
                    seen.insert(name.clone(), *span);
                }
            }
        }
        Block::Blockquote { children, .. } | Block::BlockShortcode { children, .. } => {
            for c in children {
                collect_anchor(c, src, seen, diags);
            }
        }
        Block::List { items, .. } => {
            for it in items {
                for c in &it.children {
                    collect_anchor(c, src, seen, diags);
                }
            }
        }
        Block::DefinitionList { .. } => {}
        Block::Paragraph { .. }
        | Block::CodeBlock { .. }
        | Block::Table { .. }
        | Block::HorizontalRule { .. } => {}
    }
}