Skip to main content

muffy_validation_macro/
lib.rs

1//! Macros for document validation.
2
3extern crate alloc;
4
5mod error;
6
7use self::error::MacroError;
8use alloc::collections::{BTreeMap, BTreeSet};
9use core::mem::replace;
10use muffy_rnc::{
11    Combine, Grammar, GrammarContent, Identifier, NameClass, Pattern, SchemaBody, parse_schema,
12};
13use proc_macro::TokenStream;
14use proc_macro2::Span;
15use quote::quote;
16use std::{fs::read_to_string, path::Path};
17
18/// Generates HTML validation functions.
19#[proc_macro]
20pub fn html(_input: TokenStream) -> TokenStream {
21    generate_html().unwrap_or_else(|error| {
22        syn::Error::new(Span::call_site(), error)
23            .to_compile_error()
24            .into()
25    })
26}
27
28fn generate_html() -> Result<TokenStream, MacroError> {
29    let mut definitions = Default::default();
30
31    for file in ["html5.rnc", "rdfa.rnc"] {
32        load_schema(
33            &Path::new(env!("CARGO_MANIFEST_DIR"))
34                .join("src")
35                .join("schema")
36                .join("html5")
37                .join(file),
38            &mut definitions,
39        )?;
40    }
41
42    // element -> (attributes, children)
43    let mut element_rules = BTreeMap::<String, (Vec<String>, Vec<String>)>::new();
44
45    for pattern in definitions.values() {
46        for (name_class, pattern) in collect_elements(pattern) {
47            let Some(element) = get_name(name_class) else {
48                continue;
49            };
50
51            let (attributes, children) = element_rules
52                .entry(element)
53                .or_insert_with(|| (vec![], vec![]));
54
55            attributes.extend(collect_attributes(pattern, &definitions)?);
56            children.extend(collect_children(pattern, &definitions)?);
57        }
58    }
59
60    let mut element_matches = vec![];
61
62    for (element, (mut attributes, mut children)) in element_rules {
63        attributes.sort();
64        attributes.dedup();
65        children.sort();
66        children.dedup();
67
68        let attributes = attributes.iter().map(|attribute| quote!(#attribute));
69        let children = children.iter().map(|child| quote!(#child));
70
71        element_matches.push(quote! {
72            #element => {
73                let mut attributes = ::alloc::collections::BTreeMap::<
74                    String,
75                    ::alloc::collections::BTreeSet<AttributeError>,
76                >::new();
77
78                for (attribute, _) in element.attributes() {
79                    if ignored_attributes.iter().any(|pattern| pattern.is_match(attribute)) {
80                        continue;
81                    }
82
83                    match attribute {
84                        #(#attributes |)* "_DUMMY_" => {}
85                        _ => {
86                            attributes
87                                .entry(attribute.into())
88                                .or_insert_with(Default::default)
89                                .insert(AttributeError::NotAllowed);
90                        }
91                    }
92                }
93
94                let mut children = ::alloc::collections::BTreeMap::<
95                    String,
96                    ::alloc::collections::BTreeSet<ChildError>,
97                >::new();
98
99                for child in element.children() {
100                    if let muffy_document::html::Node::Element(element) = child {
101                        let name = element.name();
102
103                        if ignored_elements.iter().any(|pattern| pattern.is_match(name)) {
104                            continue;
105                        }
106
107                        match name {
108                            #(#children |)* "_DUMMY_" => {}
109                            _ => {
110                                children
111                                    .entry(name.into())
112                                    .or_insert_with(Default::default)
113                                    .insert(ChildError::NotAllowed);
114                            }
115                        }
116                    }
117                }
118
119                if attributes.is_empty() && children.is_empty() {
120                    Ok(())
121                } else {
122                    Err(MarkupError::InvalidElement {
123                        attributes,
124                        children,
125                    })
126                }
127            }
128        });
129    }
130
131    Ok(quote! {
132        /// Validates an HTML element.
133        pub fn validate_html_element(
134            element: &Element,
135            ignored_attributes: &[::regex::Regex],
136            ignored_elements: &[::regex::Regex],
137        ) -> Result<(), MarkupError> {
138            match element.name() {
139                name if ignored_elements.iter().any(|pattern| pattern.is_match(name)) => Ok(()),
140                #(#element_matches)*
141                _ => Err(MarkupError::UnknownTag(element.name().to_string())),
142            }
143        }
144    }
145    .into())
146}
147
148fn load_schema(
149    path: &Path,
150    definitions: &mut BTreeMap<Identifier, Pattern>,
151) -> Result<(), MacroError> {
152    let schema = parse_schema(&read_to_string(path)?)?;
153
154    // We do not use the declarations.
155
156    match schema.body {
157        SchemaBody::Grammar(grammar) => {
158            load_grammar(
159                &grammar,
160                definitions,
161                path.parent().ok_or(MacroError::NoParentDirectory)?,
162            )?;
163        }
164        SchemaBody::Pattern(_) => return Err(MacroError::RncSyntax("top-level pattern")),
165    }
166
167    Ok(())
168}
169
170fn load_grammar(
171    grammar: &Grammar,
172    definitions: &mut BTreeMap<Identifier, Pattern>,
173    directory: &Path,
174) -> Result<(), MacroError> {
175    for content in &grammar.contents {
176        match content {
177            GrammarContent::Definition(definition) => {
178                let name = definition.name.clone();
179                let pattern = definition.pattern.clone();
180
181                if let Some(combine) = definition.combine {
182                    combine_patterns(
183                        definitions.entry(name).or_insert(Pattern::NotAllowed),
184                        pattern,
185                        combine,
186                    );
187                } else {
188                    definitions.insert(name, pattern);
189                }
190            }
191            GrammarContent::Div(grammar) => load_grammar(grammar, definitions, directory)?,
192            GrammarContent::Include(include) => {
193                let include_path = directory.join(&include.uri);
194
195                load_schema(&include_path, definitions)?;
196
197                if let Some(grammar) = &include.grammar {
198                    load_grammar(grammar, definitions, directory)?;
199                }
200            }
201            GrammarContent::Annotation(_) | GrammarContent::Start { .. } => {}
202        }
203    }
204
205    Ok(())
206}
207
208fn combine_patterns(existing: &mut Pattern, new: Pattern, combine: Combine) {
209    match combine {
210        Combine::Choice => match existing {
211            Pattern::Choice(choices) => choices.push(new),
212            Pattern::NotAllowed => *existing = new,
213            Pattern::Attribute { .. }
214            | Pattern::Data { .. }
215            | Pattern::Element { .. }
216            | Pattern::Empty
217            | Pattern::External(_)
218            | Pattern::Grammar(_)
219            | Pattern::Group(_)
220            | Pattern::Interleave(_)
221            | Pattern::List(_)
222            | Pattern::Many0(_)
223            | Pattern::Many1(_)
224            | Pattern::Name(_)
225            | Pattern::Optional(_)
226            | Pattern::Text
227            | Pattern::Value { .. } => {
228                let old = replace(existing, Pattern::Choice(vec![]));
229
230                if let Pattern::Choice(choices) = existing {
231                    choices.push(old);
232                    choices.push(new);
233                }
234            }
235        },
236        Combine::Interleave => match existing {
237            Pattern::Interleave(patterns) => patterns.push(new),
238            Pattern::NotAllowed => *existing = new,
239            Pattern::Attribute { .. }
240            | Pattern::Choice(_)
241            | Pattern::Data { .. }
242            | Pattern::Element { .. }
243            | Pattern::Empty
244            | Pattern::External(_)
245            | Pattern::Grammar(_)
246            | Pattern::Group(_)
247            | Pattern::List(_)
248            | Pattern::Many0(_)
249            | Pattern::Many1(_)
250            | Pattern::Name(_)
251            | Pattern::Optional(_)
252            | Pattern::Text
253            | Pattern::Value { .. } => {
254                let old = replace(existing, Pattern::Interleave(vec![]));
255
256                if let Pattern::Interleave(patterns) = existing {
257                    patterns.push(old);
258                    patterns.push(new);
259                }
260            }
261        },
262    }
263}
264
265fn collect_elements(pattern: &Pattern) -> Vec<(&NameClass, &Pattern)> {
266    match pattern {
267        Pattern::Element {
268            name_class,
269            pattern,
270        } => vec![(name_class, pattern)],
271        Pattern::Choice(patterns) | Pattern::Group(patterns) | Pattern::Interleave(patterns) => {
272            patterns.iter().flat_map(collect_elements).collect()
273        }
274        Pattern::Many0(pattern) | Pattern::Many1(pattern) | Pattern::Optional(pattern) => {
275            collect_elements(pattern)
276        }
277        Pattern::Attribute { .. }
278        | Pattern::Data { .. }
279        | Pattern::Empty
280        | Pattern::External(_)
281        | Pattern::Grammar(_)
282        | Pattern::List(_)
283        | Pattern::Name(_)
284        | Pattern::NotAllowed
285        | Pattern::Text
286        | Pattern::Value { .. } => vec![],
287    }
288}
289
290fn get_name(name_class: &NameClass) -> Option<String> {
291    match name_class {
292        NameClass::Name(name) => Some(name.local.component.clone()),
293        NameClass::Choice(choices) => choices.iter().find_map(get_name),
294        NameClass::AnyName | NameClass::Except { .. } | NameClass::NamespaceName(_) => None,
295    }
296}
297
298fn collect_attributes(
299    pattern: &Pattern,
300    definitions: &BTreeMap<Identifier, Pattern>,
301) -> Result<BTreeSet<String>, MacroError> {
302    let mut attributes = Default::default();
303
304    collect_nested_attributes(
305        pattern,
306        definitions,
307        &mut attributes,
308        &mut Default::default(),
309    )?;
310
311    Ok(attributes)
312}
313
314fn collect_nested_attributes<'a>(
315    pattern: &'a Pattern,
316    definitions: &'a BTreeMap<Identifier, Pattern>,
317    attributes: &mut BTreeSet<String>,
318    visited: &mut BTreeSet<&'a Identifier>,
319) -> Result<(), MacroError> {
320    match pattern {
321        Pattern::Attribute { name_class, .. } => {
322            if let Some(name) = get_name(name_class) {
323                attributes.insert(name);
324            }
325        }
326        Pattern::Name(name) => {
327            if !visited.contains(&name.local) {
328                visited.insert(&name.local);
329
330                if let Some(pattern) = definitions.get(&name.local) {
331                    collect_nested_attributes(pattern, definitions, attributes, visited)?;
332                }
333            }
334        }
335        Pattern::Choice(patterns) | Pattern::Group(patterns) | Pattern::Interleave(patterns) => {
336            for pattern in patterns {
337                collect_nested_attributes(pattern, definitions, attributes, visited)?;
338            }
339        }
340        Pattern::Many0(pattern) | Pattern::Many1(pattern) | Pattern::Optional(pattern) => {
341            collect_nested_attributes(pattern, definitions, attributes, visited)?;
342        }
343        Pattern::Data { .. } => return Err(MacroError::RncPattern("data")),
344        Pattern::External(_) => return Err(MacroError::RncPattern("external")),
345        Pattern::Grammar(_) => return Err(MacroError::RncPattern("grammar")),
346        Pattern::List { .. } => return Err(MacroError::RncPattern("list")),
347        Pattern::Value { .. } => return Err(MacroError::RncPattern("value")),
348        Pattern::Empty | Pattern::Element { .. } | Pattern::NotAllowed | Pattern::Text => {}
349    }
350
351    Ok(())
352}
353
354fn collect_children(
355    pattern: &Pattern,
356    definitions: &BTreeMap<Identifier, Pattern>,
357) -> Result<BTreeSet<String>, MacroError> {
358    let mut children = Default::default();
359
360    collect_nested_children(pattern, definitions, &mut children, &mut Default::default())?;
361
362    Ok(children)
363}
364
365fn collect_nested_children<'a>(
366    pattern: &'a Pattern,
367    definitions: &'a BTreeMap<Identifier, Pattern>,
368    children: &mut BTreeSet<String>,
369    visited: &mut BTreeSet<&'a Identifier>,
370) -> Result<(), MacroError> {
371    match pattern {
372        Pattern::Element { name_class, .. } => {
373            if let Some(name) = get_name(name_class) {
374                children.insert(name);
375            }
376        }
377        Pattern::Name(name) => {
378            if !visited.contains(&name.local) {
379                visited.insert(&name.local);
380
381                if let Some(pattern) = definitions.get(&name.local) {
382                    collect_nested_children(pattern, definitions, children, visited)?;
383                }
384            }
385        }
386        Pattern::Choice(patterns) | Pattern::Group(patterns) | Pattern::Interleave(patterns) => {
387            for pattern in patterns {
388                collect_nested_children(pattern, definitions, children, visited)?;
389            }
390        }
391        Pattern::Many0(pattern) | Pattern::Many1(pattern) | Pattern::Optional(pattern) => {
392            collect_nested_children(pattern, definitions, children, visited)?;
393        }
394        Pattern::Data { .. } => return Err(MacroError::RncPattern("data")),
395        Pattern::External(_) => return Err(MacroError::RncPattern("external")),
396        Pattern::Grammar(_) => return Err(MacroError::RncPattern("grammar")),
397        Pattern::List { .. } => return Err(MacroError::RncPattern("list")),
398        Pattern::Value { .. } => return Err(MacroError::RncPattern("value")),
399        Pattern::Attribute { .. } | Pattern::Empty | Pattern::NotAllowed | Pattern::Text => {}
400    }
401
402    Ok(())
403}