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