html_bindgen/parse/
aria.rs

1use std::collections::{BTreeSet, HashMap};
2
3use convert_case::{Case, Casing};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    scrape::{ScrapedAriaElement, ScrapedAriaProperty, ScrapedAriaRole},
8    Result,
9};
10
11use super::{Attribute, AttributeType};
12
13/// The parsed WebIDL definitions converted from the raw spec.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ParsedAriaRole {
16    pub name: String,
17    pub allowed_properties: Vec<String>,
18}
19
20/// The parsed WebIDL definitions converted from the raw spec.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ParsedAriaProperty {
23    pub name: String,
24    pub idl_name: String,
25    pub description: String,
26    pub is_global: bool,
27    pub value_kind: AriaPropertyValueKind,
28    pub default_value: Option<String>,
29}
30
31impl From<ParsedAriaProperty> for Attribute {
32    fn from(value: ParsedAriaProperty) -> Self {
33        let field_name = value.idl_name.to_case(Case::Snake);
34        Attribute {
35            name: value.name,
36            description: value.description,
37            field_name,
38            ty: value.value_kind.into(),
39        }
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub enum AriaPropertyValueKind {
45    Bool,
46    Tristate,
47    OptionalBool,
48    IdRef,
49    IdRefList,
50    Integer,
51    Number,
52    String,
53    Token(Vec<String>),
54    TokenList(Vec<String>),
55}
56
57impl From<AriaPropertyValueKind> for AttributeType {
58    fn from(value: AriaPropertyValueKind) -> Self {
59        match value {
60            AriaPropertyValueKind::Bool => AttributeType::Bool,
61            AriaPropertyValueKind::Tristate => AttributeType::String, // TODO: AttributeType::Enumerable
62            AriaPropertyValueKind::OptionalBool => AttributeType::Bool,
63            AriaPropertyValueKind::IdRef => AttributeType::String, // TODO: AttributeType::Identifier
64            AriaPropertyValueKind::IdRefList => AttributeType::String, // TODO: AttributeType::Vec?
65            AriaPropertyValueKind::Integer => AttributeType::Integer,
66            AriaPropertyValueKind::Number => AttributeType::Float,
67            AriaPropertyValueKind::String => AttributeType::String,
68            AriaPropertyValueKind::Token(_) => AttributeType::String, // TODO: AttributeType::Enumerable
69            AriaPropertyValueKind::TokenList(_) => AttributeType::String, // TODO: AttributeType::Vec?
70        }
71    }
72}
73
74/// The parsed WebIDL definitions converted from the raw spec.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ParsedAriaElement {
77    pub tag_name: String,
78    pub any_role: bool,
79    pub no_role: bool,
80    pub allowed_roles: BTreeSet<String>,
81    pub global_aria_attributes: bool,
82    pub no_aria_attributes: bool,
83    pub allowed_aria_attributes: BTreeSet<String>,
84    pub prohibited_aria_attributes: BTreeSet<String>,
85}
86
87pub fn parse_aria_roles(
88    iter: impl Iterator<Item = Result<ScrapedAriaRole>>,
89) -> Result<Vec<ParsedAriaRole>> {
90    let mut output = vec![];
91    for res in iter {
92        let role = res?;
93
94        if role.is_abstract {
95            continue;
96        }
97
98        let allowed_properties = role
99            .required
100            .into_iter()
101            .chain(role.inherited.into_iter())
102            .chain(role.properties.into_iter())
103            .collect();
104
105        output.push(ParsedAriaRole {
106            name: role.name,
107            allowed_properties,
108        });
109    }
110
111    Ok(output)
112}
113
114pub fn parse_aria_properties(
115    iter: impl Iterator<Item = Result<ScrapedAriaProperty>>,
116) -> Result<Vec<ParsedAriaProperty>> {
117    let mut output = vec![];
118    for res in iter {
119        let property = res?;
120
121        let default_value = property
122            .values
123            .iter()
124            .filter_map(|x| x.strip_suffix(" (default)"))
125            .next()
126            .map(String::from);
127
128        let value_kind = match property.value_kind.as_str() {
129            "true/false" => AriaPropertyValueKind::Bool,
130            "tristate" => AriaPropertyValueKind::Tristate,
131            "true/false/undefined" => AriaPropertyValueKind::OptionalBool,
132            "ID reference" => AriaPropertyValueKind::IdRef,
133            "ID reference list" => AriaPropertyValueKind::IdRefList,
134            "integer" => AriaPropertyValueKind::Integer,
135            "number" => AriaPropertyValueKind::Number,
136            "string" => AriaPropertyValueKind::String,
137            "token" | "token list" => {
138                let mut tokens = property.values;
139                for t in &mut tokens {
140                    if t.ends_with(" (default)") {
141                        t.truncate(t.len() - " (default)".len());
142                    }
143                }
144
145                if property.value_kind == "token" {
146                    AriaPropertyValueKind::Token(tokens)
147                } else {
148                    AriaPropertyValueKind::TokenList(tokens)
149                }
150            }
151            _ => panic!("Unexpected ARIA property value kind"),
152        };
153
154        let idl_name = match property.idl_name {
155            Some(name) => name,
156            None => {
157                const MISSING_IDL_NAMES: [(&str, &str); 5] = [
158                    ("aria-braillelabel", "ariaBrailleLabel"), // targeted for ARIA 1.3
159                    ("aria-brailleroledescription", "ariaBrailleRoleDescription"), // targeted for ARIA 1.3
160                    ("aria-dropeffect", "ariaDropEffect"), // deprecated in ARIA 1.1
161                    ("aria-relevant", "ariaRelevant"),     // deprecated in ARIA 1.1
162                    ("aria-grabbed", "ariaGrabbed"),       // deprecated in ARIA 1.1
163                ];
164
165                MISSING_IDL_NAMES
166                    .iter()
167                    .find(|x| x.0 == property.name)
168                    .unwrap()
169                    .1
170                    .to_owned()
171            }
172        };
173
174        output.push(ParsedAriaProperty {
175            name: property.name,
176            idl_name,
177            description: property.description.unwrap_or_default(),
178            is_global: property.is_global,
179            value_kind,
180            default_value,
181        });
182    }
183
184    Ok(output)
185}
186
187pub fn parse_aria_elements(
188    iter: impl Iterator<Item = Result<ScrapedAriaElement>>,
189) -> Result<Vec<ParsedAriaElement>> {
190    let mut output = HashMap::new();
191    for res in iter {
192        let el = res?;
193
194        for tag_name in parse_tag_names(&el.name) {
195            let mut new = false;
196            let parsed = output.entry(tag_name.to_owned()).or_insert_with(|| {
197                new = true;
198                ParsedAriaElement {
199                    tag_name: tag_name.to_owned(),
200                    any_role: false,
201                    no_role: true,
202                    allowed_roles: BTreeSet::new(),
203                    global_aria_attributes: false,
204                    no_aria_attributes: true,
205                    allowed_aria_attributes: BTreeSet::new(),
206                    prohibited_aria_attributes: BTreeSet::new(),
207                }
208            });
209
210            let any_role = el.links.iter().any(|x| x == "Any `role`")
211                || el.links.iter().any(|x| x == "any `role`");
212            let no_role = el.links.iter().any(|x| x == "No `role`")
213                || el.links.iter().any(|x| x == "no `role`");
214            let global_aria_attributes = el.global.is_some();
215            let no_aria_attributes = el
216                .strong
217                .iter()
218                .any(|x| x == "No `aria-*` attributes" || x == "No `role` or `aria-*` attributes");
219
220            let mut allowed_roles =
221                BTreeSet::from_iter(el.implicit_roles.iter().map(|x| x.replace('`', "")));
222            allowed_roles.extend(el.allowed_roles.iter().map(|x| x.replace('`', "")));
223
224            let allowed_aria_attributes =
225                BTreeSet::from_iter(el.allowed_properties.iter().map(|x| x.replace('`', "")));
226            let mut prohibited_aria_attributes =
227                if el.links.iter().any(|x| x == "Naming Prohibited") {
228                    BTreeSet::from([
229                        "aria-braillelabel".to_owned(),
230                        "aria-label".to_owned(),
231                        "aria-labelledby".to_owned(),
232                    ])
233                } else {
234                    BTreeSet::new()
235                };
236
237            // Special cases that can't be scraped
238            if tag_name == "body" {
239                // "...MUST NOT specify aria-hidden=true on the body element"
240                prohibited_aria_attributes.insert("aria-hidden".to_owned());
241            } else if tag_name == "dd" {
242                // "...and any aria-* attributes applicable to the definition role"
243                allowed_roles.insert("definition".to_owned());
244            } else if tag_name == "footer" || tag_name == "header" {
245                // "Naming Prohibited if exposed as generic."
246                // (i.e. naming is not prohibited otherwise)
247                prohibited_aria_attributes.remove("aria-braillelabel");
248                prohibited_aria_attributes.remove("aria-label");
249                prohibited_aria_attributes.remove("aria-labelledby");
250            } else if tag_name == "input" {
251                // extraneous conditional
252                allowed_roles.remove("button if used with aria-pressed");
253            }
254
255            parsed.no_role &= no_role;
256            parsed.any_role |= any_role;
257            parsed.global_aria_attributes |= global_aria_attributes;
258            parsed.no_aria_attributes &= no_aria_attributes || !allowed_aria_attributes.is_empty();
259            parsed.allowed_roles.extend(allowed_roles.into_iter());
260            parsed
261                .allowed_aria_attributes
262                .extend(allowed_aria_attributes.into_iter());
263
264            if !new {
265                prohibited_aria_attributes =
266                    &parsed.prohibited_aria_attributes & &prohibited_aria_attributes;
267            }
268            parsed.prohibited_aria_attributes = prohibited_aria_attributes;
269        }
270    }
271
272    Ok(output.into_values().collect())
273}
274
275fn parse_tag_names(name: &str) -> Vec<&str> {
276    if name.starts_with("[^") {
277        let end = name.find("^]").unwrap();
278        vec![&name[2..end]]
279    } else if name == "`h1 to h6`" {
280        vec!["h1", "h2", "h3", "h4", "h5", "h6"]
281    } else if name.starts_with("`input type=") {
282        vec!["input"]
283    } else {
284        const IGNORED_ELEMENTS: [&str; 4] = [
285            "`math`",
286            "`SVG`",
287            "form-associated custom element",
288            "autonomous custom element",
289        ];
290        assert!(IGNORED_ELEMENTS.contains(&name));
291        vec![]
292    }
293}