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#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ParsedAriaRole {
16 pub name: String,
17 pub allowed_properties: Vec<String>,
18}
19
20#[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, AriaPropertyValueKind::OptionalBool => AttributeType::Bool,
63 AriaPropertyValueKind::IdRef => AttributeType::String, AriaPropertyValueKind::IdRefList => AttributeType::String, AriaPropertyValueKind::Integer => AttributeType::Integer,
66 AriaPropertyValueKind::Number => AttributeType::Float,
67 AriaPropertyValueKind::String => AttributeType::String,
68 AriaPropertyValueKind::Token(_) => AttributeType::String, AriaPropertyValueKind::TokenList(_) => AttributeType::String, }
71 }
72}
73
74#[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"), ("aria-brailleroledescription", "ariaBrailleRoleDescription"), ("aria-dropeffect", "ariaDropEffect"), ("aria-relevant", "ariaRelevant"), ("aria-grabbed", "ariaGrabbed"), ];
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 if tag_name == "body" {
239 prohibited_aria_attributes.insert("aria-hidden".to_owned());
241 } else if tag_name == "dd" {
242 allowed_roles.insert("definition".to_owned());
244 } else if tag_name == "footer" || tag_name == "header" {
245 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 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}