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