use miette::{Diagnostic, Error}; use thiserror::Error; use crate::{ parse::{Attribute, Document, Element, Node}, tokenize::{AttributeToken, Token, TokenType}, }; 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_alphabetic_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; const VOID_TAGS: &[&str] = &[ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", ]; const ALLOWED_TOP_LEVEL_TAGS: &[&str] = &["import", "const", "block", "head", "body"]; pub struct DiagnosticsContext { source_code: &'static str, } 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, ) { if let Some(diagnostic) = diagnostic { eprintln!( "{}", Error::new(diagnostic).with_source_code(ctx.source_code) ); } } #[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<'_>], ) { for token in tokens { maybe_print_diagnostic(ctx, D::handle_token(token)); let TokenType::Tag { attribute_tokens, .. } = token.ty else { continue; }; for token in attribute_tokens { maybe_print_diagnostic(ctx, D::handle_attribute_token(token)); } } } fn run_ast_diagnostic( ctx: &DiagnosticsContext, document: &Document<'_>, ) { maybe_print_diagnostic(ctx, D::handle_document(document)); for node in document.children { maybe_print_diagnostic(ctx, D::handle_node(node)); let Node::Element(element) = node else { continue; }; maybe_print_diagnostic(ctx, D::handle_element(element)); for attribute in element.attributes { maybe_print_diagnostic(ctx, D::handle_attribute(attribute)); } } } pub fn run_token_diagnostics(ctx: &DiagnosticsContext, tokens: &[Token<'_>]) { run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); run_token_diagnostic::(ctx, tokens); // TODO invalid character reference } pub fn run_ast_diagnostics(ctx: &DiagnosticsContext, document: &Document<'_>) { run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); run_ast_diagnostic::(ctx, document); // TODO invalid // TODO invalid // TODO invalid // TODO invalid // TODO deprecated tag // TODO deprecated tag attribute // // 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 anchor with invalid href // TODO dt or dd outside dl // TODO li outside ul, ol, menu // TODO invalid dl, ul, menu // TODO invalid attribute values (see microsyntaxes) // TODO aria tags don't match tag semantics // TODO empty or missing image src // TODO missing alt text for image (unless inside figure with caption, or has title, or meta name = "generator") }