merman-render 0.8.0

Headless layout + SVG renderer for Mermaid (parity-focused; upstream SVG goldens).
Documentation
use super::*;

pub(super) fn state_is_hidden(ctx: &StateRenderCtx<'_>, id: &str) -> bool {
    ctx.hidden_prefixes
        .iter()
        .any(|p| id == p || id.starts_with(&format!("{p}----")))
}

pub(super) fn state_data_look<'a>(ctx: &'a StateRenderCtx<'_>) -> &'a str {
    let look = ctx.diagram_look.trim();
    if look.is_empty() { "classic" } else { look }
}

pub(super) fn state_note_owner_id(id: &str) -> &str {
    id.rsplit_once("----parent")
        .map(|(owner, _)| owner)
        .filter(|owner| !owner.is_empty())
        .unwrap_or(id)
}

#[derive(Debug, Clone, Copy)]
pub(super) struct StateScopedDomId<'a, 'b> {
    diagram_id: SvgDiagramId<'a>,
    id: &'b str,
}

impl std::fmt::Display for StateScopedDomId<'_, '_> {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "{}-{}",
            self.diagram_id,
            escape_xml_display(self.id)
        )
    }
}

#[derive(Debug, Clone, Copy)]
pub(super) struct StateScopedDomIdAttr<'a, 'b>(StateScopedDomId<'a, 'b>);

impl std::fmt::Display for StateScopedDomIdAttr<'_, '_> {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "{}-{}",
            self.0.diagram_id,
            escape_attr_display(self.0.id)
        )
    }
}

impl<'a, 'b> StateScopedDomId<'a, 'b> {
    pub(super) fn attr(self) -> StateScopedDomIdAttr<'a, 'b> {
        StateScopedDomIdAttr(self)
    }
}

pub(super) fn state_scoped_dom_id<'a, 'b>(
    ctx: &StateRenderCtx<'a>,
    id: &'b str,
) -> StateScopedDomId<'a, 'b> {
    StateScopedDomId {
        diagram_id: ctx.diagram_id,
        id,
    }
}

pub(super) fn state_node_scoped_dom_id<'a, 'b>(
    ctx: &'b StateRenderCtx<'a>,
    node_id: &'b str,
) -> StateScopedDomId<'a, 'b> {
    ctx.nodes_by_id
        .get(node_id)
        .map(|node| node.dom_id.trim())
        .filter(|dom_id| !dom_id.is_empty())
        .map(|dom_id| state_scoped_dom_id(ctx, dom_id))
        .unwrap_or_else(|| state_scoped_dom_id(ctx, node_id))
}

fn state_leaf_context_impl<'a>(
    ctx: &'a StateRenderCtx<'_>,
    id: &str,
    respect_nested_roots: bool,
) -> Option<&'a str> {
    let mut p = ctx.parent.get(id).copied();
    while p.is_some() {
        let pid = state_strip_note_group(ctx, p)?;
        let Some(pn) = ctx.nodes_by_id.get(pid).copied() else {
            return Some(pid);
        };
        if pn.is_group && pn.shape != "noteGroup" {
            if !respect_nested_roots || ctx.nested_roots.contains(pid) {
                return Some(pid);
            }
            p = ctx.parent.get(pid).copied();
            continue;
        }
        p = ctx.parent.get(pid).copied();
    }
    None
}

fn state_strip_note_group<'a>(
    ctx: &'a StateRenderCtx<'_>,
    mut parent: Option<&'a str>,
) -> Option<&'a str> {
    while let Some(pid) = parent {
        let Some(pn) = ctx.nodes_by_id.get(pid).copied() else {
            return Some(pid);
        };
        if pn.shape == "noteGroup" {
            parent = ctx.parent.get(pid).copied();
            continue;
        }
        return Some(pid);
    }
    None
}

fn state_insertion_context_impl<'a>(
    ctx: &'a StateRenderCtx<'_>,
    cluster_id: &str,
    respect_nested_roots: bool,
) -> Option<&'a str> {
    state_leaf_context_impl(ctx, cluster_id, respect_nested_roots)
}

