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 fs::write(&file_path, [0xFF, 0xFE, 0xFD, 0xFC])
397 .expect("write bytes");
398 let err = minify_html(&file_path).unwrap_err();
399 assert!(matches!(err, HtmlError::MinificationError(_)));
400 }
401
402 #[test]
404 fn test_minify_html_uncommon_structures() {
405 let html = r#"<div><span>Test<div><p>Nested</p></div></span></div>"#;
406 let (dir, file_path) = create_test_file(html);
407 let result = minify_html(&file_path);
408 assert!(result.is_ok());
409 assert_eq!(
410 result.unwrap(),
411 r#"<div><span>Test<div><p>Nested</p></div></span></div>"#
412 );
413 drop(dir);
414 }
415
416 #[test]
418 fn test_minify_html_mixed_encodings() {
419 let dir =
420 tempdir().expect("Failed to create temp directory");
421 let file_path = dir.path().join("mixed_encoding.html");
422 {
423 let mut file = File::create(&file_path)
424 .expect("Failed to create test file");
425 file.write_all(&[0xFF, b'T', b'e', b's', b't', 0xFE])
426 .expect("Failed to write test content");
427 }
428 let result = minify_html(&file_path);
429 assert!(matches!(
430 result,
431 Err(HtmlError::MinificationError(_))
432 ));
433 drop(dir);
434 }
435
436 #[cfg(feature = "async")]
438 #[tokio::test]
439 async fn test_async_generate_html_extremely_large() {
440 let large_markdown = "# Large Content
441"
442 .to_string()
443 + &"Content
444"
445 .repeat(100_000);
446 let result = async_generate_html(&large_markdown).await;
447 assert!(result.is_ok());
448 let html = result.unwrap();
449 assert!(html.contains("<h1>Large Content</h1>"));
450 }
451
452 #[cfg(feature = "async")]
453 #[tokio::test]
454 async fn test_async_generate_html_spawn_blocking_failure() {
455 use tokio::task;
456
457 let _markdown = "# Valid Markdown"; let result = task::spawn_blocking(|| {
462 panic!("Simulated task failure"); })
464 .await;
465
466 let converted_result: std::result::Result<
468 String,
469 HtmlError,
470 > = match result {
471 Err(e) => Err(HtmlError::MarkdownConversion {
472 message: format!(
473 "Asynchronous HTML generation failed: {e}"
474 ),
475 source: Some(std::io::Error::other(e.to_string())),
476 }),
477 Ok(_) => panic!("Expected a simulated failure"),
478 };
479
480 assert!(matches!(
482 converted_result,
483 Err(HtmlError::MarkdownConversion { .. })
484 ));
485
486 if let Err(HtmlError::MarkdownConversion {
487 message,
488 source,
489 }) = converted_result
490 {
491 assert!(message
492 .contains("Asynchronous HTML generation failed"));
493 assert!(source.is_some());
494
495 let source_message = source.unwrap().to_string();
497 assert!(
498 source_message.contains("Simulated task failure"),
499 "Unexpected source message: {source_message}"
500 );
501 }
502 }
503 #[test]
504 fn test_minify_html_empty_content() {
505 let html = "";
506 let (dir, file_path) = create_test_file(html);
507 let result = minify_html(&file_path);
508 assert!(result.is_ok());
509 assert!(
510 result.unwrap().is_empty(),
511 "Minified content should be empty"
512 );
513 drop(dir);
514 }
515 #[test]
516 fn test_minify_html_unusual_whitespace() {
517 let html =
518 "<html>\n\n\t<body>\t<p>Test</p>\n\n</body>\n\n</html>";
519 let (dir, file_path) = create_test_file(html);
520 let result = minify_html(&file_path);
521 assert!(result.is_ok());
522 assert_eq!(
523 result.unwrap(),
524 "<html><body><p>Test</p></body></html>",
525 "Unexpected minified result for unusual whitespace"
526 );
527 drop(dir);
528 }
529 #[test]
530 fn test_minify_html_with_special_characters() {
531 let html = "<div><Special> & Characters</div>";
532 let (dir, file_path) = create_test_file(html);
533 let result = minify_html(&file_path);
534 assert!(result.is_ok());
535 assert_eq!(
536 result.unwrap(),
537 "<div><Special> & Characters</div>",
544 "Character entities must survive minification unchanged"
545 );
546 drop(dir);
547 }
548
549 #[cfg(feature = "async")]
550 #[tokio::test]
551 async fn test_async_generate_html_with_special_characters() {
552 let markdown =
553 "# Special & Characters\n\nContent with < > & \" '";
554 let result = async_generate_html(markdown).await;
555 assert!(result.is_ok());
556 let html = result.unwrap();
557 assert!(
558 html.contains("<"),
559 "Less than sign not escaped"
560 );
561 assert!(
562 html.contains(">"),
563 "Greater than sign not escaped"
564 );
565 assert!(html.contains("&"), "Ampersand not escaped");
566 assert!(
569 html.contains(""") || html.contains('"'),
570 "Double quote not handled as expected"
571 );
572 assert!(
573 html.contains("'") || html.contains('\''),
574 "Single quote not handled as expected"
575 );
576 }
577 }
578}