html_generator/
performance.rs1use crate::minifier;
35use crate::{HtmlError, Result};
36use std::{fs, path::Path};
37
38#[cfg(feature = "async")]
39use tokio::task;
40
41pub const MAX_FILE_SIZE: usize = 10 * 1024 * 1024;
54
55pub fn minify_html(file_path: &Path) -> Result<String> {
89 let metadata = fs::metadata(file_path).map_err(|e| {
90 HtmlError::MinificationError(format!(
91 "Failed to read file metadata for '{}': {e}",
92 file_path.display()
93 ))
94 })?;
95
96 let file_size = metadata.len() as usize;
97 if file_size > MAX_FILE_SIZE {
98 return Err(HtmlError::MinificationError(format!(
99 "File size {file_size} bytes exceeds maximum of {MAX_FILE_SIZE} bytes"
100 )));
101 }
102
103 let content = fs::read_to_string(file_path).map_err(|e| {
104 let kind = if e
109 .to_string()
110 .contains("stream did not contain valid UTF-8")
111 {
112 "Invalid UTF-8 in input file"
113 } else {
114 "Failed to read file"
115 };
116 HtmlError::MinificationError(format!(
117 "{kind} '{}': {e}",
118 file_path.display()
119 ))
120 })?;
121
122 let minified = minifier::minify(&content)?;
123
124 Ok(minified)
129}
130
131pub fn minify_html_string(html: &str) -> Result<String> {
162 if html.len() > MAX_FILE_SIZE {
163 return Err(HtmlError::MinificationError(format!(
164 "Input size {} bytes exceeds maximum of {MAX_FILE_SIZE} bytes",
165 html.len()
166 )));
167 }
168
169 let minified = minifier::minify(html)?;
170
171 Ok(minified)
173}
174
175#[cfg(feature = "async")]
208pub async fn async_generate_html(markdown: &str) -> Result<String> {
209 let markdown = markdown.to_string();
210 task::spawn_blocking(move || {
211 crate::generator::markdown_to_html_with_extensions(&markdown)
212 })
213 .await
214 .map_err(|e| HtmlError::MarkdownConversion {
215 message: format!("Asynchronous HTML generation failed: {e}"),
216 source: Some(std::io::Error::other(e.to_string())),
217 })?
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use std::fs::File;
224 use std::io::Write;
225 use tempfile::tempdir;
226
227 fn create_test_file(
237 content: &str,
238 ) -> (tempfile::TempDir, std::path::PathBuf) {
239 let dir = tempdir().expect("Failed to create temp directory");
240 let file_path = dir.path().join("test.html");
241 let mut file = File::create(&file_path)
242 .expect("Failed to create test file");
243 file.write_all(content.as_bytes())
244 .expect("Failed to write test content");
245 (dir, file_path)
246 }
247
248 mod minify_html_tests {
249 use super::*;
250
251 #[test]
252 fn test_minify_basic_html() {
253 let html =
254 "<html> <body> <p>Test</p> </body> </html>";
255 let (dir, file_path) = create_test_file(html);
256 let result = minify_html(&file_path);
257 assert!(result.is_ok());
258 assert_eq!(
259 result.unwrap(),
260 "<html><body><p>Test</p></body></html>"
261 );
262 drop(dir);
263 }
264
265 #[test]
266 fn test_minify_with_comments() {
267 let html =
268 "<html><!-- Comment --><body><p>Test</p></body></html>";
269 let (dir, file_path) = create_test_file(html);
270 let result = minify_html(&file_path);
271 assert!(result.is_ok());
272 assert_eq!(
273 result.unwrap(),
274 "<html><body><p>Test</p></body></html>"
275 );
276 drop(dir);
277 }
278
279 #[test]
280 fn test_minify_invalid_path() {
281 let result = minify_html(Path::new("nonexistent.html"));
282 assert!(result.is_err());
283 assert!(matches!(
284 result,
285 Err(HtmlError::MinificationError(_))
286 ));
287 }
288
289 #[test]
290 fn test_minify_exceeds_max_size() {
291 let large_content = "a".repeat(MAX_FILE_SIZE + 1);
292 let (dir, file_path) = create_test_file(&large_content);
293 let result = minify_html(&file_path);
294 assert!(matches!(
295 result,
296 Err(HtmlError::MinificationError(_))
297 ));
298 let err_msg = result.unwrap_err().to_string();
299 assert!(err_msg.contains("exceeds maximum"));
300 drop(dir);
301 }
302
303 #[test]
304 fn test_minify_invalid_utf8() {
305 let dir =
306 tempdir().expect("Failed to create temp directory");
307 let file_path = dir.path().join("invalid.html");
308 {
309 let mut file = File::create(&file_path)
310 .expect("Failed to create test file");
311 file.write_all(&[0xFF, 0xFF])
312 .expect("Failed to write test content");
313 }
314
315 let result = minify_html(&file_path);
316 assert!(matches!(
317 result,
318 Err(HtmlError::MinificationError(_))
319 ));
320 let err_msg = result.unwrap_err().to_string();
321 assert!(err_msg.contains("Invalid UTF-8 in input file"));
322 drop(dir);
323 }
324
325 #[test]
326 fn test_minify_non_utf8_failure_path_via_directory_path() {
327 let dir =
334 tempdir().expect("Failed to create temp directory");
335 let result = minify_html(dir.path());
336 assert!(matches!(
337 result,
338 Err(HtmlError::MinificationError(_))
339 ));
340 let err_msg = result.unwrap_err().to_string();
341 assert!(
342 err_msg.contains("Failed to read file"),
343 "expected 'Failed to read file' branch, got: {err_msg}"
344 );
345 drop(dir);
346 }
347
348 #[test]
349 fn test_minify_utf8_content() {
350 let html = "<html><body><p>Test 你好 🦀</p></body></html>";
351 let (dir, file_path) = create_test_file(html);
352 let result = minify_html(&file_path);
353 assert!(result.is_ok());
354 assert_eq!(
355 result.unwrap(),
356 "<html><body><p>Test 你好 🦀</p></body></html>"
357 );
358 drop(dir);
359 }
360 }
361
362 #[cfg(feature = "async")]
363 mod async_generate_html_tests {
364 use super::*;
365
366 #[tokio::test]
367 async fn test_async_generate_html() {
368 let markdown = "# Test\n\nThis is a test.";
369 let result = async_generate_html(markdown).await;
370 assert!(result.is_ok());
371 let html = result.unwrap();
372 assert!(html.contains("<h1>Test</h1>"));
373 assert!(html.contains("<p>This is a test.</p>"));
374 }
375
376 #[tokio::test]
377 async fn test_async_generate_html_empty() {
378 let result = async_generate_html("").await;
379 assert!(result.is_ok());
380 assert!(result.unwrap().is_empty());
381 }
382
383 #[tokio::test]
384 async fn test_async_generate_html_large_content() {
385 let large_markdown =
386 "# Test\n\n".to_string() + &"Content\n".repeat(10_000);
387 let result = async_generate_html(&large_markdown).await;
388 assert!(result.is_ok());
389 let html = result.unwrap();
390 assert!(html.contains("<h1>Test</h1>"));
391 }
392 }
393
394 mod additional_tests {
395 use super::*;
396 use std::fs::File;
397 use std::io::Write;
398 use tempfile::tempdir;
399
400 #[test]
403 fn test_minify_html_rejects_non_utf8_path_content() {
404 let dir = tempdir().expect("failed to create temp dir");
405 let file_path = dir.path().join("non-utf8.html");
406 let mut f = File::create(&file_path).expect("create file");
407 f.write_all(&[0xFF, 0xFE, 0xFD, 0xFC])
408 .expect("write bytes");
409 drop(f);
410 let err = minify_html(&file_path).unwrap_err();
411 assert!(matches!(err, HtmlError::MinificationError(_)));
412 }
413
414 #[test]
416 fn test_minify_html_uncommon_structures() {
417 let html = r#"<div><span>Test<div><p>Nested</p></div></span></div>"#;
418 let (dir, file_path) = create_test_file(html);
419 let result = minify_html(&file_path);
420 assert!(result.is_ok());
421 assert_eq!(
422 result.unwrap(),
423 r#"<div><span>Test<div><p>Nested</p></div></span></div>"#
424 );
425 drop(dir);
426 }
427
428 #[test]
430 fn test_minify_html_mixed_encodings() {
431 let dir =
432 tempdir().expect("Failed to create temp directory");
433 let file_path = dir.path().join("mixed_encoding.html");
434 {
435 let mut file = File::create(&file_path)
436 .expect("Failed to create test file");
437 file.write_all(&[0xFF, b'T', b'e', b's', b't', 0xFE])
438 .expect("Failed to write test content");
439 }
440 let result = minify_html(&file_path);
441 assert!(matches!(
442 result,
443 Err(HtmlError::MinificationError(_))
444 ));
445 drop(dir);
446 }
447
448 #[cfg(feature = "async")]
450 #[tokio::test]
451 async fn test_async_generate_html_extremely_large() {
452 let large_markdown = "# Large Content
453"
454 .to_string()
455 + &"Content
456"
457 .repeat(100_000);
458 let result = async_generate_html(&large_markdown).await;
459 assert!(result.is_ok());
460 let html = result.unwrap();
461 assert!(html.contains("<h1>Large Content</h1>"));
462 }
463
464 #[cfg(feature = "async")]
465 #[tokio::test]
466 async fn test_async_generate_html_spawn_blocking_failure() {
467 use tokio::task;
468
469 let _markdown = "# Valid Markdown"; let result = task::spawn_blocking(|| {
474 panic!("Simulated task failure"); })
476 .await;
477
478 let converted_result: std::result::Result<
480 String,
481 HtmlError,
482 > = match result {
483 Err(e) => Err(HtmlError::MarkdownConversion {
484 message: format!(
485 "Asynchronous HTML generation failed: {e}"
486 ),
487 source: Some(std::io::Error::other(e.to_string())),
488 }),
489 Ok(_) => panic!("Expected a simulated failure"),
490 };
491
492 assert!(matches!(
494 converted_result,
495 Err(HtmlError::MarkdownConversion { .. })
496 ));
497
498 if let Err(HtmlError::MarkdownConversion {
499 message,
500 source,
501 }) = converted_result
502 {
503 assert!(message
504 .contains("Asynchronous HTML generation failed"));
505 assert!(source.is_some());
506
507 let source_message = source.unwrap().to_string();
509 assert!(
510 source_message.contains("Simulated task failure"),
511 "Unexpected source message: {source_message}"
512 );
513 }
514 }
515
516 #[test]
517 fn test_minify_html_empty_content() {
518 let html = "";
519 let (dir, file_path) = create_test_file(html);
520 let result = minify_html(&file_path);
521 assert!(result.is_ok());
522 assert!(
523 result.unwrap().is_empty(),
524 "Minified content should be empty"
525 );
526 drop(dir);
527 }
528
529 #[test]
530 fn test_minify_html_unusual_whitespace() {
531 let html =
532 "<html>\n\n\t<body>\t<p>Test</p>\n\n</body>\n\n</html>";
533 let (dir, file_path) = create_test_file(html);
534 let result = minify_html(&file_path);
535 assert!(result.is_ok());
536 assert_eq!(
537 result.unwrap(),
538 "<html><body><p>Test</p></body></html>",
539 "Unexpected minified result for unusual whitespace"
540 );
541 drop(dir);
542 }
543
544 #[test]
545 fn test_minify_html_with_special_characters() {
546 let html = "<div><Special> & Characters</div>";
547 let (dir, file_path) = create_test_file(html);
548 let result = minify_html(&file_path);
549 assert!(result.is_ok());
550 assert_eq!(
551 result.unwrap(),
552 "<div><Special> & Characters</div>",
559 "Character entities must survive minification unchanged"
560 );
561 drop(dir);
562 }
563
564 #[cfg(feature = "async")]
565 #[tokio::test]
566 async fn test_async_generate_html_with_special_characters() {
567 let markdown =
568 "# Special & Characters\n\nContent with < > & \" '";
569 let result = async_generate_html(markdown).await;
570 assert!(result.is_ok());
571 let html = result.unwrap();
572 assert!(
573 html.contains("<"),
574 "Less than sign not escaped"
575 );
576 assert!(
577 html.contains(">"),
578 "Greater than sign not escaped"
579 );
580 assert!(html.contains("&"), "Ampersand not escaped");
581 assert!(
584 html.contains(""") || html.contains('"'),
585 "Double quote not handled as expected"
586 );
587 assert!(
588 html.contains("'") || html.contains('\''),
589 "Single quote not handled as expected"
590 );
591 }
592 }
593}