Files
codex/codex-rs/mermaid/src/output.rs
T
Eric Traut 7c709f0ffd Add a bounded Mermaid text renderer (#45817)
## What changed

Add `codex-mermaid`, a standalone crate that renders supported subsets of flowchart, sequence, state, class, and ER diagrams as Unicode text. Expose `render` for plain text and `render_spans` for semantic node, edge, and text spans that callers can style.

Enforce source, diagram, canvas, and display-width limits. Return errors for unsupported input or exceeded limits without producing partial diagrams, leaving source fallback to callers. Include documentation and a stdin rendering example.

## Testing

Add snapshots for each diagram family, checks for relationship endpoints, Unicode labels, semantic spans, truncated input, and size limits, plus edge reconstruction for all 512 directed three-node graphs in all four layout directions.

GitOrigin-RevId: 5b4e65d9152abce5d833e4e6b12113a5a21f700e
2026-09-16 00:38:34 +00:00

70 lines
1.7 KiB
Rust

//! Semantic spans for theme-independent diagram output; drawing cells never contain ANSI escapes.
/// The diagram element a caller can style with its own theme.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
Node,
Edge,
Text,
}
/// Adjacent characters with one semantic role, independent of terminal styling.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
pub text: String,
pub role: Role,
}
#[derive(Clone, Copy)]
pub(super) struct Cell {
pub symbol: char,
pub role: Role,
}
impl Cell {
pub(super) fn edge(symbol: char) -> Self {
Self {
symbol,
role: Role::Edge,
}
}
pub(super) fn node(symbol: char) -> Self {
Self {
symbol,
role: Role::Node,
}
}
}
pub(super) fn finish(rows: Vec<Vec<Cell>>) -> Vec<Vec<Span>> {
rows.into_iter()
.map(|row| {
let mut spans: Vec<Span> = Vec::new();
for Cell { symbol, role } in row {
if symbol == '\0' {
continue;
}
if let Some(last) = spans.last_mut()
&& last.role == role
{
last.text.push(symbol);
} else {
spans.push(Span {
text: symbol.to_string(),
role,
});
}
}
while let Some(last) = spans.last_mut() {
last.text.truncate(last.text.trim_end().len());
if !last.text.is_empty() {
break;
}
spans.pop();
}
spans
})
.collect()
}