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 mut tunnels: Vec<String> = Vec::new();
473
474 let markdown_with_classes = add_custom_classes(
476 &content_without_front_matter,
477 config.allow_unsafe_html,
478 &mut tunnels,
479 );
480
481 let markdown_with_images = process_images_with_classes(
483 &markdown_with_classes,
484 &mut tunnels,
485 );
486
487 let mut comrak_options = BASE_COMRAK_OPTIONS.clone();
490 comrak_options.render.r#unsafe = config.allow_unsafe_html;
491 let mut md_options = MarkdownOptions::default()
504 .with_comrak_options(comrak_options)
505 .with_syntax_highlighting(config.enable_syntax_highlighting)
506 .with_max_input_size(config.max_input_size);
509
510 if let Some(ref theme) = config.syntax_theme {
511 md_options = md_options.with_custom_theme(theme.clone());
512 }
513
514 let mut html = process_markdown(&markdown_with_images, &md_options)
516 .map_err(|err| {
517 HtmlError::markdown_conversion(err.to_string(), None)
518 })?;
519
520 for (index, fragment) in tunnels.iter().enumerate() {
525 let sentinel = tunnel_sentinel(index);
526 html = html.replace(&format!("<p>{sentinel}</p>"), fragment);
527 html = html.replace(&sentinel, fragment);
528 }
529
530 Ok(html)
531}
532
533#[cfg(not(target_arch = "wasm32"))]
538fn tunnel_sentinel(index: usize) -> String {
539 format!("\u{FFFC}\u{FFFC}hgtunnel{index}\u{FFFC}\u{FFFC}")
540}
541
542#[cfg(target_arch = "wasm32")]
553fn markdown_to_html_impl(
554 markdown: &str,
555 config: &crate::HtmlConfig,
556) -> Result<String> {
557 let content_without_front_matter = extract_front_matter(markdown)
558 .unwrap_or_else(|_| markdown.to_string());
559
560 let mut opts = BASE_COMRAK_OPTIONS.clone();
561 opts.render.r#unsafe = config.allow_unsafe_html;
562
563 Ok(comrak::markdown_to_html(
564 &content_without_front_matter,
565 &opts,
566 ))
567}
568
569#[cfg(not(target_arch = "wasm32"))]
585fn add_custom_classes<'a>(
586 markdown: &'a str,
587 allow_unsafe_html: bool,
588 tunnels: &mut Vec<String>,
589) -> Cow<'a, str> {
590 CUSTOM_CLASS_REGEX.replace_all(
597 markdown,
598 |caps: ®ex::Captures| {
599 let class_name = &caps[1];
600 let block_content = &caps[2];
601
602 let inline_html = match process_markdown_inline_impl(
603 block_content,
604 allow_unsafe_html,
605 ) {
606 Ok(html) => html,
607 Err(_) => block_content.to_string(),
608 };
609
610 let fragment = format!(
613 "<div class=\"{class_name}\">{inline_html}</div>"
614 );
615 let index = tunnels.len();
616 tunnels.push(fragment);
617 tunnel_sentinel(index)
618 },
619 )
620}
621
622pub fn process_markdown_inline(
638 content: &str,
639) -> std::result::Result<String, Box<dyn Error>> {
640 process_markdown_inline_impl(content, false)
641}
642
643#[cfg(not(target_arch = "wasm32"))]
644fn process_markdown_inline_impl(
645 content: &str,
646 allow_unsafe_html: bool,
647) -> std::result::Result<String, Box<dyn Error>> {
648 let mut comrak_opts = BASE_COMRAK_OPTIONS.clone();
651 comrak_opts.render.r#unsafe = allow_unsafe_html;
652 comrak_opts.render.escape = !allow_unsafe_html;
655
656 let options =
657 MarkdownOptions::default().with_comrak_options(comrak_opts);
658 Ok(process_markdown(content, &options)?)
659}
660
661#[cfg(target_arch = "wasm32")]
664fn process_markdown_inline_impl(
665 content: &str,
666 allow_unsafe_html: bool,
667) -> std::result::Result<String, Box<dyn Error>> {
668 let mut opts = BASE_COMRAK_OPTIONS.clone();
669 opts.render.r#unsafe = allow_unsafe_html;
670 Ok(comrak::markdown_to_html(content, &opts))
671}
672
673#[cfg(not(target_arch = "wasm32"))]
676fn process_images_with_classes<'a>(
677 markdown: &'a str,
678 tunnels: &mut Vec<String>,
679) -> Cow<'a, str> {
680 IMAGE_CLASS_REGEX.replace_all(markdown, |caps: ®ex::Captures| {
684 let fragment = format!(
685 r#"<img src="{}" alt="{}" class="{}" />"#,
686 escape_html(&caps[2]), escape_html(&caps[1]), escape_html(&caps[3]), );
690 let index = tunnels.len();
691 tunnels.push(fragment);
692 tunnel_sentinel(index)
693 })
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use crate::HtmlConfig;
700
701 #[test]
705 fn test_generate_html_basic() {
706 let markdown = "# Hello, world!\n\nThis is a test.";
707 let config = HtmlConfig::default();
708 let result = generate_html(markdown, &config);
709 assert!(result.is_ok());
710 let html = result.unwrap();
711 assert!(html.contains("<h1>Hello, world!</h1>"));
712 assert!(html.contains("<p>This is a test.</p>"));
713 }
714
715 #[test]
720 fn test_markdown_to_html_with_extensions() {
721 let markdown = r"
722| Header 1 | Header 2 |
723| -------- | -------- |
724| Row 1 | Row 2 |
725";
726 let result = markdown_to_html_with_extensions(markdown);
727 assert!(result.is_ok());
728 let html = result.unwrap();
729
730 println!("{}", html);
731
732 assert!(html.contains("<div class=\"table-responsive\"><table class=\"table\">"), "Table element not found");
734 assert!(
735 html.contains("<th>Header 1</th>"),
736 "Table header not found"
737 );
738 assert!(
739 html.contains("<td class=\"text-left\">Row 1</td>"),
740 "Table row not found"
741 );
742 }
743
744 #[test]
748 fn test_generate_html_empty() {
749 let markdown = "";
750 let config = HtmlConfig::default();
751 let result = generate_html(markdown, &config);
752 assert!(result.is_ok());
753 let html = result.unwrap();
754 assert!(html.is_empty());
755 }
756
757 #[test]
762 fn test_generate_html_invalid_markdown() {
763 let markdown = "# Unclosed header\nSome **unclosed bold";
764 let config = HtmlConfig::default();
765 let result = generate_html(markdown, &config);
766 assert!(result.is_ok());
767 let html = result.unwrap();
768
769 println!("{}", html);
770
771 assert!(
772 html.contains("<h1>Unclosed header</h1>"),
773 "Header not found"
774 );
775 assert!(
776 html.contains("<p>Some **unclosed bold</p>"),
777 "Unclosed bold tag not properly handled"
778 );
779 }
780
781 #[test]
787 fn test_generate_html_complex() {
788 let markdown = r#"
789# Header
790
791## Subheader
792
793Some `inline code` and a [link](https://example.com).
794
795```rust
796fn main() {
797 println!("Hello, world!");
798}
799```
800
8011. First item
8022. Second item
803"#;
804 let config = HtmlConfig::default();
805 let result = generate_html(markdown, &config);
806 assert!(result.is_ok());
807 let html = result.unwrap();
808 println!("{}", html);
809
810 assert!(
812 html.contains("<h1>Header</h1>"),
813 "H1 Header not found"
814 );
815 assert!(
816 html.contains("<h2>Subheader</h2>"),
817 "H2 Header not found"
818 );
819
820 assert!(
822 html.contains("<code>inline code</code>"),
823 "Inline code not found"
824 );
825 assert!(
829 html.contains(r#"href="https://example.com""#),
830 "Link href not found"
831 );
832 assert!(html.contains(">link</a>"), "Link text not found");
833
834 assert!(
836 html.contains(r#"<code class="language-rust">"#),
837 "Code block with language-rust class not found"
838 );
839 assert!(
843 html.contains(
844 r#"<span class="storage type function rust">fn</span>"#
845 ),
846 "`fn` keyword with syntax highlighting not found"
847 );
848 assert!(
849 html.contains(
850 r#"<span class="entity name function rust">main</span>"#
851 ),
852 "`main` function name with syntax highlighting not found"
853 );
854
855 assert!(
857 html.contains("<li>First item</li>"),
858 "First item not found"
859 );
860 assert!(
861 html.contains("<li>Second item</li>"),
862 "Second item not found"
863 );
864 }
865
866 #[test]
868 fn test_generate_html_with_valid_front_matter() {
869 let markdown = r#"---
870title: Test
871author: Jane Doe
872---
873# Hello, world!"#;
874 let config = HtmlConfig::default();
875 let result = generate_html(markdown, &config);
876 assert!(result.is_ok());
877 let html = result.unwrap();
878 assert!(html.contains("<h1>Hello, world!</h1>"));
879 }
880
881 #[test]
883 fn test_generate_html_with_invalid_front_matter() {
884 let markdown = r#"---
885title Test
886author: Jane Doe
887---
888# Hello, world!"#;
889 let config = HtmlConfig::default();
890 let result = generate_html(markdown, &config);
891 assert!(
892 result.is_ok(),
893 "Invalid front matter should be ignored"
894 );
895 let html = result.unwrap();
896 assert!(html.contains("<h1>Hello, world!</h1>"));
897 }
898
899 #[test]
901 fn test_generate_html_large_input() {
902 let markdown = "# Large Markdown\n\n".repeat(10_000);
903 let config = HtmlConfig::default();
904 let result = generate_html(&markdown, &config);
905 assert!(result.is_ok());
906 let html = result.unwrap();
907 assert!(html.contains("<h1>Large Markdown</h1>"));
908 }
909
910 #[test]
912 fn test_generate_html_with_custom_markdown_options() {
913 let markdown = "**Bold text**";
914 let config = HtmlConfig::default();
915 let result = generate_html(markdown, &config);
916 assert!(result.is_ok());
917 let html = result.unwrap();
918 assert!(html.contains("<strong>Bold text</strong>"));
919 }
920
921 #[test]
923 fn test_generate_html_with_unsupported_elements() {
924 let markdown = "::: custom_block\nContent\n:::";
925 let config = HtmlConfig::default();
926 let result = generate_html(markdown, &config);
927 assert!(result.is_ok());
928 let html = result.unwrap();
929 assert!(html.contains("::: custom_block"));
930 }
931
932 #[test]
934 fn test_markdown_to_html_with_conversion_error() {
935 let markdown = "# Unclosed header\nSome **unclosed bold";
936 let result = markdown_to_html_with_extensions(markdown);
937 assert!(result.is_ok());
938 let html = result.unwrap();
939 assert!(html.contains("<p>Some **unclosed bold</p>"));
940 }
941
942 #[test]
944 fn test_generate_html_whitespace_only() {
945 let markdown = " \n ";
946 let config = HtmlConfig::default();
947 let result = generate_html(markdown, &config);
948 assert!(result.is_ok());
949 let html = result.unwrap();
950 assert!(
951 html.is_empty(),
952 "Whitespace-only Markdown should produce empty HTML"
953 );
954 }
955
956 #[cfg(not(target_arch = "wasm32"))]
961 #[test]
962 fn test_markdown_to_html_with_custom_comrak_options() {
963 let markdown = "^^Superscript^^\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Row 1 | Row 2 |";
964
965 let mut comrak_options = Options::default();
967 comrak_options.extension.superscript = true;
968 comrak_options.extension.table = true; let options = MarkdownOptions::default()
972 .with_comrak_options(comrak_options.clone());
973 let content_without_front_matter =
974 extract_front_matter(markdown)
975 .unwrap_or(markdown.to_string());
976
977 println!("Comrak options: {:?}", comrak_options);
978
979 let result =
980 process_markdown(&content_without_front_matter, &options);
981
982 match result {
983 Ok(ref html) => {
984 assert!(
986 html.contains("<sup>Superscript</sup>"),
987 "Superscript not found in HTML output"
988 );
989
990 assert!(
992 html.contains("<table"),
993 "Table element not found in HTML output"
994 );
995 }
996 Err(err) => {
997 panic!(
998 "Failed to process Markdown with custom Options: {:?}",
999 err
1000 );
1001 }
1002 }
1003 }
1004 #[test]
1005 fn test_generate_html_with_default_config() {
1006 let markdown = "# Default Configuration Test";
1007 let config = HtmlConfig::default();
1008 let result = generate_html(markdown, &config);
1009 assert!(result.is_ok());
1010 let html = result.unwrap();
1011 assert!(html.contains("<h1>Default Configuration Test</h1>"));
1012 }
1013
1014 #[test]
1015 fn test_generate_html_with_custom_front_matter_delimiter() {
1016 let markdown = r#";;;;
1017title: Custom
1018author: John Doe
1019;;;;
1020# Custom Front Matter Delimiter"#;
1021
1022 let config = HtmlConfig::default();
1023 let result = generate_html(markdown, &config);
1024 assert!(result.is_ok());
1025 let html = result.unwrap();
1026 assert!(html.contains("<h1>Custom Front Matter Delimiter</h1>"));
1027 }
1028 #[test]
1029 fn test_generate_html_with_task_list() {
1030 let markdown = r"
1031- [x] Task 1
1032- [ ] Task 2
1033";
1034
1035 let result = markdown_to_html_with_extensions(markdown);
1036 assert!(result.is_ok());
1037 let html = result.unwrap();
1038
1039 println!("Generated HTML:\n{}", html);
1040
1041 assert!(
1044 html.contains(r#"<li><input type="checkbox" checked="" disabled=""> Task 1</li>"#),
1045 "Task 1 checkbox not rendered as expected"
1046 );
1047 assert!(
1048 html.contains(
1049 r#"<li><input type="checkbox" disabled=""> Task 2</li>"#
1050 ),
1051 "Task 2 checkbox not rendered as expected"
1052 );
1053 }
1054 #[test]
1055 fn test_generate_html_with_large_table() {
1056 let header =
1057 "| Header 1 | Header 2 |\n| -------- | -------- |\n";
1058 let rows = "| Row 1 | Row 2 |\n".repeat(1000);
1059 let markdown = format!("{}{}", header, rows);
1060
1061 let result = markdown_to_html_with_extensions(&markdown);
1062 assert!(result.is_ok());
1063 let html = result.unwrap();
1064
1065 let row_count = html.matches("<tr>").count();
1066 assert_eq!(
1067 row_count, 1001,
1068 "Incorrect number of rows: {}",
1069 row_count
1070 ); }
1072 #[test]
1073 fn test_generate_html_with_special_characters() {
1074 let markdown = r#"Markdown with special characters: <, >, &, "quote", 'single-quote'."#;
1075 let result = markdown_to_html_with_extensions(markdown);
1076 assert!(result.is_ok());
1077 let html = result.unwrap();
1078
1079 assert!(html.contains("<"), "Less than sign not escaped");
1080 assert!(html.contains(">"), "Greater than sign not escaped");
1081 assert!(html.contains("&"), "Ampersand not escaped");
1082
1083 assert!(
1086 html.contains(""") || html.contains('"'),
1087 "Double quote not handled as expected"
1088 );
1089 assert!(
1090 html.contains("'") || html.contains('\''),
1091 "Single quote not handled as expected"
1092 );
1093 }
1094
1095 #[test]
1096 fn test_generate_html_with_invalid_markdown_syntax() {
1097 let markdown =
1099 r"# Invalid Markdown <unexpected> [bad](url <here)";
1100 let result = markdown_to_html_with_extensions(markdown);
1101 assert!(result.is_ok());
1102 let html = result.unwrap();
1103
1104 println!("Generated HTML:\n{}", html);
1105
1106 assert!(html.contains("<h1>"), "Header tag should be present");
1108 }
1109
1110 #[test]
1112 fn test_generate_html_mixed_markdown() {
1113 let markdown = r"# Valid Header
1114Some **bold text** followed by invalid Markdown:
1115~~strikethrough~~ without a closing tag.";
1116 let result = markdown_to_html_with_extensions(markdown);
1117 assert!(result.is_ok());
1118 let html = result.unwrap();
1119
1120 assert!(
1121 html.contains("<h1>Valid Header</h1>"),
1122 "Header not found"
1123 );
1124 assert!(
1125 html.contains("<strong>bold text</strong>"),
1126 "Bold text not rendered correctly"
1127 );
1128 assert!(
1129 html.contains("<del>strikethrough</del>"),
1130 "Strikethrough not rendered correctly"
1131 );
1132 }
1133
1134 #[test]
1136 fn test_generate_html_deeply_nested_content() {
1137 let markdown = r"
11381. Level 1
1139 1.1. Level 2
1140 1.1.1. Level 3
1141 1.1.1.1. Level 4
1142";
1143 let result = markdown_to_html_with_extensions(markdown);
1144 assert!(result.is_ok());
1145 let html = result.unwrap();
1146
1147 assert!(html.contains("<ol>"), "Ordered list not rendered");
1148 assert!(html.contains("<li>Level 1"), "Level 1 not rendered");
1149 assert!(
1150 html.contains("1.1.1.1. Level 4"),
1151 "Deeply nested levels not rendered correctly"
1152 );
1153 }
1154
1155 #[test]
1157 fn test_generate_html_with_raw_html() {
1158 let markdown = r"
1159# Header with HTML
1160<p>This is a paragraph with <strong>HTML</strong>.</p>
1161";
1162 let config = HtmlConfig {
1164 allow_unsafe_html: true,
1165 ..HtmlConfig::default()
1166 };
1167 let result = generate_html(markdown, &config);
1168 assert!(result.is_ok());
1169 let html = result.unwrap();
1170
1171 assert!(
1172 html.contains("<p>This is a paragraph with <strong>HTML</strong>.</p>"),
1173 "Raw HTML content not preserved in output"
1174 );
1175 }
1176
1177 #[test]
1179 fn test_generate_html_invalid_front_matter_handling() {
1180 let markdown = "---
1181key_without_value
1182another_key: valid
1183---
1184# Markdown Content
1185";
1186 let result = generate_html(markdown, &HtmlConfig::default());
1187 assert!(
1188 result.is_ok(),
1189 "Invalid front matter should not cause an error"
1190 );
1191 let html = result.unwrap();
1192 assert!(
1193 html.contains("<h1>Markdown Content</h1>"),
1194 "Content not processed correctly"
1195 );
1196 }
1197
1198 #[test]
1200 fn test_generate_html_large_front_matter() {
1201 let front_matter = "---\n".to_owned()
1202 + &"key: value\n".repeat(10_000)
1203 + "---\n# Content";
1204 let result =
1205 generate_html(&front_matter, &HtmlConfig::default());
1206 assert!(
1207 result.is_ok(),
1208 "Large front matter should be handled gracefully"
1209 );
1210 let html = result.unwrap();
1211 assert!(
1212 html.contains("<h1>Content</h1>"),
1213 "Content not rendered correctly"
1214 );
1215 }
1216
1217 #[test]
1219 fn test_generate_html_with_long_lines() {
1220 let markdown = "A ".repeat(10_000);
1221 let result = markdown_to_html_with_extensions(&markdown);
1222 assert!(result.is_ok());
1223 let html = result.unwrap();
1224
1225 assert!(
1226 html.contains("A A A A"),
1227 "Long consecutive lines should be rendered properly"
1228 );
1229 }
1230
1231 #[test]
1232 fn test_markdown_with_custom_classes() {
1233 let markdown = r":::note
1234This is a note with a custom class.
1235:::";
1236
1237 let result = markdown_to_html_with_extensions(markdown);
1238 assert!(result.is_ok(), "Markdown conversion should not fail.");
1239
1240 let html = result.unwrap();
1241 println!("HTML:\n{}", html);
1242
1243 assert!(
1245 html.contains(r#"<div class="note">"#),
1246 "Custom block should wrap in <div class=\"note\">"
1247 );
1248
1249 assert!(
1251 html.contains("This is a note with a custom class."),
1252 "Block text is missing or incorrectly rendered"
1253 );
1254 }
1255
1256 #[test]
1257 fn test_markdown_with_custom_blocks_and_images() {
1258 let markdown = ".class=\"img-fluid\"";
1259 let result = markdown_to_html_with_extensions(markdown);
1260 assert!(result.is_ok());
1261 let html = result.unwrap();
1262 println!("{}", html);
1263 assert!(
1264 html.contains(r#"<img src="https://example.com/image.webp" alt="A very tall building" class="img-fluid" />"#),
1265 "First image not rendered correctly"
1266 );
1267 }
1268
1269 #[test]
1271 fn test_empty_front_matter_handling() {
1272 let markdown = "---\n---\n# Content";
1273 let result = generate_html(markdown, &HtmlConfig::default());
1274 assert!(result.is_ok());
1275 let html = result.unwrap();
1276 assert!(
1277 html.contains("<h1>Content</h1>"),
1278 "Content should be processed correctly"
1279 );
1280 }
1281
1282 #[cfg(not(target_arch = "wasm32"))]
1289 #[test]
1290 fn test_invalid_image_syntax() {
1291 let markdown = "![Image with missing URL]()";
1292 let mut tunnels = Vec::new();
1293 let result =
1294 process_images_with_classes(markdown, &mut tunnels);
1295 assert_eq!(
1296 result, markdown,
1297 "Invalid image syntax should remain unchanged"
1298 );
1299 assert!(
1300 tunnels.is_empty(),
1301 "No image fragments should be tunnelled for invalid syntax"
1302 );
1303 }
1304
1305 #[test]
1307 fn test_incorrect_front_matter_delimiters() {
1308 let markdown = ";;;\ntitle: Test\n---\n# Header";
1309 let result = generate_html(markdown, &HtmlConfig::default());
1310 assert!(result.is_ok());
1311 let html = result.unwrap();
1312 assert!(
1313 html.contains("<h1>Header</h1>"),
1314 "Header should be processed correctly"
1315 );
1316 }
1317 #[cfg(test)]
1318 mod missing_scenarios_tests {
1319 use super::*;
1320
1321 #[test]
1325 fn test_triple_colon_warning_with_bold() {
1326 let markdown = r":::warning
1327**Caution:** This operation is sensitive.
1328:::";
1329
1330 let result = markdown_to_html_with_extensions(markdown);
1331 assert!(
1332 result.is_ok(),
1333 "Markdown conversion should succeed."
1334 );
1335
1336 let html = result.unwrap();
1337 println!("HTML:\n{}", html);
1338
1339 assert!(
1342 html.contains(r#"<div class="warning">"#),
1343 "Expected <div class=\"warning\"> wrapping the block"
1344 );
1345 assert!(html.contains("<strong>Caution:</strong>"),
1346 "Expected inline bold text to become <strong>Caution:</strong>");
1347 }
1348
1349 #[test]
1353 fn test_multiple_triple_colon_blocks() {
1354 let markdown = r":::note
1355**Note:** First block
1356:::
1357
1358:::warning
1359**Warning:** Second block
1360:::";
1361
1362 let result = markdown_to_html_with_extensions(markdown);
1363 assert!(
1364 result.is_ok(),
1365 "Markdown conversion should succeed."
1366 );
1367
1368 let html = result.unwrap();
1369 println!("HTML:\n{}", html);
1370
1371 assert!(
1373 html.contains(r#"<div class="note">"#),
1374 "Missing <div class=\"note\"> for the first block"
1375 );
1376 assert!(
1377 html.contains(r#"<div class="warning">"#),
1378 "Missing <div class=\"warning\"> for the second block"
1379 );
1380
1381 assert!(
1383 html.contains("<strong>Note:</strong>"),
1384 "Bold text in the note block not parsed"
1385 );
1386 assert!(
1387 html.contains("<strong>Warning:</strong>"),
1388 "Bold text in the warning block not parsed"
1389 );
1390 }
1391
1392 #[test]
1396 fn test_triple_colon_block_multi_paragraph() {
1397 let markdown = r":::note
1398**Paragraph 1:** This is the first paragraph.
1399
1400This is the second paragraph, also with **bold** text.
1401:::";
1402
1403 let result = markdown_to_html_with_extensions(markdown);
1404 assert!(
1405 result.is_ok(),
1406 "Markdown conversion should succeed."
1407 );
1408
1409 let html = result.unwrap();
1410 println!("HTML:\n{}", html);
1411
1412 assert!(
1417 html.contains("<strong>Paragraph 1:</strong>"),
1418 "Inline bold text not parsed in the first paragraph"
1419 );
1420 assert!(html.contains("second paragraph, also with <strong>bold</strong> text"),
1421 "Inline bold text not parsed in the second paragraph");
1422 }
1423
1424 #[test]
1429 fn test_triple_colon_block_forcing_inline_error() {
1430 let markdown = r":::error
1433This block tries < to break > inline parsing & [some link (unclosed).
1434:::";
1435
1436 let result = markdown_to_html_with_extensions(markdown);
1442 assert!(
1443 result.is_ok(),
1444 "We won't forcibly error, but let's see the output."
1445 );
1446
1447 let html = result.unwrap();
1448 println!("HTML:\n{}", html);
1449
1450 assert!(
1454 html.contains(r#"<div class="error">"#),
1455 "Block div not found for 'error' class"
1456 );
1457
1458 assert!(
1462 html.contains("This block tries "),
1463 "Expected parsed content in the block"
1464 );
1465 }
1466 }
1467}