fn state_endpoint_context_impl<'a>(
    ctx: &'a StateRenderCtx<'_>,
    id: &str,
    respect_nested_roots: bool,
) -> Option<&'a str> {
    if let Some(n) = ctx.nodes_by_id.get(id).copied()
        && n.is_group
        && n.shape != "noteGroup"
    {
        return state_insertion_context_impl(ctx, id, respect_nested_roots);
    }
    state_leaf_context_impl(ctx, id, respect_nested_roots)
}

fn state_context_chain_impl<'a>(
    ctx: &'a StateRenderCtx<'_>,
    mut c: Option<&'a str>,
    respect_nested_roots: bool,
) -> Vec<Option<&'a str>> {
    let mut out = Vec::new();
    loop {
        out.push(c);
        let Some(id) = c else {
            break;
        };
        c = state_insertion_context_impl(ctx, id, respect_nested_roots);
    }
    out
}

fn state_edge_context_impl<'a>(
    ctx: &'a StateRenderCtx<'_>,
    edge: &StateSvgEdge,
    respect_nested_roots: bool,
) -> Option<&'a str> {
    let a = state_endpoint_context_impl(ctx, edge.start.as_str(), respect_nested_roots);
    let b = state_endpoint_context_impl(ctx, edge.end.as_str(), respect_nested_roots);
    let ca = state_context_chain_impl(ctx, a, respect_nested_roots);
    let cb = state_context_chain_impl(ctx, b, respect_nested_roots);
    for anc in cb {
        if ca.contains(&anc) {
            return anc;
        }
    }
    None
}

pub(super) fn state_endpoint_context_raw<'a>(
    ctx: &'a StateRenderCtx<'_>,
    id: &str,
) -> Option<&'a str> {
    state_endpoint_context_impl(ctx, id, false)
}

pub(super) fn state_context_chain_raw<'a>(
    ctx: &'a StateRenderCtx<'_>,
    c: Option<&'a str>,
) -> Vec<Option<&'a str>> {
    state_context_chain_impl(ctx, c, false)
}

pub(super) fn state_edge_context_raw<'a>(
    ctx: &'a StateRenderCtx<'_>,
    edge: &StateSvgEdge,
) -> Option<&'a str> {
    state_edge_context_impl(ctx, edge, false)
}

pub(super) fn state_leaf_context<'a>(ctx: &'a StateRenderCtx<'_>, id: &str) -> Option<&'a str> {
    state_leaf_context_impl(ctx, id, true)
}

pub(super) fn state_insertion_context<'a>(
    ctx: &'a StateRenderCtx<'_>,
    cluster_id: &str,
) -> Option<&'a str> {
    state_insertion_context_impl(ctx, cluster_id, true)
}

pub(super) fn state_edge_context<'a>(
    ctx: &'a StateRenderCtx<'_>,
    edge: &StateSvgEdge,
) -> Option<&'a str> {
    state_edge_context_impl(ctx, edge, true)
}

pub(super) fn state_is_shadowed_self_loop_edge(
    ctx: &StateRenderCtx<'_>,
    edge_index: usize,
    edge: &StateSvgEdge,
    root: Option<&str>,
) -> bool {
    // Only Dagre replaces parallel self loops with shared dummy-node segments.
    if ctx.uses_elk_adapter_dom || edge.start != edge.end {
        return false;
    }
    if state_edge_context(ctx, edge) != root {
        return false;
    }

    for later in ctx.edges.iter().skip(edge_index + 1) {
        if later.start != later.end {
            continue;
        }
        if later.start != edge.start || later.end != edge.end {
            continue;
        }
        if state_is_hidden(ctx, later.start.as_str())
            || state_is_hidden(ctx, later.end.as_str())
            || state_is_hidden(ctx, later.id.as_str())
        {
            continue;
        }
        if state_edge_context(ctx, later) != root {
            continue;
        }
        return true;
    }

    false
}