use mermaid_text::detect::{DiagramKind, detect};
use super::*;
pub(super) fn render_mermaid_block(
src: &str,
width: usize,
palette: &Palette,
) -> Option<Vec<Line<'static>>> {
if !matches!(
detect(src),
Ok(DiagramKind::Flowchart | DiagramKind::Sequence)
) {
return None;
}
let rendered = if palette.compat {
mermaid_text::render_ascii_with_width(src, Some(width))
} else {
mermaid_text::render_with_width(src, Some(width))
}
.ok()?;
let mut lines: Vec<Line<'static>> = Vec::new();
for l in rendered.lines() {
let chars: Vec<char> = l.chars().collect();
if wrap::display_width(&chars) > width {
return None; }
lines.push(Line::from(Span::styled(
l.trim_end().to_string(),
Style::new().fg(palette.text),
)));
}
while lines
.last()
.is_some_and(|l| l.spans.iter().all(|s| s.content.trim().is_empty()))
{
lines.pop();
}
if lines.is_empty() {
return None; }
Some(lines)
}
#[cfg(test)]
mod tests {
use super::*;
const SEQ: &str = "sequenceDiagram\n participant Client\n participant Server\n Client->>Server: GET /api/data\n Server-->>Client: 200 OK\n";
const FLOW_RU: &str = "flowchart TD\n A[Пользователь] --> B{Есть токен?}\n B -->|Да| C[Доступ разрешён]\n B -->|Нет| D[Форма входа]\n";
#[test]
fn renders_sequence_diagram() {
let lines = render_mermaid_block(SEQ, 90, &Palette::default()).expect("should render");
let joined: String = lines
.iter()
.flat_map(|l| l.spans.iter().map(|s| s.content.as_ref()))
.collect();
assert!(joined.contains("Client"), "{joined}");
assert!(joined.contains('┌'), "no box-drawing frames: {joined}");
}
#[test]
fn renders_cyrillic_flowchart_without_mangling() {
let lines = render_mermaid_block(FLOW_RU, 90, &Palette::default()).expect("render");
let joined: String = lines
.iter()
.flat_map(|l| l.spans.iter().map(|s| s.content.as_ref()))
.collect();
assert!(joined.contains("Форма входа"), "node lost: {joined}");
assert!(
!joined.contains("[Доступ"),
"a bracket leaked into a label: {joined}"
);
}
#[test]
fn too_narrow_width_falls_back() {
assert!(render_mermaid_block(SEQ, 20, &Palette::default()).is_none());
}
#[test]
fn non_whitelisted_kind_falls_back() {
let pie = "pie title X\n \"A\" : 1\n";
assert!(render_mermaid_block(pie, 90, &Palette::default()).is_none());
let state = "stateDiagram-v2\n [*] --> Idle\n";
assert!(render_mermaid_block(state, 90, &Palette::default()).is_none());
}
#[test]
fn garbage_and_stream_stub_fall_back() {
assert!(render_mermaid_block("просто текст", 90, &Palette::default()).is_none());
assert!(render_mermaid_block("", 90, &Palette::default()).is_none());
let _ = render_mermaid_block(
"sequenceDiagram\n participant Ser",
90,
&Palette::default(),
);
}
#[test]
fn compat_palette_renders_ascii_frames() {
use crate::shared::config::Theme;
let p = Palette::for_theme(Theme::Auto).with_compat(true);
let lines = render_mermaid_block(SEQ, 90, &p).expect("ascii render");
let joined: String = lines
.iter()
.flat_map(|l| l.spans.iter().map(|s| s.content.as_ref()))
.collect();
assert!(
!joined.contains('┌') && !joined.contains('│'),
"there should be no box-drawing in compat mode: {joined}"
);
}
#[test]
fn every_line_fits_width_budget() {
for w in [60usize, 90, 120] {
if let Some(lines) = render_mermaid_block(FLOW_RU, w, &Palette::default()) {
for l in &lines {
let s: String = l.spans.iter().map(|s| s.content.as_ref()).collect();
let chars: Vec<char> = s.chars().collect();
assert!(
wrap::display_width(&chars) <= w,
"line wider than the budget {w}: {s:?}"
);
}
}
}
}
}