ndg_commonmark/processor/
process.rs1use log::error;
3
4use super::types::{MarkdownOptions, MarkdownProcessor, TabStyle};
5use crate::types::MarkdownResult;
6
7#[must_use]
21pub fn process_with_recovery(
22 processor: &MarkdownProcessor,
23 content: &str,
24) -> MarkdownResult {
25 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
26 processor.render(content)
27 })) {
28 Ok(result) => result,
29 Err(panic_err) => {
30 error!("Panic during markdown processing: {panic_err:?}");
31 MarkdownResult {
32 html: "<div class=\"error\">Critical error processing markdown \
33 content</div>"
34 .to_string(),
35
36 headers: Vec::new(),
37 title: None,
38 included_files: Vec::new(),
39 }
40 },
41 }
42}
43
44pub fn process_safe<F>(content: &str, processor_fn: F, fallback: &str) -> String
60where
61 F: FnOnce(&str) -> String,
62{
63 if content.is_empty() {
65 return String::new();
66 }
67
68 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
70 processor_fn(content)
71 }));
72
73 match result {
74 Ok(processed_text) => processed_text,
75 Err(e) => {
76 if let Some(error_msg) = e.downcast_ref::<String>() {
78 log::error!("Error processing markup: {error_msg}");
79 } else if let Some(error_msg) = e.downcast_ref::<&str>() {
80 log::error!("Error processing markup: {error_msg}");
81 } else {
82 log::error!("Unknown error occurred while processing markup");
83 }
84
85 if fallback.is_empty() {
88 content.to_string()
89 } else {
90 fallback.to_string()
91 }
92 },
93 }
94}
95
96pub fn process_batch<I, F>(
109 processor: &MarkdownProcessor,
110 files: I,
111 read_file_fn: F,
112) -> Vec<(String, Result<MarkdownResult, String>)>
113where
114 I: Iterator<Item = std::path::PathBuf>,
115 F: Fn(&std::path::Path) -> Result<String, std::io::Error>,
116{
117 files
118 .map(|path| {
119 let path_str = path.display().to_string();
120 let result = match read_file_fn(&path) {
121 Ok(content) => Ok(process_with_recovery(processor, &content)),
122 Err(e) => Err(format!("Failed to read file: {e}")),
123 };
124 (path_str, result)
125 })
126 .collect()
127}
128
129#[must_use]
142pub fn create_processor(preset: ProcessorPreset) -> MarkdownProcessor {
143 let options = match preset {
144 ProcessorPreset::Basic => {
145 MarkdownOptions {
146 gfm: true,
147 nixpkgs: false,
148 highlight_code: true,
149 highlight_theme: None,
150 manpage_urls_path: None,
151 auto_link_options: true,
152 tab_style: TabStyle::None,
153 valid_options: None,
154 }
155 },
156 ProcessorPreset::Ndg => {
157 MarkdownOptions {
158 gfm: true,
159 nixpkgs: false,
160 highlight_code: true,
161 highlight_theme: Some("github".to_string()),
162 manpage_urls_path: None,
163 auto_link_options: true,
164 tab_style: TabStyle::None,
165 valid_options: None,
166 }
167 },
168 ProcessorPreset::Nixpkgs => {
169 MarkdownOptions {
170 gfm: true,
171 nixpkgs: true,
172 highlight_code: true,
173 highlight_theme: Some("github".to_string()),
174 manpage_urls_path: None,
175 auto_link_options: true,
176 tab_style: TabStyle::None,
177 valid_options: None,
178 }
179 },
180 };
181
182 MarkdownProcessor::new(options)
183}
184
185#[derive(Debug, Clone, Copy)]
188pub enum ProcessorPreset {
189 Basic,
191 Nixpkgs,
193 Ndg,
196}
197
198#[must_use]
210pub fn process_markdown_string(
211 content: &str,
212 preset: ProcessorPreset,
213) -> MarkdownResult {
214 let processor = create_processor(preset);
215 process_with_recovery(&processor, content)
216}
217
218pub fn process_markdown_file(
234 file_path: &std::path::Path,
235 preset: ProcessorPreset,
236) -> Result<MarkdownResult, String> {
237 let content = std::fs::read_to_string(file_path).map_err(|e| {
238 format!("Failed to read file {}: {}", file_path.display(), e)
239 })?;
240
241 let base_dir = file_path
242 .parent()
243 .unwrap_or_else(|| std::path::Path::new("."));
244 let processor = create_processor(preset).with_base_dir(base_dir);
245 Ok(process_with_recovery(&processor, &content))
246}
247
248pub fn process_markdown_file_with_basedir(
267 file_path: &std::path::Path,
268 base_dir: &std::path::Path,
269 preset: ProcessorPreset,
270) -> Result<MarkdownResult, String> {
271 let content = std::fs::read_to_string(file_path).map_err(|e| {
272 format!("Failed to read file {}: {}", file_path.display(), e)
273 })?;
274
275 let processor = create_processor(preset).with_base_dir(base_dir);
276 Ok(process_with_recovery(&processor, &content))
277}
278
279#[cfg(test)]
280mod tests {
281 use std::path::Path;
282
283 use super::*;
284
285 #[test]
286 fn test_safely_process_markup_success() {
287 let content = "test content";
288 let result =
289 process_safe(content, |s| format!("processed: {}", s), "fallback");
290 assert_eq!(result, "processed: test content");
291 }
292
293 #[test]
294 fn test_safely_process_markup_fallback() {
295 let content = "test content";
296 let result = process_safe(content, |_| panic!("test panic"), "fallback");
297 assert_eq!(result, "fallback");
298 }
299
300 #[test]
301 fn test_process_markdown_string() {
302 let content = "# Test Header\n\nSome content.";
303 let result = process_markdown_string(content, ProcessorPreset::Basic);
304
305 assert!(result.html.contains("<h1"));
306 assert!(result.html.contains("Test Header"));
307 assert_eq!(result.title, Some("Test Header".to_string()));
308 assert_eq!(result.headers.len(), 1);
309 }
310
311 #[test]
312 fn test_create_processor_presets() {
313 let basic = create_processor(ProcessorPreset::Basic);
314 assert!(basic.options.gfm);
315 assert!(!basic.options.nixpkgs);
316 assert!(basic.options.highlight_code);
317
318 let enhanced = create_processor(ProcessorPreset::Ndg);
319 assert!(enhanced.options.gfm);
320 assert!(!enhanced.options.nixpkgs);
321 assert!(enhanced.options.highlight_code);
322
323 let nixpkgs = create_processor(ProcessorPreset::Nixpkgs);
324 assert!(nixpkgs.options.gfm);
325 assert!(nixpkgs.options.nixpkgs);
326 assert!(nixpkgs.options.highlight_code);
327 }
328
329 #[test]
330 fn test_process_batch() {
331 let processor = create_processor(ProcessorPreset::Basic);
332 let paths = vec![Path::new("test1.md"), Path::new("test2.md")];
333
334 let read_fn = |path: &Path| -> Result<String, std::io::Error> {
335 match path.file_name().and_then(|n| n.to_str()) {
336 Some("test1.md") => Ok("# Test 1".to_string()),
337 Some("test2.md") => Ok("# Test 2".to_string()),
338 _ => {
339 Err(std::io::Error::new(
340 std::io::ErrorKind::NotFound,
341 "File not found",
342 ))
343 },
344 }
345 };
346
347 let results = process_batch(
348 &processor,
349 paths.into_iter().map(|p| p.to_path_buf()),
350 read_fn,
351 );
352 assert_eq!(results.len(), 2);
353
354 for (path, result) in results {
355 match result {
356 Ok(markdown_result) => {
357 assert!(markdown_result.html.contains("<h1"));
358 if path.contains("test1") {
359 assert!(markdown_result.html.contains("Test 1"));
360 } else {
361 assert!(markdown_result.html.contains("Test 2"));
362 }
363 },
364 Err(e) => assert!(false, "Unexpected error for path {}: {}", path, e),
365 }
366 }
367 }
368}