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