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