mod assertions;
mod csp_enforcement;
mod datatypes;
mod finding;
mod infoset;
mod parse;
mod schema;
mod scripts;
mod table_integrity;
use assertions::SchematronEngine;
pub use finding::{CheckError, CheckReport, Finding, Severity, SourceLocation};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CheckOptions {
pub include_parse_errors: bool,
}
impl Default for CheckOptions {
fn default() -> Self {
Self {
include_parse_errors: true,
}
}
}
pub fn check(html: &str) -> Result<CheckReport, CheckError> {
check_with_options(html, CheckOptions::default())
}
pub fn check_with_options(html: &str, options: CheckOptions) -> Result<CheckReport, CheckError> {
let parsed = parse::parse(html);
let document = infoset::normalize(parsed.document(), parsed.source());
let mut findings = if options.include_parse_errors {
parse::findings(&parsed)
} else {
Vec::new()
};
let schema_errors =
schema::validate_document(&document).map_err(|message| CheckError::Initialization {
message: format!("schema validation setup failed: {message}"),
})?;
findings.extend(schema::findings(&schema_errors));
let assertion_failures = assertions::RuleSetEngine
.check(&document)
.map_err(|error| CheckError::Initialization {
message: format!("assertion engine setup failed: {error}"),
})?;
findings.extend(assertions::findings(&assertion_failures));
findings.extend(scripts::findings(parsed.document()));
findings.extend(csp_enforcement::findings(parsed.document()));
findings.extend(table_integrity::findings(parsed.document()));
Ok(CheckReport { findings })
}
#[cfg(test)]
mod tests {
use super::{CheckOptions, SourceLocation, check, check_with_options};
#[test]
fn valid_html_has_no_parser_findings() {
let report = check(r#"<!doctype html><html lang="en"><title>Example</title><p>Hello</p>"#)
.expect("HTML5 parsing should recover");
assert!(report.findings.is_empty());
assert!(!report.has_errors());
}
#[test]
fn parser_diagnostics_can_be_excluded() {
let report = check_with_options(
r#"<!doctype html><html lang="en"><title>Example</title><p>¬AnEntity;</p>"#,
CheckOptions {
include_parse_errors: false,
},
)
.expect("HTML5 parsing should recover");
assert!(report.findings.is_empty());
}
#[test]
fn parser_diagnostics_are_included_by_default() {
let report =
check(r#"<!doctype html><html lang="en"><title>Example</title><p>¬AnEntity;</p>"#)
.expect("HTML5 parsing should recover");
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].rule_id, "parser.html5");
}
#[test]
fn schema_violation_is_reported_as_a_finding() {
let report = check_with_options(
r#"<!doctype html><html lang="en"><p>Hello</p>"#,
CheckOptions {
include_parse_errors: false,
},
)
.expect("HTML5 parsing should recover");
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].rule_id, "schema.html5");
assert_eq!(report.findings[0].location, None);
assert!(report.has_errors());
}
#[test]
fn schema_violation_location_is_populated_for_an_explicit_element() {
let report = check_with_options(
r#"<!doctype html><html lang="en"><title>x</title><p bogus="1">hi</p>"#,
CheckOptions {
include_parse_errors: false,
},
)
.expect("HTML5 parsing should recover");
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].rule_id, "schema.html5");
assert_eq!(
report.findings[0].location,
Some(SourceLocation {
line: 1,
column: 48,
byte_offset: 47,
})
);
}
#[test]
fn assertion_violation_is_reported_as_a_finding() {
let report = check_with_options(
r#"<!doctype html><html lang="en"><title>Example</title><div aria-hidden="true" tabindex="0">x</div>"#,
CheckOptions {
include_parse_errors: false,
},
)
.expect("HTML5 parsing should recover");
assert_eq!(report.findings.len(), 1);
assert_eq!(
report.findings[0].rule_id,
"assertion.aria.hidden-not-focusable"
);
}
}