use log::error;
use super::types::{MarkdownOptions, MarkdownProcessor, TabStyle};
use crate::types::MarkdownResult;
#[must_use]
pub fn process_with_recovery(
processor: &MarkdownProcessor,
content: &str,
) -> MarkdownResult {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
processor.render(content)
})) {
Ok(result) => result,
Err(panic_err) => {
error!("Panic during markdown processing: {panic_err:?}");
MarkdownResult {
html: "<div class=\"error\">Critical error processing markdown \
content</div>"
.to_string(),
headers: Vec::new(),
title: None,
included_files: Vec::new(),
}
},
}
}
pub fn process_safe<F>(content: &str, processor_fn: F, fallback: &str) -> String
where
F: FnOnce(&str) -> String,
{
if content.is_empty() {
return String::new();
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
processor_fn(content)
}));
match result {
Ok(processed_text) => processed_text,
Err(e) => {
if let Some(error_msg) = e.downcast_ref::<String>() {
log::error!("Error processing markup: {error_msg}");
} else if let Some(error_msg) = e.downcast_ref::<&str>() {
log::error!("Error processing markup: {error_msg}");
} else {
log::error!("Unknown error occurred while processing markup");
}
if fallback.is_empty() {
content.to_string()
} else {
fallback.to_string()
}
},
}
}
pub fn process_batch<I, F>(
processor: &MarkdownProcessor,
files: I,
read_file_fn: F,
) -> Vec<(String, Result<MarkdownResult, String>)>
where
I: Iterator<Item = std::path::PathBuf>,
F: Fn(&std::path::Path) -> Result<String, std::io::Error>,
{
files
.map(|path| {
let path_str = path.display().to_string();
let result = match read_file_fn(&path) {
Ok(content) => Ok(process_with_recovery(processor, &content)),
Err(e) => Err(format!("Failed to read file: {e}")),
};
(path_str, result)
})
.collect()
}
#[must_use]
pub fn create_processor(preset: ProcessorPreset) -> MarkdownProcessor {
let options = match preset {
ProcessorPreset::Basic => {
MarkdownOptions {
gfm: true,
nixpkgs: false,
highlight_code: true,
highlight_theme: None,
manpage_urls_path: None,
auto_link_options: true,
tab_style: TabStyle::None,
valid_options: None,
}
},
ProcessorPreset::Ndg => {
MarkdownOptions {
gfm: true,
nixpkgs: false,
highlight_code: true,
highlight_theme: Some("github".to_string()),
manpage_urls_path: None,
auto_link_options: true,
tab_style: TabStyle::None,
valid_options: None,
}
},
ProcessorPreset::Nixpkgs => {
MarkdownOptions {
gfm: true,
nixpkgs: true,
highlight_code: true,
highlight_theme: Some("github".to_string()),
manpage_urls_path: None,
auto_link_options: true,
tab_style: TabStyle::None,
valid_options: None,
}
},
};
MarkdownProcessor::new(options)
}
#[derive(Debug, Clone, Copy)]
pub enum ProcessorPreset {
Basic,
Nixpkgs,
Ndg,
}
#[must_use]
pub fn process_markdown_string(
content: &str,
preset: ProcessorPreset,
) -> MarkdownResult {
let processor = create_processor(preset);
process_with_recovery(&processor, content)
}
pub fn process_markdown_file(
file_path: &std::path::Path,
preset: ProcessorPreset,
) -> Result<MarkdownResult, String> {
let content = std::fs::read_to_string(file_path).map_err(|e| {
format!("Failed to read file {}: {}", file_path.display(), e)
})?;
let base_dir = file_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let processor = create_processor(preset).with_base_dir(base_dir);
Ok(process_with_recovery(&processor, &content))
}
pub fn process_markdown_file_with_basedir(
file_path: &std::path::Path,
base_dir: &std::path::Path,
preset: ProcessorPreset,
) -> Result<MarkdownResult, String> {
let content = std::fs::read_to_string(file_path).map_err(|e| {
format!("Failed to read file {}: {}", file_path.display(), e)
})?;
let processor = create_processor(preset).with_base_dir(base_dir);
Ok(process_with_recovery(&processor, &content))
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn test_safely_process_markup_success() {
let content = "test content";
let result =
process_safe(content, |s| format!("processed: {}", s), "fallback");
assert_eq!(result, "processed: test content");
}
#[test]
#[allow(clippy::panic)]
fn test_safely_process_markup_fallback() {
let content = "test content";
let result = process_safe(content, |_| panic!("test panic"), "fallback");
assert_eq!(result, "fallback");
}
#[test]
fn test_process_markdown_string() {
let content = "# Test Header\n\nSome content.";
let result = process_markdown_string(content, ProcessorPreset::Basic);
assert!(result.html.contains("<h1"));
assert!(result.html.contains("Test Header"));
assert_eq!(result.title, Some("Test Header".to_string()));
assert_eq!(result.headers.len(), 1);
}
#[test]
fn test_create_processor_presets() {
let basic = create_processor(ProcessorPreset::Basic);
assert!(basic.options.gfm);
assert!(!basic.options.nixpkgs);
assert!(basic.options.highlight_code);
let enhanced = create_processor(ProcessorPreset::Ndg);
assert!(enhanced.options.gfm);
assert!(!enhanced.options.nixpkgs);
assert!(enhanced.options.highlight_code);
let nixpkgs = create_processor(ProcessorPreset::Nixpkgs);
assert!(nixpkgs.options.gfm);
assert!(nixpkgs.options.nixpkgs);
assert!(nixpkgs.options.highlight_code);
}
#[test]
fn test_process_batch() {
let processor = create_processor(ProcessorPreset::Basic);
let paths = vec![Path::new("test1.md"), Path::new("test2.md")];
let read_fn = |path: &Path| -> Result<String, std::io::Error> {
match path.file_name().and_then(|n| n.to_str()) {
Some("test1.md") => Ok("# Test 1".to_string()),
Some("test2.md") => Ok("# Test 2".to_string()),
_ => {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"File not found",
))
},
}
};
let results = process_batch(
&processor,
paths.into_iter().map(|p| p.to_path_buf()),
read_fn,
);
assert_eq!(results.len(), 2);
for (path, result) in results {
match result {
Ok(markdown_result) => {
assert!(markdown_result.html.contains("<h1"));
if path.contains("test1") {
assert!(markdown_result.html.contains("Test 1"));
} else {
assert!(markdown_result.html.contains("Test 2"));
}
},
Err(e) => assert!(false, "Unexpected error for path {}: {}", path, e),
}
}
}
}