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