weavatrix-rust 2.16.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::lex::Scanner;
use super::model::{Completeness, Diagram, Group, MAX_EDGES, MAX_NODES, Region};
use super::span::{file_span, span_for};
use crate::model::Diagnostic;

pub(super) fn parse(path: &str, raw: &str, region: &Region) -> Diagram {
    let text = &raw[region.start..region.end];
    let mut scanner = Scanner::new(text, region.start);
    let mut diagram = Diagram {
        key: format!("{path}#{}", region.index),
        direction: String::new(),
        kind: "flowchart".into(),
        completeness: Completeness::Full,
        span: span_for(path, raw, region.start, region.end),
        elements: Vec::new(),
        groups: Vec::new(),
        relations: Vec::new(),
        diagnostics: Vec::new(),
    };
    scanner.skip_trivia();
    if !parse_header(&mut scanner, &mut diagram) {
        note(
            &mut diagram,
            path,
            raw,
            scanner.absolute(),
            "mermaid.unsupported",
            "diagram is missing a flowchart or graph header",
        );
    }
    let mut groups = Vec::<String>::new();
    while !scanner.eof() {
        scanner.skip_trivia();
        if scanner.eof() {
            break;
        }
        if scanner.peek_char() == Some('\n') {
            scanner.skip_newline();
            continue;
        }
        if skip_directive(&mut scanner) {
            continue;
        }
        if scanner.keyword("end") {
            groups.pop();
            continue;
        }
        if scanner.keyword("subgraph") {
            if let Some(group) = parse_subgraph(path, raw, &mut scanner) {
                groups.push(group.id.clone());
                diagram.groups.push(group);
            } else {
                mark_partial(
                    &mut diagram,
                    path,
                    raw,
                    scanner.absolute(),
                    "invalid subgraph",
                );
            }
            continue;
        }
        match super::edges::parse_statement(path, raw, &mut scanner, &mut diagram, groups.last()) {
            super::edges::StatementResult::Parsed => {}
            super::edges::StatementResult::Limit => {
                note(
                    &mut diagram,
                    path,
                    raw,
                    scanner.absolute(),
                    "mermaid.limit",
                    "diagram analysis stopped at a declared limit",
                );
                diagram.completeness = Completeness::Partial;
                break;
            }
            super::edges::StatementResult::Invalid => {
                mark_partial(
                    &mut diagram,
                    path,
                    raw,
                    scanner.absolute(),
                    "unsupported flowchart statement",
                );
                scanner.skip_line();
            }
        }
        if diagram.elements.len() > MAX_NODES || diagram.relations.len() > MAX_EDGES {
            note(
                &mut diagram,
                path,
                raw,
                scanner.absolute(),
                "mermaid.limit",
                "diagram analysis stopped at a declared limit",
            );
            diagram.completeness = Completeness::Partial;
            break;
        }
    }
    diagram
}

fn parse_header(scanner: &mut Scanner<'_>, diagram: &mut Diagram) -> bool {
    if scanner.keyword("flowchart") {
        diagram.kind = "flowchart".into();
    } else if scanner.keyword("graph") {
        diagram.kind = "graph".into();
    } else {
        return false;
    }
    scanner.skip_ws();
    if let Some(direction) = scanner.take_ident()
        && matches!(direction.as_str(), "TB" | "TD" | "BT" | "RL" | "LR")
    {
        diagram.direction = direction;
    }
    true
}

fn skip_directive(scanner: &mut Scanner<'_>) -> bool {
    if scanner.keyword("classDef")
        || scanner.keyword("class")
        || scanner.keyword("style")
        || scanner.keyword("linkStyle")
        || scanner.keyword("click")
        || scanner.keyword("direction")
    {
        scanner.skip_line();
        true
    } else {
        false
    }
}

fn parse_subgraph(path: &str, raw: &str, scanner: &mut Scanner<'_>) -> Option<Group> {
    let start = scanner.absolute();
    let id = scanner.take_ident()?;
    scanner.skip_ws();
    let title = if scanner.eat("[") {
        let title = super::edges::take_until(scanner, ']')?;
        scanner.eat("]");
        title
    } else {
        id.clone()
    };
    Some(Group {
        id,
        title,
        span: span_for(path, raw, start, scanner.absolute()),
    })
}

fn mark_partial(diagram: &mut Diagram, path: &str, raw: &str, start: usize, message: &str) {
    diagram.completeness = Completeness::Partial;
    note(diagram, path, raw, start, "mermaid.unsupported", message);
}

fn note(diagram: &mut Diagram, path: &str, raw: &str, start: usize, code: &str, message: &str) {
    diagram.diagnostics.push(Diagnostic {
        code: code.into(),
        message: message.into(),
        span: Some(if start < raw.len() {
            span_for(path, raw, start, start.saturating_add(1))
        } else {
            file_span(path)
        }),
    });
}