Skip to main content

merman_render/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Headless layout + rendering for Mermaid diagrams.
4//!
5//! This crate consumes `merman-core`'s semantic models and produces:
6//! - a layout JSON (geometry + routes)
7//! - Mermaid-like SVG output with DOM parity checks against upstream baselines
8
9pub mod architecture;
10pub mod block;
11pub mod c4;
12pub mod class;
13mod entities;
14pub mod er;
15pub mod error;
16pub mod flowchart;
17pub mod gantt;
18mod generated;
19pub mod gitgraph;
20pub mod info;
21pub mod journey;
22mod json;
23pub mod kanban;
24pub mod math;
25pub mod mindmap;
26pub mod model;
27pub mod packet;
28pub mod pie;
29pub mod quadrantchart;
30pub mod radar;
31pub mod requirement;
32pub mod sankey;
33pub mod sequence;
34pub mod state;
35pub mod svg;
36pub mod text;
37pub mod timeline;
38pub mod treemap;
39mod trig_tables;
40pub mod xychart;
41
42use crate::math::MathRenderer;
43use crate::model::{LayoutDiagram, LayoutMeta, LayoutedDiagram};
44use crate::text::{DeterministicTextMeasurer, TextMeasurer};
45use merman_core::{ParsedDiagram, ParsedDiagramRender, RenderSemanticModel};
46use serde_json::Value;
47use std::sync::Arc;
48
49#[derive(Debug, thiserror::Error)]
50pub enum Error {
51    #[error("unsupported diagram type for layout: {diagram_type}")]
52    UnsupportedDiagram { diagram_type: String },
53    #[error("invalid semantic model: {message}")]
54    InvalidModel { message: String },
55    #[error("semantic model JSON error: {0}")]
56    Json(#[from] serde_json::Error),
57}
58
59pub type Result<T> = std::result::Result<T, Error>;
60
61#[derive(Clone)]
62pub struct LayoutOptions {
63    pub text_measurer: Arc<dyn TextMeasurer + Send + Sync>,
64    /// Optional math renderer for `$$...$$` style labels.
65    pub math_renderer: Option<Arc<dyn MathRenderer + Send + Sync>>,
66    pub viewport_width: f64,
67    pub viewport_height: f64,
68    /// Enable experimental layout engines (e.g. Cytoscape COSE/FCoSE ports) for diagrams that
69    /// currently use placeholder layouts in merman.
70    pub use_manatee_layout: bool,
71}
72
73impl Default for LayoutOptions {
74    fn default() -> Self {
75        Self {
76            text_measurer: Arc::new(DeterministicTextMeasurer::default()),
77            math_renderer: None,
78            viewport_width: 800.0,
79            viewport_height: 600.0,
80            use_manatee_layout: false,
81        }
82    }
83}
84
85impl LayoutOptions {
86    /// Returns layout defaults suitable for headless SVG rendering in UI integrations.
87    ///
88    /// Compared to `Default`, this uses a Mermaid-like text measurer backed by vendored font
89    /// metrics (instead of deterministic placeholder metrics).
90    pub fn headless_svg_defaults() -> Self {
91        Self {
92            text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
93            // Mermaid parity fixtures for diagrams like mindmap/architecture rely on the COSE
94            // layout port (manatee). Make the headless defaults "just work" for UI integrations.
95            use_manatee_layout: true,
96            ..Default::default()
97        }
98    }
99
100    pub fn with_text_measurer(mut self, measurer: Arc<dyn TextMeasurer + Send + Sync>) -> Self {
101        self.text_measurer = measurer;
102        self
103    }
104
105    pub fn with_math_renderer(mut self, renderer: Arc<dyn MathRenderer + Send + Sync>) -> Self {
106        self.math_renderer = Some(renderer);
107        self
108    }
109}
110
111pub fn layout_parsed(parsed: &ParsedDiagram, options: &LayoutOptions) -> Result<LayoutedDiagram> {
112    let meta = LayoutMeta::from_parse_metadata(&parsed.meta);
113    let layout = layout_parsed_layout_only(parsed, options)?;
114
115    Ok(LayoutedDiagram {
116        meta,
117        semantic: Value::clone(&parsed.model),
118        layout,
119    })
120}
121
122pub fn layout_parsed_layout_only(
123    parsed: &ParsedDiagram,
124    options: &LayoutOptions,
125) -> Result<LayoutDiagram> {
126    let diagram_type = parsed.meta.diagram_type.as_str();
127    let effective_config = parsed.meta.effective_config.as_value();
128    let title = parsed.meta.title.as_deref();
129
130    let layout = match diagram_type {
131        "error" => LayoutDiagram::ErrorDiagram(error::layout_error_diagram(
132            &parsed.model,
133            effective_config,
134            options.text_measurer.as_ref(),
135        )?),
136        "block" => LayoutDiagram::BlockDiagram(block::layout_block_diagram(
137            &parsed.model,
138            effective_config,
139            options.text_measurer.as_ref(),
140        )?),
141        "architecture" => {
142            LayoutDiagram::ArchitectureDiagram(architecture::layout_architecture_diagram(
143                &parsed.model,
144                effective_config,
145                options.text_measurer.as_ref(),
146                options.use_manatee_layout,
147            )?)
148        }
149        "requirement" => {
150            LayoutDiagram::RequirementDiagram(requirement::layout_requirement_diagram(
151                &parsed.model,
152                effective_config,
153                options.text_measurer.as_ref(),
154            )?)
155        }
156        "radar" => LayoutDiagram::RadarDiagram(radar::layout_radar_diagram(
157            &parsed.model,
158            effective_config,
159            options.text_measurer.as_ref(),
160        )?),
161        "treemap" => LayoutDiagram::TreemapDiagram(treemap::layout_treemap_diagram(
162            &parsed.model,
163            effective_config,
164            options.text_measurer.as_ref(),
165        )?),
166        "flowchart-v2" => LayoutDiagram::FlowchartV2(flowchart::layout_flowchart_v2(
167            &parsed.model,
168            &parsed.meta.effective_config,
169            options.text_measurer.as_ref(),
170            options.math_renderer.as_deref(),
171        )?),
172        "stateDiagram" => LayoutDiagram::StateDiagramV2(state::layout_state_diagram_v2(
173            &parsed.model,
174            effective_config,
175            options.text_measurer.as_ref(),
176        )?),
177        "classDiagram" | "class" => LayoutDiagram::ClassDiagramV2(class::layout_class_diagram_v2(
178            &parsed.model,
179            effective_config,
180            options.text_measurer.as_ref(),
181        )?),
182        "er" | "erDiagram" => LayoutDiagram::ErDiagram(er::layout_er_diagram(
183            &parsed.model,
184            effective_config,
185            options.text_measurer.as_ref(),
186        )?),
187        "sequence" | "zenuml" => LayoutDiagram::SequenceDiagram(sequence::layout_sequence_diagram(
188            &parsed.model,
189            effective_config,
190            options.text_measurer.as_ref(),
191        )?),
192        "info" => LayoutDiagram::InfoDiagram(info::layout_info_diagram(
193            &parsed.model,
194            effective_config,
195            options.text_measurer.as_ref(),
196        )?),
197        "packet" => LayoutDiagram::PacketDiagram(packet::layout_packet_diagram(
198            &parsed.model,
199            title,
200            effective_config,
201            options.text_measurer.as_ref(),
202        )?),
203        "timeline" => LayoutDiagram::TimelineDiagram(timeline::layout_timeline_diagram(
204            &parsed.model,
205            effective_config,
206            options.text_measurer.as_ref(),
207        )?),
208        "gantt" => LayoutDiagram::GanttDiagram(gantt::layout_gantt_diagram(
209            &parsed.model,
210            effective_config,
211            options.text_measurer.as_ref(),
212        )?),
213        "c4" => LayoutDiagram::C4Diagram(c4::layout_c4_diagram(
214            &parsed.model,
215            effective_config,
216            options.text_measurer.as_ref(),
217            options.viewport_width,
218            options.viewport_height,
219        )?),
220        "journey" => LayoutDiagram::JourneyDiagram(journey::layout_journey_diagram(
221            &parsed.model,
222            effective_config,
223            options.text_measurer.as_ref(),
224        )?),
225        "gitGraph" => LayoutDiagram::GitGraphDiagram(gitgraph::layout_gitgraph_diagram(
226            &parsed.model,
227            effective_config,
228            options.text_measurer.as_ref(),
229        )?),
230        "kanban" => LayoutDiagram::KanbanDiagram(kanban::layout_kanban_diagram(
231            &parsed.model,
232            effective_config,
233            options.text_measurer.as_ref(),
234        )?),
235        "pie" => LayoutDiagram::PieDiagram(pie::layout_pie_diagram(
236            &parsed.model,
237            effective_config,
238            options.text_measurer.as_ref(),
239        )?),
240        "xychart" => LayoutDiagram::XyChartDiagram(xychart::layout_xychart_diagram(
241            &parsed.model,
242            effective_config,
243            options.text_measurer.as_ref(),
244        )?),
245        "quadrantChart" => {
246            LayoutDiagram::QuadrantChartDiagram(quadrantchart::layout_quadrantchart_diagram(
247                &parsed.model,
248                effective_config,
249                options.text_measurer.as_ref(),
250            )?)
251        }
252        "mindmap" => LayoutDiagram::MindmapDiagram(mindmap::layout_mindmap_diagram(
253            &parsed.model,
254            effective_config,
255            options.text_measurer.as_ref(),
256            options.use_manatee_layout,
257        )?),
258        "sankey" => LayoutDiagram::SankeyDiagram(sankey::layout_sankey_diagram(
259            &parsed.model,
260            effective_config,
261            options.text_measurer.as_ref(),
262        )?),
263        other => {
264            return Err(Error::UnsupportedDiagram {
265                diagram_type: other.to_string(),
266            });
267        }
268    };
269    Ok(layout)
270}
271
272pub fn layout_parsed_render_layout_only(
273    parsed: &ParsedDiagramRender,
274    options: &LayoutOptions,
275) -> Result<LayoutDiagram> {
276    let diagram_type = parsed.meta.diagram_type.as_str();
277    let effective_config = parsed.meta.effective_config.as_value();
278    let title = parsed.meta.title.as_deref();
279
280    match (&parsed.model, diagram_type) {
281        (RenderSemanticModel::Mindmap(model), "mindmap") => Ok(LayoutDiagram::MindmapDiagram(
282            mindmap::layout_mindmap_diagram_typed(
283                model,
284                effective_config,
285                options.text_measurer.as_ref(),
286                options.use_manatee_layout,
287            )?,
288        )),
289        (RenderSemanticModel::Architecture(model), "architecture") => Ok(
290            LayoutDiagram::ArchitectureDiagram(architecture::layout_architecture_diagram_typed(
291                model,
292                effective_config,
293                options.text_measurer.as_ref(),
294                options.use_manatee_layout,
295            )?),
296        ),
297        (RenderSemanticModel::Flowchart(model), "flowchart-v2" | "flowchart" | "flowchart-elk") => {
298            Ok(LayoutDiagram::FlowchartV2(
299                flowchart::layout_flowchart_v2_typed(
300                    model,
301                    &parsed.meta.effective_config,
302                    options.text_measurer.as_ref(),
303                    options.math_renderer.as_deref(),
304                )?,
305            ))
306        }
307        (RenderSemanticModel::State(model), "stateDiagram" | "state") => Ok(
308            LayoutDiagram::StateDiagramV2(state::layout_state_diagram_v2_typed(
309                model,
310                effective_config,
311                options.text_measurer.as_ref(),
312            )?),
313        ),
314        (RenderSemanticModel::Class(model), "classDiagram" | "class") => Ok(
315            LayoutDiagram::ClassDiagramV2(class::layout_class_diagram_v2_typed(
316                model,
317                effective_config,
318                options.text_measurer.as_ref(),
319            )?),
320        ),
321        (RenderSemanticModel::Json(semantic), _) => {
322            let layout = match diagram_type {
323                "error" => LayoutDiagram::ErrorDiagram(error::layout_error_diagram(
324                    semantic,
325                    effective_config,
326                    options.text_measurer.as_ref(),
327                )?),
328                "block" => LayoutDiagram::BlockDiagram(block::layout_block_diagram(
329                    semantic,
330                    effective_config,
331                    options.text_measurer.as_ref(),
332                )?),
333                "architecture" => {
334                    LayoutDiagram::ArchitectureDiagram(architecture::layout_architecture_diagram(
335                        semantic,
336                        effective_config,
337                        options.text_measurer.as_ref(),
338                        options.use_manatee_layout,
339                    )?)
340                }
341                "requirement" => {
342                    LayoutDiagram::RequirementDiagram(requirement::layout_requirement_diagram(
343                        semantic,
344                        effective_config,
345                        options.text_measurer.as_ref(),
346                    )?)
347                }
348                "radar" => LayoutDiagram::RadarDiagram(radar::layout_radar_diagram(
349                    semantic,
350                    effective_config,
351                    options.text_measurer.as_ref(),
352                )?),
353                "treemap" => LayoutDiagram::TreemapDiagram(treemap::layout_treemap_diagram(
354                    semantic,
355                    effective_config,
356                    options.text_measurer.as_ref(),
357                )?),
358                "flowchart-v2" => LayoutDiagram::FlowchartV2(flowchart::layout_flowchart_v2(
359                    semantic,
360                    &parsed.meta.effective_config,
361                    options.text_measurer.as_ref(),
362                    options.math_renderer.as_deref(),
363                )?),
364                "stateDiagram" => LayoutDiagram::StateDiagramV2(state::layout_state_diagram_v2(
365                    semantic,
366                    effective_config,
367                    options.text_measurer.as_ref(),
368                )?),
369                "classDiagram" | "class" => {
370                    LayoutDiagram::ClassDiagramV2(class::layout_class_diagram_v2(
371                        semantic,
372                        effective_config,
373                        options.text_measurer.as_ref(),
374                    )?)
375                }
376                "er" | "erDiagram" => LayoutDiagram::ErDiagram(er::layout_er_diagram(
377                    semantic,
378                    effective_config,
379                    options.text_measurer.as_ref(),
380                )?),
381                "sequence" | "zenuml" => {
382                    LayoutDiagram::SequenceDiagram(sequence::layout_sequence_diagram(
383                        semantic,
384                        effective_config,
385                        options.text_measurer.as_ref(),
386                    )?)
387                }
388                "info" => LayoutDiagram::InfoDiagram(info::layout_info_diagram(
389                    semantic,
390                    effective_config,
391                    options.text_measurer.as_ref(),
392                )?),
393                "packet" => LayoutDiagram::PacketDiagram(packet::layout_packet_diagram(
394                    semantic,
395                    title,
396                    effective_config,
397                    options.text_measurer.as_ref(),
398                )?),
399                "timeline" => LayoutDiagram::TimelineDiagram(timeline::layout_timeline_diagram(
400                    semantic,
401                    effective_config,
402                    options.text_measurer.as_ref(),
403                )?),
404                "gantt" => LayoutDiagram::GanttDiagram(gantt::layout_gantt_diagram(
405                    semantic,
406                    effective_config,
407                    options.text_measurer.as_ref(),
408                )?),
409                "c4" => LayoutDiagram::C4Diagram(c4::layout_c4_diagram(
410                    semantic,
411                    effective_config,
412                    options.text_measurer.as_ref(),
413                    options.viewport_width,
414                    options.viewport_height,
415                )?),
416                "journey" => LayoutDiagram::JourneyDiagram(journey::layout_journey_diagram(
417                    semantic,
418                    effective_config,
419                    options.text_measurer.as_ref(),
420                )?),
421                "gitGraph" => LayoutDiagram::GitGraphDiagram(gitgraph::layout_gitgraph_diagram(
422                    semantic,
423                    effective_config,
424                    options.text_measurer.as_ref(),
425                )?),
426                "kanban" => LayoutDiagram::KanbanDiagram(kanban::layout_kanban_diagram(
427                    semantic,
428                    effective_config,
429                    options.text_measurer.as_ref(),
430                )?),
431                "pie" => LayoutDiagram::PieDiagram(pie::layout_pie_diagram(
432                    semantic,
433                    effective_config,
434                    options.text_measurer.as_ref(),
435                )?),
436                "xychart" => LayoutDiagram::XyChartDiagram(xychart::layout_xychart_diagram(
437                    semantic,
438                    effective_config,
439                    options.text_measurer.as_ref(),
440                )?),
441                "quadrantChart" => LayoutDiagram::QuadrantChartDiagram(
442                    quadrantchart::layout_quadrantchart_diagram(
443                        semantic,
444                        effective_config,
445                        options.text_measurer.as_ref(),
446                    )?,
447                ),
448                "mindmap" => LayoutDiagram::MindmapDiagram(mindmap::layout_mindmap_diagram(
449                    semantic,
450                    effective_config,
451                    options.text_measurer.as_ref(),
452                    options.use_manatee_layout,
453                )?),
454                "sankey" => LayoutDiagram::SankeyDiagram(sankey::layout_sankey_diagram(
455                    semantic,
456                    effective_config,
457                    options.text_measurer.as_ref(),
458                )?),
459                other => {
460                    return Err(Error::UnsupportedDiagram {
461                        diagram_type: other.to_string(),
462                    });
463                }
464            };
465            Ok(layout)
466        }
467        _ => Err(Error::InvalidModel {
468            message: format!("unexpected render model variant for diagram type: {diagram_type}"),
469        }),
470    }
471}