use crate::error::MarkdownError;
use crate::extensions::{
apply_syntax_highlighting, process_custom_blocks, process_tables,
};
use comrak::{markdown_to_html, ComrakOptions};
use lazy_static::lazy_static;
use log::{debug, info, warn};
use regex::Regex;
use std::fmt;
#[derive(Clone)]
pub struct MarkdownOptions<'a> {
pub comrak_options: ComrakOptions<'a>,
pub enable_custom_blocks: bool,
pub enable_syntax_highlighting: bool,
pub enable_enhanced_tables: bool,
pub syntax_theme: Option<String>,
}
impl<'a> Default for MarkdownOptions<'a> {
fn default() -> Self {
Self {
comrak_options: ComrakOptions::default(),
enable_custom_blocks: true,
enable_syntax_highlighting: true,
enable_enhanced_tables: true,
syntax_theme: None, }
}
}
impl<'a> MarkdownOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_custom_blocks(mut self, enable: bool) -> Self {
self.enable_custom_blocks = enable;
self
}
pub fn with_syntax_highlighting(mut self, enable: bool) -> Self {
self.enable_syntax_highlighting = enable;
self
}
pub fn with_enhanced_tables(mut self, enable: bool) -> Self {
self.enable_enhanced_tables = enable;
self
}
pub fn with_custom_theme(mut self, theme: String) -> Self {
self.syntax_theme = Some(theme);
self
}
pub fn with_comrak_options(
mut self,
options: ComrakOptions<'a>,
) -> Self {
self.comrak_options = options;
self
}
pub fn validate(&self) -> Result<(), String> {
if self.enable_enhanced_tables
&& !self.comrak_options.extension.table
{
return Err("Enhanced tables are enabled, but Comrak table extension is disabled.".to_string());
}
Ok(())
}
}
impl fmt::Debug for MarkdownOptions<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MarkdownOptions")
.field("enable_custom_blocks", &self.enable_custom_blocks)
.field(
"enable_syntax_highlighting",
&self.enable_syntax_highlighting,
)
.field(
"enable_enhanced_tables",
&self.enable_enhanced_tables,
)
.field("syntax_theme", &self.syntax_theme)
.finish()
}
}
pub fn default_markdown_options() -> MarkdownOptions<'static> {
MarkdownOptions::new()
.with_custom_blocks(true)
.with_syntax_highlighting(true)
.with_enhanced_tables(true)
.with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = true;
opts
})
}
pub fn process_markdown(
content: &str,
options: &MarkdownOptions,
) -> Result<String, MarkdownError> {
info!("Starting markdown processing");
debug!("Markdown options: {:?}", options);
if let Err(msg) = options.validate() {
warn!("Invalid MarkdownOptions: {}", msg);
return Err(MarkdownError::InvalidOptionsError(msg));
}
let mut comrak_opts = options.comrak_options.clone();
comrak_opts.render.unsafe_ = true;
debug!("Converting markdown to HTML using Comrak");
let mut html = markdown_to_html(content, &comrak_opts);
debug!("Initial HTML conversion result: {}", html);
if options.enable_syntax_highlighting {
debug!("Applying syntax highlighting");
match highlight_code_blocks(&html) {
Ok(highlighted) => html = highlighted,
Err(e) => {
warn!("Error during syntax highlighting: {:?}", e);
return Err(e);
}
}
}
if options.enable_enhanced_tables {
debug!("Processing enhanced tables");
html = process_tables(&html);
}
if options.enable_custom_blocks {
debug!("Processing custom blocks");
html = process_custom_blocks(&html);
}
info!("Markdown processing completed successfully");
Ok(html)
}
lazy_static! {
static ref CODE_BLOCK_RE: Regex = Regex::new(
r#"(?s)<pre><code class="language-(.*?)">(.*?)</code></pre>"#
)
.unwrap();
}
fn highlight_code_blocks(html: &str) -> Result<String, MarkdownError> {
debug!("Highlighting code blocks");
let mut highlighted_html = String::new();
let mut last_end = 0;
for cap in CODE_BLOCK_RE.captures_iter(html) {
highlighted_html
.push_str(&html[last_end..cap.get(0).unwrap().start()]);
let (lang, code) = extract_code_block(&cap)?;
let highlighted_code = highlight_code(lang, &code)?;
highlighted_html.push_str(&format_highlighted_code(
lang,
&highlighted_code,
));
last_end = cap.get(0).unwrap().end();
}
highlighted_html.push_str(&html[last_end..]);
Ok(highlighted_html)
}
fn extract_code_block<'a>(
cap: &'a regex::Captures<'a>,
) -> Result<(&'a str, String), MarkdownError> {
let lang = &cap[1];
let code = html_escape::decode_html_entities(&cap[2]);
Ok((lang, code.to_string()))
}
fn highlight_code(
lang: &str,
code: &str,
) -> Result<String, MarkdownError> {
debug!(
"Attempting to highlight code block with language: {}",
lang
);
apply_syntax_highlighting(code, lang).map_err(|e| {
MarkdownError::ConversionError(format!(
"Failed to highlight code block in language '{}': {}",
lang, e
))
})
}
fn format_highlighted_code(
lang: &str,
highlighted_code: &str,
) -> String {
format!(
"<pre><code class=\"language-{}\">{}</code></pre>",
lang, highlighted_code
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_markdown_with_all_features() {
let markdown = r#"
# Test Markdown
Here's a table:
| Left | Center | Right |
|:-----|:------:|------:|
| 1 | 2 | 3 |
```rust
fn main() {
println!("Hello, world!");
}
```
<div class="note">This is a note.</div>
<div class="warning">This is a warning.</div>
<div class="tip">This is a tip.</div>
"#;
let options = default_markdown_options();
let result = process_markdown(markdown, &options);
assert!(result.is_ok(), "Markdown processing failed");
let html = result.unwrap();
assert!(
html.contains(
r#"<div class="table-responsive"><table class="table">"#
),
"Table not processed correctly"
);
assert!(
html.contains(r#"<pre><code class="language-rust">"#),
"Syntax highlighting not applied"
);
assert!(html.contains(r#"<div class="alert alert-info" role="alert"><strong>Note:</strong>"#), "Note block not processed");
assert!(html.contains(r#"<div class="alert alert-warning" role="alert"><strong>Warning:</strong>"#), "Warning block not processed");
assert!(html.contains(r#"<div class="alert alert-success" role="alert"><strong>Tip:</strong>"#), "Tip block not processed");
}
#[test]
fn test_process_markdown_without_custom_blocks() {
let markdown = "# Test Markdown\n<div class=\"note\">This is a note.</div>";
let options = MarkdownOptions::new()
.with_custom_blocks(false)
.with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = true; opts
});
let result = process_markdown(markdown, &options);
assert!(
result.is_ok(),
"Markdown processing failed: {:?}",
result
);
let html = result.unwrap();
assert!(
html.contains("<div class=\"note\">This is a note.</div>"),
"Custom block should not be processed when disabled"
);
}
#[test]
fn test_process_markdown_without_enhanced_tables() {
let markdown = r#"
# Test Markdown
| Left | Center | Right |
|:-----|:------:|------:|
| 1 | 2 | 3 |
"#;
let options = MarkdownOptions::new()
.with_enhanced_tables(false)
.with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = true;
opts
});
let result = process_markdown(markdown, &options);
assert!(result.is_ok(), "Markdown processing failed");
let html = result.unwrap();
assert!(
!html.contains(
r#"<div class="table-responsive"><table class="table">"#
),
"Enhanced table processing applied when disabled"
);
assert!(
html.contains("<table>"),
"Basic table should still be present"
);
}
#[test]
fn test_markdown_options_validation() {
let options = MarkdownOptions::new()
.with_enhanced_tables(true)
.with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = false;
opts
});
assert!(options.validate().is_err(), "Validation should fail when enhanced tables are enabled but Comrak table extension is disabled");
let options = MarkdownOptions::new()
.with_enhanced_tables(true)
.with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = true;
opts
});
assert!(
options.validate().is_ok(),
"Validation should pass when options are consistent"
);
}
#[test]
fn test_markdown_options_builder() {
let options = MarkdownOptions::new()
.with_custom_blocks(false)
.with_syntax_highlighting(true)
.with_enhanced_tables(false);
assert!(!options.enable_custom_blocks);
assert!(options.enable_syntax_highlighting);
assert!(!options.enable_enhanced_tables);
}
#[test]
fn test_process_markdown_with_invalid_options() {
let markdown = "# Test\n\n| Column 1 | Column 2 |\n| -------- | -------- |\n| Value 1 | Value 2 |";
let options = MarkdownOptions::new()
.with_enhanced_tables(true)
.with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = false;
opts
});
let result = process_markdown(markdown, &options);
assert!(result.is_err());
assert!(matches!(
result,
Err(MarkdownError::InvalidOptionsError(_))
));
}
#[test]
fn test_process_markdown_with_empty_content() {
let markdown = "";
let options = MarkdownOptions::new()
.with_enhanced_tables(false) .with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = false; opts
});
let result = process_markdown(markdown, &options);
assert!(
result.is_ok(),
"Markdown processing failed for empty content: {:?}",
result
);
assert_eq!(result.unwrap().trim(), "");
}
#[test]
fn test_process_markdown_with_only_custom_blocks() {
let markdown = "<div class=\"note\">This is a note.</div>";
let options = MarkdownOptions::new()
.with_custom_blocks(true)
.with_enhanced_tables(false) .with_comrak_options({
let mut opts = ComrakOptions::default();
opts.extension.table = false; opts
});
let result = process_markdown(markdown, &options);
assert!(
result.is_ok(),
"Markdown processing failed for custom blocks: {:?}",
result
);
let html = result.unwrap();
assert!(html.contains(r#"<div class="alert alert-info" role="alert"><strong>Note:</strong>"#), "Custom block not processed correctly");
}
#[test]
fn test_apply_syntax_highlighting() {
let code = r#"fn main() { println!("Hello, world!"); }"#;
let result = apply_syntax_highlighting(code, "rust");
assert!(result.is_ok(), "Syntax highlighting failed");
let highlighted = result.unwrap();
assert!(
highlighted.contains("<span"),
"Highlighted code is missing expected HTML"
);
}
#[test]
fn test_process_markdown_with_no_features_enabled() {
let markdown = r#"# Title
Some plain text. "#;
let options = MarkdownOptions::new()
.with_syntax_highlighting(false)
.with_custom_blocks(false)
.with_enhanced_tables(false);
let result = process_markdown(markdown, &options);
assert!(result.is_ok(), "Markdown processing failed");
let html = result.unwrap();
assert!(
html.contains("<h1>Title</h1>"),
"Heading not processed correctly"
);
assert!(
html.contains("Some plain text."),
"Plain text not processed correctly"
);
}
}