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