1mod assertions;
16mod csp_enforcement;
17mod datatypes;
18mod finding;
19mod infoset;
20mod parse;
21mod schema;
22mod scripts;
23
24use assertions::SchematronEngine;
25
26pub use finding::{CheckError, CheckReport, Finding, Severity, SourceLocation};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct CheckOptions {
31 pub include_parse_errors: bool,
33}
34
35impl Default for CheckOptions {
36 fn default() -> Self {
37 Self {
38 include_parse_errors: true,
39 }
40 }
41}
42
43pub fn check(html: &str) -> Result<CheckReport, CheckError> {
48 check_with_options(html, CheckOptions::default())
49}
50
51pub fn check_with_options(html: &str, options: CheckOptions) -> Result<CheckReport, CheckError> {
66 let parsed = parse::parse(html);
67 let document = infoset::normalize(parsed.document(), parsed.source());
68
69 let mut findings = if options.include_parse_errors {
70 parse::findings(&parsed)
71 } else {
72 Vec::new()
73 };
74
75 let schema_errors =
76 schema::validate_document(&document).map_err(|message| CheckError::Initialization {
77 message: format!("schema validation setup failed: {message}"),
78 })?;
79 findings.extend(schema::findings(&schema_errors));
80
81 let assertion_failures = assertions::RuleSetEngine
82 .check(&document)
83 .map_err(|error| CheckError::Initialization {
84 message: format!("assertion engine setup failed: {error}"),
85 })?;
86 findings.extend(assertions::findings(&assertion_failures));
87
88 findings.extend(scripts::findings(parsed.document()));
89 findings.extend(csp_enforcement::findings(parsed.document()));
90
91 Ok(CheckReport { findings })
92}
93
94#[cfg(test)]
95mod tests {
96 use super::{CheckOptions, SourceLocation, check, check_with_options};
97
98 #[test]
99 fn valid_html_has_no_parser_findings() {
100 let report = check("<!doctype html><title>Example</title><p>Hello</p>")
101 .expect("HTML5 parsing should recover");
102
103 assert!(report.findings.is_empty());
104 assert!(!report.has_errors());
105 }
106
107 #[test]
108 fn parser_diagnostics_can_be_excluded() {
109 let report = check_with_options(
116 "<!doctype html><title>Example</title><p>¬AnEntity;</p>",
117 CheckOptions {
118 include_parse_errors: false,
119 },
120 )
121 .expect("HTML5 parsing should recover");
122
123 assert!(report.findings.is_empty());
124 }
125
126 #[test]
127 fn parser_diagnostics_are_included_by_default() {
128 let report = check("<!doctype html><title>Example</title><p>¬AnEntity;</p>")
129 .expect("HTML5 parsing should recover");
130
131 assert_eq!(report.findings.len(), 1);
132 assert_eq!(report.findings[0].rule_id, "parser.html5");
133 }
134
135 #[test]
136 fn schema_violation_is_reported_as_a_finding() {
137 let report = check_with_options(
143 "<!doctype html><p>Hello</p>",
144 CheckOptions {
145 include_parse_errors: false,
146 },
147 )
148 .expect("HTML5 parsing should recover");
149
150 assert_eq!(report.findings.len(), 1);
151 assert_eq!(report.findings[0].rule_id, "schema.html5");
152 assert_eq!(report.findings[0].location, None);
153 assert!(report.has_errors());
154 }
155
156 #[test]
157 fn schema_violation_location_is_populated_for_an_explicit_element() {
158 let report = check_with_options(
163 r#"<!doctype html><title>x</title><p bogus="1">hi</p>"#,
164 CheckOptions {
165 include_parse_errors: false,
166 },
167 )
168 .expect("HTML5 parsing should recover");
169
170 assert_eq!(report.findings.len(), 1);
171 assert_eq!(report.findings[0].rule_id, "schema.html5");
172 assert_eq!(
173 report.findings[0].location,
174 Some(SourceLocation {
175 line: 1,
176 column: 32,
177 byte_offset: 31,
178 })
179 );
180 }
181
182 #[test]
183 fn assertion_violation_is_reported_as_a_finding() {
184 let report = check_with_options(
185 r#"<!doctype html><title>Example</title><div aria-hidden="true" tabindex="0">x</div>"#,
186 CheckOptions {
187 include_parse_errors: false,
188 },
189 )
190 .expect("HTML5 parsing should recover");
191
192 assert_eq!(report.findings.len(), 1);
193 assert_eq!(
194 report.findings[0].rule_id,
195 "assertion.aria.hidden-not-focusable"
196 );
197 }
198}