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 #[allow(clippy::panic)]
295 fn test_safely_process_markup_fallback() {
296 let content = "test content";
297 let result = process_safe(content, |_| panic!("test panic"), "fallback");
298 assert_eq!(result, "fallback");
299 }
300
301 #[test]
302 fn test_process_markdown_string() {
303 let content = "# Test Header\n\nSome content.";
304 let result = process_markdown_string(content, ProcessorPreset::Basic);
305
306 assert!(result.html.contains("<h1"));
307 assert!(result.html.contains("Test Header"));
308 assert_eq!(result.title, Some("Test Header".to_string()));
309 assert_eq!(result.headers.len(), 1);
310 }
311
312 #[test]
313 fn test_create_processor_presets() {
314 let basic = create_processor(ProcessorPreset::Basic);
315 assert!(basic.options.gfm);
316 assert!(!basic.options.nixpkgs);
317 assert!(basic.options.highlight_code);
318
319 let enhanced = create_processor(ProcessorPreset::Ndg);
320 assert!(enhanced.options.gfm);
321 assert!(!enhanced.options.nixpkgs);
322 assert!(enhanced.options.highlight_code);
323
324 let nixpkgs = create_processor(ProcessorPreset::Nixpkgs);
325 assert!(nixpkgs.options.gfm);
326 assert!(nixpkgs.options.nixpkgs);
327 assert!(nixpkgs.options.highlight_code);
328 }
329
330 #[test]
331 fn test_process_batch() {
332 let processor = create_processor(ProcessorPreset::Basic);
333 let paths = vec![Path::new("test1.md"), Path::new("test2.md")];
334
335 let read_fn = |path: &Path| -> Result<String, std::io::Error> {
336 match path.file_name().and_then(|n| n.to_str()) {
337 Some("test1.md") => Ok("# Test 1".to_string()),
338 Some("test2.md") => Ok("# Test 2".to_string()),
339 _ => {
340 Err(std::io::Error::new(
341 std::io::ErrorKind::NotFound,
342 "File not found",
343 ))
344 },
345 }
346 };
347
348 let results = process_batch(
349 &processor,
350 paths.into_iter().map(|p| p.to_path_buf()),
351 read_fn,
352 );
353 assert_eq!(results.len(), 2);
354
355 for (path, result) in results {
356 match result {
357 Ok(markdown_result) => {
358 assert!(markdown_result.html.contains("<h1"));
359 if path.contains("test1") {
360 assert!(markdown_result.html.contains("Test 1"));
361 } else {
362 assert!(markdown_result.html.contains("Test 2"));
363 }
364 },
365 Err(e) => assert!(false, "Unexpected error for path {}: {}", path, e),
366 }
367 }
368 }
369}