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