mmdflux 2.4.2

Render Mermaid diagrams as Unicode text, ASCII, SVG, and MMDS JSON.
Documentation
//! MMDS replay rendering: hydrate to graph-family IR and re-render.
//!
//! The mmds module owns the interchange format (parse, hydrate, serialize).
//! This module owns the render dispatch for MMDS input.

use std::fmt::Display;

use crate::errors::RenderError;
use crate::format::OutputFormat;
use crate::graph::GeometryLevel;
use crate::mmds::{
    Document, from_document, generate_mermaid, hydrate_graph_geometry_from_document_with_diagram,
    hydrate_routed_geometry_from_document, parse_input, resolve_logical_diagram_id,
};
use crate::render::graph::{
    SvgRenderOptions, TextRenderOptions, edge_routing_from_style,
    render_svg_from_geometry_with_theme_and_routing, render_svg_from_routed_geometry_with_theme,
    render_text_from_geometry,
};
use crate::render::svg::theme::ResolvedSvgTheme;
use crate::views::VIEW_EXTENSION_NAMESPACE;

/// Render MMDS input through the MMDS replay path.
pub(crate) fn render_input(
    input: &str,
    format: OutputFormat,
    geometry_level: GeometryLevel,
    text_options: &TextRenderOptions,
    svg_options: &SvgRenderOptions,
    svg_theme: Option<&ResolvedSvgTheme>,
) -> Result<String, RenderError> {
    let payload =
        parse_input(input).map_err(|error| prefixed_display_error("parse error", error))?;
    render_document(
        &payload,
        format,
        geometry_level,
        text_options,
        svg_options,
        svg_theme,
    )
}

/// Render a parsed MMDS document through the MMDS replay path.
pub(crate) fn render_document(
    payload: &Document,
    format: OutputFormat,
    geometry_level: GeometryLevel,
    text_options: &TextRenderOptions,
    svg_options: &SvgRenderOptions,
    svg_theme: Option<&ResolvedSvgTheme>,
) -> Result<String, RenderError> {
    let diagram_id = resolve_logical_diagram_id(payload)?;
    let has_routed_geometry = payload.geometry_level == GeometryLevel::Routed;

    if matches!(format, OutputFormat::Mmds) {
        let output = if has_routed_geometry && geometry_level == GeometryLevel::Layout {
            strip_routed_fields(payload)
        } else {
            payload.clone()
        };
        return serde_json::to_string_pretty(&output)
            .map_err(|error| prefixed_display_error("MMDS serialization error", error));
    }

    if matches!(format, OutputFormat::Mermaid) {
        return generate_mermaid(payload).map_err(display_error);
    }

    let mut diagram = from_document(payload).map_err(display_error)?;

    // MMDS replay path runs the wrap pass so the hydrated graph's edge labels
    // carry the same `wrapped_label_lines` artifact the original render would
    // have. `wrapped_label_lines` is
    // `#[serde(skip)]` on the Edge, so round-tripping through MMDS drops
    // it; rehydrating it here keeps the SVG/text replay in lockstep with
    // the direct runtime render. Uses the default Proportional metrics
    // because the replay path does not receive a `RenderConfig`; the user
    // may opt out by re-rendering through the direct path with
    // `layout.edge_label_max_width: None`.
    let wrap_metrics = crate::graph::measure::default_proportional_text_metrics();
    let wrap_max_width = crate::engines::graph::LayoutConfig::default().edge_label_max_width;
    crate::graph::label_wrap::prepare_wrapped_labels(
        &mut diagram.edges,
        &wrap_metrics,
        wrap_max_width,
    );

    let geometry = hydrate_graph_geometry_from_document_with_diagram(payload, &diagram)
        .map_err(display_error)?;
    let routed = has_routed_geometry
        .then(|| hydrate_routed_geometry_from_document(payload))
        .transpose()
        .map_err(display_error)?;

    match format {
        OutputFormat::Text | OutputFormat::Ascii => {
            let mut options = text_options.clone();
            options.output_format = format;
            options.use_pinned_ranks =
                options.use_pinned_ranks || is_shared_coordinates_view(payload);
            Ok(render_text_from_geometry(
                &diagram,
                &geometry,
                routed.as_ref(),
                &options,
            ))
        }
        OutputFormat::Svg => Ok(match routed.as_ref() {
            Some(routed) => {
                render_svg_from_routed_geometry_with_theme(&diagram, routed, svg_options, svg_theme)
            }
            None => render_svg_from_geometry_with_theme_and_routing(
                &diagram,
                &geometry,
                svg_options,
                edge_routing_from_style(svg_options.routing_style),
                svg_theme,
            ),
        }),
        _ => Err(RenderError {
            message: format!("{format} output is not supported for {diagram_id} diagrams"),
        }),
    }
}

fn is_shared_coordinates_view(payload: &Document) -> bool {
    payload
        .extensions
        .get(VIEW_EXTENSION_NAMESPACE)
        .and_then(|extension| extension.get("layout_mode"))
        .and_then(serde_json::Value::as_str)
        == Some("shared_coordinates")
}

fn display_error(error: impl Display) -> RenderError {
    RenderError {
        message: error.to_string(),
    }
}

fn prefixed_display_error(prefix: &str, error: impl Display) -> RenderError {
    RenderError {
        message: format!("{prefix}: {error}"),
    }
}

fn strip_routed_fields(payload: &Document) -> Document {
    let mut output = payload.clone();
    output.geometry_level = crate::graph::GeometryLevel::Layout;
    for edge in &mut output.edges {
        edge.path = None;
        edge.label_position = None;
        edge.is_backward = None;
        edge.source_port = None;
        edge.target_port = None;
        edge.label_rect = None;
    }
    for subgraph in &mut output.subgraphs {
        subgraph.bounds = None;
    }
    output
}