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