Skip to main content

a11y_dom/
tiers.rs

1//! Fähigkeits-Tiers.
2//!
3//! Die drei Oberflächen unterscheiden sich nicht darin, wie sie dieselben Daten
4//! darstellen, sondern darin, **welche Daten es überhaupt gibt**:
5//!
6//! | | statisches HTML | Chrome via CDP | In-Page WASM |
7//! |---|---|---|---|
8//! | Struktur ([`Document`]) | ✓ | ✓ | ✓ |
9//! | [`Semantics`] | berechnet | nativ | berechnet |
10//! | [`Rendering`] | — | ✓ | ✓ |
11//! | [`Interaction`] | — | ✓ | ✓ |
12//!
13//! Ein einziges flaches Trait dafür wäre voller `Option`, die je nach Host
14//! `None` liefern — Regeln würden stillschweigend nicht laufen. Stattdessen
15//! deklariert jede Regel ihren Tier, jeder Host implementiert die Tiers, die er
16//! bedienen kann, und ein nicht erfüllter Tier wird zu `UNTESTED` statt zu
17//! Schweigen.
18//!
19//! [`Document`]: crate::Document
20
21use crate::tree::Document;
22
23/// Welche Datenschicht eine Regel braucht.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub enum Tier {
26    /// Tags, Attribute, Text, Hierarchie. Immer verfügbar.
27    Structure,
28    /// Rolle und Accessible Name.
29    Semantics,
30    /// Berechnete Stile und Geometrie.
31    Rendering,
32    /// Fokus, Ereignisse, veränderlicher DOM.
33    Interaction,
34}
35
36impl Tier {
37    pub fn as_str(self) -> &'static str {
38        match self {
39            Tier::Structure => "structure",
40            Tier::Semantics => "semantics",
41            Tier::Rendering => "rendering",
42            Tier::Interaction => "interaction",
43        }
44    }
45}
46
47/// Was ein Host liefern kann. [`Tier::Structure`] ist immer dabei — ohne
48/// Baum gäbe es nichts zu prüfen.
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
50pub struct Caps {
51    pub semantics: bool,
52    pub rendering: bool,
53    pub interaction: bool,
54}
55
56impl Caps {
57    /// Nur Struktur — der statische Fall.
58    pub const STRUCTURE_ONLY: Caps = Caps {
59        semantics: false,
60        rendering: false,
61        interaction: false,
62    };
63
64    pub fn has(self, tier: Tier) -> bool {
65        match tier {
66            Tier::Structure => true,
67            Tier::Semantics => self.semantics,
68            Tier::Rendering => self.rendering,
69            Tier::Interaction => self.interaction,
70        }
71    }
72
73    pub fn with_semantics(mut self) -> Self {
74        self.semantics = true;
75        self
76    }
77
78    pub fn with_rendering(mut self) -> Self {
79        self.rendering = true;
80        self
81    }
82
83    pub fn with_interaction(mut self) -> Self {
84        self.interaction = true;
85        self
86    }
87}
88
89/// Woher ein Accessible Name stammt. Nur der native Accessibility-Tree kennt
90/// das; berechnende Hosts lassen es bei `None`.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum NameSource {
93    AriaLabel,
94    AriaLabelledBy,
95    Label,
96    Title,
97    Alt,
98    Placeholder,
99    Contents,
100    Value,
101}
102
103/// **Tier 2** — Rolle und Accessible Name.
104///
105/// auditmysite liefert beides nativ aus dem Accessibility-Tree des Browsers,
106/// inklusive [`NameSource`] und `is_ignored`. astro-post-audit und LiveAudit
107/// berechnen es über das `accname`-Crate; dort bleibt `name_source` `None`.
108/// Die Lebenszeit des Knotens ist an die Ausleihe des Dokuments gekoppelt
109/// (`&'n self`, `Self::N<'n>`). Ohne diese Kopplung könnte ein Host, der seinen
110/// Baum nur *ausleiht* und daneben abgeleitete Daten hält — etwa einen
111/// vorberechneten ID-Index —, das Trait gar nicht erfüllen: `Self: 'n` wäre
112/// nicht herleitbar.
113pub trait Semantics: Document {
114    fn role<'n>(&'n self, node: Self::N<'n>) -> Option<String>;
115
116    fn accessible_name<'n>(&'n self, node: Self::N<'n>) -> Option<String>;
117
118    fn name_source<'n>(&'n self, _node: Self::N<'n>) -> Option<NameSource> {
119        None
120    }
121
122    /// Ob der Accessibility-Tree diesen Knoten auslässt — etwa wegen
123    /// `aria-hidden`, `display: none` oder weil er rein präsentational ist.
124    fn is_ignored<'n>(&'n self, _node: Self::N<'n>) -> bool {
125        false
126    }
127}
128
129/// Ein Rechteck in CSS-Pixeln, Ursprung links oben im Dokument.
130#[derive(Debug, Clone, Copy, PartialEq)]
131pub struct Rect {
132    pub x: f32,
133    pub y: f32,
134    pub width: f32,
135    pub height: f32,
136}
137
138impl Rect {
139    pub fn area(&self) -> f32 {
140        self.width * self.height
141    }
142
143    pub fn is_empty(&self) -> bool {
144        self.width <= 0.0 || self.height <= 0.0
145    }
146}
147
148/// sRGB mit Alpha, Kanäle 0–255.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct Color {
151    pub r: u8,
152    pub g: u8,
153    pub b: u8,
154    pub a: u8,
155}
156
157/// Die berechneten Stilwerte, die Accessibility-Regeln tatsächlich brauchen.
158/// Bewusst keine vollständige CSSOM-Abbildung.
159#[derive(Debug, Clone, PartialEq)]
160pub struct ComputedStyle {
161    pub color: Option<Color>,
162    /// Die *effektive* Hintergrundfarbe — der Host löst Transparenz über die
163    /// Vorfahren auf. Eine reine `background-color`-Ablesung pro Knoten wäre
164    /// für Kontrastprüfungen unbrauchbar.
165    pub background_color: Option<Color>,
166    pub font_size_px: Option<f32>,
167    pub font_weight: Option<u16>,
168    pub display: Option<String>,
169    pub visibility: Option<String>,
170}
171
172/// Layout-Angaben für heuristische Prüfungen — was eine Regel braucht, um eine
173/// Barriere zu *vermuten*, nicht um sie zu belegen. Die Regeln darauf melden
174/// deshalb `REVIEW`, nie `FAIL`.
175///
176/// Alles hier kostet den Host zusätzliche Arbeit; er liefert es über
177/// [`Rendering::layout`], und wer es nicht liefert, bekommt diese Heuristiken
178/// nicht.
179#[derive(Debug, Clone, Default, PartialEq)]
180pub struct Layout {
181    /// Ein Flex-Container mit `flex-direction: row-reverse` oder
182    /// `column-reverse`: Die Leserichtung weicht von der Quellreihenfolge ab.
183    pub flex_reversed: bool,
184    /// Berechnetes `order`. Ungleich 0 verschiebt das Element gegenüber der
185    /// Quellreihenfolge.
186    pub order: i32,
187    /// Berechnetes `min-width` in CSS-Pixeln, `0` ohne Angabe.
188    pub min_width_px: f32,
189    /// Berechnetes `cursor: pointer` — das Element sieht anklickbar aus.
190    pub cursor_pointer: bool,
191    /// Auf dem Element läuft eine Animation mit unendlich vielen Wiederholungen.
192    pub infinite_animation: bool,
193    /// Das Element liegt im sichtbaren Bereich, aber seine Mitte wird von
194    /// einem fixierten oder klebenden fremden Element überdeckt.
195    pub obscured: bool,
196    /// Ein fixiertes oder klebendes Element am oberen Rand, das tiefer reicht
197    /// als `scroll-padding-top`: Was beim Rückwärts-Tabben oben ausgerichtet
198    /// wird, kann ganz darunter verschwinden.
199    pub hides_focus: bool,
200    /// Ob der Fokus sichtbar wird: `Some(true)`, wenn sich beim Fokussieren
201    /// ein Stil ändert, der als Indikator taugt; `None`, wenn nicht gemessen —
202    /// Fokussieren ändert den Zustand der Seite und ist deshalb ein eigener,
203    /// ausdrücklicher Durchgang.
204    pub focus_visible: Option<bool>,
205}
206
207/// **Tier 3** — berechnete Stile und Geometrie.
208///
209/// Statische HTML-Analyse kann das nicht; Kontrast-, Target-Size- und
210/// Reflow-Regeln melden dort `UNTESTED`.
211pub trait Rendering: Document {
212    fn computed_style<'n>(&'n self, node: Self::N<'n>) -> Option<ComputedStyle>;
213
214    fn bounds<'n>(&'n self, node: Self::N<'n>) -> Option<Rect>;
215
216    /// Ob der Knoten tatsächlich sichtbar gerendert wird — nicht dasselbe wie
217    /// „steht im Markup".
218    fn is_rendered<'n>(&'n self, node: Self::N<'n>) -> bool {
219        self.bounds(node).is_some_and(|b| !b.is_empty())
220    }
221
222    /// Layout-Angaben für die heuristischen Regeln. Vorgabe `None`: Ein Host,
223    /// der sie nicht erhebt, bekommt diese Regeln nicht.
224    fn layout<'n>(&'n self, _node: Self::N<'n>) -> Option<Layout> {
225        None
226    }
227}
228
229/// **Tier 4** — Fokus, Ereignisse, veränderlicher DOM.
230///
231/// In-Page ist das billig und genau, weil der Prüfer *in* der Seite sitzt und
232/// Ereignisse real auslösen kann. Über CDP geht es auch, kostet aber einen
233/// Roundtrip je Schritt.
234pub trait Interaction: Document {
235    /// Die tatsächliche Tabreihenfolge, in der Reihenfolge des Durchlaufs.
236    fn tab_order(&self) -> Vec<crate::NodeId>;
237
238    /// Ob der Knoten bei Fokus einen sichtbaren Indikator zeigt.
239    fn has_visible_focus<'n>(&'n self, _node: Self::N<'n>) -> Option<bool> {
240        None
241    }
242}