1#[cfg(not(target_arch = "wasm32"))]
11use crate::error::HtmlError;
12use crate::{
13 accessibility::add_aria_attributes,
14 extract_front_matter,
15 performance::minify_html_string,
16 seo::{escape_html, generate_structured_data_from_doc},
17 utils::generate_table_of_contents,
18 Result,
19};
20#[cfg(target_arch = "wasm32")]
21use comrak::Options;
22use log::warn;
23#[cfg(not(target_arch = "wasm32"))]
24use mdx_gen::{process_markdown, MarkdownOptions, Options};
25use once_cell::sync::Lazy;
26#[cfg(not(target_arch = "wasm32"))]
27use regex::Regex;
28#[cfg(not(target_arch = "wasm32"))]
29use std::borrow::Cow;
30use std::error::Error;
31use std::fmt;
32
33static BASE_COMRAK_OPTIONS: Lazy<Options<'static>> = Lazy::new(|| {
39 let mut opts = Options::default();
40 opts.extension.strikethrough = true;
41 opts.extension.table = true;
42 opts.extension.autolink = true;
43 opts.extension.tasklist = true;
44 opts.extension.superscript = true;
45 opts
46});
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum DiagnosticLevel {
60 Info,
62 Warning,
64 Error,
66}
67
68#[derive(Debug, Clone)]
84pub struct Diagnostic {
85 pub step: &'static str,
87 pub level: DiagnosticLevel,
89 pub message: String,
91}
92
93impl fmt::Display for Diagnostic {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "[{:?}] {}: {}", self.level, self.step, self.message)
96 }
97}
98
99#[derive(Debug, Clone)]
113pub struct HtmlOutput {
114 pub html: String,
116 pub diagnostics: Vec<Diagnostic>,
119}
120
121#[cfg(not(target_arch = "wasm32"))]
126static CUSTOM_CLASS_REGEX: Lazy<Regex> = Lazy::new(|| {
127 Regex::new(r":::(\w+)\n([\s\S]*?)\n:::")
128 .expect("static CUSTOM_CLASS_REGEX must compile")
129});
130
131#[cfg(not(target_arch = "wasm32"))]
134static IMAGE_CLASS_REGEX: Lazy<Regex> = Lazy::new(|| {
135 Regex::new(r#"!\[(.*?)\]\((.*?)\)\.class="(.*?)""#)
136 .expect("static IMAGE_CLASS_REGEX must compile")
137});
138
139pub fn generate_html(
168 markdown: &str,
169 config: &crate::HtmlConfig,
170) -> Result<String> {
171 generate_html_with_diagnostics(markdown, config).map(|o| o.html)
172}
173
174pub fn generate_html_with_diagnostics(
202 markdown: &str,
203 config: &crate::HtmlConfig,
204) -> Result<HtmlOutput> {
205 let mut diagnostics: Vec<Diagnostic> = Vec::new();
206
207 let mut html = markdown_to_html_impl(markdown, config)?;
209
210 if config.allow_unsafe_html && config.sanitize_html {
212 html = ammonia::clean(&html);
213 diagnostics.push(Diagnostic {
214 step: "sanitization",
215 level: DiagnosticLevel::Info,
216 message: "HTML sanitized via ammonia".to_string(),
217 });
218 }
219
220 if config.add_aria_attributes {
222 match add_aria_attributes(&html, None) {
223 Ok(enhanced) => {
224 html = enhanced;
225 diagnostics.push(Diagnostic {
226 step: "accessibility",
227 level: DiagnosticLevel::Info,
228 message: "ARIA attributes added".to_string(),
229 });
230 }
231 Err(e) => {
232 let d = Diagnostic {
233 step: "accessibility",
234 level: DiagnosticLevel::Error,
235 message: format!("ARIA enhancement skipped: {e}"),
236 };
237 warn!("{d}");
238 diagnostics.push(d);
239 }
240 }
241 }
242
243 if config.generate_toc {
245 match generate_table_of_contents(&html) {
246 Ok(toc) => {
247 html = html.replace("[[TOC]]", &toc);
248 diagnostics.push(Diagnostic {
249 step: "toc",
250 level: DiagnosticLevel::Info,
251 message: "Table of contents injected".to_string(),
252 });
253 }
254 Err(e) => {
255 let d = Diagnostic {
256 step: "toc",
257 level: DiagnosticLevel::Error,
258 message: format!(
259 "Table of contents generation failed: {e}"
260 ),
261 };
262 warn!("{d}");
263 diagnostics.push(d);
264 }
265 }
266 }
267
268 #[cfg(feature = "math")]
273 if config.enable_math {
274 let before_len = html.len();
275 html = crate::math::convert_math(&html);
276 if html.len() != before_len {
277 diagnostics.push(Diagnostic {
278 step: "math",
279 level: DiagnosticLevel::Info,
280 message: "LaTeX math rendered to MathML".to_string(),
281 });
282 }
283 }
284
285 if config.enable_diagrams {
287 let before_len = html.len();
288 html = crate::math::rewrite_mermaid_blocks(&html);
289 if html.len() != before_len {
290 diagnostics.push(Diagnostic {
291 step: "diagrams",
292 level: DiagnosticLevel::Info,
293 message:
294 "Mermaid blocks rewritten for client-side rendering"
295 .to_string(),
296 });
297 }
298 }
299
300 let document = scraper::Html::parse_document(&html);
302
303 let mut json_ld_fragment = String::new();
305 if config.generate_structured_data {
306 match generate_structured_data_from_doc(&document, None) {
307 Ok(json_ld) => {
308 json_ld_fragment = json_ld;
309 diagnostics.push(Diagnostic {
310 step: "structured_data",
311 level: DiagnosticLevel::Info,
312 message: "JSON-LD structured data generated"
313 .to_string(),
314 });
315 }
316 Err(e) => {
317 let d = Diagnostic {
318 step: "structured_data",
319 level: DiagnosticLevel::Error,
320 message: format!(
321 "Structured data generation failed: {e}"
322 ),
323 };
324 warn!("{d}");
325 diagnostics.push(d);
326 }
327 }
328 }
329
330 if config.generate_full_document {
332 let title = extract_first_heading_from_doc(&document);
334 html = wrap_full_document(
335 &html,
336 &json_ld_fragment,
337 title.as_deref(),
338 config,
339 );
340 } else {
341 if !json_ld_fragment.is_empty() {
343 html.push_str(&json_ld_fragment);
344 }
345 if config.language != crate::constants::DEFAULT_LANGUAGE {
347 html = format!(
348 "<div lang=\"{}\">{}</div>",
349 escape_html(&config.language),
350 html
351 );
352 }
353 }
354
355 if config.minify_output {
357 let before_len = html.len();
358 match minify_html_string(&html) {
359 Ok(minified) => {
360 let saved = before_len.saturating_sub(minified.len());
361 html = minified;
362 diagnostics.push(Diagnostic {
363 step: "minification",
364 level: DiagnosticLevel::Info,
365 message: format!(
366 "Minified: saved {} bytes ({:.0}%)",
367 saved,
368 if before_len > 0 {
369 saved as f64 / before_len as f64 * 100.0
370 } else {
371 0.0
372 }
373 ),
374 });
375 }
376 Err(e) => {
377 let d = Diagnostic {
378 step: "minification",
379 level: DiagnosticLevel::Error,
380 message: format!("Minification failed: {e}"),
381 };
382 warn!("{d}");
383 diagnostics.push(d);
384 }
385 }
386 }
387
388 Ok(HtmlOutput { html, diagnostics })
389}
390
391fn wrap_full_document(
393 body: &str,
394 json_ld: &str,
395 title: Option<&str>,
396 config: &crate::HtmlConfig,
397) -> String {
398 let lang = escape_html(&config.language);
399 let mut head = String::from("<meta charset=\"utf-8\">");
400
401 if let Some(t) = title {
402 head.push_str(&format!("<title>{}</title>", escape_html(t)));
403 }
404
405 if !json_ld.is_empty() {
406 head.push_str(json_ld);
407 }
408
409 format!(
410 "<!DOCTYPE html>\n<html lang=\"{lang}\">\n<head>{head}</head>\n<body>\n{body}\n</body>\n</html>"
411 )
412}
413
414static H1_SELECTOR: Lazy<scraper::Selector> = Lazy::new(|| {
417 scraper::Selector::parse("h1")
418 .expect("static H1_SELECTOR must parse")
419});
420
421fn extract_first_heading_from_doc(
423 document: &scraper::Html,
424) -> Option<String> {
425 document
426 .select(&H1_SELECTOR)
427 .next()
428 .map(|el| el.text().collect::<String>())
429}
430
431pub fn markdown_to_html_with_extensions(
450 markdown: &str,
451) -> Result<String> {
452 markdown_to_html_impl(markdown, &crate::HtmlConfig::default())
453}
454
455#[cfg(not(target_arch = "wasm32"))]
456fn markdown_to_html_impl(
457 markdown: &str,
458 config: &crate::HtmlConfig,
459) -> Result<String> {
460 let content_without_front_matter = extract_front_matter(markdown)
462 .unwrap_or_else(|_| markdown.to_string());
463
464 let markdown_with_classes = add_custom_classes(
466 &content_without_front_matter,
467 config.allow_unsafe_html,
468 );
469
470 let markdown_with_images =
472 process_images_with_classes(&markdown_with_classes);
473
474 let mut comrak_options = BASE_COMRAK_OPTIONS.clone();
477 comrak_options.render.r#unsafe = config.allow_unsafe_html;
478
479 let mut md_options = MarkdownOptions::default()
480 .with_comrak_options(comrak_options)
481 .with_syntax_highlighting(config.enable_syntax_highlighting);
482
483 if let Some(ref theme) = config.syntax_theme {
484 md_options = md_options.with_custom_theme(theme.clone());
485 }
486
487 process_markdown(&markdown_with_images, &md_options).map_err(
489 |err| HtmlError::markdown_conversion(err.to_string(), None),
490 )
491}
492
493#[cfg(target_arch = "wasm32")]
504fn markdown_to_html_impl(
505 markdown: &str,
506 config: &crate::HtmlConfig,
507) -> Result<String> {
508 let content_without_front_matter = extract_front_matter(markdown)
509 .unwrap_or_else(|_| markdown.to_string());
510
511 let mut opts = BASE_COMRAK_OPTIONS.clone();
512 opts.render.r#unsafe = config.allow_unsafe_html;
513
514 Ok(comrak::markdown_to_html(
515 &content_without_front_matter,
516 &opts,
517 ))
518}
519
520#[cfg(not(target_arch = "wasm32"))]
536fn add_custom_classes(
537 markdown: &str,
538 allow_unsafe_html: bool,
539) -> Cow<'_, str> {
540 CUSTOM_CLASS_REGEX.replace_all(
544 markdown,
545 |caps: ®ex::Captures| {
546 let class_name = &caps[1];
547 let block_content = &caps[2];
548
549 let inline_html = match process_markdown_inline_impl(
550 block_content,
551 allow_unsafe_html,
552 ) {
553 Ok(html) => html,
554 Err(_) => block_content.to_string(),
555 };
556
557 format!(
559 "<div class=\"{}\">{}</div>",
560 class_name, inline_html
561 )
562 },
563 )
564}
565
566pub fn process_markdown_inline(
582 content: &str,
583) -> std::result::Result<String, Box<dyn Error>> {
584 process_markdown_inline_impl(content, false)
585}
586
587#[cfg(not(target_arch = "wasm32"))]
588fn process_markdown_inline_impl(
589 content: &str,
590 allow_unsafe_html: bool,
591) -> std::result::Result<String, Box<dyn Error>> {
592 let mut comrak_opts = BASE_COMRAK_OPTIONS.clone();
595 comrak_opts.render.r#unsafe = allow_unsafe_html;
596
597 let options =
598 MarkdownOptions::default().with_comrak_options(comrak_opts);
599 Ok(process_markdown(content, &options)?)
600}
601
602#[cfg(target_arch = "wasm32")]
605fn process_markdown_inline_impl(
606 content: &str,
607 allow_unsafe_html: bool,
608) -> std::result::Result<String, Box<dyn Error>> {
609 let mut opts = BASE_COMRAK_OPTIONS.clone();
610 opts.render.r#unsafe = allow_unsafe_html;
611 Ok(comrak::markdown_to_html(content, &opts))
612}
613
614#[cfg(not(target_arch = "wasm32"))]
617fn process_images_with_classes(markdown: &str) -> Cow<'_, str> {
618 IMAGE_CLASS_REGEX.replace_all(markdown, |caps: ®ex::Captures| {
621 format!(
622 r#"<img src="{}" alt="{}" class="{}" />"#,
623 escape_html(&caps[2]), escape_html(&caps[1]), escape_html(&caps[3]), )
627 })
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use crate::HtmlConfig;
634
635 #[test]
639 fn test_generate_html_basic() {
640 let markdown = "# Hello, world!\n\nThis is a test.";
641 let config = HtmlConfig::default();
642 let result = generate_html(markdown, &config);
643 assert!(result.is_ok());
644 let html = result.unwrap();
645 assert!(html.contains("<h1>Hello, world!</h1>"));
646 assert!(html.contains("<p>This is a test.</p>"));
647 }
648
649 #[test]
654 fn test_markdown_to_html_with_extensions() {
655 let markdown = r"
656| Header 1 | Header 2 |
657| -------- | -------- |
658| Row 1 | Row 2 |
659";
660 let result = markdown_to_html_with_extensions(markdown);
661 assert!(result.is_ok());
662 let html = result.unwrap();
663
664 println!("{}", html);
665
666 assert!(html.contains("<div class=\"table-responsive\"><table class=\"table\">"), "Table element not found");
668 assert!(
669 html.contains("<th>Header 1</th>"),
670 "Table header not found"
671 );
672 assert!(
673 html.contains("<td class=\"text-left\">Row 1</td>"),
674 "Table row not found"
675 );
676 }
677
678 #[test]
682 fn test_generate_html_empty() {
683 let markdown = "";
684 let config = HtmlConfig::default();
685 let result = generate_html(markdown, &config);
686 assert!(result.is_ok());
687 let html = result.unwrap();
688 assert!(html.is_empty());
689 }
690
691 #[test]
696 fn test_generate_html_invalid_markdown() {
697 let markdown = "# Unclosed header\nSome **unclosed bold";
698 let config = HtmlConfig::default();
699 let result = generate_html(markdown, &config);
700 assert!(result.is_ok());
701 let html = result.unwrap();
702
703 println!("{}", html);
704
705 assert!(
706 html.contains("<h1>Unclosed header</h1>"),
707 "Header not found"
708 );
709 assert!(
710 html.contains("<p>Some **unclosed bold</p>"),
711 "Unclosed bold tag not properly handled"
712 );
713 }
714
715 #[test]
721 fn test_generate_html_complex() {
722 let markdown = r#"
723# Header
724
725## Subheader
726
727Some `inline code` and a [link](https://example.com).
728
729```rust
730fn main() {
731 println!("Hello, world!");
732}
733```
734
7351. First item
7362. Second item
737"#;
738 let config = HtmlConfig::default();
739 let result = generate_html(markdown, &config);
740 assert!(result.is_ok());
741 let html = result.unwrap();
742 println!("{}", html);
743
744 assert!(
746 html.contains("<h1>Header</h1>"),
747 "H1 Header not found"
748 );
749 assert!(
750 html.contains("<h2>Subheader</h2>"),
751 "H2 Header not found"
752 );
753
754 assert!(
756 html.contains("<code>inline code</code>"),
757 "Inline code not found"
758 );
759 assert!(
760 html.contains(r#"<a href="https://example.com">link</a>"#),
761 "Link not found"
762 );
763
764 assert!(
766 html.contains(r#"<code class="language-rust">"#),
767 "Code block with language-rust class not found"
768 );
769 assert!(
770 html.contains(r#"<span style="color:#b48ead;">fn </span>"#),
771 "`fn` keyword with syntax highlighting not found"
772 );
773 assert!(
774 html.contains(
775 r#"<span style="color:#8fa1b3;">main</span>"#
776 ),
777 "`main` function name with syntax highlighting not found"
778 );
779
780 assert!(
782 html.contains("<li>First item</li>"),
783 "First item not found"
784 );
785 assert!(
786 html.contains("<li>Second item</li>"),
787 "Second item not found"
788 );
789 }
790
791 #[test]
793 fn test_generate_html_with_valid_front_matter() {
794 let markdown = r#"---
795title: Test
796author: Jane Doe
797---
798# Hello, world!"#;
799 let config = HtmlConfig::default();
800 let result = generate_html(markdown, &config);
801 assert!(result.is_ok());
802 let html = result.unwrap();
803 assert!(html.contains("<h1>Hello, world!</h1>"));
804 }
805
806 #[test]
808 fn test_generate_html_with_invalid_front_matter() {
809 let markdown = r#"---
810title Test
811author: Jane Doe
812---
813# Hello, world!"#;
814 let config = HtmlConfig::default();
815 let result = generate_html(markdown, &config);
816 assert!(
817 result.is_ok(),
818 "Invalid front matter should be ignored"
819 );
820 let html = result.unwrap();
821 assert!(html.contains("<h1>Hello, world!</h1>"));
822 }
823
824 #[test]
826 fn test_generate_html_large_input() {
827 let markdown = "# Large Markdown\n\n".repeat(10_000);
828 let config = HtmlConfig::default();
829 let result = generate_html(&markdown, &config);
830 assert!(result.is_ok());
831 let html = result.unwrap();
832 assert!(html.contains("<h1>Large Markdown</h1>"));
833 }
834
835 #[test]
837 fn test_generate_html_with_custom_markdown_options() {
838 let markdown = "**Bold text**";
839 let config = HtmlConfig::default();
840 let result = generate_html(markdown, &config);
841 assert!(result.is_ok());
842 let html = result.unwrap();
843 assert!(html.contains("<strong>Bold text</strong>"));
844 }
845
846 #[test]
848 fn test_generate_html_with_unsupported_elements() {
849 let markdown = "::: custom_block\nContent\n:::";
850 let config = HtmlConfig::default();
851 let result = generate_html(markdown, &config);
852 assert!(result.is_ok());
853 let html = result.unwrap();
854 assert!(html.contains("::: custom_block"));
855 }
856
857 #[test]
859 fn test_markdown_to_html_with_conversion_error() {
860 let markdown = "# Unclosed header\nSome **unclosed bold";
861 let result = markdown_to_html_with_extensions(markdown);
862 assert!(result.is_ok());
863 let html = result.unwrap();
864 assert!(html.contains("<p>Some **unclosed bold</p>"));
865 }
866
867 #[test]
869 fn test_generate_html_whitespace_only() {
870 let markdown = " \n ";
871 let config = HtmlConfig::default();
872 let result = generate_html(markdown, &config);
873 assert!(result.is_ok());
874 let html = result.unwrap();
875 assert!(
876 html.is_empty(),
877 "Whitespace-only Markdown should produce empty HTML"
878 );
879 }
880
881 #[cfg(not(target_arch = "wasm32"))]
886 #[test]
887 fn test_markdown_to_html_with_custom_comrak_options() {
888 let markdown = "^^Superscript^^\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Row 1 | Row 2 |";
889
890 let mut comrak_options = Options::default();
892 comrak_options.extension.superscript = true;
893 comrak_options.extension.table = true; let options = MarkdownOptions::default()
897 .with_comrak_options(comrak_options.clone());
898 let content_without_front_matter =
899 extract_front_matter(markdown)
900 .unwrap_or(markdown.to_string());
901
902 println!("Comrak options: {:?}", comrak_options);
903
904 let result =
905 process_markdown(&content_without_front_matter, &options);
906
907 match result {
908 Ok(ref html) => {
909 assert!(
911 html.contains("<sup>Superscript</sup>"),
912 "Superscript not found in HTML output"
913 );
914
915 assert!(
917 html.contains("<table"),
918 "Table element not found in HTML output"
919 );
920 }
921 Err(err) => {
922 panic!(
923 "Failed to process Markdown with custom Options: {:?}",
924 err
925 );
926 }
927 }
928 }
929 #[test]
930 fn test_generate_html_with_default_config() {
931 let markdown = "# Default Configuration Test";
932 let config = HtmlConfig::default();
933 let result = generate_html(markdown, &config);
934 assert!(result.is_ok());
935 let html = result.unwrap();
936 assert!(html.contains("<h1>Default Configuration Test</h1>"));
937 }
938
939 #[test]
940 fn test_generate_html_with_custom_front_matter_delimiter() {
941 let markdown = r#";;;;
942title: Custom
943author: John Doe
944;;;;
945# Custom Front Matter Delimiter"#;
946
947 let config = HtmlConfig::default();
948 let result = generate_html(markdown, &config);
949 assert!(result.is_ok());
950 let html = result.unwrap();
951 assert!(html.contains("<h1>Custom Front Matter Delimiter</h1>"));
952 }
953 #[test]
954 fn test_generate_html_with_task_list() {
955 let markdown = r"
956- [x] Task 1
957- [ ] Task 2
958";
959
960 let result = markdown_to_html_with_extensions(markdown);
961 assert!(result.is_ok());
962 let html = result.unwrap();
963
964 println!("Generated HTML:\n{}", html);
965
966 assert!(
968 html.contains(r#"<li><input type="checkbox" checked="" disabled="" /> Task 1</li>"#),
969 "Task 1 checkbox not rendered as expected"
970 );
971 assert!(
972 html.contains(r#"<li><input type="checkbox" disabled="" /> Task 2</li>"#),
973 "Task 2 checkbox not rendered as expected"
974 );
975 }
976 #[test]
977 fn test_generate_html_with_large_table() {
978 let header =
979 "| Header 1 | Header 2 |\n| -------- | -------- |\n";
980 let rows = "| Row 1 | Row 2 |\n".repeat(1000);
981 let markdown = format!("{}{}", header, rows);
982
983 let result = markdown_to_html_with_extensions(&markdown);
984 assert!(result.is_ok());
985 let html = result.unwrap();
986
987 let row_count = html.matches("<tr>").count();
988 assert_eq!(
989 row_count, 1001,
990 "Incorrect number of rows: {}",
991 row_count
992 ); }
994 #[test]
995 fn test_generate_html_with_special_characters() {
996 let markdown = r#"Markdown with special characters: <, >, &, "quote", 'single-quote'."#;
997 let result = markdown_to_html_with_extensions(markdown);
998 assert!(result.is_ok());
999 let html = result.unwrap();
1000
1001 assert!(html.contains("<"), "Less than sign not escaped");
1002 assert!(html.contains(">"), "Greater than sign not escaped");
1003 assert!(html.contains("&"), "Ampersand not escaped");
1004 assert!(html.contains("""), "Double quote not escaped");
1005
1006 assert!(
1008 html.contains("'") || html.contains("'"),
1009 "Single quote not handled as expected"
1010 );
1011 }
1012
1013 #[test]
1014 fn test_generate_html_with_invalid_markdown_syntax() {
1015 let markdown =
1017 r"# Invalid Markdown <unexpected> [bad](url <here)";
1018 let result = markdown_to_html_with_extensions(markdown);
1019 assert!(result.is_ok());
1020 let html = result.unwrap();
1021
1022 println!("Generated HTML:\n{}", html);
1023
1024 assert!(html.contains("<h1>"), "Header tag should be present");
1026 }
1027
1028 #[test]
1030 fn test_generate_html_mixed_markdown() {
1031 let markdown = r"# Valid Header
1032Some **bold text** followed by invalid Markdown:
1033~~strikethrough~~ without a closing tag.";
1034 let result = markdown_to_html_with_extensions(markdown);
1035 assert!(result.is_ok());
1036 let html = result.unwrap();
1037
1038 assert!(
1039 html.contains("<h1>Valid Header</h1>"),
1040 "Header not found"
1041 );
1042 assert!(
1043 html.contains("<strong>bold text</strong>"),
1044 "Bold text not rendered correctly"
1045 );
1046 assert!(
1047 html.contains("<del>strikethrough</del>"),
1048 "Strikethrough not rendered correctly"
1049 );
1050 }
1051
1052 #[test]
1054 fn test_generate_html_deeply_nested_content() {
1055 let markdown = r"
10561. Level 1
1057 1.1. Level 2
1058 1.1.1. Level 3
1059 1.1.1.1. Level 4
1060";
1061 let result = markdown_to_html_with_extensions(markdown);
1062 assert!(result.is_ok());
1063 let html = result.unwrap();
1064
1065 assert!(html.contains("<ol>"), "Ordered list not rendered");
1066 assert!(html.contains("<li>Level 1"), "Level 1 not rendered");
1067 assert!(
1068 html.contains("1.1.1.1. Level 4"),
1069 "Deeply nested levels not rendered correctly"
1070 );
1071 }
1072
1073 #[test]
1075 fn test_generate_html_with_raw_html() {
1076 let markdown = r"
1077# Header with HTML
1078<p>This is a paragraph with <strong>HTML</strong>.</p>
1079";
1080 let config = HtmlConfig {
1082 allow_unsafe_html: true,
1083 ..HtmlConfig::default()
1084 };
1085 let result = generate_html(markdown, &config);
1086 assert!(result.is_ok());
1087 let html = result.unwrap();
1088
1089 assert!(
1090 html.contains("<p>This is a paragraph with <strong>HTML</strong>.</p>"),
1091 "Raw HTML content not preserved in output"
1092 );
1093 }
1094
1095 #[test]
1097 fn test_generate_html_invalid_front_matter_handling() {
1098 let markdown = "---
1099key_without_value
1100another_key: valid
1101---
1102# Markdown Content
1103";
1104 let result = generate_html(markdown, &HtmlConfig::default());
1105 assert!(
1106 result.is_ok(),
1107 "Invalid front matter should not cause an error"
1108 );
1109 let html = result.unwrap();
1110 assert!(
1111 html.contains("<h1>Markdown Content</h1>"),
1112 "Content not processed correctly"
1113 );
1114 }
1115
1116 #[test]
1118 fn test_generate_html_large_front_matter() {
1119 let front_matter = "---\n".to_owned()
1120 + &"key: value\n".repeat(10_000)
1121 + "---\n# Content";
1122 let result =
1123 generate_html(&front_matter, &HtmlConfig::default());
1124 assert!(
1125 result.is_ok(),
1126 "Large front matter should be handled gracefully"
1127 );
1128 let html = result.unwrap();
1129 assert!(
1130 html.contains("<h1>Content</h1>"),
1131 "Content not rendered correctly"
1132 );
1133 }
1134
1135 #[test]
1137 fn test_generate_html_with_long_lines() {
1138 let markdown = "A ".repeat(10_000);
1139 let result = markdown_to_html_with_extensions(&markdown);
1140 assert!(result.is_ok());
1141 let html = result.unwrap();
1142
1143 assert!(
1144 html.contains("A A A A"),
1145 "Long consecutive lines should be rendered properly"
1146 );
1147 }
1148
1149 #[test]
1150 fn test_markdown_with_custom_classes() {
1151 let markdown = r":::note
1152This is a note with a custom class.
1153:::";
1154
1155 let result = markdown_to_html_with_extensions(markdown);
1156 assert!(result.is_ok(), "Markdown conversion should not fail.");
1157
1158 let html = result.unwrap();
1159 println!("HTML:\n{}", html);
1160
1161 assert!(
1163 html.contains(r#"<div class="note">"#),
1164 "Custom block should wrap in <div class=\"note\">"
1165 );
1166
1167 assert!(
1169 html.contains("This is a note with a custom class."),
1170 "Block text is missing or incorrectly rendered"
1171 );
1172 }
1173
1174 #[test]
1175 fn test_markdown_with_custom_blocks_and_images() {
1176 let markdown = ".class=\"img-fluid\"";
1177 let result = markdown_to_html_with_extensions(markdown);
1178 assert!(result.is_ok());
1179 let html = result.unwrap();
1180 println!("{}", html);
1181 assert!(
1182 html.contains(r#"<img src="https://example.com/image.webp" alt="A very tall building" class="img-fluid" />"#),
1183 "First image not rendered correctly"
1184 );
1185 }
1186
1187 #[test]
1189 fn test_empty_front_matter_handling() {
1190 let markdown = "---\n---\n# Content";
1191 let result = generate_html(markdown, &HtmlConfig::default());
1192 assert!(result.is_ok());
1193 let html = result.unwrap();
1194 assert!(
1195 html.contains("<h1>Content</h1>"),
1196 "Content should be processed correctly"
1197 );
1198 }
1199
1200 #[cfg(not(target_arch = "wasm32"))]
1207 #[test]
1208 fn test_invalid_image_syntax() {
1209 let markdown = "![Image with missing URL]()";
1210 let result = process_images_with_classes(markdown);
1211 assert_eq!(
1212 result, markdown,
1213 "Invalid image syntax should remain unchanged"
1214 );
1215 }
1216
1217 #[test]
1219 fn test_incorrect_front_matter_delimiters() {
1220 let markdown = ";;;\ntitle: Test\n---\n# Header";
1221 let result = generate_html(markdown, &HtmlConfig::default());
1222 assert!(result.is_ok());
1223 let html = result.unwrap();
1224 assert!(
1225 html.contains("<h1>Header</h1>"),
1226 "Header should be processed correctly"
1227 );
1228 }
1229 #[cfg(test)]
1230 mod missing_scenarios_tests {
1231 use super::*;
1232
1233 #[test]
1237 fn test_triple_colon_warning_with_bold() {
1238 let markdown = r":::warning
1239**Caution:** This operation is sensitive.
1240:::";
1241
1242 let result = markdown_to_html_with_extensions(markdown);
1243 assert!(
1244 result.is_ok(),
1245 "Markdown conversion should succeed."
1246 );
1247
1248 let html = result.unwrap();
1249 println!("HTML:\n{}", html);
1250
1251 assert!(
1254 html.contains(r#"<div class="warning">"#),
1255 "Expected <div class=\"warning\"> wrapping the block"
1256 );
1257 assert!(html.contains("<strong>Caution:</strong>"),
1258 "Expected inline bold text to become <strong>Caution:</strong>");
1259 }
1260
1261 #[test]
1265 fn test_multiple_triple_colon_blocks() {
1266 let markdown = r":::note
1267**Note:** First block
1268:::
1269
1270:::warning
1271**Warning:** Second block
1272:::";
1273
1274 let result = markdown_to_html_with_extensions(markdown);
1275 assert!(
1276 result.is_ok(),
1277 "Markdown conversion should succeed."
1278 );
1279
1280 let html = result.unwrap();
1281 println!("HTML:\n{}", html);
1282
1283 assert!(
1285 html.contains(r#"<div class="note">"#),
1286 "Missing <div class=\"note\"> for the first block"
1287 );
1288 assert!(
1289 html.contains(r#"<div class="warning">"#),
1290 "Missing <div class=\"warning\"> for the second block"
1291 );
1292
1293 assert!(
1295 html.contains("<strong>Note:</strong>"),
1296 "Bold text in the note block not parsed"
1297 );
1298 assert!(
1299 html.contains("<strong>Warning:</strong>"),
1300 "Bold text in the warning block not parsed"
1301 );
1302 }
1303
1304 #[test]
1308 fn test_triple_colon_block_multi_paragraph() {
1309 let markdown = r":::note
1310**Paragraph 1:** This is the first paragraph.
1311
1312This is the second paragraph, also with **bold** text.
1313:::";
1314
1315 let result = markdown_to_html_with_extensions(markdown);
1316 assert!(
1317 result.is_ok(),
1318 "Markdown conversion should succeed."
1319 );
1320
1321 let html = result.unwrap();
1322 println!("HTML:\n{}", html);
1323
1324 assert!(
1329 html.contains("<strong>Paragraph 1:</strong>"),
1330 "Inline bold text not parsed in the first paragraph"
1331 );
1332 assert!(html.contains("second paragraph, also with <strong>bold</strong> text"),
1333 "Inline bold text not parsed in the second paragraph");
1334 }
1335
1336 #[test]
1341 fn test_triple_colon_block_forcing_inline_error() {
1342 let markdown = r":::error
1345This block tries < to break > inline parsing & [some link (unclosed).
1346:::";
1347
1348 let result = markdown_to_html_with_extensions(markdown);
1354 assert!(
1355 result.is_ok(),
1356 "We won't forcibly error, but let's see the output."
1357 );
1358
1359 let html = result.unwrap();
1360 println!("HTML:\n{}", html);
1361
1362 assert!(
1366 html.contains(r#"<div class="error">"#),
1367 "Block div not found for 'error' class"
1368 );
1369
1370 assert!(
1374 html.contains("This block tries "),
1375 "Expected parsed content in the block"
1376 );
1377 }
1378 }
1379}