diff --git a/.typos.toml b/.typos.toml index 485f067..a367d4b 100644 --- a/.typos.toml +++ b/.typos.toml @@ -5,4 +5,4 @@ extend-ignore-re = [ ] [files] -extend-exclude = ["vendor"] +extend-exclude = ["vendor", "crates/ast/src/validation/table.rs"] diff --git a/crates/ast/src/basics.rs b/crates/ast/src/basics.rs index e049681..58e40a4 100644 --- a/crates/ast/src/basics.rs +++ b/crates/ast/src/basics.rs @@ -16,8 +16,12 @@ impl PartialEq<&str> for UnquotedName { } impl UnquotedName { + pub fn name(&self) -> String { + self.0.unraw().to_string() + } + pub fn lit(&self) -> LitStr { - LitStr::new(&self.0.unraw().to_string(), self.0.span()) + LitStr::new(&self.name(), self.0.span()) } pub fn span(&self) -> Span { diff --git a/crates/ast/src/generate.rs b/crates/ast/src/generate.rs index f4d7860..45177c8 100644 --- a/crates/ast/src/generate.rs +++ b/crates/ast/src/generate.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, iter}; +use std::collections::BTreeMap; use proc_macro2::{Ident, Span, TokenStream}; use quote::{ToTokens, format_ident, quote, quote_spanned}; @@ -12,6 +12,7 @@ use super::{ AttributeValueNode, DataModifierPart, DataModifiers, ElementNode, Nodes, SyntaxStatic, UnquotedName, }; +use crate::validation; fn escape_script_source_literal(value: &str) -> std::borrow::Cow<'_, str> { let bytes = value.as_bytes(); @@ -341,21 +342,22 @@ impl<'a> Generator<'a> { match part { Part::Static(lit) => { let mut dynamic_stmt = None; - let static_parts = iter::once(lit) - .chain(parts.by_ref().map_while(|part| match part { - Part::Static(lit) => Some(lit), + let mut static_str = lit.value(); + for part in parts.by_ref() { + match part { + Part::Static(lit) => static_str.push_str(&lit.value()), Part::Dynamic(stmt) => { dynamic_stmt = Some(stmt); - None + break; } - })) - .inspect(|static_part| { - size_estimate += static_part.value().len(); - }); + } + } + size_estimate += static_str.len(); + let static_lit = LitStr::new(&static_str, lit.span()); // XSS SAFETY: static parts are literal strings pushed by us stmts.extend(quote! { - #buffer_ident.dangerously_get_string().push_str(::core::concat!(#(#static_parts),*)); + #buffer_ident.dangerously_get_string().push_str(#static_lit); }); stmts.extend(dynamic_stmt); } @@ -733,10 +735,6 @@ impl Checks { self.recovered_errors.append(&mut other.recovered_errors); } - fn is_empty(&self) -> bool { - self.elements.is_empty() && self.recovered_errors.is_empty() - } - fn push_element(&mut self, element: ElementCheck) { self.elements.push(element); } @@ -744,41 +742,79 @@ impl Checks { fn push_diagnostic(&mut self, diagnostic: TokenStream) { self.recovered_errors.push(diagnostic); } + + fn block(module: ValidationModule, checks: &TokenStream) -> TokenStream { + quote! { + const _: fn() = || { + #[allow(unused_imports)] + use #module::*; + + #[doc(hidden)] + /// Used by the `html!`, `svg!`, and `attribute!` macros to + /// trigger compile-time element + /// validation. + fn check_element< + K: ::cheers::validation::ElementKind + >(_: impl ::cheers::validation::Element) {} + + #checks + }; + } + } +} + +fn by_module(elements: &[ElementCheck]) -> BTreeMap> { + let mut by_module: BTreeMap> = BTreeMap::new(); + for check in elements { + by_module.entry(check.module).or_default().push(check); + } + by_module +} + +/// Emits every check in `elements`, including those [`validation`] knows would pass. +pub(crate) fn all_checks(elements: &[ElementCheck]) -> TokenStream { + by_module(elements) + .into_iter() + .map(|(module, checks)| { + let checks: TokenStream = checks.iter().map(|check| check.checks()).collect(); + Checks::block(module, &checks) + }) + .collect() } +/// Emits the checks rustc needs, skipping those [`validation`] knows would pass. The names of +/// skipped checks are still referenced for rust-analyzer, which needs them for hover and +/// go-to-definition on element and attribute names. impl ToTokens for Checks { fn to_tokens(&self, tokens: &mut TokenStream) { - if self.is_empty() { - return; - } - for diagnostic in &self.recovered_errors { diagnostic.to_tokens(tokens); } - let mut by_module: BTreeMap> = BTreeMap::new(); - for check in &self.elements { - by_module.entry(check.module).or_default().push(check); - } + for (module, checks) in by_module(&self.elements) { + let mut needed = TokenStream::new(); + let mut references = TokenStream::new(); + for check in checks { + check.split_checks(&mut needed, &mut references); + } - for (module, checks) in by_module { - quote! { - const _: fn() = || { - #[allow(unused_imports)] - use #module::*; - - #[doc(hidden)] - /// Used by the `html!`, `svg!`, and `attribute!` macros to - /// trigger compile-time element - /// validation. - fn check_element< - K: ::cheers::validation::ElementKind - >(_: impl ::cheers::validation::Element) {} - - #(#checks)* - }; + if !needed.is_empty() { + Self::block(module, &needed).to_tokens(tokens); + } + if !references.is_empty() { + quote! { + #[allow(unexpected_cfgs)] + { + #[cfg(rust_analyzer)] + { + #[allow(unused_imports)] + use #module::*; + #references + } + } + } + .to_tokens(tokens); } - .to_tokens(tokens); } } } @@ -807,33 +843,51 @@ impl ElementCheck { pub fn push_attribute(&mut self, attr: AttributeNameCheck) { self.attributes.push(attr); } -} -impl ToTokens for ElementCheck { - fn to_tokens(&self, tokens: &mut TokenStream) { + fn element_check(&self) -> TokenStream { let el = &self.ident; let kind = self.kind; + quote! { + check_element::<#kind>(#el); + } + } - let el_check = { - quote! { - check_element::<#kind>(#el); - } - }; - + fn checks(&self) -> TokenStream { + let el_check = self.element_check(); let attr_checks = self .attributes .iter() - .map(|attr| attr.to_token_stream_with_el(el)); + .map(|attr| attr.to_token_stream_with_el(&self.ident)); quote! { #el_check #(#attr_checks)* } - .to_tokens(tokens); + } + + /// Splits the checks into those [`validation`] does not know to pass, and plain references to + /// the names of the others. + fn split_checks(&self, needed: &mut TokenStream, references: &mut TokenStream) { + let element = validation::element(self.module, &self.ident.name()); + + if element.is_some_and(|element| element.kind() == self.kind) { + let el = &self.ident; + references.extend(quote!(#el;)); + } else { + needed.extend(self.element_check()); + } + + for attr in &self.attributes { + if attr.is_known(element) { + references.extend(attr.reference_with_el(&self.ident)); + } else { + needed.extend(attr.to_token_stream_with_el(&self.ident)); + } + } } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElementKind { Normal, Void, @@ -860,18 +914,24 @@ pub struct AttributeNameCheck { struct DataModifierNameCheck<'a>(&'a UnquotedName); -impl ToTokens for DataModifierNameCheck<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { +impl DataModifierNameCheck<'_> { + /// `self` cannot be used as an item name, so the validation table stores it as `self_` + /// while rendering still emits the Datastar modifier name `self`. + fn item_name(&self) -> String { if self.0 == &"self" { - // `self` cannot be used as an item name, so the validation table stores it as - // `self_` while rendering still emits the Datastar modifier name `self`. - format_ident!("self_", span = self.0.span()).to_tokens(tokens); + "self_".to_owned() } else { - self.0.to_tokens(tokens); + self.0.name() } } } +impl ToTokens for DataModifierNameCheck<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + UnquotedName(Ident::new(&self.item_name(), self.0.span())).to_tokens(tokens); + } +} + impl AttributeNameCheck { pub fn new(kind: AttributeNameCheckKind, ident: UnquotedName, data: bool) -> Self { Self { @@ -882,6 +942,10 @@ impl AttributeNameCheck { } } + pub fn push_data_modifier(&mut self, modifier: UnquotedName) { + self.data_modifiers.push(modifier); + } + pub fn push_data_modifiers(&mut self, modifiers: Option<&DataModifiers>) { if let Some(modifiers) = modifiers { self.data_modifiers @@ -897,15 +961,19 @@ impl AttributeNameCheck { } } + fn data_plugin(&self) -> &UnquotedName { + match &self.kind { + AttributeNameCheckKind::Normal => &self.ident, + AttributeNameCheckKind::Namespace(namespace) => namespace, + } + } + fn data_modifier_checks(&self) -> TokenStream { if !self.data || self.data_modifiers.is_empty() { return TokenStream::new(); } - let plugin = match &self.kind { - AttributeNameCheckKind::Normal => &self.ident, - AttributeNameCheckKind::Namespace(namespace) => namespace, - }; + let plugin = self.data_plugin(); let modifiers = self.data_modifiers.iter().map(DataModifierNameCheck); quote! { @@ -915,6 +983,72 @@ impl AttributeNameCheck { } } + /// Whether this check passes for `element`, as far as [`validation`] knows. + fn is_known(&self, element: Option<&validation::Element>) -> bool { + let name = self.ident.name(); + let known = match (&self.kind, self.data) { + (AttributeNameCheckKind::Normal, false) => { + element.is_some_and(|element| element.has_attribute(&name)) + } + (AttributeNameCheckKind::Namespace(namespace), false) => { + let namespace = namespace.name(); + element.is_some_and(|element| element.has_namespace(&namespace)) + && validation::module(&namespace) + .is_some_and(|module| module.has_namespace() && module.has_attribute(&name)) + } + (AttributeNameCheckKind::Normal, true) => { + validation::module("data").is_some_and(|module| module.has_attribute(&name)) + } + (AttributeNameCheckKind::Namespace(namespace), true) => { + validation::module(&format!("data::{}", namespace.name())) + .is_some_and(|module| module.has_namespace() && module.has_attribute(&name)) + } + }; + + known && self.data_modifiers_known() + } + + fn data_modifiers_known(&self) -> bool { + if !self.data || self.data_modifiers.is_empty() { + return true; + } + + let plugin = self.data_plugin().name(); + let Some(module) = validation::module(&format!("data::modifiers::{plugin}")) else { + return false; + }; + + self.data_modifiers + .iter() + .all(|modifier| module.has_modifier(&DataModifierNameCheck(modifier).item_name())) + } + + fn reference_with_el(&self, el: &UnquotedName) -> TokenStream { + let ident = &self.ident; + let name = match &self.kind { + AttributeNameCheckKind::Namespace(namespace) if self.data => { + quote!(::cheers::validation::data::#namespace::#ident;) + } + AttributeNameCheckKind::Namespace(namespace) => { + quote!(<#el>::#namespace; ::cheers::validation::#namespace::#ident;) + } + AttributeNameCheckKind::Normal if self.data => { + quote!(::cheers::validation::data::#ident;) + } + AttributeNameCheckKind::Normal => quote!(<#el>::#ident;), + }; + if !self.data { + return name; + } + let plugin = self.data_plugin(); + let modifiers = self.data_modifiers.iter().map(DataModifierNameCheck); + + quote! { + #name + #(::cheers::validation::data::modifiers::#plugin::#modifiers;)* + } + } + fn to_token_stream_with_el(&self, el: &UnquotedName) -> TokenStream { let data_modifier_checks = self.data_modifier_checks(); diff --git a/crates/ast/src/lib.rs b/crates/ast/src/lib.rs index fc03e1d..34c9db3 100644 --- a/crates/ast/src/lib.rs +++ b/crates/ast/src/lib.rs @@ -4,6 +4,7 @@ pub mod control; pub mod generate; mod syntax; mod uses; +mod validation; use std::marker::PhantomData; @@ -18,6 +19,7 @@ use syn::{ punctuated::Punctuated, token::{Brace, Bracket, Paren}, }; +pub use validation::table_checks as validation_table_checks; use self::{ basics::Literal, @@ -871,6 +873,12 @@ impl Parse for AttributeName { } } else if lookahead.peek(LitStr) { let s = input.parse::()?; + if !s.suffix().is_empty() { + return Err(Error::new_spanned( + &s, + "Attribute names cannot have literal suffixes", + )); + } let value = s.value(); for c in value.chars() { @@ -1649,6 +1657,15 @@ mod tests { SyntaxStatic, }; + #[test] + fn quoted_attribute_names_reject_suffixes() { + let error = parse_str::(r#"div "data-x"suf="1" {}"#) + .err() + .expect("suffixed attribute name should not parse"); + + assert!(error.to_string().contains("literal suffixes"), "{error}"); + } + #[test] fn syntax_static_accepts_literal_markup() { let doc = parse_str::(r#"div class="card" !ignore { "Hello" span { "world" } }"#) diff --git a/crates/ast/src/validation.rs b/crates/ast/src/validation.rs new file mode 100644 index 0000000..de4505d --- /dev/null +++ b/crates/ast/src/validation.rs @@ -0,0 +1,298 @@ +//! The element and attribute names that the Rust items in `cheers::validation` accept. +//! +//! A name found here passes the compile-time check `html!` would emit for it, so the check can be +//! skipped. Names missing here keep their check, which is the only place a typo, a custom element +//! or a `define_events!` event is resolved. + +#[cfg(test)] +mod generate_table; +#[rustfmt::skip] +mod table; + +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +use crate::{ + UnquotedName, + generate::{ + AttributeNameCheck, AttributeNameCheckKind, ElementCheck, ElementKind, ValidationModule, + all_checks, + }, +}; + +pub struct Element { + name: &'static str, + kind: ElementKind, + globals: &'static Globals, + attributes: &'static [&'static str], + namespaces: &'static [&'static str], +} + +pub struct Globals { + attributes: &'static [&'static str], + namespaces: &'static [&'static str], +} + +/// A module of `cheers::validation`, by its path relative to it. +pub struct Module { + path: &'static str, + namespace: bool, + attributes: &'static [&'static str], + modifiers: &'static [&'static str], +} + +pub fn element(module: ValidationModule, name: &str) -> Option<&'static Element> { + let elements = match module { + ValidationModule::Html => table::HTML_ELEMENTS, + ValidationModule::Svg => table::SVG_ELEMENTS, + ValidationModule::MathMl => return None, + }; + + elements + .binary_search_by(|element| element.name.cmp(name)) + .ok() + .map(|index| &elements[index]) +} + +pub fn module(path: &str) -> Option<&'static Module> { + table::MODULES + .binary_search_by(|module| module.path.cmp(path)) + .ok() + .map(|index| &table::MODULES[index]) +} + +fn contains(names: &[&str], name: &str) -> bool { + names.binary_search(&name).is_ok() +} + +impl Element { + pub fn kind(&self) -> ElementKind { + self.kind + } + + pub fn has_attribute(&self, name: &str) -> bool { + contains(self.attributes, name) || contains(self.globals.attributes, name) + } + + pub fn has_namespace(&self, name: &str) -> bool { + // An inherent `::name` attribute shadows a namespace of the same name. + !contains(self.attributes, name) + && (contains(self.namespaces, name) || contains(self.globals.namespaces, name)) + } +} + +impl Module { + pub fn has_namespace(&self) -> bool { + self.namespace + } + + pub fn has_attribute(&self, name: &str) -> bool { + contains(self.attributes, name) + } + + pub fn has_modifier(&self, name: &str) -> bool { + contains(self.modifiers, name) + } +} + +/// Emits the check `html!` would for every name in the table, so compiling the output proves +/// that rustc accepts all of them. +pub fn table_checks() -> TokenStream { + let name = |name: &str| UnquotedName(Ident::new(name, Span::call_site())); + let modifiers = |plugin: &str, check: &mut AttributeNameCheck| { + for modifier in + module(&format!("data::modifiers::{plugin}")).map_or(&[][..], |m| m.modifiers) + { + check.push_data_modifier(name(modifier)); + } + }; + let mut checks = Vec::new(); + + for (validation_module, elements) in [ + (ValidationModule::Html, table::HTML_ELEMENTS), + (ValidationModule::Svg, table::SVG_ELEMENTS), + ] { + for element in elements { + let mut check = ElementCheck::new(&name(element.name), element.kind, validation_module); + for attribute in element.attributes.iter().chain(element.globals.attributes) { + check.push_attribute(AttributeNameCheck::new( + AttributeNameCheckKind::Normal, + name(attribute), + false, + )); + } + let namespaces = element.namespaces.iter().chain(element.globals.namespaces); + for namespace in namespaces.filter(|namespace| element.has_namespace(namespace)) { + let attributes = module(namespace).map_or(&[][..], |m| m.attributes); + for attribute in attributes { + check.push_attribute(AttributeNameCheck::new( + AttributeNameCheckKind::Namespace(name(namespace)), + name(attribute), + false, + )); + } + } + checks.push(check); + } + } + + let mut data = ElementCheck::new(&name("div"), ElementKind::Normal, ValidationModule::Html); + for module in table::MODULES { + if module.path == "data" { + for attribute in module.attributes { + let mut check = + AttributeNameCheck::new(AttributeNameCheckKind::Normal, name(attribute), true); + modifiers(attribute, &mut check); + data.push_attribute(check); + } + } else if let Some(namespace) = module.path.strip_prefix("data::") + && module.namespace + { + for attribute in module.attributes { + let mut check = AttributeNameCheck::new( + AttributeNameCheckKind::Namespace(name(namespace)), + name(attribute), + true, + ); + modifiers(namespace, &mut check); + data.push_attribute(check); + } + } + } + checks.push(data); + + let checks = all_checks(&checks); + quote! { + const _: () = { + use ::cheers::validation::attributes::*; + #checks + }; + } +} + +#[cfg(test)] +mod tests { + use quote::quote; + + use super::{element, module, table}; + use crate::{ + Document, + generate::{ElementKind, ValidationModule, lazy}, + }; + + fn expand(tokens: proc_macro2::TokenStream) -> String { + lazy::(tokens) + .expect("document should generate") + .to_string() + } + + #[test] + fn known_names_are_only_referenced_for_rust_analyzer() { + let expanded = expand(quote! { + div id="a" aria:label="b" !on:click[prevent]("c") { a href="/" {} input; } + }); + let (_, references) = expanded + .split_once("# [cfg (rust_analyzer)]") + .expect("known names should be referenced for rust-analyzer"); + + assert!(!expanded.contains("check_element"), "{expanded}"); + assert!(references.contains("< r#div > :: r#id ;"), "{references}"); + assert!( + references.contains("data :: r#on :: r#click ;") + && references.contains("modifiers :: r#on :: r#prevent ;"), + "{references}" + ); + } + + #[test] + fn unknown_names_keep_their_checks() { + let expanded = expand(quote! { + div id="a" hx_get="b" !on:my_event("c") { img {} } + }); + let (needed, references) = expanded + .split_once("# [cfg (rust_analyzer)]") + .expect("known names should be referenced for rust-analyzer"); + + assert!( + needed.contains("r#hx_get") && needed.contains("r#my_event"), + "{needed}" + ); + assert!(needed.contains("(r#img)"), "{needed}"); + assert!( + !needed.contains("r#id") && !needed.contains("(r#div)"), + "{needed}" + ); + assert!( + references.contains("r#div ;") && references.contains("r#id ;"), + "{references}" + ); + } + + #[test] + fn unknown_elements_are_checked_without_cfg() { + let expanded = expand(quote! { my_widget size="1" {} }); + + assert!(!expanded.contains("rust_analyzer"), "{expanded}"); + assert!( + expanded.contains("(r#my_widget)") && expanded.contains("r#size"), + "{expanded}" + ); + } + + #[test] + fn adjacent_static_parts_are_pushed_as_one_literal() { + let expanded = expand(quote! { div class="a" { "b" } }); + + assert!( + expanded.contains(r#"push_str ("
b
")"#), + "{expanded}" + ); + } + + #[test] + fn tables_are_sorted() { + let is_sorted = |names: &[&str]| names.windows(2).all(|pair| pair[0] < pair[1]); + for elements in [table::HTML_ELEMENTS, table::SVG_ELEMENTS] { + assert!(elements.windows(2).all(|pair| pair[0].name < pair[1].name)); + for element in elements { + assert!(is_sorted(element.attributes) && is_sorted(element.namespaces)); + assert!( + is_sorted(element.globals.attributes) && is_sorted(element.globals.namespaces) + ); + } + } + assert!( + table::MODULES + .windows(2) + .all(|pair| pair[0].path < pair[1].path) + ); + for module in table::MODULES { + assert!(is_sorted(module.attributes) && is_sorted(module.modifiers)); + } + } + + #[test] + fn looks_up_names() { + let div = element(ValidationModule::Html, "div").expect("`div` is an HTML element"); + assert_eq!(div.kind(), ElementKind::Normal); + assert!(div.has_attribute("id") && div.has_attribute("role")); + assert!(!div.has_attribute("href") && !div.has_attribute("aria")); + assert!(div.has_namespace("aria") && !div.has_namespace("on")); + + let object = element(ValidationModule::Html, "object").expect("`object` is an element"); + assert!(object.has_attribute("data") && !object.has_namespace("data")); + + let meta = element(ValidationModule::Html, "meta").expect("`meta` is an HTML element"); + assert_eq!(meta.kind(), ElementKind::Void); + assert!(meta.has_attribute("property")); + + assert!(element(ValidationModule::Html, "dvi").is_none()); + assert!(element(ValidationModule::Svg, "div").is_none()); + assert!(element(ValidationModule::MathMl, "math").is_none()); + assert!(element(ValidationModule::Html, "math").is_none()); + + let on = module("data::on").expect("`data::on` is a module"); + assert!(on.has_namespace() && on.has_attribute("click")); + assert!(module("data::modifiers::ignore").is_some_and(|m| m.has_modifier("self_"))); + } +} diff --git a/crates/ast/src/validation/generate_table.rs b/crates/ast/src/validation/generate_table.rs new file mode 100644 index 0000000..4f6d085 --- /dev/null +++ b/crates/ast/src/validation/generate_table.rs @@ -0,0 +1,619 @@ +//! Derives `table.rs` from the Rust items in `crates/cheers/src/validation`. +//! +//! The derivation mirrors how rustc resolves the checks `html!` emits, and rejects any item it +//! does not understand, so a change to the validation items fails here instead of producing a +//! table that disagrees with them. + +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Write as _, + fs, + path::{Path, PathBuf}, +}; + +use syn::{ + Attribute, File, GenericParam, Ident, ImplItem, Item, Path as SynPath, TraitItem, Type, + TypeParamBound, UseTree, Visibility, braced, + ext::IdentExt, + parse::{ParseStream, Parser}, + token::Brace, +}; + +use crate::generate::ValidationModule; + +const UPDATE_ENV: &str = "UPDATE_VALIDATION_TABLE"; + +#[derive(Clone, PartialEq, Eq)] +enum ConstType { + Attribute, + Modifier, + Namespace(String), +} + +#[derive(Default)] +struct ModuleInfo { + namespace: bool, + attributes: BTreeSet, + modifiers: BTreeSet, +} + +struct ElementInfo { + kind: &'static str, + globals: String, + attributes: BTreeSet, +} + +#[derive(Default)] +struct Source { + traits: BTreeMap>, + /// `(bound, implemented)` for `impl implemented for T {}`. + blanket_impls: Vec<(String, String)>, + /// `(implemented, module, element)` for `impl implemented for element {}`. + element_impls: Vec<(String, ValidationModule, String)>, + modules: BTreeMap, + elements: BTreeMap>, +} + +fn name(ident: &Ident) -> String { + ident.unraw().to_string() +} + +/// Whether an item with `attrs` is behind `#[cfg]`. Attributes that could change what a name +/// means, like `deprecated` or `cfg_attr`, are rejected. +fn cfg_gated(attrs: &[Attribute]) -> bool { + let mut gated = false; + for attr in attrs { + let path = attr.path(); + if path.is_ident("cfg") { + gated = true; + } else if !["doc", "allow", "expect", "derive", "non_exhaustive"] + .iter() + .any(|name| path.is_ident(name)) + { + panic!( + "unsupported attribute `#[{}]` on a validation item", + quote::quote!(#path) + ); + } + } + gated +} + +/// Items that add names must always be compiled: under another feature set, a gated one could +/// make a name the table lists ambiguous. +fn assert_ungated(attrs: &[Attribute]) { + assert!( + !cfg_gated(attrs), + "`#[cfg]` is only supported on element lists and re-exports" + ); +} + +fn use_names(tree: &UseTree, names: &mut Vec) { + match tree { + UseTree::Path(tree) => use_names(&tree.tree, names), + UseTree::Name(tree) => names.push(name(&tree.ident)), + UseTree::Rename(tree) => names.push(name(&tree.rename)), + UseTree::Glob(_) => {} + UseTree::Group(group) => { + for tree in &group.items { + use_names(tree, names); + } + } + } +} + +fn last_segment(path: &SynPath) -> String { + name(&path.segments.last().expect("paths are never empty").ident) +} + +fn const_type(ty: &Type) -> ConstType { + let Type::Path(ty) = ty else { + panic!("unsupported validation const type"); + }; + let segments: Vec<_> = ty.path.segments.iter().map(|s| name(&s.ident)).collect(); + match segments.as_slice() { + [.., last] if last == "Attribute" => ConstType::Attribute, + [.., last] if last == "Modifier" => ConstType::Modifier, + [namespace, last] if last == "Namespace" => ConstType::Namespace(namespace.clone()), + _ => panic!( + "unsupported validation const type `{}`", + segments.join("::") + ), + } +} + +fn element_path(path: &SynPath) -> (ValidationModule, String) { + let segments: Vec<_> = path.segments.iter().map(|s| name(&s.ident)).collect(); + match segments.as_slice() { + [krate, validation, elements, element] + if krate == "crate" && validation == "validation" && elements == "elements" => + { + (ValidationModule::Html, element.clone()) + } + [krate, validation, svg, elements, element] + if krate == "crate" + && validation == "validation" + && svg == "svg" + && elements == "elements" => + { + (ValidationModule::Svg, element.clone()) + } + _ => panic!("unsupported element path `{}`", segments.join("::")), + } +} + +impl Source { + fn read(dir: &Path) -> Self { + let mut source = Self::default(); + source.attribute_items(&parse(&dir.join("attributes.rs")).items, ""); + source.element_items( + &parse(&dir.join("elements.rs")).items, + ValidationModule::Html, + ); + + let svg = parse(&dir.join("svg.rs")); + let [Item::Mod(elements)] = svg.items.as_slice() else { + panic!("`svg.rs` should only contain `mod elements`"); + }; + let (_, items) = elements + .content + .as_ref() + .expect("`svg::elements` is inline"); + source.element_items(items, ValidationModule::Svg); + source.check_root_items(&parse(&dir.join("mod.rs")).items, true); + + source + } + + /// Rejects items in `validation/mod.rs` that would change what the checks resolve to without + /// this table knowing: validation trait impls, element lists, and items that shadow a module + /// the `pub use attributes::*` glob provides. + fn check_root_items(&self, items: &[Item], top_level: bool) { + for item in items { + match item { + Item::Impl(item) => { + if let Some((_, implemented, _)) = &item.trait_ { + assert!( + !self.traits.contains_key(&last_segment(implemented)), + "validation trait impls belong in `attributes.rs`" + ); + } + } + Item::Macro(item) => assert!( + !item.mac.path.is_ident("define_validation_elements"), + "element lists belong in `elements.rs` or `svg.rs`" + ), + Item::Mod(item) => { + if let Some((_, items)) = &item.content { + self.check_root_items(items, false); + } + } + _ => {} + } + + if !top_level { + continue; + } + let mut names = Vec::new(); + match item { + Item::Use(item) => use_names(&item.tree, &mut names), + Item::Mod(item) => names.push(name(&item.ident)), + Item::Struct(item) => names.push(name(&item.ident)), + Item::Enum(item) => names.push(name(&item.ident)), + Item::Const(item) => names.push(name(&item.ident)), + Item::Static(item) => names.push(name(&item.ident)), + Item::Fn(item) => names.push(name(&item.sig.ident)), + Item::Trait(item) => names.push(name(&item.ident)), + Item::Type(item) => names.push(name(&item.ident)), + _ => {} + } + for name in names { + assert!( + !self.modules.contains_key(&name), + "`validation::{name}` shadows the attribute module of the same name" + ); + } + } + } + + fn attribute_items(&mut self, items: &[Item], module: &str) { + self.modules.entry(module.to_owned()).or_default(); + + for item in items { + match item { + Item::Use(item) => { + assert!( + cfg_gated(&item.attrs) || matches!(item.vis, Visibility::Inherited), + "re-exports from validation attributes are not supported" + ); + } + Item::Trait(item) => { + assert_ungated(&item.attrs); + assert!( + item.generics.params.is_empty() && item.generics.where_clause.is_none(), + "generic validation traits are not supported" + ); + let consts = item + .items + .iter() + .map(|item| match item { + TraitItem::Const(item) => { + assert_ungated(&item.attrs); + (name(&item.ident), const_type(&item.ty)) + } + _ => panic!("validation traits should only contain consts"), + }) + .collect(); + self.traits.insert(name(&item.ident), consts); + } + Item::Impl(item) => { + assert_ungated(&item.attrs); + self.attribute_impl(item); + } + Item::Mod(item) => { + assert_ungated(&item.attrs); + assert!( + matches!(item.vis, Visibility::Public(_)), + "validation modules should be public" + ); + let path = if module.is_empty() { + name(&item.ident) + } else { + format!("{module}::{}", name(&item.ident)) + }; + let (_, items) = item.content.as_ref().expect("modules are inline"); + self.attribute_items(items, &path); + } + Item::Struct(item) => { + assert_ungated(&item.attrs); + if item.ident == "Namespace" { + self.modules.entry(module.to_owned()).or_default().namespace = true; + } + } + Item::Const(item) => { + assert_ungated(&item.attrs); + assert!( + matches!(item.vis, Visibility::Public(_)), + "validation consts should be public" + ); + let info = self.modules.entry(module.to_owned()).or_default(); + let set = match const_type(&item.ty) { + ConstType::Attribute => &mut info.attributes, + ConstType::Modifier => &mut info.modifiers, + ConstType::Namespace(_) => panic!("namespace consts belong in traits"), + }; + set.insert(name(&item.ident)); + } + _ => panic!("unsupported item in validation attributes"), + } + } + } + + fn attribute_impl(&mut self, item: &syn::ItemImpl) { + assert!( + item.items + .iter() + .all(|item| !matches!(item, ImplItem::Const(_))), + "validation impls should not override consts" + ); + let (_, implemented, _) = item + .trait_ + .as_ref() + .expect("only trait impls are supported"); + let implemented = last_segment(implemented); + + match item.generics.params.iter().collect::>().as_slice() { + [] => { + let Type::Path(self_ty) = &*item.self_ty else { + panic!("unsupported impl target"); + }; + let (module, element) = element_path(&self_ty.path); + self.element_impls.push((implemented, module, element)); + } + [GenericParam::Type(param)] => { + let [TypeParamBound::Trait(bound)] = + param.bounds.iter().collect::>().as_slice() + else { + panic!("blanket impls should have one trait bound"); + }; + assert!( + matches!(&*item.self_ty, Type::Path(ty) if ty.path.is_ident(¶m.ident)), + "only blanket impls for the type parameter are supported" + ); + self.blanket_impls + .push((last_segment(&bound.path), implemented)); + } + _ => panic!("unsupported impl generics"), + } + } + + fn element_items(&mut self, items: &[Item], module: ValidationModule) { + for item in items { + match item { + Item::Use(_) => {} + Item::Macro(item) if cfg_gated(&item.attrs) => {} + Item::Macro(item) if item.mac.path.is_ident("define_validation_elements") => { + let elements = self.elements.entry(module).or_default(); + (|input: ParseStream<'_>| parse_elements(input, elements)) + .parse2(item.mac.tokens.clone()) + .expect("`define_validation_elements!` invocations should parse"); + } + _ => panic!("unsupported item in validation elements"), + } + } + } + + /// The traits a type implementing all of `traits` implements through blanket impls. + fn implied_traits(&self, traits: impl IntoIterator) -> BTreeSet { + let mut implied: BTreeSet = traits.into_iter().collect(); + loop { + let before = implied.len(); + for (bound, implemented) in &self.blanket_impls { + if implied.contains(bound) { + implied.insert(implemented.clone()); + } + } + if implied.len() == before { + return implied; + } + } + } + + fn trait_consts<'a>( + &'a self, + traits: &'a BTreeSet, + ) -> impl Iterator { + traits.iter().flat_map(|implemented| { + self.traits + .get(implemented) + .unwrap_or_else(|| panic!("unknown validation trait `{implemented}`")) + }) + } + + /// The attribute and namespace names `::name` resolves to through `traits`, leaving + /// out names more than one of them declares, which rustc rejects as ambiguous. + fn trait_names(&self, traits: &BTreeSet) -> (BTreeSet, BTreeSet) { + let mut declared = BTreeMap::<&str, usize>::new(); + for (name, _) in self.trait_consts(traits) { + *declared.entry(name).or_default() += 1; + } + + let mut attributes = BTreeSet::new(); + let mut namespaces = BTreeSet::new(); + for (name, ty) in self.trait_consts(traits) { + if declared[name.as_str()] > 1 { + continue; + } + match ty { + ConstType::Attribute => { + attributes.insert(name.clone()); + } + ConstType::Namespace(namespace) + if namespace == name + && self.modules.get(namespace).is_some_and(|m| m.namespace) => + { + namespaces.insert(name.clone()); + } + ConstType::Namespace(_) | ConstType::Modifier => {} + } + } + (attributes, namespaces) + } +} + +fn parse(path: &Path) -> File { + let source = fs::read_to_string(path) + .unwrap_or_else(|error| panic!("failed to read `{}`: {error}", path.display())); + syn::parse_file(&source) + .unwrap_or_else(|error| panic!("failed to parse `{}`: {error}", path.display())) +} + +fn parse_doc_attrs(input: ParseStream<'_>) -> syn::Result<()> { + for attr in Attribute::parse_outer(input)? { + if !attr.path().is_ident("doc") { + return Err(syn::Error::new_spanned( + attr, + "only doc attributes are supported in validation tables", + )); + } + } + Ok(()) +} + +fn parse_elements( + input: ParseStream<'_>, + elements: &mut BTreeMap, +) -> syn::Result<()> { + let path_arg = |key: &str| -> syn::Result { + let ident: Ident = input.parse()?; + if ident != key { + return Err(syn::Error::new(ident.span(), format!("expected `{key}`"))); + } + input.parse::()?; + let path: SynPath = input.parse()?; + input.parse::()?; + Ok(last_segment(&path)) + }; + let kind_span = input.span(); + let kind = match path_arg("kind")?.as_str() { + "Normal" => "Normal", + "Void" => "Void", + "Xml" => "Xml", + kind => { + return Err(syn::Error::new( + kind_span, + format!("unknown element kind `{kind}`"), + )); + } + }; + let globals = path_arg("globals")?; + + let content; + braced!(content in input); + while !content.is_empty() { + parse_doc_attrs(&content)?; + let element = name(&content.call(Ident::parse_any)?); + let mut attributes = BTreeSet::new(); + if content.peek(Brace) { + let attrs; + braced!(attrs in content); + while !attrs.is_empty() { + parse_doc_attrs(&attrs)?; + attributes.insert(name(&attrs.call(Ident::parse_any)?)); + } + } + let info = ElementInfo { + kind, + globals: globals.clone(), + attributes, + }; + if elements.insert(element.clone(), info).is_some() { + return Err(syn::Error::new( + content.span(), + format!("element `{element}` is defined twice"), + )); + } + } + Ok(()) +} + +fn list<'a>(names: impl IntoIterator) -> String { + let names: Vec<_> = names + .into_iter() + .map(|name| format!("\"{name}\"")) + .collect(); + format!("&[{}]", names.join(", ")) +} + +fn globals_const(globals: &str) -> String { + let mut out = String::new(); + for (index, c) in globals.chars().enumerate() { + if c.is_ascii_uppercase() && index > 0 { + out.push('_'); + } + out.push(c.to_ascii_uppercase()); + } + out +} + +fn render(source: &Source) -> String { + let mut out = String::from( + "// @generated from `crates/cheers/src/validation` by the `validation_table_is_up_to_date`\n\ + // test. Regenerate it with `UPDATE_VALIDATION_TABLE=1 cargo test -p cheers-ast`.\n\n\ + use super::{Element, Globals, Module};\n\ + use crate::generate::ElementKind::{Normal, Void, Xml};\n", + ); + + let globals: BTreeSet<&String> = source + .elements + .values() + .flat_map(BTreeMap::values) + .map(|element| &element.globals) + .collect(); + for globals in globals { + let (attributes, namespaces) = + source.trait_names(&source.implied_traits([globals.clone()])); + writeln!( + out, + "\nconst {}: Globals = Globals {{\n attributes: {},\n namespaces: {},\n}};", + globals_const(globals), + list(&attributes), + list(&namespaces), + ) + .expect("writing to a String cannot fail"); + } + + for (module, table) in [ + (ValidationModule::Html, "HTML_ELEMENTS"), + (ValidationModule::Svg, "SVG_ELEMENTS"), + ] { + writeln!(out, "\npub(super) const {table}: &[Element] = &[").expect("infallible"); + for (element, info) in &source.elements[&module] { + let globals = source.implied_traits([info.globals.clone()]); + let implemented = source + .element_impls + .iter() + .filter(|(_, m, e)| *m == module && e == element) + .map(|(implemented, _, _)| implemented.clone()); + let implemented: BTreeSet<_> = source + .implied_traits(implemented) + .difference(&globals) + .cloned() + .collect(); + for (name, _) in source.trait_consts(&implemented) { + assert!( + source + .trait_consts(&globals) + .all(|(global, _)| global != name), + "`{element}` gets `{name}` from more than one trait" + ); + } + + let (mut attributes, namespaces) = source.trait_names(&implemented); + attributes.extend(info.attributes.iter().cloned()); + writeln!( + out, + " Element {{ name: \"{element}\", kind: {}, globals: &{}, attributes: {}, namespaces: {} }},", + info.kind, + globals_const(&info.globals), + list(&attributes), + list(&namespaces), + ) + .expect("infallible"); + } + out.push_str("];\n"); + } + + out.push_str("\npub(super) const MODULES: &[Module] = &[\n"); + for (path, info) in &source.modules { + if path.is_empty() { + continue; + } + writeln!( + out, + " Module {{ path: \"{path}\", namespace: {}, attributes: {}, modifiers: {} }},", + info.namespace, + list(&info.attributes), + list(&info.modifiers), + ) + .expect("infallible"); + } + out.push_str("];\n"); + + out +} + +#[test] +fn validation_table_is_up_to_date() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let validation_dir = manifest_dir.join("../cheers/src/validation"); + if !validation_dir.exists() { + eprintln!("skipped: the `cheers` sources are only available in the workspace"); + return; + } + let source = Source::read(&validation_dir); + for module in [ValidationModule::Html, ValidationModule::Svg] { + for element in source.element_impls.iter().filter(|(_, m, _)| *m == module) { + assert!( + source.elements[&module].contains_key(&element.2), + "impl for unknown element `{}`", + element.2 + ); + } + } + + let generated = render(&source); + let table_path = manifest_dir.join("src/validation/table.rs"); + + if std::env::var_os(UPDATE_ENV).is_some() { + fs::write(&table_path, generated).expect("failed to write the validation table"); + return; + } + + let current = fs::read_to_string(&table_path).unwrap_or_default(); + assert!( + current == generated, + "`src/validation/table.rs` is out of date with `crates/cheers/src/validation`; \ + regenerate it with `{UPDATE_ENV}=1 cargo test -p cheers-ast validation_table`" + ); +} diff --git a/crates/ast/src/validation/table.rs b/crates/ast/src/validation/table.rs new file mode 100644 index 0000000..c6377cf --- /dev/null +++ b/crates/ast/src/validation/table.rs @@ -0,0 +1,225 @@ +// @generated from `crates/cheers/src/validation` by the `validation_table_is_up_to_date` +// test. Regenerate it with `UPDATE_VALIDATION_TABLE=1 cargo test -p cheers-ast`. + +use super::{Element, Globals, Module}; +use crate::generate::ElementKind::{Normal, Void, Xml}; + +const GLOBAL_ATTRIBUTES: Globals = Globals { + attributes: &["access_key", "autocapitalize", "autofocus", "class", "contenteditable", "dir", "draggable", "enterkeyhint", "hidden", "id", "inert", "inputmode", "is", "itemid", "itemprop", "itemref", "itemscope", "itemtype", "lang", "nonce", "popover", "role", "slot", "spellcheck", "style", "tabindex", "title", "translate"], + namespaces: &["aria", "data"], +}; + +const SVG_GLOBAL_ATTRIBUTES: Globals = Globals { + attributes: &["alignment_baseline", "autofocus", "baseline_shift", "class", "clip", "clip_path", "clip_rule", "color", "color_interpolation", "color_interpolation_filters", "cursor", "d", "direction", "display", "dominant_baseline", "enable_background", "fill", "fill_opacity", "fill_rule", "filter", "flood_color", "flood_opacity", "font_family", "font_size", "font_size_adjust", "font_stretch", "font_style", "font_variant", "font_weight", "glyph_orientation_horizontal", "glyph_orientation_vertical", "id", "image_rendering", "kerning", "lang", "letter_spacing", "lighting_color", "marker_end", "marker_mid", "marker_start", "mask", "opacity", "overflow", "paint_order", "pointer_events", "required_extensions", "shape_rendering", "stop_color", "stop_opacity", "stroke", "stroke_dasharray", "stroke_dashoffset", "stroke_linecap", "stroke_linejoin", "stroke_miterlimit", "stroke_opacity", "stroke_width", "style", "system_language", "tabindex", "text_anchor", "text_decoration", "text_rendering", "transform", "transform_origin", "unicode_bidi", "vector_effect", "visibility", "word_spacing", "writing_mode", "xml", "xmlns"], + namespaces: &[], +}; + +pub(super) const HTML_ELEMENTS: &[Element] = &[ + Element { name: "a", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["download", "href", "hreflang", "ping", "referrerpolicy", "rel", "target", "type"], namespaces: &[] }, + Element { name: "abbr", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "address", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "area", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["alt", "coords", "download", "href", "ping", "referrerpolicy", "rel", "shape", "target"], namespaces: &[] }, + Element { name: "article", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "aside", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "audio", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["autoplay", "controls", "crossorigin", "loop", "muted", "preload", "src"], namespaces: &[] }, + Element { name: "b", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "base", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["href", "target"], namespaces: &[] }, + Element { name: "bdi", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "bdo", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "blockquote", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["cite"], namespaces: &[] }, + Element { name: "body", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "br", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "button", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["command", "commandfor", "disabled", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "name", "popovertarget", "popovertargetaction", "type", "value"], namespaces: &[] }, + Element { name: "canvas", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["height", "width"], namespaces: &[] }, + Element { name: "caption", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "cite", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "code", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "col", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["span"], namespaces: &[] }, + Element { name: "colgroup", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["span"], namespaces: &[] }, + Element { name: "data", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["value"], namespaces: &[] }, + Element { name: "datalist", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "dd", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "del", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["cite", "datetime"], namespaces: &[] }, + Element { name: "details", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["name", "open"], namespaces: &[] }, + Element { name: "dfn", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "dialog", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["open"], namespaces: &[] }, + Element { name: "div", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "dl", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "dt", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "em", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "embed", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["height", "src", "type", "width"], namespaces: &[] }, + Element { name: "fieldset", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["disabled", "form", "name"], namespaces: &[] }, + Element { name: "figcaption", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "figure", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "footer", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "form", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["accept_charset", "action", "autocomplete", "enctype", "method", "name", "novalidate", "rel", "target"], namespaces: &[] }, + Element { name: "h1", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "h2", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "h3", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "h4", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "h5", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "h6", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "head", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "header", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "hgroup", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "hr", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "html", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "i", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "iframe", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["allow", "allowfullscreen", "height", "loading", "name", "referrerpolicy", "sandbox", "src", "srcdoc", "width"], namespaces: &[] }, + Element { name: "img", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["alt", "crossorigin", "decoding", "fetchpriority", "height", "ismap", "loading", "referrerpolicy", "sizes", "src", "srcset", "usemap", "width"], namespaces: &[] }, + Element { name: "input", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["accept", "alt", "autocomplete", "capture", "checked", "dirname", "disabled", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "max", "maxlength", "min", "minlength", "multiple", "name", "pattern", "placeholder", "popovertarget", "popovertargetaction", "readonly", "required", "size", "src", "step", "type", "value", "width"], namespaces: &[] }, + Element { name: "ins", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["cite", "datetime"], namespaces: &[] }, + Element { name: "kbd", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "label", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["for"], namespaces: &[] }, + Element { name: "legend", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "li", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["value"], namespaces: &[] }, + Element { name: "link", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["as", "blocking", "color", "crossorigin", "disabled", "fetchpriority", "href", "hreflang", "imagesizes", "imagesrcset", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"], namespaces: &[] }, + Element { name: "main", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "map", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["name"], namespaces: &[] }, + Element { name: "mark", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "menu", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "meta", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["charset", "content", "http_equiv", "media", "name", "property"], namespaces: &[] }, + Element { name: "meter", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["high", "low", "max", "min", "optimum", "value"], namespaces: &[] }, + Element { name: "nav", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "noscript", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "object", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["data", "form", "height", "name", "type", "width"], namespaces: &[] }, + Element { name: "ol", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["reversed", "start", "type"], namespaces: &[] }, + Element { name: "optgroup", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["disabled", "label"], namespaces: &[] }, + Element { name: "option", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["disabled", "label", "selected", "value"], namespaces: &[] }, + Element { name: "output", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["for", "form", "name"], namespaces: &[] }, + Element { name: "p", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "picture", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "pre", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "progress", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["max", "value"], namespaces: &[] }, + Element { name: "q", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["cite"], namespaces: &[] }, + Element { name: "rp", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "rt", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "ruby", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "s", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "samp", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "script", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["async", "blocking", "crossorigin", "defer", "fetchpriority", "integrity", "nomodule", "referrerpolicy", "src", "type"], namespaces: &[] }, + Element { name: "search", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "section", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "select", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["autocomplete", "disabled", "form", "multiple", "name", "required", "size"], namespaces: &[] }, + Element { name: "slot", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["name"], namespaces: &[] }, + Element { name: "small", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "source", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["height", "media", "sizes", "src", "srcset", "type", "width"], namespaces: &[] }, + Element { name: "span", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "strong", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "style", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["blocking", "media"], namespaces: &[] }, + Element { name: "sub", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "summary", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "sup", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "svg", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["height", "preserveAspectRatio", "viewBox", "width", "x", "xmlns", "y"], namespaces: &[] }, + Element { name: "table", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "tbody", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "td", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["colspan", "headers", "rowspan"], namespaces: &[] }, + Element { name: "template", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["shadowrootdelegatesfocus", "shadowrootmode"], namespaces: &[] }, + Element { name: "textarea", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["autocomplete", "cols", "dirname", "disabled", "form", "maxlength", "minlength", "name", "placeholder", "readonly", "required", "rows", "wrap"], namespaces: &[] }, + Element { name: "tfoot", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "th", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["abbr", "colspan", "headers", "rowspan", "scope"], namespaces: &[] }, + Element { name: "thead", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "time", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["datetime"], namespaces: &[] }, + Element { name: "title", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "tr", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "track", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &["default", "kind", "label", "src", "srclang"], namespaces: &[] }, + Element { name: "u", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "ul", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "var", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "video", kind: Normal, globals: &GLOBAL_ATTRIBUTES, attributes: &["autoplay", "controls", "crossorigin", "height", "loop", "muted", "playsinline", "poster", "preload", "src", "width"], namespaces: &[] }, + Element { name: "wbr", kind: Void, globals: &GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, +]; + +pub(super) const SVG_ELEMENTS: &[Element] = &[ + Element { name: "a", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["download", "href", "hreflang", "ping", "referrerpolicy", "rel", "target", "type"], namespaces: &[] }, + Element { name: "animate", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["accumulate", "additive", "attributeName", "begin", "by", "calcMode", "dur", "end", "fill", "from", "href", "keySplines", "keyTimes", "max", "min", "repeatCount", "repeatDur", "restart", "to", "values"], namespaces: &[] }, + Element { name: "animateMotion", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["accumulate", "additive", "begin", "by", "calcMode", "dur", "end", "fill", "from", "href", "keyPoints", "keySplines", "keyTimes", "max", "min", "origin", "path", "repeatCount", "repeatDur", "restart", "rotate", "to", "values"], namespaces: &[] }, + Element { name: "animateTransform", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["accumulate", "additive", "attributeName", "begin", "by", "calcMode", "dur", "end", "fill", "from", "href", "keySplines", "keyTimes", "max", "min", "repeatCount", "repeatDur", "restart", "to", "type", "values"], namespaces: &[] }, + Element { name: "circle", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["cx", "cy", "pathLength", "r"], namespaces: &[] }, + Element { name: "clipPath", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["clipPathUnits"], namespaces: &[] }, + Element { name: "defs", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "desc", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "ellipse", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["cx", "cy", "pathLength", "rx", "ry"], namespaces: &[] }, + Element { name: "feBlend", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "in2", "mode", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feColorMatrix", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "result", "type", "values", "width", "x", "y"], namespaces: &[] }, + Element { name: "feComponentTransfer", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feComposite", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "in2", "k1", "k2", "k3", "k4", "operator", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feConvolveMatrix", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["bias", "divisor", "edgeMode", "height", "in", "kernelMatrix", "kernelUnitLength", "order", "preserveAlpha", "result", "targetX", "targetY", "width", "x", "y"], namespaces: &[] }, + Element { name: "feDiffuseLighting", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["diffuseConstant", "height", "in", "kernelUnitLength", "result", "surfaceScale", "width", "x", "y"], namespaces: &[] }, + Element { name: "feDisplacementMap", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "in2", "result", "scale", "width", "x", "xChannelSelector", "y", "yChannelSelector"], namespaces: &[] }, + Element { name: "feDistantLight", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["azimuth", "elevation"], namespaces: &[] }, + Element { name: "feDropShadow", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["dx", "dy", "height", "in", "result", "stdDeviation", "width", "x", "y"], namespaces: &[] }, + Element { name: "feFlood", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feFuncA", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["amplitude", "exponent", "intercept", "offset", "slope", "tableValues", "type"], namespaces: &[] }, + Element { name: "feFuncB", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["amplitude", "exponent", "intercept", "offset", "slope", "tableValues", "type"], namespaces: &[] }, + Element { name: "feFuncG", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["amplitude", "exponent", "intercept", "offset", "slope", "tableValues", "type"], namespaces: &[] }, + Element { name: "feFuncR", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["amplitude", "exponent", "intercept", "offset", "slope", "tableValues", "type"], namespaces: &[] }, + Element { name: "feGaussianBlur", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["edgeMode", "height", "in", "result", "stdDeviation", "width", "x", "y"], namespaces: &[] }, + Element { name: "feImage", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["crossorigin", "height", "href", "preserveAspectRatio", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feMerge", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feMergeNode", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["in"], namespaces: &[] }, + Element { name: "feMorphology", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "operator", "radius", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feOffset", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["dx", "dy", "height", "in", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "fePointLight", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["x", "y", "z"], namespaces: &[] }, + Element { name: "feSpecularLighting", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "kernelUnitLength", "result", "specularConstant", "specularExponent", "surfaceScale", "width", "x", "y"], namespaces: &[] }, + Element { name: "feSpotLight", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["limitingConeAngle", "pointsAtX", "pointsAtY", "pointsAtZ", "specularExponent", "x", "y", "z"], namespaces: &[] }, + Element { name: "feTile", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "in", "result", "width", "x", "y"], namespaces: &[] }, + Element { name: "feTurbulence", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["baseFrequency", "height", "numOctaves", "result", "seed", "stitchTiles", "type", "width", "x", "y"], namespaces: &[] }, + Element { name: "filter", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["filterUnits", "height", "primitiveUnits", "width", "x", "y"], namespaces: &[] }, + Element { name: "foreignObject", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "width", "x", "y"], namespaces: &[] }, + Element { name: "g", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "image", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["crossorigin", "decoding", "height", "href", "preserveAspectRatio", "width", "x", "y"], namespaces: &[] }, + Element { name: "line", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["pathLength", "x1", "x2", "y1", "y2"], namespaces: &[] }, + Element { name: "linearGradient", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["gradientTransform", "gradientUnits", "href", "spreadMethod", "x1", "x2", "y1", "y2"], namespaces: &[] }, + Element { name: "marker", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["markerHeight", "markerUnits", "markerWidth", "orient", "preserveAspectRatio", "refX", "refY", "viewBox"], namespaces: &[] }, + Element { name: "mask", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "maskContentUnits", "maskUnits", "width", "x", "y"], namespaces: &[] }, + Element { name: "metadata", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "mpath", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["href"], namespaces: &[] }, + Element { name: "path", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["d", "pathLength"], namespaces: &[] }, + Element { name: "pattern", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "href", "patternContentUnits", "patternTransform", "patternUnits", "preserveAspectRatio", "viewBox", "width", "x", "y"], namespaces: &[] }, + Element { name: "polygon", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["pathLength", "points"], namespaces: &[] }, + Element { name: "polyline", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["pathLength", "points"], namespaces: &[] }, + Element { name: "radialGradient", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["cx", "cy", "fr", "fx", "fy", "gradientTransform", "gradientUnits", "href", "r", "spreadMethod"], namespaces: &[] }, + Element { name: "rect", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "pathLength", "rx", "ry", "width", "x", "y"], namespaces: &[] }, + Element { name: "script", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["crossorigin", "href", "type"], namespaces: &[] }, + Element { name: "set", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["attributeName", "begin", "dur", "end", "fill", "href", "max", "min", "repeatCount", "repeatDur", "restart", "to"], namespaces: &[] }, + Element { name: "stop", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["offset"], namespaces: &[] }, + Element { name: "style", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["media", "title", "type"], namespaces: &[] }, + Element { name: "svg", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "preserveAspectRatio", "viewBox", "width", "x", "y"], namespaces: &[] }, + Element { name: "switch", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "symbol", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "preserveAspectRatio", "refX", "refY", "viewBox", "width", "x", "y"], namespaces: &[] }, + Element { name: "text", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["dx", "dy", "lengthAdjust", "rotate", "textLength", "x", "y"], namespaces: &[] }, + Element { name: "textPath", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["href", "lengthAdjust", "method", "path", "side", "spacing", "startOffset", "textLength"], namespaces: &[] }, + Element { name: "title", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &[], namespaces: &[] }, + Element { name: "tspan", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["dx", "dy", "lengthAdjust", "rotate", "textLength", "x", "y"], namespaces: &[] }, + Element { name: "use", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["height", "href", "width", "x", "y"], namespaces: &[] }, + Element { name: "view", kind: Xml, globals: &SVG_GLOBAL_ATTRIBUTES, attributes: &["preserveAspectRatio", "viewBox"], namespaces: &[] }, +]; + +pub(super) const MODULES: &[Module] = &[ + Module { path: "aria", namespace: true, attributes: &["activedescendant", "atomic", "autocomplete", "braillelabel", "brailleroledescription", "busy", "checked", "colcount", "colindex", "colindextext", "colspan", "controls", "current", "describedby", "description", "details", "disabled", "dropeffect", "errormessage", "expanded", "flowto", "grabbed", "haspopup", "hidden", "invalid", "keyshortcuts", "label", "labelledby", "level", "live", "modal", "multiline", "multiselectable", "orientation", "owns", "placeholder", "posinset", "pressed", "readonly", "relevant", "required", "roledescription", "rowcount", "rowindex", "rowindextext", "rowspan", "selected", "setsize", "sort", "valuemax", "valuemin", "valuenow", "valuetext"], modifiers: &[] }, + Module { path: "data", namespace: true, attributes: &["attr", "bind", "class", "computed", "effect", "ignore", "ignore_morph", "indicator", "init", "json_signals", "on_intersect", "on_interval", "on_signal_patch", "on_signal_patch_filter", "preserve_attr", "ref", "show", "signals", "style", "text"], modifiers: &[] }, + Module { path: "data::modifiers", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::attr", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::bind", namespace: false, attributes: &[], modifiers: &["case", "event", "prop"] }, + Module { path: "data::modifiers::class", namespace: false, attributes: &[], modifiers: &["case"] }, + Module { path: "data::modifiers::computed", namespace: false, attributes: &[], modifiers: &["case"] }, + Module { path: "data::modifiers::effect", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::ignore", namespace: false, attributes: &[], modifiers: &["self_"] }, + Module { path: "data::modifiers::ignore_morph", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::indicator", namespace: false, attributes: &[], modifiers: &["case"] }, + Module { path: "data::modifiers::init", namespace: false, attributes: &[], modifiers: &["delay", "viewtransition"] }, + Module { path: "data::modifiers::json_signals", namespace: false, attributes: &[], modifiers: &["terse"] }, + Module { path: "data::modifiers::on", namespace: false, attributes: &[], modifiers: &["capture", "case", "debounce", "delay", "document", "once", "outside", "passive", "prevent", "stop", "throttle", "viewtransition", "window"] }, + Module { path: "data::modifiers::on_intersect", namespace: false, attributes: &[], modifiers: &["debounce", "delay", "exit", "full", "half", "once", "threshold", "throttle", "viewtransition"] }, + Module { path: "data::modifiers::on_interval", namespace: false, attributes: &[], modifiers: &["duration", "viewtransition"] }, + Module { path: "data::modifiers::on_signal_patch", namespace: false, attributes: &[], modifiers: &["debounce", "delay", "throttle"] }, + Module { path: "data::modifiers::on_signal_patch_filter", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::preserve_attr", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::ref", namespace: false, attributes: &[], modifiers: &["case"] }, + Module { path: "data::modifiers::show", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::signals", namespace: false, attributes: &[], modifiers: &["case", "ifmissing"] }, + Module { path: "data::modifiers::style", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::modifiers::text", namespace: false, attributes: &[], modifiers: &[] }, + Module { path: "data::on", namespace: true, attributes: &["DOMContentLoaded", "abort", "afterprint", "animationcancel", "animationend", "animationiteration", "animationstart", "auxclick", "beforeinput", "beforeprint", "beforetoggle", "beforeunload", "blur", "cancel", "canplay", "canplaythrough", "change", "click", "close", "compositionend", "compositionstart", "compositionupdate", "contextmenu", "copy", "cut", "dblclick", "drag", "dragend", "dragenter", "dragleave", "dragover", "dragstart", "drop", "durationchange", "emptied", "ended", "error", "focus", "focusin", "focusout", "formdata", "fullscreenchange", "fullscreenerror", "gotpointercapture", "hashchange", "input", "invalid", "keydown", "keypress", "keyup", "languagechange", "load", "loadeddata", "loadedmetadata", "loadstart", "lostpointercapture", "message", "messageerror", "mousedown", "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover", "mouseup", "offline", "online", "pagehide", "pageshow", "paste", "pause", "play", "playing", "pointercancel", "pointerdown", "pointerenter", "pointerleave", "pointermove", "pointerout", "pointerover", "pointerup", "popstate", "progress", "ratechange", "readystatechange", "rejectionhandled", "reset", "resize", "scroll", "scrollend", "securitypolicyviolation", "seeked", "seeking", "select", "selectionchange", "selectstart", "slotchange", "stalled", "storage", "submit", "suspend", "timeupdate", "toggle", "touchcancel", "touchend", "touchmove", "touchstart", "transitioncancel", "transitionend", "transitionrun", "transitionstart", "unhandledrejection", "unload", "visibilitychange", "volumechange", "waiting", "wheel"], modifiers: &[] }, +]; diff --git a/crates/cheers/tests/ui.rs b/crates/cheers/tests/ui.rs index 42d1449..be23013 100644 --- a/crates/cheers/tests/ui.rs +++ b/crates/cheers/tests/ui.rs @@ -52,6 +52,12 @@ fn show_with_its_own_display_is_rejected() { tests.compile_fail("tests/ui/show_with_its_own_display.rs"); } +#[test] +fn known_element_with_wrong_kind_is_rejected() { + let tests = trybuild::TestCases::new(); + tests.compile_fail("tests/ui/known_element_with_wrong_kind.rs"); +} + #[test] fn datastar_event_does_not_ambiguous_glob_import() { let tests = trybuild::TestCases::new(); diff --git a/crates/cheers/tests/ui/datastar_event_does_not_ambiguous_glob_import.rs b/crates/cheers/tests/ui/datastar_event_does_not_ambiguous_glob_import.rs index ac8404d..4ad01d9 100644 --- a/crates/cheers/tests/ui/datastar_event_does_not_ambiguous_glob_import.rs +++ b/crates/cheers/tests/ui/datastar_event_does_not_ambiguous_glob_import.rs @@ -2,10 +2,12 @@ use cheers::prelude::*; +cheers::define_events! { saved } + fn main() { let _ = html! { - form { - input; + form !on:saved("console.log('saved')") { + input !on:input("console.log('input')"); button !on:click("console.log('ok')") { "Save" } } }; diff --git a/crates/cheers/tests/ui/known_element_with_wrong_kind.rs b/crates/cheers/tests/ui/known_element_with_wrong_kind.rs new file mode 100644 index 0000000..5050b9a --- /dev/null +++ b/crates/cheers/tests/ui/known_element_with_wrong_kind.rs @@ -0,0 +1,7 @@ +use cheers::prelude::*; + +fn main() { + let _ = html! { + input {} + }; +} diff --git a/crates/cheers/tests/ui/known_element_with_wrong_kind.stderr b/crates/cheers/tests/ui/known_element_with_wrong_kind.stderr new file mode 100644 index 0000000..760e653 --- /dev/null +++ b/crates/cheers/tests/ui/known_element_with_wrong_kind.stderr @@ -0,0 +1,19 @@ +error[E0271]: type mismatch resolving `::Kind == Normal` + --> tests/ui/known_element_with_wrong_kind.rs:5:9 + | +4 | let _ = html! { + | _____________- +5 | | input {} + | | ^^^^^ expected `Normal`, found `Void` +6 | | }; + | |_____- required by a bound introduced by this call + | +note: required by a bound in `check_element` + --> tests/ui/known_element_with_wrong_kind.rs:4:13 + | +4 | let _ = html! { + | _____________^ +5 | | input {} +6 | | }; + | |_____^ required by this bound in `check_element` + = note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/cheers/tests/validation_table.rs b/crates/cheers/tests/validation_table.rs new file mode 100644 index 0000000..3de9e66 --- /dev/null +++ b/crates/cheers/tests/validation_table.rs @@ -0,0 +1,5 @@ +//! Every name `html!` skips checking must pass the check it would otherwise emit. + +#![deny(ambiguous_glob_imports)] + +macros::__validation_table_checks!(); diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 648be43..455d1f6 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -176,6 +176,13 @@ pub fn html(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { expand_document_lazy(tokens, NodeFlavour::Html) } +#[doc(hidden)] +#[proc_macro] +/// Emits the check `html!` would for every name it skips checking. Used by Cheers' tests. +pub fn __validation_table_checks(_tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { + ast::validation_table_checks().into() +} + #[proc_macro] /// Builds a lazily rendered SVG fragment or document. ///