1#![deny(missing_docs)]
18pub(crate) mod diagrams;
19pub mod error;
24pub(crate) mod error_svg;
25pub(crate) mod icons;
26pub(crate) mod style;
27pub(crate) mod svg;
28pub(crate) mod text;
29pub(crate) mod text_browser_metrics;
30pub mod theme;
36
37pub use error::{ParseError, ParseResult, RenderError};
38
39pub struct RenderOptions {
57 pub theme: theme::Theme,
59 pub font_family: Option<String>,
65 pub font_size: Option<f64>,
71 pub max_width: Option<f64>,
77 pub background: Option<String>,
83}
84
85impl Default for RenderOptions {
86 fn default() -> Self {
87 Self {
88 theme: theme::Theme::Default,
89 font_family: None,
90 font_size: None,
91 max_width: None,
92 background: None,
93 }
94 }
95}
96
97use std::any::Any;
98use std::panic::{self, AssertUnwindSafe};
99
100#[derive(Debug, Clone, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum DiagramType {
110 Flowchart,
112 Pie,
114 Sequence,
116 Er,
118 Gantt,
120 Info,
122 State,
124 Class,
126 Git,
128 Mindmap,
130 Timeline,
132 Quadrant,
134 XyChart,
136 C4,
139 Block,
141 Packet,
143 Journey,
145 Requirement,
147 Kanban,
149 Sankey,
152 Treemap,
154 Radar,
156 Venn,
158 Architecture,
160 EventModeling,
162 Ishikawa,
164 Wardley,
166 TreeView,
168 Unknown,
170}
171
172impl DiagramType {
173 fn label(&self) -> &'static str {
176 match self {
177 DiagramType::Flowchart => "flowchart",
178 DiagramType::Pie => "pie",
179 DiagramType::Sequence => "sequenceDiagram",
180 DiagramType::Er => "erDiagram",
181 DiagramType::Gantt => "gantt",
182 DiagramType::Info => "info",
183 DiagramType::State => "stateDiagram",
184 DiagramType::Class => "classDiagram",
185 DiagramType::Git => "gitGraph",
186 DiagramType::Mindmap => "mindmap",
187 DiagramType::Timeline => "timeline",
188 DiagramType::Quadrant => "quadrantChart",
189 DiagramType::XyChart => "xychart-beta",
190 DiagramType::C4 => "C4",
191 DiagramType::Block => "block-beta",
192 DiagramType::Packet => "packet-beta",
193 DiagramType::Journey => "journey",
194 DiagramType::Requirement => "requirementDiagram",
195 DiagramType::Kanban => "kanban",
196 DiagramType::Sankey => "sankey-beta",
197 DiagramType::Treemap => "treemap",
198 DiagramType::Radar => "radar",
199 DiagramType::Venn => "venn",
200 DiagramType::Architecture => "architecture",
201 DiagramType::EventModeling => "eventmodeling",
202 DiagramType::Ishikawa => "ishikawa",
203 DiagramType::Wardley => "wardley",
204 DiagramType::TreeView => "treeView-beta",
205 DiagramType::Unknown => "unknown",
206 }
207 }
208}
209
210fn strip_frontmatter(input: &str) -> &str {
212 let trimmed = input.trim_start();
213 if !trimmed.starts_with("---") {
214 return input;
215 }
216 let after_open = &trimmed[3..];
217 if let Some(nl) = after_open.find('\n') {
218 let body_start = &after_open[nl + 1..];
219 if let Some(close_pos) = body_start.find("\n---") {
220 let remainder = &body_start[close_pos + 4..];
221 if let Some(nl2) = remainder.find('\n') {
222 return &remainder[nl2 + 1..];
223 }
224 return remainder;
225 }
226 }
227 input
228}
229
230fn unwind_message(e: Box<dyn Any + Send>) -> String {
232 if let Some(s) = e.downcast_ref::<&str>() {
233 return s.to_string();
234 }
235 if let Some(s) = e.downcast_ref::<String>() {
236 return s.clone();
237 }
238 "Rendering failed".to_string()
239}
240
241pub fn detect(input: &str) -> DiagramType {
260 let stripped = strip_frontmatter(input.trim_start());
262 let trimmed = stripped.trim_start();
263
264 if trimmed.starts_with("flowchart") || trimmed.starts_with("graph ") {
265 return DiagramType::Flowchart;
266 }
267 if trimmed.starts_with("pie") {
268 return DiagramType::Pie;
269 }
270 if trimmed.starts_with("sequenceDiagram") {
271 return DiagramType::Sequence;
272 }
273 if trimmed.starts_with("erDiagram") {
274 return DiagramType::Er;
275 }
276 if trimmed.starts_with("gantt") {
277 return DiagramType::Gantt;
278 }
279 if trimmed.starts_with("info") {
280 return DiagramType::Info;
281 }
282 if trimmed.starts_with("stateDiagram-v2") || trimmed.starts_with("stateDiagram") {
283 return DiagramType::State;
284 }
285 if trimmed.starts_with("classDiagram") {
286 return DiagramType::Class;
287 }
288 if trimmed.starts_with("gitGraph") {
289 return DiagramType::Git;
290 }
291 if trimmed.starts_with("mindmap") {
292 return DiagramType::Mindmap;
293 }
294 if trimmed.starts_with("timeline") {
295 return DiagramType::Timeline;
296 }
297 if trimmed.starts_with("quadrantChart") {
298 return DiagramType::Quadrant;
299 }
300 if trimmed.starts_with("xychart") {
301 return DiagramType::XyChart;
302 }
303 if trimmed.starts_with("C4Context")
304 || trimmed.starts_with("C4Container")
305 || trimmed.starts_with("C4Component")
306 || trimmed.starts_with("C4Dynamic")
307 || trimmed.starts_with("C4Deployment")
308 {
309 return DiagramType::C4;
310 }
311 if trimmed.starts_with("block") {
312 return DiagramType::Block;
313 }
314 if trimmed.starts_with("packet-beta") || trimmed.starts_with("packet") {
315 return DiagramType::Packet;
316 }
317 if trimmed.starts_with("journey") {
318 return DiagramType::Journey;
319 }
320 if trimmed.starts_with("requirementDiagram") || trimmed.starts_with("requirement") {
321 return DiagramType::Requirement;
322 }
323 if trimmed.starts_with("kanban") {
324 return DiagramType::Kanban;
325 }
326 if trimmed.starts_with("sankey")
327 || strip_frontmatter(trimmed)
328 .trim_start()
329 .starts_with("sankey")
330 {
331 return DiagramType::Sankey;
332 }
333 if trimmed.starts_with("treemap-beta") || trimmed.starts_with("treemap") {
334 return DiagramType::Treemap;
335 }
336 if trimmed.starts_with("radar-beta") || trimmed.starts_with("radar") {
337 return DiagramType::Radar;
338 }
339 if trimmed.starts_with("venn-beta")
340 || trimmed.starts_with("vennDiagram")
341 || trimmed.starts_with("venn")
342 {
343 return DiagramType::Venn;
344 }
345 if trimmed.starts_with("architecture-beta") || trimmed.starts_with("architecture") {
346 return DiagramType::Architecture;
347 }
348 if trimmed.starts_with("eventmodeling") || trimmed.starts_with("event-modeling") {
349 return DiagramType::EventModeling;
350 }
351 if trimmed.starts_with("fishbone") || trimmed.starts_with("ishikawa") {
352 return DiagramType::Ishikawa;
353 }
354 if trimmed.starts_with("wardley") {
355 return DiagramType::Wardley;
356 }
357 if trimmed.starts_with("treeView-beta")
358 || trimmed.starts_with("treeview-beta")
359 || trimmed.starts_with("treeView")
360 {
361 return DiagramType::TreeView;
362 }
363
364 DiagramType::Unknown
365}
366
367pub fn render(input: &str, theme: theme::Theme) -> String {
372 macro_rules! safe_render {
373 ($diagram_type:expr, $call:expr) => {{
374 let result = panic::catch_unwind(AssertUnwindSafe(|| $call));
375 match result {
376 Ok(svg) => svg::normalize_floats(&svg),
377 Err(e) => {
378 let msg = unwind_message(e);
379 error_svg::render_error_svg($diagram_type, &msg)
380 }
381 }
382 }};
383 }
384
385 let dt = detect(input.trim_start());
386 let label = dt.label();
387 match dt {
388 DiagramType::Flowchart => {
389 safe_render!(label, diagrams::flowchart::render_html(input, theme))
390 }
391 DiagramType::Pie => safe_render!(label, diagrams::pie::render_html(input, theme)),
392 DiagramType::Sequence => safe_render!(label, diagrams::sequence::render_html(input, theme)),
393 DiagramType::Er => safe_render!(label, {
394 let d = diagrams::er::parser::parse(input);
395 diagrams::er::render(&d.diagram, theme)
396 }),
397 DiagramType::Gantt => safe_render!(label, diagrams::gantt::render_html(input, theme)),
398 DiagramType::Info => safe_render!(label, {
399 let d = diagrams::info::parser::parse(input);
400 diagrams::info::render(&d, theme)
401 }),
402 DiagramType::State => safe_render!(label, {
403 let d = diagrams::state::parser::parse(input);
404 diagrams::state::render(&d, theme, true)
405 }),
406 DiagramType::Class => {
407 safe_render!(label, diagrams::class_diagram::render_html(input, theme))
408 }
409 DiagramType::Git => safe_render!(label, diagrams::git::render_html(input, theme)),
410 DiagramType::Mindmap => safe_render!(label, diagrams::mindmap::render_html(input, theme)),
411 DiagramType::Timeline => safe_render!(label, diagrams::timeline::render_html(input, theme)),
412 DiagramType::Quadrant => safe_render!(label, diagrams::quadrant::render_html(input, theme)),
413 DiagramType::XyChart => safe_render!(label, diagrams::xychart::render_html(input, theme)),
414 DiagramType::C4 => safe_render!(label, diagrams::c4::render_html(input, theme)),
415 DiagramType::Block => safe_render!(label, diagrams::block::render_html(input, theme)),
416 DiagramType::Packet => safe_render!(label, diagrams::packet::render_html(input, theme)),
417 DiagramType::Journey => safe_render!(label, diagrams::journey::render_html(input, theme)),
418 DiagramType::Requirement => {
419 safe_render!(label, diagrams::requirement::render_html(input, theme))
420 }
421 DiagramType::Kanban => safe_render!(label, diagrams::kanban::render_html(input, theme)),
422 DiagramType::Sankey => safe_render!(label, diagrams::sankey::render_html(input, theme)),
423 DiagramType::Treemap => safe_render!(label, diagrams::treemap::render_html(input, theme)),
424 DiagramType::Radar => safe_render!(label, diagrams::radar::render_html(input, theme)),
425 DiagramType::Venn => safe_render!(label, diagrams::venn::render_html(input, theme)),
426 DiagramType::Architecture => {
427 safe_render!(label, diagrams::architecture::render_html(input, theme))
428 }
429 DiagramType::EventModeling => {
430 safe_render!(label, diagrams::eventmodeling::render_html(input, theme))
431 }
432 DiagramType::Ishikawa => safe_render!(label, diagrams::ishikawa::render_html(input, theme)),
433 DiagramType::Wardley => safe_render!(label, diagrams::wardley::render_html(input, theme)),
434 DiagramType::TreeView => {
435 safe_render!(label, diagrams::treeview::render_html(input, theme))
436 }
437 DiagramType::Unknown => error_svg::render_error_svg(label, "Unrecognized diagram type."),
438 }
439}
440
441pub fn render_svg(input: &str, theme: theme::Theme) -> String {
443 render(input, theme)
444}
445
446pub fn try_render(input: &str, theme: theme::Theme) -> Result<String, RenderError> {
469 macro_rules! safe_try_render {
470 ($diagram_type:expr, $call:expr) => {{
471 let result = panic::catch_unwind(AssertUnwindSafe(|| $call));
472 match result {
473 Ok(svg) => Ok(svg::normalize_floats(&svg)),
474 Err(e) => {
475 let msg = unwind_message(e);
476 Err(RenderError::from_panic($diagram_type, msg))
477 }
478 }
479 }};
480 }
481
482 let dt = detect(input.trim_start());
483 let label = dt.label();
484 match dt {
485 DiagramType::Flowchart => {
486 safe_try_render!(label, diagrams::flowchart::render_html(input, theme))
487 }
488 DiagramType::Pie => safe_try_render!(label, diagrams::pie::render_html(input, theme)),
489 DiagramType::Sequence => {
490 safe_try_render!(label, diagrams::sequence::render_html(input, theme))
491 }
492 DiagramType::Er => safe_try_render!(label, {
493 let d = diagrams::er::parser::parse(input);
494 diagrams::er::render(&d.diagram, theme)
495 }),
496 DiagramType::Gantt => safe_try_render!(label, diagrams::gantt::render_html(input, theme)),
497 DiagramType::Info => safe_try_render!(label, {
498 let d = diagrams::info::parser::parse(input);
499 diagrams::info::render(&d, theme)
500 }),
501 DiagramType::State => safe_try_render!(label, {
502 let d = diagrams::state::parser::parse(input);
503 diagrams::state::render(&d, theme, true)
504 }),
505 DiagramType::Class => {
506 safe_try_render!(label, diagrams::class_diagram::render_html(input, theme))
507 }
508 DiagramType::Git => safe_try_render!(label, diagrams::git::render_html(input, theme)),
509 DiagramType::Mindmap => {
510 safe_try_render!(label, diagrams::mindmap::render_html(input, theme))
511 }
512 DiagramType::Timeline => {
513 safe_try_render!(label, diagrams::timeline::render_html(input, theme))
514 }
515 DiagramType::Quadrant => {
516 safe_try_render!(label, diagrams::quadrant::render_html(input, theme))
517 }
518 DiagramType::XyChart => {
519 safe_try_render!(label, diagrams::xychart::render_html(input, theme))
520 }
521 DiagramType::C4 => safe_try_render!(label, diagrams::c4::render_html(input, theme)),
522 DiagramType::Block => safe_try_render!(label, diagrams::block::render_html(input, theme)),
523 DiagramType::Packet => safe_try_render!(label, diagrams::packet::render_html(input, theme)),
524 DiagramType::Journey => {
525 safe_try_render!(label, diagrams::journey::render_html(input, theme))
526 }
527 DiagramType::Requirement => {
528 safe_try_render!(label, diagrams::requirement::render_html(input, theme))
529 }
530 DiagramType::Kanban => safe_try_render!(label, diagrams::kanban::render_html(input, theme)),
531 DiagramType::Sankey => safe_try_render!(label, diagrams::sankey::render_html(input, theme)),
532 DiagramType::Treemap => {
533 safe_try_render!(label, diagrams::treemap::render_html(input, theme))
534 }
535 DiagramType::Radar => safe_try_render!(label, diagrams::radar::render_html(input, theme)),
536 DiagramType::Venn => safe_try_render!(label, diagrams::venn::render_html(input, theme)),
537 DiagramType::Architecture => {
538 safe_try_render!(label, diagrams::architecture::render_html(input, theme))
539 }
540 DiagramType::EventModeling => {
541 safe_try_render!(label, diagrams::eventmodeling::render_html(input, theme))
542 }
543 DiagramType::Ishikawa => {
544 safe_try_render!(label, diagrams::ishikawa::render_html(input, theme))
545 }
546 DiagramType::Wardley => {
547 safe_try_render!(label, diagrams::wardley::render_html(input, theme))
548 }
549 DiagramType::TreeView => {
550 safe_try_render!(label, diagrams::treeview::render_html(input, theme))
551 }
552 DiagramType::Unknown => Err(RenderError::unknown_type()),
553 }
554}
555
556pub fn render_with_options(input: &str, options: RenderOptions) -> String {
579 render(input, options.theme)
580}
581
582pub fn try_render_with_options(input: &str, options: RenderOptions) -> Result<String, RenderError> {
613 try_render(input, options.theme)
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
623 fn render_flowchart() {
624 let svg = render("flowchart TD\n A --> B", theme::Theme::Default);
625 assert!(svg.contains("<svg"));
626 assert!(!svg.contains("Syntax error"));
627 }
628
629 #[test]
630 fn render_pie() {
631 let svg = render("pie title X\n \"A\" : 1", theme::Theme::Default);
632 assert!(svg.contains("<svg"));
633 }
634
635 #[test]
636 fn render_sequence() {
637 let svg = render(
638 "sequenceDiagram\n Alice->>Bob: Hello",
639 theme::Theme::Default,
640 );
641 assert!(svg.contains("<svg"));
642 }
643
644 #[test]
645 fn render_er() {
646 let svg = render("erDiagram\n A ||--o{ B : has", theme::Theme::Default);
647 assert!(svg.contains("<svg"));
648 }
649
650 #[test]
651 fn render_gantt() {
652 let svg = render(
653 "gantt\n dateFormat YYYY-MM-DD\n section A\n Task1: 2024-01-01, 7d",
654 theme::Theme::Default,
655 );
656 assert!(svg.contains("<svg"));
657 }
658
659 #[test]
660 fn render_state() {
661 let svg = render("stateDiagram-v2\n [*] --> A", theme::Theme::Default);
662 assert!(svg.contains("<svg"));
663 }
664
665 #[test]
666 fn render_class() {
667 let svg = render("classDiagram\n class A", theme::Theme::Default);
668 assert!(svg.contains("<svg"));
669 }
670
671 #[test]
672 fn render_git() {
673 let svg = render("gitGraph\n commit", theme::Theme::Default);
674 assert!(svg.contains("<svg"));
675 }
676
677 #[test]
680 fn render_unknown_type_returns_error_svg() {
681 let svg = render("unknownDiagram\n foo", theme::Theme::Default);
682 assert!(svg.contains("<svg"));
683 }
684
685 #[test]
688 fn try_render_ok() {
689 let result = try_render("pie title X\n \"A\" : 1", theme::Theme::Default);
690 assert!(result.is_ok());
691 assert!(result.unwrap().contains("<svg"));
692 }
693
694 #[test]
695 fn try_render_unknown_returns_err() {
696 let result = try_render("unknownDiagram\n foo", theme::Theme::Default);
697 assert!(result.is_err());
698 }
699
700 #[test]
703 fn detect_flowchart_keyword() {
704 assert_eq!(detect("flowchart TD\n A --> B"), DiagramType::Flowchart);
705 }
706
707 #[test]
708 fn detect_graph_keyword() {
709 assert_eq!(detect("graph LR\n A --> B"), DiagramType::Flowchart);
710 }
711
712 #[test]
713 fn detect_pie() {
714 assert_eq!(detect("pie title X\n \"A\" : 1"), DiagramType::Pie);
715 }
716
717 #[test]
718 fn detect_sequence() {
719 assert_eq!(
720 detect("sequenceDiagram\n A->>B: hi"),
721 DiagramType::Sequence
722 );
723 }
724
725 #[test]
726 fn detect_er() {
727 assert_eq!(detect("erDiagram\n A ||--o{ B : has"), DiagramType::Er);
728 }
729
730 #[test]
731 fn detect_gantt() {
732 assert_eq!(detect("gantt\n dateFormat YYYY-MM-DD"), DiagramType::Gantt);
733 }
734
735 #[test]
736 fn detect_state() {
737 assert_eq!(detect("stateDiagram\n [*] --> A"), DiagramType::State);
738 }
739
740 #[test]
741 fn detect_state_v2() {
742 assert_eq!(detect("stateDiagram-v2\n [*] --> A"), DiagramType::State);
743 }
744
745 #[test]
746 fn detect_class() {
747 assert_eq!(detect("classDiagram\n class A"), DiagramType::Class);
748 }
749
750 #[test]
751 fn detect_git() {
752 assert_eq!(detect("gitGraph\n commit"), DiagramType::Git);
753 }
754
755 #[test]
756 fn detect_mindmap() {
757 assert_eq!(detect("mindmap\n root((A))"), DiagramType::Mindmap);
758 }
759
760 #[test]
761 fn detect_timeline() {
762 assert_eq!(detect("timeline\n title History"), DiagramType::Timeline);
763 }
764
765 #[test]
766 fn detect_quadrant() {
767 assert_eq!(detect("quadrantChart\n title Q"), DiagramType::Quadrant);
768 }
769
770 #[test]
771 fn detect_xychart() {
772 assert_eq!(detect("xychart-beta\n line [1, 2]"), DiagramType::XyChart);
773 }
774
775 #[test]
776 fn detect_c4_context() {
777 assert_eq!(detect("C4Context\n title T"), DiagramType::C4);
778 }
779
780 #[test]
781 fn detect_c4_container() {
782 assert_eq!(detect("C4Container\n title T"), DiagramType::C4);
783 }
784
785 #[test]
786 fn detect_block() {
787 assert_eq!(detect("block-beta\n A"), DiagramType::Block);
788 }
789
790 #[test]
791 fn detect_packet() {
792 assert_eq!(detect("packet-beta\n 0-7: A"), DiagramType::Packet);
793 }
794
795 #[test]
796 fn detect_journey() {
797 assert_eq!(detect("journey\n title My"), DiagramType::Journey);
798 }
799
800 #[test]
801 fn detect_requirement() {
802 assert_eq!(
803 detect("requirementDiagram\n requirement R {}"),
804 DiagramType::Requirement
805 );
806 }
807
808 #[test]
809 fn detect_kanban() {
810 assert_eq!(detect("kanban\n Todo\n task1"), DiagramType::Kanban);
811 }
812
813 #[test]
814 fn detect_sankey() {
815 assert_eq!(detect("sankey-beta\n A,B,10"), DiagramType::Sankey);
816 }
817
818 #[test]
819 fn detect_treemap() {
820 assert_eq!(detect("treemap\n root\n A: 1"), DiagramType::Treemap);
821 }
822
823 #[test]
824 fn detect_treemap_beta() {
825 assert_eq!(
826 detect("treemap-beta\n root\n A: 1"),
827 DiagramType::Treemap
828 );
829 }
830
831 #[test]
832 fn detect_radar() {
833 assert_eq!(detect("radar\n title R"), DiagramType::Radar);
834 }
835
836 #[test]
837 fn detect_radar_beta() {
838 assert_eq!(detect("radar-beta\n title R"), DiagramType::Radar);
839 }
840
841 #[test]
842 fn detect_venn() {
843 assert_eq!(detect("venn\n A"), DiagramType::Venn);
844 }
845
846 #[test]
847 fn detect_venn_beta() {
848 assert_eq!(detect("venn-beta\n A"), DiagramType::Venn);
849 }
850
851 #[test]
852 fn detect_venn_diagram() {
853 assert_eq!(detect("vennDiagram\n A"), DiagramType::Venn);
854 }
855
856 #[test]
857 fn detect_architecture() {
858 assert_eq!(
859 detect("architecture\n service A"),
860 DiagramType::Architecture
861 );
862 }
863
864 #[test]
865 fn detect_architecture_beta() {
866 assert_eq!(
867 detect("architecture-beta\n service A"),
868 DiagramType::Architecture
869 );
870 }
871
872 #[test]
873 fn detect_event_modeling() {
874 assert_eq!(detect("eventmodeling\n A"), DiagramType::EventModeling);
875 }
876
877 #[test]
878 fn detect_event_modeling_hyphen() {
879 assert_eq!(detect("event-modeling\n A"), DiagramType::EventModeling);
880 }
881
882 #[test]
883 fn detect_ishikawa() {
884 assert_eq!(detect("ishikawa\n effect"), DiagramType::Ishikawa);
885 }
886
887 #[test]
888 fn detect_fishbone() {
889 assert_eq!(detect("fishbone\n effect"), DiagramType::Ishikawa);
890 }
891
892 #[test]
893 fn detect_wardley() {
894 assert_eq!(detect("wardley\n title W"), DiagramType::Wardley);
895 }
896
897 #[test]
898 fn detect_treeview_beta() {
899 assert_eq!(detect("treeView-beta\n \"docs\""), DiagramType::TreeView);
900 }
901
902 #[test]
903 fn detect_treeview_lowercase() {
904 assert_eq!(detect("treeview-beta\n \"docs\""), DiagramType::TreeView);
905 }
906
907 #[test]
908 fn render_treeview() {
909 let svg = render(
910 "treeView-beta\n \"docs\"\n \"build\"\n",
911 theme::Theme::Default,
912 );
913 assert!(svg.contains("<svg"));
914 assert!(svg.contains("tree-view"));
915 assert!(svg.contains("docs"));
916 }
917
918 #[test]
919 fn detect_unknown() {
920 assert_eq!(detect("not a diagram"), DiagramType::Unknown);
921 }
922
923 #[test]
926 fn render_with_options_dark_theme() {
927 let opts = RenderOptions {
928 theme: theme::Theme::Dark,
929 ..Default::default()
930 };
931 let svg = render_with_options("pie title X\n \"A\" : 1", opts);
932 assert!(svg.contains("<svg"));
933 }
934
935 #[test]
936 fn render_with_options_forest_theme() {
937 let opts = RenderOptions {
938 theme: theme::Theme::Forest,
939 ..Default::default()
940 };
941 let svg = render_with_options("flowchart TD\n A --> B", opts);
942 assert!(svg.contains("<svg"));
943 }
944
945 #[test]
948 fn try_render_with_options_ok() {
949 let opts = RenderOptions {
950 theme: theme::Theme::Dark,
951 max_width: Some(800.0),
952 ..Default::default()
953 };
954 let result =
955 try_render_with_options("pie\n title Pets\n \"Dogs\" : 40\n \"Cats\" : 60", opts);
956 assert!(result.is_ok());
957 assert!(result.unwrap().contains("<svg"));
958 }
959
960 #[test]
961 fn try_render_with_options_unknown_returns_err() {
962 let opts = RenderOptions::default();
963 let result = try_render_with_options("not a diagram", opts);
964 assert!(result.is_err());
965 }
966
967 #[test]
970 fn render_svg_alias() {
971 let svg = render_svg(
972 "gantt\n dateFormat YYYY-MM-DD\n section A\n Task1: 2024-01-01, 7d",
973 theme::Theme::Default,
974 );
975 assert!(svg.contains("<svg"));
976 }
977
978 #[test]
981 fn all_themes_render() {
982 let input = "flowchart TD\n A --> B";
983 for t in [
984 theme::Theme::Default,
985 theme::Theme::Dark,
986 theme::Theme::Forest,
987 theme::Theme::Neutral,
988 ] {
989 let svg = render(input, t);
990 assert!(svg.contains("<svg"));
991 }
992 }
993
994 #[test]
997 fn detect_ignores_leading_whitespace() {
998 assert_eq!(detect(" flowchart TD\n A --> B"), DiagramType::Flowchart);
999 }
1000
1001 #[test]
1004 fn detect_sankey_with_frontmatter() {
1005 let input = "---\nconfig:\n sankey:\n showValues: false\n---\nsankey-beta\n A,B,10";
1006 assert_eq!(detect(input), DiagramType::Sankey);
1007 }
1008}