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 #[test]
251 fn test_minify_basic_html() {
252 let html =
253 "<html> <body> <p>Test</p> </body> </html>";
254 let (dir, file_path) = create_test_file(html);
255 let result = minify_html(&file_path);
256 assert!(result.is_ok());
257 assert_eq!(
258 result.unwrap(),
259 "<html><body><p>Test</p></body></html>"
260 );
261 drop(dir);
262 }
263 #[test]
264 fn test_minify_with_comments() {
265 let html =
266 "<html><!-- Comment --><body><p>Test</p></body></html>";
267 let (dir, file_path) = create_test_file(html);
268 let result = minify_html(&file_path);
269 assert!(result.is_ok());
270 assert_eq!(
271 result.unwrap(),
272 "<html><body><p>Test</p></body></html>"
273 );
274 drop(dir);
275 }
276 #[test]
277 fn test_minify_invalid_path() {
278 let result = minify_html(Path::new("nonexistent.html"));
279 assert!(result.is_err());
280 assert!(matches!(
281 result,
282 Err(HtmlError::MinificationError(_))
283 ));
284 }
285 #[test]
286 fn test_minify_exceeds_max_size() {
287 let large_content = "a".repeat(MAX_FILE_SIZE + 1);
288 let (dir, file_path) = create_test_file(&large_content);
289 let result = minify_html(&file_path);
290 assert!(matches!(
291 result,
292 Err(HtmlError::MinificationError(_))
293 ));
294 let err_msg = result.unwrap_err().to_string();
295 assert!(err_msg.contains("exceeds maximum"));
296 drop(dir);
297 }
298 #[test]
299 fn test_minify_invalid_utf8() {
300 let dir =
301 tempdir().expect("Failed to create temp directory");
302 let file_path = dir.path().join("invalid.html");
303 {
304 let mut file = File::create(&file_path)
305 .expect("Failed to create test file");
306 file.write_all(&[0xFF, 0xFF])
307 .expect("Failed to write test content");
308 }
309
310 let result = minify_html(&file_path);
311 assert!(matches!(
312 result,
313 Err(HtmlError::MinificationError(_))
314 ));
315 let err_msg = result.unwrap_err().to_string();
316 assert!(err_msg.contains("Invalid UTF-8 in input file"));
317 drop(dir);
318 }
319 #[test]
320 fn test_minify_non_utf8_failure_path_via_directory_path() {
321 let dir =
328 tempdir().expect("Failed to create temp directory");
329 let result = minify_html(dir.path());
330 assert!(matches!(
331 result,
332 Err(HtmlError::MinificationError(_))
333 ));
334 let err_msg = result.unwrap_err().to_string();
335 assert!(
336 err_msg.contains("Failed to read file"),
337 "expected 'Failed to read file' branch, got: {err_msg}"
338 );
339 drop(dir);
340 }
341 #[test]
342 fn test_minify_utf8_content() {
343 let html = "<html><body><p>Test 你好 🦀</p></body></html>";
344 let (dir, file_path) = create_test_file(html);
345 let result = minify_html(&file_path);
346 assert!(result.is_ok());
347 assert_eq!(
348 result.unwrap(),
349 "<html><body><p>Test 你好 🦀</p></body></html>"
350 );
351 drop(dir);
352 }
353 }
354
355 #[cfg(feature = "async")]
356 mod async_generate_html_tests {
357 use super::*;
358 #[tokio::test]
359 async fn test_async_generate_html() {
360 let markdown = "# Test\n\nThis is a test.";
361 let result = async_generate_html(markdown).await;
362 assert!(result.is_ok());
363 let html = result.unwrap();
364 assert!(html.contains("<h1>Test</h1>"));
365 assert!(html.contains("<p>This is a test.</p>"));
366 }
367 #[tokio::test]
368 async fn test_async_generate_html_empty() {
369 let result = async_generate_html("").await;
370 assert!(result.is_ok());
371 assert!(result.unwrap().is_empty());
372 }
373 #[tokio::test]
374 async fn test_async_generate_html_large_content() {
375 let large_markdown =
376 "# Test\n\n".to_string() + &"Content\n".repeat(10_000);
377 let result = async_generate_html(&large_markdown).await;
378 assert!(result.is_ok());
379 let html = result.unwrap();
380 assert!(html.contains("<h1>Test</h1>"));
381 }
382 }
383
384 mod additional_tests {
385 use super::*;
386 use std::fs::File;
387 use std::io::Write;
388 use tempfile::tempdir;
389
390 #[test]
393 fn test_minify_html_rejects_non_utf8_path_content() {
394 let dir = tempdir().expect("failed to create temp dir");
395 let file_path = dir.path().join("non-utf8.html");
396 let mut f = File::create(&file_path).expect("create file");
397 f.write_all(&[0xFF, 0xFE, 0xFD, 0xFC])
398 .expect("write bytes");
399 drop(f);
400 let err = minify_html(&file_path).unwrap_err();
401 assert!(matches!(err, HtmlError::MinificationError(_)));
402 }
403
404 #[test]
406 fn test_minify_html_uncommon_structures() {
407 let html = r#"<div><span>Test<div><p>Nested</p></div></span></div>"#;
408 let (dir, file_path) = create_test_file(html);
409 let result = minify_html(&file_path);
410 assert!(result.is_ok());
411 assert_eq!(
412 result.unwrap(),
413 r#"<div><span>Test<div><p>Nested</p></div></span></div>"#
414 );
415 drop(dir);
416 }
417
418 #[test]
420 fn test_minify_html_mixed_encodings() {
421 let dir =
422 tempdir().expect("Failed to create temp directory");
423 let file_path = dir.path().join("mixed_encoding.html");
424 {
425 let mut file = File::create(&file_path)
426 .expect("Failed to create test file");
427 file.write_all(&[0xFF, b'T', b'e', b's', b't', 0xFE])
428 .expect("Failed to write test content");
429 }
430 let result = minify_html(&file_path);
431 assert!(matches!(
432 result,
433 Err(HtmlError::MinificationError(_))
434 ));
435 drop(dir);
436 }
437
438 #[cfg(feature = "async")]
440 #[tokio::test]
441 async fn test_async_generate_html_extremely_large() {
442 let large_markdown = "# Large Content
443"
444 .to_string()
445 + &"Content
446"
447 .repeat(100_000);
448 let result = async_generate_html(&large_markdown).await;
449 assert!(result.is_ok());
450 let html = result.unwrap();
451 assert!(html.contains("<h1>Large Content</h1>"));
452 }
453
454 #[cfg(feature = "async")]
455 #[tokio::test]
456 async fn test_async_generate_html_spawn_blocking_failure() {
457 use tokio::task;
458
459 let _markdown = "# Valid Markdown"; let result = task::spawn_blocking(|| {
464 panic!("Simulated task failure"); })
466 .await;
467
468 let converted_result: std::result::Result<
470 String,
471 HtmlError,
472 > = match result {
473 Err(e) => Err(HtmlError::MarkdownConversion {
474 message: format!(
475 "Asynchronous HTML generation failed: {e}"
476 ),
477 source: Some(std::io::Error::other(e.to_string())),
478 }),
479 Ok(_) => panic!("Expected a simulated failure"),
480 };
481
482 assert!(matches!(
484 converted_result,
485 Err(HtmlError::MarkdownConversion { .. })
486 ));
487
488 if let Err(HtmlError::MarkdownConversion {
489 message,
490 source,
491 }) = converted_result
492 {
493 assert!(message
494 .contains("Asynchronous HTML generation failed"));
495 assert!(source.is_some());
496
497 let source_message = source.unwrap().to_string();
499 assert!(
500 source_message.contains("Simulated task failure"),
501 "Unexpected source message: {source_message}"
502 );
503 }
504 }
505 #[test]
506 fn test_minify_html_empty_content() {
507 let html = "";
508 let (dir, file_path) = create_test_file(html);
509 let result = minify_html(&file_path);
510 assert!(result.is_ok());
511 assert!(
512 result.unwrap().is_empty(),
513 "Minified content should be empty"
514 );
515 drop(dir);
516 }
517 #[test]
518 fn test_minify_html_unusual_whitespace() {
519 let html =
520 "<html>\n\n\t<body>\t<p>Test</p>\n\n</body>\n\n</html>";
521 let (dir, file_path) = create_test_file(html);
522 let result = minify_html(&file_path);
523 assert!(result.is_ok());
524 assert_eq!(
525 result.unwrap(),
526 "<html><body><p>Test</p></body></html>",
527 "Unexpected minified result for unusual whitespace"
528 );
529 drop(dir);
530 }
531 #[test]
532 fn test_minify_html_with_special_characters() {
533 let html = "<div><Special> & Characters</div>";
534 let (dir, file_path) = create_test_file(html);
535 let result = minify_html(&file_path);
536 assert!(result.is_ok());
537 assert_eq!(
538 result.unwrap(),
539 "<div><Special> & Characters</div>",
546 "Character entities must survive minification unchanged"
547 );
548 drop(dir);
549 }
550
551 #[cfg(feature = "async")]
552 #[tokio::test]
553 async fn test_async_generate_html_with_special_characters() {
554 let markdown =
555 "# Special & Characters\n\nContent with < > & \" '";
556 let result = async_generate_html(markdown).await;
557 assert!(result.is_ok());
558 let html = result.unwrap();
559 assert!(
560 html.contains("<"),
561 "Less than sign not escaped"
562 );
563 assert!(
564 html.contains(">"),
565 "Greater than sign not escaped"
566 );
567 assert!(html.contains("&"), "Ampersand not escaped");
568 assert!(
571 html.contains(""") || html.contains('"'),
572 "Double quote not handled as expected"
573 );
574 assert!(
575 html.contains("'") || html.contains('\''),
576 "Single quote not handled as expected"
577 );
578 }
579 }
580}