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