mod spatial;
mod terminal;
mod wcag;
pub use spatial::{GridAlignment, MinSpacing, Reflow320, ResizeText200, TextSpacing};
pub use terminal::{
BreakpointOutcome, BreakpointReport, BreakpointResult, BreakpointTier, MinReadableSize,
TerminalAccessible, TerminalBreakpoint, TerminalBreakpointSet, TerminalNoOverflow,
};
pub use wcag::{
HasLabelConstraint, KeyboardAccessibleConstraint, MinTouchTargetConstraint,
NoOverflowConstraint, ValidRoleConstraint,
};
use crate::{ElementId, Viewport};
use accesskit::NodeId;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub enum SpecReference {
Wcag {
criterion: &'static str,
level: WcagLevel,
url: &'static str,
},
Css {
module: &'static str,
section: &'static str,
url: &'static str,
},
DesignSystem {
name: &'static str,
section: &'static str,
url: &'static str,
},
Iso {
standard: &'static str,
section: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WcagLevel {
A,
AA,
AAA,
}
impl std::fmt::Display for WcagLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::A => write!(f, "A"),
Self::AA => write!(f, "AA"),
Self::AAA => write!(f, "AAA"),
}
}
}
#[derive(Debug, Clone)]
pub enum Violation {
MissingLabel {
element: ElementId,
},
EmptyLabel {
element: ElementId,
},
TouchTarget {
element: ElementId,
actual_width: u32,
actual_height: u32,
min_dimension: u32,
},
Overflow {
element: ElementId,
element_x: i32,
element_y: i32,
element_width: u32,
element_height: u32,
viewport_width: u32,
viewport_height: u32,
},
Contrast {
element: ElementId,
actual_ratio: f64,
min_ratio: f64,
},
ContrastInsufficient {
actual: f32,
required: f32,
foreground: String,
background: String,
},
TextSpacing {
element1: ElementId,
element2: ElementId,
},
Reflow {
element: ElementId,
actual_width: f64,
max_width: f64,
},
GridAlignment {
element: ElementId,
position: (f64, f64),
grid_step: f64,
},
TerminalOverflow {
element: ElementId,
element_col: i32,
element_row: i32,
element_cols: u32,
element_rows: u32,
viewport_cols: u32,
viewport_rows: u32,
},
BelowMinReadableSize {
element: ElementId,
actual_cols: u32,
actual_rows: u32,
min_cols: u32,
min_rows: u32,
},
}
#[derive(Debug, Clone)]
pub struct ConstraintContext<'a> {
pub nodes: &'a BTreeMap<NodeId, accesskit::Node>,
pub viewport: Viewport,
}
pub trait Constraint: Send + Sync {
fn check(&self, node_id: NodeId, ctx: &ConstraintContext<'_>) -> Result<(), Violation>;
fn spec_ref(&self) -> SpecReference;
}
pub struct ConstraintSet {
hard: Vec<Box<dyn Constraint>>,
structural: Vec<Box<dyn Constraint>>,
advisory: Vec<Box<dyn Constraint>>,
}
impl ConstraintSet {
pub fn builder() -> ConstraintSetBuilder {
ConstraintSetBuilder::default()
}
#[tracing::instrument(skip(self, ctx))]
pub fn verify(&self, root: NodeId, ctx: &ConstraintContext<'_>) -> ConstraintVerification {
let mut hard_violations = Vec::new();
let mut structural_violations = Vec::new();
let mut warnings = Vec::new();
self.verify_recursive(
root,
ctx,
&mut hard_violations,
&mut structural_violations,
&mut warnings,
);
ConstraintVerification {
hard_violations,
structural_violations,
warnings,
}
}
fn verify_recursive(
&self,
node_id: NodeId,
ctx: &ConstraintContext<'_>,
hard_violations: &mut Vec<Violation>,
structural_violations: &mut Vec<Violation>,
warnings: &mut Vec<Violation>,
) {
for constraint in &self.hard {
if let Err(v) = constraint.check(node_id, ctx) {
hard_violations.push(v);
}
}
for constraint in &self.structural {
if let Err(v) = constraint.check(node_id, ctx) {
structural_violations.push(v);
}
}
for constraint in &self.advisory {
if let Err(v) = constraint.check(node_id, ctx) {
warnings.push(v);
}
}
if let Some(node) = ctx.nodes.get(&node_id) {
for child_id in node.children() {
self.verify_recursive(
*child_id,
ctx,
hard_violations,
structural_violations,
warnings,
);
}
}
}
pub fn hard_constraints(&self) -> &[Box<dyn Constraint>] {
&self.hard
}
pub fn structural_constraints(&self) -> &[Box<dyn Constraint>] {
&self.structural
}
pub fn advisory_constraints(&self) -> &[Box<dyn Constraint>] {
&self.advisory
}
}
#[derive(Debug, Clone, Default)]
pub struct ConstraintVerification {
pub hard_violations: Vec<Violation>,
pub structural_violations: Vec<Violation>,
pub warnings: Vec<Violation>,
}
impl ConstraintVerification {
pub fn is_valid(&self) -> bool {
self.hard_violations.is_empty() && self.structural_violations.is_empty()
}
pub fn violation_count(&self) -> usize {
self.hard_violations.len() + self.structural_violations.len()
}
pub fn warning_count(&self) -> usize {
self.warnings.len()
}
}
#[derive(Default)]
pub struct ConstraintSetBuilder {
hard: Vec<Box<dyn Constraint>>,
structural: Vec<Box<dyn Constraint>>,
advisory: Vec<Box<dyn Constraint>>,
}
impl ConstraintSetBuilder {
pub fn hard(mut self, constraint: impl Constraint + 'static) -> Self {
self.hard.push(Box::new(constraint));
self
}
pub fn structural(mut self, constraint: impl Constraint + 'static) -> Self {
self.structural.push(Box::new(constraint));
self
}
pub fn advisory(mut self, constraint: impl Constraint + 'static) -> Self {
self.advisory.push(Box::new(constraint));
self
}
pub fn build(self) -> ConstraintSet {
ConstraintSet {
hard: self.hard,
structural: self.structural,
advisory: self.advisory,
}
}
}