1use std::io;
10use thiserror::Error;
11
12#[derive(Error, Debug)]
14pub enum HtmlError {
15 #[error("Failed to compile regex: {0}")]
19 RegexCompilationError(#[from] regex::Error),
20
21 #[error("Failed to extract front matter: {0}")]
26 FrontMatterExtractionError(String),
27
28 #[error("Failed to format header: {0}")]
32 HeaderFormattingError(String),
33
34 #[error("Failed to parse selector '{0}': {1}")]
39 SelectorParseError(String, String),
40
41 #[error("Failed to minify HTML: {0}")]
45 MinificationError(String),
46
47 #[error("Failed to convert Markdown to HTML: {message}")]
51 MarkdownConversion {
52 message: String,
54 #[source]
56 source: Option<io::Error>,
57 },
58
59 #[error("HTML minification failed: {message}")]
61 Minification {
62 message: String,
64 size: Option<usize>,
66 #[source]
68 source: Option<io::Error>,
69 },
70
71 #[error("SEO optimization failed: {kind}: {message}")]
73 Seo {
74 kind: SeoErrorKind,
76 message: String,
78 element: Option<String>,
80 },
81
82 #[error("Accessibility check failed: {kind}: {message}")]
84 Accessibility {
85 kind: ErrorKind,
87 message: String,
89 wcag_guideline: Option<String>,
91 },
92
93 #[error("Missing required HTML element: {0}")]
97 MissingHtmlElement(String),
98
99 #[error("Invalid structured data: {0}")]
103 InvalidStructuredData(String),
104
105 #[error("IO error: {0}")]
109 Io(#[from] io::Error),
110
111 #[error("Invalid input: {0}")]
115 InvalidInput(String),
116
117 #[error("Invalid front matter format: {0}")]
121 InvalidFrontMatterFormat(String),
122
123 #[error("Input too large: size {0} bytes")]
127 InputTooLarge(usize),
128
129 #[error("Invalid header format: {0}")]
133 InvalidHeaderFormat(String),
134
135 #[error("UTF-8 conversion error: {0}")]
139 Utf8ConversionError(#[from] std::string::FromUtf8Error),
140
141 #[error("Parsing error: {0}")]
145 ParsingError(String),
146
147 #[error("Template rendering failed: {message}")]
149 TemplateRendering {
150 message: String,
152 #[source]
154 source: Box<dyn std::error::Error + Send + Sync>,
155 },
156
157 #[error("Validation error: {0}")]
161 ValidationError(String),
162
163 #[error("Unexpected error: {0}")]
167 UnexpectedError(String),
168}
169
170#[derive(Debug, Copy, Clone, PartialEq, Eq)]
172pub enum SeoErrorKind {
173 MissingMetaTags,
175 InvalidInput,
177 InvalidStructuredData,
179 MissingTitle,
181 MissingDescription,
183 Other,
185}
186
187#[derive(Debug, Copy, Clone, PartialEq, Eq)]
189pub enum ErrorKind {
190 MissingAriaAttributes,
192 InvalidAriaValue,
194 MissingAltText,
196 HeadingStructure,
198 MissingFormLabels,
200 Other,
202}
203
204impl std::fmt::Display for ErrorKind {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 match self {
207 ErrorKind::MissingAriaAttributes => {
208 write!(f, "Missing ARIA attributes")
209 }
210 ErrorKind::InvalidAriaValue => {
211 write!(f, "Invalid ARIA attribute values")
212 }
213 ErrorKind::MissingAltText => {
214 write!(f, "Missing alternative text")
215 }
216 ErrorKind::HeadingStructure => {
217 write!(f, "Incorrect heading structure")
218 }
219 ErrorKind::MissingFormLabels => {
220 write!(f, "Missing form labels")
221 }
222 ErrorKind::Other => {
223 write!(f, "Other accessibility-related errors")
224 }
225 }
226 }
227}
228
229impl std::fmt::Display for SeoErrorKind {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 match self {
232 SeoErrorKind::MissingMetaTags => {
233 write!(f, "Missing required meta tags")
234 }
235 SeoErrorKind::InvalidStructuredData => {
236 write!(f, "Invalid structured data")
237 }
238 SeoErrorKind::MissingTitle => write!(f, "Missing title"),
239 SeoErrorKind::InvalidInput => write!(f, "Invalid input"),
240 SeoErrorKind::MissingDescription => {
241 write!(f, "Missing description")
242 }
243 SeoErrorKind::Other => {
244 write!(f, "Other SEO-related errors")
245 }
246 }
247 }
248}
249
250impl HtmlError {
251 pub fn invalid_input(
253 message: impl Into<String>,
254 _input: Option<String>,
255 ) -> Self {
256 Self::InvalidInput(message.into())
257 }
258
259 pub fn input_too_large(size: usize) -> Self {
261 Self::InputTooLarge(size)
262 }
263
264 pub fn seo(
266 kind: SeoErrorKind,
267 message: impl Into<String>,
268 element: Option<String>,
269 ) -> Self {
270 Self::Seo {
271 kind,
272 message: message.into(),
273 element,
274 }
275 }
276
277 pub fn accessibility(
279 kind: ErrorKind,
280 message: impl Into<String>,
281 wcag_guideline: Option<String>,
282 ) -> Self {
283 Self::Accessibility {
284 kind,
285 message: message.into(),
286 wcag_guideline,
287 }
288 }
289
290 pub fn markdown_conversion(
292 message: impl Into<String>,
293 source: Option<io::Error>,
294 ) -> Self {
295 Self::MarkdownConversion {
296 message: message.into(),
297 source,
298 }
299 }
300}
301
302pub type Result<T> = std::result::Result<T, HtmlError>;
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 mod basic_errors {
314 use super::*;
315
316 #[test]
317 fn test_regex_compilation_error() {
318 let regex_error =
319 regex::Error::Syntax("invalid regex".to_string());
320 let error: HtmlError = regex_error.into();
321 assert!(matches!(
322 error,
323 HtmlError::RegexCompilationError(_)
324 ));
325 assert!(error
326 .to_string()
327 .contains("Failed to compile regex"));
328 }
329
330 #[test]
331 fn test_front_matter_extraction_error() {
332 let error = HtmlError::FrontMatterExtractionError(
333 "Missing delimiter".to_string(),
334 );
335 assert_eq!(
336 error.to_string(),
337 "Failed to extract front matter: Missing delimiter"
338 );
339 }
340
341 #[test]
342 fn test_header_formatting_error() {
343 let error = HtmlError::HeaderFormattingError(
344 "Invalid header level".to_string(),
345 );
346 assert_eq!(
347 error.to_string(),
348 "Failed to format header: Invalid header level"
349 );
350 }
351
352 #[test]
353 fn test_selector_parse_error() {
354 let error = HtmlError::SelectorParseError(
355 "div>".to_string(),
356 "Unexpected end".to_string(),
357 );
358 assert_eq!(
359 error.to_string(),
360 "Failed to parse selector 'div>': Unexpected end"
361 );
362 }
363
364 #[test]
365 fn test_minification_error() {
366 let error = HtmlError::MinificationError(
367 "Syntax error".to_string(),
368 );
369 assert_eq!(
370 error.to_string(),
371 "Failed to minify HTML: Syntax error"
372 );
373 }
374 }
375
376 mod structured_errors {
378 use super::*;
379
380 #[test]
381 fn test_markdown_conversion_with_source() {
382 let source =
383 io::Error::new(io::ErrorKind::Other, "source error");
384 let error = HtmlError::markdown_conversion(
385 "Conversion failed",
386 Some(source),
387 );
388 assert!(error
389 .to_string()
390 .contains("Failed to convert Markdown to HTML"));
391 }
392
393 #[test]
394 fn test_markdown_conversion_without_source() {
395 let error = HtmlError::markdown_conversion(
396 "Conversion failed",
397 None,
398 );
399 assert!(error.to_string().contains("Conversion failed"));
400 }
401
402 #[test]
403 fn test_minification_with_size_and_source() {
404 let error = HtmlError::Minification {
405 message: "Too large".to_string(),
406 size: Some(1024),
407 source: Some(io::Error::new(
408 io::ErrorKind::Other,
409 "IO error",
410 )),
411 };
412 assert!(error
413 .to_string()
414 .contains("HTML minification failed"));
415 }
416 }
417
418 mod seo_errors {
420 use super::*;
421
422 #[test]
423 fn test_seo_error_missing_meta_tags() {
424 let error = HtmlError::seo(
425 SeoErrorKind::MissingMetaTags,
426 "Required meta tags missing",
427 Some("head".to_string()),
428 );
429 assert!(error
430 .to_string()
431 .contains("Missing required meta tags"));
432 }
433
434 #[test]
435 fn test_seo_error_without_element() {
436 let error = HtmlError::seo(
437 SeoErrorKind::MissingTitle,
438 "Title not found",
439 None,
440 );
441 assert!(error.to_string().contains("Missing title"));
442 }
443
444 #[test]
445 fn test_all_seo_error_kinds() {
446 let kinds = [
447 SeoErrorKind::MissingMetaTags,
448 SeoErrorKind::InvalidStructuredData,
449 SeoErrorKind::MissingTitle,
450 SeoErrorKind::MissingDescription,
451 SeoErrorKind::Other,
452 ];
453 for kind in kinds {
454 assert!(!kind.to_string().is_empty());
455 }
456 }
457 }
458
459 mod accessibility_errors {
461 use super::*;
462
463 #[test]
464 fn test_accessibility_error_with_guideline() {
465 let error = HtmlError::accessibility(
466 ErrorKind::MissingAltText,
467 "Images must have alt text",
468 Some("WCAG 1.1.1".to_string()),
469 );
470 assert!(error
471 .to_string()
472 .contains("Missing alternative text"));
473 }
474
475 #[test]
476 fn test_accessibility_error_without_guideline() {
477 let error = HtmlError::accessibility(
478 ErrorKind::InvalidAriaValue,
479 "Invalid ARIA value",
480 None,
481 );
482 assert!(error
483 .to_string()
484 .contains("Invalid ARIA attribute values"));
485 }
486
487 #[test]
488 fn test_all_accessibility_error_kinds() {
489 let kinds = [
490 ErrorKind::MissingAriaAttributes,
491 ErrorKind::InvalidAriaValue,
492 ErrorKind::MissingAltText,
493 ErrorKind::HeadingStructure,
494 ErrorKind::MissingFormLabels,
495 ErrorKind::Other,
496 ];
497 for kind in kinds {
498 assert!(!kind.to_string().is_empty());
499 }
500 }
501 }
502
503 mod io_errors {
505 use super::*;
506
507 #[test]
508 fn test_io_error_kinds() {
509 let error_kinds = [
510 io::ErrorKind::NotFound,
511 io::ErrorKind::PermissionDenied,
512 io::ErrorKind::ConnectionRefused,
513 io::ErrorKind::ConnectionReset,
514 io::ErrorKind::ConnectionAborted,
515 io::ErrorKind::NotConnected,
516 io::ErrorKind::AddrInUse,
517 io::ErrorKind::AddrNotAvailable,
518 io::ErrorKind::BrokenPipe,
519 io::ErrorKind::AlreadyExists,
520 io::ErrorKind::WouldBlock,
521 io::ErrorKind::InvalidInput,
522 io::ErrorKind::InvalidData,
523 io::ErrorKind::TimedOut,
524 io::ErrorKind::WriteZero,
525 io::ErrorKind::Interrupted,
526 io::ErrorKind::Unsupported,
527 io::ErrorKind::UnexpectedEof,
528 io::ErrorKind::OutOfMemory,
529 io::ErrorKind::Other,
530 ];
531
532 for kind in error_kinds {
533 let io_error = io::Error::new(kind, "test error");
534 let html_error: HtmlError = io_error.into();
535 assert!(matches!(html_error, HtmlError::Io(_)));
536 }
537 }
538 }
539
540 mod helper_methods {
542 use super::*;
543
544 #[test]
545 fn test_invalid_input_with_content() {
546 let error = HtmlError::invalid_input(
547 "Bad input",
548 Some("problematic content".to_string()),
549 );
550 assert!(error.to_string().contains("Invalid input"));
551 }
552
553 #[test]
554 fn test_input_too_large() {
555 let error = HtmlError::input_too_large(1024);
556 assert!(error.to_string().contains("1024 bytes"));
557 }
558
559 #[test]
560 fn test_template_rendering_error() {
561 let source_error = Box::new(io::Error::new(
562 io::ErrorKind::Other,
563 "render failed",
564 ));
565 let error = HtmlError::TemplateRendering {
566 message: "Template error".to_string(),
567 source: source_error,
568 };
569 assert!(error
570 .to_string()
571 .contains("Template rendering failed"));
572 }
573 }
574
575 mod misc_errors {
577 use super::*;
578
579 #[test]
580 fn test_missing_html_element() {
581 let error =
582 HtmlError::MissingHtmlElement("title".to_string());
583 assert!(error
584 .to_string()
585 .contains("Missing required HTML element"));
586 }
587
588 #[test]
589 fn test_invalid_structured_data() {
590 let error = HtmlError::InvalidStructuredData(
591 "Invalid JSON-LD".to_string(),
592 );
593 assert!(error
594 .to_string()
595 .contains("Invalid structured data"));
596 }
597
598 #[test]
599 fn test_invalid_front_matter_format() {
600 let error = HtmlError::InvalidFrontMatterFormat(
601 "Missing closing delimiter".to_string(),
602 );
603 assert!(error
604 .to_string()
605 .contains("Invalid front matter format"));
606 }
607
608 #[test]
609 fn test_parsing_error() {
610 let error =
611 HtmlError::ParsingError("Unexpected token".to_string());
612 assert!(error.to_string().contains("Parsing error"));
613 }
614
615 #[test]
616 fn test_validation_error() {
617 let error = HtmlError::ValidationError(
618 "Schema validation failed".to_string(),
619 );
620 assert!(error.to_string().contains("Validation error"));
621 }
622
623 #[test]
624 fn test_unexpected_error() {
625 let error = HtmlError::UnexpectedError(
626 "Something went wrong".to_string(),
627 );
628 assert!(error.to_string().contains("Unexpected error"));
629 }
630 }
631}