html_generator/
generator.rs

1// Copyright © 2025 HTML Generator. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! HTML generation module for converting Markdown to HTML.
5//!
6//! This module provides functions to generate HTML from Markdown content
7//! using the `mdx-gen` library. It supports various Markdown extensions
8//! and custom configuration options.
9
10use crate::{error::HtmlError, extract_front_matter, Result};
11use mdx_gen::{process_markdown, ComrakOptions, MarkdownOptions};
12use regex::Regex;
13use std::error::Error;
14
15/// Generate HTML from Markdown content using `mdx-gen`.
16///
17/// This function takes Markdown content and a configuration object,
18/// converts the Markdown into HTML, and returns the resulting HTML string.
19pub fn generate_html(
20    markdown: &str,
21    _config: &crate::HtmlConfig,
22) -> Result<String> {
23    markdown_to_html_with_extensions(markdown)
24}
25
26/// Convert Markdown to HTML with specified extensions using `mdx-gen`.
27pub fn markdown_to_html_with_extensions(
28    markdown: &str,
29) -> Result<String> {
30    // 1) Extract front matter
31    let content_without_front_matter = extract_front_matter(markdown)
32        .unwrap_or_else(|_| markdown.to_string());
33
34    // 2) Convert triple-colon blocks, re-parsing inline Markdown inside them
35    let markdown_with_classes =
36        add_custom_classes(&content_without_front_matter);
37
38    // 3) Convert images with `.class="..."`
39    let markdown_with_images =
40        process_images_with_classes(&markdown_with_classes);
41
42    // 4) Configure Comrak/Markdown Options
43    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; // raw HTML allowed
51    comrak_options.render.escape = false;
52
53    let options =
54        MarkdownOptions::default().with_comrak_options(comrak_options);
55
56    // 5) Convert final Markdown to HTML
57    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
65/// Re-parse inline Markdown for triple-colon blocks, e.g.:
66///
67/// ```markdown
68/// :::warning
69/// **Caution:** This is risky.
70/// :::
71/// ```
72///
73/// Produces something like:
74/// ```html
75/// <div class="warning"><strong>Caution:</strong> This is risky.</div>
76/// ```
77///
78/// # Example
79/// ...
80fn add_custom_classes(markdown: &str) -> String {
81    // Regex that matches:
82    //   :::<class_name>\n
83    //   (block content, possibly multiline)
84    //   \n:::
85    let re = Regex::new(r":::(\w+)\n([\s\S]*?)\n:::").unwrap();
86
87    re.replace_all(markdown, |caps: &regex::Captures| {
88        let class_name = &caps[1];
89        let block_content = &caps[2];
90
91        // Re-parse inline Markdown syntax within the block content
92        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
107/// Processes inline Markdown (bold, italics, links, etc.) without block-level syntax.
108pub 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; // raw HTML allowed
120    comrak_opts.render.escape = false;
121
122    // mdx_gen::process_markdown_inline(...) only parses inline syntax, not block-level
123    let options =
124        MarkdownOptions::default().with_comrak_options(comrak_opts);
125    let inline_html = process_markdown(content, &options)?;
126    Ok(inline_html)
127}
128
129/// Replaces image patterns like
130/// `![Alt text](URL).class="some-class"` with `<img src="URL" alt="Alt text" class="some-class" />`.
131fn process_images_with_classes(markdown: &str) -> String {
132    let re =
133        Regex::new(r#"!\[(.*?)\]\((.*?)\)\.class="(.*?)""#).unwrap();
134    re.replace_all(markdown, |caps: &regex::Captures| {
135        format!(
136            r#"<img src="{}" alt="{}" class="{}" />"#,
137            &caps[2], // URL
138            &caps[1], // alt text
139            &caps[3], // class attribute
140        )
141    })
142    .to_string()
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::HtmlConfig;
149
150    /// Test basic Markdown to HTML conversion.
151    ///
152    /// This test verifies that a simple Markdown input is correctly converted to HTML.
153    #[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 conversion with Markdown extensions.
165    ///
166    /// This test ensures that the Markdown extensions (e.g., custom blocks, enhanced tables, etc.)
167    /// are correctly applied when converting Markdown to HTML.
168    #[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        // Update the test to look for the div wrapper and table classes
182        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 conversion of empty Markdown.
194    ///
195    /// This test checks that an empty Markdown input results in an empty HTML string.
196    #[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 handling of invalid Markdown.
207    ///
208    /// This test verifies that even with poorly formatted Markdown, the function
209    /// will not panic and will return valid HTML.
210    #[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 conversion with complex Markdown content.
231    ///
232    /// This test checks how the function handles more complex Markdown input with various
233    /// elements like lists, headers, code blocks, and links.
234    /// Test conversion with complex Markdown content.
235    #[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        // Verify the header and subheader
260        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        // Verify the inline code and link
270        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        // Verify the code block structure
280        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        // Check for the ordered list items
296        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 handling of valid front matter.
307    #[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 handling of invalid front matter.
322    #[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 with a large Markdown input.
340    #[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 with different MarkdownOptions configurations.
351    #[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 unsupported Markdown elements.
362    #[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 error handling for invalid Markdown conversion.
373    #[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 handling of whitespace-only Markdown.
383    #[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 customization of ComrakOptions.
397    #[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        // Configure ComrakOptions with necessary extensions
402        let mut comrak_options = ComrakOptions::default();
403        comrak_options.extension.superscript = true;
404        comrak_options.extension.table = true; // Enable table to match MarkdownOptions
405
406        // Synchronize MarkdownOptions with ComrakOptions
407        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 superscript rendering
421                assert!(
422                    html.contains("<sup>Superscript</sup>"),
423                    "Superscript not found in HTML output"
424                );
425
426                // Assert table rendering
427                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        // Adjust assertions to match the rendered HTML structure
476        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        ); // 1 header + 1000 rows
502    }
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("&lt;"), "Less than sign not escaped");
511        assert!(html.contains("&gt;"), "Greater than sign not escaped");
512        assert!(html.contains("&amp;"), "Ampersand not escaped");
513        assert!(html.contains("&quot;"), "Double quote not escaped");
514
515        // Adjust if single quotes are intended to remain unescaped
516        assert!(
517            html.contains("&#39;") || 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        // Validate that raw HTML tags are not escaped
533        assert!(
534            html.contains("<unexpected>"),
535            "Raw HTML tags like <unexpected> should not be escaped"
536        );
537
538        // Validate that angle brackets in links are escaped
539        assert!(
540            html.contains("&lt;here&gt;") || html.contains("&lt;here)"),
541            "Angle brackets in links should be escaped for safety"
542        );
543
544        // Validate the full header content
545        assert!(
546        html.contains("<h1>Invalid Markdown <unexpected> [bad](url &lt;here)</h1>"),
547        "Header not rendered correctly or content not properly handled"
548    );
549    }
550
551    /// Test handling of Markdown with a mix of valid and invalid syntax.
552    #[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 handling of deeply nested Markdown content.
576    #[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 Markdown with embedded raw HTML content.
597    #[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 Markdown with invalid front matter format.
614    #[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 handling of very large front matter in Markdown.
635    #[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 handling of Markdown with long consecutive lines.
654    #[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        // Ensure we see <div class="note"> in the final output:
680        assert!(
681            html.contains(r#"<div class="note">"#),
682            "Custom block should wrap in <div class=\"note\">"
683        );
684
685        // Ensure the block content is present:
686        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 = "![A very tall building](https://example.com/image.webp).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 empty front matter handling.
706    #[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 invalid image syntax.
719    #[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 incorrect front matter delimiters.
730    #[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        /// 1) Triple-colon block with inline bold text
746        ///
747        /// Verifies that **Caution:** inside `:::warning` is parsed as `<strong>Caution:</strong>`.
748        #[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            // Expect the block to contain <strong>Caution:</strong>
764            // plus a <div class="warning">
765            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        /// 2) Multiple triple-colon blocks in the same snippet.
774        ///
775        /// Ensures that the parser correctly handles more than one custom block.
776        #[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            // Expect <div class="note"> ...</div> and <div class="warning"> ...</div>
796            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            // Check inline markdown
806            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        /// 3) Triple-colon block with multi-paragraph content
817        ///
818        /// Checks how inline parsing deals with extra blank lines and multiple paragraphs.
819        #[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            // The block is inline-processed. Paragraphs might be combined or
837            // each appear in separate <p> tags, depending on the parser.
838            // Typically, inline parsing doesn't break paragraphs. If you want block-level
839            // formatting, you'd need a full block parse. But let's at least confirm bold text.
840            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        /// 4) Fallback logic: forcing an error in `process_markdown_inline`
849        ///
850        /// We'll create a scenario that intentionally breaks the inline parser.
851        /// If an error occurs, we expect the raw text (with triple-colon block content).
852        #[test]
853        fn test_triple_colon_block_forcing_inline_error() {
854            // Suppose the inline parser fails when we pass some nonsense markup or unhandled structure.
855            // It's not always guaranteed to fail, but let's try an improbable snippet:
856            let markdown = r":::error
857This block tries < to break > inline parsing & [some link (unclosed).
858:::";
859
860            // We'll artificially modify the parser to fail if it sees "[some link (unclosed)."
861            // But since your code doesn't do that by default, we can't *guarantee* a real error.
862            // We'll at least check that, if an error *did* occur, we fallback to raw text.
863            //
864            // For demonstration, let's proceed with the test and see if it just parses or not.
865            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            // If your parser did handle it, we'll just check the block.
875            // If your parser chokes, you'd see a fallback with raw text.
876            // Let's verify there's a <div class="error"> either way:
877            assert!(
878                html.contains(r#"<div class="error">"#),
879                "Block div not found for 'error' class"
880            );
881
882            // If the inline parser didn't fail, we might see <p> with weird text.
883            // If it fails, we should see the original snippet inside the block.
884            // We'll just check that it's not empty.
885            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}