1#![forbid(unsafe_code)]
2
3pub mod architecture;
10pub mod block;
11pub mod c4;
12pub mod class;
13mod entities;
14pub mod er;
15pub mod error;
16pub mod flowchart;
17pub mod gantt;
18mod generated;
19pub mod gitgraph;
20pub mod info;
21pub mod journey;
22mod json;
23pub mod kanban;
24pub mod math;
25pub mod mindmap;
26pub mod model;
27pub mod packet;
28pub mod pie;
29pub mod quadrantchart;
30pub mod radar;
31pub mod requirement;
32pub mod sankey;
33pub mod sequence;
34pub mod state;
35pub mod svg;
36pub mod text;
37pub mod timeline;
38pub mod treemap;
39mod trig_tables;
40pub mod xychart;
41
42use crate::math::MathRenderer;
43use crate::model::{LayoutDiagram, LayoutMeta, LayoutedDiagram};
44use crate::text::{DeterministicTextMeasurer, TextMeasurer};
45use merman_core::{ParsedDiagram, ParsedDiagramRender, RenderSemanticModel};
46use serde_json::Value;
47use std::sync::Arc;
48
49#[derive(Debug, thiserror::Error)]
50pub enum Error {
51 #[error("unsupported diagram type for layout: {diagram_type}")]
52 UnsupportedDiagram { diagram_type: String },
53 #[error("invalid semantic model: {message}")]
54 InvalidModel { message: String },
55 #[error("semantic model JSON error: {0}")]
56 Json(#[from] serde_json::Error),
57}
58
59pub type Result<T> = std::result::Result<T, Error>;
60
61#[derive(Clone)]
62pub struct LayoutOptions {
63 pub text_measurer: Arc<dyn TextMeasurer + Send + Sync>,
64 pub math_renderer: Option<Arc<dyn MathRenderer + Send + Sync>>,
66 pub viewport_width: f64,
67 pub viewport_height: f64,
68 pub use_manatee_layout: bool,
71}
72
73impl Default for LayoutOptions {
74 fn default() -> Self {
75 Self {
76 text_measurer: Arc::new(DeterministicTextMeasurer::default()),
77 math_renderer: None,
78 viewport_width: 800.0,
79 viewport_height: 600.0,
80 use_manatee_layout: false,
81 }
82 }
83}
84
85impl LayoutOptions {
86 pub fn headless_svg_defaults() -> Self {
91 Self {
92 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
93 use_manatee_layout: true,
96 ..Default::default()
97 }
98 }
99
100 pub fn with_text_measurer(mut self, measurer: Arc<dyn TextMeasurer + Send + Sync>) -> Self {
101 self.text_measurer = measurer;
102 self
103 }
104
105 pub fn with_math_renderer(mut self, renderer: Arc<dyn MathRenderer + Send + Sync>) -> Self {
106 self.math_renderer = Some(renderer);
107 self
108 }
109}
110
111pub fn layout_parsed(parsed: &ParsedDiagram, options: &LayoutOptions) -> Result<LayoutedDiagram> {
112 let meta = LayoutMeta::from_parse_metadata(&parsed.meta);
113 let layout = layout_parsed_layout_only(parsed, options)?;
114
115 Ok(LayoutedDiagram {
116 meta,
117 semantic: Value::clone(&parsed.model),
118 layout,
119 })
120}
121
122pub fn layout_parsed_layout_only(
123 parsed: &ParsedDiagram,
124 options: &LayoutOptions,
125) -> Result<LayoutDiagram> {
126 let diagram_type = parsed.meta.diagram_type.as_str();
127 let effective_config = parsed.meta.effective_config.as_value();
128 let title = parsed.meta.title.as_deref();
129
130 let layout = match diagram_type {
131 "error" => LayoutDiagram::ErrorDiagram(error::layout_error_diagram(
132 &parsed.model,
133 effective_config,
134 options.text_measurer.as_ref(),
135 )?),
136 "block" => LayoutDiagram::BlockDiagram(block::layout_block_diagram(
137 &parsed.model,
138 effective_config,
139 options.text_measurer.as_ref(),
140 )?),
141 "architecture" => {
142 LayoutDiagram::ArchitectureDiagram(architecture::layout_architecture_diagram(
143 &parsed.model,
144 effective_config,
145 options.text_measurer.as_ref(),
146 options.use_manatee_layout,
147 )?)
148 }
149 "requirement" => {
150 LayoutDiagram::RequirementDiagram(requirement::layout_requirement_diagram(
151 &parsed.model,
152 effective_config,
153 options.text_measurer.as_ref(),
154 )?)
155 }
156 "radar" => LayoutDiagram::RadarDiagram(radar::layout_radar_diagram(
157 &parsed.model,
158 effective_config,
159 options.text_measurer.as_ref(),
160 )?),
161 "treemap" => LayoutDiagram::TreemapDiagram(treemap::layout_treemap_diagram(
162 &parsed.model,
163 effective_config,
164 options.text_measurer.as_ref(),
165 )?),
166 "flowchart-v2" => LayoutDiagram::FlowchartV2(flowchart::layout_flowchart_v2(
167 &parsed.model,
168 &parsed.meta.effective_config,
169 options.text_measurer.as_ref(),
170 options.math_renderer.as_deref(),
171 )?),
172 "stateDiagram" => LayoutDiagram::StateDiagramV2(state::layout_state_diagram_v2(
173 &parsed.model,
174 effective_config,
175 options.text_measurer.as_ref(),
176 )?),
177 "classDiagram" | "class" => LayoutDiagram::ClassDiagramV2(class::layout_class_diagram_v2(
178 &parsed.model,
179 effective_config,
180 options.text_measurer.as_ref(),
181 )?),
182 "er" | "erDiagram" => LayoutDiagram::ErDiagram(er::layout_er_diagram(
183 &parsed.model,
184 effective_config,
185 options.text_measurer.as_ref(),
186 )?),
187 "sequence" | "zenuml" => LayoutDiagram::SequenceDiagram(sequence::layout_sequence_diagram(
188 &parsed.model,
189 effective_config,
190 options.text_measurer.as_ref(),
191 )?),
192 "info" => LayoutDiagram::InfoDiagram(info::layout_info_diagram(
193 &parsed.model,
194 effective_config,
195 options.text_measurer.as_ref(),
196 )?),
197 "packet" => LayoutDiagram::PacketDiagram(packet::layout_packet_diagram(
198 &parsed.model,
199 title,
200 effective_config,
201 options.text_measurer.as_ref(),
202 )?),
203 "timeline" => LayoutDiagram::TimelineDiagram(timeline::layout_timeline_diagram(
204 &parsed.model,
205 effective_config,
206 options.text_measurer.as_ref(),
207 )?),
208 "gantt" => LayoutDiagram::GanttDiagram(gantt::layout_gantt_diagram(
209 &parsed.model,
210 effective_config,
211 options.text_measurer.as_ref(),
212 )?),
213 "c4" => LayoutDiagram::C4Diagram(c4::layout_c4_diagram(
214 &parsed.model,
215 effective_config,
216 options.text_measurer.as_ref(),
217 options.viewport_width,
218 options.viewport_height,
219 )?),
220 "journey" => LayoutDiagram::JourneyDiagram(journey::layout_journey_diagram(
221 &parsed.model,
222 effective_config,
223 options.text_measurer.as_ref(),
224 )?),
225 "gitGraph" => LayoutDiagram::GitGraphDiagram(gitgraph::layout_gitgraph_diagram(
226 &parsed.model,
227 effective_config,
228 options.text_measurer.as_ref(),
229 )?),
230 "kanban" => LayoutDiagram::KanbanDiagram(kanban::layout_kanban_diagram(
231 &parsed.model,
232 effective_config,
233 options.text_measurer.as_ref(),
234 )?),
235 "pie" => LayoutDiagram::PieDiagram(pie::layout_pie_diagram(
236 &parsed.model,
237 effective_config,
238 options.text_measurer.as_ref(),
239 )?),
240 "xychart" => LayoutDiagram::XyChartDiagram(xychart::layout_xychart_diagram(
241 &parsed.model,
242 effective_config,
243 options.text_measurer.as_ref(),
244 )?),
245 "quadrantChart" => {
246 LayoutDiagram::QuadrantChartDiagram(quadrantchart::layout_quadrantchart_diagram(
247 &parsed.model,
248 effective_config,
249 options.text_measurer.as_ref(),
250 )?)
251 }
252 "mindmap" => LayoutDiagram::MindmapDiagram(mindmap::layout_mindmap_diagram(
253 &parsed.model,
254 effective_config,
255 options.text_measurer.as_ref(),
256 options.use_manatee_layout,
257 )?),
258 "sankey" => LayoutDiagram::SankeyDiagram(sankey::layout_sankey_diagram(
259 &parsed.model,
260 effective_config,
261 options.text_measurer.as_ref(),
262 )?),
263 other => {
264 return Err(Error::UnsupportedDiagram {
265 diagram_type: other.to_string(),
266 });
267 }
268 };
269 Ok(layout)
270}
271
272pub fn layout_parsed_render_layout_only(
273 parsed: &ParsedDiagramRender,
274 options: &LayoutOptions,
275) -> Result<LayoutDiagram> {
276 let diagram_type = parsed.meta.diagram_type.as_str();
277 let effective_config = parsed.meta.effective_config.as_value();
278 let title = parsed.meta.title.as_deref();
279
280 match (&parsed.model, diagram_type) {
281 (RenderSemanticModel::Mindmap(model), "mindmap") => Ok(LayoutDiagram::MindmapDiagram(
282 mindmap::layout_mindmap_diagram_typed(
283 model,
284 effective_config,
285 options.text_measurer.as_ref(),
286 options.use_manatee_layout,
287 )?,
288 )),
289 (RenderSemanticModel::Architecture(model), "architecture") => Ok(
290 LayoutDiagram::ArchitectureDiagram(architecture::layout_architecture_diagram_typed(
291 model,
292 effective_config,
293 options.text_measurer.as_ref(),
294 options.use_manatee_layout,
295 )?),
296 ),
297 (RenderSemanticModel::Flowchart(model), "flowchart-v2" | "flowchart" | "flowchart-elk") => {
298 Ok(LayoutDiagram::FlowchartV2(
299 flowchart::layout_flowchart_v2_typed(
300 model,
301 &parsed.meta.effective_config,
302 options.text_measurer.as_ref(),
303 options.math_renderer.as_deref(),
304 )?,
305 ))
306 }
307 (RenderSemanticModel::State(model), "stateDiagram" | "state") => Ok(
308 LayoutDiagram::StateDiagramV2(state::layout_state_diagram_v2_typed(
309 model,
310 effective_config,
311 options.text_measurer.as_ref(),
312 )?),
313 ),
314 (RenderSemanticModel::Class(model), "classDiagram" | "class") => Ok(
315 LayoutDiagram::ClassDiagramV2(class::layout_class_diagram_v2_typed(
316 model,
317 effective_config,
318 options.text_measurer.as_ref(),
319 )?),
320 ),
321 (RenderSemanticModel::Json(semantic), _) => {
322 let layout = match diagram_type {
323 "error" => LayoutDiagram::ErrorDiagram(error::layout_error_diagram(
324 semantic,
325 effective_config,
326 options.text_measurer.as_ref(),
327 )?),
328 "block" => LayoutDiagram::BlockDiagram(block::layout_block_diagram(
329 semantic,
330 effective_config,
331 options.text_measurer.as_ref(),
332 )?),
333 "architecture" => {
334 LayoutDiagram::ArchitectureDiagram(architecture::layout_architecture_diagram(
335 semantic,
336 effective_config,
337 options.text_measurer.as_ref(),
338 options.use_manatee_layout,
339 )?)
340 }
341 "requirement" => {
342 LayoutDiagram::RequirementDiagram(requirement::layout_requirement_diagram(
343 semantic,
344 effective_config,
345 options.text_measurer.as_ref(),
346 )?)
347 }
348 "radar" => LayoutDiagram::RadarDiagram(radar::layout_radar_diagram(
349 semantic,
350 effective_config,
351 options.text_measurer.as_ref(),
352 )?),
353 "treemap" => LayoutDiagram::TreemapDiagram(treemap::layout_treemap_diagram(
354 semantic,
355 effective_config,
356 options.text_measurer.as_ref(),
357 )?),
358 "flowchart-v2" => LayoutDiagram::FlowchartV2(flowchart::layout_flowchart_v2(
359 semantic,
360 &parsed.meta.effective_config,
361 options.text_measurer.as_ref(),
362 options.math_renderer.as_deref(),
363 )?),
364 "stateDiagram" => LayoutDiagram::StateDiagramV2(state::layout_state_diagram_v2(
365 semantic,
366 effective_config,
367 options.text_measurer.as_ref(),
368 )?),
369 "classDiagram" | "class" => {
370 LayoutDiagram::ClassDiagramV2(class::layout_class_diagram_v2(
371 semantic,
372 effective_config,
373 options.text_measurer.as_ref(),
374 )?)
375 }
376 "er" | "erDiagram" => LayoutDiagram::ErDiagram(er::layout_er_diagram(
377 semantic,
378 effective_config,
379 options.text_measurer.as_ref(),
380 )?),
381 "sequence" | "zenuml" => {
382 LayoutDiagram::SequenceDiagram(sequence::layout_sequence_diagram(
383 semantic,
384 effective_config,
385 options.text_measurer.as_ref(),
386 )?)
387 }
388 "info" => LayoutDiagram::InfoDiagram(info::layout_info_diagram(
389 semantic,
390 effective_config,
391 options.text_measurer.as_ref(),
392 )?),
393 "packet" => LayoutDiagram::PacketDiagram(packet::layout_packet_diagram(
394 semantic,
395 title,
396 effective_config,
397 options.text_measurer.as_ref(),
398 )?),
399 "timeline" => LayoutDiagram::TimelineDiagram(timeline::layout_timeline_diagram(
400 semantic,
401 effective_config,
402 options.text_measurer.as_ref(),
403 )?),
404 "gantt" => LayoutDiagram::GanttDiagram(gantt::layout_gantt_diagram(
405 semantic,
406 effective_config,
407 options.text_measurer.as_ref(),
408 )?),
409 "c4" => LayoutDiagram::C4Diagram(c4::layout_c4_diagram(
410 semantic,
411 effective_config,
412 options.text_measurer.as_ref(),
413 options.viewport_width,
414 options.viewport_height,
415 )?),
416 "journey" => LayoutDiagram::JourneyDiagram(journey::layout_journey_diagram(
417 semantic,
418 effective_config,
419 options.text_measurer.as_ref(),
420 )?),
421 "gitGraph" => LayoutDiagram::GitGraphDiagram(gitgraph::layout_gitgraph_diagram(
422 semantic,
423 effective_config,
424 options.text_measurer.as_ref(),
425 )?),
426 "kanban" => LayoutDiagram::KanbanDiagram(kanban::layout_kanban_diagram(
427 semantic,
428 effective_config,
429 options.text_measurer.as_ref(),
430 )?),
431 "pie" => LayoutDiagram::PieDiagram(pie::layout_pie_diagram(
432 semantic,
433 effective_config,
434 options.text_measurer.as_ref(),
435 )?),
436 "xychart" => LayoutDiagram::XyChartDiagram(xychart::layout_xychart_diagram(
437 semantic,
438 effective_config,
439 options.text_measurer.as_ref(),
440 )?),
441 "quadrantChart" => LayoutDiagram::QuadrantChartDiagram(
442 quadrantchart::layout_quadrantchart_diagram(
443 semantic,
444 effective_config,
445 options.text_measurer.as_ref(),
446 )?,
447 ),
448 "mindmap" => LayoutDiagram::MindmapDiagram(mindmap::layout_mindmap_diagram(
449 semantic,
450 effective_config,
451 options.text_measurer.as_ref(),
452 options.use_manatee_layout,
453 )?),
454 "sankey" => LayoutDiagram::SankeyDiagram(sankey::layout_sankey_diagram(
455 semantic,
456 effective_config,
457 options.text_measurer.as_ref(),
458 )?),
459 other => {
460 return Err(Error::UnsupportedDiagram {
461 diagram_type: other.to_string(),
462 });
463 }
464 };
465 Ok(layout)
466 }
467 _ => Err(Error::InvalidModel {
468 message: format!("unexpected render model variant for diagram type: {diagram_type}"),
469 }),
470 }
471}