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 comrak_options.render.escape = !config.allow_unsafe_html;
497
498 let mut md_options = MarkdownOptions::default()
499 .with_comrak_options(comrak_options)
500 .with_syntax_highlighting(config.enable_syntax_highlighting);
501
502 if let Some(ref theme) = config.syntax_theme {
503 md_options = md_options.with_custom_theme(theme.clone());
504 }
505
506 let mut html = process_markdown(&markdown_with_images, &md_options)
508 .map_err(|err| {
509 HtmlError::markdown_conversion(err.to_string(), None)
510 })?;
511
512 for (index, fragment) in tunnels.iter().enumerate() {
517 let sentinel = tunnel_sentinel(index);
518 html = html.replace(&format!("<p>{sentinel}</p>"), fragment);
519 html = html.replace(&sentinel, fragment);
520 }
521
522 Ok(html)
523}
524
525#[cfg(not(target_arch = "wasm32"))]
530fn tunnel_sentinel(index: usize) -> String {
531 format!("\u{FFFC}\u{FFFC}hgtunnel{index}\u{FFFC}\u{FFFC}")
532}
533
534#[cfg(target_arch = "wasm32")]
545fn markdown_to_html_impl(
546 markdown: &str,
547 config: &crate::HtmlConfig,
548) -> Result<String> {
549 let content_without_front_matter = extract_front_matter(markdown)
550 .unwrap_or_else(|_| markdown.to_string());
551
552 let mut opts = BASE_COMRAK_OPTIONS.clone();
553 opts.render.r#unsafe = config.allow_unsafe_html;
554
555 Ok(comrak::markdown_to_html(
556 &content_without_front_matter,
557 &opts,
558 ))
559}
560
561#[cfg(not(target_arch = "wasm32"))]
577fn add_custom_classes<'a>(
578 markdown: &'a str,
579 allow_unsafe_html: bool,
580 tunnels: &mut Vec<String>,
581) -> Cow<'a, str> {
582 CUSTOM_CLASS_REGEX.replace_all(
589 markdown,
590 |caps: ®ex::Captures| {
591 let class_name = &caps[1];
592 let block_content = &caps[2];
593
594 let inline_html = match process_markdown_inline_impl(
595 block_content,
596 allow_unsafe_html,
597 ) {
598 Ok(html) => html,
599 Err(_) => block_content.to_string(),
600 };
601
602 let fragment = format!(
605 "<div class=\"{class_name}\">{inline_html}</div>"
606 );
607 let index = tunnels.len();
608 tunnels.push(fragment);
609 tunnel_sentinel(index)
610 },
611 )
612}
613
614pub fn process_markdown_inline(
630 content: &str,
631) -> std::result::Result<String, Box<dyn Error>> {
632 process_markdown_inline_impl(content, false)
633}
634
635#[cfg(not(target_arch = "wasm32"))]
636fn process_markdown_inline_impl(
637 content: &str,
638 allow_unsafe_html: bool,
639) -> std::result::Result<String, Box<dyn Error>> {
640 let mut comrak_opts = BASE_COMRAK_OPTIONS.clone();
643 comrak_opts.render.r#unsafe = allow_unsafe_html;
644 comrak_opts.render.escape = !allow_unsafe_html;
647
648 let options =
649 MarkdownOptions::default().with_comrak_options(comrak_opts);
650 Ok(process_markdown(content, &options)?)
651}
652
653#[cfg(target_arch = "wasm32")]
656fn process_markdown_inline_impl(
657 content: &str,
658 allow_unsafe_html: bool,
659) -> std::result::Result<String, Box<dyn Error>> {
660 let mut opts = BASE_COMRAK_OPTIONS.clone();
661 opts.render.r#unsafe = allow_unsafe_html;
662 Ok(comrak::markdown_to_html(content, &opts))
663}
664
665#[cfg(not(target_arch = "wasm32"))]
668fn process_images_with_classes<'a>(
669 markdown: &'a str,
670 tunnels: &mut Vec<String>,
671) -> Cow<'a, str> {
672 IMAGE_CLASS_REGEX.replace_all(markdown, |caps: ®ex::Captures| {
676 let fragment = format!(
677 r#"<img src="{}" alt="{}" class="{}" />"#,
678 escape_html(&caps[2]), escape_html(&caps[1]), escape_html(&caps[3]), );
682 let index = tunnels.len();
683 tunnels.push(fragment);
684 tunnel_sentinel(index)
685 })
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use crate::HtmlConfig;
692
693 #[test]
697 fn test_generate_html_basic() {
698 let markdown = "# Hello, world!\n\nThis is a test.";
699 let config = HtmlConfig::default();
700 let result = generate_html(markdown, &config);
701 assert!(result.is_ok());
702 let html = result.unwrap();
703 assert!(html.contains("<h1>Hello, world!</h1>"));
704 assert!(html.contains("<p>This is a test.</p>"));
705 }
706
707 #[test]
712 fn test_markdown_to_html_with_extensions() {
713 let markdown = r"
714| Header 1 | Header 2 |
715| -------- | -------- |
716| Row 1 | Row 2 |
717";
718 let result = markdown_to_html_with_extensions(markdown);
719 assert!(result.is_ok());
720 let html = result.unwrap();
721
722 println!("{}", html);
723
724 assert!(html.contains("<div class=\"table-responsive\"><table class=\"table\">"), "Table element not found");
726 assert!(
727 html.contains("<th>Header 1</th>"),
728 "Table header not found"
729 );
730 assert!(
731 html.contains("<td class=\"text-left\">Row 1</td>"),
732 "Table row not found"
733 );
734 }
735
736 #[test]
740 fn test_generate_html_empty() {
741 let markdown = "";
742 let config = HtmlConfig::default();
743 let result = generate_html(markdown, &config);
744 assert!(result.is_ok());
745 let html = result.unwrap();
746 assert!(html.is_empty());
747 }
748
749 #[test]
754 fn test_generate_html_invalid_markdown() {
755 let markdown = "# Unclosed header\nSome **unclosed bold";
756 let config = HtmlConfig::default();
757 let result = generate_html(markdown, &config);
758 assert!(result.is_ok());
759 let html = result.unwrap();
760
761 println!("{}", html);
762
763 assert!(
764 html.contains("<h1>Unclosed header</h1>"),
765 "Header not found"
766 );
767 assert!(
768 html.contains("<p>Some **unclosed bold</p>"),
769 "Unclosed bold tag not properly handled"
770 );
771 }
772
773 #[test]
779 fn test_generate_html_complex() {
780 let markdown = r#"
781# Header
782
783## Subheader
784
785Some `inline code` and a [link](https://example.com).
786
787```rust
788fn main() {
789 println!("Hello, world!");
790}
791```
792
7931. First item
7942. Second item
795"#;
796 let config = HtmlConfig::default();
797 let result = generate_html(markdown, &config);
798 assert!(result.is_ok());
799 let html = result.unwrap();
800 println!("{}", html);
801
802 assert!(
804 html.contains("<h1>Header</h1>"),
805 "H1 Header not found"
806 );
807 assert!(
808 html.contains("<h2>Subheader</h2>"),
809 "H2 Header not found"
810 );
811
812 assert!(
814 html.contains("<code>inline code</code>"),
815 "Inline code not found"
816 );
817 assert!(
818 html.contains(r#"<a href="https://example.com">link</a>"#),
819 "Link not found"
820 );
821
822 assert!(
824 html.contains(r#"<code class="language-rust">"#),
825 "Code block with language-rust class not found"
826 );
827 assert!(
828 html.contains(r#"<span style="color:#b48ead;">fn </span>"#),
829 "`fn` keyword with syntax highlighting not found"
830 );
831 assert!(
832 html.contains(
833 r#"<span style="color:#8fa1b3;">main</span>"#
834 ),
835 "`main` function name with syntax highlighting not found"
836 );
837
838 assert!(
840 html.contains("<li>First item</li>"),
841 "First item not found"
842 );
843 assert!(
844 html.contains("<li>Second item</li>"),
845 "Second item not found"
846 );
847 }
848
849 #[test]
851 fn test_generate_html_with_valid_front_matter() {
852 let markdown = r#"---
853title: Test
854author: Jane Doe
855---
856# Hello, world!"#;
857 let config = HtmlConfig::default();
858 let result = generate_html(markdown, &config);
859 assert!(result.is_ok());
860 let html = result.unwrap();
861 assert!(html.contains("<h1>Hello, world!</h1>"));
862 }
863
864 #[test]
866 fn test_generate_html_with_invalid_front_matter() {
867 let markdown = r#"---
868title Test
869author: Jane Doe
870---
871# Hello, world!"#;
872 let config = HtmlConfig::default();
873 let result = generate_html(markdown, &config);
874 assert!(
875 result.is_ok(),
876 "Invalid front matter should be ignored"
877 );
878 let html = result.unwrap();
879 assert!(html.contains("<h1>Hello, world!</h1>"));
880 }
881
882 #[test]
884 fn test_generate_html_large_input() {
885 let markdown = "# Large Markdown\n\n".repeat(10_000);
886 let config = HtmlConfig::default();
887 let result = generate_html(&markdown, &config);
888 assert!(result.is_ok());
889 let html = result.unwrap();
890 assert!(html.contains("<h1>Large Markdown</h1>"));
891 }
892
893 #[test]
895 fn test_generate_html_with_custom_markdown_options() {
896 let markdown = "**Bold text**";
897 let config = HtmlConfig::default();
898 let result = generate_html(markdown, &config);
899 assert!(result.is_ok());
900 let html = result.unwrap();
901 assert!(html.contains("<strong>Bold text</strong>"));
902 }
903
904 #[test]
906 fn test_generate_html_with_unsupported_elements() {
907 let markdown = "::: custom_block\nContent\n:::";
908 let config = HtmlConfig::default();
909 let result = generate_html(markdown, &config);
910 assert!(result.is_ok());
911 let html = result.unwrap();
912 assert!(html.contains("::: custom_block"));
913 }
914
915 #[test]
917 fn test_markdown_to_html_with_conversion_error() {
918 let markdown = "# Unclosed header\nSome **unclosed bold";
919 let result = markdown_to_html_with_extensions(markdown);
920 assert!(result.is_ok());
921 let html = result.unwrap();
922 assert!(html.contains("<p>Some **unclosed bold</p>"));
923 }
924
925 #[test]
927 fn test_generate_html_whitespace_only() {
928 let markdown = " \n ";
929 let config = HtmlConfig::default();
930 let result = generate_html(markdown, &config);
931 assert!(result.is_ok());
932 let html = result.unwrap();
933 assert!(
934 html.is_empty(),
935 "Whitespace-only Markdown should produce empty HTML"
936 );
937 }
938
939 #[cfg(not(target_arch = "wasm32"))]
944 #[test]
945 fn test_markdown_to_html_with_custom_comrak_options() {
946 let markdown = "^^Superscript^^\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Row 1 | Row 2 |";
947
948 let mut comrak_options = Options::default();
950 comrak_options.extension.superscript = true;
951 comrak_options.extension.table = true; let options = MarkdownOptions::default()
955 .with_comrak_options(comrak_options.clone());
956 let content_without_front_matter =
957 extract_front_matter(markdown)
958 .unwrap_or(markdown.to_string());
959
960 println!("Comrak options: {:?}", comrak_options);
961
962 let result =
963 process_markdown(&content_without_front_matter, &options);
964
965 match result {
966 Ok(ref html) => {
967 assert!(
969 html.contains("<sup>Superscript</sup>"),
970 "Superscript not found in HTML output"
971 );
972
973 assert!(
975 html.contains("<table"),
976 "Table element not found in HTML output"
977 );
978 }
979 Err(err) => {
980 panic!(
981 "Failed to process Markdown with custom Options: {:?}",
982 err
983 );
984 }
985 }
986 }
987 #[test]
988 fn test_generate_html_with_default_config() {
989 let markdown = "# Default Configuration Test";
990 let config = HtmlConfig::default();
991 let result = generate_html(markdown, &config);
992 assert!(result.is_ok());
993 let html = result.unwrap();
994 assert!(html.contains("<h1>Default Configuration Test</h1>"));
995 }
996
997 #[test]
998 fn test_generate_html_with_custom_front_matter_delimiter() {
999 let markdown = r#";;;;
1000title: Custom
1001author: John Doe
1002;;;;
1003# Custom Front Matter Delimiter"#;
1004
1005 let config = HtmlConfig::default();
1006 let result = generate_html(markdown, &config);
1007 assert!(result.is_ok());
1008 let html = result.unwrap();
1009 assert!(html.contains("<h1>Custom Front Matter Delimiter</h1>"));
1010 }
1011 #[test]
1012 fn test_generate_html_with_task_list() {
1013 let markdown = r"
1014- [x] Task 1
1015- [ ] Task 2
1016";
1017
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!(
1026 html.contains(r#"<li><input type="checkbox" checked="" disabled="" /> Task 1</li>"#),
1027 "Task 1 checkbox not rendered as expected"
1028 );
1029 assert!(
1030 html.contains(r#"<li><input type="checkbox" disabled="" /> Task 2</li>"#),
1031 "Task 2 checkbox not rendered as expected"
1032 );
1033 }
1034 #[test]
1035 fn test_generate_html_with_large_table() {
1036 let header =
1037 "| Header 1 | Header 2 |\n| -------- | -------- |\n";
1038 let rows = "| Row 1 | Row 2 |\n".repeat(1000);
1039 let markdown = format!("{}{}", header, rows);
1040
1041 let result = markdown_to_html_with_extensions(&markdown);
1042 assert!(result.is_ok());
1043 let html = result.unwrap();
1044
1045 let row_count = html.matches("<tr>").count();
1046 assert_eq!(
1047 row_count, 1001,
1048 "Incorrect number of rows: {}",
1049 row_count
1050 ); }
1052 #[test]
1053 fn test_generate_html_with_special_characters() {
1054 let markdown = r#"Markdown with special characters: <, >, &, "quote", 'single-quote'."#;
1055 let result = markdown_to_html_with_extensions(markdown);
1056 assert!(result.is_ok());
1057 let html = result.unwrap();
1058
1059 assert!(html.contains("<"), "Less than sign not escaped");
1060 assert!(html.contains(">"), "Greater than sign not escaped");
1061 assert!(html.contains("&"), "Ampersand not escaped");
1062 assert!(html.contains("""), "Double quote not escaped");
1063
1064 assert!(
1066 html.contains("'") || html.contains("'"),
1067 "Single quote not handled as expected"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_generate_html_with_invalid_markdown_syntax() {
1073 let markdown =
1075 r"# Invalid Markdown <unexpected> [bad](url <here)";
1076 let result = markdown_to_html_with_extensions(markdown);
1077 assert!(result.is_ok());
1078 let html = result.unwrap();
1079
1080 println!("Generated HTML:\n{}", html);
1081
1082 assert!(html.contains("<h1>"), "Header tag should be present");
1084 }
1085
1086 #[test]
1088 fn test_generate_html_mixed_markdown() {
1089 let markdown = r"# Valid Header
1090Some **bold text** followed by invalid Markdown:
1091~~strikethrough~~ without a closing tag.";
1092 let result = markdown_to_html_with_extensions(markdown);
1093 assert!(result.is_ok());
1094 let html = result.unwrap();
1095
1096 assert!(
1097 html.contains("<h1>Valid Header</h1>"),
1098 "Header not found"
1099 );
1100 assert!(
1101 html.contains("<strong>bold text</strong>"),
1102 "Bold text not rendered correctly"
1103 );
1104 assert!(
1105 html.contains("<del>strikethrough</del>"),
1106 "Strikethrough not rendered correctly"
1107 );
1108 }
1109
1110 #[test]
1112 fn test_generate_html_deeply_nested_content() {
1113 let markdown = r"
11141. Level 1
1115 1.1. Level 2
1116 1.1.1. Level 3
1117 1.1.1.1. Level 4
1118";
1119 let result = markdown_to_html_with_extensions(markdown);
1120 assert!(result.is_ok());
1121 let html = result.unwrap();
1122
1123 assert!(html.contains("<ol>"), "Ordered list not rendered");
1124 assert!(html.contains("<li>Level 1"), "Level 1 not rendered");
1125 assert!(
1126 html.contains("1.1.1.1. Level 4"),
1127 "Deeply nested levels not rendered correctly"
1128 );
1129 }
1130
1131 #[test]
1133 fn test_generate_html_with_raw_html() {
1134 let markdown = r"
1135# Header with HTML
1136<p>This is a paragraph with <strong>HTML</strong>.</p>
1137";
1138 let config = HtmlConfig {
1140 allow_unsafe_html: true,
1141 ..HtmlConfig::default()
1142 };
1143 let result = generate_html(markdown, &config);
1144 assert!(result.is_ok());
1145 let html = result.unwrap();
1146
1147 assert!(
1148 html.contains("<p>This is a paragraph with <strong>HTML</strong>.</p>"),
1149 "Raw HTML content not preserved in output"
1150 );
1151 }
1152
1153 #[test]
1155 fn test_generate_html_invalid_front_matter_handling() {
1156 let markdown = "---
1157key_without_value
1158another_key: valid
1159---
1160# Markdown Content
1161";
1162 let result = generate_html(markdown, &HtmlConfig::default());
1163 assert!(
1164 result.is_ok(),
1165 "Invalid front matter should not cause an error"
1166 );
1167 let html = result.unwrap();
1168 assert!(
1169 html.contains("<h1>Markdown Content</h1>"),
1170 "Content not processed correctly"
1171 );
1172 }
1173
1174 #[test]
1176 fn test_generate_html_large_front_matter() {
1177 let front_matter = "---\n".to_owned()
1178 + &"key: value\n".repeat(10_000)
1179 + "---\n# Content";
1180 let result =
1181 generate_html(&front_matter, &HtmlConfig::default());
1182 assert!(
1183 result.is_ok(),
1184 "Large front matter should be handled gracefully"
1185 );
1186 let html = result.unwrap();
1187 assert!(
1188 html.contains("<h1>Content</h1>"),
1189 "Content not rendered correctly"
1190 );
1191 }
1192
1193 #[test]
1195 fn test_generate_html_with_long_lines() {
1196 let markdown = "A ".repeat(10_000);
1197 let result = markdown_to_html_with_extensions(&markdown);
1198 assert!(result.is_ok());
1199 let html = result.unwrap();
1200
1201 assert!(
1202 html.contains("A A A A"),
1203 "Long consecutive lines should be rendered properly"
1204 );
1205 }
1206
1207 #[test]
1208 fn test_markdown_with_custom_classes() {
1209 let markdown = r":::note
1210This is a note with a custom class.
1211:::";
1212
1213 let result = markdown_to_html_with_extensions(markdown);
1214 assert!(result.is_ok(), "Markdown conversion should not fail.");
1215
1216 let html = result.unwrap();
1217 println!("HTML:\n{}", html);
1218
1219 assert!(
1221 html.contains(r#"<div class="note">"#),
1222 "Custom block should wrap in <div class=\"note\">"
1223 );
1224
1225 assert!(
1227 html.contains("This is a note with a custom class."),
1228 "Block text is missing or incorrectly rendered"
1229 );
1230 }
1231
1232 #[test]
1233 fn test_markdown_with_custom_blocks_and_images() {
1234 let markdown = ".class=\"img-fluid\"";
1235 let result = markdown_to_html_with_extensions(markdown);
1236 assert!(result.is_ok());
1237 let html = result.unwrap();
1238 println!("{}", html);
1239 assert!(
1240 html.contains(r#"<img src="https://example.com/image.webp" alt="A very tall building" class="img-fluid" />"#),
1241 "First image not rendered correctly"
1242 );
1243 }
1244
1245 #[test]
1247 fn test_empty_front_matter_handling() {
1248 let markdown = "---\n---\n# Content";
1249 let result = generate_html(markdown, &HtmlConfig::default());
1250 assert!(result.is_ok());
1251 let html = result.unwrap();
1252 assert!(
1253 html.contains("<h1>Content</h1>"),
1254 "Content should be processed correctly"
1255 );
1256 }
1257
1258 #[cfg(not(target_arch = "wasm32"))]
1265 #[test]
1266 fn test_invalid_image_syntax() {
1267 let markdown = "![Image with missing URL]()";
1268 let mut tunnels = Vec::new();
1269 let result =
1270 process_images_with_classes(markdown, &mut tunnels);
1271 assert_eq!(
1272 result, markdown,
1273 "Invalid image syntax should remain unchanged"
1274 );
1275 assert!(
1276 tunnels.is_empty(),
1277 "No image fragments should be tunnelled for invalid syntax"
1278 );
1279 }
1280
1281 #[test]
1283 fn test_incorrect_front_matter_delimiters() {
1284 let markdown = ";;;\ntitle: Test\n---\n# Header";
1285 let result = generate_html(markdown, &HtmlConfig::default());
1286 assert!(result.is_ok());
1287 let html = result.unwrap();
1288 assert!(
1289 html.contains("<h1>Header</h1>"),
1290 "Header should be processed correctly"
1291 );
1292 }
1293 #[cfg(test)]
1294 mod missing_scenarios_tests {
1295 use super::*;
1296
1297 #[test]
1301 fn test_triple_colon_warning_with_bold() {
1302 let markdown = r":::warning
1303**Caution:** This operation is sensitive.
1304:::";
1305
1306 let result = markdown_to_html_with_extensions(markdown);
1307 assert!(
1308 result.is_ok(),
1309 "Markdown conversion should succeed."
1310 );
1311
1312 let html = result.unwrap();
1313 println!("HTML:\n{}", html);
1314
1315 assert!(
1318 html.contains(r#"<div class="warning">"#),
1319 "Expected <div class=\"warning\"> wrapping the block"
1320 );
1321 assert!(html.contains("<strong>Caution:</strong>"),
1322 "Expected inline bold text to become <strong>Caution:</strong>");
1323 }
1324
1325 #[test]
1329 fn test_multiple_triple_colon_blocks() {
1330 let markdown = r":::note
1331**Note:** First block
1332:::
1333
1334:::warning
1335**Warning:** Second block
1336:::";
1337
1338 let result = markdown_to_html_with_extensions(markdown);
1339 assert!(
1340 result.is_ok(),
1341 "Markdown conversion should succeed."
1342 );
1343
1344 let html = result.unwrap();
1345 println!("HTML:\n{}", html);
1346
1347 assert!(
1349 html.contains(r#"<div class="note">"#),
1350 "Missing <div class=\"note\"> for the first block"
1351 );
1352 assert!(
1353 html.contains(r#"<div class="warning">"#),
1354 "Missing <div class=\"warning\"> for the second block"
1355 );
1356
1357 assert!(
1359 html.contains("<strong>Note:</strong>"),
1360 "Bold text in the note block not parsed"
1361 );
1362 assert!(
1363 html.contains("<strong>Warning:</strong>"),
1364 "Bold text in the warning block not parsed"
1365 );
1366 }
1367
1368 #[test]
1372 fn test_triple_colon_block_multi_paragraph() {
1373 let markdown = r":::note
1374**Paragraph 1:** This is the first paragraph.
1375
1376This is the second paragraph, also with **bold** text.
1377:::";
1378
1379 let result = markdown_to_html_with_extensions(markdown);
1380 assert!(
1381 result.is_ok(),
1382 "Markdown conversion should succeed."
1383 );
1384
1385 let html = result.unwrap();
1386 println!("HTML:\n{}", html);
1387
1388 assert!(
1393 html.contains("<strong>Paragraph 1:</strong>"),
1394 "Inline bold text not parsed in the first paragraph"
1395 );
1396 assert!(html.contains("second paragraph, also with <strong>bold</strong> text"),
1397 "Inline bold text not parsed in the second paragraph");
1398 }
1399
1400 #[test]
1405 fn test_triple_colon_block_forcing_inline_error() {
1406 let markdown = r":::error
1409This block tries < to break > inline parsing & [some link (unclosed).
1410:::";
1411
1412 let result = markdown_to_html_with_extensions(markdown);
1418 assert!(
1419 result.is_ok(),
1420 "We won't forcibly error, but let's see the output."
1421 );
1422
1423 let html = result.unwrap();
1424 println!("HTML:\n{}", html);
1425
1426 assert!(
1430 html.contains(r#"<div class="error">"#),
1431 "Block div not found for 'error' class"
1432 );
1433
1434 assert!(
1438 html.contains("This block tries "),
1439 "Expected parsed content in the block"
1440 );
1441 }
1442 }
1443}