use miette::{Diagnostic, Error}; use thiserror::Error; use crate::{ parse::{Attribute, Document, Element, Node}, tokenize::{AttributeToken, Token, TokenType}, }; mod deprecated_attributes; mod deprecated_tag; mod duplicate_attribute_names; mod empty_tag_name; mod end_tag_with_attributes; mod forbidden_top_level_tag; mod html_tag; mod image_tag; mod non_alphabetic_attribute_name; mod non_alphanumeric_tag_name; mod non_self_closing_void_tag; mod non_void_self_closing_tag; mod self_closing_end_tag; mod stray_end_tag; mod stray_equal_sign; mod too_many_equal_signs; mod top_level_character_data; mod unclosed_start_tag; mod unterminated_comment; mod unterminated_tag; pub struct DiagnosticsContext { source_code: &'static str, } impl DiagnosticsContext { pub fn new(source_code: &'static str) -> Self { Self { source_code } } } trait TokenDiagnostic: Sized + Send + Sync + 'static { fn handle_token(_: &Token<'_>) -> Option { None } fn handle_attribute_token(_: &AttributeToken<'_>) -> Option { None } } trait AstDiagnostic: Sized + Send + Sync + 'static { fn handle_document(_: &Document<'_>) -> Option { None } fn handle_node(_: &Node<'_>) -> Option { None } fn handle_element(_: &Element<'_>) -> Option { None } fn handle_attribute(_: &Attribute<'_>) -> Option { None } } fn maybe_print_diagnostic( ctx: &DiagnosticsContext, diagnostic: Option, ) -> Option<()> { if let Some(diagnostic) = diagnostic { eprintln!( "{:?}", Error::new(diagnostic).with_source_code(ctx.source_code) ); return None; } Some(()) } #[derive(Debug, Error, Diagnostic)] #[error("This file is not valid UTF-8")] #[diagnostic(code(NonUtf8), severity(Error))] struct NonUtf8; #[derive(Debug, Error, Diagnostic)] #[error("Forbidden NUL character found")] #[diagnostic(code(NonUtf8), severity(Error))] struct NulCharacter { #[label("Forbidden NUL byte found here")] location: usize, } #[derive(Debug, Error, Diagnostic)] #[error("Forbidden control character found")] #[diagnostic(code(NonUtf8), severity(Error))] struct ControlCharacter { #[label("Control character found")] location: usize, } pub fn run_character_diagnostics(source: &'static [u8]) -> Option<&'static str> { let Ok(source) = str::from_utf8(source) else { eprintln!("{}", Error::new(NonUtf8)); return None; }; let ctx = DiagnosticsContext { source_code: source, }; for (i, char) in source.char_indices() { maybe_print_diagnostic(&ctx, (char == '\0').then_some(NulCharacter { location: i })); maybe_print_diagnostic( &ctx, (char.is_control() && !char.is_ascii_whitespace()) .then_some(ControlCharacter { location: i }), ); } Some(source) } fn run_token_diagnostic( ctx: &DiagnosticsContext, tokens: &[Token<'_>], ) -> Option<()> { let mut has_failed = false; for token in tokens { has_failed |= maybe_print_diagnostic(ctx, D::handle_token(token)).is_none(); let TokenType::Tag { attribute_tokens, .. } = token.ty else { continue; }; for token in attribute_tokens { has_failed |= maybe_print_diagnostic(ctx, D::handle_attribute_token(token)).is_none(); } } (!has_failed).then_some(()) } fn run_ast_diagnostic( ctx: &DiagnosticsContext, document: &Document<'_>, ) -> Option<()> { let mut has_failed = maybe_print_diagnostic(ctx, D::handle_document(document)).is_none(); for node in document.children { has_failed |= maybe_print_diagnostic(ctx, D::handle_node(node)).is_none(); let Node::Element(element) = node else { continue; }; has_failed |= maybe_print_diagnostic(ctx, D::handle_element(element)).is_none(); for attribute in element.attributes { has_failed |= maybe_print_diagnostic(ctx, D::handle_attribute(attribute)).is_none(); } } (!has_failed).then_some(()) } pub fn run_token_diagnostics(ctx: &DiagnosticsContext, tokens: &[Token<'_>]) -> Option<()> { let mut has_failed = false; has_failed |= run_token_diagnostic::(ctx, tokens) .is_none(); has_failed |= run_token_diagnostic::(ctx, tokens) .is_none(); has_failed |= run_token_diagnostic::(ctx, tokens) .is_none(); has_failed |= run_token_diagnostic::(ctx, tokens).is_none(); has_failed |= run_token_diagnostic::(ctx, tokens) .is_none(); has_failed |= run_token_diagnostic::(ctx, tokens).is_none(); has_failed |= run_token_diagnostic::(ctx, tokens).is_none(); has_failed |= run_token_diagnostic::(ctx, tokens).is_none(); has_failed |= run_token_diagnostic::( ctx, tokens, ) .is_none(); // TODO allow the AST diagnostics to still run if this fails has_failed |= run_token_diagnostic::(ctx, tokens).is_none(); // TODO invalid character reference (!has_failed).then_some(()) } pub fn run_ast_diagnostics(ctx: &DiagnosticsContext, document: &Document<'_>) -> Option<()> { let mut has_failed = false; has_failed |= run_ast_diagnostic::(ctx, document).is_none(); has_failed |= run_ast_diagnostic::(ctx, document).is_none(); has_failed |= run_ast_diagnostic::(ctx, document) .is_none(); has_failed |= run_ast_diagnostic::(ctx, document) .is_none(); has_failed |= run_ast_diagnostic::(ctx, document) .is_none(); has_failed |= run_ast_diagnostic::(ctx, document).is_none(); has_failed |= run_ast_diagnostic::(ctx, document).is_none(); has_failed |= run_ast_diagnostic::(ctx, document).is_none(); has_failed |= run_ast_diagnostic::(ctx, document).is_none(); // TODO invalid // TODO invalid // TODO invalid // TODO invalid // TODO invalid // TODO deprecated tag attribute // TODO warning-only obsolete features // TODO invalid name on anchor // TODO invalid language on script // TODO invalid type on style // TODO base without href or target // TODO multiple base elements // TODO base target with a newline, tab, or < // TODO invalid cite on
or // TODO non html1 tag or attribute // TODO empty, missing, or invalid image src // TODO invalid form enctype // TODO invalid form action // TODO invalid form method // TODO input without name // TODO invalid input attributes for type // TODO invalid select size // TODO invalid select mutliple attribute // TODO select without name // TODO invalid option selected attribute // TODO invalid cols and rows attributes for textarea // TODO textarea without name // // TODO forbidden tag in noscript // TODO head tag outside head // TODO html tag in body // TODO heading inside heading // TODO form inside form // TODO button inside button // TODO anchor inside anchor // TODO head inside body // TODO caption, col, colgroup, tbody, tfoot, thead, tr outside table // TODO table inside table // TODO non-hidden input inside table // TODO form inside table // TODO body inside body // TODO col, colgroup, tbody, tfoot, thead, tr inside caption // TODO th, td outside tr // TODO caption, col, colgroup, tr in tbody // TODO caption, col, colgroup, tbody, td, tfoot, th, thead, tr inside table cell // TODO select inside select // TODO input, keygen, textarea inside select // TODO caption, table, tbody, tfoot, thread, tr, td, th inside select // TODO more than one body // TODO invalid head child // TODO missing title // TODO title outside head // TODO warning for title longer than 64 characters // TODO warning for more than one

// TODO invalid heading hierarchy // TODO invalid

,

, 
, or content // TODO wrap on
	// TODO address inside address
	// TODO heading inside addres
	// TODO 
or