html_generator/
lib.rs

1// Copyright © 2025 HTML Generator. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4#![doc = include_str!("../README.md")]
5#![doc(
6    html_favicon_url = "https://kura.pro/html-generator/images/favicon.ico",
7    html_logo_url = "https://kura.pro/html-generator/images/logos/html-generator.svg",
8    html_root_url = "https://docs.rs/html-generator"
9)]
10#![crate_name = "html_generator"]
11#![crate_type = "lib"]
12
13use std::{
14    fmt,
15    fs::File,
16    io::{self, BufReader, BufWriter, Read, Write},
17    path::{Component, Path},
18};
19
20/// Maximum buffer size for reading files (16MB)
21const MAX_BUFFER_SIZE: usize = 16 * 1024 * 1024;
22
23// Re-export public modules
24pub mod accessibility;
25pub mod emojis;
26pub mod error;
27pub mod generator;
28pub mod performance;
29pub mod seo;
30pub mod utils;
31
32// Re-export primary types and functions for convenience
33pub use crate::error::HtmlError;
34pub use accessibility::{add_aria_attributes, validate_wcag};
35pub use emojis::load_emoji_sequences;
36pub use generator::generate_html;
37pub use performance::{async_generate_html, minify_html};
38pub use seo::{generate_meta_tags, generate_structured_data};
39pub use utils::{extract_front_matter, format_header_with_id_class};
40
41/// Common constants used throughout the library.
42///
43/// This module contains configuration values and limits that help ensure
44/// secure and efficient operation of the library.
45pub mod constants {
46    /// Maximum allowed input size (5MB) to prevent denial of service attacks
47    pub const DEFAULT_MAX_INPUT_SIZE: usize = 5 * 1024 * 1024;
48
49    /// Minimum required input size (1KB) for meaningful processing
50    pub const MIN_INPUT_SIZE: usize = 1024;
51
52    /// Default language code for HTML generation (British English)
53    pub const DEFAULT_LANGUAGE: &str = "en-GB";
54
55    /// Default syntax highlighting theme (github)
56    pub const DEFAULT_SYNTAX_THEME: &str = "github";
57
58    /// Maximum file path length
59    pub const MAX_PATH_LENGTH: usize = 4096;
60
61    /// Regular expression pattern for validating language codes
62    pub const LANGUAGE_CODE_PATTERN: &str = r"^[a-z]{2}-[A-Z]{2}$";
63
64    /// Verify invariants at compile time
65    const _: () = assert!(MIN_INPUT_SIZE <= DEFAULT_MAX_INPUT_SIZE);
66    const _: () = assert!(MAX_PATH_LENGTH > 0);
67}
68
69/// Result type alias for library operations
70pub type Result<T> = std::result::Result<T, HtmlError>;
71
72/// Configuration options for Markdown to HTML conversion.
73///
74/// This struct holds settings that control how Markdown content is processed
75/// and converted to HTML.
76#[derive(Debug, Clone, Eq, PartialEq)]
77pub struct MarkdownConfig {
78    /// The encoding to use for input/output (defaults to "utf-8")
79    pub encoding: String,
80
81    /// HTML generation configuration
82    pub html_config: HtmlConfig,
83}
84
85impl Default for MarkdownConfig {
86    fn default() -> Self {
87        Self {
88            encoding: String::from("utf-8"),
89            html_config: HtmlConfig::default(),
90        }
91    }
92}
93
94/// Errors that can occur during configuration.
95#[derive(Debug, thiserror::Error)]
96#[non_exhaustive]
97pub enum ConfigError {
98    /// Error for invalid input size configuration
99    #[error(
100        "Invalid input size: {0} bytes is below minimum of {1} bytes"
101    )]
102    InvalidInputSize(usize, usize),
103
104    /// Error for invalid language code
105    #[error("Invalid language code: {0}")]
106    InvalidLanguageCode(String),
107
108    /// Error for invalid file path
109    #[error("Invalid file path: {0}")]
110    InvalidFilePath(String),
111}
112
113/// Output destination for HTML generation.
114///
115/// Specifies where the generated HTML content should be written.
116///
117/// # Examples
118///
119/// Writing HTML to a file:
120/// ```
121/// use std::fs::File;
122/// use html_generator::OutputDestination;
123///
124/// let output = OutputDestination::File("output.html".to_string());
125/// ```
126///
127/// Writing HTML to an in-memory buffer:
128/// ```
129/// use std::io::Cursor;
130/// use html_generator::OutputDestination;
131///
132/// let buffer = Cursor::new(Vec::new());
133/// let output = OutputDestination::Writer(Box::new(buffer));
134/// ```
135///
136/// Writing HTML to standard output:
137/// ```
138/// use html_generator::OutputDestination;
139///
140/// let output = OutputDestination::Stdout;
141/// ```
142#[non_exhaustive]
143pub enum OutputDestination {
144    /// Write output to a file at the specified path.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use html_generator::OutputDestination;
150    ///
151    /// let output = OutputDestination::File("output.html".to_string());
152    /// ```
153    File(String),
154
155    /// Write output using a custom writer implementation.
156    ///
157    /// This can be used for in-memory buffers, network streams,
158    /// or other custom output destinations.
159    ///
160    /// # Example
161    ///
162    /// ```
163    /// use std::io::Cursor;
164    /// use html_generator::OutputDestination;
165    ///
166    /// let buffer = Cursor::new(Vec::new());
167    /// let output = OutputDestination::Writer(Box::new(buffer));
168    /// ```
169    Writer(Box<dyn Write>),
170
171    /// Write output to standard output (default).
172    ///
173    /// This is useful for command-line tools and scripts.
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// use html_generator::OutputDestination;
179    ///
180    /// let output = OutputDestination::Stdout;
181    /// ```
182    Stdout,
183}
184
185/// Default implementation for OutputDestination.
186impl Default for OutputDestination {
187    fn default() -> Self {
188        Self::Stdout
189    }
190}
191
192/// Debug implementation for OutputDestination.
193impl fmt::Debug for OutputDestination {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        match self {
196            Self::File(path) => {
197                f.debug_tuple("File").field(path).finish()
198            }
199            Self::Writer(_) => write!(f, "Writer(<dyn Write>)"),
200            Self::Stdout => write!(f, "Stdout"),
201        }
202    }
203}
204
205/// Implements `Display` for `OutputDestination`.
206impl fmt::Display for OutputDestination {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        match self {
209            OutputDestination::File(path) => {
210                write!(f, "File({})", path)
211            }
212            OutputDestination::Writer(_) => {
213                write!(f, "Writer(<dyn Write>)")
214            }
215            OutputDestination::Stdout => write!(f, "Stdout"),
216        }
217    }
218}
219
220/// Configuration options for HTML generation.
221///
222/// Controls various aspects of the HTML generation process including
223/// syntax highlighting, accessibility features, and output formatting.
224#[derive(Debug, PartialEq, Eq, Clone)]
225pub struct HtmlConfig {
226    /// Enable syntax highlighting for code blocks
227    pub enable_syntax_highlighting: bool,
228
229    /// Theme to use for syntax highlighting
230    pub syntax_theme: Option<String>,
231
232    /// Minify the generated HTML output
233    pub minify_output: bool,
234
235    /// Automatically add ARIA attributes for accessibility
236    pub add_aria_attributes: bool,
237
238    /// Generate structured data (JSON-LD) based on content
239    pub generate_structured_data: bool,
240
241    /// Maximum size (in bytes) for input content
242    pub max_input_size: usize,
243
244    /// Language for generated content
245    pub language: String,
246
247    /// Enable table of contents generation
248    pub generate_toc: bool,
249}
250
251impl Default for HtmlConfig {
252    fn default() -> Self {
253        Self {
254            enable_syntax_highlighting: true,
255            syntax_theme: Some("github".to_string()),
256            minify_output: false,
257            add_aria_attributes: true,
258            generate_structured_data: false,
259            max_input_size: constants::DEFAULT_MAX_INPUT_SIZE,
260            language: String::from(constants::DEFAULT_LANGUAGE),
261            generate_toc: false,
262        }
263    }
264}
265
266impl HtmlConfig {
267    /// Creates a new `HtmlConfig` using the builder pattern.
268    ///
269    /// # Examples
270    ///
271    /// ```rust
272    /// use html_generator::HtmlConfig;
273    ///
274    /// let config = HtmlConfig::builder()
275    ///     .with_syntax_highlighting(true, Some("monokai".to_string()))
276    ///     .with_language("en-GB")
277    ///     .build()
278    ///     .unwrap();
279    /// ```
280    pub fn builder() -> HtmlConfigBuilder {
281        HtmlConfigBuilder::default()
282    }
283
284    /// Validates the configuration settings.
285    ///
286    /// Checks that all configuration values are within acceptable ranges
287    /// and conform to required formats.
288    ///
289    /// # Returns
290    ///
291    /// Returns `Ok(())` if the configuration is valid, or an appropriate
292    /// error if validation fails.
293    pub fn validate(&self) -> Result<()> {
294        if self.max_input_size < constants::MIN_INPUT_SIZE {
295            return Err(HtmlError::InvalidInput(format!(
296                "Input size must be at least {} bytes",
297                constants::MIN_INPUT_SIZE
298            )));
299        }
300        if !validate_language_code(&self.language) {
301            return Err(HtmlError::InvalidInput(format!(
302                "Invalid language code: {}",
303                self.language
304            )));
305        }
306        Ok(())
307    }
308
309    /// Validates file path safety to prevent directory traversal attacks.
310    ///
311    /// # Arguments
312    ///
313    /// * `path` - The file path to validate
314    ///
315    /// # Returns
316    ///
317    /// Returns `Ok(())` if the path is safe, or an appropriate error
318    /// if validation fails.
319    pub(crate) fn validate_file_path(
320        path: impl AsRef<Path>,
321    ) -> Result<()> {
322        let path = path.as_ref();
323
324        if path.to_string_lossy().is_empty() {
325            return Err(HtmlError::InvalidInput(
326                "File path cannot be empty".to_string(),
327            ));
328        }
329
330        if path.to_string_lossy().len() > constants::MAX_PATH_LENGTH {
331            return Err(HtmlError::InvalidInput(format!(
332                "File path exceeds maximum length of {} characters",
333                constants::MAX_PATH_LENGTH
334            )));
335        }
336
337        if path.components().any(|c| matches!(c, Component::ParentDir))
338        {
339            return Err(HtmlError::InvalidInput(
340                "Directory traversal is not allowed in file paths"
341                    .to_string(),
342            ));
343        }
344
345        #[cfg(not(test))]
346        if path.is_absolute() {
347            return Err(HtmlError::InvalidInput(
348                "Only relative file paths are allowed".to_string(),
349            ));
350        }
351
352        if let Some(ext) = path.extension() {
353            if !matches!(ext.to_string_lossy().as_ref(), "md" | "html")
354            {
355                return Err(HtmlError::InvalidInput(
356                    "Invalid file extension: only .md and .html files are allowed".to_string(),
357                ));
358            }
359        }
360
361        Ok(())
362    }
363}
364
365/// Builder for constructing `HtmlConfig` instances.
366///
367/// Provides a fluent interface for creating and customizing HTML
368/// configuration options.
369#[derive(Debug, Default)]
370pub struct HtmlConfigBuilder {
371    config: HtmlConfig,
372}
373
374impl HtmlConfigBuilder {
375    /// Creates a new `HtmlConfigBuilder` with default options.
376    pub fn new() -> Self {
377        Self::default()
378    }
379
380    /// Enables or disables syntax highlighting for code blocks.
381    ///
382    /// # Arguments
383    ///
384    /// * `enable` - Whether to enable syntax highlighting
385    /// * `theme` - Optional theme name for syntax highlighting
386    #[must_use]
387    pub fn with_syntax_highlighting(
388        mut self,
389        enable: bool,
390        theme: Option<String>,
391    ) -> Self {
392        self.config.enable_syntax_highlighting = enable;
393        self.config.syntax_theme = if enable {
394            theme.or_else(|| Some("github".to_string()))
395        } else {
396            None
397        };
398        self
399    }
400
401    /// Sets the language for generated content.
402    ///
403    /// # Arguments
404    ///
405    /// * `language` - The language code (e.g., "en-GB")
406    #[must_use]
407    pub fn with_language(
408        mut self,
409        language: impl Into<String>,
410    ) -> Self {
411        self.config.language = language.into();
412        self
413    }
414
415    /// Builds the configuration, validating all settings.
416    ///
417    /// # Returns
418    ///
419    /// Returns the validated configuration or an error if validation fails.
420    pub fn build(self) -> Result<HtmlConfig> {
421        self.config.validate()?;
422        Ok(self.config)
423    }
424}
425
426/// Converts Markdown content to HTML.
427///
428/// This function processes Unicode Markdown content and returns HTML output.
429/// The input must be valid Unicode - if your input is encoded (e.g., UTF-8),
430/// you must decode it before passing it to this function.
431///
432/// # Arguments
433///
434/// * `content` - The Markdown content as a Unicode string
435/// * `config` - Optional configuration for the conversion
436///
437/// # Returns
438///
439/// Returns the generated HTML as a Unicode string wrapped in a `Result`
440///
441/// # Errors
442///
443/// Returns an error if:
444/// * The input content is invalid Unicode
445/// * HTML generation fails
446/// * Input size exceeds configured maximum
447///
448/// # Examples
449///
450/// ```rust
451/// use html_generator::{markdown_to_html, MarkdownConfig};
452///
453/// let markdown = "# Hello\n\nWorld";
454/// let html = markdown_to_html(markdown, None)?;
455/// assert!(html.contains("<h1>Hello</h1>"));
456/// # Ok::<(), html_generator::error::HtmlError>(())
457/// ```
458pub fn markdown_to_html(
459    content: &str,
460    config: Option<MarkdownConfig>,
461) -> Result<String> {
462    let config = config.unwrap_or_default();
463
464    if content.is_empty() {
465        return Err(HtmlError::InvalidInput(
466            "Input content is empty".to_string(),
467        ));
468    }
469
470    if content.len() > config.html_config.max_input_size {
471        return Err(HtmlError::InputTooLarge(content.len()));
472    }
473
474    generate_html(content, &config.html_config)
475}
476
477/// Converts a Markdown file to HTML.
478///
479/// This function reads from a file or stdin and writes the generated HTML to
480/// a specified destination. It handles encoding/decoding of content.
481///
482/// # Arguments
483///
484/// * `input` - The input source (file path or None for stdin)
485/// * `output` - The output destination (defaults to stdout)
486/// * `config` - Optional configuration including encoding settings
487///
488/// # Returns
489///
490/// Returns `Result<()>` indicating success or failure of the operation.
491///
492/// # Errors
493///
494/// Returns an error if:
495/// * Input file is not found or cannot be read
496/// * Output file cannot be written
497/// * Configuration is invalid
498/// * Input size exceeds configured maximum
499///
500/// # Examples
501///
502/// ```no_run
503/// use html_generator::{markdown_file_to_html, OutputDestination, MarkdownConfig};
504/// use std::path::{Path, PathBuf};
505///
506/// // Convert file to HTML and write to stdout
507/// markdown_file_to_html(
508///     Some(PathBuf::from("input.md")),
509///     None,
510///     None,
511/// )?;
512///
513/// // Convert stdin to HTML file
514/// markdown_file_to_html(
515///     None::<PathBuf>,  // Explicit type annotation
516///     Some(OutputDestination::File("output.html".into())),
517///     Some(MarkdownConfig::default()),
518/// )?;
519/// # Ok::<(), html_generator::error::HtmlError>(())
520/// ```
521#[inline]
522pub fn markdown_file_to_html(
523    input: Option<impl AsRef<Path>>,
524    output: Option<OutputDestination>,
525    config: Option<MarkdownConfig>,
526) -> Result<()> {
527    let config = config.unwrap_or_default();
528    let output = output.unwrap_or_default();
529
530    // Validate paths first
531    validate_paths(&input, &output)?;
532
533    // Read and process input
534    let content = read_input(input)?;
535
536    // Generate HTML
537    let html = markdown_to_html(&content, Some(config))?;
538
539    // Write output
540    write_output(output, html.as_bytes())
541}
542
543/// Validates input and output paths
544fn validate_paths(
545    input: &Option<impl AsRef<Path>>,
546    output: &OutputDestination,
547) -> Result<()> {
548    if let Some(path) = input.as_ref() {
549        HtmlConfig::validate_file_path(path)?;
550    }
551    if let OutputDestination::File(ref path) = output {
552        HtmlConfig::validate_file_path(path)?;
553    }
554    Ok(())
555}
556
557/// Reads content from the input source
558fn read_input(input: Option<impl AsRef<Path>>) -> Result<String> {
559    match input {
560        Some(path) => {
561            let file = File::open(path).map_err(HtmlError::Io)?;
562            let mut reader =
563                BufReader::with_capacity(MAX_BUFFER_SIZE, file);
564            let mut content = String::with_capacity(MAX_BUFFER_SIZE);
565            let _ =
566                reader.read_to_string(&mut content).map_err(|e| {
567                    HtmlError::Io(io::Error::new(
568                        e.kind(),
569                        format!("Failed to read input: {}", e),
570                    ))
571                })?;
572            Ok(content)
573        }
574        None => {
575            let stdin = io::stdin();
576            let mut reader =
577                BufReader::with_capacity(MAX_BUFFER_SIZE, stdin.lock());
578            let mut content = String::with_capacity(MAX_BUFFER_SIZE);
579            let _ =
580                reader.read_to_string(&mut content).map_err(|e| {
581                    HtmlError::Io(io::Error::new(
582                        e.kind(),
583                        format!("Failed to read from stdin: {}", e),
584                    ))
585                })?;
586            Ok(content)
587        }
588    }
589}
590
591/// Writes content to the output destination
592fn write_output(
593    output: OutputDestination,
594    content: &[u8],
595) -> Result<()> {
596    match output {
597        OutputDestination::File(path) => {
598            let file = File::create(&path).map_err(|e| {
599                HtmlError::Io(io::Error::new(
600                    e.kind(),
601                    format!("Failed to create file '{}': {}", path, e),
602                ))
603            })?;
604            let mut writer = BufWriter::new(file);
605            writer.write_all(content).map_err(|e| {
606                HtmlError::Io(io::Error::new(
607                    e.kind(),
608                    format!(
609                        "Failed to write to file '{}': {}",
610                        path, e
611                    ),
612                ))
613            })?;
614            writer.flush().map_err(|e| {
615                HtmlError::Io(io::Error::new(
616                    e.kind(),
617                    format!(
618                        "Failed to flush output to file '{}': {}",
619                        path, e
620                    ),
621                ))
622            })?;
623        }
624        OutputDestination::Writer(mut writer) => {
625            let mut buffered = BufWriter::new(&mut writer);
626            buffered.write_all(content).map_err(|e| {
627                HtmlError::Io(io::Error::new(
628                    e.kind(),
629                    format!("Failed to write to output: {}", e),
630                ))
631            })?;
632            buffered.flush().map_err(|e| {
633                HtmlError::Io(io::Error::new(
634                    e.kind(),
635                    format!("Failed to flush output: {}", e),
636                ))
637            })?;
638        }
639        OutputDestination::Stdout => {
640            let stdout = io::stdout();
641            let mut writer = BufWriter::new(stdout.lock());
642            writer.write_all(content).map_err(|e| {
643                HtmlError::Io(io::Error::new(
644                    e.kind(),
645                    format!("Failed to write to stdout: {}", e),
646                ))
647            })?;
648            writer.flush().map_err(|e| {
649                HtmlError::Io(io::Error::new(
650                    e.kind(),
651                    format!("Failed to flush stdout: {}", e),
652                ))
653            })?;
654        }
655    }
656    Ok(())
657}
658
659/// Validates that a language code matches the BCP 47 format (e.g., "en-GB").
660///
661/// This function checks if a given language code follows the BCP 47 format,
662/// which requires both language and region codes.
663///
664/// # Arguments
665///
666/// * `lang` - The language code to validate
667///
668/// # Returns
669///
670/// Returns true if the language code is valid (e.g., "en-GB"), false otherwise.
671///
672/// # Examples
673///
674/// ```
675/// use html_generator::validate_language_code;
676///
677/// assert!(validate_language_code("en-GB"));  // Valid
678/// assert!(!validate_language_code("en"));    // Invalid - missing region
679/// assert!(!validate_language_code("123"));   // Invalid - not a language code
680/// assert!(!validate_language_code("en_GB")); // Invalid - wrong separator
681/// ```
682pub fn validate_language_code(lang: &str) -> bool {
683    use once_cell::sync::Lazy;
684    use regex::Regex;
685
686    // Pre-compiled regex using Lazy<Regex>
687    static LANG_REGEX: Lazy<Regex> = Lazy::new(|| {
688        Regex::new(r"^[a-z]{2}(?:-[A-Z]{2})$")
689            .expect("Failed to compile language code regex")
690    });
691
692    // Match the input against the pre-compiled regex
693    LANG_REGEX.is_match(lang)
694}
695
696#[cfg(test)]
697mod tests {
698    use super::*;
699    use regex::Regex;
700    use std::io::Cursor;
701    use tempfile::{tempdir, TempDir};
702
703    /// Creates a temporary test directory for file operations.
704    ///
705    /// The directory and its contents are automatically cleaned up when
706    /// the returned TempDir is dropped.
707    fn setup_test_dir() -> TempDir {
708        tempdir().expect("Failed to create temporary directory")
709    }
710
711    /// Creates a test file with the given content.
712    ///
713    /// # Arguments
714    ///
715    /// * `dir` - The temporary directory to create the file in
716    /// * `content` - The content to write to the file
717    ///
718    /// # Returns
719    ///
720    /// Returns the path to the created file.
721    fn create_test_file(
722        dir: &TempDir,
723        content: &str,
724    ) -> std::path::PathBuf {
725        let path = dir.path().join("test.md");
726        std::fs::write(&path, content)
727            .expect("Failed to write test file");
728        path
729    }
730
731    mod config_tests {
732        use super::*;
733
734        #[test]
735        fn test_config_validation() {
736            // Test invalid input size
737            let config = HtmlConfig {
738                max_input_size: 100, // Too small
739                ..Default::default()
740            };
741            assert!(config.validate().is_err());
742
743            // Test invalid language code
744            let config = HtmlConfig {
745                language: "invalid".to_string(),
746                ..Default::default()
747            };
748            assert!(config.validate().is_err());
749
750            // Test valid default configuration
751            let config = HtmlConfig::default();
752            assert!(config.validate().is_ok());
753        }
754
755        #[test]
756        fn test_config_builder() {
757            let result = HtmlConfigBuilder::new()
758                .with_syntax_highlighting(
759                    true,
760                    Some("monokai".to_string()),
761                )
762                .with_language("en-GB")
763                .build();
764
765            assert!(result.is_ok());
766            let config = result.unwrap();
767            assert!(config.enable_syntax_highlighting);
768            assert_eq!(
769                config.syntax_theme,
770                Some("monokai".to_string())
771            );
772            assert_eq!(config.language, "en-GB");
773        }
774
775        #[test]
776        fn test_config_builder_invalid() {
777            let result = HtmlConfigBuilder::new()
778                .with_language("invalid")
779                .build();
780
781            assert!(matches!(
782                result,
783                Err(HtmlError::InvalidInput(msg)) if msg.contains("Invalid language code")
784            ));
785        }
786
787        #[test]
788        fn test_html_config_with_no_syntax_theme() {
789            let config = HtmlConfig {
790                enable_syntax_highlighting: true,
791                syntax_theme: None,
792                ..Default::default()
793            };
794
795            assert!(config.validate().is_ok());
796        }
797
798        #[test]
799        fn test_file_conversion_with_large_output() -> Result<()> {
800            let temp_dir = setup_test_dir();
801            let input_path = create_test_file(
802                &temp_dir,
803                "# Large\n\nContent".repeat(10_000).as_str(),
804            );
805            let output_path = temp_dir.path().join("large_output.html");
806
807            let result = markdown_file_to_html(
808                Some(&input_path),
809                Some(OutputDestination::File(
810                    output_path.to_string_lossy().into(),
811                )),
812                None,
813            );
814
815            assert!(result.is_ok());
816            let content = std::fs::read_to_string(output_path)?;
817            assert!(content.contains("<h1>Large</h1>"));
818
819            Ok(())
820        }
821
822        #[test]
823        fn test_markdown_with_broken_syntax() {
824            let markdown = "# Unmatched Header\n**Bold start";
825            let result = markdown_to_html(markdown, None);
826            assert!(result.is_ok());
827            let html = result.unwrap();
828            assert!(html.contains("<h1>Unmatched Header</h1>"));
829            assert!(html.contains("**Bold start</p>")); // Ensure content is preserved
830        }
831
832        #[test]
833        fn test_language_code_with_custom_regex() {
834            let custom_lang_regex =
835                Regex::new(r"^[a-z]{2}-[A-Z]{2}$").unwrap();
836            assert!(custom_lang_regex.is_match("en-GB"));
837            assert!(!custom_lang_regex.is_match("EN-gb")); // Case-sensitive check
838        }
839
840        #[test]
841        fn test_markdown_to_html_error_handling() {
842            let result = markdown_to_html("", None);
843            assert!(matches!(result, Err(HtmlError::InvalidInput(_))));
844
845            let oversized_input =
846                "a".repeat(constants::DEFAULT_MAX_INPUT_SIZE + 1);
847            let result = markdown_to_html(&oversized_input, None);
848            assert!(matches!(result, Err(HtmlError::InputTooLarge(_))));
849        }
850
851        #[test]
852        fn test_performance_with_nested_lists() {
853            let nested_list = "- Item\n".repeat(1000);
854            let result = markdown_to_html(&nested_list, None);
855            assert!(result.is_ok());
856            let html = result.unwrap();
857            assert!(html.matches("<li>").count() == 1000);
858        }
859    }
860
861    mod file_validation_tests {
862        use super::*;
863        use std::path::PathBuf;
864
865        #[test]
866        fn test_valid_paths() {
867            let valid_paths = [
868                PathBuf::from("test.md"),
869                PathBuf::from("test.html"),
870                PathBuf::from("subfolder/test.md"),
871            ];
872
873            for path in valid_paths {
874                assert!(
875                    HtmlConfig::validate_file_path(&path).is_ok(),
876                    "Path should be valid: {:?}",
877                    path
878                );
879            }
880        }
881
882        #[test]
883        fn test_invalid_paths() {
884            let invalid_paths = [
885                PathBuf::from(""),           // Empty path
886                PathBuf::from("../test.md"), // Directory traversal
887                PathBuf::from("test.exe"),   // Invalid extension
888                PathBuf::from(
889                    "a".repeat(constants::MAX_PATH_LENGTH + 1),
890                ), // Too long
891            ];
892
893            for path in invalid_paths {
894                assert!(
895                    HtmlConfig::validate_file_path(&path).is_err(),
896                    "Path should be invalid: {:?}",
897                    path
898                );
899            }
900        }
901    }
902
903    mod markdown_conversion_tests {
904        use super::*;
905
906        #[test]
907        fn test_basic_conversion() {
908            let markdown = "# Test\n\nHello world";
909            let result = markdown_to_html(markdown, None);
910            assert!(result.is_ok());
911
912            let html = result.unwrap();
913            assert!(html.contains("<h1>Test</h1>"));
914            assert!(html.contains("<p>Hello world</p>"));
915        }
916
917        #[test]
918        fn test_conversion_with_config() {
919            let markdown = "# Test\n```rust\nfn main() {}\n```";
920            let config = MarkdownConfig {
921                html_config: HtmlConfig {
922                    enable_syntax_highlighting: true,
923                    ..Default::default()
924                },
925                ..Default::default()
926            };
927
928            let result = markdown_to_html(markdown, Some(config));
929            assert!(result.is_ok());
930            assert!(result.unwrap().contains("language-rust"));
931        }
932
933        #[test]
934        fn test_empty_content() {
935            assert!(matches!(
936                markdown_to_html("", None),
937                Err(HtmlError::InvalidInput(_))
938            ));
939        }
940
941        #[test]
942        fn test_content_too_large() {
943            let large_content =
944                "a".repeat(constants::DEFAULT_MAX_INPUT_SIZE + 1);
945            assert!(matches!(
946                markdown_to_html(&large_content, None),
947                Err(HtmlError::InputTooLarge(_))
948            ));
949        }
950    }
951
952    mod file_operation_tests {
953        use super::*;
954
955        #[test]
956        fn test_file_conversion() -> Result<()> {
957            let temp_dir = setup_test_dir();
958            let input_path =
959                create_test_file(&temp_dir, "# Test\n\nHello world");
960            let output_path = temp_dir.path().join("test.html");
961
962            markdown_file_to_html(
963                Some(&input_path),
964                Some(OutputDestination::File(
965                    output_path.to_string_lossy().into(),
966                )),
967                None::<MarkdownConfig>,
968            )?;
969
970            let content = std::fs::read_to_string(output_path)?;
971            assert!(content.contains("<h1>Test</h1>"));
972
973            Ok(())
974        }
975
976        #[test]
977        fn test_writer_output() {
978            let temp_dir = setup_test_dir();
979            let input_path =
980                create_test_file(&temp_dir, "# Test\nHello");
981            let buffer = Box::new(Cursor::new(Vec::new()));
982
983            let result = markdown_file_to_html(
984                Some(&input_path),
985                Some(OutputDestination::Writer(buffer)),
986                None,
987            );
988
989            assert!(result.is_ok());
990        }
991
992        #[test]
993        fn test_writer_output_no_input() {
994            let buffer = Box::new(Cursor::new(Vec::new()));
995
996            let result = markdown_file_to_html(
997                Some(Path::new("nonexistent.md")),
998                Some(OutputDestination::Writer(buffer)),
999                None,
1000            );
1001
1002            assert!(result.is_err());
1003        }
1004    }
1005
1006    mod language_validation_tests {
1007        use super::*;
1008
1009        #[test]
1010        fn test_valid_language_codes() {
1011            let valid_codes =
1012                ["en-GB", "fr-FR", "de-DE", "es-ES", "zh-CN"];
1013
1014            for code in valid_codes {
1015                assert!(
1016                    validate_language_code(code),
1017                    "Language code '{}' should be valid",
1018                    code
1019                );
1020            }
1021        }
1022
1023        #[test]
1024        fn test_invalid_language_codes() {
1025            let invalid_codes = [
1026                "",        // Empty
1027                "en",      // Missing region
1028                "eng-GBR", // Wrong format
1029                "en_GB",   // Wrong separator
1030                "123-45",  // Invalid characters
1031                "GB-en",   // Wrong order
1032                "en-gb",   // Wrong case
1033            ];
1034
1035            for code in invalid_codes {
1036                assert!(
1037                    !validate_language_code(code),
1038                    "Language code '{}' should be invalid",
1039                    code
1040                );
1041            }
1042        }
1043    }
1044
1045    mod integration_tests {
1046        use super::*;
1047
1048        #[test]
1049        fn test_end_to_end_conversion() -> Result<()> {
1050            let temp_dir = setup_test_dir();
1051            let content = r#"---
1052title: Test Document
1053---
1054
1055# Hello World
1056
1057This is a test document with:
1058- A list
1059- And some **bold** text
1060"#;
1061            let input_path = create_test_file(&temp_dir, content);
1062            let output_path = temp_dir.path().join("test.html");
1063
1064            let config = MarkdownConfig {
1065                html_config: HtmlConfig {
1066                    enable_syntax_highlighting: true,
1067                    generate_toc: true,
1068                    ..Default::default()
1069                },
1070                ..Default::default()
1071            };
1072
1073            markdown_file_to_html(
1074                Some(&input_path),
1075                Some(OutputDestination::File(
1076                    output_path.to_string_lossy().into(),
1077                )),
1078                Some(config),
1079            )?;
1080
1081            let html = std::fs::read_to_string(&output_path)?;
1082            assert!(html.contains("<h1>Hello World</h1>"));
1083            assert!(html.contains("<strong>bold</strong>"));
1084            assert!(html.contains("<ul>"));
1085
1086            Ok(())
1087        }
1088
1089        #[test]
1090        fn test_output_destination_debug() {
1091            assert_eq!(
1092                format!(
1093                    "{:?}",
1094                    OutputDestination::File("test.html".to_string())
1095                ),
1096                r#"File("test.html")"#
1097            );
1098            assert_eq!(
1099                format!("{:?}", OutputDestination::Stdout),
1100                "Stdout"
1101            );
1102
1103            let writer = Box::new(Cursor::new(Vec::new()));
1104            assert_eq!(
1105                format!("{:?}", OutputDestination::Writer(writer)),
1106                "Writer(<dyn Write>)"
1107            );
1108        }
1109    }
1110
1111    mod markdown_config_tests {
1112        use super::*;
1113
1114        #[test]
1115        fn test_markdown_config_custom_encoding() {
1116            let config = MarkdownConfig {
1117                encoding: "latin1".to_string(),
1118                html_config: HtmlConfig::default(),
1119            };
1120            assert_eq!(config.encoding, "latin1");
1121        }
1122
1123        #[test]
1124        fn test_markdown_config_default() {
1125            let config = MarkdownConfig::default();
1126            assert_eq!(config.encoding, "utf-8");
1127            assert_eq!(config.html_config, HtmlConfig::default());
1128        }
1129
1130        #[test]
1131        fn test_markdown_config_clone() {
1132            let config = MarkdownConfig::default();
1133            let cloned = config.clone();
1134            assert_eq!(config, cloned);
1135        }
1136    }
1137
1138    mod config_error_tests {
1139        use super::*;
1140
1141        #[test]
1142        fn test_config_error_display() {
1143            let error = ConfigError::InvalidInputSize(100, 1024);
1144            assert!(error.to_string().contains("Invalid input size"));
1145
1146            let error =
1147                ConfigError::InvalidLanguageCode("xx".to_string());
1148            assert!(error
1149                .to_string()
1150                .contains("Invalid language code"));
1151
1152            let error =
1153                ConfigError::InvalidFilePath("../bad/path".to_string());
1154            assert!(error.to_string().contains("Invalid file path"));
1155        }
1156    }
1157
1158    mod output_destination_tests {
1159        use super::*;
1160
1161        #[test]
1162        fn test_output_destination_default() {
1163            assert!(matches!(
1164                OutputDestination::default(),
1165                OutputDestination::Stdout
1166            ));
1167        }
1168
1169        #[test]
1170        fn test_output_destination_file() {
1171            let dest = OutputDestination::File("test.html".to_string());
1172            assert!(matches!(dest, OutputDestination::File(_)));
1173        }
1174
1175        #[test]
1176        fn test_output_destination_writer() {
1177            let writer = Box::new(Cursor::new(Vec::new()));
1178            let dest = OutputDestination::Writer(writer);
1179            assert!(matches!(dest, OutputDestination::Writer(_)));
1180        }
1181    }
1182
1183    mod html_config_tests {
1184        use super::*;
1185
1186        #[test]
1187        fn test_html_config_builder_all_options() {
1188            let config = HtmlConfig::builder()
1189                .with_syntax_highlighting(
1190                    true,
1191                    Some("dracula".to_string()),
1192                )
1193                .with_language("en-US")
1194                .build()
1195                .unwrap();
1196
1197            assert!(config.enable_syntax_highlighting);
1198            assert_eq!(
1199                config.syntax_theme,
1200                Some("dracula".to_string())
1201            );
1202            assert_eq!(config.language, "en-US");
1203        }
1204
1205        #[test]
1206        fn test_html_config_validation_edge_cases() {
1207            let config = HtmlConfig {
1208                max_input_size: constants::MIN_INPUT_SIZE,
1209                ..Default::default()
1210            };
1211            assert!(config.validate().is_ok());
1212
1213            let config = HtmlConfig {
1214                max_input_size: constants::MIN_INPUT_SIZE - 1,
1215                ..Default::default()
1216            };
1217            assert!(config.validate().is_err());
1218        }
1219    }
1220
1221    mod markdown_processing_tests {
1222        use super::*;
1223
1224        #[test]
1225        fn test_markdown_to_html_with_front_matter() -> Result<()> {
1226            let markdown = r#"---
1227title: Test
1228author: Test Author
1229---
1230# Heading
1231Content"#;
1232            let html = markdown_to_html(markdown, None)?;
1233            assert!(html.contains("<h1>Heading</h1>"));
1234            assert!(html.contains("<p>Content</p>"));
1235            Ok(())
1236        }
1237
1238        #[test]
1239        fn test_markdown_to_html_with_code_blocks() -> Result<()> {
1240            let markdown = r#"```rust
1241fn main() {
1242    println!("Hello");
1243}
1244```"#;
1245            let config = MarkdownConfig {
1246                html_config: HtmlConfig {
1247                    enable_syntax_highlighting: true,
1248                    ..Default::default()
1249                },
1250                ..Default::default()
1251            };
1252            let html = markdown_to_html(markdown, Some(config))?;
1253            assert!(html.contains("language-rust"));
1254            Ok(())
1255        }
1256
1257        #[test]
1258        fn test_markdown_to_html_with_tables() -> Result<()> {
1259            let markdown = r#"
1260| Header 1 | Header 2 |
1261|----------|----------|
1262| Cell 1   | Cell 2   |
1263"#;
1264            let html = markdown_to_html(markdown, None)?;
1265            // First verify the HTML output to see what we're getting
1266            println!("Generated HTML for table: {}", html);
1267            // Check for common table elements - div wrapper is often used for table responsiveness
1268            assert!(html.contains("Header 1"));
1269            assert!(html.contains("Cell 1"));
1270            assert!(html.contains("Cell 2"));
1271            Ok(())
1272        }
1273
1274        #[test]
1275        fn test_invalid_encoding_handling() {
1276            let config = MarkdownConfig {
1277                encoding: "unsupported-encoding".to_string(),
1278                html_config: HtmlConfig::default(),
1279            };
1280            // Simulate usage where encoding matters
1281            let result = markdown_to_html("# Test", Some(config));
1282            assert!(result.is_ok()); // Assuming encoding isn't directly validated during processing
1283        }
1284
1285        #[test]
1286        fn test_config_error_types() {
1287            let error = ConfigError::InvalidInputSize(512, 1024);
1288            assert_eq!(format!("{}", error), "Invalid input size: 512 bytes is below minimum of 1024 bytes");
1289        }
1290    }
1291
1292    mod file_processing_tests {
1293        use crate::constants;
1294        use crate::HtmlConfig;
1295        use crate::{
1296            markdown_file_to_html, HtmlError, OutputDestination,
1297        };
1298        use std::io::Cursor;
1299        use std::path::Path;
1300        use tempfile::NamedTempFile;
1301
1302        #[test]
1303        fn test_display_file() {
1304            let output =
1305                OutputDestination::File("output.html".to_string());
1306            let display = format!("{}", output);
1307            assert_eq!(display, "File(output.html)");
1308        }
1309
1310        #[test]
1311        fn test_display_stdout() {
1312            let output = OutputDestination::Stdout;
1313            let display = format!("{}", output);
1314            assert_eq!(display, "Stdout");
1315        }
1316
1317        #[test]
1318        fn test_display_writer() {
1319            let buffer = Cursor::new(Vec::new());
1320            let output = OutputDestination::Writer(Box::new(buffer));
1321            let display = format!("{}", output);
1322            assert_eq!(display, "Writer(<dyn Write>)");
1323        }
1324
1325        #[test]
1326        fn test_debug_file() {
1327            let output =
1328                OutputDestination::File("output.html".to_string());
1329            let debug = format!("{:?}", output);
1330            assert_eq!(debug, r#"File("output.html")"#);
1331        }
1332
1333        #[test]
1334        fn test_debug_stdout() {
1335            let output = OutputDestination::Stdout;
1336            let debug = format!("{:?}", output);
1337            assert_eq!(debug, "Stdout");
1338        }
1339
1340        #[test]
1341        fn test_debug_writer() {
1342            let buffer = Cursor::new(Vec::new());
1343            let output = OutputDestination::Writer(Box::new(buffer));
1344            let debug = format!("{:?}", output);
1345            assert_eq!(debug, "Writer(<dyn Write>)");
1346        }
1347
1348        #[test]
1349        fn test_file_to_html_invalid_input() {
1350            let result = markdown_file_to_html(
1351                Some(Path::new("nonexistent.md")),
1352                None,
1353                None,
1354            );
1355            assert!(matches!(result, Err(HtmlError::Io(_))));
1356        }
1357
1358        #[test]
1359        fn test_file_to_html_with_invalid_output_path(
1360        ) -> Result<(), HtmlError> {
1361            let input = NamedTempFile::new()?;
1362            std::fs::write(&input, "# Test")?;
1363
1364            let result = markdown_file_to_html(
1365                Some(input.path()),
1366                Some(OutputDestination::File(
1367                    "/invalid/path/test.html".to_string(),
1368                )),
1369                None,
1370            );
1371            assert!(result.is_err());
1372            Ok(())
1373        }
1374
1375        // Test for Default implementation of OutputDestination
1376        #[test]
1377        fn test_output_destination_default() {
1378            let default = OutputDestination::default();
1379            assert!(matches!(default, OutputDestination::Stdout));
1380        }
1381
1382        // Test for Debug implementation of OutputDestination
1383        #[test]
1384        fn test_output_destination_debug() {
1385            let file_debug = format!(
1386                "{:?}",
1387                OutputDestination::File(
1388                    "path/to/file.html".to_string()
1389                )
1390            );
1391            assert_eq!(file_debug, r#"File("path/to/file.html")"#);
1392
1393            let writer_debug = format!(
1394                "{:?}",
1395                OutputDestination::Writer(Box::new(Cursor::new(
1396                    Vec::new()
1397                )))
1398            );
1399            assert_eq!(writer_debug, "Writer(<dyn Write>)");
1400
1401            let stdout_debug =
1402                format!("{:?}", OutputDestination::Stdout);
1403            assert_eq!(stdout_debug, "Stdout");
1404        }
1405
1406        // Test for Display implementation of OutputDestination
1407        #[test]
1408        fn test_output_destination_display() {
1409            let file_display = format!(
1410                "{}",
1411                OutputDestination::File(
1412                    "path/to/file.html".to_string()
1413                )
1414            );
1415            assert_eq!(file_display, "File(path/to/file.html)");
1416
1417            let writer_display = format!(
1418                "{}",
1419                OutputDestination::Writer(Box::new(Cursor::new(
1420                    Vec::new()
1421                )))
1422            );
1423            assert_eq!(writer_display, "Writer(<dyn Write>)");
1424
1425            let stdout_display =
1426                format!("{}", OutputDestination::Stdout);
1427            assert_eq!(stdout_display, "Stdout");
1428        }
1429
1430        // Test for Default implementation of HtmlConfig
1431        #[test]
1432        fn test_html_config_default() {
1433            let default = HtmlConfig::default();
1434            assert!(default.enable_syntax_highlighting);
1435            assert_eq!(
1436                default.syntax_theme,
1437                Some("github".to_string())
1438            );
1439            assert!(!default.minify_output);
1440            assert!(default.add_aria_attributes);
1441            assert!(!default.generate_structured_data);
1442            assert_eq!(
1443                default.max_input_size,
1444                constants::DEFAULT_MAX_INPUT_SIZE
1445            );
1446            assert_eq!(
1447                default.language,
1448                constants::DEFAULT_LANGUAGE.to_string()
1449            );
1450            assert!(!default.generate_toc);
1451        }
1452
1453        // Test for HtmlConfigBuilder
1454        #[test]
1455        fn test_html_config_builder() {
1456            let builder = HtmlConfig::builder()
1457                .with_syntax_highlighting(
1458                    true,
1459                    Some("monokai".to_string()),
1460                )
1461                .with_language("en-US")
1462                .build()
1463                .unwrap();
1464
1465            assert!(builder.enable_syntax_highlighting);
1466            assert_eq!(
1467                builder.syntax_theme,
1468                Some("monokai".to_string())
1469            );
1470            assert_eq!(builder.language, "en-US");
1471        }
1472
1473        // Test for long file path validation
1474        #[test]
1475        fn test_long_file_path_validation() {
1476            let long_path = "a".repeat(constants::MAX_PATH_LENGTH + 1);
1477            let result = HtmlConfig::validate_file_path(long_path);
1478            assert!(
1479                matches!(result, Err(HtmlError::InvalidInput(ref msg)) if msg.contains("File path exceeds maximum length"))
1480            );
1481        }
1482
1483        // Test for relative file path validation
1484        #[test]
1485        fn test_relative_file_path_validation() {
1486            #[cfg(not(test))]
1487            {
1488                let absolute_path = "/absolute/path/to/file.md";
1489                let result =
1490                    HtmlConfig::validate_file_path(absolute_path);
1491                assert!(
1492                    matches!(result, Err(HtmlError::InvalidInput(ref msg)) if msg.contains("Only relative file paths are allowed"))
1493                );
1494            }
1495        }
1496    }
1497
1498    mod language_validation_extended_tests {
1499        use super::*;
1500
1501        #[test]
1502        fn test_language_code_edge_cases() {
1503            // Test empty string
1504            assert!(!validate_language_code(""));
1505
1506            // Test single character
1507            assert!(!validate_language_code("a"));
1508
1509            // Test incorrect casing
1510            assert!(!validate_language_code("EN-GB"));
1511            assert!(!validate_language_code("en-gb"));
1512
1513            // Test invalid separators
1514            assert!(!validate_language_code("en_GB"));
1515            assert!(!validate_language_code("en GB"));
1516
1517            // Test too many segments
1518            assert!(!validate_language_code("en-GB-extra"));
1519        }
1520
1521        #[test]
1522        fn test_language_code_special_cases() {
1523            // Test with numbers
1524            assert!(!validate_language_code("e1-GB"));
1525            assert!(!validate_language_code("en-G1"));
1526
1527            // Test with special characters
1528            assert!(!validate_language_code("en-GB!"));
1529            assert!(!validate_language_code("en@GB"));
1530
1531            // Test with Unicode characters
1532            assert!(!validate_language_code("あa-GB"));
1533            assert!(!validate_language_code("en-あa"));
1534        }
1535    }
1536
1537    mod integration_extended_tests {
1538        use super::*;
1539
1540        #[test]
1541        fn test_full_conversion_pipeline() -> Result<()> {
1542            // Create temporary files
1543            let temp_dir = tempdir()?;
1544            let input_path = temp_dir.path().join("test.md");
1545            let output_path = temp_dir.path().join("test.html");
1546
1547            // Test content with various Markdown features
1548            let content = r#"---
1549title: Test Document
1550author: Test Author
1551---
1552
1553# Main Heading
1554
1555## Subheading
1556
1557This is a paragraph with *italic* and **bold** text.
1558
1559- List item 1
1560- List item 2
1561  - Nested item
1562  - Another nested item
1563
1564```rust
1565fn main() {
1566    println!("Hello, world!");
1567}
1568```
1569
1570| Column 1 | Column 2 |
1571|----------|----------|
1572| Cell 1   | Cell 2   |
1573
1574> This is a blockquote
1575
1576[Link text](https://example.com)"#;
1577
1578            std::fs::write(&input_path, content)?;
1579
1580            // Configure with all features enabled
1581            let config = MarkdownConfig {
1582                html_config: HtmlConfig {
1583                    enable_syntax_highlighting: true,
1584                    generate_toc: true,
1585                    add_aria_attributes: true,
1586                    generate_structured_data: true,
1587                    minify_output: true,
1588                    ..Default::default()
1589                },
1590                ..Default::default()
1591            };
1592
1593            markdown_file_to_html(
1594                Some(&input_path),
1595                Some(OutputDestination::File(
1596                    output_path.to_string_lossy().into(),
1597                )),
1598                Some(config),
1599            )?;
1600
1601            let html = std::fs::read_to_string(&output_path)?;
1602
1603            // Verify all expected elements are present
1604            println!("Generated HTML: {}", html);
1605            assert!(html.contains("<h1>"));
1606            assert!(html.contains("<h2>"));
1607            assert!(html.contains("<em>"));
1608            assert!(html.contains("<strong>"));
1609            assert!(html.contains("<ul>"));
1610            assert!(html.contains("<li>"));
1611            assert!(html.contains("language-rust"));
1612
1613            // Verify table content instead of specific HTML structure
1614            assert!(html.contains("Column 1"));
1615            assert!(html.contains("Column 2"));
1616            assert!(html.contains("Cell 1"));
1617            assert!(html.contains("Cell 2"));
1618
1619            assert!(html.contains("<blockquote>"));
1620            assert!(html.contains("<a href="));
1621
1622            Ok(())
1623        }
1624
1625        #[test]
1626        fn test_missing_html_config_fallback() {
1627            let config = MarkdownConfig {
1628                encoding: "utf-8".to_string(),
1629                html_config: HtmlConfig {
1630                    enable_syntax_highlighting: false,
1631                    syntax_theme: None,
1632                    ..Default::default()
1633                },
1634            };
1635            let result = markdown_to_html("# Test", Some(config));
1636            assert!(result.is_ok());
1637        }
1638
1639        #[test]
1640        fn test_invalid_output_destination() {
1641            let result = markdown_file_to_html(
1642                Some(Path::new("test.md")),
1643                Some(OutputDestination::File(
1644                    "/root/forbidden.html".to_string(),
1645                )),
1646                None,
1647            );
1648            assert!(result.is_err());
1649        }
1650    }
1651
1652    mod performance_tests {
1653        use super::*;
1654        use std::time::Instant;
1655
1656        #[test]
1657        fn test_large_document_performance() -> Result<()> {
1658            let base_content =
1659                "# Heading\n\nParagraph\n\n- List item\n\n";
1660            let large_content = base_content.repeat(1000);
1661
1662            let start = Instant::now();
1663            let html = markdown_to_html(&large_content, None)?;
1664            let duration = start.elapsed();
1665
1666            // Log performance metrics
1667            println!("Large document conversion took: {:?}", duration);
1668            println!("Input size: {} bytes", large_content.len());
1669            println!("Output size: {} bytes", html.len());
1670
1671            // Basic validation
1672            assert!(html.contains("<h1>"));
1673            assert!(html.contains("<p>"));
1674            assert!(html.contains("<ul>"));
1675
1676            Ok(())
1677        }
1678    }
1679}