ndg_commonmark/processor/
process.rs1use std::{
3 fs,
4 io::Error,
5 path::{Path, PathBuf},
6};
7
8use log::error;
9
10use super::types::{MarkdownOptions, MarkdownProcessor, TabStyle};
11use crate::types::MarkdownResult;
12
13#[must_use]
27pub fn process_with_recovery(
28 processor: &MarkdownProcessor,
29 content: &str,
30) -> MarkdownResult {
31 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
32 processor.render(content)
33 })) {
34 Ok(result) => result,
35 Err(panic_err) => {
36 error!("Panic during markdown processing: {panic_err:?}");
37 MarkdownResult {
38 html: "<div class=\"error\">Critical error processing markdown \
39 content</div>"
40 .to_string(),
41
42 headers: Vec::new(),
43 title: None,
44 included_files: Vec::new(),
45 }
46 },
47 }
48}
49
50pub fn process_safe<F>(content: &str, processor_fn: F, fallback: &str) -> String
66where
67 F: FnOnce(&str) -> String,
68{
69 if content.is_empty() {
71 return String::new();
72 }
73
74 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
76 processor_fn(content)
77 }));
78
79 match result {
80 Ok(processed_text) => processed_text,
81 Err(e) => {
82 if let Some(error_msg) = e.downcast_ref::<String>() {
84 error!("Error processing markup: {error_msg}");
85 } else if let Some(error_msg) = e.downcast_ref::<&str>() {
86 error!("Error processing markup: {error_msg}");
87 } else {
88 error!("Unknown error occurred while processing markup");
89 }
90
91 if fallback.is_empty() {
94 content.to_string()
95 } else {
96 fallback.to_string()
97 }
98 },
99 }
100}
101
102pub fn process_batch<I, F>(
115 processor: &MarkdownProcessor,
116 files: I,
117 read_file_fn: F,
118) -> Vec<(String, Result<MarkdownResult, String>)>
119where
120 I: Iterator<Item = PathBuf>,
121 F: Fn(&Path) -> Result<String, Error>,
122{
123 files
124 .map(|path| {
125 let path_str = path.display().to_string();
126 let result = match read_file_fn(&path) {
127 Ok(content) => Ok(process_with_recovery(processor, &content)),
128 Err(e) => Err(format!("Failed to read file: {e}")),
129 };
130 (path_str, result)
131 })
132 .collect()
133}
134
135#[must_use]
148pub fn create_processor(preset: ProcessorPreset) -> MarkdownProcessor {
149 let options = match preset {
150 ProcessorPreset::Basic => {
151 MarkdownOptions {
152 gfm: true,
153 nixpkgs: false,
154 highlight_code: true,
155 highlight_theme: None,
156 manpage_urls_path: None,
157 syntax_queries_path: None,
158 auto_link_options: true,
159 tab_style: TabStyle::None,
160 valid_options: None,
161 }
162 },
163 ProcessorPreset::Ndg => {
164 MarkdownOptions {
165 gfm: true,
166 nixpkgs: false,
167 highlight_code: true,
168 highlight_theme: Some("github".to_string()),
169 manpage_urls_path: None,
170 syntax_queries_path: None,
171 auto_link_options: true,
172 tab_style: TabStyle::None,
173 valid_options: None,
174 }
175 },
176 ProcessorPreset::Nixpkgs => {
177 MarkdownOptions {
178 gfm: true,
179 nixpkgs: true,
180 highlight_code: true,
181 highlight_theme: Some("github".to_string()),
182 manpage_urls_path: None,
183 syntax_queries_path: None,
184 auto_link_options: true,
185 tab_style: TabStyle::None,
186 valid_options: None,
187 }
188 },
189 };
190
191 MarkdownProcessor::new(options)
192}
193
194#[derive(Debug, Clone, Copy)]
197pub enum ProcessorPreset {
198 Basic,
200 Nixpkgs,
202 Ndg,
205}
206
207#[must_use]
219pub fn process_markdown_string(
220 content: &str,
221 preset: ProcessorPreset,
222) -> MarkdownResult {
223 let processor = create_processor(preset);
224 process_with_recovery(&processor, content)
225}
226
227pub fn process_markdown_file(
243 file_path: &Path,
244 preset: ProcessorPreset,
245) -> Result<MarkdownResult, String> {
246 let content = fs::read_to_string(file_path).map_err(|e| {
247 format!("Failed to read file {}: {}", file_path.display(), e)
248 })?;
249
250 let base_dir = file_path.parent().unwrap_or_else(|| Path::new("."));
251 let processor = create_processor(preset).with_base_dir(base_dir);
252 Ok(process_with_recovery(&processor, &content))
253}
254
255pub fn process_markdown_file_with_basedir(
274 file_path: &Path,
275 base_dir: &Path,
276 preset: ProcessorPreset,
277) -> Result<MarkdownResult, String> {
278 let content = fs::read_to_string(file_path).map_err(|e| {
279 format!("Failed to read file {}: {}", file_path.display(), e)
280 })?;
281
282 let processor = create_processor(preset).with_base_dir(base_dir);
283 Ok(process_with_recovery(&processor, &content))
284}
285
286#[cfg(test)]
287mod tests {
288 use std::path::Path;
289
290 use super::*;
291
292 #[test]
293 fn test_safely_process_markup_success() {
294 let content = "test content";
295 let result =
296 process_safe(content, |s| format!("processed: {s}"), "fallback");
297 assert_eq!(result, "processed: test content");
298 }
299
300 #[test]
301 #[allow(clippy::panic)]
302 fn test_safely_process_markup_fallback() {
303 let content = "test content";
304 let result = process_safe(content, |_| panic!("test panic"), "fallback");
305 assert_eq!(result, "fallback");
306 }
307
308 #[test]
309 fn test_process_markdown_string() {
310 let content = "# Test Header\n\nSome content.";
311 let result = process_markdown_string(content, ProcessorPreset::Basic);
312
313 assert!(result.html.contains("<h1"));
314 assert!(result.html.contains("Test Header"));
315 assert_eq!(result.title, Some("Test Header".to_string()));
316 assert_eq!(result.headers.len(), 1);
317 }
318
319 #[test]
320 fn test_create_processor_presets() {
321 let basic = create_processor(ProcessorPreset::Basic);
322 assert!(basic.options.gfm);
323 assert!(!basic.options.nixpkgs);
324 assert!(basic.options.highlight_code);
325
326 let enhanced = create_processor(ProcessorPreset::Ndg);
327 assert!(enhanced.options.gfm);
328 assert!(!enhanced.options.nixpkgs);
329 assert!(enhanced.options.highlight_code);
330
331 let nixpkgs = create_processor(ProcessorPreset::Nixpkgs);
332 assert!(nixpkgs.options.gfm);
333 assert!(nixpkgs.options.nixpkgs);
334 assert!(nixpkgs.options.highlight_code);
335 }
336
337 #[test]
338 #[allow(clippy::panic)]
339 fn test_process_batch() {
340 let processor = create_processor(ProcessorPreset::Basic);
341 let paths = vec![Path::new("test1.md"), Path::new("test2.md")];
342
343 let read_fn = |path: &Path| -> Result<String, std::io::Error> {
344 match path.file_name().and_then(|n| n.to_str()) {
345 Some("test1.md") => Ok("# Test 1".to_string()),
346 Some("test2.md") => Ok("# Test 2".to_string()),
347 _ => {
348 Err(std::io::Error::new(
349 std::io::ErrorKind::NotFound,
350 "File not found",
351 ))
352 },
353 }
354 };
355
356 let results = process_batch(
357 &processor,
358 paths.into_iter().map(std::path::Path::to_path_buf),
359 read_fn,
360 );
361 assert_eq!(results.len(), 2);
362
363 for (path, result) in results {
364 match result {
365 Ok(markdown_result) => {
366 assert!(markdown_result.html.contains("<h1"));
367 if path.contains("test1") {
368 assert!(markdown_result.html.contains("Test 1"));
369 } else {
370 assert!(markdown_result.html.contains("Test 2"));
371 }
372 },
373 Err(e) => panic!("Unexpected error for path {path}: {e}"),
374 }
375 }
376 }
377}