1#![forbid(unsafe_code)]
2
3extern crate self as web_time;
10
11#[cfg(feature = "cytoscape-layout")]
12pub mod architecture;
13#[cfg(feature = "cytoscape-layout")]
14pub(crate) mod architecture_metrics;
15pub mod block;
16pub mod c4;
17mod chart_palette;
18pub mod class;
19mod config;
20mod entities;
21pub mod er;
22pub mod error;
23pub mod eventmodeling;
24pub mod flowchart;
25pub mod gantt;
26mod generated;
27pub mod gitgraph;
28mod host_time;
29pub mod info;
30pub mod ishikawa;
31pub mod journey;
32mod json;
33pub mod kanban;
34pub mod math;
35mod mermaid_style;
36#[cfg(feature = "cytoscape-layout")]
37pub mod mindmap;
38pub mod model;
39pub mod packet;
40pub mod pie;
41pub mod quadrantchart;
42pub mod radar;
43pub mod requirement;
44pub mod resources;
45pub mod sankey;
46pub mod sequence;
47pub mod state;
48pub mod svg;
49pub mod text;
50mod theme;
51pub mod timeline;
52pub mod tree_view;
53pub mod treemap;
54mod trig_tables;
55pub mod venn;
56pub mod xychart;
57
58pub(crate) use host_time::{Duration, Instant};
59
60use crate::math::MathRenderer;
61use crate::model::{LayoutDiagram, LayoutMeta, LayoutedDiagram};
62use crate::text::{DeterministicTextMeasurer, TextMeasurer};
63use merman_core::diagrams::flowchart::FlowchartV2Model;
64use merman_core::models::class_diagram::ClassDiagram;
65use merman_core::{ParsedDiagram, ParsedDiagramRender, RenderSemanticModel};
66use serde_json::Value;
67use std::sync::Arc;
68
69pub use resources::{
70 ClassComplexity, FlowchartComplexity, RenderResourceLimits, RenderResourceProfile,
71 ResourceLimitExceeded, ResourceLimitPhase,
72};
73
74#[derive(Debug, thiserror::Error)]
75pub enum Error {
76 #[error("unsupported diagram type for layout: {diagram_type}")]
77 UnsupportedDiagram { diagram_type: String },
78 #[error("invalid semantic model: {message}")]
79 InvalidModel { message: String },
80 #[error("SVG postprocessor `{pass}` failed: {message}")]
81 SvgPostprocess { pass: String, message: String },
82 #[error(transparent)]
83 ResourceLimitExceeded(#[from] ResourceLimitExceeded),
84 #[error("semantic model JSON error: {0}")]
85 Json(#[from] serde_json::Error),
86}
87
88pub type Result<T> = std::result::Result<T, Error>;
89
90impl Error {
91 pub fn svg_postprocess(pass: impl Into<String>, message: impl Into<String>) -> Self {
92 Self::SvgPostprocess {
93 pass: pass.into(),
94 message: message.into(),
95 }
96 }
97}
98
99#[derive(Clone)]
100pub struct LayoutOptions {
101 pub text_measurer: Arc<dyn TextMeasurer + Send + Sync>,
102 pub math_renderer: Option<Arc<dyn MathRenderer + Send + Sync>>,
104 pub viewport_width: f64,
105 pub viewport_height: f64,
106 pub use_manatee_layout: bool,
109 pub flowchart_elk_backend: FlowchartElkBackend,
114 pub resource_limits: RenderResourceLimits,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119pub enum FlowchartElkBackend {
120 Compat,
121 #[default]
122 SourcePorted,
123}
124
125impl Default for LayoutOptions {
126 fn default() -> Self {
127 Self {
128 text_measurer: Arc::new(DeterministicTextMeasurer::default()),
129 math_renderer: None,
130 viewport_width: 800.0,
131 viewport_height: 600.0,
132 use_manatee_layout: false,
133 flowchart_elk_backend: FlowchartElkBackend::SourcePorted,
134 resource_limits: RenderResourceLimits::interactive(),
135 }
136 }
137}
138
139impl LayoutOptions {
140 pub fn headless_svg_defaults() -> Self {
147 Self {
148 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
149 use_manatee_layout: true,
152 ..Default::default()
153 }
154 }
155
156 pub fn with_text_measurer(mut self, measurer: Arc<dyn TextMeasurer + Send + Sync>) -> Self {
157 self.text_measurer = measurer;
158 self
159 }
160
161 pub fn with_math_renderer(mut self, renderer: Arc<dyn MathRenderer + Send + Sync>) -> Self {
162 self.math_renderer = Some(renderer);
163 self
164 }
165
166 pub fn with_resource_limits(mut self, limits: RenderResourceLimits) -> Self {
167 self.resource_limits = limits;
168 self
169 }
170}
171
172pub fn layout_parsed(parsed: &ParsedDiagram, options: &LayoutOptions) -> Result<LayoutedDiagram> {
173 let meta = LayoutMeta::from_parse_metadata(&parsed.meta);
174 let layout = layout_parsed_layout_only(parsed, options)?;
175
176 Ok(LayoutedDiagram {
177 meta,
178 semantic: crate::json::clone_value_nonrecursive(&parsed.model),
179 layout,
180 })
181}
182
183pub fn layout_parsed_layout_only(
184 parsed: &ParsedDiagram,
185 options: &LayoutOptions,
186) -> Result<LayoutDiagram> {
187 let diagram_type = parsed.meta.diagram_type.as_str();
188 let title = parsed.meta.title.as_deref();
189 layout_json_by_type(
190 diagram_type,
191 &parsed.model,
192 &parsed.meta.effective_config,
193 title,
194 options,
195 )
196}
197
198pub fn layout_parsed_render_layout_only(
199 parsed: &ParsedDiagramRender,
200 options: &LayoutOptions,
201) -> Result<LayoutDiagram> {
202 let diagram_type = parsed.meta.diagram_type.as_str();
203 let effective_config = parsed.meta.effective_config.as_value();
204 let title = parsed.meta.title.as_deref();
205
206 if !parsed.model.supports_diagram_type(diagram_type) {
207 return Err(Error::InvalidModel {
208 message: format!(
209 "unexpected render model variant {} for diagram type: {diagram_type}",
210 parsed.model.kind()
211 ),
212 });
213 }
214
215 match &parsed.model {
216 #[cfg(feature = "cytoscape-layout")]
217 RenderSemanticModel::Mindmap(model) => Ok(LayoutDiagram::MindmapDiagram(Box::new(
218 mindmap::layout_mindmap_diagram_typed(
219 model,
220 effective_config,
221 options.text_measurer.as_ref(),
222 options.use_manatee_layout,
223 )?,
224 ))),
225 #[cfg(not(feature = "cytoscape-layout"))]
226 RenderSemanticModel::Mindmap(_) => Err(Error::UnsupportedDiagram {
227 diagram_type: diagram_type.to_string(),
228 }),
229 #[cfg(feature = "cytoscape-layout")]
230 RenderSemanticModel::Architecture(model) => Ok(LayoutDiagram::ArchitectureDiagram(
231 Box::new(architecture::layout_architecture_diagram_typed(
232 model,
233 effective_config,
234 options.text_measurer.as_ref(),
235 options.use_manatee_layout,
236 )?),
237 )),
238 #[cfg(not(feature = "cytoscape-layout"))]
239 RenderSemanticModel::Architecture(_) => Err(Error::UnsupportedDiagram {
240 diagram_type: diagram_type.to_string(),
241 }),
242 RenderSemanticModel::Flowchart(model) => Ok(LayoutDiagram::FlowchartV2(Box::new(
243 layout_flowchart_typed_by_engine(
244 diagram_type,
245 model,
246 &parsed.meta.effective_config,
247 options,
248 )?,
249 ))),
250 RenderSemanticModel::State(model) => Ok(LayoutDiagram::StateDiagramV2(Box::new(
251 state::layout_state_diagram_v2_typed(
252 model,
253 effective_config,
254 options.text_measurer.as_ref(),
255 )?,
256 ))),
257 RenderSemanticModel::Sequence(model) => Ok(LayoutDiagram::SequenceDiagram(Box::new(
258 sequence::layout_sequence_diagram_typed_with_title(
259 model,
260 title,
261 effective_config,
262 options.text_measurer.as_ref(),
263 options.math_renderer.as_deref(),
264 )?,
265 ))),
266 RenderSemanticModel::Class(model) => Ok(LayoutDiagram::ClassDiagramV2(Box::new(
267 layout_class_typed_by_engine(
268 diagram_type,
269 model,
270 &parsed.meta.effective_config,
271 options,
272 )?,
273 ))),
274 RenderSemanticModel::C4(model) => Ok(LayoutDiagram::C4Diagram(Box::new(
275 c4::layout_c4_diagram_typed(
276 model,
277 effective_config,
278 options.text_measurer.as_ref(),
279 options.viewport_width,
280 options.viewport_height,
281 )?,
282 ))),
283 RenderSemanticModel::Kanban(model) => Ok(LayoutDiagram::KanbanDiagram(Box::new(
284 kanban::layout_kanban_diagram_typed(
285 model,
286 effective_config,
287 options.text_measurer.as_ref(),
288 )?,
289 ))),
290 RenderSemanticModel::Gantt(model) => Ok(LayoutDiagram::GanttDiagram(Box::new(
291 gantt::layout_gantt_diagram_typed(
292 model,
293 effective_config,
294 options.text_measurer.as_ref(),
295 )?,
296 ))),
297 RenderSemanticModel::Pie(model) => Ok(LayoutDiagram::PieDiagram(Box::new(
298 pie::layout_pie_diagram_typed(model, effective_config, options.text_measurer.as_ref())?,
299 ))),
300 RenderSemanticModel::Packet(model) => Ok(LayoutDiagram::PacketDiagram(Box::new(
301 packet::layout_packet_diagram_typed(
302 model,
303 title,
304 effective_config,
305 options.text_measurer.as_ref(),
306 )?,
307 ))),
308 RenderSemanticModel::Timeline(model) => Ok(LayoutDiagram::TimelineDiagram(Box::new(
309 timeline::layout_timeline_diagram_typed(
310 model,
311 effective_config,
312 options.text_measurer.as_ref(),
313 )?,
314 ))),
315 RenderSemanticModel::Journey(model) => Ok(LayoutDiagram::JourneyDiagram(Box::new(
316 journey::layout_journey_diagram_typed(
317 model,
318 effective_config,
319 options.text_measurer.as_ref(),
320 )?,
321 ))),
322 RenderSemanticModel::Requirement(model) => Ok(LayoutDiagram::RequirementDiagram(Box::new(
323 requirement::layout_requirement_diagram_typed(
324 model,
325 effective_config,
326 options.text_measurer.as_ref(),
327 )?,
328 ))),
329 RenderSemanticModel::Sankey(model) => Ok(LayoutDiagram::SankeyDiagram(Box::new(
330 sankey::layout_sankey_diagram_typed(
331 model,
332 effective_config,
333 options.text_measurer.as_ref(),
334 )?,
335 ))),
336 RenderSemanticModel::Radar(model) => Ok(LayoutDiagram::RadarDiagram(Box::new(
337 radar::layout_radar_diagram_typed(
338 model,
339 effective_config,
340 options.text_measurer.as_ref(),
341 )?,
342 ))),
343 RenderSemanticModel::Info(model) => Ok(LayoutDiagram::InfoDiagram(Box::new(
344 info::layout_info_diagram_typed(
345 model,
346 effective_config,
347 options.text_measurer.as_ref(),
348 )?,
349 ))),
350 RenderSemanticModel::Treemap(model) => Ok(LayoutDiagram::TreemapDiagram(Box::new(
351 treemap::layout_treemap_diagram_typed(
352 model,
353 effective_config,
354 options.text_measurer.as_ref(),
355 )?,
356 ))),
357 RenderSemanticModel::Block(model) => Ok(LayoutDiagram::BlockDiagram(Box::new(
358 block::layout_block_diagram_typed(
359 model,
360 effective_config,
361 options.text_measurer.as_ref(),
362 )?,
363 ))),
364 RenderSemanticModel::Er(model) => Ok(LayoutDiagram::ErDiagram(Box::new(
365 er::layout_er_diagram_typed(model, effective_config, options.text_measurer.as_ref())?,
366 ))),
367 RenderSemanticModel::QuadrantChart(model) => Ok(LayoutDiagram::QuadrantChartDiagram(
368 Box::new(quadrantchart::layout_quadrantchart_diagram_typed(
369 model,
370 effective_config,
371 options.text_measurer.as_ref(),
372 )?),
373 )),
374 RenderSemanticModel::XyChart(model) => Ok(LayoutDiagram::XyChartDiagram(Box::new(
375 xychart::layout_xychart_diagram_typed(
376 model,
377 effective_config,
378 options.text_measurer.as_ref(),
379 )?,
380 ))),
381 RenderSemanticModel::GitGraph(model) => Ok(LayoutDiagram::GitGraphDiagram(Box::new(
382 gitgraph::layout_gitgraph_diagram_typed(
383 model,
384 effective_config,
385 options.text_measurer.as_ref(),
386 )?,
387 ))),
388 RenderSemanticModel::TreeView(model) => Ok(LayoutDiagram::TreeViewDiagram(Box::new(
389 tree_view::layout_tree_view_diagram_typed(
390 model,
391 effective_config,
392 options.text_measurer.as_ref(),
393 )?,
394 ))),
395 RenderSemanticModel::Ishikawa(model) => Ok(LayoutDiagram::IshikawaDiagram(Box::new(
396 ishikawa::layout_ishikawa_diagram_typed(
397 model,
398 effective_config,
399 options.text_measurer.as_ref(),
400 )?,
401 ))),
402 RenderSemanticModel::EventModeling(model) => Ok(LayoutDiagram::EventModelingDiagram(
403 Box::new(eventmodeling::layout_eventmodeling_diagram_typed(
404 model,
405 effective_config,
406 options.text_measurer.as_ref(),
407 )?),
408 )),
409 RenderSemanticModel::Venn(model) => Ok(LayoutDiagram::VennDiagram(Box::new(
410 venn::layout_venn_diagram_typed(model, title, effective_config)?,
411 ))),
412 RenderSemanticModel::Json(semantic) => layout_json_by_type(
413 diagram_type,
414 semantic,
415 &parsed.meta.effective_config,
416 title,
417 options,
418 ),
419 }
420}
421
422fn flowchart_uses_elk_layout(
423 diagram_type: &str,
424 effective_config: &merman_core::MermaidConfig,
425) -> bool {
426 diagram_type == "flowchart-elk" || effective_config.get_str("layout") == Some("elk")
427}
428
429fn class_uses_elk_layout(effective_config: &merman_core::MermaidConfig) -> bool {
430 effective_config.get_str("layout") == Some("elk")
431 || effective_config.get_str("class.defaultRenderer") == Some("elk")
432}
433
434fn layout_class_typed_by_engine(
435 diagram_type: &str,
436 model: &ClassDiagram,
437 effective_config: &merman_core::MermaidConfig,
438 options: &LayoutOptions,
439) -> Result<model::ClassDiagramV2Layout> {
440 if class_uses_elk_layout(effective_config) {
441 return layout_class_elk_typed_by_feature(diagram_type, model, effective_config, options);
442 }
443
444 options.resource_limits.check_class_complexity(model)?;
445 class::layout_class_diagram_v2_typed_with_config(
446 model,
447 effective_config,
448 options.text_measurer.as_ref(),
449 )
450}
451
452fn layout_class_json_by_engine(
453 diagram_type: &str,
454 semantic: &Value,
455 effective_config: &merman_core::MermaidConfig,
456 options: &LayoutOptions,
457) -> Result<model::ClassDiagramV2Layout> {
458 let model: ClassDiagram = crate::json::from_value_ref(semantic)?;
459 if class_uses_elk_layout(effective_config) {
460 return layout_class_elk_typed_by_feature(diagram_type, &model, effective_config, options);
461 }
462
463 options.resource_limits.check_class_complexity(&model)?;
464 class::layout_class_diagram_v2_typed_with_config(
465 &model,
466 effective_config,
467 options.text_measurer.as_ref(),
468 )
469}
470
471#[cfg(feature = "elk-layout")]
472fn layout_class_elk_typed_by_feature(
473 _diagram_type: &str,
474 model: &ClassDiagram,
475 effective_config: &merman_core::MermaidConfig,
476 options: &LayoutOptions,
477) -> Result<model::ClassDiagramV2Layout> {
478 options.resource_limits.check_class_complexity(model)?;
479 class::layout_class_diagram_v2_elk_typed_with_config(
480 model,
481 effective_config,
482 options.text_measurer.as_ref(),
483 )
484}
485
486#[cfg(not(feature = "elk-layout"))]
487fn layout_class_elk_typed_by_feature(
488 diagram_type: &str,
489 _model: &ClassDiagram,
490 _effective_config: &merman_core::MermaidConfig,
491 _options: &LayoutOptions,
492) -> Result<model::ClassDiagramV2Layout> {
493 Err(Error::UnsupportedDiagram {
494 diagram_type: diagram_type.to_string(),
495 })
496}
497
498fn layout_flowchart_typed_by_engine(
499 diagram_type: &str,
500 model: &FlowchartV2Model,
501 effective_config: &merman_core::MermaidConfig,
502 options: &LayoutOptions,
503) -> Result<model::FlowchartV2Layout> {
504 if flowchart_uses_elk_layout(diagram_type, effective_config) {
505 return layout_flowchart_elk_typed_by_feature(
506 diagram_type,
507 model,
508 effective_config,
509 options,
510 );
511 }
512
513 options.resource_limits.check_flowchart_complexity(model)?;
514 flowchart::layout_flowchart_v2_typed(
515 model,
516 effective_config,
517 options.text_measurer.as_ref(),
518 options.math_renderer.as_deref(),
519 )
520}
521
522#[cfg(feature = "elk-layout")]
523fn layout_flowchart_elk_typed_by_feature(
524 _diagram_type: &str,
525 model: &FlowchartV2Model,
526 effective_config: &merman_core::MermaidConfig,
527 options: &LayoutOptions,
528) -> Result<model::FlowchartV2Layout> {
529 options.resource_limits.check_flowchart_complexity(model)?;
530 flowchart::elk::layout_flowchart_elk_typed(
531 model,
532 effective_config,
533 options.text_measurer.as_ref(),
534 options.math_renderer.as_deref(),
535 options.flowchart_elk_backend,
536 )
537}
538
539#[cfg(not(feature = "elk-layout"))]
540fn layout_flowchart_elk_typed_by_feature(
541 diagram_type: &str,
542 _model: &FlowchartV2Model,
543 _effective_config: &merman_core::MermaidConfig,
544 _options: &LayoutOptions,
545) -> Result<model::FlowchartV2Layout> {
546 Err(Error::UnsupportedDiagram {
547 diagram_type: diagram_type.to_string(),
548 })
549}
550
551fn layout_flowchart_json_by_engine(
552 diagram_type: &str,
553 semantic: &Value,
554 effective_config: &merman_core::MermaidConfig,
555 options: &LayoutOptions,
556) -> Result<model::FlowchartV2Layout> {
557 if flowchart_uses_elk_layout(diagram_type, effective_config) {
558 return layout_flowchart_elk_json_by_feature(
559 diagram_type,
560 semantic,
561 effective_config,
562 options,
563 );
564 }
565
566 let model: FlowchartV2Model = crate::json::from_value_ref(semantic)?;
567 options.resource_limits.check_flowchart_complexity(&model)?;
568 flowchart::layout_flowchart_v2(
569 semantic,
570 effective_config,
571 options.text_measurer.as_ref(),
572 options.math_renderer.as_deref(),
573 )
574}
575
576#[cfg(feature = "elk-layout")]
577fn layout_flowchart_elk_json_by_feature(
578 _diagram_type: &str,
579 semantic: &Value,
580 effective_config: &merman_core::MermaidConfig,
581 options: &LayoutOptions,
582) -> Result<model::FlowchartV2Layout> {
583 let model: FlowchartV2Model = crate::json::from_value_ref(semantic)?;
584 options.resource_limits.check_flowchart_complexity(&model)?;
585 flowchart::elk::layout_flowchart_elk(
586 semantic,
587 effective_config,
588 options.text_measurer.as_ref(),
589 options.math_renderer.as_deref(),
590 options.flowchart_elk_backend,
591 )
592}
593
594#[cfg(not(feature = "elk-layout"))]
595fn layout_flowchart_elk_json_by_feature(
596 diagram_type: &str,
597 _semantic: &Value,
598 _effective_config: &merman_core::MermaidConfig,
599 _options: &LayoutOptions,
600) -> Result<model::FlowchartV2Layout> {
601 Err(Error::UnsupportedDiagram {
602 diagram_type: diagram_type.to_string(),
603 })
604}
605
606fn layout_json_by_type(
607 diagram_type: &str,
608 semantic: &Value,
609 effective_config: &merman_core::MermaidConfig,
610 title: Option<&str>,
611 options: &LayoutOptions,
612) -> Result<LayoutDiagram> {
613 let effective_config_value = effective_config.as_value();
614
615 match diagram_type {
616 "error" => Ok(LayoutDiagram::ErrorDiagram(Box::new(
617 error::layout_error_diagram(
618 semantic,
619 effective_config_value,
620 options.text_measurer.as_ref(),
621 )?,
622 ))),
623 "block" => Ok(LayoutDiagram::BlockDiagram(Box::new(
624 block::layout_block_diagram(
625 semantic,
626 effective_config_value,
627 options.text_measurer.as_ref(),
628 )?,
629 ))),
630 #[cfg(feature = "cytoscape-layout")]
631 "architecture" => Ok(LayoutDiagram::ArchitectureDiagram(Box::new(
632 architecture::layout_architecture_diagram(
633 semantic,
634 effective_config_value,
635 options.text_measurer.as_ref(),
636 options.use_manatee_layout,
637 )?,
638 ))),
639 #[cfg(not(feature = "cytoscape-layout"))]
640 "architecture" => Err(Error::UnsupportedDiagram {
641 diagram_type: diagram_type.to_string(),
642 }),
643 "requirement" => Ok(LayoutDiagram::RequirementDiagram(Box::new(
644 requirement::layout_requirement_diagram(
645 semantic,
646 effective_config_value,
647 options.text_measurer.as_ref(),
648 )?,
649 ))),
650 "radar" => Ok(LayoutDiagram::RadarDiagram(Box::new(
651 radar::layout_radar_diagram(
652 semantic,
653 effective_config_value,
654 options.text_measurer.as_ref(),
655 )?,
656 ))),
657 "treemap" => Ok(LayoutDiagram::TreemapDiagram(Box::new(
658 treemap::layout_treemap_diagram(
659 semantic,
660 effective_config_value,
661 options.text_measurer.as_ref(),
662 )?,
663 ))),
664 "venn" => Ok(LayoutDiagram::VennDiagram(Box::new(
665 venn::layout_venn_diagram(semantic, title, effective_config_value)?,
666 ))),
667 "flowchart-v2" | "flowchart-elk" => Ok(LayoutDiagram::FlowchartV2(Box::new(
668 layout_flowchart_json_by_engine(diagram_type, semantic, effective_config, options)?,
669 ))),
670 "stateDiagram" => Ok(LayoutDiagram::StateDiagramV2(Box::new(
671 state::layout_state_diagram_v2(
672 semantic,
673 effective_config_value,
674 options.text_measurer.as_ref(),
675 )?,
676 ))),
677 "classDiagram" | "class" => Ok(LayoutDiagram::ClassDiagramV2(Box::new(
678 layout_class_json_by_engine(diagram_type, semantic, effective_config, options)?,
679 ))),
680 "er" | "erDiagram" => Ok(LayoutDiagram::ErDiagram(Box::new(er::layout_er_diagram(
681 semantic,
682 effective_config_value,
683 options.text_measurer.as_ref(),
684 )?))),
685 "sequence" | "zenuml" => Ok(LayoutDiagram::SequenceDiagram(Box::new(
686 sequence::layout_sequence_diagram_with_title(
687 semantic,
688 title,
689 effective_config_value,
690 options.text_measurer.as_ref(),
691 options.math_renderer.as_deref(),
692 )?,
693 ))),
694 "info" => Ok(LayoutDiagram::InfoDiagram(Box::new(
695 info::layout_info_diagram(
696 semantic,
697 effective_config_value,
698 options.text_measurer.as_ref(),
699 )?,
700 ))),
701 "packet" => Ok(LayoutDiagram::PacketDiagram(Box::new(
702 packet::layout_packet_diagram(
703 semantic,
704 title,
705 effective_config_value,
706 options.text_measurer.as_ref(),
707 )?,
708 ))),
709 "timeline" => Ok(LayoutDiagram::TimelineDiagram(Box::new(
710 timeline::layout_timeline_diagram(
711 semantic,
712 effective_config_value,
713 options.text_measurer.as_ref(),
714 )?,
715 ))),
716 "gantt" => Ok(LayoutDiagram::GanttDiagram(Box::new(
717 gantt::layout_gantt_diagram(
718 semantic,
719 effective_config_value,
720 options.text_measurer.as_ref(),
721 )?,
722 ))),
723 "c4" => Ok(LayoutDiagram::C4Diagram(Box::new(c4::layout_c4_diagram(
724 semantic,
725 effective_config_value,
726 options.text_measurer.as_ref(),
727 options.viewport_width,
728 options.viewport_height,
729 )?))),
730 "journey" => Ok(LayoutDiagram::JourneyDiagram(Box::new(
731 journey::layout_journey_diagram(
732 semantic,
733 effective_config_value,
734 options.text_measurer.as_ref(),
735 )?,
736 ))),
737 "gitGraph" => Ok(LayoutDiagram::GitGraphDiagram(Box::new(
738 gitgraph::layout_gitgraph_diagram(
739 semantic,
740 effective_config_value,
741 options.text_measurer.as_ref(),
742 )?,
743 ))),
744 "kanban" => Ok(LayoutDiagram::KanbanDiagram(Box::new(
745 kanban::layout_kanban_diagram(
746 semantic,
747 effective_config_value,
748 options.text_measurer.as_ref(),
749 )?,
750 ))),
751 "pie" => Ok(LayoutDiagram::PieDiagram(Box::new(
752 pie::layout_pie_diagram(
753 semantic,
754 effective_config_value,
755 options.text_measurer.as_ref(),
756 )?,
757 ))),
758 "xychart" => Ok(LayoutDiagram::XyChartDiagram(Box::new(
759 xychart::layout_xychart_diagram(
760 semantic,
761 effective_config_value,
762 options.text_measurer.as_ref(),
763 )?,
764 ))),
765 "quadrantChart" => Ok(LayoutDiagram::QuadrantChartDiagram(Box::new(
766 quadrantchart::layout_quadrantchart_diagram(
767 semantic,
768 effective_config_value,
769 options.text_measurer.as_ref(),
770 )?,
771 ))),
772 #[cfg(feature = "cytoscape-layout")]
773 "mindmap" => Ok(LayoutDiagram::MindmapDiagram(Box::new(
774 mindmap::layout_mindmap_diagram(
775 semantic,
776 effective_config_value,
777 options.text_measurer.as_ref(),
778 options.use_manatee_layout,
779 )?,
780 ))),
781 #[cfg(not(feature = "cytoscape-layout"))]
782 "mindmap" => Err(Error::UnsupportedDiagram {
783 diagram_type: diagram_type.to_string(),
784 }),
785 "sankey" => Ok(LayoutDiagram::SankeyDiagram(Box::new(
786 sankey::layout_sankey_diagram(
787 semantic,
788 effective_config_value,
789 options.text_measurer.as_ref(),
790 )?,
791 ))),
792 "treeView" => Ok(LayoutDiagram::TreeViewDiagram(Box::new(
793 tree_view::layout_tree_view_diagram(
794 semantic,
795 effective_config_value,
796 options.text_measurer.as_ref(),
797 )?,
798 ))),
799 "ishikawa" => Ok(LayoutDiagram::IshikawaDiagram(Box::new(
800 ishikawa::layout_ishikawa_diagram(
801 semantic,
802 effective_config_value,
803 options.text_measurer.as_ref(),
804 )?,
805 ))),
806 "eventmodeling" => Ok(LayoutDiagram::EventModelingDiagram(Box::new(
807 eventmodeling::layout_eventmodeling_diagram(
808 semantic,
809 effective_config_value,
810 options.text_measurer.as_ref(),
811 )?,
812 ))),
813 other => Err(Error::UnsupportedDiagram {
814 diagram_type: other.to_string(),
815 }),
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use super::*;
822 use merman_core::{Engine, ParseOptions};
823
824 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
825 #[test]
826 fn render_model_dispatch_accepts_diagram_type_aliases() {
827 let parsed = Engine::new()
828 .parse_diagram_for_render_model_with_type_sync(
829 "flowchart-elk",
830 "flowchart-elk TD\nA-->B;",
831 ParseOptions::strict(),
832 )
833 .unwrap()
834 .unwrap();
835
836 let layout = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap();
837 assert!(matches!(layout, LayoutDiagram::FlowchartV2(_)));
838 }
839
840 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
841 #[test]
842 fn render_model_dispatch_uses_elk_for_flowchart_default_renderer_config() {
843 let parsed = Engine::new()
844 .parse_diagram_for_render_model_sync(
845 r#"---
846config:
847 flowchart:
848 defaultRenderer: elk
849---
850flowchart TD
851A-->B
852"#,
853 ParseOptions::strict(),
854 )
855 .unwrap()
856 .unwrap();
857
858 assert_eq!(parsed.meta.diagram_type, "flowchart-elk");
859 let layout = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap();
860 let LayoutDiagram::FlowchartV2(layout) = layout else {
861 panic!("expected flowchart layout");
862 };
863 let a = layout.nodes.iter().find(|node| node.id == "A").unwrap();
864 let b = layout.nodes.iter().find(|node| node.id == "B").unwrap();
865 assert!(b.y > a.y);
866 }
867
868 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
869 #[test]
870 fn render_model_dispatch_rejects_flowchart_over_node_resource_limit() {
871 let parsed = Engine::new()
872 .parse_diagram_for_render_model_with_type_sync(
873 "flowchart-elk",
874 "flowchart-elk TD\nA-->B;",
875 ParseOptions::strict(),
876 )
877 .unwrap()
878 .unwrap();
879 let options = LayoutOptions {
880 resource_limits: RenderResourceLimits {
881 max_flowchart_nodes: Some(1),
882 ..RenderResourceLimits::unbounded_for_trusted_input()
883 },
884 ..LayoutOptions::default()
885 };
886
887 let err = layout_parsed_render_layout_only(&parsed, &options).unwrap_err();
888
889 let Error::ResourceLimitExceeded(limit) = err else {
890 panic!("expected resource limit error");
891 };
892 assert_eq!(limit.phase, ResourceLimitPhase::LayoutModel);
893 assert_eq!(limit.limit, "max_flowchart_nodes");
894 }
895
896 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
897 #[test]
898 fn render_model_dispatch_uses_elk_for_class_layout_config() {
899 let parsed = Engine::new()
900 .parse_diagram_for_render_model_sync(
901 r#"---
902config:
903 layout: elk
904---
905classDiagram
906direction LR
907Animal <|-- Duck
908"#,
909 ParseOptions::strict(),
910 )
911 .unwrap()
912 .unwrap();
913
914 assert_eq!(parsed.meta.diagram_type, "classDiagram");
915 let layout = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap();
916 let LayoutDiagram::ClassDiagramV2(layout) = layout else {
917 panic!("expected class layout");
918 };
919 let animal = layout
920 .nodes
921 .iter()
922 .find(|node| node.id == "Animal")
923 .unwrap();
924 let duck = layout.nodes.iter().find(|node| node.id == "Duck").unwrap();
925 assert!(
926 duck.x > animal.x,
927 "ELK LR class layout should place Duck to the right of Animal; Animal={}, Duck={}",
928 animal.x,
929 duck.x
930 );
931 }
932
933 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
934 #[test]
935 fn render_model_dispatch_uses_elk_for_class_default_renderer_config() {
936 let parsed = Engine::new()
937 .parse_diagram_for_render_model_sync(
938 r#"---
939config:
940 class:
941 defaultRenderer: elk
942---
943classDiagram
944direction LR
945Animal <|-- Duck
946"#,
947 ParseOptions::strict(),
948 )
949 .unwrap()
950 .unwrap();
951
952 assert_eq!(parsed.meta.diagram_type, "class");
953 let layout = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap();
954 let LayoutDiagram::ClassDiagramV2(layout) = layout else {
955 panic!("expected class layout");
956 };
957 let animal = layout
958 .nodes
959 .iter()
960 .find(|node| node.id == "Animal")
961 .unwrap();
962 let duck = layout.nodes.iter().find(|node| node.id == "Duck").unwrap();
963 assert!(
964 duck.x > animal.x,
965 "ELK LR class layout should place Duck to the right of Animal; Animal={}, Duck={}",
966 animal.x,
967 duck.x
968 );
969 }
970
971 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
972 #[test]
973 fn render_model_dispatch_rejects_class_over_node_resource_limit() {
974 let parsed = Engine::new()
975 .parse_diagram_for_render_model_sync(
976 "classDiagram\nAnimal <|-- Duck",
977 ParseOptions::strict(),
978 )
979 .unwrap()
980 .unwrap();
981 let options = LayoutOptions {
982 resource_limits: RenderResourceLimits {
983 max_class_nodes: Some(1),
984 ..RenderResourceLimits::unbounded_for_trusted_input()
985 },
986 ..LayoutOptions::default()
987 };
988
989 let err = layout_parsed_render_layout_only(&parsed, &options).unwrap_err();
990
991 let Error::ResourceLimitExceeded(limit) = err else {
992 panic!("expected resource limit error");
993 };
994 assert_eq!(limit.phase, ResourceLimitPhase::LayoutModel);
995 assert_eq!(limit.limit, "max_class_nodes");
996 }
997
998 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
999 #[test]
1000 fn json_dispatch_rejects_flowchart_over_edge_resource_limit() {
1001 let parsed = Engine::new()
1002 .parse_diagram_sync("flowchart TD\nA-->B\nB-->C\nC-->D", ParseOptions::strict())
1003 .unwrap()
1004 .unwrap();
1005 let options = LayoutOptions {
1006 resource_limits: RenderResourceLimits {
1007 max_flowchart_edges: Some(2),
1008 ..RenderResourceLimits::unbounded_for_trusted_input()
1009 },
1010 ..LayoutOptions::default()
1011 };
1012
1013 let err = layout_parsed(&parsed, &options).unwrap_err();
1014
1015 let Error::ResourceLimitExceeded(limit) = err else {
1016 panic!("expected resource limit error");
1017 };
1018 assert_eq!(limit.phase, ResourceLimitPhase::LayoutModel);
1019 assert_eq!(limit.limit, "max_flowchart_edges");
1020 }
1021
1022 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1023 #[test]
1024 fn json_dispatch_rejects_class_over_edge_resource_limit() {
1025 let parsed = Engine::new()
1026 .parse_diagram_sync(
1027 "classDiagram\nAnimal <|-- Duck\nDuck <|-- Mallard",
1028 ParseOptions::strict(),
1029 )
1030 .unwrap()
1031 .unwrap();
1032 let options = LayoutOptions {
1033 resource_limits: RenderResourceLimits {
1034 max_class_edges: Some(1),
1035 ..RenderResourceLimits::unbounded_for_trusted_input()
1036 },
1037 ..LayoutOptions::default()
1038 };
1039
1040 let err = layout_parsed(&parsed, &options).unwrap_err();
1041
1042 let Error::ResourceLimitExceeded(limit) = err else {
1043 panic!("expected resource limit error");
1044 };
1045 assert_eq!(limit.phase, ResourceLimitPhase::LayoutModel);
1046 assert_eq!(limit.limit, "max_class_edges");
1047 }
1048
1049 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1050 #[test]
1051 fn render_layouted_svg_preserves_flowchart_elk_roledescription() {
1052 let parsed = Engine::new()
1053 .parse_diagram_sync("flowchart-elk TD\nA-->B;", ParseOptions::strict())
1054 .unwrap()
1055 .unwrap();
1056
1057 let layout_options = LayoutOptions::default();
1058 let layouted = layout_parsed(&parsed, &layout_options).unwrap();
1059 let svg = crate::svg::render_layouted_svg(
1060 &layouted,
1061 layout_options.text_measurer.as_ref(),
1062 &crate::svg::SvgRenderOptions {
1063 diagram_id: Some("elk-smoke".to_string()),
1064 ..Default::default()
1065 },
1066 )
1067 .unwrap();
1068
1069 assert!(svg.contains(r#"aria-roledescription="flowchart-elk""#));
1070 assert!(svg.contains("elk-smoke_flowchart-elk-pointEnd"));
1071 assert!(!svg.contains(r#"aria-roledescription="flowchart-v2""#));
1072 assert!(!svg.contains(r#"<g class="root""#));
1073
1074 let marker_pos = svg
1075 .find(r#"<g><marker id="elk-smoke_flowchart-elk-pointEnd""#)
1076 .expect("ELK marker group");
1077 let defs_pos = svg
1078 .find(r#"<defs><filter id="elk-smoke-drop-shadow""#)
1079 .expect("ELK shadow defs");
1080 let subgraphs_pos = svg
1081 .find(r#"<g class="subgraphs"/>"#)
1082 .expect("ELK subgraphs group");
1083 let nodes_pos = svg.find(r#"<g class="nodes">"#).expect("ELK nodes group");
1084 let edges_pos = svg
1085 .find(r#"<g class="edges edgePaths">"#)
1086 .expect("ELK edge paths group");
1087 let labels_pos = svg
1088 .find(r#"<g class="edgeLabels">"#)
1089 .expect("ELK edge labels group");
1090
1091 assert!(marker_pos < defs_pos);
1092 assert!(defs_pos < subgraphs_pos);
1093 assert!(subgraphs_pos < nodes_pos);
1094 assert!(nodes_pos < edges_pos);
1095 assert!(edges_pos < labels_pos);
1096 }
1097
1098 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1099 #[test]
1100 fn render_layouted_svg_uses_elk_adapter_dom_for_flowchart_layout_elk() {
1101 let parsed = Engine::new()
1102 .parse_diagram_sync(
1103 r#"---
1104config:
1105 layout: elk
1106---
1107flowchart LR
1108A{A} --> B & C
1109"#,
1110 ParseOptions::strict(),
1111 )
1112 .unwrap()
1113 .unwrap();
1114
1115 let layout_options = LayoutOptions {
1116 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
1117 flowchart_elk_backend: FlowchartElkBackend::SourcePorted,
1118 ..Default::default()
1119 };
1120 let layouted = layout_parsed(&parsed, &layout_options).unwrap();
1121 let svg = crate::svg::render_layouted_svg(
1122 &layouted,
1123 layout_options.text_measurer.as_ref(),
1124 &crate::svg::SvgRenderOptions {
1125 diagram_id: Some("elk-layout-smoke".to_string()),
1126 ..Default::default()
1127 },
1128 )
1129 .unwrap();
1130
1131 assert!(svg.contains(r#"aria-roledescription="flowchart-v2""#));
1132 assert!(svg.contains("elk-layout-smoke_flowchart-v2-pointEnd"));
1133 assert!(!svg.contains(r#"<g class="root""#));
1134
1135 let marker_pos = svg
1136 .find(r#"<g><marker id="elk-layout-smoke_flowchart-v2-pointEnd""#)
1137 .expect("ELK marker group");
1138 let defs_pos = svg
1139 .find(r#"<defs><filter id="elk-layout-smoke-drop-shadow""#)
1140 .expect("ELK shadow defs");
1141 let subgraphs_pos = svg
1142 .find(r#"<g class="subgraphs"/>"#)
1143 .expect("ELK subgraphs group");
1144 let nodes_pos = svg.find(r#"<g class="nodes">"#).expect("ELK nodes group");
1145 let edges_pos = svg
1146 .find(r#"<g class="edges edgePaths">"#)
1147 .expect("ELK edge paths group");
1148 let labels_pos = svg
1149 .find(r#"<g class="edgeLabels">"#)
1150 .expect("ELK edge labels group");
1151
1152 assert!(marker_pos < defs_pos);
1153 assert!(defs_pos < subgraphs_pos);
1154 assert!(subgraphs_pos < nodes_pos);
1155 assert!(nodes_pos < edges_pos);
1156 assert!(edges_pos < labels_pos);
1157 }
1158
1159 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1160 #[test]
1161 fn render_layouted_svg_uses_right_angle_edges_for_flowchart_elk() {
1162 let parsed = Engine::new()
1163 .parse_diagram_sync("flowchart-elk LR\nA --> B\nA --> C", ParseOptions::strict())
1164 .unwrap()
1165 .unwrap();
1166
1167 let layout_options = LayoutOptions {
1168 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
1169 ..Default::default()
1170 };
1171 let layouted = layout_parsed(&parsed, &layout_options).unwrap();
1172 let svg = crate::svg::render_layouted_svg(
1173 &layouted,
1174 layout_options.text_measurer.as_ref(),
1175 &crate::svg::SvgRenderOptions::default(),
1176 )
1177 .unwrap();
1178
1179 let path = edge_path_chunk(&svg, "L_A_B_0");
1180 let d = edge_path_d(path);
1181 assert!(
1182 d.contains('L') && !d.contains('C'),
1183 "expected ELK edges to use right-angle paths without smooth curves by default: {d}"
1184 );
1185 }
1186
1187 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1188 #[test]
1189 fn render_layouted_svg_keeps_source_ported_elk_rect_edge_boundary_points() {
1190 let parsed = Engine::new()
1191 .parse_diagram_sync(
1192 r#"---
1193config:
1194 htmlLabels: true
1195 flowchart:
1196 htmlLabels: true
1197 securityLevel: loose
1198---
1199flowchart-elk LR
1200id1(Start)-->id2(Stop)
1201"#,
1202 ParseOptions::strict(),
1203 )
1204 .unwrap()
1205 .unwrap();
1206
1207 let layout_options = LayoutOptions {
1208 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
1209 flowchart_elk_backend: FlowchartElkBackend::SourcePorted,
1210 ..Default::default()
1211 };
1212 let layouted = layout_parsed(&parsed, &layout_options).unwrap();
1213 let svg = crate::svg::render_layouted_svg(
1214 &layouted,
1215 layout_options.text_measurer.as_ref(),
1216 &crate::svg::SvgRenderOptions::default(),
1217 )
1218 .unwrap();
1219
1220 let path = edge_path_chunk(&svg, "L_id1_id2_0");
1221 let d = edge_path_d(path);
1222 assert!(
1223 !d.contains('Q'),
1224 "straight ELK roundedRect edge should not gain a rounded corner: {d}"
1225 );
1226 let points = edge_data_points(path);
1227 assert_eq!(
1228 points.len(),
1229 2,
1230 "unexpected ELK edge data-points: {points:?}"
1231 );
1232 assert_eq!(points[0], (77.015625, 39.0));
1233 assert_eq!(points[1], (117.015625, 39.0));
1234 }
1235
1236 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1237 #[test]
1238 fn render_layouted_svg_keeps_source_ported_elk_self_loop_edges() {
1239 let parsed = Engine::new()
1240 .parse_diagram_sync("flowchart-elk TD\nA --> A", ParseOptions::strict())
1241 .unwrap()
1242 .unwrap();
1243
1244 let layout_options = LayoutOptions {
1245 text_measurer: Arc::new(crate::text::VendoredFontMetricsTextMeasurer::default()),
1246 flowchart_elk_backend: FlowchartElkBackend::SourcePorted,
1247 ..Default::default()
1248 };
1249 let layouted = layout_parsed(&parsed, &layout_options).unwrap();
1250 let svg = crate::svg::render_layouted_svg(
1251 &layouted,
1252 layout_options.text_measurer.as_ref(),
1253 &crate::svg::SvgRenderOptions::default(),
1254 )
1255 .unwrap();
1256
1257 let path = edge_path_chunk(&svg, "L_A_A_0");
1258 let d = edge_path_d(path);
1259 assert!(
1260 d.contains('Q'),
1261 "ELK self-loop path should be rendered from the source-backed edge: {d}"
1262 );
1263 let points = edge_data_points(path);
1264 assert_eq!(
1265 points.len(),
1266 4,
1267 "unexpected ELK self-loop data-points: {points:?}"
1268 );
1269 assert!(
1270 !svg.contains("A---A---1") && !svg.contains("cyclic-special"),
1271 "ELK renderer must not reuse Dagre self-loop helper nodes: {svg}"
1272 );
1273 assert!(svg.contains(r#"data-id="L_A_A_0" transform="translate(0,0)""#));
1274 }
1275
1276 #[cfg(all(feature = "core-full", not(feature = "elk-layout")))]
1277 #[test]
1278 fn render_model_dispatch_rejects_flowchart_elk_without_feature() {
1279 let parsed = Engine::new()
1280 .parse_diagram_for_render_model_with_type_sync(
1281 "flowchart-elk",
1282 "flowchart-elk TD\nA-->B;",
1283 ParseOptions::strict(),
1284 )
1285 .unwrap()
1286 .unwrap();
1287
1288 let err = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap_err();
1289 assert!(matches!(
1290 err,
1291 Error::UnsupportedDiagram { diagram_type } if diagram_type == "flowchart-elk"
1292 ));
1293 }
1294
1295 #[cfg(all(feature = "core-full", not(feature = "elk-layout")))]
1296 #[test]
1297 fn render_model_dispatch_rejects_class_elk_without_feature() {
1298 let parsed = Engine::new()
1299 .parse_diagram_for_render_model_sync(
1300 r#"---
1301config:
1302 layout: elk
1303---
1304classDiagram
1305Animal <|-- Duck
1306"#,
1307 ParseOptions::strict(),
1308 )
1309 .unwrap()
1310 .unwrap();
1311
1312 let err = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap_err();
1313 assert!(matches!(
1314 err,
1315 Error::UnsupportedDiagram { diagram_type } if diagram_type == "classDiagram"
1316 ));
1317 }
1318
1319 #[test]
1320 fn render_model_dispatch_rejects_mismatched_typed_model() {
1321 let mut parsed = Engine::new()
1322 .parse_diagram_for_render_model_sync(
1323 "sequenceDiagram\nAlice->>Bob: Hi",
1324 ParseOptions::strict(),
1325 )
1326 .unwrap()
1327 .unwrap();
1328 parsed.meta.diagram_type = "flowchart-v2".to_string();
1329
1330 let err = layout_parsed_render_layout_only(&parsed, &LayoutOptions::default()).unwrap_err();
1331 let message = err.to_string();
1332 assert!(message.contains("sequence"));
1333 assert!(message.contains("flowchart-v2"));
1334 }
1335
1336 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1337 fn edge_path_chunk<'a>(svg: &'a str, edge_id: &str) -> &'a str {
1338 let id_attr = format!(r#"id="merman-{edge_id}""#);
1339 let id_start = svg.find(&id_attr).expect("edge id");
1340 let path_start = svg[..id_start].rfind("<path ").expect("edge path start");
1341 let path_end = svg[id_start..].find("/>").expect("edge path end") + id_start;
1342 &svg[path_start..path_end]
1343 }
1344
1345 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1346 fn edge_path_d(path: &str) -> &str {
1347 let d_start = path.find(r#"d=""#).expect("edge path d") + r#"d=""#.len();
1348 let d_end = path[d_start..].find('"').expect("edge path d end") + d_start;
1349 &path[d_start..d_end]
1350 }
1351
1352 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1353 fn edge_attr_value<'a>(path: &'a str, attr: &str) -> &'a str {
1354 let needle = format!(r#"{attr}=""#);
1355 let start = path.find(&needle).expect("edge attr") + needle.len();
1356 let end = path[start..].find('"').expect("edge attr end") + start;
1357 &path[start..end]
1358 }
1359
1360 #[cfg(all(feature = "core-full", feature = "elk-layout"))]
1361 fn edge_data_points(path: &str) -> Vec<(f64, f64)> {
1362 use base64::Engine as _;
1363
1364 let b64 = edge_attr_value(path, "data-points");
1365 let bytes = base64::engine::general_purpose::STANDARD
1366 .decode(b64.as_bytes())
1367 .expect("data-points base64");
1368 let json: serde_json::Value =
1369 serde_json::from_slice(&bytes).expect("data-points JSON payload");
1370 json.as_array()
1371 .expect("data-points array")
1372 .iter()
1373 .map(|point| {
1374 (
1375 point.get("x").and_then(serde_json::Value::as_f64).unwrap(),
1376 point.get("y").and_then(serde_json::Value::as_f64).unwrap(),
1377 )
1378 })
1379 .collect()
1380 }
1381}