accessibility_rs/
lib.rs

1#![warn(missing_docs)]
2
3//! Audit html to see how it complies with WCAG
4//! standards.
5//!
6//! accessibility-rs is a web accessibility
7//! engine that can replicate websites without
8//! a browser to get complex accessibility reports.
9//!
10//! # How to use accessibility-rs
11//!
12//! There are a couple of ways to use accessibility-rs:
13//!
14//! - **Audit** perform an audit against an html page.
15//!   - [`audit`] is used to audit a web page for issues.
16//!
17//! [`audit`]: fn.audit.html#method.audit
18//!
19//! # Examples
20//!
21//! A basic WCAG audit for a website:
22//!
23//! ```no_run
24//! use accessibility_rs::{audit, AuditConfig};
25//!
26//! #[cfg(not(feature = "tokio"))]
27//! fn main() {
28//!     let config = AuditConfig::basic(r###"<html><body><h1>My Title</h1><input type="text" placeholder="Type me"></input><img src="tabby_cat.png"></img></body></html>"###);
29//!     let audit = audit(config);
30//!     println!("{:?}", audit);
31//! }
32//!
33//! #[cfg(all(feature = "tokio", not(feature = "spider")))]
34//! #[tokio::main]
35//! async fn main() {
36//!     let config = AuditConfig::basic(r###"<html><body><h1>My Title</h1><input type="text" placeholder="Type me"></input><img src="tabby_cat.png"></img></body></html>"###);
37//!     let audit = audit(config).await;
38//!     println!("{:?}", audit);
39//! }
40//!
41//! #[cfg(feature = "spider")]
42//! #[tokio::main]
43//! async fn main() {
44//!     let mut config = AuditConfig::default();
45//!     config.url = "https://example.com".into();
46//!     let audit = audit(config).await;
47//!     println!("{:?}", audit);
48//! }
49//! ```
50//!
51
52#[macro_use]
53extern crate lazy_static;
54#[macro_use]
55extern crate rust_i18n;
56
57#[cfg(feature = "spider")]
58pub use spider;
59
60/// the main engine for accessibility auditing.
61pub mod engine;
62/// locales for translations.
63pub mod i18n;
64
65pub use accessibility_scraper;
66pub use accessibility_scraper::fast_html5ever;
67pub use accessibility_scraper::Html;
68pub use accessibility_scraper::ElementRef;
69
70pub use crate::engine::audit::auditor::Auditor;
71pub use crate::engine::issue::Issue;
72
73i18n!("locales", fallback = "en");
74
75/// support guidelines for auditing
76#[derive(Default)]
77pub enum Conformance {
78    /// Level AAA includes all Level A, AA, and AAA requirements
79    #[default]
80    WCAGAAA,
81}
82
83/// configs for the audit
84#[derive(Default)]
85#[cfg(feature = "tokio")]
86pub struct AuditConfig {
87    /// the html source code
88    pub html: String,
89    /// the css rules to apply
90    pub css: String,
91    /// extract bounding box of elements
92    pub bounding_box: bool,
93    /// the locale of the audit translations
94    pub locale: String,
95    /// the guideline spec
96    pub conformance: Conformance,
97    /// crawl and perform audits on the entire website
98    #[cfg(feature = "spider")]
99    pub url: String,
100}
101
102/// configs for the audit
103#[derive(Default)]
104#[cfg(not(feature = "tokio"))]
105pub struct AuditConfig<'a> {
106    /// the html source code
107    pub html: &'a str,
108    /// the css rules to apply
109    pub css: &'a str,
110    /// extract bounding box of elements
111    pub bounding_box: bool,
112    /// the locale of the audit translations
113    pub locale: &'a str,
114    /// the guideline spec
115    pub conformance: Conformance,
116    /// crawl and perform audits on the entire website
117    #[cfg(feature = "spider")]
118    pub url: &'a str,
119}
120
121#[cfg(not(feature = "tokio"))]
122impl<'a> AuditConfig<'a> {
123    /// a new audit configuration
124    pub fn new(html: &'a str, css: &'a str, bounding_box: bool, locale: &'a str) -> Self {
125        AuditConfig {
126            html: html.into(),
127            css: css.into(),
128            bounding_box,
129            locale: locale.into(),
130            ..Default::default()
131        }
132    }
133
134    /// basic audit
135    pub fn basic(html: &'a str) -> Self {
136        AuditConfig {
137            html: html.into(),
138            ..Default::default()
139        }
140    }
141
142    /// a new audit configuration crawling a website. This does nothing without the 'spider' flag enabled.
143    #[cfg(feature = "spider")]
144    pub fn new_website(url: &'a str, css: &'a str, bounding_box: bool, locale: &'a str) -> Self {
145        AuditConfig {
146            url: url.into(),
147            css: css.into(),
148            bounding_box,
149            locale: locale.into(),
150            ..Default::default()
151        }
152    }
153
154    /// a new audit configuration crawling a website. This does nothing without the 'spider' flag enabled.
155    #[cfg(not(feature = "spider"))]
156    pub fn new_website(
157        _url: &'a str,
158        _css: &'a str,
159        _bounding_box: bool,
160        _locale: &'a str,
161    ) -> Self {
162        AuditConfig::default()
163    }
164}
165
166#[cfg(feature = "tokio")]
167impl AuditConfig {
168    /// a new audit configuration
169    pub fn new(html: &str, css: &str, bounding_box: bool, locale: &str) -> Self {
170        AuditConfig {
171            html: html.into(),
172            css: css.into(),
173            bounding_box,
174            locale: locale.into(),
175            ..Default::default()
176        }
177    }
178
179    /// basic audit
180    pub fn basic(html: &str) -> Self {
181        AuditConfig {
182            html: html.into(),
183            ..Default::default()
184        }
185    }
186
187    /// a new audit configuration crawling a website. This does nothing without the 'spider' flag enabled.
188    #[cfg(feature = "spider")]
189    pub fn new_website(url: &str, css: &str, bounding_box: bool, locale: &str) -> Self {
190        AuditConfig {
191            url: url.into(),
192            css: css.into(),
193            bounding_box,
194            locale: locale.into(),
195            ..Default::default()
196        }
197    }
198    /// a new audit configuration crawling a website. This does nothing without the 'spider' flag enabled.
199    #[cfg(not(feature = "spider"))]
200    pub fn new_website(_url: &str, _css: &str, _bounding_box: bool, _locale: &str) -> Self {
201        AuditConfig::default()
202    }
203}
204
205/// audit a web page passing the html and css rules.
206#[cfg(all(feature = "tokio", not(feature = "spider")))]
207pub async fn audit(config: AuditConfig) -> Vec<Issue> {
208    let document = accessibility_scraper::Html::parse_document(&config.html).await;
209    let auditor = Auditor::new(&document, &config.css, config.bounding_box, &config.locale);
210    engine::audit::wcag::WCAGAAA::audit(auditor).await
211}
212
213#[cfg(feature = "spider")]
214#[derive(Debug, Clone)]
215/// The accessibility audit results either a single page or entire website.
216pub enum AuditResults {
217    /// Crawl results from multiple websites
218    Page(spider::hashbrown::HashMap<String, Vec<Issue>>),
219    /// Raw html markup results
220    Html(Vec<Issue>),
221}
222
223/// audit a web page passing the html and css rules.
224#[cfg(all(feature = "spider"))]
225pub async fn audit(config: &AuditConfig) -> AuditResults {
226    if !config.url.is_empty() {
227        use spider::website::Website;
228        let mut website: Website = Website::new(&config.url);
229        let mut rx2: tokio::sync::broadcast::Receiver<spider::page::Page> =
230            website.subscribe(16).unwrap();
231        let bounding_box = config.bounding_box;
232        let locale = config.locale.clone();
233
234        let audits = tokio::spawn(async move {
235            let mut issues: spider::hashbrown::HashMap<String, Vec<Issue>> =
236                spider::hashbrown::HashMap::new();
237
238            while let Ok(res) = rx2.recv().await {
239                let document = accessibility_scraper::Html::parse_document(&res.get_html()).await;
240                let auditor = Auditor::new(&document, &"", bounding_box, &locale);
241                let issue = engine::audit::wcag::WCAGAAA::audit(auditor).await;
242                issues.insert(res.get_url().into(), issue);
243            }
244
245            issues
246        });
247
248        website.crawl().await;
249        website.unsubscribe();
250        AuditResults::Page(audits.await.unwrap_or_default())
251    } else {
252        let document = accessibility_scraper::Html::parse_document(&config.html).await;
253        let auditor = Auditor::new(&document, &config.css, config.bounding_box, &config.locale);
254        AuditResults::Html(engine::audit::wcag::WCAGAAA::audit(auditor).await)
255    }
256}
257
258/// audit a web page passing the html and css rules.
259#[cfg(not(feature = "tokio"))]
260pub fn audit(config: &AuditConfig) -> Vec<Issue> {
261    let document = accessibility_scraper::Html::parse_document(&config.html);
262    let auditor = Auditor::new(&document, &config.css, config.bounding_box, &config.locale);
263    engine::audit::wcag::WCAGAAA::audit(auditor)
264}