html_generator/
performance.rs1use crate::{HtmlError, Result};
35use minify_html::{minify, Cfg};
36use std::{fs, path::Path};
37
38#[cfg(feature = "async")]
39use tokio::task;
40
41pub const MAX_FILE_SIZE: usize = 10 * 1024 * 1024;
54
55#[derive(Clone)]
61struct MinifyConfig {
62 cfg: Cfg,
64}
65
66impl Default for MinifyConfig {
67 fn default() -> Self {
68 let mut cfg = Cfg::new();
69 cfg.minify_doctype = false;
71 cfg.allow_noncompliant_unquoted_attribute_values = false;
72 cfg.keep_closing_tags = true;
73 cfg.keep_html_and_head_opening_tags = true;
74 cfg.allow_removing_spaces_between_attributes = false;
75 cfg.keep_comments = false;
77 cfg.minify_css = true;
78 cfg.minify_js = true;
79 cfg.remove_bangs = true;
80 cfg.remove_processing_instructions = true;
81
82 Self { cfg }
83 }
84}
85
86impl std::fmt::Debug for MinifyConfig {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("MinifyConfig")
89 .field("minify_doctype", &self.cfg.minify_doctype)
90 .field("minify_css", &self.cfg.minify_css)
91 .field("minify_js", &self.cfg.minify_js)
92 .field("keep_comments", &self.cfg.keep_comments)
93 .finish()
94 }
95}
96
97pub fn minify_html(file_path: &Path) -> Result<String> {
131 let metadata = fs::metadata(file_path).map_err(|e| {
132 HtmlError::MinificationError(format!(
133 "Failed to read file metadata for '{}': {e}",
134 file_path.display()
135 ))
136 })?;
137
138 let file_size = metadata.len() as usize;
139 if file_size > MAX_FILE_SIZE {
140 return Err(HtmlError::MinificationError(format!(
141 "File size {file_size} bytes exceeds maximum of {MAX_FILE_SIZE} bytes"
142 )));
143 }
144
145 let content = fs::read_to_string(file_path).map_err(|e| {
146 let kind = if e
151 .to_string()
152 .contains("stream did not contain valid UTF-8")
153 {
154 "Invalid UTF-8 in input file"
155 } else {
156 "Failed to read file"
157 };
158 HtmlError::MinificationError(format!(
159 "{kind} '{}': {e}",
160 file_path.display()
161 ))
162 })?;
163
164 let config = MinifyConfig::default();
165 let minified = minify(content.as_bytes(), &config.cfg);
166
167 Ok(String::from_utf8_lossy(&minified).into_owned())
172}
173
174pub fn minify_html_string(html: &str) -> Result<String> {
205 if html.len() > MAX_FILE_SIZE {
206 return Err(HtmlError::MinificationError(format!(
207 "Input size {} bytes exceeds maximum of {MAX_FILE_SIZE} bytes",
208 html.len()
209 )));
210 }
211
212 let config = MinifyConfig::default();
213 let minified = minify(html.as_bytes(), &config.cfg);
214
215 Ok(String::from_utf8_lossy(&minified).into_owned())
217}
218
219#[cfg(feature = "async")]
252pub async fn async_generate_html(markdown: &str) -> Result<String> {
253 let markdown = markdown.to_string();
254 task::spawn_blocking(move || {
255 crate::generator::markdown_to_html_with_extensions(&markdown)
256 })
257 .await
258 .map_err(|e| HtmlError::MarkdownConversion {
259 message: format!("Asynchronous HTML generation failed: {e}"),
260 source: Some(std::io::Error::other(e.to_string())),
261 })?
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::fs::File;
268 use std::io::Write;
269 use tempfile::tempdir;
270
271 fn create_test_file(
281 content: &str,
282 ) -> (tempfile::TempDir, std::path::PathBuf) {
283 let dir = tempdir().expect("Failed to create temp directory");
284 let file_path = dir.path().join("test.html");
285 let mut file = File::create(&file_path)
286 .expect("Failed to create test file");
287 file.write_all(content.as_bytes())
288 .expect("Failed to write test content");
289 (dir, file_path)
290 }
291
292 mod minify_html_tests {
293 use super::*;
294
295 #[test]
296 fn test_minify_basic_html() {
297 let html =
298 "<html> <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_with_comments() {
311 let html =
312 "<html><!-- Comment --><body><p>Test</p></body></html>";
313 let (dir, file_path) = create_test_file(html);
314 let result = minify_html(&file_path);
315 assert!(result.is_ok());
316 assert_eq!(
317 result.unwrap(),
318 "<html><body><p>Test</p></body></html>"
319 );
320 drop(dir);
321 }
322
323 #[test]
324 fn test_minify_invalid_path() {
325 let result = minify_html(Path::new("nonexistent.html"));
326 assert!(result.is_err());
327 assert!(matches!(
328 result,
329 Err(HtmlError::MinificationError(_))
330 ));
331 }
332
333 #[test]
334 fn test_minify_exceeds_max_size() {
335 let large_content = "a".repeat(MAX_FILE_SIZE + 1);
336 let (dir, file_path) = create_test_file(&large_content);
337 let result = minify_html(&file_path);
338 assert!(matches!(
339 result,
340 Err(HtmlError::MinificationError(_))
341 ));
342 let err_msg = result.unwrap_err().to_string();
343 assert!(err_msg.contains("exceeds maximum"));
344 drop(dir);
345 }
346
347 #[test]
348 fn test_minify_invalid_utf8() {
349 let dir =
350 tempdir().expect("Failed to create temp directory");
351 let file_path = dir.path().join("invalid.html");
352 {
353 let mut file = File::create(&file_path)
354 .expect("Failed to create test file");
355 file.write_all(&[0xFF, 0xFF])
356 .expect("Failed to write test content");
357 }
358
359 let result = minify_html(&file_path);
360 assert!(matches!(
361 result,
362 Err(HtmlError::MinificationError(_))
363 ));
364 let err_msg = result.unwrap_err().to_string();
365 assert!(err_msg.contains("Invalid UTF-8 in input file"));
366 drop(dir);
367 }
368
369 #[test]
370 fn test_minify_non_utf8_failure_path_via_directory_path() {
371 let dir =
378 tempdir().expect("Failed to create temp directory");
379 let result = minify_html(dir.path());
380 assert!(matches!(
381 result,
382 Err(HtmlError::MinificationError(_))
383 ));
384 let err_msg = result.unwrap_err().to_string();
385 assert!(
386 err_msg.contains("Failed to read file"),
387 "expected 'Failed to read file' branch, got: {err_msg}"
388 );
389 drop(dir);
390 }
391
392 #[test]
393 fn test_minify_utf8_content() {
394 let html = "<html><body><p>Test 你好 🦀</p></body></html>";
395 let (dir, file_path) = create_test_file(html);
396 let result = minify_html(&file_path);
397 assert!(result.is_ok());
398 assert_eq!(
399 result.unwrap(),
400 "<html><body><p>Test 你好 🦀</p></body></html>"
401 );
402 drop(dir);
403 }
404 }
405
406 #[cfg(feature = "async")]
407 mod async_generate_html_tests {
408 use super::*;
409
410 #[tokio::test]
411 async fn test_async_generate_html() {
412 let markdown = "# Test\n\nThis is a test.";
413 let result = async_generate_html(markdown).await;
414 assert!(result.is_ok());
415 let html = result.unwrap();
416 assert!(html.contains("<h1>Test</h1>"));
417 assert!(html.contains("<p>This is a test.</p>"));
418 }
419
420 #[tokio::test]
421 async fn test_async_generate_html_empty() {
422 let result = async_generate_html("").await;
423 assert!(result.is_ok());
424 assert!(result.unwrap().is_empty());
425 }
426
427 #[tokio::test]
428 async fn test_async_generate_html_large_content() {
429 let large_markdown =
430 "# Test\n\n".to_string() + &"Content\n".repeat(10_000);
431 let result = async_generate_html(&large_markdown).await;
432 assert!(result.is_ok());
433 let html = result.unwrap();
434 assert!(html.contains("<h1>Test</h1>"));
435 }
436 }
437
438 mod additional_tests {
439 use super::*;
440 use std::fs::File;
441 use std::io::Write;
442 use tempfile::tempdir;
443
444 #[test]
446 fn test_minify_config_default() {
447 let config = MinifyConfig::default();
448 assert!(!config.cfg.minify_doctype);
449 assert!(config.cfg.minify_css);
450 assert!(config.cfg.minify_js);
451 assert!(!config.cfg.keep_comments);
452 }
453
454 #[test]
456 fn test_minify_config_custom() {
457 let mut config = MinifyConfig::default();
458 config.cfg.keep_comments = true;
459 assert!(config.cfg.keep_comments);
460 }
461
462 #[test]
466 fn test_minify_config_debug_impl() {
467 let config = MinifyConfig::default();
468 let rendered = format!("{config:?}");
469 assert!(rendered.contains("MinifyConfig"));
470 assert!(rendered.contains("minify_css"));
471 }
472
473 #[test]
476 fn test_minify_html_rejects_non_utf8_path_content() {
477 let dir = tempdir().expect("failed to create temp dir");
478 let file_path = dir.path().join("non-utf8.html");
479 let mut f = File::create(&file_path).expect("create file");
480 f.write_all(&[0xFF, 0xFE, 0xFD, 0xFC])
481 .expect("write bytes");
482 drop(f);
483 let err = minify_html(&file_path).unwrap_err();
484 assert!(matches!(err, HtmlError::MinificationError(_)));
485 }
486
487 #[test]
489 fn test_minify_html_uncommon_structures() {
490 let html = r#"<div><span>Test<div><p>Nested</p></div></span></div>"#;
491 let (dir, file_path) = create_test_file(html);
492 let result = minify_html(&file_path);
493 assert!(result.is_ok());
494 assert_eq!(
495 result.unwrap(),
496 r#"<div><span>Test<div><p>Nested</p></div></span></div>"#
497 );
498 drop(dir);
499 }
500
501 #[test]
503 fn test_minify_html_mixed_encodings() {
504 let dir =
505 tempdir().expect("Failed to create temp directory");
506 let file_path = dir.path().join("mixed_encoding.html");
507 {
508 let mut file = File::create(&file_path)
509 .expect("Failed to create test file");
510 file.write_all(&[0xFF, b'T', b'e', b's', b't', 0xFE])
511 .expect("Failed to write test content");
512 }
513 let result = minify_html(&file_path);
514 assert!(matches!(
515 result,
516 Err(HtmlError::MinificationError(_))
517 ));
518 drop(dir);
519 }
520
521 #[cfg(feature = "async")]
523 #[tokio::test]
524 async fn test_async_generate_html_extremely_large() {
525 let large_markdown = "# Large Content
526"
527 .to_string()
528 + &"Content
529"
530 .repeat(100_000);
531 let result = async_generate_html(&large_markdown).await;
532 assert!(result.is_ok());
533 let html = result.unwrap();
534 assert!(html.contains("<h1>Large Content</h1>"));
535 }
536
537 #[cfg(feature = "async")]
538 #[tokio::test]
539 async fn test_async_generate_html_spawn_blocking_failure() {
540 use tokio::task;
541
542 let _markdown = "# Valid Markdown"; let result = task::spawn_blocking(|| {
547 panic!("Simulated task failure"); })
549 .await;
550
551 let converted_result: std::result::Result<
553 String,
554 HtmlError,
555 > = match result {
556 Err(e) => Err(HtmlError::MarkdownConversion {
557 message: format!(
558 "Asynchronous HTML generation failed: {e}"
559 ),
560 source: Some(std::io::Error::other(e.to_string())),
561 }),
562 Ok(_) => panic!("Expected a simulated failure"),
563 };
564
565 assert!(matches!(
567 converted_result,
568 Err(HtmlError::MarkdownConversion { .. })
569 ));
570
571 if let Err(HtmlError::MarkdownConversion {
572 message,
573 source,
574 }) = converted_result
575 {
576 assert!(message
577 .contains("Asynchronous HTML generation failed"));
578 assert!(source.is_some());
579
580 let source_message = source.unwrap().to_string();
582 assert!(
583 source_message.contains("Simulated task failure"),
584 "Unexpected source message: {source_message}"
585 );
586 }
587 }
588
589 #[test]
590 fn test_minify_html_empty_content() {
591 let html = "";
592 let (dir, file_path) = create_test_file(html);
593 let result = minify_html(&file_path);
594 assert!(result.is_ok());
595 assert!(
596 result.unwrap().is_empty(),
597 "Minified content should be empty"
598 );
599 drop(dir);
600 }
601
602 #[test]
603 fn test_minify_html_unusual_whitespace() {
604 let html =
605 "<html>\n\n\t<body>\t<p>Test</p>\n\n</body>\n\n</html>";
606 let (dir, file_path) = create_test_file(html);
607 let result = minify_html(&file_path);
608 assert!(result.is_ok());
609 assert_eq!(
610 result.unwrap(),
611 "<html><body><p>Test</p></body></html>",
612 "Unexpected minified result for unusual whitespace"
613 );
614 drop(dir);
615 }
616
617 #[test]
618 fn test_minify_html_with_special_characters() {
619 let html = "<div><Special> & Characters</div>";
620 let (dir, file_path) = create_test_file(html);
621 let result = minify_html(&file_path);
622 assert!(result.is_ok());
623 assert_eq!(
624 result.unwrap(),
625 "<div><Special> & Characters</div>",
626 "Special characters were unexpectedly modified during minification"
627 );
628 drop(dir);
629 }
630
631 #[cfg(feature = "async")]
632 #[tokio::test]
633 async fn test_async_generate_html_with_special_characters() {
634 let markdown =
635 "# Special & Characters\n\nContent with < > & \" '";
636 let result = async_generate_html(markdown).await;
637 assert!(result.is_ok());
638 let html = result.unwrap();
639 assert!(
640 html.contains("<"),
641 "Less than sign not escaped"
642 );
643 assert!(
644 html.contains(">"),
645 "Greater than sign not escaped"
646 );
647 assert!(html.contains("&"), "Ampersand not escaped");
648 assert!(
649 html.contains("""),
650 "Double quote not escaped"
651 );
652 assert!(
653 html.contains("'") || html.contains("'"),
654 "Single quote not handled as expected"
655 );
656 }
657 }
658}