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