1#![forbid(unsafe_code)]
2#![allow(clippy::empty_line_after_outer_attr)]
5
6pub mod common;
14pub mod common_db;
15pub mod config;
16pub mod detect;
17pub mod diagram;
18pub mod diagrams;
19pub mod entities;
20pub mod error;
21pub mod generated;
22pub mod geom;
23pub mod models;
24pub mod preprocess;
25mod runtime;
26pub mod sanitize;
27mod theme;
28pub mod time;
29pub mod utils;
30
31pub use config::MermaidConfig;
32pub use detect::{Detector, DetectorRegistry};
33pub use diagram::{
34 DiagramRegistry, DiagramSemanticParser, ParsedDiagram, ParsedDiagramRender, RenderSemanticModel,
35};
36pub use error::{Error, Result};
37pub use preprocess::{PreprocessResult, preprocess_diagram, preprocess_diagram_with_known_type};
38
39#[derive(Debug, Clone, Copy, Default)]
40pub struct ParseOptions {
41 pub suppress_errors: bool,
42}
43
44impl ParseOptions {
45 pub fn strict() -> Self {
47 Self {
48 suppress_errors: false,
49 }
50 }
51
52 pub fn lenient() -> Self {
54 Self {
55 suppress_errors: true,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
61pub struct ParseMetadata {
62 pub diagram_type: String,
63 pub config: MermaidConfig,
66 pub effective_config: MermaidConfig,
68 pub title: Option<String>,
69}
70
71#[derive(Debug, Clone)]
72pub struct Engine {
73 registry: DetectorRegistry,
74 diagram_registry: DiagramRegistry,
75 site_config: MermaidConfig,
76 fixed_today_local: Option<chrono::NaiveDate>,
77 fixed_local_offset_minutes: Option<i32>,
78}
79
80impl Default for Engine {
81 fn default() -> Self {
82 let site_config = generated::default_site_config();
83
84 Self {
85 registry: DetectorRegistry::default_mermaid_11_12_2(),
86 diagram_registry: DiagramRegistry::default_mermaid_11_12_2(),
87 site_config,
88 fixed_today_local: None,
89 fixed_local_offset_minutes: None,
90 }
91 }
92}
93
94impl Engine {
95 fn parse_timing_enabled() -> bool {
96 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
97 *ENABLED.get_or_init(|| match std::env::var("MERMAN_PARSE_TIMING").as_deref() {
98 Ok("1") | Ok("true") => true,
99 _ => false,
100 })
101 }
102
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn with_fixed_today(mut self, today: Option<chrono::NaiveDate>) -> Self {
112 self.fixed_today_local = today;
113 self
114 }
115
116 pub fn with_fixed_local_offset_minutes(mut self, offset_minutes: Option<i32>) -> Self {
122 self.fixed_local_offset_minutes = offset_minutes;
123 self
124 }
125
126 pub fn with_site_config(mut self, site_config: MermaidConfig) -> Self {
127 self.site_config.deep_merge(site_config.as_value());
129 self
130 }
131
132 pub fn registry(&self) -> &DetectorRegistry {
133 &self.registry
134 }
135
136 pub fn registry_mut(&mut self) -> &mut DetectorRegistry {
137 &mut self.registry
138 }
139
140 pub fn diagram_registry(&self) -> &DiagramRegistry {
141 &self.diagram_registry
142 }
143
144 pub fn diagram_registry_mut(&mut self) -> &mut DiagramRegistry {
145 &mut self.diagram_registry
146 }
147
148 pub fn parse_metadata_sync(
154 &self,
155 text: &str,
156 options: ParseOptions,
157 ) -> Result<Option<ParseMetadata>> {
158 let Some((_, meta)) = self.preprocess_and_detect(text, options)? else {
159 return Ok(None);
160 };
161 Ok(Some(meta))
162 }
163
164 pub fn parse_metadata_as_sync(
196 &self,
197 diagram_type: &str,
198 text: &str,
199 options: ParseOptions,
200 ) -> Result<Option<ParseMetadata>> {
201 let Some((_, meta)) = self.preprocess_and_assume_type(diagram_type, text, options)? else {
202 return Ok(None);
203 };
204 Ok(Some(meta))
205 }
206
207 pub async fn parse_metadata(
208 &self,
209 text: &str,
210 options: ParseOptions,
211 ) -> Result<Option<ParseMetadata>> {
212 self.parse_metadata_sync(text, options)
213 }
214
215 pub async fn parse_metadata_as(
216 &self,
217 diagram_type: &str,
218 text: &str,
219 options: ParseOptions,
220 ) -> Result<Option<ParseMetadata>> {
221 self.parse_metadata_as_sync(diagram_type, text, options)
222 }
223
224 pub fn parse_diagram_sync(
229 &self,
230 text: &str,
231 options: ParseOptions,
232 ) -> Result<Option<ParsedDiagram>> {
233 let timing_enabled = Self::parse_timing_enabled();
234 let total_start = timing_enabled.then(std::time::Instant::now);
235
236 let preprocess_start = timing_enabled.then(std::time::Instant::now);
237 let Some((code, meta)) = self.preprocess_and_detect(text, options)? else {
238 return Ok(None);
239 };
240 let preprocess = preprocess_start.map(|s| s.elapsed());
241
242 let parse_start = timing_enabled.then(std::time::Instant::now);
243 let parse = crate::runtime::with_fixed_today_local(self.fixed_today_local, || {
244 crate::runtime::with_fixed_local_offset_minutes(self.fixed_local_offset_minutes, || {
245 diagram::parse_or_unsupported(
246 &self.diagram_registry,
247 &meta.diagram_type,
248 &code,
249 &meta,
250 )
251 })
252 });
253
254 let mut model = match parse {
255 Ok(v) => v,
256 Err(err) => {
257 if !options.suppress_errors {
258 return Err(err);
259 }
260
261 let mut error_meta = meta.clone();
262 error_meta.diagram_type = "error".to_string();
263 let mut error_model = serde_json::json!({ "type": "error" });
264 common_db::apply_common_db_sanitization(
265 &mut error_model,
266 &error_meta.effective_config,
267 );
268 if let Some(start) = total_start {
269 let parse = parse_start.map(|s| s.elapsed()).unwrap_or_default();
270 eprintln!(
271 "[parse-timing] diagram=error total={:?} preprocess={:?} parse={:?} sanitize={:?} input_bytes={}",
272 start.elapsed(),
273 preprocess.unwrap_or_default(),
274 parse,
275 std::time::Duration::default(),
276 text.len(),
277 );
278 }
279 return Ok(Some(ParsedDiagram {
280 meta: error_meta,
281 model: error_model,
282 }));
283 }
284 };
285 let parse = parse_start.map(|s| s.elapsed());
286
287 let sanitize_start = timing_enabled.then(std::time::Instant::now);
288 common_db::apply_common_db_sanitization(&mut model, &meta.effective_config);
289 let sanitize = sanitize_start.map(|s| s.elapsed());
290
291 if let Some(start) = total_start {
292 eprintln!(
293 "[parse-timing] diagram={} total={:?} preprocess={:?} parse={:?} sanitize={:?} input_bytes={}",
294 meta.diagram_type,
295 start.elapsed(),
296 preprocess.unwrap_or_default(),
297 parse.unwrap_or_default(),
298 sanitize.unwrap_or_default(),
299 text.len(),
300 );
301 }
302 Ok(Some(ParsedDiagram { meta, model }))
303 }
304
305 pub async fn parse_diagram(
306 &self,
307 text: &str,
308 options: ParseOptions,
309 ) -> Result<Option<ParsedDiagram>> {
310 self.parse_diagram_sync(text, options)
311 }
312
313 pub fn parse_diagram_for_render_sync(
320 &self,
321 text: &str,
322 options: ParseOptions,
323 ) -> Result<Option<ParsedDiagram>> {
324 let Some((code, meta)) = self.preprocess_and_detect(text, options)? else {
325 return Ok(None);
326 };
327
328 let parse_res = match meta.diagram_type.as_str() {
329 "mindmap" => crate::diagrams::mindmap::parse_mindmap_for_render(&code, &meta),
330 "stateDiagram" | "state" => {
331 crate::diagrams::state::parse_state_for_render(&code, &meta)
332 }
333 _ => diagram::parse_or_unsupported(
334 &self.diagram_registry,
335 &meta.diagram_type,
336 &code,
337 &meta,
338 ),
339 };
340
341 let mut model = match parse_res {
342 Ok(v) => v,
343 Err(err) => {
344 if !options.suppress_errors {
345 return Err(err);
346 }
347
348 let mut error_meta = meta.clone();
349 error_meta.diagram_type = "error".to_string();
350 let mut error_model = serde_json::json!({ "type": "error" });
351 common_db::apply_common_db_sanitization(
352 &mut error_model,
353 &error_meta.effective_config,
354 );
355 return Ok(Some(ParsedDiagram {
356 meta: error_meta,
357 model: error_model,
358 }));
359 }
360 };
361
362 common_db::apply_common_db_sanitization(&mut model, &meta.effective_config);
363 Ok(Some(ParsedDiagram { meta, model }))
364 }
365
366 pub async fn parse_diagram_for_render(
367 &self,
368 text: &str,
369 options: ParseOptions,
370 ) -> Result<Option<ParsedDiagram>> {
371 self.parse_diagram_for_render_sync(text, options)
372 }
373
374 pub fn parse_diagram_for_render_model_sync(
384 &self,
385 text: &str,
386 options: ParseOptions,
387 ) -> Result<Option<ParsedDiagramRender>> {
388 let timing_enabled = Self::parse_timing_enabled();
389 let total_start = timing_enabled.then(std::time::Instant::now);
390
391 let preprocess_start = timing_enabled.then(std::time::Instant::now);
392 let Some((code, meta)) = self.preprocess_and_detect(text, options)? else {
393 return Ok(None);
394 };
395 let preprocess = preprocess_start.map(|s| s.elapsed());
396
397 let parse_start = timing_enabled.then(std::time::Instant::now);
398 let parse_res: Result<RenderSemanticModel> = match meta.diagram_type.as_str() {
399 "mindmap" => crate::diagrams::mindmap::parse_mindmap_model_for_render(&code, &meta)
400 .map(RenderSemanticModel::Mindmap),
401 "stateDiagram" | "state" => {
402 crate::diagrams::state::parse_state_model_for_render(&code, &meta)
403 .map(RenderSemanticModel::State)
404 }
405 "flowchart-v2" | "flowchart" | "flowchart-elk" => {
406 crate::diagrams::flowchart::parse_flowchart_model_for_render(&code, &meta)
407 .map(RenderSemanticModel::Flowchart)
408 }
409 "classDiagram" | "class" => crate::diagrams::class::parse_class_typed(&code, &meta)
410 .map(RenderSemanticModel::Class),
411 "architecture" => {
412 crate::diagrams::architecture::parse_architecture_model_for_render(&code, &meta)
413 .map(RenderSemanticModel::Architecture)
414 }
415 _ => diagram::parse_or_unsupported(
416 &self.diagram_registry,
417 &meta.diagram_type,
418 &code,
419 &meta,
420 )
421 .map(RenderSemanticModel::Json),
422 };
423 let parse = parse_start.map(|s| s.elapsed());
424
425 let mut model = match parse_res {
426 Ok(v) => v,
427 Err(err) => {
428 if !options.suppress_errors {
429 return Err(err);
430 }
431
432 let mut error_meta = meta.clone();
433 error_meta.diagram_type = "error".to_string();
434 let mut error_model = serde_json::json!({ "type": "error" });
435 common_db::apply_common_db_sanitization(
436 &mut error_model,
437 &error_meta.effective_config,
438 );
439 if let Some(start) = total_start {
440 eprintln!(
441 "[parse-render-timing] diagram=error model=json total={:?} preprocess={:?} parse={:?} sanitize={:?} input_bytes={}",
442 start.elapsed(),
443 preprocess.unwrap_or_default(),
444 parse.unwrap_or_default(),
445 std::time::Duration::default(),
446 text.len(),
447 );
448 }
449 return Ok(Some(ParsedDiagramRender {
450 meta: error_meta,
451 model: RenderSemanticModel::Json(error_model),
452 }));
453 }
454 };
455
456 let sanitize_start = timing_enabled.then(std::time::Instant::now);
457 match &mut model {
458 RenderSemanticModel::Json(v) => {
459 common_db::apply_common_db_sanitization(v, &meta.effective_config);
460 }
461 RenderSemanticModel::State(v) => {
462 if let Some(s) = v.acc_title.as_deref() {
463 v.acc_title = Some(common_db::sanitize_acc_title(s, &meta.effective_config));
464 }
465 if let Some(s) = v.acc_descr.as_deref() {
466 v.acc_descr = Some(common_db::sanitize_acc_descr(s, &meta.effective_config));
467 }
468 }
469 RenderSemanticModel::Mindmap(_) => {}
470 RenderSemanticModel::Flowchart(_) => {}
471 RenderSemanticModel::Class(v) => {
472 if let Some(s) = v.acc_title.as_deref() {
473 v.acc_title = Some(common_db::sanitize_acc_title(s, &meta.effective_config));
474 }
475 if let Some(s) = v.acc_descr.as_deref() {
476 v.acc_descr = Some(common_db::sanitize_acc_descr(s, &meta.effective_config));
477 }
478 }
479 RenderSemanticModel::Architecture(v) => {
480 if let Some(s) = v.title.as_deref() {
481 v.title = Some(crate::sanitize::sanitize_text(s, &meta.effective_config));
482 }
483 if let Some(s) = v.acc_title.as_deref() {
484 v.acc_title = Some(common_db::sanitize_acc_title(s, &meta.effective_config));
485 }
486 if let Some(s) = v.acc_descr.as_deref() {
487 v.acc_descr = Some(common_db::sanitize_acc_descr(s, &meta.effective_config));
488 }
489 }
490 }
491 let sanitize = sanitize_start.map(|s| s.elapsed());
492
493 if let Some(start) = total_start {
494 let model_kind = match &model {
495 RenderSemanticModel::Json(_) => "json",
496 RenderSemanticModel::State(_) => "state",
497 RenderSemanticModel::Mindmap(_) => "mindmap",
498 RenderSemanticModel::Flowchart(_) => "flowchart",
499 RenderSemanticModel::Architecture(_) => "architecture",
500 RenderSemanticModel::Class(_) => "class",
501 };
502 eprintln!(
503 "[parse-render-timing] diagram={} model={} total={:?} preprocess={:?} parse={:?} sanitize={:?} input_bytes={}",
504 meta.diagram_type,
505 model_kind,
506 start.elapsed(),
507 preprocess.unwrap_or_default(),
508 parse.unwrap_or_default(),
509 sanitize.unwrap_or_default(),
510 text.len(),
511 );
512 }
513
514 Ok(Some(ParsedDiagramRender { meta, model }))
515 }
516
517 pub async fn parse_diagram_for_render_model(
518 &self,
519 text: &str,
520 options: ParseOptions,
521 ) -> Result<Option<ParsedDiagramRender>> {
522 self.parse_diagram_for_render_model_sync(text, options)
523 }
524
525 pub fn parse_diagram_for_render_model_as_sync(
532 &self,
533 diagram_type: &str,
534 text: &str,
535 options: ParseOptions,
536 ) -> Result<Option<ParsedDiagramRender>> {
537 let timing_enabled = Self::parse_timing_enabled();
538 let total_start = timing_enabled.then(std::time::Instant::now);
539
540 let preprocess_start = timing_enabled.then(std::time::Instant::now);
541 let Some((code, meta)) = self.preprocess_and_assume_type(diagram_type, text, options)?
542 else {
543 return Ok(None);
544 };
545 let preprocess = preprocess_start.map(|s| s.elapsed());
546
547 let parse_start = timing_enabled.then(std::time::Instant::now);
548 let parse_res: Result<RenderSemanticModel> = match meta.diagram_type.as_str() {
549 "mindmap" => crate::diagrams::mindmap::parse_mindmap_model_for_render(&code, &meta)
550 .map(RenderSemanticModel::Mindmap),
551 "stateDiagram" | "state" => {
552 crate::diagrams::state::parse_state_model_for_render(&code, &meta)
553 .map(RenderSemanticModel::State)
554 }
555 "flowchart-v2" | "flowchart" | "flowchart-elk" => {
556 crate::diagrams::flowchart::parse_flowchart_model_for_render(&code, &meta)
557 .map(RenderSemanticModel::Flowchart)
558 }
559 "classDiagram" | "class" => crate::diagrams::class::parse_class_typed(&code, &meta)
560 .map(RenderSemanticModel::Class),
561 "architecture" => {
562 crate::diagrams::architecture::parse_architecture_model_for_render(&code, &meta)
563 .map(RenderSemanticModel::Architecture)
564 }
565 _ => diagram::parse_or_unsupported(
566 &self.diagram_registry,
567 &meta.diagram_type,
568 &code,
569 &meta,
570 )
571 .map(RenderSemanticModel::Json),
572 };
573 let parse = parse_start.map(|s| s.elapsed());
574
575 let mut model = match parse_res {
576 Ok(v) => v,
577 Err(err) => {
578 if !options.suppress_errors {
579 return Err(err);
580 }
581
582 let mut error_meta = meta.clone();
583 error_meta.diagram_type = "error".to_string();
584 let mut error_model = serde_json::json!({ "type": "error" });
585 common_db::apply_common_db_sanitization(
586 &mut error_model,
587 &error_meta.effective_config,
588 );
589 if let Some(start) = total_start {
590 eprintln!(
591 "[parse-render-timing] diagram=error model=json total={:?} preprocess={:?} parse={:?} sanitize={:?} input_bytes={}",
592 start.elapsed(),
593 preprocess.unwrap_or_default(),
594 parse.unwrap_or_default(),
595 std::time::Duration::default(),
596 text.len(),
597 );
598 }
599 return Ok(Some(ParsedDiagramRender {
600 meta: error_meta,
601 model: RenderSemanticModel::Json(error_model),
602 }));
603 }
604 };
605
606 let sanitize_start = timing_enabled.then(std::time::Instant::now);
607 match &mut model {
608 RenderSemanticModel::Json(v) => {
609 common_db::apply_common_db_sanitization(v, &meta.effective_config);
610 }
611 RenderSemanticModel::State(v) => {
612 if let Some(s) = v.acc_title.as_deref() {
613 v.acc_title = Some(common_db::sanitize_acc_title(s, &meta.effective_config));
614 }
615 if let Some(s) = v.acc_descr.as_deref() {
616 v.acc_descr = Some(common_db::sanitize_acc_descr(s, &meta.effective_config));
617 }
618 }
619 RenderSemanticModel::Mindmap(_) => {}
620 RenderSemanticModel::Flowchart(_) => {}
621 RenderSemanticModel::Class(v) => {
622 if let Some(s) = v.acc_title.as_deref() {
623 v.acc_title = Some(common_db::sanitize_acc_title(s, &meta.effective_config));
624 }
625 if let Some(s) = v.acc_descr.as_deref() {
626 v.acc_descr = Some(common_db::sanitize_acc_descr(s, &meta.effective_config));
627 }
628 }
629 RenderSemanticModel::Architecture(v) => {
630 if let Some(s) = v.title.as_deref() {
631 v.title = Some(crate::sanitize::sanitize_text(s, &meta.effective_config));
632 }
633 if let Some(s) = v.acc_title.as_deref() {
634 v.acc_title = Some(common_db::sanitize_acc_title(s, &meta.effective_config));
635 }
636 if let Some(s) = v.acc_descr.as_deref() {
637 v.acc_descr = Some(common_db::sanitize_acc_descr(s, &meta.effective_config));
638 }
639 }
640 }
641 let sanitize = sanitize_start.map(|s| s.elapsed());
642
643 if let Some(start) = total_start {
644 let model_kind = match &model {
645 RenderSemanticModel::Json(_) => "json",
646 RenderSemanticModel::State(_) => "state",
647 RenderSemanticModel::Mindmap(_) => "mindmap",
648 RenderSemanticModel::Flowchart(_) => "flowchart",
649 RenderSemanticModel::Architecture(_) => "architecture",
650 RenderSemanticModel::Class(_) => "class",
651 };
652 eprintln!(
653 "[parse-render-timing] diagram={} model={} total={:?} preprocess={:?} parse={:?} sanitize={:?} input_bytes={}",
654 meta.diagram_type,
655 model_kind,
656 start.elapsed(),
657 preprocess.unwrap_or_default(),
658 parse.unwrap_or_default(),
659 sanitize.unwrap_or_default(),
660 text.len(),
661 );
662 }
663
664 Ok(Some(ParsedDiagramRender { meta, model }))
665 }
666
667 pub async fn parse_diagram_for_render_model_as(
668 &self,
669 diagram_type: &str,
670 text: &str,
671 options: ParseOptions,
672 ) -> Result<Option<ParsedDiagramRender>> {
673 self.parse_diagram_for_render_model_as_sync(diagram_type, text, options)
674 }
675
676 pub fn parse_diagram_as_sync(
698 &self,
699 diagram_type: &str,
700 text: &str,
701 options: ParseOptions,
702 ) -> Result<Option<ParsedDiagram>> {
703 let Some((code, meta)) = self.preprocess_and_assume_type(diagram_type, text, options)?
704 else {
705 return Ok(None);
706 };
707
708 let parse = crate::runtime::with_fixed_today_local(self.fixed_today_local, || {
709 crate::runtime::with_fixed_local_offset_minutes(self.fixed_local_offset_minutes, || {
710 diagram::parse_or_unsupported(
711 &self.diagram_registry,
712 &meta.diagram_type,
713 &code,
714 &meta,
715 )
716 })
717 });
718
719 let mut model = match parse {
720 Ok(v) => v,
721 Err(err) => {
722 if !options.suppress_errors {
723 return Err(err);
724 }
725
726 let mut error_meta = meta.clone();
727 error_meta.diagram_type = "error".to_string();
728 let mut error_model = serde_json::json!({ "type": "error" });
729 common_db::apply_common_db_sanitization(
730 &mut error_model,
731 &error_meta.effective_config,
732 );
733 return Ok(Some(ParsedDiagram {
734 meta: error_meta,
735 model: error_model,
736 }));
737 }
738 };
739 common_db::apply_common_db_sanitization(&mut model, &meta.effective_config);
740 Ok(Some(ParsedDiagram { meta, model }))
741 }
742
743 pub async fn parse_diagram_as(
744 &self,
745 diagram_type: &str,
746 text: &str,
747 options: ParseOptions,
748 ) -> Result<Option<ParsedDiagram>> {
749 self.parse_diagram_as_sync(diagram_type, text, options)
750 }
751
752 pub async fn parse(&self, text: &str, options: ParseOptions) -> Result<Option<ParseMetadata>> {
753 self.parse_metadata(text, options).await
754 }
755
756 fn preprocess_and_detect(
757 &self,
758 text: &str,
759 options: ParseOptions,
760 ) -> Result<Option<(String, ParseMetadata)>> {
761 let pre = preprocess_diagram(text, &self.registry)?;
762 if pre.code.trim_start().starts_with("---") {
763 return Err(Error::MalformedFrontMatter);
764 }
765
766 let mut effective_config = self.site_config.clone();
767 effective_config.deep_merge(pre.config.as_value());
768
769 let diagram_type = match self
770 .registry
771 .detect_type_precleaned(&pre.code, &mut effective_config)
772 {
773 Ok(t) => t.to_string(),
774 Err(err) => {
775 if options.suppress_errors {
776 return Ok(None);
777 }
778 return Err(err);
779 }
780 };
781 theme::apply_theme_defaults(&mut effective_config);
782
783 let title = pre
784 .title
785 .as_ref()
786 .map(|t| crate::sanitize::sanitize_text(t, &effective_config))
787 .filter(|t| !t.is_empty());
788
789 Ok(Some((
790 pre.code,
791 ParseMetadata {
792 diagram_type,
793 config: pre.config,
794 effective_config,
795 title,
796 },
797 )))
798 }
799
800 fn preprocess_and_assume_type(
801 &self,
802 diagram_type: &str,
803 text: &str,
804 _options: ParseOptions,
805 ) -> Result<Option<(String, ParseMetadata)>> {
806 let pre = preprocess_diagram_with_known_type(text, &self.registry, Some(diagram_type))?;
807 if pre.code.trim_start().starts_with("---") {
808 return Err(Error::MalformedFrontMatter);
809 }
810
811 let mut effective_config = self.site_config.clone();
812 effective_config.deep_merge(pre.config.as_value());
813 apply_detector_side_effects_for_known_type(diagram_type, &mut effective_config);
814 theme::apply_theme_defaults(&mut effective_config);
815
816 let title = pre
817 .title
818 .as_ref()
819 .map(|t| crate::sanitize::sanitize_text(t, &effective_config))
820 .filter(|t| !t.is_empty());
821
822 Ok(Some((
823 pre.code,
824 ParseMetadata {
825 diagram_type: diagram_type.to_string(),
826 config: pre.config,
827 effective_config,
828 title,
829 },
830 )))
831 }
832}
833
834fn apply_detector_side_effects_for_known_type(
835 diagram_type: &str,
836 effective_config: &mut MermaidConfig,
837) {
838 if diagram_type == "flowchart-elk" {
842 effective_config.set_value("layout", serde_json::Value::String("elk".to_string()));
843 return;
844 }
845
846 if matches!(diagram_type, "flowchart-v2" | "flowchart")
847 && effective_config.get_str("flowchart.defaultRenderer") == Some("elk")
848 {
849 effective_config.set_value("layout", serde_json::Value::String("elk".to_string()));
850 }
851}
852
853#[cfg(test)]
854mod tests;