html_generator/
performance.rs1use crate::{HtmlError, Result};
35use comrak::{markdown_to_html, ComrakOptions};
36use minify_html::{minify, Cfg};
37use std::{fs, path::Path};
38use tokio::task;
39
40pub const MAX_FILE_SIZE: usize = 10 * 1024 * 1024;
42
43const INITIAL_HTML_CAPACITY: usize = 1024;
45
46#[derive(Clone)]
52struct MinifyConfig {
53 cfg: Cfg,
55}
56
57impl Default for MinifyConfig {
58 fn default() -> Self {
59 let mut cfg = Cfg::new();
60 cfg.do_not_minify_doctype = true;
62 cfg.ensure_spec_compliant_unquoted_attribute_values = true;
63 cfg.keep_closing_tags = true;
64 cfg.keep_html_and_head_opening_tags = true;
65 cfg.keep_spaces_between_attributes = true;
66 cfg.keep_comments = false;
68 cfg.minify_css = true;
69 cfg.minify_js = true;
70 cfg.remove_bangs = true;
71 cfg.remove_processing_instructions = true;
72
73 Self { cfg }
74 }
75}
76
77impl std::fmt::Debug for MinifyConfig {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("MinifyConfig")
80 .field(
81 "do_not_minify_doctype",
82 &self.cfg.do_not_minify_doctype,
83 )
84 .field("minify_css", &self.cfg.minify_css)
85 .field("minify_js", &self.cfg.minify_js)
86 .field("keep_comments", &self.cfg.keep_comments)
87 .finish()
88 }
89}
90
91pub fn minify_html(file_path: &Path) -> Result<String> {
125 let metadata = fs::metadata(file_path).map_err(|e| {
126 HtmlError::MinificationError(format!(
127 "Failed to read file metadata for '{}': {e}",
128 file_path.display()
129 ))
130 })?;
131
132 let file_size = metadata.len() as usize;
133 if file_size > MAX_FILE_SIZE {
134 return Err(HtmlError::MinificationError(format!(
135 "File size {file_size} bytes exceeds maximum of {MAX_FILE_SIZE} bytes"
136 )));
137 }
138
139 let content = fs::read_to_string(file_path).map_err(|e| {
140 if e.to_string().contains("stream did not contain valid UTF-8")
141 {
142 HtmlError::MinificationError(format!(
143 "Invalid UTF-8 in input file '{}': {e}",
144 file_path.display()
145 ))
146 } else {
147 HtmlError::MinificationError(format!(
148 "Failed to read file '{}': {e}",
149 file_path.display()
150 ))
151 }
152 })?;
153
154 let config = MinifyConfig::default();
155 let minified = minify(content.as_bytes(), &config.cfg);
156
157 String::from_utf8(minified).map_err(|e| {
158 HtmlError::MinificationError(format!(
159 "Invalid UTF-8 in minified content: {e}"
160 ))
161 })
162}
163
164pub async fn async_generate_html(markdown: &str) -> Result<String> {
197 let markdown = if markdown.len() < INITIAL_HTML_CAPACITY {
199 markdown.to_string()
200 } else {
201 let mut string = String::with_capacity(markdown.len());
203 string.push_str(markdown);
204 string
205 };
206
207 task::spawn_blocking(move || {
208 let options = ComrakOptions::default();
209 Ok(markdown_to_html(&markdown, &options))
210 })
211 .await
212 .map_err(|e| HtmlError::MarkdownConversion {
213 message: format!("Asynchronous HTML generation failed: {e}"),
214 source: Some(std::io::Error::new(
215 std::io::ErrorKind::Other,
216 e.to_string(),
217 )),
218 })?
219}
220
221#[inline]
246pub fn generate_html(markdown: &str) -> Result<String> {
247 Ok(markdown_to_html(markdown, &ComrakOptions::default()))
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use std::fs::File;
254 use std::io::Write;
255 use tempfile::tempdir;
256
257 fn create_test_file(
267 content: &str,
268 ) -> (tempfile::TempDir, std::path::PathBuf) {
269 let dir = tempdir().expect("Failed to create temp directory");
270 let file_path = dir.path().join("test.html");
271 let mut file = File::create(&file_path)
272 .expect("Failed to create test file");
273 file.write_all(content.as_bytes())
274 .expect("Failed to write test content");
275 (dir, file_path)
276 }
277
278 mod minify_html_tests {
279 use super::*;
280
281 #[test]
282 fn test_minify_basic_html() {
283 let html =
284 "<html> <body> <p>Test</p> </body> </html>";
285 let (dir, file_path) = create_test_file(html);
286 let result = minify_html(&file_path);
287 assert!(result.is_ok());
288 assert_eq!(
289 result.unwrap(),
290 "<html><body><p>Test</p></body></html>"
291 );
292 drop(dir);
293 }
294
295 #[test]
296 fn test_minify_with_comments() {
297 let html =
298 "<html><!-- Comment --><body><p>Test</p></body></html>";
299 let (dir, file_path) = create_test_file(html);
300 let result = minify_html(&file_path);
301 assert!(result.is_ok());
302 assert_eq!(
303 result.unwrap(),
304 "<html><body><p>Test</p></body></html>"
305 );
306 drop(dir);
307 }
308
309 #[test]
310 fn test_minify_invalid_path() {
311 let result = minify_html(Path::new("nonexistent.html"));
312 assert!(result.is_err());
313 assert!(matches!(
314 result,
315 Err(HtmlError::MinificationError(_))
316 ));
317 }
318
319 #[test]
320 fn test_minify_exceeds_max_size() {
321 let large_content = "a".repeat(MAX_FILE_SIZE + 1);
322 let (dir, file_path) = create_test_file(&large_content);
323 let result = minify_html(&file_path);
324 assert!(matches!(
325 result,
326 Err(HtmlError::MinificationError(_))
327 ));
328 let err_msg = result.unwrap_err().to_string();
329 assert!(err_msg.contains("exceeds maximum"));
330 drop(dir);
331 }
332
333 #[test]
334 fn test_minify_invalid_utf8() {
335 let dir =
336 tempdir().expect("Failed to create temp directory");
337 let file_path = dir.path().join("invalid.html");
338 {
339 let mut file = File::create(&file_path)
340 .expect("Failed to create test file");
341 file.write_all(&[0xFF, 0xFF])
342 .expect("Failed to write test content");
343 }
344
345 let result = minify_html(&file_path);
346 assert!(matches!(
347 result,
348 Err(HtmlError::MinificationError(_))
349 ));
350 let err_msg = result.unwrap_err().to_string();
351 assert!(err_msg.contains("Invalid UTF-8 in input file"));
352 drop(dir);
353 }
354
355 #[test]
356 fn test_minify_utf8_content() {
357 let html = "<html><body><p>Test 你好 🦀</p></body></html>";
358 let (dir, file_path) = create_test_file(html);
359 let result = minify_html(&file_path);
360 assert!(result.is_ok());
361 assert_eq!(
362 result.unwrap(),
363 "<html><body><p>Test 你好 🦀</p></body></html>"
364 );
365 drop(dir);
366 }
367 }
368
369 mod async_generate_html_tests {
370 use super::*;
371
372 #[tokio::test]
373 async fn test_async_generate_html() {
374 let markdown = "# Test\n\nThis is a test.";
375 let result = async_generate_html(markdown).await;
376 assert!(result.is_ok());
377 let html = result.unwrap();
378 assert!(html.contains("<h1>Test</h1>"));
379 assert!(html.contains("<p>This is a test.</p>"));
380 }
381
382 #[tokio::test]
383 async fn test_async_generate_html_empty() {
384 let result = async_generate_html("").await;
385 assert!(result.is_ok());
386 assert!(result.unwrap().is_empty());
387 }
388
389 #[tokio::test]
390 async fn test_async_generate_html_large_content() {
391 let large_markdown =
392 "# Test\n\n".to_string() + &"Content\n".repeat(10_000);
393 let result = async_generate_html(&large_markdown).await;
394 assert!(result.is_ok());
395 let html = result.unwrap();
396 assert!(html.contains("<h1>Test</h1>"));
397 }
398 }
399
400 mod generate_html_tests {
401 use super::*;
402
403 #[test]
404 fn test_sync_generate_html() {
405 let markdown = "# Test\n\nThis is a test.";
406 let result = generate_html(markdown);
407 assert!(result.is_ok());
408 let html = result.unwrap();
409 assert!(html.contains("<h1>Test</h1>"));
410 assert!(html.contains("<p>This is a test.</p>"));
411 }
412
413 #[test]
414 fn test_sync_generate_html_empty() {
415 let result = generate_html("");
416 assert!(result.is_ok());
417 assert!(result.unwrap().is_empty());
418 }
419
420 #[test]
421 fn test_sync_generate_html_large_content() {
422 let large_markdown =
423 "# Test\n\n".to_string() + &"Content\n".repeat(10_000);
424 let result = generate_html(&large_markdown);
425 assert!(result.is_ok());
426 let html = result.unwrap();
427 assert!(html.contains("<h1>Test</h1>"));
428 }
429 }
430
431 mod additional_tests {
432 use super::*;
433 use std::fs::File;
434 use std::io::Write;
435 use tempfile::tempdir;
436
437 #[test]
439 fn test_minify_config_default() {
440 let config = MinifyConfig::default();
441 assert!(config.cfg.do_not_minify_doctype);
442 assert!(config.cfg.minify_css);
443 assert!(config.cfg.minify_js);
444 assert!(!config.cfg.keep_comments);
445 }
446
447 #[test]
449 fn test_minify_config_custom() {
450 let mut config = MinifyConfig::default();
451 config.cfg.keep_comments = true;
452 assert!(config.cfg.keep_comments);
453 }
454
455 #[test]
457 fn test_minify_html_uncommon_structures() {
458 let html = r#"<div><span>Test<div><p>Nested</p></div></span></div>"#;
459 let (dir, file_path) = create_test_file(html);
460 let result = minify_html(&file_path);
461 assert!(result.is_ok());
462 assert_eq!(
463 result.unwrap(),
464 r#"<div><span>Test<div><p>Nested</p></div></span></div>"#
465 );
466 drop(dir);
467 }
468
469 #[test]
471 fn test_minify_html_mixed_encodings() {
472 let dir =
473 tempdir().expect("Failed to create temp directory");
474 let file_path = dir.path().join("mixed_encoding.html");
475 {
476 let mut file = File::create(&file_path)
477 .expect("Failed to create test file");
478 file.write_all(&[0xFF, b'T', b'e', b's', b't', 0xFE])
479 .expect("Failed to write test content");
480 }
481 let result = minify_html(&file_path);
482 assert!(matches!(
483 result,
484 Err(HtmlError::MinificationError(_))
485 ));
486 drop(dir);
487 }
488
489 #[tokio::test]
491 async fn test_async_generate_html_extremely_large() {
492 let large_markdown = "# Large Content
493"
494 .to_string()
495 + &"Content
496"
497 .repeat(100_000);
498 let result = async_generate_html(&large_markdown).await;
499 assert!(result.is_ok());
500 let html = result.unwrap();
501 assert!(html.contains("<h1>Large Content</h1>"));
502 }
503
504 #[test]
506 fn test_generate_html_very_small() {
507 let markdown = "A";
508 let result = generate_html(markdown);
509 assert!(result.is_ok());
510 assert_eq!(
511 result.unwrap(),
512 "<p>A</p>
513"
514 );
515 }
516
517 #[tokio::test]
518 async fn test_async_generate_html_spawn_blocking_failure() {
519 use tokio::task;
520
521 let _markdown = "# Valid Markdown"; let result = task::spawn_blocking(|| {
526 panic!("Simulated task failure"); })
528 .await;
529
530 let converted_result: std::result::Result<
532 String,
533 HtmlError,
534 > = match result {
535 Err(e) => Err(HtmlError::MarkdownConversion {
536 message: format!(
537 "Asynchronous HTML generation failed: {e}"
538 ),
539 source: Some(std::io::Error::new(
540 std::io::ErrorKind::Other,
541 e.to_string(),
542 )),
543 }),
544 Ok(_) => panic!("Expected a simulated failure"),
545 };
546
547 assert!(matches!(
549 converted_result,
550 Err(HtmlError::MarkdownConversion { .. })
551 ));
552
553 if let Err(HtmlError::MarkdownConversion {
554 message,
555 source,
556 }) = converted_result
557 {
558 assert!(message
559 .contains("Asynchronous HTML generation failed"));
560 assert!(source.is_some());
561
562 let source_message = source.unwrap().to_string();
564 assert!(
565 source_message.contains("Simulated task failure"),
566 "Unexpected source message: {source_message}"
567 );
568 }
569 }
570
571 #[test]
572 fn test_minify_html_empty_content() {
573 let html = "";
574 let (dir, file_path) = create_test_file(html);
575 let result = minify_html(&file_path);
576 assert!(result.is_ok());
577 assert!(
578 result.unwrap().is_empty(),
579 "Minified content should be empty"
580 );
581 drop(dir);
582 }
583
584 #[test]
585 fn test_minify_html_unusual_whitespace() {
586 let html =
587 "<html>\n\n\t<body>\t<p>Test</p>\n\n</body>\n\n</html>";
588 let (dir, file_path) = create_test_file(html);
589 let result = minify_html(&file_path);
590 assert!(result.is_ok());
591 assert_eq!(
592 result.unwrap(),
593 "<html><body><p>Test</p></body></html>",
594 "Unexpected minified result for unusual whitespace"
595 );
596 drop(dir);
597 }
598
599 #[test]
600 fn test_minify_html_with_special_characters() {
601 let html = "<div><Special> & Characters</div>";
602 let (dir, file_path) = create_test_file(html);
603 let result = minify_html(&file_path);
604 assert!(result.is_ok());
605 assert_eq!(
606 result.unwrap(),
607 "<div><Special> & Characters</div>",
608 "Special characters were unexpectedly modified during minification"
609 );
610 drop(dir);
611 }
612
613 #[tokio::test]
614 async fn test_async_generate_html_with_special_characters() {
615 let markdown =
616 "# Special & Characters\n\nContent with < > & \" '";
617 let result = async_generate_html(markdown).await;
618 assert!(result.is_ok());
619 let html = result.unwrap();
620 assert!(
621 html.contains("<"),
622 "Less than sign not escaped"
623 );
624 assert!(
625 html.contains(">"),
626 "Greater than sign not escaped"
627 );
628 assert!(html.contains("&"), "Ampersand not escaped");
629 assert!(
630 html.contains("""),
631 "Double quote not escaped"
632 );
633 assert!(
634 html.contains("'") || html.contains("'"),
635 "Single quote not handled as expected"
636 );
637 }
638 }
639}