1use crate::{error::HtmlError, extract_front_matter, Result};
11use mdx_gen::{process_markdown, ComrakOptions, MarkdownOptions};
12use regex::Regex;
13use std::error::Error;
14
15pub fn generate_html(
20 markdown: &str,
21 _config: &crate::HtmlConfig,
22) -> Result<String> {
23 markdown_to_html_with_extensions(markdown)
24}
25
26pub fn markdown_to_html_with_extensions(
28 markdown: &str,
29) -> Result<String> {
30 let content_without_front_matter = extract_front_matter(markdown)
32 .unwrap_or_else(|_| markdown.to_string());
33
34 let markdown_with_classes =
36 add_custom_classes(&content_without_front_matter);
37
38 let markdown_with_images =
40 process_images_with_classes(&markdown_with_classes);
41
42 let mut comrak_options = ComrakOptions::default();
44 comrak_options.extension.strikethrough = true;
45 comrak_options.extension.table = true;
46 comrak_options.extension.autolink = true;
47 comrak_options.extension.tasklist = true;
48 comrak_options.extension.superscript = true;
49
50 comrak_options.render.unsafe_ = true; comrak_options.render.escape = false;
52
53 let options =
54 MarkdownOptions::default().with_comrak_options(comrak_options);
55
56 match process_markdown(&markdown_with_images, &options) {
58 Ok(html_output) => Ok(html_output),
59 Err(err) => {
60 Err(HtmlError::markdown_conversion(err.to_string(), None))
61 }
62 }
63}
64
65fn add_custom_classes(markdown: &str) -> String {
81 let re = Regex::new(r":::(\w+)\n([\s\S]*?)\n:::").unwrap();
86
87 re.replace_all(markdown, |caps: ®ex::Captures| {
88 let class_name = &caps[1];
89 let block_content = &caps[2];
90
91 let inline_html = match process_markdown_inline(block_content) {
93 Ok(html) => html,
94 Err(err) => {
95 eprintln!(
96 "Warning: failed to parse inline block content. Using raw text. Error: {err}"
97 );
98 block_content.to_string()
99 }
100 };
101
102 format!("<div class=\"{}\">{}</div>", class_name, inline_html)
103 })
104 .to_string()
105}
106
107pub fn process_markdown_inline(
109 content: &str,
110) -> std::result::Result<String, Box<dyn Error>> {
111 let mut comrak_opts = ComrakOptions::default();
112
113 comrak_opts.extension.strikethrough = true;
114 comrak_opts.extension.table = true;
115 comrak_opts.extension.autolink = true;
116 comrak_opts.extension.tasklist = true;
117 comrak_opts.extension.superscript = true;
118
119 comrak_opts.render.unsafe_ = true; comrak_opts.render.escape = false;
121
122 let options =
124 MarkdownOptions::default().with_comrak_options(comrak_opts);
125 let inline_html = process_markdown(content, &options)?;
126 Ok(inline_html)
127}
128
129fn process_images_with_classes(markdown: &str) -> String {
132 let re =
133 Regex::new(r#"!\[(.*?)\]\((.*?)\)\.class="(.*?)""#).unwrap();
134 re.replace_all(markdown, |caps: ®ex::Captures| {
135 format!(
136 r#"<img src="{}" alt="{}" class="{}" />"#,
137 &caps[2], &caps[1], &caps[3], )
141 })
142 .to_string()
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::HtmlConfig;
149
150 #[test]
154 fn test_generate_html_basic() {
155 let markdown = "# Hello, world!\n\nThis is a test.";
156 let config = HtmlConfig::default();
157 let result = generate_html(markdown, &config);
158 assert!(result.is_ok());
159 let html = result.unwrap();
160 assert!(html.contains("<h1>Hello, world!</h1>"));
161 assert!(html.contains("<p>This is a test.</p>"));
162 }
163
164 #[test]
169 fn test_markdown_to_html_with_extensions() {
170 let markdown = r"
171| Header 1 | Header 2 |
172| -------- | -------- |
173| Row 1 | Row 2 |
174";
175 let result = markdown_to_html_with_extensions(markdown);
176 assert!(result.is_ok());
177 let html = result.unwrap();
178
179 println!("{}", html);
180
181 assert!(html.contains("<div class=\"table-responsive\"><table class=\"table\">"), "Table element not found");
183 assert!(
184 html.contains("<th>Header 1</th>"),
185 "Table header not found"
186 );
187 assert!(
188 html.contains("<td class=\"text-left\">Row 1</td>"),
189 "Table row not found"
190 );
191 }
192
193 #[test]
197 fn test_generate_html_empty() {
198 let markdown = "";
199 let config = HtmlConfig::default();
200 let result = generate_html(markdown, &config);
201 assert!(result.is_ok());
202 let html = result.unwrap();
203 assert!(html.is_empty());
204 }
205
206 #[test]
211 fn test_generate_html_invalid_markdown() {
212 let markdown = "# Unclosed header\nSome **unclosed bold";
213 let config = HtmlConfig::default();
214 let result = generate_html(markdown, &config);
215 assert!(result.is_ok());
216 let html = result.unwrap();
217
218 println!("{}", html);
219
220 assert!(
221 html.contains("<h1>Unclosed header</h1>"),
222 "Header not found"
223 );
224 assert!(
225 html.contains("<p>Some **unclosed bold</p>"),
226 "Unclosed bold tag not properly handled"
227 );
228 }
229
230 #[test]
236 fn test_generate_html_complex() {
237 let markdown = r#"
238# Header
239
240## Subheader
241
242Some `inline code` and a [link](https://example.com).
243
244```rust
245fn main() {
246 println!("Hello, world!");
247}
248```
249
2501. First item
2512. Second item
252"#;
253 let config = HtmlConfig::default();
254 let result = generate_html(markdown, &config);
255 assert!(result.is_ok());
256 let html = result.unwrap();
257 println!("{}", html);
258
259 assert!(
261 html.contains("<h1>Header</h1>"),
262 "H1 Header not found"
263 );
264 assert!(
265 html.contains("<h2>Subheader</h2>"),
266 "H2 Header not found"
267 );
268
269 assert!(
271 html.contains("<code>inline code</code>"),
272 "Inline code not found"
273 );
274 assert!(
275 html.contains(r#"<a href="https://example.com">link</a>"#),
276 "Link not found"
277 );
278
279 assert!(
281 html.contains(r#"<code class="language-rust">"#),
282 "Code block with language-rust class not found"
283 );
284 assert!(
285 html.contains(r#"<span style="color:#b48ead;">fn </span>"#),
286 "`fn` keyword with syntax highlighting not found"
287 );
288 assert!(
289 html.contains(
290 r#"<span style="color:#8fa1b3;">main</span>"#
291 ),
292 "`main` function name with syntax highlighting not found"
293 );
294
295 assert!(
297 html.contains("<li>First item</li>"),
298 "First item not found"
299 );
300 assert!(
301 html.contains("<li>Second item</li>"),
302 "Second item not found"
303 );
304 }
305
306 #[test]
308 fn test_generate_html_with_valid_front_matter() {
309 let markdown = r#"---
310title: Test
311author: Jane Doe
312---
313# Hello, world!"#;
314 let config = HtmlConfig::default();
315 let result = generate_html(markdown, &config);
316 assert!(result.is_ok());
317 let html = result.unwrap();
318 assert!(html.contains("<h1>Hello, world!</h1>"));
319 }
320
321 #[test]
323 fn test_generate_html_with_invalid_front_matter() {
324 let markdown = r#"---
325title Test
326author: Jane Doe
327---
328# Hello, world!"#;
329 let config = HtmlConfig::default();
330 let result = generate_html(markdown, &config);
331 assert!(
332 result.is_ok(),
333 "Invalid front matter should be ignored"
334 );
335 let html = result.unwrap();
336 assert!(html.contains("<h1>Hello, world!</h1>"));
337 }
338
339 #[test]
341 fn test_generate_html_large_input() {
342 let markdown = "# Large Markdown\n\n".repeat(10_000);
343 let config = HtmlConfig::default();
344 let result = generate_html(&markdown, &config);
345 assert!(result.is_ok());
346 let html = result.unwrap();
347 assert!(html.contains("<h1>Large Markdown</h1>"));
348 }
349
350 #[test]
352 fn test_generate_html_with_custom_markdown_options() {
353 let markdown = "**Bold text**";
354 let config = HtmlConfig::default();
355 let result = generate_html(markdown, &config);
356 assert!(result.is_ok());
357 let html = result.unwrap();
358 assert!(html.contains("<strong>Bold text</strong>"));
359 }
360
361 #[test]
363 fn test_generate_html_with_unsupported_elements() {
364 let markdown = "::: custom_block\nContent\n:::";
365 let config = HtmlConfig::default();
366 let result = generate_html(markdown, &config);
367 assert!(result.is_ok());
368 let html = result.unwrap();
369 assert!(html.contains("::: custom_block"));
370 }
371
372 #[test]
374 fn test_markdown_to_html_with_conversion_error() {
375 let markdown = "# Unclosed header\nSome **unclosed bold";
376 let result = markdown_to_html_with_extensions(markdown);
377 assert!(result.is_ok());
378 let html = result.unwrap();
379 assert!(html.contains("<p>Some **unclosed bold</p>"));
380 }
381
382 #[test]
384 fn test_generate_html_whitespace_only() {
385 let markdown = " \n ";
386 let config = HtmlConfig::default();
387 let result = generate_html(markdown, &config);
388 assert!(result.is_ok());
389 let html = result.unwrap();
390 assert!(
391 html.is_empty(),
392 "Whitespace-only Markdown should produce empty HTML"
393 );
394 }
395
396 #[test]
398 fn test_markdown_to_html_with_custom_comrak_options() {
399 let markdown = "^^Superscript^^\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Row 1 | Row 2 |";
400
401 let mut comrak_options = ComrakOptions::default();
403 comrak_options.extension.superscript = true;
404 comrak_options.extension.table = true; let options = MarkdownOptions::default()
408 .with_comrak_options(comrak_options.clone());
409 let content_without_front_matter =
410 extract_front_matter(markdown)
411 .unwrap_or(markdown.to_string());
412
413 println!("Comrak options: {:?}", comrak_options);
414
415 let result =
416 process_markdown(&content_without_front_matter, &options);
417
418 match result {
419 Ok(ref html) => {
420 assert!(
422 html.contains("<sup>Superscript</sup>"),
423 "Superscript not found in HTML output"
424 );
425
426 assert!(
428 html.contains("<table"),
429 "Table element not found in HTML output"
430 );
431 }
432 Err(err) => {
433 eprintln!("Markdown processing error: {:?}", err);
434 panic!("Failed to process Markdown with custom ComrakOptions");
435 }
436 }
437 }
438 #[test]
439 fn test_generate_html_with_default_config() {
440 let markdown = "# Default Configuration Test";
441 let config = HtmlConfig::default();
442 let result = generate_html(markdown, &config);
443 assert!(result.is_ok());
444 let html = result.unwrap();
445 assert!(html.contains("<h1>Default Configuration Test</h1>"));
446 }
447
448 #[test]
449 fn test_generate_html_with_custom_front_matter_delimiter() {
450 let markdown = r#";;;;
451title: Custom
452author: John Doe
453;;;;
454# Custom Front Matter Delimiter"#;
455
456 let config = HtmlConfig::default();
457 let result = generate_html(markdown, &config);
458 assert!(result.is_ok());
459 let html = result.unwrap();
460 assert!(html.contains("<h1>Custom Front Matter Delimiter</h1>"));
461 }
462 #[test]
463 fn test_generate_html_with_task_list() {
464 let markdown = r"
465- [x] Task 1
466- [ ] Task 2
467";
468
469 let result = markdown_to_html_with_extensions(markdown);
470 assert!(result.is_ok());
471 let html = result.unwrap();
472
473 println!("Generated HTML:\n{}", html);
474
475 assert!(
477 html.contains(r#"<li><input type="checkbox" checked="" disabled="" /> Task 1</li>"#),
478 "Task 1 checkbox not rendered as expected"
479 );
480 assert!(
481 html.contains(r#"<li><input type="checkbox" disabled="" /> Task 2</li>"#),
482 "Task 2 checkbox not rendered as expected"
483 );
484 }
485 #[test]
486 fn test_generate_html_with_large_table() {
487 let header =
488 "| Header 1 | Header 2 |\n| -------- | -------- |\n";
489 let rows = "| Row 1 | Row 2 |\n".repeat(1000);
490 let markdown = format!("{}{}", header, rows);
491
492 let result = markdown_to_html_with_extensions(&markdown);
493 assert!(result.is_ok());
494 let html = result.unwrap();
495
496 let row_count = html.matches("<tr>").count();
497 assert_eq!(
498 row_count, 1001,
499 "Incorrect number of rows: {}",
500 row_count
501 ); }
503 #[test]
504 fn test_generate_html_with_special_characters() {
505 let markdown = r#"Markdown with special characters: <, >, &, "quote", 'single-quote'."#;
506 let result = markdown_to_html_with_extensions(markdown);
507 assert!(result.is_ok());
508 let html = result.unwrap();
509
510 assert!(html.contains("<"), "Less than sign not escaped");
511 assert!(html.contains(">"), "Greater than sign not escaped");
512 assert!(html.contains("&"), "Ampersand not escaped");
513 assert!(html.contains("""), "Double quote not escaped");
514
515 assert!(
517 html.contains("'") || html.contains("'"),
518 "Single quote not handled as expected"
519 );
520 }
521
522 #[test]
523 fn test_generate_html_with_invalid_markdown_syntax() {
524 let markdown =
525 r"# Invalid Markdown <unexpected> [bad](url <here)";
526 let result = markdown_to_html_with_extensions(markdown);
527 assert!(result.is_ok());
528 let html = result.unwrap();
529
530 println!("Generated HTML:\n{}", html);
531
532 assert!(
534 html.contains("<unexpected>"),
535 "Raw HTML tags like <unexpected> should not be escaped"
536 );
537
538 assert!(
540 html.contains("<here>") || html.contains("<here)"),
541 "Angle brackets in links should be escaped for safety"
542 );
543
544 assert!(
546 html.contains("<h1>Invalid Markdown <unexpected> [bad](url <here)</h1>"),
547 "Header not rendered correctly or content not properly handled"
548 );
549 }
550
551 #[test]
553 fn test_generate_html_mixed_markdown() {
554 let markdown = r"# Valid Header
555Some **bold text** followed by invalid Markdown:
556~~strikethrough~~ without a closing tag.";
557 let result = markdown_to_html_with_extensions(markdown);
558 assert!(result.is_ok());
559 let html = result.unwrap();
560
561 assert!(
562 html.contains("<h1>Valid Header</h1>"),
563 "Header not found"
564 );
565 assert!(
566 html.contains("<strong>bold text</strong>"),
567 "Bold text not rendered correctly"
568 );
569 assert!(
570 html.contains("<del>strikethrough</del>"),
571 "Strikethrough not rendered correctly"
572 );
573 }
574
575 #[test]
577 fn test_generate_html_deeply_nested_content() {
578 let markdown = r"
5791. Level 1
580 1.1. Level 2
581 1.1.1. Level 3
582 1.1.1.1. Level 4
583";
584 let result = markdown_to_html_with_extensions(markdown);
585 assert!(result.is_ok());
586 let html = result.unwrap();
587
588 assert!(html.contains("<ol>"), "Ordered list not rendered");
589 assert!(html.contains("<li>Level 1"), "Level 1 not rendered");
590 assert!(
591 html.contains("1.1.1.1. Level 4"),
592 "Deeply nested levels not rendered correctly"
593 );
594 }
595
596 #[test]
598 fn test_generate_html_with_raw_html() {
599 let markdown = r"
600# Header with HTML
601<p>This is a paragraph with <strong>HTML</strong>.</p>
602";
603 let result = markdown_to_html_with_extensions(markdown);
604 assert!(result.is_ok());
605 let html = result.unwrap();
606
607 assert!(
608 html.contains("<p>This is a paragraph with <strong>HTML</strong>.</p>"),
609 "Raw HTML content not preserved in output"
610 );
611 }
612
613 #[test]
615 fn test_generate_html_invalid_front_matter_handling() {
616 let markdown = "---
617key_without_value
618another_key: valid
619---
620# Markdown Content
621";
622 let result = generate_html(markdown, &HtmlConfig::default());
623 assert!(
624 result.is_ok(),
625 "Invalid front matter should not cause an error"
626 );
627 let html = result.unwrap();
628 assert!(
629 html.contains("<h1>Markdown Content</h1>"),
630 "Content not processed correctly"
631 );
632 }
633
634 #[test]
636 fn test_generate_html_large_front_matter() {
637 let front_matter = "---\n".to_owned()
638 + &"key: value\n".repeat(10_000)
639 + "---\n# Content";
640 let result =
641 generate_html(&front_matter, &HtmlConfig::default());
642 assert!(
643 result.is_ok(),
644 "Large front matter should be handled gracefully"
645 );
646 let html = result.unwrap();
647 assert!(
648 html.contains("<h1>Content</h1>"),
649 "Content not rendered correctly"
650 );
651 }
652
653 #[test]
655 fn test_generate_html_with_long_lines() {
656 let markdown = "A ".repeat(10_000);
657 let result = markdown_to_html_with_extensions(&markdown);
658 assert!(result.is_ok());
659 let html = result.unwrap();
660
661 assert!(
662 html.contains("A A A A"),
663 "Long consecutive lines should be rendered properly"
664 );
665 }
666
667 #[test]
668 fn test_markdown_with_custom_classes() {
669 let markdown = r":::note
670This is a note with a custom class.
671:::";
672
673 let result = markdown_to_html_with_extensions(markdown);
674 assert!(result.is_ok(), "Markdown conversion should not fail.");
675
676 let html = result.unwrap();
677 println!("HTML:\n{}", html);
678
679 assert!(
681 html.contains(r#"<div class="note">"#),
682 "Custom block should wrap in <div class=\"note\">"
683 );
684
685 assert!(
687 html.contains("This is a note with a custom class."),
688 "Block text is missing or incorrectly rendered"
689 );
690 }
691
692 #[test]
693 fn test_markdown_with_custom_blocks_and_images() {
694 let markdown = ".class=\"img-fluid\"";
695 let result = markdown_to_html_with_extensions(markdown);
696 assert!(result.is_ok());
697 let html = result.unwrap();
698 println!("{}", html);
699 assert!(
700 html.contains(r#"<img src="https://example.com/image.webp" alt="A very tall building" class="img-fluid" />"#),
701 "First image not rendered correctly"
702 );
703 }
704
705 #[test]
707 fn test_empty_front_matter_handling() {
708 let markdown = "---\n---\n# Content";
709 let result = generate_html(markdown, &HtmlConfig::default());
710 assert!(result.is_ok());
711 let html = result.unwrap();
712 assert!(
713 html.contains("<h1>Content</h1>"),
714 "Content should be processed correctly"
715 );
716 }
717
718 #[test]
720 fn test_invalid_image_syntax() {
721 let markdown = "![Image with missing URL]()";
722 let result = process_images_with_classes(markdown);
723 assert_eq!(
724 result, markdown,
725 "Invalid image syntax should remain unchanged"
726 );
727 }
728
729 #[test]
731 fn test_incorrect_front_matter_delimiters() {
732 let markdown = ";;;\ntitle: Test\n---\n# Header";
733 let result = generate_html(markdown, &HtmlConfig::default());
734 assert!(result.is_ok());
735 let html = result.unwrap();
736 assert!(
737 html.contains("<h1>Header</h1>"),
738 "Header should be processed correctly"
739 );
740 }
741 #[cfg(test)]
742 mod missing_scenarios_tests {
743 use super::*;
744
745 #[test]
749 fn test_triple_colon_warning_with_bold() {
750 let markdown = r":::warning
751**Caution:** This operation is sensitive.
752:::";
753
754 let result = markdown_to_html_with_extensions(markdown);
755 assert!(
756 result.is_ok(),
757 "Markdown conversion should succeed."
758 );
759
760 let html = result.unwrap();
761 println!("HTML:\n{}", html);
762
763 assert!(
766 html.contains(r#"<div class="warning">"#),
767 "Expected <div class=\"warning\"> wrapping the block"
768 );
769 assert!(html.contains("<strong>Caution:</strong>"),
770 "Expected inline bold text to become <strong>Caution:</strong>");
771 }
772
773 #[test]
777 fn test_multiple_triple_colon_blocks() {
778 let markdown = r":::note
779**Note:** First block
780:::
781
782:::warning
783**Warning:** Second block
784:::";
785
786 let result = markdown_to_html_with_extensions(markdown);
787 assert!(
788 result.is_ok(),
789 "Markdown conversion should succeed."
790 );
791
792 let html = result.unwrap();
793 println!("HTML:\n{}", html);
794
795 assert!(
797 html.contains(r#"<div class="note">"#),
798 "Missing <div class=\"note\"> for the first block"
799 );
800 assert!(
801 html.contains(r#"<div class="warning">"#),
802 "Missing <div class=\"warning\"> for the second block"
803 );
804
805 assert!(
807 html.contains("<strong>Note:</strong>"),
808 "Bold text in the note block not parsed"
809 );
810 assert!(
811 html.contains("<strong>Warning:</strong>"),
812 "Bold text in the warning block not parsed"
813 );
814 }
815
816 #[test]
820 fn test_triple_colon_block_multi_paragraph() {
821 let markdown = r":::note
822**Paragraph 1:** This is the first paragraph.
823
824This is the second paragraph, also with **bold** text.
825:::";
826
827 let result = markdown_to_html_with_extensions(markdown);
828 assert!(
829 result.is_ok(),
830 "Markdown conversion should succeed."
831 );
832
833 let html = result.unwrap();
834 println!("HTML:\n{}", html);
835
836 assert!(
841 html.contains("<strong>Paragraph 1:</strong>"),
842 "Inline bold text not parsed in the first paragraph"
843 );
844 assert!(html.contains("second paragraph, also with <strong>bold</strong> text"),
845 "Inline bold text not parsed in the second paragraph");
846 }
847
848 #[test]
853 fn test_triple_colon_block_forcing_inline_error() {
854 let markdown = r":::error
857This block tries < to break > inline parsing & [some link (unclosed).
858:::";
859
860 let result = markdown_to_html_with_extensions(markdown);
866 assert!(
867 result.is_ok(),
868 "We won't forcibly error, but let's see the output."
869 );
870
871 let html = result.unwrap();
872 println!("HTML:\n{}", html);
873
874 assert!(
878 html.contains(r#"<div class="error">"#),
879 "Block div not found for 'error' class"
880 );
881
882 assert!(html.contains("This block tries ") || html.contains("Warning: failed to parse inline block content"),
886 "Expected either parsed content or a fallback error message");
887 }
888 }
889}