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