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