1#![forbid(unsafe_code)]
2
3pub mod architecture;
10pub(crate) mod architecture_metrics;
11pub mod block;
12pub mod c4;
13mod chart_palette;
14pub mod class;
15mod config;
16mod entities;
17pub mod er;
18pub mod error;
19pub mod eventmodeling;
20pub mod flowchart;
21pub mod gantt;
22mod generated;
23pub mod gitgraph;
24pub mod info;
25pub mod ishikawa;
26pub mod journey;
27mod json;
28pub mod kanban;
29pub mod math;
30mod mermaid_style;
31pub mod mindmap;
32pub mod model;
33pub mod packet;
34pub mod pie;
35pub mod quadrantchart;
36pub mod radar;
37pub mod requirement;
38pub mod sankey;
39pub mod sequence;
40pub mod state;
41pub mod svg;
42pub mod text;
43pub mod timeline;
44pub mod tree_view;
45pub mod treemap;
46mod trig_tables;
47pub mod xychart;
48
49use crate::math::MathRenderer;
50use crate::model::{LayoutDiagram, LayoutMeta, LayoutedDiagram};
51use crate::text::{DeterministicTextMeasurer, TextMeasurer};
52use merman_core::{ParsedDiagram, ParsedDiagramRender, RenderSemanticModel};
53use serde_json::Value;
54use std::sync::Arc;
55
56#[derive(Debug, thiserror::Error)]
57pub enum Error {
58 #[error("unsupported diagram type for layout: {diagram_type}")]
59 UnsupportedDiagram { diagram_type: String },
60 #[error("invalid semantic model: {message}")]
61 InvalidModel { message: String },
62 #[error("SVG postprocessor `{pass}` failed: {message}")]
63 SvgPostprocess { pass: String, message: String },
64 #[error("semantic model JSON error: {0}")]
65 Json(#[from] serde_json::Error),
66}
67
68pub type Result<T> = std::result::Result<T, Error>;
69
70impl Error {
71 pub fn svg_postprocess(pass: impl Into<String>, message: impl Into<String>) -> Self {
72 Self::SvgPostprocess {
73 pass: pass.into(),
74 message: message.into(),
75 }
76 }
77}
78
79#[derive(Clone)]
80pub struct LayoutOptions {
81 pub text_measurer: Arc<dyn TextMeasurer + Send + Sync>,
82 pub math_renderer: Option<Arc<dyn MathRenderer + Send + Sync>>,
84 pub viewport_width: f64,
85 pub viewport_height: f64,
86 pub use_manatee_layout: bool,
89}
90
91impl Default for LayoutOptions {
92 fn default() -> Self {
93 Self {
94 text_measurer: Arc::new(DeterministicTextMeasurer::default()),
95 math_renderer: None,
96 viewport_width: 800.0,
97 viewport_height: 600.0,
98 use_manatee_layout: false,
99 }
100 }
101}
102
103impl LayoutOptions {
104 pub fn headless_svg_defaults() -> Self {
109 Self {
110 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
111 use_manatee_layout: true,
114 ..Default::default()
115 }
116 }
117
118 pub fn with_text_measurer(mut self, measurer: Arc<dyn TextMeasurer + Send + Sync>) -> Self {
119 self.text_measurer = measurer;
120 self
121 }
122
123 pub fn with_math_renderer(mut self, renderer: Arc<dyn MathRenderer + Send + Sync>) -> Self {
124 self.math_renderer = Some(renderer);
125 self
126 }
127}
128
129pub fn layout_parsed(parsed: &ParsedDiagram, options: &LayoutOptions) -> Result<LayoutedDiagram> {
130 let meta = LayoutMeta::from_parse_metadata(&parsed.meta);
131 let layout = layout_parsed_layout_only(parsed, options)?;
132
133 Ok(LayoutedDiagram {
134 meta,
135 semantic: Value::clone(&parsed.model),
136 layout,
137 })
138}
139
140pub fn layout_parsed_layout_only(
141 parsed: &ParsedDiagram,
142 options: &LayoutOptions,
143) -> Result<LayoutDiagram> {
144 let diagram_type = parsed.meta.diagram_type.as_str();
145 let title = parsed.meta.title.as_deref();
146 layout_json_by_type(
147 diagram_type,
148 &parsed.model,
149 &parsed.meta.effective_config,
150 title,
151 options,
152 )
153}
154
155pub fn layout_parsed_render_layout_only(
156 parsed: &ParsedDiagramRender,
157 options: &LayoutOptions,
158) -> Result<LayoutDiagram> {
159 let diagram_type = parsed.meta.diagram_type.as_str();
160 let effective_config = parsed.meta.effective_config.as_value();
161 let title = parsed.meta.title.as_deref();
162
163 if !parsed.model.supports_diagram_type(diagram_type) {
164 return Err(Error::InvalidModel {
165 message: format!(
166 "unexpected render model variant {} for diagram type: {diagram_type}",
167 parsed.model.kind()
168 ),
169 });
170 }
171
172 match &parsed.model {
173 RenderSemanticModel::Mindmap(model) => Ok(LayoutDiagram::MindmapDiagram(Box::new(
174 mindmap::layout_mindmap_diagram_typed(
175 model,
176 effective_config,
177 options.text_measurer.as_ref(),
178 options.use_manatee_layout,
179 )?,
180 ))),
181 RenderSemanticModel::Architecture(model) => Ok(LayoutDiagram::ArchitectureDiagram(
182 Box::new(architecture::layout_architecture_diagram_typed(
183 model,
184 effective_config,
185 options.text_measurer.as_ref(),
186 options.use_manatee_layout,
187 )?),
188 )),
189 RenderSemanticModel::Flowchart(model) => Ok(LayoutDiagram::FlowchartV2(Box::new(
190 flowchart::layout_flowchart_v2_typed(
191 model,
192 &parsed.meta.effective_config,
193 options.text_measurer.as_ref(),
194 options.math_renderer.as_deref(),
195 )?,
196 ))),
197 RenderSemanticModel::State(model) => Ok(LayoutDiagram::StateDiagramV2(Box::new(
198 state::layout_state_diagram_v2_typed(
199 model,
200 effective_config,
201 options.text_measurer.as_ref(),
202 )?,
203 ))),
204 RenderSemanticModel::Sequence(model) => Ok(LayoutDiagram::SequenceDiagram(Box::new(
205 sequence::layout_sequence_diagram_typed_with_title(
206 model,
207 title,
208 effective_config,
209 options.text_measurer.as_ref(),
210 options.math_renderer.as_deref(),
211 )?,
212 ))),
213 RenderSemanticModel::Class(model) => Ok(LayoutDiagram::ClassDiagramV2(Box::new(
214 class::layout_class_diagram_v2_typed_with_config(
215 model,
216 &parsed.meta.effective_config,
217 options.text_measurer.as_ref(),
218 )?,
219 ))),
220 RenderSemanticModel::C4(model) => Ok(LayoutDiagram::C4Diagram(Box::new(
221 c4::layout_c4_diagram_typed(
222 model,
223 effective_config,
224 options.text_measurer.as_ref(),
225 options.viewport_width,
226 options.viewport_height,
227 )?,
228 ))),
229 RenderSemanticModel::Kanban(model) => Ok(LayoutDiagram::KanbanDiagram(Box::new(
230 kanban::layout_kanban_diagram_typed(
231 model,
232 effective_config,
233 options.text_measurer.as_ref(),
234 )?,
235 ))),
236 RenderSemanticModel::Gantt(model) => Ok(LayoutDiagram::GanttDiagram(Box::new(
237 gantt::layout_gantt_diagram_typed(
238 model,
239 effective_config,
240 options.text_measurer.as_ref(),
241 )?,
242 ))),
243 RenderSemanticModel::Pie(model) => Ok(LayoutDiagram::PieDiagram(Box::new(
244 pie::layout_pie_diagram_typed(model, effective_config, options.text_measurer.as_ref())?,
245 ))),
246 RenderSemanticModel::Packet(model) => Ok(LayoutDiagram::PacketDiagram(Box::new(
247 packet::layout_packet_diagram_typed(
248 model,
249 title,
250 effective_config,
251 options.text_measurer.as_ref(),
252 )?,
253 ))),
254 RenderSemanticModel::Timeline(model) => Ok(LayoutDiagram::TimelineDiagram(Box::new(
255 timeline::layout_timeline_diagram_typed(
256 model,
257 effective_config,
258 options.text_measurer.as_ref(),
259 )?,
260 ))),
261 RenderSemanticModel::Journey(model) => Ok(LayoutDiagram::JourneyDiagram(Box::new(
262 journey::layout_journey_diagram_typed(
263 model,
264 effective_config,
265 options.text_measurer.as_ref(),
266 )?,
267 ))),
268 RenderSemanticModel::Requirement(model) => Ok(LayoutDiagram::RequirementDiagram(Box::new(
269 requirement::layout_requirement_diagram_typed(
270 model,
271 effective_config,
272 options.text_measurer.as_ref(),
273 )?,
274 ))),
275 RenderSemanticModel::Sankey(model) => Ok(LayoutDiagram::SankeyDiagram(Box::new(
276 sankey::layout_sankey_diagram_typed(
277 model,
278 effective_config,
279 options.text_measurer.as_ref(),
280 )?,
281 ))),
282 RenderSemanticModel::Radar(model) => Ok(LayoutDiagram::RadarDiagram(Box::new(
283 radar::layout_radar_diagram_typed(
284 model,
285 effective_config,
286 options.text_measurer.as_ref(),
287 )?,
288 ))),
289 RenderSemanticModel::Info(model) => Ok(LayoutDiagram::InfoDiagram(Box::new(
290 info::layout_info_diagram_typed(
291 model,
292 effective_config,
293 options.text_measurer.as_ref(),
294 )?,
295 ))),
296 RenderSemanticModel::Treemap(model) => Ok(LayoutDiagram::TreemapDiagram(Box::new(
297 treemap::layout_treemap_diagram_typed(
298 model,
299 effective_config,
300 options.text_measurer.as_ref(),
301 )?,
302 ))),
303 RenderSemanticModel::Block(model) => Ok(LayoutDiagram::BlockDiagram(Box::new(
304 block::layout_block_diagram_typed(
305 model,
306 effective_config,
307 options.text_measurer.as_ref(),
308 )?,
309 ))),
310 RenderSemanticModel::Er(model) => Ok(LayoutDiagram::ErDiagram(Box::new(
311 er::layout_er_diagram_typed(model, effective_config, options.text_measurer.as_ref())?,
312 ))),
313 RenderSemanticModel::QuadrantChart(model) => Ok(LayoutDiagram::QuadrantChartDiagram(
314 Box::new(quadrantchart::layout_quadrantchart_diagram_typed(
315 model,
316 effective_config,
317 options.text_measurer.as_ref(),
318 )?),
319 )),
320 RenderSemanticModel::XyChart(model) => Ok(LayoutDiagram::XyChartDiagram(Box::new(
321 xychart::layout_xychart_diagram_typed(
322 model,
323 effective_config,
324 options.text_measurer.as_ref(),
325 )?,
326 ))),
327 RenderSemanticModel::GitGraph(model) => Ok(LayoutDiagram::GitGraphDiagram(Box::new(
328 gitgraph::layout_gitgraph_diagram_typed(
329 model,
330 effective_config,
331 options.text_measurer.as_ref(),
332 )?,
333 ))),
334 RenderSemanticModel::TreeView(model) => Ok(LayoutDiagram::TreeViewDiagram(Box::new(
335 tree_view::layout_tree_view_diagram_typed(
336 model,
337 effective_config,
338 options.text_measurer.as_ref(),
339 )?,
340 ))),
341 RenderSemanticModel::Ishikawa(model) => Ok(LayoutDiagram::IshikawaDiagram(Box::new(
342 ishikawa::layout_ishikawa_diagram_typed(
343 model,
344 effective_config,
345 options.text_measurer.as_ref(),
346 )?,
347 ))),
348 RenderSemanticModel::EventModeling(model) => Ok(LayoutDiagram::EventModelingDiagram(
349 Box::new(eventmodeling::layout_eventmodeling_diagram_typed(
350 model,
351 effective_config,
352 options.text_measurer.as_ref(),
353 )?),
354 )),
355 RenderSemanticModel::Json(semantic) => layout_json_by_type(
356 diagram_type,
357 semantic,
358 &parsed.meta.effective_config,
359 title,
360 options,
361 ),
362 }
363}
364
365fn layout_json_by_type(
366 diagram_type: &str,
367 semantic: &Value,
368 effective_config: &merman_core::MermaidConfig,
369 title: Option<&str>,
370 options: &LayoutOptions,
371) -> Result<LayoutDiagram> {
372 let effective_config_value = effective_config.as_value();
373
374 match diagram_type {
375 "error" => Ok(LayoutDiagram::ErrorDiagram(Box::new(
376 error::layout_error_diagram(
377 semantic,
378 effective_config_value,
379 options.text_measurer.as_ref(),
380 )?,
381 ))),
382 "block" => Ok(LayoutDiagram::BlockDiagram(Box::new(
383 block::layout_block_diagram(
384 semantic,
385 effective_config_value,
386 options.text_measurer.as_ref(),
387 )?,
388 ))),
389 "architecture" => Ok(LayoutDiagram::ArchitectureDiagram(Box::new(
390 architecture::layout_architecture_diagram(
391 semantic,
392 effective_config_value,
393 options.text_measurer.as_ref(),
394 options.use_manatee_layout,
395 )?,
396 ))),
397 "requirement" => Ok(LayoutDiagram::RequirementDiagram(Box::new(
398 requirement::layout_requirement_diagram(
399 semantic,
400 effective_config_value,
401 options.text_measurer.as_ref(),
402 )?,
403 ))),
404 "radar" => Ok(LayoutDiagram::RadarDiagram(Box::new(
405 radar::layout_radar_diagram(
406 semantic,
407 effective_config_value,
408 options.text_measurer.as_ref(),
409 )?,
410 ))),
411 "treemap" => Ok(LayoutDiagram::TreemapDiagram(Box::new(
412 treemap::layout_treemap_diagram(
413 semantic,
414 effective_config_value,
415 options.text_measurer.as_ref(),
416 )?,
417 ))),
418 "flowchart-v2" => Ok(LayoutDiagram::FlowchartV2(Box::new(
419 flowchart::layout_flowchart_v2(
420 semantic,
421 effective_config,
422 options.text_measurer.as_ref(),
423 options.math_renderer.as_deref(),
424 )?,
425 ))),
426 "stateDiagram" => Ok(LayoutDiagram::StateDiagramV2(Box::new(
427 state::layout_state_diagram_v2(
428 semantic,
429 effective_config_value,
430 options.text_measurer.as_ref(),
431 )?,
432 ))),
433 "classDiagram" | "class" => Ok(LayoutDiagram::ClassDiagramV2(Box::new(
434 class::layout_class_diagram_v2_with_config(
435 semantic,
436 effective_config,
437 options.text_measurer.as_ref(),
438 )?,
439 ))),
440 "er" | "erDiagram" => Ok(LayoutDiagram::ErDiagram(Box::new(er::layout_er_diagram(
441 semantic,
442 effective_config_value,
443 options.text_measurer.as_ref(),
444 )?))),
445 "sequence" | "zenuml" => Ok(LayoutDiagram::SequenceDiagram(Box::new(
446 sequence::layout_sequence_diagram_with_title(
447 semantic,
448 title,
449 effective_config_value,
450 options.text_measurer.as_ref(),
451 options.math_renderer.as_deref(),
452 )?,
453 ))),
454 "info" => Ok(LayoutDiagram::InfoDiagram(Box::new(
455 info::layout_info_diagram(
456 semantic,
457 effective_config_value,
458 options.text_measurer.as_ref(),
459 )?,
460 ))),
461 "packet" => Ok(LayoutDiagram::PacketDiagram(Box::new(
462 packet::layout_packet_diagram(
463 semantic,
464 title,
465 effective_config_value,
466 options.text_measurer.as_ref(),
467 )?,
468 ))),
469 "timeline" => Ok(LayoutDiagram::TimelineDiagram(Box::new(
470 timeline::layout_timeline_diagram(
471 semantic,
472 effective_config_value,
473 options.text_measurer.as_ref(),
474 )?,
475 ))),
476 "gantt" => Ok(LayoutDiagram::GanttDiagram(Box::new(
477 gantt::layout_gantt_diagram(
478 semantic,
479 effective_config_value,
480 options.text_measurer.as_ref(),
481 )?,
482 ))),
483 "c4" => Ok(LayoutDiagram::C4Diagram(Box::new(c4::layout_c4_diagram(
484 semantic,
485 effective_config_value,
486 options.text_measurer.as_ref(),
487 options.viewport_width,
488 options.viewport_height,
489 )?))),
490 "journey" => Ok(LayoutDiagram::JourneyDiagram(Box::new(
491 journey::layout_journey_diagram(
492 semantic,
493 effective_config_value,
494 options.text_measurer.as_ref(),
495 )?,
496 ))),
497 "gitGraph" => Ok(LayoutDiagram::GitGraphDiagram(Box::new(
498 gitgraph::layout_gitgraph_diagram(
499 semantic,
500 effective_config_value,
501 options.text_measurer.as_ref(),
502 )?,
503 ))),
504 "kanban" => Ok(LayoutDiagram::KanbanDiagram(Box::new(
505 kanban::layout_kanban_diagram(
506 semantic,
507 effective_config_value,
508 options.text_measurer.as_ref(),
509 )?,
510 ))),
511 "pie" => Ok(LayoutDiagram::PieDiagram(Box::new(
512 pie::layout_pie_diagram(
513 semantic,
514 effective_config_value,
515 options.text_measurer.as_ref(),
516 )?,
517 ))),
518 "xychart" => Ok(LayoutDiagram::XyChartDiagram(Box::new(
519 xychart::layout_xychart_diagram(
520 semantic,
521 effective_config_value,
522 options.text_measurer.as_ref(),
523 )?,
524 ))),
525 "quadrantChart" => Ok(LayoutDiagram::QuadrantChartDiagram(Box::new(
526 quadrantchart::layout_quadrantchart_diagram(
527 semantic,
528 effective_config_value,
529 options.text_measurer.as_ref(),
530 )?,
531 ))),
532 "mindmap" => Ok(LayoutDiagram::MindmapDiagram(Box::new(
533 mindmap::layout_mindmap_diagram(
534 semantic,
535 effective_config_value,
536 options.text_measurer.as_ref(),
537 options.use_manatee_layout,
538 )?,
539 ))),
540 "sankey" => Ok(LayoutDiagram::SankeyDiagram(Box::new(
541 sankey::layout_sankey_diagram(
542 semantic,
543 effective_config_value,
544 options.text_measurer.as_ref(),
545 )?,
546 ))),
547 "treeView" => Ok(LayoutDiagram::TreeViewDiagram(Box::new(
548 tree_view::layout_tree_view_diagram(
549 semantic,
550 effective_config_value,
551 options.text_measurer.as_ref(),
552 )?,
553 ))),
554 "ishikawa" => Ok(LayoutDiagram::IshikawaDiagram(Box::new(
555 ishikawa::layout_ishikawa_diagram(
556 semantic,
557 effective_config_value,
558 options.text_measurer.as_ref(),
559 )?,
560 ))),
561 "eventmodeling" => Ok(LayoutDiagram::EventModelingDiagram(Box::new(
562 eventmodeling::layout_eventmodeling_diagram(
563 semantic,
564 effective_config_value,
565 options.text_measurer.as_ref(),
566 )?,
567 ))),
568 other => Err(Error::UnsupportedDiagram {
569 diagram_type: other.to_string(),
570 }),
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use merman_core::{Engine, ParseOptions};
578
579 #[test]
580 fn render_model_dispatch_accepts_diagram_type_aliases() {
581 let parsed = Engine::new()
582 .parse_diagram_for_render_model_as_sync(
583 "flowchart-elk",
584 "flowchart-elk TD\nA-->B;",
585 ParseOptions::strict(),
586 )
587 .unwrap()
588 .unwrap();
589
590 let layout = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap();
591 assert!(matches!(layout, LayoutDiagram::FlowchartV2(_)));
592 }
593
594 #[test]
595 fn render_model_dispatch_rejects_mismatched_typed_model() {
596 let mut parsed = Engine::new()
597 .parse_diagram_for_render_model_sync(
598 "sequenceDiagram\nAlice->>Bob: Hi",
599 ParseOptions::strict(),
600 )
601 .unwrap()
602 .unwrap();
603 parsed.meta.diagram_type = "flowchart-v2".to_string();
604
605 let err = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap_err();
606 let message = err.to_string();
607 assert!(message.contains("sequence"));
608 assert!(message.contains("flowchart-v2"));
609 }
610}