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